//! Eye-hub coverage for shared server video fan-out. //! //! Scope: include `server/src/main.rs` and exercise hub fan-out, conflict //! pruning, and shutdown behavior. //! Targets: `server/src/main.rs`. //! Why: eye-feed hubs are latency-sensitive; stale hubs must stop and be //! replaced without freezing downstream previews. #[allow(warnings)] mod server_main_eye_hub { include!(env!("LESAVKA_SERVER_MAIN_SRC")); use futures_util::stream; use temp_env::with_var; fn with_capture_power_disabled(f: impl FnOnce()) { with_var("LESAVKA_CAPTURE_POWER_UNIT", Some("none"), f); } #[test] fn shared_eye_hub_forwards_inner_packets() { let rt = tokio::runtime::Runtime::new().expect("runtime"); with_capture_power_disabled(|| { rt.block_on(async { let lease = CapturePowerManager::new().acquire().await; let packet = VideoPacket { id: 2, pts: 42, data: vec![9, 8, 7], ..Default::default() }; let (packet_tx, packet_rx) = tokio::sync::mpsc::channel(1); let hub = EyeHub::spawn(ReceiverStream::new(packet_rx), lease); hub.subscribers .fetch_add(1, std::sync::atomic::Ordering::AcqRel); let mut rx = hub.tx.subscribe(); packet_tx .send(Ok(packet.clone())) .await .expect("send synthetic packet"); let observed = tokio::time::timeout(std::time::Duration::from_secs(1), rx.recv()) .await .expect("hub packet timeout") .expect("hub packet"); assert_eq!(observed.id, packet.id); assert_eq!(observed.pts, packet.pts); assert_eq!(observed.data, packet.data); drop(packet_tx); for _ in 0..20 { if !hub.running.load(std::sync::atomic::Ordering::Relaxed) { return; } tokio::time::sleep(std::time::Duration::from_millis(10)).await; } assert!( !hub.running.load(std::sync::atomic::Ordering::Relaxed), "hub should stop after the synthetic packet stream closes" ); }); }); } #[test] fn conflicting_eye_hubs_for_the_same_source_are_pruned_before_reopen() { let rt = tokio::runtime::Runtime::new().expect("runtime"); with_capture_power_disabled(|| { rt.block_on(async { let requested_key = EyeHubKey { source_id: 1, requested_width: 1280, requested_height: 720, requested_fps: 60, }; let stale_same_source_key = EyeHubKey { source_id: 1, requested_width: 1920, requested_height: 1080, requested_fps: 60, }; let keep_other_source_key = EyeHubKey { source_id: 0, requested_width: 1920, requested_height: 1080, requested_fps: 60, }; let stale_same_source = EyeHub::spawn( stream::pending::>(), CapturePowerManager::new().acquire().await, ); let stopped_other_source = EyeHub::spawn( stream::pending::>(), CapturePowerManager::new().acquire().await, ); stopped_other_source.shutdown(); let keep_other_source = EyeHub::spawn( stream::pending::>(), CapturePowerManager::new().acquire().await, ); let mut hubs = std::collections::HashMap::new(); hubs.insert(stale_same_source_key, stale_same_source.clone()); hubs.insert( EyeHubKey { source_id: 0, requested_width: 1280, requested_height: 720, requested_fps: 60, }, stopped_other_source, ); hubs.insert(keep_other_source_key, keep_other_source.clone()); let removed = take_conflicting_eye_hubs(&mut hubs, requested_key); assert_eq!(removed.len(), 2); assert!(!hubs.contains_key(&stale_same_source_key)); assert!(hubs.contains_key(&keep_other_source_key)); }); }); } #[test] fn eye_hub_shutdown_marks_the_hub_as_not_running() { let rt = tokio::runtime::Runtime::new().expect("runtime"); with_capture_power_disabled(|| { rt.block_on(async { let hub = EyeHub::spawn( stream::pending::>(), CapturePowerManager::new().acquire().await, ); assert!(hub.running.load(std::sync::atomic::Ordering::Relaxed)); hub.shutdown(); assert!(!hub.running.load(std::sync::atomic::Ordering::Relaxed)); }); }); } #[test] #[cfg(coverage)] fn eye_hub_forwarder_handles_dropped_downstream() { let rt = tokio::runtime::Runtime::new().expect("runtime"); with_capture_power_disabled(|| { rt.block_on(async { let hub = EyeHub::spawn( stream::pending::>(), CapturePowerManager::new().acquire().await, ); hub.subscribers .store(1, std::sync::atomic::Ordering::Relaxed); let (source_tx, source_rx) = tokio::sync::broadcast::channel(1); let (out_tx, out_rx) = tokio::sync::mpsc::channel(1); drop(out_rx); source_tx .send(VideoPacket { id: 0, pts: 1, data: vec![1], ..Default::default() }) .expect("send packet"); forward_eye_hub_packets( 9, source_rx, out_tx, std::sync::Arc::clone(&hub.subscribers), std::sync::Arc::clone(&hub), ) .await; assert_eq!( hub.subscribers.load(std::sync::atomic::Ordering::Relaxed), 0 ); assert!(!hub.running.load(std::sync::atomic::Ordering::Relaxed)); }); }); } #[test] #[cfg(coverage)] fn eye_hub_forwarder_skips_lagged_frames_and_closes_cleanly() { let rt = tokio::runtime::Runtime::new().expect("runtime"); with_capture_power_disabled(|| { rt.block_on(async { let hub = EyeHub::spawn( stream::pending::>(), CapturePowerManager::new().acquire().await, ); hub.subscribers .store(1, std::sync::atomic::Ordering::Relaxed); let (source_tx, source_rx) = tokio::sync::broadcast::channel(1); let (out_tx, mut out_rx) = tokio::sync::mpsc::channel(2); source_tx .send(VideoPacket { id: 0, pts: 1, data: vec![1], ..Default::default() }) .expect("send first packet"); source_tx .send(VideoPacket { id: 0, pts: 2, data: vec![2], ..Default::default() }) .expect("send second packet"); drop(source_tx); forward_eye_hub_packets( 7, source_rx, out_tx, std::sync::Arc::clone(&hub.subscribers), std::sync::Arc::clone(&hub), ) .await; let packet = out_rx .recv() .await .expect("forwarded packet") .expect("packet"); assert_eq!(packet.id, 7); assert_eq!(packet.pts, 2); assert_eq!(packet.data, vec![2]); assert!(out_rx.recv().await.is_none()); assert_eq!( hub.subscribers.load(std::sync::atomic::Ordering::Relaxed), 0 ); assert!(!hub.running.load(std::sync::atomic::Ordering::Relaxed)); }); }); } }