crates/nornir-build-thing/src/kernel_trim/veto/tests.rs
//! One veto per run, each with a receipt: refused by name before anything runs
//! when the evidence is missing or says no, kept on a green rerun, reverted
//! with a refused receipt on a red one — over the static half's fixture tree, a
//! fixture composer and a canned suite.
use std::cell::{Cell, RefCell};
use std::path::{Path, PathBuf};
use super::*;
use crate::kernel_trim::coverage::{CoverageReport, KernelProbe, NotRun, StageResult, Status, SymbolCoverage, Totals};
use crate::kernel_trim::tests::{BUILT, FRAGMENT, FixtureComposer, reqs, s, tree};
use crate::kernel_trim::{KernelVerb, RowKernel, VerbContext, parse_kernel_verb, run_kernel_verb_in};
fn row() -> RowKernel {
let mut chosen = crate::kernel::KernelBuild { name: "k".into(), version: "6.12.104".into(), ..Default::default() };
chosen.profile = "vanilla".into();
chosen.preset = "full".into();
chosen.requirements = Some(reqs());
RowKernel { row: "node".into(), line: "--build kernel".into(), chosen, user_fragment: Some(FRAGMENT.into()) }
}
fn sym(name: &str, status: Status) -> SymbolCoverage {
SymbolCoverage {
symbol: name.into(),
status,
objects: 1,
instrumented: usize::from(status != Status::Unmeasured),
objects_run: usize::from(status == Status::Executed),
covered_by: if status == Status::Executed { vec!["cp:ready".into()] } else { vec![] },
bytes: 10,
probe_altered: false,
}
}
/// A coverage report on the fixture row's current fragment.
fn write_report(root: &Path, cgroup_bpf: Status, green: bool) {
write_report_with(root, cgroup_bpf, Status::Executed, green)
}
fn write_report_with(root: &Path, cgroup_bpf: Status, others: Status, green: bool) {
let r = row();
let frag = r.fragment("").unwrap();
let mut symbols = Vec::new();
for line in BUILT.lines() {
let k = line.split('=').next().unwrap();
let st = if k == "CONFIG_CGROUP_BPF" { cgroup_bpf } else { others };
symbols.push(sym(k, st));
}
let rep = CoverageReport {
schema: CoverageReport::SCHEMA.into(),
row: "node".into(),
kernel: "k".into(),
version: "6.12.104".into(),
fragment_sha256: nornir_hash::sha256_hex(frag.as_bytes()),
probe_fragment_sha256: String::new(),
probe: KernelProbe::Gcov,
measured_on: "2026-11-01T00:00:00Z".into(),
suite: "bridge".into(),
stages: vec![StageResult { name: "ready".into(), passed: green, detail: String::new() }],
not_run: vec![],
snapshots: vec!["cp:ready".into()],
tool: "test".into(),
totals: Totals::default(),
symbols,
never_run_objects: vec![],
};
crate::kernel_trim::coverage::store(root, &rep).unwrap();
}
/// A canned suite that counts its runs and records the veto list it saw.
struct CannedSuite {
green: bool,
config: Option<PathBuf>,
runs: Cell<usize>,
saw: RefCell<Vec<String>>,
store: PathBuf,
}
impl SuiteRunner for CannedSuite {
fn run(&self, req: &SuiteRequest) -> Result<SuiteRun> {
assert_eq!(req.probe, None, "a veto rerun is the row as it ships, not a probe kernel");
self.runs.set(self.runs.get() + 1);
self.saw.borrow_mut().push(std::fs::read_to_string(&self.store).unwrap_or_default());
Ok(SuiteRun {
stages: vec![
StageResult { name: "ready".into(), passed: true, detail: String::new() },
StageResult { name: "pod-to-pod".into(), passed: self.green, detail: String::new() },
],
not_run: vec![NotRun { name: "ha".into(), why: "not yet".into() }],
kernel_config: self.config.clone(),
..Default::default()
})
}
}
struct Fx {
_tree: tempfile::TempDir,
root: tempfile::TempDir,
work: tempfile::TempDir,
composer: FixtureComposer,
store: DirVetoStore,
}
fn fx() -> Fx {
let t = tree();
let root = tempfile::tempdir().unwrap();
let composer = FixtureComposer { tree: t.path().to_path_buf() };
let store = DirVetoStore { dir: root.path().join("kernel-vetoes") };
Fx { _tree: t, root, work: tempfile::tempdir().unwrap(), composer, store }
}
fn suite(f: &Fx, green: bool) -> CannedSuite {
CannedSuite { green, config: None, runs: Cell::new(0), saw: RefCell::new(vec![]), store: f.store.path("node") }
}
fn veto(f: &Fx, suite: &CannedSuite, symbol: &str) -> Result<(VetoReceipt, PathBuf)> {
let out = f.work.path().join("out");
let cx = VetoContext { repo_root: f.root.path(), store: &f.store, composer: &f.composer, suite, work: f.work.path(), out: &out, tools: vec!["test 0".into()] };
run_veto(&row(), symbol, "bridge", &cx)
}
#[test]
fn the_veto_list_round_trips_and_lands_as_n_requirements() {
let e = vec![VetoEntry { symbol: "CONFIG_ATA".into(), receipt: "2026-11-01T000000Z-CONFIG_ATA.json".into(), reason: "never executed".into() }];
let text = render_vetoes(&e);
assert!(text.starts_with("# Row kernel vetoes"));
assert_eq!(parse_vetoes(&text).unwrap(), e);
assert!(parse_vetoes("CONFIG_ATA a\nCONFIG_ATA b\n").unwrap_err().to_string().contains("twice"));
assert!(parse_vetoes("ATA r\n").is_err());
assert!(parse_vetoes("CONFIG_ATA\n").unwrap_err().to_string().contains("no receipt"));
let r = veto_requirements(&text).unwrap();
assert_eq!(r.len(), 1);
assert_eq!(r[0].symbol, "CONFIG_ATA");
assert_eq!(r[0].value, crate::kernel_requires::KconfigValue::N);
assert!(r[0].reason.contains("2026-11-01T000000Z-CONFIG_ATA.json"));
}
#[test]
fn the_suite_report_lines_parse_and_everything_else_is_log() {
let t = "booting…\nSTAGE ready PASS after 8.7 s\nSTAGE pod-to-pod FAIL no pong\nNOTRUN ha six VMs\nSNAPSHOT cp ready ffffffff81000000 /tmp/cp-ready.img\nVMLINUX /k/vmlinux\nKCONFIG /k/.config\nBZIMAGE /k/bzImage\n";
let r = parse_suite_output(t).unwrap();
assert_eq!(r.stages.len(), 2);
assert_eq!((r.stages[0].passed, r.stages[0].detail.as_str()), (true, "after 8.7 s"));
assert!(!r.green());
assert_eq!(r.not_run[0].why, "six VMs");
assert_eq!(r.snapshots[0].base, 0xffff_ffff_8100_0000);
assert_eq!(r.snapshots[0].path, PathBuf::from("/tmp/cp-ready.img"));
assert_eq!(r.vmlinux, Some(PathBuf::from("/k/vmlinux")));
assert!(parse_suite_output("STAGE ready MAYBE\n").is_err());
assert!(parse_suite_output("SNAPSHOT cp ready\n").is_err());
}
#[test]
fn a_green_rerun_keeps_the_one_veto_and_writes_an_applied_receipt() {
let f = fx();
write_report(f.root.path(), Status::NeverExecuted, true);
let su = suite(&f, true);
let (r, p) = veto(&f, &su, "CONFIG_CGROUP_BPF").unwrap();
assert_eq!(su.runs.get(), 1, "exactly one full rerun");
assert!(su.saw.borrow()[0].contains("CONFIG_CGROUP_BPF "), "the suite ran WITH the veto in the row's list");
assert_eq!(r.verdict, VetoVerdict::Applied);
assert_eq!((r.bz_image_before, r.bz_image_after, r.bz_image_delta), (1000, 960, -40));
assert_ne!(r.fragment_sha256_before, r.fragment_sha256_after);
assert!(r.inverse_holds);
assert_eq!(r.stages.len(), 2);
assert_eq!(r.not_run[0].name, "ha");
assert!(r.tools.iter().any(|t| t.starts_with("nornir-build-thing") || t == "test 0"));
assert!(p.starts_with(f.root.path().join(".nornir/kernel-vetoes/node/receipts")), "{}", p.display());
let list = parse_vetoes(&f.store.read("node").unwrap()).unwrap();
assert_eq!(list.len(), 1);
assert_eq!(list[0].symbol, "CONFIG_CGROUP_BPF");
assert_eq!(list[0].receipt, p.file_name().unwrap().to_string_lossy());
let back: VetoReceipt = serde_json::from_str(&std::fs::read_to_string(&p).unwrap()).unwrap();
assert_eq!(back, r);
// A second veto of the same symbol is refused.
assert!(veto(&f, &su, "CONFIG_CGROUP_BPF").unwrap_err().to_string().contains("already vetoed"));
}
#[test]
fn a_red_rerun_reverts_the_veto_and_writes_a_refused_receipt() {
let f = fx();
write_report(f.root.path(), Status::NeverExecuted, true);
f.store.write("node", "# kept\n").unwrap();
let su = suite(&f, false);
let (r, p) = veto(&f, &su, "CONFIG_CGROUP_BPF").unwrap();
assert_eq!(r.verdict, VetoVerdict::Refused);
assert!(r.reason.contains("RED") && r.reason.contains("pod-to-pod"), "{}", r.reason);
assert_eq!(f.store.read("node").unwrap(), "# kept\n", "the list is exactly what it was");
assert!(p.is_file());
let listed = list_receipts(f.root.path(), "node").unwrap();
assert_eq!(listed.len(), 1);
assert!(render_receipts("node", &listed, &[]).contains("CONFIG_CGROUP_BPF REFUSED"));
}
#[test]
fn a_medium_that_still_has_the_symbol_is_a_refused_veto() {
let f = fx();
write_report(f.root.path(), Status::NeverExecuted, true);
let cfg = f.work.path().join("medium.config");
std::fs::write(&cfg, BUILT).unwrap();
let mut su = suite(&f, true);
su.config = Some(cfg);
let (r, _) = veto(&f, &su, "CONFIG_CGROUP_BPF").unwrap();
assert_eq!(r.verdict, VetoVerdict::Refused);
assert!(r.reason.contains("did not reach the medium"), "{}", r.reason);
assert_eq!(f.store.read("node").unwrap(), "");
}
#[test]
fn a_veto_is_refused_by_name_before_anything_runs_when_the_evidence_says_no() {
let f = fx();
let su = suite(&f, true);
let e = veto(&f, &su, "CONFIG_CGROUP_BPF").unwrap_err().to_string();
assert!(e.contains("[veto-no-coverage]") && e.contains("kernel coverage node"), "{e}");
write_report(f.root.path(), Status::Executed, true);
let e = veto(&f, &su, "CONFIG_CGROUP_BPF").unwrap_err().to_string();
assert!(e.contains("[veto-executed]") && e.contains("cp:ready"), "{e}");
write_report(f.root.path(), Status::Unmeasured, true);
assert!(veto(&f, &su, "CONFIG_CGROUP_BPF").unwrap_err().to_string().contains("[veto-unmeasured]"));
write_report(f.root.path(), Status::NeverExecuted, false);
assert!(veto(&f, &su, "CONFIG_CGROUP_BPF").unwrap_err().to_string().contains("[veto-red-coverage]"));
write_report(f.root.path(), Status::NeverExecuted, true);
let pdir = veto_dir(f.root.path(), "node");
std::fs::create_dir_all(&pdir).unwrap();
std::fs::write(pdir.join("protect.list"), "CONFIG_CGROUP_* cgroup error paths\n").unwrap();
let e = veto(&f, &su, "CONFIG_CGROUP_BPF").unwrap_err().to_string();
assert!(e.contains("[veto-protected]") && e.contains("cgroup error paths"), "{e}");
std::fs::remove_file(pdir.join("protect.list")).unwrap();
// An executed symbol is refused on the evidence; a never-executed but
// REQUIRED one on the what-if.
assert!(veto(&f, &su, "CONFIG_IPV6").unwrap_err().to_string().contains("[veto-executed] CONFIG_IPV6"));
write_report_with(f.root.path(), Status::NeverExecuted, Status::NeverExecuted, true);
let e = veto(&f, &su, "CONFIG_IPV6").unwrap_err().to_string();
assert!(e.contains("[veto-what-if]") && e.contains("REFUSED CONFIG_IPV6"), "{e}");
assert_eq!(su.runs.get(), 0, "nothing was rerun");
assert_eq!(f.store.read("node").unwrap(), "", "nothing was written");
}
#[test]
fn the_verbs_parse_one_symbol_per_veto_and_list_the_receipts() {
let a = |v: &[&str]| parse_kernel_verb(&s(v));
let v = a(&["veto", "node", "CONFIG_ATA", "--run", "bridge"]).unwrap();
assert_eq!((v.symbol.as_deref(), v.suite.as_str()), (Some("CONFIG_ATA"), "bridge"));
let e = a(&["veto", "node", "CONFIG_ATA", "CONFIG_SCSI"]).unwrap_err().to_string();
assert!(e.contains("ONE symbol per run"), "{e}");
assert!(a(&["veto", "node"]).is_err());
assert!(a(&["veto", "node", "CONFIG_ATA", "--drop", "CONFIG_X"]).is_err());
let c = a(&["coverage", "node"]).unwrap();
assert_eq!((c.suite.as_str(), c.line.as_str()), ("bridge", "--build kernel"));
assert_eq!(a(&["coverage", "node", "--run", "bridge-csi"]).unwrap().suite, "bridge-csi");
assert!(a(&["vetoes", "node"]).is_ok());
let f = fx();
write_report(f.root.path(), Status::NeverExecuted, true);
let su = suite(&f, true);
let cx = VerbContext { repo_root: Some(f.root.path()), suite: Some(&su), store: Some(&f.store), tools: vec!["test 0".into()] };
let v: KernelVerb = a(&["veto", "node", "CONFIG_CGROUP_BPF"]).unwrap();
let out = run_kernel_verb_in(&v, &row(), &f.composer, f.work.path(), &cx).unwrap();
assert!(out.contains("CONFIG_CGROUP_BPF APPLIED") && out.contains("receipt: "), "{out}");
let out = run_kernel_verb_in(&a(&["vetoes", "node"]).unwrap(), &row(), &f.composer, f.work.path(), &cx).unwrap();
assert!(out.contains("1 in the list, 1 receipt(s)") && out.contains("vetoed CONFIG_CGROUP_BPF"), "{out}");
}
#[test]
fn candidates_carry_coverage_and_name_the_veto_ready_ones() {
let f = fx();
write_report(f.root.path(), Status::NeverExecuted, true);
let cx = VerbContext { repo_root: Some(f.root.path()), ..Default::default() };
let v = parse_kernel_verb(&s(&["candidates", "node"])).unwrap();
let out = run_kernel_verb_in(&v, &row(), &f.composer, f.work.path(), &cx).unwrap();
assert!(out.contains("# with coverage:"), "{out}");
let line = out.lines().find(|l| l.contains("CONFIG_CGROUP_BPF") && l.contains("never-executed")).unwrap_or_else(|| panic!("{out}"));
assert!(line.contains("veto-ready"), "{line}");
// Without a report: the door says how to get one.
let bare = tempfile::tempdir().unwrap();
let cx = VerbContext { repo_root: Some(bare.path()), ..Default::default() };
let out = run_kernel_verb_in(&v, &row(), &f.composer, f.work.path(), &cx).unwrap();
assert!(out.contains("none measured on this fragment"), "{out}");
}
// ── KTOOL fix 9: the list beside its receipts ───────────────────────────────
/// `row()` as a recipe declares it once `list` lands: each line an `=n`
/// requirement on the kernel edge.
fn row_with(list: &str) -> RowKernel {
let mut r = row();
let mut reqs = r.chosen.requirements.clone().unwrap_or_default();
for k in veto_requirements(list).unwrap() {
reqs.require("node", "kernel-vetoes", &k);
}
r.chosen.requirements = Some(reqs);
r
}
fn applied(f: &Fx) -> (String, VetoReceipt, PathBuf) {
write_report(f.root.path(), Status::NeverExecuted, true);
let su = suite(&f, true);
let (r, p) = veto(f, &su, "CONFIG_CGROUP_BPF").unwrap();
(f.store.read("node").unwrap(), r, p)
}
#[test]
fn an_applied_receipt_on_the_current_row_backs_its_line() {
let f = fx();
let (list, r, _) = applied(&f);
assert_eq!(r.verdict, VetoVerdict::Applied);
// As the recipe declares the row once the line lands, and as the verb saw it.
check_list(f.root.path(), &row_with(&list), &list).unwrap();
check_list(f.root.path(), &row(), &list).unwrap();
check_list(f.root.path(), &row(), "").unwrap();
}
#[test]
fn a_line_without_an_applied_receipt_on_the_current_row_is_refused_by_name() {
let f = fx();
let (list, _, p) = applied(&f);
let row = row_with(&list);
// No receipt file.
let bad = list.replace(p.file_name().unwrap().to_str().unwrap(), "2026-01-01T000000Z-CONFIG_CGROUP_BPF.json");
let e = check_list(f.root.path(), &row_with(&bad), &bad).unwrap_err().to_string();
assert!(e.contains("[veto-no-receipt]") && e.contains("CONFIG_CGROUP_BPF"), "{e}");
// A refused receipt.
let mut rec: VetoReceipt = serde_json::from_str(&std::fs::read_to_string(&p).unwrap()).unwrap();
let keep = rec.clone();
rec.verdict = VetoVerdict::Refused;
std::fs::write(&p, serde_json::to_string(&rec).unwrap()).unwrap();
let e = check_list(f.root.path(), &row, &list).unwrap_err().to_string();
assert!(e.contains("[veto-not-applied]"), "{e}");
// Another row's or symbol's receipt.
rec = keep.clone();
rec.symbol = "CONFIG_ATA".into();
std::fs::write(&p, serde_json::to_string(&rec).unwrap()).unwrap();
let e = check_list(f.root.path(), &row, &list).unwrap_err().to_string();
assert!(e.contains("[veto-receipt-mismatch]"), "{e}");
// The row moved under the receipt (another line in its fragment): stale.
std::fs::write(&p, serde_json::to_string(&keep).unwrap()).unwrap();
let mut moved = row.clone();
moved.user_fragment = Some(format!("{FRAGMENT}# CONFIG_ATA is not set\n"));
let e = check_list(f.root.path(), &moved, &list).unwrap_err().to_string();
assert!(e.contains("[veto-stale]") && e.contains("kernel veto node CONFIG_CGROUP_BPF"), "{e}");
check_list(f.root.path(), &row, &list).unwrap();
}
#[test]
fn a_kernel_verb_refuses_a_row_whose_list_has_an_unbacked_line() {
let f = fx();
let line = "CONFIG_CGROUP_BPF 2026-01-01T000000Z-CONFIG_CGROUP_BPF.json typed by hand\n";
f.store.write("node", line).unwrap();
let su = suite(&f, true);
let cx = VerbContext { repo_root: Some(f.root.path()), suite: Some(&su), store: Some(&f.store), tools: vec![] };
let v = parse_kernel_verb(&s(&["candidates", "node"])).unwrap();
let e = run_kernel_verb_in(&v, &row_with(line), &f.composer, f.work.path(), &cx).unwrap_err().to_string();
assert!(e.contains("[veto-no-receipt]"), "{e}");
// `vetoes` still lists it, and says it is unbacked.
let out = run_kernel_verb_in(&parse_kernel_verb(&s(&["vetoes", "node"])).unwrap(), &row(), &f.composer, f.work.path(), &cx).unwrap();
assert!(out.contains("UNBACKED"), "{out}");
}
#[test]
fn a_row_dir_store_keeps_the_list_beside_the_receipts() {
let d = tempfile::tempdir().unwrap();
let st = RowDirVetoStore { repo_root: d.path().to_path_buf() };
assert_eq!(st.path("holger-installer-metered"), veto_dir(d.path(), "holger-installer-metered").join(ROW_VETO_LIST));
assert_eq!(st.read("r").unwrap(), "");
st.write("r", "# x\n").unwrap();
assert_eq!(std::fs::read_to_string(veto_dir(d.path(), "r").join("vetoes.list")).unwrap(), "# x\n");
}