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