From 8f911da06b1b4c46922d64e3a9ed0785a692f9ce Mon Sep 17 00:00:00 2001 From: Brad Stein Date: Tue, 19 May 2026 10:23:20 -0300 Subject: [PATCH] test(lesavka): cover camera and keyboard helpers --- client/src/input/camera.rs | 41 +++++++++++ client/src/input/tests/keyboard.rs | 107 ++++++++++++++++++++++++++++- 2 files changed, 147 insertions(+), 1 deletion(-) diff --git a/client/src/input/camera.rs b/client/src/input/camera.rs index 078a248..be8bf56 100644 --- a/client/src/input/camera.rs +++ b/client/src/input/camera.rs @@ -247,4 +247,45 @@ mod tests { ("x265enc", Some("key-int-max")) ); } + + #[cfg(coverage)] + #[test] + #[serial] + /// Coverage builds can exercise H.264 encoder branches without host GPU assumptions. + fn coverage_h264_encoder_choice_honors_stable_test_overrides() { + let cases = [ + ("nvh264enc", ("nvh264enc", None)), + ("vulkanh264enc", ("vulkanh264enc", Some("idr-period"))), + ("vaapih264enc", ("vaapih264enc", Some("keyframe-period"))), + ("v4l2h264enc", ("v4l2h264enc", Some("idrcount"))), + ("unknown", ("x264enc", Some("key-int-max"))), + ]; + + for (override_value, expected) in cases { + temp_env::with_var("LESAVKA_CAM_TEST_ENCODER", Some(override_value), || { + assert_eq!(CameraCapture::choose_encoder().unwrap(), expected); + }); + } + } + + #[cfg(coverage)] + #[test] + /// Coverage mode keeps software video fallback enabled for deterministic tests. + fn coverage_software_video_fallback_is_enabled() { + assert!(CameraCapture::software_video_fallback_allowed()); + } + + #[cfg(coverage)] + #[test] + /// Coverage mode replaces the FFmpeg preview reader with an inert cancellation flag. + fn coverage_ffmpeg_preview_tap_stub_starts_stopped() { + let running = super::spawn_ffmpeg_raw_preview_tap( + std::io::empty(), + std::path::PathBuf::from("/tmp/lesavka-preview-tap.coverage"), + 1, + 1, + ); + + assert!(!running.load(std::sync::atomic::Ordering::Acquire)); + } } diff --git a/client/src/input/tests/keyboard.rs b/client/src/input/tests/keyboard.rs index 5d8fd54..05ec614 100644 --- a/client/src/input/tests/keyboard.rs +++ b/client/src/input/tests/keyboard.rs @@ -1,5 +1,14 @@ -use super::{is_paste_modifier, paste_key_available_from_env, paste_rpc_enabled}; +use super::{ + build_keyboard_report, debounce_gate, is_modifier, is_paste_modifier, keycode_to_usage, + live_modifier_delay, modifier_only_report, paste_key_available_from_env, paste_rpc_enabled, + read_clipboard_text, should_stage_modifier_report, update_pressed_keys, +}; use evdev::KeyCode; +use std::{ + collections::HashSet, + sync::atomic::{AtomicU64, Ordering}, + time::Duration, +}; use tempfile::tempdir; #[test] @@ -47,3 +56,99 @@ fn paste_modifier_recognizes_ctrl_alt_only() { assert!(!is_paste_modifier(KeyCode::KEY_V)); assert!(!is_paste_modifier(KeyCode::KEY_LEFTSHIFT)); } + +#[test] +fn keyboard_report_sorts_deduplicates_and_keeps_modifiers() { + let report = build_keyboard_report([ + KeyCode::KEY_B, + KeyCode::KEY_LEFTSHIFT, + KeyCode::KEY_A, + KeyCode::KEY_A, + KeyCode::KEY_RIGHTCTRL, + ]); + + assert_eq!( + report[0], + is_modifier(KeyCode::KEY_LEFTSHIFT).unwrap() | is_modifier(KeyCode::KEY_RIGHTCTRL).unwrap() + ); + assert_eq!(report[2], keycode_to_usage(KeyCode::KEY_A).unwrap()); + assert_eq!(report[3], keycode_to_usage(KeyCode::KEY_B).unwrap()); + assert_eq!(&report[4..], &[0, 0, 0, 0]); +} + +#[test] +fn modifier_staging_requires_modified_non_modifier_keydown() { + let mut report = [0; 8]; + report[0] = is_modifier(KeyCode::KEY_LEFTCTRL).unwrap(); + report[2] = keycode_to_usage(KeyCode::KEY_V).unwrap(); + + assert!(should_stage_modifier_report(KeyCode::KEY_V, 1, report)); + assert!(!should_stage_modifier_report(KeyCode::KEY_V, 0, report)); + assert!(!should_stage_modifier_report( + KeyCode::KEY_LEFTCTRL, + 1, + report + )); + + let mut unmodified = report; + unmodified[0] = 0; + assert!(!should_stage_modifier_report(KeyCode::KEY_V, 1, unmodified)); +} + +#[test] +fn modifier_only_report_clears_non_modifier_slots() { + assert_eq!( + modifier_only_report(is_modifier(KeyCode::KEY_LEFTALT).unwrap()), + [ + is_modifier(KeyCode::KEY_LEFTALT).unwrap(), + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + ); +} + +#[test] +fn live_modifier_delay_honors_zero_override() { + temp_env::with_var("LESAVKA_LIVE_MODIFIER_DELAY_MS", Some("0"), || { + assert_eq!(live_modifier_delay(), Duration::ZERO); + }); +} + +#[test] +fn clipboard_reader_uses_successful_command_output() { + temp_env::with_var( + "LESAVKA_CLIPBOARD_CMD", + Some("printf clipboard-data"), + || { + assert_eq!(read_clipboard_text().as_deref(), Some("clipboard-data")); + }, + ); +} + +#[test] +fn debounce_gate_updates_timestamp_even_when_blocked() { + let last = AtomicU64::new(0); + + assert!(debounce_gate(&last, 1_000, 250)); + assert_eq!(last.load(Ordering::Relaxed), 1_000); + assert!(!debounce_gate(&last, 1_100, 250)); + assert_eq!(last.load(Ordering::Relaxed), 1_100); + assert!(debounce_gate(&last, 1_400, 250)); +} + +#[test] +fn pressed_key_shadow_tracks_press_release_only() { + let mut pressed = HashSet::new(); + + update_pressed_keys(&mut pressed, KeyCode::KEY_A, 1); + assert!(pressed.contains(&KeyCode::KEY_A)); + update_pressed_keys(&mut pressed, KeyCode::KEY_B, -1); + assert!(!pressed.contains(&KeyCode::KEY_B)); + update_pressed_keys(&mut pressed, KeyCode::KEY_A, 0); + assert!(!pressed.contains(&KeyCode::KEY_A)); +}