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