Restructure randomized test to be a bit clearer and test more stuff

Antonio Scandurra created

Change summary

crates/collab/src/tests/randomized_integration_tests.rs | 257 ++++++++--
crates/fs/src/fs.rs                                     |  15 
2 files changed, 209 insertions(+), 63 deletions(-)

Detailed changes

crates/collab/src/tests/randomized_integration_tests.rs 🔗

@@ -3,17 +3,17 @@ use crate::{
     rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
     tests::{TestClient, TestServer},
 };
-use anyhow::anyhow;
+use anyhow::{anyhow, Result};
 use call::ActiveCall;
 use client::RECEIVE_TIMEOUT;
 use collections::BTreeMap;
 use fs::{FakeFs, Fs as _};
 use futures::StreamExt as _;
-use gpui::{executor::Deterministic, TestAppContext};
+use gpui::{executor::Deterministic, ModelHandle, TestAppContext};
 use language::{range_to_lsp, FakeLspAdapter, Language, LanguageConfig, PointUtf16};
 use lsp::FakeLanguageServer;
 use parking_lot::Mutex;
-use project::search::SearchQuery;
+use project::{search::SearchQuery, Project};
 use rand::prelude::*;
 use std::{env, path::PathBuf, sync::Arc};
 
@@ -77,6 +77,9 @@ async fn test_random_collaboration(
     let mut user_ids = Vec::new();
     let mut op_start_signals = Vec::new();
     let mut next_entity_id = 100000;
+    let allow_server_restarts = rng.lock().gen_bool(0.7);
+    let allow_client_reconnection = rng.lock().gen_bool(0.7);
+    let allow_client_disconnection = rng.lock().gen_bool(0.1);
 
     let mut operations = 0;
     while operations < max_operations {
@@ -105,6 +108,7 @@ async fn test_random_collaboration(
                 clients.push(client_cx.foreground().spawn(simulate_client(
                     client,
                     op_start_signal.1,
+                    allow_client_disconnection,
                     rng.clone(),
                     client_cx,
                 )));
@@ -113,7 +117,7 @@ async fn test_random_collaboration(
                 operations += 1;
             }
 
-            20..=24 if clients.len() > 1 => {
+            20..=24 if clients.len() > 1 && allow_client_disconnection => {
                 let client_ix = rng.lock().gen_range(1..clients.len());
                 log::info!(
                     "Simulating full disconnection of user {}",
@@ -172,7 +176,7 @@ async fn test_random_collaboration(
                 operations += 1;
             }
 
-            25..=29 if clients.len() > 1 => {
+            25..=29 if clients.len() > 1 && allow_client_reconnection => {
                 let client_ix = rng.lock().gen_range(1..clients.len());
                 let user_id = user_ids[client_ix];
                 log::info!("Simulating temporary disconnection of user {}", user_id);
@@ -188,7 +192,7 @@ async fn test_random_collaboration(
                 operations += 1;
             }
 
-            30..=34 => {
+            30..=34 if allow_server_restarts => {
                 log::info!("Simulating server restart");
                 server.reset().await;
                 deterministic.advance_clock(RECEIVE_TIMEOUT);
@@ -384,6 +388,7 @@ async fn test_random_collaboration(
 async fn simulate_client(
     mut client: TestClient,
     mut op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
+    can_hang_up: bool,
     rng: Arc<Mutex<StdRng>>,
     mut cx: TestAppContext,
 ) -> (TestClient, TestAppContext) {
@@ -500,7 +505,9 @@ async fn simulate_client(
     client.language_registry.add(Arc::new(language));
 
     while op_start_signal.next().await.is_some() {
-        if let Err(error) = randomly_mutate_client(&mut client, rng.clone(), &mut cx).await {
+        if let Err(error) =
+            randomly_mutate_client(&mut client, can_hang_up, rng.clone(), &mut cx).await
+        {
             log::error!("{} error: {:?}", client.username, error);
         }
 
@@ -513,12 +520,35 @@ async fn simulate_client(
 
 async fn randomly_mutate_client(
     client: &mut TestClient,
+    can_hang_up: bool,
     rng: Arc<Mutex<StdRng>>,
     cx: &mut TestAppContext,
-) -> anyhow::Result<()> {
+) -> Result<()> {
+    let choice = rng.lock().gen_range(0..100);
+    match choice {
+        0..=19 => randomly_mutate_active_call(client, can_hang_up, &rng, cx).await?,
+        20..=49 => randomly_mutate_projects(client, &rng, cx).await?,
+        50..=59 if !client.local_projects.is_empty() || !client.remote_projects.is_empty() => {
+            randomly_mutate_worktrees(client, &rng, cx).await?;
+        }
+        60..=84 if !client.local_projects.is_empty() || !client.remote_projects.is_empty() => {
+            randomly_query_and_mutate_buffers(client, &rng, cx).await?;
+        }
+        _ => randomly_mutate_fs(client, &rng).await,
+    }
+
+    Ok(())
+}
+
+async fn randomly_mutate_active_call(
+    client: &mut TestClient,
+    can_hang_up: bool,
+    rng: &Mutex<StdRng>,
+    cx: &mut TestAppContext,
+) -> Result<()> {
     let active_call = cx.read(ActiveCall::global);
     if active_call.read_with(cx, |call, _| call.incoming().borrow().is_some()) {
-        if rng.lock().gen() {
+        if rng.lock().gen_bool(0.7) {
             log::info!("{}: accepting incoming call", client.username);
             active_call
                 .update(cx, |call, cx| call.accept_incoming(cx))
@@ -550,7 +580,9 @@ async fn randomly_mutate_client(
                     .update(cx, |call, cx| call.invite(contact.user.id, None, cx))
                     .await?;
             }
-            30..=39 if active_call.read_with(cx, |call, _| call.room().is_some()) => {
+            30..=39
+                if can_hang_up && active_call.read_with(cx, |call, _| call.room().is_some()) =>
+            {
                 log::info!("{}: hanging up", client.username);
                 active_call.update(cx, |call, cx| call.hang_up(cx))?;
             }
@@ -558,6 +590,39 @@ async fn randomly_mutate_client(
         }
     }
 
+    Ok(())
+}
+
+async fn randomly_mutate_fs(client: &mut TestClient, rng: &Mutex<StdRng>) {
+    let is_dir = rng.lock().gen::<bool>();
+    let mut new_path = client
+        .fs
+        .directories()
+        .await
+        .choose(&mut *rng.lock())
+        .unwrap()
+        .clone();
+    new_path.push(gen_file_name(rng));
+    if is_dir {
+        log::info!("{}: creating local dir at {:?}", client.username, new_path);
+        client.fs.create_dir(&new_path).await.unwrap();
+    } else {
+        new_path.set_extension("rs");
+        log::info!("{}: creating local file at {:?}", client.username, new_path);
+        client
+            .fs
+            .create_file(&new_path, Default::default())
+            .await
+            .unwrap();
+    }
+}
+
+async fn randomly_mutate_projects(
+    client: &mut TestClient,
+    rng: &Mutex<StdRng>,
+    cx: &mut TestAppContext,
+) -> Result<()> {
+    let active_call = cx.read(ActiveCall::global);
     let remote_projects =
         if let Some(room) = active_call.read_with(cx, |call, _| call.room().cloned()) {
             room.read_with(cx, |room, _| {
@@ -572,8 +637,8 @@ async fn randomly_mutate_client(
 
     let project = if remote_projects.is_empty() || rng.lock().gen() {
         if client.local_projects.is_empty() || rng.lock().gen() {
-            let dir_paths = client.fs.directories().await;
-            let local_project = if dir_paths.is_empty() || rng.lock().gen() {
+            let paths = client.fs.paths().await;
+            let local_project = if paths.is_empty() || rng.lock().gen() {
                 let root_path = client.create_new_root_dir();
                 client.fs.create_dir(&root_path).await.unwrap();
                 client
@@ -588,7 +653,7 @@ async fn randomly_mutate_client(
                 );
                 client.build_local_project(root_path, cx).await.0
             } else {
-                let root_path = dir_paths.choose(&mut *rng.lock()).unwrap();
+                let root_path = paths.choose(&mut *rng.lock()).unwrap();
                 log::info!(
                     "{}: opening local project at {:?}",
                     client.username,
@@ -647,7 +712,9 @@ async fn randomly_mutate_client(
         }
     };
 
-    if active_call.read_with(cx, |call, _| call.room().is_some()) {
+    if active_call.read_with(cx, |call, _| call.room().is_some())
+        && project.read_with(cx, |project, _| project.is_local())
+    {
         if let Err(error) = active_call
             .update(cx, |call, cx| call.share_project(project.clone(), cx))
             .await
@@ -656,9 +723,99 @@ async fn randomly_mutate_client(
         }
     }
 
+    let choice = rng.lock().gen_range(0..100);
+    match choice {
+        0..=19 if project.read_with(cx, |project, _| project.is_local()) => {
+            let paths = client.fs.paths().await;
+            let path = paths.choose(&mut *rng.lock()).unwrap();
+            log::info!(
+                "{}: find or create local worktree for path {:?}",
+                client.username,
+                path
+            );
+            project
+                .update(cx, |project, cx| {
+                    project.find_or_create_local_worktree(&path, true, cx)
+                })
+                .await
+                .unwrap();
+        }
+        20..=24 if project.read_with(cx, |project, _| project.is_remote()) => {
+            log::info!(
+                "{}: dropping remote project {}",
+                client.username,
+                project.read_with(cx, |project, _| project.remote_id().unwrap())
+            );
+
+            cx.update(|_| {
+                client
+                    .remote_projects
+                    .retain(|remote_project| *remote_project != project);
+                client.buffers.remove(&project);
+                drop(project);
+            });
+        }
+        _ => {}
+    }
+
+    Ok(())
+}
+
+async fn randomly_mutate_worktrees(
+    client: &mut TestClient,
+    rng: &Mutex<StdRng>,
+    cx: &mut TestAppContext,
+) -> Result<()> {
+    let project = choose_random_project(client, rng).unwrap();
+    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())
+                    && worktree.root_entry().map_or(false, |e| e.is_dir())
+            })
+            .choose(&mut *rng.lock())
+    }) else {
+        return Ok(())
+    };
+
+    let (worktree_id, worktree_root_name) = worktree.read_with(cx, |worktree, _| {
+        (worktree.id(), worktree.root_name().to_string())
+    });
+
+    let is_dir = rng.lock().gen::<bool>();
+    let mut new_path = PathBuf::new();
+    new_path.push(gen_file_name(rng));
+    if !is_dir {
+        new_path.set_extension("rs");
+    }
+    log::info!(
+        "{}: creating {:?} in worktree {} ({})",
+        client.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?;
+    Ok(())
+}
+
+async fn randomly_query_and_mutate_buffers(
+    client: &mut TestClient,
+    rng: &Mutex<StdRng>,
+    cx: &mut TestAppContext,
+) -> Result<()> {
+    let project = choose_random_project(client, rng).unwrap();
     let buffers = client.buffers.entry(project.clone()).or_default();
     let buffer = if buffers.is_empty() || rng.lock().gen() {
-        let worktree = if let Some(worktree) = project.read_with(cx, |project, cx| {
+        let Some(worktree) = project.read_with(cx, |project, cx| {
             project
                 .worktrees(cx)
                 .filter(|worktree| {
@@ -666,10 +823,7 @@ async fn randomly_mutate_client(
                     worktree.is_visible() && worktree.entries(false).any(|e| e.is_file())
                 })
                 .choose(&mut *rng.lock())
-        }) {
-            worktree
-        } else {
-            cx.background().simulate_random_delay().await;
+        }) else {
             return Ok(());
         };
 
@@ -880,50 +1034,6 @@ async fn randomly_mutate_client(
                 buffers.extend(search.await?.into_keys());
             }
         }
-        60..=79 => {
-            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 {} ({})",
-                client.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!(
@@ -943,3 +1053,24 @@ async fn randomly_mutate_client(
 
     Ok(())
 }
+
+fn choose_random_project(
+    client: &mut TestClient,
+    rng: &Mutex<StdRng>,
+) -> Option<ModelHandle<Project>> {
+    client
+        .local_projects
+        .iter()
+        .chain(&client.remote_projects)
+        .choose(&mut *rng.lock())
+        .cloned()
+}
+
+fn gen_file_name(rng: &Mutex<StdRng>) -> String {
+    let mut name = String::new();
+    for _ in 0..10 {
+        let letter = rng.lock().gen_range('a'..='z');
+        name.push(letter);
+    }
+    name
+}

crates/fs/src/fs.rs 🔗

@@ -635,6 +635,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 directories(&self) -> Vec<PathBuf> {
         let mut result = Vec::new();
         let mut queue = collections::VecDeque::new();