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, 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_id = active_call
535 .read_with(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 = participant
542 .projects
543 .iter()
544 .find(|project| project.worktree_root_names[0] == first_root_name)?;
545 Some(project.id)
546 })
547 .expect("invalid project in test operation");
548 let project = client.build_remote_project(project_id, cx).await;
549 client.remote_projects_mut().push(project);
550 }
551
552 ClientOperation::CreateWorktreeEntry {
553 project_root_name,
554 is_local,
555 full_path,
556 is_dir,
557 } => {
558 log::info!(
559 "{}: creating {} at path {:?} in {} project {}",
560 client.username,
561 if is_dir { "dir" } else { "file" },
562 full_path,
563 if is_local { "local" } else { "remote" },
564 project_root_name,
565 );
566
567 let project = project_for_root_name(client, &project_root_name, cx)
568 .expect("invalid project in test operation");
569 ensure_project_shared(&project, client, cx).await;
570 let project_path = project_path_for_full_path(&project, &full_path, cx)
571 .expect("invalid worktree path in test operation");
572 project
573 .update(cx, |p, cx| p.create_entry(project_path, is_dir, cx))
574 .unwrap()
575 .await?;
576 }
577
578 ClientOperation::OpenBuffer {
579 project_root_name,
580 is_local,
581 full_path,
582 } => {
583 log::info!(
584 "{}: opening buffer {:?} in {} project {}",
585 client.username,
586 full_path,
587 if is_local { "local" } else { "remote" },
588 project_root_name,
589 );
590
591 let project = project_for_root_name(client, &project_root_name, cx)
592 .expect("invalid project in test operation");
593 ensure_project_shared(&project, client, cx).await;
594 let project_path = project_path_for_full_path(&project, &full_path, cx)
595 .expect("invalid buffer path in test operation");
596 let buffer = project
597 .update(cx, |project, cx| project.open_buffer(project_path, cx))
598 .await?;
599 client.buffers_for_project(&project).insert(buffer);
600 }
601
602 ClientOperation::EditBuffer {
603 project_root_name,
604 is_local,
605 full_path,
606 edits,
607 } => {
608 log::info!(
609 "{}: editing buffer {:?} in {} project {} with {:?}",
610 client.username,
611 full_path,
612 if is_local { "local" } else { "remote" },
613 project_root_name,
614 edits
615 );
616
617 let project = project_for_root_name(client, &project_root_name, cx)
618 .expect("invalid project in test operation");
619 ensure_project_shared(&project, client, cx).await;
620 let buffer =
621 buffer_for_full_path(&*client.buffers_for_project(&project), &full_path, cx)
622 .expect("invalid buffer path in test operation");
623 buffer.update(cx, |buffer, cx| {
624 buffer.edit(edits, None, cx);
625 });
626 }
627
628 ClientOperation::CloseBuffer {
629 project_root_name,
630 is_local,
631 full_path,
632 } => {
633 log::info!(
634 "{}: dropping buffer {:?} in {} project {}",
635 client.username,
636 full_path,
637 if is_local { "local" } else { "remote" },
638 project_root_name
639 );
640
641 let project = project_for_root_name(client, &project_root_name, cx)
642 .expect("invalid project in test operation");
643 ensure_project_shared(&project, client, cx).await;
644 let buffer =
645 buffer_for_full_path(&*client.buffers_for_project(&project), &full_path, cx)
646 .expect("invalid buffer path in test operation");
647 cx.update(|_| {
648 client.buffers_for_project(&project).remove(&buffer);
649 drop(buffer);
650 });
651 }
652
653 ClientOperation::SaveBuffer {
654 project_root_name,
655 is_local,
656 full_path,
657 detach,
658 } => {
659 log::info!(
660 "{}: saving buffer {:?} in {} project {}{}",
661 client.username,
662 full_path,
663 if is_local { "local" } else { "remote" },
664 project_root_name,
665 if detach { ", detaching" } else { ", awaiting" }
666 );
667
668 let project = project_for_root_name(client, &project_root_name, cx)
669 .expect("invalid project in test operation");
670 ensure_project_shared(&project, client, cx).await;
671 let buffer =
672 buffer_for_full_path(&*client.buffers_for_project(&project), &full_path, cx)
673 .expect("invalid buffer path in test operation");
674 let (requested_version, save) =
675 buffer.update(cx, |buffer, cx| (buffer.version(), buffer.save(cx)));
676 let save = cx.background().spawn(async move {
677 let (saved_version, _, _) = save
678 .await
679 .map_err(|err| anyhow!("save request failed: {:?}", err))?;
680 assert!(saved_version.observed_all(&requested_version));
681 anyhow::Ok(())
682 });
683 if detach {
684 log::info!("{}: detaching save request", client.username);
685 cx.update(|cx| save.detach_and_log_err(cx));
686 } else {
687 save.await?;
688 }
689 }
690
691 ClientOperation::RequestLspDataInBuffer {
692 project_root_name,
693 is_local,
694 full_path,
695 offset,
696 kind,
697 detach,
698 } => {
699 log::info!(
700 "{}: request LSP {:?} for buffer {:?} in {} project {}{}",
701 client.username,
702 kind,
703 full_path,
704 if is_local { "local" } else { "remote" },
705 project_root_name,
706 if detach { ", detaching" } else { ", awaiting" }
707 );
708
709 let project = project_for_root_name(client, &project_root_name, cx)
710 .expect("invalid project in test operation");
711 let buffer =
712 buffer_for_full_path(&*client.buffers_for_project(&project), &full_path, cx)
713 .expect("invalid buffer path in test operation");
714 let request = match kind {
715 LspRequestKind::Rename => cx.spawn(|mut cx| async move {
716 project
717 .update(&mut cx, |p, cx| p.prepare_rename(buffer, offset, cx))
718 .await?;
719 anyhow::Ok(())
720 }),
721 LspRequestKind::Completion => cx.spawn(|mut cx| async move {
722 project
723 .update(&mut cx, |p, cx| p.completions(&buffer, offset, cx))
724 .await?;
725 Ok(())
726 }),
727 LspRequestKind::CodeAction => cx.spawn(|mut cx| async move {
728 project
729 .update(&mut cx, |p, cx| p.code_actions(&buffer, offset..offset, cx))
730 .await?;
731 Ok(())
732 }),
733 LspRequestKind::Definition => cx.spawn(|mut cx| async move {
734 project
735 .update(&mut cx, |p, cx| p.definition(&buffer, offset, cx))
736 .await?;
737 Ok(())
738 }),
739 LspRequestKind::Highlights => cx.spawn(|mut cx| async move {
740 project
741 .update(&mut cx, |p, cx| p.document_highlights(&buffer, offset, cx))
742 .await?;
743 Ok(())
744 }),
745 };
746 if detach {
747 request.detach();
748 } else {
749 request.await?;
750 }
751 }
752
753 ClientOperation::SearchProject {
754 project_root_name,
755 query,
756 detach,
757 } => {
758 log::info!(
759 "{}: search project {} for {:?}{}",
760 client.username,
761 project_root_name,
762 query,
763 if detach { ", detaching" } else { ", awaiting" }
764 );
765 let project = project_for_root_name(client, &project_root_name, cx)
766 .expect("invalid project in test operation");
767 let search = project.update(cx, |project, cx| {
768 project.search(SearchQuery::text(query, false, false), cx)
769 });
770 let search = cx.background().spawn(async move {
771 search
772 .await
773 .map_err(|err| anyhow!("search request failed: {:?}", err))
774 });
775 if detach {
776 log::info!("{}: detaching save request", client.username);
777 cx.update(|cx| search.detach_and_log_err(cx));
778 } else {
779 search.await?;
780 }
781 }
782
783 ClientOperation::CreateFsEntry { path, is_dir } => {
784 log::info!(
785 "{}: creating {} at {:?}",
786 client.username,
787 if is_dir { "dir" } else { "file" },
788 path
789 );
790 if is_dir {
791 client.fs.create_dir(&path).await.unwrap();
792 } else {
793 client
794 .fs
795 .create_file(&path, Default::default())
796 .await
797 .unwrap();
798 }
799 }
800 }
801 Ok(())
802}
803
804struct TestPlan {
805 rng: StdRng,
806 max_operations: usize,
807 operation_ix: usize,
808 users: Vec<UserTestPlan>,
809 allow_server_restarts: bool,
810 allow_client_reconnection: bool,
811 allow_client_disconnection: bool,
812}
813
814struct UserTestPlan {
815 user_id: UserId,
816 username: String,
817 next_root_id: usize,
818 online: bool,
819}
820
821#[derive(Debug)]
822enum Operation {
823 AddConnection {
824 user_id: UserId,
825 },
826 RemoveConnection {
827 user_id: UserId,
828 },
829 BounceConnection {
830 user_id: UserId,
831 },
832 RestartServer,
833 MutateClients {
834 user_ids: Vec<UserId>,
835 quiesce: bool,
836 },
837}
838
839#[derive(Debug)]
840enum ClientOperation {
841 AcceptIncomingCall,
842 RejectIncomingCall,
843 LeaveCall,
844 InviteContactToCall {
845 user_id: UserId,
846 },
847 OpenLocalProject {
848 first_root_name: String,
849 },
850 OpenRemoteProject {
851 host_id: UserId,
852 first_root_name: String,
853 },
854 AddWorktreeToProject {
855 project_root_name: String,
856 new_root_path: PathBuf,
857 },
858 CloseRemoteProject {
859 project_root_name: String,
860 },
861 OpenBuffer {
862 project_root_name: String,
863 is_local: bool,
864 full_path: PathBuf,
865 },
866 SearchProject {
867 project_root_name: String,
868 query: String,
869 detach: bool,
870 },
871 EditBuffer {
872 project_root_name: String,
873 is_local: bool,
874 full_path: PathBuf,
875 edits: Vec<(Range<usize>, Arc<str>)>,
876 },
877 CloseBuffer {
878 project_root_name: String,
879 is_local: bool,
880 full_path: PathBuf,
881 },
882 SaveBuffer {
883 project_root_name: String,
884 is_local: bool,
885 full_path: PathBuf,
886 detach: bool,
887 },
888 RequestLspDataInBuffer {
889 project_root_name: String,
890 is_local: bool,
891 full_path: PathBuf,
892 offset: usize,
893 kind: LspRequestKind,
894 detach: bool,
895 },
896 CreateWorktreeEntry {
897 project_root_name: String,
898 is_local: bool,
899 full_path: PathBuf,
900 is_dir: bool,
901 },
902 CreateFsEntry {
903 path: PathBuf,
904 is_dir: bool,
905 },
906}
907
908#[derive(Debug)]
909enum LspRequestKind {
910 Rename,
911 Completion,
912 CodeAction,
913 Definition,
914 Highlights,
915}
916
917impl TestPlan {
918 async fn next_operation(
919 &mut self,
920 clients: &[(Rc<TestClient>, TestAppContext)],
921 ) -> Option<Operation> {
922 if self.operation_ix == self.max_operations {
923 return None;
924 }
925
926 let operation = loop {
927 break match self.rng.gen_range(0..100) {
928 0..=29 if clients.len() < self.users.len() => {
929 let user = self
930 .users
931 .iter()
932 .filter(|u| !u.online)
933 .choose(&mut self.rng)
934 .unwrap();
935 self.operation_ix += 1;
936 Operation::AddConnection {
937 user_id: user.user_id,
938 }
939 }
940 30..=34 if clients.len() > 1 && self.allow_client_disconnection => {
941 let (client, cx) = &clients[self.rng.gen_range(0..clients.len())];
942 let user_id = client.current_user_id(cx);
943 self.operation_ix += 1;
944 Operation::RemoveConnection { user_id }
945 }
946 35..=39 if clients.len() > 1 && self.allow_client_reconnection => {
947 let (client, cx) = &clients[self.rng.gen_range(0..clients.len())];
948 let user_id = client.current_user_id(cx);
949 self.operation_ix += 1;
950 Operation::BounceConnection { user_id }
951 }
952 40..=44 if self.allow_server_restarts && clients.len() > 1 => {
953 self.operation_ix += 1;
954 Operation::RestartServer
955 }
956 _ if !clients.is_empty() => {
957 let count = self
958 .rng
959 .gen_range(1..10)
960 .min(self.max_operations - self.operation_ix);
961 let user_ids = (0..count)
962 .map(|_| {
963 let ix = self.rng.gen_range(0..clients.len());
964 let (client, cx) = &clients[ix];
965 client.current_user_id(cx)
966 })
967 .collect();
968 Operation::MutateClients {
969 user_ids,
970 quiesce: self.rng.gen(),
971 }
972 }
973 _ => continue,
974 };
975 };
976 Some(operation)
977 }
978
979 async fn next_client_operation(
980 &mut self,
981 client: &TestClient,
982 cx: &TestAppContext,
983 ) -> Option<ClientOperation> {
984 if self.operation_ix == self.max_operations {
985 return None;
986 }
987
988 let user_id = client.current_user_id(cx);
989 let call = cx.read(ActiveCall::global);
990 let operation = loop {
991 match self.rng.gen_range(0..100_u32) {
992 // Mutate the call
993 0..=29 => {
994 // Respond to an incoming call
995 if call.read_with(cx, |call, _| call.incoming().borrow().is_some()) {
996 break if self.rng.gen_bool(0.7) {
997 ClientOperation::AcceptIncomingCall
998 } else {
999 ClientOperation::RejectIncomingCall
1000 };
1001 }
1002
1003 match self.rng.gen_range(0..100_u32) {
1004 // Invite a contact to the current call
1005 0..=70 => {
1006 let available_contacts =
1007 client.user_store.read_with(cx, |user_store, _| {
1008 user_store
1009 .contacts()
1010 .iter()
1011 .filter(|contact| contact.online && !contact.busy)
1012 .cloned()
1013 .collect::<Vec<_>>()
1014 });
1015 if !available_contacts.is_empty() {
1016 let contact = available_contacts.choose(&mut self.rng).unwrap();
1017 break ClientOperation::InviteContactToCall {
1018 user_id: UserId(contact.user.id as i32),
1019 };
1020 }
1021 }
1022
1023 // Leave the current call
1024 71.. => {
1025 if self.allow_client_disconnection
1026 && call.read_with(cx, |call, _| call.room().is_some())
1027 {
1028 break ClientOperation::LeaveCall;
1029 }
1030 }
1031 }
1032 }
1033
1034 // Mutate projects
1035 30..=59 => match self.rng.gen_range(0..100_u32) {
1036 // Open a new project
1037 0..=70 => {
1038 // Open a remote project
1039 if let Some(room) = call.read_with(cx, |call, _| call.room().cloned()) {
1040 let existing_remote_project_ids = cx.read(|cx| {
1041 client
1042 .remote_projects()
1043 .iter()
1044 .map(|p| p.read(cx).remote_id().unwrap())
1045 .collect::<Vec<_>>()
1046 });
1047 let new_remote_projects = room.read_with(cx, |room, _| {
1048 room.remote_participants()
1049 .values()
1050 .flat_map(|participant| {
1051 participant.projects.iter().filter_map(|project| {
1052 if existing_remote_project_ids.contains(&project.id) {
1053 None
1054 } else {
1055 Some((
1056 UserId::from_proto(participant.user.id),
1057 project.worktree_root_names[0].clone(),
1058 ))
1059 }
1060 })
1061 })
1062 .collect::<Vec<_>>()
1063 });
1064 if !new_remote_projects.is_empty() {
1065 let (host_id, first_root_name) =
1066 new_remote_projects.choose(&mut self.rng).unwrap().clone();
1067 break ClientOperation::OpenRemoteProject {
1068 host_id,
1069 first_root_name,
1070 };
1071 }
1072 }
1073 // Open a local project
1074 else {
1075 let first_root_name = self.next_root_dir_name(user_id);
1076 break ClientOperation::OpenLocalProject { first_root_name };
1077 }
1078 }
1079
1080 // Close a remote project
1081 71..=80 => {
1082 if !client.remote_projects().is_empty() {
1083 let project = client
1084 .remote_projects()
1085 .choose(&mut self.rng)
1086 .unwrap()
1087 .clone();
1088 let first_root_name = root_name_for_project(&project, cx);
1089 break ClientOperation::CloseRemoteProject {
1090 project_root_name: first_root_name,
1091 };
1092 }
1093 }
1094
1095 // Mutate project worktrees
1096 81.. => match self.rng.gen_range(0..100_u32) {
1097 // Add a worktree to a local project
1098 0..=50 => {
1099 let Some(project) = client
1100 .local_projects()
1101 .choose(&mut self.rng)
1102 .cloned() else { continue };
1103 let project_root_name = root_name_for_project(&project, cx);
1104 let mut paths = client.fs.paths().await;
1105 paths.remove(0);
1106 let new_root_path = if paths.is_empty() || self.rng.gen() {
1107 Path::new("/").join(&self.next_root_dir_name(user_id))
1108 } else {
1109 paths.choose(&mut self.rng).unwrap().clone()
1110 };
1111 break ClientOperation::AddWorktreeToProject {
1112 project_root_name,
1113 new_root_path,
1114 };
1115 }
1116
1117 // Add an entry to a worktree
1118 _ => {
1119 let Some(project) = choose_random_project(client, &mut self.rng) else { continue };
1120 let project_root_name = root_name_for_project(&project, cx);
1121 let is_local = project.read_with(cx, |project, _| project.is_local());
1122 let worktree = project.read_with(cx, |project, cx| {
1123 project
1124 .worktrees(cx)
1125 .filter(|worktree| {
1126 let worktree = worktree.read(cx);
1127 worktree.is_visible()
1128 && worktree.entries(false).any(|e| e.is_file())
1129 && worktree.root_entry().map_or(false, |e| e.is_dir())
1130 })
1131 .choose(&mut self.rng)
1132 });
1133 let Some(worktree) = worktree else { continue };
1134 let is_dir = self.rng.gen::<bool>();
1135 let mut full_path =
1136 worktree.read_with(cx, |w, _| PathBuf::from(w.root_name()));
1137 full_path.push(gen_file_name(&mut self.rng));
1138 if !is_dir {
1139 full_path.set_extension("rs");
1140 }
1141 break ClientOperation::CreateWorktreeEntry {
1142 project_root_name,
1143 is_local,
1144 full_path,
1145 is_dir,
1146 };
1147 }
1148 },
1149 },
1150
1151 // Query and mutate buffers
1152 60..=95 => {
1153 let Some(project) = choose_random_project(client, &mut self.rng) else { continue };
1154 let project_root_name = root_name_for_project(&project, cx);
1155 let is_local = project.read_with(cx, |project, _| project.is_local());
1156
1157 match self.rng.gen_range(0..100_u32) {
1158 // Manipulate an existing buffer
1159 0..=70 => {
1160 let Some(buffer) = client
1161 .buffers_for_project(&project)
1162 .iter()
1163 .choose(&mut self.rng)
1164 .cloned() else { continue };
1165
1166 let full_path = buffer
1167 .read_with(cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
1168
1169 match self.rng.gen_range(0..100_u32) {
1170 // Close the buffer
1171 0..=15 => {
1172 break ClientOperation::CloseBuffer {
1173 project_root_name,
1174 is_local,
1175 full_path,
1176 };
1177 }
1178 // Save the buffer
1179 16..=29 if buffer.read_with(cx, |b, _| b.is_dirty()) => {
1180 let detach = self.rng.gen_bool(0.3);
1181 break ClientOperation::SaveBuffer {
1182 project_root_name,
1183 is_local,
1184 full_path,
1185 detach,
1186 };
1187 }
1188 // Edit the buffer
1189 30..=69 => {
1190 let edits = buffer.read_with(cx, |buffer, _| {
1191 buffer.get_random_edits(&mut self.rng, 3)
1192 });
1193 break ClientOperation::EditBuffer {
1194 project_root_name,
1195 is_local,
1196 full_path,
1197 edits,
1198 };
1199 }
1200 // Make an LSP request
1201 _ => {
1202 let offset = buffer.read_with(cx, |buffer, _| {
1203 buffer.clip_offset(
1204 self.rng.gen_range(0..=buffer.len()),
1205 language::Bias::Left,
1206 )
1207 });
1208 let detach = self.rng.gen();
1209 break ClientOperation::RequestLspDataInBuffer {
1210 project_root_name,
1211 full_path,
1212 offset,
1213 is_local,
1214 kind: match self.rng.gen_range(0..5_u32) {
1215 0 => LspRequestKind::Rename,
1216 1 => LspRequestKind::Highlights,
1217 2 => LspRequestKind::Definition,
1218 3 => LspRequestKind::CodeAction,
1219 4.. => LspRequestKind::Completion,
1220 },
1221 detach,
1222 };
1223 }
1224 }
1225 }
1226
1227 71..=80 => {
1228 let query = self.rng.gen_range('a'..='z').to_string();
1229 let detach = self.rng.gen_bool(0.3);
1230 break ClientOperation::SearchProject {
1231 project_root_name,
1232 query,
1233 detach,
1234 };
1235 }
1236
1237 // Open a buffer
1238 81.. => {
1239 let worktree = project.read_with(cx, |project, cx| {
1240 project
1241 .worktrees(cx)
1242 .filter(|worktree| {
1243 let worktree = worktree.read(cx);
1244 worktree.is_visible()
1245 && worktree.entries(false).any(|e| e.is_file())
1246 })
1247 .choose(&mut self.rng)
1248 });
1249 let Some(worktree) = worktree else { continue };
1250 let full_path = worktree.read_with(cx, |worktree, _| {
1251 let entry = worktree
1252 .entries(false)
1253 .filter(|e| e.is_file())
1254 .choose(&mut self.rng)
1255 .unwrap();
1256 if entry.path.as_ref() == Path::new("") {
1257 Path::new(worktree.root_name()).into()
1258 } else {
1259 Path::new(worktree.root_name()).join(&entry.path)
1260 }
1261 });
1262 break ClientOperation::OpenBuffer {
1263 project_root_name,
1264 is_local,
1265 full_path,
1266 };
1267 }
1268 }
1269 }
1270
1271 // Create a file or directory
1272 96.. => {
1273 let is_dir = self.rng.gen::<bool>();
1274 let mut path = client
1275 .fs
1276 .directories()
1277 .await
1278 .choose(&mut self.rng)
1279 .unwrap()
1280 .clone();
1281 path.push(gen_file_name(&mut self.rng));
1282 if !is_dir {
1283 path.set_extension("rs");
1284 }
1285 break ClientOperation::CreateFsEntry { path, is_dir };
1286 }
1287 }
1288 };
1289 self.operation_ix += 1;
1290 Some(operation)
1291 }
1292
1293 fn next_root_dir_name(&mut self, user_id: UserId) -> String {
1294 let user_ix = self
1295 .users
1296 .iter()
1297 .position(|user| user.user_id == user_id)
1298 .unwrap();
1299 let root_id = util::post_inc(&mut self.users[user_ix].next_root_id);
1300 format!("dir-{user_id}-{root_id}")
1301 }
1302
1303 fn user(&mut self, user_id: UserId) -> &mut UserTestPlan {
1304 let ix = self
1305 .users
1306 .iter()
1307 .position(|user| user.user_id == user_id)
1308 .unwrap();
1309 &mut self.users[ix]
1310 }
1311}
1312
1313async fn simulate_client(
1314 client: Rc<TestClient>,
1315 mut operation_rx: futures::channel::mpsc::UnboundedReceiver<()>,
1316 plan: Arc<Mutex<TestPlan>>,
1317 mut cx: TestAppContext,
1318) {
1319 // Setup language server
1320 let mut language = Language::new(
1321 LanguageConfig {
1322 name: "Rust".into(),
1323 path_suffixes: vec!["rs".to_string()],
1324 ..Default::default()
1325 },
1326 None,
1327 );
1328 let _fake_language_servers = language
1329 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1330 name: "the-fake-language-server",
1331 capabilities: lsp::LanguageServer::full_capabilities(),
1332 initializer: Some(Box::new({
1333 let plan = plan.clone();
1334 let fs = client.fs.clone();
1335 move |fake_server: &mut FakeLanguageServer| {
1336 fake_server.handle_request::<lsp::request::Completion, _, _>(
1337 |_, _| async move {
1338 Ok(Some(lsp::CompletionResponse::Array(vec![
1339 lsp::CompletionItem {
1340 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1341 range: lsp::Range::new(
1342 lsp::Position::new(0, 0),
1343 lsp::Position::new(0, 0),
1344 ),
1345 new_text: "the-new-text".to_string(),
1346 })),
1347 ..Default::default()
1348 },
1349 ])))
1350 },
1351 );
1352
1353 fake_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
1354 |_, _| async move {
1355 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
1356 lsp::CodeAction {
1357 title: "the-code-action".to_string(),
1358 ..Default::default()
1359 },
1360 )]))
1361 },
1362 );
1363
1364 fake_server.handle_request::<lsp::request::PrepareRenameRequest, _, _>(
1365 |params, _| async move {
1366 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
1367 params.position,
1368 params.position,
1369 ))))
1370 },
1371 );
1372
1373 fake_server.handle_request::<lsp::request::GotoDefinition, _, _>({
1374 let fs = fs.clone();
1375 let plan = plan.clone();
1376 move |_, _| {
1377 let fs = fs.clone();
1378 let plan = plan.clone();
1379 async move {
1380 let files = fs.files().await;
1381 let mut plan = plan.lock();
1382 let count = plan.rng.gen_range::<usize, _>(1..3);
1383 let files = (0..count)
1384 .map(|_| files.choose(&mut plan.rng).unwrap())
1385 .collect::<Vec<_>>();
1386 log::info!("LSP: Returning definitions in files {:?}", &files);
1387 Ok(Some(lsp::GotoDefinitionResponse::Array(
1388 files
1389 .into_iter()
1390 .map(|file| lsp::Location {
1391 uri: lsp::Url::from_file_path(file).unwrap(),
1392 range: Default::default(),
1393 })
1394 .collect(),
1395 )))
1396 }
1397 }
1398 });
1399
1400 fake_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>({
1401 let plan = plan.clone();
1402 move |_, _| {
1403 let mut highlights = Vec::new();
1404 let highlight_count = plan.lock().rng.gen_range(1..=5);
1405 for _ in 0..highlight_count {
1406 let start_row = plan.lock().rng.gen_range(0..100);
1407 let start_column = plan.lock().rng.gen_range(0..100);
1408 let start = PointUtf16::new(start_row, start_column);
1409 let end_row = plan.lock().rng.gen_range(0..100);
1410 let end_column = plan.lock().rng.gen_range(0..100);
1411 let end = PointUtf16::new(end_row, end_column);
1412 let range = if start > end { end..start } else { start..end };
1413 highlights.push(lsp::DocumentHighlight {
1414 range: range_to_lsp(range.clone()),
1415 kind: Some(lsp::DocumentHighlightKind::READ),
1416 });
1417 }
1418 highlights.sort_unstable_by_key(|highlight| {
1419 (highlight.range.start, highlight.range.end)
1420 });
1421 async move { Ok(Some(highlights)) }
1422 }
1423 });
1424 }
1425 })),
1426 ..Default::default()
1427 }))
1428 .await;
1429 client.language_registry.add(Arc::new(language));
1430
1431 while operation_rx.next().await.is_some() {
1432 let Some(operation) = plan.lock().next_client_operation(&client, &cx).await else { break };
1433 if let Err(error) = apply_client_operation(&client, operation, &mut cx).await {
1434 log::error!("{} error: {}", client.username, error);
1435 }
1436 cx.background().simulate_random_delay().await;
1437 }
1438 log::info!("{}: done", client.username);
1439}
1440
1441fn buffer_for_full_path(
1442 buffers: &HashSet<ModelHandle<language::Buffer>>,
1443 full_path: &PathBuf,
1444 cx: &TestAppContext,
1445) -> Option<ModelHandle<language::Buffer>> {
1446 buffers
1447 .iter()
1448 .find(|buffer| {
1449 buffer.read_with(cx, |buffer, cx| {
1450 buffer.file().unwrap().full_path(cx) == *full_path
1451 })
1452 })
1453 .cloned()
1454}
1455
1456fn project_for_root_name(
1457 client: &TestClient,
1458 root_name: &str,
1459 cx: &TestAppContext,
1460) -> Option<ModelHandle<Project>> {
1461 if let Some(ix) = project_ix_for_root_name(&*client.local_projects(), root_name, cx) {
1462 return Some(client.local_projects()[ix].clone());
1463 }
1464 if let Some(ix) = project_ix_for_root_name(&*client.remote_projects(), root_name, cx) {
1465 return Some(client.remote_projects()[ix].clone());
1466 }
1467 None
1468}
1469
1470fn project_ix_for_root_name(
1471 projects: &[ModelHandle<Project>],
1472 root_name: &str,
1473 cx: &TestAppContext,
1474) -> Option<usize> {
1475 projects.iter().position(|project| {
1476 project.read_with(cx, |project, cx| {
1477 let worktree = project.visible_worktrees(cx).next().unwrap();
1478 worktree.read(cx).root_name() == root_name
1479 })
1480 })
1481}
1482
1483fn root_name_for_project(project: &ModelHandle<Project>, cx: &TestAppContext) -> String {
1484 project.read_with(cx, |project, cx| {
1485 project
1486 .visible_worktrees(cx)
1487 .next()
1488 .unwrap()
1489 .read(cx)
1490 .root_name()
1491 .to_string()
1492 })
1493}
1494
1495fn project_path_for_full_path(
1496 project: &ModelHandle<Project>,
1497 full_path: &Path,
1498 cx: &TestAppContext,
1499) -> Option<ProjectPath> {
1500 let mut components = full_path.components();
1501 let root_name = components.next().unwrap().as_os_str().to_str().unwrap();
1502 let path = components.as_path().into();
1503 let worktree_id = project.read_with(cx, |project, cx| {
1504 project.worktrees(cx).find_map(|worktree| {
1505 let worktree = worktree.read(cx);
1506 if worktree.root_name() == root_name {
1507 Some(worktree.id())
1508 } else {
1509 None
1510 }
1511 })
1512 })?;
1513 Some(ProjectPath { worktree_id, path })
1514}
1515
1516async fn ensure_project_shared(
1517 project: &ModelHandle<Project>,
1518 client: &TestClient,
1519 cx: &mut TestAppContext,
1520) {
1521 let first_root_name = root_name_for_project(project, cx);
1522 let active_call = cx.read(ActiveCall::global);
1523 if active_call.read_with(cx, |call, _| call.room().is_some())
1524 && project.read_with(cx, |project, _| project.is_local() && !project.is_shared())
1525 {
1526 match active_call
1527 .update(cx, |call, cx| call.share_project(project.clone(), cx))
1528 .await
1529 {
1530 Ok(project_id) => {
1531 log::info!(
1532 "{}: shared project {} with id {}",
1533 client.username,
1534 first_root_name,
1535 project_id
1536 );
1537 }
1538 Err(error) => {
1539 log::error!(
1540 "{}: error sharing project {}: {:?}",
1541 client.username,
1542 first_root_name,
1543 error
1544 );
1545 }
1546 }
1547 }
1548}
1549
1550fn choose_random_project(client: &TestClient, rng: &mut StdRng) -> Option<ModelHandle<Project>> {
1551 client
1552 .local_projects()
1553 .iter()
1554 .chain(client.remote_projects().iter())
1555 .choose(rng)
1556 .cloned()
1557}
1558
1559fn gen_file_name(rng: &mut StdRng) -> String {
1560 let mut name = String::new();
1561 for _ in 0..10 {
1562 let letter = rng.gen_range('a'..='z');
1563 name.push(letter);
1564 }
1565 name
1566}