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