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