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