Skip to main content
The crate is designed so that your HIL suite is just ordinary Rust integration tests. These are files under tests/, run by cargo test, that live in the same repository as your firmware.

Structure

Point tests at a box with the LAGER_BOX_HOST environment variable and construct the client with LagerBox::from_env():
Group tests by net or by DUT feature — one file per concern (boot.rs, power.rs, sensors.rs) keeps cargo test <name> filtering useful.

Parallel tests and instrument safety

cargo test runs tests on multiple threads by default. That is safe at the instrument level. The box serializes access per physical instrument: every net command runs under a per-device lock in the box’s single-owner hardware service. Because of that per-device lock, parallel tests can never interleave I/O on one instrument. Examples are a LabJack shared across GPIO/ADC/SPI nets, or a Keithley shared by supply and battery roles. Tests sharing a net still observe each other’s state changes (one test’s disable() is visible to another test reading the same supply). Either partition nets across tests, or serialize:

Timeouts

Timeout budgets mirror the Lager CLI. Quick commands use 10 s. Some operations block on the box for a caller-controlled duration: the watt and energy integration windows, and wait_for_level. Those operations widen or drop the client timeout automatically, so a healthy long measurement is never aborted mid-flight.

Hermetic tests vs. hardware tests

Mark tests that always need real hardware with #[ignore]. A plain cargo test then stays green on a laptop with no box, or in a PR check:
Then opt in explicitly where a box is available:
This is the convention that the crate itself uses. Its own suite is hermetic, and cargo test runs against a mock box. Its hardware smoke tests run with cargo test --test hardware -- --ignored.

CI

A minimal GitHub Actions job, assuming the runner can reach the box (e.g. a self-hosted runner on the lab network or a Tailscale-connected runner):
For gateway-fronted boxes, see Authentication for how the token is attached and refreshed.

A harness that runs against any box

The crate’s own hardware suite uses a pattern worth copying. Every test is #[ignore]d, so cargo test stays hermetic. Each test reads its net name from an environment variable, and skips with a note when that variable is unset. The same suite then runs against whatever nets a given bench configures.
Run it by opting in:
The environment variables are a convention, not a crate feature — name them whatever suits your bench. What matters is that a test which cannot run says so and passes, rather than failing on a bench that simply lacks that instrument.

Skipping on an older box

A box too old for an endpoint answers with Error::UnsupportedByBox, which is a better signal than a panic when a fleet is mid-upgrade.

Teardown that actually runs

A test that panics mid-way skips everything after the panic, which on a bench means leaving a supply enabled or a firewall rule in place. Put the restore on a path that survives a failure.
Reserve the whole box for the duration when a suite must not be interleaved with another job:
BoxLockGuard releases on drop, including during a panic unwind.

Verifying against real hardware

Some behaviors only show up on a bench, and are worth knowing before you write an assertion around them:
  • A supply’s measurements can be None while its output is disabled, depending on the instrument. Assert on enabled and setpoints when the output is off.
  • state() returning Ok does not mean the instrument answered — check the error field.
  • An i2c.scan() hit does not guarantee a subsequent read will ACK.
  • erase() drops the debugger connection; reconnect before reading memory.