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 project::{search::SearchQuery, Project, ProjectPath};
18use rand::{
19 distributions::{Alphanumeric, DistString},
20 prelude::*,
21};
22use serde::{Deserialize, Serialize};
23use settings::Settings;
24use std::{
25 env,
26 ops::Range,
27 path::{Path, PathBuf},
28 rc::Rc,
29 sync::{
30 atomic::{AtomicBool, Ordering::SeqCst},
31 Arc,
32 },
33};
34use util::ResultExt;
35use pretty_assertions::assert_eq;
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.iter()
832 .map(|(path, val)| (path.as_path(), val.clone()))
833 .collect::<Vec<_>>();
834
835 if client.fs.metadata(&dot_git_dir).await?.is_none() {
836 client.fs.create_dir(&dot_git_dir).await?;
837 }
838
839 client.fs.set_status_for_repo(&dot_git_dir, statuses.as_slice()).await;
840 },
841 },
842 }
843 Ok(())
844}
845
846fn check_consistency_between_clients(clients: &[(Rc<TestClient>, TestAppContext)]) {
847 for (client, client_cx) in clients {
848 for guest_project in client.remote_projects().iter() {
849 guest_project.read_with(client_cx, |guest_project, cx| {
850 let host_project = clients.iter().find_map(|(client, cx)| {
851 let project = client
852 .local_projects()
853 .iter()
854 .find(|host_project| {
855 host_project.read_with(cx, |host_project, _| {
856 host_project.remote_id() == guest_project.remote_id()
857 })
858 })?
859 .clone();
860 Some((project, cx))
861 });
862
863 if !guest_project.is_read_only() {
864 if let Some((host_project, host_cx)) = host_project {
865 let host_worktree_snapshots =
866 host_project.read_with(host_cx, |host_project, cx| {
867 host_project
868 .worktrees(cx)
869 .map(|worktree| {
870 let worktree = worktree.read(cx);
871 (worktree.id(), worktree.snapshot())
872 })
873 .collect::<BTreeMap<_, _>>()
874 });
875 let guest_worktree_snapshots = guest_project
876 .worktrees(cx)
877 .map(|worktree| {
878 let worktree = worktree.read(cx);
879 (worktree.id(), worktree.snapshot())
880 })
881 .collect::<BTreeMap<_, _>>();
882
883 assert_eq!(
884 guest_worktree_snapshots.values().map(|w| w.abs_path()).collect::<Vec<_>>(),
885 host_worktree_snapshots.values().map(|w| w.abs_path()).collect::<Vec<_>>(),
886 "{} has different worktrees than the host for project {:?}",
887 client.username, guest_project.remote_id(),
888 );
889
890 for (id, host_snapshot) in &host_worktree_snapshots {
891 let guest_snapshot = &guest_worktree_snapshots[id];
892 assert_eq!(
893 guest_snapshot.root_name(),
894 host_snapshot.root_name(),
895 "{} has different root name than the host for worktree {}, project {:?}",
896 client.username,
897 id,
898 guest_project.remote_id(),
899 );
900 assert_eq!(
901 guest_snapshot.abs_path(),
902 host_snapshot.abs_path(),
903 "{} has different abs path than the host for worktree {}, project: {:?}",
904 client.username,
905 id,
906 guest_project.remote_id(),
907 );
908 assert_eq!(
909 guest_snapshot.entries(false).collect::<Vec<_>>(),
910 host_snapshot.entries(false).collect::<Vec<_>>(),
911 "{} has different snapshot than the host for worktree {:?} and project {:?}",
912 client.username,
913 host_snapshot.abs_path(),
914 guest_project.remote_id(),
915 );
916 assert_eq!(guest_snapshot.repositories().collect::<Vec<_>>(), host_snapshot.repositories().collect::<Vec<_>>(),
917 "{} has different repositories than the host for worktree {:?} and project {:?}",
918 client.username,
919 host_snapshot.abs_path(),
920 guest_project.remote_id(),
921 );
922 assert_eq!(guest_snapshot.scan_id(), host_snapshot.scan_id(),
923 "{} has different scan id than the host for worktree {:?} and project {:?}",
924 client.username,
925 host_snapshot.abs_path(),
926 guest_project.remote_id(),
927 );
928 }
929 }
930 }
931
932 for buffer in guest_project.opened_buffers(cx) {
933 let buffer = buffer.read(cx);
934 assert_eq!(
935 buffer.deferred_ops_len(),
936 0,
937 "{} has deferred operations for buffer {:?} in project {:?}",
938 client.username,
939 buffer.file().unwrap().full_path(cx),
940 guest_project.remote_id(),
941 );
942 }
943 });
944 }
945
946 let buffers = client.buffers().clone();
947 for (guest_project, guest_buffers) in &buffers {
948 let project_id = if guest_project.read_with(client_cx, |project, _| {
949 project.is_local() || project.is_read_only()
950 }) {
951 continue;
952 } else {
953 guest_project
954 .read_with(client_cx, |project, _| project.remote_id())
955 .unwrap()
956 };
957 let guest_user_id = client.user_id().unwrap();
958
959 let host_project = clients.iter().find_map(|(client, cx)| {
960 let project = client
961 .local_projects()
962 .iter()
963 .find(|host_project| {
964 host_project.read_with(cx, |host_project, _| {
965 host_project.remote_id() == Some(project_id)
966 })
967 })?
968 .clone();
969 Some((client.user_id().unwrap(), project, cx))
970 });
971
972 let (host_user_id, host_project, host_cx) =
973 if let Some((host_user_id, host_project, host_cx)) = host_project {
974 (host_user_id, host_project, host_cx)
975 } else {
976 continue;
977 };
978
979 for guest_buffer in guest_buffers {
980 let buffer_id = guest_buffer.read_with(client_cx, |buffer, _| buffer.remote_id());
981 let host_buffer = host_project.read_with(host_cx, |project, cx| {
982 project.buffer_for_id(buffer_id, cx).unwrap_or_else(|| {
983 panic!(
984 "host does not have buffer for guest:{}, peer:{:?}, id:{}",
985 client.username,
986 client.peer_id(),
987 buffer_id
988 )
989 })
990 });
991 let path = host_buffer
992 .read_with(host_cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
993
994 assert_eq!(
995 guest_buffer.read_with(client_cx, |buffer, _| buffer.deferred_ops_len()),
996 0,
997 "{}, buffer {}, path {:?} has deferred operations",
998 client.username,
999 buffer_id,
1000 path,
1001 );
1002 assert_eq!(
1003 guest_buffer.read_with(client_cx, |buffer, _| buffer.text()),
1004 host_buffer.read_with(host_cx, |buffer, _| buffer.text()),
1005 "{}, buffer {}, path {:?}, differs from the host's buffer",
1006 client.username,
1007 buffer_id,
1008 path
1009 );
1010
1011 let host_file = host_buffer.read_with(host_cx, |b, _| b.file().cloned());
1012 let guest_file = guest_buffer.read_with(client_cx, |b, _| b.file().cloned());
1013 match (host_file, guest_file) {
1014 (Some(host_file), Some(guest_file)) => {
1015 assert_eq!(guest_file.path(), host_file.path());
1016 assert_eq!(guest_file.is_deleted(), host_file.is_deleted());
1017 assert_eq!(
1018 guest_file.mtime(),
1019 host_file.mtime(),
1020 "guest {} mtime does not match host {} for path {:?} in project {}",
1021 guest_user_id,
1022 host_user_id,
1023 guest_file.path(),
1024 project_id,
1025 );
1026 }
1027 (None, None) => {}
1028 (None, _) => panic!("host's file is None, guest's isn't"),
1029 (_, None) => panic!("guest's file is None, hosts's isn't"),
1030 }
1031
1032 let host_diff_base =
1033 host_buffer.read_with(host_cx, |b, _| b.diff_base().map(ToString::to_string));
1034 let guest_diff_base = guest_buffer
1035 .read_with(client_cx, |b, _| b.diff_base().map(ToString::to_string));
1036 assert_eq!(
1037 guest_diff_base, host_diff_base,
1038 "guest {} diff base does not match host's for path {path:?} in project {project_id}",
1039 client.username
1040 );
1041
1042 let host_saved_version =
1043 host_buffer.read_with(host_cx, |b, _| b.saved_version().clone());
1044 let guest_saved_version =
1045 guest_buffer.read_with(client_cx, |b, _| b.saved_version().clone());
1046 assert_eq!(
1047 guest_saved_version, host_saved_version,
1048 "guest {} saved version does not match host's for path {path:?} in project {project_id}",
1049 client.username
1050 );
1051
1052 let host_saved_version_fingerprint =
1053 host_buffer.read_with(host_cx, |b, _| b.saved_version_fingerprint());
1054 let guest_saved_version_fingerprint =
1055 guest_buffer.read_with(client_cx, |b, _| b.saved_version_fingerprint());
1056 assert_eq!(
1057 guest_saved_version_fingerprint, host_saved_version_fingerprint,
1058 "guest {} saved fingerprint does not match host's for path {path:?} in project {project_id}",
1059 client.username
1060 );
1061
1062 let host_saved_mtime = host_buffer.read_with(host_cx, |b, _| b.saved_mtime());
1063 let guest_saved_mtime = guest_buffer.read_with(client_cx, |b, _| b.saved_mtime());
1064 assert_eq!(
1065 guest_saved_mtime, host_saved_mtime,
1066 "guest {} saved mtime does not match host's for path {path:?} in project {project_id}",
1067 client.username
1068 );
1069
1070 let host_is_dirty = host_buffer.read_with(host_cx, |b, _| b.is_dirty());
1071 let guest_is_dirty = guest_buffer.read_with(client_cx, |b, _| b.is_dirty());
1072 assert_eq!(guest_is_dirty, host_is_dirty,
1073 "guest {} dirty status does not match host's for path {path:?} in project {project_id}",
1074 client.username
1075 );
1076
1077 let host_has_conflict = host_buffer.read_with(host_cx, |b, _| b.has_conflict());
1078 let guest_has_conflict = guest_buffer.read_with(client_cx, |b, _| b.has_conflict());
1079 assert_eq!(guest_has_conflict, host_has_conflict,
1080 "guest {} conflict status does not match host's for path {path:?} in project {project_id}",
1081 client.username
1082 );
1083 }
1084 }
1085 }
1086}
1087
1088struct TestPlan {
1089 rng: StdRng,
1090 replay: bool,
1091 stored_operations: Vec<(StoredOperation, Arc<AtomicBool>)>,
1092 max_operations: usize,
1093 operation_ix: usize,
1094 users: Vec<UserTestPlan>,
1095 next_batch_id: usize,
1096 allow_server_restarts: bool,
1097 allow_client_reconnection: bool,
1098 allow_client_disconnection: bool,
1099}
1100
1101struct UserTestPlan {
1102 user_id: UserId,
1103 username: String,
1104 next_root_id: usize,
1105 operation_ix: usize,
1106 online: bool,
1107}
1108
1109#[derive(Clone, Debug, Serialize, Deserialize)]
1110#[serde(untagged)]
1111enum StoredOperation {
1112 Server(Operation),
1113 Client {
1114 user_id: UserId,
1115 batch_id: usize,
1116 operation: ClientOperation,
1117 },
1118}
1119
1120#[derive(Clone, Debug, Serialize, Deserialize)]
1121enum Operation {
1122 AddConnection {
1123 user_id: UserId,
1124 },
1125 RemoveConnection {
1126 user_id: UserId,
1127 },
1128 BounceConnection {
1129 user_id: UserId,
1130 },
1131 RestartServer,
1132 MutateClients {
1133 batch_id: usize,
1134 #[serde(skip_serializing)]
1135 #[serde(skip_deserializing)]
1136 user_ids: Vec<UserId>,
1137 quiesce: bool,
1138 },
1139}
1140
1141#[derive(Clone, Debug, Serialize, Deserialize)]
1142enum ClientOperation {
1143 AcceptIncomingCall,
1144 RejectIncomingCall,
1145 LeaveCall,
1146 InviteContactToCall {
1147 user_id: UserId,
1148 },
1149 OpenLocalProject {
1150 first_root_name: String,
1151 },
1152 OpenRemoteProject {
1153 host_id: UserId,
1154 first_root_name: String,
1155 },
1156 AddWorktreeToProject {
1157 project_root_name: String,
1158 new_root_path: PathBuf,
1159 },
1160 CloseRemoteProject {
1161 project_root_name: String,
1162 },
1163 OpenBuffer {
1164 project_root_name: String,
1165 is_local: bool,
1166 full_path: PathBuf,
1167 },
1168 SearchProject {
1169 project_root_name: String,
1170 is_local: bool,
1171 query: String,
1172 detach: bool,
1173 },
1174 EditBuffer {
1175 project_root_name: String,
1176 is_local: bool,
1177 full_path: PathBuf,
1178 edits: Vec<(Range<usize>, Arc<str>)>,
1179 },
1180 CloseBuffer {
1181 project_root_name: String,
1182 is_local: bool,
1183 full_path: PathBuf,
1184 },
1185 SaveBuffer {
1186 project_root_name: String,
1187 is_local: bool,
1188 full_path: PathBuf,
1189 detach: bool,
1190 },
1191 RequestLspDataInBuffer {
1192 project_root_name: String,
1193 is_local: bool,
1194 full_path: PathBuf,
1195 offset: usize,
1196 kind: LspRequestKind,
1197 detach: bool,
1198 },
1199 CreateWorktreeEntry {
1200 project_root_name: String,
1201 is_local: bool,
1202 full_path: PathBuf,
1203 is_dir: bool,
1204 },
1205 WriteFsEntry {
1206 path: PathBuf,
1207 is_dir: bool,
1208 content: String,
1209 },
1210 GitOperation {
1211 operation: GitOperation,
1212 },
1213}
1214
1215#[derive(Clone, Debug, Serialize, Deserialize)]
1216enum GitOperation {
1217 WriteGitIndex {
1218 repo_path: PathBuf,
1219 contents: Vec<(PathBuf, String)>,
1220 },
1221 WriteGitBranch {
1222 repo_path: PathBuf,
1223 new_branch: Option<String>,
1224 },
1225 WriteGitStatuses {
1226 repo_path: PathBuf,
1227 statuses: Vec<(PathBuf, GitFileStatus)>,
1228 },
1229}
1230
1231#[derive(Clone, Debug, Serialize, Deserialize)]
1232enum LspRequestKind {
1233 Rename,
1234 Completion,
1235 CodeAction,
1236 Definition,
1237 Highlights,
1238}
1239
1240enum TestError {
1241 Inapplicable,
1242 Other(anyhow::Error),
1243}
1244
1245impl From<anyhow::Error> for TestError {
1246 fn from(value: anyhow::Error) -> Self {
1247 Self::Other(value)
1248 }
1249}
1250
1251impl TestPlan {
1252 fn new(mut rng: StdRng, users: Vec<UserTestPlan>, max_operations: usize) -> Self {
1253 Self {
1254 replay: false,
1255 allow_server_restarts: rng.gen_bool(0.7),
1256 allow_client_reconnection: rng.gen_bool(0.7),
1257 allow_client_disconnection: rng.gen_bool(0.1),
1258 stored_operations: Vec::new(),
1259 operation_ix: 0,
1260 next_batch_id: 0,
1261 max_operations,
1262 users,
1263 rng,
1264 }
1265 }
1266
1267 fn deserialize(&mut self, json: Vec<u8>) {
1268 let stored_operations: Vec<StoredOperation> = serde_json::from_slice(&json).unwrap();
1269 self.replay = true;
1270 self.stored_operations = stored_operations
1271 .iter()
1272 .cloned()
1273 .enumerate()
1274 .map(|(i, mut operation)| {
1275 if let StoredOperation::Server(Operation::MutateClients {
1276 batch_id: current_batch_id,
1277 user_ids,
1278 ..
1279 }) = &mut operation
1280 {
1281 assert!(user_ids.is_empty());
1282 user_ids.extend(stored_operations[i + 1..].iter().filter_map(|operation| {
1283 if let StoredOperation::Client {
1284 user_id, batch_id, ..
1285 } = operation
1286 {
1287 if batch_id == current_batch_id {
1288 return Some(user_id);
1289 }
1290 }
1291 None
1292 }));
1293 user_ids.sort_unstable();
1294 }
1295 (operation, Arc::new(AtomicBool::new(false)))
1296 })
1297 .collect()
1298 }
1299
1300 fn serialize(&mut self) -> Vec<u8> {
1301 // Format each operation as one line
1302 let mut json = Vec::new();
1303 json.push(b'[');
1304 for (operation, applied) in &self.stored_operations {
1305 if !applied.load(SeqCst) {
1306 continue;
1307 }
1308 if json.len() > 1 {
1309 json.push(b',');
1310 }
1311 json.extend_from_slice(b"\n ");
1312 serde_json::to_writer(&mut json, operation).unwrap();
1313 }
1314 json.extend_from_slice(b"\n]\n");
1315 json
1316 }
1317
1318 fn next_server_operation(
1319 &mut self,
1320 clients: &[(Rc<TestClient>, TestAppContext)],
1321 ) -> Option<(Operation, Arc<AtomicBool>)> {
1322 if self.replay {
1323 while let Some(stored_operation) = self.stored_operations.get(self.operation_ix) {
1324 self.operation_ix += 1;
1325 if let (StoredOperation::Server(operation), applied) = stored_operation {
1326 return Some((operation.clone(), applied.clone()));
1327 }
1328 }
1329 None
1330 } else {
1331 let operation = self.generate_server_operation(clients)?;
1332 let applied = Arc::new(AtomicBool::new(false));
1333 self.stored_operations
1334 .push((StoredOperation::Server(operation.clone()), applied.clone()));
1335 Some((operation, applied))
1336 }
1337 }
1338
1339 fn next_client_operation(
1340 &mut self,
1341 client: &TestClient,
1342 current_batch_id: usize,
1343 cx: &TestAppContext,
1344 ) -> Option<(ClientOperation, Arc<AtomicBool>)> {
1345 let current_user_id = client.current_user_id(cx);
1346 let user_ix = self
1347 .users
1348 .iter()
1349 .position(|user| user.user_id == current_user_id)
1350 .unwrap();
1351 let user_plan = &mut self.users[user_ix];
1352
1353 if self.replay {
1354 while let Some(stored_operation) = self.stored_operations.get(user_plan.operation_ix) {
1355 user_plan.operation_ix += 1;
1356 if let (
1357 StoredOperation::Client {
1358 user_id, operation, ..
1359 },
1360 applied,
1361 ) = stored_operation
1362 {
1363 if user_id == ¤t_user_id {
1364 return Some((operation.clone(), applied.clone()));
1365 }
1366 }
1367 }
1368 None
1369 } else {
1370 let operation = self.generate_client_operation(current_user_id, client, cx)?;
1371 let applied = Arc::new(AtomicBool::new(false));
1372 self.stored_operations.push((
1373 StoredOperation::Client {
1374 user_id: current_user_id,
1375 batch_id: current_batch_id,
1376 operation: operation.clone(),
1377 },
1378 applied.clone(),
1379 ));
1380 Some((operation, applied))
1381 }
1382 }
1383
1384 fn generate_server_operation(
1385 &mut self,
1386 clients: &[(Rc<TestClient>, TestAppContext)],
1387 ) -> Option<Operation> {
1388 if self.operation_ix == self.max_operations {
1389 return None;
1390 }
1391
1392 Some(loop {
1393 break match self.rng.gen_range(0..100) {
1394 0..=29 if clients.len() < self.users.len() => {
1395 let user = self
1396 .users
1397 .iter()
1398 .filter(|u| !u.online)
1399 .choose(&mut self.rng)
1400 .unwrap();
1401 self.operation_ix += 1;
1402 Operation::AddConnection {
1403 user_id: user.user_id,
1404 }
1405 }
1406 30..=34 if clients.len() > 1 && self.allow_client_disconnection => {
1407 let (client, cx) = &clients[self.rng.gen_range(0..clients.len())];
1408 let user_id = client.current_user_id(cx);
1409 self.operation_ix += 1;
1410 Operation::RemoveConnection { user_id }
1411 }
1412 35..=39 if clients.len() > 1 && self.allow_client_reconnection => {
1413 let (client, cx) = &clients[self.rng.gen_range(0..clients.len())];
1414 let user_id = client.current_user_id(cx);
1415 self.operation_ix += 1;
1416 Operation::BounceConnection { user_id }
1417 }
1418 40..=44 if self.allow_server_restarts && clients.len() > 1 => {
1419 self.operation_ix += 1;
1420 Operation::RestartServer
1421 }
1422 _ if !clients.is_empty() => {
1423 let count = self
1424 .rng
1425 .gen_range(1..10)
1426 .min(self.max_operations - self.operation_ix);
1427 let batch_id = util::post_inc(&mut self.next_batch_id);
1428 let mut user_ids = (0..count)
1429 .map(|_| {
1430 let ix = self.rng.gen_range(0..clients.len());
1431 let (client, cx) = &clients[ix];
1432 client.current_user_id(cx)
1433 })
1434 .collect::<Vec<_>>();
1435 user_ids.sort_unstable();
1436 Operation::MutateClients {
1437 user_ids,
1438 batch_id,
1439 quiesce: self.rng.gen_bool(0.7),
1440 }
1441 }
1442 _ => continue,
1443 };
1444 })
1445 }
1446
1447 fn generate_client_operation(
1448 &mut self,
1449 user_id: UserId,
1450 client: &TestClient,
1451 cx: &TestAppContext,
1452 ) -> Option<ClientOperation> {
1453 if self.operation_ix == self.max_operations {
1454 return None;
1455 }
1456
1457 self.operation_ix += 1;
1458 let call = cx.read(ActiveCall::global);
1459 Some(loop {
1460 match self.rng.gen_range(0..100_u32) {
1461 // Mutate the call
1462 0..=29 => {
1463 // Respond to an incoming call
1464 if call.read_with(cx, |call, _| call.incoming().borrow().is_some()) {
1465 break if self.rng.gen_bool(0.7) {
1466 ClientOperation::AcceptIncomingCall
1467 } else {
1468 ClientOperation::RejectIncomingCall
1469 };
1470 }
1471
1472 match self.rng.gen_range(0..100_u32) {
1473 // Invite a contact to the current call
1474 0..=70 => {
1475 let available_contacts =
1476 client.user_store.read_with(cx, |user_store, _| {
1477 user_store
1478 .contacts()
1479 .iter()
1480 .filter(|contact| contact.online && !contact.busy)
1481 .cloned()
1482 .collect::<Vec<_>>()
1483 });
1484 if !available_contacts.is_empty() {
1485 let contact = available_contacts.choose(&mut self.rng).unwrap();
1486 break ClientOperation::InviteContactToCall {
1487 user_id: UserId(contact.user.id as i32),
1488 };
1489 }
1490 }
1491
1492 // Leave the current call
1493 71.. => {
1494 if self.allow_client_disconnection
1495 && call.read_with(cx, |call, _| call.room().is_some())
1496 {
1497 break ClientOperation::LeaveCall;
1498 }
1499 }
1500 }
1501 }
1502
1503 // Mutate projects
1504 30..=59 => match self.rng.gen_range(0..100_u32) {
1505 // Open a new project
1506 0..=70 => {
1507 // Open a remote project
1508 if let Some(room) = call.read_with(cx, |call, _| call.room().cloned()) {
1509 let existing_remote_project_ids = cx.read(|cx| {
1510 client
1511 .remote_projects()
1512 .iter()
1513 .map(|p| p.read(cx).remote_id().unwrap())
1514 .collect::<Vec<_>>()
1515 });
1516 let new_remote_projects = room.read_with(cx, |room, _| {
1517 room.remote_participants()
1518 .values()
1519 .flat_map(|participant| {
1520 participant.projects.iter().filter_map(|project| {
1521 if existing_remote_project_ids.contains(&project.id) {
1522 None
1523 } else {
1524 Some((
1525 UserId::from_proto(participant.user.id),
1526 project.worktree_root_names[0].clone(),
1527 ))
1528 }
1529 })
1530 })
1531 .collect::<Vec<_>>()
1532 });
1533 if !new_remote_projects.is_empty() {
1534 let (host_id, first_root_name) =
1535 new_remote_projects.choose(&mut self.rng).unwrap().clone();
1536 break ClientOperation::OpenRemoteProject {
1537 host_id,
1538 first_root_name,
1539 };
1540 }
1541 }
1542 // Open a local project
1543 else {
1544 let first_root_name = self.next_root_dir_name(user_id);
1545 break ClientOperation::OpenLocalProject { first_root_name };
1546 }
1547 }
1548
1549 // Close a remote project
1550 71..=80 => {
1551 if !client.remote_projects().is_empty() {
1552 let project = client
1553 .remote_projects()
1554 .choose(&mut self.rng)
1555 .unwrap()
1556 .clone();
1557 let first_root_name = root_name_for_project(&project, cx);
1558 break ClientOperation::CloseRemoteProject {
1559 project_root_name: first_root_name,
1560 };
1561 }
1562 }
1563
1564 // Mutate project worktrees
1565 81.. => match self.rng.gen_range(0..100_u32) {
1566 // Add a worktree to a local project
1567 0..=50 => {
1568 let Some(project) = client
1569 .local_projects()
1570 .choose(&mut self.rng)
1571 .cloned() else { continue };
1572 let project_root_name = root_name_for_project(&project, cx);
1573 let mut paths = client.fs.paths();
1574 paths.remove(0);
1575 let new_root_path = if paths.is_empty() || self.rng.gen() {
1576 Path::new("/").join(&self.next_root_dir_name(user_id))
1577 } else {
1578 paths.choose(&mut self.rng).unwrap().clone()
1579 };
1580 break ClientOperation::AddWorktreeToProject {
1581 project_root_name,
1582 new_root_path,
1583 };
1584 }
1585
1586 // Add an entry to a worktree
1587 _ => {
1588 let Some(project) = choose_random_project(client, &mut self.rng) else { continue };
1589 let project_root_name = root_name_for_project(&project, cx);
1590 let is_local = project.read_with(cx, |project, _| project.is_local());
1591 let worktree = project.read_with(cx, |project, cx| {
1592 project
1593 .worktrees(cx)
1594 .filter(|worktree| {
1595 let worktree = worktree.read(cx);
1596 worktree.is_visible()
1597 && worktree.entries(false).any(|e| e.is_file())
1598 && worktree.root_entry().map_or(false, |e| e.is_dir())
1599 })
1600 .choose(&mut self.rng)
1601 });
1602 let Some(worktree) = worktree else { continue };
1603 let is_dir = self.rng.gen::<bool>();
1604 let mut full_path =
1605 worktree.read_with(cx, |w, _| PathBuf::from(w.root_name()));
1606 full_path.push(gen_file_name(&mut self.rng));
1607 if !is_dir {
1608 full_path.set_extension("rs");
1609 }
1610 break ClientOperation::CreateWorktreeEntry {
1611 project_root_name,
1612 is_local,
1613 full_path,
1614 is_dir,
1615 };
1616 }
1617 },
1618 },
1619
1620 // Query and mutate buffers
1621 60..=90 => {
1622 let Some(project) = choose_random_project(client, &mut self.rng) else { continue };
1623 let project_root_name = root_name_for_project(&project, cx);
1624 let is_local = project.read_with(cx, |project, _| project.is_local());
1625
1626 match self.rng.gen_range(0..100_u32) {
1627 // Manipulate an existing buffer
1628 0..=70 => {
1629 let Some(buffer) = client
1630 .buffers_for_project(&project)
1631 .iter()
1632 .choose(&mut self.rng)
1633 .cloned() else { continue };
1634
1635 let full_path = buffer
1636 .read_with(cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
1637
1638 match self.rng.gen_range(0..100_u32) {
1639 // Close the buffer
1640 0..=15 => {
1641 break ClientOperation::CloseBuffer {
1642 project_root_name,
1643 is_local,
1644 full_path,
1645 };
1646 }
1647 // Save the buffer
1648 16..=29 if buffer.read_with(cx, |b, _| b.is_dirty()) => {
1649 let detach = self.rng.gen_bool(0.3);
1650 break ClientOperation::SaveBuffer {
1651 project_root_name,
1652 is_local,
1653 full_path,
1654 detach,
1655 };
1656 }
1657 // Edit the buffer
1658 30..=69 => {
1659 let edits = buffer.read_with(cx, |buffer, _| {
1660 buffer.get_random_edits(&mut self.rng, 3)
1661 });
1662 break ClientOperation::EditBuffer {
1663 project_root_name,
1664 is_local,
1665 full_path,
1666 edits,
1667 };
1668 }
1669 // Make an LSP request
1670 _ => {
1671 let offset = buffer.read_with(cx, |buffer, _| {
1672 buffer.clip_offset(
1673 self.rng.gen_range(0..=buffer.len()),
1674 language::Bias::Left,
1675 )
1676 });
1677 let detach = self.rng.gen();
1678 break ClientOperation::RequestLspDataInBuffer {
1679 project_root_name,
1680 full_path,
1681 offset,
1682 is_local,
1683 kind: match self.rng.gen_range(0..5_u32) {
1684 0 => LspRequestKind::Rename,
1685 1 => LspRequestKind::Highlights,
1686 2 => LspRequestKind::Definition,
1687 3 => LspRequestKind::CodeAction,
1688 4.. => LspRequestKind::Completion,
1689 },
1690 detach,
1691 };
1692 }
1693 }
1694 }
1695
1696 71..=80 => {
1697 let query = self.rng.gen_range('a'..='z').to_string();
1698 let detach = self.rng.gen_bool(0.3);
1699 break ClientOperation::SearchProject {
1700 project_root_name,
1701 is_local,
1702 query,
1703 detach,
1704 };
1705 }
1706
1707 // Open a buffer
1708 81.. => {
1709 let worktree = project.read_with(cx, |project, cx| {
1710 project
1711 .worktrees(cx)
1712 .filter(|worktree| {
1713 let worktree = worktree.read(cx);
1714 worktree.is_visible()
1715 && worktree.entries(false).any(|e| e.is_file())
1716 })
1717 .choose(&mut self.rng)
1718 });
1719 let Some(worktree) = worktree else { continue };
1720 let full_path = worktree.read_with(cx, |worktree, _| {
1721 let entry = worktree
1722 .entries(false)
1723 .filter(|e| e.is_file())
1724 .choose(&mut self.rng)
1725 .unwrap();
1726 if entry.path.as_ref() == Path::new("") {
1727 Path::new(worktree.root_name()).into()
1728 } else {
1729 Path::new(worktree.root_name()).join(&entry.path)
1730 }
1731 });
1732 break ClientOperation::OpenBuffer {
1733 project_root_name,
1734 is_local,
1735 full_path,
1736 };
1737 }
1738 }
1739 }
1740
1741 // Update a git related action
1742 91..=95 => {
1743 break ClientOperation::GitOperation {
1744 operation: self.generate_git_operation(client),
1745 };
1746 }
1747
1748 // Create or update a file or directory
1749 96.. => {
1750 let is_dir = self.rng.gen::<bool>();
1751 let content;
1752 let mut path;
1753 let dir_paths = client.fs.directories();
1754
1755 if is_dir {
1756 content = String::new();
1757 path = dir_paths.choose(&mut self.rng).unwrap().clone();
1758 path.push(gen_file_name(&mut self.rng));
1759 } else {
1760 content = Alphanumeric.sample_string(&mut self.rng, 16);
1761
1762 // Create a new file or overwrite an existing file
1763 let file_paths = client.fs.files();
1764 if file_paths.is_empty() || self.rng.gen_bool(0.5) {
1765 path = dir_paths.choose(&mut self.rng).unwrap().clone();
1766 path.push(gen_file_name(&mut self.rng));
1767 path.set_extension("rs");
1768 } else {
1769 path = file_paths.choose(&mut self.rng).unwrap().clone()
1770 };
1771 }
1772 break ClientOperation::WriteFsEntry {
1773 path,
1774 is_dir,
1775 content,
1776 };
1777 }
1778 }
1779 })
1780 }
1781
1782 fn generate_git_operation(&mut self, client: &TestClient) -> GitOperation {
1783 fn generate_file_paths(
1784 repo_path: &Path,
1785 rng: &mut StdRng,
1786 client: &TestClient,
1787 ) -> Vec<PathBuf> {
1788 let mut paths = client
1789 .fs
1790 .files()
1791 .into_iter()
1792 .filter(|path| path.starts_with(repo_path))
1793 .collect::<Vec<_>>();
1794
1795 let count = rng.gen_range(0..=paths.len());
1796 paths.shuffle(rng);
1797 paths.truncate(count);
1798
1799 paths
1800 .iter()
1801 .map(|path| path.strip_prefix(repo_path).unwrap().to_path_buf())
1802 .collect::<Vec<_>>()
1803 }
1804
1805 let repo_path = client
1806 .fs
1807 .directories()
1808 .choose(&mut self.rng)
1809 .unwrap()
1810 .clone();
1811
1812 match self.rng.gen_range(0..100_u32) {
1813 0..=25 => {
1814 let file_paths = generate_file_paths(&repo_path, &mut self.rng, client);
1815
1816 let contents = file_paths
1817 .into_iter()
1818 .map(|path| (path, Alphanumeric.sample_string(&mut self.rng, 16)))
1819 .collect();
1820
1821 GitOperation::WriteGitIndex {
1822 repo_path,
1823 contents,
1824 }
1825 }
1826 26..=63 => {
1827 let new_branch = (self.rng.gen_range(0..10) > 3)
1828 .then(|| Alphanumeric.sample_string(&mut self.rng, 8));
1829
1830 GitOperation::WriteGitBranch {
1831 repo_path,
1832 new_branch,
1833 }
1834 }
1835 64..=100 => {
1836 let file_paths = generate_file_paths(&repo_path, &mut self.rng, client);
1837
1838 let statuses = file_paths
1839 .into_iter()
1840 .map(|paths| {
1841 (
1842 paths,
1843 match self.rng.gen_range(0..3_u32) {
1844 0 => GitFileStatus::Added,
1845 1 => GitFileStatus::Modified,
1846 2 => GitFileStatus::Conflict,
1847 _ => unreachable!(),
1848 },
1849 )
1850 })
1851 .collect::<Vec<_>>();
1852
1853 GitOperation::WriteGitStatuses {
1854 repo_path,
1855 statuses,
1856 }
1857 }
1858 _ => unreachable!(),
1859 }
1860 }
1861
1862 fn next_root_dir_name(&mut self, user_id: UserId) -> String {
1863 let user_ix = self
1864 .users
1865 .iter()
1866 .position(|user| user.user_id == user_id)
1867 .unwrap();
1868 let root_id = util::post_inc(&mut self.users[user_ix].next_root_id);
1869 format!("dir-{user_id}-{root_id}")
1870 }
1871
1872 fn user(&mut self, user_id: UserId) -> &mut UserTestPlan {
1873 let ix = self
1874 .users
1875 .iter()
1876 .position(|user| user.user_id == user_id)
1877 .unwrap();
1878 &mut self.users[ix]
1879 }
1880}
1881
1882async fn simulate_client(
1883 client: Rc<TestClient>,
1884 mut operation_rx: futures::channel::mpsc::UnboundedReceiver<usize>,
1885 plan: Arc<Mutex<TestPlan>>,
1886 mut cx: TestAppContext,
1887) {
1888 // Setup language server
1889 let mut language = Language::new(
1890 LanguageConfig {
1891 name: "Rust".into(),
1892 path_suffixes: vec!["rs".to_string()],
1893 ..Default::default()
1894 },
1895 None,
1896 );
1897 let _fake_language_servers = language
1898 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1899 name: "the-fake-language-server",
1900 capabilities: lsp::LanguageServer::full_capabilities(),
1901 initializer: Some(Box::new({
1902 let fs = client.fs.clone();
1903 move |fake_server: &mut FakeLanguageServer| {
1904 fake_server.handle_request::<lsp::request::Completion, _, _>(
1905 |_, _| async move {
1906 Ok(Some(lsp::CompletionResponse::Array(vec![
1907 lsp::CompletionItem {
1908 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1909 range: lsp::Range::new(
1910 lsp::Position::new(0, 0),
1911 lsp::Position::new(0, 0),
1912 ),
1913 new_text: "the-new-text".to_string(),
1914 })),
1915 ..Default::default()
1916 },
1917 ])))
1918 },
1919 );
1920
1921 fake_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
1922 |_, _| async move {
1923 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
1924 lsp::CodeAction {
1925 title: "the-code-action".to_string(),
1926 ..Default::default()
1927 },
1928 )]))
1929 },
1930 );
1931
1932 fake_server.handle_request::<lsp::request::PrepareRenameRequest, _, _>(
1933 |params, _| async move {
1934 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
1935 params.position,
1936 params.position,
1937 ))))
1938 },
1939 );
1940
1941 fake_server.handle_request::<lsp::request::GotoDefinition, _, _>({
1942 let fs = fs.clone();
1943 move |_, cx| {
1944 let background = cx.background();
1945 let mut rng = background.rng();
1946 let count = rng.gen_range::<usize, _>(1..3);
1947 let files = fs.files();
1948 let files = (0..count)
1949 .map(|_| files.choose(&mut *rng).unwrap().clone())
1950 .collect::<Vec<_>>();
1951 async move {
1952 log::info!("LSP: Returning definitions in files {:?}", &files);
1953 Ok(Some(lsp::GotoDefinitionResponse::Array(
1954 files
1955 .into_iter()
1956 .map(|file| lsp::Location {
1957 uri: lsp::Url::from_file_path(file).unwrap(),
1958 range: Default::default(),
1959 })
1960 .collect(),
1961 )))
1962 }
1963 }
1964 });
1965
1966 fake_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>(
1967 move |_, cx| {
1968 let mut highlights = Vec::new();
1969 let background = cx.background();
1970 let mut rng = background.rng();
1971
1972 let highlight_count = rng.gen_range(1..=5);
1973 for _ in 0..highlight_count {
1974 let start_row = rng.gen_range(0..100);
1975 let start_column = rng.gen_range(0..100);
1976 let end_row = rng.gen_range(0..100);
1977 let end_column = rng.gen_range(0..100);
1978 let start = PointUtf16::new(start_row, start_column);
1979 let end = PointUtf16::new(end_row, end_column);
1980 let range = if start > end { end..start } else { start..end };
1981 highlights.push(lsp::DocumentHighlight {
1982 range: range_to_lsp(range.clone()),
1983 kind: Some(lsp::DocumentHighlightKind::READ),
1984 });
1985 }
1986 highlights.sort_unstable_by_key(|highlight| {
1987 (highlight.range.start, highlight.range.end)
1988 });
1989 async move { Ok(Some(highlights)) }
1990 },
1991 );
1992 }
1993 })),
1994 ..Default::default()
1995 }))
1996 .await;
1997 client.language_registry.add(Arc::new(language));
1998
1999 while let Some(batch_id) = operation_rx.next().await {
2000 let Some((operation, applied)) = plan.lock().next_client_operation(&client, batch_id, &cx) else { break };
2001 applied.store(true, SeqCst);
2002 match apply_client_operation(&client, operation, &mut cx).await {
2003 Ok(()) => {}
2004 Err(TestError::Inapplicable) => {
2005 applied.store(false, SeqCst);
2006 log::info!("skipped operation");
2007 }
2008 Err(TestError::Other(error)) => {
2009 log::error!("{} error: {}", client.username, error);
2010 }
2011 }
2012 cx.background().simulate_random_delay().await;
2013 }
2014 log::info!("{}: done", client.username);
2015}
2016
2017fn buffer_for_full_path(
2018 client: &TestClient,
2019 project: &ModelHandle<Project>,
2020 full_path: &PathBuf,
2021 cx: &TestAppContext,
2022) -> Option<ModelHandle<language::Buffer>> {
2023 client
2024 .buffers_for_project(project)
2025 .iter()
2026 .find(|buffer| {
2027 buffer.read_with(cx, |buffer, cx| {
2028 buffer.file().unwrap().full_path(cx) == *full_path
2029 })
2030 })
2031 .cloned()
2032}
2033
2034fn project_for_root_name(
2035 client: &TestClient,
2036 root_name: &str,
2037 cx: &TestAppContext,
2038) -> Option<ModelHandle<Project>> {
2039 if let Some(ix) = project_ix_for_root_name(&*client.local_projects(), root_name, cx) {
2040 return Some(client.local_projects()[ix].clone());
2041 }
2042 if let Some(ix) = project_ix_for_root_name(&*client.remote_projects(), root_name, cx) {
2043 return Some(client.remote_projects()[ix].clone());
2044 }
2045 None
2046}
2047
2048fn project_ix_for_root_name(
2049 projects: &[ModelHandle<Project>],
2050 root_name: &str,
2051 cx: &TestAppContext,
2052) -> Option<usize> {
2053 projects.iter().position(|project| {
2054 project.read_with(cx, |project, cx| {
2055 let worktree = project.visible_worktrees(cx).next().unwrap();
2056 worktree.read(cx).root_name() == root_name
2057 })
2058 })
2059}
2060
2061fn root_name_for_project(project: &ModelHandle<Project>, cx: &TestAppContext) -> String {
2062 project.read_with(cx, |project, cx| {
2063 project
2064 .visible_worktrees(cx)
2065 .next()
2066 .unwrap()
2067 .read(cx)
2068 .root_name()
2069 .to_string()
2070 })
2071}
2072
2073fn project_path_for_full_path(
2074 project: &ModelHandle<Project>,
2075 full_path: &Path,
2076 cx: &TestAppContext,
2077) -> Option<ProjectPath> {
2078 let mut components = full_path.components();
2079 let root_name = components.next().unwrap().as_os_str().to_str().unwrap();
2080 let path = components.as_path().into();
2081 let worktree_id = project.read_with(cx, |project, cx| {
2082 project.worktrees(cx).find_map(|worktree| {
2083 let worktree = worktree.read(cx);
2084 if worktree.root_name() == root_name {
2085 Some(worktree.id())
2086 } else {
2087 None
2088 }
2089 })
2090 })?;
2091 Some(ProjectPath { worktree_id, path })
2092}
2093
2094async fn ensure_project_shared(
2095 project: &ModelHandle<Project>,
2096 client: &TestClient,
2097 cx: &mut TestAppContext,
2098) {
2099 let first_root_name = root_name_for_project(project, cx);
2100 let active_call = cx.read(ActiveCall::global);
2101 if active_call.read_with(cx, |call, _| call.room().is_some())
2102 && project.read_with(cx, |project, _| project.is_local() && !project.is_shared())
2103 {
2104 match active_call
2105 .update(cx, |call, cx| call.share_project(project.clone(), cx))
2106 .await
2107 {
2108 Ok(project_id) => {
2109 log::info!(
2110 "{}: shared project {} with id {}",
2111 client.username,
2112 first_root_name,
2113 project_id
2114 );
2115 }
2116 Err(error) => {
2117 log::error!(
2118 "{}: error sharing project {}: {:?}",
2119 client.username,
2120 first_root_name,
2121 error
2122 );
2123 }
2124 }
2125 }
2126}
2127
2128fn choose_random_project(client: &TestClient, rng: &mut StdRng) -> Option<ModelHandle<Project>> {
2129 client
2130 .local_projects()
2131 .iter()
2132 .chain(client.remote_projects().iter())
2133 .choose(rng)
2134 .cloned()
2135}
2136
2137fn gen_file_name(rng: &mut StdRng) -> String {
2138 let mut name = String::new();
2139 for _ in 0..10 {
2140 let letter = rng.gen_range('a'..='z');
2141 name.push(letter);
2142 }
2143 name
2144}
2145
2146fn path_env_var(name: &str) -> Option<PathBuf> {
2147 let value = env::var(name).ok()?;
2148 let mut path = PathBuf::from(value);
2149 if path.is_relative() {
2150 let mut abs_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
2151 abs_path.pop();
2152 abs_path.pop();
2153 abs_path.push(path);
2154 path = abs_path
2155 }
2156 Some(path)
2157}