// 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. use lesavka_server::uvc_runtime::{pick_uvc_device, supervise_uvc_control}; use serial_test::serial; use std::fs; use std::os::unix::fs::PermissionsExt; use std::path::Path; use std::time::Duration; use temp_env::{with_var, with_vars}; use tempfile::tempdir; use tokio::runtime::Runtime; async fn wait_for_marker(marker: &Path, is_ready: impl Fn(&str) -> bool) -> String { let deadline = tokio::time::Instant::now() + Duration::from_secs(2); loop { if let Ok(contents) = fs::read_to_string(marker) { if is_ready(&contents) { return contents; } } assert!( tokio::time::Instant::now() < deadline, "helper marker was not written in time" ); tokio::time::sleep(Duration::from_millis(20)).await; } } #[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 \"$*\" >> '{}'\nsleep 0.05\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(); let calls = with_var("LESAVKA_UVC_DEV", Some("/dev/video-loop"), || { let rt = Runtime::new().expect("runtime"); rt.block_on(async { let supervisor = tokio::spawn(supervise_uvc_control(helper_path)); let calls = wait_for_marker(&marker, |contents| { contents.contains("--device /dev/video-loop") }) .await; supervisor.abort(); let _ = supervisor.await; tokio::time::sleep(Duration::from_millis(100)).await; calls }) }); assert!( calls.contains("--device /dev/video-loop"), "expected helper to receive device args, got: {calls}" ); } #[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 \"$*\" >> '{}'\nsleep 0.05\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(); let calls = with_vars( [ ("LESAVKA_UVC_DEV", Some("/dev/video-loop")), ("LESAVKA_UVC_RESTART_DELAY_MS", Some("25")), ], || { let rt = Runtime::new().expect("runtime"); rt.block_on(async { let supervisor = tokio::spawn(supervise_uvc_control(helper_path)); let calls = wait_for_marker(&marker, |contents| { contents .lines() .filter(|line| line.contains("--device /dev/video-loop")) .count() >= 2 }) .await; supervisor.abort(); let _ = supervisor.await; tokio::time::sleep(Duration::from_millis(100)).await; calls }) }, ); 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}" ); } #[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" ); }); } #[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()); }); }); }, ); }, ); } #[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()); }); }); }, ); }, ); } #[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"); assert!( err.to_string().contains("LESAVKA_UVC_DEV"), "error should mention explicit override escape hatch: {err:#}" ); assert!( err.to_string().contains("platform--video-index0"), "error should point operators at the gadget by-path node: {err:#}" ); }); }); }, ); }, ); }