tests.rs

  1use crate::{
  2    auth,
  3    db::{self, UserId},
  4    github,
  5    rpc::{self, add_rpc_routes},
  6    AppState, Config,
  7};
  8use async_std::task;
  9use gpui::TestAppContext;
 10use rand::prelude::*;
 11use serde_json::json;
 12use sqlx::{
 13    migrate::{MigrateDatabase, Migrator},
 14    Executor as _, Postgres,
 15};
 16use std::{path::Path, sync::Arc};
 17use zed::{
 18    editor::Editor,
 19    fs::{FakeFs, Fs as _},
 20    language::LanguageRegistry,
 21    rpc::Client,
 22    settings,
 23    test::Channel,
 24    worktree::Worktree,
 25};
 26use zrpc::{ForegroundRouter, Peer, Router};
 27
 28#[gpui::test]
 29async fn test_share_worktree(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
 30    let (window_b, _) = cx_b.add_window(|_| EmptyView);
 31    let settings = settings::channel(&cx_b.font_cache()).unwrap().1;
 32    let lang_registry = Arc::new(LanguageRegistry::new());
 33
 34    // Connect to a server as 2 clients.
 35    let mut server = TestServer::start().await;
 36    let (_, client_a) = server.create_client(&mut cx_a, "user_a").await;
 37    let (_, client_b) = server.create_client(&mut cx_b, "user_b").await;
 38
 39    cx_a.foreground().forbid_parking();
 40
 41    // Share a local worktree as client A
 42    let fs = Arc::new(FakeFs::new());
 43    fs.insert_tree(
 44        "/a",
 45        json!({
 46            "a.txt": "a-contents",
 47            "b.txt": "b-contents",
 48        }),
 49    )
 50    .await;
 51    let worktree_a = Worktree::open_local(
 52        "/a".as_ref(),
 53        lang_registry.clone(),
 54        fs,
 55        &mut cx_a.to_async(),
 56    )
 57    .await
 58    .unwrap();
 59    worktree_a
 60        .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
 61        .await;
 62    let (worktree_id, worktree_token) = worktree_a
 63        .update(&mut cx_a, |tree, cx| {
 64            tree.as_local_mut().unwrap().share(client_a.clone(), cx)
 65        })
 66        .await
 67        .unwrap();
 68
 69    // Join that worktree as client B, and see that a guest has joined as client A.
 70    let worktree_b = Worktree::open_remote(
 71        client_b.clone(),
 72        worktree_id,
 73        worktree_token,
 74        lang_registry.clone(),
 75        &mut cx_b.to_async(),
 76    )
 77    .await
 78    .unwrap();
 79    let replica_id_b = worktree_b.read_with(&cx_b, |tree, _| tree.replica_id());
 80    worktree_a
 81        .condition(&cx_a, |tree, _| {
 82            tree.peers()
 83                .values()
 84                .any(|replica_id| *replica_id == replica_id_b)
 85        })
 86        .await;
 87
 88    // Open the same file as client B and client A.
 89    let buffer_b = worktree_b
 90        .update(&mut cx_b, |worktree, cx| worktree.open_buffer("b.txt", cx))
 91        .await
 92        .unwrap();
 93    buffer_b.read_with(&cx_b, |buf, _| assert_eq!(buf.text(), "b-contents"));
 94    worktree_a.read_with(&cx_a, |tree, cx| assert!(tree.has_open_buffer("b.txt", cx)));
 95    let buffer_a = worktree_a
 96        .update(&mut cx_a, |tree, cx| tree.open_buffer("b.txt", cx))
 97        .await
 98        .unwrap();
 99
100    // Create a selection set as client B and see that selection set as client A.
101    let editor_b = cx_b.add_view(window_b, |cx| Editor::for_buffer(buffer_b, settings, cx));
102    buffer_a
103        .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 1)
104        .await;
105
106    // Edit the buffer as client B and see that edit as client A.
107    editor_b.update(&mut cx_b, |editor, cx| {
108        editor.insert(&"ok, ".to_string(), cx)
109    });
110    buffer_a
111        .condition(&cx_a, |buffer, _| buffer.text() == "ok, b-contents")
112        .await;
113
114    // Remove the selection set as client B, see those selections disappear as client A.
115    cx_b.update(move |_| drop(editor_b));
116    buffer_a
117        .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 0)
118        .await;
119
120    // Close the buffer as client A, see that the buffer is closed.
121    drop(buffer_a);
122    worktree_a
123        .condition(&cx_a, |tree, cx| !tree.has_open_buffer("b.txt", cx))
124        .await;
125
126    // Dropping the worktree removes client B from client A's peers.
127    cx_b.update(move |_| drop(worktree_b));
128    worktree_a
129        .condition(&cx_a, |tree, _| tree.peers().is_empty())
130        .await;
131}
132
133#[gpui::test]
134async fn test_propagate_saves_and_fs_changes_in_shared_worktree(
135    mut cx_a: TestAppContext,
136    mut cx_b: TestAppContext,
137    mut cx_c: TestAppContext,
138) {
139    let lang_registry = Arc::new(LanguageRegistry::new());
140
141    // Connect to a server as 3 clients.
142    let mut server = TestServer::start().await;
143    let (_, client_a) = server.create_client(&mut cx_a, "user_a").await;
144    let (_, client_b) = server.create_client(&mut cx_b, "user_b").await;
145    let (_, client_c) = server.create_client(&mut cx_c, "user_c").await;
146
147    cx_a.foreground().forbid_parking();
148
149    let fs = Arc::new(FakeFs::new());
150
151    // Share a worktree as client A.
152    fs.insert_tree(
153        "/a",
154        json!({
155            "file1": "",
156            "file2": ""
157        }),
158    )
159    .await;
160
161    let worktree_a = Worktree::open_local(
162        "/a".as_ref(),
163        lang_registry.clone(),
164        fs.clone(),
165        &mut cx_a.to_async(),
166    )
167    .await
168    .unwrap();
169    worktree_a
170        .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
171        .await;
172    let (worktree_id, worktree_token) = worktree_a
173        .update(&mut cx_a, |tree, cx| {
174            tree.as_local_mut().unwrap().share(client_a.clone(), cx)
175        })
176        .await
177        .unwrap();
178
179    // Join that worktree as clients B and C.
180    let worktree_b = Worktree::open_remote(
181        client_b.clone(),
182        worktree_id,
183        worktree_token.clone(),
184        lang_registry.clone(),
185        &mut cx_b.to_async(),
186    )
187    .await
188    .unwrap();
189    let worktree_c = Worktree::open_remote(
190        client_c.clone(),
191        worktree_id,
192        worktree_token,
193        lang_registry.clone(),
194        &mut cx_c.to_async(),
195    )
196    .await
197    .unwrap();
198
199    // Open and edit a buffer as both guests B and C.
200    let buffer_b = worktree_b
201        .update(&mut cx_b, |tree, cx| tree.open_buffer("file1", cx))
202        .await
203        .unwrap();
204    let buffer_c = worktree_c
205        .update(&mut cx_c, |tree, cx| tree.open_buffer("file1", cx))
206        .await
207        .unwrap();
208    buffer_b.update(&mut cx_b, |buf, cx| buf.edit([0..0], "i-am-b, ", cx));
209    buffer_c.update(&mut cx_c, |buf, cx| buf.edit([0..0], "i-am-c, ", cx));
210
211    // Open and edit that buffer as the host.
212    let buffer_a = worktree_a
213        .update(&mut cx_a, |tree, cx| tree.open_buffer("file1", cx))
214        .await
215        .unwrap();
216
217    buffer_a
218        .condition(&mut cx_a, |buf, _| buf.text() == "i-am-c, i-am-b, ")
219        .await;
220    buffer_a.update(&mut cx_a, |buf, cx| {
221        buf.edit([buf.len()..buf.len()], "i-am-a", cx)
222    });
223
224    // Wait for edits to propagate
225    buffer_a
226        .condition(&mut cx_a, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a")
227        .await;
228    buffer_b
229        .condition(&mut cx_b, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a")
230        .await;
231    buffer_c
232        .condition(&mut cx_c, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a")
233        .await;
234
235    // Edit the buffer as the host and concurrently save as guest B.
236    let save_b = buffer_b.update(&mut cx_b, |buf, cx| buf.save(cx).unwrap());
237    buffer_a.update(&mut cx_a, |buf, cx| buf.edit([0..0], "hi-a, ", cx));
238    save_b.await.unwrap();
239    assert_eq!(
240        fs.load("/a/file1".as_ref()).await.unwrap(),
241        "hi-a, i-am-c, i-am-b, i-am-a"
242    );
243    buffer_a.read_with(&cx_a, |buf, _| assert!(!buf.is_dirty()));
244    buffer_b.read_with(&cx_b, |buf, _| assert!(!buf.is_dirty()));
245    buffer_c.condition(&cx_c, |buf, _| !buf.is_dirty()).await;
246
247    // Make changes on host's file system, see those changes on the guests.
248    fs.rename("/a/file2".as_ref(), "/a/file3".as_ref())
249        .await
250        .unwrap();
251    fs.insert_file(Path::new("/a/file4"), "4".into())
252        .await
253        .unwrap();
254
255    worktree_b
256        .condition(&cx_b, |tree, _| tree.file_count() == 3)
257        .await;
258    worktree_c
259        .condition(&cx_c, |tree, _| tree.file_count() == 3)
260        .await;
261    worktree_b.read_with(&cx_b, |tree, _| {
262        assert_eq!(
263            tree.paths()
264                .map(|p| p.to_string_lossy())
265                .collect::<Vec<_>>(),
266            &["file1", "file3", "file4"]
267        )
268    });
269    worktree_c.read_with(&cx_c, |tree, _| {
270        assert_eq!(
271            tree.paths()
272                .map(|p| p.to_string_lossy())
273                .collect::<Vec<_>>(),
274            &["file1", "file3", "file4"]
275        )
276    });
277}
278
279#[gpui::test]
280async fn test_buffer_conflict_after_save(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
281    let lang_registry = Arc::new(LanguageRegistry::new());
282
283    // Connect to a server as 2 clients.
284    let mut server = TestServer::start().await;
285    let (_, client_a) = server.create_client(&mut cx_a, "user_a").await;
286    let (_, client_b) = server.create_client(&mut cx_b, "user_b").await;
287
288    cx_a.foreground().forbid_parking();
289
290    // Share a local worktree as client A
291    let fs = Arc::new(FakeFs::new());
292    fs.save(Path::new("/a.txt"), &"a-contents".into())
293        .await
294        .unwrap();
295    let worktree_a = Worktree::open_local(
296        "/".as_ref(),
297        lang_registry.clone(),
298        fs,
299        &mut cx_a.to_async(),
300    )
301    .await
302    .unwrap();
303    worktree_a
304        .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
305        .await;
306    let (worktree_id, worktree_token) = worktree_a
307        .update(&mut cx_a, |tree, cx| {
308            tree.as_local_mut().unwrap().share(client_a.clone(), cx)
309        })
310        .await
311        .unwrap();
312
313    // Join that worktree as client B, and see that a guest has joined as client A.
314    let worktree_b = Worktree::open_remote(
315        client_b.clone(),
316        worktree_id,
317        worktree_token,
318        lang_registry.clone(),
319        &mut cx_b.to_async(),
320    )
321    .await
322    .unwrap();
323
324    let buffer_b = worktree_b
325        .update(&mut cx_b, |worktree, cx| worktree.open_buffer("a.txt", cx))
326        .await
327        .unwrap();
328    let mtime = buffer_b.read_with(&cx_b, |buf, _| buf.file().unwrap().mtime);
329
330    buffer_b.update(&mut cx_b, |buf, cx| buf.edit([0..0], "world ", cx));
331    buffer_b.read_with(&cx_b, |buf, _| {
332        assert!(buf.is_dirty());
333        assert!(!buf.has_conflict());
334    });
335
336    buffer_b
337        .update(&mut cx_b, |buf, cx| buf.save(cx))
338        .unwrap()
339        .await
340        .unwrap();
341    worktree_b
342        .condition(&cx_b, |_, cx| {
343            buffer_b.read(cx).file().unwrap().mtime != mtime
344        })
345        .await;
346    buffer_b.read_with(&cx_b, |buf, _| {
347        assert!(!buf.is_dirty());
348        assert!(!buf.has_conflict());
349    });
350
351    buffer_b.update(&mut cx_b, |buf, cx| buf.edit([0..0], "hello ", cx));
352    buffer_b.read_with(&cx_b, |buf, _| {
353        assert!(buf.is_dirty());
354        assert!(!buf.has_conflict());
355    });
356}
357
358#[gpui::test]
359async fn test_editing_while_guest_opens_buffer(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
360    let lang_registry = Arc::new(LanguageRegistry::new());
361
362    // Connect to a server as 2 clients.
363    let mut server = TestServer::start().await;
364    let (_, client_a) = server.create_client(&mut cx_a, "user_a").await;
365    let (_, client_b) = server.create_client(&mut cx_b, "user_b").await;
366
367    cx_a.foreground().forbid_parking();
368
369    // Share a local worktree as client A
370    let fs = Arc::new(FakeFs::new());
371    fs.save(Path::new("/a.txt"), &"a-contents".into())
372        .await
373        .unwrap();
374    let worktree_a = Worktree::open_local(
375        "/".as_ref(),
376        lang_registry.clone(),
377        fs,
378        &mut cx_a.to_async(),
379    )
380    .await
381    .unwrap();
382    worktree_a
383        .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
384        .await;
385    let (worktree_id, worktree_token) = worktree_a
386        .update(&mut cx_a, |tree, cx| {
387            tree.as_local_mut().unwrap().share(client_a.clone(), cx)
388        })
389        .await
390        .unwrap();
391
392    // Join that worktree as client B, and see that a guest has joined as client A.
393    let worktree_b = Worktree::open_remote(
394        client_b.clone(),
395        worktree_id,
396        worktree_token,
397        lang_registry.clone(),
398        &mut cx_b.to_async(),
399    )
400    .await
401    .unwrap();
402
403    let buffer_a = worktree_a
404        .update(&mut cx_a, |tree, cx| tree.open_buffer("a.txt", cx))
405        .await
406        .unwrap();
407    let buffer_b = cx_b
408        .background()
409        .spawn(worktree_b.update(&mut cx_b, |worktree, cx| worktree.open_buffer("a.txt", cx)));
410
411    task::yield_now().await;
412    buffer_a.update(&mut cx_a, |buf, cx| buf.edit([0..0], "z", cx));
413
414    let text = buffer_a.read_with(&cx_a, |buf, _| buf.text());
415    let buffer_b = buffer_b.await.unwrap();
416    buffer_b.condition(&cx_b, |buf, _| buf.text() == text).await;
417}
418
419#[gpui::test]
420async fn test_peer_disconnection(mut cx_a: TestAppContext, cx_b: TestAppContext) {
421    let lang_registry = Arc::new(LanguageRegistry::new());
422
423    // Connect to a server as 2 clients.
424    let mut server = TestServer::start().await;
425    let (_, client_a) = server.create_client(&mut cx_a, "user_a").await;
426    let (_, client_b) = server.create_client(&mut cx_a, "user_b").await;
427
428    cx_a.foreground().forbid_parking();
429
430    // Share a local worktree as client A
431    let fs = Arc::new(FakeFs::new());
432    fs.insert_tree(
433        "/a",
434        json!({
435            "a.txt": "a-contents",
436            "b.txt": "b-contents",
437        }),
438    )
439    .await;
440    let worktree_a = Worktree::open_local(
441        "/a".as_ref(),
442        lang_registry.clone(),
443        fs,
444        &mut cx_a.to_async(),
445    )
446    .await
447    .unwrap();
448    worktree_a
449        .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
450        .await;
451    let (worktree_id, worktree_token) = worktree_a
452        .update(&mut cx_a, |tree, cx| {
453            tree.as_local_mut().unwrap().share(client_a.clone(), cx)
454        })
455        .await
456        .unwrap();
457
458    // Join that worktree as client B, and see that a guest has joined as client A.
459    let _worktree_b = Worktree::open_remote(
460        client_b.clone(),
461        worktree_id,
462        worktree_token,
463        lang_registry.clone(),
464        &mut cx_b.to_async(),
465    )
466    .await
467    .unwrap();
468    worktree_a
469        .condition(&cx_a, |tree, _| tree.peers().len() == 1)
470        .await;
471
472    // Drop client B's connection and ensure client A observes client B leaving the worktree.
473    client_b.disconnect().await.unwrap();
474    worktree_a
475        .condition(&cx_a, |tree, _| tree.peers().len() == 0)
476        .await;
477}
478
479#[gpui::test]
480async fn test_basic_chat(mut cx_a: TestAppContext, cx_b: TestAppContext) {
481    let lang_registry = Arc::new(LanguageRegistry::new());
482
483    // Connect to a server as 2 clients.
484    let mut server = TestServer::start().await;
485    let (user_id_a, client_a) = server.create_client(&mut cx_a, "user_a").await;
486    let (user_id_b, client_b) = server.create_client(&mut cx_a, "user_b").await;
487
488    // Create a channel that includes these 2 users and 1 other user.
489    let db = &server.app_state.db;
490    let user_id_c = db.create_user("user_c", false).await.unwrap();
491    let org_id = db.create_org("Test Org", "test-org").await.unwrap();
492    let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap();
493    db.add_channel_member(channel_id, user_id_a, false)
494        .await
495        .unwrap();
496    db.add_channel_member(channel_id, user_id_b, false)
497        .await
498        .unwrap();
499    db.add_channel_member(channel_id, user_id_c, false)
500        .await
501        .unwrap();
502    db.create_channel_message(channel_id, user_id_c, "first message!")
503        .await
504        .unwrap();
505
506    // let chatroom_a = ChatRoom::
507}
508
509struct TestServer {
510    peer: Arc<Peer>,
511    app_state: Arc<AppState>,
512    db_name: String,
513    router: Arc<Router>,
514}
515
516impl TestServer {
517    async fn start() -> Self {
518        let mut rng = StdRng::from_entropy();
519        let db_name = format!("zed-test-{}", rng.gen::<u128>());
520        let app_state = Self::build_app_state(&db_name).await;
521        let peer = Peer::new();
522        let mut router = Router::new();
523        add_rpc_routes(&mut router, &app_state, &peer);
524        Self {
525            peer,
526            router: Arc::new(router),
527            app_state,
528            db_name,
529        }
530    }
531
532    async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> (UserId, Client) {
533        let user_id = self.app_state.db.create_user(name, false).await.unwrap();
534        let lang_registry = Arc::new(LanguageRegistry::new());
535        let client = Client::new(lang_registry.clone());
536        let mut client_router = ForegroundRouter::new();
537        cx.update(|cx| zed::worktree::init(cx, &client, &mut client_router));
538
539        let (client_conn, server_conn) = Channel::bidirectional();
540        cx.background()
541            .spawn(rpc::handle_connection(
542                self.peer.clone(),
543                self.router.clone(),
544                self.app_state.clone(),
545                name.to_string(),
546                server_conn,
547                user_id,
548            ))
549            .detach();
550        client
551            .add_connection(client_conn, Arc::new(client_router), cx.to_async())
552            .await
553            .unwrap();
554
555        (user_id, client)
556    }
557
558    async fn build_app_state(db_name: &str) -> Arc<AppState> {
559        let mut config = Config::default();
560        config.session_secret = "a".repeat(32);
561        config.database_url = format!("postgres://postgres@localhost/{}", db_name);
562
563        Self::create_db(&config.database_url).await;
564        let db = db::Db(
565            db::DbOptions::new()
566                .max_connections(5)
567                .connect(&config.database_url)
568                .await
569                .expect("failed to connect to postgres database"),
570        );
571        let migrator = Migrator::new(Path::new(concat!(
572            env!("CARGO_MANIFEST_DIR"),
573            "/migrations"
574        )))
575        .await
576        .unwrap();
577        migrator.run(&db.0).await.unwrap();
578
579        let github_client = github::AppClient::test();
580        Arc::new(AppState {
581            db,
582            handlebars: Default::default(),
583            auth_client: auth::build_client("", ""),
584            repo_client: github::RepoClient::test(&github_client),
585            github_client,
586            rpc: Default::default(),
587            config,
588        })
589    }
590
591    async fn create_db(url: &str) {
592        // Enable tests to run in parallel by serializing the creation of each test database.
593        lazy_static::lazy_static! {
594            static ref DB_CREATION: async_std::sync::Mutex<()> = async_std::sync::Mutex::new(());
595        }
596
597        let _lock = DB_CREATION.lock().await;
598        Postgres::create_database(url)
599            .await
600            .expect("failed to create test database");
601    }
602}
603
604impl Drop for TestServer {
605    fn drop(&mut self) {
606        task::block_on(async {
607            self.peer.reset().await;
608            self.app_state
609                .db
610                .execute(
611                    format!(
612                        "
613                        SELECT pg_terminate_backend(pg_stat_activity.pid)
614                        FROM pg_stat_activity
615                        WHERE pg_stat_activity.datname = '{}' AND pid <> pg_backend_pid();",
616                        self.db_name,
617                    )
618                    .as_str(),
619                )
620                .await
621                .unwrap();
622            self.app_state.db.close().await;
623            Postgres::drop_database(&self.app_state.config.database_url)
624                .await
625                .unwrap();
626        });
627    }
628}
629
630struct EmptyView;
631
632impl gpui::Entity for EmptyView {
633    type Event = ();
634}
635
636impl gpui::View for EmptyView {
637    fn ui_name() -> &'static str {
638        "empty view"
639    }
640
641    fn render<'a>(&self, _: &gpui::RenderContext<Self>) -> gpui::ElementBox {
642        gpui::Element::boxed(gpui::elements::Empty)
643    }
644}