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