tests: protect server RC calibration defaults
This commit is contained in:
parent
1b3b8c2cbb
commit
1f6d34b6fa
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -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",
|
||||
|
||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
||||
|
||||
[package]
|
||||
name = "lesavka_client"
|
||||
version = "0.19.29"
|
||||
version = "0.19.30"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -728,6 +728,7 @@ pub(super) fn window_segment(
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn push_audio_segment(
|
||||
segments: &mut Vec<PulseSegment>,
|
||||
samples: &[i16],
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lesavka_common"
|
||||
version = "0.19.29"
|
||||
version = "0.19.30"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ bench = false
|
||||
|
||||
[package]
|
||||
name = "lesavka_server"
|
||||
version = "0.19.29"
|
||||
version = "0.19.30"
|
||||
edition = "2024"
|
||||
autobins = false
|
||||
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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));
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user