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