diff --git a/scripts/ci/video_downstream_gate.sh b/scripts/ci/video_downstream_gate.sh new file mode 100755 index 0000000..44f7829 --- /dev/null +++ b/scripts/ci/video_downstream_gate.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Guard downstream eye-video stability before pushing video-related changes. +set -euo pipefail + +ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd) +cd "$ROOT" + +VIDEO_TESTS=( + --test video_downstream_feed_contract + --test server_video_include_contract + --test video_support_contract + --test client_output_video_include_contract + --test server_video_sinks_include_contract + --test server_video_sink_smoke_contract +) + +VIDEO_IGNORE_REGEX='(/common/src/(hid|paste|process_metrics)\.rs|/server/src/(audio|camera|gadget|paste|runtime_support|uvc_runtime)\.rs)' + +cargo fmt --all -- --check +cargo check -q --bin lesavka-client --bin lesavka-server +cargo test -q -p lesavka_testing "${VIDEO_TESTS[@]}" + +cargo llvm-cov clean --workspace +cargo llvm-cov --workspace "${VIDEO_TESTS[@]}" \ + --ignore-filename-regex "$VIDEO_IGNORE_REGEX" \ + --fail-under-lines 95 \ + --summary-only diff --git a/server/src/video.rs b/server/src/video.rs index c5e87e7..0654d20 100644 --- a/server/src/video.rs +++ b/server/src/video.rs @@ -336,12 +336,17 @@ pub async fn eye_ball_with_request( let pipeline = gst::Pipeline::new(); let (tx, rx) = tokio::sync::mpsc::channel(64); - let _ = tx.try_send(Ok(VideoPacket { - id: id.min(1), - pts: 0, - data: vec![0, 0, 0, 1, 0x65, 0x88, 0x84], - ..Default::default() - })); + for seq in 0..8 { + let _ = tx.try_send(Ok(VideoPacket { + id: id.min(1), + pts: seq * 16_666, + data: vec![0, 0, 0, 1, 0x65, 0x88, 0x84], + seq: seq + 1, + effective_fps: 60, + server_encoder_label: "coverage-testsrc".to_string(), + ..Default::default() + })); + } Ok(VideoStream { _pipeline: pipeline, diff --git a/testing/tests/client_output_video_include_contract.rs b/testing/tests/client_output_video_include_contract.rs index 3993f8b..a134d5a 100644 --- a/testing/tests/client_output_video_include_contract.rs +++ b/testing/tests/client_output_video_include_contract.rs @@ -95,6 +95,33 @@ mod video_include_contract { with_var("PATH", Some(merged), f); } + #[test] + #[serial] + fn h264_decoder_selection_honors_env_and_fallbacks() { + gst::init().expect("initialize gstreamer"); + with_var("LESAVKA_H264_DECODER", Some("decodebin"), || { + assert_eq!(pick_h264_decoder(), "decodebin"); + }); + with_var("LESAVKA_H264_DECODER", Some("fakesink"), || { + assert_eq!(pick_h264_decoder(), "fakesink"); + }); + with_var( + "LESAVKA_H264_DECODER", + Some("definitely-not-a-decoder"), + || { + assert_ne!(pick_h264_decoder(), "definitely-not-a-decoder"); + }, + ); + with_var("LESAVKA_H264_DECODER", Some(" "), || { + assert!(!pick_h264_decoder().trim().is_empty()); + }); + with_var("LESAVKA_H264_DECODER", None::<&str>, || { + assert!(!pick_h264_decoder().trim().is_empty()); + }); + assert!(buildable_decoder("fakesink")); + assert!(!buildable_decoder("definitely-not-a-real-gst-element")); + } + #[test] #[serial] fn monitor_window_new_covers_x11_backend_path() { diff --git a/testing/tests/server_video_include_contract.rs b/testing/tests/server_video_include_contract.rs index 3cf0d07..eee5cbf 100644 --- a/testing/tests/server_video_include_contract.rs +++ b/testing/tests/server_video_include_contract.rs @@ -31,6 +31,30 @@ mod video_include_contract { let _ = gst::init(); } + #[test] + fn eye_profile_and_telemetry_helpers_are_stable() { + assert_eq!(eye_source_profile(), (1920, 1080, 60)); + + let metric = server_process_cpu_metric(); + metric.store(123, std::sync::atomic::Ordering::Relaxed); + assert_eq!(metric.load(std::sync::atomic::Ordering::Relaxed), 123); + + let last_window = AtomicU64::new(5); + let source_gap = AtomicU32::new(99); + let send_gap = AtomicU32::new(88); + let queue_peak = AtomicU32::new(77); + + reset_stream_telemetry_window(&last_window, 5, &source_gap, &send_gap, &queue_peak); + assert_eq!(source_gap.load(Ordering::Relaxed), 99); + assert_eq!(send_gap.load(Ordering::Relaxed), 88); + assert_eq!(queue_peak.load(Ordering::Relaxed), 77); + + reset_stream_telemetry_window(&last_window, 6, &source_gap, &send_gap, &queue_peak); + assert_eq!(source_gap.load(Ordering::Relaxed), 0); + assert_eq!(send_gap.load(Ordering::Relaxed), 0); + assert_eq!(queue_peak.load(Ordering::Relaxed), 0); + } + #[tokio::test] async fn video_stream_forwards_inner_packets() { init_gst(); @@ -248,4 +272,24 @@ mod video_include_contract { }); }); } + + #[test] + #[serial] + fn wait_for_eye_device_reports_non_character_paths() { + let rt = tokio::runtime::Runtime::new().expect("runtime"); + let dir = tempfile::tempdir().expect("tempdir"); + let regular_file = dir.path().join("not-a-video-device"); + std::fs::write(®ular_file, "not a device").expect("write marker file"); + 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(regular_file.to_str().expect("utf8 path"), "l") + .await + .expect_err("regular files should not count as eye devices"); + let rendered = format!("{err:#}"); + assert!(rendered.contains("not a character device")); + }); + }); + }); + } } diff --git a/testing/tests/video_downstream_feed_contract.rs b/testing/tests/video_downstream_feed_contract.rs index 020b999..ef32cfc 100644 --- a/testing/tests/video_downstream_feed_contract.rs +++ b/testing/tests/video_downstream_feed_contract.rs @@ -49,6 +49,22 @@ fn native_downstream_eye_modes_stay_widescreen_and_square_pixel() { (mode.width, mode.height) ); } + assert_eq!( + display_size_for_source_mode(EyeSourceMode { + width: 720, + height: 576, + fps: 50, + }), + (1024, 576) + ); + assert_eq!( + display_size_for_source_mode(EyeSourceMode { + width: 720, + height: 480, + fps: 60, + }), + (854, 480) + ); } #[test] diff --git a/testing/tests/video_support_contract.rs b/testing/tests/video_support_contract.rs index 2f9aa0b..b37436e 100644 --- a/testing/tests/video_support_contract.rs +++ b/testing/tests/video_support_contract.rs @@ -29,6 +29,7 @@ fn contains_idr_handles_short_and_multi_nal_annex_b_streams() { assert!(contains_idr(&[0, 0, 1, 0x65, 0x00])); assert!(contains_idr(&[0, 0, 0, 1, 0x41, 0x00, 0, 0, 1, 0x65, 0x00])); assert!(!contains_idr(&[0, 0, 0, 1, 0x41, 0x00, 0x00])); + assert!(!contains_idr(&[0, 0, 2, 0x65, 0, 0, 0, 2, 0x65])); } #[test]