2026-04-13 02:52:32 -03:00
|
|
|
//! 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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-21 15:34:46 -03:00
|
|
|
#[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());
|
|
|
|
|
});
|
2026-04-23 07:00:06 -03:00
|
|
|
#[cfg(coverage)]
|
|
|
|
|
with_var("LESAVKA_TEST_DISABLE_H264_DECODERS", Some("1"), || {
|
|
|
|
|
with_var("LESAVKA_H264_DECODER", None::<&str>, || {
|
|
|
|
|
assert_eq!(pick_h264_decoder(), "decodebin");
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-04-21 15:34:46 -03:00
|
|
|
assert!(buildable_decoder("fakesink"));
|
|
|
|
|
assert!(!buildable_decoder("definitely-not-a-real-gst-element"));
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 02:52:32 -03:00
|
|
|
#[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],
|
2026-04-16 21:18:34 -03:00
|
|
|
..Default::default()
|
2026-04-13 02:52:32 -03:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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],
|
2026-04-16 21:18:34 -03:00
|
|
|
..Default::default()
|
2026-04-13 02:52:32 -03:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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);
|
|
|
|
|
}
|
2026-04-14 02:34:14 -03:00
|
|
|
|
|
|
|
|
#[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],
|
2026-04-16 21:18:34 -03:00
|
|
|
..Default::default()
|
2026-04-14 02:34:14 -03:00
|
|
|
});
|
|
|
|
|
window.push_packet(VideoPacket {
|
|
|
|
|
id: 1,
|
|
|
|
|
pts: 101,
|
|
|
|
|
data: vec![0, 0, 0, 1, 0x67],
|
2026-04-16 21:18:34 -03:00
|
|
|
..Default::default()
|
2026-04-14 02:34:14 -03:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
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 {
|
2026-04-14 04:02:39 -03:00
|
|
|
pipeline,
|
2026-04-14 02:34:14 -03:00
|
|
|
left_src,
|
|
|
|
|
right_src,
|
|
|
|
|
};
|
|
|
|
|
drop(window);
|
|
|
|
|
}
|
2026-04-13 02:52:32 -03:00
|
|
|
}
|