ZIm/crates/collab/src/tests/random_project_collaboration_tests.rs
Cole Miller e7290df02b
Finish removing git repository state and scanning logic from worktrees (#27568)
This PR completes the process of moving git repository state storage and
scanning logic from the worktree crate to `project::git_store`.

Release Notes:

- N/A

---------

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Conrad <conrad@zed.dev>
2025-04-01 17:41:20 -04:00

1646 lines
68 KiB
Rust

use super::{RandomizedTest, TestClient, TestError, TestServer, UserTestPlan};
use crate::{db::UserId, tests::run_randomized_test};
use anyhow::{Result, anyhow};
use async_trait::async_trait;
use call::ActiveCall;
use collections::{BTreeMap, HashMap};
use editor::Bias;
use fs::{FakeFs, Fs as _};
use git::status::{FileStatus, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode};
use gpui::{BackgroundExecutor, Entity, TestAppContext};
use language::{
FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, PointUtf16, range_to_lsp,
};
use lsp::FakeLanguageServer;
use pretty_assertions::assert_eq;
use project::{
DEFAULT_COMPLETION_CONTEXT, Project, ProjectPath, search::SearchQuery, search::SearchResult,
};
use rand::{
distributions::{Alphanumeric, DistString},
prelude::*,
};
use serde::{Deserialize, Serialize};
use std::{
ops::{Deref, Range},
path::{Path, PathBuf},
rc::Rc,
sync::Arc,
};
use util::{ResultExt, path};
#[gpui::test(
iterations = 100,
on_failure = "crate::tests::save_randomized_test_plan"
)]
async fn test_random_project_collaboration(
cx: &mut TestAppContext,
executor: BackgroundExecutor,
rng: StdRng,
) {
run_randomized_test::<ProjectCollaborationTest>(cx, executor, rng).await;
}
#[derive(Clone, Debug, Serialize, Deserialize)]
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,
is_local: bool,
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,
},
WriteFsEntry {
path: PathBuf,
is_dir: bool,
content: String,
},
GitOperation {
operation: GitOperation,
},
}
#[derive(Clone, Debug, Serialize, Deserialize)]
enum GitOperation {
WriteGitIndex {
repo_path: PathBuf,
contents: Vec<(PathBuf, String)>,
},
WriteGitBranch {
repo_path: PathBuf,
new_branch: Option<String>,
},
WriteGitStatuses {
repo_path: PathBuf,
statuses: Vec<(PathBuf, FileStatus)>,
},
}
#[derive(Clone, Debug, Serialize, Deserialize)]
enum LspRequestKind {
Rename,
Completion,
CodeAction,
Definition,
Highlights,
}
struct ProjectCollaborationTest;
#[async_trait(?Send)]
impl RandomizedTest for ProjectCollaborationTest {
type Operation = ClientOperation;
async fn initialize(server: &mut TestServer, users: &[UserTestPlan]) {
let db = &server.app_state.db;
for (ix, user_a) in users.iter().enumerate() {
for user_b in &users[ix + 1..] {
db.send_contact_request(user_a.user_id, user_b.user_id)
.await
.unwrap();
db.respond_to_contact_request(user_b.user_id, user_a.user_id, true)
.await
.unwrap();
}
}
}
fn generate_operation(
client: &TestClient,
rng: &mut StdRng,
plan: &mut UserTestPlan,
cx: &TestAppContext,
) -> ClientOperation {
let call = cx.read(ActiveCall::global);
loop {
match 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 rng.gen_bool(0.7) {
ClientOperation::AcceptIncomingCall
} else {
ClientOperation::RejectIncomingCall
};
}
match 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(rng).unwrap();
break ClientOperation::InviteContactToCall {
user_id: UserId(contact.user.id as i32),
};
}
}
// Leave the current call
71.. => {
if plan.allow_client_disconnection
&& call.read_with(cx, |call, _| call.room().is_some())
{
break ClientOperation::LeaveCall;
}
}
}
}
// Mutate projects
30..=59 => match 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_dev_server_project_ids = cx.read(|cx| {
client
.dev_server_projects()
.iter()
.map(|p| p.read(cx).remote_id().unwrap())
.collect::<Vec<_>>()
});
let new_dev_server_projects = room.read_with(cx, |room, _| {
room.remote_participants()
.values()
.flat_map(|participant| {
participant.projects.iter().filter_map(|project| {
if existing_dev_server_project_ids.contains(&project.id)
{
None
} else {
Some((
UserId::from_proto(participant.user.id),
project.worktree_root_names[0].clone(),
))
}
})
})
.collect::<Vec<_>>()
});
if !new_dev_server_projects.is_empty() {
let (host_id, first_root_name) =
new_dev_server_projects.choose(rng).unwrap().clone();
break ClientOperation::OpenRemoteProject {
host_id,
first_root_name,
};
}
}
// Open a local project
else {
let first_root_name = plan.next_root_dir_name();
break ClientOperation::OpenLocalProject { first_root_name };
}
}
// Close a remote project
71..=80 => {
if !client.dev_server_projects().is_empty() {
let project = client.dev_server_projects().choose(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 rng.gen_range(0..100_u32) {
// Add a worktree to a local project
0..=50 => {
let Some(project) = client.local_projects().choose(rng).cloned() else {
continue;
};
let project_root_name = root_name_for_project(&project, cx);
let mut paths = client.fs().paths(false);
paths.remove(0);
let new_root_path = if paths.is_empty() || rng.r#gen() {
Path::new(path!("/")).join(plan.next_root_dir_name())
} else {
paths.choose(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, 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, 0).any(|e| e.is_file())
&& worktree.root_entry().map_or(false, |e| e.is_dir())
})
.choose(rng)
});
let Some(worktree) = worktree else { continue };
let is_dir = rng.r#gen::<bool>();
let mut full_path =
worktree.read_with(cx, |w, _| PathBuf::from(w.root_name()));
full_path.push(gen_file_name(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..=90 => {
let Some(project) = choose_random_project(client, 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 rng.gen_range(0..100_u32) {
// Manipulate an existing buffer
0..=70 => {
let Some(buffer) = client
.buffers_for_project(&project)
.iter()
.choose(rng)
.cloned()
else {
continue;
};
let full_path = buffer
.read_with(cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
match 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 = 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(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(
rng.gen_range(0..=buffer.len()),
language::Bias::Left,
)
});
let detach = rng.r#gen();
break ClientOperation::RequestLspDataInBuffer {
project_root_name,
full_path,
offset,
is_local,
kind: match 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 = rng.gen_range('a'..='z').to_string();
let detach = rng.gen_bool(0.3);
break ClientOperation::SearchProject {
project_root_name,
is_local,
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, 0).any(|e| e.is_file())
})
.choose(rng)
});
let Some(worktree) = worktree else { continue };
let full_path = worktree.read_with(cx, |worktree, _| {
let entry = worktree
.entries(false, 0)
.filter(|e| e.is_file())
.choose(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,
};
}
}
}
// Update a git related action
91..=95 => {
break ClientOperation::GitOperation {
operation: generate_git_operation(rng, client),
};
}
// Create or update a file or directory
96.. => {
let is_dir = rng.r#gen::<bool>();
let content;
let mut path;
let dir_paths = client.fs().directories(false);
if is_dir {
content = String::new();
path = dir_paths.choose(rng).unwrap().clone();
path.push(gen_file_name(rng));
} else {
content = Alphanumeric.sample_string(rng, 16);
// Create a new file or overwrite an existing file
let file_paths = client.fs().files();
if file_paths.is_empty() || rng.gen_bool(0.5) {
path = dir_paths.choose(rng).unwrap().clone();
path.push(gen_file_name(rng));
path.set_extension("rs");
} else {
path = file_paths.choose(rng).unwrap().clone()
};
}
break ClientOperation::WriteFsEntry {
path,
is_dir,
content,
};
}
}
}
}
async fn apply_operation(
client: &TestClient,
operation: ClientOperation,
cx: &mut TestAppContext,
) -> Result<(), TestError> {
match operation {
ClientOperation::AcceptIncomingCall => {
let active_call = cx.read(ActiveCall::global);
if active_call.read_with(cx, |call, _| call.incoming().borrow().is_none()) {
Err(TestError::Inapplicable)?;
}
log::info!("{}: accepting incoming call", client.username);
active_call
.update(cx, |call, cx| call.accept_incoming(cx))
.await?;
}
ClientOperation::RejectIncomingCall => {
let active_call = cx.read(ActiveCall::global);
if active_call.read_with(cx, |call, _| call.incoming().borrow().is_none()) {
Err(TestError::Inapplicable)?;
}
log::info!("{}: declining incoming call", client.username);
active_call.update(cx, |call, cx| call.decline_incoming(cx))?;
}
ClientOperation::LeaveCall => {
let active_call = cx.read(ActiveCall::global);
if active_call.read_with(cx, |call, _| call.room().is_none()) {
Err(TestError::Inapplicable)?;
}
log::info!("{}: hanging up", client.username);
active_call.update(cx, |call, cx| call.hang_up(cx)).await?;
}
ClientOperation::InviteContactToCall { user_id } => {
let active_call = cx.read(ActiveCall::global);
log::info!("{}: inviting {}", client.username, user_id,);
active_call
.update(cx, |call, cx| call.invite(user_id.to_proto(), None, cx))
.await
.log_err();
}
ClientOperation::OpenLocalProject { first_root_name } => {
log::info!(
"{}: opening local project at {:?}",
client.username,
first_root_name
);
let root_path = Path::new(path!("/")).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());
}
ClientOperation::AddWorktreeToProject {
project_root_name,
new_root_path,
} => {
let project = project_for_root_name(client, &project_root_name, cx)
.ok_or(TestError::Inapplicable)?;
log::info!(
"{}: finding/creating local worktree at {:?} to project with root path {}",
client.username,
new_root_path,
project_root_name
);
ensure_project_shared(&project, client, cx).await;
if !client.fs().paths(false).contains(&new_root_path) {
client.fs().create_dir(&new_root_path).await.unwrap();
}
project
.update(cx, |project, cx| {
project.find_or_create_worktree(&new_root_path, true, cx)
})
.await
.unwrap();
}
ClientOperation::CloseRemoteProject { project_root_name } => {
let project = project_for_root_name(client, &project_root_name, cx)
.ok_or(TestError::Inapplicable)?;
log::info!(
"{}: closing remote project with root path {}",
client.username,
project_root_name,
);
let ix = client
.dev_server_projects()
.iter()
.position(|p| p == &project)
.unwrap();
cx.update(|_| {
client.dev_server_projects_mut().remove(ix);
client.buffers().retain(|p, _| *p != project);
drop(project);
});
}
ClientOperation::OpenRemoteProject {
host_id,
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_executor().clone()),
cx,
)
}))
})
.ok_or(TestError::Inapplicable)?;
log::info!(
"{}: joining remote project of user {}, root name {}",
client.username,
host_id,
first_root_name,
);
let project = project.await?;
client.dev_server_projects_mut().push(project.clone());
}
ClientOperation::CreateWorktreeEntry {
project_root_name,
is_local,
full_path,
is_dir,
} => {
let project = project_for_root_name(client, &project_root_name, cx)
.ok_or(TestError::Inapplicable)?;
let project_path = project_path_for_full_path(&project, &full_path, cx)
.ok_or(TestError::Inapplicable)?;
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,
);
ensure_project_shared(&project, client, cx).await;
project
.update(cx, |p, cx| p.create_entry(project_path, is_dir, cx))
.await?;
}
ClientOperation::OpenBuffer {
project_root_name,
is_local,
full_path,
} => {
let project = project_for_root_name(client, &project_root_name, cx)
.ok_or(TestError::Inapplicable)?;
let project_path = project_path_for_full_path(&project, &full_path, cx)
.ok_or(TestError::Inapplicable)?;
log::info!(
"{}: opening buffer {:?} in {} project {}",
client.username,
full_path,
if is_local { "local" } else { "remote" },
project_root_name,
);
ensure_project_shared(&project, client, cx).await;
let buffer = project
.update(cx, |project, cx| project.open_buffer(project_path, cx))
.await?;
client.buffers_for_project(&project).insert(buffer);
}
ClientOperation::EditBuffer {
project_root_name,
is_local,
full_path,
edits,
} => {
let project = project_for_root_name(client, &project_root_name, cx)
.ok_or(TestError::Inapplicable)?;
let buffer = buffer_for_full_path(client, &project, &full_path, cx)
.ok_or(TestError::Inapplicable)?;
log::info!(
"{}: editing buffer {:?} in {} project {} with {:?}",
client.username,
full_path,
if is_local { "local" } else { "remote" },
project_root_name,
edits
);
ensure_project_shared(&project, client, cx).await;
buffer.update(cx, |buffer, 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,
);
});
}
ClientOperation::CloseBuffer {
project_root_name,
is_local,
full_path,
} => {
let project = project_for_root_name(client, &project_root_name, cx)
.ok_or(TestError::Inapplicable)?;
let buffer = buffer_for_full_path(client, &project, &full_path, cx)
.ok_or(TestError::Inapplicable)?;
log::info!(
"{}: closing buffer {:?} in {} project {}",
client.username,
full_path,
if is_local { "local" } else { "remote" },
project_root_name
);
ensure_project_shared(&project, client, cx).await;
cx.update(|_| {
client.buffers_for_project(&project).remove(&buffer);
drop(buffer);
});
}
ClientOperation::SaveBuffer {
project_root_name,
is_local,
full_path,
detach,
} => {
let project = project_for_root_name(client, &project_root_name, cx)
.ok_or(TestError::Inapplicable)?;
let buffer = buffer_for_full_path(client, &project, &full_path, cx)
.ok_or(TestError::Inapplicable)?;
log::info!(
"{}: saving buffer {:?} in {} project {}, {}",
client.username,
full_path,
if is_local { "local" } else { "remote" },
project_root_name,
if detach { "detaching" } else { "awaiting" }
);
ensure_project_shared(&project, client, cx).await;
let requested_version = buffer.read_with(cx, |buffer, _| buffer.version());
let save =
project.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx));
let save = cx.spawn(|cx| async move {
save.await
.map_err(|err| anyhow!("save request failed: {:?}", err))?;
assert!(
buffer
.read_with(&cx, |buffer, _| { buffer.saved_version().to_owned() })
.expect("App should not be dropped")
.observed_all(&requested_version)
);
anyhow::Ok(())
});
if detach {
cx.update(|cx| save.detach_and_log_err(cx));
} else {
save.await?;
}
}
ClientOperation::RequestLspDataInBuffer {
project_root_name,
is_local,
full_path,
offset,
kind,
detach,
} => {
let project = project_for_root_name(client, &project_root_name, cx)
.ok_or(TestError::Inapplicable)?;
let buffer = buffer_for_full_path(client, &project, &full_path, cx)
.ok_or(TestError::Inapplicable)?;
log::info!(
"{}: request LSP {:?} for buffer {:?} in {} project {}, {}",
client.username,
kind,
full_path,
if is_local { "local" } else { "remote" },
project_root_name,
if detach { "detaching" } else { "awaiting" }
);
use futures::{FutureExt as _, TryFutureExt as _};
let offset = buffer.read_with(cx, |b, _| b.clip_offset(offset, Bias::Left));
let process_lsp_request = project.update(cx, |project, cx| match kind {
LspRequestKind::Rename => project
.prepare_rename(buffer, offset, cx)
.map_ok(|_| ())
.boxed(),
LspRequestKind::Completion => project
.completions(&buffer, offset, DEFAULT_COMPLETION_CONTEXT, cx)
.map_ok(|_| ())
.boxed(),
LspRequestKind::CodeAction => project
.code_actions(&buffer, offset..offset, None, cx)
.map(|_| Ok(()))
.boxed(),
LspRequestKind::Definition => project
.definition(&buffer, offset, cx)
.map_ok(|_| ())
.boxed(),
LspRequestKind::Highlights => project
.document_highlights(&buffer, offset, cx)
.map_ok(|_| ())
.boxed(),
});
let request = cx.foreground_executor().spawn(process_lsp_request);
if detach {
request.detach();
} else {
request.await?;
}
}
ClientOperation::SearchProject {
project_root_name,
is_local,
query,
detach,
} => {
let project = project_for_root_name(client, &project_root_name, cx)
.ok_or(TestError::Inapplicable)?;
log::info!(
"{}: search {} project {} for {:?}, {}",
client.username,
if is_local { "local" } else { "remote" },
project_root_name,
query,
if detach { "detaching" } else { "awaiting" }
);
let search = project.update(cx, |project, cx| {
project.search(
SearchQuery::text(
query,
false,
false,
false,
Default::default(),
Default::default(),
None,
)
.unwrap(),
cx,
)
});
drop(project);
let search = cx.executor().spawn(async move {
let mut results = HashMap::default();
while let Ok(result) = search.recv().await {
if let SearchResult::Buffer { buffer, ranges } = result {
results.entry(buffer).or_insert(ranges);
}
}
results
});
search.await;
}
ClientOperation::WriteFsEntry {
path,
is_dir,
content,
} => {
if !client
.fs()
.directories(false)
.contains(&path.parent().unwrap().to_owned())
{
return Err(TestError::Inapplicable);
}
if is_dir {
log::info!("{}: creating dir at {:?}", client.username, path);
client.fs().create_dir(&path).await.unwrap();
} else {
let exists = client.fs().metadata(&path).await?.is_some();
let verb = if exists { "updating" } else { "creating" };
log::info!("{}: {} file at {:?}", verb, client.username, path);
client
.fs()
.save(&path, &content.as_str().into(), text::LineEnding::Unix)
.await
.unwrap();
}
}
ClientOperation::GitOperation { operation } => match operation {
GitOperation::WriteGitIndex {
repo_path,
contents,
} => {
if !client.fs().directories(false).contains(&repo_path) {
return Err(TestError::Inapplicable);
}
for (path, _) in contents.iter() {
if !client.fs().files().contains(&repo_path.join(path)) {
return Err(TestError::Inapplicable);
}
}
log::info!(
"{}: writing git index for repo {:?}: {:?}",
client.username,
repo_path,
contents
);
let dot_git_dir = repo_path.join(".git");
let contents = contents
.into_iter()
.map(|(path, contents)| (path.into(), contents))
.collect::<Vec<_>>();
if client.fs().metadata(&dot_git_dir).await?.is_none() {
client.fs().create_dir(&dot_git_dir).await?;
}
client.fs().set_index_for_repo(&dot_git_dir, &contents);
}
GitOperation::WriteGitBranch {
repo_path,
new_branch,
} => {
if !client.fs().directories(false).contains(&repo_path) {
return Err(TestError::Inapplicable);
}
log::info!(
"{}: writing git branch for repo {:?}: {:?}",
client.username,
repo_path,
new_branch
);
let dot_git_dir = repo_path.join(".git");
if client.fs().metadata(&dot_git_dir).await?.is_none() {
client.fs().create_dir(&dot_git_dir).await?;
}
client
.fs()
.set_branch_name(&dot_git_dir, new_branch.clone());
}
GitOperation::WriteGitStatuses {
repo_path,
statuses,
} => {
if !client.fs().directories(false).contains(&repo_path) {
return Err(TestError::Inapplicable);
}
for (path, _) in statuses.iter() {
if !client.fs().files().contains(&repo_path.join(path)) {
return Err(TestError::Inapplicable);
}
}
log::info!(
"{}: writing git statuses for repo {:?}: {:?}",
client.username,
repo_path,
statuses
);
let dot_git_dir = repo_path.join(".git");
let statuses = statuses
.iter()
.map(|(path, val)| (path.as_path(), *val))
.collect::<Vec<_>>();
if client.fs().metadata(&dot_git_dir).await?.is_none() {
client.fs().create_dir(&dot_git_dir).await?;
}
client
.fs()
.set_status_for_repo(&dot_git_dir, statuses.as_slice());
}
},
}
Ok(())
}
async fn on_client_added(client: &Rc<TestClient>, _: &mut TestAppContext) {
client.language_registry().add(Arc::new(Language::new(
LanguageConfig {
name: "Rust".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
..Default::default()
},
None,
)));
client.language_registry().register_fake_lsp(
"Rust",
FakeLspAdapter {
name: "the-fake-language-server",
capabilities: lsp::LanguageServer::full_capabilities(),
initializer: Some(Box::new({
let fs = client.app_state.fs.clone();
move |fake_server: &mut FakeLanguageServer| {
fake_server.set_request_handler::<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.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
|_, _| async move {
Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
lsp::CodeAction {
title: "the-code-action".to_string(),
..Default::default()
},
)]))
},
);
fake_server
.set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
|params, _| async move {
Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
params.position,
params.position,
))))
},
);
fake_server.set_request_handler::<lsp::request::GotoDefinition, _, _>({
let fs = fs.clone();
move |_, cx| {
let background = cx.background_executor();
let mut rng = background.rng();
let count = rng.gen_range::<usize, _>(1..3);
let files = fs.as_fake().files();
let files = (0..count)
.map(|_| files.choose(&mut rng).unwrap().clone())
.collect::<Vec<_>>();
async move {
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
.set_request_handler::<lsp::request::DocumentHighlightRequest, _, _>(
move |_, cx| {
let mut highlights = Vec::new();
let background = cx.background_executor();
let mut rng = background.rng();
let highlight_count = rng.gen_range(1..=5);
for _ in 0..highlight_count {
let start_row = rng.gen_range(0..100);
let start_column = rng.gen_range(0..100);
let end_row = rng.gen_range(0..100);
let end_column = rng.gen_range(0..100);
let start = PointUtf16::new(start_row, start_column);
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()).unwrap(),
kind: Some(lsp::DocumentHighlightKind::READ),
});
}
highlights.sort_unstable_by_key(|highlight| {
(highlight.range.start, highlight.range.end)
});
async move { Ok(Some(highlights)) }
},
);
}
})),
..Default::default()
},
);
}
async fn on_quiesce(_: &mut TestServer, clients: &mut [(Rc<TestClient>, TestAppContext)]) {
for (client, client_cx) in clients.iter() {
for guest_project in client.dev_server_projects().iter() {
guest_project.read_with(client_cx, |guest_project, cx| {
let host_project = clients.iter().find_map(|(client, cx)| {
let project = client
.local_projects()
.iter()
.find(|host_project| {
host_project.read_with(cx, |host_project, _| {
host_project.remote_id() == guest_project.remote_id()
})
})?
.clone();
Some((project, cx))
});
if !guest_project.is_disconnected(cx) {
if let Some((host_project, host_cx)) = host_project {
let host_worktree_snapshots =
host_project.read_with(host_cx, |host_project, cx| {
host_project
.worktrees(cx)
.map(|worktree| {
let worktree = worktree.read(cx);
(worktree.id(), worktree.snapshot())
})
.collect::<BTreeMap<_, _>>()
});
let guest_worktree_snapshots = guest_project
.worktrees(cx)
.map(|worktree| {
let worktree = worktree.read(cx);
(worktree.id(), worktree.snapshot())
})
.collect::<BTreeMap<_, _>>();
let host_repository_snapshots = host_project.read_with(host_cx, |host_project, cx| {
host_project.git_store().read(cx).repo_snapshots(cx)
});
let guest_repository_snapshots = guest_project.git_store().read(cx).repo_snapshots(cx);
assert_eq!(
guest_worktree_snapshots.values().map(|w| w.abs_path()).collect::<Vec<_>>(),
host_worktree_snapshots.values().map(|w| w.abs_path()).collect::<Vec<_>>(),
"{} has different worktrees than the host for project {:?}",
client.username, guest_project.remote_id(),
);
assert_eq!(
guest_repository_snapshots.values().collect::<Vec<_>>(),
host_repository_snapshots.values().collect::<Vec<_>>(),
"{} has different repositories than the host for project {:?}",
client.username, guest_project.remote_id(),
);
for (id, host_snapshot) in &host_worktree_snapshots {
let guest_snapshot = &guest_worktree_snapshots[id];
assert_eq!(
guest_snapshot.root_name(),
host_snapshot.root_name(),
"{} has different root name than the host for worktree {}, project {:?}",
client.username,
id,
guest_project.remote_id(),
);
assert_eq!(
guest_snapshot.abs_path(),
host_snapshot.abs_path(),
"{} has different abs path than the host for worktree {}, project: {:?}",
client.username,
id,
guest_project.remote_id(),
);
assert_eq!(
guest_snapshot.entries(false, 0).collect::<Vec<_>>(),
host_snapshot.entries(false, 0).collect::<Vec<_>>(),
"{} has different snapshot than the host for worktree {:?} ({:?}) and project {:?}",
client.username,
host_snapshot.abs_path(),
id,
guest_project.remote_id(),
);
assert_eq!(guest_snapshot.scan_id(), host_snapshot.scan_id(),
"{} has different scan id than the host for worktree {:?} and project {:?}",
client.username,
host_snapshot.abs_path(),
guest_project.remote_id(),
);
}
}
}
for buffer in guest_project.opened_buffers(cx) {
let buffer = buffer.read(cx);
assert_eq!(
buffer.deferred_ops_len(),
0,
"{} has deferred operations for buffer {:?} in project {:?}",
client.username,
buffer.file().unwrap().full_path(cx),
guest_project.remote_id(),
);
}
});
}
let buffers = client.buffers().clone();
for (guest_project, guest_buffers) in &buffers {
let project_id = if guest_project.read_with(client_cx, |project, cx| {
project.is_local() || project.is_disconnected(cx)
}) {
continue;
} else {
guest_project
.read_with(client_cx, |project, _| project.remote_id())
.unwrap()
};
let guest_user_id = client.user_id().unwrap();
let host_project = clients.iter().find_map(|(client, cx)| {
let project = client
.local_projects()
.iter()
.find(|host_project| {
host_project.read_with(cx, |host_project, _| {
host_project.remote_id() == Some(project_id)
})
})?
.clone();
Some((client.user_id().unwrap(), project, cx))
});
let (host_user_id, host_project, host_cx) =
if let Some((host_user_id, host_project, host_cx)) = host_project {
(host_user_id, host_project, host_cx)
} else {
continue;
};
for guest_buffer in guest_buffers {
let buffer_id =
guest_buffer.read_with(client_cx, |buffer, _| buffer.remote_id());
let host_buffer = host_project.read_with(host_cx, |project, cx| {
project.buffer_for_id(buffer_id, cx).unwrap_or_else(|| {
panic!(
"host does not have buffer for guest:{}, peer:{:?}, id:{}",
client.username,
client.peer_id(),
buffer_id
)
})
});
let path = host_buffer
.read_with(host_cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
assert_eq!(
guest_buffer.read_with(client_cx, |buffer, _| buffer.deferred_ops_len()),
0,
"{}, buffer {}, path {:?} has deferred operations",
client.username,
buffer_id,
path,
);
assert_eq!(
guest_buffer.read_with(client_cx, |buffer, _| buffer.text()),
host_buffer.read_with(host_cx, |buffer, _| buffer.text()),
"{}, buffer {}, path {:?}, differs from the host's buffer",
client.username,
buffer_id,
path
);
let host_file = host_buffer.read_with(host_cx, |b, _| b.file().cloned());
let guest_file = guest_buffer.read_with(client_cx, |b, _| b.file().cloned());
match (host_file, guest_file) {
(Some(host_file), Some(guest_file)) => {
assert_eq!(guest_file.path(), host_file.path());
assert_eq!(
guest_file.disk_state(),
host_file.disk_state(),
"guest {} disk_state does not match host {} for path {:?} in project {}",
guest_user_id,
host_user_id,
guest_file.path(),
project_id,
);
}
(None, None) => {}
(None, _) => panic!("host's file is None, guest's isn't"),
(_, None) => panic!("guest's file is None, hosts's isn't"),
}
let host_diff_base = host_project.read_with(host_cx, |project, cx| {
project
.git_store()
.read(cx)
.get_unstaged_diff(host_buffer.read(cx).remote_id(), cx)
.unwrap()
.read(cx)
.base_text_string()
});
let guest_diff_base = guest_project.read_with(client_cx, |project, cx| {
project
.git_store()
.read(cx)
.get_unstaged_diff(guest_buffer.read(cx).remote_id(), cx)
.unwrap()
.read(cx)
.base_text_string()
});
assert_eq!(
guest_diff_base, host_diff_base,
"guest {} diff base does not match host's for path {path:?} in project {project_id}",
client.username
);
let host_saved_version =
host_buffer.read_with(host_cx, |b, _| b.saved_version().clone());
let guest_saved_version =
guest_buffer.read_with(client_cx, |b, _| b.saved_version().clone());
assert_eq!(
guest_saved_version, host_saved_version,
"guest {} saved version does not match host's for path {path:?} in project {project_id}",
client.username
);
let host_is_dirty = host_buffer.read_with(host_cx, |b, _| b.is_dirty());
let guest_is_dirty = guest_buffer.read_with(client_cx, |b, _| b.is_dirty());
assert_eq!(
guest_is_dirty, host_is_dirty,
"guest {} dirty state does not match host's for path {path:?} in project {project_id}",
client.username
);
let host_saved_mtime = host_buffer.read_with(host_cx, |b, _| b.saved_mtime());
let guest_saved_mtime =
guest_buffer.read_with(client_cx, |b, _| b.saved_mtime());
assert_eq!(
guest_saved_mtime, host_saved_mtime,
"guest {} saved mtime does not match host's for path {path:?} in project {project_id}",
client.username
);
let host_is_dirty = host_buffer.read_with(host_cx, |b, _| b.is_dirty());
let guest_is_dirty = guest_buffer.read_with(client_cx, |b, _| b.is_dirty());
assert_eq!(
guest_is_dirty, host_is_dirty,
"guest {} dirty status does not match host's for path {path:?} in project {project_id}",
client.username
);
let host_has_conflict = host_buffer.read_with(host_cx, |b, _| b.has_conflict());
let guest_has_conflict =
guest_buffer.read_with(client_cx, |b, _| b.has_conflict());
assert_eq!(
guest_has_conflict, host_has_conflict,
"guest {} conflict status does not match host's for path {path:?} in project {project_id}",
client.username
);
}
}
}
}
}
fn generate_git_operation(rng: &mut StdRng, client: &TestClient) -> GitOperation {
fn generate_file_paths(
repo_path: &Path,
rng: &mut StdRng,
client: &TestClient,
) -> Vec<PathBuf> {
let mut paths = client
.fs()
.files()
.into_iter()
.filter(|path| path.starts_with(repo_path))
.collect::<Vec<_>>();
let count = rng.gen_range(0..=paths.len());
paths.shuffle(rng);
paths.truncate(count);
paths
.iter()
.map(|path| path.strip_prefix(repo_path).unwrap().to_path_buf())
.collect::<Vec<_>>()
}
let repo_path = client.fs().directories(false).choose(rng).unwrap().clone();
match rng.gen_range(0..100_u32) {
0..=25 => {
let file_paths = generate_file_paths(&repo_path, rng, client);
let contents = file_paths
.into_iter()
.map(|path| (path, Alphanumeric.sample_string(rng, 16)))
.collect();
GitOperation::WriteGitIndex {
repo_path,
contents,
}
}
26..=63 => {
let new_branch = (rng.gen_range(0..10) > 3).then(|| Alphanumeric.sample_string(rng, 8));
GitOperation::WriteGitBranch {
repo_path,
new_branch,
}
}
64..=100 => {
let file_paths = generate_file_paths(&repo_path, rng, client);
let statuses = file_paths
.into_iter()
.map(|path| (path, gen_status(rng)))
.collect::<Vec<_>>();
GitOperation::WriteGitStatuses {
repo_path,
statuses,
}
}
_ => unreachable!(),
}
}
fn buffer_for_full_path(
client: &TestClient,
project: &Entity<Project>,
full_path: &PathBuf,
cx: &TestAppContext,
) -> Option<Entity<language::Buffer>> {
client
.buffers_for_project(project)
.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,
cx: &TestAppContext,
) -> Option<Entity<Project>> {
if let Some(ix) = project_ix_for_root_name(client.local_projects().deref(), root_name, cx) {
return Some(client.local_projects()[ix].clone());
}
if let Some(ix) = project_ix_for_root_name(client.dev_server_projects().deref(), root_name, cx)
{
return Some(client.dev_server_projects()[ix].clone());
}
None
}
fn project_ix_for_root_name(
projects: &[Entity<Project>],
root_name: &str,
cx: &TestAppContext,
) -> Option<usize> {
projects.iter().position(|project| {
project.read_with(cx, |project, cx| {
let worktree = project.visible_worktrees(cx).next().unwrap();
worktree.read(cx).root_name() == root_name
})
})
}
fn root_name_for_project(project: &Entity<Project>, cx: &TestAppContext) -> String {
project.read_with(cx, |project, cx| {
project
.visible_worktrees(cx)
.next()
.unwrap()
.read(cx)
.root_name()
.to_string()
})
}
fn project_path_for_full_path(
project: &Entity<Project>,
full_path: &Path,
cx: &TestAppContext,
) -> Option<ProjectPath> {
let mut components = full_path.components();
let root_name = components.next().unwrap().as_os_str().to_str().unwrap();
let path = components.as_path().into();
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
}
})
})?;
Some(ProjectPath { worktree_id, path })
}
async fn ensure_project_shared(
project: &Entity<Project>,
client: &TestClient,
cx: &mut TestAppContext,
) {
let first_root_name = root_name_for_project(project, cx);
let active_call = cx.read(ActiveCall::global);
if active_call.read_with(cx, |call, _| call.room().is_some())
&& project.read_with(cx, |project, _| project.is_local() && !project.is_shared())
{
match active_call
.update(cx, |call, cx| call.share_project(project.clone(), cx))
.await
{
Ok(project_id) => {
log::info!(
"{}: shared project {} with id {}",
client.username,
first_root_name,
project_id
);
}
Err(error) => {
log::error!(
"{}: error sharing project {}: {:?}",
client.username,
first_root_name,
error
);
}
}
}
}
fn choose_random_project(client: &TestClient, rng: &mut StdRng) -> Option<Entity<Project>> {
client
.local_projects()
.deref()
.iter()
.chain(client.dev_server_projects().iter())
.choose(rng)
.cloned()
}
fn gen_file_name(rng: &mut StdRng) -> String {
let mut name = String::new();
for _ in 0..10 {
let letter = rng.gen_range('a'..='z');
name.push(letter);
}
name
}
fn gen_status(rng: &mut StdRng) -> FileStatus {
fn gen_tracked_status(rng: &mut StdRng) -> TrackedStatus {
match rng.gen_range(0..3) {
0 => TrackedStatus {
index_status: StatusCode::Unmodified,
worktree_status: StatusCode::Unmodified,
},
1 => TrackedStatus {
index_status: StatusCode::Modified,
worktree_status: StatusCode::Modified,
},
2 => TrackedStatus {
index_status: StatusCode::Added,
worktree_status: StatusCode::Modified,
},
3 => TrackedStatus {
index_status: StatusCode::Added,
worktree_status: StatusCode::Unmodified,
},
_ => unreachable!(),
}
}
fn gen_unmerged_status_code(rng: &mut StdRng) -> UnmergedStatusCode {
match rng.gen_range(0..3) {
0 => UnmergedStatusCode::Updated,
1 => UnmergedStatusCode::Added,
2 => UnmergedStatusCode::Deleted,
_ => unreachable!(),
}
}
match rng.gen_range(0..2) {
0 => FileStatus::Unmerged(UnmergedStatus {
first_head: gen_unmerged_status_code(rng),
second_head: gen_unmerged_status_code(rng),
}),
1 => FileStatus::Tracked(gen_tracked_status(rng)),
_ => unreachable!(),
}
}