ci(lesavka): split uvc coverage startup ratchet
This commit is contained in:
parent
92b9aeecbd
commit
0d5284c9a2
@ -963,7 +963,7 @@
|
|||||||
"server/src/bin/lesavka-uvc.rs": {
|
"server/src/bin/lesavka-uvc.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
"doc_debt": 0,
|
"doc_debt": 0,
|
||||||
"loc": 19
|
"loc": 25
|
||||||
},
|
},
|
||||||
"server/src/bin/lesavka_synthetic_uplink/support.rs": {
|
"server/src/bin/lesavka_synthetic_uplink/support.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
@ -985,10 +985,25 @@
|
|||||||
"doc_debt": 0,
|
"doc_debt": 0,
|
||||||
"loc": 289
|
"loc": 289
|
||||||
},
|
},
|
||||||
|
"server/src/bin/lesavka_uvc/coverage_config.rs": {
|
||||||
|
"clippy_warnings": 0,
|
||||||
|
"doc_debt": 0,
|
||||||
|
"loc": 145
|
||||||
|
},
|
||||||
|
"server/src/bin/lesavka_uvc/coverage_frame_budget.rs": {
|
||||||
|
"clippy_warnings": 0,
|
||||||
|
"doc_debt": 0,
|
||||||
|
"loc": 127
|
||||||
|
},
|
||||||
"server/src/bin/lesavka_uvc/coverage_startup.rs": {
|
"server/src/bin/lesavka_uvc/coverage_startup.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
"doc_debt": 0,
|
"doc_debt": 0,
|
||||||
"loc": 447
|
"loc": 74
|
||||||
|
},
|
||||||
|
"server/src/bin/lesavka_uvc/coverage_stats.rs": {
|
||||||
|
"clippy_warnings": 0,
|
||||||
|
"doc_debt": 0,
|
||||||
|
"loc": 106
|
||||||
},
|
},
|
||||||
"server/src/bin/lesavka_uvc/payload_limits.rs": {
|
"server/src/bin/lesavka_uvc/payload_limits.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
|
|||||||
@ -360,9 +360,25 @@
|
|||||||
"line_percent": 100.0,
|
"line_percent": 100.0,
|
||||||
"loc": 162
|
"loc": 162
|
||||||
},
|
},
|
||||||
|
"server/src/bin/lesavka_uvc/coverage_config.rs": {
|
||||||
|
"line_percent": 100.0,
|
||||||
|
"loc": 145
|
||||||
|
},
|
||||||
|
"server/src/bin/lesavka_uvc/coverage_frame_budget.rs": {
|
||||||
|
"line_percent": 100.0,
|
||||||
|
"loc": 127
|
||||||
|
},
|
||||||
|
"server/src/bin/lesavka_uvc/coverage_model.rs": {
|
||||||
|
"line_percent": 96.97,
|
||||||
|
"loc": 289
|
||||||
|
},
|
||||||
"server/src/bin/lesavka_uvc/coverage_startup.rs": {
|
"server/src/bin/lesavka_uvc/coverage_startup.rs": {
|
||||||
"line_percent": 98.48,
|
"line_percent": 100.0,
|
||||||
"loc": 203
|
"loc": 74
|
||||||
|
},
|
||||||
|
"server/src/bin/lesavka_uvc/coverage_stats.rs": {
|
||||||
|
"line_percent": 95.65,
|
||||||
|
"loc": 106
|
||||||
},
|
},
|
||||||
"server/src/bin/lesavka_uvc/payload_limits.rs": {
|
"server/src/bin/lesavka_uvc/payload_limits.rs": {
|
||||||
"line_percent": 100.0,
|
"line_percent": 100.0,
|
||||||
|
|||||||
@ -8,6 +8,12 @@ include!("lesavka_uvc/coverage_model.rs");
|
|||||||
#[cfg(coverage)]
|
#[cfg(coverage)]
|
||||||
include!("lesavka_uvc/coverage_startup.rs");
|
include!("lesavka_uvc/coverage_startup.rs");
|
||||||
#[cfg(coverage)]
|
#[cfg(coverage)]
|
||||||
|
include!("lesavka_uvc/coverage_config.rs");
|
||||||
|
#[cfg(coverage)]
|
||||||
|
include!("lesavka_uvc/coverage_frame_budget.rs");
|
||||||
|
#[cfg(coverage)]
|
||||||
|
include!("lesavka_uvc/coverage_stats.rs");
|
||||||
|
#[cfg(coverage)]
|
||||||
include!("lesavka_uvc/control_requests.rs");
|
include!("lesavka_uvc/control_requests.rs");
|
||||||
#[cfg(coverage)]
|
#[cfg(coverage)]
|
||||||
include!("lesavka_uvc/control_payloads.rs");
|
include!("lesavka_uvc/control_payloads.rs");
|
||||||
|
|||||||
145
server/src/bin/lesavka_uvc/coverage_config.rs
Normal file
145
server/src/bin/lesavka_uvc/coverage_config.rs
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
#[cfg(coverage)]
|
||||||
|
impl UvcConfig {
|
||||||
|
/// Builds a bounded UVC mode from environment overrides.
|
||||||
|
///
|
||||||
|
/// Inputs: `LESAVKA_UVC_*` mode variables. Output: a packet-safe mode
|
||||||
|
/// snapshot. Why: coverage contracts must pin descriptor math without
|
||||||
|
/// requiring the real kernel gadget.
|
||||||
|
fn from_env() -> Self {
|
||||||
|
let width = env_u32("LESAVKA_UVC_WIDTH", 1280);
|
||||||
|
let height = env_u32("LESAVKA_UVC_HEIGHT", 720);
|
||||||
|
let fps = env_u32("LESAVKA_UVC_FPS", 30).max(1);
|
||||||
|
let interval = env_u32("LESAVKA_UVC_INTERVAL", 0);
|
||||||
|
let bulk = uvc_bulk_transfer_enabled();
|
||||||
|
let mut max_packet = env_u32("LESAVKA_UVC_MAXPACKET", 1024);
|
||||||
|
|
||||||
|
if let Some(limit) = compute_payload_cap(bulk).map(|cap| cap.limit) {
|
||||||
|
max_packet = max_packet.min(limit);
|
||||||
|
}
|
||||||
|
max_packet = if bulk {
|
||||||
|
max_packet.min(512)
|
||||||
|
} else {
|
||||||
|
max_packet.min(1024)
|
||||||
|
};
|
||||||
|
let frame_size = uvc_frame_size_for_active_mode(width, height, fps, max_packet, bulk);
|
||||||
|
|
||||||
|
let interval = if interval == 0 {
|
||||||
|
10_000_000 / fps
|
||||||
|
} else {
|
||||||
|
interval
|
||||||
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
fps,
|
||||||
|
interval,
|
||||||
|
max_packet,
|
||||||
|
frame_size,
|
||||||
|
bulk,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(coverage)]
|
||||||
|
impl UvcState {
|
||||||
|
/// Creates the control-state mirrors used by UVC request handling tests.
|
||||||
|
///
|
||||||
|
/// Inputs: one validated UVC mode. Output: default/probe/commit buffers.
|
||||||
|
/// Why: tests need deterministic state transitions for host negotiation.
|
||||||
|
fn new(cfg: UvcConfig) -> Self {
|
||||||
|
let ctrl_len = stream_ctrl_len();
|
||||||
|
let default = build_streaming_control(&cfg, ctrl_len);
|
||||||
|
Self {
|
||||||
|
cfg,
|
||||||
|
ctrl_len,
|
||||||
|
default,
|
||||||
|
probe: default,
|
||||||
|
commit: default,
|
||||||
|
cfg_snapshot: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(coverage)]
|
||||||
|
fn load_interfaces() -> UvcInterfaces {
|
||||||
|
let control = env_u8("LESAVKA_UVC_CTRL_INTF").unwrap_or(UVC_STRING_CONTROL_IDX);
|
||||||
|
let streaming = env_u8("LESAVKA_UVC_STREAM_INTF").unwrap_or(UVC_STRING_STREAMING_IDX);
|
||||||
|
UvcInterfaces { control, streaming }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(coverage)]
|
||||||
|
fn read_interface(path: &str) -> Option<u8> {
|
||||||
|
std::fs::read_to_string(path)
|
||||||
|
.ok()
|
||||||
|
.and_then(|v| v.trim().parse::<u8>().ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(coverage)]
|
||||||
|
/// Parses a boolean environment flag with a default fallback.
|
||||||
|
///
|
||||||
|
/// Inputs: variable name and default value. Output: parsed boolean. Why:
|
||||||
|
/// UVC safety switches must handle common true/false spellings consistently.
|
||||||
|
fn env_flag_enabled(name: &str, default: bool) -> bool {
|
||||||
|
env::var(name)
|
||||||
|
.ok()
|
||||||
|
.map(|value| {
|
||||||
|
let trimmed = value.trim();
|
||||||
|
if trimmed.eq_ignore_ascii_case("0")
|
||||||
|
|| trimmed.eq_ignore_ascii_case("false")
|
||||||
|
|| trimmed.eq_ignore_ascii_case("no")
|
||||||
|
|| trimmed.eq_ignore_ascii_case("off")
|
||||||
|
{
|
||||||
|
false
|
||||||
|
} else if trimmed.eq_ignore_ascii_case("1")
|
||||||
|
|| trimmed.eq_ignore_ascii_case("true")
|
||||||
|
|| trimmed.eq_ignore_ascii_case("yes")
|
||||||
|
|| trimmed.eq_ignore_ascii_case("on")
|
||||||
|
{
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
default
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or(default)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(coverage)]
|
||||||
|
/// Decides whether the coverage helper may use bulk transport.
|
||||||
|
///
|
||||||
|
/// Inputs: env override plus configfs capabilities. Output: effective mode.
|
||||||
|
/// Why: tests should not advertise bulk if the gadget tree lacks support.
|
||||||
|
fn uvc_bulk_transfer_enabled() -> bool {
|
||||||
|
if !env_flag_enabled("LESAVKA_UVC_BULK", true) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
uvc_bulk_transfer_enabled_for_base(std::path::Path::new(CONFIGFS_UVC_BASE))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(coverage)]
|
||||||
|
/// Checks whether a configfs UVC function exposes the bulk-streaming knob.
|
||||||
|
///
|
||||||
|
/// Inputs: configfs function root. Output: effective bulk capability. Why:
|
||||||
|
/// tests need this branch without depending on the host's real gadget tree.
|
||||||
|
fn uvc_bulk_transfer_enabled_for_base(base: &std::path::Path) -> bool {
|
||||||
|
if base.exists() && !base.join("streaming_bulk").exists() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(coverage)]
|
||||||
|
fn uvc_frame_size_for_active_mode(
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
fps: u32,
|
||||||
|
max_packet: u32,
|
||||||
|
bulk: bool,
|
||||||
|
) -> u32 {
|
||||||
|
env_u32_opt("LESAVKA_UVC_FRAME_SIZE")
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
derived_uvc_frame_max_bytes_for_transport(fps, max_packet, bulk)
|
||||||
|
.min(u32::MAX as usize) as u32
|
||||||
|
})
|
||||||
|
.max((width.saturating_mul(height) / 32).max(64 * 1024))
|
||||||
|
}
|
||||||
127
server/src/bin/lesavka_uvc/coverage_frame_budget.rs
Normal file
127
server/src/bin/lesavka_uvc/coverage_frame_budget.rs
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
#[cfg(coverage)]
|
||||||
|
/// Returns the bounded UVC request-buffer count used by the helper.
|
||||||
|
///
|
||||||
|
/// Inputs: `LESAVKA_UVC_BUFFER_COUNT`. Outputs: a value clamped to the safe
|
||||||
|
/// range accepted by the helper. Why: coverage contracts need the same backlog
|
||||||
|
/// bound as the real UVC binary without opening a gadget device.
|
||||||
|
fn uvc_buffer_count() -> u32 {
|
||||||
|
env_u32("LESAVKA_UVC_BUFFER_COUNT", DEFAULT_UVC_BUFFER_COUNT).clamp(1, 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(coverage)]
|
||||||
|
/// Returns the idle frame-pump sleep interval used when no fresh frame is ready.
|
||||||
|
///
|
||||||
|
/// Inputs: `LESAVKA_UVC_IDLE_PUMP_MS`. Outputs: a duration in milliseconds.
|
||||||
|
/// Why: this value controls how quickly the UVC helper retries the freshness
|
||||||
|
/// spool when browsers are already streaming.
|
||||||
|
fn uvc_idle_pump_sleep() -> std::time::Duration {
|
||||||
|
std::time::Duration::from_millis(env_u64(
|
||||||
|
"LESAVKA_UVC_IDLE_PUMP_MS",
|
||||||
|
DEFAULT_UVC_IDLE_PUMP_MS,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(coverage)]
|
||||||
|
/// Returns the frame queue pacing period for the negotiated UVC frame rate.
|
||||||
|
///
|
||||||
|
/// Inputs: `LESAVKA_UVC_QUEUE_PACING` plus the active frame rate. Output:
|
||||||
|
/// `None` when pacing is explicitly disabled. Why: the RCT-facing host must
|
||||||
|
/// not be overfed faster than the advertised descriptor cadence.
|
||||||
|
fn uvc_queue_period(fps: u32) -> Option<std::time::Duration> {
|
||||||
|
if !env_flag_enabled("LESAVKA_UVC_QUEUE_PACING", DEFAULT_UVC_QUEUE_PACING) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let fps = fps.max(1);
|
||||||
|
Some(std::time::Duration::from_nanos(1_000_000_000 / u64::from(fps)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(coverage)]
|
||||||
|
/// Bound MJPEG frames before they enter the USB UVC helper.
|
||||||
|
///
|
||||||
|
/// Inputs: UVC mode plus optional `LESAVKA_UVC_FRAME_MAX_BYTES` or
|
||||||
|
/// `LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC`. Output: maximum accepted JPEG
|
||||||
|
/// byte length. Why: coverage tests should lock the artifact-prevention budget
|
||||||
|
/// that turns oversized UVC frames into freezes instead of grey half-frames.
|
||||||
|
fn uvc_frame_max_bytes(cfg: UvcConfig) -> usize {
|
||||||
|
if !uvc_frame_size_guard_enabled() {
|
||||||
|
return MAX_MJPEG_FRAME_BYTES;
|
||||||
|
}
|
||||||
|
if let Some(limit) = env_u32_opt("LESAVKA_UVC_FRAME_MAX_BYTES") {
|
||||||
|
return if limit == 0 {
|
||||||
|
derived_uvc_frame_max_bytes(cfg)
|
||||||
|
} else {
|
||||||
|
(limit as usize).min(MAX_MJPEG_FRAME_BYTES)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
derived_uvc_frame_max_bytes(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(coverage)]
|
||||||
|
fn derived_uvc_frame_max_bytes(cfg: UvcConfig) -> usize {
|
||||||
|
derived_uvc_frame_max_bytes_for_transport(cfg.fps, cfg.max_packet, cfg.bulk)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(coverage)]
|
||||||
|
fn derived_uvc_frame_max_bytes_for_transport(fps: u32, max_packet: u32, bulk: bool) -> usize {
|
||||||
|
let fps = fps.max(1);
|
||||||
|
let budget_per_sec = effective_uvc_mjpeg_budget_bytes_per_sec(max_packet, bulk);
|
||||||
|
let per_frame = (budget_per_sec / fps).max(64 * 1024);
|
||||||
|
per_frame.min(MAX_MJPEG_FRAME_BYTES as u32) as usize
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(coverage)]
|
||||||
|
/// Returns the effective MJPEG byte budget for the selected USB transport.
|
||||||
|
///
|
||||||
|
/// Inputs: negotiated max packet size and bulk/isochronous mode. Output: bytes
|
||||||
|
/// per second. Why: frame-size checks should mirror the transport budget.
|
||||||
|
fn effective_uvc_mjpeg_budget_bytes_per_sec(max_packet: u32, bulk: bool) -> u32 {
|
||||||
|
let configured = env_u32(
|
||||||
|
"LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC",
|
||||||
|
DEFAULT_UVC_MJPEG_BUDGET_BYTES_PER_SEC,
|
||||||
|
)
|
||||||
|
.max(1);
|
||||||
|
if bulk {
|
||||||
|
return configured;
|
||||||
|
}
|
||||||
|
|
||||||
|
configured
|
||||||
|
.min(uvc_isochronous_budget_bytes_per_sec(max_packet))
|
||||||
|
.max(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(coverage)]
|
||||||
|
/// Computes the high-speed isochronous payload budget.
|
||||||
|
///
|
||||||
|
/// Inputs: endpoint packet size and `LESAVKA_UVC_ISOCHRONOUS_LIMIT_PCT`.
|
||||||
|
/// Output: capped bytes per second. Why: coverage locks the safety margin.
|
||||||
|
fn uvc_isochronous_budget_bytes_per_sec(max_packet: u32) -> u32 {
|
||||||
|
let pct = env_u32(
|
||||||
|
"LESAVKA_UVC_ISOCHRONOUS_LIMIT_PCT",
|
||||||
|
DEFAULT_UVC_ISOCHRONOUS_LIMIT_PCT,
|
||||||
|
)
|
||||||
|
.clamp(1, 100);
|
||||||
|
let bytes = u64::from(max_packet)
|
||||||
|
.saturating_mul(u64::from(HIGH_SPEED_ISOCHRONOUS_MICROFRAMES_PER_SEC))
|
||||||
|
.saturating_mul(u64::from(pct))
|
||||||
|
/ 100;
|
||||||
|
bytes.min(u64::from(u32::MAX)) as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(coverage)]
|
||||||
|
fn uvc_frame_size_guard_enabled() -> bool {
|
||||||
|
env_flag_enabled("LESAVKA_UVC_FRAME_SIZE_GUARD", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(coverage)]
|
||||||
|
/// Parses an unsigned 64-bit environment value with a fallback.
|
||||||
|
///
|
||||||
|
/// Inputs: variable name and default value. Outputs: parsed value or default.
|
||||||
|
/// Why: UVC freshness settings use millisecond durations that should not be
|
||||||
|
/// truncated to 32-bit just because the coverage harness is lightweight.
|
||||||
|
fn env_u64(name: &str, default: u64) -> u64 {
|
||||||
|
env::var(name)
|
||||||
|
.ok()
|
||||||
|
.and_then(|value| value.parse::<u64>().ok())
|
||||||
|
.unwrap_or(default)
|
||||||
|
}
|
||||||
@ -40,83 +40,6 @@ fn parse_device_arg(args: &[String]) -> Option<String> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(coverage)]
|
|
||||||
impl UvcConfig {
|
|
||||||
/// Builds a bounded UVC mode from environment overrides.
|
|
||||||
///
|
|
||||||
/// Inputs: `LESAVKA_UVC_*` mode variables. Output: a packet-safe mode
|
|
||||||
/// snapshot. Why: coverage contracts must pin descriptor math without
|
|
||||||
/// requiring the real kernel gadget.
|
|
||||||
fn from_env() -> Self {
|
|
||||||
let width = env_u32("LESAVKA_UVC_WIDTH", 1280);
|
|
||||||
let height = env_u32("LESAVKA_UVC_HEIGHT", 720);
|
|
||||||
let fps = env_u32("LESAVKA_UVC_FPS", 30).max(1);
|
|
||||||
let interval = env_u32("LESAVKA_UVC_INTERVAL", 0);
|
|
||||||
let bulk = uvc_bulk_transfer_enabled();
|
|
||||||
let mut max_packet = env_u32("LESAVKA_UVC_MAXPACKET", 1024);
|
|
||||||
|
|
||||||
if let Some(limit) = compute_payload_cap(bulk).map(|cap| cap.limit) {
|
|
||||||
max_packet = max_packet.min(limit);
|
|
||||||
}
|
|
||||||
max_packet = if bulk {
|
|
||||||
max_packet.min(512)
|
|
||||||
} else {
|
|
||||||
max_packet.min(1024)
|
|
||||||
};
|
|
||||||
let frame_size = uvc_frame_size_for_active_mode(width, height, fps, max_packet, bulk);
|
|
||||||
|
|
||||||
let interval = if interval == 0 {
|
|
||||||
10_000_000 / fps
|
|
||||||
} else {
|
|
||||||
interval
|
|
||||||
};
|
|
||||||
|
|
||||||
Self {
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
fps,
|
|
||||||
interval,
|
|
||||||
max_packet,
|
|
||||||
frame_size,
|
|
||||||
bulk,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(coverage)]
|
|
||||||
impl UvcState {
|
|
||||||
/// Creates the control-state mirrors used by UVC request handling tests.
|
|
||||||
///
|
|
||||||
/// Inputs: one validated UVC mode. Output: default/probe/commit buffers.
|
|
||||||
/// Why: tests need deterministic state transitions for host negotiation.
|
|
||||||
fn new(cfg: UvcConfig) -> Self {
|
|
||||||
let ctrl_len = stream_ctrl_len();
|
|
||||||
let default = build_streaming_control(&cfg, ctrl_len);
|
|
||||||
Self {
|
|
||||||
cfg,
|
|
||||||
ctrl_len,
|
|
||||||
default,
|
|
||||||
probe: default,
|
|
||||||
commit: default,
|
|
||||||
cfg_snapshot: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(coverage)]
|
|
||||||
fn load_interfaces() -> UvcInterfaces {
|
|
||||||
let control = env_u8("LESAVKA_UVC_CTRL_INTF").unwrap_or(UVC_STRING_CONTROL_IDX);
|
|
||||||
let streaming = env_u8("LESAVKA_UVC_STREAM_INTF").unwrap_or(UVC_STRING_STREAMING_IDX);
|
|
||||||
UvcInterfaces { control, streaming }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(coverage)]
|
|
||||||
fn read_interface(path: &str) -> Option<u8> {
|
|
||||||
std::fs::read_to_string(path)
|
|
||||||
.ok()
|
|
||||||
.and_then(|v| v.trim().parse::<u8>().ok())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(coverage)]
|
#[cfg(coverage)]
|
||||||
/// Opens the UVC control device with safe coverage defaults.
|
/// Opens the UVC control device with safe coverage defaults.
|
||||||
///
|
///
|
||||||
@ -149,299 +72,3 @@ fn uvc_control_read_only() -> bool {
|
|||||||
})
|
})
|
||||||
.unwrap_or(true)
|
.unwrap_or(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(coverage)]
|
|
||||||
/// Returns the bounded UVC request-buffer count used by the helper.
|
|
||||||
///
|
|
||||||
/// Inputs: `LESAVKA_UVC_BUFFER_COUNT`. Outputs: a value clamped to the safe
|
|
||||||
/// range accepted by the helper. Why: coverage contracts need the same backlog
|
|
||||||
/// bound as the real UVC binary without opening a gadget device.
|
|
||||||
fn uvc_buffer_count() -> u32 {
|
|
||||||
env_u32("LESAVKA_UVC_BUFFER_COUNT", DEFAULT_UVC_BUFFER_COUNT).clamp(1, 8)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(coverage)]
|
|
||||||
/// Returns the idle frame-pump sleep interval used when no fresh frame is ready.
|
|
||||||
///
|
|
||||||
/// Inputs: `LESAVKA_UVC_IDLE_PUMP_MS`. Outputs: a duration in milliseconds.
|
|
||||||
/// Why: this value controls how quickly the UVC helper retries the freshness
|
|
||||||
/// spool when browsers are already streaming.
|
|
||||||
fn uvc_idle_pump_sleep() -> std::time::Duration {
|
|
||||||
std::time::Duration::from_millis(env_u64(
|
|
||||||
"LESAVKA_UVC_IDLE_PUMP_MS",
|
|
||||||
DEFAULT_UVC_IDLE_PUMP_MS,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(coverage)]
|
|
||||||
/// Returns the frame queue pacing period for the negotiated UVC frame rate.
|
|
||||||
///
|
|
||||||
/// Inputs: `LESAVKA_UVC_QUEUE_PACING` plus the active frame rate. Output:
|
|
||||||
/// `None` when pacing is explicitly disabled. Why: the RCT-facing host must
|
|
||||||
/// not be overfed faster than the advertised descriptor cadence.
|
|
||||||
fn uvc_queue_period(fps: u32) -> Option<std::time::Duration> {
|
|
||||||
if !env_flag_enabled("LESAVKA_UVC_QUEUE_PACING", DEFAULT_UVC_QUEUE_PACING) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let fps = fps.max(1);
|
|
||||||
Some(std::time::Duration::from_nanos(1_000_000_000 / u64::from(fps)))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(coverage)]
|
|
||||||
/// Bound MJPEG frames before they enter the USB UVC helper.
|
|
||||||
///
|
|
||||||
/// Inputs: UVC mode plus optional `LESAVKA_UVC_FRAME_MAX_BYTES` or
|
|
||||||
/// `LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC`. Output: maximum accepted JPEG
|
|
||||||
/// byte length. Why: coverage tests should lock the artifact-prevention budget
|
|
||||||
/// that turns oversized UVC frames into freezes instead of grey half-frames.
|
|
||||||
fn uvc_frame_max_bytes(cfg: UvcConfig) -> usize {
|
|
||||||
if !uvc_frame_size_guard_enabled() {
|
|
||||||
return MAX_MJPEG_FRAME_BYTES;
|
|
||||||
}
|
|
||||||
if let Some(limit) = env_u32_opt("LESAVKA_UVC_FRAME_MAX_BYTES") {
|
|
||||||
return if limit == 0 {
|
|
||||||
derived_uvc_frame_max_bytes(cfg)
|
|
||||||
} else {
|
|
||||||
(limit as usize).min(MAX_MJPEG_FRAME_BYTES)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
derived_uvc_frame_max_bytes(cfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(coverage)]
|
|
||||||
fn derived_uvc_frame_max_bytes(cfg: UvcConfig) -> usize {
|
|
||||||
derived_uvc_frame_max_bytes_for_transport(cfg.fps, cfg.max_packet, cfg.bulk)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(coverage)]
|
|
||||||
fn derived_uvc_frame_max_bytes_for_transport(fps: u32, max_packet: u32, bulk: bool) -> usize {
|
|
||||||
let fps = fps.max(1);
|
|
||||||
let budget_per_sec = effective_uvc_mjpeg_budget_bytes_per_sec(max_packet, bulk);
|
|
||||||
let per_frame = (budget_per_sec / fps).max(64 * 1024);
|
|
||||||
per_frame.min(MAX_MJPEG_FRAME_BYTES as u32) as usize
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(coverage)]
|
|
||||||
/// Returns the effective MJPEG byte budget for the selected USB transport.
|
|
||||||
///
|
|
||||||
/// Inputs: negotiated max packet size and bulk/isochronous mode. Output: bytes
|
|
||||||
/// per second. Why: frame-size checks should mirror the transport budget.
|
|
||||||
fn effective_uvc_mjpeg_budget_bytes_per_sec(max_packet: u32, bulk: bool) -> u32 {
|
|
||||||
let configured = env_u32(
|
|
||||||
"LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC",
|
|
||||||
DEFAULT_UVC_MJPEG_BUDGET_BYTES_PER_SEC,
|
|
||||||
)
|
|
||||||
.max(1);
|
|
||||||
if bulk {
|
|
||||||
return configured;
|
|
||||||
}
|
|
||||||
|
|
||||||
configured
|
|
||||||
.min(uvc_isochronous_budget_bytes_per_sec(max_packet))
|
|
||||||
.max(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(coverage)]
|
|
||||||
/// Computes the high-speed isochronous payload budget.
|
|
||||||
///
|
|
||||||
/// Inputs: endpoint packet size and `LESAVKA_UVC_ISOCHRONOUS_LIMIT_PCT`.
|
|
||||||
/// Output: capped bytes per second. Why: coverage locks the safety margin.
|
|
||||||
fn uvc_isochronous_budget_bytes_per_sec(max_packet: u32) -> u32 {
|
|
||||||
let pct = env_u32(
|
|
||||||
"LESAVKA_UVC_ISOCHRONOUS_LIMIT_PCT",
|
|
||||||
DEFAULT_UVC_ISOCHRONOUS_LIMIT_PCT,
|
|
||||||
)
|
|
||||||
.clamp(1, 100);
|
|
||||||
let bytes = u64::from(max_packet)
|
|
||||||
.saturating_mul(u64::from(HIGH_SPEED_ISOCHRONOUS_MICROFRAMES_PER_SEC))
|
|
||||||
.saturating_mul(u64::from(pct))
|
|
||||||
/ 100;
|
|
||||||
bytes.min(u64::from(u32::MAX)) as u32
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(coverage)]
|
|
||||||
/// Parses a boolean environment flag with a default fallback.
|
|
||||||
///
|
|
||||||
/// Inputs: variable name and default value. Output: parsed boolean. Why:
|
|
||||||
/// UVC safety switches must handle common true/false spellings consistently.
|
|
||||||
fn env_flag_enabled(name: &str, default: bool) -> bool {
|
|
||||||
env::var(name)
|
|
||||||
.ok()
|
|
||||||
.map(|value| {
|
|
||||||
let trimmed = value.trim();
|
|
||||||
if trimmed.eq_ignore_ascii_case("0")
|
|
||||||
|| trimmed.eq_ignore_ascii_case("false")
|
|
||||||
|| trimmed.eq_ignore_ascii_case("no")
|
|
||||||
|| trimmed.eq_ignore_ascii_case("off")
|
|
||||||
{
|
|
||||||
false
|
|
||||||
} else if trimmed.eq_ignore_ascii_case("1")
|
|
||||||
|| trimmed.eq_ignore_ascii_case("true")
|
|
||||||
|| trimmed.eq_ignore_ascii_case("yes")
|
|
||||||
|| trimmed.eq_ignore_ascii_case("on")
|
|
||||||
{
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
default
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap_or(default)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(coverage)]
|
|
||||||
fn uvc_frame_size_guard_enabled() -> bool {
|
|
||||||
env_flag_enabled("LESAVKA_UVC_FRAME_SIZE_GUARD", true)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(coverage)]
|
|
||||||
/// Decides whether the coverage helper may use bulk transport.
|
|
||||||
///
|
|
||||||
/// Inputs: env override plus configfs capabilities. Output: effective mode.
|
|
||||||
/// Why: tests should not advertise bulk if the gadget tree lacks support.
|
|
||||||
fn uvc_bulk_transfer_enabled() -> bool {
|
|
||||||
if !env_flag_enabled("LESAVKA_UVC_BULK", true) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let base = std::path::Path::new(CONFIGFS_UVC_BASE);
|
|
||||||
if base.exists() && !base.join("streaming_bulk").exists() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(coverage)]
|
|
||||||
fn uvc_frame_size_for_active_mode(
|
|
||||||
width: u32,
|
|
||||||
height: u32,
|
|
||||||
fps: u32,
|
|
||||||
max_packet: u32,
|
|
||||||
bulk: bool,
|
|
||||||
) -> u32 {
|
|
||||||
env_u32_opt("LESAVKA_UVC_FRAME_SIZE")
|
|
||||||
.unwrap_or_else(|| {
|
|
||||||
derived_uvc_frame_max_bytes_for_transport(fps, max_packet, bulk)
|
|
||||||
.min(u32::MAX as usize) as u32
|
|
||||||
})
|
|
||||||
.max((width.saturating_mul(height) / 32).max(64 * 1024))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(coverage)]
|
|
||||||
/// Returns the optional JSON stats path for the UVC coverage helper.
|
|
||||||
///
|
|
||||||
/// Inputs: `LESAVKA_UVC_STATS_PATH`. Output: path or disabled state. Why:
|
|
||||||
/// tests and probes need a cheap health artifact without forcing writes.
|
|
||||||
fn uvc_stats_path() -> Option<std::path::PathBuf> {
|
|
||||||
match std::env::var("LESAVKA_UVC_STATS_PATH") {
|
|
||||||
Ok(value) => {
|
|
||||||
let trimmed = value.trim();
|
|
||||||
if trimmed.is_empty() || trimmed == "0" {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(std::path::PathBuf::from(trimmed))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => Some(std::path::PathBuf::from(DEFAULT_UVC_STATS_PATH)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(coverage)]
|
|
||||||
/// Writes a text artifact through a temporary file and atomic rename.
|
|
||||||
///
|
|
||||||
/// Inputs: target path and payload. Output: persisted file. Why: readers should
|
|
||||||
/// never observe partially-written UVC stats JSON.
|
|
||||||
fn write_atomic_text(path: &std::path::Path, text: &str) -> Result<()> {
|
|
||||||
if let Some(parent) = path.parent() {
|
|
||||||
std::fs::create_dir_all(parent)?;
|
|
||||||
}
|
|
||||||
let tmp = path.with_extension(format!("tmp.{}", std::process::id()));
|
|
||||||
std::fs::write(&tmp, text)?;
|
|
||||||
std::fs::rename(&tmp, path)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(coverage)]
|
|
||||||
/// Serializes the compact UVC video-stat snapshot consumed by probes.
|
|
||||||
///
|
|
||||||
/// Inputs: current counters and frame cap. Output: one JSON object string. Why:
|
|
||||||
/// shell probes need stable fields without depending on Rust serialization.
|
|
||||||
fn uvc_stats_snapshot_json(stats: &UvcVideoStats, frame_cap: usize) -> String {
|
|
||||||
format!(
|
|
||||||
"{{\"queued\":{},\"reloaded\":{},\"stale_replay\":{},\"rejected_oversize\":{},\"rejected_invalid\":{},\"fallback_idle\":{},\"latest_bytes\":{},\"frame_cap\":{},\"last_rejected_oversize_bytes\":{},\"last_rejected_oversize_cap\":{},\"paced_sleeps\":{},\"paced_sleep_ms\":{}}}\n",
|
|
||||||
stats.queued,
|
|
||||||
stats.reloaded,
|
|
||||||
stats.replayed_stale,
|
|
||||||
stats.rejected_oversize,
|
|
||||||
stats.rejected_invalid,
|
|
||||||
stats.fallback_idle,
|
|
||||||
stats.latest_bytes,
|
|
||||||
frame_cap,
|
|
||||||
stats.last_rejected_oversize_bytes,
|
|
||||||
stats.last_rejected_oversize_cap,
|
|
||||||
stats.paced_sleeps,
|
|
||||||
stats.paced_sleep_ms
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(coverage)]
|
|
||||||
/// Returns the optional periodic stats write interval.
|
|
||||||
///
|
|
||||||
/// Inputs: `LESAVKA_UVC_STATS_INTERVAL_MS`. Output: duration or disabled state.
|
|
||||||
/// Why: probes can ask for telemetry without slowing every coverage test.
|
|
||||||
fn uvc_stats_interval() -> Option<std::time::Duration> {
|
|
||||||
match env_u64("LESAVKA_UVC_STATS_INTERVAL_MS", DEFAULT_UVC_STATS_INTERVAL_MS) {
|
|
||||||
0 => None,
|
|
||||||
value => Some(std::time::Duration::from_millis(value)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(coverage)]
|
|
||||||
/// Returns the optional maximum age for a spooled MJPEG frame.
|
|
||||||
///
|
|
||||||
/// Inputs: `LESAVKA_UVC_FRAME_MAX_AGE_MS`. Outputs: `None` when the TTL is
|
|
||||||
/// disabled. Why: stale-frame replay must be explicit because it trades
|
|
||||||
/// smoothness against freshness during browser stream recovery.
|
|
||||||
fn frame_spool_max_age() -> Option<std::time::Duration> {
|
|
||||||
match env_u64(
|
|
||||||
"LESAVKA_UVC_FRAME_MAX_AGE_MS",
|
|
||||||
DEFAULT_UVC_FRAME_MAX_AGE_MS,
|
|
||||||
) {
|
|
||||||
0 => None,
|
|
||||||
value => Some(std::time::Duration::from_millis(value)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(coverage)]
|
|
||||||
/// Determines whether a spooled MJPEG frame is too old to replay.
|
|
||||||
///
|
|
||||||
/// Inputs: frame path and optional age limit. Outputs: true when the frame is
|
|
||||||
/// missing or older than the limit. Why: the coverage harness should guard the
|
|
||||||
/// same freshness cut-off that prevents seconds-old video from re-entering UVC.
|
|
||||||
fn frame_spool_is_stale(path: &std::path::Path, max_age: Option<std::time::Duration>) -> bool {
|
|
||||||
let Some(max_age) = max_age else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
let Ok(metadata) = std::fs::metadata(path) else {
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
let Ok(modified) = metadata.modified() else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
std::time::SystemTime::now()
|
|
||||||
.duration_since(modified)
|
|
||||||
.map(|age| age > max_age)
|
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(coverage)]
|
|
||||||
/// Parses an unsigned 64-bit environment value with a fallback.
|
|
||||||
///
|
|
||||||
/// Inputs: variable name and default value. Outputs: parsed value or default.
|
|
||||||
/// Why: UVC freshness settings use millisecond durations that should not be
|
|
||||||
/// truncated to 32-bit just because the coverage harness is lightweight.
|
|
||||||
fn env_u64(name: &str, default: u64) -> u64 {
|
|
||||||
env::var(name)
|
|
||||||
.ok()
|
|
||||||
.and_then(|value| value.parse::<u64>().ok())
|
|
||||||
.unwrap_or(default)
|
|
||||||
}
|
|
||||||
|
|||||||
106
server/src/bin/lesavka_uvc/coverage_stats.rs
Normal file
106
server/src/bin/lesavka_uvc/coverage_stats.rs
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
#[cfg(coverage)]
|
||||||
|
/// Returns the optional JSON stats path for the UVC coverage helper.
|
||||||
|
///
|
||||||
|
/// Inputs: `LESAVKA_UVC_STATS_PATH`. Output: path or disabled state. Why:
|
||||||
|
/// tests and probes need a cheap health artifact without forcing writes.
|
||||||
|
fn uvc_stats_path() -> Option<std::path::PathBuf> {
|
||||||
|
match std::env::var("LESAVKA_UVC_STATS_PATH") {
|
||||||
|
Ok(value) => {
|
||||||
|
let trimmed = value.trim();
|
||||||
|
if trimmed.is_empty() || trimmed == "0" {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(std::path::PathBuf::from(trimmed))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => Some(std::path::PathBuf::from(DEFAULT_UVC_STATS_PATH)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(coverage)]
|
||||||
|
/// Writes a text artifact through a temporary file and atomic rename.
|
||||||
|
///
|
||||||
|
/// Inputs: target path and payload. Output: persisted file. Why: readers should
|
||||||
|
/// never observe partially-written UVC stats JSON.
|
||||||
|
fn write_atomic_text(path: &std::path::Path, text: &str) -> Result<()> {
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
std::fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
let tmp = path.with_extension(format!("tmp.{}", std::process::id()));
|
||||||
|
std::fs::write(&tmp, text)?;
|
||||||
|
std::fs::rename(&tmp, path)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(coverage)]
|
||||||
|
/// Serializes the compact UVC video-stat snapshot consumed by probes.
|
||||||
|
///
|
||||||
|
/// Inputs: current counters and frame cap. Output: one JSON object string. Why:
|
||||||
|
/// shell probes need stable fields without depending on Rust serialization.
|
||||||
|
fn uvc_stats_snapshot_json(stats: &UvcVideoStats, frame_cap: usize) -> String {
|
||||||
|
format!(
|
||||||
|
"{{\"queued\":{},\"reloaded\":{},\"stale_replay\":{},\"rejected_oversize\":{},\"rejected_invalid\":{},\"fallback_idle\":{},\"latest_bytes\":{},\"frame_cap\":{},\"last_rejected_oversize_bytes\":{},\"last_rejected_oversize_cap\":{},\"paced_sleeps\":{},\"paced_sleep_ms\":{}}}\n",
|
||||||
|
stats.queued,
|
||||||
|
stats.reloaded,
|
||||||
|
stats.replayed_stale,
|
||||||
|
stats.rejected_oversize,
|
||||||
|
stats.rejected_invalid,
|
||||||
|
stats.fallback_idle,
|
||||||
|
stats.latest_bytes,
|
||||||
|
frame_cap,
|
||||||
|
stats.last_rejected_oversize_bytes,
|
||||||
|
stats.last_rejected_oversize_cap,
|
||||||
|
stats.paced_sleeps,
|
||||||
|
stats.paced_sleep_ms
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(coverage)]
|
||||||
|
/// Returns the optional periodic stats write interval.
|
||||||
|
///
|
||||||
|
/// Inputs: `LESAVKA_UVC_STATS_INTERVAL_MS`. Output: duration or disabled state.
|
||||||
|
/// Why: probes can ask for telemetry without slowing every coverage test.
|
||||||
|
fn uvc_stats_interval() -> Option<std::time::Duration> {
|
||||||
|
match env_u64("LESAVKA_UVC_STATS_INTERVAL_MS", DEFAULT_UVC_STATS_INTERVAL_MS) {
|
||||||
|
0 => None,
|
||||||
|
value => Some(std::time::Duration::from_millis(value)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(coverage)]
|
||||||
|
/// Returns the optional maximum age for a spooled MJPEG frame.
|
||||||
|
///
|
||||||
|
/// Inputs: `LESAVKA_UVC_FRAME_MAX_AGE_MS`. Outputs: `None` when the TTL is
|
||||||
|
/// disabled. Why: stale-frame replay must be explicit because it trades
|
||||||
|
/// smoothness against freshness during browser stream recovery.
|
||||||
|
fn frame_spool_max_age() -> Option<std::time::Duration> {
|
||||||
|
match env_u64(
|
||||||
|
"LESAVKA_UVC_FRAME_MAX_AGE_MS",
|
||||||
|
DEFAULT_UVC_FRAME_MAX_AGE_MS,
|
||||||
|
) {
|
||||||
|
0 => None,
|
||||||
|
value => Some(std::time::Duration::from_millis(value)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(coverage)]
|
||||||
|
/// Determines whether a spooled MJPEG frame is too old to replay.
|
||||||
|
///
|
||||||
|
/// Inputs: frame path and optional age limit. Outputs: true when the frame is
|
||||||
|
/// missing or older than the limit. Why: the coverage harness should guard the
|
||||||
|
/// same freshness cut-off that prevents seconds-old video from re-entering UVC.
|
||||||
|
fn frame_spool_is_stale(path: &std::path::Path, max_age: Option<std::time::Duration>) -> bool {
|
||||||
|
let Some(max_age) = max_age else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let Ok(metadata) = std::fs::metadata(path) else {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
let Ok(modified) = metadata.modified() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(modified)
|
||||||
|
.map(|age| age > max_age)
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
@ -149,6 +149,100 @@ fn uvc_queue_pacing_defaults_on_but_can_be_disabled() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn uvc_env_flags_and_bulk_configfs_edges_are_covered() {
|
||||||
|
with_var("LESAVKA_UVC_QUEUE_PACING", Some("maybe"), || {
|
||||||
|
assert!(env_flag_enabled("LESAVKA_UVC_QUEUE_PACING", true));
|
||||||
|
assert!(!env_flag_enabled("LESAVKA_UVC_QUEUE_PACING", false));
|
||||||
|
});
|
||||||
|
|
||||||
|
let configfs = tempfile::tempdir().expect("tempdir");
|
||||||
|
assert!(!uvc_bulk_transfer_enabled_for_base(configfs.path()));
|
||||||
|
fs::create_dir(configfs.path().join("streaming_bulk")).expect("streaming_bulk dir");
|
||||||
|
assert!(uvc_bulk_transfer_enabled_for_base(configfs.path()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn uvc_startup_open_can_opt_into_write_mode() {
|
||||||
|
let device = NamedTempFile::new().expect("device");
|
||||||
|
with_vars(
|
||||||
|
[
|
||||||
|
("LESAVKA_UVC_CONTROL_READ_ONLY", Some("0")),
|
||||||
|
("LESAVKA_UVC_BLOCKING", Some("1")),
|
||||||
|
],
|
||||||
|
|| {
|
||||||
|
let opened = open_with_retry(device.path().to_str().expect("path"));
|
||||||
|
assert!(opened.is_ok());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn uvc_stats_paths_and_intervals_cover_operator_overrides() {
|
||||||
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
let stats_path = dir.path().join("video-stats.json");
|
||||||
|
with_var(
|
||||||
|
"LESAVKA_UVC_STATS_PATH",
|
||||||
|
Some(stats_path.to_str().expect("stats path")),
|
||||||
|
|| {
|
||||||
|
assert_eq!(uvc_stats_path(), Some(stats_path.clone()));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
with_var("LESAVKA_UVC_STATS_INTERVAL_MS", Some("25"), || {
|
||||||
|
assert_eq!(
|
||||||
|
uvc_stats_interval(),
|
||||||
|
Some(std::time::Duration::from_millis(25))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn uvc_video_stream_refresh_covers_fresh_invalid_and_missing_frames() {
|
||||||
|
let valid = NamedTempFile::new().expect("valid frame");
|
||||||
|
fs::write(valid.path(), [0xff, 0xd8, 1, 2, 3, 0xff, 0xd9]).expect("write valid");
|
||||||
|
let mut stream = UvcVideoStream::new(-1);
|
||||||
|
stream.frame_path = valid.path().to_path_buf();
|
||||||
|
stream.refresh_latest_frame();
|
||||||
|
assert_eq!(stream.stats.reloaded, 1);
|
||||||
|
assert_eq!(stream.stats.latest_bytes, 7);
|
||||||
|
assert_eq!(stream.latest_frame, vec![0xff, 0xd8, 1, 2, 3, 0xff, 0xd9]);
|
||||||
|
|
||||||
|
let invalid = NamedTempFile::new().expect("invalid frame");
|
||||||
|
fs::write(invalid.path(), b"not-jpeg").expect("write invalid");
|
||||||
|
let mut stream = UvcVideoStream::new(-1);
|
||||||
|
stream.frame_path = invalid.path().to_path_buf();
|
||||||
|
stream.latest_frame = b"bad-cache".to_vec();
|
||||||
|
stream.refresh_latest_frame();
|
||||||
|
assert_eq!(stream.stats.rejected_invalid, 1);
|
||||||
|
assert_eq!(stream.stats.fallback_idle, 1);
|
||||||
|
assert_eq!(stream.latest_frame, IDLE_MJPEG_FRAME);
|
||||||
|
|
||||||
|
let missing = tempfile::tempdir()
|
||||||
|
.expect("tempdir")
|
||||||
|
.path()
|
||||||
|
.join("missing-frame.mjpg");
|
||||||
|
let mut stream = UvcVideoStream::new(-1);
|
||||||
|
stream.frame_path = missing;
|
||||||
|
stream.latest_frame = b"bad-cache".to_vec();
|
||||||
|
stream.refresh_latest_frame();
|
||||||
|
assert_eq!(stream.stats.fallback_idle, 1);
|
||||||
|
assert_eq!(stream.latest_frame, IDLE_MJPEG_FRAME);
|
||||||
|
|
||||||
|
let tiny_buffer_stream = UvcVideoStream::new(-1);
|
||||||
|
assert_eq!(
|
||||||
|
tiny_buffer_stream.frame_for_buffer(MINIMAL_MJPEG_FRAME.len()),
|
||||||
|
MINIMAL_MJPEG_FRAME
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
tiny_buffer_stream.frame_for_buffer(IDLE_MJPEG_FRAME.len()),
|
||||||
|
IDLE_MJPEG_FRAME
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[serial]
|
#[serial]
|
||||||
fn main_coverage_mode_returns_error_for_non_uvc_node() {
|
fn main_coverage_mode_returns_error_for_non_uvc_node() {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user