lesavka/testing/tests/server_video_include_contract.rs

220 lines
7.6 KiB
Rust

//! Include-based coverage for server video stream plumbing.
//!
//! Scope: include `server/src/video.rs` and exercise deterministic stream
//! behavior plus malformed pipeline inputs.
//! Targets: `server/src/video.rs`.
//! Why: keep eye-stream setup and wrapper behavior stable without depending on
//! camera hardware in CI.
#[allow(warnings)]
mod video_sinks {
pub struct CameraRelay;
pub struct HdmiSink;
pub struct WebcamSink;
}
mod video_support {
pub use lesavka_server::video_support::{
adjust_effective_fps, contains_idr, default_eye_fps, env_u32, env_usize,
should_send_frame,
};
}
#[allow(warnings)]
mod video_include_contract {
include!(env!("LESAVKA_SERVER_VIDEO_SRC"));
use futures_util::StreamExt;
use serial_test::serial;
use temp_env::with_var;
fn init_gst() {
let _ = gst::init();
}
#[tokio::test]
async fn video_stream_forwards_inner_packets() {
init_gst();
let (tx, rx) = tokio::sync::mpsc::channel(2);
tx.send(Ok(VideoPacket {
id: 1,
pts: 42,
data: vec![1, 2, 3, 4],
}))
.await
.expect("send packet");
drop(tx);
let mut stream = VideoStream {
_pipeline: gst::Pipeline::new(),
inner: ReceiverStream::new(rx),
};
let first = stream.next().await.expect("stream item").expect("packet ok");
assert_eq!(first.id, 1);
assert_eq!(first.pts, 42);
assert_eq!(first.data, vec![1, 2, 3, 4]);
assert!(stream.next().await.is_none(), "stream should be exhausted");
}
#[test]
fn video_stream_drop_is_safe_for_empty_pipeline() {
init_gst();
let (_tx, rx) = tokio::sync::mpsc::channel(1);
let stream = VideoStream {
_pipeline: gst::Pipeline::new(),
inner: ReceiverStream::new(rx),
};
drop(stream);
}
#[test]
#[serial]
fn eye_ball_returns_error_for_malformed_device_string() {
let rt = tokio::runtime::Runtime::new().expect("runtime");
with_var("LESAVKA_EYE_ADAPTIVE", Some("0"), || {
with_var("LESAVKA_EYE_QUEUE_BUFFERS", Some("4"), || {
let result = rt.block_on(eye_ball(
"/dev/video0\" ! thiswillneverparse",
0,
4_000,
));
assert!(result.is_err(), "malformed pipeline should fail parse");
});
});
}
#[test]
#[serial]
fn eye_ball_respects_env_paths_before_parse_failure() {
let rt = tokio::runtime::Runtime::new().expect("runtime");
with_var("LESAVKA_EYE_APPSINK_BUFFERS", Some("5"), || {
with_var("LESAVKA_EYE_CHAN_CAPACITY", Some("17"), || {
let result = rt.block_on(eye_ball(
"/dev/video1\" ! still-bad",
1,
8_000,
));
assert!(result.is_err(), "malformed pipeline should fail parse");
});
});
}
#[test]
fn eye_ball_panics_for_out_of_range_eye_id() {
let rt = tokio::runtime::Runtime::new().expect("runtime");
let panic_result = std::panic::catch_unwind(|| {
let _ = rt.block_on(eye_ball("/dev/video0", 2, 1_000));
});
assert!(panic_result.is_err(), "invalid eye id must panic before setup");
}
#[test]
#[serial]
fn eye_ball_attempts_runtime_setup_for_existing_non_camera_device() {
let rt = tokio::runtime::Runtime::new().expect("runtime");
with_var("LESAVKA_EYE_ADAPTIVE", Some("1"), || {
with_var("LESAVKA_EYE_QUEUE_BUFFERS", Some("3"), || {
with_var("LESAVKA_EYE_APPSINK_BUFFERS", Some("3"), || {
let result = rt.block_on(async {
tokio::time::timeout(
std::time::Duration::from_millis(250),
eye_ball("/dev/null", 0, 6_000),
)
.await
});
match result {
Ok(Ok(stream)) => drop(stream),
Ok(Err(_)) | Err(_) => {}
}
});
});
});
}
#[test]
#[serial]
fn eye_ball_second_eye_branch_runs_without_panicking() {
let rt = tokio::runtime::Runtime::new().expect("runtime");
with_var("LESAVKA_EYE_ADAPTIVE", Some("0"), || {
let result = rt.block_on(async {
tokio::time::timeout(
std::time::Duration::from_millis(250),
eye_ball("/dev/null", 1, 2_000),
)
.await
});
match result {
Ok(Ok(stream)) => drop(stream),
Ok(Err(_)) | Err(_) => {}
}
});
}
#[test]
#[serial]
fn eye_ball_testsrc_path_produces_stream_packets() {
let rt = tokio::runtime::Runtime::new().expect("runtime");
with_var("LESAVKA_EYE_ADAPTIVE", Some("1"), || {
with_var("LESAVKA_EYE_QUEUE_BUFFERS", Some("8"), || {
with_var("LESAVKA_EYE_APPSINK_BUFFERS", Some("8"), || {
with_var("LESAVKA_EYE_TESTSRC_KBIT", Some("1200"), || {
rt.block_on(async {
let setup = tokio::time::timeout(
std::time::Duration::from_secs(2),
eye_ball("testsrc", 0, 1_200),
)
.await;
let mut stream = match setup {
Ok(Ok(stream)) => stream,
Ok(Err(err)) => panic!("testsrc setup failed: {err:#}"),
Err(_) => panic!("testsrc setup timed out"),
};
let packet = tokio::time::timeout(
std::time::Duration::from_secs(2),
stream.next(),
)
.await
.expect("video packet timeout")
.expect("stream item")
.expect("packet");
assert!(packet.id <= 1);
assert!(!packet.data.is_empty());
drop(stream);
});
});
});
});
});
}
#[test]
#[serial]
fn eye_ball_testsrc_backpressure_path_is_non_panicking() {
let rt = tokio::runtime::Runtime::new().expect("runtime");
with_var("LESAVKA_EYE_ADAPTIVE", Some("1"), || {
with_var("LESAVKA_EYE_CHAN_CAPACITY", Some("16"), || {
rt.block_on(async {
let setup = tokio::time::timeout(
std::time::Duration::from_secs(2),
eye_ball("testsrc", 1, 1_800),
)
.await;
let mut stream = match setup {
Ok(Ok(stream)) => stream,
Ok(Err(err)) => panic!("testsrc setup failed: {err:#}"),
Err(_) => panic!("testsrc setup timed out"),
};
tokio::time::sleep(std::time::Duration::from_millis(300)).await;
let _ = tokio::time::timeout(
std::time::Duration::from_secs(1),
stream.next(),
)
.await;
});
});
});
}
}