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