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, HashSet};
10use fs::Fs as _;
11use futures::StreamExt as _;
12use gpui::{executor::Deterministic, ModelHandle, TestAppContext};
13use language::{range_to_lsp, FakeLspAdapter, Language, LanguageConfig, PointUtf16};
14use lsp::FakeLanguageServer;
15use parking_lot::Mutex;
16use project::{search::SearchQuery, Project};
17use rand::prelude::*;
18use std::{
19 env,
20 ops::Range,
21 path::{Path, PathBuf},
22 rc::Rc,
23 sync::Arc,
24};
25
26#[gpui::test(iterations = 100)]
27async fn test_random_collaboration(
28 cx: &mut TestAppContext,
29 deterministic: Arc<Deterministic>,
30 mut rng: StdRng,
31) {
32 deterministic.forbid_parking();
33
34 let max_peers = env::var("MAX_PEERS")
35 .map(|i| i.parse().expect("invalid `MAX_PEERS` variable"))
36 .unwrap_or(5);
37
38 let max_operations = env::var("OPERATIONS")
39 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
40 .unwrap_or(10);
41
42 let mut server = TestServer::start(&deterministic).await;
43 let db = server.app_state.db.clone();
44
45 let mut users = Vec::new();
46 for ix in 0..max_peers {
47 let username = format!("user-{}", ix + 1);
48 let user_id = db
49 .create_user(
50 &format!("{username}@example.com"),
51 false,
52 NewUserParams {
53 github_login: username.clone(),
54 github_user_id: (ix + 1) as i32,
55 invite_count: 0,
56 },
57 )
58 .await
59 .unwrap()
60 .user_id;
61 users.push(UserTestPlan {
62 user_id,
63 username,
64 online: false,
65 next_root_id: 0,
66 });
67 }
68
69 for (ix, user_a) in users.iter().enumerate() {
70 for user_b in &users[ix + 1..] {
71 server
72 .app_state
73 .db
74 .send_contact_request(user_a.user_id, user_b.user_id)
75 .await
76 .unwrap();
77 server
78 .app_state
79 .db
80 .respond_to_contact_request(user_b.user_id, user_a.user_id, true)
81 .await
82 .unwrap();
83 }
84 }
85
86 let plan = Arc::new(Mutex::new(TestPlan {
87 users,
88 allow_server_restarts: rng.gen_bool(0.7),
89 allow_client_reconnection: rng.gen_bool(0.7),
90 allow_client_disconnection: rng.gen_bool(0.1),
91 rng,
92 }));
93
94 let mut clients = Vec::new();
95 let mut client_tasks = Vec::new();
96 let mut operation_channels = Vec::new();
97 let mut next_entity_id = 100000;
98
99 let mut i = 0;
100 while i < max_operations {
101 let next_operation = plan.lock().next_operation(&clients).await;
102 match next_operation {
103 Operation::AddConnection { user_id } => {
104 let username = {
105 let mut plan = plan.lock();
106 let mut user = plan.user(user_id);
107 user.online = true;
108 user.username.clone()
109 };
110 log::info!("Adding new connection for {}", username);
111 next_entity_id += 100000;
112 let mut client_cx = TestAppContext::new(
113 cx.foreground_platform(),
114 cx.platform(),
115 deterministic.build_foreground(next_entity_id),
116 deterministic.build_background(),
117 cx.font_cache(),
118 cx.leak_detector(),
119 next_entity_id,
120 cx.function_name.clone(),
121 );
122
123 let (operation_tx, operation_rx) = futures::channel::mpsc::unbounded();
124 let client = Rc::new(server.create_client(&mut client_cx, &username).await);
125 operation_channels.push(operation_tx);
126 clients.push((client.clone(), client_cx.clone()));
127 client_tasks.push(client_cx.foreground().spawn(simulate_client(
128 client,
129 operation_rx,
130 plan.clone(),
131 client_cx,
132 )));
133
134 log::info!("Added connection for {}", username);
135 i += 1;
136 }
137
138 Operation::RemoveConnection { user_id } => {
139 log::info!("Simulating full disconnection of user {}", user_id);
140 let client_ix = clients
141 .iter()
142 .position(|(client, cx)| client.current_user_id(cx) == user_id)
143 .unwrap();
144 let user_connection_ids = server
145 .connection_pool
146 .lock()
147 .user_connection_ids(user_id)
148 .collect::<Vec<_>>();
149 assert_eq!(user_connection_ids.len(), 1);
150 let removed_peer_id = user_connection_ids[0].into();
151 let (client, mut client_cx) = clients.remove(client_ix);
152 let client_task = client_tasks.remove(client_ix);
153 operation_channels.remove(client_ix);
154 server.forbid_connections();
155 server.disconnect_client(removed_peer_id);
156 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
157 deterministic.start_waiting();
158 log::info!("Waiting for user {} to exit...", user_id);
159 client_task.await;
160 deterministic.finish_waiting();
161 server.allow_connections();
162
163 for project in client.remote_projects().iter() {
164 project.read_with(&client_cx, |project, _| {
165 assert!(
166 project.is_read_only(),
167 "project {:?} should be read only",
168 project.remote_id()
169 )
170 });
171 }
172
173 for (client, cx) in &clients {
174 let contacts = server
175 .app_state
176 .db
177 .get_contacts(client.current_user_id(cx))
178 .await
179 .unwrap();
180 let pool = server.connection_pool.lock();
181 for contact in contacts {
182 if let db::Contact::Accepted { user_id: id, .. } = contact {
183 if pool.is_user_online(id) {
184 assert_ne!(
185 id, user_id,
186 "removed client is still a contact of another peer"
187 );
188 }
189 }
190 }
191 }
192
193 log::info!("{} removed", client.username);
194 plan.lock().user(user_id).online = false;
195 client_cx.update(|cx| {
196 cx.clear_globals();
197 drop(client);
198 });
199 i += 1;
200 }
201
202 Operation::BounceConnection { user_id } => {
203 log::info!("Simulating temporary disconnection of user {}", user_id);
204 let user_connection_ids = server
205 .connection_pool
206 .lock()
207 .user_connection_ids(user_id)
208 .collect::<Vec<_>>();
209 assert_eq!(user_connection_ids.len(), 1);
210 let peer_id = user_connection_ids[0].into();
211 server.disconnect_client(peer_id);
212 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
213 i += 1;
214 }
215
216 Operation::RestartServer => {
217 log::info!("Simulating server restart");
218 server.reset().await;
219 deterministic.advance_clock(RECEIVE_TIMEOUT);
220 server.start().await.unwrap();
221 deterministic.advance_clock(CLEANUP_TIMEOUT);
222 let environment = &server.app_state.config.zed_environment;
223 let stale_room_ids = server
224 .app_state
225 .db
226 .stale_room_ids(environment, server.id())
227 .await
228 .unwrap();
229 assert_eq!(stale_room_ids, vec![]);
230 i += 1;
231 }
232
233 Operation::MutateClients { user_ids, quiesce } => {
234 for user_id in user_ids {
235 let client_ix = clients
236 .iter()
237 .position(|(client, cx)| client.current_user_id(cx) == user_id)
238 .unwrap();
239 operation_channels[client_ix].unbounded_send(()).unwrap();
240 i += 1;
241 }
242
243 if quiesce {
244 deterministic.run_until_parked();
245 }
246 }
247 }
248 }
249
250 drop(operation_channels);
251 deterministic.start_waiting();
252 futures::future::join_all(client_tasks).await;
253 deterministic.finish_waiting();
254 deterministic.run_until_parked();
255
256 for (client, client_cx) in &clients {
257 for guest_project in client.remote_projects().iter() {
258 guest_project.read_with(client_cx, |guest_project, cx| {
259 let host_project = clients.iter().find_map(|(client, cx)| {
260 let project = client
261 .local_projects()
262 .iter()
263 .find(|host_project| {
264 host_project.read_with(cx, |host_project, _| {
265 host_project.remote_id() == guest_project.remote_id()
266 })
267 })?
268 .clone();
269 Some((project, cx))
270 });
271
272 if !guest_project.is_read_only() {
273 if let Some((host_project, host_cx)) = host_project {
274 let host_worktree_snapshots =
275 host_project.read_with(host_cx, |host_project, cx| {
276 host_project
277 .worktrees(cx)
278 .map(|worktree| {
279 let worktree = worktree.read(cx);
280 (worktree.id(), worktree.snapshot())
281 })
282 .collect::<BTreeMap<_, _>>()
283 });
284 let guest_worktree_snapshots = guest_project
285 .worktrees(cx)
286 .map(|worktree| {
287 let worktree = worktree.read(cx);
288 (worktree.id(), worktree.snapshot())
289 })
290 .collect::<BTreeMap<_, _>>();
291
292 assert_eq!(
293 guest_worktree_snapshots.keys().collect::<Vec<_>>(),
294 host_worktree_snapshots.keys().collect::<Vec<_>>(),
295 "{} has different worktrees than the host",
296 client.username
297 );
298
299 for (id, host_snapshot) in &host_worktree_snapshots {
300 let guest_snapshot = &guest_worktree_snapshots[id];
301 assert_eq!(
302 guest_snapshot.root_name(),
303 host_snapshot.root_name(),
304 "{} has different root name than the host for worktree {}",
305 client.username,
306 id
307 );
308 assert_eq!(
309 guest_snapshot.abs_path(),
310 host_snapshot.abs_path(),
311 "{} has different abs path than the host for worktree {}",
312 client.username,
313 id
314 );
315 assert_eq!(
316 guest_snapshot.entries(false).collect::<Vec<_>>(),
317 host_snapshot.entries(false).collect::<Vec<_>>(),
318 "{} has different snapshot than the host for worktree {} ({:?}) and project {:?}",
319 client.username,
320 id,
321 host_snapshot.abs_path(),
322 host_project.read_with(host_cx, |project, _| project.remote_id())
323 );
324 assert_eq!(guest_snapshot.scan_id(), host_snapshot.scan_id());
325 }
326 }
327 }
328
329 guest_project.check_invariants(cx);
330 });
331 }
332
333 let buffers = client.buffers().clone();
334 for (guest_project, guest_buffers) in &buffers {
335 let project_id = if guest_project.read_with(client_cx, |project, _| {
336 project.is_local() || project.is_read_only()
337 }) {
338 continue;
339 } else {
340 guest_project
341 .read_with(client_cx, |project, _| project.remote_id())
342 .unwrap()
343 };
344 let guest_user_id = client.user_id().unwrap();
345
346 let host_project = clients.iter().find_map(|(client, cx)| {
347 let project = client
348 .local_projects()
349 .iter()
350 .find(|host_project| {
351 host_project.read_with(cx, |host_project, _| {
352 host_project.remote_id() == Some(project_id)
353 })
354 })?
355 .clone();
356 Some((client.user_id().unwrap(), project, cx))
357 });
358
359 let (host_user_id, host_project, host_cx) =
360 if let Some((host_user_id, host_project, host_cx)) = host_project {
361 (host_user_id, host_project, host_cx)
362 } else {
363 continue;
364 };
365
366 for guest_buffer in guest_buffers {
367 let buffer_id = guest_buffer.read_with(client_cx, |buffer, _| buffer.remote_id());
368 let host_buffer = host_project.read_with(host_cx, |project, cx| {
369 project.buffer_for_id(buffer_id, cx).unwrap_or_else(|| {
370 panic!(
371 "host does not have buffer for guest:{}, peer:{:?}, id:{}",
372 client.username,
373 client.peer_id(),
374 buffer_id
375 )
376 })
377 });
378 let path = host_buffer
379 .read_with(host_cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
380
381 assert_eq!(
382 guest_buffer.read_with(client_cx, |buffer, _| buffer.deferred_ops_len()),
383 0,
384 "{}, buffer {}, path {:?} has deferred operations",
385 client.username,
386 buffer_id,
387 path,
388 );
389 assert_eq!(
390 guest_buffer.read_with(client_cx, |buffer, _| buffer.text()),
391 host_buffer.read_with(host_cx, |buffer, _| buffer.text()),
392 "{}, buffer {}, path {:?}, differs from the host's buffer",
393 client.username,
394 buffer_id,
395 path
396 );
397
398 let host_file = host_buffer.read_with(host_cx, |b, _| b.file().cloned());
399 let guest_file = guest_buffer.read_with(client_cx, |b, _| b.file().cloned());
400 match (host_file, guest_file) {
401 (Some(host_file), Some(guest_file)) => {
402 assert_eq!(guest_file.path(), host_file.path());
403 assert_eq!(guest_file.is_deleted(), host_file.is_deleted());
404 assert_eq!(
405 guest_file.mtime(),
406 host_file.mtime(),
407 "guest {} mtime does not match host {} for path {:?} in project {}",
408 guest_user_id,
409 host_user_id,
410 guest_file.path(),
411 project_id,
412 );
413 }
414 (None, None) => {}
415 (None, _) => panic!("host's file is None, guest's isn't "),
416 (_, None) => panic!("guest's file is None, hosts's isn't "),
417 }
418 }
419 }
420 }
421
422 for (client, mut cx) in clients {
423 cx.update(|cx| {
424 cx.clear_globals();
425 drop(client);
426 });
427 }
428}
429
430struct TestPlan {
431 rng: StdRng,
432 users: Vec<UserTestPlan>,
433 allow_server_restarts: bool,
434 allow_client_reconnection: bool,
435 allow_client_disconnection: bool,
436}
437
438struct UserTestPlan {
439 user_id: UserId,
440 username: String,
441 next_root_id: usize,
442 online: bool,
443}
444
445#[derive(Debug)]
446enum Operation {
447 AddConnection {
448 user_id: UserId,
449 },
450 RemoveConnection {
451 user_id: UserId,
452 },
453 BounceConnection {
454 user_id: UserId,
455 },
456 RestartServer,
457 MutateClients {
458 user_ids: Vec<UserId>,
459 quiesce: bool,
460 },
461}
462
463#[derive(Debug)]
464enum ClientOperation {
465 AcceptIncomingCall,
466 RejectIncomingCall,
467 LeaveCall,
468 InviteContactToCall {
469 user_id: UserId,
470 },
471 OpenLocalProject {
472 first_root_name: String,
473 },
474 OpenRemoteProject {
475 host_id: UserId,
476 first_root_name: String,
477 },
478 AddWorktreeToProject {
479 project_root_name: String,
480 new_root_path: PathBuf,
481 },
482 CloseRemoteProject {
483 project_root_name: String,
484 },
485 OpenBuffer {
486 project_root_name: String,
487 full_path: PathBuf,
488 },
489 SearchProject {
490 project_root_name: String,
491 query: String,
492 detach: bool,
493 },
494 EditBuffer {
495 project_root_name: String,
496 full_path: PathBuf,
497 edits: Vec<(Range<usize>, Arc<str>)>,
498 },
499 CloseBuffer {
500 project_root_name: String,
501 full_path: PathBuf,
502 },
503 SaveBuffer {
504 project_root_name: String,
505 full_path: PathBuf,
506 detach: bool,
507 },
508 RequestLspDataInBuffer {
509 project_root_name: String,
510 full_path: PathBuf,
511 offset: usize,
512 kind: LspRequestKind,
513 detach: bool,
514 },
515 Other,
516}
517
518#[derive(Debug)]
519enum LspRequestKind {
520 Rename,
521 Completion,
522 CodeAction,
523 Definition,
524 Highlights,
525}
526
527impl TestPlan {
528 async fn next_operation(&mut self, clients: &[(Rc<TestClient>, TestAppContext)]) -> Operation {
529 let operation = loop {
530 break match self.rng.gen_range(0..100) {
531 0..=29 if clients.len() < self.users.len() => {
532 let user = self
533 .users
534 .iter()
535 .filter(|u| !u.online)
536 .choose(&mut self.rng)
537 .unwrap();
538 Operation::AddConnection {
539 user_id: user.user_id,
540 }
541 }
542 30..=34 if clients.len() > 1 && self.allow_client_disconnection => {
543 let (client, cx) = &clients[self.rng.gen_range(0..clients.len())];
544 let user_id = client.current_user_id(cx);
545 Operation::RemoveConnection { user_id }
546 }
547 35..=39 if clients.len() > 1 && self.allow_client_reconnection => {
548 let (client, cx) = &clients[self.rng.gen_range(0..clients.len())];
549 let user_id = client.current_user_id(cx);
550 Operation::BounceConnection { user_id }
551 }
552 40..=44 if self.allow_server_restarts && clients.len() > 1 => {
553 Operation::RestartServer
554 }
555 _ if !clients.is_empty() => {
556 let user_ids = (0..self.rng.gen_range(0..10))
557 .map(|_| {
558 let ix = self.rng.gen_range(0..clients.len());
559 let (client, cx) = &clients[ix];
560 client.current_user_id(cx)
561 })
562 .collect();
563 Operation::MutateClients {
564 user_ids,
565 quiesce: self.rng.gen(),
566 }
567 }
568 _ => continue,
569 };
570 };
571 operation
572 }
573
574 async fn next_client_operation(
575 &mut self,
576 client: &TestClient,
577 cx: &TestAppContext,
578 ) -> ClientOperation {
579 let user_id = client.current_user_id(cx);
580 let call = cx.read(ActiveCall::global);
581 let operation = loop {
582 match self.rng.gen_range(0..100) {
583 // Mutate the call
584 0..=29 => {
585 // Respond to an incoming call
586 if call.read_with(cx, |call, _| call.incoming().borrow().is_some()) {
587 break if self.rng.gen_bool(0.7) {
588 ClientOperation::AcceptIncomingCall
589 } else {
590 ClientOperation::RejectIncomingCall
591 };
592 }
593
594 match self.rng.gen_range(0..100_u32) {
595 // Invite a contact to the current call
596 0..=70 => {
597 let available_contacts =
598 client.user_store.read_with(cx, |user_store, _| {
599 user_store
600 .contacts()
601 .iter()
602 .filter(|contact| contact.online && !contact.busy)
603 .cloned()
604 .collect::<Vec<_>>()
605 });
606 if !available_contacts.is_empty() {
607 let contact = available_contacts.choose(&mut self.rng).unwrap();
608 break ClientOperation::InviteContactToCall {
609 user_id: UserId(contact.user.id as i32),
610 };
611 }
612 }
613
614 // Leave the current call
615 71.. => {
616 if self.allow_client_disconnection
617 && call.read_with(cx, |call, _| call.room().is_some())
618 {
619 break ClientOperation::LeaveCall;
620 }
621 }
622 }
623 }
624
625 // Mutate projects
626 39..=59 => match self.rng.gen_range(0..100_u32) {
627 // Open a new project
628 0..=70 => {
629 // Open a remote project
630 if let Some(room) = call.read_with(cx, |call, _| call.room().cloned()) {
631 let existing_remote_project_ids = cx.read(|cx| {
632 client
633 .remote_projects()
634 .iter()
635 .map(|p| p.read(cx).remote_id().unwrap())
636 .collect::<Vec<_>>()
637 });
638 let new_remote_projects = room.read_with(cx, |room, _| {
639 room.remote_participants()
640 .values()
641 .flat_map(|participant| {
642 participant.projects.iter().filter_map(|project| {
643 if existing_remote_project_ids.contains(&project.id) {
644 None
645 } else {
646 Some((
647 UserId::from_proto(participant.user.id),
648 project.worktree_root_names[0].clone(),
649 ))
650 }
651 })
652 })
653 .collect::<Vec<_>>()
654 });
655 if !new_remote_projects.is_empty() {
656 let (host_id, first_root_name) =
657 new_remote_projects.choose(&mut self.rng).unwrap().clone();
658 break ClientOperation::OpenRemoteProject {
659 host_id,
660 first_root_name,
661 };
662 }
663 }
664 // Open a local project
665 else {
666 let first_root_name = self.next_root_dir_name(user_id);
667 break ClientOperation::OpenLocalProject { first_root_name };
668 }
669 }
670
671 // Close a remote project
672 71..=80 => {
673 if !client.remote_projects().is_empty() {
674 let project = client
675 .remote_projects()
676 .choose(&mut self.rng)
677 .unwrap()
678 .clone();
679 let first_root_name = root_name_for_project(&project, cx);
680 break ClientOperation::CloseRemoteProject {
681 project_root_name: first_root_name,
682 };
683 }
684 }
685
686 // Add a worktree to a local project
687 81.. => {
688 if !client.local_projects().is_empty() {
689 let project = client
690 .local_projects()
691 .choose(&mut self.rng)
692 .unwrap()
693 .clone();
694 let project_root_name = root_name_for_project(&project, cx);
695
696 let mut paths = client.fs.paths().await;
697 paths.remove(0);
698 let new_root_path = if paths.is_empty() || self.rng.gen() {
699 Path::new("/").join(&self.next_root_dir_name(user_id))
700 } else {
701 paths.choose(&mut self.rng).unwrap().clone()
702 };
703
704 break ClientOperation::AddWorktreeToProject {
705 project_root_name,
706 new_root_path,
707 };
708 }
709 }
710 },
711
712 // Query and mutate buffers
713 60.. => {
714 let Some(project) = choose_random_project(client, &mut self.rng) else { continue };
715 let project_root_name = root_name_for_project(&project, cx);
716
717 match self.rng.gen_range(0..100_u32) {
718 // Manipulate an existing buffer
719 0..=70 => {
720 let Some(buffer) = client
721 .buffers_for_project(&project)
722 .iter()
723 .choose(&mut self.rng)
724 .cloned() else { continue };
725
726 let full_path = buffer
727 .read_with(cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
728
729 match self.rng.gen_range(0..100_u32) {
730 // Close the buffer
731 0..=15 => {
732 break ClientOperation::CloseBuffer {
733 project_root_name,
734 full_path,
735 };
736 }
737 // Save the buffer
738 16..=29 if buffer.read_with(cx, |b, _| b.is_dirty()) => {
739 let detach = self.rng.gen_bool(0.3);
740 break ClientOperation::SaveBuffer {
741 project_root_name,
742 full_path,
743 detach,
744 };
745 }
746 // Edit the buffer
747 30..=69 => {
748 let edits = buffer.read_with(cx, |buffer, _| {
749 buffer.get_random_edits(&mut self.rng, 3)
750 });
751 break ClientOperation::EditBuffer {
752 project_root_name,
753 full_path,
754 edits,
755 };
756 }
757 // Make an LSP request
758 _ => {
759 let offset = buffer.read_with(cx, |buffer, _| {
760 buffer.clip_offset(
761 self.rng.gen_range(0..=buffer.len()),
762 language::Bias::Left,
763 )
764 });
765 let detach = self.rng.gen();
766 break ClientOperation::RequestLspDataInBuffer {
767 project_root_name,
768 full_path,
769 offset,
770 kind: match self.rng.gen_range(0..5_u32) {
771 0 => LspRequestKind::Rename,
772 1 => LspRequestKind::Highlights,
773 2 => LspRequestKind::Definition,
774 3 => LspRequestKind::CodeAction,
775 4.. => LspRequestKind::Completion,
776 },
777 detach,
778 };
779 }
780 }
781 }
782
783 71..=80 => {
784 let query = self.rng.gen_range('a'..='z').to_string();
785 let detach = self.rng.gen_bool(0.3);
786 break ClientOperation::SearchProject {
787 project_root_name,
788 query,
789 detach,
790 };
791 }
792
793 // Open a buffer
794 81.. => {
795 let worktree = project.read_with(cx, |project, cx| {
796 project
797 .worktrees(cx)
798 .filter(|worktree| {
799 let worktree = worktree.read(cx);
800 worktree.is_visible()
801 && worktree.entries(false).any(|e| e.is_file())
802 })
803 .choose(&mut self.rng)
804 });
805 let Some(worktree) = worktree else { continue };
806 let full_path = worktree.read_with(cx, |worktree, _| {
807 let entry = worktree
808 .entries(false)
809 .filter(|e| e.is_file())
810 .choose(&mut self.rng)
811 .unwrap();
812 if entry.path.as_ref() == Path::new("") {
813 Path::new(worktree.root_name()).into()
814 } else {
815 Path::new(worktree.root_name()).join(&entry.path)
816 }
817 });
818 break ClientOperation::OpenBuffer {
819 project_root_name,
820 full_path,
821 };
822 }
823 }
824 }
825
826 _ => break ClientOperation::Other,
827 }
828 };
829 operation
830 }
831
832 fn next_root_dir_name(&mut self, user_id: UserId) -> String {
833 let user_ix = self
834 .users
835 .iter()
836 .position(|user| user.user_id == user_id)
837 .unwrap();
838 let root_id = util::post_inc(&mut self.users[user_ix].next_root_id);
839 format!("dir-{user_id}-{root_id}")
840 }
841
842 fn user(&mut self, user_id: UserId) -> &mut UserTestPlan {
843 let ix = self
844 .users
845 .iter()
846 .position(|user| user.user_id == user_id)
847 .unwrap();
848 &mut self.users[ix]
849 }
850}
851
852async fn simulate_client(
853 client: Rc<TestClient>,
854 mut operation_rx: futures::channel::mpsc::UnboundedReceiver<()>,
855 plan: Arc<Mutex<TestPlan>>,
856 mut cx: TestAppContext,
857) {
858 // Setup language server
859 let mut language = Language::new(
860 LanguageConfig {
861 name: "Rust".into(),
862 path_suffixes: vec!["rs".to_string()],
863 ..Default::default()
864 },
865 None,
866 );
867 let _fake_language_servers = language
868 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
869 name: "the-fake-language-server",
870 capabilities: lsp::LanguageServer::full_capabilities(),
871 initializer: Some(Box::new({
872 let plan = plan.clone();
873 let fs = client.fs.clone();
874 move |fake_server: &mut FakeLanguageServer| {
875 fake_server.handle_request::<lsp::request::Completion, _, _>(
876 |_, _| async move {
877 Ok(Some(lsp::CompletionResponse::Array(vec![
878 lsp::CompletionItem {
879 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
880 range: lsp::Range::new(
881 lsp::Position::new(0, 0),
882 lsp::Position::new(0, 0),
883 ),
884 new_text: "the-new-text".to_string(),
885 })),
886 ..Default::default()
887 },
888 ])))
889 },
890 );
891
892 fake_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
893 |_, _| async move {
894 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
895 lsp::CodeAction {
896 title: "the-code-action".to_string(),
897 ..Default::default()
898 },
899 )]))
900 },
901 );
902
903 fake_server.handle_request::<lsp::request::PrepareRenameRequest, _, _>(
904 |params, _| async move {
905 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
906 params.position,
907 params.position,
908 ))))
909 },
910 );
911
912 fake_server.handle_request::<lsp::request::GotoDefinition, _, _>({
913 let fs = fs.clone();
914 let plan = plan.clone();
915 move |_, _| {
916 let fs = fs.clone();
917 let plan = plan.clone();
918 async move {
919 let files = fs.files().await;
920 let mut plan = plan.lock();
921 let count = plan.rng.gen_range::<usize, _>(1..3);
922 let files = (0..count)
923 .map(|_| files.choose(&mut plan.rng).unwrap())
924 .collect::<Vec<_>>();
925 log::info!("LSP: Returning definitions in files {:?}", &files);
926 Ok(Some(lsp::GotoDefinitionResponse::Array(
927 files
928 .into_iter()
929 .map(|file| lsp::Location {
930 uri: lsp::Url::from_file_path(file).unwrap(),
931 range: Default::default(),
932 })
933 .collect(),
934 )))
935 }
936 }
937 });
938
939 fake_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>({
940 let plan = plan.clone();
941 move |_, _| {
942 let mut highlights = Vec::new();
943 let highlight_count = plan.lock().rng.gen_range(1..=5);
944 for _ in 0..highlight_count {
945 let start_row = plan.lock().rng.gen_range(0..100);
946 let start_column = plan.lock().rng.gen_range(0..100);
947 let start = PointUtf16::new(start_row, start_column);
948 let end_row = plan.lock().rng.gen_range(0..100);
949 let end_column = plan.lock().rng.gen_range(0..100);
950 let end = PointUtf16::new(end_row, end_column);
951 let range = if start > end { end..start } else { start..end };
952 highlights.push(lsp::DocumentHighlight {
953 range: range_to_lsp(range.clone()),
954 kind: Some(lsp::DocumentHighlightKind::READ),
955 });
956 }
957 highlights.sort_unstable_by_key(|highlight| {
958 (highlight.range.start, highlight.range.end)
959 });
960 async move { Ok(Some(highlights)) }
961 }
962 });
963 }
964 })),
965 ..Default::default()
966 }))
967 .await;
968 client.language_registry.add(Arc::new(language));
969
970 while operation_rx.next().await.is_some() {
971 let operation = plan.lock().next_client_operation(&client, &cx).await;
972 if let Err(error) = apply_client_operation(&client, plan.clone(), operation, &mut cx).await
973 {
974 log::error!("{} error: {}", client.username, error);
975 }
976 cx.background().simulate_random_delay().await;
977 }
978 log::info!("{}: done", client.username);
979}
980
981async fn apply_client_operation(
982 client: &TestClient,
983 plan: Arc<Mutex<TestPlan>>,
984 operation: ClientOperation,
985 cx: &mut TestAppContext,
986) -> Result<()> {
987 match operation {
988 ClientOperation::AcceptIncomingCall => {
989 log::info!("{}: accepting incoming call", client.username);
990 let active_call = cx.read(ActiveCall::global);
991 active_call
992 .update(cx, |call, cx| call.accept_incoming(cx))
993 .await?;
994 }
995
996 ClientOperation::RejectIncomingCall => {
997 log::info!("{}: declining incoming call", client.username);
998 let active_call = cx.read(ActiveCall::global);
999 active_call.update(cx, |call, _| call.decline_incoming())?;
1000 }
1001
1002 ClientOperation::LeaveCall => {
1003 log::info!("{}: hanging up", client.username);
1004 let active_call = cx.read(ActiveCall::global);
1005 active_call.update(cx, |call, cx| call.hang_up(cx))?;
1006 }
1007
1008 ClientOperation::InviteContactToCall { user_id } => {
1009 log::info!("{}: inviting {}", client.username, user_id,);
1010 let active_call = cx.read(ActiveCall::global);
1011 active_call
1012 .update(cx, |call, cx| call.invite(user_id.to_proto(), None, cx))
1013 .await?;
1014 }
1015
1016 ClientOperation::OpenLocalProject { first_root_name } => {
1017 log::info!(
1018 "{}: opening local project at {:?}",
1019 client.username,
1020 first_root_name
1021 );
1022 let root_path = Path::new("/").join(&first_root_name);
1023 client.fs.create_dir(&root_path).await.unwrap();
1024 client
1025 .fs
1026 .create_file(&root_path.join("main.rs"), Default::default())
1027 .await
1028 .unwrap();
1029 let project = client.build_local_project(root_path, cx).await.0;
1030 ensure_project_shared(&project, client, cx).await;
1031 client.local_projects_mut().push(project.clone());
1032 }
1033
1034 ClientOperation::AddWorktreeToProject {
1035 project_root_name,
1036 new_root_path,
1037 } => {
1038 log::info!(
1039 "{}: finding/creating local worktree at {:?} to project with root path {}",
1040 client.username,
1041 new_root_path,
1042 project_root_name
1043 );
1044 let project = project_for_root_name(client, &project_root_name, cx)
1045 .expect("invalid project in test operation");
1046 ensure_project_shared(&project, client, cx).await;
1047 if !client.fs.paths().await.contains(&new_root_path) {
1048 client.fs.create_dir(&new_root_path).await.unwrap();
1049 }
1050 project
1051 .update(cx, |project, cx| {
1052 project.find_or_create_local_worktree(&new_root_path, true, cx)
1053 })
1054 .await
1055 .unwrap();
1056 }
1057
1058 ClientOperation::CloseRemoteProject { project_root_name } => {
1059 log::info!(
1060 "{}: closing remote project with root path {}",
1061 client.username,
1062 project_root_name,
1063 );
1064 let ix = project_ix_for_root_name(&*client.remote_projects(), &project_root_name, cx)
1065 .expect("invalid project in test operation");
1066 cx.update(|_| client.remote_projects_mut().remove(ix));
1067 }
1068
1069 ClientOperation::OpenRemoteProject {
1070 host_id,
1071 first_root_name,
1072 } => {
1073 log::info!(
1074 "{}: joining remote project of user {}, root name {}",
1075 client.username,
1076 host_id,
1077 first_root_name,
1078 );
1079 let active_call = cx.read(ActiveCall::global);
1080 let project_id = active_call
1081 .read_with(cx, |call, cx| {
1082 let room = call.room().cloned()?;
1083 let participant = room
1084 .read(cx)
1085 .remote_participants()
1086 .get(&host_id.to_proto())?;
1087 let project = participant
1088 .projects
1089 .iter()
1090 .find(|project| project.worktree_root_names[0] == first_root_name)?;
1091 Some(project.id)
1092 })
1093 .expect("invalid project in test operation");
1094 let project = client.build_remote_project(project_id, cx).await;
1095 client.remote_projects_mut().push(project);
1096 }
1097
1098 ClientOperation::OpenBuffer {
1099 project_root_name,
1100 full_path,
1101 } => {
1102 log::info!(
1103 "{}: opening buffer {:?} in project {}",
1104 client.username,
1105 full_path,
1106 project_root_name,
1107 );
1108 let project = project_for_root_name(client, &project_root_name, cx)
1109 .expect("invalid project in test operation");
1110 // ensure_project_shared(&project, client, cx).await;
1111 let mut components = full_path.components();
1112 let root_name = components.next().unwrap().as_os_str().to_str().unwrap();
1113 let path = components.as_path();
1114 let worktree_id = project
1115 .read_with(cx, |project, cx| {
1116 project.worktrees(cx).find_map(|worktree| {
1117 let worktree = worktree.read(cx);
1118 if worktree.root_name() == root_name {
1119 Some(worktree.id())
1120 } else {
1121 None
1122 }
1123 })
1124 })
1125 .expect("invalid buffer path in test operation");
1126 let buffer = project
1127 .update(cx, |project, cx| {
1128 project.open_buffer((worktree_id, &path), cx)
1129 })
1130 .await?;
1131 client.buffers_for_project(&project).insert(buffer);
1132 }
1133
1134 ClientOperation::EditBuffer {
1135 project_root_name,
1136 full_path,
1137 edits,
1138 } => {
1139 log::info!(
1140 "{}: editing buffer {:?} in project {} with {:?}",
1141 client.username,
1142 full_path,
1143 project_root_name,
1144 edits
1145 );
1146 let project = project_for_root_name(client, &project_root_name, cx)
1147 .expect("invalid project in test operation");
1148 let buffer =
1149 buffer_for_full_path(&*client.buffers_for_project(&project), &full_path, cx)
1150 .expect("invalid buffer path in test operation");
1151 buffer.update(cx, |buffer, cx| {
1152 buffer.edit(edits, None, cx);
1153 });
1154 }
1155
1156 ClientOperation::CloseBuffer {
1157 project_root_name,
1158 full_path,
1159 } => {
1160 log::info!(
1161 "{}: dropping buffer {:?} in project {}",
1162 client.username,
1163 full_path,
1164 project_root_name
1165 );
1166 let project = project_for_root_name(client, &project_root_name, cx)
1167 .expect("invalid project in test operation");
1168 let buffer =
1169 buffer_for_full_path(&*client.buffers_for_project(&project), &full_path, cx)
1170 .expect("invalid buffer path in test operation");
1171 cx.update(|_| {
1172 client.buffers_for_project(&project).remove(&buffer);
1173 drop(buffer);
1174 });
1175 }
1176
1177 ClientOperation::SaveBuffer {
1178 project_root_name,
1179 full_path,
1180 detach,
1181 } => {
1182 log::info!(
1183 "{}: saving buffer {:?} in project {}{}",
1184 client.username,
1185 full_path,
1186 project_root_name,
1187 if detach { ", detaching" } else { ", awaiting" }
1188 );
1189 let project = project_for_root_name(client, &project_root_name, cx)
1190 .expect("invalid project in test operation");
1191 let buffer =
1192 buffer_for_full_path(&*client.buffers_for_project(&project), &full_path, cx)
1193 .expect("invalid buffer path in test operation");
1194 let (requested_version, save) =
1195 buffer.update(cx, |buffer, cx| (buffer.version(), buffer.save(cx)));
1196 let save = cx.background().spawn(async move {
1197 let (saved_version, _, _) = save
1198 .await
1199 .map_err(|err| anyhow!("save request failed: {:?}", err))?;
1200 assert!(saved_version.observed_all(&requested_version));
1201 anyhow::Ok(())
1202 });
1203 if detach {
1204 log::info!("{}: detaching save request", client.username);
1205 cx.update(|cx| save.detach_and_log_err(cx));
1206 } else {
1207 save.await?;
1208 }
1209 }
1210
1211 ClientOperation::RequestLspDataInBuffer {
1212 project_root_name,
1213 full_path,
1214 offset,
1215 kind,
1216 detach,
1217 } => {
1218 log::info!(
1219 "{}: request LSP {:?} for buffer {:?} in project {}{}",
1220 client.username,
1221 kind,
1222 full_path,
1223 project_root_name,
1224 if detach { ", detaching" } else { ", awaiting" }
1225 );
1226
1227 let project = project_for_root_name(client, &project_root_name, cx)
1228 .expect("invalid project in test operation");
1229 let buffer =
1230 buffer_for_full_path(&*client.buffers_for_project(&project), &full_path, cx)
1231 .expect("invalid buffer path in test operation");
1232 let request = match kind {
1233 LspRequestKind::Rename => cx.spawn(|mut cx| async move {
1234 project
1235 .update(&mut cx, |p, cx| p.prepare_rename(buffer, offset, cx))
1236 .await?;
1237 anyhow::Ok(())
1238 }),
1239 LspRequestKind::Completion => cx.spawn(|mut cx| async move {
1240 project
1241 .update(&mut cx, |p, cx| p.completions(&buffer, offset, cx))
1242 .await?;
1243 Ok(())
1244 }),
1245 LspRequestKind::CodeAction => cx.spawn(|mut cx| async move {
1246 project
1247 .update(&mut cx, |p, cx| p.code_actions(&buffer, offset..offset, cx))
1248 .await?;
1249 Ok(())
1250 }),
1251 LspRequestKind::Definition => cx.spawn(|mut cx| async move {
1252 project
1253 .update(&mut cx, |p, cx| p.definition(&buffer, offset, cx))
1254 .await?;
1255 Ok(())
1256 }),
1257 LspRequestKind::Highlights => cx.spawn(|mut cx| async move {
1258 project
1259 .update(&mut cx, |p, cx| p.document_highlights(&buffer, offset, cx))
1260 .await?;
1261 Ok(())
1262 }),
1263 };
1264 if detach {
1265 request.detach();
1266 } else {
1267 request.await?;
1268 }
1269 }
1270
1271 ClientOperation::SearchProject {
1272 project_root_name,
1273 query,
1274 detach,
1275 } => {
1276 log::info!(
1277 "{}: search project {} for {:?}{}",
1278 client.username,
1279 project_root_name,
1280 query,
1281 if detach { ", detaching" } else { ", awaiting" }
1282 );
1283 let project = project_for_root_name(client, &project_root_name, cx)
1284 .expect("invalid project in test operation");
1285 let search = project.update(cx, |project, cx| {
1286 project.search(SearchQuery::text(query, false, false), cx)
1287 });
1288 let search = cx.background().spawn(async move {
1289 search
1290 .await
1291 .map_err(|err| anyhow!("search request failed: {:?}", err))
1292 });
1293 if detach {
1294 log::info!("{}: detaching save request", client.username);
1295 cx.update(|cx| search.detach_and_log_err(cx));
1296 } else {
1297 search.await?;
1298 }
1299 }
1300
1301 ClientOperation::Other => {
1302 let choice = plan.lock().rng.gen_range(0..100);
1303 match choice {
1304 0..=59
1305 if !client.local_projects().is_empty()
1306 || !client.remote_projects().is_empty() =>
1307 {
1308 randomly_mutate_worktrees(client, &plan, cx).await?;
1309 }
1310 _ => randomly_mutate_fs(client, &plan).await,
1311 }
1312 }
1313 }
1314 Ok(())
1315}
1316
1317fn buffer_for_full_path(
1318 buffers: &HashSet<ModelHandle<language::Buffer>>,
1319 full_path: &PathBuf,
1320 cx: &TestAppContext,
1321) -> Option<ModelHandle<language::Buffer>> {
1322 buffers
1323 .iter()
1324 .find(|buffer| {
1325 buffer.read_with(cx, |buffer, cx| {
1326 buffer.file().unwrap().full_path(cx) == *full_path
1327 })
1328 })
1329 .cloned()
1330}
1331
1332fn project_for_root_name(
1333 client: &TestClient,
1334 root_name: &str,
1335 cx: &TestAppContext,
1336) -> Option<ModelHandle<Project>> {
1337 if let Some(ix) = project_ix_for_root_name(&*client.local_projects(), root_name, cx) {
1338 return Some(client.local_projects()[ix].clone());
1339 }
1340 if let Some(ix) = project_ix_for_root_name(&*client.remote_projects(), root_name, cx) {
1341 return Some(client.remote_projects()[ix].clone());
1342 }
1343 None
1344}
1345
1346fn project_ix_for_root_name(
1347 projects: &[ModelHandle<Project>],
1348 root_name: &str,
1349 cx: &TestAppContext,
1350) -> Option<usize> {
1351 projects.iter().position(|project| {
1352 project.read_with(cx, |project, cx| {
1353 let worktree = project.visible_worktrees(cx).next().unwrap();
1354 worktree.read(cx).root_name() == root_name
1355 })
1356 })
1357}
1358
1359fn root_name_for_project(project: &ModelHandle<Project>, cx: &TestAppContext) -> String {
1360 project.read_with(cx, |project, cx| {
1361 project
1362 .visible_worktrees(cx)
1363 .next()
1364 .unwrap()
1365 .read(cx)
1366 .root_name()
1367 .to_string()
1368 })
1369}
1370
1371async fn ensure_project_shared(
1372 project: &ModelHandle<Project>,
1373 client: &TestClient,
1374 cx: &mut TestAppContext,
1375) {
1376 let first_root_name = root_name_for_project(project, cx);
1377 let active_call = cx.read(ActiveCall::global);
1378 if active_call.read_with(cx, |call, _| call.room().is_some())
1379 && project.read_with(cx, |project, _| project.is_local() && !project.is_shared())
1380 {
1381 match active_call
1382 .update(cx, |call, cx| call.share_project(project.clone(), cx))
1383 .await
1384 {
1385 Ok(project_id) => {
1386 log::info!(
1387 "{}: shared project {} with id {}",
1388 client.username,
1389 first_root_name,
1390 project_id
1391 );
1392 }
1393 Err(error) => {
1394 log::error!(
1395 "{}: error sharing project {}: {:?}",
1396 client.username,
1397 first_root_name,
1398 error
1399 );
1400 }
1401 }
1402 }
1403}
1404
1405async fn randomly_mutate_fs(client: &TestClient, plan: &Arc<Mutex<TestPlan>>) {
1406 let is_dir = plan.lock().rng.gen::<bool>();
1407 let mut new_path = client
1408 .fs
1409 .directories()
1410 .await
1411 .choose(&mut plan.lock().rng)
1412 .unwrap()
1413 .clone();
1414 new_path.push(gen_file_name(&mut plan.lock().rng));
1415 if is_dir {
1416 log::info!("{}: creating local dir at {:?}", client.username, new_path);
1417 client.fs.create_dir(&new_path).await.unwrap();
1418 } else {
1419 new_path.set_extension("rs");
1420 log::info!("{}: creating local file at {:?}", client.username, new_path);
1421 client
1422 .fs
1423 .create_file(&new_path, Default::default())
1424 .await
1425 .unwrap();
1426 }
1427}
1428
1429async fn randomly_mutate_worktrees(
1430 client: &TestClient,
1431 plan: &Arc<Mutex<TestPlan>>,
1432 cx: &mut TestAppContext,
1433) -> Result<()> {
1434 let project = choose_random_project(client, &mut plan.lock().rng).unwrap();
1435 let Some(worktree) = project.read_with(cx, |project, cx| {
1436 project
1437 .worktrees(cx)
1438 .filter(|worktree| {
1439 let worktree = worktree.read(cx);
1440 worktree.is_visible()
1441 && worktree.entries(false).any(|e| e.is_file())
1442 && worktree.root_entry().map_or(false, |e| e.is_dir())
1443 })
1444 .choose(&mut plan.lock().rng)
1445 }) else {
1446 return Ok(())
1447 };
1448
1449 let (worktree_id, worktree_root_name) = worktree.read_with(cx, |worktree, _| {
1450 (worktree.id(), worktree.root_name().to_string())
1451 });
1452
1453 let is_dir = plan.lock().rng.gen::<bool>();
1454 let mut new_path = PathBuf::new();
1455 new_path.push(gen_file_name(&mut plan.lock().rng));
1456 if !is_dir {
1457 new_path.set_extension("rs");
1458 }
1459 log::info!(
1460 "{}: creating {:?} in worktree {} ({})",
1461 client.username,
1462 new_path,
1463 worktree_id,
1464 worktree_root_name,
1465 );
1466 project
1467 .update(cx, |project, cx| {
1468 project.create_entry((worktree_id, new_path), is_dir, cx)
1469 })
1470 .unwrap()
1471 .await?;
1472 Ok(())
1473}
1474
1475fn choose_random_project(client: &TestClient, rng: &mut StdRng) -> Option<ModelHandle<Project>> {
1476 client
1477 .local_projects()
1478 .iter()
1479 .chain(client.remote_projects().iter())
1480 .choose(rng)
1481 .cloned()
1482}
1483
1484fn gen_file_name(rng: &mut StdRng) -> String {
1485 let mut name = String::new();
1486 for _ in 0..10 {
1487 let letter = rng.gen_range('a'..='z');
1488 name.push(letter);
1489 }
1490 name
1491}