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