testing: add input classifier and mouse uinput contracts
This commit is contained in:
parent
00606c0b60
commit
23dcaf8263
@ -12,6 +12,7 @@ path = "src/lib.rs"
|
|||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
evdev = "0.13"
|
evdev = "0.13"
|
||||||
|
futures-util = "0.3"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
lesavka_client = { path = "../client" }
|
lesavka_client = { path = "../client" }
|
||||||
lesavka_common = { path = "../common" }
|
lesavka_common = { path = "../common" }
|
||||||
@ -22,4 +23,9 @@ serial_test = { workspace = true }
|
|||||||
temp-env = { workspace = true }
|
temp-env = { workspace = true }
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
tokio = { version = "1.45", features = ["full", "macros", "rt-multi-thread", "sync", "time"] }
|
tokio = { version = "1.45", features = ["full", "macros", "rt-multi-thread", "sync", "time"] }
|
||||||
|
tokio-stream = "0.1"
|
||||||
tonic = { version = "0.13", features = ["transport"] }
|
tonic = { version = "0.13", features = ["transport"] }
|
||||||
|
tonic-reflection = "0.13"
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-appender = "0.2"
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "ansi"] }
|
||||||
|
|||||||
@ -8,6 +8,18 @@ fn main() {
|
|||||||
.join("server/src/bin/lesavka-uvc.rs")
|
.join("server/src/bin/lesavka-uvc.rs")
|
||||||
.canonicalize()
|
.canonicalize()
|
||||||
.expect("canonical server uvc bin path");
|
.expect("canonical server uvc bin path");
|
||||||
|
let server_main = workspace_dir
|
||||||
|
.join("server/src/main.rs")
|
||||||
|
.canonicalize()
|
||||||
|
.expect("canonical server main path");
|
||||||
|
let client_main = workspace_dir
|
||||||
|
.join("client/src/main.rs")
|
||||||
|
.canonicalize()
|
||||||
|
.expect("canonical client main path");
|
||||||
|
let client_inputs = workspace_dir
|
||||||
|
.join("client/src/input/inputs.rs")
|
||||||
|
.canonicalize()
|
||||||
|
.expect("canonical client inputs path");
|
||||||
let common_cli = workspace_dir
|
let common_cli = workspace_dir
|
||||||
.join("common/src/bin/cli.rs")
|
.join("common/src/bin/cli.rs")
|
||||||
.canonicalize()
|
.canonicalize()
|
||||||
@ -17,6 +29,18 @@ fn main() {
|
|||||||
"cargo:rustc-env=LESAVKA_SERVER_UVC_BIN_SRC={}",
|
"cargo:rustc-env=LESAVKA_SERVER_UVC_BIN_SRC={}",
|
||||||
server_uvc.display()
|
server_uvc.display()
|
||||||
);
|
);
|
||||||
|
println!(
|
||||||
|
"cargo:rustc-env=LESAVKA_SERVER_MAIN_SRC={}",
|
||||||
|
server_main.display()
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"cargo:rustc-env=LESAVKA_CLIENT_MAIN_SRC={}",
|
||||||
|
client_main.display()
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"cargo:rustc-env=LESAVKA_CLIENT_INPUTS_SRC={}",
|
||||||
|
client_inputs.display()
|
||||||
|
);
|
||||||
println!(
|
println!(
|
||||||
"cargo:rustc-env=LESAVKA_COMMON_CLI_BIN_SRC={}",
|
"cargo:rustc-env=LESAVKA_COMMON_CLI_BIN_SRC={}",
|
||||||
common_cli.display()
|
common_cli.display()
|
||||||
|
|||||||
92
testing/tests/client_inputs_contract.rs
Normal file
92
testing/tests/client_inputs_contract.rs
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
//! 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
174
testing/tests/client_mouse_uinput_contract.rs
Normal file
174
testing/tests/client_mouse_uinput_contract.rs
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
//! Integration coverage for the client mouse input contract using uinput.
|
||||||
|
//!
|
||||||
|
//! Scope: exercise `MouseAggregator` with synthetic relative and absolute input
|
||||||
|
//! devices so event translation stays deterministic.
|
||||||
|
//! Targets: `client/src/input/mouse.rs`.
|
||||||
|
//! Why: mouse handling is event-rich and high-risk for regressions without
|
||||||
|
//! end-to-end event-path tests.
|
||||||
|
|
||||||
|
use evdev::uinput::VirtualDevice;
|
||||||
|
use evdev::{
|
||||||
|
AbsInfo, AbsoluteAxisCode, AttributeSet, Device, EventType, InputEvent, KeyCode,
|
||||||
|
RelativeAxisCode, UinputAbsSetup,
|
||||||
|
};
|
||||||
|
use lesavka_client::input::mouse::MouseAggregator;
|
||||||
|
use serial_test::serial;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::sync::broadcast;
|
||||||
|
|
||||||
|
fn open_virtual_node(vdev: &mut VirtualDevice) -> Option<PathBuf> {
|
||||||
|
let mut node = None;
|
||||||
|
for _ in 0..40 {
|
||||||
|
if let Ok(mut nodes) = vdev.enumerate_dev_nodes_blocking() {
|
||||||
|
if let Some(Ok(path)) = nodes.next() {
|
||||||
|
node = Some(path);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
thread::sleep(Duration::from_millis(10));
|
||||||
|
}
|
||||||
|
node
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_virtual_device(vdev: &mut VirtualDevice) -> Option<Device> {
|
||||||
|
let node = open_virtual_node(vdev)?;
|
||||||
|
let dev = Device::open(node).ok()?;
|
||||||
|
dev.set_nonblocking(true).ok()?;
|
||||||
|
Some(dev)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_relative_mouse(name: &str) -> Option<(VirtualDevice, Device)> {
|
||||||
|
let mut keys = AttributeSet::<KeyCode>::new();
|
||||||
|
keys.insert(KeyCode::BTN_LEFT);
|
||||||
|
keys.insert(KeyCode::BTN_RIGHT);
|
||||||
|
|
||||||
|
let mut rel = AttributeSet::<RelativeAxisCode>::new();
|
||||||
|
rel.insert(RelativeAxisCode::REL_X);
|
||||||
|
rel.insert(RelativeAxisCode::REL_Y);
|
||||||
|
rel.insert(RelativeAxisCode::REL_WHEEL);
|
||||||
|
|
||||||
|
let mut vdev = VirtualDevice::builder()
|
||||||
|
.ok()?
|
||||||
|
.name(name)
|
||||||
|
.with_keys(&keys)
|
||||||
|
.ok()?
|
||||||
|
.with_relative_axes(&rel)
|
||||||
|
.ok()?
|
||||||
|
.build()
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
let dev = open_virtual_device(&mut vdev)?;
|
||||||
|
Some((vdev, dev))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_absolute_touch_mouse(name: &str) -> Option<(VirtualDevice, Device)> {
|
||||||
|
let mut keys = AttributeSet::<KeyCode>::new();
|
||||||
|
keys.insert(KeyCode::BTN_TOUCH);
|
||||||
|
|
||||||
|
let abs = AbsInfo::new(0, 0, 1000, 0, 0, 0);
|
||||||
|
let mut vdev = VirtualDevice::builder()
|
||||||
|
.ok()?
|
||||||
|
.name(name)
|
||||||
|
.with_keys(&keys)
|
||||||
|
.ok()?
|
||||||
|
.with_absolute_axis(&UinputAbsSetup::new(AbsoluteAxisCode::ABS_X, abs))
|
||||||
|
.ok()?
|
||||||
|
.with_absolute_axis(&UinputAbsSetup::new(AbsoluteAxisCode::ABS_Y, abs))
|
||||||
|
.ok()?
|
||||||
|
.with_absolute_axis(&UinputAbsSetup::new(
|
||||||
|
AbsoluteAxisCode::ABS_MT_TRACKING_ID,
|
||||||
|
AbsInfo::new(0, -1, 10, 0, 0, 0),
|
||||||
|
))
|
||||||
|
.ok()?
|
||||||
|
.build()
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
let dev = open_virtual_device(&mut vdev)?;
|
||||||
|
Some((vdev, dev))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn relative_mouse_events_emit_expected_packets() {
|
||||||
|
let Some((mut vdev, dev)) = build_relative_mouse("lesavka-test-mouse-rel") else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let (tx, mut rx) = broadcast::channel(32);
|
||||||
|
let mut agg = MouseAggregator::new(dev, true, tx);
|
||||||
|
|
||||||
|
agg.set_grab(false);
|
||||||
|
agg.set_send(true);
|
||||||
|
|
||||||
|
vdev.emit(&[
|
||||||
|
InputEvent::new(EventType::KEY.0, KeyCode::BTN_LEFT.0, 1),
|
||||||
|
InputEvent::new(EventType::RELATIVE.0, RelativeAxisCode::REL_X.0, 12),
|
||||||
|
InputEvent::new(EventType::RELATIVE.0, RelativeAxisCode::REL_Y.0, -5),
|
||||||
|
InputEvent::new(EventType::RELATIVE.0, RelativeAxisCode::REL_WHEEL.0, 1),
|
||||||
|
])
|
||||||
|
.expect("emit relative press/move events");
|
||||||
|
|
||||||
|
thread::sleep(Duration::from_millis(25));
|
||||||
|
agg.process_events();
|
||||||
|
|
||||||
|
let pkt = rx.try_recv().expect("mouse report");
|
||||||
|
assert_eq!(pkt.data[0], 1);
|
||||||
|
assert_eq!(pkt.data[1], 12);
|
||||||
|
assert_eq!(pkt.data[2], (-5_i8) as u8);
|
||||||
|
assert_eq!(pkt.data[3], 1);
|
||||||
|
|
||||||
|
vdev.emit(&[InputEvent::new(EventType::KEY.0, KeyCode::BTN_LEFT.0, 0)])
|
||||||
|
.expect("emit button release");
|
||||||
|
thread::sleep(Duration::from_millis(25));
|
||||||
|
agg.process_events();
|
||||||
|
|
||||||
|
let release_pkt = rx.try_recv().expect("release report");
|
||||||
|
assert_eq!(release_pkt.data[0], 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn absolute_touch_events_translate_into_motion_and_reset() {
|
||||||
|
let Some((mut vdev, dev)) = build_absolute_touch_mouse("lesavka-test-mouse-abs") else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let (tx, mut rx) = broadcast::channel(32);
|
||||||
|
let mut agg = MouseAggregator::new(dev, true, tx);
|
||||||
|
|
||||||
|
vdev.emit(&[
|
||||||
|
InputEvent::new(EventType::KEY.0, KeyCode::BTN_TOUCH.0, 1),
|
||||||
|
InputEvent::new(EventType::ABSOLUTE.0, AbsoluteAxisCode::ABS_X.0, 100),
|
||||||
|
InputEvent::new(EventType::ABSOLUTE.0, AbsoluteAxisCode::ABS_Y.0, 100),
|
||||||
|
])
|
||||||
|
.expect("emit initial touch frame");
|
||||||
|
thread::sleep(Duration::from_millis(20));
|
||||||
|
agg.process_events();
|
||||||
|
|
||||||
|
vdev.emit(&[
|
||||||
|
InputEvent::new(EventType::ABSOLUTE.0, AbsoluteAxisCode::ABS_X.0, 300),
|
||||||
|
InputEvent::new(EventType::ABSOLUTE.0, AbsoluteAxisCode::ABS_Y.0, 500),
|
||||||
|
])
|
||||||
|
.expect("emit movement frame");
|
||||||
|
thread::sleep(Duration::from_millis(20));
|
||||||
|
agg.process_events();
|
||||||
|
|
||||||
|
let pkt = rx.try_recv().expect("absolute movement report");
|
||||||
|
assert_eq!(pkt.data[0], 1);
|
||||||
|
assert_ne!(pkt.data[1], 0);
|
||||||
|
assert_ne!(pkt.data[2], 0);
|
||||||
|
|
||||||
|
agg.set_send(false);
|
||||||
|
agg.reset_state();
|
||||||
|
assert!(
|
||||||
|
rx.try_recv().is_err(),
|
||||||
|
"send-disabled reset should not emit"
|
||||||
|
);
|
||||||
|
|
||||||
|
agg.set_send(true);
|
||||||
|
agg.reset_state();
|
||||||
|
let reset_pkt = rx.try_recv().expect("reset report");
|
||||||
|
assert_eq!(reset_pkt.data, vec![0, 0, 0, 0]);
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user