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