lesavka/testing/tests/client_inputs_contract.rs

241 lines
7.4 KiB
Rust

//! Integration coverage for client input-device classification helpers.
//!
//! Scope: include the input aggregator source and exercise private device
//! classification against synthetic uinput keyboard/mouse devices.
//! Targets: `client/src/input/inputs.rs`.
//! Why: device classification regressions can silently break all input capture
//! at runtime, so classifier behavior should stay under contract.
mod layout {
pub use lesavka_client::layout::*;
}
mod keyboard {
pub use lesavka_client::input::keyboard::*;
}
mod mouse {
pub use lesavka_client::input::mouse::*;
}
#[allow(warnings)]
mod inputs_contract {
include!(env!("LESAVKA_CLIENT_INPUTS_SRC"));
use evdev::AttributeSet;
use evdev::uinput::VirtualDevice;
use serial_test::serial;
use std::thread;
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() -> Option<evdev::Device> {
let mut keys = AttributeSet::<evdev::KeyCode>::new();
keys.insert(evdev::KeyCode::KEY_A);
keys.insert(evdev::KeyCode::KEY_ENTER);
let mut vdev = VirtualDevice::builder()
.ok()?
.name("lesavka-input-classify-kbd")
.with_keys(&keys)
.ok()?
.build()
.ok()?;
open_virtual_device(&mut vdev)
}
fn build_mouse() -> Option<evdev::Device> {
let mut keys = AttributeSet::<evdev::KeyCode>::new();
keys.insert(evdev::KeyCode::BTN_LEFT);
let mut rel = AttributeSet::<evdev::RelativeAxisCode>::new();
rel.insert(evdev::RelativeAxisCode::REL_X);
rel.insert(evdev::RelativeAxisCode::REL_Y);
let mut vdev = VirtualDevice::builder()
.ok()?
.name("lesavka-input-classify-mouse")
.with_keys(&keys)
.ok()?
.with_relative_axes(&rel)
.ok()?
.build()
.ok()?;
open_virtual_device(&mut vdev)
}
fn build_touch_mouse() -> Option<evdev::Device> {
let mut keys = AttributeSet::<evdev::KeyCode>::new();
keys.insert(evdev::KeyCode::BTN_TOUCH);
let abs = evdev::AbsInfo::new(0, 0, 1024, 0, 0, 0);
let mut vdev = VirtualDevice::builder()
.ok()?
.name("lesavka-input-classify-touch")
.with_keys(&keys)
.ok()?
.with_absolute_axis(&evdev::UinputAbsSetup::new(
evdev::AbsoluteAxisCode::ABS_MT_POSITION_X,
abs,
))
.ok()?
.with_absolute_axis(&evdev::UinputAbsSetup::new(
evdev::AbsoluteAxisCode::ABS_MT_POSITION_Y,
abs,
))
.ok()?
.build()
.ok()?;
open_virtual_device(&mut vdev)
}
fn build_misc_key_device() -> Option<evdev::Device> {
let mut keys = AttributeSet::<evdev::KeyCode>::new();
keys.insert(evdev::KeyCode::KEY_VOLUMEUP);
let mut vdev = VirtualDevice::builder()
.ok()?
.name("lesavka-input-classify-other")
.with_keys(&keys)
.ok()?
.build()
.ok()?;
open_virtual_device(&mut vdev)
}
fn build_keyboard_pair(name: &str) -> Option<(VirtualDevice, evdev::Device)> {
let mut keys = AttributeSet::<evdev::KeyCode>::new();
keys.insert(evdev::KeyCode::KEY_A);
keys.insert(evdev::KeyCode::KEY_ENTER);
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 new_aggregator() -> InputAggregator {
let (kbd_tx, _) = tokio::sync::broadcast::channel(32);
let (mou_tx, _) = tokio::sync::broadcast::channel(32);
InputAggregator::new(false, kbd_tx, mou_tx, None)
}
#[test]
#[serial]
fn classify_device_recognizes_keyboard_and_mouse_capabilities() {
if let Some(kbd) = build_keyboard() {
assert!(matches!(classify_device(&kbd), DeviceKind::Keyboard));
}
if let Some(mouse) = build_mouse() {
assert!(matches!(classify_device(&mouse), DeviceKind::Mouse));
}
if let Some(touch) = build_touch_mouse() {
assert!(matches!(classify_device(&touch), DeviceKind::Mouse));
}
if let Some(other) = build_misc_key_device() {
assert!(matches!(classify_device(&other), DeviceKind::Other));
}
}
#[test]
fn toggle_grab_switches_into_local_control_mode() {
let mut agg = new_aggregator();
agg.toggle_grab();
assert!(agg.pending_release);
assert!(!agg.released);
}
#[test]
fn toggle_grab_switches_back_to_remote_control_when_released() {
let mut agg = new_aggregator();
agg.released = true;
agg.pending_release = false;
agg.toggle_grab();
assert!(!agg.released);
assert!(!agg.pending_release);
}
#[test]
fn toggle_grab_ignores_requests_while_release_is_pending() {
let mut agg = new_aggregator();
agg.pending_release = true;
agg.toggle_grab();
assert!(agg.pending_release);
assert!(!agg.released);
}
#[test]
fn toggle_grab_ignores_requests_while_kill_release_is_pending() {
let mut agg = new_aggregator();
agg.pending_kill = true;
agg.toggle_grab();
assert!(agg.pending_kill);
assert!(!agg.pending_release);
assert!(!agg.released);
}
#[test]
#[serial]
fn capture_pending_keys_collects_current_keyboard_state() {
let Some((mut vdev, dev)) = build_keyboard_pair("lesavka-input-pending-keys") else {
return;
};
let (kbd_tx, _) = tokio::sync::broadcast::channel(16);
let (agg_kbd_tx, _) = tokio::sync::broadcast::channel(16);
let (agg_mou_tx, _) = tokio::sync::broadcast::channel(16);
let mut keyboard = KeyboardAggregator::new(dev, false, kbd_tx, None);
vdev.emit(&[evdev::InputEvent::new(
evdev::EventType::KEY.0,
evdev::KeyCode::KEY_A.0,
1,
)])
.expect("emit key press");
thread::sleep(std::time::Duration::from_millis(20));
keyboard.process_events();
let mut agg = InputAggregator::new(false, agg_kbd_tx, agg_mou_tx, None);
agg.keyboards.push(keyboard);
agg.capture_pending_keys();
assert!(agg.pending_keys.contains(&evdev::KeyCode::KEY_A));
}
#[tokio::test(flavor = "current_thread")]
async fn run_returns_once_pending_kill_chord_is_released() {
let mut agg = new_aggregator();
agg.pending_kill = true;
let result = tokio::time::timeout(Duration::from_millis(200), agg.run()).await;
assert!(result.is_ok(), "run should resolve instead of looping forever");
assert!(result.expect("timeout result").is_ok());
assert!(agg.released);
}
}