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, Project, ProjectPath, SearchResult, 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 = project.read_with(cx, |project, _| project.is_local());
302 let worktree = project.read_with(cx, |project, cx| {
303 project
304 .worktrees()
305 .filter(|worktree| {
306 let worktree = worktree.read(cx);
307 worktree.is_visible()
308 && worktree.entries(false, 0).any(|e| e.is_file())
309 && worktree.root_entry().map_or(false, |e| e.is_dir())
310 })
311 .choose(rng)
312 });
313 let Some(worktree) = worktree else { continue };
314 let is_dir = rng.gen::<bool>();
315 let mut full_path =
316 worktree.read_with(cx, |w, _| PathBuf::from(w.root_name()));
317 full_path.push(gen_file_name(rng));
318 if !is_dir {
319 full_path.set_extension("rs");
320 }
321 break ClientOperation::CreateWorktreeEntry {
322 project_root_name,
323 is_local,
324 full_path,
325 is_dir,
326 };
327 }
328 },
329 },
330
331 // Query and mutate buffers
332 60..=90 => {
333 let Some(project) = choose_random_project(client, rng) else {
334 continue;
335 };
336 let project_root_name = root_name_for_project(&project, cx);
337 let is_local = project.read_with(cx, |project, _| project.is_local());
338
339 match rng.gen_range(0..100_u32) {
340 // Manipulate an existing buffer
341 0..=70 => {
342 let Some(buffer) = client
343 .buffers_for_project(&project)
344 .iter()
345 .choose(rng)
346 .cloned()
347 else {
348 continue;
349 };
350
351 let full_path = buffer
352 .read_with(cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
353
354 match rng.gen_range(0..100_u32) {
355 // Close the buffer
356 0..=15 => {
357 break ClientOperation::CloseBuffer {
358 project_root_name,
359 is_local,
360 full_path,
361 };
362 }
363 // Save the buffer
364 16..=29 if buffer.read_with(cx, |b, _| b.is_dirty()) => {
365 let detach = rng.gen_bool(0.3);
366 break ClientOperation::SaveBuffer {
367 project_root_name,
368 is_local,
369 full_path,
370 detach,
371 };
372 }
373 // Edit the buffer
374 30..=69 => {
375 let edits = buffer
376 .read_with(cx, |buffer, _| buffer.get_random_edits(rng, 3));
377 break ClientOperation::EditBuffer {
378 project_root_name,
379 is_local,
380 full_path,
381 edits,
382 };
383 }
384 // Make an LSP request
385 _ => {
386 let offset = buffer.read_with(cx, |buffer, _| {
387 buffer.clip_offset(
388 rng.gen_range(0..=buffer.len()),
389 language::Bias::Left,
390 )
391 });
392 let detach = rng.gen();
393 break ClientOperation::RequestLspDataInBuffer {
394 project_root_name,
395 full_path,
396 offset,
397 is_local,
398 kind: match rng.gen_range(0..5_u32) {
399 0 => LspRequestKind::Rename,
400 1 => LspRequestKind::Highlights,
401 2 => LspRequestKind::Definition,
402 3 => LspRequestKind::CodeAction,
403 4.. => LspRequestKind::Completion,
404 },
405 detach,
406 };
407 }
408 }
409 }
410
411 71..=80 => {
412 let query = rng.gen_range('a'..='z').to_string();
413 let detach = rng.gen_bool(0.3);
414 break ClientOperation::SearchProject {
415 project_root_name,
416 is_local,
417 query,
418 detach,
419 };
420 }
421
422 // Open a buffer
423 81.. => {
424 let worktree = project.read_with(cx, |project, cx| {
425 project
426 .worktrees()
427 .filter(|worktree| {
428 let worktree = worktree.read(cx);
429 worktree.is_visible()
430 && worktree.entries(false, 0).any(|e| e.is_file())
431 })
432 .choose(rng)
433 });
434 let Some(worktree) = worktree else { continue };
435 let full_path = worktree.read_with(cx, |worktree, _| {
436 let entry = worktree
437 .entries(false, 0)
438 .filter(|e| e.is_file())
439 .choose(rng)
440 .unwrap();
441 if entry.path.as_ref() == Path::new("") {
442 Path::new(worktree.root_name()).into()
443 } else {
444 Path::new(worktree.root_name()).join(&entry.path)
445 }
446 });
447 break ClientOperation::OpenBuffer {
448 project_root_name,
449 is_local,
450 full_path,
451 };
452 }
453 }
454 }
455
456 // Update a git related action
457 91..=95 => {
458 break ClientOperation::GitOperation {
459 operation: generate_git_operation(rng, client),
460 };
461 }
462
463 // Create or update a file or directory
464 96.. => {
465 let is_dir = rng.gen::<bool>();
466 let content;
467 let mut path;
468 let dir_paths = client.fs().directories(false);
469
470 if is_dir {
471 content = String::new();
472 path = dir_paths.choose(rng).unwrap().clone();
473 path.push(gen_file_name(rng));
474 } else {
475 content = Alphanumeric.sample_string(rng, 16);
476
477 // Create a new file or overwrite an existing file
478 let file_paths = client.fs().files();
479 if file_paths.is_empty() || rng.gen_bool(0.5) {
480 path = dir_paths.choose(rng).unwrap().clone();
481 path.push(gen_file_name(rng));
482 path.set_extension("rs");
483 } else {
484 path = file_paths.choose(rng).unwrap().clone()
485 };
486 }
487 break ClientOperation::WriteFsEntry {
488 path,
489 is_dir,
490 content,
491 };
492 }
493 }
494 }
495 }
496
497 async fn apply_operation(
498 client: &TestClient,
499 operation: ClientOperation,
500 cx: &mut TestAppContext,
501 ) -> Result<(), TestError> {
502 match operation {
503 ClientOperation::AcceptIncomingCall => {
504 let active_call = cx.read(ActiveCall::global);
505 if active_call.read_with(cx, |call, _| call.incoming().borrow().is_none()) {
506 Err(TestError::Inapplicable)?;
507 }
508
509 log::info!("{}: accepting incoming call", client.username);
510 active_call
511 .update(cx, |call, cx| call.accept_incoming(cx))
512 .await?;
513 }
514
515 ClientOperation::RejectIncomingCall => {
516 let active_call = cx.read(ActiveCall::global);
517 if active_call.read_with(cx, |call, _| call.incoming().borrow().is_none()) {
518 Err(TestError::Inapplicable)?;
519 }
520
521 log::info!("{}: declining incoming call", client.username);
522 active_call.update(cx, |call, cx| call.decline_incoming(cx))?;
523 }
524
525 ClientOperation::LeaveCall => {
526 let active_call = cx.read(ActiveCall::global);
527 if active_call.read_with(cx, |call, _| call.room().is_none()) {
528 Err(TestError::Inapplicable)?;
529 }
530
531 log::info!("{}: hanging up", client.username);
532 active_call.update(cx, |call, cx| call.hang_up(cx)).await?;
533 }
534
535 ClientOperation::InviteContactToCall { user_id } => {
536 let active_call = cx.read(ActiveCall::global);
537
538 log::info!("{}: inviting {}", client.username, user_id,);
539 active_call
540 .update(cx, |call, cx| call.invite(user_id.to_proto(), None, cx))
541 .await
542 .log_err();
543 }
544
545 ClientOperation::OpenLocalProject { first_root_name } => {
546 log::info!(
547 "{}: opening local project at {:?}",
548 client.username,
549 first_root_name
550 );
551
552 let root_path = Path::new("/").join(&first_root_name);
553 client.fs().create_dir(&root_path).await.unwrap();
554 client
555 .fs()
556 .create_file(&root_path.join("main.rs"), Default::default())
557 .await
558 .unwrap();
559 let project = client.build_local_project(root_path, cx).await.0;
560 ensure_project_shared(&project, client, cx).await;
561 client.local_projects_mut().push(project.clone());
562 }
563
564 ClientOperation::AddWorktreeToProject {
565 project_root_name,
566 new_root_path,
567 } => {
568 let project = project_for_root_name(client, &project_root_name, cx)
569 .ok_or(TestError::Inapplicable)?;
570
571 log::info!(
572 "{}: finding/creating local worktree at {:?} to project with root path {}",
573 client.username,
574 new_root_path,
575 project_root_name
576 );
577
578 ensure_project_shared(&project, client, cx).await;
579 if !client.fs().paths(false).contains(&new_root_path) {
580 client.fs().create_dir(&new_root_path).await.unwrap();
581 }
582 project
583 .update(cx, |project, cx| {
584 project.find_or_create_worktree(&new_root_path, true, cx)
585 })
586 .await
587 .unwrap();
588 }
589
590 ClientOperation::CloseRemoteProject { project_root_name } => {
591 let project = project_for_root_name(client, &project_root_name, cx)
592 .ok_or(TestError::Inapplicable)?;
593
594 log::info!(
595 "{}: closing remote project with root path {}",
596 client.username,
597 project_root_name,
598 );
599
600 let ix = client
601 .dev_server_projects()
602 .iter()
603 .position(|p| p == &project)
604 .unwrap();
605 cx.update(|_| {
606 client.dev_server_projects_mut().remove(ix);
607 client.buffers().retain(|p, _| *p != project);
608 drop(project);
609 });
610 }
611
612 ClientOperation::OpenRemoteProject {
613 host_id,
614 first_root_name,
615 } => {
616 let active_call = cx.read(ActiveCall::global);
617 let project = active_call
618 .update(cx, |call, cx| {
619 let room = call.room().cloned()?;
620 let participant = room
621 .read(cx)
622 .remote_participants()
623 .get(&host_id.to_proto())?;
624 let project_id = participant
625 .projects
626 .iter()
627 .find(|project| project.worktree_root_names[0] == first_root_name)?
628 .id;
629 Some(room.update(cx, |room, cx| {
630 room.join_project(
631 project_id,
632 client.language_registry().clone(),
633 FakeFs::new(cx.background_executor().clone()),
634 cx,
635 )
636 }))
637 })
638 .ok_or(TestError::Inapplicable)?;
639
640 log::info!(
641 "{}: joining remote project of user {}, root name {}",
642 client.username,
643 host_id,
644 first_root_name,
645 );
646
647 let project = project.await?;
648 client.dev_server_projects_mut().push(project.clone());
649 }
650
651 ClientOperation::CreateWorktreeEntry {
652 project_root_name,
653 is_local,
654 full_path,
655 is_dir,
656 } => {
657 let project = project_for_root_name(client, &project_root_name, cx)
658 .ok_or(TestError::Inapplicable)?;
659 let project_path = project_path_for_full_path(&project, &full_path, cx)
660 .ok_or(TestError::Inapplicable)?;
661
662 log::info!(
663 "{}: creating {} at path {:?} in {} project {}",
664 client.username,
665 if is_dir { "dir" } else { "file" },
666 full_path,
667 if is_local { "local" } else { "remote" },
668 project_root_name,
669 );
670
671 ensure_project_shared(&project, client, cx).await;
672 project
673 .update(cx, |p, cx| p.create_entry(project_path, is_dir, cx))
674 .await?;
675 }
676
677 ClientOperation::OpenBuffer {
678 project_root_name,
679 is_local,
680 full_path,
681 } => {
682 let project = project_for_root_name(client, &project_root_name, cx)
683 .ok_or(TestError::Inapplicable)?;
684 let project_path = project_path_for_full_path(&project, &full_path, cx)
685 .ok_or(TestError::Inapplicable)?;
686
687 log::info!(
688 "{}: opening buffer {:?} in {} project {}",
689 client.username,
690 full_path,
691 if is_local { "local" } else { "remote" },
692 project_root_name,
693 );
694
695 ensure_project_shared(&project, client, cx).await;
696 let buffer = project
697 .update(cx, |project, cx| project.open_buffer(project_path, cx))
698 .await?;
699 client.buffers_for_project(&project).insert(buffer);
700 }
701
702 ClientOperation::EditBuffer {
703 project_root_name,
704 is_local,
705 full_path,
706 edits,
707 } => {
708 let project = project_for_root_name(client, &project_root_name, cx)
709 .ok_or(TestError::Inapplicable)?;
710 let buffer = buffer_for_full_path(client, &project, &full_path, cx)
711 .ok_or(TestError::Inapplicable)?;
712
713 log::info!(
714 "{}: editing buffer {:?} in {} project {} with {:?}",
715 client.username,
716 full_path,
717 if is_local { "local" } else { "remote" },
718 project_root_name,
719 edits
720 );
721
722 ensure_project_shared(&project, client, cx).await;
723 buffer.update(cx, |buffer, cx| {
724 let snapshot = buffer.snapshot();
725 buffer.edit(
726 edits.into_iter().map(|(range, text)| {
727 let start = snapshot.clip_offset(range.start, Bias::Left);
728 let end = snapshot.clip_offset(range.end, Bias::Right);
729 (start..end, text)
730 }),
731 None,
732 cx,
733 );
734 });
735 }
736
737 ClientOperation::CloseBuffer {
738 project_root_name,
739 is_local,
740 full_path,
741 } => {
742 let project = project_for_root_name(client, &project_root_name, cx)
743 .ok_or(TestError::Inapplicable)?;
744 let buffer = buffer_for_full_path(client, &project, &full_path, cx)
745 .ok_or(TestError::Inapplicable)?;
746
747 log::info!(
748 "{}: closing buffer {:?} in {} project {}",
749 client.username,
750 full_path,
751 if is_local { "local" } else { "remote" },
752 project_root_name
753 );
754
755 ensure_project_shared(&project, client, cx).await;
756 cx.update(|_| {
757 client.buffers_for_project(&project).remove(&buffer);
758 drop(buffer);
759 });
760 }
761
762 ClientOperation::SaveBuffer {
763 project_root_name,
764 is_local,
765 full_path,
766 detach,
767 } => {
768 let project = project_for_root_name(client, &project_root_name, cx)
769 .ok_or(TestError::Inapplicable)?;
770 let buffer = buffer_for_full_path(client, &project, &full_path, cx)
771 .ok_or(TestError::Inapplicable)?;
772
773 log::info!(
774 "{}: saving buffer {:?} in {} project {}, {}",
775 client.username,
776 full_path,
777 if is_local { "local" } else { "remote" },
778 project_root_name,
779 if detach { "detaching" } else { "awaiting" }
780 );
781
782 ensure_project_shared(&project, client, cx).await;
783 let requested_version = buffer.read_with(cx, |buffer, _| buffer.version());
784 let save =
785 project.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx));
786 let save = cx.spawn(|cx| async move {
787 save.await
788 .map_err(|err| anyhow!("save request failed: {:?}", err))?;
789 assert!(buffer
790 .read_with(&cx, |buffer, _| { buffer.saved_version().to_owned() })
791 .expect("App should not be dropped")
792 .observed_all(&requested_version));
793 anyhow::Ok(())
794 });
795 if detach {
796 cx.update(|cx| save.detach_and_log_err(cx));
797 } else {
798 save.await?;
799 }
800 }
801
802 ClientOperation::RequestLspDataInBuffer {
803 project_root_name,
804 is_local,
805 full_path,
806 offset,
807 kind,
808 detach,
809 } => {
810 let project = project_for_root_name(client, &project_root_name, cx)
811 .ok_or(TestError::Inapplicable)?;
812 let buffer = buffer_for_full_path(client, &project, &full_path, cx)
813 .ok_or(TestError::Inapplicable)?;
814
815 log::info!(
816 "{}: request LSP {:?} for buffer {:?} in {} project {}, {}",
817 client.username,
818 kind,
819 full_path,
820 if is_local { "local" } else { "remote" },
821 project_root_name,
822 if detach { "detaching" } else { "awaiting" }
823 );
824
825 use futures::{FutureExt as _, TryFutureExt as _};
826 let offset = buffer.read_with(cx, |b, _| b.clip_offset(offset, Bias::Left));
827
828 let process_lsp_request = project.update(cx, |project, cx| match kind {
829 LspRequestKind::Rename => project
830 .prepare_rename(buffer, offset, cx)
831 .map_ok(|_| ())
832 .boxed(),
833 LspRequestKind::Completion => project
834 .completions(&buffer, offset, DEFAULT_COMPLETION_CONTEXT, cx)
835 .map_ok(|_| ())
836 .boxed(),
837 LspRequestKind::CodeAction => project
838 .code_actions(&buffer, offset..offset, cx)
839 .map(|_| Ok(()))
840 .boxed(),
841 LspRequestKind::Definition => project
842 .definition(&buffer, offset, cx)
843 .map_ok(|_| ())
844 .boxed(),
845 LspRequestKind::Highlights => project
846 .document_highlights(&buffer, offset, cx)
847 .map_ok(|_| ())
848 .boxed(),
849 });
850 let request = cx.foreground_executor().spawn(process_lsp_request);
851 if detach {
852 request.detach();
853 } else {
854 request.await?;
855 }
856 }
857
858 ClientOperation::SearchProject {
859 project_root_name,
860 is_local,
861 query,
862 detach,
863 } => {
864 let project = project_for_root_name(client, &project_root_name, cx)
865 .ok_or(TestError::Inapplicable)?;
866
867 log::info!(
868 "{}: search {} project {} for {:?}, {}",
869 client.username,
870 if is_local { "local" } else { "remote" },
871 project_root_name,
872 query,
873 if detach { "detaching" } else { "awaiting" }
874 );
875
876 let mut search = project.update(cx, |project, cx| {
877 project.search(
878 SearchQuery::text(
879 query,
880 false,
881 false,
882 false,
883 Default::default(),
884 Default::default(),
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 Some(result) = search.next().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 .iter()
957 .map(|(path, contents)| (path.as_path(), contents.clone()))
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_adapter(
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()),
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() {
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()
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()
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().collect::<Vec<_>>(), host_snapshot.repositories().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, _| {
1257 project.is_local() || project.is_disconnected()
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.is_deleted(), host_file.is_deleted());
1326 assert_eq!(
1327 guest_file.mtime(),
1328 host_file.mtime(),
1329 "guest {} mtime does not match host {} for path {:?} in project {}",
1330 guest_user_id,
1331 host_user_id,
1332 guest_file.path(),
1333 project_id,
1334 );
1335 }
1336 (None, None) => {}
1337 (None, _) => panic!("host's file is None, guest's isn't"),
1338 (_, None) => panic!("guest's file is None, hosts's isn't"),
1339 }
1340
1341 let host_diff_base = host_buffer
1342 .read_with(host_cx, |b, _| b.diff_base().map(ToString::to_string));
1343 let guest_diff_base = guest_buffer
1344 .read_with(client_cx, |b, _| b.diff_base().map(ToString::to_string));
1345 assert_eq!(
1346 guest_diff_base, host_diff_base,
1347 "guest {} diff base does not match host's for path {path:?} in project {project_id}",
1348 client.username
1349 );
1350
1351 let host_saved_version =
1352 host_buffer.read_with(host_cx, |b, _| b.saved_version().clone());
1353 let guest_saved_version =
1354 guest_buffer.read_with(client_cx, |b, _| b.saved_version().clone());
1355 assert_eq!(
1356 guest_saved_version, host_saved_version,
1357 "guest {} saved version does not match host's for path {path:?} in project {project_id}",
1358 client.username
1359 );
1360
1361 let host_is_dirty = host_buffer.read_with(host_cx, |b, _| b.is_dirty());
1362 let guest_is_dirty = guest_buffer.read_with(client_cx, |b, _| b.is_dirty());
1363 assert_eq!(
1364 guest_is_dirty, host_is_dirty,
1365 "guest {} dirty state does not match host's for path {path:?} in project {project_id}",
1366 client.username
1367 );
1368
1369 let host_saved_mtime = host_buffer.read_with(host_cx, |b, _| b.saved_mtime());
1370 let guest_saved_mtime =
1371 guest_buffer.read_with(client_cx, |b, _| b.saved_mtime());
1372 assert_eq!(
1373 guest_saved_mtime, host_saved_mtime,
1374 "guest {} saved mtime does not match host's for path {path:?} in project {project_id}",
1375 client.username
1376 );
1377
1378 let host_is_dirty = host_buffer.read_with(host_cx, |b, _| b.is_dirty());
1379 let guest_is_dirty = guest_buffer.read_with(client_cx, |b, _| b.is_dirty());
1380 assert_eq!(guest_is_dirty, host_is_dirty,
1381 "guest {} dirty status does not match host's for path {path:?} in project {project_id}",
1382 client.username
1383 );
1384
1385 let host_has_conflict = host_buffer.read_with(host_cx, |b, _| b.has_conflict());
1386 let guest_has_conflict =
1387 guest_buffer.read_with(client_cx, |b, _| b.has_conflict());
1388 assert_eq!(guest_has_conflict, host_has_conflict,
1389 "guest {} conflict status does not match host's for path {path:?} in project {project_id}",
1390 client.username
1391 );
1392 }
1393 }
1394 }
1395 }
1396}
1397
1398fn generate_git_operation(rng: &mut StdRng, client: &TestClient) -> GitOperation {
1399 fn generate_file_paths(
1400 repo_path: &Path,
1401 rng: &mut StdRng,
1402 client: &TestClient,
1403 ) -> Vec<PathBuf> {
1404 let mut paths = client
1405 .fs()
1406 .files()
1407 .into_iter()
1408 .filter(|path| path.starts_with(repo_path))
1409 .collect::<Vec<_>>();
1410
1411 let count = rng.gen_range(0..=paths.len());
1412 paths.shuffle(rng);
1413 paths.truncate(count);
1414
1415 paths
1416 .iter()
1417 .map(|path| path.strip_prefix(repo_path).unwrap().to_path_buf())
1418 .collect::<Vec<_>>()
1419 }
1420
1421 let repo_path = client.fs().directories(false).choose(rng).unwrap().clone();
1422
1423 match rng.gen_range(0..100_u32) {
1424 0..=25 => {
1425 let file_paths = generate_file_paths(&repo_path, rng, client);
1426
1427 let contents = file_paths
1428 .into_iter()
1429 .map(|path| (path, Alphanumeric.sample_string(rng, 16)))
1430 .collect();
1431
1432 GitOperation::WriteGitIndex {
1433 repo_path,
1434 contents,
1435 }
1436 }
1437 26..=63 => {
1438 let new_branch = (rng.gen_range(0..10) > 3).then(|| Alphanumeric.sample_string(rng, 8));
1439
1440 GitOperation::WriteGitBranch {
1441 repo_path,
1442 new_branch,
1443 }
1444 }
1445 64..=100 => {
1446 let file_paths = generate_file_paths(&repo_path, rng, client);
1447
1448 let statuses = file_paths
1449 .into_iter()
1450 .map(|paths| {
1451 (
1452 paths,
1453 match rng.gen_range(0..3_u32) {
1454 0 => GitFileStatus::Added,
1455 1 => GitFileStatus::Modified,
1456 2 => GitFileStatus::Conflict,
1457 _ => unreachable!(),
1458 },
1459 )
1460 })
1461 .collect::<Vec<_>>();
1462
1463 let git_operation = rng.gen::<bool>();
1464
1465 GitOperation::WriteGitStatuses {
1466 repo_path,
1467 statuses,
1468 git_operation,
1469 }
1470 }
1471 _ => unreachable!(),
1472 }
1473}
1474
1475fn buffer_for_full_path(
1476 client: &TestClient,
1477 project: &Model<Project>,
1478 full_path: &PathBuf,
1479 cx: &TestAppContext,
1480) -> Option<Model<language::Buffer>> {
1481 client
1482 .buffers_for_project(project)
1483 .iter()
1484 .find(|buffer| {
1485 buffer.read_with(cx, |buffer, cx| {
1486 buffer.file().unwrap().full_path(cx) == *full_path
1487 })
1488 })
1489 .cloned()
1490}
1491
1492fn project_for_root_name(
1493 client: &TestClient,
1494 root_name: &str,
1495 cx: &TestAppContext,
1496) -> Option<Model<Project>> {
1497 if let Some(ix) = project_ix_for_root_name(client.local_projects().deref(), root_name, cx) {
1498 return Some(client.local_projects()[ix].clone());
1499 }
1500 if let Some(ix) = project_ix_for_root_name(client.dev_server_projects().deref(), root_name, cx)
1501 {
1502 return Some(client.dev_server_projects()[ix].clone());
1503 }
1504 None
1505}
1506
1507fn project_ix_for_root_name(
1508 projects: &[Model<Project>],
1509 root_name: &str,
1510 cx: &TestAppContext,
1511) -> Option<usize> {
1512 projects.iter().position(|project| {
1513 project.read_with(cx, |project, cx| {
1514 let worktree = project.visible_worktrees(cx).next().unwrap();
1515 worktree.read(cx).root_name() == root_name
1516 })
1517 })
1518}
1519
1520fn root_name_for_project(project: &Model<Project>, cx: &TestAppContext) -> String {
1521 project.read_with(cx, |project, cx| {
1522 project
1523 .visible_worktrees(cx)
1524 .next()
1525 .unwrap()
1526 .read(cx)
1527 .root_name()
1528 .to_string()
1529 })
1530}
1531
1532fn project_path_for_full_path(
1533 project: &Model<Project>,
1534 full_path: &Path,
1535 cx: &TestAppContext,
1536) -> Option<ProjectPath> {
1537 let mut components = full_path.components();
1538 let root_name = components.next().unwrap().as_os_str().to_str().unwrap();
1539 let path = components.as_path().into();
1540 let worktree_id = project.read_with(cx, |project, cx| {
1541 project.worktrees().find_map(|worktree| {
1542 let worktree = worktree.read(cx);
1543 if worktree.root_name() == root_name {
1544 Some(worktree.id())
1545 } else {
1546 None
1547 }
1548 })
1549 })?;
1550 Some(ProjectPath { worktree_id, path })
1551}
1552
1553async fn ensure_project_shared(
1554 project: &Model<Project>,
1555 client: &TestClient,
1556 cx: &mut TestAppContext,
1557) {
1558 let first_root_name = root_name_for_project(project, cx);
1559 let active_call = cx.read(ActiveCall::global);
1560 if active_call.read_with(cx, |call, _| call.room().is_some())
1561 && project.read_with(cx, |project, _| project.is_local() && !project.is_shared())
1562 {
1563 match active_call
1564 .update(cx, |call, cx| call.share_project(project.clone(), cx))
1565 .await
1566 {
1567 Ok(project_id) => {
1568 log::info!(
1569 "{}: shared project {} with id {}",
1570 client.username,
1571 first_root_name,
1572 project_id
1573 );
1574 }
1575 Err(error) => {
1576 log::error!(
1577 "{}: error sharing project {}: {:?}",
1578 client.username,
1579 first_root_name,
1580 error
1581 );
1582 }
1583 }
1584 }
1585}
1586
1587fn choose_random_project(client: &TestClient, rng: &mut StdRng) -> Option<Model<Project>> {
1588 client
1589 .local_projects()
1590 .deref()
1591 .iter()
1592 .chain(client.dev_server_projects().iter())
1593 .choose(rng)
1594 .cloned()
1595}
1596
1597fn gen_file_name(rng: &mut StdRng) -> String {
1598 let mut name = String::new();
1599 for _ in 0..10 {
1600 let letter = rng.gen_range('a'..='z');
1601 name.push(letter);
1602 }
1603 name
1604}