From 659a77ff5f2d339c435b42d32a00012f87777cf8 Mon Sep 17 00:00:00 2001 From: Brad Stein Date: Sun, 12 Apr 2026 12:22:41 -0300 Subject: [PATCH] testing: expand layout and camera contracts --- testing/coverage_contract.json | 3 + testing/tests/client_layout_contract.rs | 55 +++++++++++- testing/tests/server_camera_contract.rs | 113 ++++++++++++++++++++++++ 3 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 testing/tests/server_camera_contract.rs diff --git a/testing/coverage_contract.json b/testing/coverage_contract.json index a4afa1d..6c4f085 100644 --- a/testing/coverage_contract.json +++ b/testing/coverage_contract.json @@ -3,9 +3,12 @@ "files": [ "client/src/app_support.rs", "client/src/input/keymap.rs", + "client/src/layout.rs", "client/src/output/layout.rs", + "client/src/paste.rs", "common/src/cli.rs", "common/src/hid.rs", + "common/src/lib.rs", "common/src/paste.rs", "server/src/handshake.rs", "server/src/paste.rs", diff --git a/testing/tests/client_layout_contract.rs b/testing/tests/client_layout_contract.rs index 73af553..eb968bd 100644 --- a/testing/tests/client_layout_contract.rs +++ b/testing/tests/client_layout_contract.rs @@ -20,9 +20,10 @@ fn install_sway_stub(dir: &std::path::Path) -> std::path::PathBuf { set -euo pipefail if [[ "${1:-}" == "-t" && "${2:-}" == "get_outputs" && "${3:-}" == "-r" ]]; then printf '%s\n' "${LESAVKA_TEST_SWAY_OUTPUTS:-[]}" - exit 0 + exit "${LESAVKA_TEST_SWAY_GET_OUTPUTS_EXIT:-0}" fi echo "$*" >> "${LESAVKA_TEST_SWAY_LOG}" +exit "${LESAVKA_TEST_SWAY_PLACE_EXIT:-0}" "#, ) .expect("write swaymsg stub"); @@ -53,6 +54,16 @@ fn run_apply_with_outputs(layout: Layout, outputs_json: &str) -> Option fs::read_to_string(log_path).ok() } +fn run_apply_without_swaymsg(layout: Layout) { + with_var( + "PATH", + Some("/definitely-missing-lesavka-test-path"), + || { + apply(layout); + }, + ); +} + #[test] #[serial] fn side_by_side_places_each_eye_on_half_of_focused_output() { @@ -97,3 +108,45 @@ fn apply_skips_window_commands_when_no_output_is_focused() { "expected no placement calls, got: {log}" ); } + +#[test] +#[serial] +fn apply_skips_window_commands_when_outputs_shape_is_not_an_array() { + let log = run_apply_with_outputs(Layout::SideBySide, r#"{"focused":true}"#).unwrap_or_default(); + assert!( + log.trim().is_empty(), + "expected no placement calls, got: {log}" + ); +} + +#[test] +#[serial] +fn apply_tolerates_nonzero_window_placement_exit_status() { + let dir = tempdir().expect("create temp dir"); + install_sway_stub(dir.path()); + let log_path = dir.path().join("sway.log"); + let old_path = std::env::var("PATH").unwrap_or_default(); + let new_path = format!("{}:{old_path}", dir.path().display()); + let outputs = r#"[{"focused":true,"rect":{"x":0,"y":0,"width":640,"height":360}}]"#; + let log_value = log_path.to_string_lossy().to_string(); + + with_var("PATH", Some(new_path.as_str()), || { + with_var("LESAVKA_TEST_SWAY_LOG", Some(log_value.as_str()), || { + with_var("LESAVKA_TEST_SWAY_OUTPUTS", Some(outputs), || { + with_var("LESAVKA_TEST_SWAY_PLACE_EXIT", Some("2"), || { + apply(Layout::SideBySide); + }); + }); + }); + }); + + let log = fs::read_to_string(log_path).expect("read command log"); + assert!(log.contains(r#"[title="^Lesavka-eye-0$"]"#)); + assert!(log.contains(r#"[title="^Lesavka-eye-1$"]"#)); +} + +#[test] +#[serial] +fn apply_handles_missing_sway_binary_without_panicking() { + run_apply_without_swaymsg(Layout::SideBySide); +} diff --git a/testing/tests/server_camera_contract.rs b/testing/tests/server_camera_contract.rs new file mode 100644 index 0000000..a0f1182 --- /dev/null +++ b/testing/tests/server_camera_contract.rs @@ -0,0 +1,113 @@ +//! Integration coverage for the server camera-selection contract. +//! +//! Scope: exercise environment-driven camera mode selection and stable enum +//! string mappings via the public camera module API. +//! Targets: `server/src/camera.rs`. +//! Why: camera policy decides the entire stream transport path, so these +//! behavior contracts belong in centralized testing. + +use lesavka_server::camera::{ + CameraCodec, CameraOutput, current_camera_config, update_camera_config, +}; +use serial_test::serial; +use temp_env::with_var; + +#[test] +fn camera_enum_strings_are_stable() { + assert_eq!(CameraOutput::Uvc.as_str(), "uvc"); + assert_eq!(CameraOutput::Hdmi.as_str(), "hdmi"); + assert_eq!(CameraCodec::Mjpeg.as_str(), "mjpeg"); + assert_eq!(CameraCodec::H264.as_str(), "h264"); +} + +#[test] +#[serial] +fn camera_config_uses_interval_when_fps_is_unset_or_invalid() { + with_var("LESAVKA_CAM_OUTPUT", Some("uvc"), || { + with_var("LESAVKA_UVC_WIDTH", Some("640"), || { + with_var("LESAVKA_UVC_HEIGHT", Some("360"), || { + with_var("LESAVKA_UVC_FPS", None::<&str>, || { + with_var("LESAVKA_UVC_INTERVAL", Some("250000"), || { + let cfg = update_camera_config(); + assert_eq!(cfg.output, CameraOutput::Uvc); + assert_eq!(cfg.codec, CameraCodec::Mjpeg); + assert_eq!(cfg.width, 640); + assert_eq!(cfg.height, 360); + assert_eq!(cfg.fps, 40); + }); + }); + + with_var("LESAVKA_UVC_FPS", Some("not-a-number"), || { + with_var("LESAVKA_UVC_INTERVAL", Some("250000"), || { + let cfg = update_camera_config(); + assert_eq!(cfg.output, CameraOutput::Uvc); + assert_eq!(cfg.codec, CameraCodec::Mjpeg); + assert_eq!(cfg.width, 640); + assert_eq!(cfg.height, 360); + assert_eq!(cfg.fps, 40); + }); + }); + }); + }); + }); +} + +#[test] +#[serial] +fn camera_config_zero_interval_falls_back_to_default_fps() { + with_var("LESAVKA_CAM_OUTPUT", Some("uvc"), || { + with_var("LESAVKA_UVC_WIDTH", Some("800"), || { + with_var("LESAVKA_UVC_HEIGHT", Some("600"), || { + with_var("LESAVKA_UVC_FPS", None::<&str>, || { + with_var("LESAVKA_UVC_INTERVAL", Some("0"), || { + let cfg = update_camera_config(); + assert_eq!(cfg.output, CameraOutput::Uvc); + assert_eq!(cfg.codec, CameraCodec::Mjpeg); + assert_eq!(cfg.width, 800); + assert_eq!(cfg.height, 600); + assert_eq!(cfg.fps, 25); + }); + }); + }); + }); + }); +} + +#[test] +#[serial] +fn camera_config_forced_hdmi_tracks_cached_state() { + with_var("LESAVKA_CAM_OUTPUT", Some("hdmi"), || { + let cfg = update_camera_config(); + assert_eq!(cfg.output, CameraOutput::Hdmi); + assert_eq!(cfg.codec, CameraCodec::H264); + assert_eq!(cfg.fps, 30); + assert!(matches!( + (cfg.width, cfg.height), + (1920, 1080) | (1280, 720) + )); + + let cached = current_camera_config(); + assert_eq!(cached.output, CameraOutput::Hdmi); + assert_eq!(cached.codec, CameraCodec::H264); + assert_eq!(cached.fps, 30); + }); +} + +#[test] +#[serial] +fn camera_config_output_override_is_case_insensitive() { + with_var("LESAVKA_CAM_OUTPUT", Some(" UVC "), || { + with_var("LESAVKA_UVC_WIDTH", Some("1024"), || { + with_var("LESAVKA_UVC_HEIGHT", Some("576"), || { + with_var("LESAVKA_UVC_FPS", Some("12"), || { + let cfg = update_camera_config(); + assert_eq!(cfg.output, CameraOutput::Uvc); + assert_eq!(cfg.codec, CameraCodec::Mjpeg); + assert_eq!(cfg.width, 1024); + assert_eq!(cfg.height, 576); + assert_eq!(cfg.fps, 12); + }); + }); + }); + }); +}