test(gate): tighten coverage contract
This commit is contained in:
parent
2b27b1d72f
commit
31d4ee47b4
@ -10,4 +10,4 @@ pub mod runtime_support;
|
||||
pub mod uvc_runtime;
|
||||
pub mod video;
|
||||
pub(crate) mod video_sinks;
|
||||
pub(crate) mod video_support;
|
||||
pub mod video_support;
|
||||
|
||||
@ -15,6 +15,9 @@ lesavka_client = { path = "../client" }
|
||||
lesavka_common = { path = "../common" }
|
||||
lesavka_server = { path = "../server" }
|
||||
chacha20poly1305 = "0.10"
|
||||
gstreamer = { version = "0.23", features = ["v1_22"] }
|
||||
serial_test = { workspace = true }
|
||||
temp-env = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
tokio = { version = "1.45", features = ["full", "macros", "rt-multi-thread", "sync", "time"] }
|
||||
tonic = { version = "0.13", features = ["transport"] }
|
||||
|
||||
@ -3,8 +3,12 @@
|
||||
"files": [
|
||||
"client/src/app_support.rs",
|
||||
"client/src/input/keymap.rs",
|
||||
"client/src/output/layout.rs",
|
||||
"common/src/cli.rs",
|
||||
"common/src/hid.rs",
|
||||
"common/src/paste.rs",
|
||||
"server/src/paste.rs"
|
||||
"server/src/handshake.rs",
|
||||
"server/src/paste.rs",
|
||||
"server/src/video_support.rs"
|
||||
]
|
||||
}
|
||||
|
||||
126
testing/tests/handshake_camera_contract.rs
Normal file
126
testing/tests/handshake_camera_contract.rs
Normal file
@ -0,0 +1,126 @@
|
||||
//! Integration coverage for the handshake and camera-selection contract.
|
||||
//!
|
||||
//! Scope: exercise the real client handshake against a local server and the
|
||||
//! server camera selection logic that feeds the handshake response.
|
||||
//! Targets: `client/src/handshake.rs`, `server/src/handshake.rs`, and
|
||||
//! `server/src/camera.rs`.
|
||||
//! Why: the handshake path is the narrow entrypoint that decides whether the
|
||||
//! client and server agree on output mode, so it deserves a centralized
|
||||
//! contract test.
|
||||
|
||||
use lesavka_client::handshake::{PeerCaps, negotiate};
|
||||
use serial_test::serial;
|
||||
use std::net::TcpListener;
|
||||
use std::time::Duration;
|
||||
use temp_env::with_var;
|
||||
use tokio::runtime::Runtime;
|
||||
use tokio::sync::oneshot;
|
||||
use tonic::transport::Server;
|
||||
|
||||
async fn negotiate_against_local_server() -> PeerCaps {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").expect("bind local handshake listener");
|
||||
let addr = listener.local_addr().expect("listener addr");
|
||||
drop(listener);
|
||||
|
||||
let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>();
|
||||
let server = tokio::spawn(async move {
|
||||
Server::builder()
|
||||
.add_service(lesavka_server::handshake::HandshakeSvc::server())
|
||||
.serve_with_shutdown(addr, async move {
|
||||
let _ = shutdown_rx.await;
|
||||
})
|
||||
.await
|
||||
.expect("serve handshake server");
|
||||
});
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
let caps = negotiate(&format!("http://{addr}")).await;
|
||||
let _ = shutdown_tx.send(());
|
||||
let _ = server.await;
|
||||
caps
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn handshake_returns_uvc_caps_with_explicit_dimensions_and_fps() {
|
||||
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 rt = Runtime::new().expect("create runtime");
|
||||
let caps = rt.block_on(negotiate_against_local_server());
|
||||
assert!(caps.camera);
|
||||
assert!(caps.microphone);
|
||||
assert_eq!(caps.camera_output, Some(String::from("uvc")));
|
||||
assert_eq!(caps.camera_codec, Some(String::from("mjpeg")));
|
||||
assert_eq!(caps.camera_width, Some(1024));
|
||||
assert_eq!(caps.camera_height, Some(576));
|
||||
assert_eq!(caps.camera_fps, Some(12));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn handshake_uses_uvc_interval_when_fps_is_unset() {
|
||||
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_INTERVAL", Some("250000"), || {
|
||||
let rt = Runtime::new().expect("create runtime");
|
||||
let caps = rt.block_on(negotiate_against_local_server());
|
||||
assert_eq!(caps.camera_output, Some(String::from("uvc")));
|
||||
assert_eq!(caps.camera_codec, Some(String::from("mjpeg")));
|
||||
assert_eq!(caps.camera_width, Some(640));
|
||||
assert_eq!(caps.camera_height, Some(360));
|
||||
assert_eq!(caps.camera_fps, Some(40));
|
||||
assert!(caps.camera);
|
||||
assert!(caps.microphone);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn handshake_returns_hdmi_caps_with_h264_codec() {
|
||||
with_var("LESAVKA_CAM_OUTPUT", Some("hdmi"), || {
|
||||
with_var("LESAVKA_DISABLE_UAC", Some("1"), || {
|
||||
let rt = Runtime::new().expect("create runtime");
|
||||
let caps = rt.block_on(negotiate_against_local_server());
|
||||
assert_eq!(caps.camera_output, Some(String::from("hdmi")));
|
||||
assert_eq!(caps.camera_codec, Some(String::from("h264")));
|
||||
assert_eq!(caps.camera_fps, Some(30));
|
||||
assert!(!caps.microphone);
|
||||
assert!(caps.camera);
|
||||
assert!(matches!(caps.camera_width, Some(1280) | Some(1920)));
|
||||
assert!(matches!(caps.camera_height, Some(720) | Some(1080)));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn handshake_auto_mode_falls_back_to_a_valid_camera_configuration() {
|
||||
with_var("LESAVKA_CAM_OUTPUT", Some("auto"), || {
|
||||
let rt = Runtime::new().expect("create runtime");
|
||||
let caps = rt.block_on(negotiate_against_local_server());
|
||||
match caps.camera_output.as_deref() {
|
||||
Some("uvc") => {
|
||||
assert_eq!(caps.camera_codec.as_deref(), Some("mjpeg"));
|
||||
assert!(matches!(caps.camera_width, Some(1280) | Some(640)));
|
||||
assert!(matches!(caps.camera_height, Some(720) | Some(360)));
|
||||
}
|
||||
Some("hdmi") => {
|
||||
assert_eq!(caps.camera_codec.as_deref(), Some("h264"));
|
||||
assert!(matches!(caps.camera_width, Some(1280) | Some(1920)));
|
||||
assert!(matches!(caps.camera_height, Some(720) | Some(1080)));
|
||||
}
|
||||
other => panic!("unexpected camera output: {other:?}"),
|
||||
}
|
||||
assert!(caps.camera);
|
||||
});
|
||||
}
|
||||
129
testing/tests/video_support_contract.rs
Normal file
129
testing/tests/video_support_contract.rs
Normal file
@ -0,0 +1,129 @@
|
||||
//! Integration coverage for the server video-support helpers.
|
||||
//!
|
||||
//! Scope: exercise the pure policy helpers and cached env flag behavior from
|
||||
//! the top-level testing module.
|
||||
//! Targets: `server/src/video_support.rs`.
|
||||
//! Why: these helpers are small but central to the adaptive video policy, so
|
||||
//! they are a good fit for a centralized contract test.
|
||||
|
||||
use gstreamer as gst;
|
||||
use serial_test::serial;
|
||||
use std::process::Command;
|
||||
use temp_env::with_var;
|
||||
|
||||
use lesavka_server::video_support::{
|
||||
adjust_effective_fps, contains_idr, default_eye_fps, dev_mode_enabled, env_u32, env_usize,
|
||||
next_local_pts, pick_h264_decoder, should_send_frame,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn default_eye_fps_tracks_the_expected_bitrate_steps() {
|
||||
assert_eq!(default_eye_fps(0), 25);
|
||||
assert_eq!(default_eye_fps(2_500), 15);
|
||||
assert_eq!(default_eye_fps(2_501), 20);
|
||||
assert_eq!(default_eye_fps(4_001), 25);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contains_idr_handles_short_and_multi_nal_annex_b_streams() {
|
||||
assert!(contains_idr(&[0, 0, 1, 0x65, 0x00]));
|
||||
assert!(contains_idr(&[0, 0, 0, 1, 0x41, 0x00, 0, 0, 1, 0x65, 0x00]));
|
||||
assert!(!contains_idr(&[0, 0, 0, 1, 0x41, 0x00, 0x00]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adjust_effective_fps_and_timestamp_helpers_stay_stable() {
|
||||
assert_eq!(adjust_effective_fps(20, 12, 25, 5, 10), 17);
|
||||
assert_eq!(adjust_effective_fps(20, 12, 25, 0, 20), 21);
|
||||
assert_eq!(adjust_effective_fps(12, 12, 25, 10, 10), 12);
|
||||
assert_eq!(adjust_effective_fps(18, 12, 25, 0, 0), 18);
|
||||
|
||||
assert!(should_send_frame(0, 10, 25));
|
||||
assert!(!should_send_frame(40_000, 50_000, 25));
|
||||
assert!(should_send_frame(40_000, 90_000, 25));
|
||||
|
||||
let counter = std::sync::atomic::AtomicU64::new(0);
|
||||
assert_eq!(next_local_pts(&counter, 40_000), 0);
|
||||
assert_eq!(next_local_pts(&counter, 40_000), 40_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn env_helpers_parse_and_fallback_without_panicking() {
|
||||
with_var("LESAVKA_TEST_U32", Some("42"), || {
|
||||
assert_eq!(env_u32("LESAVKA_TEST_U32", 7), 42);
|
||||
});
|
||||
with_var("LESAVKA_TEST_U32", Some("oops"), || {
|
||||
assert_eq!(env_u32("LESAVKA_TEST_U32", 7), 7);
|
||||
});
|
||||
with_var("LESAVKA_TEST_USIZE", Some("128"), || {
|
||||
assert_eq!(env_usize("LESAVKA_TEST_USIZE", 64), 128);
|
||||
});
|
||||
with_var("LESAVKA_TEST_USIZE", None::<&str>, || {
|
||||
assert_eq!(env_usize("LESAVKA_TEST_USIZE", 64), 64);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pick_h264_decoder_returns_a_known_decoder_name() {
|
||||
gst::init().expect("initialize gstreamer");
|
||||
assert!(matches!(
|
||||
pick_h264_decoder(),
|
||||
"v4l2h264dec" | "v4l2slh264dec" | "omxh264dec" | "avdec_h264"
|
||||
));
|
||||
}
|
||||
|
||||
fn run_ignored_probe(test_name: &str, envs: &[(&str, Option<&str>)]) -> std::process::Output {
|
||||
let exe = std::env::current_exe().expect("resolve test binary");
|
||||
let mut cmd = Command::new(exe);
|
||||
cmd.arg(test_name).arg("--ignored").arg("--exact");
|
||||
for (key, value) in envs {
|
||||
match value {
|
||||
Some(v) => {
|
||||
cmd.env(key, v);
|
||||
}
|
||||
None => {
|
||||
cmd.env_remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
cmd.output().expect("run probe")
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn dev_mode_flag_is_cached_per_process() {
|
||||
let enabled = run_ignored_probe(
|
||||
"probe_dev_mode_enabled_true",
|
||||
&[("LESAVKA_DEV_MODE", Some("1"))],
|
||||
);
|
||||
assert!(
|
||||
enabled.status.success(),
|
||||
"expected dev-mode probe to pass:\nstdout:\n{}\nstderr:\n{}",
|
||||
String::from_utf8_lossy(&enabled.stdout),
|
||||
String::from_utf8_lossy(&enabled.stderr)
|
||||
);
|
||||
|
||||
let disabled = run_ignored_probe(
|
||||
"probe_dev_mode_enabled_false",
|
||||
&[("LESAVKA_DEV_MODE", None)],
|
||||
);
|
||||
assert!(
|
||||
disabled.status.success(),
|
||||
"expected dev-mode false probe to pass:\nstdout:\n{}\nstderr:\n{}",
|
||||
String::from_utf8_lossy(&disabled.stdout),
|
||||
String::from_utf8_lossy(&disabled.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn probe_dev_mode_enabled_true() {
|
||||
assert!(dev_mode_enabled());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn probe_dev_mode_enabled_false() {
|
||||
assert!(!dev_mode_enabled());
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user