Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/monetize

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);
    }
}