Mock
A 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 cloning
fn 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.Device wrapper with_mock and open_mock
fn with_mock(mock: MockBox) -> Device
No round-trip
fn open_mock(mock: MockBox) -> Result<Device>
Blocks
| Name | Handshake | Returns | Description |
|---|---|---|---|
with_mock | No | Device | Wraps the fake. |
open_mock | Yes | Result<Device> | Also runs the version handshake, so it can fail as a real port can. |
use medius::{Device, MockBox};
let device = Device::open_mock(MockBox::new())?;
device.move_rel(5, 5)?;The silent box section lists the two ways open_mock can fail.
Scripting query replies Version, health and device-info replies
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 with_render(self, mode: RenderMode, full: bool) -> MockBox
No round-trip
fn with_render_ready(self, ready: bool) -> MockBox
No round-trip
fn with_spread_learned(self, period_us: u32) -> 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's version or health mid-test.
The structs they take are on the types page. Health::from_flags builds one from the raw u16 flags word.
use medius::{Device, Health, MockBox, Version};
let mock = MockBox::new()
.with_version(Version { proto_ver: 9, 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);Pushing inbound frames 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, dpan: 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
Each puts bytes on the inbound stream as if the box sent them. The three event calls share one seq counter, as on the wire.
| Name | Raises | Notes |
|---|---|---|
push_log | a LogLine on logs() | - |
push_raw | arbitrary bytes | - |
push_motion | Motion on an EventStream | Stamps itself ClockDomain::HostChip, the only domain the box stamps this frame in. |
push_usages | Usages on an EventStream | Stamps itself HostChip for the same reason, and carries a class, so a test can push the empty snapshot. |
push_traffic | Traffic on an EventStream | true_len may differ from bytes.len(), to exercise truncated() with no real capture. |
The three event rows each name one CatchEvent variant. A push records no loss. To exercise loss handling, use CatchState::dropped.
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
| Name | Returns | Description |
|---|---|---|
recorded_frames | Vec<DecodedFrame> | Every command the host sent so far, decoded, in order. |
recorded | usize | 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 later asserts see only the next phase. |
A DecodedFrame from a press(Button::LEFT) is 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 recordClips and packet triggers Mock clip replies, and clip_packet
fn with_clip_settings(self, settings: ClipSettings) -> MockBox
No round-trip
fn set_clip_settings(&self, settings: ClipSettings)
No round-trip
fn clip_packet(&self, class: TrafficClass, id: u16, direction: Direction, head: &[u8])
-> (Option<ClipAction>, bool)No round-trip
| Name | Returns | Description |
|---|---|---|
with_clip_settings, set_clip_settings | MockBox, nothing | Set the ClipSettings that query_config replies with. Its packet triggers are bound in order, as bind_packet binds them, under the opt-in the mock holds at that moment. For a consuming trigger, script the opt-in first. |
clip_packet | (Option<ClipAction>, bool) | Run one packet through the packet triggers, as the box does. The most specific trigger the packet matches counts it in its hits. The action is None if no trigger matches, or if the matching trigger is once_per_run and the packet continues a run. The bool is whether that trigger consumes the packet. |
| Call | Effect |
|---|---|
query_status | Replies with the scripted ClipStatus. |
query_config | Replies with the scripted settings plus the mock's bound packet triggers, each with its hits. |
bind_packet, unbind_packet | Add to or remove from the mock's packet triggers, in the box's order. The box's checks apply, the 112-byte pool and the opt-in included. |
clear_triggers | Clears both kinds. reset clears the whole clip config. |
set_imperfect_status, with_imperfect, allow_imperfect_clones | With the opt-in off, the mock drops its consuming packet triggers, as the box does. |
set_retain, finalize, bind, append, the engine verbs | Recorded frames. The query_config and query_status replies stay as scripted. |
use medius::{ClipAction, ClipPacketTrigger, Device, Direction, MockBox, TrafficClass};
let mock = MockBox::new();
let device = Device::with_mock(mock.clone());
let held = ClipPacketTrigger::new(TrafficClass::HidIn, 2, Direction::IN, ClipAction::Start)
.matching([0x07, 0x20], [0xFF, 0x20])
.once_per_run(1);
device.clip().bind_packet(&held)?;
// The first report of the hold runs the action; the repeats continue the run.
let down = [0x07, 0x20, 0x00];
assert_eq!(mock.clip_packet(TrafficClass::HidIn, 2, Direction::IN, &down), (Some(ClipAction::Start), false));
assert_eq!(mock.clip_packet(TrafficClass::HidIn, 2, Direction::IN, &down), (None, false));
// The trigger was the top-ranked match for both packets, so both count.
assert_eq!(device.clip().query_config()?.packet_triggers[0].hits, 2);Silent box silent, and the handshake failures
fn silent(self) -> MockBox
No round-trip
silent() records commands and never replies 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: 10,
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: 10 }));Restart and release restart, link_lost, detach, attach
fn restart(&self)
No round-trip
fn link_lost(&self)
No round-trip
fn detach(&self, back_within_grace: bool)
No round-trip
fn attach(&self)
No round-trip
Each drops the mock's session state as the box does (locks, held input, rewrite rules, transforms, the clip and its config). Each keeps what the mock stores and triggers the same session recovery.
| Name | Simulates | Detected by |
|---|---|---|
restart | A device-chip boot. A RESET with its store flag and reboot(RebootTarget::DeviceRun) restart the mock too. | The hello, sent now and again on the next frame the mock receives. session goes back to 0. |
link_lost | The inter-chip link dropping and coming back. The clone stays up. | Stats::session moving. |
detach | The real device detaching. With back_within_grace, it re-attaches inside the 250 ms grace and the clone stays up. Without it, the clone is down until attach. | session moving. The recovery waits for a clone. |
attach | A real device attaching. If the clone was up, it is cloned again, which releases the session. If it was down, it comes up with nothing to release. | A clone for the recovery to re-send to. |
As on the box, session counts a release only if a command other than a QUERY arrived since it last counted one.
use medius::{Axis, Device, Direction, MockBox};
use std::time::Duration;
let mock = MockBox::new();
let device = Device::open_mock(mock.clone())?;
device.scale(Axis::X, Direction::Both, 40)?;
let scale_x = || device.query_locks().map(|l| l.scale_of(Axis::X, Direction::Positive));
mock.detach(false); // the clone goes down, and the scale with it
assert_eq!(scale_x()?, 100);
mock.attach(); // a new clone: the library re-sends the scale
while scale_x()? != 40 {
std::thread::sleep(Duration::from_millis(5));
}
assert_eq!(device.counters().restarts, 0); // a release, not a restart
mock.restart(); // a boot: the hello, and the same recovery
while device.counters().restarts == 0 {
std::thread::sleep(Duration::from_millis(5));
}
assert_eq!(scale_x()?, 40);Firmware updates refuse_next_activate, reboot_after_activate
fn refuse_next_activate(&self, status: UpdateStatus)
fn reboot_after_activate(&self, unanswered: u32, pending: u32)
No round-trip
| Name | Simulates |
|---|---|
refuse_next_activate | The next ACTIVATE refused with status. The staged image is kept. |
reboot_after_activate | The reboot after each accepted activate. The first unanswered firmware queries get no reply, and the device chip then reads on probation for the next pending queries. |
use medius::{Device, MockBox, UpdateStatus, UpdateTarget};
let image = vec![0u8; 4096];
let mock = MockBox::new();
let device = Device::with_mock(mock.clone());
mock.refuse_next_activate(UpdateStatus::LINK_DOWN);
assert!(device.update_firmware(UpdateTarget::Host, &image, &mut |_| {}).is_err());
mock.reboot_after_activate(1, 2);
device.update_firmware(UpdateTarget::Device, &image, &mut |_| {})?;
assert!(!device.wait_firmware_confirmed()?.any_pending());