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