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