lesavka/testing/tests/server_uvc_binary_extra_contract.rs

386 lines
12 KiB
Rust

//! 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]
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"
);
});
});
}
}