lesavka/testing/tests/client_keyboard_process_contract.rs

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);
});
}
}