Building a MockBoxnew, and cloning

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.

Device wrapperwith_mock and open_mock

fn with_mock(mock: MockBox) -> Device

No round-trip

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

Blocks

CONSTRUCTORS
NameHandshakeReturnsDescription
with_mockNoDeviceWraps the fake.
open_mockYesResult<Device>Also runs the version handshake, so it can fail as a real port can.
EXAMPLE
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 repliesVersion, 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.

EXAMPLE
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 framespush_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.

METHODS
NameRaisesNotes
push_loga LogLine on logs()-
push_rawarbitrary bytes-
push_motionMotion on an EventStreamStamps itself ClockDomain::HostChip, the only domain the box stamps this frame in.
push_usagesUsages on an EventStreamStamps itself HostChip for the same reason, and carries a class, so a test can push the empty snapshot.
push_trafficTraffic on an EventStreamtrue_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.

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 sentrecorded_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
NameReturnsDescription
recorded_framesVec<DecodedFrame>Every command the host sent so far, decoded, in order.
recordedusizeCommands recorded so far.
sawboolWhether 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).

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

Clips and packet triggersMock 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

METHODS
NameReturnsDescription
with_clip_settings, set_clip_settingsMockBox, nothingSet 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.
EFFECT
CallEffect
query_statusReplies with the scripted ClipStatus.
query_configReplies with the scripted settings plus the mock's bound packet triggers, each with its hits.
bind_packet, unbind_packetAdd 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_triggersClears both kinds. reset clears the whole clip config.
set_imperfect_status, with_imperfect, allow_imperfect_clonesWith the opt-in off, the mock drops its consuming packet triggers, as the box does.
set_retain, finalize, bind, append, the engine verbsRecorded frames. The query_config and query_status replies stay as scripted.
EXAMPLE
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 boxsilent, 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).

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: 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 releaserestart, 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.

METHODS
NameSimulatesDetected by
restartA 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_lostThe inter-chip link dropping and coming back. The clone stays up.Stats::session moving.
detachThe 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.
attachA 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.

EXAMPLE
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 updatesrefuse_next_activate, reboot_after_activate

fn refuse_next_activate(&self, status: UpdateStatus)
fn reboot_after_activate(&self, unanswered: u32, pending: u32)

No round-trip

NameSimulates
refuse_next_activateThe next ACTIVATE refused with status. The staged image is kept.
reboot_after_activateThe 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.
EXAMPLE
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());