test(lesavka): cover input routing and synthetic uplink helpers
This commit is contained in:
parent
4dd2bfad51
commit
679a744ec8
@ -1,5 +1,18 @@
|
|||||||
use super::parse_quick_toggle_key;
|
use super::{
|
||||||
|
InputAggregator, input_device_override_from_env, matches_selected_input_device,
|
||||||
|
pending_release_timeout_from_env, quick_toggle_debounce_from_env,
|
||||||
|
remote_failsafe_timeout_from_env, update_shadow_pressed_keys,
|
||||||
|
};
|
||||||
use evdev::KeyCode;
|
use evdev::KeyCode;
|
||||||
|
use serial_test::serial;
|
||||||
|
use std::{
|
||||||
|
collections::HashSet,
|
||||||
|
path::Path,
|
||||||
|
sync::atomic::Ordering,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::parse_quick_toggle_key;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_quick_toggle_key_supports_letters_digits_and_function_keys() {
|
fn parse_quick_toggle_key_supports_letters_digits_and_function_keys() {
|
||||||
@ -28,3 +41,160 @@ fn parse_quick_toggle_key_can_disable_or_fall_back() {
|
|||||||
Some(KeyCode::KEY_PAUSE)
|
Some(KeyCode::KEY_PAUSE)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn runtime_timeouts_apply_safe_defaults_and_floors() {
|
||||||
|
temp_env::with_vars(
|
||||||
|
[
|
||||||
|
("LESAVKA_INPUT_TOGGLE_DEBOUNCE_MS", None::<&str>),
|
||||||
|
("LESAVKA_INPUT_RELEASE_TIMEOUT_MS", None::<&str>),
|
||||||
|
("LESAVKA_INPUT_REMOTE_FAILSAFE_MS", None::<&str>),
|
||||||
|
("LESAVKA_INPUT_REMOTE_FAILSAFE_SECS", None::<&str>),
|
||||||
|
],
|
||||||
|
|| {
|
||||||
|
assert_eq!(quick_toggle_debounce_from_env(), Duration::from_millis(350));
|
||||||
|
assert_eq!(
|
||||||
|
pending_release_timeout_from_env(),
|
||||||
|
Duration::from_millis(750)
|
||||||
|
);
|
||||||
|
assert_eq!(remote_failsafe_timeout_from_env(), Duration::ZERO);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
temp_env::with_vars(
|
||||||
|
[
|
||||||
|
("LESAVKA_INPUT_TOGGLE_DEBOUNCE_MS", Some("1")),
|
||||||
|
("LESAVKA_INPUT_RELEASE_TIMEOUT_MS", Some("1")),
|
||||||
|
("LESAVKA_INPUT_REMOTE_FAILSAFE_MS", Some("250")),
|
||||||
|
("LESAVKA_INPUT_REMOTE_FAILSAFE_SECS", None::<&str>),
|
||||||
|
],
|
||||||
|
|| {
|
||||||
|
assert_eq!(quick_toggle_debounce_from_env(), Duration::from_millis(50));
|
||||||
|
assert_eq!(
|
||||||
|
pending_release_timeout_from_env(),
|
||||||
|
Duration::from_millis(100)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
remote_failsafe_timeout_from_env(),
|
||||||
|
Duration::from_millis(250)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
temp_env::with_vars(
|
||||||
|
[
|
||||||
|
("LESAVKA_INPUT_REMOTE_FAILSAFE_MS", Some("250")),
|
||||||
|
("LESAVKA_INPUT_REMOTE_FAILSAFE_SECS", Some("2")),
|
||||||
|
],
|
||||||
|
|| {
|
||||||
|
assert_eq!(remote_failsafe_timeout_from_env(), Duration::from_secs(2));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn input_device_overrides_ignore_empty_and_all_values() {
|
||||||
|
temp_env::with_var("LESAVKA_KEYBOARD_DEVICE", Some("/dev/input/event4"), || {
|
||||||
|
assert_eq!(
|
||||||
|
input_device_override_from_env("LESAVKA_KEYBOARD_DEVICE").as_deref(),
|
||||||
|
Some("/dev/input/event4")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
for raw in ["", " ", "all", "ALL"] {
|
||||||
|
temp_env::with_var("LESAVKA_KEYBOARD_DEVICE", Some(raw), || {
|
||||||
|
assert_eq!(
|
||||||
|
input_device_override_from_env("LESAVKA_KEYBOARD_DEVICE"),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn selected_input_device_matches_exact_paths_or_all_devices() {
|
||||||
|
let event_path = Path::new("/dev/input/event7");
|
||||||
|
|
||||||
|
assert!(matches_selected_input_device(event_path, None));
|
||||||
|
assert!(matches_selected_input_device(
|
||||||
|
event_path,
|
||||||
|
Some("/dev/input/event7")
|
||||||
|
));
|
||||||
|
assert!(!matches_selected_input_device(
|
||||||
|
event_path,
|
||||||
|
Some("/dev/input/event8")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shadow_pressed_keys_tracks_press_hold_and_release() {
|
||||||
|
let mut pressed = HashSet::new();
|
||||||
|
|
||||||
|
update_shadow_pressed_keys(&mut pressed, KeyCode::KEY_A, 1);
|
||||||
|
update_shadow_pressed_keys(&mut pressed, KeyCode::KEY_B, 2);
|
||||||
|
update_shadow_pressed_keys(&mut pressed, KeyCode::KEY_C, -1);
|
||||||
|
assert!(pressed.contains(&KeyCode::KEY_A));
|
||||||
|
assert!(pressed.contains(&KeyCode::KEY_B));
|
||||||
|
assert!(!pressed.contains(&KeyCode::KEY_C));
|
||||||
|
|
||||||
|
update_shadow_pressed_keys(&mut pressed, KeyCode::KEY_A, 0);
|
||||||
|
assert!(!pressed.contains(&KeyCode::KEY_A));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn input_aggregator_respects_capture_boot_and_failsafe_state() {
|
||||||
|
let (kbd_tx, _) = tokio::sync::broadcast::channel(4);
|
||||||
|
let (mou_tx, _) = tokio::sync::broadcast::channel(4);
|
||||||
|
|
||||||
|
temp_env::with_vars(
|
||||||
|
[
|
||||||
|
("LESAVKA_INPUT_REMOTE_FAILSAFE_MS", Some("500")),
|
||||||
|
("LESAVKA_INPUT_REMOTE_FAILSAFE_SECS", None::<&str>),
|
||||||
|
("LESAVKA_INPUT_TOGGLE_DEBOUNCE_MS", Some("50")),
|
||||||
|
("LESAVKA_INPUT_RELEASE_TIMEOUT_MS", Some("100")),
|
||||||
|
],
|
||||||
|
|| {
|
||||||
|
let mut aggregator =
|
||||||
|
InputAggregator::new_with_capture_mode(false, kbd_tx, mou_tx, None, true);
|
||||||
|
|
||||||
|
assert!(!aggregator.released);
|
||||||
|
assert!(aggregator.remote_capture_active());
|
||||||
|
assert!(
|
||||||
|
aggregator
|
||||||
|
.remote_capture_enabled_handle()
|
||||||
|
.load(Ordering::Relaxed)
|
||||||
|
);
|
||||||
|
assert_eq!(aggregator.quick_toggle_debounce, Duration::from_millis(50));
|
||||||
|
assert_eq!(
|
||||||
|
aggregator.pending_release_timeout,
|
||||||
|
Duration::from_millis(100)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
aggregator.remote_failsafe_timeout,
|
||||||
|
Duration::from_millis(500)
|
||||||
|
);
|
||||||
|
|
||||||
|
aggregator.remote_failsafe_started_at =
|
||||||
|
Some(Instant::now() - Duration::from_millis(750));
|
||||||
|
assert!(aggregator.remote_failsafe_expired());
|
||||||
|
|
||||||
|
aggregator.begin_local_release();
|
||||||
|
assert!(!aggregator.remote_capture_active());
|
||||||
|
assert!(aggregator.pending_release);
|
||||||
|
aggregator.pending_release_started_at =
|
||||||
|
Some(Instant::now() - Duration::from_millis(150));
|
||||||
|
assert!(aggregator.pending_release_timed_out());
|
||||||
|
|
||||||
|
aggregator.finish_local_release(false);
|
||||||
|
assert!(aggregator.released);
|
||||||
|
assert!(!aggregator.pending_release);
|
||||||
|
|
||||||
|
aggregator.enable_remote_capture();
|
||||||
|
assert!(!aggregator.released);
|
||||||
|
assert!(aggregator.remote_capture_active());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -444,45 +444,5 @@ async fn main() -> Result<()> {
|
|||||||
include!("lesavka_synthetic_uplink/support.rs");
|
include!("lesavka_synthetic_uplink/support.rs");
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
#[path = "lesavka_synthetic_uplink/tests.rs"]
|
||||||
use super::*;
|
mod tests;
|
||||||
|
|
||||||
fn test_args(width: usize, height: usize, fps: u32) -> Args {
|
|
||||||
Args {
|
|
||||||
server: DEFAULT_SERVER.to_string(),
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
fps,
|
|
||||||
duration: Duration::from_secs(1),
|
|
||||||
jpeg_quality: DEFAULT_JPEG_QUALITY,
|
|
||||||
session_id: 1,
|
|
||||||
artifact_dir: None,
|
|
||||||
print_every: 0,
|
|
||||||
max_frame_bytes: 232_106,
|
|
||||||
tls_ca: None,
|
|
||||||
tls_client_cert: None,
|
|
||||||
tls_client_key: None,
|
|
||||||
tls_domain: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn synthetic_frames_fit_safe_720p_and_1080p_isochronous_budget() {
|
|
||||||
for (width, height, fps) in [(1280, 720, 30), (1920, 1080, 30)] {
|
|
||||||
let args = test_args(width, height, fps);
|
|
||||||
let mut encoder = MjpegEncoder::new(&args).expect("synthetic encoder");
|
|
||||||
for sequence in [0, 1, 30, 120, 300] {
|
|
||||||
let encoded = encoder.encode(sequence).expect("encode frame");
|
|
||||||
assert!(
|
|
||||||
encoded.len() <= args.max_frame_bytes,
|
|
||||||
"{}x{}@{} synthetic frame {sequence} encoded to {} bytes, above {}",
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
fps,
|
|
||||||
encoded.len(),
|
|
||||||
args.max_frame_bytes
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
194
server/src/bin/lesavka_synthetic_uplink/tests.rs
Normal file
194
server/src/bin/lesavka_synthetic_uplink/tests.rs
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
use super::*;
|
||||||
|
use serial_test::serial;
|
||||||
|
|
||||||
|
fn test_args(width: usize, height: usize, fps: u32) -> Args {
|
||||||
|
Args {
|
||||||
|
server: DEFAULT_SERVER.to_string(),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
fps,
|
||||||
|
duration: Duration::from_secs(1),
|
||||||
|
jpeg_quality: DEFAULT_JPEG_QUALITY,
|
||||||
|
session_id: 1,
|
||||||
|
artifact_dir: None,
|
||||||
|
print_every: 0,
|
||||||
|
max_frame_bytes: 232_106,
|
||||||
|
tls_ca: None,
|
||||||
|
tls_client_cert: None,
|
||||||
|
tls_client_key: None,
|
||||||
|
tls_domain: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn args_frame_timing_rounds_up_short_runs() {
|
||||||
|
let mut args = test_args(640, 360, 30);
|
||||||
|
args.duration = Duration::from_millis(1_050);
|
||||||
|
|
||||||
|
assert_eq!(args.frame_step_us(), 33_333);
|
||||||
|
assert_eq!(args.total_frames(), 32);
|
||||||
|
|
||||||
|
args.duration = Duration::ZERO;
|
||||||
|
assert_eq!(args.total_frames(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn encode_stats_track_bounds_mean_and_oversize_frames() {
|
||||||
|
let mut stats = EncodeStats::default();
|
||||||
|
|
||||||
|
assert_eq!(stats.mean_bytes(), 0);
|
||||||
|
|
||||||
|
stats.record(100, 150);
|
||||||
|
stats.record_sent();
|
||||||
|
stats.record(200, 150);
|
||||||
|
stats.record_sent();
|
||||||
|
|
||||||
|
assert_eq!(stats.frames, 2);
|
||||||
|
assert_eq!(stats.sent_frames, 2);
|
||||||
|
assert_eq!(stats.min_bytes, 100);
|
||||||
|
assert_eq!(stats.max_bytes, 200);
|
||||||
|
assert_eq!(stats.mean_bytes(), 150);
|
||||||
|
assert_eq!(stats.oversize_frames, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn synthetic_bundle_preserves_video_audio_and_session_metadata() {
|
||||||
|
let args = test_args(320, 180, 60);
|
||||||
|
let bundle = synthetic_bundle(&args, 7, 116_667, vec![1, 2, 3, 4]);
|
||||||
|
let video = bundle.video.as_ref().expect("video packet");
|
||||||
|
let audio = bundle.audio.first().expect("audio packet");
|
||||||
|
|
||||||
|
assert_eq!(bundle.session_id, 1);
|
||||||
|
assert_eq!(bundle.seq, 7);
|
||||||
|
assert_eq!(bundle.capture_start_us, 116_667);
|
||||||
|
assert_eq!(bundle.video_width, 320);
|
||||||
|
assert_eq!(bundle.video_height, 180);
|
||||||
|
assert_eq!(bundle.video_fps, 60);
|
||||||
|
assert_eq!(video.seq, 7);
|
||||||
|
assert_eq!(video.pts, 116_667);
|
||||||
|
assert_eq!(video.data, vec![1, 2, 3, 4]);
|
||||||
|
assert_eq!(audio.seq, 7);
|
||||||
|
assert_eq!(audio.sample_rate, DEFAULT_SAMPLE_RATE);
|
||||||
|
assert_eq!(audio.channels, DEFAULT_CHANNELS);
|
||||||
|
assert_eq!(audio.frame_duration_us, args.frame_step_us() as u32);
|
||||||
|
assert_eq!(audio.data.len(), silence_pcm(args.frame_step_us()).len());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn synthetic_frame_helpers_handle_markers_bounds_and_silence() {
|
||||||
|
assert_eq!(marker_cell(320, 240), 6);
|
||||||
|
assert_eq!(marker_cell(3840, 2160), 16);
|
||||||
|
assert_eq!(silence_pcm(0).len(), 4);
|
||||||
|
assert_eq!(silence_pcm(20_000).len(), 3_840);
|
||||||
|
|
||||||
|
let tiny = synthetic_rgb_frame(3, 2, 42);
|
||||||
|
assert_eq!(tiny.len(), 18);
|
||||||
|
|
||||||
|
let mut frame = vec![0_u8; 6 * 4 * 3];
|
||||||
|
fill_rect(&mut frame, 6, 4, 2, 8, 8, 77);
|
||||||
|
assert_eq!(&frame[(2 * 6 + 4) * 3..(2 * 6 + 5) * 3], &[77, 77, 77]);
|
||||||
|
assert_eq!(&frame[(3 * 6 + 5) * 3..(3 * 6 + 6) * 3], &[77, 77, 77]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_mode_and_duration_helpers_cover_valid_and_invalid_inputs() {
|
||||||
|
assert_eq!(parse_mode("1920x1080@30").unwrap(), (1920, 1080, 30));
|
||||||
|
assert!(parse_mode("1920x1080").is_err());
|
||||||
|
assert!(parse_mode("1920@30").is_err());
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
duration_mul(Duration::from_millis(33), 3),
|
||||||
|
Duration::from_millis(99)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
duration_mul(Duration::from_secs(u64::MAX), 2),
|
||||||
|
Duration::from_nanos(u64::MAX)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn uri_and_tls_helpers_identify_https_and_host_names() {
|
||||||
|
assert!(is_https(" https://example.test:8443/path"));
|
||||||
|
assert!(!is_https("http://example.test"));
|
||||||
|
assert_eq!(
|
||||||
|
host_from_uri("https://user@example.test:8443/path").as_deref(),
|
||||||
|
Some("example.test")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
host_from_uri("https://[fd00::1]:50051").as_deref(),
|
||||||
|
Some("fd00::1")
|
||||||
|
);
|
||||||
|
assert_eq!(host_from_uri("not-a-uri"), None);
|
||||||
|
|
||||||
|
let mut args = test_args(320, 180, 30);
|
||||||
|
args.server = "https://relay.example.test:50051".to_string();
|
||||||
|
args.tls_ca = None;
|
||||||
|
let err = client_tls_config(&args).expect_err("missing CA should fail");
|
||||||
|
assert!(err.to_string().contains("--tls-ca"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn env_path_and_default_pki_path_ignore_empty_values() {
|
||||||
|
temp_env::with_vars(
|
||||||
|
[
|
||||||
|
("LESAVKA_SYNTHETIC_TEST_PATH", Some("/tmp/lesavka-test")),
|
||||||
|
("HOME", Some("/home/tester")),
|
||||||
|
],
|
||||||
|
|| {
|
||||||
|
assert_eq!(
|
||||||
|
env_path("LESAVKA_SYNTHETIC_TEST_PATH").as_deref(),
|
||||||
|
Some(PathBuf::from("/tmp/lesavka-test").as_path())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
default_pki_path("ca.crt").as_deref(),
|
||||||
|
Some(PathBuf::from("/home/tester/.config/lesavka/pki/ca.crt").as_path())
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
temp_env::with_var("LESAVKA_SYNTHETIC_TEST_PATH", Some(""), || {
|
||||||
|
assert_eq!(env_path("LESAVKA_SYNTHETIC_TEST_PATH"), None);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn env_usize_and_summary_json_handle_empty_stats() {
|
||||||
|
temp_env::with_var("LESAVKA_SYNTHETIC_MAX_FRAME_BYTES", Some("2048"), || {
|
||||||
|
assert_eq!(env_usize("LESAVKA_SYNTHETIC_MAX_FRAME_BYTES"), Some(2048));
|
||||||
|
});
|
||||||
|
temp_env::with_var("LESAVKA_SYNTHETIC_MAX_FRAME_BYTES", Some("nope"), || {
|
||||||
|
assert_eq!(env_usize("LESAVKA_SYNTHETIC_MAX_FRAME_BYTES"), None);
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut args = test_args(320, 180, 30);
|
||||||
|
args.server = "https://relay.example.test:50051".to_string();
|
||||||
|
args.duration = Duration::from_millis(1500);
|
||||||
|
let summary = args_summary_json(&args, None, Some("complete"));
|
||||||
|
|
||||||
|
assert!(summary.contains("\"tls\":true"));
|
||||||
|
assert!(summary.contains("\"encoded_frames\":0"));
|
||||||
|
assert!(summary.contains("\"encoded_min_bytes\":null"));
|
||||||
|
assert!(summary.contains("\"exit_reason\":\"complete\""));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn synthetic_frames_fit_safe_720p_and_1080p_isochronous_budget() {
|
||||||
|
for (width, height, fps) in [(1280, 720, 30), (1920, 1080, 30)] {
|
||||||
|
let args = test_args(width, height, fps);
|
||||||
|
let mut encoder = MjpegEncoder::new(&args).expect("synthetic encoder");
|
||||||
|
for sequence in [0, 1, 30, 120, 300] {
|
||||||
|
let encoded = encoder.encode(sequence).expect("encode frame");
|
||||||
|
assert!(
|
||||||
|
encoded.len() <= args.max_frame_bytes,
|
||||||
|
"{}x{}@{} synthetic frame {sequence} encoded to {} bytes, above {}",
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
fps,
|
||||||
|
encoded.len(),
|
||||||
|
args.max_frame_bytes
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user