//! Integration coverage for the camera runtime activation contract. //! //! Scope: validate activation error handling and configuration equality edges //! through public camera-runtime APIs. //! Targets: `server/src/camera_runtime.rs`. //! Why: camera runtime generation and guardrails are core to safe stream //! transitions, so they need direct integration-level assertions. use lesavka_server::camera::{CameraCodec, CameraConfig, CameraOutput, HdmiConnector}; use lesavka_server::camera_runtime::{CameraRuntime, camera_cfg_eq}; use serial_test::serial; use temp_env::with_var; use tokio::runtime::Runtime; use tonic::Code; #[test] #[serial] fn activate_rejects_uvc_when_disabled_and_bumps_generation() { let runtime = CameraRuntime::new(); let cfg = CameraConfig { output: CameraOutput::Uvc, codec: CameraCodec::Mjpeg, width: 1280, height: 720, fps: 25, hdmi: None, }; with_var("LESAVKA_DISABLE_UVC", Some("1"), || { let rt = Runtime::new().expect("runtime"); let result = rt.block_on(runtime.activate(&cfg)); match result { Ok(_) => panic!("UVC should be disabled"), Err(err) => assert_eq!(err.code(), Code::FailedPrecondition), } }); assert!(runtime.is_active(1)); assert!(!runtime.is_active(2)); } #[test] #[serial] fn activate_tracks_latest_generation_across_repeated_failures() { let runtime = CameraRuntime::new(); let cfg = CameraConfig { output: CameraOutput::Uvc, codec: CameraCodec::Mjpeg, width: 640, height: 360, fps: 30, hdmi: None, }; with_var("LESAVKA_DISABLE_UVC", Some("1"), || { let rt = Runtime::new().expect("runtime"); for expected in [1_u64, 2_u64, 3_u64] { let result = rt.block_on(runtime.activate(&cfg)); match result { Ok(_) => panic!("UVC should remain disabled"), Err(err) => assert_eq!(err.code(), Code::FailedPrecondition), } assert!(runtime.is_active(expected)); assert!(!runtime.is_active(expected + 1)); } }); } #[test] #[cfg(coverage)] fn activate_non_uvc_returns_noop_relay_in_coverage_harness() { let runtime = CameraRuntime::new(); let cfg = CameraConfig { output: CameraOutput::Hdmi, codec: CameraCodec::H264, width: 1920, height: 1080, fps: 30, hdmi: Some(HdmiConnector { name: String::from("HDMI-A-1"), id: Some(1), }), }; let rt = Runtime::new().expect("runtime"); let result = rt.block_on(runtime.activate(&cfg)); let (session_id, relay) = result.expect("coverage harness should create a no-op relay"); assert_eq!(session_id, 1); relay.feed(lesavka_common::lesavka::VideoPacket { id: 2, pts: 1, data: vec![0, 0, 0, 1, 0x65], ..Default::default() }); assert!(runtime.is_active(1)); assert!(!runtime.is_active(2)); } #[test] fn camera_cfg_eq_handles_none_and_hdmi_connector_edges() { let uvc_a = CameraConfig { output: CameraOutput::Uvc, codec: CameraCodec::Mjpeg, width: 640, height: 360, fps: 30, hdmi: None, }; let uvc_b = uvc_a.clone(); assert!(camera_cfg_eq(&uvc_a, &uvc_b)); let hdmi_a = CameraConfig { output: CameraOutput::Hdmi, codec: CameraCodec::H264, width: 1920, height: 1080, fps: 30, hdmi: Some(HdmiConnector { name: String::from("HDMI-A-1"), id: Some(7), }), }; let hdmi_b = hdmi_a.clone(); assert!(camera_cfg_eq(&hdmi_a, &hdmi_b)); let mut different_id = hdmi_a.clone(); different_id.hdmi = Some(HdmiConnector { name: String::from("HDMI-A-1"), id: Some(8), }); assert!(!camera_cfg_eq(&hdmi_a, &different_id)); let mut missing_connector = hdmi_a; missing_connector.hdmi = None; assert!(!camera_cfg_eq(&missing_connector, &hdmi_b)); } #[test] fn camera_cfg_eq_rejects_output_codec_resolution_and_fps_changes() { let base = CameraConfig { output: CameraOutput::Uvc, codec: CameraCodec::Mjpeg, width: 1280, height: 720, fps: 25, hdmi: None, }; let mut changed = base.clone(); changed.output = CameraOutput::Hdmi; assert!(!camera_cfg_eq(&base, &changed)); changed = base.clone(); changed.codec = CameraCodec::H264; assert!(!camera_cfg_eq(&base, &changed)); changed = base.clone(); changed.width = 1920; assert!(!camera_cfg_eq(&base, &changed)); changed = base.clone(); changed.height = 1080; assert!(!camera_cfg_eq(&base, &changed)); changed = base.clone(); changed.fps = 30; assert!(!camera_cfg_eq(&base, &changed)); }