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