1use crate::{
2 rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
3 tests::{channel_id, room_participants, RoomParticipants, TestClient, TestServer},
4};
5use call::{room, ActiveCall, ParticipantLocation, Room};
6use client::{User, RECEIVE_TIMEOUT};
7use collections::{HashMap, HashSet};
8use fs::{repository::GitFileStatus, FakeFs, Fs as _, RemoveOptions};
9use futures::StreamExt as _;
10use gpui::{AppContext, BackgroundExecutor, Model, TestAppContext};
11use language::{
12 language_settings::{AllLanguageSettings, Formatter},
13 tree_sitter_rust, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, LanguageConfig,
14 LineEnding, OffsetRangeExt, Point, Rope,
15};
16use live_kit_client::MacOSDisplay;
17use lsp::LanguageServerId;
18use project::{
19 search::SearchQuery, DiagnosticSummary, FormatTrigger, HoverBlockKind, Project, ProjectPath,
20};
21use rand::prelude::*;
22use rpc::proto::ChannelRole;
23use serde_json::json;
24use settings::SettingsStore;
25use std::{
26 cell::{Cell, RefCell},
27 env, future, mem,
28 path::{Path, PathBuf},
29 rc::Rc,
30 sync::{
31 atomic::{AtomicBool, Ordering::SeqCst},
32 Arc,
33 },
34};
35use unindent::Unindent as _;
36
37#[ctor::ctor]
38fn init_logger() {
39 if std::env::var("RUST_LOG").is_ok() {
40 env_logger::init();
41 }
42}
43
44#[gpui::test(iterations = 10)]
45async fn test_basic_calls(
46 executor: BackgroundExecutor,
47 cx_a: &mut TestAppContext,
48 cx_b: &mut TestAppContext,
49 cx_b2: &mut TestAppContext,
50 cx_c: &mut TestAppContext,
51) {
52 let mut server = TestServer::start(executor.clone()).await;
53
54 let client_a = server.create_client(cx_a, "user_a").await;
55 let client_b = server.create_client(cx_b, "user_b").await;
56 let client_c = server.create_client(cx_c, "user_c").await;
57 server
58 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
59 .await;
60
61 let active_call_a = cx_a.read(ActiveCall::global);
62 let active_call_b = cx_b.read(ActiveCall::global);
63 let active_call_c = cx_c.read(ActiveCall::global);
64
65 // Call user B from client A.
66 active_call_a
67 .update(cx_a, |call, cx| {
68 call.invite(client_b.user_id().unwrap(), None, cx)
69 })
70 .await
71 .unwrap();
72 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
73 executor.run_until_parked();
74 assert_eq!(
75 room_participants(&room_a, cx_a),
76 RoomParticipants {
77 remote: Default::default(),
78 pending: vec!["user_b".to_string()]
79 }
80 );
81
82 // User B receives the call.
83
84 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
85 let call_b = incoming_call_b.next().await.unwrap().unwrap();
86 assert_eq!(call_b.calling_user.github_login, "user_a");
87
88 // User B connects via another client and also receives a ring on the newly-connected client.
89 let _client_b2 = server.create_client(cx_b2, "user_b").await;
90 let active_call_b2 = cx_b2.read(ActiveCall::global);
91
92 let mut incoming_call_b2 = active_call_b2.read_with(cx_b2, |call, _| call.incoming());
93 executor.run_until_parked();
94 let call_b2 = incoming_call_b2.next().await.unwrap().unwrap();
95 assert_eq!(call_b2.calling_user.github_login, "user_a");
96
97 // User B joins the room using the first client.
98 active_call_b
99 .update(cx_b, |call, cx| call.accept_incoming(cx))
100 .await
101 .unwrap();
102
103 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
104 assert!(incoming_call_b.next().await.unwrap().is_none());
105
106 executor.run_until_parked();
107 assert_eq!(
108 room_participants(&room_a, cx_a),
109 RoomParticipants {
110 remote: vec!["user_b".to_string()],
111 pending: Default::default()
112 }
113 );
114 assert_eq!(
115 room_participants(&room_b, cx_b),
116 RoomParticipants {
117 remote: vec!["user_a".to_string()],
118 pending: Default::default()
119 }
120 );
121
122 // Call user C from client B.
123
124 let mut incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming());
125 active_call_b
126 .update(cx_b, |call, cx| {
127 call.invite(client_c.user_id().unwrap(), None, cx)
128 })
129 .await
130 .unwrap();
131
132 executor.run_until_parked();
133 assert_eq!(
134 room_participants(&room_a, cx_a),
135 RoomParticipants {
136 remote: vec!["user_b".to_string()],
137 pending: vec!["user_c".to_string()]
138 }
139 );
140 assert_eq!(
141 room_participants(&room_b, cx_b),
142 RoomParticipants {
143 remote: vec!["user_a".to_string()],
144 pending: vec!["user_c".to_string()]
145 }
146 );
147
148 // User C receives the call, but declines it.
149 let call_c = incoming_call_c.next().await.unwrap().unwrap();
150 assert_eq!(call_c.calling_user.github_login, "user_b");
151 active_call_c.update(cx_c, |call, cx| call.decline_incoming(cx).unwrap());
152 assert!(incoming_call_c.next().await.unwrap().is_none());
153
154 executor.run_until_parked();
155 assert_eq!(
156 room_participants(&room_a, cx_a),
157 RoomParticipants {
158 remote: vec!["user_b".to_string()],
159 pending: Default::default()
160 }
161 );
162 assert_eq!(
163 room_participants(&room_b, cx_b),
164 RoomParticipants {
165 remote: vec!["user_a".to_string()],
166 pending: Default::default()
167 }
168 );
169
170 // Call user C again from user A.
171 active_call_a
172 .update(cx_a, |call, cx| {
173 call.invite(client_c.user_id().unwrap(), None, cx)
174 })
175 .await
176 .unwrap();
177
178 executor.run_until_parked();
179 assert_eq!(
180 room_participants(&room_a, cx_a),
181 RoomParticipants {
182 remote: vec!["user_b".to_string()],
183 pending: vec!["user_c".to_string()]
184 }
185 );
186 assert_eq!(
187 room_participants(&room_b, cx_b),
188 RoomParticipants {
189 remote: vec!["user_a".to_string()],
190 pending: vec!["user_c".to_string()]
191 }
192 );
193
194 // User C accepts the call.
195 let call_c = incoming_call_c.next().await.unwrap().unwrap();
196 assert_eq!(call_c.calling_user.github_login, "user_a");
197 active_call_c
198 .update(cx_c, |call, cx| call.accept_incoming(cx))
199 .await
200 .unwrap();
201 assert!(incoming_call_c.next().await.unwrap().is_none());
202
203 let room_c = active_call_c.read_with(cx_c, |call, _| call.room().unwrap().clone());
204
205 executor.run_until_parked();
206 assert_eq!(
207 room_participants(&room_a, cx_a),
208 RoomParticipants {
209 remote: vec!["user_b".to_string(), "user_c".to_string()],
210 pending: Default::default()
211 }
212 );
213 assert_eq!(
214 room_participants(&room_b, cx_b),
215 RoomParticipants {
216 remote: vec!["user_a".to_string(), "user_c".to_string()],
217 pending: Default::default()
218 }
219 );
220 assert_eq!(
221 room_participants(&room_c, cx_c),
222 RoomParticipants {
223 remote: vec!["user_a".to_string(), "user_b".to_string()],
224 pending: Default::default()
225 }
226 );
227
228 // User A shares their screen
229 let display = MacOSDisplay::new();
230 let events_b = active_call_events(cx_b);
231 let events_c = active_call_events(cx_c);
232 active_call_a
233 .update(cx_a, |call, cx| {
234 call.room().unwrap().update(cx, |room, cx| {
235 room.set_display_sources(vec![display.clone()]);
236 room.share_screen(cx)
237 })
238 })
239 .await
240 .unwrap();
241
242 executor.run_until_parked();
243
244 // User B observes the remote screen sharing track.
245 assert_eq!(events_b.borrow().len(), 1);
246 let event_b = events_b.borrow().first().unwrap().clone();
247 if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event_b {
248 assert_eq!(participant_id, client_a.peer_id().unwrap());
249
250 room_b.read_with(cx_b, |room, _| {
251 assert_eq!(
252 room.remote_participants()[&client_a.user_id().unwrap()]
253 .video_tracks
254 .len(),
255 1
256 );
257 });
258 } else {
259 panic!("unexpected event")
260 }
261
262 // User C observes the remote screen sharing track.
263 assert_eq!(events_c.borrow().len(), 1);
264 let event_c = events_c.borrow().first().unwrap().clone();
265 if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event_c {
266 assert_eq!(participant_id, client_a.peer_id().unwrap());
267
268 room_c.read_with(cx_c, |room, _| {
269 assert_eq!(
270 room.remote_participants()[&client_a.user_id().unwrap()]
271 .video_tracks
272 .len(),
273 1
274 );
275 });
276 } else {
277 panic!("unexpected event")
278 }
279
280 // User A leaves the room.
281 active_call_a
282 .update(cx_a, |call, cx| {
283 let hang_up = call.hang_up(cx);
284 assert!(call.room().is_none());
285 hang_up
286 })
287 .await
288 .unwrap();
289 executor.run_until_parked();
290 assert_eq!(
291 room_participants(&room_a, cx_a),
292 RoomParticipants {
293 remote: Default::default(),
294 pending: Default::default()
295 }
296 );
297 assert_eq!(
298 room_participants(&room_b, cx_b),
299 RoomParticipants {
300 remote: vec!["user_c".to_string()],
301 pending: Default::default()
302 }
303 );
304 assert_eq!(
305 room_participants(&room_c, cx_c),
306 RoomParticipants {
307 remote: vec!["user_b".to_string()],
308 pending: Default::default()
309 }
310 );
311
312 // User B gets disconnected from the LiveKit server, which causes them
313 // to automatically leave the room. User C leaves the room as well because
314 // nobody else is in there.
315 server
316 .test_live_kit_server
317 .disconnect_client(client_b.user_id().unwrap().to_string())
318 .await;
319 executor.run_until_parked();
320
321 active_call_b.read_with(cx_b, |call, _| assert!(call.room().is_none()));
322
323 active_call_c.read_with(cx_c, |call, _| assert!(call.room().is_none()));
324 assert_eq!(
325 room_participants(&room_a, cx_a),
326 RoomParticipants {
327 remote: Default::default(),
328 pending: Default::default()
329 }
330 );
331 assert_eq!(
332 room_participants(&room_b, cx_b),
333 RoomParticipants {
334 remote: Default::default(),
335 pending: Default::default()
336 }
337 );
338 assert_eq!(
339 room_participants(&room_c, cx_c),
340 RoomParticipants {
341 remote: Default::default(),
342 pending: Default::default()
343 }
344 );
345}
346
347#[gpui::test(iterations = 10)]
348async fn test_calling_multiple_users_simultaneously(
349 executor: BackgroundExecutor,
350 cx_a: &mut TestAppContext,
351 cx_b: &mut TestAppContext,
352 cx_c: &mut TestAppContext,
353 cx_d: &mut TestAppContext,
354) {
355 let mut server = TestServer::start(executor.clone()).await;
356
357 let client_a = server.create_client(cx_a, "user_a").await;
358 let client_b = server.create_client(cx_b, "user_b").await;
359 let client_c = server.create_client(cx_c, "user_c").await;
360 let client_d = server.create_client(cx_d, "user_d").await;
361 server
362 .make_contacts(&mut [
363 (&client_a, cx_a),
364 (&client_b, cx_b),
365 (&client_c, cx_c),
366 (&client_d, cx_d),
367 ])
368 .await;
369
370 let active_call_a = cx_a.read(ActiveCall::global);
371 let active_call_b = cx_b.read(ActiveCall::global);
372 let active_call_c = cx_c.read(ActiveCall::global);
373 let active_call_d = cx_d.read(ActiveCall::global);
374
375 // Simultaneously call user B and user C from client A.
376 let b_invite = active_call_a.update(cx_a, |call, cx| {
377 call.invite(client_b.user_id().unwrap(), None, cx)
378 });
379 let c_invite = active_call_a.update(cx_a, |call, cx| {
380 call.invite(client_c.user_id().unwrap(), None, cx)
381 });
382 b_invite.await.unwrap();
383 c_invite.await.unwrap();
384
385 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
386 executor.run_until_parked();
387 assert_eq!(
388 room_participants(&room_a, cx_a),
389 RoomParticipants {
390 remote: Default::default(),
391 pending: vec!["user_b".to_string(), "user_c".to_string()]
392 }
393 );
394
395 // Call client D from client A.
396 active_call_a
397 .update(cx_a, |call, cx| {
398 call.invite(client_d.user_id().unwrap(), None, cx)
399 })
400 .await
401 .unwrap();
402 executor.run_until_parked();
403 assert_eq!(
404 room_participants(&room_a, cx_a),
405 RoomParticipants {
406 remote: Default::default(),
407 pending: vec![
408 "user_b".to_string(),
409 "user_c".to_string(),
410 "user_d".to_string()
411 ]
412 }
413 );
414
415 // Accept the call on all clients simultaneously.
416 let accept_b = active_call_b.update(cx_b, |call, cx| call.accept_incoming(cx));
417 let accept_c = active_call_c.update(cx_c, |call, cx| call.accept_incoming(cx));
418 let accept_d = active_call_d.update(cx_d, |call, cx| call.accept_incoming(cx));
419 accept_b.await.unwrap();
420 accept_c.await.unwrap();
421 accept_d.await.unwrap();
422
423 executor.run_until_parked();
424
425 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
426
427 let room_c = active_call_c.read_with(cx_c, |call, _| call.room().unwrap().clone());
428
429 let room_d = active_call_d.read_with(cx_d, |call, _| call.room().unwrap().clone());
430 assert_eq!(
431 room_participants(&room_a, cx_a),
432 RoomParticipants {
433 remote: vec![
434 "user_b".to_string(),
435 "user_c".to_string(),
436 "user_d".to_string(),
437 ],
438 pending: Default::default()
439 }
440 );
441 assert_eq!(
442 room_participants(&room_b, cx_b),
443 RoomParticipants {
444 remote: vec![
445 "user_a".to_string(),
446 "user_c".to_string(),
447 "user_d".to_string(),
448 ],
449 pending: Default::default()
450 }
451 );
452 assert_eq!(
453 room_participants(&room_c, cx_c),
454 RoomParticipants {
455 remote: vec![
456 "user_a".to_string(),
457 "user_b".to_string(),
458 "user_d".to_string(),
459 ],
460 pending: Default::default()
461 }
462 );
463 assert_eq!(
464 room_participants(&room_d, cx_d),
465 RoomParticipants {
466 remote: vec![
467 "user_a".to_string(),
468 "user_b".to_string(),
469 "user_c".to_string(),
470 ],
471 pending: Default::default()
472 }
473 );
474}
475
476#[gpui::test(iterations = 10)]
477async fn test_joining_channels_and_calling_multiple_users_simultaneously(
478 executor: BackgroundExecutor,
479 cx_a: &mut TestAppContext,
480 cx_b: &mut TestAppContext,
481 cx_c: &mut TestAppContext,
482) {
483 let mut server = TestServer::start(executor.clone()).await;
484
485 let client_a = server.create_client(cx_a, "user_a").await;
486 let client_b = server.create_client(cx_b, "user_b").await;
487 let client_c = server.create_client(cx_c, "user_c").await;
488 server
489 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
490 .await;
491
492 let channel_1 = server
493 .make_channel(
494 "channel1",
495 None,
496 (&client_a, cx_a),
497 &mut [(&client_b, cx_b), (&client_c, cx_c)],
498 )
499 .await;
500
501 let channel_2 = server
502 .make_channel(
503 "channel2",
504 None,
505 (&client_a, cx_a),
506 &mut [(&client_b, cx_b), (&client_c, cx_c)],
507 )
508 .await;
509
510 let active_call_a = cx_a.read(ActiveCall::global);
511
512 // Simultaneously join channel 1 and then channel 2
513 active_call_a
514 .update(cx_a, |call, cx| call.join_channel(channel_1, cx))
515 .detach();
516 let join_channel_2 = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_2, cx));
517
518 join_channel_2.await.unwrap();
519
520 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
521 executor.run_until_parked();
522
523 assert_eq!(channel_id(&room_a, cx_a), Some(channel_2));
524
525 // Leave the room
526 active_call_a
527 .update(cx_a, |call, cx| {
528 let hang_up = call.hang_up(cx);
529 hang_up
530 })
531 .await
532 .unwrap();
533
534 // Initiating invites and then joining a channel should fail gracefully
535 let b_invite = active_call_a.update(cx_a, |call, cx| {
536 call.invite(client_b.user_id().unwrap(), None, cx)
537 });
538 let c_invite = active_call_a.update(cx_a, |call, cx| {
539 call.invite(client_c.user_id().unwrap(), None, cx)
540 });
541
542 let join_channel = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, cx));
543
544 b_invite.await.unwrap();
545 c_invite.await.unwrap();
546 join_channel.await.unwrap();
547
548 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
549 executor.run_until_parked();
550
551 assert_eq!(
552 room_participants(&room_a, cx_a),
553 RoomParticipants {
554 remote: Default::default(),
555 pending: vec!["user_b".to_string(), "user_c".to_string()]
556 }
557 );
558
559 assert_eq!(channel_id(&room_a, cx_a), None);
560
561 // Leave the room
562 active_call_a
563 .update(cx_a, |call, cx| {
564 let hang_up = call.hang_up(cx);
565 hang_up
566 })
567 .await
568 .unwrap();
569
570 // Simultaneously join channel 1 and call user B and user C from client A.
571 let join_channel = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, cx));
572
573 let b_invite = active_call_a.update(cx_a, |call, cx| {
574 call.invite(client_b.user_id().unwrap(), None, cx)
575 });
576 let c_invite = active_call_a.update(cx_a, |call, cx| {
577 call.invite(client_c.user_id().unwrap(), None, cx)
578 });
579
580 join_channel.await.unwrap();
581 b_invite.await.unwrap();
582 c_invite.await.unwrap();
583
584 active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
585 executor.run_until_parked();
586}
587
588#[gpui::test(iterations = 10)]
589async fn test_room_uniqueness(
590 executor: BackgroundExecutor,
591 cx_a: &mut TestAppContext,
592 cx_a2: &mut TestAppContext,
593 cx_b: &mut TestAppContext,
594 cx_b2: &mut TestAppContext,
595 cx_c: &mut TestAppContext,
596) {
597 let mut server = TestServer::start(executor.clone()).await;
598 let client_a = server.create_client(cx_a, "user_a").await;
599 let _client_a2 = server.create_client(cx_a2, "user_a").await;
600 let client_b = server.create_client(cx_b, "user_b").await;
601 let _client_b2 = server.create_client(cx_b2, "user_b").await;
602 let client_c = server.create_client(cx_c, "user_c").await;
603 server
604 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
605 .await;
606
607 let active_call_a = cx_a.read(ActiveCall::global);
608 let active_call_a2 = cx_a2.read(ActiveCall::global);
609 let active_call_b = cx_b.read(ActiveCall::global);
610 let active_call_b2 = cx_b2.read(ActiveCall::global);
611 let active_call_c = cx_c.read(ActiveCall::global);
612
613 // Call user B from client A.
614 active_call_a
615 .update(cx_a, |call, cx| {
616 call.invite(client_b.user_id().unwrap(), None, cx)
617 })
618 .await
619 .unwrap();
620
621 // Ensure a new room can't be created given user A just created one.
622 active_call_a2
623 .update(cx_a2, |call, cx| {
624 call.invite(client_c.user_id().unwrap(), None, cx)
625 })
626 .await
627 .unwrap_err();
628
629 active_call_a2.read_with(cx_a2, |call, _| assert!(call.room().is_none()));
630
631 // User B receives the call from user A.
632
633 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
634 let call_b1 = incoming_call_b.next().await.unwrap().unwrap();
635 assert_eq!(call_b1.calling_user.github_login, "user_a");
636
637 // Ensure calling users A and B from client C fails.
638 active_call_c
639 .update(cx_c, |call, cx| {
640 call.invite(client_a.user_id().unwrap(), None, cx)
641 })
642 .await
643 .unwrap_err();
644 active_call_c
645 .update(cx_c, |call, cx| {
646 call.invite(client_b.user_id().unwrap(), None, cx)
647 })
648 .await
649 .unwrap_err();
650
651 // Ensure User B can't create a room while they still have an incoming call.
652 active_call_b2
653 .update(cx_b2, |call, cx| {
654 call.invite(client_c.user_id().unwrap(), None, cx)
655 })
656 .await
657 .unwrap_err();
658
659 active_call_b2.read_with(cx_b2, |call, _| assert!(call.room().is_none()));
660
661 // User B joins the room and calling them after they've joined still fails.
662 active_call_b
663 .update(cx_b, |call, cx| call.accept_incoming(cx))
664 .await
665 .unwrap();
666 active_call_c
667 .update(cx_c, |call, cx| {
668 call.invite(client_b.user_id().unwrap(), None, cx)
669 })
670 .await
671 .unwrap_err();
672
673 // Ensure User B can't create a room while they belong to another room.
674 active_call_b2
675 .update(cx_b2, |call, cx| {
676 call.invite(client_c.user_id().unwrap(), None, cx)
677 })
678 .await
679 .unwrap_err();
680
681 active_call_b2.read_with(cx_b2, |call, _| assert!(call.room().is_none()));
682
683 // Client C can successfully call client B after client B leaves the room.
684 active_call_b
685 .update(cx_b, |call, cx| call.hang_up(cx))
686 .await
687 .unwrap();
688 executor.run_until_parked();
689 active_call_c
690 .update(cx_c, |call, cx| {
691 call.invite(client_b.user_id().unwrap(), None, cx)
692 })
693 .await
694 .unwrap();
695 executor.run_until_parked();
696 let call_b2 = incoming_call_b.next().await.unwrap().unwrap();
697 assert_eq!(call_b2.calling_user.github_login, "user_c");
698}
699
700#[gpui::test(iterations = 10)]
701async fn test_client_disconnecting_from_room(
702 executor: BackgroundExecutor,
703 cx_a: &mut TestAppContext,
704 cx_b: &mut TestAppContext,
705) {
706 let mut server = TestServer::start(executor.clone()).await;
707 let client_a = server.create_client(cx_a, "user_a").await;
708 let client_b = server.create_client(cx_b, "user_b").await;
709 server
710 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)])
711 .await;
712
713 let active_call_a = cx_a.read(ActiveCall::global);
714 let active_call_b = cx_b.read(ActiveCall::global);
715
716 // Call user B from client A.
717 active_call_a
718 .update(cx_a, |call, cx| {
719 call.invite(client_b.user_id().unwrap(), None, cx)
720 })
721 .await
722 .unwrap();
723
724 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
725
726 // User B receives the call and joins the room.
727
728 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
729 incoming_call_b.next().await.unwrap().unwrap();
730 active_call_b
731 .update(cx_b, |call, cx| call.accept_incoming(cx))
732 .await
733 .unwrap();
734
735 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
736 executor.run_until_parked();
737 assert_eq!(
738 room_participants(&room_a, cx_a),
739 RoomParticipants {
740 remote: vec!["user_b".to_string()],
741 pending: Default::default()
742 }
743 );
744 assert_eq!(
745 room_participants(&room_b, cx_b),
746 RoomParticipants {
747 remote: vec!["user_a".to_string()],
748 pending: Default::default()
749 }
750 );
751
752 // User A automatically reconnects to the room upon disconnection.
753 server.disconnect_client(client_a.peer_id().unwrap());
754 executor.advance_clock(RECEIVE_TIMEOUT);
755 executor.run_until_parked();
756 assert_eq!(
757 room_participants(&room_a, cx_a),
758 RoomParticipants {
759 remote: vec!["user_b".to_string()],
760 pending: Default::default()
761 }
762 );
763 assert_eq!(
764 room_participants(&room_b, cx_b),
765 RoomParticipants {
766 remote: vec!["user_a".to_string()],
767 pending: Default::default()
768 }
769 );
770
771 // When user A disconnects, both client A and B clear their room on the active call.
772 server.forbid_connections();
773 server.disconnect_client(client_a.peer_id().unwrap());
774 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
775
776 active_call_a.read_with(cx_a, |call, _| assert!(call.room().is_none()));
777
778 active_call_b.read_with(cx_b, |call, _| assert!(call.room().is_none()));
779 assert_eq!(
780 room_participants(&room_a, cx_a),
781 RoomParticipants {
782 remote: Default::default(),
783 pending: Default::default()
784 }
785 );
786 assert_eq!(
787 room_participants(&room_b, cx_b),
788 RoomParticipants {
789 remote: Default::default(),
790 pending: Default::default()
791 }
792 );
793
794 // Allow user A to reconnect to the server.
795 server.allow_connections();
796 executor.advance_clock(RECEIVE_TIMEOUT);
797
798 // Call user B again from client A.
799 active_call_a
800 .update(cx_a, |call, cx| {
801 call.invite(client_b.user_id().unwrap(), None, cx)
802 })
803 .await
804 .unwrap();
805
806 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
807
808 // User B receives the call and joins the room.
809
810 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
811 incoming_call_b.next().await.unwrap().unwrap();
812 active_call_b
813 .update(cx_b, |call, cx| call.accept_incoming(cx))
814 .await
815 .unwrap();
816
817 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
818 executor.run_until_parked();
819 assert_eq!(
820 room_participants(&room_a, cx_a),
821 RoomParticipants {
822 remote: vec!["user_b".to_string()],
823 pending: Default::default()
824 }
825 );
826 assert_eq!(
827 room_participants(&room_b, cx_b),
828 RoomParticipants {
829 remote: vec!["user_a".to_string()],
830 pending: Default::default()
831 }
832 );
833
834 // User B gets disconnected from the LiveKit server, which causes it
835 // to automatically leave the room.
836 server
837 .test_live_kit_server
838 .disconnect_client(client_b.user_id().unwrap().to_string())
839 .await;
840 executor.run_until_parked();
841 active_call_a.update(cx_a, |call, _| assert!(call.room().is_none()));
842 active_call_b.update(cx_b, |call, _| assert!(call.room().is_none()));
843 assert_eq!(
844 room_participants(&room_a, cx_a),
845 RoomParticipants {
846 remote: Default::default(),
847 pending: Default::default()
848 }
849 );
850 assert_eq!(
851 room_participants(&room_b, cx_b),
852 RoomParticipants {
853 remote: Default::default(),
854 pending: Default::default()
855 }
856 );
857}
858
859#[gpui::test(iterations = 10)]
860async fn test_server_restarts(
861 executor: BackgroundExecutor,
862 cx_a: &mut TestAppContext,
863 cx_b: &mut TestAppContext,
864 cx_c: &mut TestAppContext,
865 cx_d: &mut TestAppContext,
866) {
867 let mut server = TestServer::start(executor.clone()).await;
868 let client_a = server.create_client(cx_a, "user_a").await;
869 client_a
870 .fs()
871 .insert_tree("/a", json!({ "a.txt": "a-contents" }))
872 .await;
873
874 // Invite client B to collaborate on a project
875 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
876
877 let client_b = server.create_client(cx_b, "user_b").await;
878 let client_c = server.create_client(cx_c, "user_c").await;
879 let client_d = server.create_client(cx_d, "user_d").await;
880 server
881 .make_contacts(&mut [
882 (&client_a, cx_a),
883 (&client_b, cx_b),
884 (&client_c, cx_c),
885 (&client_d, cx_d),
886 ])
887 .await;
888
889 let active_call_a = cx_a.read(ActiveCall::global);
890 let active_call_b = cx_b.read(ActiveCall::global);
891 let active_call_c = cx_c.read(ActiveCall::global);
892 let active_call_d = cx_d.read(ActiveCall::global);
893
894 // User A calls users B, C, and D.
895 active_call_a
896 .update(cx_a, |call, cx| {
897 call.invite(client_b.user_id().unwrap(), Some(project_a.clone()), cx)
898 })
899 .await
900 .unwrap();
901 active_call_a
902 .update(cx_a, |call, cx| {
903 call.invite(client_c.user_id().unwrap(), Some(project_a.clone()), cx)
904 })
905 .await
906 .unwrap();
907 active_call_a
908 .update(cx_a, |call, cx| {
909 call.invite(client_d.user_id().unwrap(), Some(project_a.clone()), cx)
910 })
911 .await
912 .unwrap();
913
914 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
915
916 // User B receives the call and joins the room.
917
918 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
919 assert!(incoming_call_b.next().await.unwrap().is_some());
920 active_call_b
921 .update(cx_b, |call, cx| call.accept_incoming(cx))
922 .await
923 .unwrap();
924
925 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
926
927 // User C receives the call and joins the room.
928
929 let mut incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming());
930 assert!(incoming_call_c.next().await.unwrap().is_some());
931 active_call_c
932 .update(cx_c, |call, cx| call.accept_incoming(cx))
933 .await
934 .unwrap();
935
936 let room_c = active_call_c.read_with(cx_c, |call, _| call.room().unwrap().clone());
937
938 // User D receives the call but doesn't join the room yet.
939
940 let mut incoming_call_d = active_call_d.read_with(cx_d, |call, _| call.incoming());
941 assert!(incoming_call_d.next().await.unwrap().is_some());
942
943 executor.run_until_parked();
944 assert_eq!(
945 room_participants(&room_a, cx_a),
946 RoomParticipants {
947 remote: vec!["user_b".to_string(), "user_c".to_string()],
948 pending: vec!["user_d".to_string()]
949 }
950 );
951 assert_eq!(
952 room_participants(&room_b, cx_b),
953 RoomParticipants {
954 remote: vec!["user_a".to_string(), "user_c".to_string()],
955 pending: vec!["user_d".to_string()]
956 }
957 );
958 assert_eq!(
959 room_participants(&room_c, cx_c),
960 RoomParticipants {
961 remote: vec!["user_a".to_string(), "user_b".to_string()],
962 pending: vec!["user_d".to_string()]
963 }
964 );
965
966 // The server is torn down.
967 server.reset().await;
968
969 // Users A and B reconnect to the call. User C has troubles reconnecting, so it leaves the room.
970 client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
971 executor.advance_clock(RECONNECT_TIMEOUT);
972 assert_eq!(
973 room_participants(&room_a, cx_a),
974 RoomParticipants {
975 remote: vec!["user_b".to_string(), "user_c".to_string()],
976 pending: vec!["user_d".to_string()]
977 }
978 );
979 assert_eq!(
980 room_participants(&room_b, cx_b),
981 RoomParticipants {
982 remote: vec!["user_a".to_string(), "user_c".to_string()],
983 pending: vec!["user_d".to_string()]
984 }
985 );
986 assert_eq!(
987 room_participants(&room_c, cx_c),
988 RoomParticipants {
989 remote: vec![],
990 pending: vec![]
991 }
992 );
993
994 // User D is notified again of the incoming call and accepts it.
995 assert!(incoming_call_d.next().await.unwrap().is_some());
996 active_call_d
997 .update(cx_d, |call, cx| call.accept_incoming(cx))
998 .await
999 .unwrap();
1000 executor.run_until_parked();
1001
1002 let room_d = active_call_d.read_with(cx_d, |call, _| call.room().unwrap().clone());
1003 assert_eq!(
1004 room_participants(&room_a, cx_a),
1005 RoomParticipants {
1006 remote: vec![
1007 "user_b".to_string(),
1008 "user_c".to_string(),
1009 "user_d".to_string(),
1010 ],
1011 pending: vec![]
1012 }
1013 );
1014 assert_eq!(
1015 room_participants(&room_b, cx_b),
1016 RoomParticipants {
1017 remote: vec![
1018 "user_a".to_string(),
1019 "user_c".to_string(),
1020 "user_d".to_string(),
1021 ],
1022 pending: vec![]
1023 }
1024 );
1025 assert_eq!(
1026 room_participants(&room_c, cx_c),
1027 RoomParticipants {
1028 remote: vec![],
1029 pending: vec![]
1030 }
1031 );
1032 assert_eq!(
1033 room_participants(&room_d, cx_d),
1034 RoomParticipants {
1035 remote: vec![
1036 "user_a".to_string(),
1037 "user_b".to_string(),
1038 "user_c".to_string(),
1039 ],
1040 pending: vec![]
1041 }
1042 );
1043
1044 // The server finishes restarting, cleaning up stale connections.
1045 server.start().await.unwrap();
1046 executor.advance_clock(CLEANUP_TIMEOUT);
1047 assert_eq!(
1048 room_participants(&room_a, cx_a),
1049 RoomParticipants {
1050 remote: vec!["user_b".to_string(), "user_d".to_string()],
1051 pending: vec![]
1052 }
1053 );
1054 assert_eq!(
1055 room_participants(&room_b, cx_b),
1056 RoomParticipants {
1057 remote: vec!["user_a".to_string(), "user_d".to_string()],
1058 pending: vec![]
1059 }
1060 );
1061 assert_eq!(
1062 room_participants(&room_c, cx_c),
1063 RoomParticipants {
1064 remote: vec![],
1065 pending: vec![]
1066 }
1067 );
1068 assert_eq!(
1069 room_participants(&room_d, cx_d),
1070 RoomParticipants {
1071 remote: vec!["user_a".to_string(), "user_b".to_string()],
1072 pending: vec![]
1073 }
1074 );
1075
1076 // User D hangs up.
1077 active_call_d
1078 .update(cx_d, |call, cx| call.hang_up(cx))
1079 .await
1080 .unwrap();
1081 executor.run_until_parked();
1082 assert_eq!(
1083 room_participants(&room_a, cx_a),
1084 RoomParticipants {
1085 remote: vec!["user_b".to_string()],
1086 pending: vec![]
1087 }
1088 );
1089 assert_eq!(
1090 room_participants(&room_b, cx_b),
1091 RoomParticipants {
1092 remote: vec!["user_a".to_string()],
1093 pending: vec![]
1094 }
1095 );
1096 assert_eq!(
1097 room_participants(&room_c, cx_c),
1098 RoomParticipants {
1099 remote: vec![],
1100 pending: vec![]
1101 }
1102 );
1103 assert_eq!(
1104 room_participants(&room_d, cx_d),
1105 RoomParticipants {
1106 remote: vec![],
1107 pending: vec![]
1108 }
1109 );
1110
1111 // User B calls user D again.
1112 active_call_b
1113 .update(cx_b, |call, cx| {
1114 call.invite(client_d.user_id().unwrap(), None, cx)
1115 })
1116 .await
1117 .unwrap();
1118
1119 // User D receives the call but doesn't join the room yet.
1120
1121 let mut incoming_call_d = active_call_d.read_with(cx_d, |call, _| call.incoming());
1122 assert!(incoming_call_d.next().await.unwrap().is_some());
1123 executor.run_until_parked();
1124 assert_eq!(
1125 room_participants(&room_a, cx_a),
1126 RoomParticipants {
1127 remote: vec!["user_b".to_string()],
1128 pending: vec!["user_d".to_string()]
1129 }
1130 );
1131 assert_eq!(
1132 room_participants(&room_b, cx_b),
1133 RoomParticipants {
1134 remote: vec!["user_a".to_string()],
1135 pending: vec!["user_d".to_string()]
1136 }
1137 );
1138
1139 // The server is torn down.
1140 server.reset().await;
1141
1142 // Users A and B have troubles reconnecting, so they leave the room.
1143 client_a.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
1144 client_b.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
1145 client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
1146 executor.advance_clock(RECONNECT_TIMEOUT);
1147 assert_eq!(
1148 room_participants(&room_a, cx_a),
1149 RoomParticipants {
1150 remote: vec![],
1151 pending: vec![]
1152 }
1153 );
1154 assert_eq!(
1155 room_participants(&room_b, cx_b),
1156 RoomParticipants {
1157 remote: vec![],
1158 pending: vec![]
1159 }
1160 );
1161
1162 // User D is notified again of the incoming call but doesn't accept it.
1163 assert!(incoming_call_d.next().await.unwrap().is_some());
1164
1165 // The server finishes restarting, cleaning up stale connections and canceling the
1166 // call to user D because the room has become empty.
1167 server.start().await.unwrap();
1168 executor.advance_clock(CLEANUP_TIMEOUT);
1169 assert!(incoming_call_d.next().await.unwrap().is_none());
1170}
1171
1172#[gpui::test(iterations = 10)]
1173async fn test_calls_on_multiple_connections(
1174 executor: BackgroundExecutor,
1175 cx_a: &mut TestAppContext,
1176 cx_b1: &mut TestAppContext,
1177 cx_b2: &mut TestAppContext,
1178) {
1179 let mut server = TestServer::start(executor.clone()).await;
1180 let client_a = server.create_client(cx_a, "user_a").await;
1181 let client_b1 = server.create_client(cx_b1, "user_b").await;
1182 let client_b2 = server.create_client(cx_b2, "user_b").await;
1183 server
1184 .make_contacts(&mut [(&client_a, cx_a), (&client_b1, cx_b1)])
1185 .await;
1186
1187 let active_call_a = cx_a.read(ActiveCall::global);
1188 let active_call_b1 = cx_b1.read(ActiveCall::global);
1189 let active_call_b2 = cx_b2.read(ActiveCall::global);
1190
1191 let mut incoming_call_b1 = active_call_b1.read_with(cx_b1, |call, _| call.incoming());
1192
1193 let mut incoming_call_b2 = active_call_b2.read_with(cx_b2, |call, _| call.incoming());
1194 assert!(incoming_call_b1.next().await.unwrap().is_none());
1195 assert!(incoming_call_b2.next().await.unwrap().is_none());
1196
1197 // Call user B from client A, ensuring both clients for user B ring.
1198 active_call_a
1199 .update(cx_a, |call, cx| {
1200 call.invite(client_b1.user_id().unwrap(), None, cx)
1201 })
1202 .await
1203 .unwrap();
1204 executor.run_until_parked();
1205 assert!(incoming_call_b1.next().await.unwrap().is_some());
1206 assert!(incoming_call_b2.next().await.unwrap().is_some());
1207
1208 // User B declines the call on one of the two connections, causing both connections
1209 // to stop ringing.
1210 active_call_b2.update(cx_b2, |call, cx| call.decline_incoming(cx).unwrap());
1211 executor.run_until_parked();
1212 assert!(incoming_call_b1.next().await.unwrap().is_none());
1213 assert!(incoming_call_b2.next().await.unwrap().is_none());
1214
1215 // Call user B again from client A.
1216 active_call_a
1217 .update(cx_a, |call, cx| {
1218 call.invite(client_b1.user_id().unwrap(), None, cx)
1219 })
1220 .await
1221 .unwrap();
1222 executor.run_until_parked();
1223 assert!(incoming_call_b1.next().await.unwrap().is_some());
1224 assert!(incoming_call_b2.next().await.unwrap().is_some());
1225
1226 // User B accepts the call on one of the two connections, causing both connections
1227 // to stop ringing.
1228 active_call_b2
1229 .update(cx_b2, |call, cx| call.accept_incoming(cx))
1230 .await
1231 .unwrap();
1232 executor.run_until_parked();
1233 assert!(incoming_call_b1.next().await.unwrap().is_none());
1234 assert!(incoming_call_b2.next().await.unwrap().is_none());
1235
1236 // User B disconnects the client that is not on the call. Everything should be fine.
1237 client_b1.disconnect(&cx_b1.to_async());
1238 executor.advance_clock(RECEIVE_TIMEOUT);
1239 client_b1
1240 .authenticate_and_connect(false, &cx_b1.to_async())
1241 .await
1242 .unwrap();
1243
1244 // User B hangs up, and user A calls them again.
1245 active_call_b2
1246 .update(cx_b2, |call, cx| call.hang_up(cx))
1247 .await
1248 .unwrap();
1249 executor.run_until_parked();
1250 active_call_a
1251 .update(cx_a, |call, cx| {
1252 call.invite(client_b1.user_id().unwrap(), None, cx)
1253 })
1254 .await
1255 .unwrap();
1256 executor.run_until_parked();
1257 assert!(incoming_call_b1.next().await.unwrap().is_some());
1258 assert!(incoming_call_b2.next().await.unwrap().is_some());
1259
1260 // User A cancels the call, causing both connections to stop ringing.
1261 active_call_a
1262 .update(cx_a, |call, cx| {
1263 call.cancel_invite(client_b1.user_id().unwrap(), cx)
1264 })
1265 .await
1266 .unwrap();
1267 executor.run_until_parked();
1268 assert!(incoming_call_b1.next().await.unwrap().is_none());
1269 assert!(incoming_call_b2.next().await.unwrap().is_none());
1270
1271 // User A calls user B again.
1272 active_call_a
1273 .update(cx_a, |call, cx| {
1274 call.invite(client_b1.user_id().unwrap(), None, cx)
1275 })
1276 .await
1277 .unwrap();
1278 executor.run_until_parked();
1279 assert!(incoming_call_b1.next().await.unwrap().is_some());
1280 assert!(incoming_call_b2.next().await.unwrap().is_some());
1281
1282 // User A hangs up, causing both connections to stop ringing.
1283 active_call_a
1284 .update(cx_a, |call, cx| call.hang_up(cx))
1285 .await
1286 .unwrap();
1287 executor.run_until_parked();
1288 assert!(incoming_call_b1.next().await.unwrap().is_none());
1289 assert!(incoming_call_b2.next().await.unwrap().is_none());
1290
1291 // User A calls user B again.
1292 active_call_a
1293 .update(cx_a, |call, cx| {
1294 call.invite(client_b1.user_id().unwrap(), None, cx)
1295 })
1296 .await
1297 .unwrap();
1298 executor.run_until_parked();
1299 assert!(incoming_call_b1.next().await.unwrap().is_some());
1300 assert!(incoming_call_b2.next().await.unwrap().is_some());
1301
1302 // User A disconnects, causing both connections to stop ringing.
1303 server.forbid_connections();
1304 server.disconnect_client(client_a.peer_id().unwrap());
1305 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
1306 assert!(incoming_call_b1.next().await.unwrap().is_none());
1307 assert!(incoming_call_b2.next().await.unwrap().is_none());
1308
1309 // User A reconnects automatically, then calls user B again.
1310 server.allow_connections();
1311 executor.advance_clock(RECEIVE_TIMEOUT);
1312 active_call_a
1313 .update(cx_a, |call, cx| {
1314 call.invite(client_b1.user_id().unwrap(), None, cx)
1315 })
1316 .await
1317 .unwrap();
1318 executor.run_until_parked();
1319 assert!(incoming_call_b1.next().await.unwrap().is_some());
1320 assert!(incoming_call_b2.next().await.unwrap().is_some());
1321
1322 // User B disconnects all clients, causing user A to no longer see a pending call for them.
1323 server.forbid_connections();
1324 server.disconnect_client(client_b1.peer_id().unwrap());
1325 server.disconnect_client(client_b2.peer_id().unwrap());
1326 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
1327
1328 active_call_a.read_with(cx_a, |call, _| assert!(call.room().is_none()));
1329}
1330
1331#[gpui::test(iterations = 10)]
1332async fn test_unshare_project(
1333 executor: BackgroundExecutor,
1334 cx_a: &mut TestAppContext,
1335 cx_b: &mut TestAppContext,
1336 cx_c: &mut TestAppContext,
1337) {
1338 let mut server = TestServer::start(executor.clone()).await;
1339 let client_a = server.create_client(cx_a, "user_a").await;
1340 let client_b = server.create_client(cx_b, "user_b").await;
1341 let client_c = server.create_client(cx_c, "user_c").await;
1342 server
1343 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
1344 .await;
1345
1346 let active_call_a = cx_a.read(ActiveCall::global);
1347 let active_call_b = cx_b.read(ActiveCall::global);
1348
1349 client_a
1350 .fs()
1351 .insert_tree(
1352 "/a",
1353 json!({
1354 "a.txt": "a-contents",
1355 "b.txt": "b-contents",
1356 }),
1357 )
1358 .await;
1359
1360 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1361 let project_id = active_call_a
1362 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1363 .await
1364 .unwrap();
1365
1366 let worktree_a = project_a.read_with(cx_a, |project, _| project.worktrees().next().unwrap());
1367 let project_b = client_b.build_remote_project(project_id, cx_b).await;
1368 executor.run_until_parked();
1369
1370 assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
1371
1372 project_b
1373 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
1374 .await
1375 .unwrap();
1376
1377 // When client B leaves the room, the project becomes read-only.
1378 active_call_b
1379 .update(cx_b, |call, cx| call.hang_up(cx))
1380 .await
1381 .unwrap();
1382 executor.run_until_parked();
1383
1384 assert!(project_b.read_with(cx_b, |project, _| project.is_disconnected()));
1385
1386 // Client C opens the project.
1387 let project_c = client_c.build_remote_project(project_id, cx_c).await;
1388
1389 // When client A unshares the project, client C's project becomes read-only.
1390 project_a
1391 .update(cx_a, |project, cx| project.unshare(cx))
1392 .unwrap();
1393 executor.run_until_parked();
1394
1395 assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared()));
1396
1397 assert!(project_c.read_with(cx_c, |project, _| project.is_disconnected()));
1398
1399 // Client C can open the project again after client A re-shares.
1400 let project_id = active_call_a
1401 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1402 .await
1403 .unwrap();
1404 let project_c2 = client_c.build_remote_project(project_id, cx_c).await;
1405 executor.run_until_parked();
1406
1407 assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
1408 project_c2
1409 .update(cx_c, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
1410 .await
1411 .unwrap();
1412
1413 // When client A (the host) leaves the room, the project gets unshared and guests are notified.
1414 active_call_a
1415 .update(cx_a, |call, cx| call.hang_up(cx))
1416 .await
1417 .unwrap();
1418 executor.run_until_parked();
1419
1420 project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
1421
1422 project_c2.read_with(cx_c, |project, _| {
1423 assert!(project.is_disconnected());
1424 assert!(project.collaborators().is_empty());
1425 });
1426}
1427
1428#[gpui::test(iterations = 10)]
1429async fn test_project_reconnect(
1430 executor: BackgroundExecutor,
1431 cx_a: &mut TestAppContext,
1432 cx_b: &mut TestAppContext,
1433) {
1434 let mut server = TestServer::start(executor.clone()).await;
1435 let client_a = server.create_client(cx_a, "user_a").await;
1436 let client_b = server.create_client(cx_b, "user_b").await;
1437 server
1438 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1439 .await;
1440
1441 cx_b.update(editor::init);
1442
1443 client_a
1444 .fs()
1445 .insert_tree(
1446 "/root-1",
1447 json!({
1448 "dir1": {
1449 "a.txt": "a",
1450 "b.txt": "b",
1451 "subdir1": {
1452 "c.txt": "c",
1453 "d.txt": "d",
1454 "e.txt": "e",
1455 }
1456 },
1457 "dir2": {
1458 "v.txt": "v",
1459 },
1460 "dir3": {
1461 "w.txt": "w",
1462 "x.txt": "x",
1463 "y.txt": "y",
1464 },
1465 "dir4": {
1466 "z.txt": "z",
1467 },
1468 }),
1469 )
1470 .await;
1471 client_a
1472 .fs()
1473 .insert_tree(
1474 "/root-2",
1475 json!({
1476 "2.txt": "2",
1477 }),
1478 )
1479 .await;
1480 client_a
1481 .fs()
1482 .insert_tree(
1483 "/root-3",
1484 json!({
1485 "3.txt": "3",
1486 }),
1487 )
1488 .await;
1489
1490 let active_call_a = cx_a.read(ActiveCall::global);
1491 let (project_a1, _) = client_a.build_local_project("/root-1/dir1", cx_a).await;
1492 let (project_a2, _) = client_a.build_local_project("/root-2", cx_a).await;
1493 let (project_a3, _) = client_a.build_local_project("/root-3", cx_a).await;
1494 let worktree_a1 = project_a1.read_with(cx_a, |project, _| project.worktrees().next().unwrap());
1495 let project1_id = active_call_a
1496 .update(cx_a, |call, cx| call.share_project(project_a1.clone(), cx))
1497 .await
1498 .unwrap();
1499 let project2_id = active_call_a
1500 .update(cx_a, |call, cx| call.share_project(project_a2.clone(), cx))
1501 .await
1502 .unwrap();
1503 let project3_id = active_call_a
1504 .update(cx_a, |call, cx| call.share_project(project_a3.clone(), cx))
1505 .await
1506 .unwrap();
1507
1508 let project_b1 = client_b.build_remote_project(project1_id, cx_b).await;
1509 let project_b2 = client_b.build_remote_project(project2_id, cx_b).await;
1510 let project_b3 = client_b.build_remote_project(project3_id, cx_b).await;
1511 executor.run_until_parked();
1512
1513 let worktree1_id = worktree_a1.read_with(cx_a, |worktree, _| {
1514 assert!(worktree.as_local().unwrap().is_shared());
1515 worktree.id()
1516 });
1517 let (worktree_a2, _) = project_a1
1518 .update(cx_a, |p, cx| {
1519 p.find_or_create_local_worktree("/root-1/dir2", true, cx)
1520 })
1521 .await
1522 .unwrap();
1523 executor.run_until_parked();
1524
1525 let worktree2_id = worktree_a2.read_with(cx_a, |tree, _| {
1526 assert!(tree.as_local().unwrap().is_shared());
1527 tree.id()
1528 });
1529 executor.run_until_parked();
1530
1531 project_b1.read_with(cx_b, |project, cx| {
1532 assert!(project.worktree_for_id(worktree2_id, cx).is_some())
1533 });
1534
1535 let buffer_a1 = project_a1
1536 .update(cx_a, |p, cx| p.open_buffer((worktree1_id, "a.txt"), cx))
1537 .await
1538 .unwrap();
1539 let buffer_b1 = project_b1
1540 .update(cx_b, |p, cx| p.open_buffer((worktree1_id, "a.txt"), cx))
1541 .await
1542 .unwrap();
1543
1544 // Drop client A's connection.
1545 server.forbid_connections();
1546 server.disconnect_client(client_a.peer_id().unwrap());
1547 executor.advance_clock(RECEIVE_TIMEOUT);
1548
1549 project_a1.read_with(cx_a, |project, _| {
1550 assert!(project.is_shared());
1551 assert_eq!(project.collaborators().len(), 1);
1552 });
1553
1554 project_b1.read_with(cx_b, |project, _| {
1555 assert!(!project.is_disconnected());
1556 assert_eq!(project.collaborators().len(), 1);
1557 });
1558
1559 worktree_a1.read_with(cx_a, |tree, _| {
1560 assert!(tree.as_local().unwrap().is_shared())
1561 });
1562
1563 // While client A is disconnected, add and remove files from client A's project.
1564 client_a
1565 .fs()
1566 .insert_tree(
1567 "/root-1/dir1/subdir2",
1568 json!({
1569 "f.txt": "f-contents",
1570 "g.txt": "g-contents",
1571 "h.txt": "h-contents",
1572 "i.txt": "i-contents",
1573 }),
1574 )
1575 .await;
1576 client_a
1577 .fs()
1578 .remove_dir(
1579 "/root-1/dir1/subdir1".as_ref(),
1580 RemoveOptions {
1581 recursive: true,
1582 ..Default::default()
1583 },
1584 )
1585 .await
1586 .unwrap();
1587
1588 // While client A is disconnected, add and remove worktrees from client A's project.
1589 project_a1.update(cx_a, |project, cx| {
1590 project.remove_worktree(worktree2_id, cx)
1591 });
1592 let (worktree_a3, _) = project_a1
1593 .update(cx_a, |p, cx| {
1594 p.find_or_create_local_worktree("/root-1/dir3", true, cx)
1595 })
1596 .await
1597 .unwrap();
1598 worktree_a3
1599 .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
1600 .await;
1601
1602 let worktree3_id = worktree_a3.read_with(cx_a, |tree, _| {
1603 assert!(!tree.as_local().unwrap().is_shared());
1604 tree.id()
1605 });
1606 executor.run_until_parked();
1607
1608 // While client A is disconnected, close project 2
1609 cx_a.update(|_| drop(project_a2));
1610
1611 // While client A is disconnected, mutate a buffer on both the host and the guest.
1612 buffer_a1.update(cx_a, |buf, cx| buf.edit([(0..0, "W")], None, cx));
1613 buffer_b1.update(cx_b, |buf, cx| buf.edit([(1..1, "Z")], None, cx));
1614 executor.run_until_parked();
1615
1616 // Client A reconnects. Their project is re-shared, and client B re-joins it.
1617 server.allow_connections();
1618 client_a
1619 .authenticate_and_connect(false, &cx_a.to_async())
1620 .await
1621 .unwrap();
1622 executor.run_until_parked();
1623
1624 project_a1.read_with(cx_a, |project, cx| {
1625 assert!(project.is_shared());
1626 assert!(worktree_a1.read(cx).as_local().unwrap().is_shared());
1627 assert_eq!(
1628 worktree_a1
1629 .read(cx)
1630 .snapshot()
1631 .paths()
1632 .map(|p| p.to_str().unwrap())
1633 .collect::<Vec<_>>(),
1634 vec![
1635 "a.txt",
1636 "b.txt",
1637 "subdir2",
1638 "subdir2/f.txt",
1639 "subdir2/g.txt",
1640 "subdir2/h.txt",
1641 "subdir2/i.txt"
1642 ]
1643 );
1644 assert!(worktree_a3.read(cx).as_local().unwrap().is_shared());
1645 assert_eq!(
1646 worktree_a3
1647 .read(cx)
1648 .snapshot()
1649 .paths()
1650 .map(|p| p.to_str().unwrap())
1651 .collect::<Vec<_>>(),
1652 vec!["w.txt", "x.txt", "y.txt"]
1653 );
1654 });
1655
1656 project_b1.read_with(cx_b, |project, cx| {
1657 assert!(!project.is_disconnected());
1658 assert_eq!(
1659 project
1660 .worktree_for_id(worktree1_id, cx)
1661 .unwrap()
1662 .read(cx)
1663 .snapshot()
1664 .paths()
1665 .map(|p| p.to_str().unwrap())
1666 .collect::<Vec<_>>(),
1667 vec![
1668 "a.txt",
1669 "b.txt",
1670 "subdir2",
1671 "subdir2/f.txt",
1672 "subdir2/g.txt",
1673 "subdir2/h.txt",
1674 "subdir2/i.txt"
1675 ]
1676 );
1677 assert!(project.worktree_for_id(worktree2_id, cx).is_none());
1678 assert_eq!(
1679 project
1680 .worktree_for_id(worktree3_id, cx)
1681 .unwrap()
1682 .read(cx)
1683 .snapshot()
1684 .paths()
1685 .map(|p| p.to_str().unwrap())
1686 .collect::<Vec<_>>(),
1687 vec!["w.txt", "x.txt", "y.txt"]
1688 );
1689 });
1690
1691 project_b2.read_with(cx_b, |project, _| assert!(project.is_disconnected()));
1692
1693 project_b3.read_with(cx_b, |project, _| assert!(!project.is_disconnected()));
1694
1695 buffer_a1.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "WaZ"));
1696
1697 buffer_b1.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "WaZ"));
1698
1699 // Drop client B's connection.
1700 server.forbid_connections();
1701 server.disconnect_client(client_b.peer_id().unwrap());
1702 executor.advance_clock(RECEIVE_TIMEOUT);
1703
1704 // While client B is disconnected, add and remove files from client A's project
1705 client_a
1706 .fs()
1707 .insert_file("/root-1/dir1/subdir2/j.txt", "j-contents".into())
1708 .await;
1709 client_a
1710 .fs()
1711 .remove_file("/root-1/dir1/subdir2/i.txt".as_ref(), Default::default())
1712 .await
1713 .unwrap();
1714
1715 // While client B is disconnected, add and remove worktrees from client A's project.
1716 let (worktree_a4, _) = project_a1
1717 .update(cx_a, |p, cx| {
1718 p.find_or_create_local_worktree("/root-1/dir4", true, cx)
1719 })
1720 .await
1721 .unwrap();
1722 executor.run_until_parked();
1723
1724 let worktree4_id = worktree_a4.read_with(cx_a, |tree, _| {
1725 assert!(tree.as_local().unwrap().is_shared());
1726 tree.id()
1727 });
1728 project_a1.update(cx_a, |project, cx| {
1729 project.remove_worktree(worktree3_id, cx)
1730 });
1731 executor.run_until_parked();
1732
1733 // While client B is disconnected, mutate a buffer on both the host and the guest.
1734 buffer_a1.update(cx_a, |buf, cx| buf.edit([(1..1, "X")], None, cx));
1735 buffer_b1.update(cx_b, |buf, cx| buf.edit([(2..2, "Y")], None, cx));
1736 executor.run_until_parked();
1737
1738 // While disconnected, close project 3
1739 cx_a.update(|_| drop(project_a3));
1740
1741 // Client B reconnects. They re-join the room and the remaining shared project.
1742 server.allow_connections();
1743 client_b
1744 .authenticate_and_connect(false, &cx_b.to_async())
1745 .await
1746 .unwrap();
1747 executor.run_until_parked();
1748
1749 project_b1.read_with(cx_b, |project, cx| {
1750 assert!(!project.is_disconnected());
1751 assert_eq!(
1752 project
1753 .worktree_for_id(worktree1_id, cx)
1754 .unwrap()
1755 .read(cx)
1756 .snapshot()
1757 .paths()
1758 .map(|p| p.to_str().unwrap())
1759 .collect::<Vec<_>>(),
1760 vec![
1761 "a.txt",
1762 "b.txt",
1763 "subdir2",
1764 "subdir2/f.txt",
1765 "subdir2/g.txt",
1766 "subdir2/h.txt",
1767 "subdir2/j.txt"
1768 ]
1769 );
1770 assert!(project.worktree_for_id(worktree2_id, cx).is_none());
1771 assert_eq!(
1772 project
1773 .worktree_for_id(worktree4_id, cx)
1774 .unwrap()
1775 .read(cx)
1776 .snapshot()
1777 .paths()
1778 .map(|p| p.to_str().unwrap())
1779 .collect::<Vec<_>>(),
1780 vec!["z.txt"]
1781 );
1782 });
1783
1784 project_b3.read_with(cx_b, |project, _| assert!(project.is_disconnected()));
1785
1786 buffer_a1.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "WXaYZ"));
1787
1788 buffer_b1.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "WXaYZ"));
1789}
1790
1791#[gpui::test(iterations = 10)]
1792async fn test_active_call_events(
1793 executor: BackgroundExecutor,
1794 cx_a: &mut TestAppContext,
1795 cx_b: &mut TestAppContext,
1796) {
1797 let mut server = TestServer::start(executor.clone()).await;
1798 let client_a = server.create_client(cx_a, "user_a").await;
1799 let client_b = server.create_client(cx_b, "user_b").await;
1800 client_a.fs().insert_tree("/a", json!({})).await;
1801 client_b.fs().insert_tree("/b", json!({})).await;
1802
1803 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
1804 let (project_b, _) = client_b.build_local_project("/b", cx_b).await;
1805
1806 server
1807 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1808 .await;
1809 let active_call_a = cx_a.read(ActiveCall::global);
1810 let active_call_b = cx_b.read(ActiveCall::global);
1811
1812 let events_a = active_call_events(cx_a);
1813 let events_b = active_call_events(cx_b);
1814
1815 let project_a_id = active_call_a
1816 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1817 .await
1818 .unwrap();
1819 executor.run_until_parked();
1820 assert_eq!(mem::take(&mut *events_a.borrow_mut()), vec![]);
1821 assert_eq!(
1822 mem::take(&mut *events_b.borrow_mut()),
1823 vec![room::Event::RemoteProjectShared {
1824 owner: Arc::new(User {
1825 id: client_a.user_id().unwrap(),
1826 github_login: "user_a".to_string(),
1827 avatar_uri: "avatar_a".into(),
1828 }),
1829 project_id: project_a_id,
1830 worktree_root_names: vec!["a".to_string()],
1831 }]
1832 );
1833
1834 let project_b_id = active_call_b
1835 .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
1836 .await
1837 .unwrap();
1838 executor.run_until_parked();
1839 assert_eq!(
1840 mem::take(&mut *events_a.borrow_mut()),
1841 vec![room::Event::RemoteProjectShared {
1842 owner: Arc::new(User {
1843 id: client_b.user_id().unwrap(),
1844 github_login: "user_b".to_string(),
1845 avatar_uri: "avatar_b".into(),
1846 }),
1847 project_id: project_b_id,
1848 worktree_root_names: vec!["b".to_string()]
1849 }]
1850 );
1851 assert_eq!(mem::take(&mut *events_b.borrow_mut()), vec![]);
1852
1853 // Sharing a project twice is idempotent.
1854 let project_b_id_2 = active_call_b
1855 .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
1856 .await
1857 .unwrap();
1858 assert_eq!(project_b_id_2, project_b_id);
1859 executor.run_until_parked();
1860 assert_eq!(mem::take(&mut *events_a.borrow_mut()), vec![]);
1861 assert_eq!(mem::take(&mut *events_b.borrow_mut()), vec![]);
1862}
1863
1864fn active_call_events(cx: &mut TestAppContext) -> Rc<RefCell<Vec<room::Event>>> {
1865 let events = Rc::new(RefCell::new(Vec::new()));
1866 let active_call = cx.read(ActiveCall::global);
1867 cx.update({
1868 let events = events.clone();
1869 |cx| {
1870 cx.subscribe(&active_call, move |_, event, _| {
1871 events.borrow_mut().push(event.clone())
1872 })
1873 .detach()
1874 }
1875 });
1876 events
1877}
1878
1879#[gpui::test]
1880async fn test_mute_deafen(
1881 executor: BackgroundExecutor,
1882 cx_a: &mut TestAppContext,
1883 cx_b: &mut TestAppContext,
1884 cx_c: &mut TestAppContext,
1885) {
1886 let mut server = TestServer::start(executor.clone()).await;
1887 let client_a = server.create_client(cx_a, "user_a").await;
1888 let client_b = server.create_client(cx_b, "user_b").await;
1889 let client_c = server.create_client(cx_c, "user_c").await;
1890
1891 server
1892 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
1893 .await;
1894
1895 let active_call_a = cx_a.read(ActiveCall::global);
1896 let active_call_b = cx_b.read(ActiveCall::global);
1897 let active_call_c = cx_c.read(ActiveCall::global);
1898
1899 // User A calls user B, B answers.
1900 active_call_a
1901 .update(cx_a, |call, cx| {
1902 call.invite(client_b.user_id().unwrap(), None, cx)
1903 })
1904 .await
1905 .unwrap();
1906 executor.run_until_parked();
1907 active_call_b
1908 .update(cx_b, |call, cx| call.accept_incoming(cx))
1909 .await
1910 .unwrap();
1911 executor.run_until_parked();
1912
1913 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
1914 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
1915
1916 room_a.read_with(cx_a, |room, _| assert!(!room.is_muted()));
1917 room_b.read_with(cx_b, |room, _| assert!(!room.is_muted()));
1918
1919 // Users A and B are both muted.
1920 assert_eq!(
1921 participant_audio_state(&room_a, cx_a),
1922 &[ParticipantAudioState {
1923 user_id: client_b.user_id().unwrap(),
1924 is_muted: false,
1925 audio_tracks_playing: vec![true],
1926 }]
1927 );
1928 assert_eq!(
1929 participant_audio_state(&room_b, cx_b),
1930 &[ParticipantAudioState {
1931 user_id: client_a.user_id().unwrap(),
1932 is_muted: false,
1933 audio_tracks_playing: vec![true],
1934 }]
1935 );
1936
1937 // User A mutes
1938 room_a.update(cx_a, |room, cx| room.toggle_mute(cx));
1939 executor.run_until_parked();
1940
1941 // User A hears user B, but B doesn't hear A.
1942 room_a.read_with(cx_a, |room, _| assert!(room.is_muted()));
1943 room_b.read_with(cx_b, |room, _| assert!(!room.is_muted()));
1944 assert_eq!(
1945 participant_audio_state(&room_a, cx_a),
1946 &[ParticipantAudioState {
1947 user_id: client_b.user_id().unwrap(),
1948 is_muted: false,
1949 audio_tracks_playing: vec![true],
1950 }]
1951 );
1952 assert_eq!(
1953 participant_audio_state(&room_b, cx_b),
1954 &[ParticipantAudioState {
1955 user_id: client_a.user_id().unwrap(),
1956 is_muted: true,
1957 audio_tracks_playing: vec![true],
1958 }]
1959 );
1960
1961 // User A deafens
1962 room_a.update(cx_a, |room, cx| room.toggle_deafen(cx));
1963 executor.run_until_parked();
1964
1965 // User A does not hear user B.
1966 room_a.read_with(cx_a, |room, _| assert!(room.is_muted()));
1967 room_b.read_with(cx_b, |room, _| assert!(!room.is_muted()));
1968 assert_eq!(
1969 participant_audio_state(&room_a, cx_a),
1970 &[ParticipantAudioState {
1971 user_id: client_b.user_id().unwrap(),
1972 is_muted: false,
1973 audio_tracks_playing: vec![false],
1974 }]
1975 );
1976 assert_eq!(
1977 participant_audio_state(&room_b, cx_b),
1978 &[ParticipantAudioState {
1979 user_id: client_a.user_id().unwrap(),
1980 is_muted: true,
1981 audio_tracks_playing: vec![true],
1982 }]
1983 );
1984
1985 // User B calls user C, C joins.
1986 active_call_b
1987 .update(cx_b, |call, cx| {
1988 call.invite(client_c.user_id().unwrap(), None, cx)
1989 })
1990 .await
1991 .unwrap();
1992 executor.run_until_parked();
1993 active_call_c
1994 .update(cx_c, |call, cx| call.accept_incoming(cx))
1995 .await
1996 .unwrap();
1997 executor.run_until_parked();
1998
1999 // User A does not hear users B or C.
2000 assert_eq!(
2001 participant_audio_state(&room_a, cx_a),
2002 &[
2003 ParticipantAudioState {
2004 user_id: client_b.user_id().unwrap(),
2005 is_muted: false,
2006 audio_tracks_playing: vec![false],
2007 },
2008 ParticipantAudioState {
2009 user_id: client_c.user_id().unwrap(),
2010 is_muted: false,
2011 audio_tracks_playing: vec![false],
2012 }
2013 ]
2014 );
2015 assert_eq!(
2016 participant_audio_state(&room_b, cx_b),
2017 &[
2018 ParticipantAudioState {
2019 user_id: client_a.user_id().unwrap(),
2020 is_muted: true,
2021 audio_tracks_playing: vec![true],
2022 },
2023 ParticipantAudioState {
2024 user_id: client_c.user_id().unwrap(),
2025 is_muted: false,
2026 audio_tracks_playing: vec![true],
2027 }
2028 ]
2029 );
2030
2031 #[derive(PartialEq, Eq, Debug)]
2032 struct ParticipantAudioState {
2033 user_id: u64,
2034 is_muted: bool,
2035 audio_tracks_playing: Vec<bool>,
2036 }
2037
2038 fn participant_audio_state(
2039 room: &Model<Room>,
2040 cx: &TestAppContext,
2041 ) -> Vec<ParticipantAudioState> {
2042 room.read_with(cx, |room, _| {
2043 room.remote_participants()
2044 .iter()
2045 .map(|(user_id, participant)| ParticipantAudioState {
2046 user_id: *user_id,
2047 is_muted: participant.muted,
2048 audio_tracks_playing: participant
2049 .audio_tracks
2050 .values()
2051 .map(|track| track.is_playing())
2052 .collect(),
2053 })
2054 .collect::<Vec<_>>()
2055 })
2056 }
2057}
2058
2059#[gpui::test(iterations = 10)]
2060async fn test_room_location(
2061 executor: BackgroundExecutor,
2062 cx_a: &mut TestAppContext,
2063 cx_b: &mut TestAppContext,
2064) {
2065 let mut server = TestServer::start(executor.clone()).await;
2066 let client_a = server.create_client(cx_a, "user_a").await;
2067 let client_b = server.create_client(cx_b, "user_b").await;
2068 client_a.fs().insert_tree("/a", json!({})).await;
2069 client_b.fs().insert_tree("/b", json!({})).await;
2070
2071 let active_call_a = cx_a.read(ActiveCall::global);
2072 let active_call_b = cx_b.read(ActiveCall::global);
2073
2074 let a_notified = Rc::new(Cell::new(false));
2075 cx_a.update({
2076 let notified = a_notified.clone();
2077 |cx| {
2078 cx.observe(&active_call_a, move |_, _| notified.set(true))
2079 .detach()
2080 }
2081 });
2082
2083 let b_notified = Rc::new(Cell::new(false));
2084 cx_b.update({
2085 let b_notified = b_notified.clone();
2086 |cx| {
2087 cx.observe(&active_call_b, move |_, _| b_notified.set(true))
2088 .detach()
2089 }
2090 });
2091
2092 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
2093 active_call_a
2094 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
2095 .await
2096 .unwrap();
2097 let (project_b, _) = client_b.build_local_project("/b", cx_b).await;
2098
2099 server
2100 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2101 .await;
2102
2103 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
2104
2105 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
2106 executor.run_until_parked();
2107 assert!(a_notified.take());
2108 assert_eq!(
2109 participant_locations(&room_a, cx_a),
2110 vec![("user_b".to_string(), ParticipantLocation::External)]
2111 );
2112 assert!(b_notified.take());
2113 assert_eq!(
2114 participant_locations(&room_b, cx_b),
2115 vec![("user_a".to_string(), ParticipantLocation::UnsharedProject)]
2116 );
2117
2118 let project_a_id = active_call_a
2119 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2120 .await
2121 .unwrap();
2122 executor.run_until_parked();
2123 assert!(a_notified.take());
2124 assert_eq!(
2125 participant_locations(&room_a, cx_a),
2126 vec![("user_b".to_string(), ParticipantLocation::External)]
2127 );
2128 assert!(b_notified.take());
2129 assert_eq!(
2130 participant_locations(&room_b, cx_b),
2131 vec![(
2132 "user_a".to_string(),
2133 ParticipantLocation::SharedProject {
2134 project_id: project_a_id
2135 }
2136 )]
2137 );
2138
2139 let project_b_id = active_call_b
2140 .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
2141 .await
2142 .unwrap();
2143 executor.run_until_parked();
2144 assert!(a_notified.take());
2145 assert_eq!(
2146 participant_locations(&room_a, cx_a),
2147 vec![("user_b".to_string(), ParticipantLocation::External)]
2148 );
2149 assert!(b_notified.take());
2150 assert_eq!(
2151 participant_locations(&room_b, cx_b),
2152 vec![(
2153 "user_a".to_string(),
2154 ParticipantLocation::SharedProject {
2155 project_id: project_a_id
2156 }
2157 )]
2158 );
2159
2160 active_call_b
2161 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
2162 .await
2163 .unwrap();
2164 executor.run_until_parked();
2165 assert!(a_notified.take());
2166 assert_eq!(
2167 participant_locations(&room_a, cx_a),
2168 vec![(
2169 "user_b".to_string(),
2170 ParticipantLocation::SharedProject {
2171 project_id: project_b_id
2172 }
2173 )]
2174 );
2175 assert!(b_notified.take());
2176 assert_eq!(
2177 participant_locations(&room_b, cx_b),
2178 vec![(
2179 "user_a".to_string(),
2180 ParticipantLocation::SharedProject {
2181 project_id: project_a_id
2182 }
2183 )]
2184 );
2185
2186 active_call_b
2187 .update(cx_b, |call, cx| call.set_location(None, cx))
2188 .await
2189 .unwrap();
2190 executor.run_until_parked();
2191 assert!(a_notified.take());
2192 assert_eq!(
2193 participant_locations(&room_a, cx_a),
2194 vec![("user_b".to_string(), ParticipantLocation::External)]
2195 );
2196 assert!(b_notified.take());
2197 assert_eq!(
2198 participant_locations(&room_b, cx_b),
2199 vec![(
2200 "user_a".to_string(),
2201 ParticipantLocation::SharedProject {
2202 project_id: project_a_id
2203 }
2204 )]
2205 );
2206
2207 fn participant_locations(
2208 room: &Model<Room>,
2209 cx: &TestAppContext,
2210 ) -> Vec<(String, ParticipantLocation)> {
2211 room.read_with(cx, |room, _| {
2212 room.remote_participants()
2213 .values()
2214 .map(|participant| {
2215 (
2216 participant.user.github_login.to_string(),
2217 participant.location,
2218 )
2219 })
2220 .collect()
2221 })
2222 }
2223}
2224
2225#[gpui::test(iterations = 10)]
2226async fn test_propagate_saves_and_fs_changes(
2227 executor: BackgroundExecutor,
2228 cx_a: &mut TestAppContext,
2229 cx_b: &mut TestAppContext,
2230 cx_c: &mut TestAppContext,
2231) {
2232 let mut server = TestServer::start(executor.clone()).await;
2233 let client_a = server.create_client(cx_a, "user_a").await;
2234 let client_b = server.create_client(cx_b, "user_b").await;
2235 let client_c = server.create_client(cx_c, "user_c").await;
2236
2237 server
2238 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
2239 .await;
2240 let active_call_a = cx_a.read(ActiveCall::global);
2241
2242 let rust = Arc::new(Language::new(
2243 LanguageConfig {
2244 name: "Rust".into(),
2245 path_suffixes: vec!["rs".to_string()],
2246 ..Default::default()
2247 },
2248 Some(tree_sitter_rust::language()),
2249 ));
2250 let javascript = Arc::new(Language::new(
2251 LanguageConfig {
2252 name: "JavaScript".into(),
2253 path_suffixes: vec!["js".to_string()],
2254 ..Default::default()
2255 },
2256 Some(tree_sitter_rust::language()),
2257 ));
2258 for client in [&client_a, &client_b, &client_c] {
2259 client.language_registry().add(rust.clone());
2260 client.language_registry().add(javascript.clone());
2261 }
2262
2263 client_a
2264 .fs()
2265 .insert_tree(
2266 "/a",
2267 json!({
2268 "file1.rs": "",
2269 "file2": ""
2270 }),
2271 )
2272 .await;
2273 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
2274
2275 let worktree_a = project_a.read_with(cx_a, |p, _| p.worktrees().next().unwrap());
2276 let project_id = active_call_a
2277 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2278 .await
2279 .unwrap();
2280
2281 // Join that worktree as clients B and C.
2282 let project_b = client_b.build_remote_project(project_id, cx_b).await;
2283 let project_c = client_c.build_remote_project(project_id, cx_c).await;
2284
2285 let worktree_b = project_b.read_with(cx_b, |p, _| p.worktrees().next().unwrap());
2286
2287 let worktree_c = project_c.read_with(cx_c, |p, _| p.worktrees().next().unwrap());
2288
2289 // Open and edit a buffer as both guests B and C.
2290 let buffer_b = project_b
2291 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "file1.rs"), cx))
2292 .await
2293 .unwrap();
2294 let buffer_c = project_c
2295 .update(cx_c, |p, cx| p.open_buffer((worktree_id, "file1.rs"), cx))
2296 .await
2297 .unwrap();
2298
2299 buffer_b.read_with(cx_b, |buffer, _| {
2300 assert_eq!(&*buffer.language().unwrap().name(), "Rust");
2301 });
2302
2303 buffer_c.read_with(cx_c, |buffer, _| {
2304 assert_eq!(&*buffer.language().unwrap().name(), "Rust");
2305 });
2306 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "i-am-b, ")], None, cx));
2307 buffer_c.update(cx_c, |buf, cx| buf.edit([(0..0, "i-am-c, ")], None, cx));
2308
2309 // Open and edit that buffer as the host.
2310 let buffer_a = project_a
2311 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "file1.rs"), cx))
2312 .await
2313 .unwrap();
2314
2315 executor.run_until_parked();
2316
2317 buffer_a.read_with(cx_a, |buf, _| assert_eq!(buf.text(), "i-am-c, i-am-b, "));
2318 buffer_a.update(cx_a, |buf, cx| {
2319 buf.edit([(buf.len()..buf.len(), "i-am-a")], None, cx)
2320 });
2321
2322 executor.run_until_parked();
2323
2324 buffer_a.read_with(cx_a, |buf, _| {
2325 assert_eq!(buf.text(), "i-am-c, i-am-b, i-am-a");
2326 });
2327
2328 buffer_b.read_with(cx_b, |buf, _| {
2329 assert_eq!(buf.text(), "i-am-c, i-am-b, i-am-a");
2330 });
2331
2332 buffer_c.read_with(cx_c, |buf, _| {
2333 assert_eq!(buf.text(), "i-am-c, i-am-b, i-am-a");
2334 });
2335
2336 // Edit the buffer as the host and concurrently save as guest B.
2337 let save_b = project_b.update(cx_b, |project, cx| {
2338 project.save_buffer(buffer_b.clone(), cx)
2339 });
2340 buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "hi-a, ")], None, cx));
2341 save_b.await.unwrap();
2342 assert_eq!(
2343 client_a.fs().load("/a/file1.rs".as_ref()).await.unwrap(),
2344 "hi-a, i-am-c, i-am-b, i-am-a"
2345 );
2346
2347 executor.run_until_parked();
2348
2349 buffer_a.read_with(cx_a, |buf, _| assert!(!buf.is_dirty()));
2350
2351 buffer_b.read_with(cx_b, |buf, _| assert!(!buf.is_dirty()));
2352
2353 buffer_c.read_with(cx_c, |buf, _| assert!(!buf.is_dirty()));
2354
2355 // Make changes on host's file system, see those changes on guest worktrees.
2356 client_a
2357 .fs()
2358 .rename(
2359 "/a/file1.rs".as_ref(),
2360 "/a/file1.js".as_ref(),
2361 Default::default(),
2362 )
2363 .await
2364 .unwrap();
2365 client_a
2366 .fs()
2367 .rename("/a/file2".as_ref(), "/a/file3".as_ref(), Default::default())
2368 .await
2369 .unwrap();
2370 client_a.fs().insert_file("/a/file4", "4".into()).await;
2371 executor.run_until_parked();
2372
2373 worktree_a.read_with(cx_a, |tree, _| {
2374 assert_eq!(
2375 tree.paths()
2376 .map(|p| p.to_string_lossy())
2377 .collect::<Vec<_>>(),
2378 ["file1.js", "file3", "file4"]
2379 )
2380 });
2381
2382 worktree_b.read_with(cx_b, |tree, _| {
2383 assert_eq!(
2384 tree.paths()
2385 .map(|p| p.to_string_lossy())
2386 .collect::<Vec<_>>(),
2387 ["file1.js", "file3", "file4"]
2388 )
2389 });
2390
2391 worktree_c.read_with(cx_c, |tree, _| {
2392 assert_eq!(
2393 tree.paths()
2394 .map(|p| p.to_string_lossy())
2395 .collect::<Vec<_>>(),
2396 ["file1.js", "file3", "file4"]
2397 )
2398 });
2399
2400 // Ensure buffer files are updated as well.
2401
2402 buffer_a.read_with(cx_a, |buffer, _| {
2403 assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js"));
2404 assert_eq!(&*buffer.language().unwrap().name(), "JavaScript");
2405 });
2406
2407 buffer_b.read_with(cx_b, |buffer, _| {
2408 assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js"));
2409 assert_eq!(&*buffer.language().unwrap().name(), "JavaScript");
2410 });
2411
2412 buffer_c.read_with(cx_c, |buffer, _| {
2413 assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js"));
2414 assert_eq!(&*buffer.language().unwrap().name(), "JavaScript");
2415 });
2416
2417 let new_buffer_a = project_a
2418 .update(cx_a, |p, cx| p.create_buffer("", None, cx))
2419 .unwrap();
2420
2421 let new_buffer_id = new_buffer_a.read_with(cx_a, |buffer, _| buffer.remote_id());
2422 let new_buffer_b = project_b
2423 .update(cx_b, |p, cx| p.open_buffer_by_id(new_buffer_id, cx))
2424 .await
2425 .unwrap();
2426
2427 new_buffer_b.read_with(cx_b, |buffer, _| {
2428 assert!(buffer.file().is_none());
2429 });
2430
2431 new_buffer_a.update(cx_a, |buffer, cx| {
2432 buffer.edit([(0..0, "ok")], None, cx);
2433 });
2434 project_a
2435 .update(cx_a, |project, cx| {
2436 project.save_buffer_as(new_buffer_a.clone(), "/a/file3.rs".into(), cx)
2437 })
2438 .await
2439 .unwrap();
2440
2441 executor.run_until_parked();
2442
2443 new_buffer_b.read_with(cx_b, |buffer_b, _| {
2444 assert_eq!(
2445 buffer_b.file().unwrap().path().as_ref(),
2446 Path::new("file3.rs")
2447 );
2448
2449 new_buffer_a.read_with(cx_a, |buffer_a, _| {
2450 assert_eq!(buffer_b.saved_mtime(), buffer_a.saved_mtime());
2451 assert_eq!(buffer_b.saved_version(), buffer_a.saved_version());
2452 });
2453 });
2454}
2455
2456#[gpui::test(iterations = 10)]
2457async fn test_git_diff_base_change(
2458 executor: BackgroundExecutor,
2459 cx_a: &mut TestAppContext,
2460 cx_b: &mut TestAppContext,
2461) {
2462 let mut server = TestServer::start(executor.clone()).await;
2463 let client_a = server.create_client(cx_a, "user_a").await;
2464 let client_b = server.create_client(cx_b, "user_b").await;
2465 server
2466 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2467 .await;
2468 let active_call_a = cx_a.read(ActiveCall::global);
2469
2470 client_a
2471 .fs()
2472 .insert_tree(
2473 "/dir",
2474 json!({
2475 ".git": {},
2476 "sub": {
2477 ".git": {},
2478 "b.txt": "
2479 one
2480 two
2481 three
2482 ".unindent(),
2483 },
2484 "a.txt": "
2485 one
2486 two
2487 three
2488 ".unindent(),
2489 }),
2490 )
2491 .await;
2492
2493 let (project_local, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2494 let project_id = active_call_a
2495 .update(cx_a, |call, cx| {
2496 call.share_project(project_local.clone(), cx)
2497 })
2498 .await
2499 .unwrap();
2500
2501 let project_remote = client_b.build_remote_project(project_id, cx_b).await;
2502
2503 let diff_base = "
2504 one
2505 three
2506 "
2507 .unindent();
2508
2509 let new_diff_base = "
2510 one
2511 two
2512 "
2513 .unindent();
2514
2515 client_a.fs().set_index_for_repo(
2516 Path::new("/dir/.git"),
2517 &[(Path::new("a.txt"), diff_base.clone())],
2518 );
2519
2520 // Create the buffer
2521 let buffer_local_a = project_local
2522 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
2523 .await
2524 .unwrap();
2525
2526 // Wait for it to catch up to the new diff
2527 executor.run_until_parked();
2528
2529 // Smoke test diffing
2530
2531 buffer_local_a.read_with(cx_a, |buffer, _| {
2532 assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
2533 git::diff::assert_hunks(
2534 buffer.snapshot().git_diff_hunks_in_row_range(0..4),
2535 &buffer,
2536 &diff_base,
2537 &[(1..2, "", "two\n")],
2538 );
2539 });
2540
2541 // Create remote buffer
2542 let buffer_remote_a = project_remote
2543 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
2544 .await
2545 .unwrap();
2546
2547 // Wait remote buffer to catch up to the new diff
2548 executor.run_until_parked();
2549
2550 // Smoke test diffing
2551
2552 buffer_remote_a.read_with(cx_b, |buffer, _| {
2553 assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
2554 git::diff::assert_hunks(
2555 buffer.snapshot().git_diff_hunks_in_row_range(0..4),
2556 &buffer,
2557 &diff_base,
2558 &[(1..2, "", "two\n")],
2559 );
2560 });
2561
2562 client_a.fs().set_index_for_repo(
2563 Path::new("/dir/.git"),
2564 &[(Path::new("a.txt"), new_diff_base.clone())],
2565 );
2566
2567 // Wait for buffer_local_a to receive it
2568 executor.run_until_parked();
2569
2570 // Smoke test new diffing
2571
2572 buffer_local_a.read_with(cx_a, |buffer, _| {
2573 assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
2574
2575 git::diff::assert_hunks(
2576 buffer.snapshot().git_diff_hunks_in_row_range(0..4),
2577 &buffer,
2578 &diff_base,
2579 &[(2..3, "", "three\n")],
2580 );
2581 });
2582
2583 // Smoke test B
2584
2585 buffer_remote_a.read_with(cx_b, |buffer, _| {
2586 assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
2587 git::diff::assert_hunks(
2588 buffer.snapshot().git_diff_hunks_in_row_range(0..4),
2589 &buffer,
2590 &diff_base,
2591 &[(2..3, "", "three\n")],
2592 );
2593 });
2594
2595 //Nested git dir
2596
2597 let diff_base = "
2598 one
2599 three
2600 "
2601 .unindent();
2602
2603 let new_diff_base = "
2604 one
2605 two
2606 "
2607 .unindent();
2608
2609 client_a.fs().set_index_for_repo(
2610 Path::new("/dir/sub/.git"),
2611 &[(Path::new("b.txt"), diff_base.clone())],
2612 );
2613
2614 // Create the buffer
2615 let buffer_local_b = project_local
2616 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "sub/b.txt"), cx))
2617 .await
2618 .unwrap();
2619
2620 // Wait for it to catch up to the new diff
2621 executor.run_until_parked();
2622
2623 // Smoke test diffing
2624
2625 buffer_local_b.read_with(cx_a, |buffer, _| {
2626 assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
2627 git::diff::assert_hunks(
2628 buffer.snapshot().git_diff_hunks_in_row_range(0..4),
2629 &buffer,
2630 &diff_base,
2631 &[(1..2, "", "two\n")],
2632 );
2633 });
2634
2635 // Create remote buffer
2636 let buffer_remote_b = project_remote
2637 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "sub/b.txt"), cx))
2638 .await
2639 .unwrap();
2640
2641 // Wait remote buffer to catch up to the new diff
2642 executor.run_until_parked();
2643
2644 // Smoke test diffing
2645
2646 buffer_remote_b.read_with(cx_b, |buffer, _| {
2647 assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
2648 git::diff::assert_hunks(
2649 buffer.snapshot().git_diff_hunks_in_row_range(0..4),
2650 &buffer,
2651 &diff_base,
2652 &[(1..2, "", "two\n")],
2653 );
2654 });
2655
2656 client_a.fs().set_index_for_repo(
2657 Path::new("/dir/sub/.git"),
2658 &[(Path::new("b.txt"), new_diff_base.clone())],
2659 );
2660
2661 // Wait for buffer_local_b to receive it
2662 executor.run_until_parked();
2663
2664 // Smoke test new diffing
2665
2666 buffer_local_b.read_with(cx_a, |buffer, _| {
2667 assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
2668 println!("{:?}", buffer.as_rope().to_string());
2669 println!("{:?}", buffer.diff_base());
2670 println!(
2671 "{:?}",
2672 buffer
2673 .snapshot()
2674 .git_diff_hunks_in_row_range(0..4)
2675 .collect::<Vec<_>>()
2676 );
2677
2678 git::diff::assert_hunks(
2679 buffer.snapshot().git_diff_hunks_in_row_range(0..4),
2680 &buffer,
2681 &diff_base,
2682 &[(2..3, "", "three\n")],
2683 );
2684 });
2685
2686 // Smoke test B
2687
2688 buffer_remote_b.read_with(cx_b, |buffer, _| {
2689 assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
2690 git::diff::assert_hunks(
2691 buffer.snapshot().git_diff_hunks_in_row_range(0..4),
2692 &buffer,
2693 &diff_base,
2694 &[(2..3, "", "three\n")],
2695 );
2696 });
2697}
2698
2699#[gpui::test]
2700async fn test_git_branch_name(
2701 executor: BackgroundExecutor,
2702 cx_a: &mut TestAppContext,
2703 cx_b: &mut TestAppContext,
2704 cx_c: &mut TestAppContext,
2705) {
2706 let mut server = TestServer::start(executor.clone()).await;
2707 let client_a = server.create_client(cx_a, "user_a").await;
2708 let client_b = server.create_client(cx_b, "user_b").await;
2709 let client_c = server.create_client(cx_c, "user_c").await;
2710 server
2711 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
2712 .await;
2713 let active_call_a = cx_a.read(ActiveCall::global);
2714
2715 client_a
2716 .fs()
2717 .insert_tree(
2718 "/dir",
2719 json!({
2720 ".git": {},
2721 }),
2722 )
2723 .await;
2724
2725 let (project_local, _worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2726 let project_id = active_call_a
2727 .update(cx_a, |call, cx| {
2728 call.share_project(project_local.clone(), cx)
2729 })
2730 .await
2731 .unwrap();
2732
2733 let project_remote = client_b.build_remote_project(project_id, cx_b).await;
2734 client_a
2735 .fs()
2736 .set_branch_name(Path::new("/dir/.git"), Some("branch-1"));
2737
2738 // Wait for it to catch up to the new branch
2739 executor.run_until_parked();
2740
2741 #[track_caller]
2742 fn assert_branch(branch_name: Option<impl Into<String>>, project: &Project, cx: &AppContext) {
2743 let branch_name = branch_name.map(Into::into);
2744 let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
2745 assert_eq!(worktrees.len(), 1);
2746 let worktree = worktrees[0].clone();
2747 let root_entry = worktree.read(cx).snapshot().root_git_entry().unwrap();
2748 assert_eq!(root_entry.branch(), branch_name.map(Into::into));
2749 }
2750
2751 // Smoke test branch reading
2752
2753 project_local.read_with(cx_a, |project, cx| {
2754 assert_branch(Some("branch-1"), project, cx)
2755 });
2756
2757 project_remote.read_with(cx_b, |project, cx| {
2758 assert_branch(Some("branch-1"), project, cx)
2759 });
2760
2761 client_a
2762 .fs()
2763 .set_branch_name(Path::new("/dir/.git"), Some("branch-2"));
2764
2765 // Wait for buffer_local_a to receive it
2766 executor.run_until_parked();
2767
2768 // Smoke test branch reading
2769
2770 project_local.read_with(cx_a, |project, cx| {
2771 assert_branch(Some("branch-2"), project, cx)
2772 });
2773
2774 project_remote.read_with(cx_b, |project, cx| {
2775 assert_branch(Some("branch-2"), project, cx)
2776 });
2777
2778 let project_remote_c = client_c.build_remote_project(project_id, cx_c).await;
2779 executor.run_until_parked();
2780
2781 project_remote_c.read_with(cx_c, |project, cx| {
2782 assert_branch(Some("branch-2"), project, cx)
2783 });
2784}
2785
2786#[gpui::test]
2787async fn test_git_status_sync(
2788 executor: BackgroundExecutor,
2789 cx_a: &mut TestAppContext,
2790 cx_b: &mut TestAppContext,
2791 cx_c: &mut TestAppContext,
2792) {
2793 let mut server = TestServer::start(executor.clone()).await;
2794 let client_a = server.create_client(cx_a, "user_a").await;
2795 let client_b = server.create_client(cx_b, "user_b").await;
2796 let client_c = server.create_client(cx_c, "user_c").await;
2797 server
2798 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
2799 .await;
2800 let active_call_a = cx_a.read(ActiveCall::global);
2801
2802 client_a
2803 .fs()
2804 .insert_tree(
2805 "/dir",
2806 json!({
2807 ".git": {},
2808 "a.txt": "a",
2809 "b.txt": "b",
2810 }),
2811 )
2812 .await;
2813
2814 const A_TXT: &'static str = "a.txt";
2815 const B_TXT: &'static str = "b.txt";
2816
2817 client_a.fs().set_status_for_repo_via_git_operation(
2818 Path::new("/dir/.git"),
2819 &[
2820 (&Path::new(A_TXT), GitFileStatus::Added),
2821 (&Path::new(B_TXT), GitFileStatus::Added),
2822 ],
2823 );
2824
2825 let (project_local, _worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2826 let project_id = active_call_a
2827 .update(cx_a, |call, cx| {
2828 call.share_project(project_local.clone(), cx)
2829 })
2830 .await
2831 .unwrap();
2832
2833 let project_remote = client_b.build_remote_project(project_id, cx_b).await;
2834
2835 // Wait for it to catch up to the new status
2836 executor.run_until_parked();
2837
2838 #[track_caller]
2839 fn assert_status(
2840 file: &impl AsRef<Path>,
2841 status: Option<GitFileStatus>,
2842 project: &Project,
2843 cx: &AppContext,
2844 ) {
2845 let file = file.as_ref();
2846 let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
2847 assert_eq!(worktrees.len(), 1);
2848 let worktree = worktrees[0].clone();
2849 let snapshot = worktree.read(cx).snapshot();
2850 assert_eq!(snapshot.status_for_file(file), status);
2851 }
2852
2853 // Smoke test status reading
2854
2855 project_local.read_with(cx_a, |project, cx| {
2856 assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx);
2857 assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx);
2858 });
2859
2860 project_remote.read_with(cx_b, |project, cx| {
2861 assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx);
2862 assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx);
2863 });
2864
2865 client_a.fs().set_status_for_repo_via_working_copy_change(
2866 Path::new("/dir/.git"),
2867 &[
2868 (&Path::new(A_TXT), GitFileStatus::Modified),
2869 (&Path::new(B_TXT), GitFileStatus::Modified),
2870 ],
2871 );
2872
2873 // Wait for buffer_local_a to receive it
2874 executor.run_until_parked();
2875
2876 // Smoke test status reading
2877
2878 project_local.read_with(cx_a, |project, cx| {
2879 assert_status(
2880 &Path::new(A_TXT),
2881 Some(GitFileStatus::Modified),
2882 project,
2883 cx,
2884 );
2885 assert_status(
2886 &Path::new(B_TXT),
2887 Some(GitFileStatus::Modified),
2888 project,
2889 cx,
2890 );
2891 });
2892
2893 project_remote.read_with(cx_b, |project, cx| {
2894 assert_status(
2895 &Path::new(A_TXT),
2896 Some(GitFileStatus::Modified),
2897 project,
2898 cx,
2899 );
2900 assert_status(
2901 &Path::new(B_TXT),
2902 Some(GitFileStatus::Modified),
2903 project,
2904 cx,
2905 );
2906 });
2907
2908 // And synchronization while joining
2909 let project_remote_c = client_c.build_remote_project(project_id, cx_c).await;
2910 executor.run_until_parked();
2911
2912 project_remote_c.read_with(cx_c, |project, cx| {
2913 assert_status(
2914 &Path::new(A_TXT),
2915 Some(GitFileStatus::Modified),
2916 project,
2917 cx,
2918 );
2919 assert_status(
2920 &Path::new(B_TXT),
2921 Some(GitFileStatus::Modified),
2922 project,
2923 cx,
2924 );
2925 });
2926}
2927
2928#[gpui::test(iterations = 10)]
2929async fn test_fs_operations(
2930 executor: BackgroundExecutor,
2931 cx_a: &mut TestAppContext,
2932 cx_b: &mut TestAppContext,
2933) {
2934 let mut server = TestServer::start(executor.clone()).await;
2935 let client_a = server.create_client(cx_a, "user_a").await;
2936 let client_b = server.create_client(cx_b, "user_b").await;
2937 server
2938 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2939 .await;
2940 let active_call_a = cx_a.read(ActiveCall::global);
2941
2942 client_a
2943 .fs()
2944 .insert_tree(
2945 "/dir",
2946 json!({
2947 "a.txt": "a-contents",
2948 "b.txt": "b-contents",
2949 }),
2950 )
2951 .await;
2952 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2953 let project_id = active_call_a
2954 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2955 .await
2956 .unwrap();
2957 let project_b = client_b.build_remote_project(project_id, cx_b).await;
2958
2959 let worktree_a = project_a.read_with(cx_a, |project, _| project.worktrees().next().unwrap());
2960
2961 let worktree_b = project_b.read_with(cx_b, |project, _| project.worktrees().next().unwrap());
2962
2963 let entry = project_b
2964 .update(cx_b, |project, cx| {
2965 project.create_entry((worktree_id, "c.txt"), false, cx)
2966 })
2967 .await
2968 .unwrap()
2969 .unwrap();
2970
2971 worktree_a.read_with(cx_a, |worktree, _| {
2972 assert_eq!(
2973 worktree
2974 .paths()
2975 .map(|p| p.to_string_lossy())
2976 .collect::<Vec<_>>(),
2977 ["a.txt", "b.txt", "c.txt"]
2978 );
2979 });
2980
2981 worktree_b.read_with(cx_b, |worktree, _| {
2982 assert_eq!(
2983 worktree
2984 .paths()
2985 .map(|p| p.to_string_lossy())
2986 .collect::<Vec<_>>(),
2987 ["a.txt", "b.txt", "c.txt"]
2988 );
2989 });
2990
2991 project_b
2992 .update(cx_b, |project, cx| {
2993 project.rename_entry(entry.id, Path::new("d.txt"), cx)
2994 })
2995 .await
2996 .unwrap()
2997 .unwrap();
2998
2999 worktree_a.read_with(cx_a, |worktree, _| {
3000 assert_eq!(
3001 worktree
3002 .paths()
3003 .map(|p| p.to_string_lossy())
3004 .collect::<Vec<_>>(),
3005 ["a.txt", "b.txt", "d.txt"]
3006 );
3007 });
3008
3009 worktree_b.read_with(cx_b, |worktree, _| {
3010 assert_eq!(
3011 worktree
3012 .paths()
3013 .map(|p| p.to_string_lossy())
3014 .collect::<Vec<_>>(),
3015 ["a.txt", "b.txt", "d.txt"]
3016 );
3017 });
3018
3019 let dir_entry = project_b
3020 .update(cx_b, |project, cx| {
3021 project.create_entry((worktree_id, "DIR"), true, cx)
3022 })
3023 .await
3024 .unwrap()
3025 .unwrap();
3026
3027 worktree_a.read_with(cx_a, |worktree, _| {
3028 assert_eq!(
3029 worktree
3030 .paths()
3031 .map(|p| p.to_string_lossy())
3032 .collect::<Vec<_>>(),
3033 ["DIR", "a.txt", "b.txt", "d.txt"]
3034 );
3035 });
3036
3037 worktree_b.read_with(cx_b, |worktree, _| {
3038 assert_eq!(
3039 worktree
3040 .paths()
3041 .map(|p| p.to_string_lossy())
3042 .collect::<Vec<_>>(),
3043 ["DIR", "a.txt", "b.txt", "d.txt"]
3044 );
3045 });
3046
3047 project_b
3048 .update(cx_b, |project, cx| {
3049 project.create_entry((worktree_id, "DIR/e.txt"), false, cx)
3050 })
3051 .await
3052 .unwrap()
3053 .unwrap();
3054 project_b
3055 .update(cx_b, |project, cx| {
3056 project.create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
3057 })
3058 .await
3059 .unwrap()
3060 .unwrap();
3061 project_b
3062 .update(cx_b, |project, cx| {
3063 project.create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
3064 })
3065 .await
3066 .unwrap()
3067 .unwrap();
3068
3069 worktree_a.read_with(cx_a, |worktree, _| {
3070 assert_eq!(
3071 worktree
3072 .paths()
3073 .map(|p| p.to_string_lossy())
3074 .collect::<Vec<_>>(),
3075 [
3076 "DIR",
3077 "DIR/SUBDIR",
3078 "DIR/SUBDIR/f.txt",
3079 "DIR/e.txt",
3080 "a.txt",
3081 "b.txt",
3082 "d.txt"
3083 ]
3084 );
3085 });
3086
3087 worktree_b.read_with(cx_b, |worktree, _| {
3088 assert_eq!(
3089 worktree
3090 .paths()
3091 .map(|p| p.to_string_lossy())
3092 .collect::<Vec<_>>(),
3093 [
3094 "DIR",
3095 "DIR/SUBDIR",
3096 "DIR/SUBDIR/f.txt",
3097 "DIR/e.txt",
3098 "a.txt",
3099 "b.txt",
3100 "d.txt"
3101 ]
3102 );
3103 });
3104
3105 project_b
3106 .update(cx_b, |project, cx| {
3107 project.copy_entry(entry.id, Path::new("f.txt"), cx)
3108 })
3109 .await
3110 .unwrap()
3111 .unwrap();
3112
3113 worktree_a.read_with(cx_a, |worktree, _| {
3114 assert_eq!(
3115 worktree
3116 .paths()
3117 .map(|p| p.to_string_lossy())
3118 .collect::<Vec<_>>(),
3119 [
3120 "DIR",
3121 "DIR/SUBDIR",
3122 "DIR/SUBDIR/f.txt",
3123 "DIR/e.txt",
3124 "a.txt",
3125 "b.txt",
3126 "d.txt",
3127 "f.txt"
3128 ]
3129 );
3130 });
3131
3132 worktree_b.read_with(cx_b, |worktree, _| {
3133 assert_eq!(
3134 worktree
3135 .paths()
3136 .map(|p| p.to_string_lossy())
3137 .collect::<Vec<_>>(),
3138 [
3139 "DIR",
3140 "DIR/SUBDIR",
3141 "DIR/SUBDIR/f.txt",
3142 "DIR/e.txt",
3143 "a.txt",
3144 "b.txt",
3145 "d.txt",
3146 "f.txt"
3147 ]
3148 );
3149 });
3150
3151 project_b
3152 .update(cx_b, |project, cx| {
3153 project.delete_entry(dir_entry.id, cx).unwrap()
3154 })
3155 .await
3156 .unwrap();
3157 executor.run_until_parked();
3158
3159 worktree_a.read_with(cx_a, |worktree, _| {
3160 assert_eq!(
3161 worktree
3162 .paths()
3163 .map(|p| p.to_string_lossy())
3164 .collect::<Vec<_>>(),
3165 ["a.txt", "b.txt", "d.txt", "f.txt"]
3166 );
3167 });
3168
3169 worktree_b.read_with(cx_b, |worktree, _| {
3170 assert_eq!(
3171 worktree
3172 .paths()
3173 .map(|p| p.to_string_lossy())
3174 .collect::<Vec<_>>(),
3175 ["a.txt", "b.txt", "d.txt", "f.txt"]
3176 );
3177 });
3178
3179 project_b
3180 .update(cx_b, |project, cx| {
3181 project.delete_entry(entry.id, cx).unwrap()
3182 })
3183 .await
3184 .unwrap();
3185
3186 worktree_a.read_with(cx_a, |worktree, _| {
3187 assert_eq!(
3188 worktree
3189 .paths()
3190 .map(|p| p.to_string_lossy())
3191 .collect::<Vec<_>>(),
3192 ["a.txt", "b.txt", "f.txt"]
3193 );
3194 });
3195
3196 worktree_b.read_with(cx_b, |worktree, _| {
3197 assert_eq!(
3198 worktree
3199 .paths()
3200 .map(|p| p.to_string_lossy())
3201 .collect::<Vec<_>>(),
3202 ["a.txt", "b.txt", "f.txt"]
3203 );
3204 });
3205}
3206
3207#[gpui::test(iterations = 10)]
3208async fn test_local_settings(
3209 executor: BackgroundExecutor,
3210 cx_a: &mut TestAppContext,
3211 cx_b: &mut TestAppContext,
3212) {
3213 let mut server = TestServer::start(executor.clone()).await;
3214 let client_a = server.create_client(cx_a, "user_a").await;
3215 let client_b = server.create_client(cx_b, "user_b").await;
3216 server
3217 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3218 .await;
3219 let active_call_a = cx_a.read(ActiveCall::global);
3220
3221 // As client A, open a project that contains some local settings files
3222 client_a
3223 .fs()
3224 .insert_tree(
3225 "/dir",
3226 json!({
3227 ".zed": {
3228 "settings.json": r#"{ "tab_size": 2 }"#
3229 },
3230 "a": {
3231 ".zed": {
3232 "settings.json": r#"{ "tab_size": 8 }"#
3233 },
3234 "a.txt": "a-contents",
3235 },
3236 "b": {
3237 "b.txt": "b-contents",
3238 }
3239 }),
3240 )
3241 .await;
3242 let (project_a, _) = client_a.build_local_project("/dir", cx_a).await;
3243 executor.run_until_parked();
3244 let project_id = active_call_a
3245 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3246 .await
3247 .unwrap();
3248 executor.run_until_parked();
3249
3250 // As client B, join that project and observe the local settings.
3251 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3252
3253 let worktree_b = project_b.read_with(cx_b, |project, _| project.worktrees().next().unwrap());
3254 executor.run_until_parked();
3255 cx_b.read(|cx| {
3256 let store = cx.global::<SettingsStore>();
3257 assert_eq!(
3258 store
3259 .local_settings(worktree_b.read(cx).id().to_usize())
3260 .collect::<Vec<_>>(),
3261 &[
3262 (Path::new("").into(), r#"{"tab_size":2}"#.to_string()),
3263 (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
3264 ]
3265 )
3266 });
3267
3268 // As client A, update a settings file. As Client B, see the changed settings.
3269 client_a
3270 .fs()
3271 .insert_file("/dir/.zed/settings.json", r#"{}"#.into())
3272 .await;
3273 executor.run_until_parked();
3274 cx_b.read(|cx| {
3275 let store = cx.global::<SettingsStore>();
3276 assert_eq!(
3277 store
3278 .local_settings(worktree_b.read(cx).id().to_usize())
3279 .collect::<Vec<_>>(),
3280 &[
3281 (Path::new("").into(), r#"{}"#.to_string()),
3282 (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
3283 ]
3284 )
3285 });
3286
3287 // As client A, create and remove some settings files. As client B, see the changed settings.
3288 client_a
3289 .fs()
3290 .remove_file("/dir/.zed/settings.json".as_ref(), Default::default())
3291 .await
3292 .unwrap();
3293 client_a
3294 .fs()
3295 .create_dir("/dir/b/.zed".as_ref())
3296 .await
3297 .unwrap();
3298 client_a
3299 .fs()
3300 .insert_file("/dir/b/.zed/settings.json", r#"{"tab_size": 4}"#.into())
3301 .await;
3302 executor.run_until_parked();
3303 cx_b.read(|cx| {
3304 let store = cx.global::<SettingsStore>();
3305 assert_eq!(
3306 store
3307 .local_settings(worktree_b.read(cx).id().to_usize())
3308 .collect::<Vec<_>>(),
3309 &[
3310 (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
3311 (Path::new("b").into(), r#"{"tab_size":4}"#.to_string()),
3312 ]
3313 )
3314 });
3315
3316 // As client B, disconnect.
3317 server.forbid_connections();
3318 server.disconnect_client(client_b.peer_id().unwrap());
3319
3320 // As client A, change and remove settings files while client B is disconnected.
3321 client_a
3322 .fs()
3323 .insert_file("/dir/a/.zed/settings.json", r#"{"hard_tabs":true}"#.into())
3324 .await;
3325 client_a
3326 .fs()
3327 .remove_file("/dir/b/.zed/settings.json".as_ref(), Default::default())
3328 .await
3329 .unwrap();
3330 executor.run_until_parked();
3331
3332 // As client B, reconnect and see the changed settings.
3333 server.allow_connections();
3334 executor.advance_clock(RECEIVE_TIMEOUT);
3335 cx_b.read(|cx| {
3336 let store = cx.global::<SettingsStore>();
3337 assert_eq!(
3338 store
3339 .local_settings(worktree_b.read(cx).id().to_usize())
3340 .collect::<Vec<_>>(),
3341 &[(Path::new("a").into(), r#"{"hard_tabs":true}"#.to_string()),]
3342 )
3343 });
3344}
3345
3346#[gpui::test(iterations = 10)]
3347async fn test_buffer_conflict_after_save(
3348 executor: BackgroundExecutor,
3349 cx_a: &mut TestAppContext,
3350 cx_b: &mut TestAppContext,
3351) {
3352 let mut server = TestServer::start(executor.clone()).await;
3353 let client_a = server.create_client(cx_a, "user_a").await;
3354 let client_b = server.create_client(cx_b, "user_b").await;
3355 server
3356 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3357 .await;
3358 let active_call_a = cx_a.read(ActiveCall::global);
3359
3360 client_a
3361 .fs()
3362 .insert_tree(
3363 "/dir",
3364 json!({
3365 "a.txt": "a-contents",
3366 }),
3367 )
3368 .await;
3369 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3370 let project_id = active_call_a
3371 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3372 .await
3373 .unwrap();
3374 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3375
3376 // Open a buffer as client B
3377 let buffer_b = project_b
3378 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3379 .await
3380 .unwrap();
3381
3382 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "world ")], None, cx));
3383
3384 buffer_b.read_with(cx_b, |buf, _| {
3385 assert!(buf.is_dirty());
3386 assert!(!buf.has_conflict());
3387 });
3388
3389 project_b
3390 .update(cx_b, |project, cx| {
3391 project.save_buffer(buffer_b.clone(), cx)
3392 })
3393 .await
3394 .unwrap();
3395
3396 buffer_b.read_with(cx_b, |buffer_b, _| assert!(!buffer_b.is_dirty()));
3397
3398 buffer_b.read_with(cx_b, |buf, _| {
3399 assert!(!buf.has_conflict());
3400 });
3401
3402 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "hello ")], None, cx));
3403
3404 buffer_b.read_with(cx_b, |buf, _| {
3405 assert!(buf.is_dirty());
3406 assert!(!buf.has_conflict());
3407 });
3408}
3409
3410#[gpui::test(iterations = 10)]
3411async fn test_buffer_reloading(
3412 executor: BackgroundExecutor,
3413 cx_a: &mut TestAppContext,
3414 cx_b: &mut TestAppContext,
3415) {
3416 let mut server = TestServer::start(executor.clone()).await;
3417 let client_a = server.create_client(cx_a, "user_a").await;
3418 let client_b = server.create_client(cx_b, "user_b").await;
3419 server
3420 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3421 .await;
3422 let active_call_a = cx_a.read(ActiveCall::global);
3423
3424 client_a
3425 .fs()
3426 .insert_tree(
3427 "/dir",
3428 json!({
3429 "a.txt": "a\nb\nc",
3430 }),
3431 )
3432 .await;
3433 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3434 let project_id = active_call_a
3435 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3436 .await
3437 .unwrap();
3438 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3439
3440 // Open a buffer as client B
3441 let buffer_b = project_b
3442 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3443 .await
3444 .unwrap();
3445
3446 buffer_b.read_with(cx_b, |buf, _| {
3447 assert!(!buf.is_dirty());
3448 assert!(!buf.has_conflict());
3449 assert_eq!(buf.line_ending(), LineEnding::Unix);
3450 });
3451
3452 let new_contents = Rope::from("d\ne\nf");
3453 client_a
3454 .fs()
3455 .save("/dir/a.txt".as_ref(), &new_contents, LineEnding::Windows)
3456 .await
3457 .unwrap();
3458
3459 executor.run_until_parked();
3460
3461 buffer_b.read_with(cx_b, |buf, _| {
3462 assert_eq!(buf.text(), new_contents.to_string());
3463 assert!(!buf.is_dirty());
3464 assert!(!buf.has_conflict());
3465 assert_eq!(buf.line_ending(), LineEnding::Windows);
3466 });
3467}
3468
3469#[gpui::test(iterations = 10)]
3470async fn test_editing_while_guest_opens_buffer(
3471 executor: BackgroundExecutor,
3472 cx_a: &mut TestAppContext,
3473 cx_b: &mut TestAppContext,
3474) {
3475 let mut server = TestServer::start(executor.clone()).await;
3476 let client_a = server.create_client(cx_a, "user_a").await;
3477 let client_b = server.create_client(cx_b, "user_b").await;
3478 server
3479 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3480 .await;
3481 let active_call_a = cx_a.read(ActiveCall::global);
3482
3483 client_a
3484 .fs()
3485 .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
3486 .await;
3487 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3488 let project_id = active_call_a
3489 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3490 .await
3491 .unwrap();
3492 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3493
3494 // Open a buffer as client A
3495 let buffer_a = project_a
3496 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3497 .await
3498 .unwrap();
3499
3500 // Start opening the same buffer as client B
3501 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx));
3502 let buffer_b = cx_b.executor().spawn(open_buffer);
3503
3504 // Edit the buffer as client A while client B is still opening it.
3505 cx_b.executor().simulate_random_delay().await;
3506 buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "X")], None, cx));
3507 cx_b.executor().simulate_random_delay().await;
3508 buffer_a.update(cx_a, |buf, cx| buf.edit([(1..1, "Y")], None, cx));
3509
3510 let text = buffer_a.read_with(cx_a, |buf, _| buf.text());
3511 let buffer_b = buffer_b.await.unwrap();
3512 executor.run_until_parked();
3513
3514 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), text));
3515}
3516
3517#[gpui::test(iterations = 10)]
3518async fn test_leaving_worktree_while_opening_buffer(
3519 executor: BackgroundExecutor,
3520 cx_a: &mut TestAppContext,
3521 cx_b: &mut TestAppContext,
3522) {
3523 let mut server = TestServer::start(executor.clone()).await;
3524 let client_a = server.create_client(cx_a, "user_a").await;
3525 let client_b = server.create_client(cx_b, "user_b").await;
3526 server
3527 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3528 .await;
3529 let active_call_a = cx_a.read(ActiveCall::global);
3530
3531 client_a
3532 .fs()
3533 .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
3534 .await;
3535 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3536 let project_id = active_call_a
3537 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3538 .await
3539 .unwrap();
3540 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3541
3542 // See that a guest has joined as client A.
3543 executor.run_until_parked();
3544
3545 project_a.read_with(cx_a, |p, _| assert_eq!(p.collaborators().len(), 1));
3546
3547 // Begin opening a buffer as client B, but leave the project before the open completes.
3548 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx));
3549 let buffer_b = cx_b.executor().spawn(open_buffer);
3550 cx_b.update(|_| drop(project_b));
3551 drop(buffer_b);
3552
3553 // See that the guest has left.
3554 executor.run_until_parked();
3555
3556 project_a.read_with(cx_a, |p, _| assert!(p.collaborators().is_empty()));
3557}
3558
3559#[gpui::test(iterations = 10)]
3560async fn test_canceling_buffer_opening(
3561 executor: BackgroundExecutor,
3562 cx_a: &mut TestAppContext,
3563 cx_b: &mut TestAppContext,
3564) {
3565 let mut server = TestServer::start(executor.clone()).await;
3566 let client_a = server.create_client(cx_a, "user_a").await;
3567 let client_b = server.create_client(cx_b, "user_b").await;
3568 server
3569 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3570 .await;
3571 let active_call_a = cx_a.read(ActiveCall::global);
3572
3573 client_a
3574 .fs()
3575 .insert_tree(
3576 "/dir",
3577 json!({
3578 "a.txt": "abc",
3579 }),
3580 )
3581 .await;
3582 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3583 let project_id = active_call_a
3584 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3585 .await
3586 .unwrap();
3587 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3588
3589 let buffer_a = project_a
3590 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3591 .await
3592 .unwrap();
3593
3594 // Open a buffer as client B but cancel after a random amount of time.
3595 let buffer_b = project_b.update(cx_b, |p, cx| {
3596 p.open_buffer_by_id(buffer_a.read_with(cx_a, |a, _| a.remote_id()), cx)
3597 });
3598 executor.simulate_random_delay().await;
3599 drop(buffer_b);
3600
3601 // Try opening the same buffer again as client B, and ensure we can
3602 // still do it despite the cancellation above.
3603 let buffer_b = project_b
3604 .update(cx_b, |p, cx| {
3605 p.open_buffer_by_id(buffer_a.read_with(cx_a, |a, _| a.remote_id()), cx)
3606 })
3607 .await
3608 .unwrap();
3609
3610 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "abc"));
3611}
3612
3613#[gpui::test(iterations = 10)]
3614async fn test_leaving_project(
3615 executor: BackgroundExecutor,
3616 cx_a: &mut TestAppContext,
3617 cx_b: &mut TestAppContext,
3618 cx_c: &mut TestAppContext,
3619) {
3620 let mut server = TestServer::start(executor.clone()).await;
3621 let client_a = server.create_client(cx_a, "user_a").await;
3622 let client_b = server.create_client(cx_b, "user_b").await;
3623 let client_c = server.create_client(cx_c, "user_c").await;
3624 server
3625 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
3626 .await;
3627 let active_call_a = cx_a.read(ActiveCall::global);
3628
3629 client_a
3630 .fs()
3631 .insert_tree(
3632 "/a",
3633 json!({
3634 "a.txt": "a-contents",
3635 "b.txt": "b-contents",
3636 }),
3637 )
3638 .await;
3639 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
3640 let project_id = active_call_a
3641 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3642 .await
3643 .unwrap();
3644 let project_b1 = client_b.build_remote_project(project_id, cx_b).await;
3645 let project_c = client_c.build_remote_project(project_id, cx_c).await;
3646
3647 // Client A sees that a guest has joined.
3648 executor.run_until_parked();
3649
3650 project_a.read_with(cx_a, |project, _| {
3651 assert_eq!(project.collaborators().len(), 2);
3652 });
3653
3654 project_b1.read_with(cx_b, |project, _| {
3655 assert_eq!(project.collaborators().len(), 2);
3656 });
3657
3658 project_c.read_with(cx_c, |project, _| {
3659 assert_eq!(project.collaborators().len(), 2);
3660 });
3661
3662 // Client B opens a buffer.
3663 let buffer_b1 = project_b1
3664 .update(cx_b, |project, cx| {
3665 let worktree_id = project.worktrees().next().unwrap().read(cx).id();
3666 project.open_buffer((worktree_id, "a.txt"), cx)
3667 })
3668 .await
3669 .unwrap();
3670
3671 buffer_b1.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
3672
3673 // Drop client B's project and ensure client A and client C observe client B leaving.
3674 cx_b.update(|_| drop(project_b1));
3675 executor.run_until_parked();
3676
3677 project_a.read_with(cx_a, |project, _| {
3678 assert_eq!(project.collaborators().len(), 1);
3679 });
3680
3681 project_c.read_with(cx_c, |project, _| {
3682 assert_eq!(project.collaborators().len(), 1);
3683 });
3684
3685 // Client B re-joins the project and can open buffers as before.
3686 let project_b2 = client_b.build_remote_project(project_id, cx_b).await;
3687 executor.run_until_parked();
3688
3689 project_a.read_with(cx_a, |project, _| {
3690 assert_eq!(project.collaborators().len(), 2);
3691 });
3692
3693 project_b2.read_with(cx_b, |project, _| {
3694 assert_eq!(project.collaborators().len(), 2);
3695 });
3696
3697 project_c.read_with(cx_c, |project, _| {
3698 assert_eq!(project.collaborators().len(), 2);
3699 });
3700
3701 let buffer_b2 = project_b2
3702 .update(cx_b, |project, cx| {
3703 let worktree_id = project.worktrees().next().unwrap().read(cx).id();
3704 project.open_buffer((worktree_id, "a.txt"), cx)
3705 })
3706 .await
3707 .unwrap();
3708
3709 buffer_b2.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
3710
3711 // Drop client B's connection and ensure client A and client C observe client B leaving.
3712 client_b.disconnect(&cx_b.to_async());
3713 executor.advance_clock(RECONNECT_TIMEOUT);
3714
3715 project_a.read_with(cx_a, |project, _| {
3716 assert_eq!(project.collaborators().len(), 1);
3717 });
3718
3719 project_b2.read_with(cx_b, |project, _| {
3720 assert!(project.is_disconnected());
3721 });
3722
3723 project_c.read_with(cx_c, |project, _| {
3724 assert_eq!(project.collaborators().len(), 1);
3725 });
3726
3727 // Client B can't join the project, unless they re-join the room.
3728 cx_b.spawn(|cx| {
3729 Project::remote(
3730 project_id,
3731 client_b.app_state.client.clone(),
3732 client_b.user_store().clone(),
3733 client_b.language_registry().clone(),
3734 FakeFs::new(cx.background_executor().clone()),
3735 ChannelRole::Member,
3736 cx,
3737 )
3738 })
3739 .await
3740 .unwrap_err();
3741
3742 // Simulate connection loss for client C and ensure client A observes client C leaving the project.
3743 client_c.wait_for_current_user(cx_c).await;
3744 server.forbid_connections();
3745 server.disconnect_client(client_c.peer_id().unwrap());
3746 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
3747 executor.run_until_parked();
3748
3749 project_a.read_with(cx_a, |project, _| {
3750 assert_eq!(project.collaborators().len(), 0);
3751 });
3752
3753 project_b2.read_with(cx_b, |project, _| {
3754 assert!(project.is_disconnected());
3755 });
3756
3757 project_c.read_with(cx_c, |project, _| {
3758 assert!(project.is_disconnected());
3759 });
3760}
3761
3762#[gpui::test(iterations = 10)]
3763async fn test_collaborating_with_diagnostics(
3764 executor: BackgroundExecutor,
3765 cx_a: &mut TestAppContext,
3766 cx_b: &mut TestAppContext,
3767 cx_c: &mut TestAppContext,
3768) {
3769 let mut server = TestServer::start(executor.clone()).await;
3770 let client_a = server.create_client(cx_a, "user_a").await;
3771 let client_b = server.create_client(cx_b, "user_b").await;
3772 let client_c = server.create_client(cx_c, "user_c").await;
3773 server
3774 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
3775 .await;
3776 let active_call_a = cx_a.read(ActiveCall::global);
3777
3778 // Set up a fake language server.
3779 let mut language = Language::new(
3780 LanguageConfig {
3781 name: "Rust".into(),
3782 path_suffixes: vec!["rs".to_string()],
3783 ..Default::default()
3784 },
3785 Some(tree_sitter_rust::language()),
3786 );
3787 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
3788 client_a.language_registry().add(Arc::new(language));
3789
3790 // Share a project as client A
3791 client_a
3792 .fs()
3793 .insert_tree(
3794 "/a",
3795 json!({
3796 "a.rs": "let one = two",
3797 "other.rs": "",
3798 }),
3799 )
3800 .await;
3801 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
3802
3803 // Cause the language server to start.
3804 let _buffer = project_a
3805 .update(cx_a, |project, cx| {
3806 project.open_buffer(
3807 ProjectPath {
3808 worktree_id,
3809 path: Path::new("other.rs").into(),
3810 },
3811 cx,
3812 )
3813 })
3814 .await
3815 .unwrap();
3816
3817 // Simulate a language server reporting errors for a file.
3818 let mut fake_language_server = fake_language_servers.next().await.unwrap();
3819 fake_language_server
3820 .receive_notification::<lsp::notification::DidOpenTextDocument>()
3821 .await;
3822 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3823 lsp::PublishDiagnosticsParams {
3824 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3825 version: None,
3826 diagnostics: vec![lsp::Diagnostic {
3827 severity: Some(lsp::DiagnosticSeverity::WARNING),
3828 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3829 message: "message 0".to_string(),
3830 ..Default::default()
3831 }],
3832 },
3833 );
3834
3835 // Client A shares the project and, simultaneously, the language server
3836 // publishes a diagnostic. This is done to ensure that the server always
3837 // observes the latest diagnostics for a worktree.
3838 let project_id = active_call_a
3839 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3840 .await
3841 .unwrap();
3842 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3843 lsp::PublishDiagnosticsParams {
3844 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3845 version: None,
3846 diagnostics: vec![lsp::Diagnostic {
3847 severity: Some(lsp::DiagnosticSeverity::ERROR),
3848 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3849 message: "message 1".to_string(),
3850 ..Default::default()
3851 }],
3852 },
3853 );
3854
3855 // Join the worktree as client B.
3856 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3857
3858 // Wait for server to see the diagnostics update.
3859 executor.run_until_parked();
3860
3861 // Ensure client B observes the new diagnostics.
3862
3863 project_b.read_with(cx_b, |project, cx| {
3864 assert_eq!(
3865 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
3866 &[(
3867 ProjectPath {
3868 worktree_id,
3869 path: Arc::from(Path::new("a.rs")),
3870 },
3871 LanguageServerId(0),
3872 DiagnosticSummary {
3873 error_count: 1,
3874 warning_count: 0,
3875 ..Default::default()
3876 },
3877 )]
3878 )
3879 });
3880
3881 // Join project as client C and observe the diagnostics.
3882 let project_c = client_c.build_remote_project(project_id, cx_c).await;
3883 let project_c_diagnostic_summaries =
3884 Rc::new(RefCell::new(project_c.read_with(cx_c, |project, cx| {
3885 project.diagnostic_summaries(false, cx).collect::<Vec<_>>()
3886 })));
3887 project_c.update(cx_c, |_, cx| {
3888 let summaries = project_c_diagnostic_summaries.clone();
3889 cx.subscribe(&project_c, {
3890 move |p, _, event, cx| {
3891 if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
3892 *summaries.borrow_mut() = p.diagnostic_summaries(false, cx).collect();
3893 }
3894 }
3895 })
3896 .detach();
3897 });
3898
3899 executor.run_until_parked();
3900 assert_eq!(
3901 project_c_diagnostic_summaries.borrow().as_slice(),
3902 &[(
3903 ProjectPath {
3904 worktree_id,
3905 path: Arc::from(Path::new("a.rs")),
3906 },
3907 LanguageServerId(0),
3908 DiagnosticSummary {
3909 error_count: 1,
3910 warning_count: 0,
3911 ..Default::default()
3912 },
3913 )]
3914 );
3915
3916 // Simulate a language server reporting more errors for a file.
3917 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3918 lsp::PublishDiagnosticsParams {
3919 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3920 version: None,
3921 diagnostics: vec![
3922 lsp::Diagnostic {
3923 severity: Some(lsp::DiagnosticSeverity::ERROR),
3924 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3925 message: "message 1".to_string(),
3926 ..Default::default()
3927 },
3928 lsp::Diagnostic {
3929 severity: Some(lsp::DiagnosticSeverity::WARNING),
3930 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 13)),
3931 message: "message 2".to_string(),
3932 ..Default::default()
3933 },
3934 ],
3935 },
3936 );
3937
3938 // Clients B and C get the updated summaries
3939 executor.run_until_parked();
3940
3941 project_b.read_with(cx_b, |project, cx| {
3942 assert_eq!(
3943 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
3944 [(
3945 ProjectPath {
3946 worktree_id,
3947 path: Arc::from(Path::new("a.rs")),
3948 },
3949 LanguageServerId(0),
3950 DiagnosticSummary {
3951 error_count: 1,
3952 warning_count: 1,
3953 },
3954 )]
3955 );
3956 });
3957
3958 project_c.read_with(cx_c, |project, cx| {
3959 assert_eq!(
3960 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
3961 [(
3962 ProjectPath {
3963 worktree_id,
3964 path: Arc::from(Path::new("a.rs")),
3965 },
3966 LanguageServerId(0),
3967 DiagnosticSummary {
3968 error_count: 1,
3969 warning_count: 1,
3970 },
3971 )]
3972 );
3973 });
3974
3975 // Open the file with the errors on client B. They should be present.
3976 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
3977 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
3978
3979 buffer_b.read_with(cx_b, |buffer, _| {
3980 assert_eq!(
3981 buffer
3982 .snapshot()
3983 .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
3984 .collect::<Vec<_>>(),
3985 &[
3986 DiagnosticEntry {
3987 range: Point::new(0, 4)..Point::new(0, 7),
3988 diagnostic: Diagnostic {
3989 group_id: 2,
3990 message: "message 1".to_string(),
3991 severity: lsp::DiagnosticSeverity::ERROR,
3992 is_primary: true,
3993 ..Default::default()
3994 }
3995 },
3996 DiagnosticEntry {
3997 range: Point::new(0, 10)..Point::new(0, 13),
3998 diagnostic: Diagnostic {
3999 group_id: 3,
4000 severity: lsp::DiagnosticSeverity::WARNING,
4001 message: "message 2".to_string(),
4002 is_primary: true,
4003 ..Default::default()
4004 }
4005 }
4006 ]
4007 );
4008 });
4009
4010 // Simulate a language server reporting no errors for a file.
4011 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
4012 lsp::PublishDiagnosticsParams {
4013 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
4014 version: None,
4015 diagnostics: vec![],
4016 },
4017 );
4018 executor.run_until_parked();
4019
4020 project_a.read_with(cx_a, |project, cx| {
4021 assert_eq!(
4022 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4023 []
4024 )
4025 });
4026
4027 project_b.read_with(cx_b, |project, cx| {
4028 assert_eq!(
4029 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4030 []
4031 )
4032 });
4033
4034 project_c.read_with(cx_c, |project, cx| {
4035 assert_eq!(
4036 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4037 []
4038 )
4039 });
4040}
4041
4042#[gpui::test(iterations = 10)]
4043async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
4044 executor: BackgroundExecutor,
4045 cx_a: &mut TestAppContext,
4046 cx_b: &mut TestAppContext,
4047) {
4048 let mut server = TestServer::start(executor.clone()).await;
4049 let client_a = server.create_client(cx_a, "user_a").await;
4050 let client_b = server.create_client(cx_b, "user_b").await;
4051 server
4052 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4053 .await;
4054
4055 // Set up a fake language server.
4056 let mut language = Language::new(
4057 LanguageConfig {
4058 name: "Rust".into(),
4059 path_suffixes: vec!["rs".to_string()],
4060 ..Default::default()
4061 },
4062 Some(tree_sitter_rust::language()),
4063 );
4064 let mut fake_language_servers = language
4065 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
4066 disk_based_diagnostics_progress_token: Some("the-disk-based-token".into()),
4067 disk_based_diagnostics_sources: vec!["the-disk-based-diagnostics-source".into()],
4068 ..Default::default()
4069 }))
4070 .await;
4071 client_a.language_registry().add(Arc::new(language));
4072
4073 let file_names = &["one.rs", "two.rs", "three.rs", "four.rs", "five.rs"];
4074 client_a
4075 .fs()
4076 .insert_tree(
4077 "/test",
4078 json!({
4079 "one.rs": "const ONE: usize = 1;",
4080 "two.rs": "const TWO: usize = 2;",
4081 "three.rs": "const THREE: usize = 3;",
4082 "four.rs": "const FOUR: usize = 3;",
4083 "five.rs": "const FIVE: usize = 3;",
4084 }),
4085 )
4086 .await;
4087
4088 let (project_a, worktree_id) = client_a.build_local_project("/test", cx_a).await;
4089
4090 // Share a project as client A
4091 let active_call_a = cx_a.read(ActiveCall::global);
4092 let project_id = active_call_a
4093 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4094 .await
4095 .unwrap();
4096
4097 // Join the project as client B and open all three files.
4098 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4099 let guest_buffers = futures::future::try_join_all(file_names.iter().map(|file_name| {
4100 project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, file_name), cx))
4101 }))
4102 .await
4103 .unwrap();
4104
4105 // Simulate a language server reporting errors for a file.
4106 let fake_language_server = fake_language_servers.next().await.unwrap();
4107 fake_language_server
4108 .request::<lsp::request::WorkDoneProgressCreate>(lsp::WorkDoneProgressCreateParams {
4109 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
4110 })
4111 .await
4112 .unwrap();
4113 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
4114 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
4115 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
4116 lsp::WorkDoneProgressBegin {
4117 title: "Progress Began".into(),
4118 ..Default::default()
4119 },
4120 )),
4121 });
4122 for file_name in file_names {
4123 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
4124 lsp::PublishDiagnosticsParams {
4125 uri: lsp::Url::from_file_path(Path::new("/test").join(file_name)).unwrap(),
4126 version: None,
4127 diagnostics: vec![lsp::Diagnostic {
4128 severity: Some(lsp::DiagnosticSeverity::WARNING),
4129 source: Some("the-disk-based-diagnostics-source".into()),
4130 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
4131 message: "message one".to_string(),
4132 ..Default::default()
4133 }],
4134 },
4135 );
4136 }
4137 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
4138 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
4139 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
4140 lsp::WorkDoneProgressEnd { message: None },
4141 )),
4142 });
4143
4144 // When the "disk base diagnostics finished" message is received, the buffers'
4145 // diagnostics are expected to be present.
4146 let disk_based_diagnostics_finished = Arc::new(AtomicBool::new(false));
4147 project_b.update(cx_b, {
4148 let project_b = project_b.clone();
4149 let disk_based_diagnostics_finished = disk_based_diagnostics_finished.clone();
4150 move |_, cx| {
4151 cx.subscribe(&project_b, move |_, _, event, cx| {
4152 if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
4153 disk_based_diagnostics_finished.store(true, SeqCst);
4154 for buffer in &guest_buffers {
4155 assert_eq!(
4156 buffer
4157 .read(cx)
4158 .snapshot()
4159 .diagnostics_in_range::<_, usize>(0..5, false)
4160 .count(),
4161 1,
4162 "expected a diagnostic for buffer {:?}",
4163 buffer.read(cx).file().unwrap().path(),
4164 );
4165 }
4166 }
4167 })
4168 .detach();
4169 }
4170 });
4171
4172 executor.run_until_parked();
4173 assert!(disk_based_diagnostics_finished.load(SeqCst));
4174}
4175
4176#[gpui::test(iterations = 10)]
4177async fn test_reloading_buffer_manually(
4178 executor: BackgroundExecutor,
4179 cx_a: &mut TestAppContext,
4180 cx_b: &mut TestAppContext,
4181) {
4182 let mut server = TestServer::start(executor.clone()).await;
4183 let client_a = server.create_client(cx_a, "user_a").await;
4184 let client_b = server.create_client(cx_b, "user_b").await;
4185 server
4186 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4187 .await;
4188 let active_call_a = cx_a.read(ActiveCall::global);
4189
4190 client_a
4191 .fs()
4192 .insert_tree("/a", json!({ "a.rs": "let one = 1;" }))
4193 .await;
4194 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
4195 let buffer_a = project_a
4196 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
4197 .await
4198 .unwrap();
4199 let project_id = active_call_a
4200 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4201 .await
4202 .unwrap();
4203
4204 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4205
4206 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
4207 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4208 buffer_b.update(cx_b, |buffer, cx| {
4209 buffer.edit([(4..7, "six")], None, cx);
4210 buffer.edit([(10..11, "6")], None, cx);
4211 assert_eq!(buffer.text(), "let six = 6;");
4212 assert!(buffer.is_dirty());
4213 assert!(!buffer.has_conflict());
4214 });
4215 executor.run_until_parked();
4216
4217 buffer_a.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "let six = 6;"));
4218
4219 client_a
4220 .fs()
4221 .save(
4222 "/a/a.rs".as_ref(),
4223 &Rope::from("let seven = 7;"),
4224 LineEnding::Unix,
4225 )
4226 .await
4227 .unwrap();
4228 executor.run_until_parked();
4229
4230 buffer_a.read_with(cx_a, |buffer, _| assert!(buffer.has_conflict()));
4231
4232 buffer_b.read_with(cx_b, |buffer, _| assert!(buffer.has_conflict()));
4233
4234 project_b
4235 .update(cx_b, |project, cx| {
4236 project.reload_buffers(HashSet::from_iter([buffer_b.clone()]), true, cx)
4237 })
4238 .await
4239 .unwrap();
4240
4241 buffer_a.read_with(cx_a, |buffer, _| {
4242 assert_eq!(buffer.text(), "let seven = 7;");
4243 assert!(!buffer.is_dirty());
4244 assert!(!buffer.has_conflict());
4245 });
4246
4247 buffer_b.read_with(cx_b, |buffer, _| {
4248 assert_eq!(buffer.text(), "let seven = 7;");
4249 assert!(!buffer.is_dirty());
4250 assert!(!buffer.has_conflict());
4251 });
4252
4253 buffer_a.update(cx_a, |buffer, cx| {
4254 // Undoing on the host is a no-op when the reload was initiated by the guest.
4255 buffer.undo(cx);
4256 assert_eq!(buffer.text(), "let seven = 7;");
4257 assert!(!buffer.is_dirty());
4258 assert!(!buffer.has_conflict());
4259 });
4260 buffer_b.update(cx_b, |buffer, cx| {
4261 // Undoing on the guest rolls back the buffer to before it was reloaded but the conflict gets cleared.
4262 buffer.undo(cx);
4263 assert_eq!(buffer.text(), "let six = 6;");
4264 assert!(buffer.is_dirty());
4265 assert!(!buffer.has_conflict());
4266 });
4267}
4268
4269#[gpui::test(iterations = 10)]
4270async fn test_formatting_buffer(
4271 executor: BackgroundExecutor,
4272 cx_a: &mut TestAppContext,
4273 cx_b: &mut TestAppContext,
4274) {
4275 executor.allow_parking();
4276 let mut server = TestServer::start(executor.clone()).await;
4277 let client_a = server.create_client(cx_a, "user_a").await;
4278 let client_b = server.create_client(cx_b, "user_b").await;
4279 server
4280 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4281 .await;
4282 let active_call_a = cx_a.read(ActiveCall::global);
4283
4284 // Set up a fake language server.
4285 let mut language = Language::new(
4286 LanguageConfig {
4287 name: "Rust".into(),
4288 path_suffixes: vec!["rs".to_string()],
4289 ..Default::default()
4290 },
4291 Some(tree_sitter_rust::language()),
4292 );
4293 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4294 client_a.language_registry().add(Arc::new(language));
4295
4296 // Here we insert a fake tree with a directory that exists on disk. This is needed
4297 // because later we'll invoke a command, which requires passing a working directory
4298 // that points to a valid location on disk.
4299 let directory = env::current_dir().unwrap();
4300 client_a
4301 .fs()
4302 .insert_tree(&directory, json!({ "a.rs": "let one = \"two\"" }))
4303 .await;
4304 let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
4305 let project_id = active_call_a
4306 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4307 .await
4308 .unwrap();
4309 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4310
4311 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
4312 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4313
4314 let fake_language_server = fake_language_servers.next().await.unwrap();
4315 fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
4316 Ok(Some(vec![
4317 lsp::TextEdit {
4318 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)),
4319 new_text: "h".to_string(),
4320 },
4321 lsp::TextEdit {
4322 range: lsp::Range::new(lsp::Position::new(0, 7), lsp::Position::new(0, 7)),
4323 new_text: "y".to_string(),
4324 },
4325 ]))
4326 });
4327
4328 project_b
4329 .update(cx_b, |project, cx| {
4330 project.format(
4331 HashSet::from_iter([buffer_b.clone()]),
4332 true,
4333 FormatTrigger::Save,
4334 cx,
4335 )
4336 })
4337 .await
4338 .unwrap();
4339
4340 // The edits from the LSP are applied, and a final newline is added.
4341 assert_eq!(
4342 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4343 "let honey = \"two\"\n"
4344 );
4345
4346 // Ensure buffer can be formatted using an external command. Notice how the
4347 // host's configuration is honored as opposed to using the guest's settings.
4348 cx_a.update(|cx| {
4349 cx.update_global(|store: &mut SettingsStore, cx| {
4350 store.update_user_settings::<AllLanguageSettings>(cx, |file| {
4351 file.defaults.formatter = Some(Formatter::External {
4352 command: "awk".into(),
4353 arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into(),
4354 });
4355 });
4356 });
4357 });
4358 project_b
4359 .update(cx_b, |project, cx| {
4360 project.format(
4361 HashSet::from_iter([buffer_b.clone()]),
4362 true,
4363 FormatTrigger::Save,
4364 cx,
4365 )
4366 })
4367 .await
4368 .unwrap();
4369 assert_eq!(
4370 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4371 format!("let honey = \"{}/a.rs\"\n", directory.to_str().unwrap())
4372 );
4373}
4374
4375#[gpui::test(iterations = 10)]
4376async fn test_prettier_formatting_buffer(
4377 executor: BackgroundExecutor,
4378 cx_a: &mut TestAppContext,
4379 cx_b: &mut TestAppContext,
4380) {
4381 let mut server = TestServer::start(executor.clone()).await;
4382 let client_a = server.create_client(cx_a, "user_a").await;
4383 let client_b = server.create_client(cx_b, "user_b").await;
4384 server
4385 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4386 .await;
4387 let active_call_a = cx_a.read(ActiveCall::global);
4388
4389 // Set up a fake language server.
4390 let mut language = Language::new(
4391 LanguageConfig {
4392 name: "Rust".into(),
4393 path_suffixes: vec!["rs".to_string()],
4394 prettier_parser_name: Some("test_parser".to_string()),
4395 ..Default::default()
4396 },
4397 Some(tree_sitter_rust::language()),
4398 );
4399 let test_plugin = "test_plugin";
4400 let mut fake_language_servers = language
4401 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
4402 prettier_plugins: vec![test_plugin],
4403 ..Default::default()
4404 }))
4405 .await;
4406 let language = Arc::new(language);
4407 client_a.language_registry().add(Arc::clone(&language));
4408
4409 // Here we insert a fake tree with a directory that exists on disk. This is needed
4410 // because later we'll invoke a command, which requires passing a working directory
4411 // that points to a valid location on disk.
4412 let directory = env::current_dir().unwrap();
4413 let buffer_text = "let one = \"two\"";
4414 client_a
4415 .fs()
4416 .insert_tree(&directory, json!({ "a.rs": buffer_text }))
4417 .await;
4418 let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
4419 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
4420 let open_buffer = project_a.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
4421 let buffer_a = cx_a.executor().spawn(open_buffer).await.unwrap();
4422
4423 let project_id = active_call_a
4424 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4425 .await
4426 .unwrap();
4427 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4428 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
4429 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4430
4431 cx_a.update(|cx| {
4432 cx.update_global(|store: &mut SettingsStore, cx| {
4433 store.update_user_settings::<AllLanguageSettings>(cx, |file| {
4434 file.defaults.formatter = Some(Formatter::Auto);
4435 });
4436 });
4437 });
4438 cx_b.update(|cx| {
4439 cx.update_global(|store: &mut SettingsStore, cx| {
4440 store.update_user_settings::<AllLanguageSettings>(cx, |file| {
4441 file.defaults.formatter = Some(Formatter::LanguageServer);
4442 });
4443 });
4444 });
4445 let fake_language_server = fake_language_servers.next().await.unwrap();
4446 fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
4447 panic!(
4448 "Unexpected: prettier should be preferred since it's enabled and language supports it"
4449 )
4450 });
4451
4452 project_b
4453 .update(cx_b, |project, cx| {
4454 project.format(
4455 HashSet::from_iter([buffer_b.clone()]),
4456 true,
4457 FormatTrigger::Save,
4458 cx,
4459 )
4460 })
4461 .await
4462 .unwrap();
4463
4464 executor.run_until_parked();
4465 assert_eq!(
4466 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4467 buffer_text.to_string() + "\n" + prettier_format_suffix,
4468 "Prettier formatting was not applied to client buffer after client's request"
4469 );
4470
4471 project_a
4472 .update(cx_a, |project, cx| {
4473 project.format(
4474 HashSet::from_iter([buffer_a.clone()]),
4475 true,
4476 FormatTrigger::Manual,
4477 cx,
4478 )
4479 })
4480 .await
4481 .unwrap();
4482
4483 executor.run_until_parked();
4484 assert_eq!(
4485 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4486 buffer_text.to_string() + "\n" + prettier_format_suffix + "\n" + prettier_format_suffix,
4487 "Prettier formatting was not applied to client buffer after host's request"
4488 );
4489}
4490
4491#[gpui::test(iterations = 10)]
4492async fn test_definition(
4493 executor: BackgroundExecutor,
4494 cx_a: &mut TestAppContext,
4495 cx_b: &mut TestAppContext,
4496) {
4497 let mut server = TestServer::start(executor.clone()).await;
4498 let client_a = server.create_client(cx_a, "user_a").await;
4499 let client_b = server.create_client(cx_b, "user_b").await;
4500 server
4501 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4502 .await;
4503 let active_call_a = cx_a.read(ActiveCall::global);
4504
4505 // Set up a fake language server.
4506 let mut language = Language::new(
4507 LanguageConfig {
4508 name: "Rust".into(),
4509 path_suffixes: vec!["rs".to_string()],
4510 ..Default::default()
4511 },
4512 Some(tree_sitter_rust::language()),
4513 );
4514 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4515 client_a.language_registry().add(Arc::new(language));
4516
4517 client_a
4518 .fs()
4519 .insert_tree(
4520 "/root",
4521 json!({
4522 "dir-1": {
4523 "a.rs": "const ONE: usize = b::TWO + b::THREE;",
4524 },
4525 "dir-2": {
4526 "b.rs": "const TWO: c::T2 = 2;\nconst THREE: usize = 3;",
4527 "c.rs": "type T2 = usize;",
4528 }
4529 }),
4530 )
4531 .await;
4532 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
4533 let project_id = active_call_a
4534 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4535 .await
4536 .unwrap();
4537 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4538
4539 // Open the file on client B.
4540 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
4541 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4542
4543 // Request the definition of a symbol as the guest.
4544 let fake_language_server = fake_language_servers.next().await.unwrap();
4545 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
4546 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4547 lsp::Location::new(
4548 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
4549 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
4550 ),
4551 )))
4552 });
4553
4554 let definitions_1 = project_b
4555 .update(cx_b, |p, cx| p.definition(&buffer_b, 23, cx))
4556 .await
4557 .unwrap();
4558 cx_b.read(|cx| {
4559 assert_eq!(definitions_1.len(), 1);
4560 assert_eq!(project_b.read(cx).worktrees().count(), 2);
4561 let target_buffer = definitions_1[0].target.buffer.read(cx);
4562 assert_eq!(
4563 target_buffer.text(),
4564 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
4565 );
4566 assert_eq!(
4567 definitions_1[0].target.range.to_point(target_buffer),
4568 Point::new(0, 6)..Point::new(0, 9)
4569 );
4570 });
4571
4572 // Try getting more definitions for the same buffer, ensuring the buffer gets reused from
4573 // the previous call to `definition`.
4574 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
4575 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4576 lsp::Location::new(
4577 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
4578 lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)),
4579 ),
4580 )))
4581 });
4582
4583 let definitions_2 = project_b
4584 .update(cx_b, |p, cx| p.definition(&buffer_b, 33, cx))
4585 .await
4586 .unwrap();
4587 cx_b.read(|cx| {
4588 assert_eq!(definitions_2.len(), 1);
4589 assert_eq!(project_b.read(cx).worktrees().count(), 2);
4590 let target_buffer = definitions_2[0].target.buffer.read(cx);
4591 assert_eq!(
4592 target_buffer.text(),
4593 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
4594 );
4595 assert_eq!(
4596 definitions_2[0].target.range.to_point(target_buffer),
4597 Point::new(1, 6)..Point::new(1, 11)
4598 );
4599 });
4600 assert_eq!(
4601 definitions_1[0].target.buffer,
4602 definitions_2[0].target.buffer
4603 );
4604
4605 fake_language_server.handle_request::<lsp::request::GotoTypeDefinition, _, _>(
4606 |req, _| async move {
4607 assert_eq!(
4608 req.text_document_position_params.position,
4609 lsp::Position::new(0, 7)
4610 );
4611 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4612 lsp::Location::new(
4613 lsp::Url::from_file_path("/root/dir-2/c.rs").unwrap(),
4614 lsp::Range::new(lsp::Position::new(0, 5), lsp::Position::new(0, 7)),
4615 ),
4616 )))
4617 },
4618 );
4619
4620 let type_definitions = project_b
4621 .update(cx_b, |p, cx| p.type_definition(&buffer_b, 7, cx))
4622 .await
4623 .unwrap();
4624 cx_b.read(|cx| {
4625 assert_eq!(type_definitions.len(), 1);
4626 let target_buffer = type_definitions[0].target.buffer.read(cx);
4627 assert_eq!(target_buffer.text(), "type T2 = usize;");
4628 assert_eq!(
4629 type_definitions[0].target.range.to_point(target_buffer),
4630 Point::new(0, 5)..Point::new(0, 7)
4631 );
4632 });
4633}
4634
4635#[gpui::test(iterations = 10)]
4636async fn test_references(
4637 executor: BackgroundExecutor,
4638 cx_a: &mut TestAppContext,
4639 cx_b: &mut TestAppContext,
4640) {
4641 let mut server = TestServer::start(executor.clone()).await;
4642 let client_a = server.create_client(cx_a, "user_a").await;
4643 let client_b = server.create_client(cx_b, "user_b").await;
4644 server
4645 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4646 .await;
4647 let active_call_a = cx_a.read(ActiveCall::global);
4648
4649 // Set up a fake language server.
4650 let mut language = Language::new(
4651 LanguageConfig {
4652 name: "Rust".into(),
4653 path_suffixes: vec!["rs".to_string()],
4654 ..Default::default()
4655 },
4656 Some(tree_sitter_rust::language()),
4657 );
4658 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4659 client_a.language_registry().add(Arc::new(language));
4660
4661 client_a
4662 .fs()
4663 .insert_tree(
4664 "/root",
4665 json!({
4666 "dir-1": {
4667 "one.rs": "const ONE: usize = 1;",
4668 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
4669 },
4670 "dir-2": {
4671 "three.rs": "const THREE: usize = two::TWO + one::ONE;",
4672 }
4673 }),
4674 )
4675 .await;
4676 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
4677 let project_id = active_call_a
4678 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4679 .await
4680 .unwrap();
4681 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4682
4683 // Open the file on client B.
4684 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx));
4685 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4686
4687 // Request references to a symbol as the guest.
4688 let fake_language_server = fake_language_servers.next().await.unwrap();
4689 fake_language_server.handle_request::<lsp::request::References, _, _>(|params, _| async move {
4690 assert_eq!(
4691 params.text_document_position.text_document.uri.as_str(),
4692 "file:///root/dir-1/one.rs"
4693 );
4694 Ok(Some(vec![
4695 lsp::Location {
4696 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
4697 range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)),
4698 },
4699 lsp::Location {
4700 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
4701 range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)),
4702 },
4703 lsp::Location {
4704 uri: lsp::Url::from_file_path("/root/dir-2/three.rs").unwrap(),
4705 range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)),
4706 },
4707 ]))
4708 });
4709
4710 let references = project_b
4711 .update(cx_b, |p, cx| p.references(&buffer_b, 7, cx))
4712 .await
4713 .unwrap();
4714 cx_b.read(|cx| {
4715 assert_eq!(references.len(), 3);
4716 assert_eq!(project_b.read(cx).worktrees().count(), 2);
4717
4718 let two_buffer = references[0].buffer.read(cx);
4719 let three_buffer = references[2].buffer.read(cx);
4720 assert_eq!(
4721 two_buffer.file().unwrap().path().as_ref(),
4722 Path::new("two.rs")
4723 );
4724 assert_eq!(references[1].buffer, references[0].buffer);
4725 assert_eq!(
4726 three_buffer.file().unwrap().full_path(cx),
4727 Path::new("/root/dir-2/three.rs")
4728 );
4729
4730 assert_eq!(references[0].range.to_offset(two_buffer), 24..27);
4731 assert_eq!(references[1].range.to_offset(two_buffer), 35..38);
4732 assert_eq!(references[2].range.to_offset(three_buffer), 37..40);
4733 });
4734}
4735
4736#[gpui::test(iterations = 10)]
4737async fn test_project_search(
4738 executor: BackgroundExecutor,
4739 cx_a: &mut TestAppContext,
4740 cx_b: &mut TestAppContext,
4741) {
4742 let mut server = TestServer::start(executor.clone()).await;
4743 let client_a = server.create_client(cx_a, "user_a").await;
4744 let client_b = server.create_client(cx_b, "user_b").await;
4745 server
4746 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4747 .await;
4748 let active_call_a = cx_a.read(ActiveCall::global);
4749
4750 client_a
4751 .fs()
4752 .insert_tree(
4753 "/root",
4754 json!({
4755 "dir-1": {
4756 "a": "hello world",
4757 "b": "goodnight moon",
4758 "c": "a world of goo",
4759 "d": "world champion of clown world",
4760 },
4761 "dir-2": {
4762 "e": "disney world is fun",
4763 }
4764 }),
4765 )
4766 .await;
4767 let (project_a, _) = client_a.build_local_project("/root/dir-1", cx_a).await;
4768 let (worktree_2, _) = project_a
4769 .update(cx_a, |p, cx| {
4770 p.find_or_create_local_worktree("/root/dir-2", true, cx)
4771 })
4772 .await
4773 .unwrap();
4774 worktree_2
4775 .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
4776 .await;
4777 let project_id = active_call_a
4778 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4779 .await
4780 .unwrap();
4781
4782 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4783
4784 // Perform a search as the guest.
4785 let mut results = HashMap::default();
4786 let mut search_rx = project_b.update(cx_b, |project, cx| {
4787 project.search(
4788 SearchQuery::text("world", false, false, false, Vec::new(), Vec::new()).unwrap(),
4789 cx,
4790 )
4791 });
4792 while let Some((buffer, ranges)) = search_rx.next().await {
4793 results.entry(buffer).or_insert(ranges);
4794 }
4795
4796 let mut ranges_by_path = results
4797 .into_iter()
4798 .map(|(buffer, ranges)| {
4799 buffer.read_with(cx_b, |buffer, cx| {
4800 let path = buffer.file().unwrap().full_path(cx);
4801 let offset_ranges = ranges
4802 .into_iter()
4803 .map(|range| range.to_offset(buffer))
4804 .collect::<Vec<_>>();
4805 (path, offset_ranges)
4806 })
4807 })
4808 .collect::<Vec<_>>();
4809 ranges_by_path.sort_by_key(|(path, _)| path.clone());
4810
4811 assert_eq!(
4812 ranges_by_path,
4813 &[
4814 (PathBuf::from("dir-1/a"), vec![6..11]),
4815 (PathBuf::from("dir-1/c"), vec![2..7]),
4816 (PathBuf::from("dir-1/d"), vec![0..5, 24..29]),
4817 (PathBuf::from("dir-2/e"), vec![7..12]),
4818 ]
4819 );
4820}
4821
4822#[gpui::test(iterations = 10)]
4823async fn test_document_highlights(
4824 executor: BackgroundExecutor,
4825 cx_a: &mut TestAppContext,
4826 cx_b: &mut TestAppContext,
4827) {
4828 let mut server = TestServer::start(executor.clone()).await;
4829 let client_a = server.create_client(cx_a, "user_a").await;
4830 let client_b = server.create_client(cx_b, "user_b").await;
4831 server
4832 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4833 .await;
4834 let active_call_a = cx_a.read(ActiveCall::global);
4835
4836 client_a
4837 .fs()
4838 .insert_tree(
4839 "/root-1",
4840 json!({
4841 "main.rs": "fn double(number: i32) -> i32 { number + number }",
4842 }),
4843 )
4844 .await;
4845
4846 // Set up a fake language server.
4847 let mut language = Language::new(
4848 LanguageConfig {
4849 name: "Rust".into(),
4850 path_suffixes: vec!["rs".to_string()],
4851 ..Default::default()
4852 },
4853 Some(tree_sitter_rust::language()),
4854 );
4855 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4856 client_a.language_registry().add(Arc::new(language));
4857
4858 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
4859 let project_id = active_call_a
4860 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4861 .await
4862 .unwrap();
4863 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4864
4865 // Open the file on client B.
4866 let open_b = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx));
4867 let buffer_b = cx_b.executor().spawn(open_b).await.unwrap();
4868
4869 // Request document highlights as the guest.
4870 let fake_language_server = fake_language_servers.next().await.unwrap();
4871 fake_language_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>(
4872 |params, _| async move {
4873 assert_eq!(
4874 params
4875 .text_document_position_params
4876 .text_document
4877 .uri
4878 .as_str(),
4879 "file:///root-1/main.rs"
4880 );
4881 assert_eq!(
4882 params.text_document_position_params.position,
4883 lsp::Position::new(0, 34)
4884 );
4885 Ok(Some(vec![
4886 lsp::DocumentHighlight {
4887 kind: Some(lsp::DocumentHighlightKind::WRITE),
4888 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 16)),
4889 },
4890 lsp::DocumentHighlight {
4891 kind: Some(lsp::DocumentHighlightKind::READ),
4892 range: lsp::Range::new(lsp::Position::new(0, 32), lsp::Position::new(0, 38)),
4893 },
4894 lsp::DocumentHighlight {
4895 kind: Some(lsp::DocumentHighlightKind::READ),
4896 range: lsp::Range::new(lsp::Position::new(0, 41), lsp::Position::new(0, 47)),
4897 },
4898 ]))
4899 },
4900 );
4901
4902 let highlights = project_b
4903 .update(cx_b, |p, cx| p.document_highlights(&buffer_b, 34, cx))
4904 .await
4905 .unwrap();
4906
4907 buffer_b.read_with(cx_b, |buffer, _| {
4908 let snapshot = buffer.snapshot();
4909
4910 let highlights = highlights
4911 .into_iter()
4912 .map(|highlight| (highlight.kind, highlight.range.to_offset(&snapshot)))
4913 .collect::<Vec<_>>();
4914 assert_eq!(
4915 highlights,
4916 &[
4917 (lsp::DocumentHighlightKind::WRITE, 10..16),
4918 (lsp::DocumentHighlightKind::READ, 32..38),
4919 (lsp::DocumentHighlightKind::READ, 41..47)
4920 ]
4921 )
4922 });
4923}
4924
4925#[gpui::test(iterations = 10)]
4926async fn test_lsp_hover(
4927 executor: BackgroundExecutor,
4928 cx_a: &mut TestAppContext,
4929 cx_b: &mut TestAppContext,
4930) {
4931 let mut server = TestServer::start(executor.clone()).await;
4932 let client_a = server.create_client(cx_a, "user_a").await;
4933 let client_b = server.create_client(cx_b, "user_b").await;
4934 server
4935 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4936 .await;
4937 let active_call_a = cx_a.read(ActiveCall::global);
4938
4939 client_a
4940 .fs()
4941 .insert_tree(
4942 "/root-1",
4943 json!({
4944 "main.rs": "use std::collections::HashMap;",
4945 }),
4946 )
4947 .await;
4948
4949 // Set up a fake language server.
4950 let mut language = Language::new(
4951 LanguageConfig {
4952 name: "Rust".into(),
4953 path_suffixes: vec!["rs".to_string()],
4954 ..Default::default()
4955 },
4956 Some(tree_sitter_rust::language()),
4957 );
4958 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4959 client_a.language_registry().add(Arc::new(language));
4960
4961 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
4962 let project_id = active_call_a
4963 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4964 .await
4965 .unwrap();
4966 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4967
4968 // Open the file as the guest
4969 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx));
4970 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4971
4972 // Request hover information as the guest.
4973 let fake_language_server = fake_language_servers.next().await.unwrap();
4974 fake_language_server.handle_request::<lsp::request::HoverRequest, _, _>(
4975 |params, _| async move {
4976 assert_eq!(
4977 params
4978 .text_document_position_params
4979 .text_document
4980 .uri
4981 .as_str(),
4982 "file:///root-1/main.rs"
4983 );
4984 assert_eq!(
4985 params.text_document_position_params.position,
4986 lsp::Position::new(0, 22)
4987 );
4988 Ok(Some(lsp::Hover {
4989 contents: lsp::HoverContents::Array(vec![
4990 lsp::MarkedString::String("Test hover content.".to_string()),
4991 lsp::MarkedString::LanguageString(lsp::LanguageString {
4992 language: "Rust".to_string(),
4993 value: "let foo = 42;".to_string(),
4994 }),
4995 ]),
4996 range: Some(lsp::Range::new(
4997 lsp::Position::new(0, 22),
4998 lsp::Position::new(0, 29),
4999 )),
5000 }))
5001 },
5002 );
5003
5004 let hover_info = project_b
5005 .update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx))
5006 .await
5007 .unwrap()
5008 .unwrap();
5009
5010 buffer_b.read_with(cx_b, |buffer, _| {
5011 let snapshot = buffer.snapshot();
5012 assert_eq!(hover_info.range.unwrap().to_offset(&snapshot), 22..29);
5013 assert_eq!(
5014 hover_info.contents,
5015 vec![
5016 project::HoverBlock {
5017 text: "Test hover content.".to_string(),
5018 kind: HoverBlockKind::Markdown,
5019 },
5020 project::HoverBlock {
5021 text: "let foo = 42;".to_string(),
5022 kind: HoverBlockKind::Code {
5023 language: "Rust".to_string()
5024 },
5025 }
5026 ]
5027 );
5028 });
5029}
5030
5031#[gpui::test(iterations = 10)]
5032async fn test_project_symbols(
5033 executor: BackgroundExecutor,
5034 cx_a: &mut TestAppContext,
5035 cx_b: &mut TestAppContext,
5036) {
5037 let mut server = TestServer::start(executor.clone()).await;
5038 let client_a = server.create_client(cx_a, "user_a").await;
5039 let client_b = server.create_client(cx_b, "user_b").await;
5040 server
5041 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5042 .await;
5043 let active_call_a = cx_a.read(ActiveCall::global);
5044
5045 // Set up a fake language server.
5046 let mut language = Language::new(
5047 LanguageConfig {
5048 name: "Rust".into(),
5049 path_suffixes: vec!["rs".to_string()],
5050 ..Default::default()
5051 },
5052 Some(tree_sitter_rust::language()),
5053 );
5054 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
5055 client_a.language_registry().add(Arc::new(language));
5056
5057 client_a
5058 .fs()
5059 .insert_tree(
5060 "/code",
5061 json!({
5062 "crate-1": {
5063 "one.rs": "const ONE: usize = 1;",
5064 },
5065 "crate-2": {
5066 "two.rs": "const TWO: usize = 2; const THREE: usize = 3;",
5067 },
5068 "private": {
5069 "passwords.txt": "the-password",
5070 }
5071 }),
5072 )
5073 .await;
5074 let (project_a, worktree_id) = client_a.build_local_project("/code/crate-1", cx_a).await;
5075 let project_id = active_call_a
5076 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5077 .await
5078 .unwrap();
5079 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5080
5081 // Cause the language server to start.
5082 let open_buffer_task =
5083 project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx));
5084 let _buffer = cx_b.executor().spawn(open_buffer_task).await.unwrap();
5085
5086 let fake_language_server = fake_language_servers.next().await.unwrap();
5087 fake_language_server.handle_request::<lsp::WorkspaceSymbolRequest, _, _>(|_, _| async move {
5088 Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![
5089 #[allow(deprecated)]
5090 lsp::SymbolInformation {
5091 name: "TWO".into(),
5092 location: lsp::Location {
5093 uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(),
5094 range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
5095 },
5096 kind: lsp::SymbolKind::CONSTANT,
5097 tags: None,
5098 container_name: None,
5099 deprecated: None,
5100 },
5101 ])))
5102 });
5103
5104 // Request the definition of a symbol as the guest.
5105 let symbols = project_b
5106 .update(cx_b, |p, cx| p.symbols("two", cx))
5107 .await
5108 .unwrap();
5109 assert_eq!(symbols.len(), 1);
5110 assert_eq!(symbols[0].name, "TWO");
5111
5112 // Open one of the returned symbols.
5113 let buffer_b_2 = project_b
5114 .update(cx_b, |project, cx| {
5115 project.open_buffer_for_symbol(&symbols[0], cx)
5116 })
5117 .await
5118 .unwrap();
5119
5120 buffer_b_2.read_with(cx_b, |buffer, cx| {
5121 assert_eq!(
5122 buffer.file().unwrap().full_path(cx),
5123 Path::new("/code/crate-2/two.rs")
5124 );
5125 });
5126
5127 // Attempt to craft a symbol and violate host's privacy by opening an arbitrary file.
5128 let mut fake_symbol = symbols[0].clone();
5129 fake_symbol.path.path = Path::new("/code/secrets").into();
5130 let error = project_b
5131 .update(cx_b, |project, cx| {
5132 project.open_buffer_for_symbol(&fake_symbol, cx)
5133 })
5134 .await
5135 .unwrap_err();
5136 assert!(error.to_string().contains("invalid symbol signature"));
5137}
5138
5139#[gpui::test(iterations = 10)]
5140async fn test_open_buffer_while_getting_definition_pointing_to_it(
5141 executor: BackgroundExecutor,
5142 cx_a: &mut TestAppContext,
5143 cx_b: &mut TestAppContext,
5144 mut rng: StdRng,
5145) {
5146 let mut server = TestServer::start(executor.clone()).await;
5147 let client_a = server.create_client(cx_a, "user_a").await;
5148 let client_b = server.create_client(cx_b, "user_b").await;
5149 server
5150 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5151 .await;
5152 let active_call_a = cx_a.read(ActiveCall::global);
5153
5154 // Set up a fake language server.
5155 let mut language = Language::new(
5156 LanguageConfig {
5157 name: "Rust".into(),
5158 path_suffixes: vec!["rs".to_string()],
5159 ..Default::default()
5160 },
5161 Some(tree_sitter_rust::language()),
5162 );
5163 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
5164 client_a.language_registry().add(Arc::new(language));
5165
5166 client_a
5167 .fs()
5168 .insert_tree(
5169 "/root",
5170 json!({
5171 "a.rs": "const ONE: usize = b::TWO;",
5172 "b.rs": "const TWO: usize = 2",
5173 }),
5174 )
5175 .await;
5176 let (project_a, worktree_id) = client_a.build_local_project("/root", cx_a).await;
5177 let project_id = active_call_a
5178 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5179 .await
5180 .unwrap();
5181 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5182
5183 let open_buffer_task = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
5184 let buffer_b1 = cx_b.executor().spawn(open_buffer_task).await.unwrap();
5185
5186 let fake_language_server = fake_language_servers.next().await.unwrap();
5187 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
5188 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
5189 lsp::Location::new(
5190 lsp::Url::from_file_path("/root/b.rs").unwrap(),
5191 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
5192 ),
5193 )))
5194 });
5195
5196 let definitions;
5197 let buffer_b2;
5198 if rng.gen() {
5199 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
5200 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
5201 } else {
5202 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
5203 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
5204 }
5205
5206 let buffer_b2 = buffer_b2.await.unwrap();
5207 let definitions = definitions.await.unwrap();
5208 assert_eq!(definitions.len(), 1);
5209 assert_eq!(definitions[0].target.buffer, buffer_b2);
5210}
5211
5212#[gpui::test(iterations = 10)]
5213async fn test_contacts(
5214 executor: BackgroundExecutor,
5215 cx_a: &mut TestAppContext,
5216 cx_b: &mut TestAppContext,
5217 cx_c: &mut TestAppContext,
5218 cx_d: &mut TestAppContext,
5219) {
5220 let mut server = TestServer::start(executor.clone()).await;
5221 let client_a = server.create_client(cx_a, "user_a").await;
5222 let client_b = server.create_client(cx_b, "user_b").await;
5223 let client_c = server.create_client(cx_c, "user_c").await;
5224 let client_d = server.create_client(cx_d, "user_d").await;
5225 server
5226 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
5227 .await;
5228 let active_call_a = cx_a.read(ActiveCall::global);
5229 let active_call_b = cx_b.read(ActiveCall::global);
5230 let active_call_c = cx_c.read(ActiveCall::global);
5231 let _active_call_d = cx_d.read(ActiveCall::global);
5232
5233 executor.run_until_parked();
5234 assert_eq!(
5235 contacts(&client_a, cx_a),
5236 [
5237 ("user_b".to_string(), "online", "free"),
5238 ("user_c".to_string(), "online", "free")
5239 ]
5240 );
5241 assert_eq!(
5242 contacts(&client_b, cx_b),
5243 [
5244 ("user_a".to_string(), "online", "free"),
5245 ("user_c".to_string(), "online", "free")
5246 ]
5247 );
5248 assert_eq!(
5249 contacts(&client_c, cx_c),
5250 [
5251 ("user_a".to_string(), "online", "free"),
5252 ("user_b".to_string(), "online", "free")
5253 ]
5254 );
5255 assert_eq!(contacts(&client_d, cx_d), []);
5256
5257 server.disconnect_client(client_c.peer_id().unwrap());
5258 server.forbid_connections();
5259 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
5260 assert_eq!(
5261 contacts(&client_a, cx_a),
5262 [
5263 ("user_b".to_string(), "online", "free"),
5264 ("user_c".to_string(), "offline", "free")
5265 ]
5266 );
5267 assert_eq!(
5268 contacts(&client_b, cx_b),
5269 [
5270 ("user_a".to_string(), "online", "free"),
5271 ("user_c".to_string(), "offline", "free")
5272 ]
5273 );
5274 assert_eq!(contacts(&client_c, cx_c), []);
5275 assert_eq!(contacts(&client_d, cx_d), []);
5276
5277 server.allow_connections();
5278 client_c
5279 .authenticate_and_connect(false, &cx_c.to_async())
5280 .await
5281 .unwrap();
5282
5283 executor.run_until_parked();
5284 assert_eq!(
5285 contacts(&client_a, cx_a),
5286 [
5287 ("user_b".to_string(), "online", "free"),
5288 ("user_c".to_string(), "online", "free")
5289 ]
5290 );
5291 assert_eq!(
5292 contacts(&client_b, cx_b),
5293 [
5294 ("user_a".to_string(), "online", "free"),
5295 ("user_c".to_string(), "online", "free")
5296 ]
5297 );
5298 assert_eq!(
5299 contacts(&client_c, cx_c),
5300 [
5301 ("user_a".to_string(), "online", "free"),
5302 ("user_b".to_string(), "online", "free")
5303 ]
5304 );
5305 assert_eq!(contacts(&client_d, cx_d), []);
5306
5307 active_call_a
5308 .update(cx_a, |call, cx| {
5309 call.invite(client_b.user_id().unwrap(), None, cx)
5310 })
5311 .await
5312 .unwrap();
5313 executor.run_until_parked();
5314 assert_eq!(
5315 contacts(&client_a, cx_a),
5316 [
5317 ("user_b".to_string(), "online", "busy"),
5318 ("user_c".to_string(), "online", "free")
5319 ]
5320 );
5321 assert_eq!(
5322 contacts(&client_b, cx_b),
5323 [
5324 ("user_a".to_string(), "online", "busy"),
5325 ("user_c".to_string(), "online", "free")
5326 ]
5327 );
5328 assert_eq!(
5329 contacts(&client_c, cx_c),
5330 [
5331 ("user_a".to_string(), "online", "busy"),
5332 ("user_b".to_string(), "online", "busy")
5333 ]
5334 );
5335 assert_eq!(contacts(&client_d, cx_d), []);
5336
5337 // Client B and client D become contacts while client B is being called.
5338 server
5339 .make_contacts(&mut [(&client_b, cx_b), (&client_d, cx_d)])
5340 .await;
5341 executor.run_until_parked();
5342 assert_eq!(
5343 contacts(&client_a, cx_a),
5344 [
5345 ("user_b".to_string(), "online", "busy"),
5346 ("user_c".to_string(), "online", "free")
5347 ]
5348 );
5349 assert_eq!(
5350 contacts(&client_b, cx_b),
5351 [
5352 ("user_a".to_string(), "online", "busy"),
5353 ("user_c".to_string(), "online", "free"),
5354 ("user_d".to_string(), "online", "free"),
5355 ]
5356 );
5357 assert_eq!(
5358 contacts(&client_c, cx_c),
5359 [
5360 ("user_a".to_string(), "online", "busy"),
5361 ("user_b".to_string(), "online", "busy")
5362 ]
5363 );
5364 assert_eq!(
5365 contacts(&client_d, cx_d),
5366 [("user_b".to_string(), "online", "busy")]
5367 );
5368
5369 active_call_b.update(cx_b, |call, cx| call.decline_incoming(cx).unwrap());
5370 executor.run_until_parked();
5371 assert_eq!(
5372 contacts(&client_a, cx_a),
5373 [
5374 ("user_b".to_string(), "online", "free"),
5375 ("user_c".to_string(), "online", "free")
5376 ]
5377 );
5378 assert_eq!(
5379 contacts(&client_b, cx_b),
5380 [
5381 ("user_a".to_string(), "online", "free"),
5382 ("user_c".to_string(), "online", "free"),
5383 ("user_d".to_string(), "online", "free")
5384 ]
5385 );
5386 assert_eq!(
5387 contacts(&client_c, cx_c),
5388 [
5389 ("user_a".to_string(), "online", "free"),
5390 ("user_b".to_string(), "online", "free")
5391 ]
5392 );
5393 assert_eq!(
5394 contacts(&client_d, cx_d),
5395 [("user_b".to_string(), "online", "free")]
5396 );
5397
5398 active_call_c
5399 .update(cx_c, |call, cx| {
5400 call.invite(client_a.user_id().unwrap(), None, cx)
5401 })
5402 .await
5403 .unwrap();
5404 executor.run_until_parked();
5405 assert_eq!(
5406 contacts(&client_a, cx_a),
5407 [
5408 ("user_b".to_string(), "online", "free"),
5409 ("user_c".to_string(), "online", "busy")
5410 ]
5411 );
5412 assert_eq!(
5413 contacts(&client_b, cx_b),
5414 [
5415 ("user_a".to_string(), "online", "busy"),
5416 ("user_c".to_string(), "online", "busy"),
5417 ("user_d".to_string(), "online", "free")
5418 ]
5419 );
5420 assert_eq!(
5421 contacts(&client_c, cx_c),
5422 [
5423 ("user_a".to_string(), "online", "busy"),
5424 ("user_b".to_string(), "online", "free")
5425 ]
5426 );
5427 assert_eq!(
5428 contacts(&client_d, cx_d),
5429 [("user_b".to_string(), "online", "free")]
5430 );
5431
5432 active_call_a
5433 .update(cx_a, |call, cx| call.accept_incoming(cx))
5434 .await
5435 .unwrap();
5436 executor.run_until_parked();
5437 assert_eq!(
5438 contacts(&client_a, cx_a),
5439 [
5440 ("user_b".to_string(), "online", "free"),
5441 ("user_c".to_string(), "online", "busy")
5442 ]
5443 );
5444 assert_eq!(
5445 contacts(&client_b, cx_b),
5446 [
5447 ("user_a".to_string(), "online", "busy"),
5448 ("user_c".to_string(), "online", "busy"),
5449 ("user_d".to_string(), "online", "free")
5450 ]
5451 );
5452 assert_eq!(
5453 contacts(&client_c, cx_c),
5454 [
5455 ("user_a".to_string(), "online", "busy"),
5456 ("user_b".to_string(), "online", "free")
5457 ]
5458 );
5459 assert_eq!(
5460 contacts(&client_d, cx_d),
5461 [("user_b".to_string(), "online", "free")]
5462 );
5463
5464 active_call_a
5465 .update(cx_a, |call, cx| {
5466 call.invite(client_b.user_id().unwrap(), None, cx)
5467 })
5468 .await
5469 .unwrap();
5470 executor.run_until_parked();
5471 assert_eq!(
5472 contacts(&client_a, cx_a),
5473 [
5474 ("user_b".to_string(), "online", "busy"),
5475 ("user_c".to_string(), "online", "busy")
5476 ]
5477 );
5478 assert_eq!(
5479 contacts(&client_b, cx_b),
5480 [
5481 ("user_a".to_string(), "online", "busy"),
5482 ("user_c".to_string(), "online", "busy"),
5483 ("user_d".to_string(), "online", "free")
5484 ]
5485 );
5486 assert_eq!(
5487 contacts(&client_c, cx_c),
5488 [
5489 ("user_a".to_string(), "online", "busy"),
5490 ("user_b".to_string(), "online", "busy")
5491 ]
5492 );
5493 assert_eq!(
5494 contacts(&client_d, cx_d),
5495 [("user_b".to_string(), "online", "busy")]
5496 );
5497
5498 active_call_a
5499 .update(cx_a, |call, cx| call.hang_up(cx))
5500 .await
5501 .unwrap();
5502 executor.run_until_parked();
5503 assert_eq!(
5504 contacts(&client_a, cx_a),
5505 [
5506 ("user_b".to_string(), "online", "free"),
5507 ("user_c".to_string(), "online", "free")
5508 ]
5509 );
5510 assert_eq!(
5511 contacts(&client_b, cx_b),
5512 [
5513 ("user_a".to_string(), "online", "free"),
5514 ("user_c".to_string(), "online", "free"),
5515 ("user_d".to_string(), "online", "free")
5516 ]
5517 );
5518 assert_eq!(
5519 contacts(&client_c, cx_c),
5520 [
5521 ("user_a".to_string(), "online", "free"),
5522 ("user_b".to_string(), "online", "free")
5523 ]
5524 );
5525 assert_eq!(
5526 contacts(&client_d, cx_d),
5527 [("user_b".to_string(), "online", "free")]
5528 );
5529
5530 active_call_a
5531 .update(cx_a, |call, cx| {
5532 call.invite(client_b.user_id().unwrap(), None, cx)
5533 })
5534 .await
5535 .unwrap();
5536 executor.run_until_parked();
5537 assert_eq!(
5538 contacts(&client_a, cx_a),
5539 [
5540 ("user_b".to_string(), "online", "busy"),
5541 ("user_c".to_string(), "online", "free")
5542 ]
5543 );
5544 assert_eq!(
5545 contacts(&client_b, cx_b),
5546 [
5547 ("user_a".to_string(), "online", "busy"),
5548 ("user_c".to_string(), "online", "free"),
5549 ("user_d".to_string(), "online", "free")
5550 ]
5551 );
5552 assert_eq!(
5553 contacts(&client_c, cx_c),
5554 [
5555 ("user_a".to_string(), "online", "busy"),
5556 ("user_b".to_string(), "online", "busy")
5557 ]
5558 );
5559 assert_eq!(
5560 contacts(&client_d, cx_d),
5561 [("user_b".to_string(), "online", "busy")]
5562 );
5563
5564 server.forbid_connections();
5565 server.disconnect_client(client_a.peer_id().unwrap());
5566 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
5567 assert_eq!(contacts(&client_a, cx_a), []);
5568 assert_eq!(
5569 contacts(&client_b, cx_b),
5570 [
5571 ("user_a".to_string(), "offline", "free"),
5572 ("user_c".to_string(), "online", "free"),
5573 ("user_d".to_string(), "online", "free")
5574 ]
5575 );
5576 assert_eq!(
5577 contacts(&client_c, cx_c),
5578 [
5579 ("user_a".to_string(), "offline", "free"),
5580 ("user_b".to_string(), "online", "free")
5581 ]
5582 );
5583 assert_eq!(
5584 contacts(&client_d, cx_d),
5585 [("user_b".to_string(), "online", "free")]
5586 );
5587
5588 // Test removing a contact
5589 client_b
5590 .user_store()
5591 .update(cx_b, |store, cx| {
5592 store.remove_contact(client_c.user_id().unwrap(), cx)
5593 })
5594 .await
5595 .unwrap();
5596 executor.run_until_parked();
5597 assert_eq!(
5598 contacts(&client_b, cx_b),
5599 [
5600 ("user_a".to_string(), "offline", "free"),
5601 ("user_d".to_string(), "online", "free")
5602 ]
5603 );
5604 assert_eq!(
5605 contacts(&client_c, cx_c),
5606 [("user_a".to_string(), "offline", "free"),]
5607 );
5608
5609 fn contacts(
5610 client: &TestClient,
5611 cx: &TestAppContext,
5612 ) -> Vec<(String, &'static str, &'static str)> {
5613 client.user_store().read_with(cx, |store, _| {
5614 store
5615 .contacts()
5616 .iter()
5617 .map(|contact| {
5618 (
5619 contact.user.github_login.clone(),
5620 if contact.online { "online" } else { "offline" },
5621 if contact.busy { "busy" } else { "free" },
5622 )
5623 })
5624 .collect()
5625 })
5626 }
5627}
5628
5629#[gpui::test(iterations = 10)]
5630async fn test_contact_requests(
5631 executor: BackgroundExecutor,
5632 cx_a: &mut TestAppContext,
5633 cx_a2: &mut TestAppContext,
5634 cx_b: &mut TestAppContext,
5635 cx_b2: &mut TestAppContext,
5636 cx_c: &mut TestAppContext,
5637 cx_c2: &mut TestAppContext,
5638) {
5639 // Connect to a server as 3 clients.
5640 let mut server = TestServer::start(executor.clone()).await;
5641 let client_a = server.create_client(cx_a, "user_a").await;
5642 let client_a2 = server.create_client(cx_a2, "user_a").await;
5643 let client_b = server.create_client(cx_b, "user_b").await;
5644 let client_b2 = server.create_client(cx_b2, "user_b").await;
5645 let client_c = server.create_client(cx_c, "user_c").await;
5646 let client_c2 = server.create_client(cx_c2, "user_c").await;
5647
5648 assert_eq!(client_a.user_id().unwrap(), client_a2.user_id().unwrap());
5649 assert_eq!(client_b.user_id().unwrap(), client_b2.user_id().unwrap());
5650 assert_eq!(client_c.user_id().unwrap(), client_c2.user_id().unwrap());
5651
5652 // User A and User C request that user B become their contact.
5653 client_a
5654 .user_store()
5655 .update(cx_a, |store, cx| {
5656 store.request_contact(client_b.user_id().unwrap(), cx)
5657 })
5658 .await
5659 .unwrap();
5660 client_c
5661 .user_store()
5662 .update(cx_c, |store, cx| {
5663 store.request_contact(client_b.user_id().unwrap(), cx)
5664 })
5665 .await
5666 .unwrap();
5667 executor.run_until_parked();
5668
5669 // All users see the pending request appear in all their clients.
5670 assert_eq!(
5671 client_a.summarize_contacts(cx_a).outgoing_requests,
5672 &["user_b"]
5673 );
5674 assert_eq!(
5675 client_a2.summarize_contacts(cx_a2).outgoing_requests,
5676 &["user_b"]
5677 );
5678 assert_eq!(
5679 client_b.summarize_contacts(cx_b).incoming_requests,
5680 &["user_a", "user_c"]
5681 );
5682 assert_eq!(
5683 client_b2.summarize_contacts(cx_b2).incoming_requests,
5684 &["user_a", "user_c"]
5685 );
5686 assert_eq!(
5687 client_c.summarize_contacts(cx_c).outgoing_requests,
5688 &["user_b"]
5689 );
5690 assert_eq!(
5691 client_c2.summarize_contacts(cx_c2).outgoing_requests,
5692 &["user_b"]
5693 );
5694
5695 // Contact requests are present upon connecting (tested here via disconnect/reconnect)
5696 disconnect_and_reconnect(&client_a, cx_a).await;
5697 disconnect_and_reconnect(&client_b, cx_b).await;
5698 disconnect_and_reconnect(&client_c, cx_c).await;
5699 executor.run_until_parked();
5700 assert_eq!(
5701 client_a.summarize_contacts(cx_a).outgoing_requests,
5702 &["user_b"]
5703 );
5704 assert_eq!(
5705 client_b.summarize_contacts(cx_b).incoming_requests,
5706 &["user_a", "user_c"]
5707 );
5708 assert_eq!(
5709 client_c.summarize_contacts(cx_c).outgoing_requests,
5710 &["user_b"]
5711 );
5712
5713 // User B accepts the request from user A.
5714 client_b
5715 .user_store()
5716 .update(cx_b, |store, cx| {
5717 store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
5718 })
5719 .await
5720 .unwrap();
5721
5722 executor.run_until_parked();
5723
5724 // User B sees user A as their contact now in all client, and the incoming request from them is removed.
5725 let contacts_b = client_b.summarize_contacts(cx_b);
5726 assert_eq!(contacts_b.current, &["user_a"]);
5727 assert_eq!(contacts_b.incoming_requests, &["user_c"]);
5728 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
5729 assert_eq!(contacts_b2.current, &["user_a"]);
5730 assert_eq!(contacts_b2.incoming_requests, &["user_c"]);
5731
5732 // User A sees user B as their contact now in all clients, and the outgoing request to them is removed.
5733 let contacts_a = client_a.summarize_contacts(cx_a);
5734 assert_eq!(contacts_a.current, &["user_b"]);
5735 assert!(contacts_a.outgoing_requests.is_empty());
5736 let contacts_a2 = client_a2.summarize_contacts(cx_a2);
5737 assert_eq!(contacts_a2.current, &["user_b"]);
5738 assert!(contacts_a2.outgoing_requests.is_empty());
5739
5740 // Contacts are present upon connecting (tested here via disconnect/reconnect)
5741 disconnect_and_reconnect(&client_a, cx_a).await;
5742 disconnect_and_reconnect(&client_b, cx_b).await;
5743 disconnect_and_reconnect(&client_c, cx_c).await;
5744 executor.run_until_parked();
5745 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
5746 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
5747 assert_eq!(
5748 client_b.summarize_contacts(cx_b).incoming_requests,
5749 &["user_c"]
5750 );
5751 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
5752 assert_eq!(
5753 client_c.summarize_contacts(cx_c).outgoing_requests,
5754 &["user_b"]
5755 );
5756
5757 // User B rejects the request from user C.
5758 client_b
5759 .user_store()
5760 .update(cx_b, |store, cx| {
5761 store.respond_to_contact_request(client_c.user_id().unwrap(), false, cx)
5762 })
5763 .await
5764 .unwrap();
5765
5766 executor.run_until_parked();
5767
5768 // User B doesn't see user C as their contact, and the incoming request from them is removed.
5769 let contacts_b = client_b.summarize_contacts(cx_b);
5770 assert_eq!(contacts_b.current, &["user_a"]);
5771 assert!(contacts_b.incoming_requests.is_empty());
5772 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
5773 assert_eq!(contacts_b2.current, &["user_a"]);
5774 assert!(contacts_b2.incoming_requests.is_empty());
5775
5776 // User C doesn't see user B as their contact, and the outgoing request to them is removed.
5777 let contacts_c = client_c.summarize_contacts(cx_c);
5778 assert!(contacts_c.current.is_empty());
5779 assert!(contacts_c.outgoing_requests.is_empty());
5780 let contacts_c2 = client_c2.summarize_contacts(cx_c2);
5781 assert!(contacts_c2.current.is_empty());
5782 assert!(contacts_c2.outgoing_requests.is_empty());
5783
5784 // Incoming/outgoing requests are not present upon connecting (tested here via disconnect/reconnect)
5785 disconnect_and_reconnect(&client_a, cx_a).await;
5786 disconnect_and_reconnect(&client_b, cx_b).await;
5787 disconnect_and_reconnect(&client_c, cx_c).await;
5788 executor.run_until_parked();
5789 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
5790 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
5791 assert!(client_b
5792 .summarize_contacts(cx_b)
5793 .incoming_requests
5794 .is_empty());
5795 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
5796 assert!(client_c
5797 .summarize_contacts(cx_c)
5798 .outgoing_requests
5799 .is_empty());
5800
5801 async fn disconnect_and_reconnect(client: &TestClient, cx: &mut TestAppContext) {
5802 client.disconnect(&cx.to_async());
5803 client.clear_contacts(cx).await;
5804 client
5805 .authenticate_and_connect(false, &cx.to_async())
5806 .await
5807 .unwrap();
5808 }
5809}
5810
5811#[gpui::test(iterations = 10)]
5812async fn test_join_call_after_screen_was_shared(
5813 executor: BackgroundExecutor,
5814 cx_a: &mut TestAppContext,
5815 cx_b: &mut TestAppContext,
5816) {
5817 let mut server = TestServer::start(executor.clone()).await;
5818
5819 let client_a = server.create_client(cx_a, "user_a").await;
5820 let client_b = server.create_client(cx_b, "user_b").await;
5821 server
5822 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5823 .await;
5824
5825 let active_call_a = cx_a.read(ActiveCall::global);
5826 let active_call_b = cx_b.read(ActiveCall::global);
5827
5828 // Call users B and C from client A.
5829 active_call_a
5830 .update(cx_a, |call, cx| {
5831 call.invite(client_b.user_id().unwrap(), None, cx)
5832 })
5833 .await
5834 .unwrap();
5835
5836 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
5837 executor.run_until_parked();
5838 assert_eq!(
5839 room_participants(&room_a, cx_a),
5840 RoomParticipants {
5841 remote: Default::default(),
5842 pending: vec!["user_b".to_string()]
5843 }
5844 );
5845
5846 // User B receives the call.
5847
5848 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
5849 let call_b = incoming_call_b.next().await.unwrap().unwrap();
5850 assert_eq!(call_b.calling_user.github_login, "user_a");
5851
5852 // User A shares their screen
5853 let display = MacOSDisplay::new();
5854 active_call_a
5855 .update(cx_a, |call, cx| {
5856 call.room().unwrap().update(cx, |room, cx| {
5857 room.set_display_sources(vec![display.clone()]);
5858 room.share_screen(cx)
5859 })
5860 })
5861 .await
5862 .unwrap();
5863
5864 client_b.user_store().update(cx_b, |user_store, _| {
5865 user_store.clear_cache();
5866 });
5867
5868 // User B joins the room
5869 active_call_b
5870 .update(cx_b, |call, cx| call.accept_incoming(cx))
5871 .await
5872 .unwrap();
5873
5874 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
5875 assert!(incoming_call_b.next().await.unwrap().is_none());
5876
5877 executor.run_until_parked();
5878 assert_eq!(
5879 room_participants(&room_a, cx_a),
5880 RoomParticipants {
5881 remote: vec!["user_b".to_string()],
5882 pending: vec![],
5883 }
5884 );
5885 assert_eq!(
5886 room_participants(&room_b, cx_b),
5887 RoomParticipants {
5888 remote: vec!["user_a".to_string()],
5889 pending: vec![],
5890 }
5891 );
5892
5893 // Ensure User B sees User A's screenshare.
5894
5895 room_b.read_with(cx_b, |room, _| {
5896 assert_eq!(
5897 room.remote_participants()
5898 .get(&client_a.user_id().unwrap())
5899 .unwrap()
5900 .video_tracks
5901 .len(),
5902 1
5903 );
5904 });
5905}