2026-04-12 12:25:48 -03:00
|
|
|
//! Integration coverage for the UVC runtime supervision contract.
|
|
|
|
|
//!
|
|
|
|
|
//! Scope: exercise the long-running UVC helper supervisor in both successful
|
|
|
|
|
//! spawn and spawn-failure modes.
|
|
|
|
|
//! Targets: `server/src/uvc_runtime.rs`.
|
|
|
|
|
//! Why: the helper supervisor is operationally critical and should be covered
|
|
|
|
|
//! through top-level integration behavior, not only unit checks.
|
|
|
|
|
|
2026-04-13 02:52:32 -03:00
|
|
|
use lesavka_server::uvc_runtime::{pick_uvc_device, supervise_uvc_control};
|
2026-04-12 12:25:48 -03:00
|
|
|
use serial_test::serial;
|
|
|
|
|
use std::fs;
|
|
|
|
|
use std::os::unix::fs::PermissionsExt;
|
|
|
|
|
use std::time::Duration;
|
|
|
|
|
use temp_env::with_var;
|
|
|
|
|
use tempfile::tempdir;
|
|
|
|
|
use tokio::runtime::Runtime;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[serial]
|
|
|
|
|
fn supervise_uvc_control_starts_helper_when_device_is_set() {
|
|
|
|
|
let dir = tempdir().expect("create temp dir");
|
|
|
|
|
let marker = dir.path().join("marker.log");
|
|
|
|
|
let helper = dir.path().join("helper.sh");
|
|
|
|
|
fs::write(
|
|
|
|
|
&helper,
|
|
|
|
|
format!(
|
|
|
|
|
"#!/usr/bin/env bash\nset -euo pipefail\necho \"$*\" >> '{}'\n",
|
|
|
|
|
marker.display()
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
.expect("write helper script");
|
|
|
|
|
let mut perms = fs::metadata(&helper)
|
|
|
|
|
.expect("helper metadata")
|
|
|
|
|
.permissions();
|
|
|
|
|
perms.set_mode(0o755);
|
|
|
|
|
fs::set_permissions(&helper, perms).expect("chmod helper script");
|
|
|
|
|
|
|
|
|
|
let helper_path = helper.to_string_lossy().to_string();
|
|
|
|
|
with_var("LESAVKA_UVC_DEV", Some("/dev/video-loop"), || {
|
|
|
|
|
let rt = Runtime::new().expect("runtime");
|
|
|
|
|
let result = rt.block_on(async {
|
|
|
|
|
tokio::time::timeout(
|
|
|
|
|
Duration::from_millis(350),
|
|
|
|
|
supervise_uvc_control(helper_path),
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
});
|
|
|
|
|
assert!(result.is_err(), "supervisor should still be running");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let calls = fs::read_to_string(marker).expect("read helper marker");
|
|
|
|
|
assert!(
|
|
|
|
|
calls.contains("--device /dev/video-loop"),
|
|
|
|
|
"expected helper to receive device args, got: {calls}"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 15:04:19 -03:00
|
|
|
#[test]
|
|
|
|
|
#[serial]
|
|
|
|
|
fn supervise_uvc_control_restarts_helper_after_exit() {
|
|
|
|
|
let dir = tempdir().expect("create temp dir");
|
|
|
|
|
let marker = dir.path().join("marker.log");
|
|
|
|
|
let helper = dir.path().join("helper.sh");
|
|
|
|
|
fs::write(
|
|
|
|
|
&helper,
|
|
|
|
|
format!(
|
|
|
|
|
"#!/usr/bin/env bash\nset -euo pipefail\necho \"$*\" >> '{}'\nexit 0\n",
|
|
|
|
|
marker.display()
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
.expect("write helper script");
|
|
|
|
|
let mut perms = fs::metadata(&helper)
|
|
|
|
|
.expect("helper metadata")
|
|
|
|
|
.permissions();
|
|
|
|
|
perms.set_mode(0o755);
|
|
|
|
|
fs::set_permissions(&helper, perms).expect("chmod helper script");
|
|
|
|
|
|
|
|
|
|
let helper_path = helper.to_string_lossy().to_string();
|
|
|
|
|
with_var("LESAVKA_UVC_DEV", Some("/dev/video-loop"), || {
|
|
|
|
|
let rt = Runtime::new().expect("runtime");
|
|
|
|
|
let result = rt.block_on(async {
|
|
|
|
|
tokio::time::timeout(
|
|
|
|
|
Duration::from_millis(2_450),
|
|
|
|
|
supervise_uvc_control(helper_path),
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
});
|
|
|
|
|
assert!(result.is_err(), "supervisor should still be running");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let calls = fs::read_to_string(marker).expect("read helper marker");
|
|
|
|
|
let restart_count = calls
|
|
|
|
|
.lines()
|
|
|
|
|
.filter(|line| line.contains("--device /dev/video-loop"))
|
|
|
|
|
.count();
|
|
|
|
|
assert!(
|
|
|
|
|
restart_count >= 2,
|
|
|
|
|
"expected helper restart loop to run at least twice, got {restart_count}: {calls}"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 12:25:48 -03:00
|
|
|
#[test]
|
|
|
|
|
#[serial]
|
|
|
|
|
fn supervise_uvc_control_survives_missing_helper_binary() {
|
|
|
|
|
with_var("LESAVKA_UVC_DEV", Some("/dev/video-loop"), || {
|
|
|
|
|
let rt = Runtime::new().expect("runtime");
|
|
|
|
|
let result = rt.block_on(async {
|
|
|
|
|
tokio::time::timeout(
|
|
|
|
|
Duration::from_millis(350),
|
|
|
|
|
supervise_uvc_control(String::from("/definitely/missing/lesavka-uvc")),
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
});
|
|
|
|
|
assert!(
|
|
|
|
|
result.is_err(),
|
|
|
|
|
"supervisor should continue retrying forever"
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-04-13 02:52:32 -03:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[serial]
|
|
|
|
|
fn pick_uvc_device_prefers_controller_by_path_override_root() {
|
|
|
|
|
let dir = tempdir().expect("tempdir");
|
|
|
|
|
let sys_root = dir.path().join("sys");
|
|
|
|
|
let by_path = dir.path().join("v4l/by-path");
|
|
|
|
|
fs::create_dir_all(sys_root.join("class/udc/fake-ctrl.usb")).expect("create fake udc");
|
|
|
|
|
fs::create_dir_all(&by_path).expect("create by-path dir");
|
|
|
|
|
let expected = by_path.join("platform-fake-ctrl.usb-video-index0");
|
|
|
|
|
fs::write(&expected, "").expect("touch by-path node");
|
|
|
|
|
|
|
|
|
|
temp_env::with_var(
|
|
|
|
|
"LESAVKA_GADGET_SYSFS_ROOT",
|
|
|
|
|
Some(sys_root.to_string_lossy().to_string()),
|
|
|
|
|
|| {
|
|
|
|
|
temp_env::with_var(
|
|
|
|
|
"LESAVKA_UVC_BY_PATH_ROOT",
|
|
|
|
|
Some(by_path.to_string_lossy().to_string()),
|
|
|
|
|
|| {
|
|
|
|
|
temp_env::with_var("LESAVKA_UVC_SKIP_UDEV", Some("1"), || {
|
|
|
|
|
temp_env::with_var("LESAVKA_UVC_DEV", None::<&str>, || {
|
|
|
|
|
let picked = pick_uvc_device().expect("pick by-path device");
|
|
|
|
|
assert_eq!(picked, expected.to_string_lossy());
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 05:06:19 -03:00
|
|
|
#[test]
|
|
|
|
|
#[serial]
|
|
|
|
|
fn pick_uvc_device_uses_platform_by_path_when_controller_is_unknown() {
|
|
|
|
|
let dir = tempdir().expect("tempdir");
|
|
|
|
|
let sys_root = dir.path().join("sys");
|
|
|
|
|
let by_path = dir.path().join("v4l/by-path");
|
|
|
|
|
fs::create_dir_all(&sys_root).expect("create fake sys root");
|
|
|
|
|
fs::create_dir_all(&by_path).expect("create fake by-path root");
|
|
|
|
|
let expected = by_path.join("platform-fallback.usb-video-index0");
|
|
|
|
|
fs::write(&expected, "").expect("touch by-path node");
|
|
|
|
|
|
|
|
|
|
temp_env::with_var(
|
|
|
|
|
"LESAVKA_GADGET_SYSFS_ROOT",
|
|
|
|
|
Some(sys_root.to_string_lossy().to_string()),
|
|
|
|
|
|| {
|
|
|
|
|
temp_env::with_var(
|
|
|
|
|
"LESAVKA_UVC_BY_PATH_ROOT",
|
|
|
|
|
Some(by_path.to_string_lossy().to_string()),
|
|
|
|
|
|| {
|
|
|
|
|
temp_env::with_var("LESAVKA_UVC_SKIP_UDEV", Some("1"), || {
|
|
|
|
|
temp_env::with_var("LESAVKA_UVC_DEV", None::<&str>, || {
|
|
|
|
|
let picked = pick_uvc_device().expect("pick platform by-path device");
|
|
|
|
|
assert_eq!(picked, expected.to_string_lossy());
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 02:52:32 -03:00
|
|
|
#[test]
|
|
|
|
|
#[serial]
|
|
|
|
|
fn pick_uvc_device_errors_when_overrides_disable_all_discovery_paths() {
|
|
|
|
|
let dir = tempdir().expect("tempdir");
|
|
|
|
|
let sys_root = dir.path().join("sys");
|
|
|
|
|
let by_path = dir.path().join("v4l/by-path");
|
|
|
|
|
fs::create_dir_all(&sys_root).expect("create fake sys root");
|
|
|
|
|
fs::create_dir_all(&by_path).expect("create fake by-path root");
|
|
|
|
|
|
|
|
|
|
temp_env::with_var(
|
|
|
|
|
"LESAVKA_GADGET_SYSFS_ROOT",
|
|
|
|
|
Some(sys_root.to_string_lossy().to_string()),
|
|
|
|
|
|| {
|
|
|
|
|
temp_env::with_var(
|
|
|
|
|
"LESAVKA_UVC_BY_PATH_ROOT",
|
|
|
|
|
Some(by_path.to_string_lossy().to_string()),
|
|
|
|
|
|| {
|
|
|
|
|
temp_env::with_var("LESAVKA_UVC_SKIP_UDEV", Some("1"), || {
|
|
|
|
|
temp_env::with_var("LESAVKA_UVC_DEV", None::<&str>, || {
|
|
|
|
|
let err = pick_uvc_device().expect_err("missing paths should error");
|
2026-04-28 05:06:19 -03:00
|
|
|
assert!(
|
|
|
|
|
err.to_string().contains("LESAVKA_UVC_DEV"),
|
|
|
|
|
"error should mention explicit override escape hatch: {err:#}"
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
err.to_string().contains("platform-<udc>-video-index0"),
|
|
|
|
|
"error should point operators at the gadget by-path node: {err:#}"
|
|
|
|
|
);
|
2026-04-13 02:52:32 -03:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|