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