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