crates/monetize-fast/src/outer.rs
//! **The outer run's verdict**: the binary re-runs itself under `podman unshare
//! --rootless-netns`, and the exit code it gets back is podman's, not the run's.
//!
//! On oden (ODEN-CI round 2, 2026-10-25) both fast tests wrote a PASS receipt (holger 15 of
//! 15, ready reached) and the verb still exited 1, because podman's own teardown of the
//! rootless network namespace failed after the run: `rootless netns: cleanup: kill network
//! process: permission denied`. The run is judged by its receipt instead.
/// The warning a run that passed but whose network teardown failed carries, by name.
pub const TEARDOWN_WARNING: &str = "[fast-teardown]";
/// What the outer run decides.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Outer {
/// podman exited 0: the run's own exit code came through.
Green,
/// The receipt says PASS, none of the run's containers is left, and the only error podman
/// printed is the teardown of its rootless network namespace. Green, with these words.
GreenTeardown(String),
/// Red, with the reason.
Red(String),
}
/// Pure: the verdict from podman's exit, this run's fresh receipt (`None`: none was written),
/// podman's stderr, and how many of the run's containers are left (`None`: they could not be
/// listed).
pub fn judge(
podman_ok: bool,
receipt_pass: Option<bool>,
stderr: &str,
ours_left: Option<usize>,
) -> Outer {
if podman_ok {
return Outer::Green;
}
match receipt_pass {
Some(true) => {}
Some(false) => return Outer::Red("the run's receipt says FAIL".into()),
None => {
return Outer::Red(
"podman unshare --rootless-netns exited non-zero and this run wrote no receipt"
.into(),
)
}
}
let errors: Vec<&str> = stderr
.lines()
.map(str::trim)
.filter(|l| l.starts_with("Error:"))
.collect();
if errors.is_empty() {
return Outer::Red("the receipt says PASS but podman unshare --rootless-netns exited non-zero without naming its netns teardown".into());
}
if let Some(other) = errors.iter().find(|l| !is_netns_teardown(l)) {
return Outer::Red(format!("the receipt says PASS but podman also failed on something that is not its netns teardown: {other}"));
}
match ours_left {
Some(0) => {}
Some(n) => {
return Outer::Red(format!(
"{n} container(s) of the run are left after the teardown: a leak is a failure"
))
}
None => {
return Outer::Red("the run's containers could not be listed after the teardown".into())
}
}
let said: Vec<&str> = stderr
.lines()
.map(str::trim)
.filter(|l| is_netns_teardown(l))
.collect();
Outer::GreenTeardown(format!(
"{TEARDOWN_WARNING} the run passed and left no container; only podman's teardown of its rootless network namespace failed: {}",
said.join(" / ")
))
}
/// A line of podman's rootless-netns teardown error.
fn is_netns_teardown(line: &str) -> bool {
line.contains("rootless netns")
}
/// Append the warning to the receipt this run wrote in `out` (`receipt.txt` gets a line,
/// `receipt.json` a `warnings` entry), so the green carries its words.
pub fn record_warning(out: &std::path::Path, words: &str) -> std::io::Result<()> {
use std::io::Write as _;
let mut t = std::fs::OpenOptions::new()
.append(true)
.open(out.join("receipt.txt"))?;
writeln!(t, "WARN {words}")?;
let jp = out.join("receipt.json");
let mut j: serde_json::Value =
serde_json::from_slice(&std::fs::read(&jp)?).map_err(std::io::Error::other)?;
match j.get_mut("warnings").and_then(|w| w.as_array_mut()) {
Some(w) => w.push(serde_json::Value::String(words.to_string())),
None => j["warnings"] = serde_json::json!([words]),
}
std::fs::write(&jp, serde_json::to_vec_pretty(&j)?)
}
/// `pass` of the `receipt.json` in `out`, if it was written at or after `since`.
pub fn fresh_receipt_pass(out: &std::path::Path, since: std::time::SystemTime) -> Option<bool> {
let jp = out.join("receipt.json");
let written = std::fs::metadata(&jp).ok()?.modified().ok()?;
if written < since {
return None;
}
let j: serde_json::Value = serde_json::from_slice(&std::fs::read(&jp).ok()?).ok()?;
j["pass"].as_bool()
}
#[cfg(test)]
mod tests {
use super::*;
const NETNS: &str = "fast: PASS, receipt at target/fast/receipt.txt\n\
Error: rootless netns: cleanup: 1 error occurred:\n\
\t* rootless netns: kill network process: permission denied\n";
/// ★ A PASS receipt whose only failure is podman's network teardown is GREEN, with the
/// named warning.
#[test]
fn a_pass_receipt_with_only_the_netns_teardown_failing_is_green_with_a_named_warning() {
match judge(false, Some(true), NETNS, Some(0)) {
Outer::GreenTeardown(w) => {
assert!(w.starts_with(TEARDOWN_WARNING), "{w}");
assert!(
w.contains("kill network process: permission denied"),
"the warning names podman's words: {w}"
);
}
other => {
panic!("a PASS run with only the netns teardown failing must be green: {other:?}")
}
}
}
/// ★ A leak stays red: one of OUR containers left after the teardown is a real failure.
#[test]
fn a_container_of_the_run_left_after_the_teardown_stays_red() {
let Outer::Red(why) = judge(false, Some(true), NETNS, Some(1)) else {
panic!("a leak must be red")
};
assert!(why.contains("1 container"), "{why}");
assert!(
matches!(judge(false, Some(true), NETNS, None), Outer::Red(_)),
"a list that could not be read is not 0"
);
}
/// The warning is recorded in both receipts, and only a receipt written by THIS run counts.
#[test]
fn the_warning_is_recorded_in_the_receipt_and_a_stale_receipt_is_not_read() {
let dir = std::env::temp_dir().join(format!("monetize-fast-outer-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let before = std::time::SystemTime::now() - std::time::Duration::from_secs(5);
let r = crate::Receipt::default();
r.start();
r.step("one", Ok("fine".into()));
r.write(&dir, "holger", &Default::default()).unwrap();
assert_eq!(fresh_receipt_pass(&dir, before), Some(true));
let later = std::time::SystemTime::now() + std::time::Duration::from_secs(60);
assert_eq!(
fresh_receipt_pass(&dir, later),
None,
"a receipt older than the run is not this run's"
);
record_warning(&dir, "[fast-teardown] words").unwrap();
let t = std::fs::read_to_string(dir.join("receipt.txt")).unwrap();
assert!(t.ends_with("WARN [fast-teardown] words\n"), "{t}");
let j: serde_json::Value =
serde_json::from_slice(&std::fs::read(dir.join("receipt.json")).unwrap()).unwrap();
assert_eq!(j["warnings"], serde_json::json!(["[fast-teardown] words"]));
assert_eq!(
j["pass"],
serde_json::json!(true),
"the verdict in the receipt is unchanged"
);
let _ = std::fs::remove_dir_all(dir);
}
/// Everything else that is not exactly that stays red.
#[test]
fn a_fail_receipt_a_missing_receipt_or_another_error_stays_red() {
assert!(
matches!(judge(false, Some(false), NETNS, Some(0)), Outer::Red(_)),
"a FAIL receipt"
);
assert!(
matches!(judge(false, None, NETNS, Some(0)), Outer::Red(_)),
"no fresh receipt"
);
let other = format!("{NETNS}Error: crun: something else broke\n");
assert!(
matches!(judge(false, Some(true), &other, Some(0)), Outer::Red(_)),
"another podman error"
);
assert!(
matches!(judge(false, Some(true), "", Some(0)), Outer::Red(_)),
"a non-zero exit with no teardown error"
);
assert_eq!(judge(true, Some(true), "", Some(0)), Outer::Green);
}
}