//! 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] #[serial] fn uvc_config_from_env_applies_payload_caps_and_interval_defaults() { with_var("LESAVKA_UVC_WIDTH", Some("640"), || { with_var("LESAVKA_UVC_HEIGHT", Some("480"), || { with_var("LESAVKA_UVC_FPS", Some("30"), || { with_var("LESAVKA_UVC_INTERVAL", Some("0"), || { with_var("LESAVKA_UVC_MAXPAYLOAD_LIMIT", Some("300"), || { with_var("LESAVKA_UVC_MAXPACKET", Some("4096"), || { with_var("LESAVKA_UVC_BULK", Some("1"), || { let cfg = UvcConfig::from_env(); assert_eq!(cfg.width, 640); assert_eq!(cfg.height, 480); assert_eq!(cfg.fps, 30); assert_eq!(cfg.interval, 10_000_000 / 30); assert!(cfg.max_packet <= 300); assert!(cfg.max_packet <= 512); }); }); }); }); }); }); }); } #[test] #[serial] fn uvc_config_from_env_keeps_explicit_interval_and_non_bulk_cap() { with_var("LESAVKA_UVC_INTERVAL", Some("200000"), || { with_var("LESAVKA_UVC_MAXPAYLOAD_LIMIT", Some("1500"), || { with_var("LESAVKA_UVC_MAXPACKET", Some("1200"), || { with_var("LESAVKA_UVC_BULK", None::<&str>, || { let cfg = UvcConfig::from_env(); assert_eq!(cfg.interval, 200_000); assert_eq!(cfg.max_packet, 1024); }); }); }); }); } #[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] #[serial] fn uvc_control_open_mode_defaults_read_only_with_escape_hatch() { with_var("LESAVKA_UVC_CONTROL_READ_ONLY", None::<&str>, || { assert!(uvc_control_read_only()); }); for disabled in ["0", "false", "no", "off"] { with_var("LESAVKA_UVC_CONTROL_READ_ONLY", Some(disabled), || { assert!(!uvc_control_read_only()); }); } with_var("LESAVKA_UVC_CONTROL_READ_ONLY", Some("1"), || { assert!(uvc_control_read_only()); }); } #[test] fn interface_helpers_and_configfs_snapshot_are_stable_without_sysfs() { let tmp = NamedTempFile::new().expect("tmp"); fs::write(tmp.path(), "7\n").expect("write"); assert_eq!(read_interface(tmp.path().to_str().expect("path")), Some(7)); fs::write(tmp.path(), "bad\n").expect("write bad"); assert_eq!(read_interface(tmp.path().to_str().expect("path")), None); let interfaces = load_interfaces(); assert_eq!(interfaces.control, UVC_STRING_CONTROL_IDX); assert_eq!(interfaces.streaming, UVC_STRING_STREAMING_IDX); assert!(read_configfs_snapshot().is_none()); let mut state = UvcState::new(sample_cfg()); state.cfg_snapshot = Some(ConfigfsSnapshot { width: 640, height: 480, default_interval: 333_333, frame_interval: 333_333, maxpacket: 1024, maxburst: 0, }); log_configfs_snapshot(&mut state, "contract"); assert!(state.cfg_snapshot.is_some()); } #[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()); } }