use super::{RandomizedTest, TestClient, TestError, TestServer, UserTestPlan};use crate::{db::UserId, tests::run_randomized_test};use anyhow::{anyhow, Result};use async_trait::async_trait;use call::ActiveCall;use collections::{BTreeMap, HashMap};use editor::Bias;use fs::{FakeFs, Fs as _};use futures::StreamExt;use git::repository::GitFileStatus;use gpui::{BackgroundExecutor, Model, TestAppContext};use language::{ range_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, PointUtf16,};use lsp::FakeLanguageServer;use pretty_assertions::assert_eq;use project::{ search::SearchQuery, search::SearchResult, Project, ProjectPath, DEFAULT_COMPLETION_CONTEXT,};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; #[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, GitFileStatus)>, git_operation: bool, },} #[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.gen() { Path::new("/").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.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.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.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,Lines 1–500 of 1616. Load more
use super::{RandomizedTest, TestClient, TestError, TestServer, UserTestPlan}; use crate::{db::UserId, tests::run_randomized_test}; use anyhow::{anyhow, Result}; use async_trait::async_trait; use call::ActiveCall; use collections::{BTreeMap, HashMap}; use editor::Bias; use fs::{FakeFs, Fs as _}; use futures::StreamExt; use git::repository::GitFileStatus; use gpui::{BackgroundExecutor, Model, TestAppContext}; use language::{ range_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, PointUtf16, }; use lsp::FakeLanguageServer; use pretty_assertions::assert_eq; use project::{ search::SearchQuery, search::SearchResult, Project, ProjectPath, DEFAULT_COMPLETION_CONTEXT, }; 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; #[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, GitFileStatus)>, git_operation: bool, }, } #[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.gen() { Path::new("/").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.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.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.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,