fn main() -> Result<()> { let (dev, _cfg) = parse_args()?; let _ = load_interfaces(); let _ = UvcConfig::from_env(); let _ = open_with_retry(&dev)?; anyhow::bail!("coverage harness: control loop disabled"); } #[cfg(coverage)] fn parse_args() -> Result<(String, UvcConfig)> { let args: Vec = env::args().skip(1).collect(); let dev = parse_device_arg(&args) .or_else(|| env::var("LESAVKA_UVC_DEV").ok()) .context("missing --device (or LESAVKA_UVC_DEV)")?; Ok((dev, UvcConfig::from_env())) } #[cfg(coverage)] fn parse_device_arg(args: &[String]) -> Option { args .windows(2) .find_map(|pair| (pair[0] == "--device" || pair[0] == "-d").then(|| pair[1].clone())) .or_else(|| { args.iter() .rev() .find(|arg| { arg.as_str() != "--device" && arg.as_str() != "-d" && arg.starts_with('/') }) .cloned() }) } #[cfg(coverage)] impl UvcConfig { fn from_env() -> Self { let width = env_u32("LESAVKA_UVC_WIDTH", 1280); let height = env_u32("LESAVKA_UVC_HEIGHT", 720); let fps = env_u32("LESAVKA_UVC_FPS", 30).max(1); let frame_size = uvc_frame_size_for_active_mode(width, height, fps); let interval = env_u32("LESAVKA_UVC_INTERVAL", 0); let bulk = uvc_bulk_transfer_enabled(); let mut max_packet = env_u32("LESAVKA_UVC_MAXPACKET", 1024); if let Some(limit) = compute_payload_cap(bulk).map(|cap| cap.limit) { max_packet = max_packet.min(limit); } max_packet = if bulk { max_packet.min(512) } else { max_packet.min(1024) }; let interval = if interval == 0 { 10_000_000 / fps } else { interval }; Self { width, height, fps, interval, max_packet, frame_size, } } } #[cfg(coverage)] impl UvcState { fn new(cfg: UvcConfig) -> Self { let ctrl_len = stream_ctrl_len(); let default = build_streaming_control(&cfg, ctrl_len); Self { cfg, ctrl_len, default, probe: default, commit: default, cfg_snapshot: None, } } } #[cfg(coverage)] fn load_interfaces() -> UvcInterfaces { let control = env_u8("LESAVKA_UVC_CTRL_INTF").unwrap_or(UVC_STRING_CONTROL_IDX); let streaming = env_u8("LESAVKA_UVC_STREAM_INTF").unwrap_or(UVC_STRING_STREAMING_IDX); UvcInterfaces { control, streaming } } #[cfg(coverage)] fn read_interface(path: &str) -> Option { std::fs::read_to_string(path) .ok() .and_then(|v| v.trim().parse::().ok()) } #[cfg(coverage)] fn open_with_retry(path: &str) -> Result { let read_only = uvc_control_read_only(); let mut opts = OpenOptions::new(); opts.read(true); if !read_only { opts.write(true); } if env::var("LESAVKA_UVC_BLOCKING").is_err() { opts.custom_flags(libc::O_NONBLOCK); } opts.open(path).with_context(|| format!("open {path}")) } #[cfg(coverage)] /// Keep coverage-mode UVC control opens read-only unless a test opts into writes. fn uvc_control_read_only() -> bool { env::var("LESAVKA_UVC_CONTROL_READ_ONLY") .ok() .map(|value| { let trimmed = value.trim(); !(trimmed.eq_ignore_ascii_case("0") || trimmed.eq_ignore_ascii_case("false") || trimmed.eq_ignore_ascii_case("no") || trimmed.eq_ignore_ascii_case("off")) }) .unwrap_or(true) } #[cfg(coverage)] /// Returns the bounded UVC request-buffer count used by the helper. /// /// Inputs: `LESAVKA_UVC_BUFFER_COUNT`. Outputs: a value clamped to the safe /// range accepted by the helper. Why: coverage contracts need the same backlog /// bound as the real UVC binary without opening a gadget device. fn uvc_buffer_count() -> u32 { env_u32("LESAVKA_UVC_BUFFER_COUNT", DEFAULT_UVC_BUFFER_COUNT).clamp(1, 8) } #[cfg(coverage)] /// Returns the idle frame-pump sleep interval used when no fresh frame is ready. /// /// Inputs: `LESAVKA_UVC_IDLE_PUMP_MS`. Outputs: a duration in milliseconds. /// Why: this value controls how quickly the UVC helper retries the freshness /// spool when browsers are already streaming. fn uvc_idle_pump_sleep() -> std::time::Duration { std::time::Duration::from_millis(env_u64( "LESAVKA_UVC_IDLE_PUMP_MS", DEFAULT_UVC_IDLE_PUMP_MS, )) } #[cfg(coverage)] /// Returns the frame queue pacing period for the negotiated UVC frame rate. /// /// Inputs: `LESAVKA_UVC_QUEUE_PACING` plus the active frame rate. Output: /// `None` when pacing is explicitly disabled. Why: the RCT-facing host must /// not be overfed faster than the advertised descriptor cadence. fn uvc_queue_period(fps: u32) -> Option { if !env_flag_enabled("LESAVKA_UVC_QUEUE_PACING", true) { return None; } let fps = fps.max(1); Some(std::time::Duration::from_nanos(1_000_000_000 / u64::from(fps))) } #[cfg(coverage)] /// Bound MJPEG frames before they enter the USB UVC helper. /// /// Inputs: UVC mode plus optional `LESAVKA_UVC_FRAME_MAX_BYTES` or /// `LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC`. Output: maximum accepted JPEG /// byte length. Why: coverage tests should lock the artifact-prevention budget /// that turns oversized UVC frames into freezes instead of grey half-frames. fn uvc_frame_max_bytes(cfg: UvcConfig) -> usize { if !uvc_frame_size_guard_enabled() { return MAX_MJPEG_FRAME_BYTES; } if let Some(limit) = env_u32_opt("LESAVKA_UVC_FRAME_MAX_BYTES") { return if limit == 0 { derived_uvc_frame_max_bytes(cfg) } else { (limit as usize).min(MAX_MJPEG_FRAME_BYTES) }; } derived_uvc_frame_max_bytes(cfg) } #[cfg(coverage)] fn derived_uvc_frame_max_bytes(cfg: UvcConfig) -> usize { derived_uvc_frame_max_bytes_for_fps(cfg.fps) } #[cfg(coverage)] fn derived_uvc_frame_max_bytes_for_fps(fps: u32) -> usize { let fps = fps.max(1); let budget_per_sec = env_u32( "LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC", DEFAULT_UVC_MJPEG_BUDGET_BYTES_PER_SEC, ) .max(1); let per_frame = (budget_per_sec / fps).max(64 * 1024); per_frame.min(MAX_MJPEG_FRAME_BYTES as u32) as usize } #[cfg(coverage)] fn env_flag_enabled(name: &str, default: bool) -> bool { env::var(name) .ok() .map(|value| { let trimmed = value.trim(); if trimmed.eq_ignore_ascii_case("0") || trimmed.eq_ignore_ascii_case("false") || trimmed.eq_ignore_ascii_case("no") || trimmed.eq_ignore_ascii_case("off") { false } else if trimmed.eq_ignore_ascii_case("1") || trimmed.eq_ignore_ascii_case("true") || trimmed.eq_ignore_ascii_case("yes") || trimmed.eq_ignore_ascii_case("on") { true } else { default } }) .unwrap_or(default) } #[cfg(coverage)] fn uvc_frame_size_guard_enabled() -> bool { env_flag_enabled("LESAVKA_UVC_FRAME_SIZE_GUARD", true) } #[cfg(coverage)] fn uvc_bulk_transfer_enabled() -> bool { if !env_flag_enabled("LESAVKA_UVC_BULK", true) { return false; } let base = std::path::Path::new(CONFIGFS_UVC_BASE); if base.exists() && !base.join("streaming_bulk").exists() { return false; } true } #[cfg(coverage)] fn uvc_frame_size_for_active_mode(width: u32, height: u32, fps: u32) -> u32 { env_u32_opt("LESAVKA_UVC_FRAME_SIZE") .unwrap_or_else(|| derived_uvc_frame_max_bytes_for_fps(fps).min(u32::MAX as usize) as u32) .max((width.saturating_mul(height) / 32).max(64 * 1024)) } #[cfg(coverage)] fn uvc_stats_path() -> Option { match std::env::var("LESAVKA_UVC_STATS_PATH") { Ok(value) => { let trimmed = value.trim(); if trimmed.is_empty() || trimmed == "0" { None } else { Some(std::path::PathBuf::from(trimmed)) } } Err(_) => Some(std::path::PathBuf::from(DEFAULT_UVC_STATS_PATH)), } } #[cfg(coverage)] fn write_atomic_text(path: &std::path::Path, text: &str) -> Result<()> { if let Some(parent) = path.parent() { std::fs::create_dir_all(parent)?; } let tmp = path.with_extension(format!("tmp.{}", std::process::id())); std::fs::write(&tmp, text)?; std::fs::rename(&tmp, path)?; Ok(()) } #[cfg(coverage)] fn uvc_stats_snapshot_json(stats: &UvcVideoStats, frame_cap: usize) -> String { format!( "{{\"queued\":{},\"reloaded\":{},\"stale_replay\":{},\"rejected_oversize\":{},\"rejected_invalid\":{},\"fallback_idle\":{},\"latest_bytes\":{},\"frame_cap\":{},\"paced_sleeps\":{},\"paced_sleep_ms\":{}}}\n", stats.queued, stats.reloaded, stats.replayed_stale, stats.rejected_oversize, stats.rejected_invalid, stats.fallback_idle, stats.latest_bytes, frame_cap, stats.paced_sleeps, stats.paced_sleep_ms ) } #[cfg(coverage)] fn uvc_stats_interval() -> Option { match env_u64("LESAVKA_UVC_STATS_INTERVAL_MS", DEFAULT_UVC_STATS_INTERVAL_MS) { 0 => None, value => Some(std::time::Duration::from_millis(value)), } } #[cfg(coverage)] /// Returns the optional maximum age for a spooled MJPEG frame. /// /// Inputs: `LESAVKA_UVC_FRAME_MAX_AGE_MS`. Outputs: `None` when the TTL is /// disabled. Why: stale-frame replay must be explicit because it trades /// smoothness against freshness during browser stream recovery. fn frame_spool_max_age() -> Option { match env_u64( "LESAVKA_UVC_FRAME_MAX_AGE_MS", DEFAULT_UVC_FRAME_MAX_AGE_MS, ) { 0 => None, value => Some(std::time::Duration::from_millis(value)), } } #[cfg(coverage)] /// Determines whether a spooled MJPEG frame is too old to replay. /// /// Inputs: frame path and optional age limit. Outputs: true when the frame is /// missing or older than the limit. Why: the coverage harness should guard the /// same freshness cut-off that prevents seconds-old video from re-entering UVC. fn frame_spool_is_stale(path: &std::path::Path, max_age: Option) -> bool { let Some(max_age) = max_age else { return false; }; let Ok(metadata) = std::fs::metadata(path) else { return true; }; let Ok(modified) = metadata.modified() else { return false; }; std::time::SystemTime::now() .duration_since(modified) .map(|age| age > max_age) .unwrap_or(false) } #[cfg(coverage)] /// Parses an unsigned 64-bit environment value with a fallback. /// /// Inputs: variable name and default value. Outputs: parsed value or default. /// Why: UVC freshness settings use millisecond durations that should not be /// truncated to 32-bit just because the coverage harness is lightweight. fn env_u64(name: &str, default: u64) -> u64 { env::var(name) .ok() .and_then(|value| value.parse::().ok()) .unwrap_or(default) }