1use super::{RandomizedTest, TestClient, TestError, TestServer, UserTestPlan};
2use crate::{db::UserId, tests::run_randomized_test};
3use anyhow::{Context as _, Result};
4use async_trait::async_trait;
5use call::ActiveCall;
6use collections::{BTreeMap, HashMap};
7use editor::Bias;
8use encoding_rs::UTF_8;
9use fs::{FakeFs, Fs as _, encodings::EncodingWrapper};
10use git::status::{FileStatus, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode};
11use gpui::{BackgroundExecutor, Entity, TestAppContext};
12use language::{
13 FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, PointUtf16, range_to_lsp,
14};
15use lsp::FakeLanguageServer;
16use pretty_assertions::assert_eq;
17use project::{
18 DEFAULT_COMPLETION_CONTEXT, Project, ProjectPath, search::SearchQuery, search::SearchResult,
19};
20use rand::{
21 distr::{self, SampleString},
22 prelude::*,
23};
24use serde::{Deserialize, Serialize};
25use std::{
26 ops::{Deref, Range},
27 path::{Path, PathBuf},
28 rc::Rc,
29 sync::Arc,
30};
31use text::Rope;
32use util::{
33 ResultExt, path,
34 paths::PathStyle,
35 rel_path::{RelPath, RelPathBuf, rel_path},
36};
37
38#[gpui::test(
39 iterations = 100,
40 on_failure = "crate::tests::save_randomized_test_plan"
41)]
42async fn test_random_project_collaboration(
43 cx: &mut TestAppContext,
44 executor: BackgroundExecutor,
45 rng: StdRng,
46) {
47 run_randomized_test::<ProjectCollaborationTest>(cx, executor, rng).await;
48}
49
50#[derive(Clone, Debug, Serialize, Deserialize)]
51enum ClientOperation {
52 AcceptIncomingCall,
53 RejectIncomingCall,
54 LeaveCall,
55 InviteContactToCall {
56 user_id: UserId,
57 },
58 OpenLocalProject {
59 first_root_name: String,
60 },
61 OpenRemoteProject {
62 host_id: UserId,
63 first_root_name: String,
64 },
65 AddWorktreeToProject {
66 project_root_name: String,
67 new_root_path: PathBuf,
68 },
69 CloseRemoteProject {
70 project_root_name: String,
71 },
72 OpenBuffer {
73 project_root_name: String,
74 is_local: bool,
75 full_path: RelPathBuf,
76 },
77 SearchProject {
78 project_root_name: String,
79 is_local: bool,
80 query: String,
81 detach: bool,
82 },
83 EditBuffer {
84 project_root_name: String,
85 is_local: bool,
86 full_path: RelPathBuf,
87 edits: Vec<(Range<usize>, Arc<str>)>,
88 },
89 CloseBuffer {
90 project_root_name: String,
91 is_local: bool,
92 full_path: RelPathBuf,
93 },
94 SaveBuffer {
95 project_root_name: String,
96 is_local: bool,
97 full_path: RelPathBuf,
98 detach: bool,
99 },
100 RequestLspDataInBuffer {
101 project_root_name: String,
102 is_local: bool,
103 full_path: RelPathBuf,
104 offset: usize,
105 kind: LspRequestKind,
106 detach: bool,
107 },
108 CreateWorktreeEntry {
109 project_root_name: String,
110 is_local: bool,
111 full_path: RelPathBuf,
112 is_dir: bool,
113 },
114 WriteFsEntry {
115 path: PathBuf,
116 is_dir: bool,
117 content: String,
118 },
119 GitOperation {
120 operation: GitOperation,
121 },
122}
123
124#[derive(Clone, Debug, Serialize, Deserialize)]
125enum GitOperation {
126 WriteGitIndex {
127 repo_path: PathBuf,
128 contents: Vec<(RelPathBuf, String)>,
129 },
130 WriteGitBranch {
131 repo_path: PathBuf,
132 new_branch: Option<String>,
133 },
134 WriteGitStatuses {
135 repo_path: PathBuf,
136 statuses: Vec<(RelPathBuf, FileStatus)>,
137 },
138}
139
140#[derive(Clone, Debug, Serialize, Deserialize)]
141enum LspRequestKind {
142 Rename,
143 Completion,
144 CodeAction,
145 Definition,
146 Highlights,
147}
148
149struct ProjectCollaborationTest;
150
151#[async_trait(?Send)]
152impl RandomizedTest for ProjectCollaborationTest {
153 type Operation = ClientOperation;
154
155 async fn initialize(server: &mut TestServer, users: &[UserTestPlan]) {
156 let db = &server.app_state.db;
157 for (ix, user_a) in users.iter().enumerate() {
158 for user_b in &users[ix + 1..] {
159 db.send_contact_request(user_a.user_id, user_b.user_id)
160 .await
161 .unwrap();
162 db.respond_to_contact_request(user_b.user_id, user_a.user_id, true)
163 .await
164 .unwrap();
165 }
166 }
167 }
168
169 fn generate_operation(
170 client: &TestClient,
171 rng: &mut StdRng,
172 plan: &mut UserTestPlan,
173 cx: &TestAppContext,
174 ) -> ClientOperation {
175 let call = cx.read(ActiveCall::global);
176 loop {
177 match rng.random_range(0..100_u32) {
178 // Mutate the call
179 0..=29 => {
180 // Respond to an incoming call
181 if call.read_with(cx, |call, _| call.incoming().borrow().is_some()) {
182 break if rng.random_bool(0.7) {
183 ClientOperation::AcceptIncomingCall
184 } else {
185 ClientOperation::RejectIncomingCall
186 };
187 }
188
189 match rng.random_range(0..100_u32) {
190 // Invite a contact to the current call
191 0..=70 => {
192 let available_contacts =
193 client.user_store().read_with(cx, |user_store, _| {
194 user_store
195 .contacts()
196 .iter()
197 .filter(|contact| contact.online && !contact.busy)
198 .cloned()
199 .collect::<Vec<_>>()
200 });
201 if !available_contacts.is_empty() {
202 let contact = available_contacts.choose(rng).unwrap();
203 break ClientOperation::InviteContactToCall {
204 user_id: UserId(contact.user.id as i32),
205 };
206 }
207 }
208
209 // Leave the current call
210 71.. => {
211 if plan.allow_client_disconnection
212 && call.read_with(cx, |call, _| call.room().is_some())
213 {
214 break ClientOperation::LeaveCall;
215 }
216 }
217 }
218 }
219
220 // Mutate projects
221 30..=59 => match rng.random_range(0..100_u32) {
222 // Open a new project
223 0..=70 => {
224 // Open a remote project
225 if let Some(room) = call.read_with(cx, |call, _| call.room().cloned()) {
226 let existing_dev_server_project_ids = cx.read(|cx| {
227 client
228 .dev_server_projects()
229 .iter()
230 .map(|p| p.read(cx).remote_id().unwrap())
231 .collect::<Vec<_>>()
232 });
233 let new_dev_server_projects = room.read_with(cx, |room, _| {
234 room.remote_participants()
235 .values()
236 .flat_map(|participant| {
237 participant.projects.iter().filter_map(|project| {
238 if existing_dev_server_project_ids.contains(&project.id)
239 {
240 None
241 } else {
242 Some((
243 UserId::from_proto(participant.user.id),
244 project.worktree_root_names[0].clone(),
245 ))
246 }
247 })
248 })
249 .collect::<Vec<_>>()
250 });
251 if !new_dev_server_projects.is_empty() {
252 let (host_id, first_root_name) =
253 new_dev_server_projects.choose(rng).unwrap().clone();
254 break ClientOperation::OpenRemoteProject {
255 host_id,
256 first_root_name,
257 };
258 }
259 }
260 // Open a local project
261 else {
262 let first_root_name = plan.next_root_dir_name();
263 break ClientOperation::OpenLocalProject { first_root_name };
264 }
265 }
266
267 // Close a remote project
268 71..=80 => {
269 if !client.dev_server_projects().is_empty() {
270 let project = client.dev_server_projects().choose(rng).unwrap().clone();
271 let first_root_name = root_name_for_project(&project, cx);
272 break ClientOperation::CloseRemoteProject {
273 project_root_name: first_root_name,
274 };
275 }
276 }
277
278 // Mutate project worktrees
279 81.. => match rng.random_range(0..100_u32) {
280 // Add a worktree to a local project
281 0..=50 => {
282 let Some(project) = client.local_projects().choose(rng).cloned() else {
283 continue;
284 };
285 let project_root_name = root_name_for_project(&project, cx);
286 let mut paths = client.fs().paths(false);
287 paths.remove(0);
288 let new_root_path = if paths.is_empty() || rng.random() {
289 Path::new(path!("/")).join(plan.next_root_dir_name())
290 } else {
291 paths.choose(rng).unwrap().clone()
292 };
293 break ClientOperation::AddWorktreeToProject {
294 project_root_name,
295 new_root_path,
296 };
297 }
298
299 // Add an entry to a worktree
300 _ => {
301 let Some(project) = choose_random_project(client, rng) else {
302 continue;
303 };
304 let project_root_name = root_name_for_project(&project, cx);
305 let is_local = project.read_with(cx, |project, _| project.is_local());
306 let worktree = project.read_with(cx, |project, cx| {
307 project
308 .worktrees(cx)
309 .filter(|worktree| {
310 let worktree = worktree.read(cx);
311 worktree.is_visible()
312 && worktree.entries(false, 0).any(|e| e.is_file())
313 && worktree.root_entry().is_some_and(|e| e.is_dir())
314 })
315 .choose(rng)
316 });
317 let Some(worktree) = worktree else { continue };
318 let is_dir = rng.random::<bool>();
319 let mut full_path =
320 worktree.read_with(cx, |w, _| w.root_name().to_rel_path_buf());
321 full_path.push(rel_path(&gen_file_name(rng)));
322 if !is_dir {
323 full_path.set_extension("rs");
324 }
325 break ClientOperation::CreateWorktreeEntry {
326 project_root_name,
327 is_local,
328 full_path,
329 is_dir,
330 };
331 }
332 },
333 },
334
335 // Query and mutate buffers
336 60..=90 => {
337 let Some(project) = choose_random_project(client, rng) else {
338 continue;
339 };
340 let project_root_name = root_name_for_project(&project, cx);
341 let is_local = project.read_with(cx, |project, _| project.is_local());
342
343 match rng.random_range(0..100_u32) {
344 // Manipulate an existing buffer
345 0..=70 => {
346 let Some(buffer) = client
347 .buffers_for_project(&project)
348 .iter()
349 .choose(rng)
350 .cloned()
351 else {
352 continue;
353 };
354
355 let full_path = buffer.read_with(cx, |buffer, cx| {
356 let file = buffer.file().unwrap();
357 let worktree = project
358 .read(cx)
359 .worktree_for_id(file.worktree_id(cx), cx)
360 .unwrap();
361 worktree
362 .read(cx)
363 .root_name()
364 .join(file.path())
365 .to_rel_path_buf()
366 });
367
368 match rng.random_range(0..100_u32) {
369 // Close the buffer
370 0..=15 => {
371 break ClientOperation::CloseBuffer {
372 project_root_name,
373 is_local,
374 full_path,
375 };
376 }
377 // Save the buffer
378 16..=29 if buffer.read_with(cx, |b, _| b.is_dirty()) => {
379 let detach = rng.random_bool(0.3);
380 break ClientOperation::SaveBuffer {
381 project_root_name,
382 is_local,
383 full_path,
384 detach,
385 };
386 }
387 // Edit the buffer
388 30..=69 => {
389 let edits = buffer
390 .read_with(cx, |buffer, _| buffer.get_random_edits(rng, 3));
391 break ClientOperation::EditBuffer {
392 project_root_name,
393 is_local,
394 full_path,
395 edits,
396 };
397 }
398 // Make an LSP request
399 _ => {
400 let offset = buffer.read_with(cx, |buffer, _| {
401 buffer.clip_offset(
402 rng.random_range(0..=buffer.len()),
403 language::Bias::Left,
404 )
405 });
406 let detach = rng.random();
407 break ClientOperation::RequestLspDataInBuffer {
408 project_root_name,
409 full_path,
410 offset,
411 is_local,
412 kind: match rng.random_range(0..5_u32) {
413 0 => LspRequestKind::Rename,
414 1 => LspRequestKind::Highlights,
415 2 => LspRequestKind::Definition,
416 3 => LspRequestKind::CodeAction,
417 4.. => LspRequestKind::Completion,
418 },
419 detach,
420 };
421 }
422 }
423 }
424
425 71..=80 => {
426 let query = rng.random_range('a'..='z').to_string();
427 let detach = rng.random_bool(0.3);
428 break ClientOperation::SearchProject {
429 project_root_name,
430 is_local,
431 query,
432 detach,
433 };
434 }
435
436 // Open a buffer
437 81.. => {
438 let worktree = project.read_with(cx, |project, cx| {
439 project
440 .worktrees(cx)
441 .filter(|worktree| {
442 let worktree = worktree.read(cx);
443 worktree.is_visible()
444 && worktree.entries(false, 0).any(|e| e.is_file())
445 })
446 .choose(rng)
447 });
448 let Some(worktree) = worktree else { continue };
449 let full_path = worktree.read_with(cx, |worktree, _| {
450 let entry = worktree
451 .entries(false, 0)
452 .filter(|e| e.is_file())
453 .choose(rng)
454 .unwrap();
455 if entry.path.as_ref().is_empty() {
456 worktree.root_name().into()
457 } else {
458 worktree.root_name().join(&entry.path)
459 }
460 });
461 break ClientOperation::OpenBuffer {
462 project_root_name,
463 is_local,
464 full_path: full_path.to_rel_path_buf(),
465 };
466 }
467 }
468 }
469
470 // Update a git related action
471 91..=95 => {
472 break ClientOperation::GitOperation {
473 operation: generate_git_operation(rng, client),
474 };
475 }
476
477 // Create or update a file or directory
478 96.. => {
479 let is_dir = rng.random::<bool>();
480 let content;
481 let mut path;
482 let dir_paths = client.fs().directories(false);
483
484 if is_dir {
485 content = String::new();
486 path = dir_paths.choose(rng).unwrap().clone();
487 path.push(gen_file_name(rng));
488 } else {
489 content = distr::Alphanumeric.sample_string(rng, 16);
490
491 // Create a new file or overwrite an existing file
492 let file_paths = client.fs().files();
493 if file_paths.is_empty() || rng.random_bool(0.5) {
494 path = dir_paths.choose(rng).unwrap().clone();
495 path.push(gen_file_name(rng));
496 path.set_extension("rs");
497 } else {
498 path = file_paths.choose(rng).unwrap().clone()
499 };
500 }
501 break ClientOperation::WriteFsEntry {
502 path,
503 is_dir,
504 content,
505 };
506 }
507 }
508 }
509 }
510
511 async fn apply_operation(
512 client: &TestClient,
513 operation: ClientOperation,
514 cx: &mut TestAppContext,
515 ) -> Result<(), TestError> {
516 match operation {
517 ClientOperation::AcceptIncomingCall => {
518 let active_call = cx.read(ActiveCall::global);
519 if active_call.read_with(cx, |call, _| call.incoming().borrow().is_none()) {
520 Err(TestError::Inapplicable)?;
521 }
522
523 log::info!("{}: accepting incoming call", client.username);
524 active_call
525 .update(cx, |call, cx| call.accept_incoming(cx))
526 .await?;
527 }
528
529 ClientOperation::RejectIncomingCall => {
530 let active_call = cx.read(ActiveCall::global);
531 if active_call.read_with(cx, |call, _| call.incoming().borrow().is_none()) {
532 Err(TestError::Inapplicable)?;
533 }
534
535 log::info!("{}: declining incoming call", client.username);
536 active_call.update(cx, |call, cx| call.decline_incoming(cx))?;
537 }
538
539 ClientOperation::LeaveCall => {
540 let active_call = cx.read(ActiveCall::global);
541 if active_call.read_with(cx, |call, _| call.room().is_none()) {
542 Err(TestError::Inapplicable)?;
543 }
544
545 log::info!("{}: hanging up", client.username);
546 active_call.update(cx, |call, cx| call.hang_up(cx)).await?;
547 }
548
549 ClientOperation::InviteContactToCall { user_id } => {
550 let active_call = cx.read(ActiveCall::global);
551
552 log::info!("{}: inviting {}", client.username, user_id,);
553 active_call
554 .update(cx, |call, cx| call.invite(user_id.to_proto(), None, cx))
555 .await
556 .log_err();
557 }
558
559 ClientOperation::OpenLocalProject { first_root_name } => {
560 log::info!(
561 "{}: opening local project at {:?}",
562 client.username,
563 first_root_name
564 );
565
566 let root_path = Path::new(path!("/")).join(&first_root_name);
567 client.fs().create_dir(&root_path).await.unwrap();
568 client
569 .fs()
570 .create_file(&root_path.join("main.rs"), Default::default())
571 .await
572 .unwrap();
573 let project = client.build_local_project(root_path, cx).await.0;
574 ensure_project_shared(&project, client, cx).await;
575 client.local_projects_mut().push(project.clone());
576 }
577
578 ClientOperation::AddWorktreeToProject {
579 project_root_name,
580 new_root_path,
581 } => {
582 let project = project_for_root_name(client, &project_root_name, cx)
583 .ok_or(TestError::Inapplicable)?;
584
585 log::info!(
586 "{}: finding/creating local worktree at {:?} to project with root path {}",
587 client.username,
588 new_root_path,
589 project_root_name
590 );
591
592 ensure_project_shared(&project, client, cx).await;
593 if !client.fs().paths(false).contains(&new_root_path) {
594 client.fs().create_dir(&new_root_path).await.unwrap();
595 }
596 project
597 .update(cx, |project, cx| {
598 project.find_or_create_worktree(&new_root_path, true, cx)
599 })
600 .await
601 .unwrap();
602 }
603
604 ClientOperation::CloseRemoteProject { project_root_name } => {
605 let project = project_for_root_name(client, &project_root_name, cx)
606 .ok_or(TestError::Inapplicable)?;
607
608 log::info!(
609 "{}: closing remote project with root path {}",
610 client.username,
611 project_root_name,
612 );
613
614 let ix = client
615 .dev_server_projects()
616 .iter()
617 .position(|p| p == &project)
618 .unwrap();
619 cx.update(|_| {
620 client.dev_server_projects_mut().remove(ix);
621 client.buffers().retain(|p, _| *p != project);
622 drop(project);
623 });
624 }
625
626 ClientOperation::OpenRemoteProject {
627 host_id,
628 first_root_name,
629 } => {
630 let active_call = cx.read(ActiveCall::global);
631 let project = active_call
632 .update(cx, |call, cx| {
633 let room = call.room().cloned()?;
634 let participant = room
635 .read(cx)
636 .remote_participants()
637 .get(&host_id.to_proto())?;
638 let project_id = participant
639 .projects
640 .iter()
641 .find(|project| project.worktree_root_names[0] == first_root_name)?
642 .id;
643 Some(room.update(cx, |room, cx| {
644 room.join_project(
645 project_id,
646 client.language_registry().clone(),
647 FakeFs::new(cx.background_executor().clone()),
648 cx,
649 )
650 }))
651 })
652 .ok_or(TestError::Inapplicable)?;
653
654 log::info!(
655 "{}: joining remote project of user {}, root name {}",
656 client.username,
657 host_id,
658 first_root_name,
659 );
660
661 let project = project.await?;
662 client.dev_server_projects_mut().push(project);
663 }
664
665 ClientOperation::CreateWorktreeEntry {
666 project_root_name,
667 is_local,
668 full_path,
669 is_dir,
670 } => {
671 let project = project_for_root_name(client, &project_root_name, cx)
672 .ok_or(TestError::Inapplicable)?;
673 let project_path = project_path_for_full_path(&project, &full_path, cx)
674 .ok_or(TestError::Inapplicable)?;
675
676 log::info!(
677 "{}: creating {} at path {:?} in {} project {}",
678 client.username,
679 if is_dir { "dir" } else { "file" },
680 full_path,
681 if is_local { "local" } else { "remote" },
682 project_root_name,
683 );
684
685 ensure_project_shared(&project, client, cx).await;
686 project
687 .update(cx, |p, cx| p.create_entry(project_path, is_dir, cx))
688 .await?;
689 }
690
691 ClientOperation::OpenBuffer {
692 project_root_name,
693 is_local,
694 full_path,
695 } => {
696 let project = project_for_root_name(client, &project_root_name, cx)
697 .ok_or(TestError::Inapplicable)?;
698 let project_path = project_path_for_full_path(&project, &full_path, cx)
699 .ok_or(TestError::Inapplicable)?;
700
701 log::info!(
702 "{}: opening buffer {:?} in {} project {}",
703 client.username,
704 full_path,
705 if is_local { "local" } else { "remote" },
706 project_root_name,
707 );
708
709 ensure_project_shared(&project, client, cx).await;
710 let buffer = project
711 .update(cx, |project, cx| project.open_buffer(project_path, cx))
712 .await?;
713 client.buffers_for_project(&project).insert(buffer);
714 }
715
716 ClientOperation::EditBuffer {
717 project_root_name,
718 is_local,
719 full_path,
720 edits,
721 } => {
722 let project = project_for_root_name(client, &project_root_name, cx)
723 .ok_or(TestError::Inapplicable)?;
724 let buffer = buffer_for_full_path(client, &project, &full_path, cx)
725 .ok_or(TestError::Inapplicable)?;
726
727 log::info!(
728 "{}: editing buffer {:?} in {} project {} with {:?}",
729 client.username,
730 full_path,
731 if is_local { "local" } else { "remote" },
732 project_root_name,
733 edits
734 );
735
736 ensure_project_shared(&project, client, cx).await;
737 buffer.update(cx, |buffer, cx| {
738 let snapshot = buffer.snapshot();
739 buffer.edit(
740 edits.into_iter().map(|(range, text)| {
741 let start = snapshot.clip_offset(range.start, Bias::Left);
742 let end = snapshot.clip_offset(range.end, Bias::Right);
743 (start..end, text)
744 }),
745 None,
746 cx,
747 );
748 });
749 }
750
751 ClientOperation::CloseBuffer {
752 project_root_name,
753 is_local,
754 full_path,
755 } => {
756 let project = project_for_root_name(client, &project_root_name, cx)
757 .ok_or(TestError::Inapplicable)?;
758 let buffer = buffer_for_full_path(client, &project, &full_path, cx)
759 .ok_or(TestError::Inapplicable)?;
760
761 log::info!(
762 "{}: closing buffer {:?} in {} project {}",
763 client.username,
764 full_path,
765 if is_local { "local" } else { "remote" },
766 project_root_name
767 );
768
769 ensure_project_shared(&project, client, cx).await;
770 cx.update(|_| {
771 client.buffers_for_project(&project).remove(&buffer);
772 drop(buffer);
773 });
774 }
775
776 ClientOperation::SaveBuffer {
777 project_root_name,
778 is_local,
779 full_path,
780 detach,
781 } => {
782 let project = project_for_root_name(client, &project_root_name, cx)
783 .ok_or(TestError::Inapplicable)?;
784 let buffer = buffer_for_full_path(client, &project, &full_path, cx)
785 .ok_or(TestError::Inapplicable)?;
786
787 log::info!(
788 "{}: saving buffer {:?} in {} project {}, {}",
789 client.username,
790 full_path,
791 if is_local { "local" } else { "remote" },
792 project_root_name,
793 if detach { "detaching" } else { "awaiting" }
794 );
795
796 ensure_project_shared(&project, client, cx).await;
797 let requested_version = buffer.read_with(cx, |buffer, _| buffer.version());
798 let save =
799 project.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx));
800 let save = cx.spawn(|cx| async move {
801 save.await.context("save request failed")?;
802 assert!(
803 buffer
804 .read_with(&cx, |buffer, _| { buffer.saved_version().to_owned() })
805 .expect("App should not be dropped")
806 .observed_all(&requested_version)
807 );
808 anyhow::Ok(())
809 });
810 if detach {
811 cx.update(|cx| save.detach_and_log_err(cx));
812 } else {
813 save.await?;
814 }
815 }
816
817 ClientOperation::RequestLspDataInBuffer {
818 project_root_name,
819 is_local,
820 full_path,
821 offset,
822 kind,
823 detach,
824 } => {
825 let project = project_for_root_name(client, &project_root_name, cx)
826 .ok_or(TestError::Inapplicable)?;
827 let buffer = buffer_for_full_path(client, &project, &full_path, cx)
828 .ok_or(TestError::Inapplicable)?;
829
830 log::info!(
831 "{}: request LSP {:?} for buffer {:?} in {} project {}, {}",
832 client.username,
833 kind,
834 full_path,
835 if is_local { "local" } else { "remote" },
836 project_root_name,
837 if detach { "detaching" } else { "awaiting" }
838 );
839
840 use futures::{FutureExt as _, TryFutureExt as _};
841 let offset = buffer.read_with(cx, |b, _| b.clip_offset(offset, Bias::Left));
842
843 let process_lsp_request = project.update(cx, |project, cx| match kind {
844 LspRequestKind::Rename => project
845 .prepare_rename(buffer, offset, cx)
846 .map_ok(|_| ())
847 .boxed(),
848 LspRequestKind::Completion => project
849 .completions(&buffer, offset, DEFAULT_COMPLETION_CONTEXT, cx)
850 .map_ok(|_| ())
851 .boxed(),
852 LspRequestKind::CodeAction => project
853 .code_actions(&buffer, offset..offset, None, cx)
854 .map(|_| Ok(()))
855 .boxed(),
856 LspRequestKind::Definition => project
857 .definitions(&buffer, offset, cx)
858 .map_ok(|_| ())
859 .boxed(),
860 LspRequestKind::Highlights => project
861 .document_highlights(&buffer, offset, cx)
862 .map_ok(|_| ())
863 .boxed(),
864 });
865 let request = cx.foreground_executor().spawn(process_lsp_request);
866 if detach {
867 request.detach();
868 } else {
869 request.await?;
870 }
871 }
872
873 ClientOperation::SearchProject {
874 project_root_name,
875 is_local,
876 query,
877 detach,
878 } => {
879 let project = project_for_root_name(client, &project_root_name, cx)
880 .ok_or(TestError::Inapplicable)?;
881
882 log::info!(
883 "{}: search {} project {} for {:?}, {}",
884 client.username,
885 if is_local { "local" } else { "remote" },
886 project_root_name,
887 query,
888 if detach { "detaching" } else { "awaiting" }
889 );
890
891 let search = project.update(cx, |project, cx| {
892 project.search(
893 SearchQuery::text(
894 query,
895 false,
896 false,
897 false,
898 Default::default(),
899 Default::default(),
900 false,
901 None,
902 )
903 .unwrap(),
904 cx,
905 )
906 });
907 drop(project);
908 let search = cx.executor().spawn(async move {
909 let mut results = HashMap::default();
910 while let Ok(result) = search.recv().await {
911 if let SearchResult::Buffer { buffer, ranges } = result {
912 results.entry(buffer).or_insert(ranges);
913 }
914 }
915 results
916 });
917 search.await;
918 }
919
920 ClientOperation::WriteFsEntry {
921 path,
922 is_dir,
923 content,
924 } => {
925 if !client
926 .fs()
927 .directories(false)
928 .contains(&path.parent().unwrap().to_owned())
929 {
930 return Err(TestError::Inapplicable);
931 }
932
933 if is_dir {
934 log::info!("{}: creating dir at {:?}", client.username, path);
935 client.fs().create_dir(&path).await.unwrap();
936 } else {
937 let exists = client.fs().metadata(&path).await?.is_some();
938 let verb = if exists { "updating" } else { "creating" };
939 log::info!("{}: {} file at {:?}", verb, client.username, path);
940
941 client
942 .fs()
943 .save(
944 &path,
945 &Rope::from_str_small(content.as_str()),
946 text::LineEnding::Unix,
947 EncodingWrapper::new(UTF_8),
948 )
949 .await
950 .unwrap();
951 }
952 }
953
954 ClientOperation::GitOperation { operation } => match operation {
955 GitOperation::WriteGitIndex {
956 repo_path,
957 contents,
958 } => {
959 if !client.fs().directories(false).contains(&repo_path) {
960 return Err(TestError::Inapplicable);
961 }
962
963 for (path, _) in contents.iter() {
964 if !client
965 .fs()
966 .files()
967 .contains(&repo_path.join(path.as_std_path()))
968 {
969 return Err(TestError::Inapplicable);
970 }
971 }
972
973 log::info!(
974 "{}: writing git index for repo {:?}: {:?}",
975 client.username,
976 repo_path,
977 contents
978 );
979
980 let dot_git_dir = repo_path.join(".git");
981 let contents = contents
982 .iter()
983 .map(|(path, contents)| (path.as_unix_str(), contents.clone()))
984 .collect::<Vec<_>>();
985 if client.fs().metadata(&dot_git_dir).await?.is_none() {
986 client.fs().create_dir(&dot_git_dir).await?;
987 }
988 client.fs().set_index_for_repo(&dot_git_dir, &contents);
989 }
990 GitOperation::WriteGitBranch {
991 repo_path,
992 new_branch,
993 } => {
994 if !client.fs().directories(false).contains(&repo_path) {
995 return Err(TestError::Inapplicable);
996 }
997
998 log::info!(
999 "{}: writing git branch for repo {:?}: {:?}",
1000 client.username,
1001 repo_path,
1002 new_branch
1003 );
1004
1005 let dot_git_dir = repo_path.join(".git");
1006 if client.fs().metadata(&dot_git_dir).await?.is_none() {
1007 client.fs().create_dir(&dot_git_dir).await?;
1008 }
1009 client
1010 .fs()
1011 .set_branch_name(&dot_git_dir, new_branch.clone());
1012 }
1013 GitOperation::WriteGitStatuses {
1014 repo_path,
1015 statuses,
1016 } => {
1017 if !client.fs().directories(false).contains(&repo_path) {
1018 return Err(TestError::Inapplicable);
1019 }
1020 for (path, _) in statuses.iter() {
1021 if !client
1022 .fs()
1023 .files()
1024 .contains(&repo_path.join(path.as_std_path()))
1025 {
1026 return Err(TestError::Inapplicable);
1027 }
1028 }
1029
1030 log::info!(
1031 "{}: writing git statuses for repo {:?}: {:?}",
1032 client.username,
1033 repo_path,
1034 statuses
1035 );
1036
1037 let dot_git_dir = repo_path.join(".git");
1038
1039 let statuses = statuses
1040 .iter()
1041 .map(|(path, val)| (path.as_unix_str(), *val))
1042 .collect::<Vec<_>>();
1043
1044 if client.fs().metadata(&dot_git_dir).await?.is_none() {
1045 client.fs().create_dir(&dot_git_dir).await?;
1046 }
1047
1048 client
1049 .fs()
1050 .set_status_for_repo(&dot_git_dir, statuses.as_slice());
1051 }
1052 },
1053 }
1054 Ok(())
1055 }
1056
1057 async fn on_client_added(client: &Rc<TestClient>, _: &mut TestAppContext) {
1058 client.language_registry().add(Arc::new(Language::new(
1059 LanguageConfig {
1060 name: "Rust".into(),
1061 matcher: LanguageMatcher {
1062 path_suffixes: vec!["rs".to_string()],
1063 ..Default::default()
1064 },
1065 ..Default::default()
1066 },
1067 None,
1068 )));
1069 client.language_registry().register_fake_lsp(
1070 "Rust",
1071 FakeLspAdapter {
1072 name: "the-fake-language-server",
1073 capabilities: lsp::LanguageServer::full_capabilities(),
1074 initializer: Some(Box::new({
1075 let fs = client.app_state.fs.clone();
1076 move |fake_server: &mut FakeLanguageServer| {
1077 fake_server.set_request_handler::<lsp::request::Completion, _, _>(
1078 |_, _| async move {
1079 Ok(Some(lsp::CompletionResponse::Array(vec![
1080 lsp::CompletionItem {
1081 text_edit: Some(lsp::CompletionTextEdit::Edit(
1082 lsp::TextEdit {
1083 range: lsp::Range::new(
1084 lsp::Position::new(0, 0),
1085 lsp::Position::new(0, 0),
1086 ),
1087 new_text: "the-new-text".to_string(),
1088 },
1089 )),
1090 ..Default::default()
1091 },
1092 ])))
1093 },
1094 );
1095
1096 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
1097 |_, _| async move {
1098 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
1099 lsp::CodeAction {
1100 title: "the-code-action".to_string(),
1101 ..Default::default()
1102 },
1103 )]))
1104 },
1105 );
1106
1107 fake_server
1108 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
1109 |params, _| async move {
1110 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
1111 params.position,
1112 params.position,
1113 ))))
1114 },
1115 );
1116
1117 fake_server.set_request_handler::<lsp::request::GotoDefinition, _, _>({
1118 let fs = fs.clone();
1119 move |_, cx| {
1120 let background = cx.background_executor();
1121 let mut rng = background.rng();
1122 let count = rng.random_range::<usize, _>(1..3);
1123 let files = fs.as_fake().files();
1124 let files = (0..count)
1125 .map(|_| files.choose(&mut rng).unwrap().clone())
1126 .collect::<Vec<_>>();
1127 async move {
1128 log::info!("LSP: Returning definitions in files {:?}", &files);
1129 Ok(Some(lsp::GotoDefinitionResponse::Array(
1130 files
1131 .into_iter()
1132 .map(|file| lsp::Location {
1133 uri: lsp::Uri::from_file_path(file).unwrap(),
1134 range: Default::default(),
1135 })
1136 .collect(),
1137 )))
1138 }
1139 }
1140 });
1141
1142 fake_server
1143 .set_request_handler::<lsp::request::DocumentHighlightRequest, _, _>(
1144 move |_, cx| {
1145 let mut highlights = Vec::new();
1146 let background = cx.background_executor();
1147 let mut rng = background.rng();
1148
1149 let highlight_count = rng.random_range(1..=5);
1150 for _ in 0..highlight_count {
1151 let start_row = rng.random_range(0..100);
1152 let start_column = rng.random_range(0..100);
1153 let end_row = rng.random_range(0..100);
1154 let end_column = rng.random_range(0..100);
1155 let start = PointUtf16::new(start_row, start_column);
1156 let end = PointUtf16::new(end_row, end_column);
1157 let range =
1158 if start > end { end..start } else { start..end };
1159 highlights.push(lsp::DocumentHighlight {
1160 range: range_to_lsp(range.clone()).unwrap(),
1161 kind: Some(lsp::DocumentHighlightKind::READ),
1162 });
1163 }
1164 highlights.sort_unstable_by_key(|highlight| {
1165 (highlight.range.start, highlight.range.end)
1166 });
1167 async move { Ok(Some(highlights)) }
1168 },
1169 );
1170 }
1171 })),
1172 ..Default::default()
1173 },
1174 );
1175 }
1176
1177 async fn on_quiesce(_: &mut TestServer, clients: &mut [(Rc<TestClient>, TestAppContext)]) {
1178 for (client, client_cx) in clients.iter() {
1179 for guest_project in client.dev_server_projects().iter() {
1180 guest_project.read_with(client_cx, |guest_project, cx| {
1181 let host_project = clients.iter().find_map(|(client, cx)| {
1182 let project = client
1183 .local_projects()
1184 .iter()
1185 .find(|host_project| {
1186 host_project.read_with(cx, |host_project, _| {
1187 host_project.remote_id() == guest_project.remote_id()
1188 })
1189 })?
1190 .clone();
1191 Some((project, cx))
1192 });
1193
1194 if !guest_project.is_disconnected(cx)
1195 && let Some((host_project, host_cx)) = host_project {
1196 let host_worktree_snapshots =
1197 host_project.read_with(host_cx, |host_project, cx| {
1198 host_project
1199 .worktrees(cx)
1200 .map(|worktree| {
1201 let worktree = worktree.read(cx);
1202 (worktree.id(), worktree.snapshot())
1203 })
1204 .collect::<BTreeMap<_, _>>()
1205 });
1206 let guest_worktree_snapshots = guest_project
1207 .worktrees(cx)
1208 .map(|worktree| {
1209 let worktree = worktree.read(cx);
1210 (worktree.id(), worktree.snapshot())
1211 })
1212 .collect::<BTreeMap<_, _>>();
1213 let host_repository_snapshots = host_project.read_with(host_cx, |host_project, cx| {
1214 host_project.git_store().read(cx).repo_snapshots(cx)
1215 });
1216 let guest_repository_snapshots = guest_project.git_store().read(cx).repo_snapshots(cx);
1217
1218 assert_eq!(
1219 guest_worktree_snapshots.values().map(|w| w.abs_path()).collect::<Vec<_>>(),
1220 host_worktree_snapshots.values().map(|w| w.abs_path()).collect::<Vec<_>>(),
1221 "{} has different worktrees than the host for project {:?}",
1222 client.username, guest_project.remote_id(),
1223 );
1224
1225 assert_eq!(
1226 guest_repository_snapshots.values().collect::<Vec<_>>(),
1227 host_repository_snapshots.values().collect::<Vec<_>>(),
1228 "{} has different repositories than the host for project {:?}",
1229 client.username, guest_project.remote_id(),
1230 );
1231
1232 for (id, host_snapshot) in &host_worktree_snapshots {
1233 let guest_snapshot = &guest_worktree_snapshots[id];
1234 assert_eq!(
1235 guest_snapshot.root_name(),
1236 host_snapshot.root_name(),
1237 "{} has different root name than the host for worktree {}, project {:?}",
1238 client.username,
1239 id,
1240 guest_project.remote_id(),
1241 );
1242 assert_eq!(
1243 guest_snapshot.abs_path(),
1244 host_snapshot.abs_path(),
1245 "{} has different abs path than the host for worktree {}, project: {:?}",
1246 client.username,
1247 id,
1248 guest_project.remote_id(),
1249 );
1250 assert_eq!(
1251 guest_snapshot.entries(false, 0).map(null_out_entry_size).collect::<Vec<_>>(),
1252 host_snapshot.entries(false, 0).map(null_out_entry_size).collect::<Vec<_>>(),
1253 "{} has different snapshot than the host for worktree {:?} ({:?}) and project {:?}",
1254 client.username,
1255 host_snapshot.abs_path(),
1256 id,
1257 guest_project.remote_id(),
1258 );
1259 assert_eq!(guest_snapshot.scan_id(), host_snapshot.scan_id(),
1260 "{} has different scan id than the host for worktree {:?} and project {:?}",
1261 client.username,
1262 host_snapshot.abs_path(),
1263 guest_project.remote_id(),
1264 );
1265 }
1266 }
1267
1268 for buffer in guest_project.opened_buffers(cx) {
1269 let buffer = buffer.read(cx);
1270 assert_eq!(
1271 buffer.deferred_ops_len(),
1272 0,
1273 "{} has deferred operations for buffer {:?} in project {:?}",
1274 client.username,
1275 buffer.file().unwrap().full_path(cx),
1276 guest_project.remote_id(),
1277 );
1278 }
1279 });
1280
1281 // A hack to work around a hack in
1282 // https://github.com/zed-industries/zed/pull/16696 that wasn't
1283 // detected until we upgraded the rng crate. This whole crate is
1284 // going away with DeltaDB soon, so we hold our nose and
1285 // continue.
1286 fn null_out_entry_size(entry: &project::Entry) -> project::Entry {
1287 project::Entry {
1288 size: 0,
1289 ..entry.clone()
1290 }
1291 }
1292 }
1293
1294 let buffers = client.buffers().clone();
1295 for (guest_project, guest_buffers) in &buffers {
1296 let project_id = if guest_project.read_with(client_cx, |project, cx| {
1297 project.is_local() || project.is_disconnected(cx)
1298 }) {
1299 continue;
1300 } else {
1301 guest_project
1302 .read_with(client_cx, |project, _| project.remote_id())
1303 .unwrap()
1304 };
1305 let guest_user_id = client.user_id().unwrap();
1306
1307 let host_project = clients.iter().find_map(|(client, cx)| {
1308 let project = client
1309 .local_projects()
1310 .iter()
1311 .find(|host_project| {
1312 host_project.read_with(cx, |host_project, _| {
1313 host_project.remote_id() == Some(project_id)
1314 })
1315 })?
1316 .clone();
1317 Some((client.user_id().unwrap(), project, cx))
1318 });
1319
1320 let (host_user_id, host_project, host_cx) =
1321 if let Some((host_user_id, host_project, host_cx)) = host_project {
1322 (host_user_id, host_project, host_cx)
1323 } else {
1324 continue;
1325 };
1326
1327 for guest_buffer in guest_buffers {
1328 let buffer_id =
1329 guest_buffer.read_with(client_cx, |buffer, _| buffer.remote_id());
1330 let host_buffer = host_project.read_with(host_cx, |project, cx| {
1331 project.buffer_for_id(buffer_id, cx).unwrap_or_else(|| {
1332 panic!(
1333 "host does not have buffer for guest:{}, peer:{:?}, id:{}",
1334 client.username,
1335 client.peer_id(),
1336 buffer_id
1337 )
1338 })
1339 });
1340 let path = host_buffer
1341 .read_with(host_cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
1342
1343 assert_eq!(
1344 guest_buffer.read_with(client_cx, |buffer, _| buffer.deferred_ops_len()),
1345 0,
1346 "{}, buffer {}, path {:?} has deferred operations",
1347 client.username,
1348 buffer_id,
1349 path,
1350 );
1351 assert_eq!(
1352 guest_buffer.read_with(client_cx, |buffer, _| buffer.text()),
1353 host_buffer.read_with(host_cx, |buffer, _| buffer.text()),
1354 "{}, buffer {}, path {:?}, differs from the host's buffer",
1355 client.username,
1356 buffer_id,
1357 path
1358 );
1359
1360 let host_file = host_buffer.read_with(host_cx, |b, _| b.file().cloned());
1361 let guest_file = guest_buffer.read_with(client_cx, |b, _| b.file().cloned());
1362 match (host_file, guest_file) {
1363 (Some(host_file), Some(guest_file)) => {
1364 assert_eq!(guest_file.path(), host_file.path());
1365 assert_eq!(
1366 guest_file.disk_state(),
1367 host_file.disk_state(),
1368 "guest {} disk_state does not match host {} for path {:?} in project {}",
1369 guest_user_id,
1370 host_user_id,
1371 guest_file.path(),
1372 project_id,
1373 );
1374 }
1375 (None, None) => {}
1376 (None, _) => panic!("host's file is None, guest's isn't"),
1377 (_, None) => panic!("guest's file is None, hosts's isn't"),
1378 }
1379
1380 let host_diff_base = host_project.read_with(host_cx, |project, cx| {
1381 project
1382 .git_store()
1383 .read(cx)
1384 .get_unstaged_diff(host_buffer.read(cx).remote_id(), cx)
1385 .unwrap()
1386 .read(cx)
1387 .base_text_string()
1388 });
1389 let guest_diff_base = guest_project.read_with(client_cx, |project, cx| {
1390 project
1391 .git_store()
1392 .read(cx)
1393 .get_unstaged_diff(guest_buffer.read(cx).remote_id(), cx)
1394 .unwrap()
1395 .read(cx)
1396 .base_text_string()
1397 });
1398 assert_eq!(
1399 guest_diff_base, host_diff_base,
1400 "guest {} diff base does not match host's for path {path:?} in project {project_id}",
1401 client.username
1402 );
1403
1404 let host_saved_version =
1405 host_buffer.read_with(host_cx, |b, _| b.saved_version().clone());
1406 let guest_saved_version =
1407 guest_buffer.read_with(client_cx, |b, _| b.saved_version().clone());
1408 assert_eq!(
1409 guest_saved_version, host_saved_version,
1410 "guest {} saved version does not match host's for path {path:?} in project {project_id}",
1411 client.username
1412 );
1413
1414 let host_is_dirty = host_buffer.read_with(host_cx, |b, _| b.is_dirty());
1415 let guest_is_dirty = guest_buffer.read_with(client_cx, |b, _| b.is_dirty());
1416 assert_eq!(
1417 guest_is_dirty, host_is_dirty,
1418 "guest {} dirty state does not match host's for path {path:?} in project {project_id}",
1419 client.username
1420 );
1421
1422 let host_saved_mtime = host_buffer.read_with(host_cx, |b, _| b.saved_mtime());
1423 let guest_saved_mtime =
1424 guest_buffer.read_with(client_cx, |b, _| b.saved_mtime());
1425 assert_eq!(
1426 guest_saved_mtime, host_saved_mtime,
1427 "guest {} saved mtime does not match host's for path {path:?} in project {project_id}",
1428 client.username
1429 );
1430
1431 let host_is_dirty = host_buffer.read_with(host_cx, |b, _| b.is_dirty());
1432 let guest_is_dirty = guest_buffer.read_with(client_cx, |b, _| b.is_dirty());
1433 assert_eq!(
1434 guest_is_dirty, host_is_dirty,
1435 "guest {} dirty status does not match host's for path {path:?} in project {project_id}",
1436 client.username
1437 );
1438
1439 let host_has_conflict = host_buffer.read_with(host_cx, |b, _| b.has_conflict());
1440 let guest_has_conflict =
1441 guest_buffer.read_with(client_cx, |b, _| b.has_conflict());
1442 assert_eq!(
1443 guest_has_conflict, host_has_conflict,
1444 "guest {} conflict status does not match host's for path {path:?} in project {project_id}",
1445 client.username
1446 );
1447 }
1448 }
1449 }
1450 }
1451}
1452
1453fn generate_git_operation(rng: &mut StdRng, client: &TestClient) -> GitOperation {
1454 fn generate_file_paths(
1455 repo_path: &Path,
1456 rng: &mut StdRng,
1457 client: &TestClient,
1458 ) -> Vec<RelPathBuf> {
1459 let mut paths = client
1460 .fs()
1461 .files()
1462 .into_iter()
1463 .filter(|path| path.starts_with(repo_path))
1464 .collect::<Vec<_>>();
1465
1466 let count = rng.random_range(0..=paths.len());
1467 paths.shuffle(rng);
1468 paths.truncate(count);
1469
1470 paths
1471 .iter()
1472 .map(|path| {
1473 RelPath::new(path.strip_prefix(repo_path).unwrap(), PathStyle::local())
1474 .unwrap()
1475 .to_rel_path_buf()
1476 })
1477 .collect::<Vec<_>>()
1478 }
1479
1480 let repo_path = client.fs().directories(false).choose(rng).unwrap().clone();
1481
1482 match rng.random_range(0..100_u32) {
1483 0..=25 => {
1484 let file_paths = generate_file_paths(&repo_path, rng, client);
1485
1486 let contents = file_paths
1487 .into_iter()
1488 .map(|path| (path, distr::Alphanumeric.sample_string(rng, 16)))
1489 .collect();
1490
1491 GitOperation::WriteGitIndex {
1492 repo_path,
1493 contents,
1494 }
1495 }
1496 26..=63 => {
1497 let new_branch =
1498 (rng.random_range(0..10) > 3).then(|| distr::Alphanumeric.sample_string(rng, 8));
1499
1500 GitOperation::WriteGitBranch {
1501 repo_path,
1502 new_branch,
1503 }
1504 }
1505 64..=100 => {
1506 let file_paths = generate_file_paths(&repo_path, rng, client);
1507 let statuses = file_paths
1508 .into_iter()
1509 .map(|path| (path, gen_status(rng)))
1510 .collect::<Vec<_>>();
1511 GitOperation::WriteGitStatuses {
1512 repo_path,
1513 statuses,
1514 }
1515 }
1516 _ => unreachable!(),
1517 }
1518}
1519
1520fn buffer_for_full_path(
1521 client: &TestClient,
1522 project: &Entity<Project>,
1523 full_path: &RelPath,
1524 cx: &TestAppContext,
1525) -> Option<Entity<language::Buffer>> {
1526 client
1527 .buffers_for_project(project)
1528 .iter()
1529 .find(|buffer| {
1530 buffer.read_with(cx, |buffer, cx| {
1531 let file = buffer.file().unwrap();
1532 let Some(worktree) = project.read(cx).worktree_for_id(file.worktree_id(cx), cx)
1533 else {
1534 return false;
1535 };
1536 worktree.read(cx).root_name().join(&file.path()).as_ref() == full_path
1537 })
1538 })
1539 .cloned()
1540}
1541
1542fn project_for_root_name(
1543 client: &TestClient,
1544 root_name: &str,
1545 cx: &TestAppContext,
1546) -> Option<Entity<Project>> {
1547 if let Some(ix) = project_ix_for_root_name(client.local_projects().deref(), root_name, cx) {
1548 return Some(client.local_projects()[ix].clone());
1549 }
1550 if let Some(ix) = project_ix_for_root_name(client.dev_server_projects().deref(), root_name, cx)
1551 {
1552 return Some(client.dev_server_projects()[ix].clone());
1553 }
1554 None
1555}
1556
1557fn project_ix_for_root_name(
1558 projects: &[Entity<Project>],
1559 root_name: &str,
1560 cx: &TestAppContext,
1561) -> Option<usize> {
1562 projects.iter().position(|project| {
1563 project.read_with(cx, |project, cx| {
1564 let worktree = project.visible_worktrees(cx).next().unwrap();
1565 worktree.read(cx).root_name() == root_name
1566 })
1567 })
1568}
1569
1570fn root_name_for_project(project: &Entity<Project>, cx: &TestAppContext) -> String {
1571 project.read_with(cx, |project, cx| {
1572 project
1573 .visible_worktrees(cx)
1574 .next()
1575 .unwrap()
1576 .read(cx)
1577 .root_name_str()
1578 .to_string()
1579 })
1580}
1581
1582fn project_path_for_full_path(
1583 project: &Entity<Project>,
1584 full_path: &RelPath,
1585 cx: &TestAppContext,
1586) -> Option<ProjectPath> {
1587 let mut components = full_path.components();
1588 let root_name = components.next().unwrap();
1589 let path = components.rest().into();
1590 let worktree_id = project.read_with(cx, |project, cx| {
1591 project.worktrees(cx).find_map(|worktree| {
1592 let worktree = worktree.read(cx);
1593 if worktree.root_name_str() == root_name {
1594 Some(worktree.id())
1595 } else {
1596 None
1597 }
1598 })
1599 })?;
1600 Some(ProjectPath { worktree_id, path })
1601}
1602
1603async fn ensure_project_shared(
1604 project: &Entity<Project>,
1605 client: &TestClient,
1606 cx: &mut TestAppContext,
1607) {
1608 let first_root_name = root_name_for_project(project, cx);
1609 let active_call = cx.read(ActiveCall::global);
1610 if active_call.read_with(cx, |call, _| call.room().is_some())
1611 && project.read_with(cx, |project, _| project.is_local() && !project.is_shared())
1612 {
1613 match active_call
1614 .update(cx, |call, cx| call.share_project(project.clone(), cx))
1615 .await
1616 {
1617 Ok(project_id) => {
1618 log::info!(
1619 "{}: shared project {} with id {}",
1620 client.username,
1621 first_root_name,
1622 project_id
1623 );
1624 }
1625 Err(error) => {
1626 log::error!(
1627 "{}: error sharing project {}: {:?}",
1628 client.username,
1629 first_root_name,
1630 error
1631 );
1632 }
1633 }
1634 }
1635}
1636
1637fn choose_random_project(client: &TestClient, rng: &mut StdRng) -> Option<Entity<Project>> {
1638 client
1639 .local_projects()
1640 .deref()
1641 .iter()
1642 .chain(client.dev_server_projects().iter())
1643 .choose(rng)
1644 .cloned()
1645}
1646
1647fn gen_file_name(rng: &mut StdRng) -> String {
1648 let mut name = String::new();
1649 for _ in 0..10 {
1650 let letter = rng.random_range('a'..='z');
1651 name.push(letter);
1652 }
1653 name
1654}
1655
1656fn gen_status(rng: &mut StdRng) -> FileStatus {
1657 fn gen_tracked_status(rng: &mut StdRng) -> TrackedStatus {
1658 match rng.random_range(0..3) {
1659 0 => TrackedStatus {
1660 index_status: StatusCode::Unmodified,
1661 worktree_status: StatusCode::Unmodified,
1662 },
1663 1 => TrackedStatus {
1664 index_status: StatusCode::Modified,
1665 worktree_status: StatusCode::Modified,
1666 },
1667 2 => TrackedStatus {
1668 index_status: StatusCode::Added,
1669 worktree_status: StatusCode::Modified,
1670 },
1671 3 => TrackedStatus {
1672 index_status: StatusCode::Added,
1673 worktree_status: StatusCode::Unmodified,
1674 },
1675 _ => unreachable!(),
1676 }
1677 }
1678
1679 fn gen_unmerged_status_code(rng: &mut StdRng) -> UnmergedStatusCode {
1680 match rng.random_range(0..3) {
1681 0 => UnmergedStatusCode::Updated,
1682 1 => UnmergedStatusCode::Added,
1683 2 => UnmergedStatusCode::Deleted,
1684 _ => unreachable!(),
1685 }
1686 }
1687
1688 match rng.random_range(0..2) {
1689 0 => FileStatus::Unmerged(UnmergedStatus {
1690 first_head: gen_unmerged_status_code(rng),
1691 second_head: gen_unmerged_status_code(rng),
1692 }),
1693 1 => FileStatus::Tracked(gen_tracked_status(rng)),
1694 _ => unreachable!(),
1695 }
1696}