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::{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::{Deref, 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 executor: BackgroundExecutor,
35 rng: StdRng,
36) {
37 run_randomized_test::<ProjectCollaborationTest>(cx, executor, 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()
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()
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_executor().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 .expect("App should not be dropped")
787 .observed_all(&requested_version));
788 anyhow::Ok(())
789 });
790 if detach {
791 cx.update(|cx| save.detach_and_log_err(cx));
792 } else {
793 save.await?;
794 }
795 }
796
797 ClientOperation::RequestLspDataInBuffer {
798 project_root_name,
799 is_local,
800 full_path,
801 offset,
802 kind,
803 detach,
804 } => {
805 let project = project_for_root_name(client, &project_root_name, cx)
806 .ok_or(TestError::Inapplicable)?;
807 let buffer = buffer_for_full_path(client, &project, &full_path, cx)
808 .ok_or(TestError::Inapplicable)?;
809
810 log::info!(
811 "{}: request LSP {:?} for buffer {:?} in {} project {}, {}",
812 client.username,
813 kind,
814 full_path,
815 if is_local { "local" } else { "remote" },
816 project_root_name,
817 if detach { "detaching" } else { "awaiting" }
818 );
819
820 use futures::{FutureExt as _, TryFutureExt as _};
821 let offset = buffer.read_with(cx, |b, _| b.clip_offset(offset, Bias::Left));
822
823 let process_lsp_request = project.update(cx, |project, cx| match kind {
824 LspRequestKind::Rename => project
825 .prepare_rename(buffer, offset, cx)
826 .map_ok(|_| ())
827 .boxed(),
828 LspRequestKind::Completion => project
829 .completions(&buffer, offset, cx)
830 .map_ok(|_| ())
831 .boxed(),
832 LspRequestKind::CodeAction => project
833 .code_actions(&buffer, offset..offset, cx)
834 .map_ok(|_| ())
835 .boxed(),
836 LspRequestKind::Definition => project
837 .definition(&buffer, offset, cx)
838 .map_ok(|_| ())
839 .boxed(),
840 LspRequestKind::Highlights => project
841 .document_highlights(&buffer, offset, cx)
842 .map_ok(|_| ())
843 .boxed(),
844 });
845 let request = cx.foreground_executor().spawn(process_lsp_request);
846 if detach {
847 request.detach();
848 } else {
849 request.await?;
850 }
851 }
852
853 ClientOperation::SearchProject {
854 project_root_name,
855 is_local,
856 query,
857 detach,
858 } => {
859 let project = project_for_root_name(client, &project_root_name, cx)
860 .ok_or(TestError::Inapplicable)?;
861
862 log::info!(
863 "{}: search {} project {} for {:?}, {}",
864 client.username,
865 if is_local { "local" } else { "remote" },
866 project_root_name,
867 query,
868 if detach { "detaching" } else { "awaiting" }
869 );
870
871 let mut search = project.update(cx, |project, cx| {
872 project.search(
873 SearchQuery::text(query, false, false, false, Vec::new(), Vec::new())
874 .unwrap(),
875 cx,
876 )
877 });
878 drop(project);
879 let search = cx.executor().spawn(async move {
880 let mut results = HashMap::default();
881 while let Some((buffer, ranges)) = search.next().await {
882 results.entry(buffer).or_insert(ranges);
883 }
884 results
885 });
886 search.await;
887 }
888
889 ClientOperation::WriteFsEntry {
890 path,
891 is_dir,
892 content,
893 } => {
894 if !client
895 .fs()
896 .directories(false)
897 .contains(&path.parent().unwrap().to_owned())
898 {
899 return Err(TestError::Inapplicable);
900 }
901
902 if is_dir {
903 log::info!("{}: creating dir at {:?}", client.username, path);
904 client.fs().create_dir(&path).await.unwrap();
905 } else {
906 let exists = client.fs().metadata(&path).await?.is_some();
907 let verb = if exists { "updating" } else { "creating" };
908 log::info!("{}: {} file at {:?}", verb, client.username, path);
909
910 client
911 .fs()
912 .save(&path, &content.as_str().into(), text::LineEnding::Unix)
913 .await
914 .unwrap();
915 }
916 }
917
918 ClientOperation::GitOperation { operation } => match operation {
919 GitOperation::WriteGitIndex {
920 repo_path,
921 contents,
922 } => {
923 if !client.fs().directories(false).contains(&repo_path) {
924 return Err(TestError::Inapplicable);
925 }
926
927 for (path, _) in contents.iter() {
928 if !client.fs().files().contains(&repo_path.join(path)) {
929 return Err(TestError::Inapplicable);
930 }
931 }
932
933 log::info!(
934 "{}: writing git index for repo {:?}: {:?}",
935 client.username,
936 repo_path,
937 contents
938 );
939
940 let dot_git_dir = repo_path.join(".git");
941 let contents = contents
942 .iter()
943 .map(|(path, contents)| (path.as_path(), contents.clone()))
944 .collect::<Vec<_>>();
945 if client.fs().metadata(&dot_git_dir).await?.is_none() {
946 client.fs().create_dir(&dot_git_dir).await?;
947 }
948 client.fs().set_index_for_repo(&dot_git_dir, &contents);
949 }
950 GitOperation::WriteGitBranch {
951 repo_path,
952 new_branch,
953 } => {
954 if !client.fs().directories(false).contains(&repo_path) {
955 return Err(TestError::Inapplicable);
956 }
957
958 log::info!(
959 "{}: writing git branch for repo {:?}: {:?}",
960 client.username,
961 repo_path,
962 new_branch
963 );
964
965 let dot_git_dir = repo_path.join(".git");
966 if client.fs().metadata(&dot_git_dir).await?.is_none() {
967 client.fs().create_dir(&dot_git_dir).await?;
968 }
969 client
970 .fs()
971 .set_branch_name(&dot_git_dir, new_branch.clone());
972 }
973 GitOperation::WriteGitStatuses {
974 repo_path,
975 statuses,
976 git_operation,
977 } => {
978 if !client.fs().directories(false).contains(&repo_path) {
979 return Err(TestError::Inapplicable);
980 }
981 for (path, _) in statuses.iter() {
982 if !client.fs().files().contains(&repo_path.join(path)) {
983 return Err(TestError::Inapplicable);
984 }
985 }
986
987 log::info!(
988 "{}: writing git statuses for repo {:?}: {:?}",
989 client.username,
990 repo_path,
991 statuses
992 );
993
994 let dot_git_dir = repo_path.join(".git");
995
996 let statuses = statuses
997 .iter()
998 .map(|(path, val)| (path.as_path(), val.clone()))
999 .collect::<Vec<_>>();
1000
1001 if client.fs().metadata(&dot_git_dir).await?.is_none() {
1002 client.fs().create_dir(&dot_git_dir).await?;
1003 }
1004
1005 if git_operation {
1006 client.fs().set_status_for_repo_via_git_operation(
1007 &dot_git_dir,
1008 statuses.as_slice(),
1009 );
1010 } else {
1011 client.fs().set_status_for_repo_via_working_copy_change(
1012 &dot_git_dir,
1013 statuses.as_slice(),
1014 );
1015 }
1016 }
1017 },
1018 }
1019 Ok(())
1020 }
1021
1022 async fn on_client_added(client: &Rc<TestClient>, _: &mut TestAppContext) {
1023 let mut language = Language::new(
1024 LanguageConfig {
1025 name: "Rust".into(),
1026 path_suffixes: vec!["rs".to_string()],
1027 ..Default::default()
1028 },
1029 None,
1030 );
1031 language
1032 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1033 name: "the-fake-language-server",
1034 capabilities: lsp::LanguageServer::full_capabilities(),
1035 initializer: Some(Box::new({
1036 let fs = client.app_state.fs.clone();
1037 move |fake_server: &mut FakeLanguageServer| {
1038 fake_server.handle_request::<lsp::request::Completion, _, _>(
1039 |_, _| async move {
1040 Ok(Some(lsp::CompletionResponse::Array(vec![
1041 lsp::CompletionItem {
1042 text_edit: Some(lsp::CompletionTextEdit::Edit(
1043 lsp::TextEdit {
1044 range: lsp::Range::new(
1045 lsp::Position::new(0, 0),
1046 lsp::Position::new(0, 0),
1047 ),
1048 new_text: "the-new-text".to_string(),
1049 },
1050 )),
1051 ..Default::default()
1052 },
1053 ])))
1054 },
1055 );
1056
1057 fake_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
1058 |_, _| async move {
1059 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
1060 lsp::CodeAction {
1061 title: "the-code-action".to_string(),
1062 ..Default::default()
1063 },
1064 )]))
1065 },
1066 );
1067
1068 fake_server.handle_request::<lsp::request::PrepareRenameRequest, _, _>(
1069 |params, _| async move {
1070 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
1071 params.position,
1072 params.position,
1073 ))))
1074 },
1075 );
1076
1077 fake_server.handle_request::<lsp::request::GotoDefinition, _, _>({
1078 let fs = fs.clone();
1079 move |_, cx| {
1080 let background = cx.background_executor();
1081 let mut rng = background.rng();
1082 let count = rng.gen_range::<usize, _>(1..3);
1083 let files = fs.as_fake().files();
1084 let files = (0..count)
1085 .map(|_| files.choose(&mut rng).unwrap().clone())
1086 .collect::<Vec<_>>();
1087 async move {
1088 log::info!("LSP: Returning definitions in files {:?}", &files);
1089 Ok(Some(lsp::GotoDefinitionResponse::Array(
1090 files
1091 .into_iter()
1092 .map(|file| lsp::Location {
1093 uri: lsp::Url::from_file_path(file).unwrap(),
1094 range: Default::default(),
1095 })
1096 .collect(),
1097 )))
1098 }
1099 }
1100 });
1101
1102 fake_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>(
1103 move |_, cx| {
1104 let mut highlights = Vec::new();
1105 let background = cx.background_executor();
1106 let mut rng = background.rng();
1107
1108 let highlight_count = rng.gen_range(1..=5);
1109 for _ in 0..highlight_count {
1110 let start_row = rng.gen_range(0..100);
1111 let start_column = rng.gen_range(0..100);
1112 let end_row = rng.gen_range(0..100);
1113 let end_column = rng.gen_range(0..100);
1114 let start = PointUtf16::new(start_row, start_column);
1115 let end = PointUtf16::new(end_row, end_column);
1116 let range = if start > end { end..start } else { start..end };
1117 highlights.push(lsp::DocumentHighlight {
1118 range: range_to_lsp(range.clone()),
1119 kind: Some(lsp::DocumentHighlightKind::READ),
1120 });
1121 }
1122 highlights.sort_unstable_by_key(|highlight| {
1123 (highlight.range.start, highlight.range.end)
1124 });
1125 async move { Ok(Some(highlights)) }
1126 },
1127 );
1128 }
1129 })),
1130 ..Default::default()
1131 }))
1132 .await;
1133 client.app_state.languages.add(Arc::new(language));
1134 }
1135
1136 async fn on_quiesce(_: &mut TestServer, clients: &mut [(Rc<TestClient>, TestAppContext)]) {
1137 for (client, client_cx) in clients.iter() {
1138 for guest_project in client.remote_projects().iter() {
1139 guest_project.read_with(client_cx, |guest_project, cx| {
1140 let host_project = clients.iter().find_map(|(client, cx)| {
1141 let project = client
1142 .local_projects()
1143 .iter()
1144 .find(|host_project| {
1145 host_project.read_with(cx, |host_project, _| {
1146 host_project.remote_id() == guest_project.remote_id()
1147 })
1148 })?
1149 .clone();
1150 Some((project, cx))
1151 });
1152
1153 if !guest_project.is_read_only() {
1154 if let Some((host_project, host_cx)) = host_project {
1155 let host_worktree_snapshots =
1156 host_project.read_with(host_cx, |host_project, cx| {
1157 host_project
1158 .worktrees()
1159 .map(|worktree| {
1160 let worktree = worktree.read(cx);
1161 (worktree.id(), worktree.snapshot())
1162 })
1163 .collect::<BTreeMap<_, _>>()
1164 });
1165 let guest_worktree_snapshots = guest_project
1166 .worktrees()
1167 .map(|worktree| {
1168 let worktree = worktree.read(cx);
1169 (worktree.id(), worktree.snapshot())
1170 })
1171 .collect::<BTreeMap<_, _>>();
1172
1173 assert_eq!(
1174 guest_worktree_snapshots.values().map(|w| w.abs_path()).collect::<Vec<_>>(),
1175 host_worktree_snapshots.values().map(|w| w.abs_path()).collect::<Vec<_>>(),
1176 "{} has different worktrees than the host for project {:?}",
1177 client.username, guest_project.remote_id(),
1178 );
1179
1180 for (id, host_snapshot) in &host_worktree_snapshots {
1181 let guest_snapshot = &guest_worktree_snapshots[id];
1182 assert_eq!(
1183 guest_snapshot.root_name(),
1184 host_snapshot.root_name(),
1185 "{} has different root name than the host for worktree {}, project {:?}",
1186 client.username,
1187 id,
1188 guest_project.remote_id(),
1189 );
1190 assert_eq!(
1191 guest_snapshot.abs_path(),
1192 host_snapshot.abs_path(),
1193 "{} has different abs path than the host for worktree {}, project: {:?}",
1194 client.username,
1195 id,
1196 guest_project.remote_id(),
1197 );
1198 assert_eq!(
1199 guest_snapshot.entries(false).collect::<Vec<_>>(),
1200 host_snapshot.entries(false).collect::<Vec<_>>(),
1201 "{} has different snapshot than the host for worktree {:?} ({:?}) and project {:?}",
1202 client.username,
1203 host_snapshot.abs_path(),
1204 id,
1205 guest_project.remote_id(),
1206 );
1207 assert_eq!(guest_snapshot.repositories().collect::<Vec<_>>(), host_snapshot.repositories().collect::<Vec<_>>(),
1208 "{} has different repositories than the host for worktree {:?} and project {:?}",
1209 client.username,
1210 host_snapshot.abs_path(),
1211 guest_project.remote_id(),
1212 );
1213 assert_eq!(guest_snapshot.scan_id(), host_snapshot.scan_id(),
1214 "{} has different scan id than the host for worktree {:?} and project {:?}",
1215 client.username,
1216 host_snapshot.abs_path(),
1217 guest_project.remote_id(),
1218 );
1219 }
1220 }
1221 }
1222
1223 for buffer in guest_project.opened_buffers() {
1224 let buffer = buffer.read(cx);
1225 assert_eq!(
1226 buffer.deferred_ops_len(),
1227 0,
1228 "{} has deferred operations for buffer {:?} in project {:?}",
1229 client.username,
1230 buffer.file().unwrap().full_path(cx),
1231 guest_project.remote_id(),
1232 );
1233 }
1234 });
1235 }
1236
1237 let buffers = client.buffers().clone();
1238 for (guest_project, guest_buffers) in &buffers {
1239 let project_id = if guest_project.read_with(client_cx, |project, _| {
1240 project.is_local() || project.is_read_only()
1241 }) {
1242 continue;
1243 } else {
1244 guest_project
1245 .read_with(client_cx, |project, _| project.remote_id())
1246 .unwrap()
1247 };
1248 let guest_user_id = client.user_id().unwrap();
1249
1250 let host_project = clients.iter().find_map(|(client, cx)| {
1251 let project = client
1252 .local_projects()
1253 .iter()
1254 .find(|host_project| {
1255 host_project.read_with(cx, |host_project, _| {
1256 host_project.remote_id() == Some(project_id)
1257 })
1258 })?
1259 .clone();
1260 Some((client.user_id().unwrap(), project, cx))
1261 });
1262
1263 let (host_user_id, host_project, host_cx) =
1264 if let Some((host_user_id, host_project, host_cx)) = host_project {
1265 (host_user_id, host_project, host_cx)
1266 } else {
1267 continue;
1268 };
1269
1270 for guest_buffer in guest_buffers {
1271 let buffer_id =
1272 guest_buffer.read_with(client_cx, |buffer, _| buffer.remote_id());
1273 let host_buffer = host_project.read_with(host_cx, |project, _| {
1274 project.buffer_for_id(buffer_id).unwrap_or_else(|| {
1275 panic!(
1276 "host does not have buffer for guest:{}, peer:{:?}, id:{}",
1277 client.username,
1278 client.peer_id(),
1279 buffer_id
1280 )
1281 })
1282 });
1283 let path = host_buffer
1284 .read_with(host_cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
1285
1286 assert_eq!(
1287 guest_buffer.read_with(client_cx, |buffer, _| buffer.deferred_ops_len()),
1288 0,
1289 "{}, buffer {}, path {:?} has deferred operations",
1290 client.username,
1291 buffer_id,
1292 path,
1293 );
1294 assert_eq!(
1295 guest_buffer.read_with(client_cx, |buffer, _| buffer.text()),
1296 host_buffer.read_with(host_cx, |buffer, _| buffer.text()),
1297 "{}, buffer {}, path {:?}, differs from the host's buffer",
1298 client.username,
1299 buffer_id,
1300 path
1301 );
1302
1303 let host_file = host_buffer.read_with(host_cx, |b, _| b.file().cloned());
1304 let guest_file = guest_buffer.read_with(client_cx, |b, _| b.file().cloned());
1305 match (host_file, guest_file) {
1306 (Some(host_file), Some(guest_file)) => {
1307 assert_eq!(guest_file.path(), host_file.path());
1308 assert_eq!(guest_file.is_deleted(), host_file.is_deleted());
1309 assert_eq!(
1310 guest_file.mtime(),
1311 host_file.mtime(),
1312 "guest {} mtime does not match host {} for path {:?} in project {}",
1313 guest_user_id,
1314 host_user_id,
1315 guest_file.path(),
1316 project_id,
1317 );
1318 }
1319 (None, None) => {}
1320 (None, _) => panic!("host's file is None, guest's isn't"),
1321 (_, None) => panic!("guest's file is None, hosts's isn't"),
1322 }
1323
1324 let host_diff_base = host_buffer
1325 .read_with(host_cx, |b, _| b.diff_base().map(ToString::to_string));
1326 let guest_diff_base = guest_buffer
1327 .read_with(client_cx, |b, _| b.diff_base().map(ToString::to_string));
1328 assert_eq!(
1329 guest_diff_base, host_diff_base,
1330 "guest {} diff base does not match host's for path {path:?} in project {project_id}",
1331 client.username
1332 );
1333
1334 let host_saved_version =
1335 host_buffer.read_with(host_cx, |b, _| b.saved_version().clone());
1336 let guest_saved_version =
1337 guest_buffer.read_with(client_cx, |b, _| b.saved_version().clone());
1338 assert_eq!(
1339 guest_saved_version, host_saved_version,
1340 "guest {} saved version does not match host's for path {path:?} in project {project_id}",
1341 client.username
1342 );
1343
1344 let host_saved_version_fingerprint =
1345 host_buffer.read_with(host_cx, |b, _| b.saved_version_fingerprint());
1346 let guest_saved_version_fingerprint =
1347 guest_buffer.read_with(client_cx, |b, _| b.saved_version_fingerprint());
1348 assert_eq!(
1349 guest_saved_version_fingerprint, host_saved_version_fingerprint,
1350 "guest {} saved fingerprint does not match host's for path {path:?} in project {project_id}",
1351 client.username
1352 );
1353
1354 let host_saved_mtime = host_buffer.read_with(host_cx, |b, _| b.saved_mtime());
1355 let guest_saved_mtime =
1356 guest_buffer.read_with(client_cx, |b, _| b.saved_mtime());
1357 assert_eq!(
1358 guest_saved_mtime, host_saved_mtime,
1359 "guest {} saved mtime does not match host's for path {path:?} in project {project_id}",
1360 client.username
1361 );
1362
1363 let host_is_dirty = host_buffer.read_with(host_cx, |b, _| b.is_dirty());
1364 let guest_is_dirty = guest_buffer.read_with(client_cx, |b, _| b.is_dirty());
1365 assert_eq!(guest_is_dirty, host_is_dirty,
1366 "guest {} dirty status does not match host's for path {path:?} in project {project_id}",
1367 client.username
1368 );
1369
1370 let host_has_conflict = host_buffer.read_with(host_cx, |b, _| b.has_conflict());
1371 let guest_has_conflict =
1372 guest_buffer.read_with(client_cx, |b, _| b.has_conflict());
1373 assert_eq!(guest_has_conflict, host_has_conflict,
1374 "guest {} conflict status does not match host's for path {path:?} in project {project_id}",
1375 client.username
1376 );
1377 }
1378 }
1379 }
1380 }
1381}
1382
1383fn generate_git_operation(rng: &mut StdRng, client: &TestClient) -> GitOperation {
1384 fn generate_file_paths(
1385 repo_path: &Path,
1386 rng: &mut StdRng,
1387 client: &TestClient,
1388 ) -> Vec<PathBuf> {
1389 let mut paths = client
1390 .fs()
1391 .files()
1392 .into_iter()
1393 .filter(|path| path.starts_with(repo_path))
1394 .collect::<Vec<_>>();
1395
1396 let count = rng.gen_range(0..=paths.len());
1397 paths.shuffle(rng);
1398 paths.truncate(count);
1399
1400 paths
1401 .iter()
1402 .map(|path| path.strip_prefix(repo_path).unwrap().to_path_buf())
1403 .collect::<Vec<_>>()
1404 }
1405
1406 let repo_path = client.fs().directories(false).choose(rng).unwrap().clone();
1407
1408 match rng.gen_range(0..100_u32) {
1409 0..=25 => {
1410 let file_paths = generate_file_paths(&repo_path, rng, client);
1411
1412 let contents = file_paths
1413 .into_iter()
1414 .map(|path| (path, Alphanumeric.sample_string(rng, 16)))
1415 .collect();
1416
1417 GitOperation::WriteGitIndex {
1418 repo_path,
1419 contents,
1420 }
1421 }
1422 26..=63 => {
1423 let new_branch = (rng.gen_range(0..10) > 3).then(|| Alphanumeric.sample_string(rng, 8));
1424
1425 GitOperation::WriteGitBranch {
1426 repo_path,
1427 new_branch,
1428 }
1429 }
1430 64..=100 => {
1431 let file_paths = generate_file_paths(&repo_path, rng, client);
1432
1433 let statuses = file_paths
1434 .into_iter()
1435 .map(|paths| {
1436 (
1437 paths,
1438 match rng.gen_range(0..3_u32) {
1439 0 => GitFileStatus::Added,
1440 1 => GitFileStatus::Modified,
1441 2 => GitFileStatus::Conflict,
1442 _ => unreachable!(),
1443 },
1444 )
1445 })
1446 .collect::<Vec<_>>();
1447
1448 let git_operation = rng.gen::<bool>();
1449
1450 GitOperation::WriteGitStatuses {
1451 repo_path,
1452 statuses,
1453 git_operation,
1454 }
1455 }
1456 _ => unreachable!(),
1457 }
1458}
1459
1460fn buffer_for_full_path(
1461 client: &TestClient,
1462 project: &Model<Project>,
1463 full_path: &PathBuf,
1464 cx: &TestAppContext,
1465) -> Option<Model<language::Buffer>> {
1466 client
1467 .buffers_for_project(project)
1468 .iter()
1469 .find(|buffer| {
1470 buffer.read_with(cx, |buffer, cx| {
1471 buffer.file().unwrap().full_path(cx) == *full_path
1472 })
1473 })
1474 .cloned()
1475}
1476
1477fn project_for_root_name(
1478 client: &TestClient,
1479 root_name: &str,
1480 cx: &TestAppContext,
1481) -> Option<Model<Project>> {
1482 if let Some(ix) = project_ix_for_root_name(&*client.local_projects().deref(), root_name, cx) {
1483 return Some(client.local_projects()[ix].clone());
1484 }
1485 if let Some(ix) = project_ix_for_root_name(&*client.remote_projects().deref(), root_name, cx) {
1486 return Some(client.remote_projects()[ix].clone());
1487 }
1488 None
1489}
1490
1491fn project_ix_for_root_name(
1492 projects: &[Model<Project>],
1493 root_name: &str,
1494 cx: &TestAppContext,
1495) -> Option<usize> {
1496 projects.iter().position(|project| {
1497 project.read_with(cx, |project, cx| {
1498 let worktree = project.visible_worktrees(cx).next().unwrap();
1499 worktree.read(cx).root_name() == root_name
1500 })
1501 })
1502}
1503
1504fn root_name_for_project(project: &Model<Project>, cx: &TestAppContext) -> String {
1505 project.read_with(cx, |project, cx| {
1506 project
1507 .visible_worktrees(cx)
1508 .next()
1509 .unwrap()
1510 .read(cx)
1511 .root_name()
1512 .to_string()
1513 })
1514}
1515
1516fn project_path_for_full_path(
1517 project: &Model<Project>,
1518 full_path: &Path,
1519 cx: &TestAppContext,
1520) -> Option<ProjectPath> {
1521 let mut components = full_path.components();
1522 let root_name = components.next().unwrap().as_os_str().to_str().unwrap();
1523 let path = components.as_path().into();
1524 let worktree_id = project.read_with(cx, |project, cx| {
1525 project.worktrees().find_map(|worktree| {
1526 let worktree = worktree.read(cx);
1527 if worktree.root_name() == root_name {
1528 Some(worktree.id())
1529 } else {
1530 None
1531 }
1532 })
1533 })?;
1534 Some(ProjectPath { worktree_id, path })
1535}
1536
1537async fn ensure_project_shared(
1538 project: &Model<Project>,
1539 client: &TestClient,
1540 cx: &mut TestAppContext,
1541) {
1542 let first_root_name = root_name_for_project(project, cx);
1543 let active_call = cx.read(ActiveCall::global);
1544 if active_call.read_with(cx, |call, _| call.room().is_some())
1545 && project.read_with(cx, |project, _| project.is_local() && !project.is_shared())
1546 {
1547 match active_call
1548 .update(cx, |call, cx| call.share_project(project.clone(), cx))
1549 .await
1550 {
1551 Ok(project_id) => {
1552 log::info!(
1553 "{}: shared project {} with id {}",
1554 client.username,
1555 first_root_name,
1556 project_id
1557 );
1558 }
1559 Err(error) => {
1560 log::error!(
1561 "{}: error sharing project {}: {:?}",
1562 client.username,
1563 first_root_name,
1564 error
1565 );
1566 }
1567 }
1568 }
1569}
1570
1571fn choose_random_project(client: &TestClient, rng: &mut StdRng) -> Option<Model<Project>> {
1572 client
1573 .local_projects()
1574 .deref()
1575 .iter()
1576 .chain(client.remote_projects().iter())
1577 .choose(rng)
1578 .cloned()
1579}
1580
1581fn gen_file_name(rng: &mut StdRng) -> String {
1582 let mut name = String::new();
1583 for _ in 0..10 {
1584 let letter = rng.gen_range('a'..='z');
1585 name.push(letter);
1586 }
1587 name
1588}