//! Extra include-based coverage for mouse aggregator branches. //! //! Scope: keep additional branch assertions in a separate file so each testing //! module stays under the 500 LOC contract. //! Targets: `client/src/input/mouse.rs`. //! Why: keep branch coverage growing without violating testing module size contracts. #[allow(warnings)] mod mouse_contract_extra { 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 { 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 { 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 { 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::::new(); keys.insert(evdev::KeyCode::BTN_LEFT); keys.insert(evdev::KeyCode::BTN_RIGHT); keys.insert(evdev::KeyCode::BTN_MIDDLE); let mut rel = evdev::AttributeSet::::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)) } #[test] #[serial] fn set_grab_path_and_slog_behave_across_dev_mode_flags() { let Some(dev_true) = open_any_mouse_device().or_else(|| { build_relative_mouse("lesavka-include-mouse-slog-true").map(|(_, dev)| dev) }) else { return; }; let Some(dev_false) = open_any_mouse_device().or_else(|| { build_relative_mouse("lesavka-include-mouse-slog-false").map(|(_, dev)| dev) }) else { return; }; let (tx_true, _rx_true) = tokio::sync::broadcast::channel(4); let (tx_false, _rx_false) = tokio::sync::broadcast::channel(4); let mut agg_true = MouseAggregator::new(dev_true, true, tx_true); let mut agg_false = MouseAggregator::new(dev_false, false, tx_false); agg_true.set_grab(false); agg_false.set_grab(false); let called_true = std::cell::Cell::new(0usize); agg_true.slog(|| called_true.set(called_true.get() + 1)); assert_eq!(called_true.get(), 1); let called_false = std::cell::Cell::new(0usize); agg_false.slog(|| called_false.set(called_false.get() + 1)); assert_eq!(called_false.get(), 0); } #[test] #[serial] fn flush_covers_dev_mode_send_error_and_send_success_paths() { let Some(dev_err) = open_any_mouse_device().or_else(|| { build_relative_mouse("lesavka-include-mouse-flush-dev-err").map(|(_, dev)| dev) }) else { return; }; let Some(dev_ok) = open_any_mouse_device().or_else(|| { build_relative_mouse("lesavka-include-mouse-flush-dev-ok").map(|(_, dev)| dev) }) else { return; }; let (tx_err, rx_err) = tokio::sync::broadcast::channel(1); drop(rx_err); let mut agg_err = MouseAggregator::new(dev_err, true, tx_err); agg_err.buttons = 1; agg_err.last_buttons = 0; agg_err.next_send = std::time::Instant::now() - std::time::Duration::from_millis(1); agg_err.flush(); assert_eq!(agg_err.last_buttons, 1); let (tx_ok, mut rx_ok) = tokio::sync::broadcast::channel(4); let mut agg_ok = MouseAggregator::new(dev_ok, true, tx_ok); agg_ok.buttons = 2; agg_ok.last_buttons = 0; agg_ok.dx = 3; agg_ok.dy = -2; agg_ok.next_send = std::time::Instant::now() - std::time::Duration::from_millis(1); agg_ok.flush(); let pkt = rx_ok.try_recv().expect("flush packet"); assert_eq!(pkt.data[0], 2); assert_eq!(pkt.data[1], 3); assert_eq!(pkt.data[2], (-2_i8) as u8); } #[test] #[serial] fn process_events_tolerates_idle_nonblocking_device() { let Some((_vdev, dev)) = build_relative_mouse("lesavka-include-mouse-idle") else { return; }; let (tx, _rx) = tokio::sync::broadcast::channel(4); let mut agg = MouseAggregator::new(dev, true, tx); agg.process_events(); } }