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