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