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 drop(project);
601 });
602 }
603
604 ClientOperation::OpenRemoteProject {
605 host_id,
606 first_root_name,
607 } => {
608 let active_call = cx.read(ActiveCall::global);
609 let project = active_call.update(cx, |call, cx| {
610 let room = call.room().cloned()?;
611 let participant = room
612 .read(cx)
613 .remote_participants()
614 .get(&host_id.to_proto())?;
615 let project_id = participant
616 .projects
617 .iter()
618 .find(|project| project.worktree_root_names[0] == first_root_name)?
619 .id;
620 Some(room.update(cx, |room, cx| {
621 room.join_project(
622 project_id,
623 client.language_registry.clone(),
624 FakeFs::new(cx.background().clone()),
625 cx,
626 )
627 }))
628 });
629 let Some(project) = project else {
630 return Ok(false)
631 };
632
633 log::info!(
634 "{}: joining remote project of user {}, root name {}",
635 client.username,
636 host_id,
637 first_root_name,
638 );
639
640 let project = project.await?;
641 client.remote_projects_mut().push(project.clone());
642 }
643
644 ClientOperation::CreateWorktreeEntry {
645 project_root_name,
646 is_local,
647 full_path,
648 is_dir,
649 } => {
650 let Some(project) = project_for_root_name(client, &project_root_name, cx) else {
651 return Ok(false);
652 };
653 let Some(project_path) = project_path_for_full_path(&project, &full_path, cx) else {
654 return Ok(false);
655 };
656
657 log::info!(
658 "{}: creating {} at path {:?} in {} project {}",
659 client.username,
660 if is_dir { "dir" } else { "file" },
661 full_path,
662 if is_local { "local" } else { "remote" },
663 project_root_name,
664 );
665
666 ensure_project_shared(&project, client, cx).await;
667 project
668 .update(cx, |p, cx| p.create_entry(project_path, is_dir, cx))
669 .unwrap()
670 .await?;
671 }
672
673 ClientOperation::OpenBuffer {
674 project_root_name,
675 is_local,
676 full_path,
677 } => {
678 let Some(project) = project_for_root_name(client, &project_root_name, cx) else {
679 return Ok(false);
680 };
681 let Some(project_path) = project_path_for_full_path(&project, &full_path, cx) else {
682 return Ok(false);
683 };
684
685 log::info!(
686 "{}: opening buffer {:?} in {} project {}",
687 client.username,
688 full_path,
689 if is_local { "local" } else { "remote" },
690 project_root_name,
691 );
692
693 ensure_project_shared(&project, client, cx).await;
694 let buffer = project
695 .update(cx, |project, cx| project.open_buffer(project_path, cx))
696 .await?;
697 client.buffers_for_project(&project).insert(buffer);
698 }
699
700 ClientOperation::EditBuffer {
701 project_root_name,
702 is_local,
703 full_path,
704 edits,
705 } => {
706 let Some(project) = project_for_root_name(client, &project_root_name, cx) else {
707 return Ok(false);
708 };
709 let Some(buffer) =
710 buffer_for_full_path(&*client.buffers_for_project(&project), &full_path, cx) else {
711 return Ok(false);
712 };
713
714 log::info!(
715 "{}: editing buffer {:?} in {} project {} with {:?}",
716 client.username,
717 full_path,
718 if is_local { "local" } else { "remote" },
719 project_root_name,
720 edits
721 );
722
723 ensure_project_shared(&project, client, cx).await;
724 buffer.update(cx, |buffer, cx| {
725 let snapshot = buffer.snapshot();
726 buffer.edit(
727 edits.into_iter().map(|(range, text)| {
728 let start = snapshot.clip_offset(range.start, Bias::Left);
729 let end = snapshot.clip_offset(range.end, Bias::Right);
730 (start..end, text)
731 }),
732 None,
733 cx,
734 );
735 });
736 }
737
738 ClientOperation::CloseBuffer {
739 project_root_name,
740 is_local,
741 full_path,
742 } => {
743 let Some(project) = project_for_root_name(client, &project_root_name, cx) else {
744 return Ok(false);
745 };
746 let Some(buffer) =
747 buffer_for_full_path(&*client.buffers_for_project(&project), &full_path, cx) else {
748 return Ok(false);
749 };
750
751 log::info!(
752 "{}: closing buffer {:?} in {} project {}",
753 client.username,
754 full_path,
755 if is_local { "local" } else { "remote" },
756 project_root_name
757 );
758
759 ensure_project_shared(&project, client, cx).await;
760 cx.update(|_| {
761 client.buffers_for_project(&project).remove(&buffer);
762 drop(buffer);
763 });
764 }
765
766 ClientOperation::SaveBuffer {
767 project_root_name,
768 is_local,
769 full_path,
770 detach,
771 } => {
772 let Some(project) = project_for_root_name(client, &project_root_name, cx) else {
773 return Ok(false);
774 };
775 let Some(buffer) =
776 buffer_for_full_path(&*client.buffers_for_project(&project), &full_path, cx) else {
777 return Ok(false);
778 };
779
780 log::info!(
781 "{}: saving buffer {:?} in {} project {}{}",
782 client.username,
783 full_path,
784 if is_local { "local" } else { "remote" },
785 project_root_name,
786 if detach { ", detaching" } else { ", awaiting" }
787 );
788
789 ensure_project_shared(&project, client, cx).await;
790 let (requested_version, save) =
791 buffer.update(cx, |buffer, cx| (buffer.version(), buffer.save(cx)));
792 let save = cx.background().spawn(async move {
793 let (saved_version, _, _) = save
794 .await
795 .map_err(|err| anyhow!("save request failed: {:?}", err))?;
796 assert!(saved_version.observed_all(&requested_version));
797 anyhow::Ok(())
798 });
799 if detach {
800 log::info!("{}: detaching save request", client.username);
801 cx.update(|cx| save.detach_and_log_err(cx));
802 } else {
803 save.await?;
804 }
805 }
806
807 ClientOperation::RequestLspDataInBuffer {
808 project_root_name,
809 is_local,
810 full_path,
811 offset,
812 kind,
813 detach,
814 } => {
815 let Some(project) = project_for_root_name(client, &project_root_name, cx) else {
816 return Ok(false);
817 };
818 let Some(buffer) =
819 buffer_for_full_path(&*client.buffers_for_project(&project), &full_path, cx) else {
820 return Ok(false);
821 };
822
823 log::info!(
824 "{}: request LSP {:?} for buffer {:?} in {} project {}{}",
825 client.username,
826 kind,
827 full_path,
828 if is_local { "local" } else { "remote" },
829 project_root_name,
830 if detach { ", detaching" } else { ", awaiting" }
831 );
832
833 let offset = buffer.read_with(cx, |b, _| b.clip_offset(offset, Bias::Left));
834 let request = match kind {
835 LspRequestKind::Rename => cx.spawn(|mut cx| async move {
836 project
837 .update(&mut cx, |p, cx| p.prepare_rename(buffer, offset, cx))
838 .await?;
839 anyhow::Ok(())
840 }),
841 LspRequestKind::Completion => cx.spawn(|mut cx| async move {
842 project
843 .update(&mut cx, |p, cx| p.completions(&buffer, offset, cx))
844 .await?;
845 Ok(())
846 }),
847 LspRequestKind::CodeAction => cx.spawn(|mut cx| async move {
848 project
849 .update(&mut cx, |p, cx| p.code_actions(&buffer, offset..offset, cx))
850 .await?;
851 Ok(())
852 }),
853 LspRequestKind::Definition => cx.spawn(|mut cx| async move {
854 project
855 .update(&mut cx, |p, cx| p.definition(&buffer, offset, cx))
856 .await?;
857 Ok(())
858 }),
859 LspRequestKind::Highlights => cx.spawn(|mut cx| async move {
860 project
861 .update(&mut cx, |p, cx| p.document_highlights(&buffer, offset, cx))
862 .await?;
863 Ok(())
864 }),
865 };
866 if detach {
867 request.detach();
868 } else {
869 request.await?;
870 }
871 }
872
873 ClientOperation::SearchProject {
874 project_root_name,
875 is_local,
876 query,
877 detach,
878 } => {
879 let Some(project) = project_for_root_name(client, &project_root_name, cx) else {
880 return Ok(false);
881 };
882
883 log::info!(
884 "{}: search {} project {} for {:?}{}",
885 client.username,
886 if is_local { "local" } else { "remote" },
887 project_root_name,
888 query,
889 if detach { ", detaching" } else { ", awaiting" }
890 );
891
892 let search = project.update(cx, |project, cx| {
893 project.search(SearchQuery::text(query, false, false), cx)
894 });
895 let search = cx.background().spawn(async move {
896 search
897 .await
898 .map_err(|err| anyhow!("search request failed: {:?}", err))
899 });
900 if detach {
901 log::info!("{}: detaching save request", client.username);
902 cx.update(|cx| search.detach_and_log_err(cx));
903 } else {
904 search.await?;
905 }
906 }
907
908 ClientOperation::CreateFsEntry { path, is_dir } => {
909 if client.fs.metadata(&path.parent().unwrap()).await?.is_none() {
910 return Ok(false);
911 }
912
913 log::info!(
914 "{}: creating {} at {:?}",
915 client.username,
916 if is_dir { "dir" } else { "file" },
917 path
918 );
919
920 if is_dir {
921 client.fs.create_dir(&path).await.unwrap();
922 } else {
923 client
924 .fs
925 .create_file(&path, Default::default())
926 .await
927 .unwrap();
928 }
929 }
930
931 ClientOperation::WriteGitIndex {
932 repo_path,
933 contents,
934 } => {
935 if !client
936 .fs
937 .metadata(&repo_path)
938 .await?
939 .map_or(false, |m| m.is_dir)
940 {
941 return Ok(false);
942 }
943
944 log::info!(
945 "{}: writing git index for repo {:?}: {:?}",
946 client.username,
947 repo_path,
948 contents
949 );
950
951 let dot_git_dir = repo_path.join(".git");
952 let contents = contents
953 .iter()
954 .map(|(path, contents)| (path.as_path(), contents.clone()))
955 .collect::<Vec<_>>();
956 if client.fs.metadata(&dot_git_dir).await?.is_none() {
957 client.fs.create_dir(&dot_git_dir).await?;
958 }
959 client.fs.set_index_for_repo(&dot_git_dir, &contents).await;
960 }
961 }
962 Ok(true)
963}
964
965struct TestPlan {
966 rng: StdRng,
967 replay: bool,
968 stored_operations: Vec<(StoredOperation, Arc<AtomicBool>)>,
969 max_operations: usize,
970 operation_ix: usize,
971 users: Vec<UserTestPlan>,
972 allow_server_restarts: bool,
973 allow_client_reconnection: bool,
974 allow_client_disconnection: bool,
975}
976
977struct UserTestPlan {
978 user_id: UserId,
979 username: String,
980 next_root_id: usize,
981 operation_ix: usize,
982 online: bool,
983}
984
985#[derive(Clone, Debug, Serialize, Deserialize)]
986#[serde(untagged)]
987enum StoredOperation {
988 Server(Operation),
989 Client {
990 user_id: UserId,
991 operation: ClientOperation,
992 },
993}
994
995#[derive(Clone, Debug, Serialize, Deserialize)]
996enum Operation {
997 AddConnection {
998 user_id: UserId,
999 },
1000 RemoveConnection {
1001 user_id: UserId,
1002 },
1003 BounceConnection {
1004 user_id: UserId,
1005 },
1006 RestartServer,
1007 MutateClients {
1008 user_ids: Vec<UserId>,
1009 quiesce: bool,
1010 },
1011}
1012
1013#[derive(Clone, Debug, Serialize, Deserialize)]
1014enum ClientOperation {
1015 AcceptIncomingCall,
1016 RejectIncomingCall,
1017 LeaveCall,
1018 InviteContactToCall {
1019 user_id: UserId,
1020 },
1021 OpenLocalProject {
1022 first_root_name: String,
1023 },
1024 OpenRemoteProject {
1025 host_id: UserId,
1026 first_root_name: String,
1027 },
1028 AddWorktreeToProject {
1029 project_root_name: String,
1030 new_root_path: PathBuf,
1031 },
1032 CloseRemoteProject {
1033 project_root_name: String,
1034 },
1035 OpenBuffer {
1036 project_root_name: String,
1037 is_local: bool,
1038 full_path: PathBuf,
1039 },
1040 SearchProject {
1041 project_root_name: String,
1042 is_local: bool,
1043 query: String,
1044 detach: bool,
1045 },
1046 EditBuffer {
1047 project_root_name: String,
1048 is_local: bool,
1049 full_path: PathBuf,
1050 edits: Vec<(Range<usize>, Arc<str>)>,
1051 },
1052 CloseBuffer {
1053 project_root_name: String,
1054 is_local: bool,
1055 full_path: PathBuf,
1056 },
1057 SaveBuffer {
1058 project_root_name: String,
1059 is_local: bool,
1060 full_path: PathBuf,
1061 detach: bool,
1062 },
1063 RequestLspDataInBuffer {
1064 project_root_name: String,
1065 is_local: bool,
1066 full_path: PathBuf,
1067 offset: usize,
1068 kind: LspRequestKind,
1069 detach: bool,
1070 },
1071 CreateWorktreeEntry {
1072 project_root_name: String,
1073 is_local: bool,
1074 full_path: PathBuf,
1075 is_dir: bool,
1076 },
1077 CreateFsEntry {
1078 path: PathBuf,
1079 is_dir: bool,
1080 },
1081 WriteGitIndex {
1082 repo_path: PathBuf,
1083 contents: Vec<(PathBuf, String)>,
1084 },
1085}
1086
1087#[derive(Clone, Debug, Serialize, Deserialize)]
1088enum LspRequestKind {
1089 Rename,
1090 Completion,
1091 CodeAction,
1092 Definition,
1093 Highlights,
1094}
1095
1096impl TestPlan {
1097 fn new(mut rng: StdRng, users: Vec<UserTestPlan>, max_operations: usize) -> Self {
1098 Self {
1099 replay: false,
1100 allow_server_restarts: rng.gen_bool(0.7),
1101 allow_client_reconnection: rng.gen_bool(0.7),
1102 allow_client_disconnection: rng.gen_bool(0.1),
1103 stored_operations: Vec::new(),
1104 operation_ix: 0,
1105 max_operations,
1106 users,
1107 rng,
1108 }
1109 }
1110
1111 fn load(&mut self, path: &Path) {
1112 let json = std::fs::read_to_string(path).unwrap();
1113 self.replay = true;
1114 let stored_operations: Vec<StoredOperation> = serde_json::from_str(&json).unwrap();
1115 self.stored_operations = stored_operations
1116 .into_iter()
1117 .map(|operation| (operation, Arc::new(AtomicBool::new(false))))
1118 .collect()
1119 }
1120
1121 fn save(&mut self, path: &Path) {
1122 // Format each operation as one line
1123 let mut json = Vec::new();
1124 json.push(b'[');
1125 for (operation, skipped) in &self.stored_operations {
1126 if skipped.load(SeqCst) {
1127 continue;
1128 }
1129 if json.len() > 1 {
1130 json.push(b',');
1131 }
1132 json.extend_from_slice(b"\n ");
1133 serde_json::to_writer(&mut json, operation).unwrap();
1134 }
1135 json.extend_from_slice(b"\n]\n");
1136 std::fs::write(path, &json).unwrap();
1137 }
1138
1139 fn next_server_operation(
1140 &mut self,
1141 clients: &[(Rc<TestClient>, TestAppContext)],
1142 ) -> Option<(Operation, Arc<AtomicBool>)> {
1143 if self.replay {
1144 while let Some(stored_operation) = self.stored_operations.get(self.operation_ix) {
1145 self.operation_ix += 1;
1146 if let (StoredOperation::Server(operation), skipped) = stored_operation {
1147 return Some((operation.clone(), skipped.clone()));
1148 }
1149 }
1150 None
1151 } else {
1152 let operation = self.generate_server_operation(clients)?;
1153 let skipped = Arc::new(AtomicBool::new(false));
1154 self.stored_operations
1155 .push((StoredOperation::Server(operation.clone()), skipped.clone()));
1156 Some((operation, skipped))
1157 }
1158 }
1159
1160 fn next_client_operation(
1161 &mut self,
1162 client: &TestClient,
1163 cx: &TestAppContext,
1164 ) -> Option<(ClientOperation, Arc<AtomicBool>)> {
1165 let current_user_id = client.current_user_id(cx);
1166 let user_ix = self
1167 .users
1168 .iter()
1169 .position(|user| user.user_id == current_user_id)
1170 .unwrap();
1171 let user_plan = &mut self.users[user_ix];
1172
1173 if self.replay {
1174 while let Some(stored_operation) = self.stored_operations.get(user_plan.operation_ix) {
1175 user_plan.operation_ix += 1;
1176 if let (StoredOperation::Client { user_id, operation }, skipped) = stored_operation
1177 {
1178 if user_id == ¤t_user_id {
1179 return Some((operation.clone(), skipped.clone()));
1180 }
1181 }
1182 }
1183 None
1184 } else {
1185 let operation = self.generate_client_operation(current_user_id, client, cx)?;
1186 let skipped = Arc::new(AtomicBool::new(false));
1187 self.stored_operations.push((
1188 StoredOperation::Client {
1189 user_id: current_user_id,
1190 operation: operation.clone(),
1191 },
1192 skipped.clone(),
1193 ));
1194 Some((operation, skipped))
1195 }
1196 }
1197
1198 fn generate_server_operation(
1199 &mut self,
1200 clients: &[(Rc<TestClient>, TestAppContext)],
1201 ) -> Option<Operation> {
1202 if self.operation_ix == self.max_operations {
1203 return None;
1204 }
1205
1206 Some(loop {
1207 break match self.rng.gen_range(0..100) {
1208 0..=29 if clients.len() < self.users.len() => {
1209 let user = self
1210 .users
1211 .iter()
1212 .filter(|u| !u.online)
1213 .choose(&mut self.rng)
1214 .unwrap();
1215 self.operation_ix += 1;
1216 Operation::AddConnection {
1217 user_id: user.user_id,
1218 }
1219 }
1220 30..=34 if clients.len() > 1 && self.allow_client_disconnection => {
1221 let (client, cx) = &clients[self.rng.gen_range(0..clients.len())];
1222 let user_id = client.current_user_id(cx);
1223 self.operation_ix += 1;
1224 Operation::RemoveConnection { user_id }
1225 }
1226 35..=39 if clients.len() > 1 && self.allow_client_reconnection => {
1227 let (client, cx) = &clients[self.rng.gen_range(0..clients.len())];
1228 let user_id = client.current_user_id(cx);
1229 self.operation_ix += 1;
1230 Operation::BounceConnection { user_id }
1231 }
1232 40..=44 if self.allow_server_restarts && clients.len() > 1 => {
1233 self.operation_ix += 1;
1234 Operation::RestartServer
1235 }
1236 _ if !clients.is_empty() => {
1237 let count = self
1238 .rng
1239 .gen_range(1..10)
1240 .min(self.max_operations - self.operation_ix);
1241 let user_ids = (0..count)
1242 .map(|_| {
1243 let ix = self.rng.gen_range(0..clients.len());
1244 let (client, cx) = &clients[ix];
1245 client.current_user_id(cx)
1246 })
1247 .collect();
1248 Operation::MutateClients {
1249 user_ids,
1250 quiesce: self.rng.gen_bool(0.7),
1251 }
1252 }
1253 _ => continue,
1254 };
1255 })
1256 }
1257
1258 fn generate_client_operation(
1259 &mut self,
1260 user_id: UserId,
1261 client: &TestClient,
1262 cx: &TestAppContext,
1263 ) -> Option<ClientOperation> {
1264 if self.operation_ix == self.max_operations {
1265 return None;
1266 }
1267
1268 let executor = cx.background();
1269 self.operation_ix += 1;
1270 let call = cx.read(ActiveCall::global);
1271 Some(loop {
1272 match self.rng.gen_range(0..100_u32) {
1273 // Mutate the call
1274 0..=29 => {
1275 // Respond to an incoming call
1276 if call.read_with(cx, |call, _| call.incoming().borrow().is_some()) {
1277 break if self.rng.gen_bool(0.7) {
1278 ClientOperation::AcceptIncomingCall
1279 } else {
1280 ClientOperation::RejectIncomingCall
1281 };
1282 }
1283
1284 match self.rng.gen_range(0..100_u32) {
1285 // Invite a contact to the current call
1286 0..=70 => {
1287 let available_contacts =
1288 client.user_store.read_with(cx, |user_store, _| {
1289 user_store
1290 .contacts()
1291 .iter()
1292 .filter(|contact| contact.online && !contact.busy)
1293 .cloned()
1294 .collect::<Vec<_>>()
1295 });
1296 if !available_contacts.is_empty() {
1297 let contact = available_contacts.choose(&mut self.rng).unwrap();
1298 break ClientOperation::InviteContactToCall {
1299 user_id: UserId(contact.user.id as i32),
1300 };
1301 }
1302 }
1303
1304 // Leave the current call
1305 71.. => {
1306 if self.allow_client_disconnection
1307 && call.read_with(cx, |call, _| call.room().is_some())
1308 {
1309 break ClientOperation::LeaveCall;
1310 }
1311 }
1312 }
1313 }
1314
1315 // Mutate projects
1316 30..=59 => match self.rng.gen_range(0..100_u32) {
1317 // Open a new project
1318 0..=70 => {
1319 // Open a remote project
1320 if let Some(room) = call.read_with(cx, |call, _| call.room().cloned()) {
1321 let existing_remote_project_ids = cx.read(|cx| {
1322 client
1323 .remote_projects()
1324 .iter()
1325 .map(|p| p.read(cx).remote_id().unwrap())
1326 .collect::<Vec<_>>()
1327 });
1328 let new_remote_projects = room.read_with(cx, |room, _| {
1329 room.remote_participants()
1330 .values()
1331 .flat_map(|participant| {
1332 participant.projects.iter().filter_map(|project| {
1333 if existing_remote_project_ids.contains(&project.id) {
1334 None
1335 } else {
1336 Some((
1337 UserId::from_proto(participant.user.id),
1338 project.worktree_root_names[0].clone(),
1339 ))
1340 }
1341 })
1342 })
1343 .collect::<Vec<_>>()
1344 });
1345 if !new_remote_projects.is_empty() {
1346 let (host_id, first_root_name) =
1347 new_remote_projects.choose(&mut self.rng).unwrap().clone();
1348 break ClientOperation::OpenRemoteProject {
1349 host_id,
1350 first_root_name,
1351 };
1352 }
1353 }
1354 // Open a local project
1355 else {
1356 let first_root_name = self.next_root_dir_name(user_id);
1357 break ClientOperation::OpenLocalProject { first_root_name };
1358 }
1359 }
1360
1361 // Close a remote project
1362 71..=80 => {
1363 if !client.remote_projects().is_empty() {
1364 let project = client
1365 .remote_projects()
1366 .choose(&mut self.rng)
1367 .unwrap()
1368 .clone();
1369 let first_root_name = root_name_for_project(&project, cx);
1370 break ClientOperation::CloseRemoteProject {
1371 project_root_name: first_root_name,
1372 };
1373 }
1374 }
1375
1376 // Mutate project worktrees
1377 81.. => match self.rng.gen_range(0..100_u32) {
1378 // Add a worktree to a local project
1379 0..=50 => {
1380 let Some(project) = client
1381 .local_projects()
1382 .choose(&mut self.rng)
1383 .cloned() else { continue };
1384 let project_root_name = root_name_for_project(&project, cx);
1385 let mut paths = executor.block(client.fs.paths());
1386 paths.remove(0);
1387 let new_root_path = if paths.is_empty() || self.rng.gen() {
1388 Path::new("/").join(&self.next_root_dir_name(user_id))
1389 } else {
1390 paths.choose(&mut self.rng).unwrap().clone()
1391 };
1392 break ClientOperation::AddWorktreeToProject {
1393 project_root_name,
1394 new_root_path,
1395 };
1396 }
1397
1398 // Add an entry to a worktree
1399 _ => {
1400 let Some(project) = choose_random_project(client, &mut self.rng) else { continue };
1401 let project_root_name = root_name_for_project(&project, cx);
1402 let is_local = project.read_with(cx, |project, _| project.is_local());
1403 let worktree = project.read_with(cx, |project, cx| {
1404 project
1405 .worktrees(cx)
1406 .filter(|worktree| {
1407 let worktree = worktree.read(cx);
1408 worktree.is_visible()
1409 && worktree.entries(false).any(|e| e.is_file())
1410 && worktree.root_entry().map_or(false, |e| e.is_dir())
1411 })
1412 .choose(&mut self.rng)
1413 });
1414 let Some(worktree) = worktree else { continue };
1415 let is_dir = self.rng.gen::<bool>();
1416 let mut full_path =
1417 worktree.read_with(cx, |w, _| PathBuf::from(w.root_name()));
1418 full_path.push(gen_file_name(&mut self.rng));
1419 if !is_dir {
1420 full_path.set_extension("rs");
1421 }
1422 break ClientOperation::CreateWorktreeEntry {
1423 project_root_name,
1424 is_local,
1425 full_path,
1426 is_dir,
1427 };
1428 }
1429 },
1430 },
1431
1432 // Query and mutate buffers
1433 60..=90 => {
1434 let Some(project) = choose_random_project(client, &mut self.rng) else { continue };
1435 let project_root_name = root_name_for_project(&project, cx);
1436 let is_local = project.read_with(cx, |project, _| project.is_local());
1437
1438 match self.rng.gen_range(0..100_u32) {
1439 // Manipulate an existing buffer
1440 0..=70 => {
1441 let Some(buffer) = client
1442 .buffers_for_project(&project)
1443 .iter()
1444 .choose(&mut self.rng)
1445 .cloned() else { continue };
1446
1447 let full_path = buffer
1448 .read_with(cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
1449
1450 match self.rng.gen_range(0..100_u32) {
1451 // Close the buffer
1452 0..=15 => {
1453 break ClientOperation::CloseBuffer {
1454 project_root_name,
1455 is_local,
1456 full_path,
1457 };
1458 }
1459 // Save the buffer
1460 16..=29 if buffer.read_with(cx, |b, _| b.is_dirty()) => {
1461 let detach = self.rng.gen_bool(0.3);
1462 break ClientOperation::SaveBuffer {
1463 project_root_name,
1464 is_local,
1465 full_path,
1466 detach,
1467 };
1468 }
1469 // Edit the buffer
1470 30..=69 => {
1471 let edits = buffer.read_with(cx, |buffer, _| {
1472 buffer.get_random_edits(&mut self.rng, 3)
1473 });
1474 break ClientOperation::EditBuffer {
1475 project_root_name,
1476 is_local,
1477 full_path,
1478 edits,
1479 };
1480 }
1481 // Make an LSP request
1482 _ => {
1483 let offset = buffer.read_with(cx, |buffer, _| {
1484 buffer.clip_offset(
1485 self.rng.gen_range(0..=buffer.len()),
1486 language::Bias::Left,
1487 )
1488 });
1489 let detach = self.rng.gen();
1490 break ClientOperation::RequestLspDataInBuffer {
1491 project_root_name,
1492 full_path,
1493 offset,
1494 is_local,
1495 kind: match self.rng.gen_range(0..5_u32) {
1496 0 => LspRequestKind::Rename,
1497 1 => LspRequestKind::Highlights,
1498 2 => LspRequestKind::Definition,
1499 3 => LspRequestKind::CodeAction,
1500 4.. => LspRequestKind::Completion,
1501 },
1502 detach,
1503 };
1504 }
1505 }
1506 }
1507
1508 71..=80 => {
1509 let query = self.rng.gen_range('a'..='z').to_string();
1510 let detach = self.rng.gen_bool(0.3);
1511 break ClientOperation::SearchProject {
1512 project_root_name,
1513 is_local,
1514 query,
1515 detach,
1516 };
1517 }
1518
1519 // Open a buffer
1520 81.. => {
1521 let worktree = project.read_with(cx, |project, cx| {
1522 project
1523 .worktrees(cx)
1524 .filter(|worktree| {
1525 let worktree = worktree.read(cx);
1526 worktree.is_visible()
1527 && worktree.entries(false).any(|e| e.is_file())
1528 })
1529 .choose(&mut self.rng)
1530 });
1531 let Some(worktree) = worktree else { continue };
1532 let full_path = worktree.read_with(cx, |worktree, _| {
1533 let entry = worktree
1534 .entries(false)
1535 .filter(|e| e.is_file())
1536 .choose(&mut self.rng)
1537 .unwrap();
1538 if entry.path.as_ref() == Path::new("") {
1539 Path::new(worktree.root_name()).into()
1540 } else {
1541 Path::new(worktree.root_name()).join(&entry.path)
1542 }
1543 });
1544 break ClientOperation::OpenBuffer {
1545 project_root_name,
1546 is_local,
1547 full_path,
1548 };
1549 }
1550 }
1551 }
1552
1553 // Update a git index
1554 91..=95 => {
1555 let repo_path = executor
1556 .block(client.fs.directories())
1557 .choose(&mut self.rng)
1558 .unwrap()
1559 .clone();
1560
1561 let mut file_paths = executor
1562 .block(client.fs.files())
1563 .into_iter()
1564 .filter(|path| path.starts_with(&repo_path))
1565 .collect::<Vec<_>>();
1566 let count = self.rng.gen_range(0..=file_paths.len());
1567 file_paths.shuffle(&mut self.rng);
1568 file_paths.truncate(count);
1569
1570 let mut contents = Vec::new();
1571 for abs_child_file_path in &file_paths {
1572 let child_file_path = abs_child_file_path
1573 .strip_prefix(&repo_path)
1574 .unwrap()
1575 .to_path_buf();
1576 let new_base = Alphanumeric.sample_string(&mut self.rng, 16);
1577 contents.push((child_file_path, new_base));
1578 }
1579
1580 break ClientOperation::WriteGitIndex {
1581 repo_path,
1582 contents,
1583 };
1584 }
1585
1586 // Create a file or directory
1587 96.. => {
1588 let is_dir = self.rng.gen::<bool>();
1589 let mut path = cx
1590 .background()
1591 .block(client.fs.directories())
1592 .choose(&mut self.rng)
1593 .unwrap()
1594 .clone();
1595 path.push(gen_file_name(&mut self.rng));
1596 if !is_dir {
1597 path.set_extension("rs");
1598 }
1599 break ClientOperation::CreateFsEntry { path, is_dir };
1600 }
1601 }
1602 })
1603 }
1604
1605 fn next_root_dir_name(&mut self, user_id: UserId) -> String {
1606 let user_ix = self
1607 .users
1608 .iter()
1609 .position(|user| user.user_id == user_id)
1610 .unwrap();
1611 let root_id = util::post_inc(&mut self.users[user_ix].next_root_id);
1612 format!("dir-{user_id}-{root_id}")
1613 }
1614
1615 fn user(&mut self, user_id: UserId) -> &mut UserTestPlan {
1616 let ix = self
1617 .users
1618 .iter()
1619 .position(|user| user.user_id == user_id)
1620 .unwrap();
1621 &mut self.users[ix]
1622 }
1623}
1624
1625async fn simulate_client(
1626 client: Rc<TestClient>,
1627 mut operation_rx: futures::channel::mpsc::UnboundedReceiver<()>,
1628 plan: Arc<Mutex<TestPlan>>,
1629 mut cx: TestAppContext,
1630) {
1631 // Setup language server
1632 let mut language = Language::new(
1633 LanguageConfig {
1634 name: "Rust".into(),
1635 path_suffixes: vec!["rs".to_string()],
1636 ..Default::default()
1637 },
1638 None,
1639 );
1640 let _fake_language_servers = language
1641 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1642 name: "the-fake-language-server",
1643 capabilities: lsp::LanguageServer::full_capabilities(),
1644 initializer: Some(Box::new({
1645 let plan = plan.clone();
1646 let fs = client.fs.clone();
1647 move |fake_server: &mut FakeLanguageServer| {
1648 fake_server.handle_request::<lsp::request::Completion, _, _>(
1649 |_, _| async move {
1650 Ok(Some(lsp::CompletionResponse::Array(vec![
1651 lsp::CompletionItem {
1652 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1653 range: lsp::Range::new(
1654 lsp::Position::new(0, 0),
1655 lsp::Position::new(0, 0),
1656 ),
1657 new_text: "the-new-text".to_string(),
1658 })),
1659 ..Default::default()
1660 },
1661 ])))
1662 },
1663 );
1664
1665 fake_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
1666 |_, _| async move {
1667 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
1668 lsp::CodeAction {
1669 title: "the-code-action".to_string(),
1670 ..Default::default()
1671 },
1672 )]))
1673 },
1674 );
1675
1676 fake_server.handle_request::<lsp::request::PrepareRenameRequest, _, _>(
1677 |params, _| async move {
1678 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
1679 params.position,
1680 params.position,
1681 ))))
1682 },
1683 );
1684
1685 fake_server.handle_request::<lsp::request::GotoDefinition, _, _>({
1686 let fs = fs.clone();
1687 let plan = plan.clone();
1688 move |_, _| {
1689 let fs = fs.clone();
1690 let plan = plan.clone();
1691 async move {
1692 let files = fs.files().await;
1693 let count = plan.lock().rng.gen_range::<usize, _>(1..3);
1694 let files = (0..count)
1695 .map(|_| files.choose(&mut plan.lock().rng).unwrap())
1696 .collect::<Vec<_>>();
1697 log::info!("LSP: Returning definitions in files {:?}", &files);
1698 Ok(Some(lsp::GotoDefinitionResponse::Array(
1699 files
1700 .into_iter()
1701 .map(|file| lsp::Location {
1702 uri: lsp::Url::from_file_path(file).unwrap(),
1703 range: Default::default(),
1704 })
1705 .collect(),
1706 )))
1707 }
1708 }
1709 });
1710
1711 fake_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>({
1712 let plan = plan.clone();
1713 move |_, _| {
1714 let mut highlights = Vec::new();
1715 let highlight_count = plan.lock().rng.gen_range(1..=5);
1716 for _ in 0..highlight_count {
1717 let start_row = plan.lock().rng.gen_range(0..100);
1718 let start_column = plan.lock().rng.gen_range(0..100);
1719 let start = PointUtf16::new(start_row, start_column);
1720 let end_row = plan.lock().rng.gen_range(0..100);
1721 let end_column = plan.lock().rng.gen_range(0..100);
1722 let end = PointUtf16::new(end_row, end_column);
1723 let range = if start > end { end..start } else { start..end };
1724 highlights.push(lsp::DocumentHighlight {
1725 range: range_to_lsp(range.clone()),
1726 kind: Some(lsp::DocumentHighlightKind::READ),
1727 });
1728 }
1729 highlights.sort_unstable_by_key(|highlight| {
1730 (highlight.range.start, highlight.range.end)
1731 });
1732 async move { Ok(Some(highlights)) }
1733 }
1734 });
1735 }
1736 })),
1737 ..Default::default()
1738 }))
1739 .await;
1740 client.language_registry.add(Arc::new(language));
1741
1742 while operation_rx.next().await.is_some() {
1743 let Some((operation, skipped)) = plan.lock().next_client_operation(&client, &cx) else { break };
1744 match apply_client_operation(&client, operation, &mut cx).await {
1745 Err(error) => {
1746 log::error!("{} error: {}", client.username, error);
1747 }
1748 Ok(applied) => {
1749 if !applied {
1750 skipped.store(true, SeqCst);
1751 }
1752 }
1753 }
1754 cx.background().simulate_random_delay().await;
1755 }
1756 log::info!("{}: done", client.username);
1757}
1758
1759fn buffer_for_full_path(
1760 buffers: &HashSet<ModelHandle<language::Buffer>>,
1761 full_path: &PathBuf,
1762 cx: &TestAppContext,
1763) -> Option<ModelHandle<language::Buffer>> {
1764 buffers
1765 .iter()
1766 .find(|buffer| {
1767 buffer.read_with(cx, |buffer, cx| {
1768 buffer.file().unwrap().full_path(cx) == *full_path
1769 })
1770 })
1771 .cloned()
1772}
1773
1774fn project_for_root_name(
1775 client: &TestClient,
1776 root_name: &str,
1777 cx: &TestAppContext,
1778) -> Option<ModelHandle<Project>> {
1779 if let Some(ix) = project_ix_for_root_name(&*client.local_projects(), root_name, cx) {
1780 return Some(client.local_projects()[ix].clone());
1781 }
1782 if let Some(ix) = project_ix_for_root_name(&*client.remote_projects(), root_name, cx) {
1783 return Some(client.remote_projects()[ix].clone());
1784 }
1785 None
1786}
1787
1788fn project_ix_for_root_name(
1789 projects: &[ModelHandle<Project>],
1790 root_name: &str,
1791 cx: &TestAppContext,
1792) -> Option<usize> {
1793 projects.iter().position(|project| {
1794 project.read_with(cx, |project, cx| {
1795 let worktree = project.visible_worktrees(cx).next().unwrap();
1796 worktree.read(cx).root_name() == root_name
1797 })
1798 })
1799}
1800
1801fn root_name_for_project(project: &ModelHandle<Project>, cx: &TestAppContext) -> String {
1802 project.read_with(cx, |project, cx| {
1803 project
1804 .visible_worktrees(cx)
1805 .next()
1806 .unwrap()
1807 .read(cx)
1808 .root_name()
1809 .to_string()
1810 })
1811}
1812
1813fn project_path_for_full_path(
1814 project: &ModelHandle<Project>,
1815 full_path: &Path,
1816 cx: &TestAppContext,
1817) -> Option<ProjectPath> {
1818 let mut components = full_path.components();
1819 let root_name = components.next().unwrap().as_os_str().to_str().unwrap();
1820 let path = components.as_path().into();
1821 let worktree_id = project.read_with(cx, |project, cx| {
1822 project.worktrees(cx).find_map(|worktree| {
1823 let worktree = worktree.read(cx);
1824 if worktree.root_name() == root_name {
1825 Some(worktree.id())
1826 } else {
1827 None
1828 }
1829 })
1830 })?;
1831 Some(ProjectPath { worktree_id, path })
1832}
1833
1834async fn ensure_project_shared(
1835 project: &ModelHandle<Project>,
1836 client: &TestClient,
1837 cx: &mut TestAppContext,
1838) {
1839 let first_root_name = root_name_for_project(project, cx);
1840 let active_call = cx.read(ActiveCall::global);
1841 if active_call.read_with(cx, |call, _| call.room().is_some())
1842 && project.read_with(cx, |project, _| project.is_local() && !project.is_shared())
1843 {
1844 match active_call
1845 .update(cx, |call, cx| call.share_project(project.clone(), cx))
1846 .await
1847 {
1848 Ok(project_id) => {
1849 log::info!(
1850 "{}: shared project {} with id {}",
1851 client.username,
1852 first_root_name,
1853 project_id
1854 );
1855 }
1856 Err(error) => {
1857 log::error!(
1858 "{}: error sharing project {}: {:?}",
1859 client.username,
1860 first_root_name,
1861 error
1862 );
1863 }
1864 }
1865 }
1866}
1867
1868fn choose_random_project(client: &TestClient, rng: &mut StdRng) -> Option<ModelHandle<Project>> {
1869 client
1870 .local_projects()
1871 .iter()
1872 .chain(client.remote_projects().iter())
1873 .choose(rng)
1874 .cloned()
1875}
1876
1877fn gen_file_name(rng: &mut StdRng) -> String {
1878 let mut name = String::new();
1879 for _ in 0..10 {
1880 let letter = rng.gen_range('a'..='z');
1881 name.push(letter);
1882 }
1883 name
1884}
1885
1886fn path_env_var(name: &str) -> Option<PathBuf> {
1887 let value = env::var(name).ok()?;
1888 let mut path = PathBuf::from(value);
1889 if path.is_relative() {
1890 let mut abs_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1891 abs_path.pop();
1892 abs_path.pop();
1893 abs_path.push(path);
1894 path = abs_path
1895 }
1896 Some(path)
1897}