@@ -13,7 +13,7 @@ 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, Project};
+use project::{search::SearchQuery, Project, ProjectPath};
use rand::prelude::*;
use std::{
env,
@@ -22,6 +22,7 @@ use std::{
rc::Rc,
sync::Arc,
};
+use util::ResultExt;
#[gpui::test(iterations = 100)]
async fn test_random_collaboration(
@@ -84,10 +85,12 @@ async fn test_random_collaboration(
}
let plan = Arc::new(Mutex::new(TestPlan {
- users,
allow_server_restarts: rng.gen_bool(0.7),
allow_client_reconnection: rng.gen_bool(0.7),
allow_client_disconnection: rng.gen_bool(0.1),
+ operation_ix: 0,
+ max_operations,
+ users,
rng,
}));
@@ -96,9 +99,8 @@ async fn test_random_collaboration(
let mut operation_channels = Vec::new();
let mut next_entity_id = 100000;
- let mut i = 0;
- while i < max_operations {
- let next_operation = plan.lock().next_operation(&clients).await;
+ loop {
+ let Some(next_operation) = plan.lock().next_operation(&clients).await else { break };
match next_operation {
Operation::AddConnection { user_id } => {
let username = {
@@ -132,7 +134,6 @@ async fn test_random_collaboration(
)));
log::info!("Added connection for {}", username);
- i += 1;
}
Operation::RemoveConnection { user_id } => {
@@ -196,7 +197,6 @@ async fn test_random_collaboration(
cx.clear_globals();
drop(client);
});
- i += 1;
}
Operation::BounceConnection { user_id } => {
@@ -210,7 +210,6 @@ async fn test_random_collaboration(
let peer_id = user_connection_ids[0].into();
server.disconnect_client(peer_id);
deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
- i += 1;
}
Operation::RestartServer => {
@@ -227,7 +226,6 @@ async fn test_random_collaboration(
.await
.unwrap();
assert_eq!(stale_room_ids, vec![]);
- i += 1;
}
Operation::MutateClients { user_ids, quiesce } => {
@@ -237,7 +235,6 @@ async fn test_random_collaboration(
.position(|(client, cx)| client.current_user_id(cx) == user_id)
.unwrap();
operation_channels[client_ix].unbounded_send(()).unwrap();
- i += 1;
}
if quiesce {
@@ -427,891 +424,1018 @@ async fn test_random_collaboration(
}
}
-struct TestPlan {
- rng: StdRng,
- users: Vec<UserTestPlan>,
- allow_server_restarts: bool,
- allow_client_reconnection: bool,
- allow_client_disconnection: bool,
-}
+async fn apply_client_operation(
+ client: &TestClient,
+ operation: ClientOperation,
+ cx: &mut TestAppContext,
+) -> Result<()> {
+ match operation {
+ ClientOperation::AcceptIncomingCall => {
+ log::info!("{}: accepting incoming call", client.username);
-struct UserTestPlan {
- user_id: UserId,
- username: String,
- next_root_id: usize,
- online: bool,
-}
+ let active_call = cx.read(ActiveCall::global);
+ active_call
+ .update(cx, |call, cx| call.accept_incoming(cx))
+ .await?;
+ }
-#[derive(Debug)]
-enum Operation {
- AddConnection {
- user_id: UserId,
- },
- RemoveConnection {
- user_id: UserId,
- },
- BounceConnection {
- user_id: UserId,
- },
- RestartServer,
- MutateClients {
- user_ids: Vec<UserId>,
- quiesce: bool,
- },
-}
+ ClientOperation::RejectIncomingCall => {
+ log::info!("{}: declining incoming call", client.username);
-#[derive(Debug)]
-enum ClientOperation {
- AcceptIncomingCall,
- RejectIncomingCall,
- LeaveCall,
- InviteContactToCall {
- user_id: UserId,
- },
- OpenLocalProject {
- first_root_name: String,
- },
- OpenRemoteProject {
- host_id: UserId,
- first_root_name: String,
- },
- AddWorktreeToProject {
- project_root_name: String,
- new_root_path: PathBuf,
- },
- CloseRemoteProject {
- project_root_name: String,
- },
- OpenBuffer {
- project_root_name: String,
- full_path: PathBuf,
- },
- SearchProject {
- project_root_name: String,
- query: String,
- detach: bool,
- },
- EditBuffer {
- project_root_name: String,
- full_path: PathBuf,
- edits: Vec<(Range<usize>, Arc<str>)>,
- },
- CloseBuffer {
- project_root_name: String,
- full_path: PathBuf,
- },
- SaveBuffer {
- project_root_name: String,
- full_path: PathBuf,
- detach: bool,
- },
- RequestLspDataInBuffer {
- project_root_name: String,
- full_path: PathBuf,
- offset: usize,
- kind: LspRequestKind,
- detach: bool,
- },
- Other,
-}
+ let active_call = cx.read(ActiveCall::global);
+ active_call.update(cx, |call, _| call.decline_incoming())?;
+ }
-#[derive(Debug)]
-enum LspRequestKind {
- Rename,
- Completion,
- CodeAction,
- Definition,
- Highlights,
-}
+ ClientOperation::LeaveCall => {
+ log::info!("{}: hanging up", client.username);
-impl TestPlan {
- async fn next_operation(&mut self, clients: &[(Rc<TestClient>, TestAppContext)]) -> Operation {
- let operation = loop {
- break match self.rng.gen_range(0..100) {
- 0..=29 if clients.len() < self.users.len() => {
- let user = self
- .users
- .iter()
- .filter(|u| !u.online)
- .choose(&mut self.rng)
- .unwrap();
- Operation::AddConnection {
- user_id: user.user_id,
- }
- }
- 30..=34 if clients.len() > 1 && self.allow_client_disconnection => {
- let (client, cx) = &clients[self.rng.gen_range(0..clients.len())];
- let user_id = client.current_user_id(cx);
- Operation::RemoveConnection { user_id }
- }
- 35..=39 if clients.len() > 1 && self.allow_client_reconnection => {
- let (client, cx) = &clients[self.rng.gen_range(0..clients.len())];
- let user_id = client.current_user_id(cx);
- Operation::BounceConnection { user_id }
- }
- 40..=44 if self.allow_server_restarts && clients.len() > 1 => {
- Operation::RestartServer
- }
- _ if !clients.is_empty() => {
- let user_ids = (0..self.rng.gen_range(0..10))
- .map(|_| {
- let ix = self.rng.gen_range(0..clients.len());
- let (client, cx) = &clients[ix];
- client.current_user_id(cx)
- })
- .collect();
- Operation::MutateClients {
- user_ids,
- quiesce: self.rng.gen(),
- }
- }
- _ => continue,
- };
- };
- operation
- }
+ let active_call = cx.read(ActiveCall::global);
+ active_call.update(cx, |call, cx| call.hang_up(cx))?;
+ }
- async fn next_client_operation(
- &mut self,
- client: &TestClient,
- cx: &TestAppContext,
- ) -> ClientOperation {
- let user_id = client.current_user_id(cx);
- let call = cx.read(ActiveCall::global);
- let operation = loop {
- match self.rng.gen_range(0..100) {
- // Mutate the call
- 0..=29 => {
- // Respond to an incoming call
- if call.read_with(cx, |call, _| call.incoming().borrow().is_some()) {
- break if self.rng.gen_bool(0.7) {
- ClientOperation::AcceptIncomingCall
- } else {
- ClientOperation::RejectIncomingCall
- };
- }
+ ClientOperation::InviteContactToCall { user_id } => {
+ log::info!("{}: inviting {}", client.username, user_id,);
- match self.rng.gen_range(0..100_u32) {
- // Invite a contact to the current call
- 0..=70 => {
- let available_contacts =
- client.user_store.read_with(cx, |user_store, _| {
- user_store
- .contacts()
- .iter()
- .filter(|contact| contact.online && !contact.busy)
- .cloned()
- .collect::<Vec<_>>()
- });
- if !available_contacts.is_empty() {
- let contact = available_contacts.choose(&mut self.rng).unwrap();
- break ClientOperation::InviteContactToCall {
- user_id: UserId(contact.user.id as i32),
- };
- }
- }
+ let active_call = cx.read(ActiveCall::global);
+ active_call
+ .update(cx, |call, cx| call.invite(user_id.to_proto(), None, cx))
+ .await
+ .log_err();
+ }
- // Leave the current call
- 71.. => {
- if self.allow_client_disconnection
- && call.read_with(cx, |call, _| call.room().is_some())
- {
- break ClientOperation::LeaveCall;
- }
- }
- }
- }
+ ClientOperation::OpenLocalProject { first_root_name } => {
+ log::info!(
+ "{}: opening local project at {:?}",
+ client.username,
+ first_root_name
+ );
- // Mutate projects
- 39..=59 => match self.rng.gen_range(0..100_u32) {
- // Open a new project
- 0..=70 => {
- // Open a remote project
- if let Some(room) = call.read_with(cx, |call, _| call.room().cloned()) {
- let existing_remote_project_ids = cx.read(|cx| {
- client
- .remote_projects()
- .iter()
- .map(|p| p.read(cx).remote_id().unwrap())
- .collect::<Vec<_>>()
- });
- let new_remote_projects = room.read_with(cx, |room, _| {
- room.remote_participants()
- .values()
- .flat_map(|participant| {
- participant.projects.iter().filter_map(|project| {
- if existing_remote_project_ids.contains(&project.id) {
- None
- } else {
- Some((
- UserId::from_proto(participant.user.id),
- project.worktree_root_names[0].clone(),
- ))
- }
- })
- })
- .collect::<Vec<_>>()
- });
- if !new_remote_projects.is_empty() {
- let (host_id, first_root_name) =
- new_remote_projects.choose(&mut self.rng).unwrap().clone();
- break ClientOperation::OpenRemoteProject {
- host_id,
- first_root_name,
- };
- }
- }
- // Open a local project
- else {
- let first_root_name = self.next_root_dir_name(user_id);
- break ClientOperation::OpenLocalProject { first_root_name };
- }
- }
-
- // Close a remote project
- 71..=80 => {
- if !client.remote_projects().is_empty() {
- let project = client
- .remote_projects()
- .choose(&mut self.rng)
- .unwrap()
- .clone();
- let first_root_name = root_name_for_project(&project, cx);
- break ClientOperation::CloseRemoteProject {
- project_root_name: first_root_name,
- };
- }
- }
+ let root_path = Path::new("/").join(&first_root_name);
+ client.fs.create_dir(&root_path).await.unwrap();
+ client
+ .fs
+ .create_file(&root_path.join("main.rs"), Default::default())
+ .await
+ .unwrap();
+ let project = client.build_local_project(root_path, cx).await.0;
+ ensure_project_shared(&project, client, cx).await;
+ client.local_projects_mut().push(project.clone());
+ }
- // Add a worktree to a local project
- 81.. => {
- if !client.local_projects().is_empty() {
- let project = client
- .local_projects()
- .choose(&mut self.rng)
- .unwrap()
- .clone();
- let project_root_name = root_name_for_project(&project, cx);
+ ClientOperation::AddWorktreeToProject {
+ project_root_name,
+ new_root_path,
+ } => {
+ log::info!(
+ "{}: finding/creating local worktree at {:?} to project with root path {}",
+ client.username,
+ new_root_path,
+ project_root_name
+ );
- let mut paths = client.fs.paths().await;
- paths.remove(0);
- let new_root_path = if paths.is_empty() || self.rng.gen() {
- Path::new("/").join(&self.next_root_dir_name(user_id))
- } else {
- paths.choose(&mut self.rng).unwrap().clone()
- };
+ let project = project_for_root_name(client, &project_root_name, cx)
+ .expect("invalid project in test operation");
+ ensure_project_shared(&project, client, cx).await;
+ if !client.fs.paths().await.contains(&new_root_path) {
+ client.fs.create_dir(&new_root_path).await.unwrap();
+ }
+ project
+ .update(cx, |project, cx| {
+ project.find_or_create_local_worktree(&new_root_path, true, cx)
+ })
+ .await
+ .unwrap();
+ }
- break ClientOperation::AddWorktreeToProject {
- project_root_name,
- new_root_path,
- };
- }
- }
- },
+ ClientOperation::CloseRemoteProject { project_root_name } => {
+ log::info!(
+ "{}: closing remote project with root path {}",
+ client.username,
+ project_root_name,
+ );
- // Query and mutate buffers
- 60.. => {
- let Some(project) = choose_random_project(client, &mut self.rng) else { continue };
- let project_root_name = root_name_for_project(&project, cx);
+ let ix = project_ix_for_root_name(&*client.remote_projects(), &project_root_name, cx)
+ .expect("invalid project in test operation");
+ cx.update(|_| client.remote_projects_mut().remove(ix));
+ }
- match self.rng.gen_range(0..100_u32) {
- // Manipulate an existing buffer
- 0..=70 => {
- let Some(buffer) = client
- .buffers_for_project(&project)
- .iter()
- .choose(&mut self.rng)
- .cloned() else { continue };
+ ClientOperation::OpenRemoteProject {
+ host_id,
+ first_root_name,
+ } => {
+ log::info!(
+ "{}: joining remote project of user {}, root name {}",
+ client.username,
+ host_id,
+ first_root_name,
+ );
- let full_path = buffer
- .read_with(cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
+ let active_call = cx.read(ActiveCall::global);
+ let project_id = active_call
+ .read_with(cx, |call, cx| {
+ let room = call.room().cloned()?;
+ let participant = room
+ .read(cx)
+ .remote_participants()
+ .get(&host_id.to_proto())?;
+ let project = participant
+ .projects
+ .iter()
+ .find(|project| project.worktree_root_names[0] == first_root_name)?;
+ Some(project.id)
+ })
+ .expect("invalid project in test operation");
+ let project = client.build_remote_project(project_id, cx).await;
+ client.remote_projects_mut().push(project);
+ }
- match self.rng.gen_range(0..100_u32) {
- // Close the buffer
- 0..=15 => {
- break ClientOperation::CloseBuffer {
- project_root_name,
- full_path,
- };
- }
- // Save the buffer
- 16..=29 if buffer.read_with(cx, |b, _| b.is_dirty()) => {
- let detach = self.rng.gen_bool(0.3);
- break ClientOperation::SaveBuffer {
- project_root_name,
- full_path,
- detach,
- };
- }
- // Edit the buffer
- 30..=69 => {
- let edits = buffer.read_with(cx, |buffer, _| {
- buffer.get_random_edits(&mut self.rng, 3)
- });
- break ClientOperation::EditBuffer {
- project_root_name,
- full_path,
- edits,
- };
- }
- // Make an LSP request
- _ => {
- let offset = buffer.read_with(cx, |buffer, _| {
- buffer.clip_offset(
- self.rng.gen_range(0..=buffer.len()),
- language::Bias::Left,
- )
- });
- let detach = self.rng.gen();
- break ClientOperation::RequestLspDataInBuffer {
- project_root_name,
- full_path,
- offset,
- kind: match self.rng.gen_range(0..5_u32) {
- 0 => LspRequestKind::Rename,
- 1 => LspRequestKind::Highlights,
- 2 => LspRequestKind::Definition,
- 3 => LspRequestKind::CodeAction,
- 4.. => LspRequestKind::Completion,
- },
- detach,
- };
- }
- }
- }
+ ClientOperation::CreateWorktreeEntry {
+ project_root_name,
+ is_local,
+ full_path,
+ is_dir,
+ } => {
+ log::info!(
+ "{}: creating {} at path {:?} in {} project {}",
+ client.username,
+ if is_dir { "dir" } else { "file" },
+ full_path,
+ if is_local { "local" } else { "remote" },
+ project_root_name,
+ );
- 71..=80 => {
- let query = self.rng.gen_range('a'..='z').to_string();
- let detach = self.rng.gen_bool(0.3);
- break ClientOperation::SearchProject {
- project_root_name,
- query,
- detach,
- };
- }
+ let project = project_for_root_name(client, &project_root_name, cx)
+ .expect("invalid project in test operation");
+ ensure_project_shared(&project, client, cx).await;
+ let project_path = project_path_for_full_path(&project, &full_path, cx)
+ .expect("invalid worktree path in test operation");
+ project
+ .update(cx, |p, cx| p.create_entry(project_path, is_dir, cx))
+ .unwrap()
+ .await?;
+ }
- // Open a buffer
- 81.. => {
- 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())
- })
- .choose(&mut self.rng)
- });
- let Some(worktree) = worktree else { continue };
- let full_path = worktree.read_with(cx, |worktree, _| {
- let entry = worktree
- .entries(false)
- .filter(|e| e.is_file())
- .choose(&mut self.rng)
- .unwrap();
- if entry.path.as_ref() == Path::new("") {
- Path::new(worktree.root_name()).into()
- } else {
- Path::new(worktree.root_name()).join(&entry.path)
- }
- });
- break ClientOperation::OpenBuffer {
- project_root_name,
- full_path,
- };
- }
- }
- }
+ ClientOperation::OpenBuffer {
+ project_root_name,
+ is_local,
+ full_path,
+ } => {
+ log::info!(
+ "{}: opening buffer {:?} in {} project {}",
+ client.username,
+ full_path,
+ if is_local { "local" } else { "remote" },
+ project_root_name,
+ );
- _ => break ClientOperation::Other,
- }
- };
- operation
- }
+ let project = project_for_root_name(client, &project_root_name, cx)
+ .expect("invalid project in test operation");
+ ensure_project_shared(&project, client, cx).await;
+ let project_path = project_path_for_full_path(&project, &full_path, cx)
+ .expect("invalid buffer path in test operation");
+ let buffer = project
+ .update(cx, |project, cx| project.open_buffer(project_path, cx))
+ .await?;
+ client.buffers_for_project(&project).insert(buffer);
+ }
- fn next_root_dir_name(&mut self, user_id: UserId) -> String {
- let user_ix = self
- .users
- .iter()
- .position(|user| user.user_id == user_id)
- .unwrap();
- let root_id = util::post_inc(&mut self.users[user_ix].next_root_id);
- format!("dir-{user_id}-{root_id}")
- }
-
- fn user(&mut self, user_id: UserId) -> &mut UserTestPlan {
- let ix = self
- .users
- .iter()
- .position(|user| user.user_id == user_id)
- .unwrap();
- &mut self.users[ix]
- }
-}
-
-async fn simulate_client(
- client: Rc<TestClient>,
- mut operation_rx: futures::channel::mpsc::UnboundedReceiver<()>,
- plan: Arc<Mutex<TestPlan>>,
- mut cx: TestAppContext,
-) {
- // Setup language server
- let mut language = Language::new(
- LanguageConfig {
- name: "Rust".into(),
- path_suffixes: vec!["rs".to_string()],
- ..Default::default()
- },
- None,
- );
- let _fake_language_servers = language
- .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
- name: "the-fake-language-server",
- capabilities: lsp::LanguageServer::full_capabilities(),
- initializer: Some(Box::new({
- let plan = plan.clone();
- let fs = client.fs.clone();
- move |fake_server: &mut FakeLanguageServer| {
- fake_server.handle_request::<lsp::request::Completion, _, _>(
- |_, _| async move {
- Ok(Some(lsp::CompletionResponse::Array(vec![
- lsp::CompletionItem {
- text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
- range: lsp::Range::new(
- lsp::Position::new(0, 0),
- lsp::Position::new(0, 0),
- ),
- new_text: "the-new-text".to_string(),
- })),
- ..Default::default()
- },
- ])))
- },
- );
-
- fake_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
- |_, _| async move {
- Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
- lsp::CodeAction {
- title: "the-code-action".to_string(),
- ..Default::default()
- },
- )]))
- },
- );
-
- fake_server.handle_request::<lsp::request::PrepareRenameRequest, _, _>(
- |params, _| async move {
- Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
- params.position,
- params.position,
- ))))
- },
- );
-
- fake_server.handle_request::<lsp::request::GotoDefinition, _, _>({
- let fs = fs.clone();
- let plan = plan.clone();
- move |_, _| {
- let fs = fs.clone();
- let plan = plan.clone();
- async move {
- let files = fs.files().await;
- let mut plan = plan.lock();
- let count = plan.rng.gen_range::<usize, _>(1..3);
- let files = (0..count)
- .map(|_| files.choose(&mut plan.rng).unwrap())
- .collect::<Vec<_>>();
- log::info!("LSP: Returning definitions in files {:?}", &files);
- Ok(Some(lsp::GotoDefinitionResponse::Array(
- files
- .into_iter()
- .map(|file| lsp::Location {
- uri: lsp::Url::from_file_path(file).unwrap(),
- range: Default::default(),
- })
- .collect(),
- )))
- }
- }
- });
-
- fake_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>({
- let plan = plan.clone();
- move |_, _| {
- let mut highlights = Vec::new();
- let highlight_count = plan.lock().rng.gen_range(1..=5);
- for _ in 0..highlight_count {
- let start_row = plan.lock().rng.gen_range(0..100);
- let start_column = plan.lock().rng.gen_range(0..100);
- let start = PointUtf16::new(start_row, start_column);
- let end_row = plan.lock().rng.gen_range(0..100);
- let end_column = plan.lock().rng.gen_range(0..100);
- let end = PointUtf16::new(end_row, end_column);
- let range = if start > end { end..start } else { start..end };
- highlights.push(lsp::DocumentHighlight {
- range: range_to_lsp(range.clone()),
- kind: Some(lsp::DocumentHighlightKind::READ),
- });
- }
- highlights.sort_unstable_by_key(|highlight| {
- (highlight.range.start, highlight.range.end)
- });
- async move { Ok(Some(highlights)) }
- }
- });
- }
- })),
- ..Default::default()
- }))
- .await;
- client.language_registry.add(Arc::new(language));
-
- while operation_rx.next().await.is_some() {
- let operation = plan.lock().next_client_operation(&client, &cx).await;
- if let Err(error) = apply_client_operation(&client, plan.clone(), operation, &mut cx).await
- {
- log::error!("{} error: {}", client.username, error);
- }
- cx.background().simulate_random_delay().await;
- }
- log::info!("{}: done", client.username);
-}
-
-async fn apply_client_operation(
- client: &TestClient,
- plan: Arc<Mutex<TestPlan>>,
- operation: ClientOperation,
- cx: &mut TestAppContext,
-) -> Result<()> {
- match operation {
- ClientOperation::AcceptIncomingCall => {
- log::info!("{}: accepting incoming call", client.username);
- let active_call = cx.read(ActiveCall::global);
- active_call
- .update(cx, |call, cx| call.accept_incoming(cx))
- .await?;
- }
-
- ClientOperation::RejectIncomingCall => {
- log::info!("{}: declining incoming call", client.username);
- let active_call = cx.read(ActiveCall::global);
- active_call.update(cx, |call, _| call.decline_incoming())?;
- }
-
- ClientOperation::LeaveCall => {
- log::info!("{}: hanging up", client.username);
- let active_call = cx.read(ActiveCall::global);
- active_call.update(cx, |call, cx| call.hang_up(cx))?;
- }
-
- ClientOperation::InviteContactToCall { user_id } => {
- log::info!("{}: inviting {}", client.username, user_id,);
- let active_call = cx.read(ActiveCall::global);
- active_call
- .update(cx, |call, cx| call.invite(user_id.to_proto(), None, cx))
- .await?;
- }
-
- ClientOperation::OpenLocalProject { first_root_name } => {
+ ClientOperation::EditBuffer {
+ project_root_name,
+ is_local,
+ full_path,
+ edits,
+ } => {
log::info!(
- "{}: opening local project at {:?}",
+ "{}: editing buffer {:?} in {} project {} with {:?}",
client.username,
- first_root_name
+ full_path,
+ if is_local { "local" } else { "remote" },
+ project_root_name,
+ edits
);
- let root_path = Path::new("/").join(&first_root_name);
- client.fs.create_dir(&root_path).await.unwrap();
- client
- .fs
- .create_file(&root_path.join("main.rs"), Default::default())
- .await
- .unwrap();
- let project = client.build_local_project(root_path, cx).await.0;
+
+ let project = project_for_root_name(client, &project_root_name, cx)
+ .expect("invalid project in test operation");
ensure_project_shared(&project, client, cx).await;
- client.local_projects_mut().push(project.clone());
+ let buffer =
+ buffer_for_full_path(&*client.buffers_for_project(&project), &full_path, cx)
+ .expect("invalid buffer path in test operation");
+ buffer.update(cx, |buffer, cx| {
+ buffer.edit(edits, None, cx);
+ });
}
- ClientOperation::AddWorktreeToProject {
+ ClientOperation::CloseBuffer {
project_root_name,
- new_root_path,
+ is_local,
+ full_path,
} => {
log::info!(
- "{}: finding/creating local worktree at {:?} to project with root path {}",
+ "{}: dropping buffer {:?} in {} project {}",
client.username,
- new_root_path,
+ full_path,
+ if is_local { "local" } else { "remote" },
project_root_name
);
+
let project = project_for_root_name(client, &project_root_name, cx)
.expect("invalid project in test operation");
ensure_project_shared(&project, client, cx).await;
- if !client.fs.paths().await.contains(&new_root_path) {
- client.fs.create_dir(&new_root_path).await.unwrap();
- }
- project
- .update(cx, |project, cx| {
- project.find_or_create_local_worktree(&new_root_path, true, cx)
- })
- .await
- .unwrap();
+ let buffer =
+ buffer_for_full_path(&*client.buffers_for_project(&project), &full_path, cx)
+ .expect("invalid buffer path in test operation");
+ cx.update(|_| {
+ client.buffers_for_project(&project).remove(&buffer);
+ drop(buffer);
+ });
}
- ClientOperation::CloseRemoteProject { project_root_name } => {
+ ClientOperation::SaveBuffer {
+ project_root_name,
+ is_local,
+ full_path,
+ detach,
+ } => {
log::info!(
- "{}: closing remote project with root path {}",
+ "{}: saving buffer {:?} in {} project {}{}",
client.username,
+ full_path,
+ if is_local { "local" } else { "remote" },
project_root_name,
+ if detach { ", detaching" } else { ", awaiting" }
);
- let ix = project_ix_for_root_name(&*client.remote_projects(), &project_root_name, cx)
- .expect("invalid project in test operation");
- cx.update(|_| client.remote_projects_mut().remove(ix));
- }
- ClientOperation::OpenRemoteProject {
- host_id,
- first_root_name,
- } => {
- log::info!(
- "{}: joining remote project of user {}, root name {}",
- client.username,
- host_id,
- first_root_name,
- );
- let active_call = cx.read(ActiveCall::global);
- let project_id = active_call
- .read_with(cx, |call, cx| {
- let room = call.room().cloned()?;
- let participant = room
- .read(cx)
- .remote_participants()
- .get(&host_id.to_proto())?;
- let project = participant
- .projects
- .iter()
- .find(|project| project.worktree_root_names[0] == first_root_name)?;
- Some(project.id)
- })
- .expect("invalid project in test operation");
- let project = client.build_remote_project(project_id, cx).await;
- client.remote_projects_mut().push(project);
- }
-
- ClientOperation::OpenBuffer {
- project_root_name,
- full_path,
- } => {
- log::info!(
- "{}: opening buffer {:?} in project {}",
- client.username,
- full_path,
- project_root_name,
- );
- let project = project_for_root_name(client, &project_root_name, cx)
- .expect("invalid project in test operation");
- // ensure_project_shared(&project, client, cx).await;
- let mut components = full_path.components();
- let root_name = components.next().unwrap().as_os_str().to_str().unwrap();
- let path = components.as_path();
- let worktree_id = project
- .read_with(cx, |project, cx| {
- project.worktrees(cx).find_map(|worktree| {
- let worktree = worktree.read(cx);
- if worktree.root_name() == root_name {
- Some(worktree.id())
- } else {
- None
- }
- })
- })
- .expect("invalid buffer path in test operation");
- let buffer = project
- .update(cx, |project, cx| {
- project.open_buffer((worktree_id, &path), cx)
- })
- .await?;
- client.buffers_for_project(&project).insert(buffer);
- }
-
- ClientOperation::EditBuffer {
- project_root_name,
- full_path,
- edits,
- } => {
- log::info!(
- "{}: editing buffer {:?} in project {} with {:?}",
- client.username,
- full_path,
- project_root_name,
- edits
- );
let project = project_for_root_name(client, &project_root_name, cx)
.expect("invalid project in test operation");
+ ensure_project_shared(&project, client, cx).await;
let buffer =
buffer_for_full_path(&*client.buffers_for_project(&project), &full_path, cx)
.expect("invalid buffer path in test operation");
- buffer.update(cx, |buffer, cx| {
- buffer.edit(edits, None, cx);
+ let (requested_version, save) =
+ buffer.update(cx, |buffer, 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));
+ anyhow::Ok(())
});
+ if detach {
+ log::info!("{}: detaching save request", client.username);
+ cx.update(|cx| save.detach_and_log_err(cx));
+ } else {
+ save.await?;
+ }
}
- ClientOperation::CloseBuffer {
+ ClientOperation::RequestLspDataInBuffer {
project_root_name,
+ is_local,
full_path,
+ offset,
+ kind,
+ detach,
} => {
log::info!(
- "{}: dropping buffer {:?} in project {}",
+ "{}: request LSP {:?} for buffer {:?} in {} project {}{}",
client.username,
+ kind,
full_path,
- project_root_name
+ if is_local { "local" } else { "remote" },
+ project_root_name,
+ if detach { ", detaching" } else { ", awaiting" }
);
+
let project = project_for_root_name(client, &project_root_name, cx)
.expect("invalid project in test operation");
let buffer =
buffer_for_full_path(&*client.buffers_for_project(&project), &full_path, cx)
.expect("invalid buffer path in test operation");
- cx.update(|_| {
- client.buffers_for_project(&project).remove(&buffer);
- drop(buffer);
- });
+ let request = match kind {
+ LspRequestKind::Rename => cx.spawn(|mut cx| async move {
+ project
+ .update(&mut cx, |p, cx| p.prepare_rename(buffer, offset, cx))
+ .await?;
+ anyhow::Ok(())
+ }),
+ LspRequestKind::Completion => cx.spawn(|mut cx| async move {
+ project
+ .update(&mut cx, |p, cx| p.completions(&buffer, offset, cx))
+ .await?;
+ Ok(())
+ }),
+ LspRequestKind::CodeAction => cx.spawn(|mut cx| async move {
+ project
+ .update(&mut cx, |p, cx| p.code_actions(&buffer, offset..offset, cx))
+ .await?;
+ Ok(())
+ }),
+ LspRequestKind::Definition => cx.spawn(|mut cx| async move {
+ project
+ .update(&mut cx, |p, cx| p.definition(&buffer, offset, cx))
+ .await?;
+ Ok(())
+ }),
+ LspRequestKind::Highlights => cx.spawn(|mut cx| async move {
+ project
+ .update(&mut cx, |p, cx| p.document_highlights(&buffer, offset, cx))
+ .await?;
+ Ok(())
+ }),
+ };
+ if detach {
+ request.detach();
+ } else {
+ request.await?;
+ }
}
- ClientOperation::SaveBuffer {
+ ClientOperation::SearchProject {
project_root_name,
- full_path,
+ query,
detach,
} => {
log::info!(
- "{}: saving buffer {:?} in project {}{}",
+ "{}: search project {} for {:?}{}",
client.username,
- full_path,
project_root_name,
+ query,
if detach { ", detaching" } else { ", awaiting" }
);
let project = project_for_root_name(client, &project_root_name, cx)
.expect("invalid project in test operation");
- let buffer =
- buffer_for_full_path(&*client.buffers_for_project(&project), &full_path, cx)
- .expect("invalid buffer path in test operation");
- let (requested_version, save) =
- buffer.update(cx, |buffer, cx| (buffer.version(), buffer.save(cx)));
- let save = cx.background().spawn(async move {
- let (saved_version, _, _) = save
+ let search = project.update(cx, |project, cx| {
+ project.search(SearchQuery::text(query, false, false), cx)
+ });
+ let search = cx.background().spawn(async move {
+ search
.await
- .map_err(|err| anyhow!("save request failed: {:?}", err))?;
- assert!(saved_version.observed_all(&requested_version));
- anyhow::Ok(())
+ .map_err(|err| anyhow!("search request failed: {:?}", err))
});
if detach {
log::info!("{}: detaching save request", client.username);
- cx.update(|cx| save.detach_and_log_err(cx));
+ cx.update(|cx| search.detach_and_log_err(cx));
} else {
- save.await?;
+ search.await?;
+ }
+ }
+
+ ClientOperation::CreateFsEntry { path, is_dir } => {
+ log::info!(
+ "{}: creating {} at {:?}",
+ client.username,
+ if is_dir { "dir" } else { "file" },
+ path
+ );
+ if is_dir {
+ client.fs.create_dir(&path).await.unwrap();
+ } else {
+ client
+ .fs
+ .create_file(&path, Default::default())
+ .await
+ .unwrap();
+ }
+ }
+ }
+ Ok(())
+}
+
+struct TestPlan {
+ rng: StdRng,
+ max_operations: usize,
+ operation_ix: usize,
+ users: Vec<UserTestPlan>,
+ allow_server_restarts: bool,
+ allow_client_reconnection: bool,
+ allow_client_disconnection: bool,
+}
+
+struct UserTestPlan {
+ user_id: UserId,
+ username: String,
+ next_root_id: usize,
+ online: bool,
+}
+
+#[derive(Debug)]
+enum Operation {
+ AddConnection {
+ user_id: UserId,
+ },
+ RemoveConnection {
+ user_id: UserId,
+ },
+ BounceConnection {
+ user_id: UserId,
+ },
+ RestartServer,
+ MutateClients {
+ user_ids: Vec<UserId>,
+ quiesce: bool,
+ },
+}
+
+#[derive(Debug)]
+enum ClientOperation {
+ AcceptIncomingCall,
+ RejectIncomingCall,
+ LeaveCall,
+ InviteContactToCall {
+ user_id: UserId,
+ },
+ OpenLocalProject {
+ first_root_name: String,
+ },
+ OpenRemoteProject {
+ host_id: UserId,
+ first_root_name: String,
+ },
+ AddWorktreeToProject {
+ project_root_name: String,
+ new_root_path: PathBuf,
+ },
+ CloseRemoteProject {
+ project_root_name: String,
+ },
+ OpenBuffer {
+ project_root_name: String,
+ is_local: bool,
+ full_path: PathBuf,
+ },
+ SearchProject {
+ project_root_name: String,
+ query: String,
+ detach: bool,
+ },
+ EditBuffer {
+ project_root_name: String,
+ is_local: bool,
+ full_path: PathBuf,
+ edits: Vec<(Range<usize>, Arc<str>)>,
+ },
+ CloseBuffer {
+ project_root_name: String,
+ is_local: bool,
+ full_path: PathBuf,
+ },
+ SaveBuffer {
+ project_root_name: String,
+ is_local: bool,
+ full_path: PathBuf,
+ detach: bool,
+ },
+ RequestLspDataInBuffer {
+ project_root_name: String,
+ is_local: bool,
+ full_path: PathBuf,
+ offset: usize,
+ kind: LspRequestKind,
+ detach: bool,
+ },
+ CreateWorktreeEntry {
+ project_root_name: String,
+ is_local: bool,
+ full_path: PathBuf,
+ is_dir: bool,
+ },
+ CreateFsEntry {
+ path: PathBuf,
+ is_dir: bool,
+ },
+}
+
+#[derive(Debug)]
+enum LspRequestKind {
+ Rename,
+ Completion,
+ CodeAction,
+ Definition,
+ Highlights,
+}
+
+impl TestPlan {
+ async fn next_operation(
+ &mut self,
+ clients: &[(Rc<TestClient>, TestAppContext)],
+ ) -> Option<Operation> {
+ if self.operation_ix == self.max_operations {
+ return None;
+ }
+
+ let operation = loop {
+ break match self.rng.gen_range(0..100) {
+ 0..=29 if clients.len() < self.users.len() => {
+ let user = self
+ .users
+ .iter()
+ .filter(|u| !u.online)
+ .choose(&mut self.rng)
+ .unwrap();
+ self.operation_ix += 1;
+ Operation::AddConnection {
+ user_id: user.user_id,
+ }
+ }
+ 30..=34 if clients.len() > 1 && self.allow_client_disconnection => {
+ let (client, cx) = &clients[self.rng.gen_range(0..clients.len())];
+ let user_id = client.current_user_id(cx);
+ self.operation_ix += 1;
+ Operation::RemoveConnection { user_id }
+ }
+ 35..=39 if clients.len() > 1 && self.allow_client_reconnection => {
+ let (client, cx) = &clients[self.rng.gen_range(0..clients.len())];
+ let user_id = client.current_user_id(cx);
+ self.operation_ix += 1;
+ Operation::BounceConnection { user_id }
+ }
+ 40..=44 if self.allow_server_restarts && clients.len() > 1 => {
+ self.operation_ix += 1;
+ Operation::RestartServer
+ }
+ _ if !clients.is_empty() => {
+ let count = self
+ .rng
+ .gen_range(1..10)
+ .min(self.max_operations - self.operation_ix);
+ let user_ids = (0..count)
+ .map(|_| {
+ let ix = self.rng.gen_range(0..clients.len());
+ let (client, cx) = &clients[ix];
+ client.current_user_id(cx)
+ })
+ .collect();
+ Operation::MutateClients {
+ user_ids,
+ quiesce: self.rng.gen(),
+ }
+ }
+ _ => continue,
+ };
+ };
+ Some(operation)
+ }
+
+ async fn next_client_operation(
+ &mut self,
+ client: &TestClient,
+ cx: &TestAppContext,
+ ) -> Option<ClientOperation> {
+ if self.operation_ix == self.max_operations {
+ return None;
+ }
+
+ let user_id = client.current_user_id(cx);
+ let call = cx.read(ActiveCall::global);
+ let operation = loop {
+ match self.rng.gen_range(0..100_u32) {
+ // Mutate the call
+ 0..=29 => {
+ // Respond to an incoming call
+ if call.read_with(cx, |call, _| call.incoming().borrow().is_some()) {
+ break if self.rng.gen_bool(0.7) {
+ ClientOperation::AcceptIncomingCall
+ } else {
+ ClientOperation::RejectIncomingCall
+ };
+ }
+
+ match self.rng.gen_range(0..100_u32) {
+ // Invite a contact to the current call
+ 0..=70 => {
+ let available_contacts =
+ client.user_store.read_with(cx, |user_store, _| {
+ user_store
+ .contacts()
+ .iter()
+ .filter(|contact| contact.online && !contact.busy)
+ .cloned()
+ .collect::<Vec<_>>()
+ });
+ if !available_contacts.is_empty() {
+ let contact = available_contacts.choose(&mut self.rng).unwrap();
+ break ClientOperation::InviteContactToCall {
+ user_id: UserId(contact.user.id as i32),
+ };
+ }
+ }
+
+ // Leave the current call
+ 71.. => {
+ if self.allow_client_disconnection
+ && call.read_with(cx, |call, _| call.room().is_some())
+ {
+ break ClientOperation::LeaveCall;
+ }
+ }
+ }
+ }
+
+ // Mutate projects
+ 30..=59 => match self.rng.gen_range(0..100_u32) {
+ // Open a new project
+ 0..=70 => {
+ // Open a remote project
+ if let Some(room) = call.read_with(cx, |call, _| call.room().cloned()) {
+ let existing_remote_project_ids = cx.read(|cx| {
+ client
+ .remote_projects()
+ .iter()
+ .map(|p| p.read(cx).remote_id().unwrap())
+ .collect::<Vec<_>>()
+ });
+ let new_remote_projects = room.read_with(cx, |room, _| {
+ room.remote_participants()
+ .values()
+ .flat_map(|participant| {
+ participant.projects.iter().filter_map(|project| {
+ if existing_remote_project_ids.contains(&project.id) {
+ None
+ } else {
+ Some((
+ UserId::from_proto(participant.user.id),
+ project.worktree_root_names[0].clone(),
+ ))
+ }
+ })
+ })
+ .collect::<Vec<_>>()
+ });
+ if !new_remote_projects.is_empty() {
+ let (host_id, first_root_name) =
+ new_remote_projects.choose(&mut self.rng).unwrap().clone();
+ break ClientOperation::OpenRemoteProject {
+ host_id,
+ first_root_name,
+ };
+ }
+ }
+ // Open a local project
+ else {
+ let first_root_name = self.next_root_dir_name(user_id);
+ break ClientOperation::OpenLocalProject { first_root_name };
+ }
+ }
+
+ // Close a remote project
+ 71..=80 => {
+ if !client.remote_projects().is_empty() {
+ let project = client
+ .remote_projects()
+ .choose(&mut self.rng)
+ .unwrap()
+ .clone();
+ let first_root_name = root_name_for_project(&project, cx);
+ break ClientOperation::CloseRemoteProject {
+ project_root_name: first_root_name,
+ };
+ }
+ }
+
+ // Mutate project worktrees
+ 81.. => match self.rng.gen_range(0..100_u32) {
+ // Add a worktree to a local project
+ 0..=50 => {
+ let Some(project) = client
+ .local_projects()
+ .choose(&mut self.rng)
+ .cloned() else { continue };
+ let project_root_name = root_name_for_project(&project, cx);
+ let mut paths = client.fs.paths().await;
+ paths.remove(0);
+ let new_root_path = if paths.is_empty() || self.rng.gen() {
+ Path::new("/").join(&self.next_root_dir_name(user_id))
+ } else {
+ paths.choose(&mut self.rng).unwrap().clone()
+ };
+ break ClientOperation::AddWorktreeToProject {
+ project_root_name,
+ new_root_path,
+ };
+ }
+
+ // Add an entry to a worktree
+ _ => {
+ let Some(project) = choose_random_project(client, &mut self.rng) else { continue };
+ let project_root_name = root_name_for_project(&project, cx);
+ let is_local = project.read_with(cx, |project, _| project.is_local());
+ 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 self.rng)
+ });
+ let Some(worktree) = worktree else { continue };
+ let is_dir = self.rng.gen::<bool>();
+ let mut full_path =
+ worktree.read_with(cx, |w, _| PathBuf::from(w.root_name()));
+ full_path.push(gen_file_name(&mut self.rng));
+ if !is_dir {
+ full_path.set_extension("rs");
+ }
+ break ClientOperation::CreateWorktreeEntry {
+ project_root_name,
+ is_local,
+ full_path,
+ is_dir,
+ };
+ }
+ },
+ },
+
+ // Query and mutate buffers
+ 60..=95 => {
+ let Some(project) = choose_random_project(client, &mut self.rng) else { continue };
+ let project_root_name = root_name_for_project(&project, cx);
+ let is_local = project.read_with(cx, |project, _| project.is_local());
+
+ match self.rng.gen_range(0..100_u32) {
+ // Manipulate an existing buffer
+ 0..=70 => {
+ let Some(buffer) = client
+ .buffers_for_project(&project)
+ .iter()
+ .choose(&mut self.rng)
+ .cloned() else { continue };
+
+ let full_path = buffer
+ .read_with(cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
+
+ match self.rng.gen_range(0..100_u32) {
+ // Close the buffer
+ 0..=15 => {
+ break ClientOperation::CloseBuffer {
+ project_root_name,
+ is_local,
+ full_path,
+ };
+ }
+ // Save the buffer
+ 16..=29 if buffer.read_with(cx, |b, _| b.is_dirty()) => {
+ let detach = self.rng.gen_bool(0.3);
+ break ClientOperation::SaveBuffer {
+ project_root_name,
+ is_local,
+ full_path,
+ detach,
+ };
+ }
+ // Edit the buffer
+ 30..=69 => {
+ let edits = buffer.read_with(cx, |buffer, _| {
+ buffer.get_random_edits(&mut self.rng, 3)
+ });
+ break ClientOperation::EditBuffer {
+ project_root_name,
+ is_local,
+ full_path,
+ edits,
+ };
+ }
+ // Make an LSP request
+ _ => {
+ let offset = buffer.read_with(cx, |buffer, _| {
+ buffer.clip_offset(
+ self.rng.gen_range(0..=buffer.len()),
+ language::Bias::Left,
+ )
+ });
+ let detach = self.rng.gen();
+ break ClientOperation::RequestLspDataInBuffer {
+ project_root_name,
+ full_path,
+ offset,
+ is_local,
+ kind: match self.rng.gen_range(0..5_u32) {
+ 0 => LspRequestKind::Rename,
+ 1 => LspRequestKind::Highlights,
+ 2 => LspRequestKind::Definition,
+ 3 => LspRequestKind::CodeAction,
+ 4.. => LspRequestKind::Completion,
+ },
+ detach,
+ };
+ }
+ }
+ }
+
+ 71..=80 => {
+ let query = self.rng.gen_range('a'..='z').to_string();
+ let detach = self.rng.gen_bool(0.3);
+ break ClientOperation::SearchProject {
+ project_root_name,
+ query,
+ detach,
+ };
+ }
+
+ // Open a buffer
+ 81.. => {
+ 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())
+ })
+ .choose(&mut self.rng)
+ });
+ let Some(worktree) = worktree else { continue };
+ let full_path = worktree.read_with(cx, |worktree, _| {
+ let entry = worktree
+ .entries(false)
+ .filter(|e| e.is_file())
+ .choose(&mut self.rng)
+ .unwrap();
+ if entry.path.as_ref() == Path::new("") {
+ Path::new(worktree.root_name()).into()
+ } else {
+ Path::new(worktree.root_name()).join(&entry.path)
+ }
+ });
+ break ClientOperation::OpenBuffer {
+ project_root_name,
+ is_local,
+ full_path,
+ };
+ }
+ }
+ }
+
+ // Create a file or directory
+ 96.. => {
+ let is_dir = self.rng.gen::<bool>();
+ let mut path = client
+ .fs
+ .directories()
+ .await
+ .choose(&mut self.rng)
+ .unwrap()
+ .clone();
+ path.push(gen_file_name(&mut self.rng));
+ if !is_dir {
+ path.set_extension("rs");
+ }
+ break ClientOperation::CreateFsEntry { path, is_dir };
+ }
}
- }
+ };
+ self.operation_ix += 1;
+ Some(operation)
+ }
- ClientOperation::RequestLspDataInBuffer {
- project_root_name,
- full_path,
- offset,
- kind,
- detach,
- } => {
- log::info!(
- "{}: request LSP {:?} for buffer {:?} in project {}{}",
- client.username,
- kind,
- full_path,
- project_root_name,
- if detach { ", detaching" } else { ", awaiting" }
- );
+ fn next_root_dir_name(&mut self, user_id: UserId) -> String {
+ let user_ix = self
+ .users
+ .iter()
+ .position(|user| user.user_id == user_id)
+ .unwrap();
+ let root_id = util::post_inc(&mut self.users[user_ix].next_root_id);
+ format!("dir-{user_id}-{root_id}")
+ }
- let project = project_for_root_name(client, &project_root_name, cx)
- .expect("invalid project in test operation");
- let buffer =
- buffer_for_full_path(&*client.buffers_for_project(&project), &full_path, cx)
- .expect("invalid buffer path in test operation");
- let request = match kind {
- LspRequestKind::Rename => cx.spawn(|mut cx| async move {
- project
- .update(&mut cx, |p, cx| p.prepare_rename(buffer, offset, cx))
- .await?;
- anyhow::Ok(())
- }),
- LspRequestKind::Completion => cx.spawn(|mut cx| async move {
- project
- .update(&mut cx, |p, cx| p.completions(&buffer, offset, cx))
- .await?;
- Ok(())
- }),
- LspRequestKind::CodeAction => cx.spawn(|mut cx| async move {
- project
- .update(&mut cx, |p, cx| p.code_actions(&buffer, offset..offset, cx))
- .await?;
- Ok(())
- }),
- LspRequestKind::Definition => cx.spawn(|mut cx| async move {
- project
- .update(&mut cx, |p, cx| p.definition(&buffer, offset, cx))
- .await?;
- Ok(())
- }),
- LspRequestKind::Highlights => cx.spawn(|mut cx| async move {
- project
- .update(&mut cx, |p, cx| p.document_highlights(&buffer, offset, cx))
- .await?;
- Ok(())
- }),
- };
- if detach {
- request.detach();
- } else {
- request.await?;
- }
- }
+ fn user(&mut self, user_id: UserId) -> &mut UserTestPlan {
+ let ix = self
+ .users
+ .iter()
+ .position(|user| user.user_id == user_id)
+ .unwrap();
+ &mut self.users[ix]
+ }
+}
- ClientOperation::SearchProject {
- project_root_name,
- query,
- detach,
- } => {
- log::info!(
- "{}: search project {} for {:?}{}",
- client.username,
- project_root_name,
- query,
- if detach { ", detaching" } else { ", awaiting" }
- );
- let project = project_for_root_name(client, &project_root_name, cx)
- .expect("invalid project in test operation");
- let search = project.update(cx, |project, cx| {
- 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 detach {
- log::info!("{}: detaching save request", client.username);
- cx.update(|cx| search.detach_and_log_err(cx));
- } else {
- search.await?;
- }
- }
+async fn simulate_client(
+ client: Rc<TestClient>,
+ mut operation_rx: futures::channel::mpsc::UnboundedReceiver<()>,
+ plan: Arc<Mutex<TestPlan>>,
+ mut cx: TestAppContext,
+) {
+ // Setup language server
+ let mut language = Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ None,
+ );
+ let _fake_language_servers = language
+ .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ name: "the-fake-language-server",
+ capabilities: lsp::LanguageServer::full_capabilities(),
+ initializer: Some(Box::new({
+ let plan = plan.clone();
+ let fs = client.fs.clone();
+ move |fake_server: &mut FakeLanguageServer| {
+ fake_server.handle_request::<lsp::request::Completion, _, _>(
+ |_, _| async move {
+ Ok(Some(lsp::CompletionResponse::Array(vec![
+ lsp::CompletionItem {
+ text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
+ range: lsp::Range::new(
+ lsp::Position::new(0, 0),
+ lsp::Position::new(0, 0),
+ ),
+ new_text: "the-new-text".to_string(),
+ })),
+ ..Default::default()
+ },
+ ])))
+ },
+ );
+
+ fake_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
+ |_, _| async move {
+ Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
+ lsp::CodeAction {
+ title: "the-code-action".to_string(),
+ ..Default::default()
+ },
+ )]))
+ },
+ );
+
+ fake_server.handle_request::<lsp::request::PrepareRenameRequest, _, _>(
+ |params, _| async move {
+ Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
+ params.position,
+ params.position,
+ ))))
+ },
+ );
+
+ fake_server.handle_request::<lsp::request::GotoDefinition, _, _>({
+ let fs = fs.clone();
+ let plan = plan.clone();
+ move |_, _| {
+ let fs = fs.clone();
+ let plan = plan.clone();
+ async move {
+ let files = fs.files().await;
+ let mut plan = plan.lock();
+ let count = plan.rng.gen_range::<usize, _>(1..3);
+ let files = (0..count)
+ .map(|_| files.choose(&mut plan.rng).unwrap())
+ .collect::<Vec<_>>();
+ log::info!("LSP: Returning definitions in files {:?}", &files);
+ Ok(Some(lsp::GotoDefinitionResponse::Array(
+ files
+ .into_iter()
+ .map(|file| lsp::Location {
+ uri: lsp::Url::from_file_path(file).unwrap(),
+ range: Default::default(),
+ })
+ .collect(),
+ )))
+ }
+ }
+ });
- ClientOperation::Other => {
- let choice = plan.lock().rng.gen_range(0..100);
- match choice {
- 0..=59
- if !client.local_projects().is_empty()
- || !client.remote_projects().is_empty() =>
- {
- randomly_mutate_worktrees(client, &plan, cx).await?;
+ fake_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>({
+ let plan = plan.clone();
+ move |_, _| {
+ let mut highlights = Vec::new();
+ let highlight_count = plan.lock().rng.gen_range(1..=5);
+ for _ in 0..highlight_count {
+ let start_row = plan.lock().rng.gen_range(0..100);
+ let start_column = plan.lock().rng.gen_range(0..100);
+ let start = PointUtf16::new(start_row, start_column);
+ let end_row = plan.lock().rng.gen_range(0..100);
+ let end_column = plan.lock().rng.gen_range(0..100);
+ let end = PointUtf16::new(end_row, end_column);
+ let range = if start > end { end..start } else { start..end };
+ highlights.push(lsp::DocumentHighlight {
+ range: range_to_lsp(range.clone()),
+ kind: Some(lsp::DocumentHighlightKind::READ),
+ });
+ }
+ highlights.sort_unstable_by_key(|highlight| {
+ (highlight.range.start, highlight.range.end)
+ });
+ async move { Ok(Some(highlights)) }
+ }
+ });
}
- _ => randomly_mutate_fs(client, &plan).await,
- }
+ })),
+ ..Default::default()
+ }))
+ .await;
+ client.language_registry.add(Arc::new(language));
+
+ while operation_rx.next().await.is_some() {
+ let Some(operation) = plan.lock().next_client_operation(&client, &cx).await else { break };
+ if let Err(error) = apply_client_operation(&client, operation, &mut cx).await {
+ log::error!("{} error: {}", client.username, error);
}
+ cx.background().simulate_random_delay().await;
}
- Ok(())
+ log::info!("{}: done", client.username);
}
fn buffer_for_full_path(