Gunnar

mikronova/draupnir

tests/live_oci_exec_streaming.rs

//! **`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:?}");
}