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