Expand description
The recorder: turning a run that happened into evidence that lasts.
The runs already write their results down. test/run.sh writes a TSV row per case when
MJ_TEST_REPORT is set — name, result, seconds, phase — and CI already sets it.
cargo test prints a Running <binary> line and a test result: line per test binary.
Neither is durable, neither carries a commit, and neither is joined to anything.
This module reads what those runs wrote, carries into each result what the run measured about its own checkout (which commit, which tree) with the digest, the time and the origin, and merges it into the ledger.
§It records, it does not decide
A case that failed is a case the runner said failed. Nothing here re-judges a result,
and nothing here can produce one: a test id that appears in no report is simply not
recorded, and the report will say not run for it, which is true.
§A crate binary states only what ran
“Records what the runner said” needs care with cargo test, whose result line starts
with ok for a binary that ran nothing at all: every test ignored, or every test
filtered out of a run that named one. So read_crate_output reads the counts, not
only the word. A binary that ran no test, or only a filtered subset, is a skip; one with
a failed test is a failure whatever its word says; one whose result line carries no
count, or that printed no result line at all, is an error, because the harness could not
say what ran. Colour codes are stripped before any line is matched, so a coloured run and
a plain one read the same. What the output held that no claim can name yet, the crate’s
own unit-test binary and its doctests, is listed in RecordOutcome::dropped rather
than ignored.
§The run measures; the recorder carries
The recorder does not measure its own checkout. A run measures the checkout it left with
majordomus evidence stamp (super::provenance::stamp): the commit, and the tree with
the evidence ledger ignored and the run’s own untracked outputs excluded. The recorder
carries that measurement into every execution of the report it names, and refuses one of
another commit or of other bytes. A report recorded without a measurement carries the
tree unknown: the recorder does not vouch for a tree it did not see, and such a row
never reads proven. When the measured tree was clean, the digest is taken from the
commit, which holds the bytes the run executed.
A stamp measures the checkout when it is taken, after the report was written, and not the tree while the run executed: a run on a dirty tree that is cleaned before the stamp reads clean.
§A run, recorded
A repository with one case, a report the runner wrote, and the execution that comes out of joining the two:
use majordomus_cli::evidence::{
record, stamp, EvidenceProducer, Ledger, Origin, Outcome, RecordRequest, StampRequest,
};
use std::process::Command;
let root = tempfile::tempdir().unwrap();
let git = |args: &[&str]| {
Command::new("git").arg("-C").arg(root.path()).args(args).output().unwrap()
};
git(&["init", "-q"]);
git(&["config", "user.email", "t@example.com"]);
git(&["config", "user.name", "t"]);
std::fs::create_dir_all(root.path().join("test/cases")).unwrap();
std::fs::write(root.path().join("test/cases/07_scope.sh"), "echo scope\n").unwrap();
git(&["add", "-A"]);
git(&["commit", "-qm", "init"]);
// what `test/run.sh` already writes when `MJ_TEST_REPORT` is set; kept outside the
// work tree, so the recorded tree state describes the run and not the report
let reports = tempfile::tempdir().unwrap();
let tsv = reports.path().join("run.tsv");
std::fs::write(&tsv, "07_scope\tok\t12\tparallel\n").unwrap();
// the run measures its own checkout, naming its report
let stamp_req = StampRequest {
producer: Some(EvidenceProducer::Suite),
report: Some(tsv.clone()),
..StampRequest::default()
};
let measured = stamp(root.path(), &stamp_req).unwrap();
let mut request = RecordRequest { suite: Some(tsv), ..RecordRequest::new(Origin::Ci) };
request.provenance.insert(EvidenceProducer::Suite, measured);
let outcome = record(root.path(), &request).unwrap();
assert_eq!(outcome.recorded, 1);
assert_eq!(outcome.passed, 1);
assert_eq!(outcome.commit.len(), 40);
assert_eq!(outcome.working_tree, "clean", "the tree the run's own measurement stated");
// and it is in the ledger, stamped, with the command that produces it again
let ledger = Ledger::load(root.path()).unwrap();
let execution = ledger.latest("suite:07_scope").unwrap();
assert_eq!(execution.outcome, Outcome::Pass);
assert_eq!(execution.seconds, 12);
assert_eq!(execution.origin, Origin::Ci);
assert_eq!(execution.command, "bash test/run.sh 07_scope");
assert_eq!(execution.digest_matches(root.path()), Some(true));§Example
The recorder reads what a run already wrote and stamps it with the provenance the run did not carry. Parsing is separable from recording, which is what makes it testable.
use majordomus_cli::evidence::{parse_crate_binaries, Outcome};
let got = parse_crate_binaries(" Running tests/product.rs (target/debug/deps/product-1)\ntest result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.10s\n");
assert_eq!(got.len(), 1);
assert_eq!(got[0].1, Outcome::Pass);Structs§
- Crate
Binary - One integration test binary of a
cargo testrun, as its result line stated it. - Crate
Read - What
cargo test’s output held: the integration binaries a claim can name, and what it cannot, listed inCrateRead::droppedin the order the output held it. - Record
Outcome - What a recording did: how much of the run reached the ledger, how much of it passed, the provenance every execution was stamped with, and what the run named that this repository does not have.
- Record
Request - What to record, and where from.
Functions§
- parse_
crate_ binaries - One
Running tests/<name>.rs/test result:pair per test binary, joined: the binaries ofread_crate_outputas(name, outcome, seconds), without what it listed. - read_
crate_ output - Read
cargo test’s output: eachRunning tests/<name>.rsline joined to thetest result:line that follows it, judged by its counts, and everything else it ran listed rather than ignored. - record
- Read the runs a request names, stamp each result with provenance, and merge into the
ledger of
root.