testing: expand handshake and camera runtime contracts
This commit is contained in:
parent
3533b46eb8
commit
7a4bea63c0
@ -9,15 +9,22 @@
|
||||
//! contract test.
|
||||
|
||||
use lesavka_client::handshake::{PeerCaps, negotiate};
|
||||
use lesavka_common::lesavka::{
|
||||
Empty, HandshakeSet,
|
||||
handshake_server::{Handshake, HandshakeServer},
|
||||
};
|
||||
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;
|
||||
use tonic::{Request, Response, Status, transport::Server};
|
||||
|
||||
async fn negotiate_against_local_server() -> PeerCaps {
|
||||
async fn negotiate_against_service<S>(service: S) -> PeerCaps
|
||||
where
|
||||
S: Handshake + Send + Sync + 'static,
|
||||
{
|
||||
let listener = TcpListener::bind("127.0.0.1:0").expect("bind local handshake listener");
|
||||
let addr = listener.local_addr().expect("listener addr");
|
||||
drop(listener);
|
||||
@ -25,7 +32,7 @@ async fn negotiate_against_local_server() -> PeerCaps {
|
||||
let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>();
|
||||
let server = tokio::spawn(async move {
|
||||
Server::builder()
|
||||
.add_service(lesavka_server::handshake::HandshakeSvc::server())
|
||||
.add_service(HandshakeServer::new(service))
|
||||
.serve_with_shutdown(addr, async move {
|
||||
let _ = shutdown_rx.await;
|
||||
})
|
||||
@ -40,6 +47,77 @@ async fn negotiate_against_local_server() -> PeerCaps {
|
||||
caps
|
||||
}
|
||||
|
||||
async fn negotiate_against_local_server() -> PeerCaps {
|
||||
negotiate_against_service(lesavka_server::handshake::HandshakeSvc).await
|
||||
}
|
||||
|
||||
fn assert_default_caps(caps: &PeerCaps) {
|
||||
assert!(!caps.camera);
|
||||
assert!(!caps.microphone);
|
||||
assert_eq!(caps.camera_output, None);
|
||||
assert_eq!(caps.camera_codec, None);
|
||||
assert_eq!(caps.camera_width, None);
|
||||
assert_eq!(caps.camera_height, None);
|
||||
assert_eq!(caps.camera_fps, None);
|
||||
}
|
||||
|
||||
struct UnimplementedHandshakeSvc;
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl Handshake for UnimplementedHandshakeSvc {
|
||||
async fn get_capabilities(
|
||||
&self,
|
||||
_req: Request<Empty>,
|
||||
) -> Result<Response<HandshakeSet>, Status> {
|
||||
Err(Status::unimplemented("handshake disabled"))
|
||||
}
|
||||
}
|
||||
|
||||
struct InternalErrorHandshakeSvc;
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl Handshake for InternalErrorHandshakeSvc {
|
||||
async fn get_capabilities(
|
||||
&self,
|
||||
_req: Request<Empty>,
|
||||
) -> Result<Response<HandshakeSet>, Status> {
|
||||
Err(Status::internal("boom"))
|
||||
}
|
||||
}
|
||||
|
||||
struct SparseHandshakeSvc;
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl Handshake for SparseHandshakeSvc {
|
||||
async fn get_capabilities(
|
||||
&self,
|
||||
_req: Request<Empty>,
|
||||
) -> Result<Response<HandshakeSet>, Status> {
|
||||
Ok(Response::new(HandshakeSet {
|
||||
camera: true,
|
||||
microphone: false,
|
||||
camera_output: String::new(),
|
||||
camera_codec: String::new(),
|
||||
camera_width: 0,
|
||||
camera_height: 0,
|
||||
camera_fps: 0,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
struct SlowHandshakeSvc;
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl Handshake for SlowHandshakeSvc {
|
||||
async fn get_capabilities(
|
||||
&self,
|
||||
_req: Request<Empty>,
|
||||
) -> Result<Response<HandshakeSet>, Status> {
|
||||
tokio::time::sleep(Duration::from_secs(6)).await;
|
||||
Ok(Response::new(HandshakeSet::default()))
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn handshake_returns_uvc_caps_with_explicit_dimensions_and_fps() {
|
||||
@ -124,3 +202,77 @@ fn handshake_auto_mode_falls_back_to_a_valid_camera_configuration() {
|
||||
assert!(caps.camera);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn handshake_returns_defaults_when_server_is_unreachable() {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").expect("bind local listener");
|
||||
let addr = listener.local_addr().expect("listener addr");
|
||||
drop(listener);
|
||||
|
||||
let rt = Runtime::new().expect("create runtime");
|
||||
let caps = rt.block_on(negotiate(&format!("http://{addr}")));
|
||||
assert_default_caps(&caps);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn handshake_returns_defaults_when_service_is_unimplemented() {
|
||||
let rt = Runtime::new().expect("create runtime");
|
||||
let caps = rt.block_on(negotiate_against_service(UnimplementedHandshakeSvc));
|
||||
assert_default_caps(&caps);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn handshake_returns_defaults_when_service_returns_internal_error() {
|
||||
let rt = Runtime::new().expect("create runtime");
|
||||
let caps = rt.block_on(negotiate_against_service(InternalErrorHandshakeSvc));
|
||||
assert_default_caps(&caps);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn handshake_maps_empty_optional_fields_to_none() {
|
||||
let rt = Runtime::new().expect("create runtime");
|
||||
let caps = rt.block_on(negotiate_against_service(SparseHandshakeSvc));
|
||||
assert!(caps.camera);
|
||||
assert!(!caps.microphone);
|
||||
assert_eq!(caps.camera_output, None);
|
||||
assert_eq!(caps.camera_codec, None);
|
||||
assert_eq!(caps.camera_width, None);
|
||||
assert_eq!(caps.camera_height, None);
|
||||
assert_eq!(caps.camera_fps, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn handshake_times_out_slow_capabilities_call() {
|
||||
let rt = Runtime::new().expect("create runtime");
|
||||
let caps = rt.block_on(negotiate_against_service(SlowHandshakeSvc));
|
||||
assert_default_caps(&caps);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn handshake_service_direct_call_reports_capabilities() {
|
||||
with_var("LESAVKA_CAM_OUTPUT", Some("uvc"), || {
|
||||
let rt = Runtime::new().expect("create runtime");
|
||||
let response = rt.block_on(async {
|
||||
lesavka_server::handshake::HandshakeSvc
|
||||
.get_capabilities(Request::new(Empty {}))
|
||||
.await
|
||||
.expect("handshake service response")
|
||||
.into_inner()
|
||||
});
|
||||
assert!(response.camera);
|
||||
assert!(response.microphone);
|
||||
assert_eq!(response.camera_output, "uvc");
|
||||
assert_eq!(response.camera_codec, "mjpeg");
|
||||
assert!(response.camera_width > 0);
|
||||
assert!(response.camera_height > 0);
|
||||
assert!(response.camera_fps > 0);
|
||||
|
||||
let _ = lesavka_server::handshake::HandshakeSvc::server();
|
||||
});
|
||||
}
|
||||
|
||||
@ -111,3 +111,63 @@ fn camera_config_output_override_is_case_insensitive() {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn camera_config_defaults_when_uvc_dimensions_and_rate_are_missing() {
|
||||
with_var("LESAVKA_CAM_OUTPUT", Some("uvc"), || {
|
||||
with_var("LESAVKA_UVC_WIDTH", None::<&str>, || {
|
||||
with_var("LESAVKA_UVC_HEIGHT", None::<&str>, || {
|
||||
with_var("LESAVKA_UVC_FPS", None::<&str>, || {
|
||||
with_var("LESAVKA_UVC_INTERVAL", None::<&str>, || {
|
||||
let cfg = update_camera_config();
|
||||
assert_eq!(cfg.output, CameraOutput::Uvc);
|
||||
assert_eq!(cfg.codec, CameraCodec::Mjpeg);
|
||||
assert_eq!(cfg.width, 1280);
|
||||
assert_eq!(cfg.height, 720);
|
||||
assert_eq!(cfg.fps, 25);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn camera_config_prefers_explicit_fps_over_interval() {
|
||||
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", Some("48"), || {
|
||||
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, 800);
|
||||
assert_eq!(cfg.height, 600);
|
||||
assert_eq!(cfg.fps, 48);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn camera_config_invalid_output_falls_back_to_detected_policy() {
|
||||
with_var("LESAVKA_CAM_OUTPUT", Some("not-a-real-mode"), || {
|
||||
let cfg = update_camera_config();
|
||||
match cfg.output {
|
||||
CameraOutput::Uvc => {
|
||||
assert_eq!(cfg.codec, CameraCodec::Mjpeg);
|
||||
assert!(cfg.fps > 0);
|
||||
}
|
||||
CameraOutput::Hdmi => {
|
||||
assert_eq!(cfg.codec, CameraCodec::H264);
|
||||
assert_eq!(cfg.fps, 30);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -39,6 +39,33 @@ fn activate_rejects_uvc_when_disabled_and_bumps_generation() {
|
||||
assert!(!runtime.is_active(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn activate_tracks_latest_generation_across_repeated_failures() {
|
||||
let runtime = CameraRuntime::new();
|
||||
let cfg = CameraConfig {
|
||||
output: CameraOutput::Uvc,
|
||||
codec: CameraCodec::Mjpeg,
|
||||
width: 640,
|
||||
height: 360,
|
||||
fps: 30,
|
||||
hdmi: None,
|
||||
};
|
||||
|
||||
with_var("LESAVKA_DISABLE_UVC", Some("1"), || {
|
||||
let rt = Runtime::new().expect("runtime");
|
||||
for expected in [1_u64, 2_u64, 3_u64] {
|
||||
let result = rt.block_on(runtime.activate(&cfg));
|
||||
match result {
|
||||
Ok(_) => panic!("UVC should remain disabled"),
|
||||
Err(err) => assert_eq!(err.code(), Code::FailedPrecondition),
|
||||
}
|
||||
assert!(runtime.is_active(expected));
|
||||
assert!(!runtime.is_active(expected + 1));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn camera_cfg_eq_handles_none_and_hdmi_connector_edges() {
|
||||
let uvc_a = CameraConfig {
|
||||
@ -77,3 +104,34 @@ fn camera_cfg_eq_handles_none_and_hdmi_connector_edges() {
|
||||
missing_connector.hdmi = None;
|
||||
assert!(!camera_cfg_eq(&missing_connector, &hdmi_b));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn camera_cfg_eq_rejects_output_codec_resolution_and_fps_changes() {
|
||||
let base = CameraConfig {
|
||||
output: CameraOutput::Uvc,
|
||||
codec: CameraCodec::Mjpeg,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
fps: 25,
|
||||
hdmi: None,
|
||||
};
|
||||
let mut changed = base.clone();
|
||||
changed.output = CameraOutput::Hdmi;
|
||||
assert!(!camera_cfg_eq(&base, &changed));
|
||||
|
||||
changed = base.clone();
|
||||
changed.codec = CameraCodec::H264;
|
||||
assert!(!camera_cfg_eq(&base, &changed));
|
||||
|
||||
changed = base.clone();
|
||||
changed.width = 1920;
|
||||
assert!(!camera_cfg_eq(&base, &changed));
|
||||
|
||||
changed = base.clone();
|
||||
changed.height = 1080;
|
||||
assert!(!camera_cfg_eq(&base, &changed));
|
||||
|
||||
changed = base.clone();
|
||||
changed.fps = 30;
|
||||
assert!(!camera_cfg_eq(&base, &changed));
|
||||
}
|
||||
|
||||
@ -55,6 +55,50 @@ fn supervise_uvc_control_starts_helper_when_device_is_set() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn supervise_uvc_control_restarts_helper_after_exit() {
|
||||
let dir = tempdir().expect("create temp dir");
|
||||
let marker = dir.path().join("marker.log");
|
||||
let helper = dir.path().join("helper.sh");
|
||||
fs::write(
|
||||
&helper,
|
||||
format!(
|
||||
"#!/usr/bin/env bash\nset -euo pipefail\necho \"$*\" >> '{}'\nexit 0\n",
|
||||
marker.display()
|
||||
),
|
||||
)
|
||||
.expect("write helper script");
|
||||
let mut perms = fs::metadata(&helper)
|
||||
.expect("helper metadata")
|
||||
.permissions();
|
||||
perms.set_mode(0o755);
|
||||
fs::set_permissions(&helper, perms).expect("chmod helper script");
|
||||
|
||||
let helper_path = helper.to_string_lossy().to_string();
|
||||
with_var("LESAVKA_UVC_DEV", Some("/dev/video-loop"), || {
|
||||
let rt = Runtime::new().expect("runtime");
|
||||
let result = rt.block_on(async {
|
||||
tokio::time::timeout(
|
||||
Duration::from_millis(2_450),
|
||||
supervise_uvc_control(helper_path),
|
||||
)
|
||||
.await
|
||||
});
|
||||
assert!(result.is_err(), "supervisor should still be running");
|
||||
});
|
||||
|
||||
let calls = fs::read_to_string(marker).expect("read helper marker");
|
||||
let restart_count = calls
|
||||
.lines()
|
||||
.filter(|line| line.contains("--device /dev/video-loop"))
|
||||
.count();
|
||||
assert!(
|
||||
restart_count >= 2,
|
||||
"expected helper restart loop to run at least twice, got {restart_count}: {calls}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn supervise_uvc_control_survives_missing_helper_binary() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user