//! 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::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}" ); } #[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}" ); } #[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:#}" ); }); }); }, ); }, ); }