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::{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}