test(gate): cover uvc/common binaries and fix selector routing

This commit is contained in:
Brad Stein 2026-04-12 19:18:18 -03:00
parent 7c1f441387
commit 00606c0b60
5 changed files with 395 additions and 11 deletions

View File

@ -520,21 +520,20 @@ fn handle_setup(
}
fn map_interface(raw: u8, selector: u8, interfaces: UvcInterfaces, debug: bool) -> u8 {
let mapped = if matches!(selector, UVC_VS_PROBE_CONTROL | UVC_VS_COMMIT_CONTROL) {
let mapped = if selector == UVC_VS_PROBE_CONTROL {
interfaces.streaming
} else if selector == UVC_VC_REQUEST_ERROR_CODE_CONTROL {
interfaces.control
} else if raw == interfaces.streaming || raw == interfaces.control {
raw
} else if selector == UVC_VS_COMMIT_CONTROL || selector == UVC_VC_REQUEST_ERROR_CODE_CONTROL {
if raw == interfaces.control {
interfaces.control
} else {
interfaces.streaming
}
} else {
raw
};
if debug && mapped != raw {
eprintln!(
"[lesavka-uvc] remapped interface {} -> {} for selector {selector}",
raw, mapped
);
eprintln!("[lesavka-uvc] remapped interface {raw} -> {mapped} for selector {selector}");
}
mapped
@ -1009,12 +1008,10 @@ const IOC_NRBITS: u8 = 8;
const IOC_TYPEBITS: u8 = 8;
const IOC_SIZEBITS: u8 = 14;
const IOC_DIRBITS: u8 = 2;
const IOC_NRSHIFT: u8 = 0;
const IOC_TYPESHIFT: u8 = IOC_NRSHIFT + IOC_NRBITS;
const IOC_SIZESHIFT: u8 = IOC_TYPESHIFT + IOC_TYPEBITS;
const IOC_DIRSHIFT: u8 = IOC_SIZESHIFT + IOC_SIZEBITS;
const IOC_READ: u8 = 2;
const IOC_WRITE: u8 = 1;

View File

@ -3,12 +3,14 @@ name = "lesavka_testing"
version = "0.1.0"
edition = "2024"
publish = false
build = "build.rs"
[lib]
name = "lesavka_testing"
path = "src/lib.rs"
[dev-dependencies]
anyhow = "1.0"
evdev = "0.13"
libc = "0.2"
lesavka_client = { path = "../client" }

24
testing/build.rs Normal file
View File

@ -0,0 +1,24 @@
use std::path::PathBuf;
fn main() {
let manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect("manifest dir"));
let workspace_dir = manifest_dir.parent().expect("workspace dir");
let server_uvc = workspace_dir
.join("server/src/bin/lesavka-uvc.rs")
.canonicalize()
.expect("canonical server uvc bin path");
let common_cli = workspace_dir
.join("common/src/bin/cli.rs")
.canonicalize()
.expect("canonical common cli bin path");
println!(
"cargo:rustc-env=LESAVKA_SERVER_UVC_BIN_SRC={}",
server_uvc.display()
);
println!(
"cargo:rustc-env=LESAVKA_COMMON_CLI_BIN_SRC={}",
common_cli.display()
);
}

View File

@ -0,0 +1,14 @@
//! Integration coverage for the common CLI binary entrypoint.
//!
//! Scope: include the common CLI bin source and execute `main` directly.
//! Targets: `common/src/bin/cli.rs`.
//! Why: keep binary entrypoint coverage in the centralized testing crate.
mod common_cli_binary {
include!(env!("LESAVKA_COMMON_CLI_BIN_SRC"));
#[test]
fn cli_main_executes_without_panicking() {
main();
}
}

View File

@ -0,0 +1,347 @@
//! Integration coverage for the `lesavka-uvc` binary control-path contracts.
//!
//! Scope: include the production UVC binary source in the centralized testing
//! crate and exercise selector routing, payload shaping, env parsing, and I/O
//! helper behavior.
//! Targets: `server/src/bin/lesavka-uvc.rs`.
//! Why: this improves coverage for the operational UVC binary while keeping
//! source-file hygiene baselines stable.
mod uvc_binary {
#![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 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 build_streaming_control_populates_core_fields_for_11_and_15_byte_profiles() {
let cfg = sample_cfg();
let ctrl_11 = build_streaming_control(&cfg, STREAM_CTRL_SIZE_11);
assert_eq!(read_le32(&ctrl_11, 4), cfg.interval);
assert_eq!(read_le32(&ctrl_11, 18), cfg.frame_size);
assert_eq!(read_le32(&ctrl_11, 22), cfg.max_packet);
assert_eq!(ctrl_11[30], 0);
let ctrl_15 = build_streaming_control(&cfg, STREAM_CTRL_SIZE_15);
assert_eq!(read_le32(&ctrl_15, 4), cfg.interval);
assert_eq!(read_le32(&ctrl_15, 18), cfg.frame_size);
assert_eq!(read_le32(&ctrl_15, 22), cfg.max_packet);
assert_eq!(read_le32(&ctrl_15, 26), 48_000_000);
assert_eq!(ctrl_15[30], 0x03);
}
#[test]
#[serial]
fn stream_ctrl_len_accepts_only_supported_sizes() {
with_var("LESAVKA_UVC_CTRL_LEN", Some("26"), || {
assert_eq!(stream_ctrl_len(), STREAM_CTRL_SIZE_11);
});
with_var("LESAVKA_UVC_CTRL_LEN", Some("34"), || {
assert_eq!(stream_ctrl_len(), STREAM_CTRL_SIZE_15);
});
with_var("LESAVKA_UVC_CTRL_LEN", Some("99"), || {
assert_eq!(stream_ctrl_len(), STREAM_CTRL_SIZE_11);
});
}
#[test]
fn maybe_update_ctrl_len_rebuilds_state_profiles() {
let mut state = UvcState::new(sample_cfg());
assert_eq!(state.ctrl_len, STREAM_CTRL_SIZE_15);
maybe_update_ctrl_len(&mut state, STREAM_CTRL_SIZE_11 as u16, false);
assert_eq!(state.ctrl_len, STREAM_CTRL_SIZE_11);
assert_eq!(state.probe[2], 1);
assert_eq!(state.commit[3], 1);
}
#[test]
fn sanitize_streaming_control_applies_supported_fields() {
let state = UvcState::new(sample_cfg());
let mut data = [0u8; STREAM_CTRL_SIZE_MAX];
data[2] = 1;
data[3] = 1;
write_le32(&mut data[4..8], 333_333);
write_le32(&mut data[22..26], 4096);
let out = sanitize_streaming_control(&data, &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 handle_data_updates_probe_and_commit_requests() {
let interfaces = sample_interfaces();
let mut state = UvcState::new(sample_cfg());
let mut pending = Some(PendingRequest {
interface: interfaces.streaming,
selector: UVC_VS_PROBE_CONTROL,
expected_len: STREAM_CTRL_SIZE_11,
});
let mut payload = [0u8; UVC_DATA_SIZE];
payload[2] = 1;
payload[3] = 1;
write_le32(&mut payload[4..8], 250_000);
write_le32(&mut payload[22..26], 2048);
handle_data(
-1,
0,
&mut state,
&mut pending,
interfaces,
UvcRequestData {
length: STREAM_CTRL_SIZE_11 as i32,
data: payload,
},
false,
);
assert!(pending.is_none());
assert_eq!(read_le32(&state.probe, 4), 250_000);
assert_eq!(read_le32(&state.probe, 22), state.cfg.max_packet);
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,
},
false,
);
assert_eq!(read_le32(&state.commit, 4), 250_000);
}
#[test]
fn map_interface_prefers_selector_specific_routes() {
let interfaces = sample_interfaces();
assert_eq!(
map_interface(interfaces.control, UVC_VS_PROBE_CONTROL, interfaces, false),
interfaces.streaming
);
assert_eq!(
map_interface(
interfaces.control,
UVC_VC_REQUEST_ERROR_CODE_CONTROL,
interfaces,
false
),
interfaces.control
);
assert_eq!(
map_interface(
interfaces.streaming,
UVC_VC_REQUEST_ERROR_CODE_CONTROL,
interfaces,
false
),
interfaces.streaming
);
assert_eq!(
map_interface(interfaces.streaming, 0xff, interfaces, false),
interfaces.streaming
);
}
#[test]
fn build_in_response_and_streaming_response_return_expected_shapes() {
let state = UvcState::new(sample_cfg());
let interfaces = sample_interfaces();
let cur = build_streaming_response(&state, UVC_VS_PROBE_CONTROL, UVC_GET_CUR)
.expect("streaming GET_CUR");
assert_eq!(cur.len(), state.ctrl_len);
let info = build_streaming_response(&state, UVC_VS_COMMIT_CONTROL, UVC_GET_INFO)
.expect("streaming GET_INFO");
assert_eq!(info, vec![0x03]);
let none = build_streaming_response(&state, 0xff, UVC_GET_CUR);
assert!(none.is_none());
let built = build_in_response(
&state,
interfaces,
interfaces.streaming,
UVC_VS_PROBE_CONTROL,
UVC_GET_CUR,
8,
)
.expect("build in response");
assert_eq!(built.len(), 8);
}
#[test]
fn build_control_response_covers_supported_and_unknown_selectors() {
let info = build_control_response(UVC_VC_REQUEST_ERROR_CODE_CONTROL, UVC_GET_INFO)
.expect("control info");
assert_eq!(info, vec![0x03]);
let cur = build_control_response(UVC_VC_REQUEST_ERROR_CODE_CONTROL, UVC_GET_CUR)
.expect("control cur");
assert_eq!(cur, vec![0x00]);
let unk = build_control_response(0xff, UVC_GET_CUR).expect("unknown selector placeholder");
assert_eq!(unk, vec![0x00]);
assert!(build_control_response(0xff, 0x55).is_none());
}
#[test]
fn adjust_length_truncates_and_pads_payloads() {
assert_eq!(adjust_length(vec![1, 2, 3, 4], 2), vec![1, 2]);
assert_eq!(adjust_length(vec![1, 2], 4), vec![1, 2, 0, 0]);
assert_eq!(adjust_length(vec![1], 0), Vec::<u8>::new());
}
#[test]
fn parse_helpers_decode_usb_structures() {
let mut raw = [0u8; 64];
raw[0] = 0x81;
raw[1] = UVC_GET_CUR;
raw[2] = 0x34;
raw[3] = 0x12;
raw[4] = 0x78;
raw[5] = 0x56;
raw[6] = 0xBC;
raw[7] = 0x9A;
let req = parse_ctrl_request(raw);
assert_eq!(req.b_request_type, 0x81);
assert_eq!(req.b_request, UVC_GET_CUR);
assert_eq!(req.w_value, 0x1234);
assert_eq!(req.w_index, 0x5678);
assert_eq!(req.w_length, 0x9ABC);
let mut raw = [0u8; 64];
raw[0..4].copy_from_slice(&(26_i32.to_le_bytes()));
raw[4] = 0xAA;
raw[63] = 0xBB;
let data = parse_request_data(raw);
assert_eq!(data.length, 26);
assert_eq!(data.data[0], 0xAA);
assert_eq!(data.data[59], 0xBB);
}
#[test]
#[serial]
fn env_helpers_and_payload_cap_env_override_behave() {
with_var("LESAVKA_UVC_MAXPAYLOAD_LIMIT", Some("777"), || {
let cap = compute_payload_cap(false).expect("payload cap from env");
assert_eq!(cap.limit, 777);
assert_eq!(cap.source, "env");
assert_eq!(cap.pct, 100);
});
with_var("LESAVKA_TEST_U32", Some("42"), || {
assert_eq!(env_u32("LESAVKA_TEST_U32", 1), 42);
});
with_var("LESAVKA_TEST_U8", Some("9"), || {
assert_eq!(env_u8("LESAVKA_TEST_U8"), Some(9));
});
with_var("LESAVKA_TEST_U32_OPT", Some("77"), || {
assert_eq!(env_u32_opt("LESAVKA_TEST_U32_OPT"), Some(77));
});
}
#[test]
fn io_helpers_read_values_and_fifo_minimums() {
let tmp = NamedTempFile::new().expect("tmp");
fs::write(tmp.path(), "123\n").expect("write");
assert_eq!(read_u32_file(tmp.path().to_str().unwrap()), Some(123));
let tmp = NamedTempFile::new().expect("tmp");
fs::write(tmp.path(), "400000 500000 600000\n").expect("write");
assert_eq!(read_u32_first(tmp.path().to_str().unwrap()), Some(400000));
let tmp = NamedTempFile::new().expect("tmp");
fs::write(tmp.path(), "512, 256 1024\n").expect("write");
assert_eq!(read_fifo_min(tmp.path().to_str().unwrap()), Some(256));
}
#[test]
fn ioctl_helpers_produce_non_zero_numbers() {
let r = ioctl_read::<V4l2Event>(b'V', 89);
let w = ioctl_write::<UvcRequestData>(b'U', 1);
assert_ne!(r, 0);
assert_ne!(w, 0);
assert_ne!(r, w);
assert_ne!(ioc(IOC_READ, b'V', 1, 64), 0);
}
#[test]
fn open_with_retry_works_for_existing_temp_file() {
let tmp = NamedTempFile::new().expect("tmp");
let file = open_with_retry(tmp.path().to_str().unwrap()).expect("open with retry");
assert!(file.metadata().is_ok());
}
#[test]
fn send_response_and_stall_fail_with_invalid_fd() {
assert!(send_response(-1, 0, &[1, 2, 3]).is_err());
assert!(send_stall(-1, 0).is_err());
}
#[test]
fn handle_setup_tracks_streaming_set_cur_pending_request() {
let interfaces = sample_interfaces();
let mut state = UvcState::new(sample_cfg());
let mut pending = None;
let req = 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: STREAM_CTRL_SIZE_11 as u16,
};
handle_setup(-1, 0, &mut state, &mut pending, interfaces, req, false);
let pending = pending.expect("expected pending request");
assert_eq!(pending.interface, interfaces.streaming);
assert_eq!(pending.selector, UVC_VS_PROBE_CONTROL);
assert_eq!(pending.expected_len, STREAM_CTRL_SIZE_11);
}
#[test]
fn handle_setup_accepts_control_set_cur_without_pending() {
let interfaces = sample_interfaces();
let mut state = UvcState::new(sample_cfg());
let mut pending = None;
let req = UsbCtrlRequest {
b_request_type: 0x00,
b_request: UVC_SET_CUR,
w_value: (UVC_VC_REQUEST_ERROR_CODE_CONTROL as u16) << 8,
w_index: interfaces.control as u16,
w_length: 1,
};
handle_setup(-1, 0, &mut state, &mut pending, interfaces, req, false);
assert!(pending.is_none());
}
}