tests.rs

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