2026-04-11 03:36:42 -03:00
|
|
|
//! 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.
|
|
|
|
|
|
2026-04-23 03:49:49 -03:00
|
|
|
use lesavka_client::handshake::{HandshakeProbe, PeerCaps, negotiate, probe};
|
2026-04-12 15:04:19 -03:00
|
|
|
use lesavka_common::lesavka::{
|
|
|
|
|
Empty, HandshakeSet,
|
|
|
|
|
handshake_server::{Handshake, HandshakeServer},
|
|
|
|
|
};
|
2026-04-11 03:36:42 -03:00
|
|
|
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;
|
2026-04-12 15:04:19 -03:00
|
|
|
use tonic::{Request, Response, Status, transport::Server};
|
2026-04-11 03:36:42 -03:00
|
|
|
|
2026-04-12 15:04:19 -03:00
|
|
|
async fn negotiate_against_service<S>(service: S) -> PeerCaps
|
|
|
|
|
where
|
|
|
|
|
S: Handshake + Send + Sync + 'static,
|
|
|
|
|
{
|
2026-04-11 03:36:42 -03:00
|
|
|
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()
|
2026-04-12 15:04:19 -03:00
|
|
|
.add_service(HandshakeServer::new(service))
|
2026-04-11 03:36:42 -03:00
|
|
|
.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
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-23 03:49:49 -03:00
|
|
|
async fn probe_against_service<S>(service: S) -> HandshakeProbe
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>();
|
|
|
|
|
let server = tokio::spawn(async move {
|
|
|
|
|
Server::builder()
|
|
|
|
|
.add_service(HandshakeServer::new(service))
|
|
|
|
|
.serve_with_shutdown(addr, async move {
|
|
|
|
|
let _ = shutdown_rx.await;
|
|
|
|
|
})
|
|
|
|
|
.await
|
|
|
|
|
.expect("serve handshake server");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
tokio::time::sleep(Duration::from_millis(50)).await;
|
|
|
|
|
let result = probe(&format!("http://{addr}")).await;
|
|
|
|
|
let _ = shutdown_tx.send(());
|
|
|
|
|
let _ = server.await;
|
|
|
|
|
result
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 15:04:19 -03:00
|
|
|
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);
|
2026-04-16 15:59:42 -03:00
|
|
|
assert_eq!(caps.server_version, None);
|
2026-04-12 15:04:19 -03:00
|
|
|
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);
|
2026-04-16 12:58:05 -03:00
|
|
|
assert_eq!(caps.eye_width, None);
|
|
|
|
|
assert_eq!(caps.eye_height, None);
|
|
|
|
|
assert_eq!(caps.eye_fps, None);
|
2026-04-12 15:04:19 -03:00
|
|
|
}
|
|
|
|
|
|
2026-04-23 03:49:49 -03:00
|
|
|
fn assert_default_probe(probe_result: &HandshakeProbe) {
|
|
|
|
|
assert_default_caps(&probe_result.caps);
|
|
|
|
|
assert_eq!(probe_result.rtt_ms, None);
|
|
|
|
|
assert!(!probe_result.reachable);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 15:04:19 -03:00
|
|
|
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,
|
2026-04-16 15:59:42 -03:00
|
|
|
server_version: String::new(),
|
2026-04-12 15:04:19 -03:00
|
|
|
camera_output: String::new(),
|
|
|
|
|
camera_codec: String::new(),
|
|
|
|
|
camera_width: 0,
|
|
|
|
|
camera_height: 0,
|
|
|
|
|
camera_fps: 0,
|
2026-04-16 12:58:05 -03:00
|
|
|
eye_width: 0,
|
|
|
|
|
eye_height: 0,
|
|
|
|
|
eye_fps: 0,
|
2026-05-01 20:25:56 -03:00
|
|
|
server_revision: String::new(),
|
2026-05-03 02:13:39 -03:00
|
|
|
bundled_webcam_media: false,
|
2026-04-12 15:04:19 -03:00
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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()))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 03:36:42 -03:00
|
|
|
#[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);
|
2026-04-16 15:59:42 -03:00
|
|
|
assert_eq!(
|
|
|
|
|
caps.server_version,
|
|
|
|
|
Some(lesavka_server::VERSION.to_string())
|
|
|
|
|
);
|
2026-04-11 03:36:42 -03:00
|
|
|
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]
|
2026-04-24 14:49:57 -03:00
|
|
|
fn handshake_returns_hdmi_caps_with_mjpeg_when_h264_decode_is_unavailable() {
|
2026-04-11 03:36:42 -03:00
|
|
|
with_var("LESAVKA_CAM_OUTPUT", Some("hdmi"), || {
|
|
|
|
|
with_var("LESAVKA_DISABLE_UAC", Some("1"), || {
|
2026-04-24 14:49:57 -03:00
|
|
|
with_var("LESAVKA_HW_H264", None::<&str>, || {
|
|
|
|
|
with_var("LESAVKA_CAM_CODEC", None::<&str>, || {
|
|
|
|
|
let _ = lesavka_server::camera::update_camera_config();
|
|
|
|
|
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("mjpeg")));
|
|
|
|
|
assert_eq!(caps.camera_fps, Some(30));
|
|
|
|
|
assert!(!caps.microphone);
|
|
|
|
|
assert!(caps.camera);
|
|
|
|
|
assert_eq!(caps.camera_width, Some(1280));
|
|
|
|
|
assert_eq!(caps.camera_height, Some(720));
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[serial]
|
|
|
|
|
fn handshake_honors_explicit_hdmi_h264_override() {
|
|
|
|
|
with_var("LESAVKA_CAM_OUTPUT", Some("hdmi"), || {
|
|
|
|
|
with_var("LESAVKA_DISABLE_UAC", Some("1"), || {
|
|
|
|
|
with_var("LESAVKA_HW_H264", None::<&str>, || {
|
|
|
|
|
with_var("LESAVKA_CAM_CODEC", Some("h264"), || {
|
|
|
|
|
let _ = lesavka_server::camera::update_camera_config();
|
|
|
|
|
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_eq!(caps.camera_width, Some(1280));
|
|
|
|
|
assert_eq!(caps.camera_height, Some(720));
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-04-11 03:36:42 -03:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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") => {
|
2026-04-24 14:49:57 -03:00
|
|
|
assert!(matches!(
|
|
|
|
|
caps.camera_codec.as_deref(),
|
|
|
|
|
Some("mjpeg") | Some("h264")
|
|
|
|
|
));
|
2026-04-11 03:36:42 -03:00
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-04-12 15:04:19 -03:00
|
|
|
|
|
|
|
|
#[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);
|
2026-04-16 12:58:05 -03:00
|
|
|
assert_eq!(caps.eye_width, None);
|
|
|
|
|
assert_eq!(caps.eye_height, None);
|
|
|
|
|
assert_eq!(caps.eye_fps, None);
|
2026-04-12 15:04:19 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-23 03:49:49 -03:00
|
|
|
#[test]
|
|
|
|
|
#[serial]
|
|
|
|
|
fn handshake_probe_reports_reachable_caps_and_rtt() {
|
|
|
|
|
with_var("LESAVKA_CAM_OUTPUT", Some("uvc"), || {
|
|
|
|
|
let rt = Runtime::new().expect("create runtime");
|
|
|
|
|
let probe_result = rt.block_on(probe_against_service(
|
|
|
|
|
lesavka_server::handshake::HandshakeSvc,
|
|
|
|
|
));
|
|
|
|
|
assert!(probe_result.reachable);
|
|
|
|
|
assert!(probe_result.rtt_ms.is_some_and(|rtt| rtt >= 0.0));
|
|
|
|
|
assert!(probe_result.caps.camera);
|
|
|
|
|
assert!(probe_result.caps.microphone);
|
|
|
|
|
assert_eq!(probe_result.caps.camera_output.as_deref(), Some("uvc"));
|
|
|
|
|
assert_eq!(probe_result.caps.camera_codec.as_deref(), Some("mjpeg"));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[serial]
|
|
|
|
|
fn handshake_probe_marks_unimplemented_service_reachable() {
|
|
|
|
|
let rt = Runtime::new().expect("create runtime");
|
|
|
|
|
let probe_result = rt.block_on(probe_against_service(UnimplementedHandshakeSvc));
|
|
|
|
|
assert!(probe_result.reachable);
|
|
|
|
|
assert!(probe_result.rtt_ms.is_some());
|
|
|
|
|
assert_default_caps(&probe_result.caps);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[serial]
|
|
|
|
|
fn handshake_probe_marks_internal_error_service_reachable() {
|
|
|
|
|
let rt = Runtime::new().expect("create runtime");
|
|
|
|
|
let probe_result = rt.block_on(probe_against_service(InternalErrorHandshakeSvc));
|
|
|
|
|
assert!(probe_result.reachable);
|
|
|
|
|
assert!(probe_result.rtt_ms.is_some());
|
|
|
|
|
assert_default_caps(&probe_result.caps);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[serial]
|
|
|
|
|
fn handshake_probe_maps_empty_optional_fields_to_none() {
|
|
|
|
|
let rt = Runtime::new().expect("create runtime");
|
|
|
|
|
let probe_result = rt.block_on(probe_against_service(SparseHandshakeSvc));
|
|
|
|
|
assert!(probe_result.reachable);
|
|
|
|
|
assert!(probe_result.caps.camera);
|
|
|
|
|
assert!(!probe_result.caps.microphone);
|
|
|
|
|
assert_eq!(probe_result.caps.camera_output, None);
|
|
|
|
|
assert_eq!(probe_result.caps.camera_codec, None);
|
|
|
|
|
assert_eq!(probe_result.caps.camera_width, None);
|
|
|
|
|
assert_eq!(probe_result.caps.camera_height, None);
|
|
|
|
|
assert_eq!(probe_result.caps.camera_fps, None);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[serial]
|
|
|
|
|
fn handshake_probe_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 probe_result = rt.block_on(probe(&format!("http://{addr}")));
|
|
|
|
|
assert_default_probe(&probe_result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[serial]
|
|
|
|
|
fn handshake_probe_returns_defaults_for_invalid_endpoint() {
|
|
|
|
|
let rt = Runtime::new().expect("create runtime");
|
|
|
|
|
let probe_result = rt.block_on(probe("not a uri"));
|
|
|
|
|
assert_default_probe(&probe_result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[serial]
|
|
|
|
|
fn handshake_probe_times_out_slow_capabilities_call() {
|
|
|
|
|
let rt = Runtime::new().expect("create runtime");
|
|
|
|
|
let probe_result = rt.block_on(probe_against_service(SlowHandshakeSvc));
|
|
|
|
|
assert_default_probe(&probe_result);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 15:04:19 -03:00
|
|
|
#[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);
|
2026-04-16 12:58:05 -03:00
|
|
|
assert!(response.eye_width > 0);
|
|
|
|
|
assert!(response.eye_height > 0);
|
|
|
|
|
assert!(response.eye_fps > 0);
|
2026-04-12 15:04:19 -03:00
|
|
|
|
|
|
|
|
let _ = lesavka_server::handshake::HandshakeSvc::server();
|
|
|
|
|
});
|
|
|
|
|
}
|