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