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