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