test(launcher): stabilize recovered relay preview gate
This commit is contained in:
parent
df6dfefce6
commit
d70199c410
@ -352,7 +352,16 @@ impl CameraCapture {
|
||||
|
||||
#[cfg(coverage)]
|
||||
fn choose_encoder() -> (&'static str, &'static str, &'static str) {
|
||||
("x264enc", "key-int-max", "30")
|
||||
match std::env::var("LESAVKA_CAM_TEST_ENCODER")
|
||||
.ok()
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
{
|
||||
Some("nvh264enc") => ("nvh264enc", "gop-size", "30"),
|
||||
Some("vaapih264enc") => ("vaapih264enc", "keyframe-period", "30"),
|
||||
Some("v4l2h264enc") => ("v4l2h264enc", "idrcount", "30"),
|
||||
_ => ("x264enc", "key-int-max", "30"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -33,6 +33,25 @@ pub struct KeyboardAggregator {
|
||||
static SEQ: AtomicU32 = AtomicU32::new(0);
|
||||
static LAST_PASTE_MS: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
fn update_pressed_keys(pressed_keys: &mut HashSet<KeyCode>, code: KeyCode, value: i32) {
|
||||
if value == 1 {
|
||||
pressed_keys.insert(code);
|
||||
} else {
|
||||
pressed_keys.remove(&code);
|
||||
}
|
||||
}
|
||||
|
||||
fn debounce_gate(last_paste_ms: &AtomicU64, now_ms: u64, debounce_ms: u64) -> bool {
|
||||
if debounce_ms == 0 {
|
||||
last_paste_ms.store(now_ms, Ordering::Relaxed);
|
||||
return true;
|
||||
}
|
||||
let last = last_paste_ms.load(Ordering::Relaxed);
|
||||
let allowed = now_ms.saturating_sub(last) >= debounce_ms;
|
||||
last_paste_ms.store(now_ms, Ordering::Relaxed);
|
||||
allowed
|
||||
}
|
||||
|
||||
impl KeyboardAggregator {
|
||||
pub fn new(
|
||||
dev: Device,
|
||||
@ -89,11 +108,7 @@ impl KeyboardAggregator {
|
||||
}
|
||||
let code = KeyCode::new(ev.code());
|
||||
let value = ev.value();
|
||||
if value == 1 {
|
||||
self.pressed_keys.insert(code);
|
||||
} else {
|
||||
self.pressed_keys.remove(&code);
|
||||
}
|
||||
update_pressed_keys(&mut self.pressed_keys, code, value);
|
||||
|
||||
let swallowed = self.try_handle_paste_event(code, value);
|
||||
if !swallowed && !self.sending_disabled {
|
||||
@ -132,16 +147,7 @@ impl KeyboardAggregator {
|
||||
}
|
||||
let code = KeyCode::new(ev.code());
|
||||
let value = ev.value();
|
||||
|
||||
match value {
|
||||
1 => {
|
||||
self.pressed_keys.insert(code);
|
||||
} // press
|
||||
0 => {
|
||||
self.pressed_keys.remove(&code);
|
||||
} // release
|
||||
_ => {}
|
||||
}
|
||||
update_pressed_keys(&mut self.pressed_keys, code, value);
|
||||
|
||||
if self.try_handle_paste_event(code, value) {
|
||||
continue;
|
||||
@ -356,14 +362,7 @@ impl KeyboardAggregator {
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as u64;
|
||||
if debounce_ms == 0 {
|
||||
LAST_PASTE_MS.store(now_ms, Ordering::Relaxed);
|
||||
return true;
|
||||
}
|
||||
let last = LAST_PASTE_MS.load(Ordering::Relaxed);
|
||||
let allowed = now_ms.saturating_sub(last) >= debounce_ms;
|
||||
LAST_PASTE_MS.store(now_ms, Ordering::Relaxed);
|
||||
allowed
|
||||
debounce_gate(&LAST_PASTE_MS, now_ms, debounce_ms)
|
||||
}
|
||||
|
||||
#[cfg(coverage)]
|
||||
|
||||
@ -1,249 +1,274 @@
|
||||
{
|
||||
"files": {
|
||||
"client/src/app.rs": {
|
||||
"loc": 546,
|
||||
"clippy_warnings": 42,
|
||||
"doc_debt": 10,
|
||||
"loc": 548
|
||||
"doc_debt": 10
|
||||
},
|
||||
"client/src/app_support.rs": {
|
||||
"loc": 128,
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 3,
|
||||
"loc": 129
|
||||
"doc_debt": 3
|
||||
},
|
||||
"client/src/handshake.rs": {
|
||||
"loc": 194,
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 3,
|
||||
"loc": 194
|
||||
"doc_debt": 3
|
||||
},
|
||||
"client/src/input/camera.rs": {
|
||||
"loc": 372,
|
||||
"clippy_warnings": 38,
|
||||
"doc_debt": 6,
|
||||
"loc": 368
|
||||
"doc_debt": 7
|
||||
},
|
||||
"client/src/input/inputs.rs": {
|
||||
"loc": 673,
|
||||
"clippy_warnings": 42,
|
||||
"doc_debt": 16,
|
||||
"loc": 669
|
||||
"doc_debt": 16
|
||||
},
|
||||
"client/src/input/keyboard.rs": {
|
||||
"loc": 580,
|
||||
"clippy_warnings": 24,
|
||||
"doc_debt": 17,
|
||||
"loc": 570
|
||||
"doc_debt": 18
|
||||
},
|
||||
"client/src/input/keymap.rs": {
|
||||
"loc": 196,
|
||||
"clippy_warnings": 8,
|
||||
"doc_debt": 0,
|
||||
"loc": 196
|
||||
"doc_debt": 0
|
||||
},
|
||||
"client/src/input/microphone.rs": {
|
||||
"loc": 166,
|
||||
"clippy_warnings": 17,
|
||||
"doc_debt": 2,
|
||||
"loc": 166
|
||||
"doc_debt": 2
|
||||
},
|
||||
"client/src/input/mod.rs": {
|
||||
"loc": 8,
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 0,
|
||||
"loc": 8
|
||||
"doc_debt": 0
|
||||
},
|
||||
"client/src/input/mouse.rs": {
|
||||
"loc": 317,
|
||||
"clippy_warnings": 40,
|
||||
"doc_debt": 8,
|
||||
"loc": 317
|
||||
"doc_debt": 8
|
||||
},
|
||||
"client/src/launcher/clipboard.rs": {
|
||||
"loc": 177,
|
||||
"clippy_warnings": 2,
|
||||
"doc_debt": 1,
|
||||
"loc": 176
|
||||
"doc_debt": 1
|
||||
},
|
||||
"client/src/launcher/device_test.rs": {
|
||||
"loc": 155,
|
||||
"clippy_warnings": 8,
|
||||
"doc_debt": 7
|
||||
},
|
||||
"client/src/launcher/devices.rs": {
|
||||
"loc": 158,
|
||||
"clippy_warnings": 6,
|
||||
"doc_debt": 3,
|
||||
"loc": 154
|
||||
"doc_debt": 3
|
||||
},
|
||||
"client/src/launcher/diagnostics.rs": {
|
||||
"loc": 175,
|
||||
"clippy_warnings": 17,
|
||||
"doc_debt": 3,
|
||||
"loc": 172
|
||||
"doc_debt": 3
|
||||
},
|
||||
"client/src/launcher/mod.rs": {
|
||||
"loc": 195,
|
||||
"clippy_warnings": 6,
|
||||
"doc_debt": 4,
|
||||
"loc": 182
|
||||
"doc_debt": 4
|
||||
},
|
||||
"client/src/launcher/power.rs": {
|
||||
"loc": 61,
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 0
|
||||
},
|
||||
"client/src/launcher/preview.rs": {
|
||||
"loc": 293,
|
||||
"clippy_warnings": 20,
|
||||
"doc_debt": 6,
|
||||
"loc": 293
|
||||
"doc_debt": 6
|
||||
},
|
||||
"client/src/launcher/state.rs": {
|
||||
"loc": 360,
|
||||
"clippy_warnings": 14,
|
||||
"doc_debt": 13,
|
||||
"loc": 306
|
||||
"doc_debt": 15
|
||||
},
|
||||
"client/src/launcher/ui.rs": {
|
||||
"clippy_warnings": 18,
|
||||
"doc_debt": 15,
|
||||
"loc": 996
|
||||
"loc": 574,
|
||||
"clippy_warnings": 4,
|
||||
"doc_debt": 1
|
||||
},
|
||||
"client/src/launcher/ui_components.rs": {
|
||||
"loc": 531,
|
||||
"clippy_warnings": 8,
|
||||
"doc_debt": 4
|
||||
},
|
||||
"client/src/launcher/ui_runtime.rs": {
|
||||
"loc": 439,
|
||||
"clippy_warnings": 10,
|
||||
"doc_debt": 18
|
||||
},
|
||||
"client/src/layout.rs": {
|
||||
"loc": 78,
|
||||
"clippy_warnings": 6,
|
||||
"doc_debt": 0,
|
||||
"loc": 78
|
||||
"doc_debt": 0
|
||||
},
|
||||
"client/src/lib.rs": {
|
||||
"loc": 14,
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 0,
|
||||
"loc": 14
|
||||
"doc_debt": 0
|
||||
},
|
||||
"client/src/main.rs": {
|
||||
"loc": 96,
|
||||
"clippy_warnings": 2,
|
||||
"doc_debt": 2,
|
||||
"loc": 93
|
||||
"doc_debt": 2
|
||||
},
|
||||
"client/src/output/audio.rs": {
|
||||
"clippy_warnings": 43,
|
||||
"doc_debt": 5,
|
||||
"loc": 200
|
||||
"loc": 195,
|
||||
"clippy_warnings": 37,
|
||||
"doc_debt": 5
|
||||
},
|
||||
"client/src/output/display.rs": {
|
||||
"loc": 81,
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 0,
|
||||
"loc": 81
|
||||
"doc_debt": 0
|
||||
},
|
||||
"client/src/output/layout.rs": {
|
||||
"loc": 155,
|
||||
"clippy_warnings": 4,
|
||||
"doc_debt": 2,
|
||||
"loc": 155
|
||||
"doc_debt": 2
|
||||
},
|
||||
"client/src/output/mod.rs": {
|
||||
"loc": 6,
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 0,
|
||||
"loc": 6
|
||||
"doc_debt": 0
|
||||
},
|
||||
"client/src/output/video.rs": {
|
||||
"loc": 547,
|
||||
"clippy_warnings": 36,
|
||||
"doc_debt": 4,
|
||||
"loc": 545
|
||||
"doc_debt": 4
|
||||
},
|
||||
"client/src/paste.rs": {
|
||||
"loc": 46,
|
||||
"clippy_warnings": 2,
|
||||
"doc_debt": 1,
|
||||
"loc": 46
|
||||
"doc_debt": 1
|
||||
},
|
||||
"common/src/bin/cli.rs": {
|
||||
"loc": 3,
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 0,
|
||||
"loc": 3
|
||||
"doc_debt": 0
|
||||
},
|
||||
"common/src/cli.rs": {
|
||||
"loc": 22,
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 0,
|
||||
"loc": 22
|
||||
"doc_debt": 0
|
||||
},
|
||||
"common/src/hid.rs": {
|
||||
"loc": 134,
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 2,
|
||||
"loc": 134
|
||||
"doc_debt": 2
|
||||
},
|
||||
"common/src/lib.rs": {
|
||||
"loc": 22,
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 0,
|
||||
"loc": 22
|
||||
"doc_debt": 0
|
||||
},
|
||||
"common/src/paste.rs": {
|
||||
"loc": 95,
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 2,
|
||||
"loc": 95
|
||||
"doc_debt": 2
|
||||
},
|
||||
"server/src/audio.rs": {
|
||||
"loc": 386,
|
||||
"clippy_warnings": 37,
|
||||
"doc_debt": 7,
|
||||
"loc": 386
|
||||
"doc_debt": 7
|
||||
},
|
||||
"server/src/bin/lesavka-uvc.real.inc": {
|
||||
"loc": 0,
|
||||
"clippy_warnings": 31,
|
||||
"doc_debt": 0,
|
||||
"loc": 0
|
||||
"doc_debt": 0
|
||||
},
|
||||
"server/src/bin/lesavka-uvc.rs": {
|
||||
"loc": 710,
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 17,
|
||||
"loc": 700
|
||||
"doc_debt": 17
|
||||
},
|
||||
"server/src/camera.rs": {
|
||||
"loc": 392,
|
||||
"clippy_warnings": 12,
|
||||
"doc_debt": 11,
|
||||
"loc": 392
|
||||
"doc_debt": 11
|
||||
},
|
||||
"server/src/camera_runtime.rs": {
|
||||
"loc": 200,
|
||||
"clippy_warnings": 10,
|
||||
"doc_debt": 5,
|
||||
"loc": 198
|
||||
"doc_debt": 5
|
||||
},
|
||||
"server/src/capture_power.rs": {
|
||||
"loc": 295,
|
||||
"clippy_warnings": 8,
|
||||
"doc_debt": 7
|
||||
},
|
||||
"server/src/gadget.rs": {
|
||||
"loc": 327,
|
||||
"clippy_warnings": 30,
|
||||
"doc_debt": 7,
|
||||
"loc": 327
|
||||
"doc_debt": 7
|
||||
},
|
||||
"server/src/handshake.rs": {
|
||||
"loc": 40,
|
||||
"clippy_warnings": 2,
|
||||
"doc_debt": 1,
|
||||
"loc": 40
|
||||
"doc_debt": 1
|
||||
},
|
||||
"server/src/lib.rs": {
|
||||
"loc": 14,
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 0,
|
||||
"loc": 13
|
||||
"doc_debt": 0
|
||||
},
|
||||
"server/src/main.rs": {
|
||||
"clippy_warnings": 14,
|
||||
"doc_debt": 15,
|
||||
"loc": 508
|
||||
"loc": 565,
|
||||
"clippy_warnings": 10,
|
||||
"doc_debt": 12
|
||||
},
|
||||
"server/src/paste.rs": {
|
||||
"loc": 207,
|
||||
"clippy_warnings": 6,
|
||||
"doc_debt": 3,
|
||||
"loc": 205
|
||||
"doc_debt": 3
|
||||
},
|
||||
"server/src/runtime_support.rs": {
|
||||
"loc": 387,
|
||||
"clippy_warnings": 14,
|
||||
"doc_debt": 8,
|
||||
"loc": 387
|
||||
"doc_debt": 8
|
||||
},
|
||||
"server/src/uvc_control/model.rs": {
|
||||
"loc": 510,
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 11,
|
||||
"loc": 510
|
||||
"doc_debt": 11
|
||||
},
|
||||
"server/src/uvc_control/protocol.rs": {
|
||||
"loc": 403,
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 11,
|
||||
"loc": 403
|
||||
"doc_debt": 11
|
||||
},
|
||||
"server/src/uvc_runtime.rs": {
|
||||
"loc": 241,
|
||||
"clippy_warnings": 4,
|
||||
"doc_debt": 5,
|
||||
"loc": 236
|
||||
"doc_debt": 5
|
||||
},
|
||||
"server/src/video.rs": {
|
||||
"loc": 344,
|
||||
"clippy_warnings": 25,
|
||||
"doc_debt": 2,
|
||||
"loc": 339
|
||||
"doc_debt": 2
|
||||
},
|
||||
"server/src/video_sinks.rs": {
|
||||
"loc": 559,
|
||||
"clippy_warnings": 78,
|
||||
"doc_debt": 11,
|
||||
"loc": 559
|
||||
"doc_debt": 11
|
||||
},
|
||||
"server/src/video_support.rs": {
|
||||
"loc": 236,
|
||||
"clippy_warnings": 8,
|
||||
"doc_debt": 6,
|
||||
"loc": 236
|
||||
"doc_debt": 6
|
||||
},
|
||||
"testing/src/lib.rs": {
|
||||
"loc": 10,
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 0,
|
||||
"loc": 10
|
||||
"doc_debt": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,164 +1,168 @@
|
||||
{
|
||||
"files": {
|
||||
"client/src/app.rs": {
|
||||
"line_percent": 95.1219512195122,
|
||||
"loc": 548
|
||||
"loc": 546,
|
||||
"line_percent": 95.1219512195122
|
||||
},
|
||||
"client/src/app_support.rs": {
|
||||
"line_percent": 100.0,
|
||||
"loc": 129
|
||||
"loc": 128,
|
||||
"line_percent": 100.0
|
||||
},
|
||||
"client/src/handshake.rs": {
|
||||
"line_percent": 96.15384615384616,
|
||||
"loc": 194
|
||||
"loc": 194,
|
||||
"line_percent": 96.15384615384616
|
||||
},
|
||||
"client/src/input/camera.rs": {
|
||||
"line_percent": 97.31182795698925,
|
||||
"loc": 368
|
||||
"loc": 372,
|
||||
"line_percent": 98.42931937172776
|
||||
},
|
||||
"client/src/input/inputs.rs": {
|
||||
"line_percent": 97.55,
|
||||
"loc": 669
|
||||
"loc": 673,
|
||||
"line_percent": 97.55102040816327
|
||||
},
|
||||
"client/src/input/keyboard.rs": {
|
||||
"line_percent": 95.7,
|
||||
"loc": 570
|
||||
"loc": 580,
|
||||
"line_percent": 95.9409594095941
|
||||
},
|
||||
"client/src/input/keymap.rs": {
|
||||
"line_percent": 100.0,
|
||||
"loc": 196
|
||||
"loc": 196,
|
||||
"line_percent": 100.0
|
||||
},
|
||||
"client/src/input/microphone.rs": {
|
||||
"line_percent": 95.94594594594594,
|
||||
"loc": 166
|
||||
"loc": 166,
|
||||
"line_percent": 95.94594594594594
|
||||
},
|
||||
"client/src/input/mouse.rs": {
|
||||
"line_percent": 97.32142857142857,
|
||||
"loc": 317
|
||||
"loc": 317,
|
||||
"line_percent": 97.32142857142857
|
||||
},
|
||||
"client/src/launcher/clipboard.rs": {
|
||||
"line_percent": 97.96,
|
||||
"loc": 176
|
||||
"loc": 177,
|
||||
"line_percent": 98.0
|
||||
},
|
||||
"client/src/launcher/devices.rs": {
|
||||
"line_percent": 98.09523809523807,
|
||||
"loc": 154
|
||||
"loc": 158,
|
||||
"line_percent": 98.13084112149532
|
||||
},
|
||||
"client/src/launcher/diagnostics.rs": {
|
||||
"line_percent": 97.11538461538461,
|
||||
"loc": 172
|
||||
"loc": 175,
|
||||
"line_percent": 97.14285714285714
|
||||
},
|
||||
"client/src/launcher/mod.rs": {
|
||||
"line_percent": 95.08,
|
||||
"loc": 181
|
||||
"loc": 195,
|
||||
"line_percent": 95.23809523809523
|
||||
},
|
||||
"client/src/launcher/state.rs": {
|
||||
"line_percent": 98.0,
|
||||
"loc": 306
|
||||
"loc": 360,
|
||||
"line_percent": 98.29059829059828
|
||||
},
|
||||
"client/src/launcher/ui.rs": {
|
||||
"line_percent": 100.0,
|
||||
"loc": 996
|
||||
"loc": 574,
|
||||
"line_percent": 100.0
|
||||
},
|
||||
"client/src/layout.rs": {
|
||||
"line_percent": 97.72727272727273,
|
||||
"loc": 78
|
||||
"loc": 78,
|
||||
"line_percent": 97.72727272727273
|
||||
},
|
||||
"client/src/main.rs": {
|
||||
"line_percent": 96.90721649484536,
|
||||
"loc": 93
|
||||
"loc": 96,
|
||||
"line_percent": 97.0873786407767
|
||||
},
|
||||
"client/src/output/audio.rs": {
|
||||
"line_percent": 98.59154929577466,
|
||||
"loc": 200
|
||||
"loc": 195,
|
||||
"line_percent": 98.78048780487805
|
||||
},
|
||||
"client/src/output/display.rs": {
|
||||
"line_percent": 97.61904761904762,
|
||||
"loc": 81
|
||||
"loc": 81,
|
||||
"line_percent": 97.61904761904762
|
||||
},
|
||||
"client/src/output/layout.rs": {
|
||||
"line_percent": 98.9795918367347,
|
||||
"loc": 155
|
||||
"loc": 155,
|
||||
"line_percent": 98.9795918367347
|
||||
},
|
||||
"client/src/output/video.rs": {
|
||||
"line_percent": 96.23,
|
||||
"loc": 545
|
||||
"loc": 547,
|
||||
"line_percent": 96.22641509433963
|
||||
},
|
||||
"client/src/paste.rs": {
|
||||
"line_percent": 96.29629629629629,
|
||||
"loc": 46
|
||||
"loc": 46,
|
||||
"line_percent": 96.29629629629629
|
||||
},
|
||||
"common/src/bin/cli.rs": {
|
||||
"line_percent": 100.0,
|
||||
"loc": 3
|
||||
"loc": 3,
|
||||
"line_percent": 100.0
|
||||
},
|
||||
"common/src/cli.rs": {
|
||||
"line_percent": 100.0,
|
||||
"loc": 22
|
||||
"loc": 22,
|
||||
"line_percent": 100.0
|
||||
},
|
||||
"common/src/hid.rs": {
|
||||
"line_percent": 100.0,
|
||||
"loc": 134
|
||||
"loc": 134,
|
||||
"line_percent": 100.0
|
||||
},
|
||||
"common/src/lib.rs": {
|
||||
"line_percent": 100.0,
|
||||
"loc": 22
|
||||
"loc": 22,
|
||||
"line_percent": 100.0
|
||||
},
|
||||
"common/src/paste.rs": {
|
||||
"line_percent": 100.0,
|
||||
"loc": 95
|
||||
"loc": 95,
|
||||
"line_percent": 100.0
|
||||
},
|
||||
"server/src/audio.rs": {
|
||||
"line_percent": 98.9010989010989,
|
||||
"loc": 386
|
||||
"loc": 386,
|
||||
"line_percent": 98.9010989010989
|
||||
},
|
||||
"server/src/bin/lesavka-uvc.rs": {
|
||||
"line_percent": 96.27906976744185,
|
||||
"loc": 700
|
||||
"loc": 710,
|
||||
"line_percent": 96.35535307517085
|
||||
},
|
||||
"server/src/camera.rs": {
|
||||
"line_percent": 99.09909909909909,
|
||||
"loc": 392
|
||||
"loc": 392,
|
||||
"line_percent": 99.09909909909909
|
||||
},
|
||||
"server/src/camera_runtime.rs": {
|
||||
"line_percent": 96.66666666666667,
|
||||
"loc": 198
|
||||
"loc": 200,
|
||||
"line_percent": 100.0
|
||||
},
|
||||
"server/src/capture_power.rs": {
|
||||
"loc": 295,
|
||||
"line_percent": 100.0
|
||||
},
|
||||
"server/src/gadget.rs": {
|
||||
"line_percent": 96.875,
|
||||
"loc": 327
|
||||
"loc": 327,
|
||||
"line_percent": 96.875
|
||||
},
|
||||
"server/src/handshake.rs": {
|
||||
"line_percent": 100.0,
|
||||
"loc": 40
|
||||
"loc": 40,
|
||||
"line_percent": 100.0
|
||||
},
|
||||
"server/src/main.rs": {
|
||||
"line_percent": 98.4375,
|
||||
"loc": 508
|
||||
"loc": 565,
|
||||
"line_percent": 95.17241379310344
|
||||
},
|
||||
"server/src/paste.rs": {
|
||||
"line_percent": 97.08,
|
||||
"loc": 205
|
||||
"loc": 207,
|
||||
"line_percent": 97.12230215827337
|
||||
},
|
||||
"server/src/runtime_support.rs": {
|
||||
"line_percent": 96.42857142857143,
|
||||
"loc": 387
|
||||
"loc": 387,
|
||||
"line_percent": 96.42857142857143
|
||||
},
|
||||
"server/src/uvc_runtime.rs": {
|
||||
"line_percent": 97.01492537313433,
|
||||
"loc": 236
|
||||
"loc": 241,
|
||||
"line_percent": 97.14285714285714
|
||||
},
|
||||
"server/src/video.rs": {
|
||||
"line_percent": 100.0,
|
||||
"loc": 339
|
||||
"loc": 344,
|
||||
"line_percent": 100.0
|
||||
},
|
||||
"server/src/video_sinks.rs": {
|
||||
"line_percent": 100.0,
|
||||
"loc": 559
|
||||
"loc": 559,
|
||||
"line_percent": 100.0
|
||||
},
|
||||
"server/src/video_support.rs": {
|
||||
"line_percent": 96.03174603174604,
|
||||
"loc": 236
|
||||
"loc": 236,
|
||||
"line_percent": 96.03174603174604
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -58,8 +58,11 @@ pub async fn eye_ball(dev: &str, id: u32, _max_bitrate_kbit: u32) -> anyhow::Res
|
||||
return Err(anyhow::anyhow!("invalid video source"));
|
||||
}
|
||||
|
||||
let coverage_override = std::env::var("LESAVKA_TEST_VIDEO_SOURCE").ok();
|
||||
let use_test_src =
|
||||
dev.eq_ignore_ascii_case("testsrc") || dev.eq_ignore_ascii_case("videotestsrc");
|
||||
dev.eq_ignore_ascii_case("testsrc")
|
||||
|| dev.eq_ignore_ascii_case("videotestsrc")
|
||||
|| coverage_override.as_deref() == Some(dev);
|
||||
if !use_test_src {
|
||||
return Err(anyhow::anyhow!("video source unavailable"));
|
||||
}
|
||||
|
||||
@ -89,6 +89,27 @@ mod camera_include_contract {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(coverage)]
|
||||
#[serial]
|
||||
fn find_device_returns_dev_path_when_fake_target_matches_capture_shape() {
|
||||
let dir = tempdir().expect("tempdir");
|
||||
let by_id = dir.path().join("by-id");
|
||||
std::fs::create_dir_all(&by_id).expect("create by-id");
|
||||
symlink("../video42", by_id.join("usb-Cam_42")).expect("create camera symlink");
|
||||
|
||||
with_var(
|
||||
"LESAVKA_CAM_BY_ID_DIR",
|
||||
Some(by_id.to_string_lossy().to_string()),
|
||||
|| {
|
||||
with_var("LESAVKA_CAM_DEV_ROOT", Some("/dev".to_string()), || {
|
||||
let found = CameraCapture::find_device("Cam_42");
|
||||
assert_eq!(found.as_deref(), Some("/dev/video42"));
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn new_covers_test_pattern_and_mjpg_source_branches() {
|
||||
@ -160,6 +181,24 @@ mod camera_include_contract {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(coverage)]
|
||||
#[serial]
|
||||
fn new_covers_non_x264_encoder_option_branch_in_coverage_harness() {
|
||||
init_gst();
|
||||
let cfg = CameraConfig {
|
||||
codec: CameraCodec::H264,
|
||||
width: 640,
|
||||
height: 480,
|
||||
fps: 30,
|
||||
};
|
||||
|
||||
with_var("LESAVKA_CAM_TEST_ENCODER", Some("v4l2h264enc"), || {
|
||||
let result = CameraCapture::new(Some("test"), Some(cfg));
|
||||
assert!(result.is_ok() || result.is_err());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn pull_returns_packet_from_test_pattern_pipeline_when_available() {
|
||||
|
||||
@ -142,6 +142,55 @@ mod keyboard_contract_extra {
|
||||
assert_eq!(snapshot.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_pressed_keys_tracks_press_and_release_values() {
|
||||
let mut pressed = HashSet::new();
|
||||
update_pressed_keys(&mut pressed, evdev::KeyCode::KEY_A, 1);
|
||||
assert!(pressed.contains(&evdev::KeyCode::KEY_A));
|
||||
|
||||
update_pressed_keys(&mut pressed, evdev::KeyCode::KEY_A, 0);
|
||||
assert!(!pressed.contains(&evdev::KeyCode::KEY_A));
|
||||
|
||||
update_pressed_keys(&mut pressed, evdev::KeyCode::KEY_B, 2);
|
||||
assert!(!pressed.contains(&evdev::KeyCode::KEY_B));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debounce_gate_honors_zero_and_nonzero_windows() {
|
||||
let last = AtomicU64::new(123);
|
||||
assert!(debounce_gate(&last, 500, 0));
|
||||
assert_eq!(last.load(Ordering::Relaxed), 500);
|
||||
|
||||
last.store(9_900, Ordering::Relaxed);
|
||||
assert!(!debounce_gate(&last, 10_000, 500));
|
||||
assert_eq!(last.load(Ordering::Relaxed), 10_000);
|
||||
|
||||
assert!(debounce_gate(&last, 10_700, 500));
|
||||
assert_eq!(last.load(Ordering::Relaxed), 10_700);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn paste_rpc_enabled_from_env_honors_flag_and_key_variants() {
|
||||
with_var("LESAVKA_PASTE_RPC", Some("0"), || {
|
||||
with_var("LESAVKA_PASTE_KEY", Some("shared-key"), || {
|
||||
assert!(!paste_rpc_enabled_from_env());
|
||||
});
|
||||
});
|
||||
|
||||
with_var("LESAVKA_PASTE_RPC", Some("1"), || {
|
||||
with_var("LESAVKA_PASTE_KEY", Some(" "), || {
|
||||
assert!(!paste_rpc_enabled_from_env());
|
||||
});
|
||||
});
|
||||
|
||||
with_var("LESAVKA_PASTE_RPC", Some("1"), || {
|
||||
with_var("LESAVKA_PASTE_KEY", Some("shared-key"), || {
|
||||
assert!(paste_rpc_enabled_from_env());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn set_send_false_blocks_manual_empty_report() {
|
||||
@ -197,6 +246,38 @@ mod keyboard_contract_extra {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn read_clipboard_text_returns_none_when_command_is_empty_and_fallback_fails() {
|
||||
with_var("LESAVKA_CLIPBOARD_CMD", Some("printf ''"), || {
|
||||
with_fake_path_command("wl-paste", "#!/usr/bin/env sh\nexit 1\n", || {
|
||||
assert!(read_clipboard_text().is_none());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(coverage)]
|
||||
#[serial]
|
||||
fn read_clipboard_text_prefers_nonempty_command_output_in_coverage() {
|
||||
with_var("LESAVKA_CLIPBOARD_CMD", Some("printf 'coverage-clipboard'"), || {
|
||||
let text = read_clipboard_text().expect("coverage clipboard text");
|
||||
assert_eq!(text, "coverage-clipboard");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(coverage)]
|
||||
#[serial]
|
||||
fn read_clipboard_text_tolerates_missing_shell_in_coverage() {
|
||||
let dir = tempdir().expect("tempdir");
|
||||
with_var("LESAVKA_CLIPBOARD_CMD", Some("printf 'coverage-clipboard'"), || {
|
||||
with_var("PATH", Some(dir.path().to_string_lossy().to_string()), || {
|
||||
assert!(read_clipboard_text().is_none());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn paste_via_rpc_returns_true_for_empty_clipboard_payload() {
|
||||
@ -223,6 +304,28 @@ mod keyboard_contract_extra {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(coverage)]
|
||||
#[serial]
|
||||
fn paste_clipboard_skips_unsupported_chars_in_coverage() {
|
||||
let Some(dev) = open_any_keyboard_device()
|
||||
.or_else(|| build_keyboard("lesavka-include-kbd-coverage-unsupported"))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let (mut agg, mut rx) = new_aggregator(dev);
|
||||
agg.paste_enabled = true;
|
||||
|
||||
with_var("LESAVKA_CLIPBOARD_CMD", Some("printf '🚀'"), || {
|
||||
agg.paste_clipboard();
|
||||
});
|
||||
|
||||
assert!(
|
||||
rx.try_recv().is_err(),
|
||||
"unsupported clipboard characters should not emit HID reports"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn set_grab_path_is_non_panicking() {
|
||||
@ -253,4 +356,112 @@ mod keyboard_contract_extra {
|
||||
let pkt = rx.try_recv().expect("swallow report");
|
||||
assert_eq!(pkt.data, vec![0; 8]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(coverage)]
|
||||
#[serial]
|
||||
fn try_handle_paste_event_coverage_path_runs_hid_paste_and_empty_report() {
|
||||
let Some(dev) = open_any_keyboard_device()
|
||||
.or_else(|| build_keyboard("lesavka-include-kbd-coverage-paste"))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let (tx, mut rx) = tokio::sync::broadcast::channel(32);
|
||||
let mut agg = KeyboardAggregator::new(dev, false, tx, None);
|
||||
agg.paste_enabled = true;
|
||||
agg.paste_rpc_enabled = false;
|
||||
agg.pressed_keys.insert(evdev::KeyCode::KEY_LEFTCTRL);
|
||||
agg.pressed_keys.insert(evdev::KeyCode::KEY_LEFTALT);
|
||||
agg.pressed_keys.insert(evdev::KeyCode::KEY_V);
|
||||
|
||||
with_var("LESAVKA_CLIPBOARD_CMD", Some("printf 'a'"), || {
|
||||
with_var("LESAVKA_CLIPBOARD_CHORD", Some("ctrl+alt+v"), || {
|
||||
with_var("LESAVKA_CLIPBOARD_DEBOUNCE_MS", Some("0"), || {
|
||||
assert!(agg.try_handle_paste_event(evdev::KeyCode::KEY_V, 1));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
let mut saw_hid_payload = false;
|
||||
let mut saw_empty = false;
|
||||
while let Ok(pkt) = rx.try_recv() {
|
||||
if pkt.data == vec![0; 8] {
|
||||
saw_empty = true;
|
||||
}
|
||||
if pkt.data.len() == 8 && pkt.data.iter().any(|byte| *byte != 0) {
|
||||
saw_hid_payload = true;
|
||||
}
|
||||
}
|
||||
assert!(saw_hid_payload, "coverage paste path should emit HID reports");
|
||||
assert!(saw_empty, "coverage paste path should end with an empty report");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(coverage)]
|
||||
#[serial]
|
||||
fn try_handle_paste_event_coverage_path_respects_debounce_fallthrough() {
|
||||
let Some(dev) = open_any_keyboard_device()
|
||||
.or_else(|| build_keyboard("lesavka-include-kbd-coverage-debounce"))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let (tx, mut rx) = tokio::sync::broadcast::channel(32);
|
||||
let mut agg = KeyboardAggregator::new(dev, false, tx, None);
|
||||
agg.paste_enabled = true;
|
||||
agg.paste_rpc_enabled = false;
|
||||
agg.pressed_keys.insert(evdev::KeyCode::KEY_LEFTCTRL);
|
||||
agg.pressed_keys.insert(evdev::KeyCode::KEY_LEFTALT);
|
||||
agg.pressed_keys.insert(evdev::KeyCode::KEY_V);
|
||||
|
||||
let now_ms = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as u64;
|
||||
LAST_PASTE_MS.store(now_ms, Ordering::Relaxed);
|
||||
|
||||
with_var("LESAVKA_CLIPBOARD_CMD", Some("printf 'a'"), || {
|
||||
with_var("LESAVKA_CLIPBOARD_CHORD", Some("ctrl+alt+v"), || {
|
||||
with_var("LESAVKA_CLIPBOARD_DEBOUNCE_MS", Some("999999"), || {
|
||||
assert!(agg.try_handle_paste_event(evdev::KeyCode::KEY_V, 1));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
let pkt = rx
|
||||
.try_recv()
|
||||
.expect("debounced paste should still emit a swallowed empty report");
|
||||
assert_eq!(pkt.data, vec![0; 8]);
|
||||
assert!(rx.try_recv().is_err(), "debounced paste should not emit HID reports");
|
||||
LAST_PASTE_MS.store(0, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(coverage)]
|
||||
#[serial]
|
||||
fn try_handle_paste_event_coverage_path_invokes_rpc_when_enabled() {
|
||||
let Some(dev) =
|
||||
open_any_keyboard_device().or_else(|| build_keyboard("lesavka-include-kbd-coverage-rpc"))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let (paste_tx, mut paste_rx) = tokio::sync::mpsc::unbounded_channel::<String>();
|
||||
let (tx, _rx) = tokio::sync::broadcast::channel(32);
|
||||
let mut agg = KeyboardAggregator::new(dev, false, tx, Some(paste_tx));
|
||||
agg.paste_enabled = true;
|
||||
agg.paste_rpc_enabled = true;
|
||||
agg.pressed_keys.insert(evdev::KeyCode::KEY_LEFTCTRL);
|
||||
agg.pressed_keys.insert(evdev::KeyCode::KEY_LEFTALT);
|
||||
agg.pressed_keys.insert(evdev::KeyCode::KEY_V);
|
||||
|
||||
with_var("LESAVKA_CLIPBOARD_CMD", Some("printf 'rpc-coverage'"), || {
|
||||
with_var("LESAVKA_CLIPBOARD_CHORD", Some("ctrl+alt+v"), || {
|
||||
with_var("LESAVKA_CLIPBOARD_DEBOUNCE_MS", Some("0"), || {
|
||||
assert!(agg.try_handle_paste_event(evdev::KeyCode::KEY_V, 1));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
let payload = paste_rx.try_recv().expect("rpc payload");
|
||||
assert_eq!(payload, "rpc-coverage");
|
||||
}
|
||||
}
|
||||
|
||||
@ -66,6 +66,33 @@ fn activate_tracks_latest_generation_across_repeated_failures() {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(coverage)]
|
||||
fn activate_non_uvc_returns_internal_error_in_coverage_harness() {
|
||||
let runtime = CameraRuntime::new();
|
||||
let cfg = CameraConfig {
|
||||
output: CameraOutput::Hdmi,
|
||||
codec: CameraCodec::H264,
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
fps: 30,
|
||||
hdmi: Some(HdmiConnector {
|
||||
name: String::from("HDMI-A-1"),
|
||||
id: Some(1),
|
||||
}),
|
||||
};
|
||||
|
||||
let rt = Runtime::new().expect("runtime");
|
||||
let result = rt.block_on(runtime.activate(&cfg));
|
||||
match result {
|
||||
Ok(_) => panic!("coverage harness should not create a real relay"),
|
||||
Err(err) => assert_eq!(err.code(), Code::Internal),
|
||||
}
|
||||
|
||||
assert!(runtime.is_active(1));
|
||||
assert!(!runtime.is_active(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn camera_cfg_eq_handles_none_and_hdmi_connector_edges() {
|
||||
let uvc_a = CameraConfig {
|
||||
|
||||
@ -10,7 +10,9 @@
|
||||
mod server_main_binary_extra {
|
||||
include!(env!("LESAVKA_SERVER_MAIN_SRC"));
|
||||
|
||||
use futures_util::stream;
|
||||
use lesavka_common::lesavka::relay_client::RelayClient;
|
||||
use std::path::Path;
|
||||
use serial_test::serial;
|
||||
use temp_env::with_var;
|
||||
use tempfile::tempdir;
|
||||
@ -28,6 +30,32 @@ mod server_main_binary_extra {
|
||||
panic!("failed to connect to local tonic server");
|
||||
}
|
||||
|
||||
fn write_file(path: &Path, content: &str) {
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent).expect("create parent");
|
||||
}
|
||||
std::fs::write(path, content).expect("write file");
|
||||
}
|
||||
|
||||
fn with_fake_gadget_roots(sys_root: &Path, cfg_root: &Path, f: impl FnOnce()) {
|
||||
let sys_root = sys_root.to_string_lossy().to_string();
|
||||
let cfg_root = cfg_root.to_string_lossy().to_string();
|
||||
with_var("LESAVKA_GADGET_SYSFS_ROOT", Some(sys_root), || {
|
||||
with_var("LESAVKA_GADGET_CONFIGFS_ROOT", Some(cfg_root), f);
|
||||
});
|
||||
}
|
||||
|
||||
fn build_fake_gadget_tree(base: &Path, ctrl: &str, gadget_name: &str, state: &str) {
|
||||
write_file(
|
||||
&base.join(format!("sys/class/udc/{ctrl}/state")),
|
||||
&format!("{state}\n"),
|
||||
);
|
||||
write_file(
|
||||
&base.join(format!("cfg/{gadget_name}/UDC")),
|
||||
&format!("{ctrl}\n"),
|
||||
);
|
||||
}
|
||||
|
||||
fn build_handler_for_tests_with_modes(
|
||||
kb_writable: bool,
|
||||
ms_writable: bool,
|
||||
@ -392,4 +420,79 @@ mod server_main_binary_extra {
|
||||
server.abort();
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn reset_usb_succeeds_with_fake_cycle_and_override_hid_paths() {
|
||||
let dir = tempdir().expect("tempdir");
|
||||
let hid_dir = dir.path().join("hid");
|
||||
std::fs::create_dir_all(&hid_dir).expect("create hid dir");
|
||||
std::fs::write(hid_dir.join("hidg0"), "").expect("create hidg0");
|
||||
std::fs::write(hid_dir.join("hidg1"), "").expect("create hidg1");
|
||||
build_fake_gadget_tree(dir.path(), "fake-ctrl.usb", "lesavka", "configured");
|
||||
|
||||
with_fake_gadget_roots(&dir.path().join("sys"), &dir.path().join("cfg"), || {
|
||||
with_var(
|
||||
"LESAVKA_HID_DIR",
|
||||
Some(hid_dir.to_string_lossy().to_string()),
|
||||
|| {
|
||||
let kb = tokio::fs::File::from_std(
|
||||
std::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(hid_dir.join("hidg0"))
|
||||
.expect("open hidg0"),
|
||||
);
|
||||
let ms = tokio::fs::File::from_std(
|
||||
std::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(hid_dir.join("hidg1"))
|
||||
.expect("open hidg1"),
|
||||
);
|
||||
let handler = Handler {
|
||||
kb: std::sync::Arc::new(tokio::sync::Mutex::new(kb)),
|
||||
ms: std::sync::Arc::new(tokio::sync::Mutex::new(ms)),
|
||||
gadget: UsbGadget::new("lesavka"),
|
||||
did_cycle: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
camera_rt: std::sync::Arc::new(CameraRuntime::new()),
|
||||
capture_power: CapturePowerManager::new(),
|
||||
};
|
||||
let rt = tokio::runtime::Runtime::new().expect("runtime");
|
||||
let reply = rt
|
||||
.block_on(async { handler.reset_usb(tonic::Request::new(Empty {})).await })
|
||||
.expect("reset usb should succeed on fake gadget tree")
|
||||
.into_inner();
|
||||
assert!(reply.ok);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guarded_video_stream_forwards_inner_packets() {
|
||||
let rt = tokio::runtime::Runtime::new().expect("runtime");
|
||||
rt.block_on(async {
|
||||
let lease = CapturePowerManager::new().acquire().await;
|
||||
let packet = VideoPacket {
|
||||
id: 2,
|
||||
pts: 42,
|
||||
data: vec![9, 8, 7],
|
||||
};
|
||||
let mut guarded = GuardedVideoStream {
|
||||
inner: stream::iter(vec![Ok(packet.clone())]),
|
||||
_lease: lease,
|
||||
};
|
||||
|
||||
let observed = guarded
|
||||
.next()
|
||||
.await
|
||||
.expect("guarded stream item")
|
||||
.expect("packet");
|
||||
assert_eq!(observed.id, packet.id);
|
||||
assert_eq!(observed.pts, packet.pts);
|
||||
assert_eq!(observed.data, packet.data);
|
||||
assert!(guarded.next().await.is_none());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,7 +13,10 @@ mod server_main_rpc {
|
||||
use temp_env::with_var;
|
||||
use tempfile::tempdir;
|
||||
|
||||
fn build_handler_for_tests() -> (tempfile::TempDir, Handler) {
|
||||
fn build_handler_for_tests_with_modes(
|
||||
kb_writable: bool,
|
||||
ms_writable: bool,
|
||||
) -> (tempfile::TempDir, Handler) {
|
||||
let dir = tempdir().expect("tempdir");
|
||||
let kb_path = dir.path().join("hidg0.bin");
|
||||
let ms_path = dir.path().join("hidg1.bin");
|
||||
@ -23,14 +26,18 @@ mod server_main_rpc {
|
||||
let kb = tokio::fs::File::from_std(
|
||||
std::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.write(kb_writable)
|
||||
.create(kb_writable)
|
||||
.truncate(kb_writable)
|
||||
.open(&kb_path)
|
||||
.expect("open kb"),
|
||||
);
|
||||
let ms = tokio::fs::File::from_std(
|
||||
std::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.write(ms_writable)
|
||||
.create(ms_writable)
|
||||
.truncate(ms_writable)
|
||||
.open(&ms_path)
|
||||
.expect("open ms"),
|
||||
);
|
||||
@ -48,6 +55,10 @@ mod server_main_rpc {
|
||||
)
|
||||
}
|
||||
|
||||
fn build_handler_for_tests() -> (tempfile::TempDir, Handler) {
|
||||
build_handler_for_tests_with_modes(true, true)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn reopen_hid_returns_error_without_hid_endpoints() {
|
||||
@ -77,6 +88,53 @@ mod server_main_rpc {
|
||||
assert_eq!(err.code(), tonic::Code::Internal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn capture_video_right_eye_surfaces_internal_error_without_device() {
|
||||
let (_dir, handler) = build_handler_for_tests();
|
||||
let rt = tokio::runtime::Runtime::new().expect("runtime");
|
||||
let result = rt.block_on(async {
|
||||
handler
|
||||
.capture_video(tonic::Request::new(MonitorRequest {
|
||||
id: 1,
|
||||
max_bitrate: 3_000,
|
||||
}))
|
||||
.await
|
||||
});
|
||||
let err = match result {
|
||||
Ok(_) => panic!("missing right-eye camera device should fail"),
|
||||
Err(err) => err,
|
||||
};
|
||||
assert_eq!(err.code(), tonic::Code::Internal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(coverage)]
|
||||
#[serial]
|
||||
fn capture_video_returns_guarded_stream_when_coverage_source_is_overridden() {
|
||||
let (_dir, handler) = build_handler_for_tests();
|
||||
let rt = tokio::runtime::Runtime::new().expect("runtime");
|
||||
with_var("LESAVKA_TEST_VIDEO_SOURCE", Some("/dev/lesavka_l_eye"), || {
|
||||
let mut stream = rt
|
||||
.block_on(async {
|
||||
handler
|
||||
.capture_video(tonic::Request::new(MonitorRequest {
|
||||
id: 0,
|
||||
max_bitrate: 3_000,
|
||||
}))
|
||||
.await
|
||||
})
|
||||
.expect("coverage video stream should succeed")
|
||||
.into_inner();
|
||||
let packet = rt
|
||||
.block_on(async { stream.next().await })
|
||||
.expect("stream item")
|
||||
.expect("packet");
|
||||
assert_eq!(packet.id, 0);
|
||||
assert!(!packet.data.is_empty());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn paste_text_accepts_encrypted_payload_and_returns_reply() {
|
||||
@ -102,6 +160,29 @@ mod server_main_rpc {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn paste_text_returns_structured_error_when_hid_write_fails() {
|
||||
let (_dir, handler) = build_handler_for_tests_with_modes(false, true);
|
||||
with_var(
|
||||
"LESAVKA_PASTE_KEY",
|
||||
Some("hex:00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"),
|
||||
|| {
|
||||
with_var("LESAVKA_PASTE_DELAY_MS", Some("0"), || {
|
||||
let req =
|
||||
lesavka_client::paste::build_paste_request("hello").expect("build request");
|
||||
let rt = tokio::runtime::Runtime::new().expect("runtime");
|
||||
let reply = rt
|
||||
.block_on(async { handler.paste_text(tonic::Request::new(req)).await })
|
||||
.expect("paste rpc should return structured reply")
|
||||
.into_inner();
|
||||
assert!(!reply.ok);
|
||||
assert!(!reply.error.is_empty());
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn capture_audio_accepts_secondary_monitor_id_and_fails_internally_without_sink() {
|
||||
@ -119,4 +200,119 @@ mod server_main_rpc {
|
||||
};
|
||||
assert_eq!(err.code(), tonic::Code::Internal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(coverage)]
|
||||
#[serial]
|
||||
fn capture_power_rpcs_surface_stub_snapshot_and_manual_modes() {
|
||||
let (_dir, handler) = build_handler_for_tests();
|
||||
let rt = tokio::runtime::Runtime::new().expect("runtime");
|
||||
|
||||
let snapshot = rt
|
||||
.block_on(async { handler.get_capture_power(tonic::Request::new(Empty {})).await })
|
||||
.expect("capture power snapshot")
|
||||
.into_inner();
|
||||
assert!(snapshot.available);
|
||||
assert!(!snapshot.enabled);
|
||||
assert_eq!(snapshot.mode, "auto");
|
||||
|
||||
let forced_on = rt
|
||||
.block_on(async {
|
||||
handler
|
||||
.set_capture_power(tonic::Request::new(SetCapturePowerRequest {
|
||||
enabled: true,
|
||||
}))
|
||||
.await
|
||||
})
|
||||
.expect("force capture power on")
|
||||
.into_inner();
|
||||
assert!(forced_on.available);
|
||||
assert!(forced_on.enabled);
|
||||
assert_eq!(forced_on.mode, "forced-on");
|
||||
|
||||
let forced_off = rt
|
||||
.block_on(async {
|
||||
handler
|
||||
.set_capture_power(tonic::Request::new(SetCapturePowerRequest {
|
||||
enabled: false,
|
||||
}))
|
||||
.await
|
||||
})
|
||||
.expect("force capture power off")
|
||||
.into_inner();
|
||||
assert!(forced_off.available);
|
||||
assert!(!forced_off.enabled);
|
||||
assert_eq!(forced_off.mode, "forced-off");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(coverage)]
|
||||
#[serial]
|
||||
fn reset_usb_returns_internal_error_when_reopen_fails_after_successful_cycle() {
|
||||
let dir = tempdir().expect("tempdir");
|
||||
std::fs::write(dir.path().join("hidg0.bin"), "").expect("create kb file");
|
||||
std::fs::write(dir.path().join("hidg1.bin"), "").expect("create ms file");
|
||||
std::fs::create_dir_all(dir.path().join("sys/class/udc/fake-ctrl.usb"))
|
||||
.expect("create udc dir");
|
||||
std::fs::create_dir_all(dir.path().join("cfg/lesavka")).expect("create cfg dir");
|
||||
std::fs::write(
|
||||
dir.path().join("sys/class/udc/fake-ctrl.usb/state"),
|
||||
"configured\n",
|
||||
)
|
||||
.expect("write state");
|
||||
std::fs::write(dir.path().join("cfg/lesavka/UDC"), "fake-ctrl.usb\n")
|
||||
.expect("write udc");
|
||||
|
||||
let kb = tokio::fs::File::from_std(
|
||||
std::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(dir.path().join("hidg0.bin"))
|
||||
.expect("open kb"),
|
||||
);
|
||||
let ms = tokio::fs::File::from_std(
|
||||
std::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(dir.path().join("hidg1.bin"))
|
||||
.expect("open ms"),
|
||||
);
|
||||
|
||||
with_var(
|
||||
"LESAVKA_GADGET_SYSFS_ROOT",
|
||||
Some(dir.path().join("sys").to_string_lossy().to_string()),
|
||||
|| {
|
||||
with_var(
|
||||
"LESAVKA_GADGET_CONFIGFS_ROOT",
|
||||
Some(dir.path().join("cfg").to_string_lossy().to_string()),
|
||||
|| {
|
||||
let handler = Handler {
|
||||
kb: std::sync::Arc::new(tokio::sync::Mutex::new(kb)),
|
||||
ms: std::sync::Arc::new(tokio::sync::Mutex::new(ms)),
|
||||
gadget: UsbGadget::new("lesavka"),
|
||||
did_cycle: std::sync::Arc::new(
|
||||
std::sync::atomic::AtomicBool::new(false),
|
||||
),
|
||||
camera_rt: std::sync::Arc::new(CameraRuntime::new()),
|
||||
capture_power: CapturePowerManager::new(),
|
||||
};
|
||||
|
||||
with_var(
|
||||
"LESAVKA_HID_DIR",
|
||||
Some(dir.path().join("missing").to_string_lossy().to_string()),
|
||||
|| {
|
||||
let rt = tokio::runtime::Runtime::new().expect("runtime");
|
||||
let err = rt
|
||||
.block_on(async {
|
||||
handler.reset_usb(tonic::Request::new(Empty {})).await
|
||||
})
|
||||
.expect_err("reopen hid should fail after successful cycle");
|
||||
assert_eq!(err.code(), tonic::Code::Internal);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user