435 lines
14 KiB
Rust
435 lines
14 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]
|
|
#[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"
|
|
);
|
|
});
|
|
});
|
|
}
|
|
}
|