testing: add keyboard and mouse include coverage contracts
This commit is contained in:
parent
28d490c5eb
commit
ed60b3e0ba
@ -20,6 +20,14 @@ fn main() {
|
||||
.join("client/src/input/inputs.rs")
|
||||
.canonicalize()
|
||||
.expect("canonical client inputs path");
|
||||
let client_keyboard = workspace_dir
|
||||
.join("client/src/input/keyboard.rs")
|
||||
.canonicalize()
|
||||
.expect("canonical client keyboard path");
|
||||
let client_mouse = workspace_dir
|
||||
.join("client/src/input/mouse.rs")
|
||||
.canonicalize()
|
||||
.expect("canonical client mouse path");
|
||||
let common_cli = workspace_dir
|
||||
.join("common/src/bin/cli.rs")
|
||||
.canonicalize()
|
||||
@ -41,6 +49,14 @@ fn main() {
|
||||
"cargo:rustc-env=LESAVKA_CLIENT_INPUTS_SRC={}",
|
||||
client_inputs.display()
|
||||
);
|
||||
println!(
|
||||
"cargo:rustc-env=LESAVKA_CLIENT_KEYBOARD_SRC={}",
|
||||
client_keyboard.display()
|
||||
);
|
||||
println!(
|
||||
"cargo:rustc-env=LESAVKA_CLIENT_MOUSE_SRC={}",
|
||||
client_mouse.display()
|
||||
);
|
||||
println!(
|
||||
"cargo:rustc-env=LESAVKA_COMMON_CLI_BIN_SRC={}",
|
||||
common_cli.display()
|
||||
|
||||
272
testing/tests/client_keyboard_include_contract.rs
Normal file
272
testing/tests/client_keyboard_include_contract.rs
Normal file
@ -0,0 +1,272 @@
|
||||
//! Integration coverage for client keyboard aggregator internals.
|
||||
//!
|
||||
//! Scope: include keyboard input source and validate report shaping, magic
|
||||
//! chords, paste handling, and event processing against synthetic keyboards.
|
||||
//! Targets: `client/src/input/keyboard.rs`.
|
||||
//! Why: keyboard chord and paste logic is stateful and needs direct branch
|
||||
//! coverage to avoid regressions.
|
||||
|
||||
mod keymap {
|
||||
pub use lesavka_client::input::keymap::*;
|
||||
}
|
||||
|
||||
#[allow(warnings)]
|
||||
mod keyboard_contract {
|
||||
include!(env!("LESAVKA_CLIENT_KEYBOARD_SRC"));
|
||||
|
||||
use serial_test::serial;
|
||||
use std::thread;
|
||||
use temp_env::with_var;
|
||||
|
||||
fn open_virtual_device(vdev: &mut evdev::uinput::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() {
|
||||
let dev = evdev::Device::open(path).ok()?;
|
||||
dev.set_nonblocking(true).ok()?;
|
||||
return Some(dev);
|
||||
}
|
||||
}
|
||||
thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn open_any_keyboard_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 looks_like_keyboard = dev
|
||||
.supported_keys()
|
||||
.map(|keys| {
|
||||
keys.contains(evdev::KeyCode::KEY_A)
|
||||
&& keys.contains(evdev::KeyCode::KEY_ENTER)
|
||||
&& keys.contains(evdev::KeyCode::KEY_LEFTCTRL)
|
||||
})
|
||||
.unwrap_or(false);
|
||||
if looks_like_keyboard {
|
||||
return Some(dev);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn build_keyboard(name: &str) -> Option<(evdev::uinput::VirtualDevice, evdev::Device)> {
|
||||
let mut keys = evdev::AttributeSet::<evdev::KeyCode>::new();
|
||||
keys.insert(evdev::KeyCode::KEY_A);
|
||||
keys.insert(evdev::KeyCode::KEY_B);
|
||||
keys.insert(evdev::KeyCode::KEY_C);
|
||||
keys.insert(evdev::KeyCode::KEY_D);
|
||||
keys.insert(evdev::KeyCode::KEY_E);
|
||||
keys.insert(evdev::KeyCode::KEY_F);
|
||||
keys.insert(evdev::KeyCode::KEY_G);
|
||||
keys.insert(evdev::KeyCode::KEY_V);
|
||||
keys.insert(evdev::KeyCode::KEY_LEFTCTRL);
|
||||
keys.insert(evdev::KeyCode::KEY_LEFTALT);
|
||||
keys.insert(evdev::KeyCode::KEY_LEFTSHIFT);
|
||||
keys.insert(evdev::KeyCode::KEY_ESC);
|
||||
keys.insert(evdev::KeyCode::KEY_LEFT);
|
||||
keys.insert(evdev::KeyCode::KEY_RIGHT);
|
||||
|
||||
let mut vdev = evdev::uinput::VirtualDevice::builder()
|
||||
.ok()?
|
||||
.name(name)
|
||||
.with_keys(&keys)
|
||||
.ok()?
|
||||
.build()
|
||||
.ok()?;
|
||||
let dev = open_virtual_device(&mut vdev)?;
|
||||
Some((vdev, dev))
|
||||
}
|
||||
|
||||
fn new_aggregator(
|
||||
dev: evdev::Device,
|
||||
) -> (
|
||||
KeyboardAggregator,
|
||||
tokio::sync::broadcast::Receiver<KeyboardReport>,
|
||||
) {
|
||||
let (tx, rx) = tokio::sync::broadcast::channel(128);
|
||||
(KeyboardAggregator::new(dev, true, tx, None), rx)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn process_events_emits_press_and_release_reports() {
|
||||
let Some((mut vdev, dev)) = build_keyboard("lesavka-include-kbd-events") 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,
|
||||
)])
|
||||
.expect("emit key press");
|
||||
thread::sleep(std::time::Duration::from_millis(20));
|
||||
agg.process_events();
|
||||
let press = rx.try_recv().expect("press report");
|
||||
assert_ne!(press.data[2], 0);
|
||||
|
||||
vdev.emit(&[evdev::InputEvent::new(
|
||||
evdev::EventType::KEY.0,
|
||||
evdev::KeyCode::KEY_A.0,
|
||||
0,
|
||||
)])
|
||||
.expect("emit key release");
|
||||
thread::sleep(std::time::Duration::from_millis(20));
|
||||
agg.process_events();
|
||||
let release = rx.try_recv().expect("release report");
|
||||
assert_eq!(release.data[2], 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn build_report_sets_modifiers_and_limits_to_six_keys() {
|
||||
let Some(dev) = open_any_keyboard_device()
|
||||
.or_else(|| build_keyboard("lesavka-include-kbd-report").map(|(_, dev)| dev))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let (mut agg, _) = new_aggregator(dev);
|
||||
|
||||
agg.pressed_keys.insert(evdev::KeyCode::KEY_LEFTCTRL);
|
||||
for key in [
|
||||
evdev::KeyCode::KEY_A,
|
||||
evdev::KeyCode::KEY_B,
|
||||
evdev::KeyCode::KEY_C,
|
||||
evdev::KeyCode::KEY_D,
|
||||
evdev::KeyCode::KEY_E,
|
||||
evdev::KeyCode::KEY_F,
|
||||
evdev::KeyCode::KEY_G,
|
||||
] {
|
||||
agg.pressed_keys.insert(key);
|
||||
}
|
||||
|
||||
let report = agg.build_report();
|
||||
assert_ne!(report[0], 0);
|
||||
assert!(report[2..].iter().filter(|value| **value != 0).count() <= 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn magic_chords_track_expected_combinations() {
|
||||
let Some(dev) = open_any_keyboard_device()
|
||||
.or_else(|| build_keyboard("lesavka-include-kbd-magic").map(|(_, dev)| dev))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let (mut agg, _) = new_aggregator(dev);
|
||||
|
||||
agg.pressed_keys.insert(evdev::KeyCode::KEY_LEFTCTRL);
|
||||
agg.pressed_keys.insert(evdev::KeyCode::KEY_LEFTSHIFT);
|
||||
agg.pressed_keys.insert(evdev::KeyCode::KEY_G);
|
||||
assert!(agg.magic_grab());
|
||||
|
||||
agg.pressed_keys.remove(&evdev::KeyCode::KEY_G);
|
||||
agg.pressed_keys.insert(evdev::KeyCode::KEY_LEFT);
|
||||
assert!(agg.magic_left());
|
||||
|
||||
agg.pressed_keys.remove(&evdev::KeyCode::KEY_LEFT);
|
||||
agg.pressed_keys.insert(evdev::KeyCode::KEY_RIGHT);
|
||||
assert!(agg.magic_right());
|
||||
|
||||
agg.pressed_keys.clear();
|
||||
agg.pressed_keys.insert(evdev::KeyCode::KEY_LEFTCTRL);
|
||||
agg.pressed_keys.insert(evdev::KeyCode::KEY_ESC);
|
||||
assert!(agg.magic_kill());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paste_rpc_enabled_contract_requires_flag_and_key() {
|
||||
assert!(!paste_rpc_enabled(false, true));
|
||||
assert!(!paste_rpc_enabled(true, false));
|
||||
assert!(paste_rpc_enabled(true, true));
|
||||
assert!(is_paste_modifier(evdev::KeyCode::KEY_LEFTCTRL));
|
||||
assert!(is_paste_modifier(evdev::KeyCode::KEY_RIGHTALT));
|
||||
assert!(!is_paste_modifier(evdev::KeyCode::KEY_A));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn try_handle_paste_event_consumes_chord_and_sends_rpc_payload() {
|
||||
let Some(dev) = open_any_keyboard_device()
|
||||
.or_else(|| build_keyboard("lesavka-include-kbd-paste-rpc").map(|(_, dev)| dev))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let (paste_tx, mut rx_rpc) = tokio::sync::mpsc::unbounded_channel::<String>();
|
||||
let (kbd_tx, _rx) = tokio::sync::broadcast::channel(128);
|
||||
let mut agg = KeyboardAggregator::new(dev, true, kbd_tx, Some(paste_tx));
|
||||
agg.paste_enabled = true;
|
||||
agg.paste_rpc_enabled = true;
|
||||
|
||||
agg.pressed_keys.insert(evdev::KeyCode::KEY_LEFTCTRL);
|
||||
agg.pressed_keys.insert(evdev::KeyCode::KEY_LEFTALT);
|
||||
agg.pressed_keys.insert(evdev::KeyCode::KEY_V);
|
||||
|
||||
with_var("LESAVKA_CLIPBOARD_CMD", Some("printf 'hello-from-clipboard'"), || {
|
||||
with_var("LESAVKA_CLIPBOARD_CHORD", Some("ctrl+alt+v"), || {
|
||||
with_var("LESAVKA_CLIPBOARD_DEBOUNCE_MS", Some("0"), || {
|
||||
assert!(agg.try_handle_paste_event(evdev::KeyCode::KEY_V, 1));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
let payload: String = rx_rpc.try_recv().expect("rpc payload");
|
||||
assert!(payload.contains("hello-from-clipboard"));
|
||||
assert!(agg.paste_chord_consumed);
|
||||
|
||||
assert!(agg.try_handle_paste_event(evdev::KeyCode::KEY_V, 0));
|
||||
assert!(!agg.paste_chord_consumed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn paste_clipboard_emits_hid_reports_for_supported_chars() {
|
||||
let Some(dev) = open_any_keyboard_device()
|
||||
.or_else(|| build_keyboard("lesavka-include-kbd-paste-hid").map(|(_, dev)| dev))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let (mut agg, mut rx) = new_aggregator(dev);
|
||||
agg.paste_enabled = true;
|
||||
|
||||
with_var("LESAVKA_CLIPBOARD_CMD", Some("printf 'ab'"), || {
|
||||
with_var("LESAVKA_CLIPBOARD_MAX", Some("8"), || {
|
||||
with_var("LESAVKA_CLIPBOARD_DELAY_MS", Some("0"), || {
|
||||
agg.paste_clipboard();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
let mut seen = 0usize;
|
||||
while rx.try_recv().is_ok() {
|
||||
seen += 1;
|
||||
}
|
||||
assert!(seen >= 2, "expected multiple key reports for pasted characters");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn reset_state_clears_pressed_keys_and_emits_empty_report() {
|
||||
let Some(dev) = open_any_keyboard_device()
|
||||
.or_else(|| build_keyboard("lesavka-include-kbd-reset").map(|(_, dev)| dev))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let (mut agg, mut rx) = new_aggregator(dev);
|
||||
|
||||
agg.pressed_keys.insert(evdev::KeyCode::KEY_A);
|
||||
agg.reset_state();
|
||||
assert!(agg.pressed_keys.is_empty());
|
||||
let pkt = rx.try_recv().expect("empty report after reset");
|
||||
assert_eq!(pkt.data, vec![0; 8]);
|
||||
}
|
||||
}
|
||||
296
testing/tests/client_mouse_include_contract.rs
Normal file
296
testing/tests/client_mouse_include_contract.rs
Normal file
@ -0,0 +1,296 @@
|
||||
//! 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);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user