crates/nornir-bench/src/lib.rs
//! # nornir-bench
//!
//! Benchmark harness — project-agnostic.
//!
//! Provides a uniform run/result envelope plus an append-only JSONL
//! history. Repo-specific metrics live in [`BenchResult::metrics`] as
//! a free-form JSON object, so one envelope serves any project.
//!
//! nornir re-exports this as `nornir::bench` (plus the `register_bench!` /
//! `register_bench_ordered!` macros), so every `crate::bench::…` call site
//! resolves unchanged. The live progress trail is written directly to
//! `$NORNIR_VIZ_ACTIONLOG` in nornir's `viz::action_log` line format (no egui /
//! viz dependency is pulled in).
pub mod api;
pub mod assets;
pub mod diff;
pub mod history;
pub mod legacy;
/// **What the box IS** — the stable identity of the host, beside the id.
/// Distinct from [`telemetry`], which is what a RUN experienced.
pub mod machine;
pub mod progress;
/// The OUTER runner — find, build, run and parse a repo's `nornir-bench`
/// example. Behind the off-by-default `runner` feature so a consumer that only
/// wants the envelope links neither `toml` nor a subprocess. See
/// [`runner`](runner) for why the forge and the warehouse share one copy of it.
#[cfg(feature = "runner")]
pub mod runner;
pub mod selftest;
pub mod telemetry;
use serde::{Deserialize, Serialize};
pub use uuid::Uuid;
/// Process-wide serial lock for tests that mutate GLOBAL state (the
/// `NORNIR_VIZ_ACTIONLOG` env var). `cargo test` is multi-threaded by default,
/// so two tests touching the same env var race; such tests take
/// `let _g = crate::serial_guard();` to run one-at-a-time. Poison-tolerant.
#[cfg(test)]
pub(crate) fn serial_guard() -> std::sync::MutexGuard<'static, ()> {
static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
/// Where a [`BenchResult`]'s numbers came from — the capabilities/source tag
/// (issue #13). A **measured** result (the default) is a timed benchmark:
/// throughput/latency the harness clocked. A **static** result is a
/// capabilities probe — machine facts captured with NO timed work (core count,
/// SIMD flags, cache sizes, feature toggles). The tag lets a consumer tell a
/// static-capability row apart from a real measurement without inspecting the
/// (free-form) metric keys, and lets the no-regression gate SKIP static rows
/// (a machine-fact "drop" is not a perf regression).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum BenchSource {
/// A timed benchmark measurement (the default).
#[default]
Measured,
/// A static capabilities probe — machine facts, not a timed measurement.
Static,
}
impl BenchSource {
/// True for the default (`Measured`) — used by `skip_serializing_if` so the
/// tag is omitted for ordinary measured rows (byte-for-byte backward-compat
/// JSON: legacy `bench_history.jsonl` lines and every existing writer stay
/// unchanged on the wire).
pub fn is_measured(&self) -> bool {
matches!(self, BenchSource::Measured)
}
}
/// A single benchmark result. Repo-specific metrics live in `metrics`
/// so both holger (`holger_ops_sec` / `nexus_ops_sec`) and znippy
/// (`compress_mbs` / `decompress_mbs` / `files`) can share the same
/// run envelope.
///
/// `source` (issue #13) tags whether the row is a timed measurement or a
/// static-capabilities probe. It is `#[serde(default)]` (legacy JSONL lines
/// lacking it read as `Measured`) and skipped on the wire when `Measured`, so
/// adding it changes nothing for existing measured rows.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct BenchResult {
pub name: String,
#[serde(default, skip_serializing_if = "BenchSource::is_measured")]
pub source: BenchSource,
#[serde(flatten)]
pub metrics: serde_json::Map<String, serde_json::Value>,
}
impl BenchResult {
/// A **measured** result — a timed benchmark (the default source). The
/// canonical constructor for a bencher's timed numbers.
pub fn measured(
name: impl Into<String>,
metrics: serde_json::Map<String, serde_json::Value>,
) -> Self {
Self { name: name.into(), source: BenchSource::Measured, metrics }
}
/// A **static-capabilities** result — machine facts captured with no timed
/// work (the writer for issue #13's seam). The row carries the same
/// free-form `metrics` map (e.g. `n_cores`, `avx512`, `l3_kb`) but is
/// tagged [`BenchSource::Static`] so consumers can distinguish it.
pub fn static_capabilities(
name: impl Into<String>,
metrics: serde_json::Map<String, serde_json::Value>,
) -> Self {
Self { name: name.into(), source: BenchSource::Static, metrics }
}
/// True iff this row is a static-capabilities probe (not a timed measurement).
pub fn is_static(&self) -> bool {
matches!(self.source, BenchSource::Static)
}
}
/// The canonical **static-capabilities writer** (issue #13): probe the host for
/// machine facts (no timed work) and emit them as a [`BenchSource::Static`]
/// [`BenchResult`] named `name` (conventionally `<repo>.capabilities`). Sits
/// alongside a run's measured rows so a consumer knows the machine the numbers
/// were taken on, while the no-regression gate skips it (a machine-fact change
/// is not a perf regression). All facts are numeric so they survive the
/// warehouse's numeric metric ingest; booleans are encoded 1.0/0.0.
pub fn machine_capabilities(name: impl Into<String>) -> BenchResult {
let n_cores = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1) as f64;
let mut m = serde_json::Map::new();
m.insert("n_cores".into(), serde_json::json!(n_cores));
m.insert("pointer_width_bits".into(), serde_json::json!((usize::BITS) as f64));
// Compile-time target facts — static by definition (the binary's own build).
m.insert(
"target_x86_64".into(),
serde_json::json!(if cfg!(target_arch = "x86_64") { 1.0 } else { 0.0 }),
);
m.insert(
"target_aarch64".into(),
serde_json::json!(if cfg!(target_arch = "aarch64") { 1.0 } else { 0.0 }),
);
BenchResult::static_capabilities(name, m)
}
/// The single-thread bench-variant suffix. A bench name ending in `_st`
/// (e.g. `zoomies-lgz_st`) is the deliberately single-threaded variant of the
/// SAME system as its un-suffixed sibling (`zoomies-lgz`). It is written by the
/// single-core pass in [`api`] (which appends this suffix to every result +
/// test name of a serial-pinned pass). Two nornir subsystems must agree on this
/// one notion — hence the shared constant/helpers here rather than a literal
/// `"_st"` duplicated at each site:
/// - the **core-saturation gate** EXEMPTS `_st` rows (serial by design — one
/// busy core is correct, not an under-use bug);
/// - the **mashup docs** treat an `_st` row as a DISTINCT row of the same base
/// system (the single-core-vs-multi-core story), grouped under that system.
pub const SINGLE_THREAD_SUFFIX: &str = "_st";
/// True when a bench name is a single-thread (`_st`) variant — the deliberately
/// serial sibling of an un-suffixed base bench. See [`SINGLE_THREAD_SUFFIX`].
pub fn is_single_thread(name: &str) -> bool {
name.ends_with(SINGLE_THREAD_SUFFIX)
}
/// Strip the single-thread (`_st`) suffix, yielding the BASE bench name a
/// variant shares with its un-suffixed sibling (`zoomies-lgz_st` →
/// `zoomies-lgz`; a name without the suffix is returned unchanged). Lets a
/// consumer keep an `_st` row distinct while still relating it to its base
/// system. See [`SINGLE_THREAD_SUFFIX`].
pub fn strip_single_thread(name: &str) -> &str {
name.strip_suffix(SINGLE_THREAD_SUFFIX).unwrap_or(name)
}
/// Pass/fail outcome of a single test that ran alongside the bench.
///
/// `tests` lives next to `results` on a [`BenchRun`] so the no-regression
/// gate can fail a release both for scalar drops *and* for any test
/// flipping red. Optional fields (`duration_ms`, `message`) carry the
/// extra detail when the runner provides it (criterion / `cargo test`
/// JSON output etc.).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestOutcome {
pub name: String,
pub passed: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub duration_ms: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
/// One full bench run — one line in `bench_history.jsonl`.
///
/// Backward-compat fields:
/// - `version`, `machine`, `cores` are `#[serde(default)]` so legacy
/// entries (holger 2026-05-27 lines lacking machine/version/cores;
/// znippy lines lacking machine) deserialize cleanly. Going forward
/// all newly written runs must populate them — `history::append`
/// rejects empty `machine`.
/// - `date` (YYYY-MM-DD string) is retained for legacy compatibility.
/// New writers should also populate `timestamp` (RFC 3339, UTC) which
/// becomes the canonical time column in the Iceberg `bench_runs`
/// table once the warehouse phase lands.
/// - `tests` defaults to empty so existing JSONL parses; new runs are
/// expected to populate it.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BenchRun {
/// **The run's identity, produced HERE, by the producer, at measurement
/// time** — a UUID **v7**, which is time-ordered: the id sorts the way the
/// date sorts and rows written minutes apart cluster in storage.
///
/// Until 2026-09-20 this did not exist. The uuid was minted SERVER-SIDE by
/// `nornir`'s `append_bench_run_async` and returned, so the receipt on disk
/// had no identity at all, with three measured consequences:
/// - a second upload of the same run made a second row, so a failed submit
/// could not be retried;
/// - gunnar's `bench-landed` guard had to join receipts to warehouse rows
/// on a GUESS (timestamp truncated to the second plus the arm-name set),
/// because the two stores disagree on sub-second precision;
/// - 16 of 46 receipts never reached the warehouse and could not be
/// repaired afterwards, because nothing identified them.
///
/// `#[serde(default)]` yields [`Uuid::nil`] for a line written before this
/// field existed, so every `bench_history.jsonl` already on disk still
/// parses. A nil id means "legacy, no identity" — see [`BenchRun::has_id`];
/// consumers must fall back to the old heuristic join for those and MUST
/// NOT treat nil as a real id (every legacy row would collide).
#[serde(default)]
pub id: Uuid,
pub date: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timestamp: Option<String>,
#[serde(default)]
pub version: String,
#[serde(default)]
pub machine: String,
#[serde(default)]
pub cores: u32,
/// **What the BOX is** — hostname, CPU model, physical cores, logical
/// threads, total RAM, GPU, kernel, OS. Distinct from `machine` (the
/// operator's label) and from [`telemetry::Telemetry`] (what the RUN
/// experienced). `None` on a legacy line and on a producer that chose not
/// to look. See [`machine::Machine`].
#[serde(default, skip_serializing_if = "Option::is_none")]
pub machine_facts: Option<machine::Machine>,
pub results: Vec<BenchResult>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tests: Vec<TestOutcome>,
}
impl BenchRun {
/// True when this run carries a real producer-minted id. False for a line
/// written before the field existed (nil id) — such a run must be joined
/// heuristically and must never be deduplicated on its id, because every
/// legacy line has the same one.
pub fn has_id(&self) -> bool {
!self.id.is_nil()
}
/// Mint a fresh v7 id. Call this exactly where a run is MADE.
pub fn new_id() -> Uuid {
Uuid::now_v7()
}
/// The id as it rides a **non-JSON** wire (the gRPC `BenchRun.id` field,
/// which is a `string` because proto has no uuid type). A nil id — a legacy
/// receipt with no identity — is the EMPTY STRING, never
/// `"00000000-0000-…"`: the empty string is what tells the far side "this
/// one cannot be deduplicated", and a printed nil uuid would look like a
/// real key that every legacy run shares.
///
/// One copy of the spelling, here, because five call sites encode it
/// (`server_grpc`, `mcp_stdio`, the CLI's submit and history decoders) and
/// a sixth would eventually disagree with the rest.
pub fn id_wire(&self) -> String {
if self.has_id() { self.id.to_string() } else { String::new() }
}
/// Inverse of [`BenchRun::id_wire`]. An empty or unparsable field yields
/// the nil id, i.e. "no identity" — never an error, because an old peer
/// that does not send the field must still be able to submit a run.
pub fn id_from_wire(s: &str) -> Uuid {
Uuid::parse_str(s).unwrap_or_else(|_| Uuid::nil())
}
/// [`BenchRun::machine_facts`] as JSON for the same non-JSON wire. Empty
/// string when the producer did not look at the box.
pub fn machine_facts_wire(&self) -> String {
self.machine_facts
.as_ref()
.and_then(|m| serde_json::to_string(m).ok())
.unwrap_or_default()
}
/// Inverse of [`BenchRun::machine_facts_wire`]. Unparsable JSON yields
/// `None` — the facts are context, never a reason to drop a measurement.
pub fn machine_facts_from_wire(s: &str) -> Option<machine::Machine> {
if s.trim().is_empty() {
return None;
}
serde_json::from_str(s).ok()
}
pub fn find(&self, name: &str) -> Option<&BenchResult> {
self.results.iter().find(|r| r.name == name)
}
/// True iff every test in `tests` passed. An empty `tests` vec is
/// treated as "no tests run" → returns true.
pub fn all_tests_passed(&self) -> bool {
self.tests.iter().all(|t| t.passed)
}
/// Convenience: list the names of any failed tests.
pub fn failed_tests(&self) -> Vec<&str> {
self.tests
.iter()
.filter(|t| !t.passed)
.map(|t| t.name.as_str())
.collect()
}
}
/// Which way is "better" for a metric — used by the `benches` doc renderer
/// to **bold the winning cell** in a comparison row (e.g. ours vs a legacy
/// tool). `High` = bigger is better (throughput), `Low` = smaller is better
/// (latency/time), `Neutral` = not a competition (counts, ratios) → never
/// bolded.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MetricDirection {
High,
Low,
Neutral,
}
/// Resolve a metric's direction: an explicit `overrides` entry wins (the
/// `benches` renderer fills this from a `best=metric:low` marker arg); otherwise
/// fall back to the unit implied by the metric name (the `_mbs` / `_ms` / …
/// convention — see [`unit_of`]).
pub fn direction_of(
overrides: &std::collections::HashMap<String, MetricDirection>,
metric: &str,
) -> MetricDirection {
if let Some(d) = overrides.get(metric) {
return *d;
}
unit_of(metric).map(|u| u.direction).unwrap_or(MetricDirection::Neutral)
}
/// A recognised metric unit and its natural direction. Two metric cells are
/// "comparable" (compete in a row, eligible for bolding) iff they share the
/// same `unit.name`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Unit {
pub name: &'static str,
pub direction: MetricDirection,
}
/// Recognise the trailing unit of a metric key. Covers both the
/// `<corpus>_<suffix>` convention (`_mbs`/`_ms`/`_pct`) and the longhand
/// holger-style keys (`mb_per_sec`/`ops_per_sec`/`seconds`). Returns `None`
/// for plain counts (`bytes`, `ops`, `files`) — those never bold.
pub fn unit_of(metric: &str) -> Option<Unit> {
use MetricDirection::*;
let m = metric.to_ascii_lowercase();
// (suffix, unit-name, direction) — order matters: longest/most-specific first.
const TABLE: &[(&str, &str, MetricDirection)] = &[
("mb_per_sec", "mbs", High),
("mbps", "mbs", High),
("_mbs", "mbs", High),
("gb_per_sec", "gbs", High),
("_gbs", "gbs", High),
("ops_per_sec", "ops_sec", High),
("ops_sec", "ops_sec", High),
("_ops_sec", "ops_sec", High),
// generic throughput (rows_per_sec, commits_per_sec, …) — more is better.
("_per_sec", "per_sec", High),
("seconds", "secs", Low),
("_secs", "secs", Low),
// bare seconds columns (build_s, query_s, total_s) — less is better.
("_s", "secs", Low),
("_ms", "ms", Low),
("_us", "us", Low),
("_ns", "ns", Low),
("_pct", "pct", Neutral),
("_x", "x", Neutral),
("speedup", "x", Neutral),
];
for (suffix, name, dir) in TABLE {
if m == *suffix || m.ends_with(suffix) {
return Some(Unit { name, direction: *dir });
}
}
None
}
#[cfg(test)]
mod source_tests {
use super::*;
#[test]
fn static_and_measured_are_distinguishable() {
let mut m = serde_json::Map::new();
m.insert("n_cores".into(), serde_json::json!(12));
let cap = BenchResult::static_capabilities("nornir.capabilities", m.clone());
let meas = BenchResult::measured("nornir.dep_graph_build", m);
assert!(cap.is_static(), "capabilities probe is static");
assert!(!meas.is_static(), "a measured bench is not static");
assert_eq!(cap.source, BenchSource::Static);
assert_eq!(meas.source, BenchSource::Measured);
}
#[test]
fn measured_tag_is_omitted_on_the_wire_but_static_survives_roundtrip() {
// Backward-compat: a Measured row serializes WITHOUT a `source` key, so
// legacy readers/JSONL are unaffected.
let meas = BenchResult::measured("b", serde_json::Map::new());
let j = serde_json::to_value(&meas).unwrap();
assert!(j.get("source").is_none(), "Measured omits the tag: {j}");
// A Static row emits `"source":"static"` and round-trips back to Static.
let mut m = serde_json::Map::new();
m.insert("avx512".into(), serde_json::json!(1.0));
let cap = BenchResult::static_capabilities("caps", m);
let s = serde_json::to_string(&cap).unwrap();
assert!(s.contains("\"source\":\"static\""), "Static emits the tag: {s}");
let back: BenchResult = serde_json::from_str(&s).unwrap();
assert!(back.is_static());
assert_eq!(back.metrics.get("avx512"), Some(&serde_json::json!(1.0)));
}
#[test]
fn legacy_line_without_source_reads_as_measured() {
// A pre-#13 JSONL result (no `source` key) must default to Measured.
let back: BenchResult =
serde_json::from_str(r#"{"name":"b","x_mbs":1.0}"#).unwrap();
assert_eq!(back.source, BenchSource::Measured);
assert!(!back.is_static());
assert_eq!(back.metrics.get("x_mbs"), Some(&serde_json::json!(1.0)));
}
}
#[cfg(test)]
mod direction_tests {
use super::*;
use std::collections::HashMap;
#[test]
fn unit_and_direction_from_name() {
assert_eq!(unit_of("ljar_mbs").unwrap().name, "mbs");
assert_eq!(unit_of("unzip_mbs").unwrap().direction, MetricDirection::High);
assert_eq!(unit_of("mb_per_sec").unwrap().name, "mbs");
assert_eq!(unit_of("holger_ops_sec").unwrap().name, "ops_sec");
assert_eq!(unit_of("decode_ms").unwrap().direction, MetricDirection::Low);
assert_eq!(unit_of("seconds").unwrap().direction, MetricDirection::Low);
assert_eq!(unit_of("speedup_x").unwrap().direction, MetricDirection::Neutral);
// plain counts have no unit → never bolded
assert!(unit_of("bytes").is_none());
assert!(unit_of("files").is_none());
}
#[test]
fn explicit_override_wins() {
let mut o = HashMap::new();
o.insert("weird_metric".to_string(), MetricDirection::Low);
assert_eq!(direction_of(&o, "weird_metric"), MetricDirection::Low);
// unlisted → suffix fallback
assert_eq!(direction_of(&o, "ljar_mbs"), MetricDirection::High);
assert_eq!(direction_of(&o, "count"), MetricDirection::Neutral);
}
}
/// `bench history-show` rendered as a uniform [`nornir_cli_outcome::CommandOutcome`]
/// — the shared DATA FACE for the CLI (`run_bench`) and, by parity, the viz Bench
/// tab. `runs` arrive from the canonical source (warehouse `query_bench_runs` in fat
/// mode, the `Bench.History` RPC in thin mode); this fn only SHAPES + renders them,
/// it does not fetch (CommandOutcome is presentation, never a transport). Newest
/// first, optional `limit` cap. `ok ⟺ ≥1 run` (RAGNARÖK: an empty history is RED,
/// never silently green). AUT9 — see `.nornir/cli-command-contract.md`.
pub fn history_outcome(
repo: &str,
mut runs: Vec<BenchRun>,
limit: usize,
) -> nornir_cli_outcome::CommandOutcome {
use nornir_cli_outcome::CommandOutcome;
// Newest first (timestamp if present, else date), then optional cap.
runs.sort_by(|x, y| {
let kx = x.timestamp.as_deref().unwrap_or(&x.date);
let ky = y.timestamp.as_deref().unwrap_or(&y.date);
ky.cmp(kx)
});
if limit > 0 {
runs.truncate(limit);
}
if runs.is_empty() {
return CommandOutcome::fail(
"bench history-show",
format!("no bench runs recorded for `{repo}`"),
);
}
// The elaborate per-run summary the CLI used to print inline (now shared).
let mut human = format!("{repo} — {} run(s)", runs.len());
for r in &runs {
let date = if r.date.is_empty() { "-" } else { r.date.as_str() };
let machine = if r.machine.is_empty() { "-" } else { r.machine.as_str() };
human.push_str(&format!("\n\nv{} · {} · {} cores · {}", r.version, machine, r.cores, date));
let mut scalars: Vec<String> = Vec::new();
for res in &r.results {
for (k, v) in &res.metrics {
if let Some(f) = v.as_f64() {
scalars.push(format!("{}.{k}={f:.2}", res.name));
}
}
}
scalars.sort();
if scalars.is_empty() {
human.push_str("\n metrics: (none)");
} else {
human.push_str(&format!("\n metrics: {}", scalars.join(" ")));
}
if r.tests.is_empty() {
human.push_str("\n tests: (none recorded)");
} else {
let passed = r.tests.iter().filter(|t| t.passed).count();
let failed = r.tests.len() - passed;
human.push_str(&format!("\n tests: {passed} passed, {failed} failed"));
for t in r.tests.iter().filter(|t| !t.passed) {
let msg = t.message.as_deref().unwrap_or("");
human.push_str(&format!("\n ✗ {} {}", t.name, msg));
}
}
}
let data = serde_json::json!({ "repo": repo, "runs": runs });
CommandOutcome::ok("bench history-show", data, human)
}
#[cfg(test)]
mod history_outcome_tests {
use super::*;
fn sample_run(date: &str, machine: &str) -> BenchRun {
BenchRun {
id: crate::Uuid::nil(),
machine_facts: None,
date: date.to_string(),
timestamp: Some(format!("{date}T00:00:00Z")),
version: "0.1.0".into(),
machine: machine.into(),
cores: 8,
results: Vec::new(),
tests: Vec::new(),
}
}
#[test]
fn empty_history_is_red_not_silently_green() {
let o = history_outcome("holger", Vec::new(), 0);
assert_eq!(o.command, "bench history-show");
assert!(!o.is_sannr(), "empty bench history must be RED (RAGNARÖK)");
assert!(o.human.contains("no bench runs"));
}
#[test]
fn real_runs_are_sannr_newest_first_and_limited() {
let runs = vec![sample_run("2026-06-01", "ryzen"), sample_run("2026-06-03", "epyc")];
let o = history_outcome("holger", runs, 1);
assert!(o.is_sannr(), "a real run is a true (sannr) outcome");
let arr = o.data["runs"].as_array().unwrap();
assert_eq!(arr.len(), 1, "limit=1 truncates");
assert_eq!(arr[0]["machine"], serde_json::json!("epyc"), "newest (2026-06-03) first");
assert_eq!(o.data["repo"], serde_json::json!("holger"));
}
}
/// Identity and machine facts on the envelope — the R6 contract.
#[cfg(test)]
mod identity_tests {
use super::*;
/// HISTORY COMPAT, the one that must never break: every line already in a
/// `bench_history.jsonl` predates `id` and `machine_facts`. It must still
/// parse, and it must report `has_id() == false` so a consumer falls back
/// to the heuristic join instead of deduplicating on a nil id (which every
/// legacy line shares and which would collapse them all into one).
#[test]
fn a_legacy_line_without_an_id_still_parses_and_says_it_has_none() {
let legacy = r#"{"date":"2026-05-27","version":"0.6.4","machine":"ryzen-9-7900","cores":32,"results":[{"name":"rust_crate_st","ops_sec":420072}]}"#;
let run: BenchRun = serde_json::from_str(legacy).unwrap();
assert!(run.id.is_nil());
assert!(!run.has_id(), "a legacy line must NOT claim an identity");
assert!(run.machine_facts.is_none());
assert_eq!(run.results.len(), 1);
}
/// The id is v7 — time-ordered. Two ids minted in sequence must compare in
/// that order, which is the property that makes the id sort like the date.
#[test]
fn the_id_is_v7_and_sorts_in_mint_order() {
let a = BenchRun::new_id();
std::thread::sleep(std::time::Duration::from_millis(2));
let b = BenchRun::new_id();
assert_eq!(a.get_version_num(), 7, "must be v7, not v4");
assert_eq!(b.get_version_num(), 7);
assert!(a < b, "v7 ids must sort in mint order: {a} !< {b}");
}
/// A run made by the producer carries a real id and real machine facts, and
/// both survive the JSON hop the receipt takes (bench binary → stdout →
/// `bench_history.jsonl` → warehouse).
#[test]
fn a_produced_run_round_trips_its_id_and_machine_through_json() {
let run = BenchRun {
id: BenchRun::new_id(),
date: "2026-09-20".into(),
timestamp: Some("2026-09-20T10:00:00Z".into()),
version: "1.2.3".into(),
machine: "loki".into(),
cores: 12,
machine_facts: Some(machine::Machine::detect()),
results: Vec::new(),
tests: Vec::new(),
};
let s = serde_json::to_string(&run).unwrap();
let back: BenchRun = serde_json::from_str(&s).unwrap();
assert_eq!(back.id, run.id);
assert!(back.has_id());
assert_eq!(back.machine_facts, run.machine_facts);
assert!(s.contains("\"id\":"), "the id must be ON the receipt: {s}");
}
}