//! 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 { 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::::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 { build_keyboard_pair(name).map(|(_, dev)| dev) } fn new_aggregator( dev: evdev::Device, ) -> ( KeyboardAggregator, tokio::sync::broadcast::Receiver, ) { 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:?}" ); } }