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")
|
.join("client/src/input/inputs.rs")
|
||||||
.canonicalize()
|
.canonicalize()
|
||||||
.expect("canonical client inputs path");
|
.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
|
let common_cli = workspace_dir
|
||||||
.join("common/src/bin/cli.rs")
|
.join("common/src/bin/cli.rs")
|
||||||
.canonicalize()
|
.canonicalize()
|
||||||
@ -41,6 +49,14 @@ fn main() {
|
|||||||
"cargo:rustc-env=LESAVKA_CLIENT_INPUTS_SRC={}",
|
"cargo:rustc-env=LESAVKA_CLIENT_INPUTS_SRC={}",
|
||||||
client_inputs.display()
|
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!(
|
println!(
|
||||||
"cargo:rustc-env=LESAVKA_COMMON_CLI_BIN_SRC={}",
|
"cargo:rustc-env=LESAVKA_COMMON_CLI_BIN_SRC={}",
|
||||||
common_cli.display()
|
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