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