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