remote_editing_collaboration_tests.rs

  1use crate::tests::TestServer;
  2use call::ActiveCall;
  3use fs::{FakeFs, Fs as _};
  4use gpui::{BackgroundExecutor, Context as _, TestAppContext};
  5use http_client::BlockedHttpClient;
  6use language::{language_settings::language_settings, LanguageRegistry};
  7use node_runtime::NodeRuntime;
  8use project::ProjectPath;
  9use remote::SshRemoteClient;
 10use remote_server::{HeadlessAppState, HeadlessProject};
 11use serde_json::json;
 12use std::{path::Path, sync::Arc};
 13
 14#[gpui::test(iterations = 10)]
 15async fn test_sharing_an_ssh_remote_project(
 16    cx_a: &mut TestAppContext,
 17    cx_b: &mut TestAppContext,
 18    server_cx: &mut TestAppContext,
 19) {
 20    let executor = cx_a.executor();
 21    let mut server = TestServer::start(executor.clone()).await;
 22    let client_a = server.create_client(cx_a, "user_a").await;
 23    let client_b = server.create_client(cx_b, "user_b").await;
 24    server
 25        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
 26        .await;
 27
 28    // Set up project on remote FS
 29    let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
 30    let remote_fs = FakeFs::new(server_cx.executor());
 31    remote_fs
 32        .insert_tree(
 33            "/code",
 34            json!({
 35                "project1": {
 36                    ".zed": {
 37                        "settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
 38                    },
 39                    "README.md": "# project 1",
 40                    "src": {
 41                        "lib.rs": "fn one() -> usize { 1 }"
 42                    }
 43                },
 44                "project2": {
 45                    "README.md": "# project 2",
 46                },
 47            }),
 48        )
 49        .await;
 50
 51    // User A connects to the remote project via SSH.
 52    server_cx.update(HeadlessProject::init);
 53    let remote_http_client = Arc::new(BlockedHttpClient);
 54    let node = NodeRuntime::unavailable();
 55    let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
 56    let _headless_project = server_cx.new_model(|cx| {
 57        client::init_settings(cx);
 58        HeadlessProject::new(
 59            HeadlessAppState {
 60                session: server_ssh,
 61                fs: remote_fs.clone(),
 62                http_client: remote_http_client,
 63                node_runtime: node,
 64                languages,
 65            },
 66            cx,
 67        )
 68    });
 69
 70    let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
 71    let (project_a, worktree_id) = client_a
 72        .build_ssh_project("/code/project1", client_ssh, cx_a)
 73        .await;
 74
 75    // While the SSH worktree is being scanned, user A shares the remote project.
 76    let active_call_a = cx_a.read(ActiveCall::global);
 77    let project_id = active_call_a
 78        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 79        .await
 80        .unwrap();
 81
 82    // User B joins the project.
 83    let project_b = client_b.join_remote_project(project_id, cx_b).await;
 84    let worktree_b = project_b
 85        .update(cx_b, |project, cx| project.worktree_for_id(worktree_id, cx))
 86        .unwrap();
 87
 88    let worktree_a = project_a
 89        .update(cx_a, |project, cx| project.worktree_for_id(worktree_id, cx))
 90        .unwrap();
 91
 92    executor.run_until_parked();
 93
 94    worktree_a.update(cx_a, |worktree, _cx| {
 95        assert_eq!(
 96            worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
 97            vec![
 98                Path::new(".zed"),
 99                Path::new(".zed/settings.json"),
100                Path::new("README.md"),
101                Path::new("src"),
102                Path::new("src/lib.rs"),
103            ]
104        );
105    });
106
107    worktree_b.update(cx_b, |worktree, _cx| {
108        assert_eq!(
109            worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
110            vec![
111                Path::new(".zed"),
112                Path::new(".zed/settings.json"),
113                Path::new("README.md"),
114                Path::new("src"),
115                Path::new("src/lib.rs"),
116            ]
117        );
118    });
119
120    // User B can open buffers in the remote project.
121    let buffer_b = project_b
122        .update(cx_b, |project, cx| {
123            project.open_buffer((worktree_id, "src/lib.rs"), cx)
124        })
125        .await
126        .unwrap();
127    buffer_b.update(cx_b, |buffer, cx| {
128        assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
129        let ix = buffer.text().find('1').unwrap();
130        buffer.edit([(ix..ix + 1, "100")], None, cx);
131    });
132
133    executor.run_until_parked();
134
135    cx_b.read(|cx| {
136        let file = buffer_b.read(cx).file();
137        assert_eq!(
138            language_settings(Some("Rust".into()), file, cx).language_servers,
139            ["override-rust-analyzer".to_string()]
140        )
141    });
142
143    project_b
144        .update(cx_b, |project, cx| {
145            project.save_buffer_as(
146                buffer_b.clone(),
147                ProjectPath {
148                    worktree_id: worktree_id.to_owned(),
149                    path: Arc::from(Path::new("src/renamed.rs")),
150                },
151                cx,
152            )
153        })
154        .await
155        .unwrap();
156    assert_eq!(
157        remote_fs
158            .load("/code/project1/src/renamed.rs".as_ref())
159            .await
160            .unwrap(),
161        "fn one() -> usize { 100 }"
162    );
163    cx_b.run_until_parked();
164    cx_b.update(|cx| {
165        assert_eq!(
166            buffer_b
167                .read(cx)
168                .file()
169                .unwrap()
170                .path()
171                .to_string_lossy()
172                .to_string(),
173            "src/renamed.rs".to_string()
174        );
175    });
176}
177
178#[gpui::test]
179async fn test_ssh_collaboration_git_branches(
180    executor: BackgroundExecutor,
181    cx_a: &mut TestAppContext,
182    cx_b: &mut TestAppContext,
183    server_cx: &mut TestAppContext,
184) {
185    cx_a.set_name("a");
186    cx_b.set_name("b");
187    server_cx.set_name("server");
188
189    let mut server = TestServer::start(executor.clone()).await;
190    let client_a = server.create_client(cx_a, "user_a").await;
191    let client_b = server.create_client(cx_b, "user_b").await;
192    server
193        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
194        .await;
195
196    // Set up project on remote FS
197    let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
198    let remote_fs = FakeFs::new(server_cx.executor());
199    remote_fs
200        .insert_tree("/project", serde_json::json!({ ".git":{} }))
201        .await;
202
203    let branches = ["main", "dev", "feature-1"];
204    remote_fs.insert_branches(Path::new("/project/.git"), &branches);
205
206    // User A connects to the remote project via SSH.
207    server_cx.update(HeadlessProject::init);
208    let remote_http_client = Arc::new(BlockedHttpClient);
209    let node = NodeRuntime::unavailable();
210    let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
211    let headless_project = server_cx.new_model(|cx| {
212        client::init_settings(cx);
213        HeadlessProject::new(
214            HeadlessAppState {
215                session: server_ssh,
216                fs: remote_fs.clone(),
217                http_client: remote_http_client,
218                node_runtime: node,
219                languages,
220            },
221            cx,
222        )
223    });
224
225    let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
226    let (project_a, worktree_id) = client_a
227        .build_ssh_project("/project", client_ssh, cx_a)
228        .await;
229
230    // While the SSH worktree is being scanned, user A shares the remote project.
231    let active_call_a = cx_a.read(ActiveCall::global);
232    let project_id = active_call_a
233        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
234        .await
235        .unwrap();
236
237    // User B joins the project.
238    let project_b = client_b.join_remote_project(project_id, cx_b).await;
239
240    // Give client A sometime to see that B has joined, and that the headless server
241    // has some git repositories
242    executor.run_until_parked();
243
244    let root_path = ProjectPath::root_path(worktree_id);
245
246    let branches_b = cx_b
247        .update(|cx| project_b.update(cx, |project, cx| project.branches(root_path.clone(), cx)))
248        .await
249        .unwrap();
250
251    let new_branch = branches[2];
252
253    let branches_b = branches_b
254        .into_iter()
255        .map(|branch| branch.name)
256        .collect::<Vec<_>>();
257
258    assert_eq!(&branches_b, &branches);
259
260    cx_b.update(|cx| {
261        project_b.update(cx, |project, cx| {
262            project.update_or_create_branch(root_path.clone(), new_branch.to_string(), cx)
263        })
264    })
265    .await
266    .unwrap();
267
268    executor.run_until_parked();
269
270    let server_branch = server_cx.update(|cx| {
271        headless_project.update(cx, |headless_project, cx| {
272            headless_project
273                .worktree_store
274                .update(cx, |worktree_store, cx| {
275                    worktree_store
276                        .current_branch(root_path.clone(), cx)
277                        .unwrap()
278                })
279        })
280    });
281
282    assert_eq!(server_branch.as_ref(), branches[2]);
283
284    // Also try creating a new branch
285    cx_b.update(|cx| {
286        project_b.update(cx, |project, cx| {
287            project.update_or_create_branch(root_path.clone(), "totally-new-branch".to_string(), cx)
288        })
289    })
290    .await
291    .unwrap();
292
293    executor.run_until_parked();
294
295    let server_branch = server_cx.update(|cx| {
296        headless_project.update(cx, |headless_project, cx| {
297            headless_project
298                .worktree_store
299                .update(cx, |worktree_store, cx| {
300                    worktree_store.current_branch(root_path, cx).unwrap()
301                })
302        })
303    });
304
305    assert_eq!(server_branch.as_ref(), "totally-new-branch");
306}