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    let lang_registry = Arc::new(LanguageRegistry::new());
484
485    // Connect to a server as 2 clients.
486    let mut server = TestServer::start().await;
487    let (user_id_a, client_a) = server.create_client(&mut cx_a, "user_a").await;
488    let (user_id_b, client_b) = server.create_client(&mut cx_a, "user_b").await;
489
490    // Create an org that includes these 2 users and 1 other user.
491    let db = &server.app_state.db;
492    let user_id_c = db.create_user("user_c", false).await.unwrap();
493    let org_id = db.create_org("Test Org", "test-org").await.unwrap();
494    db.add_org_member(org_id, user_id_a, false).await.unwrap();
495    db.add_org_member(org_id, user_id_b, false).await.unwrap();
496    db.add_org_member(org_id, user_id_c, false).await.unwrap();
497
498    // Create a channel that includes all the users.
499    let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap();
500    db.add_channel_member(channel_id, user_id_a, false)
501        .await
502        .unwrap();
503    db.add_channel_member(channel_id, user_id_b, false)
504        .await
505        .unwrap();
506    db.add_channel_member(channel_id, user_id_c, false)
507        .await
508        .unwrap();
509    db.create_channel_message(
510        channel_id,
511        user_id_c,
512        "first message!",
513        OffsetDateTime::now_utc(),
514    )
515    .await
516    .unwrap();
517
518    let channels_a = ChannelList::new(client_a, &mut cx_a.to_async())
519        .await
520        .unwrap();
521    let channels_b = ChannelList::new(client_b, &mut cx_b.to_async())
522        .await
523        .unwrap();
524    channels_a.read_with(&cx_a, |list, _| {
525        assert_eq!(
526            list.available_channels(),
527            &[ChannelDetails {
528                id: channel_id.to_proto(),
529                name: "test-channel".to_string()
530            }]
531        )
532    });
533
534    let channel_a = channels_a.read_with(&cx_a, |this, cx| {
535        this.get_channel(channel_id.to_proto(), &cx).unwrap()
536    });
537}
538
539struct TestServer {
540    peer: Arc<Peer>,
541    app_state: Arc<AppState>,
542    server: Arc<rpc::Server>,
543    db_name: String,
544}
545
546impl TestServer {
547    async fn start() -> Self {
548        let mut rng = StdRng::from_entropy();
549        let db_name = format!("zed-test-{}", rng.gen::<u128>());
550        let app_state = Self::build_app_state(&db_name).await;
551        let peer = Peer::new();
552        let server = rpc::Server::new(app_state.clone(), peer.clone());
553        Self {
554            peer,
555            app_state,
556            server,
557            db_name,
558        }
559    }
560
561    async fn create_client(
562        &mut self,
563        cx: &mut TestAppContext,
564        name: &str,
565    ) -> (UserId, Arc<Client>) {
566        let user_id = self.app_state.db.create_user(name, false).await.unwrap();
567        let client = Client::new();
568        let (client_conn, server_conn) = Channel::bidirectional();
569        cx.background()
570            .spawn(
571                self.server
572                    .handle_connection(server_conn, name.to_string(), user_id),
573            )
574            .detach();
575        client
576            .add_connection(client_conn, cx.to_async())
577            .await
578            .unwrap();
579
580        (user_id, client)
581    }
582
583    async fn build_app_state(db_name: &str) -> Arc<AppState> {
584        let mut config = Config::default();
585        config.session_secret = "a".repeat(32);
586        config.database_url = format!("postgres://postgres@localhost/{}", db_name);
587
588        Self::create_db(&config.database_url).await;
589        let db = db::Db(
590            db::DbOptions::new()
591                .max_connections(5)
592                .connect(&config.database_url)
593                .await
594                .expect("failed to connect to postgres database"),
595        );
596        let migrator = Migrator::new(Path::new(concat!(
597            env!("CARGO_MANIFEST_DIR"),
598            "/migrations"
599        )))
600        .await
601        .unwrap();
602        migrator.run(&db.0).await.unwrap();
603
604        let github_client = github::AppClient::test();
605        Arc::new(AppState {
606            db,
607            handlebars: Default::default(),
608            auth_client: auth::build_client("", ""),
609            repo_client: github::RepoClient::test(&github_client),
610            github_client,
611            config,
612        })
613    }
614
615    async fn create_db(url: &str) {
616        // Enable tests to run in parallel by serializing the creation of each test database.
617        lazy_static::lazy_static! {
618            static ref DB_CREATION: async_std::sync::Mutex<()> = async_std::sync::Mutex::new(());
619        }
620
621        let _lock = DB_CREATION.lock().await;
622        Postgres::create_database(url)
623            .await
624            .expect("failed to create test database");
625    }
626}
627
628impl Drop for TestServer {
629    fn drop(&mut self) {
630        task::block_on(async {
631            self.peer.reset().await;
632            self.app_state
633                .db
634                .execute(
635                    format!(
636                        "
637                        SELECT pg_terminate_backend(pg_stat_activity.pid)
638                        FROM pg_stat_activity
639                        WHERE pg_stat_activity.datname = '{}' AND pid <> pg_backend_pid();",
640                        self.db_name,
641                    )
642                    .as_str(),
643                )
644                .await
645                .unwrap();
646            self.app_state.db.close().await;
647            Postgres::drop_database(&self.app_state.config.database_url)
648                .await
649                .unwrap();
650        });
651    }
652}
653
654struct EmptyView;
655
656impl gpui::Entity for EmptyView {
657    type Event = ();
658}
659
660impl gpui::View for EmptyView {
661    fn ui_name() -> &'static str {
662        "empty view"
663    }
664
665    fn render<'a>(&self, _: &gpui::RenderContext<Self>) -> gpui::ElementBox {
666        gpui::Element::boxed(gpui::elements::Empty)
667    }
668}