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 .expect("App should not be dropped")
804 .observed_all(&requested_version)
805 );
806 anyhow::Ok(())
807 });
808 if detach {
809 cx.update(|cx| save.detach_and_log_err(cx));
810 } else {
811 save.await?;
812 }
813 }
814
815 ClientOperation::RequestLspDataInBuffer {
816 project_root_name,
817 is_local,
818 full_path,
819 offset,
820 kind,
821 detach,
822 } => {
823 let project = project_for_root_name(client, &project_root_name, cx)
824 .ok_or(TestError::Inapplicable)?;
825 let buffer = buffer_for_full_path(client, &project, &full_path, cx)
826 .ok_or(TestError::Inapplicable)?;
827
828 log::info!(
829 "{}: request LSP {:?} for buffer {:?} in {} project {}, {}",
830 client.username,
831 kind,
832 full_path,
833 if is_local { "local" } else { "remote" },
834 project_root_name,
835 if detach { "detaching" } else { "awaiting" }
836 );
837
838 use futures::{FutureExt as _, TryFutureExt as _};
839 let offset = buffer.read_with(cx, |b, _| b.clip_offset(offset, Bias::Left));
840
841 let process_lsp_request = project.update(cx, |project, cx| match kind {
842 LspRequestKind::Rename => project
843 .prepare_rename(buffer, offset, cx)
844 .map_ok(|_| ())
845 .boxed(),
846 LspRequestKind::Completion => project
847 .completions(&buffer, offset, DEFAULT_COMPLETION_CONTEXT, cx)
848 .map_ok(|_| ())
849 .boxed(),
850 LspRequestKind::CodeAction => project
851 .code_actions(&buffer, offset..offset, None, cx)
852 .map(|_| Ok(()))
853 .boxed(),
854 LspRequestKind::Definition => project
855 .definitions(&buffer, offset, cx)
856 .map_ok(|_| ())
857 .boxed(),
858 LspRequestKind::Highlights => project
859 .document_highlights(&buffer, offset, cx)
860 .map_ok(|_| ())
861 .boxed(),
862 });
863 let request = cx.foreground_executor().spawn(process_lsp_request);
864 if detach {
865 request.detach();
866 } else {
867 request.await?;
868 }
869 }
870
871 ClientOperation::SearchProject {
872 project_root_name,
873 is_local,
874 query,
875 detach,
876 } => {
877 let project = project_for_root_name(client, &project_root_name, cx)
878 .ok_or(TestError::Inapplicable)?;
879
880 log::info!(
881 "{}: search {} project {} for {:?}, {}",
882 client.username,
883 if is_local { "local" } else { "remote" },
884 project_root_name,
885 query,
886 if detach { "detaching" } else { "awaiting" }
887 );
888
889 let search = project.update(cx, |project, cx| {
890 project.search(
891 SearchQuery::text(
892 query,
893 false,
894 false,
895 false,
896 Default::default(),
897 Default::default(),
898 false,
899 None,
900 )
901 .unwrap(),
902 cx,
903 )
904 });
905 drop(project);
906 let search = cx.executor().spawn(async move {
907 let mut results = HashMap::default();
908 while let Ok(result) = search.recv().await {
909 if let SearchResult::Buffer { buffer, ranges } = result {
910 results.entry(buffer).or_insert(ranges);
911 }
912 }
913 results
914 });
915 search.await;
916 }
917
918 ClientOperation::WriteFsEntry {
919 path,
920 is_dir,
921 content,
922 } => {
923 if !client
924 .fs()
925 .directories(false)
926 .contains(&path.parent().unwrap().to_owned())
927 {
928 return Err(TestError::Inapplicable);
929 }
930
931 if is_dir {
932 log::info!("{}: creating dir at {:?}", client.username, path);
933 client.fs().create_dir(&path).await.unwrap();
934 } else {
935 let exists = client.fs().metadata(&path).await?.is_some();
936 let verb = if exists { "updating" } else { "creating" };
937 log::info!("{}: {} file at {:?}", verb, client.username, path);
938
939 client
940 .fs()
941 .save(&path, &content.as_str().into(), text::LineEnding::Unix)
942 .await
943 .unwrap();
944 }
945 }
946
947 ClientOperation::GitOperation { operation } => match operation {
948 GitOperation::WriteGitIndex {
949 repo_path,
950 contents,
951 } => {
952 if !client.fs().directories(false).contains(&repo_path) {
953 return Err(TestError::Inapplicable);
954 }
955
956 for (path, _) in contents.iter() {
957 if !client
958 .fs()
959 .files()
960 .contains(&repo_path.join(path.as_std_path()))
961 {
962 return Err(TestError::Inapplicable);
963 }
964 }
965
966 log::info!(
967 "{}: writing git index for repo {:?}: {:?}",
968 client.username,
969 repo_path,
970 contents
971 );
972
973 let dot_git_dir = repo_path.join(".git");
974 let contents = contents
975 .iter()
976 .map(|(path, contents)| (path.as_unix_str(), contents.clone()))
977 .collect::<Vec<_>>();
978 if client.fs().metadata(&dot_git_dir).await?.is_none() {
979 client.fs().create_dir(&dot_git_dir).await?;
980 }
981 client.fs().set_index_for_repo(&dot_git_dir, &contents);
982 }
983 GitOperation::WriteGitBranch {
984 repo_path,
985 new_branch,
986 } => {
987 if !client.fs().directories(false).contains(&repo_path) {
988 return Err(TestError::Inapplicable);
989 }
990
991 log::info!(
992 "{}: writing git branch for repo {:?}: {:?}",
993 client.username,
994 repo_path,
995 new_branch
996 );
997
998 let dot_git_dir = repo_path.join(".git");
999 if client.fs().metadata(&dot_git_dir).await?.is_none() {
1000 client.fs().create_dir(&dot_git_dir).await?;
1001 }
1002 client
1003 .fs()
1004 .set_branch_name(&dot_git_dir, new_branch.clone());
1005 }
1006 GitOperation::WriteGitStatuses {
1007 repo_path,
1008 statuses,
1009 } => {
1010 if !client.fs().directories(false).contains(&repo_path) {
1011 return Err(TestError::Inapplicable);
1012 }
1013 for (path, _) in statuses.iter() {
1014 if !client
1015 .fs()
1016 .files()
1017 .contains(&repo_path.join(path.as_std_path()))
1018 {
1019 return Err(TestError::Inapplicable);
1020 }
1021 }
1022
1023 log::info!(
1024 "{}: writing git statuses for repo {:?}: {:?}",
1025 client.username,
1026 repo_path,
1027 statuses
1028 );
1029
1030 let dot_git_dir = repo_path.join(".git");
1031
1032 let statuses = statuses
1033 .iter()
1034 .map(|(path, val)| (path.as_unix_str(), *val))
1035 .collect::<Vec<_>>();
1036
1037 if client.fs().metadata(&dot_git_dir).await?.is_none() {
1038 client.fs().create_dir(&dot_git_dir).await?;
1039 }
1040
1041 client
1042 .fs()
1043 .set_status_for_repo(&dot_git_dir, statuses.as_slice());
1044 }
1045 },
1046 }
1047 Ok(())
1048 }
1049
1050 async fn on_client_added(client: &Rc<TestClient>, _: &mut TestAppContext) {
1051 client.language_registry().add(Arc::new(Language::new(
1052 LanguageConfig {
1053 name: "Rust".into(),
1054 matcher: LanguageMatcher {
1055 path_suffixes: vec!["rs".to_string()],
1056 ..Default::default()
1057 },
1058 ..Default::default()
1059 },
1060 None,
1061 )));
1062 client.language_registry().register_fake_lsp(
1063 "Rust",
1064 FakeLspAdapter {
1065 name: "the-fake-language-server",
1066 capabilities: lsp::LanguageServer::full_capabilities(),
1067 initializer: Some(Box::new({
1068 let fs = client.app_state.fs.clone();
1069 move |fake_server: &mut FakeLanguageServer| {
1070 fake_server.set_request_handler::<lsp::request::Completion, _, _>(
1071 |_, _| async move {
1072 Ok(Some(lsp::CompletionResponse::Array(vec![
1073 lsp::CompletionItem {
1074 text_edit: Some(lsp::CompletionTextEdit::Edit(
1075 lsp::TextEdit {
1076 range: lsp::Range::new(
1077 lsp::Position::new(0, 0),
1078 lsp::Position::new(0, 0),
1079 ),
1080 new_text: "the-new-text".to_string(),
1081 },
1082 )),
1083 ..Default::default()
1084 },
1085 ])))
1086 },
1087 );
1088
1089 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
1090 |_, _| async move {
1091 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
1092 lsp::CodeAction {
1093 title: "the-code-action".to_string(),
1094 ..Default::default()
1095 },
1096 )]))
1097 },
1098 );
1099
1100 fake_server
1101 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
1102 |params, _| async move {
1103 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
1104 params.position,
1105 params.position,
1106 ))))
1107 },
1108 );
1109
1110 fake_server.set_request_handler::<lsp::request::GotoDefinition, _, _>({
1111 let fs = fs.clone();
1112 move |_, cx| {
1113 let background = cx.background_executor();
1114 let mut rng = background.rng();
1115 let count = rng.random_range::<usize, _>(1..3);
1116 let files = fs.as_fake().files();
1117 let files = (0..count)
1118 .map(|_| files.choose(&mut rng).unwrap().clone())
1119 .collect::<Vec<_>>();
1120 async move {
1121 log::info!("LSP: Returning definitions in files {:?}", &files);
1122 Ok(Some(lsp::GotoDefinitionResponse::Array(
1123 files
1124 .into_iter()
1125 .map(|file| lsp::Location {
1126 uri: lsp::Uri::from_file_path(file).unwrap(),
1127 range: Default::default(),
1128 })
1129 .collect(),
1130 )))
1131 }
1132 }
1133 });
1134
1135 fake_server
1136 .set_request_handler::<lsp::request::DocumentHighlightRequest, _, _>(
1137 move |_, cx| {
1138 let mut highlights = Vec::new();
1139 let background = cx.background_executor();
1140 let mut rng = background.rng();
1141
1142 let highlight_count = rng.random_range(1..=5);
1143 for _ in 0..highlight_count {
1144 let start_row = rng.random_range(0..100);
1145 let start_column = rng.random_range(0..100);
1146 let end_row = rng.random_range(0..100);
1147 let end_column = rng.random_range(0..100);
1148 let start = PointUtf16::new(start_row, start_column);
1149 let end = PointUtf16::new(end_row, end_column);
1150 let range =
1151 if start > end { end..start } else { start..end };
1152 highlights.push(lsp::DocumentHighlight {
1153 range: range_to_lsp(range.clone()).unwrap(),
1154 kind: Some(lsp::DocumentHighlightKind::READ),
1155 });
1156 }
1157 highlights.sort_unstable_by_key(|highlight| {
1158 (highlight.range.start, highlight.range.end)
1159 });
1160 async move { Ok(Some(highlights)) }
1161 },
1162 );
1163 }
1164 })),
1165 ..Default::default()
1166 },
1167 );
1168 }
1169
1170 async fn on_quiesce(_: &mut TestServer, clients: &mut [(Rc<TestClient>, TestAppContext)]) {
1171 for (client, client_cx) in clients.iter() {
1172 for guest_project in client.dev_server_projects().iter() {
1173 guest_project.read_with(client_cx, |guest_project, cx| {
1174 let host_project = clients.iter().find_map(|(client, cx)| {
1175 let project = client
1176 .local_projects()
1177 .iter()
1178 .find(|host_project| {
1179 host_project.read_with(cx, |host_project, _| {
1180 host_project.remote_id() == guest_project.remote_id()
1181 })
1182 })?
1183 .clone();
1184 Some((project, cx))
1185 });
1186
1187 if !guest_project.is_disconnected(cx)
1188 && let Some((host_project, host_cx)) = host_project {
1189 let host_worktree_snapshots =
1190 host_project.read_with(host_cx, |host_project, cx| {
1191 host_project
1192 .worktrees(cx)
1193 .map(|worktree| {
1194 let worktree = worktree.read(cx);
1195 (worktree.id(), worktree.snapshot())
1196 })
1197 .collect::<BTreeMap<_, _>>()
1198 });
1199 let guest_worktree_snapshots = guest_project
1200 .worktrees(cx)
1201 .map(|worktree| {
1202 let worktree = worktree.read(cx);
1203 (worktree.id(), worktree.snapshot())
1204 })
1205 .collect::<BTreeMap<_, _>>();
1206 let host_repository_snapshots = host_project.read_with(host_cx, |host_project, cx| {
1207 host_project.git_store().read(cx).repo_snapshots(cx)
1208 });
1209 let guest_repository_snapshots = guest_project.git_store().read(cx).repo_snapshots(cx);
1210
1211 assert_eq!(
1212 guest_worktree_snapshots.values().map(|w| w.abs_path()).collect::<Vec<_>>(),
1213 host_worktree_snapshots.values().map(|w| w.abs_path()).collect::<Vec<_>>(),
1214 "{} has different worktrees than the host for project {:?}",
1215 client.username, guest_project.remote_id(),
1216 );
1217
1218 assert_eq!(
1219 guest_repository_snapshots.values().collect::<Vec<_>>(),
1220 host_repository_snapshots.values().collect::<Vec<_>>(),
1221 "{} has different repositories than the host for project {:?}",
1222 client.username, guest_project.remote_id(),
1223 );
1224
1225 for (id, host_snapshot) in &host_worktree_snapshots {
1226 let guest_snapshot = &guest_worktree_snapshots[id];
1227 assert_eq!(
1228 guest_snapshot.root_name(),
1229 host_snapshot.root_name(),
1230 "{} has different root name than the host for worktree {}, project {:?}",
1231 client.username,
1232 id,
1233 guest_project.remote_id(),
1234 );
1235 assert_eq!(
1236 guest_snapshot.abs_path(),
1237 host_snapshot.abs_path(),
1238 "{} has different abs path than the host for worktree {}, project: {:?}",
1239 client.username,
1240 id,
1241 guest_project.remote_id(),
1242 );
1243 assert_eq!(
1244 guest_snapshot.entries(false, 0).map(null_out_entry_size).collect::<Vec<_>>(),
1245 host_snapshot.entries(false, 0).map(null_out_entry_size).collect::<Vec<_>>(),
1246 "{} has different snapshot than the host for worktree {:?} ({:?}) and project {:?}",
1247 client.username,
1248 host_snapshot.abs_path(),
1249 id,
1250 guest_project.remote_id(),
1251 );
1252 assert_eq!(guest_snapshot.scan_id(), host_snapshot.scan_id(),
1253 "{} has different scan id than the host for worktree {:?} and project {:?}",
1254 client.username,
1255 host_snapshot.abs_path(),
1256 guest_project.remote_id(),
1257 );
1258 }
1259 }
1260
1261 for buffer in guest_project.opened_buffers(cx) {
1262 let buffer = buffer.read(cx);
1263 assert_eq!(
1264 buffer.deferred_ops_len(),
1265 0,
1266 "{} has deferred operations for buffer {:?} in project {:?}",
1267 client.username,
1268 buffer.file().unwrap().full_path(cx),
1269 guest_project.remote_id(),
1270 );
1271 }
1272 });
1273
1274 // A hack to work around a hack in
1275 // https://github.com/zed-industries/zed/pull/16696 that wasn't
1276 // detected until we upgraded the rng crate. This whole crate is
1277 // going away with DeltaDB soon, so we hold our nose and
1278 // continue.
1279 fn null_out_entry_size(entry: &project::Entry) -> project::Entry {
1280 project::Entry {
1281 size: 0,
1282 ..entry.clone()
1283 }
1284 }
1285 }
1286
1287 let buffers = client.buffers().clone();
1288 for (guest_project, guest_buffers) in &buffers {
1289 let project_id = if guest_project.read_with(client_cx, |project, cx| {
1290 project.is_local() || project.is_disconnected(cx)
1291 }) {
1292 continue;
1293 } else {
1294 guest_project
1295 .read_with(client_cx, |project, _| project.remote_id())
1296 .unwrap()
1297 };
1298 let guest_user_id = client.user_id().unwrap();
1299
1300 let host_project = clients.iter().find_map(|(client, cx)| {
1301 let project = client
1302 .local_projects()
1303 .iter()
1304 .find(|host_project| {
1305 host_project.read_with(cx, |host_project, _| {
1306 host_project.remote_id() == Some(project_id)
1307 })
1308 })?
1309 .clone();
1310 Some((client.user_id().unwrap(), project, cx))
1311 });
1312
1313 let (host_user_id, host_project, host_cx) =
1314 if let Some((host_user_id, host_project, host_cx)) = host_project {
1315 (host_user_id, host_project, host_cx)
1316 } else {
1317 continue;
1318 };
1319
1320 for guest_buffer in guest_buffers {
1321 let buffer_id =
1322 guest_buffer.read_with(client_cx, |buffer, _| buffer.remote_id());
1323 let host_buffer = host_project.read_with(host_cx, |project, cx| {
1324 project.buffer_for_id(buffer_id, cx).unwrap_or_else(|| {
1325 panic!(
1326 "host does not have buffer for guest:{}, peer:{:?}, id:{}",
1327 client.username,
1328 client.peer_id(),
1329 buffer_id
1330 )
1331 })
1332 });
1333 let path = host_buffer
1334 .read_with(host_cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
1335
1336 assert_eq!(
1337 guest_buffer.read_with(client_cx, |buffer, _| buffer.deferred_ops_len()),
1338 0,
1339 "{}, buffer {}, path {:?} has deferred operations",
1340 client.username,
1341 buffer_id,
1342 path,
1343 );
1344 assert_eq!(
1345 guest_buffer.read_with(client_cx, |buffer, _| buffer.text()),
1346 host_buffer.read_with(host_cx, |buffer, _| buffer.text()),
1347 "{}, buffer {}, path {:?}, differs from the host's buffer",
1348 client.username,
1349 buffer_id,
1350 path
1351 );
1352
1353 let host_file = host_buffer.read_with(host_cx, |b, _| b.file().cloned());
1354 let guest_file = guest_buffer.read_with(client_cx, |b, _| b.file().cloned());
1355 match (host_file, guest_file) {
1356 (Some(host_file), Some(guest_file)) => {
1357 assert_eq!(guest_file.path(), host_file.path());
1358 assert_eq!(
1359 guest_file.disk_state(),
1360 host_file.disk_state(),
1361 "guest {} disk_state does not match host {} for path {:?} in project {}",
1362 guest_user_id,
1363 host_user_id,
1364 guest_file.path(),
1365 project_id,
1366 );
1367 }
1368 (None, None) => {}
1369 (None, _) => panic!("host's file is None, guest's isn't"),
1370 (_, None) => panic!("guest's file is None, hosts's isn't"),
1371 }
1372
1373 let host_diff_base = host_project.read_with(host_cx, |project, cx| {
1374 project
1375 .git_store()
1376 .read(cx)
1377 .get_unstaged_diff(host_buffer.read(cx).remote_id(), cx)
1378 .unwrap()
1379 .read(cx)
1380 .base_text_string()
1381 });
1382 let guest_diff_base = guest_project.read_with(client_cx, |project, cx| {
1383 project
1384 .git_store()
1385 .read(cx)
1386 .get_unstaged_diff(guest_buffer.read(cx).remote_id(), cx)
1387 .unwrap()
1388 .read(cx)
1389 .base_text_string()
1390 });
1391 assert_eq!(
1392 guest_diff_base, host_diff_base,
1393 "guest {} diff base does not match host's for path {path:?} in project {project_id}",
1394 client.username
1395 );
1396
1397 let host_saved_version =
1398 host_buffer.read_with(host_cx, |b, _| b.saved_version().clone());
1399 let guest_saved_version =
1400 guest_buffer.read_with(client_cx, |b, _| b.saved_version().clone());
1401 assert_eq!(
1402 guest_saved_version, host_saved_version,
1403 "guest {} saved version does not match host's for path {path:?} in project {project_id}",
1404 client.username
1405 );
1406
1407 let host_is_dirty = host_buffer.read_with(host_cx, |b, _| b.is_dirty());
1408 let guest_is_dirty = guest_buffer.read_with(client_cx, |b, _| b.is_dirty());
1409 assert_eq!(
1410 guest_is_dirty, host_is_dirty,
1411 "guest {} dirty state does not match host's for path {path:?} in project {project_id}",
1412 client.username
1413 );
1414
1415 let host_saved_mtime = host_buffer.read_with(host_cx, |b, _| b.saved_mtime());
1416 let guest_saved_mtime =
1417 guest_buffer.read_with(client_cx, |b, _| b.saved_mtime());
1418 assert_eq!(
1419 guest_saved_mtime, host_saved_mtime,
1420 "guest {} saved mtime does not match host's for path {path:?} in project {project_id}",
1421 client.username
1422 );
1423
1424 let host_is_dirty = host_buffer.read_with(host_cx, |b, _| b.is_dirty());
1425 let guest_is_dirty = guest_buffer.read_with(client_cx, |b, _| b.is_dirty());
1426 assert_eq!(
1427 guest_is_dirty, host_is_dirty,
1428 "guest {} dirty status does not match host's for path {path:?} in project {project_id}",
1429 client.username
1430 );
1431
1432 let host_has_conflict = host_buffer.read_with(host_cx, |b, _| b.has_conflict());
1433 let guest_has_conflict =
1434 guest_buffer.read_with(client_cx, |b, _| b.has_conflict());
1435 assert_eq!(
1436 guest_has_conflict, host_has_conflict,
1437 "guest {} conflict status does not match host's for path {path:?} in project {project_id}",
1438 client.username
1439 );
1440 }
1441 }
1442 }
1443 }
1444}
1445
1446fn generate_git_operation(rng: &mut StdRng, client: &TestClient) -> GitOperation {
1447 fn generate_file_paths(
1448 repo_path: &Path,
1449 rng: &mut StdRng,
1450 client: &TestClient,
1451 ) -> Vec<RelPathBuf> {
1452 let mut paths = client
1453 .fs()
1454 .files()
1455 .into_iter()
1456 .filter(|path| path.starts_with(repo_path))
1457 .collect::<Vec<_>>();
1458
1459 let count = rng.random_range(0..=paths.len());
1460 paths.shuffle(rng);
1461 paths.truncate(count);
1462
1463 paths
1464 .iter()
1465 .map(|path| {
1466 RelPath::new(path.strip_prefix(repo_path).unwrap(), PathStyle::local())
1467 .unwrap()
1468 .to_rel_path_buf()
1469 })
1470 .collect::<Vec<_>>()
1471 }
1472
1473 let repo_path = client.fs().directories(false).choose(rng).unwrap().clone();
1474
1475 match rng.random_range(0..100_u32) {
1476 0..=25 => {
1477 let file_paths = generate_file_paths(&repo_path, rng, client);
1478
1479 let contents = file_paths
1480 .into_iter()
1481 .map(|path| (path, distr::Alphanumeric.sample_string(rng, 16)))
1482 .collect();
1483
1484 GitOperation::WriteGitIndex {
1485 repo_path,
1486 contents,
1487 }
1488 }
1489 26..=63 => {
1490 let new_branch =
1491 (rng.random_range(0..10) > 3).then(|| distr::Alphanumeric.sample_string(rng, 8));
1492
1493 GitOperation::WriteGitBranch {
1494 repo_path,
1495 new_branch,
1496 }
1497 }
1498 64..=100 => {
1499 let file_paths = generate_file_paths(&repo_path, rng, client);
1500 let statuses = file_paths
1501 .into_iter()
1502 .map(|path| (path, gen_status(rng)))
1503 .collect::<Vec<_>>();
1504 GitOperation::WriteGitStatuses {
1505 repo_path,
1506 statuses,
1507 }
1508 }
1509 _ => unreachable!(),
1510 }
1511}
1512
1513fn buffer_for_full_path(
1514 client: &TestClient,
1515 project: &Entity<Project>,
1516 full_path: &RelPath,
1517 cx: &TestAppContext,
1518) -> Option<Entity<language::Buffer>> {
1519 client
1520 .buffers_for_project(project)
1521 .iter()
1522 .find(|buffer| {
1523 buffer.read_with(cx, |buffer, cx| {
1524 let file = buffer.file().unwrap();
1525 let Some(worktree) = project.read(cx).worktree_for_id(file.worktree_id(cx), cx)
1526 else {
1527 return false;
1528 };
1529 worktree.read(cx).root_name().join(&file.path()).as_ref() == full_path
1530 })
1531 })
1532 .cloned()
1533}
1534
1535fn project_for_root_name(
1536 client: &TestClient,
1537 root_name: &str,
1538 cx: &TestAppContext,
1539) -> Option<Entity<Project>> {
1540 if let Some(ix) = project_ix_for_root_name(client.local_projects().deref(), root_name, cx) {
1541 return Some(client.local_projects()[ix].clone());
1542 }
1543 if let Some(ix) = project_ix_for_root_name(client.dev_server_projects().deref(), root_name, cx)
1544 {
1545 return Some(client.dev_server_projects()[ix].clone());
1546 }
1547 None
1548}
1549
1550fn project_ix_for_root_name(
1551 projects: &[Entity<Project>],
1552 root_name: &str,
1553 cx: &TestAppContext,
1554) -> Option<usize> {
1555 projects.iter().position(|project| {
1556 project.read_with(cx, |project, cx| {
1557 let worktree = project.visible_worktrees(cx).next().unwrap();
1558 worktree.read(cx).root_name() == root_name
1559 })
1560 })
1561}
1562
1563fn root_name_for_project(project: &Entity<Project>, cx: &TestAppContext) -> String {
1564 project.read_with(cx, |project, cx| {
1565 project
1566 .visible_worktrees(cx)
1567 .next()
1568 .unwrap()
1569 .read(cx)
1570 .root_name_str()
1571 .to_string()
1572 })
1573}
1574
1575fn project_path_for_full_path(
1576 project: &Entity<Project>,
1577 full_path: &RelPath,
1578 cx: &TestAppContext,
1579) -> Option<ProjectPath> {
1580 let mut components = full_path.components();
1581 let root_name = components.next().unwrap();
1582 let path = components.rest().into();
1583 let worktree_id = project.read_with(cx, |project, cx| {
1584 project.worktrees(cx).find_map(|worktree| {
1585 let worktree = worktree.read(cx);
1586 if worktree.root_name_str() == root_name {
1587 Some(worktree.id())
1588 } else {
1589 None
1590 }
1591 })
1592 })?;
1593 Some(ProjectPath { worktree_id, path })
1594}
1595
1596async fn ensure_project_shared(
1597 project: &Entity<Project>,
1598 client: &TestClient,
1599 cx: &mut TestAppContext,
1600) {
1601 let first_root_name = root_name_for_project(project, cx);
1602 let active_call = cx.read(ActiveCall::global);
1603 if active_call.read_with(cx, |call, _| call.room().is_some())
1604 && project.read_with(cx, |project, _| project.is_local() && !project.is_shared())
1605 {
1606 match active_call
1607 .update(cx, |call, cx| call.share_project(project.clone(), cx))
1608 .await
1609 {
1610 Ok(project_id) => {
1611 log::info!(
1612 "{}: shared project {} with id {}",
1613 client.username,
1614 first_root_name,
1615 project_id
1616 );
1617 }
1618 Err(error) => {
1619 log::error!(
1620 "{}: error sharing project {}: {:?}",
1621 client.username,
1622 first_root_name,
1623 error
1624 );
1625 }
1626 }
1627 }
1628}
1629
1630fn choose_random_project(client: &TestClient, rng: &mut StdRng) -> Option<Entity<Project>> {
1631 client
1632 .local_projects()
1633 .deref()
1634 .iter()
1635 .chain(client.dev_server_projects().iter())
1636 .choose(rng)
1637 .cloned()
1638}
1639
1640fn gen_file_name(rng: &mut StdRng) -> String {
1641 let mut name = String::new();
1642 for _ in 0..10 {
1643 let letter = rng.random_range('a'..='z');
1644 name.push(letter);
1645 }
1646 name
1647}
1648
1649fn gen_status(rng: &mut StdRng) -> FileStatus {
1650 fn gen_tracked_status(rng: &mut StdRng) -> TrackedStatus {
1651 match rng.random_range(0..3) {
1652 0 => TrackedStatus {
1653 index_status: StatusCode::Unmodified,
1654 worktree_status: StatusCode::Unmodified,
1655 },
1656 1 => TrackedStatus {
1657 index_status: StatusCode::Modified,
1658 worktree_status: StatusCode::Modified,
1659 },
1660 2 => TrackedStatus {
1661 index_status: StatusCode::Added,
1662 worktree_status: StatusCode::Modified,
1663 },
1664 3 => TrackedStatus {
1665 index_status: StatusCode::Added,
1666 worktree_status: StatusCode::Unmodified,
1667 },
1668 _ => unreachable!(),
1669 }
1670 }
1671
1672 fn gen_unmerged_status_code(rng: &mut StdRng) -> UnmergedStatusCode {
1673 match rng.random_range(0..3) {
1674 0 => UnmergedStatusCode::Updated,
1675 1 => UnmergedStatusCode::Added,
1676 2 => UnmergedStatusCode::Deleted,
1677 _ => unreachable!(),
1678 }
1679 }
1680
1681 match rng.random_range(0..2) {
1682 0 => FileStatus::Unmerged(UnmergedStatus {
1683 first_head: gen_unmerged_status_code(rng),
1684 second_head: gen_unmerged_status_code(rng),
1685 }),
1686 1 => FileStatus::Tracked(gen_tracked_status(rng)),
1687 _ => unreachable!(),
1688 }
1689}