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