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