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