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