diff --git a/testing/tests/server_camera_runtime_contract.rs b/testing/tests/server_camera_runtime_contract.rs new file mode 100644 index 0000000..0d391fc --- /dev/null +++ b/testing/tests/server_camera_runtime_contract.rs @@ -0,0 +1,79 @@ +//! 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] +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)); +} diff --git a/testing/tests/server_uvc_runtime_contract.rs b/testing/tests/server_uvc_runtime_contract.rs new file mode 100644 index 0000000..73d7405 --- /dev/null +++ b/testing/tests/server_uvc_runtime_contract.rs @@ -0,0 +1,75 @@ +//! Integration coverage for the UVC runtime supervision contract. +//! +//! Scope: exercise the long-running UVC helper supervisor in both successful +//! spawn and spawn-failure modes. +//! Targets: `server/src/uvc_runtime.rs`. +//! Why: the helper supervisor is operationally critical and should be covered +//! through top-level integration behavior, not only unit checks. + +use lesavka_server::uvc_runtime::supervise_uvc_control; +use serial_test::serial; +use std::fs; +use std::os::unix::fs::PermissionsExt; +use std::time::Duration; +use temp_env::with_var; +use tempfile::tempdir; +use tokio::runtime::Runtime; + +#[test] +#[serial] +fn supervise_uvc_control_starts_helper_when_device_is_set() { + let dir = tempdir().expect("create temp dir"); + let marker = dir.path().join("marker.log"); + let helper = dir.path().join("helper.sh"); + fs::write( + &helper, + format!( + "#!/usr/bin/env bash\nset -euo pipefail\necho \"$*\" >> '{}'\n", + marker.display() + ), + ) + .expect("write helper script"); + let mut perms = fs::metadata(&helper) + .expect("helper metadata") + .permissions(); + perms.set_mode(0o755); + fs::set_permissions(&helper, perms).expect("chmod helper script"); + + let helper_path = helper.to_string_lossy().to_string(); + with_var("LESAVKA_UVC_DEV", Some("/dev/video-loop"), || { + let rt = Runtime::new().expect("runtime"); + let result = rt.block_on(async { + tokio::time::timeout( + Duration::from_millis(350), + supervise_uvc_control(helper_path), + ) + .await + }); + assert!(result.is_err(), "supervisor should still be running"); + }); + + let calls = fs::read_to_string(marker).expect("read helper marker"); + assert!( + calls.contains("--device /dev/video-loop"), + "expected helper to receive device args, got: {calls}" + ); +} + +#[test] +#[serial] +fn supervise_uvc_control_survives_missing_helper_binary() { + with_var("LESAVKA_UVC_DEV", Some("/dev/video-loop"), || { + let rt = Runtime::new().expect("runtime"); + let result = rt.block_on(async { + tokio::time::timeout( + Duration::from_millis(350), + supervise_uvc_control(String::from("/definitely/missing/lesavka-uvc")), + ) + .await + }); + assert!( + result.is_err(), + "supervisor should continue retrying forever" + ); + }); +}