//! Extra coverage for `lesavka-uvc` control/error branches. //! //! Scope: keep additive branch tests in a separate file so each testing module //! remains under the 500 LOC contract. //! Targets: `server/src/bin/lesavka-uvc.rs`. //! Why: preserve expanded UVC branch coverage while satisfying test module contracts. mod uvc_binary_extra { #![allow(warnings)] #![allow(clippy::all)] #![allow(dead_code)] #![allow(unused_imports)] #![allow(unused_variables)] include!(env!("LESAVKA_SERVER_UVC_BIN_SRC")); use serial_test::serial; use std::fs; use std::path::PathBuf; use temp_env::with_var; use tempfile::NamedTempFile; fn sample_cfg() -> UvcConfig { UvcConfig { width: 1280, height: 720, fps: 25, interval: 400_000, max_packet: 1024, frame_size: 1_843_200, } } fn sample_interfaces() -> UvcInterfaces { UvcInterfaces { control: UVC_STRING_CONTROL_IDX, streaming: UVC_STRING_STREAMING_IDX, } } #[test] fn handle_setup_stalls_non_streaming_set_cur_and_non_in_requests() { let interfaces = sample_interfaces(); let mut state = UvcState::new(sample_cfg()); let mut pending = None; let set_cur_other_iface = UsbCtrlRequest { b_request_type: 0x00, b_request: UVC_SET_CUR, w_value: (0xFEu16) << 8, w_index: 0x00FF, w_length: 8, }; handle_setup( -1, 0, &mut state, &mut pending, interfaces, set_cur_other_iface, true, ); assert!(pending.is_none()); let non_in_non_set_cur = UsbCtrlRequest { b_request_type: 0x00, b_request: UVC_GET_CUR, w_value: (UVC_VS_PROBE_CONTROL as u16) << 8, w_index: interfaces.streaming as u16, w_length: 8, }; handle_setup( -1, 0, &mut state, &mut pending, interfaces, non_in_non_set_cur, true, ); assert!(pending.is_none()); } #[test] fn handle_setup_rejects_oversized_set_cur_payload() { let interfaces = sample_interfaces(); let mut state = UvcState::new(sample_cfg()); let mut pending = None; let oversized = UsbCtrlRequest { b_request_type: 0x00, b_request: UVC_SET_CUR, w_value: (UVC_VS_PROBE_CONTROL as u16) << 8, w_index: interfaces.streaming as u16, w_length: (UVC_DATA_SIZE as u16).saturating_add(1), }; handle_setup(-1, 0, &mut state, &mut pending, interfaces, oversized, true); assert!(pending.is_none()); } #[test] fn handle_setup_stalls_unknown_in_selector() { let interfaces = sample_interfaces(); let mut state = UvcState::new(sample_cfg()); let mut pending = None; let req = UsbCtrlRequest { b_request_type: USB_DIR_IN, b_request: UVC_GET_CUR, w_value: (0xFEu16) << 8, w_index: interfaces.streaming as u16, w_length: 8, }; handle_setup(-1, 0, &mut state, &mut pending, interfaces, req, true); assert!(pending.is_none()); } #[test] fn handle_data_ignores_missing_pending_and_negative_lengths() { let interfaces = sample_interfaces(); let mut state = UvcState::new(sample_cfg()); let mut pending = None; handle_data( -1, 0, &mut state, &mut pending, interfaces, UvcRequestData { length: 8, data: [0u8; UVC_DATA_SIZE], }, true, ); pending = Some(PendingRequest { interface: interfaces.streaming, selector: UVC_VS_PROBE_CONTROL, expected_len: STREAM_CTRL_SIZE_11, }); handle_data( -1, 0, &mut state, &mut pending, interfaces, UvcRequestData { length: -1, data: [0u8; UVC_DATA_SIZE], }, true, ); assert!(pending.is_none()); } #[test] fn handle_data_ignores_non_streaming_pending_requests() { let interfaces = sample_interfaces(); let mut state = UvcState::new(sample_cfg()); let mut pending = Some(PendingRequest { interface: interfaces.control, selector: UVC_VS_PROBE_CONTROL, expected_len: STREAM_CTRL_SIZE_11, }); let mut payload = [0u8; UVC_DATA_SIZE]; payload[2] = 1; handle_data( -1, 0, &mut state, &mut pending, interfaces, UvcRequestData { length: STREAM_CTRL_SIZE_11 as i32, data: payload, }, true, ); assert!(pending.is_none()); assert_eq!(state.probe, state.default); } #[test] fn build_in_response_returns_none_for_unknown_selector() { let state = UvcState::new(sample_cfg()); let interfaces = sample_interfaces(); let response = build_in_response( &state, interfaces, interfaces.streaming, 0xFE, UVC_GET_CUR, 8, ); assert!(response.is_none()); } #[test] fn sanitize_streaming_control_keeps_defaults_for_short_payload() { let state = UvcState::new(sample_cfg()); let short = [0u8; 8]; let out = sanitize_streaming_control(&short, &state); assert_eq!(out, state.default); } #[test] fn control_length_updates_only_for_supported_changed_sizes() { let mut state = UvcState::new(sample_cfg()); let original = state.ctrl_len; maybe_update_ctrl_len(&mut state, 99, true); assert_eq!(state.ctrl_len, original); maybe_update_ctrl_len(&mut state, original as u16, true); assert_eq!(state.ctrl_len, original); let next = if original == STREAM_CTRL_SIZE_11 { STREAM_CTRL_SIZE_15 } else { STREAM_CTRL_SIZE_11 }; maybe_update_ctrl_len(&mut state, next as u16, true); assert_eq!(state.ctrl_len, next); } #[test] fn streaming_response_handles_info_commit_and_unknown_selectors() { let state = UvcState::new(sample_cfg()); assert_eq!( build_streaming_response(&state, UVC_VS_PROBE_CONTROL, UVC_GET_INFO), Some(vec![0x03]) ); assert_eq!( build_streaming_response(&state, UVC_VS_COMMIT_CONTROL, UVC_GET_CUR) .map(|payload| payload.len()), Some(state.ctrl_len) ); assert!(build_streaming_response(&state, 0xFE, UVC_GET_CUR).is_none()); } #[test] fn sanitize_streaming_control_accepts_set_bits_and_payload_limits() { let state = UvcState::new(sample_cfg()); let mut payload = [0u8; UVC_DATA_SIZE]; payload[2] = 1; payload[3] = 1; payload[4..8].copy_from_slice(&333_333u32.to_le_bytes()); payload[22..26].copy_from_slice(&(state.cfg.max_packet + 500).to_le_bytes()); let out = sanitize_streaming_control(&payload, &state); assert_eq!(out[2], 1); assert_eq!(out[3], 1); assert_eq!(read_le32(&out, 4), 333_333); assert_eq!(read_le32(&out, 22), state.cfg.max_packet); } #[test] fn sanitize_streaming_control_keeps_zero_fields_at_defaults() { let state = UvcState::new(sample_cfg()); let mut payload = [0u8; UVC_DATA_SIZE]; payload[2] = 0; payload[3] = 0; payload[4..8].copy_from_slice(&0u32.to_le_bytes()); payload[22..26].copy_from_slice(&0u32.to_le_bytes()); let out = sanitize_streaming_control(&payload, &state); assert_eq!(out[2], state.default[2]); assert_eq!(out[3], state.default[3]); assert_eq!(read_le32(&out, 4), read_le32(&state.default, 4)); assert_eq!(read_le32(&out, 22), read_le32(&state.default, 22)); } #[test] #[cfg(coverage)] fn parse_device_arg_accepts_flags_and_positional_paths() { assert_eq!( parse_device_arg(&["--device".to_string(), "/dev/video7".to_string()]), Some("/dev/video7".to_string()) ); assert_eq!( parse_device_arg(&["-d".to_string(), "/dev/video8".to_string()]), Some("/dev/video8".to_string()) ); assert_eq!( parse_device_arg(&["--debug".to_string(), "/dev/video9".to_string()]), Some("/dev/video9".to_string()) ); assert_eq!(parse_device_arg(&["--device".to_string()]), None); } #[test] fn handle_data_updates_commit_and_ignores_unknown_streaming_selector() { let interfaces = sample_interfaces(); let mut state = UvcState::new(sample_cfg()); let original_commit = state.commit; let mut payload = [0u8; UVC_DATA_SIZE]; payload[4..8].copy_from_slice(&222_222u32.to_le_bytes()); let mut pending = Some(PendingRequest { interface: interfaces.streaming, selector: UVC_VS_COMMIT_CONTROL, expected_len: STREAM_CTRL_SIZE_11, }); handle_data( -1, 0, &mut state, &mut pending, interfaces, UvcRequestData { length: STREAM_CTRL_SIZE_11 as i32, data: payload, }, true, ); assert!(pending.is_none()); assert_ne!(state.commit, original_commit); let after_commit = state.commit; let mut pending = Some(PendingRequest { interface: interfaces.streaming, selector: 0xFE, expected_len: STREAM_CTRL_SIZE_11, }); handle_data( -1, 0, &mut state, &mut pending, interfaces, UvcRequestData { length: STREAM_CTRL_SIZE_11 as i32, data: payload, }, true, ); assert!(pending.is_none()); assert_eq!(state.commit, after_commit); } #[test] fn io_helpers_return_none_for_empty_or_missing_input() { let empty = NamedTempFile::new().expect("tmp"); fs::write(empty.path(), "\n").expect("write empty"); assert_eq!(read_u32_first(empty.path().to_str().expect("path")), None); let missing = PathBuf::from(format!( "/tmp/lesavka-missing-fifo-{}-{}", std::process::id(), std::thread::current().name().unwrap_or("anon") )); assert_eq!(read_fifo_min(missing.to_str().expect("missing")), None); } #[test] #[serial] fn uvc_freshness_defaults_bound_live_video_backlog() { with_var("LESAVKA_UVC_BUFFER_COUNT", None::<&str>, || { with_var("LESAVKA_UVC_IDLE_PUMP_MS", None::<&str>, || { with_var("LESAVKA_UVC_FRAME_MAX_AGE_MS", None::<&str>, || { assert_eq!(uvc_buffer_count(), 2); assert_eq!(uvc_idle_pump_sleep(), std::time::Duration::from_millis(2)); assert_eq!( frame_spool_max_age(), Some(std::time::Duration::from_millis(1_000)) ); }); }); }); } #[test] #[serial] fn uvc_freshness_env_clamps_buffers_and_allows_disabling_frame_ttl() { with_var("LESAVKA_UVC_BUFFER_COUNT", Some("99"), || { with_var("LESAVKA_UVC_IDLE_PUMP_MS", Some("11"), || { with_var("LESAVKA_UVC_FRAME_MAX_AGE_MS", Some("0"), || { assert_eq!(uvc_buffer_count(), 8); assert_eq!(uvc_idle_pump_sleep(), std::time::Duration::from_millis(11)); assert_eq!(frame_spool_max_age(), None); }); }); }); } #[test] fn frame_spool_staleness_uses_mtime_and_respects_disabled_ttl() { let frame = NamedTempFile::new().expect("tmp frame"); fs::write(frame.path(), [0xff, 0xd8, 0xff, 0xd9]).expect("write frame"); assert!(!frame_spool_is_stale(frame.path(), None)); let missing = PathBuf::from("/tmp/lesavka-definitely-missing-frame.mjpg"); assert!(frame_spool_is_stale( missing.as_path(), Some(std::time::Duration::from_millis(1)) )); std::thread::sleep(std::time::Duration::from_millis(2)); assert!(frame_spool_is_stale( frame.path(), Some(std::time::Duration::from_millis(1)) )); } #[test] fn compute_payload_cap_clamps_limit_pct_bounds() { with_var("LESAVKA_UVC_MAXPAYLOAD_LIMIT", None::<&str>, || { with_var("LESAVKA_UVC_LIMIT_PCT", Some("0"), || { let cap = compute_payload_cap(false); if let Some(cap) = cap { assert!(cap.pct >= 1); } }); with_var("LESAVKA_UVC_LIMIT_PCT", Some("250"), || { let cap = compute_payload_cap(true); if let Some(cap) = cap { assert!(cap.pct <= 100); } }); }); } #[test] #[serial] fn main_returns_error_for_non_uvc_device_node() { with_var("LESAVKA_UVC_DEV", Some("/dev/null"), || { with_var("LESAVKA_UVC_BLOCKING", Some("1"), || { let result = main(); assert!( result.is_err(), "non-UVC node should fail during event subscribe" ); }); }); } }