lesavka/testing/tests/client_keyboard_shift_contract.rs

269 lines
9.1 KiB
Rust

//! Focused coverage for shifted live-key emission.
//!
//! Scope: verify the keyboard aggregator stages modifier state before shifted
//! printable keys so firmware and bootloaders do not miss the modifier bit.
//! Targets: `client/src/input/keyboard.rs`.
//! Why: modifier chords and overlapping presses must remain trustworthy under
//! real evdev timing so remote typing stays usable.
mod keymap {
pub use lesavka_client::input::keymap::*;
}
#[allow(warnings)]
mod keyboard_shift_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;
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_LEFTSHIFT,
evdev::KeyCode::KEY_LEFTCTRL,
evdev::KeyCode::KEY_LEFTALT,
evdev::KeyCode::KEY_A,
evdev::KeyCode::KEY_S,
evdev::KeyCode::KEY_F,
evdev::KeyCode::KEY_9,
] {
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))
}
fn build_keyboard(name: &str) -> Option<evdev::Device> {
build_keyboard_pair(name).map(|(_, dev)| dev)
}
fn new_aggregator(
dev: evdev::Device,
) -> (
KeyboardAggregator,
tokio::sync::broadcast::Receiver<KeyboardReport>,
) {
let (tx, rx) = tokio::sync::broadcast::channel(16);
(KeyboardAggregator::new(dev, false, tx, None), rx)
}
#[test]
#[serial]
fn shifted_live_keypress_reasserts_modifier_before_key_usage() {
let Some(dev) = build_keyboard("lesavka-kbd-shift-stage") else {
return;
};
let (mut agg, mut rx) = new_aggregator(dev);
agg.pressed_keys.insert(evdev::KeyCode::KEY_LEFTSHIFT);
agg.pressed_keys.insert(evdev::KeyCode::KEY_9);
with_var("LESAVKA_LIVE_MODIFIER_DELAY_MS", Some("0"), || {
let report = agg.build_report();
agg.emit_live_report(evdev::KeyCode::KEY_9, 1, report);
});
let staged = rx.try_recv().expect("modifier stage report");
assert_eq!(staged.data, vec![0x02, 0, 0, 0, 0, 0, 0, 0]);
let combined = rx.try_recv().expect("combined shifted key report");
assert_eq!(combined.data, vec![0x02, 0, 0x26, 0, 0, 0, 0, 0]);
}
#[test]
#[serial]
fn unshifted_live_keypress_stays_single_report() {
let Some(dev) = build_keyboard("lesavka-kbd-unshifted-single") else {
return;
};
let (mut agg, mut rx) = new_aggregator(dev);
agg.pressed_keys.insert(evdev::KeyCode::KEY_9);
with_var("LESAVKA_LIVE_MODIFIER_DELAY_MS", Some("0"), || {
let report = agg.build_report();
agg.emit_live_report(evdev::KeyCode::KEY_9, 1, report);
});
let combined = rx.try_recv().expect("plain key report");
assert_eq!(combined.data, vec![0, 0, 0x26, 0, 0, 0, 0, 0]);
assert!(rx.try_recv().is_err());
}
#[test]
#[serial]
fn ctrl_chord_reasserts_modifier_before_key_usage() {
let Some(dev) = build_keyboard("lesavka-kbd-ctrl-stage") else {
return;
};
let (mut agg, mut rx) = new_aggregator(dev);
agg.pressed_keys.insert(evdev::KeyCode::KEY_LEFTCTRL);
agg.pressed_keys.insert(evdev::KeyCode::KEY_A);
with_var("LESAVKA_LIVE_MODIFIER_DELAY_MS", Some("0"), || {
let report = agg.build_report();
agg.emit_live_report(evdev::KeyCode::KEY_A, 1, report);
});
let staged = rx.try_recv().expect("modifier stage report");
assert_eq!(staged.data, vec![0x01, 0, 0, 0, 0, 0, 0, 0]);
let combined = rx.try_recv().expect("combined ctrl chord report");
assert_eq!(combined.data, vec![0x01, 0, 0x04, 0, 0, 0, 0, 0]);
}
#[test]
#[serial]
fn alt_chord_reasserts_modifier_before_key_usage() {
let Some(dev) = build_keyboard("lesavka-kbd-alt-stage") else {
return;
};
let (mut agg, mut rx) = new_aggregator(dev);
agg.pressed_keys.insert(evdev::KeyCode::KEY_LEFTALT);
agg.pressed_keys.insert(evdev::KeyCode::KEY_F);
with_var("LESAVKA_LIVE_MODIFIER_DELAY_MS", Some("0"), || {
let report = agg.build_report();
agg.emit_live_report(evdev::KeyCode::KEY_F, 1, report);
});
let staged = rx.try_recv().expect("modifier stage report");
assert_eq!(staged.data, vec![0x04, 0, 0, 0, 0, 0, 0, 0]);
let combined = rx.try_recv().expect("combined alt chord report");
assert_eq!(combined.data, vec![0x04, 0, 0x09, 0, 0, 0, 0, 0]);
}
#[test]
#[serial]
fn process_events_emits_shifted_letter_with_modifier_bit() {
let Some((mut vdev, dev)) = build_keyboard_pair("lesavka-kbd-shift-live") else {
return;
};
let (mut agg, mut rx) = new_aggregator(dev);
vdev.emit(&[
evdev::InputEvent::new(evdev::EventType::KEY.0, evdev::KeyCode::KEY_LEFTSHIFT.0, 1),
evdev::InputEvent::new(evdev::EventType::KEY.0, evdev::KeyCode::KEY_A.0, 1),
])
.expect("emit shifted key");
thread::sleep(std::time::Duration::from_millis(25));
with_var("LESAVKA_LIVE_MODIFIER_DELAY_MS", Some("0"), || {
agg.process_events();
});
let mut saw_shifted_a = false;
while let Ok(pkt) = rx.try_recv() {
if pkt.data == vec![0x02, 0, 0x04, 0, 0, 0, 0, 0] {
saw_shifted_a = true;
break;
}
}
assert!(
saw_shifted_a,
"expected shifted A report in live event stream"
);
}
#[test]
#[serial]
fn process_events_emits_ctrl_chord_with_modifier_bit() {
let Some((mut vdev, dev)) = build_keyboard_pair("lesavka-kbd-ctrl-live") else {
return;
};
let (mut agg, mut rx) = new_aggregator(dev);
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_A.0, 1),
])
.expect("emit ctrl chord");
thread::sleep(std::time::Duration::from_millis(25));
with_var("LESAVKA_LIVE_MODIFIER_DELAY_MS", Some("0"), || {
agg.process_events();
});
let mut saw_ctrl_a = false;
while let Ok(pkt) = rx.try_recv() {
if pkt.data == vec![0x01, 0, 0x04, 0, 0, 0, 0, 0] {
saw_ctrl_a = true;
break;
}
}
assert!(saw_ctrl_a, "expected ctrl+A report in live event stream");
}
#[test]
#[serial]
fn process_events_tracks_overlapping_plain_keys_without_sticking() {
let Some((mut vdev, dev)) = build_keyboard_pair("lesavka-kbd-overlap-live") else {
return;
};
let (mut agg, mut rx) = new_aggregator(dev);
vdev.emit(&[
evdev::InputEvent::new(evdev::EventType::KEY.0, evdev::KeyCode::KEY_A.0, 1),
evdev::InputEvent::new(evdev::EventType::KEY.0, evdev::KeyCode::KEY_S.0, 1),
evdev::InputEvent::new(evdev::EventType::KEY.0, evdev::KeyCode::KEY_A.0, 0),
evdev::InputEvent::new(evdev::EventType::KEY.0, evdev::KeyCode::KEY_S.0, 0),
])
.expect("emit overlapping plain keys");
thread::sleep(std::time::Duration::from_millis(25));
agg.process_events();
let mut reports = Vec::new();
while let Ok(pkt) = rx.try_recv() {
reports.push(pkt.data);
}
assert!(
reports.contains(&vec![0, 0, 0x04, 0, 0, 0, 0, 0]),
"expected A down report, got {reports:?}"
);
assert!(
reports.iter().any(|pkt| {
let keys = &pkt[2..8];
keys.contains(&0x04) && keys.contains(&0x16)
}),
"expected A+S overlap report, got {reports:?}"
);
assert!(
reports.contains(&vec![0, 0, 0x16, 0, 0, 0, 0, 0]),
"expected lone S report after A released, got {reports:?}"
);
assert!(
reports.contains(&vec![0; 8]),
"expected final empty report after both releases, got {reports:?}"
);
}
}