//! **`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))
}