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