281 lines
10 KiB
Rust
281 lines
10 KiB
Rust
|
|
use super::{
|
||
|
|
CalibrationAction, CapturePowerCommand, CommandKind, Config, ParseOutcome,
|
||
|
|
calibration_request_for, capture_power_request, parse_args_from, parse_args_outcome_from,
|
||
|
|
};
|
||
|
|
use lesavka_common::lesavka::CapturePowerState;
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
/// Verifies safe recovery commands stay separate from explicit hard reset.
|
||
|
|
fn command_aliases_parse_to_stable_actions() {
|
||
|
|
assert_eq!(CommandKind::parse("status"), Some(CommandKind::Status));
|
||
|
|
assert_eq!(CommandKind::parse("get"), Some(CommandKind::Status));
|
||
|
|
assert_eq!(CommandKind::parse("version"), Some(CommandKind::Version));
|
||
|
|
assert_eq!(CommandKind::parse("versions"), Some(CommandKind::Version));
|
||
|
|
assert_eq!(
|
||
|
|
CommandKind::parse("calibration"),
|
||
|
|
Some(CommandKind::CalibrationStatus)
|
||
|
|
);
|
||
|
|
assert_eq!(
|
||
|
|
CommandKind::parse("calibrate"),
|
||
|
|
Some(CommandKind::CalibrationAdjust)
|
||
|
|
);
|
||
|
|
assert_eq!(
|
||
|
|
CommandKind::parse("calibration-restore-default"),
|
||
|
|
Some(CommandKind::CalibrationRestoreDefault)
|
||
|
|
);
|
||
|
|
assert_eq!(
|
||
|
|
CommandKind::parse("calibration-restore-factory"),
|
||
|
|
Some(CommandKind::CalibrationRestoreFactory)
|
||
|
|
);
|
||
|
|
assert_eq!(
|
||
|
|
CommandKind::parse("calibration-save-default"),
|
||
|
|
Some(CommandKind::CalibrationSaveDefault)
|
||
|
|
);
|
||
|
|
assert_eq!(
|
||
|
|
CommandKind::parse("upstream-sync"),
|
||
|
|
Some(CommandKind::UpstreamSync)
|
||
|
|
);
|
||
|
|
assert_eq!(CommandKind::parse("sync"), Some(CommandKind::UpstreamSync));
|
||
|
|
assert_eq!(
|
||
|
|
CommandKind::parse("output-delay-probe"),
|
||
|
|
Some(CommandKind::OutputDelayProbe)
|
||
|
|
);
|
||
|
|
assert_eq!(
|
||
|
|
CommandKind::parse("probe-output-delay"),
|
||
|
|
Some(CommandKind::OutputDelayProbe)
|
||
|
|
);
|
||
|
|
assert_eq!(CommandKind::parse("force-on"), Some(CommandKind::On));
|
||
|
|
assert_eq!(CommandKind::parse("force-off"), Some(CommandKind::Off));
|
||
|
|
assert_eq!(
|
||
|
|
CommandKind::parse("recover-usb"),
|
||
|
|
Some(CommandKind::RecoverUsb)
|
||
|
|
);
|
||
|
|
assert_eq!(
|
||
|
|
CommandKind::parse("recover-uac"),
|
||
|
|
Some(CommandKind::RecoverUac)
|
||
|
|
);
|
||
|
|
assert_eq!(
|
||
|
|
CommandKind::parse("recover-uvc"),
|
||
|
|
Some(CommandKind::RecoverUvc)
|
||
|
|
);
|
||
|
|
assert_eq!(
|
||
|
|
CommandKind::parse("hard-reset-usb"),
|
||
|
|
Some(CommandKind::ResetUsb)
|
||
|
|
);
|
||
|
|
assert_eq!(CommandKind::parse("wat"), None);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn parse_args_defaults_to_local_status() {
|
||
|
|
let config = parse_args_from(std::iter::empty::<&str>()).expect("default config");
|
||
|
|
assert_eq!(config.server, "http://127.0.0.1:50051");
|
||
|
|
assert_eq!(config.command, CommandKind::Status);
|
||
|
|
assert_eq!(config.audio_delta_us, 0);
|
||
|
|
assert_eq!(config.video_delta_us, 0);
|
||
|
|
assert!(config.note.is_empty());
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn parse_args_accepts_server_and_command() {
|
||
|
|
let config =
|
||
|
|
parse_args_from(["--server", " http://lab:50051 ", "upstream-sync"]).expect("config");
|
||
|
|
assert_eq!(config.server, "http://lab:50051");
|
||
|
|
assert_eq!(config.command, CommandKind::UpstreamSync);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
/// Keeps `parse_args_accepts_output_delay_probe_config` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
|
||
|
|
/// Inputs are the typed parameters; output is the return value or side effect.
|
||
|
|
fn parse_args_accepts_output_delay_probe_config() {
|
||
|
|
let config = parse_args_from([
|
||
|
|
"--server",
|
||
|
|
"http://lab:50051",
|
||
|
|
"output-delay-probe",
|
||
|
|
"20",
|
||
|
|
"4",
|
||
|
|
"1000",
|
||
|
|
"120",
|
||
|
|
"1,2,3,4",
|
||
|
|
"0",
|
||
|
|
"157712",
|
||
|
|
])
|
||
|
|
.expect("probe config");
|
||
|
|
|
||
|
|
assert_eq!(config.command, CommandKind::OutputDelayProbe);
|
||
|
|
assert_eq!(config.probe_duration_seconds, 20);
|
||
|
|
assert_eq!(config.probe_warmup_seconds, 4);
|
||
|
|
assert_eq!(config.probe_pulse_period_ms, 1000);
|
||
|
|
assert_eq!(config.probe_pulse_width_ms, 120);
|
||
|
|
assert_eq!(config.probe_event_width_codes, "1,2,3,4");
|
||
|
|
assert_eq!(config.probe_audio_delay_us, 0);
|
||
|
|
assert_eq!(config.probe_video_delay_us, 157_712);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
/// Keeps `parse_args_accepts_calibration_adjustment` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
|
||
|
|
/// Inputs are the typed parameters; output is the return value or side effect.
|
||
|
|
fn parse_args_accepts_calibration_adjustment() {
|
||
|
|
let config = parse_args_from([
|
||
|
|
"--server",
|
||
|
|
"http://lab:50051",
|
||
|
|
"calibrate",
|
||
|
|
"0",
|
||
|
|
"71600",
|
||
|
|
"probe",
|
||
|
|
"median",
|
||
|
|
])
|
||
|
|
.expect("calibration config");
|
||
|
|
assert_eq!(config.command, CommandKind::CalibrationAdjust);
|
||
|
|
assert_eq!(config.audio_delta_us, 0);
|
||
|
|
assert_eq!(config.video_delta_us, 71_600);
|
||
|
|
assert_eq!(config.note, "probe median");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
/// Keeps `parse_args_rejects_bad_inputs` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
|
||
|
|
/// Inputs are the typed parameters; output is the return value or side effect.
|
||
|
|
fn parse_args_rejects_bad_inputs() {
|
||
|
|
assert!(parse_args_from(["--server"]).is_err());
|
||
|
|
assert!(parse_args_from(["nope"]).is_err());
|
||
|
|
assert!(parse_args_from(["status", "extra"]).is_err());
|
||
|
|
assert!(parse_args_from(["calibrate"]).is_err());
|
||
|
|
assert!(parse_args_from(["calibrate", "0", "not-int"]).is_err());
|
||
|
|
assert!(
|
||
|
|
parse_args_from([
|
||
|
|
"output-delay-probe",
|
||
|
|
"1",
|
||
|
|
"2",
|
||
|
|
"3",
|
||
|
|
"4",
|
||
|
|
"1",
|
||
|
|
"0",
|
||
|
|
"0",
|
||
|
|
"extra"
|
||
|
|
])
|
||
|
|
.is_err()
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn parse_args_reports_help_without_exiting_test_process() {
|
||
|
|
assert_eq!(
|
||
|
|
parse_args_outcome_from(["--help"]).unwrap(),
|
||
|
|
ParseOutcome::Help
|
||
|
|
);
|
||
|
|
assert_eq!(parse_args_outcome_from(["-h"]).unwrap(), ParseOutcome::Help);
|
||
|
|
assert!(parse_args_from(["--help"]).is_err());
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn parse_args_runtime_wrapper_is_non_panicking_under_tests() {
|
||
|
|
let _ = super::parse_args();
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(coverage)]
|
||
|
|
#[test]
|
||
|
|
fn coverage_main_references_runtime_parser() {
|
||
|
|
super::main();
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(coverage)]
|
||
|
|
#[tokio::test(flavor = "current_thread")]
|
||
|
|
async fn coverage_connect_uses_lazy_channel_after_endpoint_validation() {
|
||
|
|
let client = super::connect("http://127.0.0.1:1")
|
||
|
|
.await
|
||
|
|
.expect("coverage lazy channel");
|
||
|
|
drop(client);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
/// Keeps status/read commands from accidentally mutating capture power.
|
||
|
|
fn mutating_commands_map_to_capture_power_requests() {
|
||
|
|
let auto = capture_power_request(CommandKind::Auto).expect("auto request");
|
||
|
|
assert!(!auto.enabled);
|
||
|
|
assert_eq!(auto.command, CapturePowerCommand::Auto as i32);
|
||
|
|
|
||
|
|
let on = capture_power_request(CommandKind::On).expect("on request");
|
||
|
|
assert!(on.enabled);
|
||
|
|
assert_eq!(on.command, CapturePowerCommand::ForceOn as i32);
|
||
|
|
|
||
|
|
let off = capture_power_request(CommandKind::Off).expect("off request");
|
||
|
|
assert!(!off.enabled);
|
||
|
|
assert_eq!(off.command, CapturePowerCommand::ForceOff as i32);
|
||
|
|
|
||
|
|
assert!(capture_power_request(CommandKind::Status).is_none());
|
||
|
|
assert!(capture_power_request(CommandKind::Version).is_none());
|
||
|
|
assert!(capture_power_request(CommandKind::CalibrationStatus).is_none());
|
||
|
|
assert!(capture_power_request(CommandKind::CalibrationAdjust).is_none());
|
||
|
|
assert!(capture_power_request(CommandKind::CalibrationRestoreDefault).is_none());
|
||
|
|
assert!(capture_power_request(CommandKind::CalibrationRestoreFactory).is_none());
|
||
|
|
assert!(capture_power_request(CommandKind::CalibrationSaveDefault).is_none());
|
||
|
|
assert!(capture_power_request(CommandKind::RecoverUsb).is_none());
|
||
|
|
assert!(capture_power_request(CommandKind::RecoverUac).is_none());
|
||
|
|
assert!(capture_power_request(CommandKind::RecoverUvc).is_none());
|
||
|
|
assert!(capture_power_request(CommandKind::ResetUsb).is_none());
|
||
|
|
assert!(capture_power_request(CommandKind::UpstreamSync).is_none());
|
||
|
|
assert!(capture_power_request(CommandKind::OutputDelayProbe).is_none());
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn print_state_accepts_full_capture_power_payload() {
|
||
|
|
super::print_state(CapturePowerState {
|
||
|
|
available: true,
|
||
|
|
enabled: false,
|
||
|
|
mode: "auto".to_string(),
|
||
|
|
detected_devices: 2,
|
||
|
|
active_leases: 1,
|
||
|
|
unit: "lesavka-capture-power.service".to_string(),
|
||
|
|
detail: "ready".to_string(),
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
/// Keeps `print_calibration_accepts_full_payload` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
|
||
|
|
/// Inputs are the typed parameters; output is the return value or side effect.
|
||
|
|
fn print_calibration_accepts_full_payload() {
|
||
|
|
super::print_calibration_state(lesavka_common::lesavka::CalibrationState {
|
||
|
|
profile: "mjpeg".to_string(),
|
||
|
|
factory_audio_offset_us: 0,
|
||
|
|
factory_video_offset_us: 1_090_000,
|
||
|
|
default_audio_offset_us: 0,
|
||
|
|
default_video_offset_us: 1_090_000,
|
||
|
|
active_audio_offset_us: 0,
|
||
|
|
active_video_offset_us: 1_161_600,
|
||
|
|
source: "manual".to_string(),
|
||
|
|
confidence: "manual".to_string(),
|
||
|
|
updated_at: "2026-05-02T00:00:00Z".to_string(),
|
||
|
|
detail: "probe nudge".to_string(),
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
/// Keeps `calibration_requests_are_only_built_for_calibration_mutations` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
|
||
|
|
/// Inputs are the typed parameters; output is the return value or side effect.
|
||
|
|
fn calibration_requests_are_only_built_for_calibration_mutations() {
|
||
|
|
let config = Config {
|
||
|
|
server: "http://127.0.0.1:50051".to_string(),
|
||
|
|
command: CommandKind::CalibrationAdjust,
|
||
|
|
audio_delta_us: 0,
|
||
|
|
video_delta_us: 71_600,
|
||
|
|
note: "probe".to_string(),
|
||
|
|
probe_duration_seconds: 0,
|
||
|
|
probe_warmup_seconds: 0,
|
||
|
|
probe_pulse_period_ms: 0,
|
||
|
|
probe_pulse_width_ms: 0,
|
||
|
|
probe_event_width_codes: String::new(),
|
||
|
|
probe_audio_delay_us: 0,
|
||
|
|
probe_video_delay_us: 0,
|
||
|
|
};
|
||
|
|
let request = calibration_request_for(&config).expect("request");
|
||
|
|
assert_eq!(request.action, CalibrationAction::AdjustActive as i32);
|
||
|
|
assert_eq!(request.audio_delta_us, 0);
|
||
|
|
assert_eq!(request.video_delta_us, 71_600);
|
||
|
|
assert_eq!(request.note, "probe");
|
||
|
|
|
||
|
|
let status = Config {
|
||
|
|
command: CommandKind::CalibrationStatus,
|
||
|
|
..config
|
||
|
|
};
|
||
|
|
assert!(calibration_request_for(&status).is_none());
|
||
|
|
}
|