1use crate::{
2 admin, auth, github,
3 rpc::{self, add_rpc_routes},
4 AppState, Config,
5};
6use async_std::task;
7use gpui::TestAppContext;
8use rand::prelude::*;
9use serde_json::json;
10use sqlx::{
11 migrate::{MigrateDatabase, Migrator},
12 postgres::PgPoolOptions,
13 Executor as _, Postgres,
14};
15use std::{fs, path::Path, sync::Arc};
16use zed::{
17 editor::Editor,
18 language::LanguageRegistry,
19 rpc::Client,
20 settings,
21 test::{temp_tree, Channel},
22 worktree::{Fs, InMemoryFs, Worktree},
23};
24use zed_rpc::{ForegroundRouter, Peer, Router};
25
26#[gpui::test]
27async fn test_share_worktree(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
28 let (window_b, _) = cx_b.add_window(|_| EmptyView);
29 let settings = settings::channel(&cx_b.font_cache()).unwrap().1;
30 let lang_registry = Arc::new(LanguageRegistry::new());
31
32 // Connect to a server as 2 clients.
33 let mut server = TestServer::start().await;
34 let client_a = server.create_client(&mut cx_a, "user_a").await;
35 let client_b = server.create_client(&mut cx_b, "user_b").await;
36
37 // Share a local worktree as client A
38 let dir = temp_tree(json!({
39 "a.txt": "a-contents",
40 "b.txt": "b-contents",
41 }));
42 let worktree_a = cx_a.add_model(|cx| Worktree::local(dir.path(), lang_registry.clone(), cx));
43 worktree_a
44 .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
45 .await;
46 let (worktree_id, worktree_token) = worktree_a
47 .update(&mut cx_a, |tree, cx| {
48 tree.as_local_mut().unwrap().share(client_a.clone(), cx)
49 })
50 .await
51 .unwrap();
52
53 // Join that worktree as client B, and see that a guest has joined as client A.
54 let worktree_b = Worktree::open_remote(
55 client_b.clone(),
56 worktree_id,
57 worktree_token,
58 lang_registry.clone(),
59 &mut cx_b.to_async(),
60 )
61 .await
62 .unwrap();
63 let replica_id_b = worktree_b.read_with(&cx_b, |tree, _| tree.replica_id());
64 worktree_a
65 .condition(&cx_a, |tree, _| {
66 tree.peers()
67 .values()
68 .any(|replica_id| *replica_id == replica_id_b)
69 })
70 .await;
71
72 // Open the same file as client B and client A.
73 let buffer_b = worktree_b
74 .update(&mut cx_b, |worktree, cx| worktree.open_buffer("b.txt", cx))
75 .await
76 .unwrap();
77 buffer_b.read_with(&cx_b, |buf, _| assert_eq!(buf.text(), "b-contents"));
78 worktree_a.read_with(&cx_a, |tree, cx| assert!(tree.has_open_buffer("b.txt", cx)));
79 let buffer_a = worktree_a
80 .update(&mut cx_a, |tree, cx| tree.open_buffer("b.txt", cx))
81 .await
82 .unwrap();
83
84 // Create a selection set as client B and see that selection set as client A.
85 let editor_b = cx_b.add_view(window_b, |cx| Editor::for_buffer(buffer_b, settings, cx));
86 buffer_a
87 .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 1)
88 .await;
89
90 // Edit the buffer as client B and see that edit as client A.
91 editor_b.update(&mut cx_b, |editor, cx| {
92 editor.insert(&"ok, ".to_string(), cx)
93 });
94 buffer_a
95 .condition(&cx_a, |buffer, _| buffer.text() == "ok, b-contents")
96 .await;
97
98 // Remove the selection set as client B, see those selections disappear as client A.
99 cx_b.update(move |_| drop(editor_b));
100 buffer_a
101 .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 0)
102 .await;
103
104 // Close the buffer as client A, see that the buffer is closed.
105 drop(buffer_a);
106 worktree_a
107 .condition(&cx_a, |tree, cx| !tree.has_open_buffer("b.txt", cx))
108 .await;
109
110 // Dropping the worktree removes client B from client A's peers.
111 cx_b.update(move |_| drop(worktree_b));
112 worktree_a
113 .condition(&cx_a, |tree, _| tree.peers().is_empty())
114 .await;
115}
116
117#[gpui::test]
118async fn test_propagate_saves_and_fs_changes_in_shared_worktree(
119 mut cx_a: TestAppContext,
120 mut cx_b: TestAppContext,
121 mut cx_c: TestAppContext,
122) {
123 let lang_registry = Arc::new(LanguageRegistry::new());
124
125 // Connect to a server as 3 clients.
126 let mut server = TestServer::start().await;
127 let client_a = server.create_client(&mut cx_a, "user_a").await;
128 let client_b = server.create_client(&mut cx_b, "user_b").await;
129 let client_c = server.create_client(&mut cx_c, "user_c").await;
130
131 // Share a worktree as client A.
132 let dir = temp_tree(json!({
133 "file1": "",
134 "file2": ""
135 }));
136 let worktree_a = cx_a.add_model(|cx| Worktree::local(dir.path(), lang_registry.clone(), cx));
137 worktree_a
138 .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
139 .await;
140 let (worktree_id, worktree_token) = worktree_a
141 .update(&mut cx_a, |tree, cx| {
142 tree.as_local_mut().unwrap().share(client_a.clone(), cx)
143 })
144 .await
145 .unwrap();
146
147 // Join that worktree as clients B and C.
148 let worktree_b = Worktree::open_remote(
149 client_b.clone(),
150 worktree_id,
151 worktree_token.clone(),
152 lang_registry.clone(),
153 &mut cx_b.to_async(),
154 )
155 .await
156 .unwrap();
157 let worktree_c = Worktree::open_remote(
158 client_c.clone(),
159 worktree_id,
160 worktree_token,
161 lang_registry.clone(),
162 &mut cx_c.to_async(),
163 )
164 .await
165 .unwrap();
166
167 // Open and edit a buffer as both guests B and C.
168 let buffer_b = worktree_b
169 .update(&mut cx_b, |tree, cx| tree.open_buffer("file1", cx))
170 .await
171 .unwrap();
172 let buffer_c = worktree_c
173 .update(&mut cx_c, |tree, cx| tree.open_buffer("file1", cx))
174 .await
175 .unwrap();
176 buffer_b.update(&mut cx_b, |buf, cx| buf.edit([0..0], "i-am-b, ", cx));
177 buffer_c.update(&mut cx_c, |buf, cx| buf.edit([0..0], "i-am-c, ", cx));
178
179 // Open and edit that buffer as the host.
180 let buffer_a = worktree_a
181 .update(&mut cx_a, |tree, cx| tree.open_buffer("file1", cx))
182 .await
183 .unwrap();
184 buffer_a.update(&mut cx_a, |buf, cx| buf.edit([0..0], "i-am-a", cx));
185
186 // Wait for edits to propagate
187 buffer_a
188 .condition(&mut cx_a, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a")
189 .await;
190 buffer_b
191 .condition(&mut cx_b, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a")
192 .await;
193 buffer_c
194 .condition(&mut cx_c, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a")
195 .await;
196
197 // Edit the buffer as the host and concurrently save as guest B.
198 let save_b = buffer_b.update(&mut cx_b, |buf, cx| buf.save(cx).unwrap());
199 buffer_a.update(&mut cx_a, |buf, cx| buf.edit([0..0], "hi-a, ", cx));
200 save_b.await.unwrap();
201 assert_eq!(
202 fs::read_to_string(dir.path().join("file1")).unwrap(),
203 "hi-a, i-am-c, i-am-b, i-am-a"
204 );
205 buffer_a.read_with(&cx_a, |buf, _| assert!(!buf.is_dirty()));
206 buffer_b.read_with(&cx_b, |buf, _| assert!(!buf.is_dirty()));
207 buffer_c.condition(&cx_c, |buf, _| !buf.is_dirty()).await;
208
209 // Make changes on host's file system, see those changes on the guests.
210 fs::rename(dir.path().join("file2"), dir.path().join("file3")).unwrap();
211 fs::write(dir.path().join("file4"), "4").unwrap();
212 worktree_b
213 .condition(&cx_b, |tree, _| tree.file_count() == 3)
214 .await;
215 worktree_c
216 .condition(&cx_c, |tree, _| tree.file_count() == 3)
217 .await;
218 worktree_b.read_with(&cx_b, |tree, _| {
219 assert_eq!(
220 tree.paths()
221 .map(|p| p.to_string_lossy())
222 .collect::<Vec<_>>(),
223 &["file1", "file3", "file4"]
224 )
225 });
226 worktree_c.read_with(&cx_c, |tree, _| {
227 assert_eq!(
228 tree.paths()
229 .map(|p| p.to_string_lossy())
230 .collect::<Vec<_>>(),
231 &["file1", "file3", "file4"]
232 )
233 });
234}
235
236#[gpui::test]
237async fn test_buffer_conflict_after_save(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
238 let lang_registry = Arc::new(LanguageRegistry::new());
239
240 // Connect to a server as 2 clients.
241 let mut server = TestServer::start().await;
242 let client_a = server.create_client(&mut cx_a, "user_a").await;
243 let client_b = server.create_client(&mut cx_b, "user_b").await;
244
245 // Share a local worktree as client A
246 let fs = Arc::new(InMemoryFs::new());
247 fs.save(Path::new("/a.txt"), &"a-contents".into())
248 .await
249 .unwrap();
250 let worktree_a =
251 cx_a.add_model(|cx| Worktree::test(Path::new("/"), lang_registry.clone(), fs.clone(), cx));
252 worktree_a
253 .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
254 .await;
255 let (worktree_id, worktree_token) = worktree_a
256 .update(&mut cx_a, |tree, cx| {
257 tree.as_local_mut().unwrap().share(client_a.clone(), cx)
258 })
259 .await
260 .unwrap();
261
262 // Join that worktree as client B, and see that a guest has joined as client A.
263 let worktree_b = Worktree::open_remote(
264 client_b.clone(),
265 worktree_id,
266 worktree_token,
267 lang_registry.clone(),
268 &mut cx_b.to_async(),
269 )
270 .await
271 .unwrap();
272
273 let buffer_b = worktree_b
274 .update(&mut cx_b, |worktree, cx| worktree.open_buffer("a.txt", cx))
275 .await
276 .unwrap();
277 let mtime = buffer_b.read_with(&cx_b, |buf, _| buf.file().unwrap().mtime);
278
279 buffer_b.update(&mut cx_b, |buf, cx| buf.edit([0..0], "world ", cx));
280 buffer_b.read_with(&cx_b, |buf, _| {
281 assert!(buf.is_dirty());
282 assert!(!buf.has_conflict());
283 });
284
285 buffer_b
286 .update(&mut cx_b, |buf, cx| buf.save(cx))
287 .unwrap()
288 .await
289 .unwrap();
290 worktree_b
291 .condition(&cx_b, |_, cx| {
292 buffer_b.read(cx).file().unwrap().mtime != mtime
293 })
294 .await;
295 buffer_b.read_with(&cx_b, |buf, _| {
296 assert!(!buf.is_dirty());
297 assert!(!buf.has_conflict());
298 });
299
300 buffer_b.update(&mut cx_b, |buf, cx| buf.edit([0..0], "hello ", cx));
301 buffer_b.read_with(&cx_b, |buf, _| {
302 assert!(buf.is_dirty());
303 assert!(!buf.has_conflict());
304 });
305}
306
307#[gpui::test]
308async fn test_editing_while_guest_opens_buffer(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
309 let lang_registry = Arc::new(LanguageRegistry::new());
310
311 // Connect to a server as 2 clients.
312 let mut server = TestServer::start().await;
313 let client_a = server.create_client(&mut cx_a, "user_a").await;
314 let client_b = server.create_client(&mut cx_b, "user_b").await;
315
316 // Share a local worktree as client A
317 let fs = Arc::new(InMemoryFs::new());
318 fs.save(Path::new("/a.txt"), &"a-contents".into())
319 .await
320 .unwrap();
321 let worktree_a =
322 cx_a.add_model(|cx| Worktree::test(Path::new("/"), lang_registry.clone(), fs.clone(), cx));
323 worktree_a
324 .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
325 .await;
326 let (worktree_id, worktree_token) = worktree_a
327 .update(&mut cx_a, |tree, cx| {
328 tree.as_local_mut().unwrap().share(client_a.clone(), cx)
329 })
330 .await
331 .unwrap();
332
333 // Join that worktree as client B, and see that a guest has joined as client A.
334 let worktree_b = Worktree::open_remote(
335 client_b.clone(),
336 worktree_id,
337 worktree_token,
338 lang_registry.clone(),
339 &mut cx_b.to_async(),
340 )
341 .await
342 .unwrap();
343
344 let buffer_a = worktree_a
345 .update(&mut cx_a, |tree, cx| tree.open_buffer("a.txt", cx))
346 .await
347 .unwrap();
348 let buffer_b = cx_b
349 .background()
350 .spawn(worktree_b.update(&mut cx_b, |worktree, cx| worktree.open_buffer("a.txt", cx)));
351
352 task::yield_now().await;
353 buffer_a.update(&mut cx_a, |buf, cx| buf.edit([0..0], "z", cx));
354
355 let text = buffer_a.read_with(&cx_a, |buf, _| buf.text());
356 let buffer_b = buffer_b.await.unwrap();
357 buffer_b.condition(&cx_b, |buf, _| buf.text() == text).await;
358}
359
360#[gpui::test]
361async fn test_peer_disconnection(mut cx_a: TestAppContext, cx_b: TestAppContext) {
362 let lang_registry = Arc::new(LanguageRegistry::new());
363
364 // Connect to a server as 2 clients.
365 let mut server = TestServer::start().await;
366 let client_a = server.create_client(&mut cx_a, "user_a").await;
367 let client_b = server.create_client(&mut cx_a, "user_b").await;
368
369 // Share a local worktree as client A
370 let dir = temp_tree(json!({
371 "a.txt": "a-contents",
372 "b.txt": "b-contents",
373 }));
374 let worktree_a = cx_a.add_model(|cx| Worktree::local(dir.path(), lang_registry.clone(), cx));
375 worktree_a
376 .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
377 .await;
378 let (worktree_id, worktree_token) = worktree_a
379 .update(&mut cx_a, |tree, cx| {
380 tree.as_local_mut().unwrap().share(client_a.clone(), cx)
381 })
382 .await
383 .unwrap();
384
385 // Join that worktree as client B, and see that a guest has joined as client A.
386 let _worktree_b = Worktree::open_remote(
387 client_b.clone(),
388 worktree_id,
389 worktree_token,
390 lang_registry.clone(),
391 &mut cx_b.to_async(),
392 )
393 .await
394 .unwrap();
395 worktree_a
396 .condition(&cx_a, |tree, _| tree.peers().len() == 1)
397 .await;
398
399 // Drop client B's connection and ensure client A observes client B leaving the worktree.
400 client_b.disconnect().await.unwrap();
401 worktree_a
402 .condition(&cx_a, |tree, _| tree.peers().len() == 0)
403 .await;
404}
405
406struct TestServer {
407 peer: Arc<Peer>,
408 app_state: Arc<AppState>,
409 db_name: String,
410 router: Arc<Router>,
411}
412
413impl TestServer {
414 async fn start() -> Self {
415 let mut rng = StdRng::from_entropy();
416 let db_name = format!("zed-test-{}", rng.gen::<u128>());
417 let app_state = Self::build_app_state(&db_name).await;
418 let peer = Peer::new();
419 let mut router = Router::new();
420 add_rpc_routes(&mut router, &app_state, &peer);
421 Self {
422 peer,
423 router: Arc::new(router),
424 app_state,
425 db_name,
426 }
427 }
428
429 async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> Client {
430 let user_id = admin::create_user(&self.app_state.db, name, false)
431 .await
432 .unwrap();
433 let lang_registry = Arc::new(LanguageRegistry::new());
434 let client = Client::new(lang_registry.clone());
435 let mut client_router = ForegroundRouter::new();
436 cx.update(|cx| zed::worktree::init(cx, &client, &mut client_router));
437
438 let (client_conn, server_conn) = Channel::bidirectional();
439 cx.background()
440 .spawn(rpc::handle_connection(
441 self.peer.clone(),
442 self.router.clone(),
443 self.app_state.clone(),
444 name.to_string(),
445 server_conn,
446 user_id,
447 ))
448 .detach();
449 client
450 .add_connection(client_conn, Arc::new(client_router), cx.to_async())
451 .await
452 .unwrap();
453
454 // Reset the executor because running SQL queries has a non-deterministic impact on it.
455 cx.foreground().reset();
456 client
457 }
458
459 async fn build_app_state(db_name: &str) -> Arc<AppState> {
460 let mut config = Config::default();
461 config.session_secret = "a".repeat(32);
462 config.database_url = format!("postgres://postgres@localhost/{}", db_name);
463
464 Self::create_db(&config.database_url).await;
465 let db = PgPoolOptions::new()
466 .max_connections(5)
467 .connect(&config.database_url)
468 .await
469 .expect("failed to connect to postgres database");
470 let migrator = Migrator::new(Path::new("./migrations")).await.unwrap();
471 migrator.run(&db).await.unwrap();
472
473 let github_client = github::AppClient::test();
474 Arc::new(AppState {
475 db,
476 handlebars: Default::default(),
477 auth_client: auth::build_client("", ""),
478 repo_client: github::RepoClient::test(&github_client),
479 github_client,
480 rpc: Default::default(),
481 config,
482 })
483 }
484
485 async fn create_db(url: &str) {
486 // Enable tests to run in parallel by serializing the creation of each test database.
487 lazy_static::lazy_static! {
488 static ref DB_CREATION: async_std::sync::Mutex<()> = async_std::sync::Mutex::new(());
489 }
490
491 let _lock = DB_CREATION.lock().await;
492 Postgres::create_database(url)
493 .await
494 .expect("failed to create test database");
495 }
496}
497
498impl Drop for TestServer {
499 fn drop(&mut self) {
500 task::block_on(async {
501 self.peer.reset().await;
502 self.app_state
503 .db
504 .execute(
505 format!(
506 "
507 SELECT pg_terminate_backend(pg_stat_activity.pid)
508 FROM pg_stat_activity
509 WHERE pg_stat_activity.datname = '{}' AND pid <> pg_backend_pid();",
510 self.db_name,
511 )
512 .as_str(),
513 )
514 .await
515 .unwrap();
516 self.app_state.db.close().await;
517 Postgres::drop_database(&self.app_state.config.database_url)
518 .await
519 .unwrap();
520 });
521 }
522}
523
524struct EmptyView;
525
526impl gpui::Entity for EmptyView {
527 type Event = ();
528}
529
530impl gpui::View for EmptyView {
531 fn ui_name() -> &'static str {
532 "empty view"
533 }
534
535 fn render<'a>(&self, _: &gpui::AppContext) -> gpui::ElementBox {
536 gpui::Element::boxed(gpui::elements::Empty)
537 }
538}