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