Medius - Rust LibraryMock

Mock

Test without hardware

A MockBox is an in-process fake Medius box behind the mock cargo feature.

cargo add medius --features mock

It'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 it
fn new() -> MockBox

No round-trip

new() records every command and auto-replies to QUERY(VERSION) and QUERY(HEALTH) with defaults.

EXAMPLE
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_mock
fn with_mock(mock: MockBox) -> Device

No round-trip

fn open_mock(mock: MockBox) -> Result<Device>

Blocks

CONSTRUCTORS
ConstructorHandshakeReturnsDescription
with_mockNoDeviceWraps the fake and returns the device directly.
open_mockYesResult<Device>Also runs the version handshake, so it can fail the same way a real port can.
EXAMPLE
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 returns
fn 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.

EXAMPLE
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 pushes
fn 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.

EXAMPLE
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_recorded
fn 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

METHODS
MethodReturnsDescription
recorded_framesVec<DecodedFrame>Every command the host sent so far, decoded, in order.
recordedusizeThe count of commands recorded so far.
sawboolWhether 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).

EXAMPLE
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 record

Simulating a box that never replies

silent, and the handshake failures
fn 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).

EXAMPLE
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 }));