tests: protect server RC calibration defaults

This commit is contained in:
Brad Stein 2026-05-06 04:50:08 -03:00
parent 1b3b8c2cbb
commit 1f6d34b6fa
13 changed files with 233 additions and 55 deletions

6
Cargo.lock generated
View File

@ -1652,7 +1652,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "lesavka_client"
version = "0.19.29"
version = "0.19.30"
dependencies = [
"anyhow",
"async-stream",
@ -1686,7 +1686,7 @@ dependencies = [
[[package]]
name = "lesavka_common"
version = "0.19.29"
version = "0.19.30"
dependencies = [
"anyhow",
"base64",
@ -1698,7 +1698,7 @@ dependencies = [
[[package]]
name = "lesavka_server"
version = "0.19.29"
version = "0.19.30"
dependencies = [
"anyhow",
"base64",

View File

@ -4,7 +4,7 @@ path = "src/main.rs"
[package]
name = "lesavka_client"
version = "0.19.29"
version = "0.19.30"
edition = "2024"
[dependencies]

View File

@ -1,6 +1,7 @@
impl LesavkaClientApp {
/*──────────────── bundled webcam + mic stream ─────────────────*/
#[cfg(not(coverage))]
#[allow(clippy::too_many_arguments)]
async fn webcam_media_loop(
ep: Channel,
initial_camera_source: Option<String>,
@ -985,6 +986,7 @@ fn retain_newest_pending_audio(pending_audio: &mut Vec<AudioPacket>) -> usize {
}
#[cfg(not(coverage))]
#[allow(clippy::too_many_arguments)]
fn emit_bundled_media(
session_id: u64,
bundle_seq: &mut u64,
@ -1297,9 +1299,18 @@ mod uplink_timing_tests {
packet.client_send_pts_us >= packet.client_capture_pts_us,
"enqueue/send stamp must be on or after the shared-clock capture estimate"
);
let capture_to_enqueue = Duration::from_micros(
packet
.client_send_pts_us
.saturating_sub(packet.client_capture_pts_us),
);
assert_eq!(
capture_to_enqueue, enqueue_age,
"enqueue timing metadata should stay anchored to the pre-pop stamp"
);
assert!(
packet.client_send_pts_us - packet.client_capture_pts_us <= 3_000,
"capture-to-enqueue age, not async pop delay, should define the timing window"
packet.client_queue_age_ms >= duration_ms_u32(enqueue_age).saturating_add(4),
"queue age should include the simulated async pop delay without mutating send timing"
);
}
@ -1331,9 +1342,18 @@ mod uplink_timing_tests {
packet.client_send_pts_us >= packet.client_capture_pts_us,
"enqueue/send stamp must be on or after the shared-clock capture estimate"
);
let capture_to_enqueue = Duration::from_micros(
packet
.client_send_pts_us
.saturating_sub(packet.client_capture_pts_us),
);
assert_eq!(
capture_to_enqueue, enqueue_age,
"enqueue timing metadata should stay anchored to the pre-pop stamp"
);
assert!(
packet.client_send_pts_us - packet.client_capture_pts_us <= 4_000,
"capture-to-enqueue age, not async pop delay, should define the timing window"
packet.client_queue_age_ms >= duration_ms_u32(enqueue_age).saturating_add(4),
"queue age should include the simulated async pop delay without mutating send timing"
);
}

View File

@ -279,7 +279,7 @@ fn adaptive_gray_roi_mask(frames: &[&[u8]], pixel_count: usize) -> Option<Vec<bo
return None;
}
let mut scores = vec![0.0; pixel_count];
for pixel_index in 0..pixel_count {
for (pixel_index, score) in scores.iter_mut().enumerate() {
let mut min = u8::MAX;
let mut max = u8::MIN;
for frame in frames {
@ -287,7 +287,7 @@ fn adaptive_gray_roi_mask(frames: &[&[u8]], pixel_count: usize) -> Option<Vec<bo
min = min.min(value);
max = max.max(value);
}
scores[pixel_index] = f64::from(max.saturating_sub(min)) * dark_roi_factor(min);
*score = f64::from(max.saturating_sub(min)) * dark_roi_factor(min);
}
adaptive_roi_mask_from_scores(&scores, MIN_GRAY_ROI_SCORE)
}
@ -297,7 +297,7 @@ fn adaptive_rgb_roi_mask(frames: &[&[u8]], pixel_count: usize) -> Option<Vec<boo
return None;
}
let mut scores = vec![0.0; pixel_count];
for pixel_index in 0..pixel_count {
for (pixel_index, score) in scores.iter_mut().enumerate() {
let mut min_r = u8::MAX;
let mut min_g = u8::MAX;
let mut min_b = u8::MAX;
@ -329,7 +329,7 @@ fn adaptive_rgb_roi_mask(frames: &[&[u8]], pixel_count: usize) -> Option<Vec<boo
+ f64::from(max_g.saturating_sub(min_g))
+ f64::from(max_b.saturating_sub(min_b));
let luma_span = f64::from(max_luma.saturating_sub(min_luma));
scores[pixel_index] =
*score =
(rgb_span + (2.0 * luma_span)) * (1.0 + best_palette_score) * dark_roi_factor(min_luma);
}
adaptive_roi_mask_from_scores(&scores, MIN_RGB_ROI_SCORE)
@ -354,13 +354,11 @@ fn adaptive_roi_mask_from_scores(scores: &[f64], min_score: f64) -> Option<Vec<b
.min(scores.len());
let score_floor = (max_score * ADAPTIVE_ROI_SCORE_FRACTION).max(min_score);
let mut mask = vec![false; scores.len()];
let mut selected = 0usize;
for (index, score) in ranked.into_iter().take(max_selected) {
for (selected, (index, score)) in ranked.into_iter().take(max_selected).enumerate() {
if score < score_floor && selected >= MIN_ADAPTIVE_ROI_PIXELS {
break;
}
mask[index] = true;
selected += 1;
}
let mask = retain_largest_connected_roi(mask);

View File

@ -728,6 +728,7 @@ pub(super) fn window_segment(
}
}
#[allow(clippy::too_many_arguments)]
fn push_audio_segment(
segments: &mut Vec<PulseSegment>,
samples: &[i16],

View File

@ -206,7 +206,7 @@ pub(crate) fn correlate_coded_segments(
if event_width_codes.is_empty() {
bail!("event width code sequence must not be empty");
}
if event_width_codes.iter().any(|code| *code == 0) {
if event_width_codes.contains(&0) {
bail!("event width codes must stay positive");
}
if pulse_period_s <= 0.0 {

View File

@ -191,7 +191,7 @@ async fn collect_probe_audio_grace(
pending_audio: &mut Vec<AudioPacket>,
audio_done: &mut bool,
audio_seq: &mut u64,
mut audio_dump: Option<&mut File>,
audio_dump: Option<&mut File>,
) {
if *audio_done || !pending_audio.is_empty() {
return;
@ -202,7 +202,7 @@ async fn collect_probe_audio_grace(
};
if let Some(mut packet) = next.packet {
stamp_probe_audio_packet(&mut packet, audio_seq, next.queue_depth);
write_probe_audio_dump(audio_dump.as_mut().map(|file| &mut **file), &packet);
write_probe_audio_dump(audio_dump, &packet);
pending_audio.push(packet);
retain_newest_probe_audio(pending_audio);
} else if next.closed {

View File

@ -1,6 +1,6 @@
[package]
name = "lesavka_common"
version = "0.19.29"
version = "0.19.30"
edition = "2024"
build = "build.rs"

View File

@ -10,7 +10,7 @@ bench = false
[package]
name = "lesavka_server"
version = "0.19.29"
version = "0.19.30"
edition = "2024"
autobins = false

View File

@ -477,8 +477,14 @@ mod tests {
use super::*;
use tempfile::NamedTempFile;
#[test]
fn default_snapshot_uses_factory_mjpeg_calibration() {
const BLESSED_SERVER_TO_RCT_VIDEO_OFFSETS: &[(&str, i64)] = &[
("1280x720@20", 162_659),
("1280x720@30", 135_090),
("1920x1080@20", 160_045),
("1920x1080@30", 127_952),
];
fn with_clean_offset_env(test: impl FnOnce()) {
temp_env::with_vars(
[
("LESAVKA_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US", None::<&str>),
@ -491,18 +497,70 @@ mod tests {
"LESAVKA_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US",
None::<&str>,
),
("UVC_MODE", None::<&str>),
("LESAVKA_UVC_MODE", None::<&str>),
("LESAVKA_UVC_WIDTH", None::<&str>),
("LESAVKA_UVC_HEIGHT", None::<&str>),
("LESAVKA_UVC_FPS", None::<&str>),
("LESAVKA_UVC_INTERVAL", None::<&str>),
("LESAVKA_CAM_WIDTH", None::<&str>),
("LESAVKA_CAM_HEIGHT", None::<&str>),
("LESAVKA_CAM_FPS", None::<&str>),
],
|| {
test,
);
}
#[test]
fn blessed_server_to_rct_offsets_are_release_defaults() {
assert_eq!(
FACTORY_MJPEG_VIDEO_OFFSET_US, FACTORY_MJPEG_VIDEO_OFFSET_1280X720_30_US,
"720p30 is the blessed default profile until a new lab matrix replaces it"
);
assert_eq!(FACTORY_MJPEG_VIDEO_OFFSET_1280X720_20_US, 162_659);
assert_eq!(FACTORY_MJPEG_VIDEO_OFFSET_1280X720_30_US, 135_090);
assert_eq!(FACTORY_MJPEG_VIDEO_OFFSET_1920X1080_20_US, 160_045);
assert_eq!(FACTORY_MJPEG_VIDEO_OFFSET_1920X1080_30_US, 127_952);
assert_eq!(
FACTORY_MJPEG_AUDIO_MODE_OFFSETS_US,
"1280x720@20=0,1280x720@30=0,1920x1080@20=0,1920x1080@30=0"
);
assert_eq!(
FACTORY_MJPEG_VIDEO_MODE_OFFSETS_US,
"1280x720@20=162659,1280x720@30=135090,1920x1080@20=160045,1920x1080@30=127952"
);
}
#[test]
fn every_supported_uvc_mode_loads_tailored_factory_offset() {
for (mode, expected_offset_us) in BLESSED_SERVER_TO_RCT_VIDEO_OFFSETS {
with_clean_offset_env(|| {
temp_env::with_var("UVC_MODE", Some(*mode), || {
let state = snapshot_from_env();
assert_eq!(
state.factory_video_offset_us, *expected_offset_us,
"{mode} should use its baked server-to-RCT factory offset"
);
assert_eq!(
state.default_video_offset_us, *expected_offset_us,
"{mode} should default to its baked server-to-RCT offset"
);
assert_eq!(state.default_audio_offset_us, 0);
assert_eq!(state.source, "factory");
assert_eq!(state.confidence, FACTORY_CONFIDENCE);
});
});
}
}
#[test]
fn default_snapshot_uses_factory_mjpeg_calibration() {
with_clean_offset_env(|| {
let state = snapshot_from_env();
assert_eq!(state.default_audio_offset_us, 0);
assert_eq!(state.active_video_offset_us, FACTORY_MJPEG_VIDEO_OFFSET_US);
assert_eq!(state.source, "factory");
},
);
});
}
#[test]

View File

@ -174,6 +174,7 @@ async fn push_media_v2_audio(
}
#[cfg(not(coverage))]
#[allow(clippy::too_many_arguments)]
async fn feed_media_v2_video(
video: Option<VideoPacket>,
clock: &mut MediaV2Clock,

View File

@ -130,20 +130,15 @@ impl ScalarWindow {
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
enum UpstreamSyncPhase {
#[default]
Acquiring,
Syncing,
Live,
Healing,
}
impl Default for UpstreamSyncPhase {
fn default() -> Self {
Self::Acquiring
}
}
impl UpstreamSyncPhase {
fn as_str(self) -> &'static str {
match self {
@ -803,3 +798,80 @@ fn apply_offset(instant: Instant, offset_us: i64) -> Instant {
.unwrap_or(instant)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn with_clean_offset_env(test: impl FnOnce()) {
temp_env::with_vars(
[
("LESAVKA_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US", None::<&str>),
("LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US", None::<&str>),
(
"LESAVKA_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US",
None::<&str>,
),
(
"LESAVKA_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US",
None::<&str>,
),
("LESAVKA_UVC_WIDTH", None::<&str>),
("LESAVKA_UVC_HEIGHT", None::<&str>),
("LESAVKA_UVC_FPS", None::<&str>),
("LESAVKA_UVC_INTERVAL", None::<&str>),
],
test,
);
}
#[test]
fn runtime_uses_baked_mode_offsets_before_calibration_store_loads() {
for (width, height, fps, expected_video_offset_us) in [
("1280", "720", "20", 162_659),
("1280", "720", "30", 135_090),
("1920", "1080", "20", 160_045),
("1920", "1080", "30", 127_952),
] {
with_clean_offset_env(|| {
temp_env::with_vars(
[
("LESAVKA_UVC_WIDTH", Some(width)),
("LESAVKA_UVC_HEIGHT", Some(height)),
("LESAVKA_UVC_FPS", Some(fps)),
],
|| {
let runtime = UpstreamMediaRuntime::new();
assert_eq!(
runtime.playout_offsets(),
(expected_video_offset_us, 0),
"{width}x{height}@{fps} should use its baked startup offset"
);
},
);
});
}
}
#[test]
fn runtime_prefers_mode_offset_map_over_scalar_fallback() {
with_clean_offset_env(|| {
temp_env::with_vars(
[
("LESAVKA_UVC_WIDTH", Some("1280")),
("LESAVKA_UVC_HEIGHT", Some("720")),
("LESAVKA_UVC_FPS", Some("30")),
("LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US", Some("999999")),
(
"LESAVKA_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US",
Some("1280x720@30=135090"),
),
],
|| {
let runtime = UpstreamMediaRuntime::new();
assert_eq!(runtime.playout_offsets(), (135_090, 0));
},
);
});
}
}

View File

@ -26,15 +26,17 @@ mod inputs_contract {
use evdev::uinput::VirtualDevice;
use serial_test::serial;
use std::thread;
use temp_env::with_var;
use temp_env::{with_var, with_vars};
fn open_virtual_device(vdev: &mut VirtualDevice) -> Option<evdev::Device> {
fn open_virtual_device_with_path(
vdev: &mut VirtualDevice,
) -> Option<(std::path::PathBuf, evdev::Device)> {
for _ in 0..40 {
if let Ok(mut nodes) = vdev.enumerate_dev_nodes_blocking() {
if let Some(Ok(path)) = nodes.next() {
if let Ok(dev) = evdev::Device::open(path) {
if let Ok(dev) = evdev::Device::open(&path) {
let _ = dev.set_nonblocking(true);
return Some(dev);
return Some((path.to_path_buf(), dev));
}
}
}
@ -43,6 +45,10 @@ mod inputs_contract {
None
}
fn open_virtual_device(vdev: &mut VirtualDevice) -> Option<evdev::Device> {
open_virtual_device_with_path(vdev).map(|(_, dev)| dev)
}
fn build_keyboard() -> Option<evdev::Device> {
let mut keys = AttributeSet::<evdev::KeyCode>::new();
keys.insert(evdev::KeyCode::KEY_A);
@ -138,12 +144,13 @@ mod inputs_contract {
fn build_keyboard_pair(name: &str) -> Option<(VirtualDevice, evdev::Device)> {
build_keyboard_pair_with_keys(name, &[evdev::KeyCode::KEY_A, evdev::KeyCode::KEY_ENTER])
.map(|(vdev, _, dev)| (vdev, dev))
}
fn build_keyboard_pair_with_keys(
name: &str,
supported_keys: &[evdev::KeyCode],
) -> Option<(VirtualDevice, evdev::Device)> {
) -> Option<(VirtualDevice, std::path::PathBuf, evdev::Device)> {
let mut keys = AttributeSet::<evdev::KeyCode>::new();
for key in supported_keys {
keys.insert(*key);
@ -157,11 +164,17 @@ mod inputs_contract {
.build()
.ok()?;
let dev = open_virtual_device(&mut vdev)?;
Some((vdev, dev))
let (path, dev) = open_virtual_device_with_path(&mut vdev)?;
Some((vdev, path, dev))
}
fn build_mouse_pair(name: &str) -> Option<(VirtualDevice, evdev::Device)> {
build_mouse_pair_with_path(name).map(|(vdev, _, dev)| (vdev, dev))
}
fn build_mouse_pair_with_path(
name: &str,
) -> Option<(VirtualDevice, std::path::PathBuf, evdev::Device)> {
let mut keys = AttributeSet::<evdev::KeyCode>::new();
keys.insert(evdev::KeyCode::BTN_LEFT);
let mut rel = AttributeSet::<evdev::RelativeAxisCode>::new();
@ -178,8 +191,8 @@ mod inputs_contract {
.build()
.ok()?;
let dev = open_virtual_device(&mut vdev)?;
Some((vdev, dev))
let (path, dev) = open_virtual_device_with_path(&mut vdev)?;
Some((vdev, path, dev))
}
fn new_aggregator() -> InputAggregator {
@ -287,22 +300,37 @@ mod inputs_contract {
#[test]
#[serial]
fn init_grabs_virtual_keyboard_and_mouse_when_available() {
let Some((_kbd_vdev, _kbd_dev)) = build_keyboard_pair("lesavka-input-init-kbd") else {
let Some((_kbd_vdev, kbd_path, _kbd_dev)) = build_keyboard_pair_with_keys(
"lesavka-input-init-kbd",
&[evdev::KeyCode::KEY_A, evdev::KeyCode::KEY_ENTER],
) else {
return;
};
let Some((_mouse_vdev, _mouse_dev)) = build_mouse_pair("lesavka-input-init-mouse") else {
let Some((_mouse_vdev, mouse_path, _mouse_dev)) =
build_mouse_pair_with_path("lesavka-input-init-mouse")
else {
return;
};
let kbd_path = kbd_path.to_string_lossy().into_owned();
let mouse_path = mouse_path.to_string_lossy().into_owned();
with_vars(
[
("LESAVKA_KEYBOARD_DEVICE", Some(kbd_path.as_str())),
("LESAVKA_MOUSE_DEVICE", Some(mouse_path.as_str())),
],
|| {
let mut agg = new_aggregator();
let result = agg.init();
assert!(
result.is_ok(),
"init should succeed with virtual input devices"
"init should succeed with selected virtual input devices"
);
assert!(
!agg.keyboards.is_empty() || !agg.mice.is_empty(),
"init should discover at least one virtual input device"
"init should discover at least one selected virtual input device"
);
},
);
}