Gunnar

mikronova/draupnir

tests/live_oci_list_and_env.rs

//! **`list_containers` and `container_env` against a live rootless podman
//! socket** — the two calls whose absence made consumers shell out.
//!
//! Both went in on 2026-08-11 because gunnar's benchmark suite carried, in the
//! middle of an otherwise zero-shell container path:
//!
//! ```text
//! // draupnir has no list-containers call, so this is `podman` directly
//! Command::new("podman").args(["ps", "-a", "--filter", "name=…"])
//! Command::new("podman").args(["inspect", id, "--format", "{{range .Config.Env}}…"])
//! ```
//!
//! A boot path with no shell fallback buys little if the janitor and the
//! read-back beside it shell out: that is where `podman`-not-on-PATH and
//! wrong-engine failures come back in, and they come back **silently**, because
//! a failed `podman ps` and a genuinely empty box produce the same empty stdout.
//!
//! `#[ignore]` by default, like this crate's other live proofs: it needs
//! `backend-oci`, a live rootless podman socket and a local image. Run it on the
//! box with podman:
//!
//! ```text
//! cargo test --features backend-oci --test live_oci_list_and_env -- --ignored --nocapture
//! ```

#![cfg(feature = "backend-oci")]

use draupnir::container::{ContainerBoot, ContainerControl, ContainerState, Readiness};
use draupnir::{Boot, BootSpec, NetMode};

/// An image that is certain to be on this box and needs no network to start.
const IMAGE: &str = "docker.io/library/ubuntu:26.04";

/// **What was passed comes back, and a name that was not booted does not.**
///
/// The two halves are one test on purpose: they need one container, and the
/// interesting failure of each is the other's success shape. A `container_env`
/// that answered an empty vector on every input would satisfy "no crash" and
/// prove nothing, so the assertion is on a **specific variable with a specific
/// value** that this test chose; a `list_containers` that answered everything
/// would satisfy "found it" and prove nothing, so a second, unbooted prefix must
/// come back empty.
#[test]
#[ignore = "needs a live rootless podman socket + a local ubuntu:26.04 image"]
fn list_and_env_read_back_what_was_booted() {
    let boot = ContainerBoot::new();
    let name = format!("listenv-{}", std::process::id());

    let spec = BootSpec::container(&name, IMAGE)
        .with_net(NetMode::Default)
        .with_env("DRAUPNIR_PROOF", "a-value-no-image-sets")
        .with_cmd(vec![
            "/bin/sh".to_string(),
            "-c".to_string(),
            "sleep 120".to_string(),
        ]);

    let machine = boot.boot(&spec).expect("booting the probe container");
    let cleanup = scopeguard(|| boot.stop(&machine));

    boot.wait_ready(
        &machine,
        &Readiness::Running,
        std::time::Duration::from_secs(60),
    )
    .expect("the probe container never reached Running");

    // ── the environment, read back out of the daemon's own record ───────────
    let env = boot
        .container_env(&machine)
        .expect("container_env must answer for a running container");
    assert!(
        env.iter()
            .any(|v| v == "DRAUPNIR_PROOF=a-value-no-image-sets"),
        "the daemon does not report the variable this test set. Read back {} variable(s): {env:?}",
        env.len()
    );

    // ── the listing ─────────────────────────────────────────────────────────
    //
    // The prefix is the DAEMON-side name, which is `draupnir-<spec name>` —
    // `Machine::container_name` adds that prefix and `machine.id` is the result.
    // Passing the spec name here matches nothing, which is how this test found
    // the trap; the doc on `list_containers` now says so.
    let found = boot
        .list_containers(&machine.id)
        .expect("list_containers must answer");
    assert!(
        found.iter().any(|(n, _)| n == &machine.id),
        "the container this test booted is not in the listing: {found:?}"
    );
    assert!(
        found
            .iter()
            .all(|(_, s)| matches!(s, ContainerState::Running)),
        "a container this test booted and waited for is not reported Running: {found:?}"
    );

    // The negative half. Without it, a `list_containers` that ignored its
    // argument and returned every container on the box would pass everything
    // above.
    let absent = boot
        .list_containers("draupnir-listenv-nothing-booted-under-this-")
        .expect("list_containers must answer for an unmatched prefix");
    assert!(
        absent.is_empty(),
        "a prefix nothing was booted under matched {absent:?}, so the filter is not filtering"
    );

    drop(cleanup);
}

/// A minimal drop-guard, so a panicking assertion above still removes the
/// container. Pulling in `scopeguard` for six lines would be a dependency this
/// crate's budget does not need.
fn scopeguard<F: FnOnce()>(f: F) -> impl Drop {
    struct G<F: FnOnce()>(Option<F>);
    impl<F: FnOnce()> Drop for G<F> {
        fn drop(&mut self) {
            if let Some(f) = self.0.take() {
                f();
            }
        }
    }
    G(Some(f))
}