//! 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; }); }); }); } }