2026-04-12 19:47:26 -03:00
|
|
|
//! 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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 20:08:45 -03:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 19:47:26 -03:00
|
|
|
#[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));
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-12 20:08:45 -03:00
|
|
|
|
|
|
|
|
#[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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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);
|
|
|
|
|
}
|
2026-04-12 19:47:26 -03:00
|
|
|
}
|