lesavka/testing/tests/client_output_video_include_contract.rs

308 lines
9.1 KiB
Rust

//! Include-based coverage for client video output window plumbing.
//!
//! Scope: include `client/src/output/video.rs` with deterministic display/layout
//! stubs and exercise backend/placement branches without a real desktop session.
//! Targets: `client/src/output/video.rs`.
//! Why: monitor window orchestration contains branch-heavy environment logic that
//! should remain stable in CI.
mod output {
pub mod display {
#[derive(Clone, Copy, Debug)]
pub struct MonitorInfo {
pub x: i32,
pub y: i32,
pub w: i32,
pub h: i32,
}
pub fn enumerate_monitors() -> Vec<MonitorInfo> {
vec![
MonitorInfo {
x: 0,
y: 0,
w: 1920,
h: 1080,
},
MonitorInfo {
x: 1920,
y: 0,
w: 1920,
h: 1080,
},
]
}
}
pub mod layout {
#[derive(Clone, Copy, Debug)]
pub struct Rect {
pub x: i32,
pub y: i32,
pub w: i32,
pub h: i32,
}
pub fn assign_rectangles(
monitors: &[super::display::MonitorInfo],
streams: &[(&str, i32, i32)],
) -> Vec<Rect> {
streams
.iter()
.enumerate()
.map(|(idx, _)| {
let mon = monitors.get(idx).unwrap_or(&monitors[0]);
Rect {
x: mon.x,
y: mon.y,
w: mon.w,
h: mon.h,
}
})
.collect()
}
}
}
#[allow(warnings)]
mod video_include_contract {
include!(env!("LESAVKA_CLIENT_OUTPUT_VIDEO_SRC"));
use serial_test::serial;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use temp_env::with_var;
use tempfile::tempdir;
fn write_executable(dir: &Path, name: &str, body: &str) {
let path = dir.join(name);
fs::write(&path, body).expect("write script");
let mut perms = fs::metadata(&path).expect("metadata").permissions();
perms.set_mode(0o755);
fs::set_permissions(path, perms).expect("chmod");
}
fn with_fake_bin(name: &str, script_body: &str, f: impl FnOnce()) {
let dir = tempdir().expect("tempdir");
write_executable(dir.path(), name, script_body);
let prior = std::env::var("PATH").unwrap_or_default();
let merged = if prior.is_empty() {
dir.path().display().to_string()
} else {
format!("{}:{prior}", dir.path().display())
};
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() {
with_var("GDK_BACKEND", Some("x11"), || {
with_var("DISPLAY", Some(":99"), || {
with_var("WAYLAND_DISPLAY", None::<&str>, || {
let result = MonitorWindow::new(0);
if let Ok(window) = result {
window.push_packet(VideoPacket {
id: 0,
pts: 5,
data: vec![0, 0, 0, 1, 0x67],
..Default::default()
});
}
});
});
});
}
#[test]
#[serial]
fn monitor_window_new_covers_wayland_swaymsg_placement_branch() {
let swaymsg = r#"#!/usr/bin/env sh
if [ "$1" = "-t" ] && [ "$2" = "get_tree" ]; then
echo '{}'
exit 0
fi
exit 0
"#;
with_fake_bin("swaymsg", swaymsg, || {
with_var("WAYLAND_DISPLAY", Some("wayland-0"), || {
with_var("DISPLAY", None::<&str>, || {
with_var("GDK_BACKEND", None::<&str>, || {
let _ = MonitorWindow::new(1);
});
});
});
});
}
#[test]
#[serial]
fn monitor_window_new_covers_wayland_hyprctl_fallback_branch() {
let hyprctl = r#"#!/usr/bin/env sh
if [ "$1" = "version" ]; then
echo 'Hyprland test'
exit 0
fi
if [ "$1" = "dispatch" ]; then
exit 0
fi
exit 0
"#;
with_fake_bin("hyprctl", hyprctl, || {
with_var("WAYLAND_DISPLAY", Some("wayland-0"), || {
with_var("DISPLAY", None::<&str>, || {
with_var("GDK_BACKEND", None::<&str>, || {
let _ = MonitorWindow::new(0);
});
});
});
});
}
#[test]
#[serial]
fn monitor_window_new_covers_display_wmctrl_branch() {
let wmctrl = r#"#!/usr/bin/env sh
exit 0
"#;
with_fake_bin("wmctrl", wmctrl, || {
with_var("WAYLAND_DISPLAY", None::<&str>, || {
with_var("DISPLAY", Some(":99"), || {
with_var("GDK_BACKEND", None::<&str>, || {
let _ = MonitorWindow::new(0);
});
});
});
});
}
#[test]
fn push_packet_sets_pts_on_appsrc_buffers() {
gst::init().ok();
let pipeline = gst::Pipeline::new();
let src = gst::ElementFactory::make("appsrc")
.build()
.expect("appsrc")
.downcast::<gst_app::AppSrc>()
.expect("downcast appsrc");
pipeline
.add(src.upcast_ref::<gst::Element>())
.expect("add appsrc");
let window = MonitorWindow {
_pipeline: pipeline,
src,
};
window.push_packet(VideoPacket {
id: 1,
pts: 12_345,
data: vec![0, 0, 0, 1, 0x65],
..Default::default()
});
}
#[test]
fn drop_is_safe_for_manually_built_window() {
gst::init().ok();
let pipeline = gst::Pipeline::new();
let src = gst::ElementFactory::make("appsrc")
.build()
.expect("appsrc")
.downcast::<gst_app::AppSrc>()
.expect("downcast appsrc");
pipeline
.add(src.upcast_ref::<gst::Element>())
.expect("add appsrc");
let window = MonitorWindow {
_pipeline: pipeline,
src,
};
drop(window);
}
#[test]
fn unified_monitor_window_constructor_and_push_are_stable() {
match UnifiedMonitorWindow::new() {
Ok(window) => {
window.push_packet(VideoPacket {
id: 0,
pts: 100,
data: vec![0, 0, 0, 1, 0x65],
..Default::default()
});
window.push_packet(VideoPacket {
id: 1,
pts: 101,
data: vec![0, 0, 0, 1, 0x67],
..Default::default()
});
}
Err(err) => {
assert!(
!err.to_string().trim().is_empty(),
"unified constructor returned an empty error"
);
}
}
}
#[test]
fn unified_drop_is_safe_for_manually_built_window() {
gst::init().ok();
let pipeline = gst::Pipeline::new();
let left_src = gst::ElementFactory::make("appsrc")
.build()
.expect("left appsrc")
.downcast::<gst_app::AppSrc>()
.expect("downcast left appsrc");
let right_src = gst::ElementFactory::make("appsrc")
.build()
.expect("right appsrc")
.downcast::<gst_app::AppSrc>()
.expect("downcast right appsrc");
pipeline
.add(left_src.upcast_ref::<gst::Element>())
.expect("add left appsrc");
pipeline
.add(right_src.upcast_ref::<gst::Element>())
.expect("add right appsrc");
let window = UnifiedMonitorWindow {
pipeline,
left_src,
right_src,
};
drop(window);
}
}