1use std::collections::{vec_deque::Drain, VecDeque};
2use std::fmt;
3use std::sync::Arc;
4
5use crate::{
6 frame_info::PlayerInput,
7 network::{
8 messages::ConnectionStatus,
9 protocol::{Event, UdpProtocol},
10 },
11 report_violation, safe_frame_add,
12 telemetry::{ViolationKind, ViolationObserver, ViolationSeverity},
13 Config, FortressError, FortressEvent, FortressRequest, Frame, InputStatus, InputVec,
14 InternalErrorKind, InvalidFrameReason, NetworkStats, NonBlockingSocket, PlayerHandle,
15 SessionState,
16};
17
18const NORMAL_SPEED: usize = 1;
23
24pub struct SpectatorSession<T>
29where
30 T: Config,
31{
32 state: SessionState,
33 num_players: usize,
34 buffer_size: usize,
35 inputs: Vec<Vec<PlayerInput<T::Input>>>,
36 host_connect_status: Vec<ConnectionStatus>,
37 socket: Box<dyn NonBlockingSocket<T::Address>>,
38 host: UdpProtocol<T>,
39 event_queue: VecDeque<FortressEvent<T>>,
40 current_frame: Frame,
41 last_recv_frame: Frame,
42 max_frames_behind: usize,
43 catchup_speed: usize,
44 violation_observer: Option<Arc<dyn ViolationObserver>>,
46 max_event_queue_size: usize,
48}
49
50impl<T: Config> SpectatorSession<T> {
51 #[allow(clippy::too_many_arguments)]
55 pub(crate) fn new(
56 num_players: usize,
57 socket: Box<dyn NonBlockingSocket<T::Address>>,
58 host: UdpProtocol<T>,
59 buffer_size: usize,
60 max_frames_behind: usize,
61 catchup_speed: usize,
62 violation_observer: Option<Arc<dyn ViolationObserver>>,
63 event_queue_size: usize,
64 ) -> Self {
65 let host_connect_status = vec![ConnectionStatus::default(); num_players];
67
68 let actual_buffer_size = buffer_size.max(1);
70
71 Self {
72 state: SessionState::Synchronizing,
73 num_players,
74 buffer_size: actual_buffer_size,
75 inputs: vec![
76 vec![PlayerInput::blank_input(Frame::NULL); num_players];
77 actual_buffer_size
78 ],
79 host_connect_status,
80 socket,
81 host,
82 event_queue: VecDeque::new(),
83 current_frame: Frame::NULL,
84 last_recv_frame: Frame::NULL,
85 max_frames_behind,
86 catchup_speed,
87 violation_observer,
88 max_event_queue_size: event_queue_size,
89 }
90 }
91
92 pub fn current_state(&self) -> SessionState {
94 self.state
95 }
96
97 pub fn frames_behind_host(&self) -> usize {
99 let diff = self.last_recv_frame - self.current_frame;
100 if diff < 0 {
103 report_violation!(
104 ViolationSeverity::Warning,
105 ViolationKind::FrameSync,
106 "frames_behind_host: current_frame {} exceeds last_recv_frame {} - returning 0",
107 self.current_frame,
108 self.last_recv_frame
109 );
110 return 0;
111 }
112 diff as usize
113 }
114
115 #[must_use = "network stats should be inspected or logged"]
121 pub fn network_stats(&self) -> Result<NetworkStats, FortressError> {
122 self.host.network_stats()
123 }
124
125 pub fn events(&mut self) -> Drain<'_, FortressEvent<T>> {
127 self.event_queue.drain(..)
128 }
129
130 pub fn violation_observer(&self) -> Option<&Arc<dyn ViolationObserver>> {
137 self.violation_observer.as_ref()
138 }
139
140 #[must_use = "FortressRequests must be processed to advance the game state"]
151 pub fn advance_frame(&mut self) -> Result<Vec<FortressRequest<T>>, FortressError> {
152 self.poll_remote_clients();
154
155 if self.state != SessionState::Running {
156 return Err(FortressError::NotSynchronized);
157 }
158
159 let frames_to_advance = if self.frames_behind_host() > self.max_frames_behind {
160 self.catchup_speed
161 } else {
162 NORMAL_SPEED
163 };
164
165 let mut requests = Vec::with_capacity(frames_to_advance);
168
169 for _ in 0..frames_to_advance {
170 let frame_to_grab = safe_frame_add!(
172 self.current_frame,
173 1,
174 "SpectatorSession::advance_frames next"
175 );
176 let synced_inputs = self.inputs_at_frame(frame_to_grab)?;
177
178 requests.push(FortressRequest::AdvanceFrame {
179 inputs: synced_inputs,
180 });
181
182 self.current_frame = safe_frame_add!(
184 self.current_frame,
185 1,
186 "SpectatorSession::advance_frames current"
187 );
188 }
189
190 Ok(requests)
191 }
192
193 pub fn poll_remote_clients(&mut self) {
196 for (from, msg) in &self.socket.receive_all_messages() {
199 if self.host.is_handling_message(from) {
200 self.host.handle_message(msg);
201 }
202 }
203
204 let mut events = VecDeque::new();
206 let addr = self.host.peer_addr();
207 for event in self.host.poll(&self.host_connect_status) {
208 events.push_back((event, addr.clone()));
209 }
210
211 for (event, addr) in std::mem::take(&mut events) {
213 self.handle_event(event, addr);
214 }
215
216 self.host.send_all_messages(&mut self.socket);
218 }
219
220 pub fn current_frame(&self) -> Frame {
222 self.current_frame
223 }
224
225 pub fn num_players(&self) -> usize {
227 self.num_players
228 }
229
230 fn inputs_at_frame(&self, frame_to_grab: Frame) -> Result<InputVec<T::Input>, FortressError> {
231 if frame_to_grab.is_null() || frame_to_grab.as_i32() < 0 {
233 report_violation!(
234 ViolationSeverity::Error,
235 ViolationKind::FrameSync,
236 "inputs_at_frame called with invalid frame {:?}",
237 frame_to_grab
238 );
239 return Err(FortressError::InvalidFrameStructured {
240 frame: frame_to_grab,
241 reason: InvalidFrameReason::NullOrNegative,
242 });
243 }
244
245 let buffer_index = frame_to_grab.as_i32() as usize % self.buffer_size;
246 let player_inputs =
247 self.inputs
248 .get(buffer_index)
249 .ok_or(FortressError::InternalErrorStructured {
250 kind: InternalErrorKind::BufferIndexOutOfBounds,
251 })?;
252
253 let first_input = player_inputs
255 .first()
256 .ok_or(FortressError::InternalErrorStructured {
257 kind: InternalErrorKind::EmptyPlayerInputs,
258 })?;
259 if first_input.frame < frame_to_grab {
260 return Err(FortressError::PredictionThreshold);
261 }
262
263 if first_input.frame > frame_to_grab {
265 return Err(FortressError::SpectatorTooFarBehind);
266 }
267
268 Ok(player_inputs
269 .iter()
270 .enumerate()
271 .map(|(handle, player_input)| {
272 if let Some(status) = self.host_connect_status.get(handle) {
273 if status.disconnected && status.last_frame < frame_to_grab {
274 (player_input.input, InputStatus::Disconnected)
275 } else {
276 (player_input.input, InputStatus::Confirmed)
277 }
278 } else {
279 (player_input.input, InputStatus::Confirmed)
281 }
282 })
283 .collect())
284 }
285
286 fn handle_event(&mut self, event: Event<T>, addr: T::Address) {
287 match event {
288 Event::Synchronizing {
290 total,
291 count,
292 total_requests_sent,
293 elapsed_ms,
294 } => {
295 self.event_queue.push_back(FortressEvent::Synchronizing {
296 addr,
297 total,
298 count,
299 total_requests_sent,
300 elapsed_ms,
301 });
302 },
303 Event::NetworkInterrupted { disconnect_timeout } => {
305 self.event_queue
306 .push_back(FortressEvent::NetworkInterrupted {
307 addr,
308 disconnect_timeout,
309 });
310 },
311 Event::NetworkResumed => {
313 self.event_queue
314 .push_back(FortressEvent::NetworkResumed { addr });
315 },
316 Event::Synchronized => {
318 self.state = SessionState::Running;
319 self.event_queue
320 .push_back(FortressEvent::Synchronized { addr });
321 },
322 Event::Disconnected => {
324 self.event_queue
325 .push_back(FortressEvent::Disconnected { addr });
326 },
327 Event::SyncTimeout { elapsed_ms } => {
329 self.event_queue
330 .push_back(FortressEvent::SyncTimeout { addr, elapsed_ms });
331 },
332 Event::Input { input, player } => {
334 if input.frame.is_null() || input.frame.as_i32() < 0 {
336 report_violation!(
337 ViolationSeverity::Warning,
338 ViolationKind::FrameSync,
339 "Received input with invalid frame {:?} for player {} - ignoring",
340 input.frame,
341 player
342 );
343 return;
344 }
345
346 if player.as_usize() >= self.num_players {
348 report_violation!(
349 ViolationSeverity::Warning,
350 ViolationKind::InternalError,
351 "Received input for player {} but only {} players configured - ignoring",
352 player,
353 self.num_players
354 );
355 return;
356 }
357
358 let frame_index = input.frame.as_i32() as usize % self.buffer_size;
360 if let Some(frame_inputs) = self.inputs.get_mut(frame_index) {
361 if let Some(player_input) = frame_inputs.get_mut(player.as_usize()) {
362 *player_input = input;
363 } else {
364 report_violation!(
365 ViolationSeverity::Warning,
366 ViolationKind::InternalError,
367 "Failed to store input for player {} at frame {} - player index out of bounds",
368 player,
369 input.frame
370 );
371 return;
372 }
373 } else {
374 report_violation!(
375 ViolationSeverity::Warning,
376 ViolationKind::InternalError,
377 "Failed to store input at frame {} - frame index {} out of bounds",
378 input.frame,
379 frame_index
380 );
381 return;
382 }
383
384 if input.frame < self.last_recv_frame {
386 report_violation!(
387 ViolationSeverity::Warning,
388 ViolationKind::FrameSync,
389 "Received out-of-order input: frame {} is older than last_recv_frame {}",
390 input.frame,
391 self.last_recv_frame
392 );
393 }
395 if input.frame > self.last_recv_frame {
396 self.last_recv_frame = input.frame;
397 }
398
399 self.host.update_local_frame_advantage(input.frame);
401
402 for i in 0..self.num_players {
404 if let Some(status) = self.host_connect_status.get_mut(i) {
405 *status = self.host.peer_connect_status(PlayerHandle::new(i));
406 } else {
407 report_violation!(
408 ViolationSeverity::Warning,
409 ViolationKind::InternalError,
410 "Failed to update connection status for player {} - index out of bounds",
411 i
412 );
413 }
414 }
415 },
416 }
417
418 while self.event_queue.len() > self.max_event_queue_size {
420 self.event_queue.pop_front();
421 }
422 }
423}
424
425impl<T: Config> fmt::Debug for SpectatorSession<T> {
426 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
427 f.debug_struct("SpectatorSession")
428 .field("state", &self.state)
429 .field("num_players", &self.num_players)
430 .field("current_frame", &self.current_frame)
431 .field("last_recv_frame", &self.last_recv_frame)
432 .field("buffer_size", &self.buffer_size)
433 .field("max_frames_behind", &self.max_frames_behind)
434 .field("catchup_speed", &self.catchup_speed)
435 .finish_non_exhaustive()
436 }
437}
438
439#[cfg(test)]
440#[allow(
441 clippy::panic,
442 clippy::unwrap_used,
443 clippy::expect_used,
444 clippy::indexing_slicing,
445 clippy::needless_collect
446)]
447mod tests {
448 use super::*;
449 use crate::{Config, Message, NonBlockingSocket, SessionBuilder};
450 use std::net::{IpAddr, Ipv4Addr, SocketAddr};
451
452 struct TestConfig;
454
455 impl Config for TestConfig {
456 type Input = u8;
457 type State = u8;
458 type Address = SocketAddr;
459 }
460
461 fn test_addr(port: u16) -> SocketAddr {
462 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port)
463 }
464
465 struct DummySocket;
468
469 impl NonBlockingSocket<SocketAddr> for DummySocket {
470 fn send_to(&mut self, _msg: &Message, _addr: &SocketAddr) {}
471 fn receive_all_messages(&mut self) -> Vec<(SocketAddr, Message)> {
472 Vec::new()
473 }
474 }
475
476 fn create_test_spectator_session() -> Option<SpectatorSession<TestConfig>> {
478 SessionBuilder::new()
479 .with_num_players(2)
480 .unwrap()
481 .start_spectator_session(test_addr(7000), DummySocket)
482 }
483
484 fn create_test_spectator_session_with_config(
485 num_players: usize,
486 buffer_size: usize,
487 max_frames_behind: usize,
488 catchup_speed: usize,
489 ) -> Option<SpectatorSession<TestConfig>> {
490 use crate::SpectatorConfig;
491 SessionBuilder::new()
492 .with_num_players(num_players)
493 .unwrap()
494 .with_spectator_config(SpectatorConfig {
495 buffer_size,
496 catchup_speed,
497 max_frames_behind,
498 })
499 .start_spectator_session(test_addr(7001), DummySocket)
500 }
501
502 #[test]
507 fn spectator_session_creates_successfully() {
508 let session = create_test_spectator_session();
509 assert!(session.is_some());
510 }
511
512 #[test]
513 fn spectator_session_with_custom_config() {
514 let session = create_test_spectator_session_with_config(4, 120, 20, 3);
515 assert!(session.is_some());
516 let session = session.unwrap();
517 assert_eq!(session.num_players(), 4);
518 }
519
520 #[test]
521 fn spectator_session_single_player() {
522 let session = create_test_spectator_session_with_config(1, 60, 10, 1);
523 assert!(session.is_some());
524 let session = session.unwrap();
525 assert_eq!(session.num_players(), 1);
526 }
527
528 #[test]
529 fn spectator_session_many_players() {
530 let session = create_test_spectator_session_with_config(8, 60, 10, 1);
531 assert!(session.is_some());
532 let session = session.unwrap();
533 assert_eq!(session.num_players(), 8);
534 }
535
536 #[test]
541 fn spectator_session_initial_state_is_synchronizing() {
542 let session = create_test_spectator_session().unwrap();
543 assert_eq!(session.current_state(), SessionState::Synchronizing);
544 }
545
546 #[test]
547 fn spectator_session_initial_frame_is_null() {
548 let session = create_test_spectator_session().unwrap();
549 assert_eq!(session.current_frame(), Frame::NULL);
550 }
551
552 #[test]
553 fn spectator_session_num_players_returns_correct_count() {
554 let session2 = create_test_spectator_session_with_config(2, 60, 10, 1).unwrap();
555 assert_eq!(session2.num_players(), 2);
556
557 let session4 = create_test_spectator_session_with_config(4, 60, 10, 1).unwrap();
558 assert_eq!(session4.num_players(), 4);
559 }
560
561 #[test]
562 fn spectator_session_frames_behind_host_initially_zero() {
563 let session = create_test_spectator_session().unwrap();
564 assert_eq!(session.frames_behind_host(), 0);
567 }
568
569 #[test]
574 fn spectator_session_advance_frame_returns_not_synchronized_when_not_running() {
575 let mut session = create_test_spectator_session().unwrap();
576
577 let result = session.advance_frame();
579 assert!(result.is_err());
580 assert!(matches!(result, Err(FortressError::NotSynchronized)));
581 }
582
583 #[test]
588 fn spectator_session_network_stats_returns_not_synchronized_initially() {
589 let session = create_test_spectator_session().unwrap();
590
591 let result = session.network_stats();
593 assert!(result.is_err());
594 }
595
596 #[test]
601 fn spectator_session_events_initially_empty() {
602 let mut session = create_test_spectator_session().unwrap();
603 let events: Vec<_> = session.events().collect();
604 assert!(events.is_empty());
605 }
606
607 #[test]
608 fn spectator_session_events_drains_queue() {
609 let mut session = create_test_spectator_session().unwrap();
610
611 let events1: Vec<_> = session.events().collect();
613 assert!(events1.is_empty());
614
615 let events2: Vec<_> = session.events().collect();
617 assert!(events2.is_empty());
618 }
619
620 #[test]
625 fn spectator_session_violation_observer_none_by_default() {
626 let session = create_test_spectator_session().unwrap();
627 assert!(session.violation_observer().is_none());
628 }
629
630 #[test]
631 fn spectator_session_with_violation_observer() {
632 use crate::telemetry::CollectingObserver;
633
634 let observer = Arc::new(CollectingObserver::new());
635 let session: Option<SpectatorSession<TestConfig>> = SessionBuilder::new()
636 .with_num_players(2)
637 .unwrap()
638 .with_violation_observer(observer)
639 .start_spectator_session(test_addr(7002), DummySocket);
640
641 let session = session.unwrap();
642 assert!(session.violation_observer().is_some());
643 }
644
645 #[test]
650 fn spectator_session_poll_remote_clients_does_not_panic() {
651 let mut session = create_test_spectator_session().unwrap();
652
653 session.poll_remote_clients();
655
656 assert_eq!(session.current_state(), SessionState::Synchronizing);
658 }
659
660 #[test]
661 fn spectator_session_poll_remote_clients_multiple_times() {
662 let mut session = create_test_spectator_session().unwrap();
663
664 for _ in 0..10 {
666 session.poll_remote_clients();
667 }
668
669 assert_eq!(session.current_state(), SessionState::Synchronizing);
670 }
671
672 #[test]
677 fn spectator_config_default_values() {
678 use crate::SpectatorConfig;
679
680 let config = SpectatorConfig::default();
681 assert_eq!(config.buffer_size, 60);
682 assert_eq!(config.catchup_speed, 1);
683 assert_eq!(config.max_frames_behind, 10);
684 }
685
686 #[test]
687 fn spectator_config_new_equals_default() {
688 use crate::SpectatorConfig;
689
690 let new_config = SpectatorConfig::new();
691 let default_config = SpectatorConfig::default();
692 assert_eq!(new_config, default_config);
693 }
694
695 #[test]
696 fn spectator_config_fast_paced_preset() {
697 use crate::SpectatorConfig;
698
699 let config = SpectatorConfig::fast_paced();
700 assert_eq!(config.buffer_size, 90);
701 assert_eq!(config.catchup_speed, 2);
702 assert_eq!(config.max_frames_behind, 15);
703 }
704
705 #[test]
706 fn spectator_config_slow_connection_preset() {
707 use crate::SpectatorConfig;
708
709 let config = SpectatorConfig::slow_connection();
710 assert_eq!(config.buffer_size, 120);
711 assert_eq!(config.catchup_speed, 1);
712 assert_eq!(config.max_frames_behind, 20);
713 }
714
715 #[test]
716 fn spectator_config_local_preset() {
717 use crate::SpectatorConfig;
718
719 let config = SpectatorConfig::local();
720 assert_eq!(config.buffer_size, 30);
721 assert_eq!(config.catchup_speed, 2);
722 assert_eq!(config.max_frames_behind, 5);
723 }
724
725 #[test]
726 fn spectator_config_equality() {
727 use crate::SpectatorConfig;
728
729 let a = SpectatorConfig {
730 buffer_size: 100,
731 catchup_speed: 2,
732 max_frames_behind: 15,
733 };
734 let b = SpectatorConfig {
735 buffer_size: 100,
736 catchup_speed: 2,
737 max_frames_behind: 15,
738 };
739 assert_eq!(a, b);
740 }
741
742 #[test]
743 fn spectator_config_inequality() {
744 use crate::SpectatorConfig;
745
746 let a = SpectatorConfig::default();
747 let b = SpectatorConfig::fast_paced();
748 assert_ne!(a, b);
749 }
750
751 #[test]
752 fn spectator_config_clone() {
753 use crate::SpectatorConfig;
754
755 let original = SpectatorConfig::fast_paced();
756 let cloned = original;
757 assert_eq!(original, cloned);
758 }
759
760 #[test]
761 fn spectator_config_debug_format() {
762 use crate::SpectatorConfig;
763
764 let config = SpectatorConfig::default();
765 let debug_str = format!("{:?}", config);
766 assert!(debug_str.contains("SpectatorConfig"));
767 assert!(debug_str.contains("buffer_size"));
768 assert!(debug_str.contains("60"));
769 }
770
771 #[test]
772 fn spectator_config_all_presets_are_distinct() {
773 use crate::SpectatorConfig;
774
775 let default = SpectatorConfig::default();
776 let fast_paced = SpectatorConfig::fast_paced();
777 let slow_connection = SpectatorConfig::slow_connection();
778 let local = SpectatorConfig::local();
779
780 assert_ne!(default, fast_paced);
782 assert_ne!(default, slow_connection);
783 assert_ne!(default, local);
784 assert_ne!(fast_paced, slow_connection);
785 assert_ne!(fast_paced, local);
786 assert_ne!(slow_connection, local);
787 }
788
789 #[test]
794 fn spectator_session_with_minimum_buffer_size() {
795 let session = create_test_spectator_session_with_config(2, 1, 10, 1);
797 assert!(session.is_some());
798 }
799
800 #[test]
801 fn spectator_session_with_zero_buffer_size_uses_minimum() {
802 let session = create_test_spectator_session_with_config(2, 0, 10, 1);
804 assert!(session.is_some());
805 }
806
807 #[test]
808 fn spectator_session_with_large_buffer_size() {
809 let session = create_test_spectator_session_with_config(2, 1000, 10, 1);
810 assert!(session.is_some());
811 }
812
813 #[test]
814 fn spectator_session_with_high_catchup_speed() {
815 let session = create_test_spectator_session_with_config(2, 60, 10, 10);
816 assert!(session.is_some());
817 }
818
819 #[test]
820 fn spectator_session_with_zero_max_frames_behind() {
821 let session = create_test_spectator_session_with_config(2, 60, 0, 2);
823 assert!(session.is_some());
824 }
825
826 #[test]
831 fn spectator_session_buffer_size_respects_minimum() {
832 let session = create_test_spectator_session_with_config(2, 0, 10, 1).unwrap();
834 assert_eq!(session.num_players(), 2);
837 }
838
839 #[test]
840 fn spectator_session_host_connect_status_initialized() {
841 let session = create_test_spectator_session_with_config(4, 60, 10, 1).unwrap();
843 assert_eq!(session.num_players(), 4);
846 }
847
848 #[test]
849 fn spectator_session_last_recv_frame_initially_null() {
850 let session = create_test_spectator_session().unwrap();
851 assert_eq!(session.frames_behind_host(), 0);
854 }
855
856 #[test]
861 fn normal_speed_is_one() {
862 assert_eq!(NORMAL_SPEED, 1);
864 }
865
866 #[test]
871 fn spectator_session_current_frame_is_null_initially() {
872 let session = create_test_spectator_session().unwrap();
873 assert!(session.current_frame().is_null());
874 assert_eq!(session.current_frame(), Frame::NULL);
875 }
876
877 #[test]
882 fn spectator_session_state_transitions() {
883 let session = create_test_spectator_session().unwrap();
885 assert_eq!(session.current_state(), SessionState::Synchronizing);
886
887 }
890
891 #[test]
896 fn spectator_config_with_zero_catchup_speed() {
897 use crate::SpectatorConfig;
898
899 let config = SpectatorConfig {
901 buffer_size: 60,
902 catchup_speed: 0,
903 max_frames_behind: 10,
904 };
905 assert_eq!(config.catchup_speed, 0);
906 }
907
908 #[test]
909 fn spectator_config_extreme_values() {
910 use crate::SpectatorConfig;
911
912 let config = SpectatorConfig {
914 buffer_size: usize::MAX,
915 catchup_speed: usize::MAX,
916 max_frames_behind: usize::MAX,
917 };
918 assert_eq!(config.buffer_size, usize::MAX);
919 assert_eq!(config.catchup_speed, usize::MAX);
920 assert_eq!(config.max_frames_behind, usize::MAX);
921 }
922
923 #[test]
928 fn spectator_session_poll_preserves_state() {
929 let mut session = create_test_spectator_session().unwrap();
930
931 let initial_state = session.current_state();
933 let initial_frame = session.current_frame();
934
935 for _ in 0..5 {
937 session.poll_remote_clients();
938 }
939
940 assert_eq!(session.current_state(), initial_state);
942 assert_eq!(session.current_frame(), initial_frame);
943 }
944
945 #[test]
946 fn spectator_session_events_empty_after_drain() {
947 let mut session = create_test_spectator_session().unwrap();
948
949 let events: Vec<_> = session.events().collect();
951 assert!(events.is_empty());
952
953 session.poll_remote_clients();
955 let events: Vec<_> = session.events().collect();
956 assert!(events.is_empty());
957 }
958
959 #[test]
964 fn spectator_session_network_stats_before_sync() {
965 let session = create_test_spectator_session().unwrap();
966
967 let result = session.network_stats();
969 assert!(result.is_err());
970 assert!(matches!(result, Err(FortressError::NotSynchronized)));
971 }
972
973 #[test]
978 fn spectator_session_violation_observer_is_arc() {
979 use crate::telemetry::CollectingObserver;
980
981 let observer = Arc::new(CollectingObserver::new());
982 let observer_clone = Arc::clone(&observer);
983
984 let session: Option<SpectatorSession<TestConfig>> = SessionBuilder::new()
985 .with_num_players(2)
986 .unwrap()
987 .with_violation_observer(observer)
988 .start_spectator_session(test_addr(7003), DummySocket);
989
990 let session = session.unwrap();
991
992 assert!(session.violation_observer().is_some());
994
995 assert_eq!(observer_clone.violations().len(), 0);
997 }
998
999 #[test]
1000 fn spectator_session_without_violation_observer() {
1001 let session = create_test_spectator_session().unwrap();
1002 assert!(session.violation_observer().is_none());
1003 }
1004
1005 #[test]
1010 fn spectator_session_frames_behind_with_both_null() {
1011 let session = create_test_spectator_session().unwrap();
1012 assert_eq!(session.frames_behind_host(), 0);
1015 }
1016}