From 4c4ebbfa19a1811be622afdf6e6c2552ddeef357 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 31 Oct 2022 16:35:31 +0100 Subject: [PATCH] Start removing distinction between host and guest in random collab test --- crates/collab/src/integration_tests.rs | 630 ++++++++++++++++++------- crates/fs/src/fs.rs | 15 + 2 files changed, 468 insertions(+), 177 deletions(-) diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 41c9052ebeb454bdba83c6f73e446c4a3b3cea17..74c07cac870c64a8c9d7cb0aba09d330cebd11f4 100644 --- a/crates/collab/src/integration_tests.rs +++ b/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::>() - }); - - 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::>() - }); - assert_eq!( - worktree_snapshots.keys().collect::>(), - host_worktree_snapshots.keys().collect::>(), - "{} 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::>(), - host_snapshot.entries(false).collect::>(), - "{} 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::>() + }); + let guest_worktree_snapshots = guest_project + .worktrees(cx) + .map(|worktree| { + let worktree = worktree.read(cx); + (worktree.id(), worktree.snapshot()) + }) + .collect::>(); + + assert_eq!( + guest_worktree_snapshots.keys().collect::>(), + host_worktree_snapshots.keys().collect::>(), + "{} 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::>(), + host_snapshot.entries(false).collect::>(), + "{} 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, username: String, + local_projects: Vec>, + remote_projects: Vec>, + next_root_dir_id: usize, pub user_store: ModelHandle, pub project_store: ModelHandle, language_registry: Arc, @@ -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>, + mut cx: TestAppContext, + ) -> (Self, TestAppContext, Option) { + async fn simulate_internal( + client: &mut TestClient, + username: &str, + mut op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>, + rng: Arc>, + 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::>() + }); + 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::(); + 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 { diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 7bd7346b4b30ef2e1a99707e194e575f6cbc2f47..5d9a10a6413316fd8f0151b3369089ed63140fab 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -631,6 +631,21 @@ impl FakeFs { } } + pub async fn paths(&self) -> Vec { + 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 { let mut result = Vec::new(); let mut queue = collections::VecDeque::new();