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