//! Integration coverage for server state-oriented RPC handler branches. //! //! Scope: include `server/src/main.rs` and exercise calibration, capture-power, //! and upstream-sync RPC surfaces. //! Targets: `server/src/main.rs`. //! Why: these RPCs expose live operational state, so tests should guard reply //! shapes without requiring gadget, HID, or capture hardware. #[allow(warnings)] mod server_main_state_rpc { include!(env!("LESAVKA_SERVER_MAIN_SRC")); use serial_test::serial; use temp_env::with_var; use tempfile::tempdir; fn build_handler_for_tests_with_modes( kb_writable: bool, ms_writable: bool, ) -> (tempfile::TempDir, Handler) { let dir = tempdir().expect("tempdir"); let kb_path = dir.path().join("hidg0.bin"); let ms_path = dir.path().join("hidg1.bin"); std::fs::write(&kb_path, []).expect("create kb file"); std::fs::write(&ms_path, []).expect("create ms file"); let kb = tokio::fs::File::from_std( std::fs::OpenOptions::new() .read(true) .write(kb_writable) .create(kb_writable) .truncate(kb_writable) .open(&kb_path) .expect("open kb"), ); let ms = tokio::fs::File::from_std( std::fs::OpenOptions::new() .read(true) .write(ms_writable) .create(ms_writable) .truncate(ms_writable) .open(&ms_path) .expect("open ms"), ); let handler = with_var("LESAVKA_CAPTURE_POWER_UNIT", Some("none"), || Handler { kb: std::sync::Arc::new(tokio::sync::Mutex::new(Some(kb))), ms: std::sync::Arc::new(tokio::sync::Mutex::new(Some(ms))), gadget: UsbGadget::new("lesavka"), did_cycle: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)), camera_rt: std::sync::Arc::new(CameraRuntime::new()), upstream_media_rt: std::sync::Arc::new(UpstreamMediaRuntime::new()), calibration: std::sync::Arc::new(CalibrationStore::load(std::sync::Arc::new( UpstreamMediaRuntime::new(), ))), capture_power: CapturePowerManager::new(), eye_hubs: std::sync::Arc::new( tokio::sync::Mutex::new(std::collections::HashMap::new()), ), }); (dir, handler) } fn build_handler_for_tests() -> (tempfile::TempDir, Handler) { build_handler_for_tests_with_modes(true, true) } #[test] #[cfg(coverage)] #[serial] fn capture_power_rpcs_surface_stub_snapshot_and_manual_modes() { let (_dir, handler) = build_handler_for_tests(); let rt = tokio::runtime::Runtime::new().expect("runtime"); with_var( "LESAVKA_TEST_UDEV_CAPTURE_DEVICES", Some("not-a-number"), || { assert_eq!(Handler::detected_capture_devices_from_udev(), 0); }, ); with_var("LESAVKA_TEST_UDEV_CAPTURE_DEVICES", Some("9"), || { assert_eq!(Handler::detected_capture_devices_from_udev(), 2); }); let snapshot = rt .block_on(async { handler .get_capture_power(tonic::Request::new(Empty {})) .await }) .expect("capture power snapshot") .into_inner(); assert!(snapshot.available); assert!(!snapshot.enabled); assert_eq!(snapshot.mode, "auto"); let forced_on = rt .block_on(async { handler .set_capture_power(tonic::Request::new(SetCapturePowerRequest { enabled: true, command: CapturePowerCommand::ForceOn as i32, })) .await }) .expect("force capture power on") .into_inner(); assert!(forced_on.available); assert!(forced_on.enabled); assert_eq!(forced_on.mode, "forced-on"); let forced_off = rt .block_on(async { handler .set_capture_power(tonic::Request::new(SetCapturePowerRequest { enabled: false, command: CapturePowerCommand::ForceOff as i32, })) .await }) .expect("force capture power off") .into_inner(); assert!(forced_off.available); assert!(!forced_off.enabled); assert_eq!(forced_off.mode, "forced-off"); let auto = rt .block_on(async { handler .set_capture_power(tonic::Request::new(SetCapturePowerRequest { enabled: false, command: CapturePowerCommand::Auto as i32, })) .await }) .expect("return capture power to auto") .into_inner(); assert!(auto.available); assert!(!auto.enabled); assert_eq!(auto.mode, "auto"); let legacy_fallback = rt .block_on(async { handler .set_capture_power(tonic::Request::new(SetCapturePowerRequest { enabled: true, command: CapturePowerCommand::Unspecified as i32, })) .await }) .expect("legacy bool fallback") .into_inner(); assert!(legacy_fallback.available); assert!(legacy_fallback.enabled); assert_eq!(legacy_fallback.mode, "forced-on"); } #[test] #[cfg(coverage)] #[serial] fn calibration_rpcs_surface_current_state_and_apply_updates() { let dir = tempdir().expect("calibration dir"); let calibration_path = dir.path().join("calibration.toml"); with_var( "LESAVKA_CALIBRATION_PATH", Some(calibration_path.to_string_lossy().to_string()), || { let (_dir, handler) = build_handler_for_tests(); let rt = tokio::runtime::Runtime::new().expect("runtime"); let initial = rt .block_on(async { handler.get_calibration(tonic::Request::new(Empty {})).await }) .expect("initial calibration") .into_inner(); assert_eq!(initial.profile, "mjpeg"); assert_eq!(initial.active_audio_offset_us, 0); let initial_video_offset_us = initial.active_video_offset_us; let adjusted = rt .block_on(async { handler .calibrate(tonic::Request::new(CalibrationRequest { action: lesavka_common::lesavka::CalibrationAction::BlindEstimate as i32, audio_delta_us: 10_000, video_delta_us: 2_000, observed_delivery_skew_ms: 42.0, observed_enqueue_skew_ms: 2.5, note: "coverage estimate".to_string(), })) .await }) .expect("calibrate") .into_inner(); assert_eq!(adjusted.source, "blind"); assert_eq!(adjusted.active_audio_offset_us, 10_000); assert_eq!( adjusted.active_video_offset_us, initial_video_offset_us + 2_000 ); assert!( std::fs::read_to_string(calibration_path) .expect("persisted") .contains("active_audio_offset_us=10000") ); }, ); } #[test] #[cfg(coverage)] #[serial] fn upstream_sync_rpc_surfaces_planner_snapshot() { let (_dir, handler) = build_handler_for_tests(); let rt = tokio::runtime::Runtime::new().expect("runtime"); let lease_camera = handler.upstream_media_rt.activate_camera(); let lease_microphone = handler.upstream_media_rt.activate_microphone(); assert_eq!(lease_camera.session_id, lease_microphone.session_id); let initial = rt .block_on(async { handler .get_upstream_sync(tonic::Request::new(Empty {})) .await }) .expect("planner sync state") .into_inner(); assert_eq!(initial.phase, "acquiring"); assert_eq!(initial.session_id, lease_camera.session_id); handler.upstream_media_rt.record_client_timing( UpstreamMediaKind::Camera, UpstreamClientTiming { capture_pts_us: 1_000_000, send_pts_us: 1_010_000, queue_depth: 2, queue_age_ms: 20, }, ); std::thread::sleep(Duration::from_millis(1)); handler.upstream_media_rt.record_client_timing( UpstreamMediaKind::Microphone, UpstreamClientTiming { capture_pts_us: 1_001_500, send_pts_us: 1_012_000, queue_depth: 3, queue_age_ms: 25, }, ); let due = tokio::time::Instant::now() - Duration::from_millis(3); handler.upstream_media_rt.mark_video_presented(10_000, due); handler.upstream_media_rt.mark_audio_presented(11_500, due); let live = rt .block_on(async { handler .get_upstream_sync(tonic::Request::new(Empty {})) .await }) .expect("live planner sync state") .into_inner(); assert_eq!(live.phase, "live"); assert_eq!(live.latest_camera_remote_pts_us, Some(1_000_000)); assert_eq!(live.latest_microphone_remote_pts_us, Some(1_001_500)); assert_eq!(live.last_video_presented_pts_us, Some(10_000)); assert_eq!(live.last_audio_presented_pts_us, Some(11_500)); assert!(live.live_lag_ms.is_some()); assert_eq!(live.planner_skew_ms, Some(1.5)); assert_eq!(live.client_capture_skew_ms, Some(1.5)); assert_eq!(live.client_send_skew_ms, Some(2.0)); assert!(live.server_receive_skew_ms.is_some()); assert_eq!(live.camera_client_queue_age_ms, Some(20.0)); assert_eq!(live.microphone_client_queue_age_ms, Some(25.0)); assert!(live.camera_server_receive_age_ms.is_some()); assert!(live.microphone_server_receive_age_ms.is_some()); assert!(live.client_capture_abs_skew_p95_ms.is_some()); assert!(live.client_send_abs_skew_p95_ms.is_some()); assert!(live.server_receive_abs_skew_p95_ms.is_some()); assert!(live.camera_client_queue_age_p95_ms.is_some()); assert!(live.microphone_client_queue_age_p95_ms.is_some()); assert!(live.sink_handoff_skew_ms.is_some()); assert!(live.sink_handoff_abs_skew_p95_ms.is_some()); assert!(live.camera_sink_late_ms.is_some()); assert!(live.microphone_sink_late_ms.is_some()); assert!(live.camera_sink_late_p95_ms.is_some()); assert!(live.microphone_sink_late_p95_ms.is_some()); assert_eq!(live.client_timing_window_samples, 1); assert_eq!(live.sink_handoff_window_samples, 1); handler .upstream_media_rt .record_video_freeze("coverage freeze"); let healing = rt .block_on(async { handler .get_upstream_sync(tonic::Request::new(Empty {})) .await }) .expect("healing planner sync state") .into_inner(); assert_eq!(healing.phase, "healing"); assert_eq!(healing.video_freezes, 1); assert_eq!(healing.last_reason, "coverage freeze"); } #[test] #[cfg(coverage)] #[serial] fn recover_soft_rpcs_surface_uac_success_and_non_uvc_guard() { with_var("LESAVKA_CAM_OUTPUT", Some("hdmi"), || { let (_dir, handler) = build_handler_for_tests(); let rt = tokio::runtime::Runtime::new().expect("runtime"); let uac = rt .block_on(async { handler.recover_uac(tonic::Request::new(Empty {})).await }) .expect("uac recovery") .into_inner(); assert!(uac.ok); let uvc = rt .block_on(async { handler.recover_uvc(tonic::Request::new(Empty {})).await }) .expect_err("HDMI output should reject soft UVC recovery"); assert_eq!(uvc.code(), tonic::Code::FailedPrecondition); assert!( uvc.to_string().contains("hdmi"), "unexpected UVC recovery error: {uvc}" ); }); } #[test] #[cfg(coverage)] #[serial] fn recover_soft_helpers_cover_usb_state_failure_and_uvc_success() { let (_dir, handler) = build_handler_for_tests(); let rt = tokio::runtime::Runtime::new().expect("runtime"); let usb = rt .block_on(async { handler.recover_usb_reply().await }) .expect_err("missing fake UDC state should be reported clearly"); assert_eq!(usb.code(), tonic::Code::FailedPrecondition); assert!(usb.to_string().contains("could not read UDC state")); let usb_rpc = rt .block_on(async { handler.recover_usb(tonic::Request::new(Empty {})).await }) .expect_err("RPC wrapper should surface the same missing fake UDC state"); assert_eq!(usb_rpc.code(), tonic::Code::FailedPrecondition); assert!(usb_rpc.to_string().contains("could not read UDC state")); with_var("LESAVKA_CAM_OUTPUT", Some("uvc"), || { let uvc = rt .block_on(async { handler.recover_uvc_reply().await }) .expect("UVC soft recovery should retire the active relay") .into_inner(); assert!(uvc.ok); }); } }