tests.rs

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