lesavka/testing/tests/client_mouse_include_contract.rs

297 lines
10 KiB
Rust
Raw Normal View History

//! Integration coverage for client mouse aggregator internals.
//!
//! Scope: include mouse input source and validate relative/absolute event
//! handling, flush behavior, and threshold helpers.
//! Targets: `client/src/input/mouse.rs`.
//! Why: mouse state transitions are regress-prone and must remain deterministic
//! under synthetic device traffic.
#[allow(warnings)]
mod mouse_contract {
include!(env!("LESAVKA_CLIENT_MOUSE_SRC"));
use serial_test::serial;
use std::path::PathBuf;
use std::thread;
fn open_virtual_node(vdev: &mut evdev::uinput::VirtualDevice) -> Option<PathBuf> {
for _ in 0..40 {
if let Ok(mut nodes) = vdev.enumerate_dev_nodes_blocking() {
if let Some(Ok(path)) = nodes.next() {
return Some(path);
}
}
thread::sleep(std::time::Duration::from_millis(10));
}
None
}
fn open_virtual_device(vdev: &mut evdev::uinput::VirtualDevice) -> Option<evdev::Device> {
let node = open_virtual_node(vdev)?;
let dev = evdev::Device::open(node).ok()?;
dev.set_nonblocking(true).ok()?;
Some(dev)
}
fn open_any_mouse_device() -> Option<evdev::Device> {
let entries = std::fs::read_dir("/dev/input").ok()?;
for entry in entries.flatten() {
let path = entry.path();
let name = path.file_name()?.to_string_lossy();
if !name.starts_with("event") {
continue;
}
let dev = evdev::Device::open(path).ok()?;
let _ = dev.set_nonblocking(true);
let rel_mouse = dev
.supported_relative_axes()
.map(|axes| {
axes.contains(evdev::RelativeAxisCode::REL_X)
&& axes.contains(evdev::RelativeAxisCode::REL_Y)
})
.unwrap_or(false)
&& dev
.supported_keys()
.map(|keys| {
keys.contains(evdev::KeyCode::BTN_LEFT)
|| keys.contains(evdev::KeyCode::BTN_RIGHT)
})
.unwrap_or(false);
let abs_touch = dev
.supported_absolute_axes()
.map(|axes| {
axes.contains(evdev::AbsoluteAxisCode::ABS_X)
|| axes.contains(evdev::AbsoluteAxisCode::ABS_MT_POSITION_X)
})
.unwrap_or(false);
if rel_mouse || abs_touch {
return Some(dev);
}
}
None
}
fn build_relative_mouse(name: &str) -> Option<(evdev::uinput::VirtualDevice, evdev::Device)> {
let mut keys = evdev::AttributeSet::<evdev::KeyCode>::new();
keys.insert(evdev::KeyCode::BTN_LEFT);
keys.insert(evdev::KeyCode::BTN_RIGHT);
keys.insert(evdev::KeyCode::BTN_MIDDLE);
let mut rel = evdev::AttributeSet::<evdev::RelativeAxisCode>::new();
rel.insert(evdev::RelativeAxisCode::REL_X);
rel.insert(evdev::RelativeAxisCode::REL_Y);
rel.insert(evdev::RelativeAxisCode::REL_WHEEL);
let mut vdev = evdev::uinput::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_touch_device(name: &str) -> Option<(evdev::uinput::VirtualDevice, evdev::Device)> {
let mut keys = evdev::AttributeSet::<evdev::KeyCode>::new();
keys.insert(evdev::KeyCode::BTN_TOUCH);
let abs = evdev::AbsInfo::new(0, 0, 1000, 0, 0, 0);
let mut vdev = evdev::uinput::VirtualDevice::builder()
.ok()?
.name(name)
.with_keys(&keys)
.ok()?
.with_absolute_axis(&evdev::UinputAbsSetup::new(evdev::AbsoluteAxisCode::ABS_X, abs))
.ok()?
.with_absolute_axis(&evdev::UinputAbsSetup::new(evdev::AbsoluteAxisCode::ABS_Y, abs))
.ok()?
.with_absolute_axis(&evdev::UinputAbsSetup::new(
evdev::AbsoluteAxisCode::ABS_MT_TRACKING_ID,
evdev::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_events_emit_button_motion_and_wheel_packets() {
let Some((mut vdev, dev)) = build_relative_mouse("lesavka-include-mouse-rel") else {
return;
};
let (tx, mut rx) = tokio::sync::broadcast::channel(32);
let mut agg = MouseAggregator::new(dev, true, tx);
agg.set_grab(false);
agg.set_send(true);
vdev.emit(&[
evdev::InputEvent::new(evdev::EventType::KEY.0, evdev::KeyCode::BTN_LEFT.0, 1),
evdev::InputEvent::new(evdev::EventType::KEY.0, evdev::KeyCode::BTN_RIGHT.0, 1),
evdev::InputEvent::new(evdev::EventType::KEY.0, evdev::KeyCode::BTN_MIDDLE.0, 1),
evdev::InputEvent::new(
evdev::EventType::RELATIVE.0,
evdev::RelativeAxisCode::REL_X.0,
11,
),
evdev::InputEvent::new(
evdev::EventType::RELATIVE.0,
evdev::RelativeAxisCode::REL_Y.0,
-7,
),
evdev::InputEvent::new(
evdev::EventType::RELATIVE.0,
evdev::RelativeAxisCode::REL_WHEEL.0,
1,
),
])
.expect("emit relative frame");
thread::sleep(std::time::Duration::from_millis(20));
agg.process_events();
let pkt = rx.try_recv().expect("mouse packet");
assert_eq!(pkt.data[0] & 0b0000_0111, 0b0000_0111);
assert_eq!(pkt.data[1], 11);
assert_eq!(pkt.data[2], (-7_i8) as u8);
assert_eq!(pkt.data[3], 1);
}
#[test]
#[serial]
fn touch_tracking_updates_touch_state_and_clears_abs_origins_on_release() {
let Some((mut vdev, dev)) = build_touch_device("lesavka-include-mouse-touch") else {
return;
};
let (tx, _rx) = tokio::sync::broadcast::channel(8);
let mut agg = MouseAggregator::new(dev, true, tx);
vdev.emit(&[
evdev::InputEvent::new(evdev::EventType::KEY.0, evdev::KeyCode::BTN_TOUCH.0, 1),
evdev::InputEvent::new(
evdev::EventType::ABSOLUTE.0,
evdev::AbsoluteAxisCode::ABS_X.0,
100,
),
evdev::InputEvent::new(
evdev::EventType::ABSOLUTE.0,
evdev::AbsoluteAxisCode::ABS_Y.0,
120,
),
evdev::InputEvent::new(
evdev::EventType::ABSOLUTE.0,
evdev::AbsoluteAxisCode::ABS_MT_TRACKING_ID.0,
1,
),
])
.expect("emit touch start");
thread::sleep(std::time::Duration::from_millis(20));
agg.process_events();
assert!(agg.touch_guarded);
assert!(agg.touch_active);
vdev.emit(&[evdev::InputEvent::new(
evdev::EventType::ABSOLUTE.0,
evdev::AbsoluteAxisCode::ABS_MT_TRACKING_ID.0,
-1,
)])
.expect("emit touch end");
thread::sleep(std::time::Duration::from_millis(20));
agg.process_events();
assert!(agg.touch_guarded);
assert!(!agg.touch_active);
assert!(agg.last_abs_x.is_none());
assert!(agg.last_abs_y.is_none());
}
#[test]
#[serial]
fn flush_and_reset_state_honor_send_toggle_and_clear_accumulators() {
let Some(dev) = open_any_mouse_device()
.or_else(|| build_relative_mouse("lesavka-include-mouse-flush").map(|(_, dev)| dev))
else {
return;
};
let (tx, mut rx) = tokio::sync::broadcast::channel(32);
let mut agg = MouseAggregator::new(dev, false, tx);
agg.buttons = 1;
agg.last_buttons = 0;
agg.dx = 7;
agg.dy = -3;
agg.wheel = 1;
agg.next_send = std::time::Instant::now() - std::time::Duration::from_millis(10);
agg.set_send(false);
agg.flush();
assert!(rx.try_recv().is_err(), "send-disabled flush should not emit");
agg.buttons = 2;
agg.last_buttons = 0;
agg.dx = 4;
agg.dy = -2;
agg.wheel = -1;
agg.next_send = std::time::Instant::now() - std::time::Duration::from_millis(10);
agg.set_send(true);
agg.flush();
let pkt = rx.try_recv().expect("flush packet");
assert_eq!(pkt.data[0], 2);
assert_eq!(pkt.data[1], 4);
assert_eq!(pkt.data[2], (-2_i8) as u8);
assert_eq!(pkt.data[3], (-1_i8) as u8);
assert_eq!(agg.dx, 0);
assert_eq!(agg.dy, 0);
assert_eq!(agg.wheel, 0);
agg.reset_state();
let reset_pkt = rx.try_recv().expect("reset packet");
assert_eq!(reset_pkt.data, vec![0, 0, 0, 0]);
}
#[test]
#[serial]
fn abs_jump_threshold_uses_scale_based_minimum_without_absinfo() {
let Some(dev) = open_any_mouse_device().or_else(|| {
build_relative_mouse("lesavka-include-mouse-threshold").map(|(_, dev)| dev)
}) else {
return;
};
let threshold =
MouseAggregator::abs_jump_threshold(&dev, &[evdev::AbsoluteAxisCode::ABS_X], 3);
assert!(threshold >= 120, "threshold should honor scale-derived minimum");
}
#[test]
#[serial]
fn set_btn_toggles_expected_button_bits() {
let Some(dev) = open_any_mouse_device()
.or_else(|| build_relative_mouse("lesavka-include-mouse-bits").map(|(_, dev)| dev))
else {
return;
};
let (tx, _rx) = tokio::sync::broadcast::channel(8);
let mut agg = MouseAggregator::new(dev, false, tx);
agg.set_btn(0, 1);
agg.set_btn(1, 1);
assert_eq!(agg.buttons & 0b11, 0b11);
agg.set_btn(0, 0);
assert_eq!(agg.buttons & 0b11, 0b10);
}
}