crates/nornir-testmatrix/src/functional.rs
//! **Functional-status mode** — a component reports whether it *actually works*.
//!
//! The other aspects ([`crate::Aspect`]) shell `cargo` and judge a repo from the
//! outside. This one lets a component judge ITSELF from the inside: during its
//! headless-render / self-test path a component calls
//! [`functional_status`]`(component, check, ok, detail)` to record e.g.
//! `facett-map / basemap_rendered / ok=false / "0 ways, blank framebuffer"`. The
//! [`Aspect::Functional`](crate::Aspect::Functional) runner drains those records
//! ([`drain_functional_rows`]) into [`TestResultRow`]s so a broken render shows
//! up as a **RED matrix row** in `nornir test` / the viz 🧪 Test pane —
//! *automatically, without anyone looking at a GUI*.
//!
//! ## Release-gating (the whole point)
//! The recording machinery lives behind the **`testmatrix` cargo feature** so a
//! release build pays ZERO cost:
//!
//! * `testmatrix` ON → [`functional_status`] pushes a record into a
//! process-global buffer; [`drain_functional_rows`] returns + clears them.
//! * `testmatrix` OFF → [`functional_status`] is an `#[inline]` empty stub that
//! takes no args by value it can't drop cheaply, touches no global, and
//! compiles to nothing; [`drain_functional_rows`] returns an empty `Vec`.
//!
//! ```text
//! #[cfg(feature = "testmatrix")] pub fn functional_status(..) { record }
//! #[cfg(not(feature = "testmatrix"))] #[inline] pub fn functional_status(..) {} // no-op
//! ```
//!
//! ### Two adoption patterns (a consumer uses ONE per call site)
//! 1. **Cheap detail** — just call it; the stub strips the call in release:
//! ```ignore
//! nornir_testmatrix::functional_status("facett-map", "basemap_rendered", ok, "0 ways");
//! ```
//! 2. **Expensive detail** — if building `detail` costs real work (formatting a
//! scene dump, counting framebuffer pixels), wrap the WHOLE thing so even the
//! string-building is stripped in release:
//! ```ignore
//! #[cfg(feature = "testmatrix")]
//! {
//! let detail = format!("{} ways, {} px lit", scene.way_count(), fb.lit_pixels());
//! nornir_testmatrix::functional_status("facett-map", "basemap_rendered", ok, &detail);
//! }
//! ```
use crate::model::{status, TestResultRow};
/// Build a single functional [`TestResultRow`] (aspect `functional`) for one
/// component's self-reported check. `suite = component`, `test_name = check`,
/// `status = pass|fail`, `message = detail`. PURE — no globals, always
/// available (release too) so the row shape is testable without the feature.
///
/// The `run_id` / `repo` / `ts_micros` are left blank/zero here because the
/// [`Aspect::Functional`](crate::Aspect::Functional) runner restamps every
/// drained row with the run's shared identity (see `outcome_to_rows`).
/// The `aspect` string stamped on every functional-status row.
pub const ASPECT_FUNCTIONAL: &str = "functional";
/// **The word a row uses to say: this kind has no such stage.**
///
/// Not `pass` (nothing was proven), not `fail` (nothing is broken), not silence
/// (which reads as a hole somebody must go fill). A `skip` row whose message
/// opens with this marker is the third answer —
/// [`ProofVerdict::Na`](crate::appliance::ProofVerdict::Na) — and the reason
/// after the marker is the whole point: *why* there is no such stage here.
///
/// ONE spelling, in one place, because the emitter
/// ([`functional_na`]) and the two readers
/// ([`ApplianceRollup`](crate::appliance::ApplianceRollup) and
/// [`inventory`](crate::inventory)) must not be able to spell it differently.
pub const NA_MARKER: &str = "n/a:";
/// Build the `skip` row that declares a check **does not apply** to this
/// component, with the reason. PURE, like [`functional_row`].
pub fn functional_na_row(component: &str, check: &str, reason: &str) -> TestResultRow {
TestResultRow {
run_id: String::new(),
repo: String::new(),
suite: component.to_string(),
test_name: check.to_string(),
status: status::SKIP.to_string(),
duration_ms: 0.0,
ts_micros: 0,
message: format!("{NA_MARKER} {}", reason.trim()),
aspect: ASPECT_FUNCTIONAL.to_string(),
metric: 0.0,
}
}
pub fn functional_row(component: &str, check: &str, ok: bool, detail: &str) -> TestResultRow {
TestResultRow {
run_id: String::new(),
repo: String::new(),
suite: component.to_string(),
test_name: check.to_string(),
status: if ok { status::PASS } else { status::FAIL }.to_string(),
duration_ms: 0.0,
ts_micros: 0,
message: detail.to_string(),
aspect: "functional".to_string(),
metric: if ok { 0.0 } else { 1.0 },
}
}
// ─── feature ON: record into a process-global buffer ────────────────────────
#[cfg(feature = "testmatrix")]
mod recorder {
use super::functional_row;
use crate::model::TestResultRow;
use std::sync::{Mutex, OnceLock};
fn buffer() -> &'static Mutex<Vec<TestResultRow>> {
static BUF: OnceLock<Mutex<Vec<TestResultRow>>> = OnceLock::new();
BUF.get_or_init(|| Mutex::new(Vec::new()))
}
/// **The ONE writer to the cross-process sink.** Both [`record`] (which
/// builds a row from parts) and [`emit_rows`] (which is handed rows already
/// built) go through here, so "a row reached the JSONL" has exactly one
/// implementation and cannot drift between the two doors.
///
/// Best-effort by design: a sink error never wedges a test.
fn mirror_to_sink(row: &TestResultRow) {
if let Ok(out) = std::env::var("NORNIR_TESTMATRIX_OUT") {
if !out.is_empty() {
let mut frow = row.clone();
if let Ok(repo) = std::env::var("NORNIR_TESTMATRIX_REPO") {
if !repo.is_empty() {
frow.repo = repo;
}
}
// Concurrency-safe: cargo runs tests in PARALLEL, so a
// read-modify-write (JsonFileSink::append) would race and lose
// rows. O_APPEND of one compact JSON-array line (< PIPE_BUF) is
// atomic; `JsonFileSink::read_all` flattens each line's array.
if let Ok(mut line) = serde_json::to_string(std::slice::from_ref(&frow)) {
// ONE pre-built buffer → ONE write_all syscall. `writeln!` can
// fragment into several write() calls which, under parallel
// test threads + O_APPEND, interleave into corrupt lines that
// fail to parse. A single <PIPE_BUF append write is atomic.
line.push('\n');
use std::io::Write;
if let Ok(mut f) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&out)
{
let _ = f.write_all(line.as_bytes());
}
}
}
}
}
/// Record a component's functional self-report (feature ON). Lock poisoning
/// is recovered from — a panic mid-record must never wedge the matrix.
///
/// Buffers the row AND mirrors it through [`mirror_to_sink`], so a leaf
/// repo's `cargo test --features testmatrix` subprocess (whose in-memory
/// buffer dies with it) still reaches the parent via the JSONL file. With
/// `NORNIR_TESTMATRIX_OUT` unset this is just the buffer push — no I/O.
pub fn record(component: &str, check: &str, ok: bool, detail: &str) {
let row = functional_row(component, check, ok, detail);
mirror_to_sink(&row);
let mut buf = buffer().lock().unwrap_or_else(|p| p.into_inner());
buf.push(row);
}
/// **Emit already-built rows AS A PRODUCER** — buffer them *and* mirror each
/// one to the cross-process sink, exactly as [`record`] does.
///
/// This exists because [`push_rows`] does NOT mirror, and that asymmetry bit
/// a real drive. Measured 2026-08-15: a product's drive emitted its
/// pass/fail cells through `functional_status` (which mirrors) and its
/// LOUD-SKIP cells through `record_rows` (which does not) — so in a
/// subprocess run the skip row was the ONE row that vanished. A silent cap
/// wearing the word "loud", and precisely the shape the matrix exists to
/// catch. Two consumers had already grown private workarounds for it.
///
/// The split is kept rather than collapsed because the two directions are
/// genuinely different: [`push_rows`] FOLDS BACK rows the harness already
/// read *out of* the sink (mirroring those would duplicate every one), while
/// this door is for a producer emitting its own. One writer underneath
/// ([`mirror_to_sink`]); two doors, each named for its direction.
pub fn emit_rows<I: IntoIterator<Item = TestResultRow>>(rows: I) {
let rows: Vec<TestResultRow> = rows.into_iter().collect();
for row in &rows {
mirror_to_sink(row);
}
let mut buf = buffer().lock().unwrap_or_else(|p| p.into_inner());
buf.extend(rows);
}
/// Take + clear every recorded row (feature ON).
pub fn drain() -> Vec<TestResultRow> {
let mut buf = buffer().lock().unwrap_or_else(|p| p.into_inner());
std::mem::take(&mut *buf)
}
/// Push already-built rows into the buffer (feature ON). Used to fold
/// functional rows a leaf-repo SUBPROCESS emitted — which the matrix harness
/// read back from the `NORNIR_TESTMATRIX_OUT` JSONL sink — into the same
/// process-global buffer the [`Aspect::Functional`](crate::Aspect::Functional)
/// runner drains, so a subprocess's self-reports land exactly like in-process
/// ones. Lock poisoning is recovered from.
pub fn push_rows<I: IntoIterator<Item = TestResultRow>>(rows: I) {
let mut buf = buffer().lock().unwrap_or_else(|p| p.into_inner());
buf.extend(rows);
}
}
/// Record a component's **functional status** for the test matrix.
///
/// `component` is the unit reporting (e.g. `"facett-map"`), `check` is what it
/// verified (e.g. `"basemap_rendered"`), `ok` is the verdict, and `detail` is a
/// short human note (e.g. `"0 ways, blank framebuffer"`). With the `testmatrix`
/// feature ON the record lands in a process-global buffer that the
/// [`Aspect::Functional`](crate::Aspect::Functional) runner drains into matrix
/// rows. With the feature OFF this is an `#[inline]` no-op (see the module-level
/// release-gating note) — nothing is compiled or written.
#[cfg(feature = "testmatrix")]
pub fn functional_status(component: &str, check: &str, ok: bool, detail: &str) {
recorder::record(component, check, ok, detail);
}
/// No-op stub when the `testmatrix` feature is OFF (release builds). Inlined and
/// empty so the call — and, with the wrap-the-call-site pattern, the detail
/// string-building — is stripped entirely.
#[cfg(not(feature = "testmatrix"))]
#[inline]
pub fn functional_status(_component: &str, _check: &str, _ok: bool, _detail: &str) {}
/// **Declare that `check` does not apply to `component`, and say why.**
///
/// The third answer, next to [`functional_status`]'s two. A ladder rung that
/// this product was never going to climb — a library that never boots off a
/// disk, a forge with no installer ISO — must not sit in the grid as a hole
/// somebody could go fill, and must never be faked green to get rid of the
/// hole. It emits a `skip` row marked [`NA_MARKER`], which the appliance
/// rollup folds to [`ProofVerdict::Na`](crate::appliance::ProofVerdict::Na)
/// and the [`inventory`](crate::inventory) scan reads straight out of the
/// source as `n/a`.
///
/// The reason is not optional prose: it is the whole content of the claim.
#[cfg(feature = "testmatrix")]
pub fn functional_na(component: &str, check: &str, reason: &str) {
recorder::emit_rows([functional_na_row(component, check, reason)]);
}
/// No-op stub when the `testmatrix` feature is OFF — the zero-cost twin of
/// [`functional_status`]'s stub.
#[cfg(not(feature = "testmatrix"))]
#[inline]
pub fn functional_na(_component: &str, _check: &str, _reason: &str) {}
/// Drain (take + clear) the recorded functional rows. The
/// [`Aspect::Functional`](crate::Aspect::Functional) runner calls this. With the
/// `testmatrix` feature OFF this is an `#[inline]` empty `Vec` — no global, no
/// allocation path compiled in.
#[cfg(feature = "testmatrix")]
pub fn drain_functional_rows() -> Vec<TestResultRow> {
recorder::drain()
}
/// No-op drain when the `testmatrix` feature is OFF: always empty.
#[cfg(not(feature = "testmatrix"))]
#[inline]
pub fn drain_functional_rows() -> Vec<TestResultRow> {
Vec::new()
}
/// Fold already-built functional rows into the process-global buffer (feature
/// ON) so the [`Aspect::Functional`](crate::Aspect::Functional) runner drains
/// them alongside in-process emits. The matrix harness uses this to inject the
/// rows a leaf-repo `cargo test --features testmatrix` SUBPROCESS wrote to its
/// `NORNIR_TESTMATRIX_OUT` sink (the in-process buffer of that child is lost on
/// exit, so the parent reads the file and re-records the rows here).
#[cfg(feature = "testmatrix")]
pub fn record_rows<I: IntoIterator<Item = TestResultRow>>(rows: I) {
recorder::push_rows(rows);
}
/// No-op fold when the `testmatrix` feature is OFF (release builds): there is no
/// buffer to push into, so the rows are dropped — matching the zero-cost stub.
#[cfg(not(feature = "testmatrix"))]
#[inline]
pub fn record_rows<I: IntoIterator<Item = TestResultRow>>(_rows: I) {}
/// **Emit already-built rows as a PRODUCER** — buffered *and* mirrored to the
/// cross-process `NORNIR_TESTMATRIX_OUT` sink, exactly like
/// [`functional_status`].
///
/// Use this whenever YOUR code is the source of the rows. Use [`record_rows`]
/// only to fold back rows the harness already read *out of* the sink.
///
/// Measured 2026-08-15: a product drive emitted its pass/fail cells through
/// `functional_status` (which mirrors) and its LOUD-SKIP cells through
/// `record_rows` (which does not), so in a subprocess run the skip row was the
/// ONE row that vanished — a silent cap wearing the word "loud". Two separate
/// consumers had already grown private workarounds for it, which is the signal
/// that the door, not the callers, was wrong.
#[cfg(feature = "testmatrix")]
pub fn emit_rows<I: IntoIterator<Item = TestResultRow>>(rows: I) {
recorder::emit_rows(rows);
}
/// No-op emit when the `testmatrix` feature is OFF — the zero-cost stub.
#[cfg(not(feature = "testmatrix"))]
#[inline]
pub fn emit_rows<I: IntoIterator<Item = TestResultRow>>(_rows: I) {}
/// Drain the cross-process functional sink at `path` — the JSONL file a leaf
/// repo's `cargo test --features testmatrix` subprocess appended its
/// [`functional_status`] emits to (via `NORNIR_TESTMATRIX_OUT`). Reads every row
/// back (the same JSONL-of-arrays format [`crate::sink::JsonFileSink`] writes),
/// then REMOVES the file so a later drain can't double-count the same run.
/// Always available (pure file I/O, no feature gate) so the runner / aspect
/// engine can collect subprocess emits regardless of how the parent was built. A
/// missing or unreadable file yields an empty `Vec` — collection never hard-fails.
pub fn drain_functional_file(path: &std::path::Path) -> Vec<TestResultRow> {
let rows = crate::sink::JsonFileSink::new(path.to_path_buf())
.read_all()
.unwrap_or_default();
let _ = std::fs::remove_file(path);
rows
}
/// Test-only serialization lock for the process-global functional buffer. Tests
/// that emit-then-drain across the whole crate take this so an interleaving
/// drain in another test never steals their records. (Re-used by `aspect.rs`'s
/// functional-aspect tests — same buffer, same lock.)
#[cfg(all(test, feature = "testmatrix"))]
pub(crate) fn test_lock() -> std::sync::MutexGuard<'static, ()> {
use std::sync::{Mutex, OnceLock};
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
.lock()
.unwrap_or_else(|p| p.into_inner())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::ASPECT_UNIT;
/// RED-BEFORE-GREEN: an inapplicable rung is neither pass nor fail, and it
/// says why — a `skip` with no reason is the silence this crate refuses.
#[test]
fn an_inapplicable_check_is_a_skip_that_carries_its_reason() {
let r = functional_na_row("nornir-appliance", "install_ok", "nornir ships no installer");
assert_eq!(r.status, status::SKIP, "not pass — nothing was proven");
assert_ne!(r.status, status::FAIL, "and not fail — nothing is broken");
assert!(r.message.starts_with(NA_MARKER), "the marker leads: {}", r.message);
assert!(r.message.contains("no installer"), "the REASON is the content: {}", r.message);
assert_eq!(r.metric, 0.0, "an n/a is not a red on any dashboard");
assert_eq!((r.suite.as_str(), r.test_name.as_str()), ("nornir-appliance", "install_ok"));
}
#[test]
fn functional_row_maps_ok_false_to_red_fail() {
// ok=false → a RED fail row the matrix reads as broken, carrying the detail.
let r = functional_row(
"facett-map",
"basemap_rendered",
false,
"0 ways, blank framebuffer",
);
assert_eq!(r.suite, "facett-map", "component → suite");
assert_eq!(r.test_name, "basemap_rendered", "check → test_name");
assert_eq!(r.status, status::FAIL, "ok=false is a fail");
assert!(
status::is_red(&r.status),
"and it reads as RED for the matrix"
);
assert_eq!(r.message, "0 ways, blank framebuffer", "detail preserved");
assert_eq!(r.aspect, "functional");
assert_eq!(r.metric, 1.0, "one red check");
// sanity: it is NOT the legacy unit aspect.
assert_ne!(r.aspect, ASPECT_UNIT);
}
#[test]
fn functional_row_maps_ok_true_to_green_pass() {
let r = functional_row("facett-map", "basemap_rendered", true, "1024 ways drawn");
assert_eq!(r.status, status::PASS, "ok=true is a pass");
assert!(status::is_green(&r.status), "GREEN for the matrix");
assert!(!status::is_red(&r.status));
assert_eq!(r.metric, 0.0, "no red checks");
assert_eq!(r.message, "1024 ways drawn");
}
// ── feature ON: emit → drain → it materializes as the matrix sees it ──
#[cfg(feature = "testmatrix")]
#[test]
fn emit_red_then_drain_yields_red_row_and_clears() {
// Serialize against every other emit/drain test (shared global buffer).
let _guard = super::test_lock();
let _ = drain_functional_rows();
functional_status("facett-map", "drain_red_check", false, "blank framebuffer");
let rows = drain_functional_rows();
let mine: Vec<_> = rows
.iter()
.filter(|r| r.test_name == "drain_red_check")
.collect();
assert_eq!(mine.len(), 1, "exactly the one I emitted");
assert_eq!(mine[0].status, status::FAIL);
assert!(status::is_red(&mine[0].status), "broken render = RED row");
assert_eq!(mine[0].message, "blank framebuffer");
// Drained → buffer cleared: a re-drain has none of mine.
let again = drain_functional_rows();
assert!(
!again.iter().any(|r| r.test_name == "drain_red_check"),
"drain clears the buffer"
);
}
#[cfg(feature = "testmatrix")]
#[test]
fn emit_green_drains_as_green_row() {
let _guard = super::test_lock();
let _ = drain_functional_rows();
functional_status("facett-map", "drain_green_check", true, "1024 ways");
let rows = drain_functional_rows();
let mine: Vec<_> = rows
.iter()
.filter(|r| r.test_name == "drain_green_check")
.collect();
assert_eq!(mine.len(), 1);
assert_eq!(mine[0].status, status::PASS);
assert!(status::is_green(&mine[0].status));
}
// ── feature OFF: prove the release path is a true no-op ──
#[cfg(not(feature = "testmatrix"))]
#[test]
fn release_path_is_a_noop_drain_is_empty() {
// The emit compiles to nothing; the drain is hard-wired empty regardless
// of how many times we "record". No global, no write.
functional_status("facett-map", "basemap_rendered", false, "would be red");
functional_status("facett-map", "anything", true, "");
assert!(
drain_functional_rows().is_empty(),
"release build records + drains NOTHING — zero matrix write"
);
}
/// **The two doors must differ, and in the direction each is named for.**
///
/// RED-when-broken: point `emit_rows` at `recorder::push_rows` and the first
/// assertion fails — the producer's rows never reach the sink, which is the
/// exact defect measured 2026-08-15 (a LOUD-SKIP row that vanished in a
/// subprocess run). Make `record_rows` mirror too and the second fails —
/// the harness folding rows it already READ from the sink would write every
/// one of them back, doubling the file.
#[cfg(feature = "testmatrix")]
#[test]
fn emit_rows_reaches_the_sink_and_record_rows_deliberately_does_not() {
let dir = std::env::temp_dir().join(format!("tm-doors-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let sink = dir.join("rows.jsonl");
let _ = std::fs::remove_file(&sink);
// SAFETY: single-threaded within this test; the env is restored below.
unsafe { std::env::set_var("NORNIR_TESTMATRIX_OUT", &sink) };
emit_rows([functional_row("doors", "produced", true, "a producer emitted this")]);
let after_emit = std::fs::read_to_string(&sink).unwrap_or_default();
assert!(
after_emit.contains("produced"),
"emit_rows is the PRODUCER door — its rows must reach the cross-process sink, \
or a subprocess loses them silently:\n{after_emit}"
);
record_rows([functional_row("doors", "folded", true, "the harness folded this back")]);
let after_fold = std::fs::read_to_string(&sink).unwrap_or_default();
assert!(
!after_fold.contains("folded"),
"record_rows FOLDS BACK rows already read OUT of the sink — mirroring them would \
duplicate every row the harness re-injects:\n{after_fold}"
);
unsafe { std::env::remove_var("NORNIR_TESTMATRIX_OUT") };
let _ = std::fs::remove_dir_all(&dir);
let _ = drain_functional_rows();
}
}