Start removing distinction between host and guest in random collab test

Antonio Scandurra created

Change summary

crates/collab/src/integration_tests.rs | 630 ++++++++++++++++++++-------
crates/fs/src/fs.rs                    |  15 
2 files changed, 468 insertions(+), 177 deletions(-)

Detailed changes

crates/collab/src/integration_tests.rs 🔗

@@ -54,6 +54,7 @@ use std::{
 };
 use theme::ThemeRegistry;
 use unindent::Unindent as _;
+use util::post_inc;
 use workspace::{shared_screen::SharedScreen, Item, SplitDirection, ToggleFollow, Workspace};
 
 #[ctor::ctor]
@@ -5859,37 +5860,6 @@ async fn test_random_collaboration(
     let mut op_start_signals = Vec::new();
 
     let mut next_entity_id = 100000;
-    let mut host_cx = TestAppContext::new(
-        cx.foreground_platform(),
-        cx.platform(),
-        deterministic.build_foreground(next_entity_id),
-        deterministic.build_background(),
-        cx.font_cache(),
-        cx.leak_detector(),
-        next_entity_id,
-        cx.function_name.clone(),
-    );
-    let host = server.create_client(&mut host_cx, "host").await;
-    let host_project = host_cx.update(|cx| {
-        Project::local(
-            host.client.clone(),
-            host.user_store.clone(),
-            host.project_store.clone(),
-            host_language_registry.clone(),
-            fs.clone(),
-            cx,
-        )
-    });
-
-    let (collab_worktree, _) = host_project
-        .update(&mut host_cx, |project, cx| {
-            project.find_or_create_local_worktree("/_collab", true, cx)
-        })
-        .await
-        .unwrap();
-    collab_worktree
-        .read_with(&host_cx, |tree, _| tree.as_local().unwrap().scan_complete())
-        .await;
 
     // Set up fake language servers.
     let mut language = Language::new(
@@ -6018,83 +5988,12 @@ async fn test_random_collaboration(
             ..Default::default()
         }))
         .await;
-    host_language_registry.add(Arc::new(language));
-
-    let host_user_id = host.current_user_id(&host_cx);
-    active_call
-        .update(cx, |call, cx| {
-            call.invite(host_user_id.to_proto(), None, cx)
-        })
-        .await
-        .unwrap();
-    active_call.read_with(cx, |call, cx| call.room().unwrap().read(cx).id());
-    deterministic.run_until_parked();
-    let host_active_call = host_cx.read(ActiveCall::global);
-    host_active_call
-        .update(&mut host_cx, |call, cx| call.accept_incoming(cx))
-        .await
-        .unwrap();
-
-    let host_project_id = host_active_call
-        .update(&mut host_cx, |call, cx| {
-            call.share_project(host_project.clone(), cx)
-        })
-        .await
-        .unwrap();
+    let language = Arc::new(language);
 
     let op_start_signal = futures::channel::mpsc::unbounded();
-    user_ids.push(host_user_id);
-    peer_ids.push(host.peer_id().unwrap());
-    op_start_signals.push(op_start_signal.0);
-    clients.push(host_cx.foreground().spawn(host.simulate_host(
-        host_project,
-        op_start_signal.1,
-        rng.clone(),
-        host_cx,
-    )));
-
-    let disconnect_host_at = if rng.lock().gen_bool(0.2) {
-        rng.lock().gen_range(0..max_operations)
-    } else {
-        max_operations
-    };
 
     let mut operations = 0;
     while operations < max_operations {
-        if operations == disconnect_host_at {
-            server.disconnect_client(peer_ids[0]);
-            deterministic.advance_clock(RECEIVE_TIMEOUT);
-            drop(op_start_signals);
-
-            deterministic.start_waiting();
-            let mut clients = futures::future::join_all(clients).await;
-            deterministic.finish_waiting();
-            deterministic.run_until_parked();
-
-            let (host, host_project, mut host_cx, host_err) = clients.remove(0);
-            if let Some(host_err) = host_err {
-                log::error!("host error - {:?}", host_err);
-            }
-            host_project.read_with(&host_cx, |project, _| assert!(!project.is_shared()));
-            for (guest, guest_project, mut guest_cx, guest_err) in clients {
-                if let Some(guest_err) = guest_err {
-                    log::error!("{} error - {:?}", guest.username, guest_err);
-                }
-
-                guest_project.read_with(&guest_cx, |project, _| assert!(project.is_read_only()));
-                guest_cx.update(|cx| {
-                    cx.clear_globals();
-                    drop((guest, guest_project));
-                });
-            }
-            host_cx.update(|cx| {
-                cx.clear_globals();
-                drop((host, host_project));
-            });
-
-            return;
-        }
-
         let distribution = rng.lock().gen_range(0..100);
         match distribution {
             0..=19 if !available_guests.is_empty() => {
@@ -6115,6 +6014,7 @@ async fn test_random_collaboration(
 
                 deterministic.start_waiting();
                 let guest = server.create_client(&mut guest_cx, &guest_username).await;
+                guest.language_registry.add(language.clone());
                 let guest_user_id = guest.current_user_id(&guest_cx);
 
                 active_call
@@ -6129,27 +6029,14 @@ async fn test_random_collaboration(
                     .update(&mut guest_cx, |call, cx| call.accept_incoming(cx))
                     .await
                     .unwrap();
-
-                let guest_project = Project::remote(
-                    host_project_id,
-                    guest.client.clone(),
-                    guest.user_store.clone(),
-                    guest.project_store.clone(),
-                    guest_lang_registry.clone(),
-                    FakeFs::new(cx.background()),
-                    guest_cx.to_async(),
-                )
-                .await
-                .unwrap();
                 deterministic.finish_waiting();
 
                 let op_start_signal = futures::channel::mpsc::unbounded();
                 user_ids.push(guest_user_id);
                 peer_ids.push(guest.peer_id().unwrap());
                 op_start_signals.push(op_start_signal.0);
-                clients.push(guest_cx.foreground().spawn(guest.simulate_guest(
+                clients.push(guest_cx.foreground().spawn(guest.simulate(
                     guest_username.clone(),
-                    guest_project,
                     op_start_signal.1,
                     rng.clone(),
                     guest_cx,
@@ -6170,14 +6057,17 @@ async fn test_random_collaboration(
                 deterministic.advance_clock(RECEIVE_TIMEOUT);
                 deterministic.start_waiting();
                 log::info!("Waiting for guest {} to exit...", removed_guest_id);
-                let (guest, guest_project, mut guest_cx, guest_err) = guest.await;
+                let (guest, mut guest_cx, guest_err) = guest.await;
                 deterministic.finish_waiting();
                 server.allow_connections();
 
                 if let Some(guest_err) = guest_err {
                     log::error!("{} error - {:?}", guest.username, guest_err);
                 }
-                guest_project.read_with(&guest_cx, |project, _| assert!(project.is_read_only()));
+
+                for project in &guest.remote_projects {
+                    project.read_with(&guest_cx, |project, _| assert!(project.is_read_only()));
+                }
                 for user_id in &user_ids {
                     let contacts = server.app_state.db.get_contacts(*user_id).await.unwrap();
                     let contacts = server
@@ -6200,7 +6090,7 @@ async fn test_random_collaboration(
                 available_guests.push(guest.username.clone());
                 guest_cx.update(|cx| {
                     cx.clear_globals();
-                    drop((guest, guest_project));
+                    drop(guest);
                 });
 
                 operations += 1;
@@ -6228,62 +6118,73 @@ async fn test_random_collaboration(
     deterministic.finish_waiting();
     deterministic.run_until_parked();
 
-    let (host_client, host_project, mut host_cx, host_err) = clients.remove(0);
-    if let Some(host_err) = host_err {
-        panic!("host error - {:?}", host_err);
-    }
-    let host_worktree_snapshots = host_project.read_with(&host_cx, |project, cx| {
-        project
-            .worktrees(cx)
-            .map(|worktree| {
-                let snapshot = worktree.read(cx).snapshot();
-                (snapshot.id(), snapshot)
-            })
-            .collect::<BTreeMap<_, _>>()
-    });
-
-    host_project.read_with(&host_cx, |project, cx| project.check_invariants(cx));
-
-    for (guest_client, guest_project, mut guest_cx, guest_err) in clients.into_iter() {
+    for (guest_client, mut guest_cx, guest_err) in &clients {
         if let Some(guest_err) = guest_err {
             panic!("{} error - {:?}", guest_client.username, guest_err);
         }
-        let worktree_snapshots = guest_project.read_with(&guest_cx, |project, cx| {
-            project
-                .worktrees(cx)
-                .map(|worktree| {
-                    let worktree = worktree.read(cx);
-                    (worktree.id(), worktree.snapshot())
-                })
-                .collect::<BTreeMap<_, _>>()
-        });
 
-        assert_eq!(
-            worktree_snapshots.keys().collect::<Vec<_>>(),
-            host_worktree_snapshots.keys().collect::<Vec<_>>(),
-            "{} has different worktrees than the host",
-            guest_client.username
-        );
-        for (id, host_snapshot) in &host_worktree_snapshots {
-            let guest_snapshot = &worktree_snapshots[id];
-            assert_eq!(
-                guest_snapshot.root_name(),
-                host_snapshot.root_name(),
-                "{} has different root name than the host for worktree {}",
-                guest_client.username,
-                id
-            );
-            assert_eq!(
-                guest_snapshot.entries(false).collect::<Vec<_>>(),
-                host_snapshot.entries(false).collect::<Vec<_>>(),
-                "{} has different snapshot than the host for worktree {}",
-                guest_client.username,
-                id
-            );
-            assert_eq!(guest_snapshot.scan_id(), host_snapshot.scan_id());
-        }
+        for guest_project in guest_client.remote_projects {
+            guest_project.read_with(&guest_cx, |guest_project, cx| {
+                let host_project = clients.iter().find_map(|(client, cx, _)| {
+                    let project = client.local_projects.iter().find(|host_project| {
+                        host_project.read_with(cx, |host_project, cx| {
+                            host_project.remote_id() == guest_project.remote_id()
+                        })
+                    })?;
+                    Some((client, project, cx))
+                });
+
+                if let Some((host_client, host_project, host_cx)) = host_project {
+                    let host_worktree_snapshots =
+                        host_project.read_with(host_cx, |host_project, cx| {
+                            host_project
+                                .worktrees(cx)
+                                .map(|worktree| {
+                                    let worktree = worktree.read(cx);
+                                    (worktree.id(), worktree.snapshot())
+                                })
+                                .collect::<BTreeMap<_, _>>()
+                        });
+                    let guest_worktree_snapshots = guest_project
+                        .worktrees(cx)
+                        .map(|worktree| {
+                            let worktree = worktree.read(cx);
+                            (worktree.id(), worktree.snapshot())
+                        })
+                        .collect::<BTreeMap<_, _>>();
+
+                    assert_eq!(
+                        guest_worktree_snapshots.keys().collect::<Vec<_>>(),
+                        host_worktree_snapshots.keys().collect::<Vec<_>>(),
+                        "{} has different worktrees than the host",
+                        guest_client.username
+                    );
+
+                    for (id, host_snapshot) in &host_worktree_snapshots {
+                        let guest_snapshot = &guest_worktree_snapshots[id];
+                        assert_eq!(
+                            guest_snapshot.root_name(),
+                            host_snapshot.root_name(),
+                            "{} has different root name than the host for worktree {}",
+                            guest_client.username,
+                            id
+                        );
+                        assert_eq!(
+                            guest_snapshot.entries(false).collect::<Vec<_>>(),
+                            host_snapshot.entries(false).collect::<Vec<_>>(),
+                            "{} has different snapshot than the host for worktree {}",
+                            guest_client.username,
+                            id
+                        );
+                        assert_eq!(guest_snapshot.scan_id(), host_snapshot.scan_id());
+                    }
+                } else {
+                    assert!(guest_project.is_read_only());
+                }
 
-        guest_project.read_with(&guest_cx, |project, cx| project.check_invariants(cx));
+                guest_project.check_invariants(cx);
+            });
+        }
 
         for guest_buffer in &guest_client.buffers {
             let buffer_id = guest_buffer.read_with(&guest_cx, |buffer, _| buffer.remote_id());
@@ -6317,17 +6218,14 @@ async fn test_random_collaboration(
                 path
             );
         }
+    }
 
+    for (guest_client, guest_cx, _) in clients {
         guest_cx.update(|cx| {
             cx.clear_globals();
-            drop((guest_project, guest_client));
+            drop(guest_client);
         });
     }
-
-    host_cx.update(|cx| {
-        cx.clear_globals();
-        drop((host_client, host_project))
-    });
 }
 
 struct TestServer {
@@ -6494,6 +6392,9 @@ impl TestServer {
         let client = TestClient {
             client,
             username: name.to_string(),
+            local_projects: Default::default(),
+            remote_projects: Default::default(),
+            next_root_dir_id: 0,
             user_store,
             project_store,
             fs,
@@ -6612,6 +6513,9 @@ impl Drop for TestServer {
 struct TestClient {
     client: Arc<Client>,
     username: String,
+    local_projects: Vec<ModelHandle<Project>>,
+    remote_projects: Vec<ModelHandle<Project>>,
+    next_root_dir_id: usize,
     pub user_store: ModelHandle<UserStore>,
     pub project_store: ModelHandle<ProjectStore>,
     language_registry: Arc<LanguageRegistry>,
@@ -7196,6 +7100,378 @@ impl TestClient {
 
         (self, project, cx, result.err())
     }
+
+    pub async fn simulate(
+        mut self,
+        username: String,
+        op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
+        rng: Arc<Mutex<StdRng>>,
+        mut cx: TestAppContext,
+    ) -> (Self, TestAppContext, Option<anyhow::Error>) {
+        async fn simulate_internal(
+            client: &mut TestClient,
+            username: &str,
+            mut op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
+            rng: Arc<Mutex<StdRng>>,
+            cx: &mut TestAppContext,
+        ) -> anyhow::Result<()> {
+            while op_start_signal.next().await.is_some() {
+                let active_call = cx.read(ActiveCall::global);
+                let room = active_call.read_with(cx, |call, _| call.room().unwrap().clone());
+                let remote_projects = room.read_with(cx, |room, _| {
+                    room.remote_participants()
+                        .values()
+                        .flat_map(|participant| participant.projects.clone())
+                        .collect::<Vec<_>>()
+                });
+                let project = if remote_projects.is_empty() || rng.lock().gen() {
+                    if client.local_projects.is_empty() || rng.lock().gen() {
+                        let paths = client.fs.paths().await;
+                        let local_project = if paths.is_empty() || rng.lock().gen() {
+                            let root_path = format!(
+                                "/{}-root-{}",
+                                username,
+                                post_inc(&mut client.next_root_dir_id)
+                            );
+                            client.fs.create_dir(Path::new(&root_path)).await.unwrap();
+                            client.build_local_project(root_path, cx).await.0
+                        } else {
+                            let root_path = paths.choose(&mut *rng.lock()).unwrap();
+                            client.build_local_project(root_path, cx).await.0
+                        };
+                        client.local_projects.push(local_project.clone());
+                        local_project
+                    } else {
+                        client
+                            .local_projects
+                            .choose(&mut *rng.lock())
+                            .unwrap()
+                            .clone()
+                    }
+                } else {
+                    if client.remote_projects.is_empty() || rng.lock().gen() {
+                        let remote_project_id =
+                            remote_projects.choose(&mut *rng.lock()).unwrap().id;
+                        let remote_project =
+                            client.build_remote_project(remote_project_id, cx).await;
+                        client.remote_projects.push(remote_project.clone());
+                        remote_project
+                    } else {
+                        client
+                            .remote_projects
+                            .choose(&mut *rng.lock())
+                            .unwrap()
+                            .clone()
+                    }
+                };
+                active_call
+                    .update(cx, |call, cx| call.share_project(project.clone(), cx))
+                    .await
+                    .unwrap();
+
+                let buffer = if client.buffers.is_empty() || rng.lock().gen() {
+                    let worktree = if let Some(worktree) = project.read_with(cx, |project, cx| {
+                        project
+                            .worktrees(cx)
+                            .filter(|worktree| {
+                                let worktree = worktree.read(cx);
+                                worktree.is_visible()
+                                    && worktree.entries(false).any(|e| e.is_file())
+                            })
+                            .choose(&mut *rng.lock())
+                    }) {
+                        worktree
+                    } else {
+                        cx.background().simulate_random_delay().await;
+                        continue;
+                    };
+
+                    let (worktree_root_name, project_path) =
+                        worktree.read_with(cx, |worktree, _| {
+                            let entry = worktree
+                                .entries(false)
+                                .filter(|e| e.is_file())
+                                .choose(&mut *rng.lock())
+                                .unwrap();
+                            (
+                                worktree.root_name().to_string(),
+                                (worktree.id(), entry.path.clone()),
+                            )
+                        });
+                    log::info!(
+                        "{}: opening path {:?} in worktree {} ({})",
+                        username,
+                        project_path.1,
+                        project_path.0,
+                        worktree_root_name,
+                    );
+                    let buffer = project
+                        .update(cx, |project, cx| {
+                            project.open_buffer(project_path.clone(), cx)
+                        })
+                        .await?;
+                    log::info!(
+                        "{}: opened path {:?} in worktree {} ({}) with buffer id {}",
+                        username,
+                        project_path.1,
+                        project_path.0,
+                        worktree_root_name,
+                        buffer.read_with(cx, |buffer, _| buffer.remote_id())
+                    );
+                    client.buffers.insert(buffer.clone());
+                    buffer
+                } else {
+                    client
+                        .buffers
+                        .iter()
+                        .choose(&mut *rng.lock())
+                        .unwrap()
+                        .clone()
+                };
+
+                let choice = rng.lock().gen_range(0..100);
+                match choice {
+                    0..=9 => {
+                        cx.update(|cx| {
+                            log::info!(
+                                "{}: dropping buffer {:?}",
+                                username,
+                                buffer.read(cx).file().unwrap().full_path(cx)
+                            );
+                            client.buffers.remove(&buffer);
+                            drop(buffer);
+                        });
+                    }
+                    10..=19 => {
+                        let completions = project.update(cx, |project, cx| {
+                            log::info!(
+                                "{}: requesting completions for buffer {} ({:?})",
+                                username,
+                                buffer.read(cx).remote_id(),
+                                buffer.read(cx).file().unwrap().full_path(cx)
+                            );
+                            let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
+                            project.completions(&buffer, offset, cx)
+                        });
+                        let completions = cx.background().spawn(async move {
+                            completions
+                                .await
+                                .map_err(|err| anyhow!("completions request failed: {:?}", err))
+                        });
+                        if rng.lock().gen_bool(0.3) {
+                            log::info!("{}: detaching completions request", username);
+                            cx.update(|cx| completions.detach_and_log_err(cx));
+                        } else {
+                            completions.await?;
+                        }
+                    }
+                    20..=29 => {
+                        let code_actions = project.update(cx, |project, cx| {
+                            log::info!(
+                                "{}: requesting code actions for buffer {} ({:?})",
+                                username,
+                                buffer.read(cx).remote_id(),
+                                buffer.read(cx).file().unwrap().full_path(cx)
+                            );
+                            let range = buffer.read(cx).random_byte_range(0, &mut *rng.lock());
+                            project.code_actions(&buffer, range, cx)
+                        });
+                        let code_actions = cx.background().spawn(async move {
+                            code_actions
+                                .await
+                                .map_err(|err| anyhow!("code actions request failed: {:?}", err))
+                        });
+                        if rng.lock().gen_bool(0.3) {
+                            log::info!("{}: detaching code actions request", username);
+                            cx.update(|cx| code_actions.detach_and_log_err(cx));
+                        } else {
+                            code_actions.await?;
+                        }
+                    }
+                    30..=39 if buffer.read_with(cx, |buffer, _| buffer.is_dirty()) => {
+                        let (requested_version, save) = buffer.update(cx, |buffer, cx| {
+                            log::info!(
+                                "{}: saving buffer {} ({:?})",
+                                username,
+                                buffer.remote_id(),
+                                buffer.file().unwrap().full_path(cx)
+                            );
+                            (buffer.version(), buffer.save(cx))
+                        });
+                        let save = cx.background().spawn(async move {
+                            let (saved_version, _, _) = save
+                                .await
+                                .map_err(|err| anyhow!("save request failed: {:?}", err))?;
+                            assert!(saved_version.observed_all(&requested_version));
+                            Ok::<_, anyhow::Error>(())
+                        });
+                        if rng.lock().gen_bool(0.3) {
+                            log::info!("{}: detaching save request", username);
+                            cx.update(|cx| save.detach_and_log_err(cx));
+                        } else {
+                            save.await?;
+                        }
+                    }
+                    40..=44 => {
+                        let prepare_rename = project.update(cx, |project, cx| {
+                            log::info!(
+                                "{}: preparing rename for buffer {} ({:?})",
+                                username,
+                                buffer.read(cx).remote_id(),
+                                buffer.read(cx).file().unwrap().full_path(cx)
+                            );
+                            let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
+                            project.prepare_rename(buffer, offset, cx)
+                        });
+                        let prepare_rename = cx.background().spawn(async move {
+                            prepare_rename
+                                .await
+                                .map_err(|err| anyhow!("prepare rename request failed: {:?}", err))
+                        });
+                        if rng.lock().gen_bool(0.3) {
+                            log::info!("{}: detaching prepare rename request", username);
+                            cx.update(|cx| prepare_rename.detach_and_log_err(cx));
+                        } else {
+                            prepare_rename.await?;
+                        }
+                    }
+                    45..=49 => {
+                        let definitions = project.update(cx, |project, cx| {
+                            log::info!(
+                                "{}: requesting definitions for buffer {} ({:?})",
+                                username,
+                                buffer.read(cx).remote_id(),
+                                buffer.read(cx).file().unwrap().full_path(cx)
+                            );
+                            let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
+                            project.definition(&buffer, offset, cx)
+                        });
+                        let definitions = cx.background().spawn(async move {
+                            definitions
+                                .await
+                                .map_err(|err| anyhow!("definitions request failed: {:?}", err))
+                        });
+                        if rng.lock().gen_bool(0.3) {
+                            log::info!("{}: detaching definitions request", username);
+                            cx.update(|cx| definitions.detach_and_log_err(cx));
+                        } else {
+                            client.buffers.extend(
+                                definitions.await?.into_iter().map(|loc| loc.target.buffer),
+                            );
+                        }
+                    }
+                    50..=54 => {
+                        let highlights = project.update(cx, |project, cx| {
+                            log::info!(
+                                "{}: requesting highlights for buffer {} ({:?})",
+                                username,
+                                buffer.read(cx).remote_id(),
+                                buffer.read(cx).file().unwrap().full_path(cx)
+                            );
+                            let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
+                            project.document_highlights(&buffer, offset, cx)
+                        });
+                        let highlights = cx.background().spawn(async move {
+                            highlights
+                                .await
+                                .map_err(|err| anyhow!("highlights request failed: {:?}", err))
+                        });
+                        if rng.lock().gen_bool(0.3) {
+                            log::info!("{}: detaching highlights request", username);
+                            cx.update(|cx| highlights.detach_and_log_err(cx));
+                        } else {
+                            highlights.await?;
+                        }
+                    }
+                    55..=59 => {
+                        let search = project.update(cx, |project, cx| {
+                            let query = rng.lock().gen_range('a'..='z');
+                            log::info!("{}: project-wide search {:?}", username, query);
+                            project.search(SearchQuery::text(query, false, false), cx)
+                        });
+                        let search = cx.background().spawn(async move {
+                            search
+                                .await
+                                .map_err(|err| anyhow!("search request failed: {:?}", err))
+                        });
+                        if rng.lock().gen_bool(0.3) {
+                            log::info!("{}: detaching search request", username);
+                            cx.update(|cx| search.detach_and_log_err(cx));
+                        } else {
+                            client.buffers.extend(search.await?.into_keys());
+                        }
+                    }
+                    60..=69 => {
+                        let worktree = project
+                            .read_with(cx, |project, cx| {
+                                project
+                                    .worktrees(cx)
+                                    .filter(|worktree| {
+                                        let worktree = worktree.read(cx);
+                                        worktree.is_visible()
+                                            && worktree.entries(false).any(|e| e.is_file())
+                                            && worktree.root_entry().map_or(false, |e| e.is_dir())
+                                    })
+                                    .choose(&mut *rng.lock())
+                            })
+                            .unwrap();
+                        let (worktree_id, worktree_root_name) = worktree
+                            .read_with(cx, |worktree, _| {
+                                (worktree.id(), worktree.root_name().to_string())
+                            });
+
+                        let mut new_name = String::new();
+                        for _ in 0..10 {
+                            let letter = rng.lock().gen_range('a'..='z');
+                            new_name.push(letter);
+                        }
+
+                        let is_dir = rng.lock().gen::<bool>();
+                        let mut new_path = PathBuf::new();
+                        new_path.push(new_name);
+                        if !is_dir {
+                            new_path.set_extension("rs");
+                        }
+                        log::info!(
+                            "{}: creating {:?} in worktree {} ({})",
+                            username,
+                            new_path,
+                            worktree_id,
+                            worktree_root_name,
+                        );
+                        project
+                            .update(cx, |project, cx| {
+                                project.create_entry((worktree_id, new_path), is_dir, cx)
+                            })
+                            .unwrap()
+                            .await?;
+                    }
+                    _ => {
+                        buffer.update(cx, |buffer, cx| {
+                            log::info!(
+                                "{}: updating buffer {} ({:?})",
+                                username,
+                                buffer.remote_id(),
+                                buffer.file().unwrap().full_path(cx)
+                            );
+                            if rng.lock().gen_bool(0.7) {
+                                buffer.randomly_edit(&mut *rng.lock(), 5, cx);
+                            } else {
+                                buffer.randomly_undo_redo(&mut *rng.lock(), cx);
+                            }
+                        });
+                    }
+                }
+                cx.background().simulate_random_delay().await;
+            }
+            Ok(())
+        }
+
+        let result = simulate_internal(&mut self, &username, op_start_signal, rng, &mut cx).await;
+        log::info!("{}: done", username);
+
+        (self, cx, result.err())
+    }
 }
 
 impl Drop for TestClient {

crates/fs/src/fs.rs 🔗

@@ -631,6 +631,21 @@ impl FakeFs {
         }
     }
 
+    pub async fn paths(&self) -> Vec<PathBuf> {
+        let mut result = Vec::new();
+        let mut queue = collections::VecDeque::new();
+        queue.push_back((PathBuf::from("/"), self.state.lock().await.root.clone()));
+        while let Some((path, entry)) = queue.pop_front() {
+            if let FakeFsEntry::Dir { entries, .. } = &*entry.lock().await {
+                for (name, entry) in entries {
+                    queue.push_back((path.join(name), entry.clone()));
+                }
+            }
+            result.push(path);
+        }
+        result
+    }
+
     pub async fn files(&self) -> Vec<PathBuf> {
         let mut result = Vec::new();
         let mut queue = collections::VecDeque::new();