269 lines
9.1 KiB
Rust
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:?}"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|