171 lines
6.0 KiB
Rust
171 lines
6.0 KiB
Rust
|
|
//! Coverage for keyboard paste debounce and RPC branches.
|
||
|
|
//!
|
||
|
|
//! Scope: include keyboard input source and cover paste debounce/RPC paths.
|
||
|
|
//! Targets: `client/src/input/keyboard.rs`.
|
||
|
|
//! Why: paste routing has operator-visible side effects, and keeping it in a
|
||
|
|
//! focused contract keeps keyboard coverage under the 500 LOC hygiene cap.
|
||
|
|
|
||
|
|
mod keymap {
|
||
|
|
pub use lesavka_client::input::keymap::*;
|
||
|
|
}
|
||
|
|
|
||
|
|
#[allow(warnings)]
|
||
|
|
mod keyboard_paste_rpc_contract {
|
||
|
|
include!(env!("LESAVKA_CLIENT_KEYBOARD_SRC"));
|
||
|
|
|
||
|
|
use serial_test::serial;
|
||
|
|
use temp_env::with_var;
|
||
|
|
|
||
|
|
fn open_any_keyboard_device() -> Option<evdev::Device> {
|
||
|
|
let entries = std::fs::read_dir("/dev/input").ok()?;
|
||
|
|
for entry in entries.flatten() {
|
||
|
|
let path = entry.path();
|
||
|
|
let name = path.file_name()?.to_string_lossy();
|
||
|
|
if !name.starts_with("event") {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
let dev = evdev::Device::open(path).ok()?;
|
||
|
|
let _ = dev.set_nonblocking(true);
|
||
|
|
let looks_like_keyboard = dev
|
||
|
|
.supported_keys()
|
||
|
|
.map(|keys| {
|
||
|
|
keys.contains(evdev::KeyCode::KEY_A)
|
||
|
|
&& keys.contains(evdev::KeyCode::KEY_ENTER)
|
||
|
|
&& keys.contains(evdev::KeyCode::KEY_LEFTCTRL)
|
||
|
|
})
|
||
|
|
.unwrap_or(false);
|
||
|
|
if looks_like_keyboard {
|
||
|
|
return Some(dev);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
None
|
||
|
|
}
|
||
|
|
|
||
|
|
fn build_keyboard(name: &str) -> Option<evdev::Device> {
|
||
|
|
let mut keys = evdev::AttributeSet::<evdev::KeyCode>::new();
|
||
|
|
for key in [
|
||
|
|
evdev::KeyCode::KEY_A,
|
||
|
|
evdev::KeyCode::KEY_B,
|
||
|
|
evdev::KeyCode::KEY_V,
|
||
|
|
evdev::KeyCode::KEY_LEFTCTRL,
|
||
|
|
evdev::KeyCode::KEY_LEFTALT,
|
||
|
|
] {
|
||
|
|
keys.insert(key);
|
||
|
|
}
|
||
|
|
|
||
|
|
let mut vdev = evdev::uinput::VirtualDevice::builder()
|
||
|
|
.ok()?
|
||
|
|
.name(name)
|
||
|
|
.with_keys(&keys)
|
||
|
|
.ok()?
|
||
|
|
.build()
|
||
|
|
.ok()?;
|
||
|
|
for _ in 0..40 {
|
||
|
|
if let Ok(mut nodes) = vdev.enumerate_dev_nodes_blocking() {
|
||
|
|
if let Some(Ok(path)) = nodes.next() {
|
||
|
|
let dev = evdev::Device::open(path).ok()?;
|
||
|
|
dev.set_nonblocking(true).ok()?;
|
||
|
|
return Some(dev);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
std::thread::sleep(std::time::Duration::from_millis(10));
|
||
|
|
}
|
||
|
|
None
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
#[serial]
|
||
|
|
fn live_modifier_report_honors_configured_staging_delay() {
|
||
|
|
let (tx, mut rx) = tokio::sync::broadcast::channel(4);
|
||
|
|
with_var("LESAVKA_LIVE_MODIFIER_DELAY_MS", Some("1"), || {
|
||
|
|
emit_live_keyboard_report(
|
||
|
|
&tx,
|
||
|
|
evdev::KeyCode::KEY_A,
|
||
|
|
1,
|
||
|
|
[0x01, 0, 0x04, 0, 0, 0, 0, 0],
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
let staged = rx.try_recv().expect("modifier-only report");
|
||
|
|
assert_eq!(staged.data, vec![0x01, 0, 0, 0, 0, 0, 0, 0]);
|
||
|
|
let final_report = rx.try_recv().expect("final key report");
|
||
|
|
assert_eq!(final_report.data, vec![0x01, 0, 0x04, 0, 0, 0, 0, 0]);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[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");
|
||
|
|
}
|
||
|
|
}
|