ui(hygiene): refresh webcam placeholder and mouse coverage

This commit is contained in:
Brad Stein 2026-04-23 19:55:16 -03:00
parent 069d17980f
commit e0b2b70b29
5 changed files with 693 additions and 172 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 614 KiB

After

Width:  |  Height:  |  Size: 662 KiB

View File

@ -3,13 +3,212 @@
use evdev::{AbsoluteAxisCode, Device, EventType, InputEvent, KeyCode, RelativeAxisCode};
use std::time::{Duration, Instant};
use tokio::sync::broadcast::Sender;
#[cfg(not(coverage))]
use tracing::{debug, error, trace, warn};
use lesavka_common::lesavka::MouseReport;
const SEND_INTERVAL: Duration = Duration::from_millis(1);
struct MouseEventState<'a> {
buttons: &'a mut u8,
dx: &'a mut i8,
dy: &'a mut i8,
wheel: &'a mut i8,
last_abs_x: &'a mut Option<i32>,
last_abs_y: &'a mut Option<i32>,
abs_scale: i32,
abs_jump_x: i32,
abs_jump_y: i32,
has_touch_state: bool,
touch_guarded: &'a mut bool,
touch_active: &'a mut bool,
}
impl MouseEventState<'_> {
fn set_btn(&mut self, bit: u8, val: i32) {
if val != 0 {
*self.buttons |= 1 << bit;
} else {
*self.buttons &= !(1 << bit);
}
}
fn apply_event(&mut self, event: &InputEvent) {
match event.event_type() {
EventType::KEY => match event.code() {
c if c == KeyCode::BTN_LEFT.0 => self.set_btn(0, event.value()),
c if c == KeyCode::BTN_RIGHT.0 => self.set_btn(1, event.value()),
c if c == KeyCode::BTN_MIDDLE.0 => self.set_btn(2, event.value()),
c if c == KeyCode::BTN_TOUCH.0 => {
*self.touch_guarded = true;
*self.touch_active = event.value() != 0;
if !*self.touch_active {
*self.last_abs_x = None;
*self.last_abs_y = None;
}
self.set_btn(0, event.value());
}
_ => {}
},
EventType::RELATIVE => match event.code() {
c if c == RelativeAxisCode::REL_X.0 => {
*self.dx = self.dx.saturating_add(event.value().clamp(-127, 127) as i8)
}
c if c == RelativeAxisCode::REL_Y.0 => {
*self.dy = self.dy.saturating_add(event.value().clamp(-127, 127) as i8)
}
c if c == RelativeAxisCode::REL_WHEEL.0 => {
*self.wheel = self.wheel.saturating_add(event.value().clamp(-1, 1) as i8)
}
_ => {}
},
EventType::ABSOLUTE => match event.code() {
c if c == AbsoluteAxisCode::ABS_X.0
|| c == AbsoluteAxisCode::ABS_MT_POSITION_X.0 =>
{
if *self.touch_guarded && !*self.touch_active {
*self.last_abs_x = Some(event.value());
return;
}
if let Some(prev) = *self.last_abs_x {
if !self.has_touch_state {
let delta = (event.value() - prev).abs();
if delta > self.abs_jump_x {
*self.last_abs_x = Some(event.value());
return;
}
}
let delta = (event.value() - prev) / self.abs_scale;
if delta != 0 {
*self.dx = self.dx.saturating_add(delta.clamp(-127, 127) as i8);
}
}
*self.last_abs_x = Some(event.value());
}
c if c == AbsoluteAxisCode::ABS_Y.0
|| c == AbsoluteAxisCode::ABS_MT_POSITION_Y.0 =>
{
if *self.touch_guarded && !*self.touch_active {
*self.last_abs_y = Some(event.value());
return;
}
if let Some(prev) = *self.last_abs_y {
if !self.has_touch_state {
let delta = (event.value() - prev).abs();
if delta > self.abs_jump_y {
*self.last_abs_y = Some(event.value());
return;
}
}
let delta = (event.value() - prev) / self.abs_scale;
if delta != 0 {
*self.dy = self.dy.saturating_add(delta.clamp(-127, 127) as i8);
}
}
*self.last_abs_y = Some(event.value());
}
c if c == AbsoluteAxisCode::ABS_MT_TRACKING_ID.0 => {
if event.value() < 0 {
*self.touch_guarded = true;
*self.touch_active = false;
*self.last_abs_x = None;
*self.last_abs_y = None;
} else {
*self.touch_guarded = true;
*self.touch_active = true;
}
}
_ => {}
},
_ => {}
}
}
}
struct MouseRuntime<'a> {
tx: &'a Sender<MouseReport>,
dev_mode: bool,
sending_disabled: bool,
next_send: &'a mut Instant,
last_buttons: &'a mut u8,
event_state: MouseEventState<'a>,
}
impl MouseRuntime<'_> {
fn replay_events(&mut self, events: Vec<InputEvent>) {
for event in events {
if event.event_type() == EventType::SYNCHRONIZATION {
self.flush();
} else {
self.event_state.apply_event(&event);
}
}
}
fn flush(&mut self) {
let buttons = *self.event_state.buttons;
if buttons == *self.last_buttons && Instant::now() < *self.next_send {
return;
}
*self.next_send = Instant::now() + SEND_INTERVAL;
let pkt = [
buttons,
(*self.event_state.dx).clamp(-127, 127) as u8,
(*self.event_state.dy).clamp(-127, 127) as u8,
*self.event_state.wheel as u8,
];
if !self.sending_disabled {
#[cfg(not(coverage))]
if let Err(tokio::sync::broadcast::error::SendError(_)) =
self.tx.send(MouseReport { data: pkt.to_vec() })
{
if self.dev_mode {
warn!("❌🖱️ no HID receiver (mouse)");
}
} else if self.dev_mode {
debug!("📤🖱️ mouse {:?}", pkt);
}
#[cfg(coverage)]
{
let _ = self.tx.send(MouseReport { data: pkt.to_vec() });
}
}
*self.event_state.dx = 0;
*self.event_state.dy = 0;
*self.event_state.wheel = 0;
*self.last_buttons = buttons;
}
}
fn collect_fetched_events<I>(
fetch_result: Result<I, std::io::Error>,
dev_mode: bool,
) -> Option<Vec<InputEvent>>
where
I: IntoIterator<Item = InputEvent>,
{
match fetch_result {
Ok(events) => Some(events.into_iter().collect()),
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => None,
Err(e) => {
if dev_mode {
error!("🖱️❌ mouse read err: {e}");
}
None
}
}
}
fn log_event_batch(dev_mode: bool, device_name: Option<&str>, evts: &[InputEvent]) {
if dev_mode && !evts.is_empty() {
trace!("🖱️ {} evts from {}", evts.len(), device_name.unwrap_or("?"));
}
}
pub struct MouseAggregator {
dev: Device,
tx: Sender<MouseReport>,
@ -98,124 +297,12 @@ impl MouseAggregator {
self.sending_disabled = !send;
}
#[cfg(not(coverage))]
pub fn process_events(&mut self) {
let evts: Vec<InputEvent> = match self.dev.fetch_events() {
Ok(it) => it.collect(),
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => return,
Err(e) => {
if self.dev_mode {
error!("🖱️❌ mouse read err: {e}");
}
let Some(evts) = collect_fetched_events(self.dev.fetch_events(), self.dev_mode) else {
return;
}
};
if self.dev_mode && !evts.is_empty() {
trace!(
"🖱️ {} evts from {}",
evts.len(),
self.dev.name().unwrap_or("?")
);
}
for e in evts {
match e.event_type() {
EventType::KEY => match e.code() {
c if c == KeyCode::BTN_LEFT.0 => self.set_btn(0, e.value()),
c if c == KeyCode::BTN_RIGHT.0 => self.set_btn(1, e.value()),
c if c == KeyCode::BTN_MIDDLE.0 => self.set_btn(2, e.value()),
c if c == KeyCode::BTN_TOUCH.0 => {
self.touch_guarded = true;
self.touch_active = e.value() != 0;
if !self.touch_active {
self.last_abs_x = None;
self.last_abs_y = None;
}
self.set_btn(0, e.value());
}
_ => {}
},
EventType::RELATIVE => match e.code() {
c if c == RelativeAxisCode::REL_X.0 => {
self.dx = self.dx.saturating_add(e.value().clamp(-127, 127) as i8)
}
c if c == RelativeAxisCode::REL_Y.0 => {
self.dy = self.dy.saturating_add(e.value().clamp(-127, 127) as i8)
}
c if c == RelativeAxisCode::REL_WHEEL.0 => {
self.wheel = self.wheel.saturating_add(e.value().clamp(-1, 1) as i8)
}
_ => {}
},
EventType::ABSOLUTE => match e.code() {
c if c == AbsoluteAxisCode::ABS_X.0
|| c == AbsoluteAxisCode::ABS_MT_POSITION_X.0 =>
{
if self.touch_guarded && !self.touch_active {
self.last_abs_x = Some(e.value());
continue;
}
if let Some(prev) = self.last_abs_x {
if !self.has_touch_state {
let delta = (e.value() - prev).abs();
if delta > self.abs_jump_x {
self.last_abs_x = Some(e.value());
continue;
}
}
let delta = (e.value() - prev) / self.abs_scale;
if delta != 0 {
self.dx = self.dx.saturating_add(delta.clamp(-127, 127) as i8);
}
}
self.last_abs_x = Some(e.value());
}
c if c == AbsoluteAxisCode::ABS_Y.0
|| c == AbsoluteAxisCode::ABS_MT_POSITION_Y.0 =>
{
if self.touch_guarded && !self.touch_active {
self.last_abs_y = Some(e.value());
continue;
}
if let Some(prev) = self.last_abs_y {
if !self.has_touch_state {
let delta = (e.value() - prev).abs();
if delta > self.abs_jump_y {
self.last_abs_y = Some(e.value());
continue;
}
}
let delta = (e.value() - prev) / self.abs_scale;
if delta != 0 {
self.dy = self.dy.saturating_add(delta.clamp(-127, 127) as i8);
}
}
self.last_abs_y = Some(e.value());
}
c if c == AbsoluteAxisCode::ABS_MT_TRACKING_ID.0 => {
if e.value() < 0 {
self.touch_guarded = true;
self.touch_active = false;
self.last_abs_x = None;
self.last_abs_y = None;
} else {
self.touch_guarded = true;
self.touch_active = true;
}
}
_ => {}
},
EventType::SYNCHRONIZATION => self.flush(),
_ => {}
}
}
}
#[cfg(coverage)]
pub fn process_events(&mut self) {
let _ = self.dev.fetch_events();
self.flush();
log_event_batch(self.dev_mode, self.dev.name(), &evts);
self.runtime().replay_events(evts);
}
pub fn reset_state(&mut self) {
@ -234,49 +321,27 @@ impl MouseAggregator {
}
fn flush(&mut self) {
if self.buttons == self.last_buttons && Instant::now() < self.next_send {
return;
}
self.next_send = Instant::now() + SEND_INTERVAL;
let pkt = [
self.buttons,
self.dx.clamp(-127, 127) as u8,
self.dy.clamp(-127, 127) as u8,
self.wheel as u8,
];
if !self.sending_disabled {
#[cfg(not(coverage))]
if let Err(tokio::sync::broadcast::error::SendError(_)) =
self.tx.send(MouseReport { data: pkt.to_vec() })
{
if self.dev_mode {
warn!("❌🖱️ no HID receiver (mouse)");
}
} else if self.dev_mode {
debug!("📤🖱️ mouse {:?}", pkt);
}
#[cfg(coverage)]
{
let _ = self.tx.send(MouseReport { data: pkt.to_vec() });
}
}
self.dx = 0;
self.dy = 0;
self.wheel = 0;
self.last_buttons = self.buttons;
self.runtime().flush();
}
#[inline]
#[allow(dead_code)]
fn set_btn(&mut self, bit: u8, val: i32) {
if val != 0 {
self.buttons |= 1 << bit
} else {
self.buttons &= !(1 << bit)
MouseEventState {
buttons: &mut self.buttons,
dx: &mut self.dx,
dy: &mut self.dy,
wheel: &mut self.wheel,
last_abs_x: &mut self.last_abs_x,
last_abs_y: &mut self.last_abs_y,
abs_scale: self.abs_scale,
abs_jump_x: self.abs_jump_x,
abs_jump_y: self.abs_jump_y,
has_touch_state: self.has_touch_state,
touch_guarded: &mut self.touch_guarded,
touch_active: &mut self.touch_active,
}
.set_btn(bit, val);
}
#[cfg(not(coverage))]
@ -305,6 +370,30 @@ impl MouseAggregator {
fn abs_jump_threshold(_dev: &Device, _codes: &[AbsoluteAxisCode], abs_scale: i32) -> i32 {
(abs_scale * 40).max(50)
}
fn runtime(&mut self) -> MouseRuntime<'_> {
MouseRuntime {
tx: &self.tx,
dev_mode: self.dev_mode,
sending_disabled: self.sending_disabled,
next_send: &mut self.next_send,
last_buttons: &mut self.last_buttons,
event_state: MouseEventState {
buttons: &mut self.buttons,
dx: &mut self.dx,
dy: &mut self.dy,
wheel: &mut self.wheel,
last_abs_x: &mut self.last_abs_x,
last_abs_y: &mut self.last_abs_y,
abs_scale: self.abs_scale,
abs_jump_x: self.abs_jump_x,
abs_jump_y: self.abs_jump_y,
has_touch_state: self.has_touch_state,
touch_guarded: &mut self.touch_guarded,
touch_active: &mut self.touch_active,
},
}
}
}
impl Drop for MouseAggregator {
@ -315,3 +404,7 @@ impl Drop for MouseAggregator {
});
}
}
#[cfg(test)]
#[path = "mouse_event_contract_tests.rs"]
mod mouse_event_contract_tests;

View File

@ -0,0 +1,439 @@
//! Unit coverage for mouse event state transitions.
//!
//! Scope: exercise the private event decoder used by `MouseAggregator`.
//! Targets: `client/src/input/mouse.rs`.
//! Why: the quality gate tracks the real crate source, so the core event
//! handling needs deterministic coverage without depending on `/dev/uinput`.
use super::*;
use serial_test::serial;
use std::path::PathBuf;
use std::thread;
use std::time::Duration;
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(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 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))
}
struct StateHarness {
buttons: u8,
dx: i8,
dy: i8,
wheel: i8,
last_abs_x: Option<i32>,
last_abs_y: Option<i32>,
abs_scale: i32,
abs_jump_x: i32,
abs_jump_y: i32,
has_touch_state: bool,
touch_guarded: bool,
touch_active: bool,
}
impl StateHarness {
fn new() -> Self {
Self {
buttons: 0,
dx: 0,
dy: 0,
wheel: 0,
last_abs_x: None,
last_abs_y: None,
abs_scale: 8,
abs_jump_x: 120,
abs_jump_y: 120,
has_touch_state: false,
touch_guarded: false,
touch_active: true,
}
}
fn with_touch_state() -> Self {
Self {
has_touch_state: true,
..Self::new()
}
}
fn state(&mut self) -> MouseEventState<'_> {
MouseEventState {
buttons: &mut self.buttons,
dx: &mut self.dx,
dy: &mut self.dy,
wheel: &mut self.wheel,
last_abs_x: &mut self.last_abs_x,
last_abs_y: &mut self.last_abs_y,
abs_scale: self.abs_scale,
abs_jump_x: self.abs_jump_x,
abs_jump_y: self.abs_jump_y,
has_touch_state: self.has_touch_state,
touch_guarded: &mut self.touch_guarded,
touch_active: &mut self.touch_active,
}
}
}
#[test]
fn key_events_update_button_bits_and_touch_release_clears_origins() {
let mut harness = StateHarness::with_touch_state();
harness.last_abs_x = Some(100);
harness.last_abs_y = Some(120);
{
let mut state = harness.state();
state.apply_event(&InputEvent::new(EventType::KEY.0, KeyCode::BTN_LEFT.0, 1));
state.apply_event(&InputEvent::new(EventType::KEY.0, KeyCode::BTN_RIGHT.0, 1));
state.apply_event(&InputEvent::new(EventType::KEY.0, KeyCode::BTN_MIDDLE.0, 1));
state.apply_event(&InputEvent::new(EventType::KEY.0, KeyCode::BTN_TOUCH.0, 1));
state.apply_event(&InputEvent::new(EventType::KEY.0, KeyCode::KEY_A.0, 1));
}
assert_eq!(harness.buttons & 0b111, 0b111);
assert!(harness.touch_guarded);
assert!(harness.touch_active);
{
let mut state = harness.state();
state.apply_event(&InputEvent::new(EventType::KEY.0, KeyCode::BTN_TOUCH.0, 0));
}
assert!(!harness.touch_active);
assert!(harness.last_abs_x.is_none());
assert!(harness.last_abs_y.is_none());
assert_eq!(harness.buttons & 0b001, 0);
}
#[test]
fn relative_events_accumulate_motion_and_ignore_unknown_codes() {
let mut harness = StateHarness::new();
{
let mut state = harness.state();
state.apply_event(&InputEvent::new(
EventType::RELATIVE.0,
RelativeAxisCode::REL_X.0,
11,
));
state.apply_event(&InputEvent::new(
EventType::RELATIVE.0,
RelativeAxisCode::REL_Y.0,
-7,
));
state.apply_event(&InputEvent::new(
EventType::RELATIVE.0,
RelativeAxisCode::REL_WHEEL.0,
1,
));
state.apply_event(&InputEvent::new(
EventType::RELATIVE.0,
RelativeAxisCode::REL_RX.0,
42,
));
}
assert_eq!(harness.dx, 11);
assert_eq!(harness.dy, -7);
assert_eq!(harness.wheel, 1);
}
#[test]
fn absolute_events_apply_scaled_delta_and_ignore_large_jumps_without_touch_state() {
let mut harness = StateHarness::new();
harness.last_abs_x = Some(100);
harness.last_abs_y = Some(100);
{
let mut state = harness.state();
state.apply_event(&InputEvent::new(
EventType::ABSOLUTE.0,
AbsoluteAxisCode::ABS_X.0,
140,
));
state.apply_event(&InputEvent::new(
EventType::ABSOLUTE.0,
AbsoluteAxisCode::ABS_Y.0,
180,
));
}
assert_eq!(harness.dx, 5);
assert_eq!(harness.dy, 10);
assert_eq!(harness.last_abs_x, Some(140));
assert_eq!(harness.last_abs_y, Some(180));
harness.dx = 0;
harness.dy = 0;
{
let mut state = harness.state();
state.apply_event(&InputEvent::new(
EventType::ABSOLUTE.0,
AbsoluteAxisCode::ABS_X.0,
900,
));
state.apply_event(&InputEvent::new(
EventType::ABSOLUTE.0,
AbsoluteAxisCode::ABS_Y.0,
900,
));
}
assert_eq!(harness.dx, 0);
assert_eq!(harness.dy, 0);
assert_eq!(harness.last_abs_x, Some(900));
assert_eq!(harness.last_abs_y, Some(900));
}
#[test]
fn first_absolute_samples_only_seed_origin_without_motion() {
let mut harness = StateHarness::new();
{
let mut state = harness.state();
state.apply_event(&InputEvent::new(
EventType::ABSOLUTE.0,
AbsoluteAxisCode::ABS_X.0,
320,
));
state.apply_event(&InputEvent::new(
EventType::ABSOLUTE.0,
AbsoluteAxisCode::ABS_Y.0,
640,
));
}
assert_eq!(harness.dx, 0);
assert_eq!(harness.dy, 0);
assert_eq!(harness.last_abs_x, Some(320));
assert_eq!(harness.last_abs_y, Some(640));
}
#[test]
fn touch_guarded_absolute_updates_only_refresh_origins_until_touch_returns() {
let mut harness = StateHarness::with_touch_state();
harness.touch_guarded = true;
harness.touch_active = false;
harness.last_abs_x = Some(10);
harness.last_abs_y = Some(20);
{
let mut state = harness.state();
state.apply_event(&InputEvent::new(
EventType::ABSOLUTE.0,
AbsoluteAxisCode::ABS_MT_POSITION_X.0,
50,
));
state.apply_event(&InputEvent::new(
EventType::ABSOLUTE.0,
AbsoluteAxisCode::ABS_MT_POSITION_Y.0,
70,
));
state.apply_event(&InputEvent::new(
EventType::ABSOLUTE.0,
AbsoluteAxisCode::ABS_MT_TRACKING_ID.0,
3,
));
state.apply_event(&InputEvent::new(
EventType::ABSOLUTE.0,
AbsoluteAxisCode::ABS_MT_POSITION_X.0,
82,
));
state.apply_event(&InputEvent::new(
EventType::ABSOLUTE.0,
AbsoluteAxisCode::ABS_MT_POSITION_Y.0,
102,
));
state.apply_event(&InputEvent::new(
EventType::ABSOLUTE.0,
AbsoluteAxisCode::ABS_PRESSURE.0,
999,
));
}
assert!(harness.touch_guarded);
assert!(harness.touch_active);
assert_eq!(harness.last_abs_x, Some(82));
assert_eq!(harness.last_abs_y, Some(102));
assert_eq!(harness.dx, 4);
assert_eq!(harness.dy, 4);
}
#[test]
fn tracking_release_and_unhandled_event_types_stay_safe() {
let mut harness = StateHarness::with_touch_state();
harness.last_abs_x = Some(33);
harness.last_abs_y = Some(44);
{
let mut state = harness.state();
state.apply_event(&InputEvent::new(
EventType::ABSOLUTE.0,
AbsoluteAxisCode::ABS_MT_TRACKING_ID.0,
-1,
));
state.apply_event(&InputEvent::new(EventType::SWITCH.0, 0, 1));
}
assert!(harness.touch_guarded);
assert!(!harness.touch_active);
assert!(harness.last_abs_x.is_none());
assert!(harness.last_abs_y.is_none());
}
#[test]
fn replay_events_applies_regular_events_and_flushes_on_sync_boundaries() {
let mut harness = StateHarness::new();
let (tx, mut rx) = tokio::sync::broadcast::channel(8);
let mut next_send = Instant::now() - Duration::from_millis(5);
let mut last_buttons = 0_u8;
MouseRuntime {
tx: &tx,
dev_mode: true,
sending_disabled: false,
next_send: &mut next_send,
last_buttons: &mut last_buttons,
event_state: harness.state(),
}
.replay_events(vec![
InputEvent::new(EventType::KEY.0, KeyCode::BTN_LEFT.0, 1),
InputEvent::new(EventType::RELATIVE.0, RelativeAxisCode::REL_X.0, 9),
InputEvent::new(EventType::SYNCHRONIZATION.0, 0, 0),
]);
let pkt = rx.try_recv().expect("sync flush packet");
assert_eq!(pkt.data[0], 1);
assert_eq!(pkt.data[1], 9);
assert_eq!(pkt.data[2], 0);
assert_eq!(pkt.data[3], 0);
assert_eq!(harness.dx, 0);
assert_eq!(harness.dy, 0);
assert_eq!(harness.wheel, 0);
assert_eq!(last_buttons, 1);
}
#[test]
fn collect_fetched_events_handles_ok_would_block_and_other_errors() {
let ok_events = collect_fetched_events(
Ok(vec![InputEvent::new(
EventType::RELATIVE.0,
RelativeAxisCode::REL_X.0,
1,
)]),
false,
)
.expect("ok result should yield an event batch");
assert_eq!(ok_events.len(), 1);
let would_block = collect_fetched_events::<Vec<InputEvent>>(
Err(std::io::Error::new(
std::io::ErrorKind::WouldBlock,
"synthetic would-block",
)),
false,
);
assert!(would_block.is_none());
let other_error = collect_fetched_events::<Vec<InputEvent>>(
Err(std::io::Error::other("synthetic read failure")),
true,
);
assert!(other_error.is_none());
}
#[test]
fn log_event_batch_tolerates_empty_and_populated_batches() {
log_event_batch(false, Some("quiet-mouse"), &[]);
log_event_batch(
true,
Some("trace-mouse"),
&[InputEvent::new(
EventType::RELATIVE.0,
RelativeAxisCode::REL_Y.0,
4,
)],
);
}
#[test]
#[serial]
fn process_events_emits_live_relative_packets_for_the_real_crate_path() {
let Some((mut vdev, dev)) = build_relative_mouse("lesavka-unit-mouse-rel") else {
return;
};
let (tx, mut rx) = tokio::sync::broadcast::channel(16);
let mut agg = MouseAggregator::new(dev, true, tx);
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 frame");
thread::sleep(Duration::from_millis(25));
agg.process_events();
let pkt = rx.try_recv().expect("mouse packet");
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);
}
#[test]
#[serial]
fn process_events_handles_disconnected_virtual_devices_without_panicking() {
let Some((vdev, dev)) = build_relative_mouse("lesavka-unit-mouse-err") else {
return;
};
let (tx, _rx) = tokio::sync::broadcast::channel(4);
let mut agg = MouseAggregator::new(dev, true, tx);
drop(vdev);
thread::sleep(Duration::from_millis(40));
agg.process_events();
}

View File

@ -89,8 +89,8 @@
"loc": 398
},
"client/src/input/mouse.rs": {
"line_percent": 100.0,
"loc": 317
"line_percent": 98.85,
"loc": 410
},
"client/src/launcher/clipboard.rs": {
"line_percent": 100.0,
@ -132,6 +132,10 @@
"line_percent": 100.0,
"loc": 184
},
"client/src/launcher/ui/session_preview_coverage.rs": {
"line_percent": 100.0,
"loc": 7
},
"client/src/layout.rs": {
"line_percent": 97.56,
"loc": 78

View File

@ -158,9 +158,6 @@ mod mouse_contract {
#[test]
#[serial]
fn relative_events_emit_button_motion_and_wheel_packets() {
if cfg!(coverage) {
return;
}
let Some((mut vdev, dev)) = build_relative_mouse("lesavka-include-mouse-rel") else {
return;
};
@ -205,9 +202,6 @@ mod mouse_contract {
#[test]
#[serial]
fn touch_tracking_updates_touch_state_and_clears_abs_origins_on_release() {
if cfg!(coverage) {
return;
}
let Some((mut vdev, dev)) = build_touch_device("lesavka-include-mouse-touch") else {
return;
};
@ -342,9 +336,6 @@ mod mouse_contract {
#[test]
#[serial]
fn absolute_motion_ignores_large_jumps_without_touch_state() {
if cfg!(coverage) {
return;
}
let Some((mut vdev, dev)) = build_absolute_mouse_without_touch("lesavka-include-abs-jump")
else {
return;
@ -382,9 +373,6 @@ mod mouse_contract {
#[test]
#[serial]
fn absolute_motion_applies_scaled_delta_within_threshold() {
if cfg!(coverage) {
return;
}
let Some((mut vdev, dev)) = build_absolute_mouse_without_touch("lesavka-include-abs-delta")
else {
return;
@ -418,9 +406,6 @@ mod mouse_contract {
#[test]
#[serial]
fn touch_guarded_inactive_abs_events_only_update_origins() {
if cfg!(coverage) {
return;
}
let Some((mut vdev, dev)) = build_touch_device("lesavka-include-touch-guarded") else {
return;
};