1use crate::{
2 db::{self, NewUserParams, UserId},
3 rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
4 tests::{TestClient, TestServer},
5};
6use anyhow::{anyhow, Result};
7use call::ActiveCall;
8use client::RECEIVE_TIMEOUT;
9use collections::BTreeMap;
10use editor::Bias;
11use fs::{FakeFs, Fs as _};
12use futures::StreamExt as _;
13use gpui::{executor::Deterministic, ModelHandle, Task, TestAppContext};
14use language::{range_to_lsp, FakeLspAdapter, Language, LanguageConfig, PointUtf16};
15use lsp::FakeLanguageServer;
16use parking_lot::Mutex;
17use project::{search::SearchQuery, Project, ProjectPath};
18use rand::{
19 distributions::{Alphanumeric, DistString},
20 prelude::*,
21};
22use serde::{Deserialize, Serialize};
23use settings::Settings;
24use std::{
25 env,
26 ops::Range,
27 path::{Path, PathBuf},
28 rc::Rc,
29 sync::{
30 atomic::{AtomicBool, Ordering::SeqCst},
31 Arc,
32 },
33};
34use util::ResultExt;
35
36lazy_static::lazy_static! {
37 static ref PLAN_LOAD_PATH: Option<PathBuf> = path_env_var("LOAD_PLAN");
38 static ref PLAN_SAVE_PATH: Option<PathBuf> = path_env_var("SAVE_PLAN");
39 static ref LOADED_PLAN_JSON: Mutex<Option<Vec<u8>>> = Default::default();
40 static ref PLAN: Mutex<Option<Arc<Mutex<TestPlan>>>> = Default::default();
41}
42
43#[gpui::test(iterations = 100, on_failure = "on_failure")]
44async fn test_random_collaboration(
45 cx: &mut TestAppContext,
46 deterministic: Arc<Deterministic>,
47 rng: StdRng,
48) {
49 deterministic.forbid_parking();
50
51 let max_peers = env::var("MAX_PEERS")
52 .map(|i| i.parse().expect("invalid `MAX_PEERS` variable"))
53 .unwrap_or(3);
54 let max_operations = env::var("OPERATIONS")
55 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
56 .unwrap_or(10);
57
58 let mut server = TestServer::start(&deterministic).await;
59 let db = server.app_state.db.clone();
60
61 let mut users = Vec::new();
62 for ix in 0..max_peers {
63 let username = format!("user-{}", ix + 1);
64 let user_id = db
65 .create_user(
66 &format!("{username}@example.com"),
67 false,
68 NewUserParams {
69 github_login: username.clone(),
70 github_user_id: (ix + 1) as i32,
71 invite_count: 0,
72 },
73 )
74 .await
75 .unwrap()
76 .user_id;
77 users.push(UserTestPlan {
78 user_id,
79 username,
80 online: false,
81 next_root_id: 0,
82 operation_ix: 0,
83 });
84 }
85
86 for (ix, user_a) in users.iter().enumerate() {
87 for user_b in &users[ix + 1..] {
88 server
89 .app_state
90 .db
91 .send_contact_request(user_a.user_id, user_b.user_id)
92 .await
93 .unwrap();
94 server
95 .app_state
96 .db
97 .respond_to_contact_request(user_b.user_id, user_a.user_id, true)
98 .await
99 .unwrap();
100 }
101 }
102
103 let plan = Arc::new(Mutex::new(TestPlan::new(rng, users, max_operations)));
104
105 if let Some(path) = &*PLAN_LOAD_PATH {
106 let json = LOADED_PLAN_JSON
107 .lock()
108 .get_or_insert_with(|| {
109 eprintln!("loaded test plan from path {:?}", path);
110 std::fs::read(path).unwrap()
111 })
112 .clone();
113 plan.lock().deserialize(json);
114 }
115
116 PLAN.lock().replace(plan.clone());
117
118 let mut clients = Vec::new();
119 let mut client_tasks = Vec::new();
120 let mut operation_channels = Vec::new();
121
122 loop {
123 let Some((next_operation, applied)) = plan.lock().next_server_operation(&clients) else { break };
124 let did_apply = apply_server_operation(
125 deterministic.clone(),
126 &mut server,
127 &mut clients,
128 &mut client_tasks,
129 &mut operation_channels,
130 plan.clone(),
131 next_operation,
132 cx,
133 )
134 .await;
135 if did_apply {
136 applied.store(true, SeqCst);
137 }
138 }
139
140 drop(operation_channels);
141 deterministic.start_waiting();
142 futures::future::join_all(client_tasks).await;
143 deterministic.finish_waiting();
144 deterministic.run_until_parked();
145
146 check_consistency_between_clients(&clients);
147
148 for (client, mut cx) in clients {
149 cx.update(|cx| {
150 cx.clear_globals();
151 cx.set_global(Settings::test(cx));
152 drop(client);
153 });
154 }
155
156 deterministic.run_until_parked();
157}
158
159fn on_failure() {
160 if let Some(plan) = PLAN.lock().clone() {
161 if let Some(path) = &*PLAN_SAVE_PATH {
162 eprintln!("saved test plan to path {:?}", path);
163 std::fs::write(path, plan.lock().serialize()).unwrap();
164 }
165 }
166}
167
168async fn apply_server_operation(
169 deterministic: Arc<Deterministic>,
170 server: &mut TestServer,
171 clients: &mut Vec<(Rc<TestClient>, TestAppContext)>,
172 client_tasks: &mut Vec<Task<()>>,
173 operation_channels: &mut Vec<futures::channel::mpsc::UnboundedSender<usize>>,
174 plan: Arc<Mutex<TestPlan>>,
175 operation: Operation,
176 cx: &mut TestAppContext,
177) -> bool {
178 match operation {
179 Operation::AddConnection { user_id } => {
180 let username;
181 {
182 let mut plan = plan.lock();
183 let mut user = plan.user(user_id);
184 if user.online {
185 return false;
186 }
187 user.online = true;
188 username = user.username.clone();
189 };
190 log::info!("Adding new connection for {}", username);
191 let next_entity_id = (user_id.0 * 10_000) as usize;
192 let mut client_cx = TestAppContext::new(
193 cx.foreground_platform(),
194 cx.platform(),
195 deterministic.build_foreground(user_id.0 as usize),
196 deterministic.build_background(),
197 cx.font_cache(),
198 cx.leak_detector(),
199 next_entity_id,
200 cx.function_name.clone(),
201 );
202
203 let (operation_tx, operation_rx) = futures::channel::mpsc::unbounded();
204 let client = Rc::new(server.create_client(&mut client_cx, &username).await);
205 operation_channels.push(operation_tx);
206 clients.push((client.clone(), client_cx.clone()));
207 client_tasks.push(client_cx.foreground().spawn(simulate_client(
208 client,
209 operation_rx,
210 plan.clone(),
211 client_cx,
212 )));
213
214 log::info!("Added connection for {}", username);
215 }
216
217 Operation::RemoveConnection {
218 user_id: removed_user_id,
219 } => {
220 log::info!("Simulating full disconnection of user {}", removed_user_id);
221 let client_ix = clients
222 .iter()
223 .position(|(client, cx)| client.current_user_id(cx) == removed_user_id);
224 let Some(client_ix) = client_ix else { return false };
225 let user_connection_ids = server
226 .connection_pool
227 .lock()
228 .user_connection_ids(removed_user_id)
229 .collect::<Vec<_>>();
230 assert_eq!(user_connection_ids.len(), 1);
231 let removed_peer_id = user_connection_ids[0].into();
232 let (client, mut client_cx) = clients.remove(client_ix);
233 let client_task = client_tasks.remove(client_ix);
234 operation_channels.remove(client_ix);
235 server.forbid_connections();
236 server.disconnect_client(removed_peer_id);
237 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
238 deterministic.start_waiting();
239 log::info!("Waiting for user {} to exit...", removed_user_id);
240 client_task.await;
241 deterministic.finish_waiting();
242 server.allow_connections();
243
244 for project in client.remote_projects().iter() {
245 project.read_with(&client_cx, |project, _| {
246 assert!(
247 project.is_read_only(),
248 "project {:?} should be read only",
249 project.remote_id()
250 )
251 });
252 }
253
254 for (client, cx) in clients {
255 let contacts = server
256 .app_state
257 .db
258 .get_contacts(client.current_user_id(cx))
259 .await
260 .unwrap();
261 let pool = server.connection_pool.lock();
262 for contact in contacts {
263 if let db::Contact::Accepted { user_id, busy, .. } = contact {
264 if user_id == removed_user_id {
265 assert!(!pool.is_user_online(user_id));
266 assert!(!busy);
267 }
268 }
269 }
270 }
271
272 log::info!("{} removed", client.username);
273 plan.lock().user(removed_user_id).online = false;
274 client_cx.update(|cx| {
275 cx.clear_globals();
276 drop(client);
277 });
278 }
279
280 Operation::BounceConnection { user_id } => {
281 log::info!("Simulating temporary disconnection of user {}", user_id);
282 let user_connection_ids = server
283 .connection_pool
284 .lock()
285 .user_connection_ids(user_id)
286 .collect::<Vec<_>>();
287 if user_connection_ids.is_empty() {
288 return false;
289 }
290 assert_eq!(user_connection_ids.len(), 1);
291 let peer_id = user_connection_ids[0].into();
292 server.disconnect_client(peer_id);
293 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
294 }
295
296 Operation::RestartServer => {
297 log::info!("Simulating server restart");
298 server.reset().await;
299 deterministic.advance_clock(RECEIVE_TIMEOUT);
300 server.start().await.unwrap();
301 deterministic.advance_clock(CLEANUP_TIMEOUT);
302 let environment = &server.app_state.config.zed_environment;
303 let stale_room_ids = server
304 .app_state
305 .db
306 .stale_room_ids(environment, server.id())
307 .await
308 .unwrap();
309 assert_eq!(stale_room_ids, vec![]);
310 }
311
312 Operation::MutateClients {
313 user_ids,
314 batch_id,
315 quiesce,
316 } => {
317 let mut applied = false;
318 for user_id in user_ids {
319 let client_ix = clients
320 .iter()
321 .position(|(client, cx)| client.current_user_id(cx) == user_id);
322 let Some(client_ix) = client_ix else { continue };
323 applied = true;
324 if let Err(err) = operation_channels[client_ix].unbounded_send(batch_id) {
325 log::error!("error signaling user {user_id}: {err}");
326 }
327 }
328
329 if quiesce && applied {
330 deterministic.run_until_parked();
331 check_consistency_between_clients(&clients);
332 }
333
334 return applied;
335 }
336 }
337 true
338}
339
340async fn apply_client_operation(
341 client: &TestClient,
342 operation: ClientOperation,
343 cx: &mut TestAppContext,
344) -> Result<(), TestError> {
345 match operation {
346 ClientOperation::AcceptIncomingCall => {
347 let active_call = cx.read(ActiveCall::global);
348 if active_call.read_with(cx, |call, _| call.incoming().borrow().is_none()) {
349 Err(TestError::Inapplicable)?;
350 }
351
352 log::info!("{}: accepting incoming call", client.username);
353 active_call
354 .update(cx, |call, cx| call.accept_incoming(cx))
355 .await?;
356 }
357
358 ClientOperation::RejectIncomingCall => {
359 let active_call = cx.read(ActiveCall::global);
360 if active_call.read_with(cx, |call, _| call.incoming().borrow().is_none()) {
361 Err(TestError::Inapplicable)?;
362 }
363
364 log::info!("{}: declining incoming call", client.username);
365 active_call.update(cx, |call, _| call.decline_incoming())?;
366 }
367
368 ClientOperation::LeaveCall => {
369 let active_call = cx.read(ActiveCall::global);
370 if active_call.read_with(cx, |call, _| call.room().is_none()) {
371 Err(TestError::Inapplicable)?;
372 }
373
374 log::info!("{}: hanging up", client.username);
375 active_call.update(cx, |call, cx| call.hang_up(cx)).await?;
376 }
377
378 ClientOperation::InviteContactToCall { user_id } => {
379 let active_call = cx.read(ActiveCall::global);
380
381 log::info!("{}: inviting {}", client.username, user_id,);
382 active_call
383 .update(cx, |call, cx| call.invite(user_id.to_proto(), None, cx))
384 .await
385 .log_err();
386 }
387
388 ClientOperation::OpenLocalProject { first_root_name } => {
389 log::info!(
390 "{}: opening local project at {:?}",
391 client.username,
392 first_root_name
393 );
394
395 let root_path = Path::new("/").join(&first_root_name);
396 client.fs.create_dir(&root_path).await.unwrap();
397 client
398 .fs
399 .create_file(&root_path.join("main.rs"), Default::default())
400 .await
401 .unwrap();
402 let project = client.build_local_project(root_path, cx).await.0;
403 ensure_project_shared(&project, client, cx).await;
404 client.local_projects_mut().push(project.clone());
405 }
406
407 ClientOperation::AddWorktreeToProject {
408 project_root_name,
409 new_root_path,
410 } => {
411 let project = project_for_root_name(client, &project_root_name, cx)
412 .ok_or(TestError::Inapplicable)?;
413
414 log::info!(
415 "{}: finding/creating local worktree at {:?} to project with root path {}",
416 client.username,
417 new_root_path,
418 project_root_name
419 );
420
421 ensure_project_shared(&project, client, cx).await;
422 if !client.fs.paths().contains(&new_root_path) {
423 client.fs.create_dir(&new_root_path).await.unwrap();
424 }
425 project
426 .update(cx, |project, cx| {
427 project.find_or_create_local_worktree(&new_root_path, true, cx)
428 })
429 .await
430 .unwrap();
431 }
432
433 ClientOperation::CloseRemoteProject { project_root_name } => {
434 let project = project_for_root_name(client, &project_root_name, cx)
435 .ok_or(TestError::Inapplicable)?;
436
437 log::info!(
438 "{}: closing remote project with root path {}",
439 client.username,
440 project_root_name,
441 );
442
443 let ix = client
444 .remote_projects()
445 .iter()
446 .position(|p| p == &project)
447 .unwrap();
448 cx.update(|_| {
449 client.remote_projects_mut().remove(ix);
450 client.buffers().retain(|project, _| project != project);
451 drop(project);
452 });
453 }
454
455 ClientOperation::OpenRemoteProject {
456 host_id,
457 first_root_name,
458 } => {
459 let active_call = cx.read(ActiveCall::global);
460 let project = active_call
461 .update(cx, |call, cx| {
462 let room = call.room().cloned()?;
463 let participant = room
464 .read(cx)
465 .remote_participants()
466 .get(&host_id.to_proto())?;
467 let project_id = participant
468 .projects
469 .iter()
470 .find(|project| project.worktree_root_names[0] == first_root_name)?
471 .id;
472 Some(room.update(cx, |room, cx| {
473 room.join_project(
474 project_id,
475 client.language_registry.clone(),
476 FakeFs::new(cx.background().clone()),
477 cx,
478 )
479 }))
480 })
481 .ok_or(TestError::Inapplicable)?;
482
483 log::info!(
484 "{}: joining remote project of user {}, root name {}",
485 client.username,
486 host_id,
487 first_root_name,
488 );
489
490 let project = project.await?;
491 client.remote_projects_mut().push(project.clone());
492 }
493
494 ClientOperation::CreateWorktreeEntry {
495 project_root_name,
496 is_local,
497 full_path,
498 is_dir,
499 } => {
500 let project = project_for_root_name(client, &project_root_name, cx)
501 .ok_or(TestError::Inapplicable)?;
502 let project_path = project_path_for_full_path(&project, &full_path, cx)
503 .ok_or(TestError::Inapplicable)?;
504
505 log::info!(
506 "{}: creating {} at path {:?} in {} project {}",
507 client.username,
508 if is_dir { "dir" } else { "file" },
509 full_path,
510 if is_local { "local" } else { "remote" },
511 project_root_name,
512 );
513
514 ensure_project_shared(&project, client, cx).await;
515 project
516 .update(cx, |p, cx| p.create_entry(project_path, is_dir, cx))
517 .unwrap()
518 .await?;
519 }
520
521 ClientOperation::OpenBuffer {
522 project_root_name,
523 is_local,
524 full_path,
525 } => {
526 let project = project_for_root_name(client, &project_root_name, cx)
527 .ok_or(TestError::Inapplicable)?;
528 let project_path = project_path_for_full_path(&project, &full_path, cx)
529 .ok_or(TestError::Inapplicable)?;
530
531 log::info!(
532 "{}: opening buffer {:?} in {} project {}",
533 client.username,
534 full_path,
535 if is_local { "local" } else { "remote" },
536 project_root_name,
537 );
538
539 ensure_project_shared(&project, client, cx).await;
540 let buffer = project
541 .update(cx, |project, cx| project.open_buffer(project_path, cx))
542 .await?;
543 client.buffers_for_project(&project).insert(buffer);
544 }
545
546 ClientOperation::EditBuffer {
547 project_root_name,
548 is_local,
549 full_path,
550 edits,
551 } => {
552 let project = project_for_root_name(client, &project_root_name, cx)
553 .ok_or(TestError::Inapplicable)?;
554 let buffer = buffer_for_full_path(client, &project, &full_path, cx)
555 .ok_or(TestError::Inapplicable)?;
556
557 log::info!(
558 "{}: editing buffer {:?} in {} project {} with {:?}",
559 client.username,
560 full_path,
561 if is_local { "local" } else { "remote" },
562 project_root_name,
563 edits
564 );
565
566 ensure_project_shared(&project, client, cx).await;
567 buffer.update(cx, |buffer, cx| {
568 let snapshot = buffer.snapshot();
569 buffer.edit(
570 edits.into_iter().map(|(range, text)| {
571 let start = snapshot.clip_offset(range.start, Bias::Left);
572 let end = snapshot.clip_offset(range.end, Bias::Right);
573 (start..end, text)
574 }),
575 None,
576 cx,
577 );
578 });
579 }
580
581 ClientOperation::CloseBuffer {
582 project_root_name,
583 is_local,
584 full_path,
585 } => {
586 let project = project_for_root_name(client, &project_root_name, cx)
587 .ok_or(TestError::Inapplicable)?;
588 let buffer = buffer_for_full_path(client, &project, &full_path, cx)
589 .ok_or(TestError::Inapplicable)?;
590
591 log::info!(
592 "{}: closing buffer {:?} in {} project {}",
593 client.username,
594 full_path,
595 if is_local { "local" } else { "remote" },
596 project_root_name
597 );
598
599 ensure_project_shared(&project, client, cx).await;
600 cx.update(|_| {
601 client.buffers_for_project(&project).remove(&buffer);
602 drop(buffer);
603 });
604 }
605
606 ClientOperation::SaveBuffer {
607 project_root_name,
608 is_local,
609 full_path,
610 detach,
611 } => {
612 let project = project_for_root_name(client, &project_root_name, cx)
613 .ok_or(TestError::Inapplicable)?;
614 let buffer = buffer_for_full_path(client, &project, &full_path, cx)
615 .ok_or(TestError::Inapplicable)?;
616
617 log::info!(
618 "{}: saving buffer {:?} in {} project {}, {}",
619 client.username,
620 full_path,
621 if is_local { "local" } else { "remote" },
622 project_root_name,
623 if detach { "detaching" } else { "awaiting" }
624 );
625
626 ensure_project_shared(&project, client, cx).await;
627 let requested_version = buffer.read_with(cx, |buffer, _| buffer.version());
628 let save = project.update(cx, |project, cx| project.save_buffer(buffer, cx));
629 let save = cx.background().spawn(async move {
630 let (saved_version, _, _) = save
631 .await
632 .map_err(|err| anyhow!("save request failed: {:?}", err))?;
633 assert!(saved_version.observed_all(&requested_version));
634 anyhow::Ok(())
635 });
636 if detach {
637 cx.update(|cx| save.detach_and_log_err(cx));
638 } else {
639 save.await?;
640 }
641 }
642
643 ClientOperation::RequestLspDataInBuffer {
644 project_root_name,
645 is_local,
646 full_path,
647 offset,
648 kind,
649 detach,
650 } => {
651 let project = project_for_root_name(client, &project_root_name, cx)
652 .ok_or(TestError::Inapplicable)?;
653 let buffer = buffer_for_full_path(client, &project, &full_path, cx)
654 .ok_or(TestError::Inapplicable)?;
655
656 log::info!(
657 "{}: request LSP {:?} for buffer {:?} in {} project {}, {}",
658 client.username,
659 kind,
660 full_path,
661 if is_local { "local" } else { "remote" },
662 project_root_name,
663 if detach { "detaching" } else { "awaiting" }
664 );
665
666 use futures::{FutureExt as _, TryFutureExt as _};
667 let offset = buffer.read_with(cx, |b, _| b.clip_offset(offset, Bias::Left));
668 let request = cx.foreground().spawn(project.update(cx, |project, cx| {
669 match kind {
670 LspRequestKind::Rename => project
671 .prepare_rename(buffer, offset, cx)
672 .map_ok(|_| ())
673 .boxed(),
674 LspRequestKind::Completion => project
675 .completions(&buffer, offset, cx)
676 .map_ok(|_| ())
677 .boxed(),
678 LspRequestKind::CodeAction => project
679 .code_actions(&buffer, offset..offset, cx)
680 .map_ok(|_| ())
681 .boxed(),
682 LspRequestKind::Definition => project
683 .definition(&buffer, offset, cx)
684 .map_ok(|_| ())
685 .boxed(),
686 LspRequestKind::Highlights => project
687 .document_highlights(&buffer, offset, cx)
688 .map_ok(|_| ())
689 .boxed(),
690 }
691 }));
692 if detach {
693 request.detach();
694 } else {
695 request.await?;
696 }
697 }
698
699 ClientOperation::SearchProject {
700 project_root_name,
701 is_local,
702 query,
703 detach,
704 } => {
705 let project = project_for_root_name(client, &project_root_name, cx)
706 .ok_or(TestError::Inapplicable)?;
707
708 log::info!(
709 "{}: search {} project {} for {:?}, {}",
710 client.username,
711 if is_local { "local" } else { "remote" },
712 project_root_name,
713 query,
714 if detach { "detaching" } else { "awaiting" }
715 );
716
717 let search = project.update(cx, |project, cx| {
718 project.search(SearchQuery::text(query, false, false), cx)
719 });
720 drop(project);
721 let search = cx.background().spawn(async move {
722 search
723 .await
724 .map_err(|err| anyhow!("search request failed: {:?}", err))
725 });
726 if detach {
727 cx.update(|cx| search.detach_and_log_err(cx));
728 } else {
729 search.await?;
730 }
731 }
732
733 ClientOperation::WriteFsEntry {
734 path,
735 is_dir,
736 content,
737 } => {
738 client
739 .fs
740 .metadata(&path.parent().unwrap())
741 .await?
742 .ok_or(TestError::Inapplicable)?;
743
744 if is_dir {
745 log::info!("{}: creating dir at {:?}", client.username, path);
746 client.fs.create_dir(&path).await.unwrap();
747 } else {
748 let exists = client.fs.metadata(&path).await?.is_some();
749 let verb = if exists { "updating" } else { "creating" };
750 log::info!("{}: {} file at {:?}", verb, client.username, path);
751
752 client
753 .fs
754 .save(&path, &content.as_str().into(), fs::LineEnding::Unix)
755 .await
756 .unwrap();
757 }
758 }
759
760 ClientOperation::WriteGitIndex {
761 repo_path,
762 contents,
763 } => {
764 if !client
765 .fs
766 .metadata(&repo_path)
767 .await?
768 .map_or(false, |m| m.is_dir)
769 {
770 Err(TestError::Inapplicable)?;
771 }
772
773 log::info!(
774 "{}: writing git index for repo {:?}: {:?}",
775 client.username,
776 repo_path,
777 contents
778 );
779
780 let dot_git_dir = repo_path.join(".git");
781 let contents = contents
782 .iter()
783 .map(|(path, contents)| (path.as_path(), contents.clone()))
784 .collect::<Vec<_>>();
785 if client.fs.metadata(&dot_git_dir).await?.is_none() {
786 client.fs.create_dir(&dot_git_dir).await?;
787 }
788 client.fs.set_index_for_repo(&dot_git_dir, &contents).await;
789 }
790 }
791 Ok(())
792}
793
794fn check_consistency_between_clients(clients: &[(Rc<TestClient>, TestAppContext)]) {
795 for (client, client_cx) in clients {
796 for guest_project in client.remote_projects().iter() {
797 guest_project.read_with(client_cx, |guest_project, cx| {
798 let host_project = clients.iter().find_map(|(client, cx)| {
799 let project = client
800 .local_projects()
801 .iter()
802 .find(|host_project| {
803 host_project.read_with(cx, |host_project, _| {
804 host_project.remote_id() == guest_project.remote_id()
805 })
806 })?
807 .clone();
808 Some((project, cx))
809 });
810
811 if !guest_project.is_read_only() {
812 if let Some((host_project, host_cx)) = host_project {
813 let host_worktree_snapshots =
814 host_project.read_with(host_cx, |host_project, cx| {
815 host_project
816 .worktrees(cx)
817 .map(|worktree| {
818 let worktree = worktree.read(cx);
819 (worktree.id(), worktree.snapshot())
820 })
821 .collect::<BTreeMap<_, _>>()
822 });
823 let guest_worktree_snapshots = guest_project
824 .worktrees(cx)
825 .map(|worktree| {
826 let worktree = worktree.read(cx);
827 (worktree.id(), worktree.snapshot())
828 })
829 .collect::<BTreeMap<_, _>>();
830
831 assert_eq!(
832 guest_worktree_snapshots.values().map(|w| w.abs_path()).collect::<Vec<_>>(),
833 host_worktree_snapshots.values().map(|w| w.abs_path()).collect::<Vec<_>>(),
834 "{} has different worktrees than the host for project {:?}",
835 client.username, guest_project.remote_id(),
836 );
837
838 for (id, host_snapshot) in &host_worktree_snapshots {
839 let guest_snapshot = &guest_worktree_snapshots[id];
840 assert_eq!(
841 guest_snapshot.root_name(),
842 host_snapshot.root_name(),
843 "{} has different root name than the host for worktree {}, project {:?}",
844 client.username,
845 id,
846 guest_project.remote_id(),
847 );
848 assert_eq!(
849 guest_snapshot.abs_path(),
850 host_snapshot.abs_path(),
851 "{} has different abs path than the host for worktree {}, project: {:?}",
852 client.username,
853 id,
854 guest_project.remote_id(),
855 );
856 assert_eq!(
857 guest_snapshot.entries(false).collect::<Vec<_>>(),
858 host_snapshot.entries(false).collect::<Vec<_>>(),
859 "{} has different snapshot than the host for worktree {:?} and project {:?}",
860 client.username,
861 host_snapshot.abs_path(),
862 guest_project.remote_id(),
863 );
864 assert_eq!(guest_snapshot.scan_id(), host_snapshot.scan_id(),
865 "{} has different scan id than the host for worktree {:?} and project {:?}",
866 client.username,
867 host_snapshot.abs_path(),
868 guest_project.remote_id(),
869 );
870 }
871 }
872 }
873
874 guest_project.check_invariants(cx);
875 });
876 }
877
878 let buffers = client.buffers().clone();
879 for (guest_project, guest_buffers) in &buffers {
880 let project_id = if guest_project.read_with(client_cx, |project, _| {
881 project.is_local() || project.is_read_only()
882 }) {
883 continue;
884 } else {
885 guest_project
886 .read_with(client_cx, |project, _| project.remote_id())
887 .unwrap()
888 };
889 let guest_user_id = client.user_id().unwrap();
890
891 let host_project = clients.iter().find_map(|(client, cx)| {
892 let project = client
893 .local_projects()
894 .iter()
895 .find(|host_project| {
896 host_project.read_with(cx, |host_project, _| {
897 host_project.remote_id() == Some(project_id)
898 })
899 })?
900 .clone();
901 Some((client.user_id().unwrap(), project, cx))
902 });
903
904 let (host_user_id, host_project, host_cx) =
905 if let Some((host_user_id, host_project, host_cx)) = host_project {
906 (host_user_id, host_project, host_cx)
907 } else {
908 continue;
909 };
910
911 for guest_buffer in guest_buffers {
912 let buffer_id = guest_buffer.read_with(client_cx, |buffer, _| buffer.remote_id());
913 let host_buffer = host_project.read_with(host_cx, |project, cx| {
914 project.buffer_for_id(buffer_id, cx).unwrap_or_else(|| {
915 panic!(
916 "host does not have buffer for guest:{}, peer:{:?}, id:{}",
917 client.username,
918 client.peer_id(),
919 buffer_id
920 )
921 })
922 });
923 let path = host_buffer
924 .read_with(host_cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
925
926 assert_eq!(
927 guest_buffer.read_with(client_cx, |buffer, _| buffer.deferred_ops_len()),
928 0,
929 "{}, buffer {}, path {:?} has deferred operations",
930 client.username,
931 buffer_id,
932 path,
933 );
934 assert_eq!(
935 guest_buffer.read_with(client_cx, |buffer, _| buffer.text()),
936 host_buffer.read_with(host_cx, |buffer, _| buffer.text()),
937 "{}, buffer {}, path {:?}, differs from the host's buffer",
938 client.username,
939 buffer_id,
940 path
941 );
942
943 let host_file = host_buffer.read_with(host_cx, |b, _| b.file().cloned());
944 let guest_file = guest_buffer.read_with(client_cx, |b, _| b.file().cloned());
945 match (host_file, guest_file) {
946 (Some(host_file), Some(guest_file)) => {
947 assert_eq!(guest_file.path(), host_file.path());
948 assert_eq!(guest_file.is_deleted(), host_file.is_deleted());
949 assert_eq!(
950 guest_file.mtime(),
951 host_file.mtime(),
952 "guest {} mtime does not match host {} for path {:?} in project {}",
953 guest_user_id,
954 host_user_id,
955 guest_file.path(),
956 project_id,
957 );
958 }
959 (None, None) => {}
960 (None, _) => panic!("host's file is None, guest's isn't"),
961 (_, None) => panic!("guest's file is None, hosts's isn't"),
962 }
963
964 let host_diff_base =
965 host_buffer.read_with(host_cx, |b, _| b.diff_base().map(ToString::to_string));
966 let guest_diff_base = guest_buffer
967 .read_with(client_cx, |b, _| b.diff_base().map(ToString::to_string));
968 assert_eq!(guest_diff_base, host_diff_base);
969
970 let host_saved_version =
971 host_buffer.read_with(host_cx, |b, _| b.saved_version().clone());
972 let guest_saved_version =
973 guest_buffer.read_with(client_cx, |b, _| b.saved_version().clone());
974 assert_eq!(
975 guest_saved_version, host_saved_version,
976 "guest saved version does not match host's for path {path:?} in project {project_id}",
977 );
978
979 let host_saved_version_fingerprint =
980 host_buffer.read_with(host_cx, |b, _| b.saved_version_fingerprint());
981 let guest_saved_version_fingerprint =
982 guest_buffer.read_with(client_cx, |b, _| b.saved_version_fingerprint());
983 assert_eq!(
984 guest_saved_version_fingerprint, host_saved_version_fingerprint,
985 "guest's saved fingerprint does not match host's for path {path:?} in project {project_id}",
986 );
987
988 let host_saved_mtime = host_buffer.read_with(host_cx, |b, _| b.saved_mtime());
989 let guest_saved_mtime = guest_buffer.read_with(client_cx, |b, _| b.saved_mtime());
990 assert_eq!(
991 guest_saved_mtime, host_saved_mtime,
992 "guest's saved mtime does not match host's for path {path:?} in project {project_id}",
993 );
994
995 let host_is_dirty = host_buffer.read_with(host_cx, |b, _| b.is_dirty());
996 let guest_is_dirty = guest_buffer.read_with(client_cx, |b, _| b.is_dirty());
997 assert_eq!(guest_is_dirty, host_is_dirty,
998 "guest's dirty status does not match host's for path {path:?} in project {project_id}",
999 );
1000
1001 let host_has_conflict = host_buffer.read_with(host_cx, |b, _| b.has_conflict());
1002 let guest_has_conflict = guest_buffer.read_with(client_cx, |b, _| b.has_conflict());
1003 assert_eq!(guest_has_conflict, host_has_conflict,
1004 "guest's conflict status does not match host's for path {path:?} in project {project_id}",
1005 );
1006 }
1007 }
1008 }
1009}
1010
1011struct TestPlan {
1012 rng: StdRng,
1013 replay: bool,
1014 stored_operations: Vec<(StoredOperation, Arc<AtomicBool>)>,
1015 max_operations: usize,
1016 operation_ix: usize,
1017 users: Vec<UserTestPlan>,
1018 next_batch_id: usize,
1019 allow_server_restarts: bool,
1020 allow_client_reconnection: bool,
1021 allow_client_disconnection: bool,
1022}
1023
1024struct UserTestPlan {
1025 user_id: UserId,
1026 username: String,
1027 next_root_id: usize,
1028 operation_ix: usize,
1029 online: bool,
1030}
1031
1032#[derive(Clone, Debug, Serialize, Deserialize)]
1033#[serde(untagged)]
1034enum StoredOperation {
1035 Server(Operation),
1036 Client {
1037 user_id: UserId,
1038 batch_id: usize,
1039 operation: ClientOperation,
1040 },
1041}
1042
1043#[derive(Clone, Debug, Serialize, Deserialize)]
1044enum Operation {
1045 AddConnection {
1046 user_id: UserId,
1047 },
1048 RemoveConnection {
1049 user_id: UserId,
1050 },
1051 BounceConnection {
1052 user_id: UserId,
1053 },
1054 RestartServer,
1055 MutateClients {
1056 batch_id: usize,
1057 #[serde(skip_serializing)]
1058 #[serde(skip_deserializing)]
1059 user_ids: Vec<UserId>,
1060 quiesce: bool,
1061 },
1062}
1063
1064#[derive(Clone, Debug, Serialize, Deserialize)]
1065enum ClientOperation {
1066 AcceptIncomingCall,
1067 RejectIncomingCall,
1068 LeaveCall,
1069 InviteContactToCall {
1070 user_id: UserId,
1071 },
1072 OpenLocalProject {
1073 first_root_name: String,
1074 },
1075 OpenRemoteProject {
1076 host_id: UserId,
1077 first_root_name: String,
1078 },
1079 AddWorktreeToProject {
1080 project_root_name: String,
1081 new_root_path: PathBuf,
1082 },
1083 CloseRemoteProject {
1084 project_root_name: String,
1085 },
1086 OpenBuffer {
1087 project_root_name: String,
1088 is_local: bool,
1089 full_path: PathBuf,
1090 },
1091 SearchProject {
1092 project_root_name: String,
1093 is_local: bool,
1094 query: String,
1095 detach: bool,
1096 },
1097 EditBuffer {
1098 project_root_name: String,
1099 is_local: bool,
1100 full_path: PathBuf,
1101 edits: Vec<(Range<usize>, Arc<str>)>,
1102 },
1103 CloseBuffer {
1104 project_root_name: String,
1105 is_local: bool,
1106 full_path: PathBuf,
1107 },
1108 SaveBuffer {
1109 project_root_name: String,
1110 is_local: bool,
1111 full_path: PathBuf,
1112 detach: bool,
1113 },
1114 RequestLspDataInBuffer {
1115 project_root_name: String,
1116 is_local: bool,
1117 full_path: PathBuf,
1118 offset: usize,
1119 kind: LspRequestKind,
1120 detach: bool,
1121 },
1122 CreateWorktreeEntry {
1123 project_root_name: String,
1124 is_local: bool,
1125 full_path: PathBuf,
1126 is_dir: bool,
1127 },
1128 WriteFsEntry {
1129 path: PathBuf,
1130 is_dir: bool,
1131 content: String,
1132 },
1133 WriteGitIndex {
1134 repo_path: PathBuf,
1135 contents: Vec<(PathBuf, String)>,
1136 },
1137}
1138
1139#[derive(Clone, Debug, Serialize, Deserialize)]
1140enum LspRequestKind {
1141 Rename,
1142 Completion,
1143 CodeAction,
1144 Definition,
1145 Highlights,
1146}
1147
1148enum TestError {
1149 Inapplicable,
1150 Other(anyhow::Error),
1151}
1152
1153impl From<anyhow::Error> for TestError {
1154 fn from(value: anyhow::Error) -> Self {
1155 Self::Other(value)
1156 }
1157}
1158
1159impl TestPlan {
1160 fn new(mut rng: StdRng, users: Vec<UserTestPlan>, max_operations: usize) -> Self {
1161 Self {
1162 replay: false,
1163 allow_server_restarts: rng.gen_bool(0.7),
1164 allow_client_reconnection: rng.gen_bool(0.7),
1165 allow_client_disconnection: rng.gen_bool(0.1),
1166 stored_operations: Vec::new(),
1167 operation_ix: 0,
1168 next_batch_id: 0,
1169 max_operations,
1170 users,
1171 rng,
1172 }
1173 }
1174
1175 fn deserialize(&mut self, json: Vec<u8>) {
1176 let stored_operations: Vec<StoredOperation> = serde_json::from_slice(&json).unwrap();
1177 self.replay = true;
1178 self.stored_operations = stored_operations
1179 .iter()
1180 .cloned()
1181 .enumerate()
1182 .map(|(i, mut operation)| {
1183 if let StoredOperation::Server(Operation::MutateClients {
1184 batch_id: current_batch_id,
1185 user_ids,
1186 ..
1187 }) = &mut operation
1188 {
1189 assert!(user_ids.is_empty());
1190 user_ids.extend(stored_operations[i + 1..].iter().filter_map(|operation| {
1191 if let StoredOperation::Client {
1192 user_id, batch_id, ..
1193 } = operation
1194 {
1195 if batch_id == current_batch_id {
1196 return Some(user_id);
1197 }
1198 }
1199 None
1200 }));
1201 user_ids.sort_unstable();
1202 }
1203 (operation, Arc::new(AtomicBool::new(false)))
1204 })
1205 .collect()
1206 }
1207
1208 fn serialize(&mut self) -> Vec<u8> {
1209 // Format each operation as one line
1210 let mut json = Vec::new();
1211 json.push(b'[');
1212 for (operation, applied) in &self.stored_operations {
1213 if !applied.load(SeqCst) {
1214 continue;
1215 }
1216 if json.len() > 1 {
1217 json.push(b',');
1218 }
1219 json.extend_from_slice(b"\n ");
1220 serde_json::to_writer(&mut json, operation).unwrap();
1221 }
1222 json.extend_from_slice(b"\n]\n");
1223 json
1224 }
1225
1226 fn next_server_operation(
1227 &mut self,
1228 clients: &[(Rc<TestClient>, TestAppContext)],
1229 ) -> Option<(Operation, Arc<AtomicBool>)> {
1230 if self.replay {
1231 while let Some(stored_operation) = self.stored_operations.get(self.operation_ix) {
1232 self.operation_ix += 1;
1233 if let (StoredOperation::Server(operation), applied) = stored_operation {
1234 return Some((operation.clone(), applied.clone()));
1235 }
1236 }
1237 None
1238 } else {
1239 let operation = self.generate_server_operation(clients)?;
1240 let applied = Arc::new(AtomicBool::new(false));
1241 self.stored_operations
1242 .push((StoredOperation::Server(operation.clone()), applied.clone()));
1243 Some((operation, applied))
1244 }
1245 }
1246
1247 fn next_client_operation(
1248 &mut self,
1249 client: &TestClient,
1250 current_batch_id: usize,
1251 cx: &TestAppContext,
1252 ) -> Option<(ClientOperation, Arc<AtomicBool>)> {
1253 let current_user_id = client.current_user_id(cx);
1254 let user_ix = self
1255 .users
1256 .iter()
1257 .position(|user| user.user_id == current_user_id)
1258 .unwrap();
1259 let user_plan = &mut self.users[user_ix];
1260
1261 if self.replay {
1262 while let Some(stored_operation) = self.stored_operations.get(user_plan.operation_ix) {
1263 user_plan.operation_ix += 1;
1264 if let (
1265 StoredOperation::Client {
1266 user_id, operation, ..
1267 },
1268 applied,
1269 ) = stored_operation
1270 {
1271 if user_id == ¤t_user_id {
1272 return Some((operation.clone(), applied.clone()));
1273 }
1274 }
1275 }
1276 None
1277 } else {
1278 let operation = self.generate_client_operation(current_user_id, client, cx)?;
1279 let applied = Arc::new(AtomicBool::new(false));
1280 self.stored_operations.push((
1281 StoredOperation::Client {
1282 user_id: current_user_id,
1283 batch_id: current_batch_id,
1284 operation: operation.clone(),
1285 },
1286 applied.clone(),
1287 ));
1288 Some((operation, applied))
1289 }
1290 }
1291
1292 fn generate_server_operation(
1293 &mut self,
1294 clients: &[(Rc<TestClient>, TestAppContext)],
1295 ) -> Option<Operation> {
1296 if self.operation_ix == self.max_operations {
1297 return None;
1298 }
1299
1300 Some(loop {
1301 break match self.rng.gen_range(0..100) {
1302 0..=29 if clients.len() < self.users.len() => {
1303 let user = self
1304 .users
1305 .iter()
1306 .filter(|u| !u.online)
1307 .choose(&mut self.rng)
1308 .unwrap();
1309 self.operation_ix += 1;
1310 Operation::AddConnection {
1311 user_id: user.user_id,
1312 }
1313 }
1314 30..=34 if clients.len() > 1 && self.allow_client_disconnection => {
1315 let (client, cx) = &clients[self.rng.gen_range(0..clients.len())];
1316 let user_id = client.current_user_id(cx);
1317 self.operation_ix += 1;
1318 Operation::RemoveConnection { user_id }
1319 }
1320 35..=39 if clients.len() > 1 && self.allow_client_reconnection => {
1321 let (client, cx) = &clients[self.rng.gen_range(0..clients.len())];
1322 let user_id = client.current_user_id(cx);
1323 self.operation_ix += 1;
1324 Operation::BounceConnection { user_id }
1325 }
1326 40..=44 if self.allow_server_restarts && clients.len() > 1 => {
1327 self.operation_ix += 1;
1328 Operation::RestartServer
1329 }
1330 _ if !clients.is_empty() => {
1331 let count = self
1332 .rng
1333 .gen_range(1..10)
1334 .min(self.max_operations - self.operation_ix);
1335 let batch_id = util::post_inc(&mut self.next_batch_id);
1336 let mut user_ids = (0..count)
1337 .map(|_| {
1338 let ix = self.rng.gen_range(0..clients.len());
1339 let (client, cx) = &clients[ix];
1340 client.current_user_id(cx)
1341 })
1342 .collect::<Vec<_>>();
1343 user_ids.sort_unstable();
1344 Operation::MutateClients {
1345 user_ids,
1346 batch_id,
1347 quiesce: self.rng.gen_bool(0.7),
1348 }
1349 }
1350 _ => continue,
1351 };
1352 })
1353 }
1354
1355 fn generate_client_operation(
1356 &mut self,
1357 user_id: UserId,
1358 client: &TestClient,
1359 cx: &TestAppContext,
1360 ) -> Option<ClientOperation> {
1361 if self.operation_ix == self.max_operations {
1362 return None;
1363 }
1364
1365 self.operation_ix += 1;
1366 let call = cx.read(ActiveCall::global);
1367 Some(loop {
1368 match self.rng.gen_range(0..100_u32) {
1369 // Mutate the call
1370 0..=29 => {
1371 // Respond to an incoming call
1372 if call.read_with(cx, |call, _| call.incoming().borrow().is_some()) {
1373 break if self.rng.gen_bool(0.7) {
1374 ClientOperation::AcceptIncomingCall
1375 } else {
1376 ClientOperation::RejectIncomingCall
1377 };
1378 }
1379
1380 match self.rng.gen_range(0..100_u32) {
1381 // Invite a contact to the current call
1382 0..=70 => {
1383 let available_contacts =
1384 client.user_store.read_with(cx, |user_store, _| {
1385 user_store
1386 .contacts()
1387 .iter()
1388 .filter(|contact| contact.online && !contact.busy)
1389 .cloned()
1390 .collect::<Vec<_>>()
1391 });
1392 if !available_contacts.is_empty() {
1393 let contact = available_contacts.choose(&mut self.rng).unwrap();
1394 break ClientOperation::InviteContactToCall {
1395 user_id: UserId(contact.user.id as i32),
1396 };
1397 }
1398 }
1399
1400 // Leave the current call
1401 71.. => {
1402 if self.allow_client_disconnection
1403 && call.read_with(cx, |call, _| call.room().is_some())
1404 {
1405 break ClientOperation::LeaveCall;
1406 }
1407 }
1408 }
1409 }
1410
1411 // Mutate projects
1412 30..=59 => match self.rng.gen_range(0..100_u32) {
1413 // Open a new project
1414 0..=70 => {
1415 // Open a remote project
1416 if let Some(room) = call.read_with(cx, |call, _| call.room().cloned()) {
1417 let existing_remote_project_ids = cx.read(|cx| {
1418 client
1419 .remote_projects()
1420 .iter()
1421 .map(|p| p.read(cx).remote_id().unwrap())
1422 .collect::<Vec<_>>()
1423 });
1424 let new_remote_projects = room.read_with(cx, |room, _| {
1425 room.remote_participants()
1426 .values()
1427 .flat_map(|participant| {
1428 participant.projects.iter().filter_map(|project| {
1429 if existing_remote_project_ids.contains(&project.id) {
1430 None
1431 } else {
1432 Some((
1433 UserId::from_proto(participant.user.id),
1434 project.worktree_root_names[0].clone(),
1435 ))
1436 }
1437 })
1438 })
1439 .collect::<Vec<_>>()
1440 });
1441 if !new_remote_projects.is_empty() {
1442 let (host_id, first_root_name) =
1443 new_remote_projects.choose(&mut self.rng).unwrap().clone();
1444 break ClientOperation::OpenRemoteProject {
1445 host_id,
1446 first_root_name,
1447 };
1448 }
1449 }
1450 // Open a local project
1451 else {
1452 let first_root_name = self.next_root_dir_name(user_id);
1453 break ClientOperation::OpenLocalProject { first_root_name };
1454 }
1455 }
1456
1457 // Close a remote project
1458 71..=80 => {
1459 if !client.remote_projects().is_empty() {
1460 let project = client
1461 .remote_projects()
1462 .choose(&mut self.rng)
1463 .unwrap()
1464 .clone();
1465 let first_root_name = root_name_for_project(&project, cx);
1466 break ClientOperation::CloseRemoteProject {
1467 project_root_name: first_root_name,
1468 };
1469 }
1470 }
1471
1472 // Mutate project worktrees
1473 81.. => match self.rng.gen_range(0..100_u32) {
1474 // Add a worktree to a local project
1475 0..=50 => {
1476 let Some(project) = client
1477 .local_projects()
1478 .choose(&mut self.rng)
1479 .cloned() else { continue };
1480 let project_root_name = root_name_for_project(&project, cx);
1481 let mut paths = client.fs.paths();
1482 paths.remove(0);
1483 let new_root_path = if paths.is_empty() || self.rng.gen() {
1484 Path::new("/").join(&self.next_root_dir_name(user_id))
1485 } else {
1486 paths.choose(&mut self.rng).unwrap().clone()
1487 };
1488 break ClientOperation::AddWorktreeToProject {
1489 project_root_name,
1490 new_root_path,
1491 };
1492 }
1493
1494 // Add an entry to a worktree
1495 _ => {
1496 let Some(project) = choose_random_project(client, &mut self.rng) else { continue };
1497 let project_root_name = root_name_for_project(&project, cx);
1498 let is_local = project.read_with(cx, |project, _| project.is_local());
1499 let worktree = project.read_with(cx, |project, cx| {
1500 project
1501 .worktrees(cx)
1502 .filter(|worktree| {
1503 let worktree = worktree.read(cx);
1504 worktree.is_visible()
1505 && worktree.entries(false).any(|e| e.is_file())
1506 && worktree.root_entry().map_or(false, |e| e.is_dir())
1507 })
1508 .choose(&mut self.rng)
1509 });
1510 let Some(worktree) = worktree else { continue };
1511 let is_dir = self.rng.gen::<bool>();
1512 let mut full_path =
1513 worktree.read_with(cx, |w, _| PathBuf::from(w.root_name()));
1514 full_path.push(gen_file_name(&mut self.rng));
1515 if !is_dir {
1516 full_path.set_extension("rs");
1517 }
1518 break ClientOperation::CreateWorktreeEntry {
1519 project_root_name,
1520 is_local,
1521 full_path,
1522 is_dir,
1523 };
1524 }
1525 },
1526 },
1527
1528 // Query and mutate buffers
1529 60..=90 => {
1530 let Some(project) = choose_random_project(client, &mut self.rng) else { continue };
1531 let project_root_name = root_name_for_project(&project, cx);
1532 let is_local = project.read_with(cx, |project, _| project.is_local());
1533
1534 match self.rng.gen_range(0..100_u32) {
1535 // Manipulate an existing buffer
1536 0..=70 => {
1537 let Some(buffer) = client
1538 .buffers_for_project(&project)
1539 .iter()
1540 .choose(&mut self.rng)
1541 .cloned() else { continue };
1542
1543 let full_path = buffer
1544 .read_with(cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
1545
1546 match self.rng.gen_range(0..100_u32) {
1547 // Close the buffer
1548 0..=15 => {
1549 break ClientOperation::CloseBuffer {
1550 project_root_name,
1551 is_local,
1552 full_path,
1553 };
1554 }
1555 // Save the buffer
1556 16..=29 if buffer.read_with(cx, |b, _| b.is_dirty()) => {
1557 let detach = self.rng.gen_bool(0.3);
1558 break ClientOperation::SaveBuffer {
1559 project_root_name,
1560 is_local,
1561 full_path,
1562 detach,
1563 };
1564 }
1565 // Edit the buffer
1566 30..=69 => {
1567 let edits = buffer.read_with(cx, |buffer, _| {
1568 buffer.get_random_edits(&mut self.rng, 3)
1569 });
1570 break ClientOperation::EditBuffer {
1571 project_root_name,
1572 is_local,
1573 full_path,
1574 edits,
1575 };
1576 }
1577 // Make an LSP request
1578 _ => {
1579 let offset = buffer.read_with(cx, |buffer, _| {
1580 buffer.clip_offset(
1581 self.rng.gen_range(0..=buffer.len()),
1582 language::Bias::Left,
1583 )
1584 });
1585 let detach = self.rng.gen();
1586 break ClientOperation::RequestLspDataInBuffer {
1587 project_root_name,
1588 full_path,
1589 offset,
1590 is_local,
1591 kind: match self.rng.gen_range(0..5_u32) {
1592 0 => LspRequestKind::Rename,
1593 1 => LspRequestKind::Highlights,
1594 2 => LspRequestKind::Definition,
1595 3 => LspRequestKind::CodeAction,
1596 4.. => LspRequestKind::Completion,
1597 },
1598 detach,
1599 };
1600 }
1601 }
1602 }
1603
1604 71..=80 => {
1605 let query = self.rng.gen_range('a'..='z').to_string();
1606 let detach = self.rng.gen_bool(0.3);
1607 break ClientOperation::SearchProject {
1608 project_root_name,
1609 is_local,
1610 query,
1611 detach,
1612 };
1613 }
1614
1615 // Open a buffer
1616 81.. => {
1617 let worktree = project.read_with(cx, |project, cx| {
1618 project
1619 .worktrees(cx)
1620 .filter(|worktree| {
1621 let worktree = worktree.read(cx);
1622 worktree.is_visible()
1623 && worktree.entries(false).any(|e| e.is_file())
1624 })
1625 .choose(&mut self.rng)
1626 });
1627 let Some(worktree) = worktree else { continue };
1628 let full_path = worktree.read_with(cx, |worktree, _| {
1629 let entry = worktree
1630 .entries(false)
1631 .filter(|e| e.is_file())
1632 .choose(&mut self.rng)
1633 .unwrap();
1634 if entry.path.as_ref() == Path::new("") {
1635 Path::new(worktree.root_name()).into()
1636 } else {
1637 Path::new(worktree.root_name()).join(&entry.path)
1638 }
1639 });
1640 break ClientOperation::OpenBuffer {
1641 project_root_name,
1642 is_local,
1643 full_path,
1644 };
1645 }
1646 }
1647 }
1648
1649 // Update a git index
1650 91..=95 => {
1651 let repo_path = client
1652 .fs
1653 .directories()
1654 .choose(&mut self.rng)
1655 .unwrap()
1656 .clone();
1657
1658 let mut file_paths = client
1659 .fs
1660 .files()
1661 .into_iter()
1662 .filter(|path| path.starts_with(&repo_path))
1663 .collect::<Vec<_>>();
1664 let count = self.rng.gen_range(0..=file_paths.len());
1665 file_paths.shuffle(&mut self.rng);
1666 file_paths.truncate(count);
1667
1668 let mut contents = Vec::new();
1669 for abs_child_file_path in &file_paths {
1670 let child_file_path = abs_child_file_path
1671 .strip_prefix(&repo_path)
1672 .unwrap()
1673 .to_path_buf();
1674 let new_base = Alphanumeric.sample_string(&mut self.rng, 16);
1675 contents.push((child_file_path, new_base));
1676 }
1677
1678 break ClientOperation::WriteGitIndex {
1679 repo_path,
1680 contents,
1681 };
1682 }
1683
1684 // Create or update a file or directory
1685 96.. => {
1686 let is_dir = self.rng.gen::<bool>();
1687 let content;
1688 let mut path;
1689 let dir_paths = client.fs.directories();
1690
1691 if is_dir {
1692 content = String::new();
1693 path = dir_paths.choose(&mut self.rng).unwrap().clone();
1694 path.push(gen_file_name(&mut self.rng));
1695 } else {
1696 content = Alphanumeric.sample_string(&mut self.rng, 16);
1697
1698 // Create a new file or overwrite an existing file
1699 let file_paths = client.fs.files();
1700 if file_paths.is_empty() || self.rng.gen_bool(0.5) {
1701 path = dir_paths.choose(&mut self.rng).unwrap().clone();
1702 path.push(gen_file_name(&mut self.rng));
1703 path.set_extension("rs");
1704 } else {
1705 path = file_paths.choose(&mut self.rng).unwrap().clone()
1706 };
1707 }
1708 break ClientOperation::WriteFsEntry {
1709 path,
1710 is_dir,
1711 content,
1712 };
1713 }
1714 }
1715 })
1716 }
1717
1718 fn next_root_dir_name(&mut self, user_id: UserId) -> String {
1719 let user_ix = self
1720 .users
1721 .iter()
1722 .position(|user| user.user_id == user_id)
1723 .unwrap();
1724 let root_id = util::post_inc(&mut self.users[user_ix].next_root_id);
1725 format!("dir-{user_id}-{root_id}")
1726 }
1727
1728 fn user(&mut self, user_id: UserId) -> &mut UserTestPlan {
1729 let ix = self
1730 .users
1731 .iter()
1732 .position(|user| user.user_id == user_id)
1733 .unwrap();
1734 &mut self.users[ix]
1735 }
1736}
1737
1738async fn simulate_client(
1739 client: Rc<TestClient>,
1740 mut operation_rx: futures::channel::mpsc::UnboundedReceiver<usize>,
1741 plan: Arc<Mutex<TestPlan>>,
1742 mut cx: TestAppContext,
1743) {
1744 // Setup language server
1745 let mut language = Language::new(
1746 LanguageConfig {
1747 name: "Rust".into(),
1748 path_suffixes: vec!["rs".to_string()],
1749 ..Default::default()
1750 },
1751 None,
1752 );
1753 let _fake_language_servers = language
1754 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1755 name: "the-fake-language-server",
1756 capabilities: lsp::LanguageServer::full_capabilities(),
1757 initializer: Some(Box::new({
1758 let plan = plan.clone();
1759 let fs = client.fs.clone();
1760 move |fake_server: &mut FakeLanguageServer| {
1761 fake_server.handle_request::<lsp::request::Completion, _, _>(
1762 |_, _| async move {
1763 Ok(Some(lsp::CompletionResponse::Array(vec![
1764 lsp::CompletionItem {
1765 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1766 range: lsp::Range::new(
1767 lsp::Position::new(0, 0),
1768 lsp::Position::new(0, 0),
1769 ),
1770 new_text: "the-new-text".to_string(),
1771 })),
1772 ..Default::default()
1773 },
1774 ])))
1775 },
1776 );
1777
1778 fake_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
1779 |_, _| async move {
1780 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
1781 lsp::CodeAction {
1782 title: "the-code-action".to_string(),
1783 ..Default::default()
1784 },
1785 )]))
1786 },
1787 );
1788
1789 fake_server.handle_request::<lsp::request::PrepareRenameRequest, _, _>(
1790 |params, _| async move {
1791 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
1792 params.position,
1793 params.position,
1794 ))))
1795 },
1796 );
1797
1798 fake_server.handle_request::<lsp::request::GotoDefinition, _, _>({
1799 let fs = fs.clone();
1800 let plan = plan.clone();
1801 move |_, _| {
1802 let fs = fs.clone();
1803 let plan = plan.clone();
1804 async move {
1805 let files = fs.files();
1806 let count = plan.lock().rng.gen_range::<usize, _>(1..3);
1807 let files = (0..count)
1808 .map(|_| files.choose(&mut plan.lock().rng).unwrap())
1809 .collect::<Vec<_>>();
1810 log::info!("LSP: Returning definitions in files {:?}", &files);
1811 Ok(Some(lsp::GotoDefinitionResponse::Array(
1812 files
1813 .into_iter()
1814 .map(|file| lsp::Location {
1815 uri: lsp::Url::from_file_path(file).unwrap(),
1816 range: Default::default(),
1817 })
1818 .collect(),
1819 )))
1820 }
1821 }
1822 });
1823
1824 fake_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>({
1825 let plan = plan.clone();
1826 move |_, _| {
1827 let mut highlights = Vec::new();
1828 let highlight_count = plan.lock().rng.gen_range(1..=5);
1829 for _ in 0..highlight_count {
1830 let start_row = plan.lock().rng.gen_range(0..100);
1831 let start_column = plan.lock().rng.gen_range(0..100);
1832 let start = PointUtf16::new(start_row, start_column);
1833 let end_row = plan.lock().rng.gen_range(0..100);
1834 let end_column = plan.lock().rng.gen_range(0..100);
1835 let end = PointUtf16::new(end_row, end_column);
1836 let range = if start > end { end..start } else { start..end };
1837 highlights.push(lsp::DocumentHighlight {
1838 range: range_to_lsp(range.clone()),
1839 kind: Some(lsp::DocumentHighlightKind::READ),
1840 });
1841 }
1842 highlights.sort_unstable_by_key(|highlight| {
1843 (highlight.range.start, highlight.range.end)
1844 });
1845 async move { Ok(Some(highlights)) }
1846 }
1847 });
1848 }
1849 })),
1850 ..Default::default()
1851 }))
1852 .await;
1853 client.language_registry.add(Arc::new(language));
1854
1855 while let Some(batch_id) = operation_rx.next().await {
1856 let Some((operation, applied)) = plan.lock().next_client_operation(&client, batch_id, &cx) else { break };
1857 match apply_client_operation(&client, operation, &mut cx).await {
1858 Ok(()) => applied.store(true, SeqCst),
1859 Err(TestError::Inapplicable) => {
1860 log::info!("skipped operation");
1861 }
1862 Err(TestError::Other(error)) => {
1863 applied.store(true, SeqCst);
1864 log::error!("{} error: {}", client.username, error);
1865 }
1866 }
1867 cx.background().simulate_random_delay().await;
1868 }
1869 log::info!("{}: done", client.username);
1870}
1871
1872fn buffer_for_full_path(
1873 client: &TestClient,
1874 project: &ModelHandle<Project>,
1875 full_path: &PathBuf,
1876 cx: &TestAppContext,
1877) -> Option<ModelHandle<language::Buffer>> {
1878 client
1879 .buffers_for_project(project)
1880 .iter()
1881 .find(|buffer| {
1882 buffer.read_with(cx, |buffer, cx| {
1883 buffer.file().unwrap().full_path(cx) == *full_path
1884 })
1885 })
1886 .cloned()
1887}
1888
1889fn project_for_root_name(
1890 client: &TestClient,
1891 root_name: &str,
1892 cx: &TestAppContext,
1893) -> Option<ModelHandle<Project>> {
1894 if let Some(ix) = project_ix_for_root_name(&*client.local_projects(), root_name, cx) {
1895 return Some(client.local_projects()[ix].clone());
1896 }
1897 if let Some(ix) = project_ix_for_root_name(&*client.remote_projects(), root_name, cx) {
1898 return Some(client.remote_projects()[ix].clone());
1899 }
1900 None
1901}
1902
1903fn project_ix_for_root_name(
1904 projects: &[ModelHandle<Project>],
1905 root_name: &str,
1906 cx: &TestAppContext,
1907) -> Option<usize> {
1908 projects.iter().position(|project| {
1909 project.read_with(cx, |project, cx| {
1910 let worktree = project.visible_worktrees(cx).next().unwrap();
1911 worktree.read(cx).root_name() == root_name
1912 })
1913 })
1914}
1915
1916fn root_name_for_project(project: &ModelHandle<Project>, cx: &TestAppContext) -> String {
1917 project.read_with(cx, |project, cx| {
1918 project
1919 .visible_worktrees(cx)
1920 .next()
1921 .unwrap()
1922 .read(cx)
1923 .root_name()
1924 .to_string()
1925 })
1926}
1927
1928fn project_path_for_full_path(
1929 project: &ModelHandle<Project>,
1930 full_path: &Path,
1931 cx: &TestAppContext,
1932) -> Option<ProjectPath> {
1933 let mut components = full_path.components();
1934 let root_name = components.next().unwrap().as_os_str().to_str().unwrap();
1935 let path = components.as_path().into();
1936 let worktree_id = project.read_with(cx, |project, cx| {
1937 project.worktrees(cx).find_map(|worktree| {
1938 let worktree = worktree.read(cx);
1939 if worktree.root_name() == root_name {
1940 Some(worktree.id())
1941 } else {
1942 None
1943 }
1944 })
1945 })?;
1946 Some(ProjectPath { worktree_id, path })
1947}
1948
1949async fn ensure_project_shared(
1950 project: &ModelHandle<Project>,
1951 client: &TestClient,
1952 cx: &mut TestAppContext,
1953) {
1954 let first_root_name = root_name_for_project(project, cx);
1955 let active_call = cx.read(ActiveCall::global);
1956 if active_call.read_with(cx, |call, _| call.room().is_some())
1957 && project.read_with(cx, |project, _| project.is_local() && !project.is_shared())
1958 {
1959 match active_call
1960 .update(cx, |call, cx| call.share_project(project.clone(), cx))
1961 .await
1962 {
1963 Ok(project_id) => {
1964 log::info!(
1965 "{}: shared project {} with id {}",
1966 client.username,
1967 first_root_name,
1968 project_id
1969 );
1970 }
1971 Err(error) => {
1972 log::error!(
1973 "{}: error sharing project {}: {:?}",
1974 client.username,
1975 first_root_name,
1976 error
1977 );
1978 }
1979 }
1980 }
1981}
1982
1983fn choose_random_project(client: &TestClient, rng: &mut StdRng) -> Option<ModelHandle<Project>> {
1984 client
1985 .local_projects()
1986 .iter()
1987 .chain(client.remote_projects().iter())
1988 .choose(rng)
1989 .cloned()
1990}
1991
1992fn gen_file_name(rng: &mut StdRng) -> String {
1993 let mut name = String::new();
1994 for _ in 0..10 {
1995 let letter = rng.gen_range('a'..='z');
1996 name.push(letter);
1997 }
1998 name
1999}
2000
2001fn path_env_var(name: &str) -> Option<PathBuf> {
2002 let value = env::var(name).ok()?;
2003 let mut path = PathBuf::from(value);
2004 if path.is_relative() {
2005 let mut abs_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
2006 abs_path.pop();
2007 abs_path.pop();
2008 abs_path.push(path);
2009 path = abs_path
2010 }
2011 Some(path)
2012}