Skip to content

A network laboratory in CI

A unit test can tell you a gap-healing request is well-formed. It cannot tell you what happens when the only router on the ridge dies at hour six while a client is walking across three cells. Those are network questions — topology + physics + time + faults — and answering them on real hardware would take a hillside, a van, and a summer.

LoRa Relay Chat’s answer is lrcsim: a unix-native binary that runs whole-network scenarios against the real node logic — the same compiled lrc::Node that ships in the daemon and the firmware — driven by a virtual clock over a geometric field model. It exists because of an architectural bet the project made on day one: keep the core portable and pure (no sockets, no Arduino, platform access through seams), and everything becomes testable on a host. The scenario lab is that bet paying off.

A scenario is a flat, diff-friendly text file. The flagship one, abridged:

# hilltop_asym.scn
seed 42
field pathloss 3.0 shadow 2.0
node hill role=router x=0 y=0 z=150 tx_dbm=27
node plain[1..200] role=client grid=1000x1000 tx_dbm=14
link hill->plain[*] snr=+8 # everyone hears the hill
link plain[6..200]->hill snr=off # the hill hears only plain[1..5]
at 60s chanmsg from=plain[137] chan=#field text="can anyone hear me"
at 300s kill hill
at 420s revive hill # reboot-with-persistence
assert converged chan=#field by 900s

Note what the link lines are doing: the hilltop router is heard by all 200 nodes but hears only five of them — asymmetry as a first-class input, not an approximation layered onto a symmetric model. Directional SNR overrides exist precisely because the routing design lives or dies on asymmetric links, and a test harness that couldn’t express “loud downhill, deaf uphill” would quietly certify the wrong protocol.

The physics underneath is deliberately modest but honest: log-distance path loss with per-scenario exponent, optional log-normal shadowing, preset SNR floors shared with the rest of the test stack, co-SF collisions unless one signal captures, and — a detail most simulations get wrong — imperfect orthogonality between spreading factors, because “SFs don’t interfere” is a myth that flatters everyone’s airtime math.

A scenario run is a pure function of (scenario file, seed). Even the shadowing draws are seeded per-(seed, from, to, time) rather than pulled from a shared stream, so replays and out-of-order queries can’t smear randomness across runs. That buys the three things a network lab actually needs: a failure reproduces exactly, a failure bisects (same seed, narrower scenario), and CI can run the whole scenario library on every commit and mean something when it’s green — thousands of simulated network-hours per CPU-minute, no bench time consumed.

The long-term rule the project has set for itself is the best part: every field incident becomes a scenario. Weird behavior on a real hillside gets encoded as a .scn with an assertion, joins the library, and can never regress silently again. The lab grows a case file instead of a folklore channel.

Deeper reading: lrcsim (normative, what ships), the testbed design (research tier), and Testing for the whole host-first ladder.