//! **`exec_streaming` against a live podman socket** — the call a long-lived
//! toolchain container runs every tool through (nornir's Mímir road,
//! 2026-10-17: one container per run instead of one per tool).
//!
//! What must hold, each asserted on a value this test chose:
//! * the exec gets ITS env, user and working dir;
//! * the exit code is the process's (7 stays 7);
//! * stdout and stderr come back byte for byte — a 200 000-byte line whole
//! (the engine's LOG frames cut long lines; the attach stream must not);
//! * several execs run at once in one container (wall ≈ one, not the sum).
//!
//! `#[ignore]` by default: it needs `backend-oci`, a podman socket and an
//! image with `sh` (`DRAUPNIR_EXEC_IMAGE`, default the runa toolchain image):
//!
//! ```text
//! cargo test --features backend-oci --test live_oci_exec_streaming -- --ignored --nocapture
//! ```
#![cfg(feature = "backend-oci")]
use draupnir::container::{ContainerBoot, ContainerControl, ExecSpec, LogStream, Readiness};
use draupnir::{Boot, BootSpec, Machine};
fn image() -> String {
std::env::var("DRAUPNIR_EXEC_IMAGE").unwrap_or_else(|_| "localhost/nordisk-runa-toolchain:nightly-2026-10-05".to_string())
}
struct Stop<'a>(&'a ContainerBoot, Machine);
impl Drop for Stop<'_> {
fn drop(&mut self) {
self.0.stop(&self.1);
}
}
fn sh(script: &str) -> ExecSpec {
ExecSpec::new(vec!["sh".into(), "-c".into(), script.into()])
}
#[test]
#[ignore = "needs a live podman socket + DRAUPNIR_EXEC_IMAGE (an image with sh)"]
fn exec_streaming_is_exact_and_concurrent() {
let boot = ContainerBoot::new();
let name = format!("execstream-{}", std::process::id());
let spec = BootSpec::container(&name, image()).with_cmd(vec!["sleep".to_string(), "300".to_string()]).with_env("BOX_ENV", "from-the-box");
let machine = boot.boot(&spec).expect("boot the box");
let _stop = Stop(&boot, machine.clone());
boot.wait_ready(&machine, &Readiness::Running, std::time::Duration::from_secs(60)).expect("the box never ran");
// env (its own + the box's), user, working dir, exit code, stderr apart
let mut live = Vec::new();
let run = boot
.exec_streaming(
&machine,
&sh("echo \"$CALL_ENV $BOX_ENV\"; pwd; id -u; echo to-err >&2; exit 7").with_env("CALL_ENV", "from-the-call").with_user("0:0").with_working_dir("/tmp"),
&mut |st, b| live.push((st, b.to_vec())),
)
.expect("exec");
assert_eq!(run.exit_code, Some(7));
assert_eq!(String::from_utf8_lossy(&run.stdout), "from-the-call from-the-box\n/tmp\n0\n");
assert_eq!(run.stderr, b"to-err\n");
let streamed: Vec<u8> = live.iter().filter(|(s, _)| *s == LogStream::Stdout).flat_map(|(_, b)| b.clone()).collect();
assert_eq!(streamed, run.stdout, "the sink saw exactly what came back");
// one line of 200 000 bytes, whole; then a short line
let run = boot.exec_streaming(&machine, &sh("head -c 200000 /dev/zero | tr '\\0' a; echo; echo tail"), &mut |_, _| {}).expect("long exec");
assert_eq!(run.exit_code, Some(0));
assert_eq!(run.stdout.len(), 200_006, "the long line came back cut");
assert!(run.stdout[..200_000].iter().all(|&b| b == b'a') && run.stdout.ends_with(b"\ntail\n"));
// four execs at once: about one sleep of wall, not four
let t = std::time::Instant::now();
let outs: Vec<_> = std::thread::scope(|s| {
let hs: Vec<_> = (0..4).map(|i| {
let (boot, machine) = (&boot, &machine);
s.spawn(move || boot.exec_streaming(machine, &sh(&format!("sleep 2; echo {i}")), &mut |_, _| {}).unwrap())
}).collect();
hs.into_iter().map(|h| h.join().unwrap()).collect()
});
let wall = t.elapsed();
for (i, o) in outs.iter().enumerate() {
assert_eq!((o.exit_code, o.stdout.clone()), (Some(0), format!("{i}\n").into_bytes()));
}
assert!(wall < std::time::Duration::from_secs(6), "four 2 s execs took {wall:?}: they did not run at once");
eprintln!("exec_streaming: 4 concurrent 2 s execs in {wall:?}");
}