Make operations for all buffer manipulations

This commit is contained in:
Max Brunsfeld 2023-01-06 12:55:50 -08:00
parent f1b3692a35
commit 210286da48

View file

@ -6,7 +6,7 @@ use crate::{
use anyhow::{anyhow, Result};
use call::ActiveCall;
use client::RECEIVE_TIMEOUT;
use collections::BTreeMap;
use collections::{BTreeMap, HashSet};
use fs::Fs as _;
use futures::StreamExt as _;
use gpui::{executor::Deterministic, ModelHandle, TestAppContext};
@ -486,14 +486,44 @@ enum ClientOperation {
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,
}
#[derive(Debug)]
enum LspRequestKind {
Rename,
Completion,
CodeAction,
Definition,
Highlights,
}
impl TestPlan {
async fn next_operation(&mut self, clients: &[(Rc<TestClient>, TestAppContext)]) -> Operation {
let operation = loop {
@ -679,27 +709,44 @@ impl TestPlan {
}
},
// Mutate buffers
// 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);
match self.rng.gen_range(0..100_u32) {
// Manipulate an existing buffer
0..=80 => {
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) {
0..=9 => {
let (full_path, edits) = buffer.read_with(cx, |buffer, cx| {
(
buffer.file().unwrap().full_path(cx),
buffer.get_random_edits(&mut self.rng, 3),
)
// 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,
@ -707,10 +754,42 @@ impl TestPlan {
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,
};
}
}
}
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| {
@ -1066,21 +1145,159 @@ async fn apply_client_operation(
);
let project = project_for_root_name(client, &project_root_name, cx)
.expect("invalid project in test operation");
let buffer = client
.buffers_for_project(&project)
.iter()
.find(|buffer| {
buffer.read_with(cx, |buffer, cx| {
buffer.file().unwrap().full_path(cx) == full_path
})
})
.cloned()
.expect("invalid buffer path in test operation");
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::CloseBuffer {
project_root_name,
full_path,
} => {
log::info!(
"{}: dropping 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");
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::SaveBuffer {
project_root_name,
full_path,
detach,
} => {
log::info!(
"{}: saving buffer {:?} in project {}{}",
client.username,
full_path,
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");
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::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" }
);
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?;
}
}
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?;
}
}
ClientOperation::Other => {
let choice = plan.lock().rng.gen_range(0..100);
match choice {
@ -1090,12 +1307,6 @@ async fn apply_client_operation(
{
randomly_mutate_worktrees(client, &plan, cx).await?;
}
60..=84
if !client.local_projects().is_empty()
|| !client.remote_projects().is_empty() =>
{
randomly_query_and_mutate_buffers(client, &plan, cx).await?;
}
_ => randomly_mutate_fs(client, &plan).await,
}
}
@ -1103,6 +1314,21 @@ async fn apply_client_operation(
Ok(())
}
fn buffer_for_full_path(
buffers: &HashSet<ModelHandle<language::Buffer>>,
full_path: &PathBuf,
cx: &TestAppContext,
) -> Option<ModelHandle<language::Buffer>> {
buffers
.iter()
.find(|buffer| {
buffer.read_with(cx, |buffer, cx| {
buffer.file().unwrap().full_path(cx) == *full_path
})
})
.cloned()
}
fn project_for_root_name(
client: &TestClient,
root_name: &str,
@ -1246,264 +1472,6 @@ async fn randomly_mutate_worktrees(
Ok(())
}
async fn randomly_query_and_mutate_buffers(
client: &TestClient,
plan: &Arc<Mutex<TestPlan>>,
cx: &mut TestAppContext,
) -> Result<()> {
let project = choose_random_project(client, &mut plan.lock().rng).unwrap();
let has_buffers_for_project = !client.buffers_for_project(&project).is_empty();
let buffer = if !has_buffers_for_project || plan.lock().rng.gen() {
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 plan.lock().rng)
}) else {
return Ok(());
};
let (worktree_root_name, project_path) = worktree.read_with(cx, |worktree, _| {
let entry = worktree
.entries(false)
.filter(|e| e.is_file())
.choose(&mut plan.lock().rng)
.unwrap();
(
worktree.root_name().to_string(),
(worktree.id(), entry.path.clone()),
)
});
log::info!(
"{}: opening path {:?} in worktree {} ({})",
client.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 {}",
client.username,
project_path.1,
project_path.0,
worktree_root_name,
buffer.read_with(cx, |buffer, _| buffer.remote_id())
);
client.buffers_for_project(&project).insert(buffer.clone());
buffer
} else {
client
.buffers_for_project(&project)
.iter()
.choose(&mut plan.lock().rng)
.unwrap()
.clone()
};
let choice = plan.lock().rng.gen_range(0..100);
match choice {
0..=9 => {
cx.update(|cx| {
log::info!(
"{}: dropping buffer {:?}",
client.username,
buffer.read(cx).file().unwrap().full_path(cx)
);
client.buffers_for_project(&project).remove(&buffer);
drop(buffer);
});
}
10..=19 => {
let completions = project.update(cx, |project, cx| {
log::info!(
"{}: requesting completions for buffer {} ({:?})",
client.username,
buffer.read(cx).remote_id(),
buffer.read(cx).file().unwrap().full_path(cx)
);
let offset = plan.lock().rng.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 plan.lock().rng.gen_bool(0.3) {
log::info!("{}: detaching completions request", client.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 {} ({:?})",
client.username,
buffer.read(cx).remote_id(),
buffer.read(cx).file().unwrap().full_path(cx)
);
let range = buffer.read(cx).random_byte_range(0, &mut plan.lock().rng);
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 plan.lock().rng.gen_bool(0.3) {
log::info!("{}: detaching code actions request", client.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 {} ({:?})",
client.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 plan.lock().rng.gen_bool(0.3) {
log::info!("{}: detaching save request", client.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 {} ({:?})",
client.username,
buffer.read(cx).remote_id(),
buffer.read(cx).file().unwrap().full_path(cx)
);
let offset = plan.lock().rng.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 plan.lock().rng.gen_bool(0.3) {
log::info!("{}: detaching prepare rename request", client.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 {} ({:?})",
client.username,
buffer.read(cx).remote_id(),
buffer.read(cx).file().unwrap().full_path(cx)
);
let offset = plan.lock().rng.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 plan.lock().rng.gen_bool(0.3) {
log::info!("{}: detaching definitions request", client.username);
cx.update(|cx| definitions.detach_and_log_err(cx));
} else {
let definitions = definitions.await?;
client
.buffers_for_project(&project)
.extend(definitions.into_iter().map(|loc| loc.target.buffer));
}
}
50..=54 => {
let highlights = project.update(cx, |project, cx| {
log::info!(
"{}: requesting highlights for buffer {} ({:?})",
client.username,
buffer.read(cx).remote_id(),
buffer.read(cx).file().unwrap().full_path(cx)
);
let offset = plan.lock().rng.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 plan.lock().rng.gen_bool(0.3) {
log::info!("{}: detaching highlights request", client.username);
cx.update(|cx| highlights.detach_and_log_err(cx));
} else {
highlights.await?;
}
}
55..=59 => {
let search = project.update(cx, |project, cx| {
let query = plan.lock().rng.gen_range('a'..='z');
log::info!("{}: project-wide search {:?}", client.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 plan.lock().rng.gen_bool(0.3) {
log::info!("{}: detaching search request", client.username);
cx.update(|cx| search.detach_and_log_err(cx));
} else {
let search = search.await?;
client
.buffers_for_project(&project)
.extend(search.into_keys());
}
}
_ => {
buffer.update(cx, |buffer, cx| {
log::info!(
"{}: updating buffer {} ({:?})",
client.username,
buffer.remote_id(),
buffer.file().unwrap().full_path(cx)
);
if plan.lock().rng.gen_bool(0.7) {
buffer.randomly_edit(&mut plan.lock().rng, 5, cx);
} else {
buffer.randomly_undo_redo(&mut plan.lock().rng, cx);
}
});
}
}
Ok(())
}
fn choose_random_project(client: &TestClient, rng: &mut StdRng) -> Option<ModelHandle<Project>> {
client
.local_projects()