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