137 lines
4.6 KiB
Rust
137 lines
4.6 KiB
Rust
//! Keyboard event-processing coverage for swallowed and live-report paths.
|
|
//!
|
|
//! Scope: include keyboard aggregation and drive synthetic evdev updates through
|
|
//! `process_events`/`drain_key_updates`.
|
|
//! Targets: `client/src/input/keyboard.rs`.
|
|
//! Why: paste chords must be swallowed cleanly while normal keys keep flowing.
|
|
|
|
mod keymap {
|
|
pub use lesavka_client::input::keymap::*;
|
|
}
|
|
|
|
#[allow(warnings)]
|
|
mod keyboard_process_contract {
|
|
include!(env!("LESAVKA_CLIENT_KEYBOARD_SRC"));
|
|
|
|
use evdev::AttributeSet;
|
|
use evdev::uinput::VirtualDevice;
|
|
use serial_test::serial;
|
|
use std::thread;
|
|
use temp_env::{with_var, with_vars};
|
|
|
|
fn open_virtual_device(vdev: &mut VirtualDevice) -> Option<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) {
|
|
let _ = dev.set_nonblocking(true);
|
|
return Some(dev);
|
|
}
|
|
}
|
|
}
|
|
thread::sleep(std::time::Duration::from_millis(10));
|
|
}
|
|
None
|
|
}
|
|
|
|
fn build_keyboard_pair(name: &str) -> Option<(VirtualDevice, evdev::Device)> {
|
|
let mut keys = AttributeSet::<evdev::KeyCode>::new();
|
|
for key in [
|
|
evdev::KeyCode::KEY_A,
|
|
evdev::KeyCode::KEY_V,
|
|
evdev::KeyCode::KEY_LEFTCTRL,
|
|
evdev::KeyCode::KEY_LEFTALT,
|
|
] {
|
|
keys.insert(key);
|
|
}
|
|
|
|
let mut vdev = VirtualDevice::builder()
|
|
.ok()?
|
|
.name(name)
|
|
.with_keys(&keys)
|
|
.ok()?
|
|
.build()
|
|
.ok()?;
|
|
let dev = open_virtual_device(&mut vdev)?;
|
|
Some((vdev, dev))
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(coverage)]
|
|
#[serial]
|
|
fn process_events_skips_swallowed_paste_chord_updates() {
|
|
let Some((mut vdev, dev)) = build_keyboard_pair("lesavka-kbd-process-paste") else {
|
|
return;
|
|
};
|
|
let (tx, mut rx) = tokio::sync::broadcast::channel(64);
|
|
let mut agg = KeyboardAggregator::new(dev, true, tx, None);
|
|
agg.paste_enabled = true;
|
|
agg.paste_rpc_enabled = false;
|
|
|
|
with_vars(
|
|
[
|
|
("LESAVKA_CLIPBOARD_CHORD", Some("ctrl+alt+v")),
|
|
("LESAVKA_CLIPBOARD_DEBOUNCE_MS", Some("0")),
|
|
("LESAVKA_CLIPBOARD_CMD", Some("printf 'a'")),
|
|
],
|
|
|| {
|
|
vdev.emit(&[
|
|
evdev::InputEvent::new(
|
|
evdev::EventType::KEY.0,
|
|
evdev::KeyCode::KEY_LEFTCTRL.0,
|
|
1,
|
|
),
|
|
evdev::InputEvent::new(
|
|
evdev::EventType::KEY.0,
|
|
evdev::KeyCode::KEY_LEFTALT.0,
|
|
1,
|
|
),
|
|
evdev::InputEvent::new(evdev::EventType::KEY.0, evdev::KeyCode::KEY_V.0, 1),
|
|
])
|
|
.expect("emit paste chord");
|
|
thread::sleep(std::time::Duration::from_millis(25));
|
|
agg.process_events();
|
|
},
|
|
);
|
|
|
|
let reports: Vec<Vec<u8>> =
|
|
std::iter::from_fn(|| rx.try_recv().ok().map(|pkt| pkt.data)).collect();
|
|
assert!(
|
|
reports.iter().any(|report| report == &vec![0; 8]),
|
|
"swallowed paste chord should publish empty guard reports"
|
|
);
|
|
assert!(
|
|
reports
|
|
.iter()
|
|
.all(|report| report.get(2).copied() != Some(evdev::KeyCode::KEY_V.0 as u8)),
|
|
"literal V should not leak after the paste chord is swallowed"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(coverage)]
|
|
#[serial]
|
|
fn drain_key_updates_covers_dev_mode_logging_and_env_disable() {
|
|
let Some((mut vdev, dev)) = build_keyboard_pair("lesavka-kbd-process-live") else {
|
|
return;
|
|
};
|
|
let (tx, _rx) = tokio::sync::broadcast::channel(16);
|
|
|
|
with_var("LESAVKA_CLIPBOARD_PASTE", Some("0"), || {
|
|
let mut agg = KeyboardAggregator::new(dev, true, tx, None);
|
|
assert!(!agg.paste_enabled);
|
|
vdev.emit(&[evdev::InputEvent::new(
|
|
evdev::EventType::KEY.0,
|
|
evdev::KeyCode::KEY_A.0,
|
|
1,
|
|
)])
|
|
.expect("emit live key");
|
|
thread::sleep(std::time::Duration::from_millis(25));
|
|
|
|
let updates = agg.drain_key_updates();
|
|
assert_eq!(updates.len(), 1);
|
|
assert!(!updates[0].swallowed);
|
|
});
|
|
}
|
|
}
|