diff --git a/server/src/bin/lesavka-uvc.rs b/server/src/bin/lesavka-uvc.rs index 03c11ed..681d821 100644 --- a/server/src/bin/lesavka-uvc.rs +++ b/server/src/bin/lesavka-uvc.rs @@ -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; diff --git a/testing/Cargo.toml b/testing/Cargo.toml index 4a54cd0..195c566 100644 --- a/testing/Cargo.toml +++ b/testing/Cargo.toml @@ -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" } diff --git a/testing/build.rs b/testing/build.rs new file mode 100644 index 0000000..0972f79 --- /dev/null +++ b/testing/build.rs @@ -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() + ); +} diff --git a/testing/tests/common_cli_binary_contract.rs b/testing/tests/common_cli_binary_contract.rs new file mode 100644 index 0000000..d051100 --- /dev/null +++ b/testing/tests/common_cli_binary_contract.rs @@ -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(); + } +} diff --git a/testing/tests/server_uvc_binary_contract.rs b/testing/tests/server_uvc_binary_contract.rs new file mode 100644 index 0000000..5964e26 --- /dev/null +++ b/testing/tests/server_uvc_binary_contract.rs @@ -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::::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::(b'V', 89); + let w = ioctl_write::(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()); + } +}