Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

crates/nornir-testmatrix/README.md

nornir-testmatrix

The portable core of nornir's multi-aspect test matrix — wrap a repo's native Rust test framework and test many aspects of its health (build, doctest, clippy, fmt, audit, coverage, feature-powerset, msrv, examples), parse the results into rows, and ship them to any TestSink.

Pure std + serde/serde_json + anyhow. No iceberg, arrow, eframe, tantivy, or skade — so any leaf repo can pull it cheaply.

use nornir_testmatrix::{run_full_matrix, Aspect, JsonFileSink, TestSink};
use std::path::Path;

let aspects = Aspect::DEFAULT; // build, unit, doctest, clippy, fmt, audit
let rows = run_full_matrix(Path::new("."), aspects);
JsonFileSink::new("target/nornir-testmatrix.json").append(&rows).unwrap();

A missing tool (clippy/audit/llvm-cov/hack/fmt) is recorded as a neutral skip row — never a hard failure. See .nornir/testmatrix-crate.md.

Functional-status mode (testmatrix feature)

A component can report whether it actually works — even when the result can't be eyeballed (a map that renders nothing). Behind the testmatrix cargo feature (off for release → zero cost), call from your self-test path:

nornir_testmatrix::functional_status("facett-map", "basemap_rendered", ok, "0 ways, blank framebuffer");

Aspect::Functional drains these into test_results rows (ok=false → a RED matrix row), so a dead render shows up in nornir test / the viz Test pane without anyone looking at a GUI. With the feature off, functional_status is an inlined no-op. See .nornir/testmatrix-functional-status.md.

Silenced-test guard (gatedtests module, Aspect::GatedTests)

Silence is not success. A tests/*.rs carrying a crate-level #![cfg(feature = "X")] where X is not in that crate's default set compiles to an empty test binary: cargo test prints running 0 tests ... ok and the file is green by vacuum. A 2026-07-21 sweep found 78 such files across 11 repos, hiding ~216 test functions — most in the camouflage shape all(default-on, default-on, default-OFF), which reads exactly like its running siblings.

let rep = nornir_testmatrix::audit_repo(std::path::Path::new("."), &nornir_testmatrix::new_run_id())?;
assert!(rep.is_green(), "{}", rep.summary());
// 93 test targets scanned · 21 feature-gated · 0 rescued by declared arms ·
// 15 SILENCED (66 hidden test fns) — RED

The guard walks every test target (cargo metadata), parses the leading #![cfg] (all/any/not, nested; non-feature predicates evaluate to unknown, never false), resolves each crate's transitive default closure, and emits a RED TestResultRow on the gated-tests aspect per dark file. A file IS covered when the repo declares the arm that re-invokes it, in .nornir/testmatrix-arms.json (holger's xtask/tests/holger_matrix.rs pattern):

{ "arms": [ { "package": "holger-ui", "features": ["gui"], "test": "robot_ui_app" },
            { "features": ["testmatrix"] } ] }

It is in Aspect::DEFAULT, so every repo running the matrix inherits it. See .nornir/testmatrix-gated-tests.md.

Test targets that do not COMPILE (targetcheck, Aspect::TargetCompile)

cargo check is not a proxy for cargo test, and a test target that does not compile is reported by nothing. cargo test never lists it, the matrix has no row for it, and the silenced-test guard above is looking at the other failure — a file that compiles to an empty binary. A file that does not compile at all never reaches that question.

Four independent instances, all measured on 2026-08-22:

# where what check said what was actually true
1 korp --features server clean cargo test --features server --lib → 4× E0308; control::assets_tests had never run
2 nornir clean 26 of 101 declared test targets could not compile — the whole viz_* / robot_* surface
3 korp tests/robot_scene.rs clean two #[test]s behind a two-feature gate; its arm had been RED for 25 days
4 korp-ui clean the client,map3d-gpu,collector-feeds --no-default-features combination — declared in the repo's own build-variants.toml — did not compile at all
let crates  = nornir_testmatrix::workspace_crates(root)?;
let configs = nornir_testmatrix::configs_from_metadata(&crates);
let report  = nornir_testmatrix::check_targets(root, &crates, &configs);
assert!(report.is_green(), "{}", report.summary());
// nornir: 26 target×config FAILURE(S)
//   nornir::viz_buttons  [default / default]
//     E0599: no method named `build_state` found for struct `HarnessBuilder<State>`

Three things make it a guard rather than a build:

Which configurations — enumerated from data, never a list somebody writes

source where it comes from why it is not optional
default a plain workspace build what cargo test compiles
--all-features the opposite corner the caller's, not the aspect's (a repo with exclusive features cannot build it)
required-features each test target's own manifest entry cargo silently skips such a target — no warning, no error, no output line
declared arms .nornir/testmatrix-arms.json the rescue arms the silenced-test guard already trusts
build variants .nornir/build-variants.toml the combinations the repo ships — instance 4 is declared here and nowhere else

Not covered, plainly: the feature powerset (Aspect::FeaturePowerset owns that axis and it is exponential), cross-compile targets, non-dev profiles, and any combination a human runs that no file in the repo declares. The claim is "every combination this repo declares", not "every combination that exists".

configs_from_variants takes plain VariantSpecs: nornir-build-thing is the one writer of that TOML, this crate's dependency budget is serde + anyhow, and the edge between them is a cargo metadata cycle. examples/target_compile.rs carries the six-line bridge, and doubles as the multi-repo driver:

cargo run --example target_compile -- ../korp ../nornir [--only-default] [--json rows.json]

Cost, and where this belongs

Compiling is the whole cost, and the default depth is cargo check --all-targets, not a build. Measured on oden (32 cores, CARGO_BUILD_JOBS=8, CARGO_INCREMENTAL=0), nornir at origin/main, cold target dir:

depth wall-clock target dir broken targets found
Depth::Check (default) 2 min 20 s 2.8 GB 26
Depth::Build 9 min 51 s ~170 GB 26

Same twenty-six, 4.2× the wall-clock and 60× the disk — 170 GB per repo is how /home hit 100 % five times in one night. Depth::Build is still not redundant: it is the only one that reaches codegen and the linker, so it catches a monomorphisation error in a generic nothing instantiates while checking, a const evaluated at codegen, and a missing native library. Every instance measured so far is a type or name error, which check sees.

Per-repo, one default configuration at check depth, cold shared target dir, 2026-08-22: funnel-core 5 s · draupnir 6 s · Skidbladnir 13 s · korp-collectors 13 s · znippy-zoomies 18 s · tunnr 21 s · skade 39 s · gunnar 58 s · korp 78 s · facett 83 s · holger 84 s · knut 85 s · nornir 94 s · dwarves 185 s · znippy 223 s · edda 65 s. Eighteen repos in eighteen minutes. Each extra configuration is another pass of the same size — korp's own twenty declared configurations (default + 11 arms + 7 build variants + required-features) take 354 s together.

What the sweep found (2026-08-22, worktrees off each repo's origin/HEAD)

55 of 1 115 declared test targets across 18 repos cannot be compiled today — 4.9 %, in 7 of 18 repos. Not all the same cause: 29 are per-target compile errors (nornir 26, dwarves 1, and edda 2 — found by pointing the guard at the repo it was written in), 22 are two repos whose dependencies do not resolve at all (modgunn, ordning), 4 sit behind a broken lib (tunnr 3, knut 1).

That is the DEFAULT configuration only, and it is a floor rather than the number. korp is the one repo swept across everything it declares, and it goes from 158/158 green on a default build to 38 target×config failures — including two targets that are red under every one of its seven declared build variants, i.e. every binary that ships.

Aspect::TargetCompile is deliberately not in Aspect::DEFAULT — a guard that makes the default matrix unaffordable is a guard that gets switched off. It belongs in an xtask verb or an explicit --aspect target-compile run, not on every cargo test. Note that a repo with broken targets pays the cost every run: a target that fails never caches a success.

UI-plane reachability — LAW 9 (uiplane module)

Isolation tests green "the component renders" while the running app can't navigate to it. uiplane models the UI as planes (UiPlane: a tab / dialog / view) + a navigation plan (UiPlan: planes as nodes, transitions as edges) and RobotPlan::walk(&plan, &mut driver) walks the REAL app via a PlaneDriver seam — reaching every plane and asserting each declared surface is present + RAN (read from emitted data, not pixels). A surface that is unreachable (orphan plane / missing transition) or reachable-but-didn't-run is a RED row. Generic + UI-agnostic (a headless library has an empty plan → trivially green); the native robot-UI and the deployed-wasm headless browser are two backends behind the same PlaneDriver. See .nornir/testmatrix-uiplane.md.