Mock
Test without hardwareA MockBox is an in-process fake Medius box behind the mock cargo feature.
cargo add medius --features mockIt's a cheap Clone: hand one to the Device, keep one to script and inspect.
See also: testing with MockBox.
Building a MockBox
new, and why you clone itfn new() -> MockBox
No round-trip
new() records every command and auto-answers QUERY(VERSION) and QUERY(HEALTH) with defaults.
use medius::{Device, MockBox};
let mock = MockBox::new();
let device = Device::with_mock(mock.clone());
// `device` drives the fake; `mock` still scripts and observes it.Wrapping it in a Device
with_mock and open_mockfn with_mock(mock: MockBox) -> Device
No round-trip
fn open_mock(mock: MockBox) -> Result<Device>
Blocks
| Constructor | Handshake | Returns | Description |
|---|---|---|---|
with_mock | No | Device | Wraps the fake and hands back the device directly. |
open_mock | Yes | Result<Device> | Also runs the version handshake, so it can fail the same way a real port can. |
use medius::{Device, MockBox};
let device = Device::open_mock(MockBox::new())?;
device.move_rel(5, 5)?;See the dead-box card for the two ways open_mock can fail.
Scripting query answers
Set the version, health, and device-info a query returnsfn with_version(self, version: Version) -> MockBox
No round-trip
fn with_health(self, health: Health) -> MockBox
No round-trip
fn with_device_info(self, device_info: DeviceInfo) -> MockBox
No round-trip
fn with_caps(self, caps: Caps) -> MockBox
No round-trip
fn with_mouse_caps(self, mouse: MouseCaps) -> MockBox
No round-trip
fn with_kbd_caps(self, keyboard: KbdCaps) -> MockBox
No round-trip
fn with_rate(self, rate: Rate) -> MockBox
No round-trip
fn with_stats(self, stats: Stats) -> MockBox
No round-trip
fn set_version(&self, version: Version)
No round-trip
fn set_health(&self, health: Health)
No round-trip
The with_* builders set what each query returns: query_version, query_health, and the device-info queries (device_info, caps, query_rate, query_stats). set_* changes a live fake in place to flip the version or health mid-test. Version, Health, and the device-info structs live on the types page, and Health::from_flags builds one from the raw status byte.
use medius::{Device, Health, MockBox, Version};
let mock = MockBox::new()
.with_version(Version { proto_ver: 3, fw_major: 5, fw_minor: 6, fw_patch: 7, mac: [0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc], name: "Loki".into() })
.with_health(Health::from_flags(0x0F));
let device = Device::with_mock(mock.clone());
let v = device.query_version()?;
assert_eq!((v.fw_major, v.fw_minor, v.fw_patch), (5, 6, 7));
assert!(device.query_health()?.mouse_attached);
// Change it mid-test: flip a later query_health.
mock.set_health(Health::from_flags(0x00));
assert!(!device.query_health()?.mouse_attached);Injecting inbound traffic
push_log and push_rawfn push_log(&self, level: LogLevel, text: &str)
No round-trip
fn push_raw(&self, bytes: &[u8])
No round-trip
fn push_motion(&self, seq: u8, dx: i16, dy: i16, dz: i16)
No round-trip
fn push_usages(&self, seq: u8, usages: &[Usage])
No round-trip
All put bytes on the inbound stream as if the box emitted them. push_log frames a LOG line that surfaces on logs() as a LogLine ( LogLevel plus text); push_raw sends arbitrary bytes. The two event calls feed the EventStream: push_motion arrives as a CatchEvent::Motion (a MotionEvent) and push_usages as a CatchEvent::Usages (a UsageSnapshot), with seq as the rolling counter so a test can assert gap detection.
use std::time::Duration;
use medius::{CatchEvent, CatchMask, Device, Key, LogLevel, MockBox, Usage};
let mock = MockBox::new();
let device = Device::with_mock(mock.clone());
let rx = device.logs();
mock.push_log(LogLevel::Warn, "overheating");
let line = rx.recv_timeout(Duration::from_secs(1))?;
assert_eq!(line.text, "overheating");
// Fake a catch subscription seeing the user hold A.
let stream = device.catch_events(CatchMask::KEYS)?;
mock.push_usages(0, &[Usage::from(Key::A)]);
assert!(matches!(stream.recv()?, CatchEvent::Usages(s) if s.is_held(Key::A)));Asserting what was sent
recorded_frames, saw, recorded, clear_recordedfn recorded_frames(&self) -> Vec<DecodedFrame>
No round-trip
fn recorded(&self) -> usize
No round-trip
fn saw(&self, ty: FrameType) -> bool
No round-trip
fn clear_recorded(&self)
No round-trip
| Method | Returns | Description |
|---|---|---|
recorded_frames | Vec<DecodedFrame> | Every command the host sent so far, decoded, in order. |
recorded | usize | The count of commands recorded so far. |
saw | bool | Whether the host sent at least one frame of the given type. |
clear_recorded | () | Drops the recorded history so you can assert only on the next phase. |
A DecodedFrame is { ty, seq, payload }; a press(Button::Left) records a FrameType::Inject frame with payload [0, 0, 0, 1] (class 0 = button, id 0, action 1).
use medius::{Button, Device, FrameType, MockBox};
let mock = MockBox::new();
let device = Device::with_mock(mock.clone());
device.press(Button::Left)?;
let frames = mock.recorded_frames();
let inject = frames
.iter()
.find(|f| f.ty == FrameType::Inject)
.expect("press recorded");
assert_eq!(inject.payload, vec![0, 0, 0, 1]);
assert!(mock.saw(FrameType::Inject));
mock.clear_recorded(); // next assertions see a fresh logSimulating a dead box
silent, and the handshake failuresfn silent(self) -> MockBox
No round-trip
silent() records commands but never answers a query. The two open_mock failures are a silent box (Error::NoReply) and an unknown protocol version (Error::BadProtoVer).
use medius::{Device, Error, MockBox, Version};
// A silent box answers nothing: no reply.
let err = Device::open_mock(MockBox::new().silent()).unwrap_err();
assert!(matches!(err, Error::NoReply));
// A box on an unknown protocol version fails the handshake.
let mock = MockBox::new().with_version(Version {
proto_ver: 9,
fw_major: 0,
fw_minor: 0,
fw_patch: 0,
mac: [0; 6],
name: String::new(),
});
let err = Device::open_mock(mock).unwrap_err();
assert!(matches!(err, Error::BadProtoVer { got: 9 }));