Skip inapplicable operations when running an edited test plan

This commit is contained in:
Max Brunsfeld 2023-01-09 11:36:53 -08:00
parent c503ba00b6
commit 3e3a703b60

View file

@ -7,9 +7,10 @@ use anyhow::{anyhow, Result};
use call::ActiveCall; use call::ActiveCall;
use client::RECEIVE_TIMEOUT; use client::RECEIVE_TIMEOUT;
use collections::{BTreeMap, HashSet}; use collections::{BTreeMap, HashSet};
use editor::Bias;
use fs::{FakeFs, Fs as _}; use fs::{FakeFs, Fs as _};
use futures::StreamExt as _; use futures::StreamExt as _;
use gpui::{executor::Deterministic, ModelHandle, TestAppContext}; use gpui::{executor::Deterministic, ModelHandle, Task, TestAppContext};
use language::{range_to_lsp, FakeLspAdapter, Language, LanguageConfig, PointUtf16}; use language::{range_to_lsp, FakeLspAdapter, Language, LanguageConfig, PointUtf16};
use lsp::FakeLanguageServer; use lsp::FakeLanguageServer;
use parking_lot::Mutex; use parking_lot::Mutex;
@ -21,7 +22,10 @@ use std::{
ops::Range, ops::Range,
path::{Path, PathBuf}, path::{Path, PathBuf},
rc::Rc, rc::Rc,
sync::Arc, sync::{
atomic::{AtomicBool, Ordering::SeqCst},
Arc,
},
}; };
use util::ResultExt; use util::ResultExt;
@ -101,147 +105,21 @@ async fn test_random_collaboration(
let mut next_entity_id = 100000; let mut next_entity_id = 100000;
loop { loop {
let Some(next_operation) = plan.lock().next_operation(&clients).await else { break }; let Some((next_operation, skipped)) = plan.lock().next_server_operation(&clients) else { break };
match next_operation { let applied = apply_server_operation(
Operation::AddConnection { user_id } => { deterministic.clone(),
let username = { &mut server,
let mut plan = plan.lock(); &mut clients,
let mut user = plan.user(user_id); &mut client_tasks,
user.online = true; &mut operation_channels,
user.username.clone() &mut next_entity_id,
}; plan.clone(),
log::info!("Adding new connection for {}", username); next_operation,
next_entity_id += 100000; cx,
let mut client_cx = TestAppContext::new( )
cx.foreground_platform(), .await;
cx.platform(), if !applied {
deterministic.build_foreground(next_entity_id), skipped.store(false, SeqCst);
deterministic.build_background(),
cx.font_cache(),
cx.leak_detector(),
next_entity_id,
cx.function_name.clone(),
);
let (operation_tx, operation_rx) = futures::channel::mpsc::unbounded();
let client = Rc::new(server.create_client(&mut client_cx, &username).await);
operation_channels.push(operation_tx);
clients.push((client.clone(), client_cx.clone()));
client_tasks.push(client_cx.foreground().spawn(simulate_client(
client,
operation_rx,
plan.clone(),
client_cx,
)));
log::info!("Added connection for {}", username);
}
Operation::RemoveConnection { user_id } => {
log::info!("Simulating full disconnection of user {}", user_id);
let client_ix = clients
.iter()
.position(|(client, cx)| client.current_user_id(cx) == user_id)
.unwrap();
let user_connection_ids = server
.connection_pool
.lock()
.user_connection_ids(user_id)
.collect::<Vec<_>>();
assert_eq!(user_connection_ids.len(), 1);
let removed_peer_id = user_connection_ids[0].into();
let (client, mut client_cx) = clients.remove(client_ix);
let client_task = client_tasks.remove(client_ix);
operation_channels.remove(client_ix);
server.forbid_connections();
server.disconnect_client(removed_peer_id);
deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
deterministic.start_waiting();
log::info!("Waiting for user {} to exit...", user_id);
client_task.await;
deterministic.finish_waiting();
server.allow_connections();
for project in client.remote_projects().iter() {
project.read_with(&client_cx, |project, _| {
assert!(
project.is_read_only(),
"project {:?} should be read only",
project.remote_id()
)
});
}
for (client, cx) in &clients {
let contacts = server
.app_state
.db
.get_contacts(client.current_user_id(cx))
.await
.unwrap();
let pool = server.connection_pool.lock();
for contact in contacts {
if let db::Contact::Accepted { user_id: id, .. } = contact {
if pool.is_user_online(id) {
assert_ne!(
id, user_id,
"removed client is still a contact of another peer"
);
}
}
}
}
log::info!("{} removed", client.username);
plan.lock().user(user_id).online = false;
client_cx.update(|cx| {
cx.clear_globals();
drop(client);
});
}
Operation::BounceConnection { user_id } => {
log::info!("Simulating temporary disconnection of user {}", user_id);
let user_connection_ids = server
.connection_pool
.lock()
.user_connection_ids(user_id)
.collect::<Vec<_>>();
assert_eq!(user_connection_ids.len(), 1);
let peer_id = user_connection_ids[0].into();
server.disconnect_client(peer_id);
deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
}
Operation::RestartServer => {
log::info!("Simulating server restart");
server.reset().await;
deterministic.advance_clock(RECEIVE_TIMEOUT);
server.start().await.unwrap();
deterministic.advance_clock(CLEANUP_TIMEOUT);
let environment = &server.app_state.config.zed_environment;
let stale_room_ids = server
.app_state
.db
.stale_room_ids(environment, server.id())
.await
.unwrap();
assert_eq!(stale_room_ids, vec![]);
}
Operation::MutateClients { user_ids, quiesce } => {
for user_id in user_ids {
let client_ix = clients
.iter()
.position(|(client, cx)| client.current_user_id(cx) == user_id)
.unwrap();
operation_channels[client_ix].unbounded_send(()).unwrap();
}
if quiesce {
deterministic.run_until_parked();
}
}
} }
} }
@ -430,39 +308,216 @@ async fn test_random_collaboration(
} }
} }
async fn apply_server_operation(
deterministic: Arc<Deterministic>,
server: &mut TestServer,
clients: &mut Vec<(Rc<TestClient>, TestAppContext)>,
client_tasks: &mut Vec<Task<()>>,
operation_channels: &mut Vec<futures::channel::mpsc::UnboundedSender<()>>,
next_entity_id: &mut usize,
plan: Arc<Mutex<TestPlan>>,
operation: Operation,
cx: &mut TestAppContext,
) -> bool {
match operation {
Operation::AddConnection { user_id } => {
let username;
{
let mut plan = plan.lock();
let mut user = plan.user(user_id);
if user.online {
return false;
}
user.online = true;
username = user.username.clone();
};
log::info!("Adding new connection for {}", username);
*next_entity_id += 100000;
let mut client_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 (operation_tx, operation_rx) = futures::channel::mpsc::unbounded();
let client = Rc::new(server.create_client(&mut client_cx, &username).await);
operation_channels.push(operation_tx);
clients.push((client.clone(), client_cx.clone()));
client_tasks.push(client_cx.foreground().spawn(simulate_client(
client,
operation_rx,
plan.clone(),
client_cx,
)));
log::info!("Added connection for {}", username);
}
Operation::RemoveConnection { user_id } => {
log::info!("Simulating full disconnection of user {}", user_id);
let client_ix = clients
.iter()
.position(|(client, cx)| client.current_user_id(cx) == user_id);
let Some(client_ix) = client_ix else { return false };
let user_connection_ids = server
.connection_pool
.lock()
.user_connection_ids(user_id)
.collect::<Vec<_>>();
assert_eq!(user_connection_ids.len(), 1);
let removed_peer_id = user_connection_ids[0].into();
let (client, mut client_cx) = clients.remove(client_ix);
let client_task = client_tasks.remove(client_ix);
operation_channels.remove(client_ix);
server.forbid_connections();
server.disconnect_client(removed_peer_id);
deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
deterministic.start_waiting();
log::info!("Waiting for user {} to exit...", user_id);
client_task.await;
deterministic.finish_waiting();
server.allow_connections();
for project in client.remote_projects().iter() {
project.read_with(&client_cx, |project, _| {
assert!(
project.is_read_only(),
"project {:?} should be read only",
project.remote_id()
)
});
}
for (client, cx) in clients {
let contacts = server
.app_state
.db
.get_contacts(client.current_user_id(cx))
.await
.unwrap();
let pool = server.connection_pool.lock();
for contact in contacts {
if let db::Contact::Accepted { user_id: id, .. } = contact {
if pool.is_user_online(id) {
assert_ne!(
id, user_id,
"removed client is still a contact of another peer"
);
}
}
}
}
log::info!("{} removed", client.username);
plan.lock().user(user_id).online = false;
client_cx.update(|cx| {
cx.clear_globals();
drop(client);
});
}
Operation::BounceConnection { user_id } => {
log::info!("Simulating temporary disconnection of user {}", user_id);
let user_connection_ids = server
.connection_pool
.lock()
.user_connection_ids(user_id)
.collect::<Vec<_>>();
if user_connection_ids.is_empty() {
return false;
}
assert_eq!(user_connection_ids.len(), 1);
let peer_id = user_connection_ids[0].into();
server.disconnect_client(peer_id);
deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
}
Operation::RestartServer => {
log::info!("Simulating server restart");
server.reset().await;
deterministic.advance_clock(RECEIVE_TIMEOUT);
server.start().await.unwrap();
deterministic.advance_clock(CLEANUP_TIMEOUT);
let environment = &server.app_state.config.zed_environment;
let stale_room_ids = server
.app_state
.db
.stale_room_ids(environment, server.id())
.await
.unwrap();
assert_eq!(stale_room_ids, vec![]);
}
Operation::MutateClients { user_ids, quiesce } => {
let mut applied = false;
for user_id in user_ids {
let client_ix = clients
.iter()
.position(|(client, cx)| client.current_user_id(cx) == user_id);
let Some(client_ix) = client_ix else { continue };
applied = true;
if let Err(err) = operation_channels[client_ix].unbounded_send(()) {
// panic!("error signaling user {}, client {}", user_id, client_ix);
}
}
if quiesce && applied {
deterministic.run_until_parked();
}
return applied;
}
}
true
}
async fn apply_client_operation( async fn apply_client_operation(
client: &TestClient, client: &TestClient,
operation: ClientOperation, operation: ClientOperation,
cx: &mut TestAppContext, cx: &mut TestAppContext,
) -> Result<()> { ) -> Result<bool> {
match operation { match operation {
ClientOperation::AcceptIncomingCall => { ClientOperation::AcceptIncomingCall => {
log::info!("{}: accepting incoming call", client.username);
let active_call = cx.read(ActiveCall::global); let active_call = cx.read(ActiveCall::global);
if active_call.read_with(cx, |call, _| call.incoming().borrow().is_none()) {
return Ok(false);
}
log::info!("{}: accepting incoming call", client.username);
active_call active_call
.update(cx, |call, cx| call.accept_incoming(cx)) .update(cx, |call, cx| call.accept_incoming(cx))
.await?; .await?;
} }
ClientOperation::RejectIncomingCall => { ClientOperation::RejectIncomingCall => {
log::info!("{}: declining incoming call", client.username);
let active_call = cx.read(ActiveCall::global); let active_call = cx.read(ActiveCall::global);
if active_call.read_with(cx, |call, _| call.incoming().borrow().is_none()) {
return Ok(false);
}
log::info!("{}: declining incoming call", client.username);
active_call.update(cx, |call, _| call.decline_incoming())?; active_call.update(cx, |call, _| call.decline_incoming())?;
} }
ClientOperation::LeaveCall => { ClientOperation::LeaveCall => {
log::info!("{}: hanging up", client.username);
let active_call = cx.read(ActiveCall::global); let active_call = cx.read(ActiveCall::global);
if active_call.read_with(cx, |call, _| call.room().is_none()) {
return Ok(false);
}
log::info!("{}: hanging up", client.username);
active_call.update(cx, |call, cx| call.hang_up(cx))?; active_call.update(cx, |call, cx| call.hang_up(cx))?;
} }
ClientOperation::InviteContactToCall { user_id } => { ClientOperation::InviteContactToCall { user_id } => {
log::info!("{}: inviting {}", client.username, user_id,);
let active_call = cx.read(ActiveCall::global); let active_call = cx.read(ActiveCall::global);
log::info!("{}: inviting {}", client.username, user_id,);
active_call active_call
.update(cx, |call, cx| call.invite(user_id.to_proto(), None, cx)) .update(cx, |call, cx| call.invite(user_id.to_proto(), None, cx))
.await .await
@ -492,6 +547,10 @@ async fn apply_client_operation(
project_root_name, project_root_name,
new_root_path, new_root_path,
} => { } => {
let Some(project) = project_for_root_name(client, &project_root_name, cx) else {
return Ok(false)
};
log::info!( log::info!(
"{}: finding/creating local worktree at {:?} to project with root path {}", "{}: finding/creating local worktree at {:?} to project with root path {}",
client.username, client.username,
@ -499,8 +558,6 @@ async fn apply_client_operation(
project_root_name 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; ensure_project_shared(&project, client, cx).await;
if !client.fs.paths().await.contains(&new_root_path) { if !client.fs.paths().await.contains(&new_root_path) {
client.fs.create_dir(&new_root_path).await.unwrap(); client.fs.create_dir(&new_root_path).await.unwrap();
@ -514,21 +571,56 @@ async fn apply_client_operation(
} }
ClientOperation::CloseRemoteProject { project_root_name } => { ClientOperation::CloseRemoteProject { project_root_name } => {
let Some(project) = project_for_root_name(client, &project_root_name, cx) else {
return Ok(false)
};
log::info!( log::info!(
"{}: closing remote project with root path {}", "{}: closing remote project with root path {}",
client.username, client.username,
project_root_name, project_root_name,
); );
let ix = project_ix_for_root_name(&*client.remote_projects(), &project_root_name, cx) let ix = client
.expect("invalid project in test operation"); .remote_projects()
cx.update(|_| client.remote_projects_mut().remove(ix)); .iter()
.position(|p| p == &project)
.unwrap();
cx.update(|_| {
client.remote_projects_mut().remove(ix);
drop(project);
});
} }
ClientOperation::OpenRemoteProject { ClientOperation::OpenRemoteProject {
host_id, host_id,
first_root_name, first_root_name,
} => { } => {
let active_call = cx.read(ActiveCall::global);
let project = active_call.update(cx, |call, cx| {
let room = call.room().cloned()?;
let participant = room
.read(cx)
.remote_participants()
.get(&host_id.to_proto())?;
let project_id = participant
.projects
.iter()
.find(|project| project.worktree_root_names[0] == first_root_name)?
.id;
Some(room.update(cx, |room, cx| {
room.join_project(
project_id,
client.language_registry.clone(),
FakeFs::new(cx.background().clone()),
cx,
)
}))
});
let Some(project) = project else {
return Ok(false)
};
log::info!( log::info!(
"{}: joining remote project of user {}, root name {}", "{}: joining remote project of user {}, root name {}",
client.username, client.username,
@ -536,30 +628,7 @@ async fn apply_client_operation(
first_root_name, first_root_name,
); );
let active_call = cx.read(ActiveCall::global); let project = project.await?;
let project = active_call
.update(cx, |call, cx| {
let room = call.room().cloned()?;
let participant = room
.read(cx)
.remote_participants()
.get(&host_id.to_proto())?;
let project_id = participant
.projects
.iter()
.find(|project| project.worktree_root_names[0] == first_root_name)?
.id;
Some(room.update(cx, |room, cx| {
room.join_project(
project_id,
client.language_registry.clone(),
FakeFs::new(cx.background().clone()),
cx,
)
}))
})
.expect("invalid project in test operation")
.await?;
client.remote_projects_mut().push(project.clone()); client.remote_projects_mut().push(project.clone());
} }
@ -569,6 +638,13 @@ async fn apply_client_operation(
full_path, full_path,
is_dir, is_dir,
} => { } => {
let Some(project) = project_for_root_name(client, &project_root_name, cx) else {
return Ok(false);
};
let Some(project_path) = project_path_for_full_path(&project, &full_path, cx) else {
return Ok(false);
};
log::info!( log::info!(
"{}: creating {} at path {:?} in {} project {}", "{}: creating {} at path {:?} in {} project {}",
client.username, client.username,
@ -578,11 +654,7 @@ async fn apply_client_operation(
project_root_name, 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; 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 project
.update(cx, |p, cx| p.create_entry(project_path, is_dir, cx)) .update(cx, |p, cx| p.create_entry(project_path, is_dir, cx))
.unwrap() .unwrap()
@ -594,6 +666,13 @@ async fn apply_client_operation(
is_local, is_local,
full_path, full_path,
} => { } => {
let Some(project) = project_for_root_name(client, &project_root_name, cx) else {
return Ok(false);
};
let Some(project_path) = project_path_for_full_path(&project, &full_path, cx) else {
return Ok(false);
};
log::info!( log::info!(
"{}: opening buffer {:?} in {} project {}", "{}: opening buffer {:?} in {} project {}",
client.username, client.username,
@ -602,11 +681,7 @@ async fn apply_client_operation(
project_root_name, 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; 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 let buffer = project
.update(cx, |project, cx| project.open_buffer(project_path, cx)) .update(cx, |project, cx| project.open_buffer(project_path, cx))
.await?; .await?;
@ -619,6 +694,14 @@ async fn apply_client_operation(
full_path, full_path,
edits, edits,
} => { } => {
let Some(project) = project_for_root_name(client, &project_root_name, cx) else {
return Ok(false);
};
let Some(buffer) =
buffer_for_full_path(&*client.buffers_for_project(&project), &full_path, cx) else {
return Ok(false);
};
log::info!( log::info!(
"{}: editing buffer {:?} in {} project {} with {:?}", "{}: editing buffer {:?} in {} project {} with {:?}",
client.username, client.username,
@ -628,14 +711,18 @@ async fn apply_client_operation(
edits 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; 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.update(cx, |buffer, cx| {
buffer.edit(edits, None, cx); let snapshot = buffer.snapshot();
buffer.edit(
edits.into_iter().map(|(range, text)| {
let start = snapshot.clip_offset(range.start, Bias::Left);
let end = snapshot.clip_offset(range.end, Bias::Right);
(start..end, text)
}),
None,
cx,
);
}); });
} }
@ -644,20 +731,23 @@ async fn apply_client_operation(
is_local, is_local,
full_path, full_path,
} => { } => {
let Some(project) = project_for_root_name(client, &project_root_name, cx) else {
return Ok(false);
};
let Some(buffer) =
buffer_for_full_path(&*client.buffers_for_project(&project), &full_path, cx) else {
return Ok(false);
};
log::info!( log::info!(
"{}: dropping buffer {:?} in {} project {}", "{}: closing buffer {:?} in {} project {}",
client.username, client.username,
full_path, full_path,
if is_local { "local" } else { "remote" }, if is_local { "local" } else { "remote" },
project_root_name 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; 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");
cx.update(|_| { cx.update(|_| {
client.buffers_for_project(&project).remove(&buffer); client.buffers_for_project(&project).remove(&buffer);
drop(buffer); drop(buffer);
@ -670,6 +760,14 @@ async fn apply_client_operation(
full_path, full_path,
detach, detach,
} => { } => {
let Some(project) = project_for_root_name(client, &project_root_name, cx) else {
return Ok(false);
};
let Some(buffer) =
buffer_for_full_path(&*client.buffers_for_project(&project), &full_path, cx) else {
return Ok(false);
};
log::info!( log::info!(
"{}: saving buffer {:?} in {} project {}{}", "{}: saving buffer {:?} in {} project {}{}",
client.username, client.username,
@ -679,12 +777,7 @@ async fn apply_client_operation(
if detach { ", detaching" } else { ", awaiting" } if detach { ", detaching" } else { ", awaiting" }
); );
let project = project_for_root_name(client, &project_root_name, cx)
.expect("invalid project in test operation");
ensure_project_shared(&project, client, cx).await; 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");
let (requested_version, save) = let (requested_version, save) =
buffer.update(cx, |buffer, cx| (buffer.version(), buffer.save(cx))); buffer.update(cx, |buffer, cx| (buffer.version(), buffer.save(cx)));
let save = cx.background().spawn(async move { let save = cx.background().spawn(async move {
@ -710,6 +803,14 @@ async fn apply_client_operation(
kind, kind,
detach, detach,
} => { } => {
let Some(project) = project_for_root_name(client, &project_root_name, cx) else {
return Ok(false);
};
let Some(buffer) =
buffer_for_full_path(&*client.buffers_for_project(&project), &full_path, cx) else {
return Ok(false);
};
log::info!( log::info!(
"{}: request LSP {:?} for buffer {:?} in {} project {}{}", "{}: request LSP {:?} for buffer {:?} in {} project {}{}",
client.username, client.username,
@ -720,11 +821,7 @@ async fn apply_client_operation(
if detach { ", detaching" } else { ", awaiting" } if detach { ", detaching" } else { ", awaiting" }
); );
let project = project_for_root_name(client, &project_root_name, cx) let offset = buffer.read_with(cx, |b, _| b.clip_offset(offset, Bias::Left));
.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 { let request = match kind {
LspRequestKind::Rename => cx.spawn(|mut cx| async move { LspRequestKind::Rename => cx.spawn(|mut cx| async move {
project project
@ -770,6 +867,10 @@ async fn apply_client_operation(
query, query,
detach, detach,
} => { } => {
let Some(project) = project_for_root_name(client, &project_root_name, cx) else {
return Ok(false);
};
log::info!( log::info!(
"{}: search {} project {} for {:?}{}", "{}: search {} project {} for {:?}{}",
client.username, client.username,
@ -778,8 +879,7 @@ async fn apply_client_operation(
query, query,
if detach { ", detaching" } else { ", awaiting" } 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| { let search = project.update(cx, |project, cx| {
project.search(SearchQuery::text(query, false, false), cx) project.search(SearchQuery::text(query, false, false), cx)
}); });
@ -797,12 +897,17 @@ async fn apply_client_operation(
} }
ClientOperation::CreateFsEntry { path, is_dir } => { ClientOperation::CreateFsEntry { path, is_dir } => {
if client.fs.metadata(&path.parent().unwrap()).await?.is_none() {
return Ok(false);
}
log::info!( log::info!(
"{}: creating {} at {:?}", "{}: creating {} at {:?}",
client.username, client.username,
if is_dir { "dir" } else { "file" }, if is_dir { "dir" } else { "file" },
path path
); );
if is_dir { if is_dir {
client.fs.create_dir(&path).await.unwrap(); client.fs.create_dir(&path).await.unwrap();
} else { } else {
@ -814,13 +919,13 @@ async fn apply_client_operation(
} }
} }
} }
Ok(()) Ok(true)
} }
struct TestPlan { struct TestPlan {
rng: StdRng, rng: StdRng,
replay: bool, replay: bool,
stored_operations: Vec<StoredOperation>, stored_operations: Vec<(StoredOperation, Arc<AtomicBool>)>,
max_operations: usize, max_operations: usize,
operation_ix: usize, operation_ix: usize,
users: Vec<UserTestPlan>, users: Vec<UserTestPlan>,
@ -962,51 +1067,57 @@ impl TestPlan {
fn load(&mut self, path: &Path) { fn load(&mut self, path: &Path) {
let json = std::fs::read_to_string(path).unwrap(); let json = std::fs::read_to_string(path).unwrap();
self.replay = true; self.replay = true;
self.stored_operations = serde_json::from_str(&json).unwrap(); let stored_operations: Vec<StoredOperation> = serde_json::from_str(&json).unwrap();
self.stored_operations = stored_operations
.into_iter()
.map(|operation| (operation, Arc::new(AtomicBool::new(false))))
.collect()
} }
fn save(&mut self, path: &Path) { fn save(&mut self, path: &Path) {
// Format each operation as one line // Format each operation as one line
let mut json = Vec::new(); let mut json = Vec::new();
json.push(b'['); json.push(b'[');
for (i, stored_operation) in self.stored_operations.iter().enumerate() { for (operation, skipped) in &self.stored_operations {
if i > 0 { if skipped.load(SeqCst) {
continue;
}
if json.len() > 1 {
json.push(b','); json.push(b',');
} }
json.extend_from_slice(b"\n "); json.extend_from_slice(b"\n ");
serde_json::to_writer(&mut json, stored_operation).unwrap(); serde_json::to_writer(&mut json, operation).unwrap();
} }
json.extend_from_slice(b"\n]\n"); json.extend_from_slice(b"\n]\n");
std::fs::write(path, &json).unwrap(); std::fs::write(path, &json).unwrap();
} }
async fn next_operation( fn next_server_operation(
&mut self, &mut self,
clients: &[(Rc<TestClient>, TestAppContext)], clients: &[(Rc<TestClient>, TestAppContext)],
) -> Option<Operation> { ) -> Option<(Operation, Arc<AtomicBool>)> {
if self.replay { if self.replay {
while let Some(stored_operation) = self.stored_operations.get(self.operation_ix) { while let Some(stored_operation) = self.stored_operations.get(self.operation_ix) {
self.operation_ix += 1; self.operation_ix += 1;
if let StoredOperation::Server(operation) = stored_operation { if let (StoredOperation::Server(operation), skipped) = stored_operation {
return Some(operation.clone()); return Some((operation.clone(), skipped.clone()));
} }
} }
None None
} else { } else {
let operation = self.generate_operation(clients).await; let operation = self.generate_server_operation(clients)?;
if let Some(operation) = &operation { let skipped = Arc::new(AtomicBool::new(false));
self.stored_operations self.stored_operations
.push(StoredOperation::Server(operation.clone())) .push((StoredOperation::Server(operation.clone()), skipped.clone()));
} Some((operation, skipped))
operation
} }
} }
async fn next_client_operation( fn next_client_operation(
&mut self, &mut self,
client: &TestClient, client: &TestClient,
cx: &TestAppContext, cx: &TestAppContext,
) -> Option<ClientOperation> { ) -> Option<(ClientOperation, Arc<AtomicBool>)> {
let current_user_id = client.current_user_id(cx); let current_user_id = client.current_user_id(cx);
let user_ix = self let user_ix = self
.users .users
@ -1018,28 +1129,29 @@ impl TestPlan {
if self.replay { if self.replay {
while let Some(stored_operation) = self.stored_operations.get(user_plan.operation_ix) { while let Some(stored_operation) = self.stored_operations.get(user_plan.operation_ix) {
user_plan.operation_ix += 1; user_plan.operation_ix += 1;
if let StoredOperation::Client { user_id, operation } = stored_operation { if let (StoredOperation::Client { user_id, operation }, skipped) = stored_operation
{
if user_id == &current_user_id { if user_id == &current_user_id {
return Some(operation.clone()); return Some((operation.clone(), skipped.clone()));
} }
} }
} }
None None
} else { } else {
let operation = self let operation = self.generate_client_operation(current_user_id, client, cx)?;
.generate_client_operation(current_user_id, client, cx) let skipped = Arc::new(AtomicBool::new(false));
.await; self.stored_operations.push((
if let Some(operation) = &operation { StoredOperation::Client {
self.stored_operations.push(StoredOperation::Client {
user_id: current_user_id, user_id: current_user_id,
operation: operation.clone(), operation: operation.clone(),
}) },
} skipped.clone(),
operation ));
Some((operation, skipped))
} }
} }
async fn generate_operation( fn generate_server_operation(
&mut self, &mut self,
clients: &[(Rc<TestClient>, TestAppContext)], clients: &[(Rc<TestClient>, TestAppContext)],
) -> Option<Operation> { ) -> Option<Operation> {
@ -1091,7 +1203,7 @@ impl TestPlan {
.collect(); .collect();
Operation::MutateClients { Operation::MutateClients {
user_ids, user_ids,
quiesce: self.rng.gen(), quiesce: self.rng.gen_bool(0.7),
} }
} }
_ => continue, _ => continue,
@ -1099,7 +1211,7 @@ impl TestPlan {
}) })
} }
async fn generate_client_operation( fn generate_client_operation(
&mut self, &mut self,
user_id: UserId, user_id: UserId,
client: &TestClient, client: &TestClient,
@ -1221,11 +1333,11 @@ impl TestPlan {
// Add a worktree to a local project // Add a worktree to a local project
0..=50 => { 0..=50 => {
let Some(project) = client let Some(project) = client
.local_projects() .local_projects()
.choose(&mut self.rng) .choose(&mut self.rng)
.cloned() else { continue }; .cloned() else { continue };
let project_root_name = root_name_for_project(&project, cx); let project_root_name = root_name_for_project(&project, cx);
let mut paths = client.fs.paths().await; let mut paths = cx.background().block(client.fs.paths());
paths.remove(0); paths.remove(0);
let new_root_path = if paths.is_empty() || self.rng.gen() { let new_root_path = if paths.is_empty() || self.rng.gen() {
Path::new("/").join(&self.next_root_dir_name(user_id)) Path::new("/").join(&self.next_root_dir_name(user_id))
@ -1396,10 +1508,9 @@ impl TestPlan {
// Create a file or directory // Create a file or directory
96.. => { 96.. => {
let is_dir = self.rng.gen::<bool>(); let is_dir = self.rng.gen::<bool>();
let mut path = client let mut path = cx
.fs .background()
.directories() .block(client.fs.directories())
.await
.choose(&mut self.rng) .choose(&mut self.rng)
.unwrap() .unwrap()
.clone(); .clone();
@ -1501,10 +1612,9 @@ async fn simulate_client(
let plan = plan.clone(); let plan = plan.clone();
async move { async move {
let files = fs.files().await; let files = fs.files().await;
let mut plan = plan.lock(); let count = plan.lock().rng.gen_range::<usize, _>(1..3);
let count = plan.rng.gen_range::<usize, _>(1..3);
let files = (0..count) let files = (0..count)
.map(|_| files.choose(&mut plan.rng).unwrap()) .map(|_| files.choose(&mut plan.lock().rng).unwrap())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
log::info!("LSP: Returning definitions in files {:?}", &files); log::info!("LSP: Returning definitions in files {:?}", &files);
Ok(Some(lsp::GotoDefinitionResponse::Array( Ok(Some(lsp::GotoDefinitionResponse::Array(
@ -1552,9 +1662,16 @@ async fn simulate_client(
client.language_registry.add(Arc::new(language)); client.language_registry.add(Arc::new(language));
while operation_rx.next().await.is_some() { while operation_rx.next().await.is_some() {
let Some(operation) = plan.lock().next_client_operation(&client, &cx).await else { break }; let Some((operation, skipped)) = plan.lock().next_client_operation(&client, &cx) else { break };
if let Err(error) = apply_client_operation(&client, operation, &mut cx).await { match apply_client_operation(&client, operation, &mut cx).await {
log::error!("{} error: {}", client.username, error); Err(error) => {
log::error!("{} error: {}", client.username, error);
}
Ok(applied) => {
if !applied {
skipped.store(true, SeqCst);
}
}
} }
cx.background().simulate_random_delay().await; cx.background().simulate_random_delay().await;
} }