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 editor::Editor,
18 fs::{FakeFs, Fs as _},
19 language::LanguageRegistry,
20 rpc::Client,
21 settings,
22 test::Channel,
23 worktree::Worktree,
24};
25use zrpc::Peer;
26
27#[gpui::test]
28async fn test_share_worktree(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
29 tide::log::start();
30
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
517 // let channels_a = client_a.get_channels().await;
518 // assert_eq!(channels_a.len(), 1);
519 // assert_eq!(channels_a[0].read(&cx_a).name(), "test-channel");
520
521 // assert_eq!(
522 // db.get_recent_channel_messages(channel_id, 50)
523 // .await
524 // .unwrap()[0]
525 // .body,
526 // "first message!"
527 // );
528}
529
530struct TestServer {
531 peer: Arc<Peer>,
532 app_state: Arc<AppState>,
533 server: Arc<rpc::Server>,
534 db_name: String,
535}
536
537impl TestServer {
538 async fn start() -> Self {
539 let mut rng = StdRng::from_entropy();
540 let db_name = format!("zed-test-{}", rng.gen::<u128>());
541 let app_state = Self::build_app_state(&db_name).await;
542 let peer = Peer::new();
543 let server = rpc::build_server(&app_state, &peer);
544 Self {
545 peer,
546 app_state,
547 server,
548 db_name,
549 }
550 }
551
552 async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> (UserId, Client) {
553 let user_id = self.app_state.db.create_user(name, false).await.unwrap();
554 let client = Client::new();
555 let (client_conn, server_conn) = Channel::bidirectional();
556 cx.background()
557 .spawn(
558 self.server
559 .handle_connection(server_conn, name.to_string(), user_id),
560 )
561 .detach();
562 client
563 .add_connection(client_conn, cx.to_async())
564 .await
565 .unwrap();
566
567 (user_id, client)
568 }
569
570 async fn build_app_state(db_name: &str) -> Arc<AppState> {
571 let mut config = Config::default();
572 config.session_secret = "a".repeat(32);
573 config.database_url = format!("postgres://postgres@localhost/{}", db_name);
574
575 Self::create_db(&config.database_url).await;
576 let db = db::Db(
577 db::DbOptions::new()
578 .max_connections(5)
579 .connect(&config.database_url)
580 .await
581 .expect("failed to connect to postgres database"),
582 );
583 let migrator = Migrator::new(Path::new(concat!(
584 env!("CARGO_MANIFEST_DIR"),
585 "/migrations"
586 )))
587 .await
588 .unwrap();
589 migrator.run(&db.0).await.unwrap();
590
591 let github_client = github::AppClient::test();
592 Arc::new(AppState {
593 db,
594 handlebars: Default::default(),
595 auth_client: auth::build_client("", ""),
596 repo_client: github::RepoClient::test(&github_client),
597 github_client,
598 rpc: Default::default(),
599 config,
600 })
601 }
602
603 async fn create_db(url: &str) {
604 // Enable tests to run in parallel by serializing the creation of each test database.
605 lazy_static::lazy_static! {
606 static ref DB_CREATION: async_std::sync::Mutex<()> = async_std::sync::Mutex::new(());
607 }
608
609 let _lock = DB_CREATION.lock().await;
610 Postgres::create_database(url)
611 .await
612 .expect("failed to create test database");
613 }
614}
615
616impl Drop for TestServer {
617 fn drop(&mut self) {
618 task::block_on(async {
619 self.peer.reset().await;
620 self.app_state
621 .db
622 .execute(
623 format!(
624 "
625 SELECT pg_terminate_backend(pg_stat_activity.pid)
626 FROM pg_stat_activity
627 WHERE pg_stat_activity.datname = '{}' AND pid <> pg_backend_pid();",
628 self.db_name,
629 )
630 .as_str(),
631 )
632 .await
633 .unwrap();
634 self.app_state.db.close().await;
635 Postgres::drop_database(&self.app_state.config.database_url)
636 .await
637 .unwrap();
638 });
639 }
640}
641
642struct EmptyView;
643
644impl gpui::Entity for EmptyView {
645 type Event = ();
646}
647
648impl gpui::View for EmptyView {
649 fn ui_name() -> &'static str {
650 "empty view"
651 }
652
653 fn render<'a>(&self, _: &gpui::RenderContext<Self>) -> gpui::ElementBox {
654 gpui::Element::boxed(gpui::elements::Empty)
655 }
656}