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-replies to 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 returns 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 silent-box card for the two ways open_mock can fail.
Scripting query replies
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. set_* changes a live fake in place to flip the version or health mid-test.
The structs they take live on the types page; 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: 5, 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, push_raw, and the three event pushesfn 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, ts_us: u32, dx: i16, dy: i16, dz: i16)
No round-trip
fn push_usages(&self, seq: u8, ts_us: u32, class: Class, direction: Direction, usages: &[Usage])
No round-trip
fn push_traffic(&self, seq: u8, ts_us: u32, clock: ClockDomain, class: CatchClass, id: u16, direction: Direction, flags: u8, true_len: u16, bytes: &[u8])
No round-trip
All put bytes on the inbound stream as if the box emitted them. The three event calls each raise one CatchEvent variant on an EventStream; push_log raises a LogLine on logs(), and push_raw sends arbitrary bytes.
The seq counter is shared across all three, exactly as it is on the wire.
Real losses do not show up here. Exercise loss handling through CatchState::dropped instead.
push_motion and push_usages stamp themselves ClockDomain::HostChip, the only domain the box stamps those two frames in. push_usages carries its own class, so a test can push the empty snapshot.
On push_traffic, true_len need not agree with bytes.len(), which is how you exercise truncated() with no real capture behind it.
use std::time::Duration;
use medius::{CatchClass, CatchEvent, CatchFilter, Class, ClockDomain, Device, Direction, 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)).expect("a log line");
assert_eq!(line.text, "overheating");
// Feed the snapshot a Key subscription gets while A is held.
let stream = device.catch_events([CatchFilter::watch_class(Class::Key)])?;
mock.push_usages(0, 1_000, Class::Key, Direction::PRESS, &[Usage::from(Key::A)]);
assert!(matches!(stream.recv()?, CatchEvent::Usages(s) if s.is_held(Key::A)));
// Fake a truncated vendor-interrupt capture: 4 bytes captured of a 64-byte packet.
mock.push_traffic(
1, 2_000, ClockDomain::HostChip, CatchClass::VendorInterrupt, 0x83, Direction::IN,
0, 64, &[0x11, 0x22, 0x33, 0x44],
);
assert!(matches!(stream.recv()?, CatchEvent::Traffic(t) if t.truncated()));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 start from an empty recordSimulating a box that never replies
silent, and the handshake failuresfn silent(self) -> MockBox
No round-trip
silent() records commands but sends no reply to 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 sends 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 }));