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::{fs, 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 zed_rpc::{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    // Share a worktree as client A.
139    let dir = temp_tree(json!({
140        "file1": "",
141        "file2": ""
142    }));
143    let worktree_a = Worktree::open_local(
144        dir.path(),
145        lang_registry.clone(),
146        Arc::new(RealFs),
147        &mut cx_a.to_async(),
148    )
149    .await
150    .unwrap();
151    worktree_a
152        .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
153        .await;
154    let (worktree_id, worktree_token) = worktree_a
155        .update(&mut cx_a, |tree, cx| {
156            tree.as_local_mut().unwrap().share(client_a.clone(), cx)
157        })
158        .await
159        .unwrap();
160
161    // Join that worktree as clients B and C.
162    let worktree_b = Worktree::open_remote(
163        client_b.clone(),
164        worktree_id,
165        worktree_token.clone(),
166        lang_registry.clone(),
167        &mut cx_b.to_async(),
168    )
169    .await
170    .unwrap();
171    let worktree_c = Worktree::open_remote(
172        client_c.clone(),
173        worktree_id,
174        worktree_token,
175        lang_registry.clone(),
176        &mut cx_c.to_async(),
177    )
178    .await
179    .unwrap();
180
181    // Open and edit a buffer as both guests B and C.
182    let buffer_b = worktree_b
183        .update(&mut cx_b, |tree, cx| tree.open_buffer("file1", cx))
184        .await
185        .unwrap();
186    let buffer_c = worktree_c
187        .update(&mut cx_c, |tree, cx| tree.open_buffer("file1", cx))
188        .await
189        .unwrap();
190    buffer_b.update(&mut cx_b, |buf, cx| buf.edit([0..0], "i-am-b, ", cx));
191    buffer_c.update(&mut cx_c, |buf, cx| buf.edit([0..0], "i-am-c, ", cx));
192
193    // Open and edit that buffer as the host.
194    let buffer_a = worktree_a
195        .update(&mut cx_a, |tree, cx| tree.open_buffer("file1", cx))
196        .await
197        .unwrap();
198    buffer_a.update(&mut cx_a, |buf, cx| buf.edit([0..0], "i-am-a", cx));
199
200    // Wait for edits to propagate
201    buffer_a
202        .condition(&mut cx_a, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a")
203        .await;
204    buffer_b
205        .condition(&mut cx_b, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a")
206        .await;
207    buffer_c
208        .condition(&mut cx_c, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a")
209        .await;
210
211    // Edit the buffer as the host and concurrently save as guest B.
212    let save_b = buffer_b.update(&mut cx_b, |buf, cx| buf.save(cx).unwrap());
213    buffer_a.update(&mut cx_a, |buf, cx| buf.edit([0..0], "hi-a, ", cx));
214    save_b.await.unwrap();
215    assert_eq!(
216        fs::read_to_string(dir.path().join("file1")).unwrap(),
217        "hi-a, i-am-c, i-am-b, i-am-a"
218    );
219    buffer_a.read_with(&cx_a, |buf, _| assert!(!buf.is_dirty()));
220    buffer_b.read_with(&cx_b, |buf, _| assert!(!buf.is_dirty()));
221    buffer_c.condition(&cx_c, |buf, _| !buf.is_dirty()).await;
222
223    // Make changes on host's file system, see those changes on the guests.
224    fs::rename(dir.path().join("file2"), dir.path().join("file3")).unwrap();
225    fs::write(dir.path().join("file4"), "4").unwrap();
226    worktree_b
227        .condition(&cx_b, |tree, _| tree.file_count() == 3)
228        .await;
229    worktree_c
230        .condition(&cx_c, |tree, _| tree.file_count() == 3)
231        .await;
232    worktree_b.read_with(&cx_b, |tree, _| {
233        assert_eq!(
234            tree.paths()
235                .map(|p| p.to_string_lossy())
236                .collect::<Vec<_>>(),
237            &["file1", "file3", "file4"]
238        )
239    });
240    worktree_c.read_with(&cx_c, |tree, _| {
241        assert_eq!(
242            tree.paths()
243                .map(|p| p.to_string_lossy())
244                .collect::<Vec<_>>(),
245            &["file1", "file3", "file4"]
246        )
247    });
248}
249
250#[gpui::test]
251async fn test_buffer_conflict_after_save(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
252    let lang_registry = Arc::new(LanguageRegistry::new());
253
254    // Connect to a server as 2 clients.
255    let mut server = TestServer::start().await;
256    let client_a = server.create_client(&mut cx_a, "user_a").await;
257    let client_b = server.create_client(&mut cx_b, "user_b").await;
258
259    // Share a local worktree as client A
260    let fs = Arc::new(FakeFs::new());
261    fs.save(Path::new("/a.txt"), &"a-contents".into())
262        .await
263        .unwrap();
264    let worktree_a = Worktree::open_local(
265        "/".as_ref(),
266        lang_registry.clone(),
267        Arc::new(RealFs),
268        &mut cx_a.to_async(),
269    )
270    .await
271    .unwrap();
272    worktree_a
273        .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
274        .await;
275    let (worktree_id, worktree_token) = worktree_a
276        .update(&mut cx_a, |tree, cx| {
277            tree.as_local_mut().unwrap().share(client_a.clone(), cx)
278        })
279        .await
280        .unwrap();
281
282    // Join that worktree as client B, and see that a guest has joined as client A.
283    let worktree_b = Worktree::open_remote(
284        client_b.clone(),
285        worktree_id,
286        worktree_token,
287        lang_registry.clone(),
288        &mut cx_b.to_async(),
289    )
290    .await
291    .unwrap();
292
293    let buffer_b = worktree_b
294        .update(&mut cx_b, |worktree, cx| worktree.open_buffer("a.txt", cx))
295        .await
296        .unwrap();
297    let mtime = buffer_b.read_with(&cx_b, |buf, _| buf.file().unwrap().mtime);
298
299    buffer_b.update(&mut cx_b, |buf, cx| buf.edit([0..0], "world ", cx));
300    buffer_b.read_with(&cx_b, |buf, _| {
301        assert!(buf.is_dirty());
302        assert!(!buf.has_conflict());
303    });
304
305    buffer_b
306        .update(&mut cx_b, |buf, cx| buf.save(cx))
307        .unwrap()
308        .await
309        .unwrap();
310    worktree_b
311        .condition(&cx_b, |_, cx| {
312            buffer_b.read(cx).file().unwrap().mtime != mtime
313        })
314        .await;
315    buffer_b.read_with(&cx_b, |buf, _| {
316        assert!(!buf.is_dirty());
317        assert!(!buf.has_conflict());
318    });
319
320    buffer_b.update(&mut cx_b, |buf, cx| buf.edit([0..0], "hello ", cx));
321    buffer_b.read_with(&cx_b, |buf, _| {
322        assert!(buf.is_dirty());
323        assert!(!buf.has_conflict());
324    });
325}
326
327#[gpui::test]
328async fn test_editing_while_guest_opens_buffer(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
329    let lang_registry = Arc::new(LanguageRegistry::new());
330
331    // Connect to a server as 2 clients.
332    let mut server = TestServer::start().await;
333    let client_a = server.create_client(&mut cx_a, "user_a").await;
334    let client_b = server.create_client(&mut cx_b, "user_b").await;
335
336    // Share a local worktree as client A
337    let fs = Arc::new(FakeFs::new());
338    fs.save(Path::new("/a.txt"), &"a-contents".into())
339        .await
340        .unwrap();
341    let worktree_a = Worktree::open_local(
342        "/".as_ref(),
343        lang_registry.clone(),
344        Arc::new(RealFs),
345        &mut cx_a.to_async(),
346    )
347    .await
348    .unwrap();
349    worktree_a
350        .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
351        .await;
352    let (worktree_id, worktree_token) = worktree_a
353        .update(&mut cx_a, |tree, cx| {
354            tree.as_local_mut().unwrap().share(client_a.clone(), cx)
355        })
356        .await
357        .unwrap();
358
359    // Join that worktree as client B, and see that a guest has joined as client A.
360    let worktree_b = Worktree::open_remote(
361        client_b.clone(),
362        worktree_id,
363        worktree_token,
364        lang_registry.clone(),
365        &mut cx_b.to_async(),
366    )
367    .await
368    .unwrap();
369
370    let buffer_a = worktree_a
371        .update(&mut cx_a, |tree, cx| tree.open_buffer("a.txt", cx))
372        .await
373        .unwrap();
374    let buffer_b = cx_b
375        .background()
376        .spawn(worktree_b.update(&mut cx_b, |worktree, cx| worktree.open_buffer("a.txt", cx)));
377
378    task::yield_now().await;
379    buffer_a.update(&mut cx_a, |buf, cx| buf.edit([0..0], "z", cx));
380
381    let text = buffer_a.read_with(&cx_a, |buf, _| buf.text());
382    let buffer_b = buffer_b.await.unwrap();
383    buffer_b.condition(&cx_b, |buf, _| buf.text() == text).await;
384}
385
386#[gpui::test]
387async fn test_peer_disconnection(mut cx_a: TestAppContext, cx_b: TestAppContext) {
388    let lang_registry = Arc::new(LanguageRegistry::new());
389
390    // Connect to a server as 2 clients.
391    let mut server = TestServer::start().await;
392    let client_a = server.create_client(&mut cx_a, "user_a").await;
393    let client_b = server.create_client(&mut cx_a, "user_b").await;
394
395    // Share a local worktree as client A
396    let dir = temp_tree(json!({
397        "a.txt": "a-contents",
398        "b.txt": "b-contents",
399    }));
400    let worktree_a = Worktree::open_local(
401        dir.path(),
402        lang_registry.clone(),
403        Arc::new(RealFs),
404        &mut cx_a.to_async(),
405    )
406    .await
407    .unwrap();
408    worktree_a
409        .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
410        .await;
411    let (worktree_id, worktree_token) = worktree_a
412        .update(&mut cx_a, |tree, cx| {
413            tree.as_local_mut().unwrap().share(client_a.clone(), cx)
414        })
415        .await
416        .unwrap();
417
418    // Join that worktree as client B, and see that a guest has joined as client A.
419    let _worktree_b = Worktree::open_remote(
420        client_b.clone(),
421        worktree_id,
422        worktree_token,
423        lang_registry.clone(),
424        &mut cx_b.to_async(),
425    )
426    .await
427    .unwrap();
428    worktree_a
429        .condition(&cx_a, |tree, _| tree.peers().len() == 1)
430        .await;
431
432    // Drop client B's connection and ensure client A observes client B leaving the worktree.
433    client_b.disconnect().await.unwrap();
434    worktree_a
435        .condition(&cx_a, |tree, _| tree.peers().len() == 0)
436        .await;
437}
438
439struct TestServer {
440    peer: Arc<Peer>,
441    app_state: Arc<AppState>,
442    db_name: String,
443    router: Arc<Router>,
444}
445
446impl TestServer {
447    async fn start() -> Self {
448        let mut rng = StdRng::from_entropy();
449        let db_name = format!("zed-test-{}", rng.gen::<u128>());
450        let app_state = Self::build_app_state(&db_name).await;
451        let peer = Peer::new();
452        let mut router = Router::new();
453        add_rpc_routes(&mut router, &app_state, &peer);
454        Self {
455            peer,
456            router: Arc::new(router),
457            app_state,
458            db_name,
459        }
460    }
461
462    async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> Client {
463        let user_id = admin::create_user(&self.app_state.db, name, false)
464            .await
465            .unwrap();
466        let lang_registry = Arc::new(LanguageRegistry::new());
467        let client = Client::new(lang_registry.clone());
468        let mut client_router = ForegroundRouter::new();
469        cx.update(|cx| zed::worktree::init(cx, &client, &mut client_router));
470
471        let (client_conn, server_conn) = Channel::bidirectional();
472        cx.background()
473            .spawn(rpc::handle_connection(
474                self.peer.clone(),
475                self.router.clone(),
476                self.app_state.clone(),
477                name.to_string(),
478                server_conn,
479                user_id,
480            ))
481            .detach();
482        client
483            .add_connection(client_conn, Arc::new(client_router), cx.to_async())
484            .await
485            .unwrap();
486
487        // Reset the executor because running SQL queries has a non-deterministic impact on it.
488        cx.foreground().reset();
489        client
490    }
491
492    async fn build_app_state(db_name: &str) -> Arc<AppState> {
493        let mut config = Config::default();
494        config.session_secret = "a".repeat(32);
495        config.database_url = format!("postgres://postgres@localhost/{}", db_name);
496
497        Self::create_db(&config.database_url).await;
498        let db = PgPoolOptions::new()
499            .max_connections(5)
500            .connect(&config.database_url)
501            .await
502            .expect("failed to connect to postgres database");
503        let migrator = Migrator::new(Path::new("./migrations")).await.unwrap();
504        migrator.run(&db).await.unwrap();
505
506        let github_client = github::AppClient::test();
507        Arc::new(AppState {
508            db,
509            handlebars: Default::default(),
510            auth_client: auth::build_client("", ""),
511            repo_client: github::RepoClient::test(&github_client),
512            github_client,
513            rpc: Default::default(),
514            config,
515        })
516    }
517
518    async fn create_db(url: &str) {
519        // Enable tests to run in parallel by serializing the creation of each test database.
520        lazy_static::lazy_static! {
521            static ref DB_CREATION: async_std::sync::Mutex<()> = async_std::sync::Mutex::new(());
522        }
523
524        let _lock = DB_CREATION.lock().await;
525        Postgres::create_database(url)
526            .await
527            .expect("failed to create test database");
528    }
529}
530
531impl Drop for TestServer {
532    fn drop(&mut self) {
533        task::block_on(async {
534            self.peer.reset().await;
535            self.app_state
536                .db
537                .execute(
538                    format!(
539                        "
540                        SELECT pg_terminate_backend(pg_stat_activity.pid)
541                        FROM pg_stat_activity
542                        WHERE pg_stat_activity.datname = '{}' AND pid <> pg_backend_pid();",
543                        self.db_name,
544                    )
545                    .as_str(),
546                )
547                .await
548                .unwrap();
549            self.app_state.db.close().await;
550            Postgres::drop_database(&self.app_state.config.database_url)
551                .await
552                .unwrap();
553        });
554    }
555}
556
557struct EmptyView;
558
559impl gpui::Entity for EmptyView {
560    type Event = ();
561}
562
563impl gpui::View for EmptyView {
564    fn ui_name() -> &'static str {
565        "empty view"
566    }
567
568    fn render<'a>(&self, _: &gpui::AppContext) -> gpui::ElementBox {
569        gpui::Element::boxed(gpui::elements::Empty)
570    }
571}