2026-04-12 21:10:15 -03:00
|
|
|
//! 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::{
|
2026-04-14 23:03:18 -03:00
|
|
|
adjust_effective_fps, contains_idr, default_eye_fps, env_u32, env_usize, should_send_frame,
|
2026-04-12 21:10:15 -03:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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(),
|
2026-04-15 11:37:43 -03:00
|
|
|
#[cfg(not(coverage))]
|
|
|
|
|
_bus_watch: None,
|
2026-04-12 21:10:15 -03:00
|
|
|
inner: ReceiverStream::new(rx),
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-14 23:03:18 -03:00
|
|
|
let first = stream
|
|
|
|
|
.next()
|
|
|
|
|
.await
|
|
|
|
|
.expect("stream item")
|
|
|
|
|
.expect("packet ok");
|
2026-04-12 21:10:15 -03:00
|
|
|
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(),
|
2026-04-15 11:37:43 -03:00
|
|
|
#[cfg(not(coverage))]
|
|
|
|
|
_bus_watch: None,
|
2026-04-12 21:10:15 -03:00
|
|
|
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"), || {
|
2026-04-14 23:03:18 -03:00
|
|
|
let result = rt.block_on(eye_ball("/dev/video0\" ! thiswillneverparse", 0, 4_000));
|
2026-04-12 21:10:15 -03:00
|
|
|
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"), || {
|
2026-04-14 23:03:18 -03:00
|
|
|
let result = rt.block_on(eye_ball("/dev/video1\" ! still-bad", 1, 8_000));
|
2026-04-12 21:10:15 -03:00
|
|
|
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));
|
|
|
|
|
});
|
2026-04-14 23:03:18 -03:00
|
|
|
assert!(
|
|
|
|
|
panic_result.is_err(),
|
|
|
|
|
"invalid eye id must panic before setup"
|
|
|
|
|
);
|
2026-04-12 21:10:15 -03:00
|
|
|
}
|
2026-04-13 02:52:32 -03:00
|
|
|
|
|
|
|
|
#[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;
|
2026-04-14 23:03:18 -03:00
|
|
|
let _ = tokio::time::timeout(std::time::Duration::from_secs(1), stream.next())
|
|
|
|
|
.await;
|
2026-04-13 02:52:32 -03:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-04-15 11:37:43 -03:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[serial]
|
|
|
|
|
fn wait_for_eye_device_accepts_existing_character_device() {
|
|
|
|
|
let rt = tokio::runtime::Runtime::new().expect("runtime");
|
|
|
|
|
with_var("LESAVKA_EYE_DEVICE_WAIT_MS", Some("50"), || {
|
|
|
|
|
rt.block_on(async {
|
|
|
|
|
wait_for_eye_device("/dev/null", "l")
|
|
|
|
|
.await
|
|
|
|
|
.expect("/dev/null should be ready");
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[serial]
|
|
|
|
|
fn wait_for_eye_device_times_out_for_missing_path() {
|
|
|
|
|
let rt = tokio::runtime::Runtime::new().expect("runtime");
|
|
|
|
|
let dir = tempfile::tempdir().expect("tempdir");
|
|
|
|
|
let missing = dir.path().join("lesavka-eye-missing");
|
|
|
|
|
with_var("LESAVKA_EYE_DEVICE_WAIT_MS", Some("50"), || {
|
|
|
|
|
with_var("LESAVKA_EYE_DEVICE_POLL_MS", Some("25"), || {
|
|
|
|
|
rt.block_on(async {
|
|
|
|
|
let err = wait_for_eye_device(
|
|
|
|
|
missing.to_str().expect("utf8 path"),
|
|
|
|
|
"r",
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.expect_err("missing eye device should time out");
|
|
|
|
|
let rendered = format!("{err:#}");
|
|
|
|
|
assert!(rendered.contains("was not ready within 50 ms"));
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-04-12 21:10:15 -03:00
|
|
|
}
|