use predicates::prelude::*;
use std::path::Path;
use std::time::{Duration, Instant};
mod common;
fn wait_for_exit_file(home: &Path, deadline: Duration) -> Option<String> {
let start = Instant::now();
while start.elapsed() < deadline {
for box_dir in std::fs::read_dir(home.join("boxes")).into_iter().flatten() {
let Ok(box_dir) = box_dir else { continue };
let containers = box_dir.path().join("shared").join("containers");
for container in std::fs::read_dir(&containers).into_iter().flatten() {
let Ok(container) = container else { continue };
if let Ok(body) = std::fs::read_to_string(container.path().join("exit.json")) {
return Some(body);
}
}
}
std::thread::sleep(Duration::from_millis(250));
}
None
}
fn wait_for_stopped(ctx: &common::TestContext, name: &str, deadline: Duration) -> String {
let start = Instant::now();
let mut last = String::new();
while start.elapsed() < deadline {
let out = ctx
.new_cmd()
.args(["inspect", "-f", "{{.State.Status}}", name])
.output()
.expect("inspect runs");
last = String::from_utf8_lossy(&out.stdout).trim().to_string();
if !last.is_empty() && last != "running" {
return last;
}
std::thread::sleep(Duration::from_millis(500));
}
last
}
fn revive_if_zombie(ctx: &common::TestContext, name: &str, poll: Duration) -> bool {
let start = Instant::now();
while start.elapsed() < poll {
let out = ctx
.new_cmd()
.args(["inspect", "-f", "{{.State.Status}}", name])
.output()
.expect("inspect runs");
assert!(
out.status.success(),
"inspect must succeed while polling {name}; stderr: {}",
String::from_utf8_lossy(&out.stderr).trim()
);
let status = String::from_utf8_lossy(&out.stdout).trim().to_string();
if !status.is_empty() && status != "running" {
return false; }
std::thread::sleep(Duration::from_millis(200));
}
let _ = ctx
.new_cmd()
.args(["exec", name, "--", "echo", "revive"])
.output()
.expect("exec runs");
true
}
#[test]
fn test_init_semantics_command_is_pid1() {
let mut ctx = common::boxlite();
ctx.cmd.args([
"run",
"--rm",
"alpine:latest",
"sh",
"-c",
"tr '\\0' ' ' </proc/1/cmdline",
]);
ctx.cmd
.assert()
.success()
.stdout(predicate::str::contains("tr"));
}
#[test]
fn test_init_semantics_user_applies_to_init() {
let mut ctx = common::boxlite();
ctx.cmd
.args(["run", "--rm", "--user", "1000", "alpine:latest", "id", "-u"]);
ctx.cmd
.assert()
.success()
.stdout(predicate::str::contains("1000"));
}
#[test]
fn test_init_semantics_workdir_applies_to_init() {
let mut ctx = common::boxlite();
ctx.cmd
.args(["run", "--rm", "-w", "/tmp", "alpine:latest", "pwd"]);
ctx.cmd
.assert()
.success()
.stdout(predicate::str::contains("/tmp"));
}
#[test]
fn test_init_semantics_detached_exit_updates_status() {
let ctx = common::boxlite();
ctx.cleanup_box("init-sem-status");
let mut run = ctx.new_cmd();
run.args([
"run",
"-d",
"--name",
"init-sem-status",
"alpine:latest",
"sh",
"-c",
"exit 42",
]);
run.assert().success();
revive_if_zombie(&ctx, "init-sem-status", Duration::from_secs(8));
let status = wait_for_stopped(&ctx, "init-sem-status", Duration::from_secs(30));
assert!(
status.contains("stopped") || status.contains("exited"),
"box should stop after its command exits; last status: {status:?}"
);
ctx.new_cmd()
.args(["inspect", "-f", "{{.State.ExitCode}}", "init-sem-status"])
.assert()
.success()
.stdout(predicate::str::contains("42"));
ctx.cleanup_box("init-sem-status");
}
#[test]
fn test_init_semantics_create_stores_the_command_as_init() {
let ctx = common::boxlite();
ctx.cleanup_box("init-sem-create");
ctx.new_cmd()
.args([
"create",
"--name",
"init-sem-create",
"alpine:latest",
"sh",
"-c",
"exit 23",
])
.assert()
.success();
ctx.new_cmd()
.args(["inspect", "-f", "{{.State.Status}}", "init-sem-create"])
.assert()
.success()
.stdout(predicate::str::contains("configured"));
let cp = ctx
.new_cmd()
.args(["cp", "init-sem-create:/etc/hostname", "."])
.output()
.expect("cp runs");
assert!(
!cp.status.success(),
"cp must not boot a created job — booting it runs the command"
);
ctx.new_cmd()
.args(["start", "init-sem-create"])
.assert()
.success();
revive_if_zombie(&ctx, "init-sem-create", Duration::from_secs(8));
let stopped = wait_for_stopped(&ctx, "init-sem-create", Duration::from_secs(30));
assert!(
stopped.contains("stopped") || stopped.contains("exited"),
"the stored command must run and end the box, got {stopped:?}"
);
ctx.new_cmd()
.args(["inspect", "-f", "{{.State.ExitCode}}", "init-sem-create"])
.assert()
.success()
.stdout(predicate::str::contains("23"));
ctx.cleanup_box("init-sem-create");
}
#[test]
fn test_init_semantics_cp_from_a_finished_job_does_not_rerun_it() {
let ctx = common::boxlite();
ctx.cleanup_box("init-sem-cp");
ctx.new_cmd()
.args([
"run",
"-d",
"--name",
"init-sem-cp",
"--entrypoint",
"/bin/true",
"alpine:latest",
])
.assert()
.success();
revive_if_zombie(&ctx, "init-sem-cp", Duration::from_secs(8));
let stopped = wait_for_stopped(&ctx, "init-sem-cp", Duration::from_secs(30));
assert!(
stopped.contains("stopped") || stopped.contains("exited"),
"precondition: the job must finish, got {stopped:?}"
);
let cp = ctx
.new_cmd()
.args(["cp", "init-sem-cp:/etc/hostname", "."])
.output()
.expect("cp runs");
assert!(
!cp.status.success(),
"cp on a finished job must be refused, not silently satisfied by a reboot"
);
let after = ctx
.new_cmd()
.args(["inspect", "-f", "{{.State.Status}}", "init-sem-cp"])
.output()
.expect("inspect runs");
let status = String::from_utf8_lossy(&after.stdout).trim().to_string();
assert!(
!status.contains("running"),
"cp must not restart a finished job — that re-runs the user's command. Status: {status:?}"
);
ctx.cleanup_box("init-sem-cp");
}
#[test]
fn test_init_semantics_signal_death_recorded_as_128_plus_n() {
let ctx = common::boxlite();
ctx.cleanup_box("init-sem-signal");
ctx.new_cmd()
.args([
"run",
"-d",
"--name",
"init-sem-signal",
"alpine:latest",
"sleep",
"300",
])
.assert()
.success();
ctx.new_cmd()
.args(["stop", "init-sem-signal"])
.assert()
.success();
ctx.new_cmd()
.args(["inspect", "-f", "{{.State.ExitCode}}", "init-sem-signal"])
.assert()
.success()
.stdout(predicate::str::contains("137"));
let record = wait_for_exit_file(&ctx.home, Duration::from_secs(5))
.expect("the guest must record the main command's exit");
assert!(
record.contains("137"),
"SIGKILL must be recorded as 128+9=137 (docker's convention), got: {record}"
);
ctx.cleanup_box("init-sem-signal");
}
#[test]
fn test_init_semantics_exec_after_exit_refused_without_restarting() {
let ctx = common::boxlite();
ctx.cleanup_box("init-sem-exec");
ctx.new_cmd()
.args([
"run",
"-d",
"--name",
"init-sem-exec",
"alpine:latest",
"sh",
"-c",
"test -f /tmp/ran && sleep 300; touch /tmp/ran",
])
.assert()
.success();
revive_if_zombie(&ctx, "init-sem-exec", Duration::from_secs(8));
let stopped = wait_for_stopped(&ctx, "init-sem-exec", Duration::from_secs(30));
assert!(
stopped.contains("stopped") || stopped.contains("exited"),
"precondition: the box must stop when its main command exits, got {stopped:?}"
);
let assert = ctx
.new_cmd()
.args(["exec", "init-sem-exec", "--", "echo", "should-not-run"])
.assert()
.failure();
let stderr = String::from_utf8_lossy(&assert.get_output().stderr).to_string();
assert!(
!stderr.contains("procfs") && !stderr.contains("please report this"),
"exec refusal must be a clean status error, got: {stderr}"
);
let after = ctx
.new_cmd()
.args(["inspect", "-f", "{{.State.Status}}", "init-sem-exec"])
.output()
.expect("inspect runs");
let status = String::from_utf8_lossy(&after.stdout).trim().to_string();
assert!(
!status.contains("running"),
"exec must not restart a finished box — that re-runs the user's main command. Status: {status:?}"
);
ctx.cleanup_box("init-sem-exec");
}
#[test]
fn test_init_semantics_restart_reruns_init_and_rerecords_exit() {
let ctx = common::boxlite();
ctx.cleanup_box("init-sem-restart");
ctx.new_cmd()
.args([
"run",
"-d",
"--name",
"init-sem-restart",
"alpine:latest",
"sh",
"-c",
"test -e /ran && exit 8; touch /ran; exit 7",
])
.assert()
.success();
revive_if_zombie(&ctx, "init-sem-restart", Duration::from_secs(8));
let stopped = wait_for_stopped(&ctx, "init-sem-restart", Duration::from_secs(30));
assert!(
stopped.contains("stopped") || stopped.contains("exited"),
"first run must end the box, got {stopped:?}"
);
ctx.new_cmd()
.args(["inspect", "-f", "{{.State.ExitCode}}", "init-sem-restart"])
.assert()
.success()
.stdout(predicate::str::contains("7"));
ctx.new_cmd()
.args(["start", "init-sem-restart"])
.assert()
.success();
revive_if_zombie(&ctx, "init-sem-restart", Duration::from_secs(8));
let stopped = wait_for_stopped(&ctx, "init-sem-restart", Duration::from_secs(30));
assert!(
stopped.contains("stopped") || stopped.contains("exited"),
"restarted run must end the box again, got {stopped:?}"
);
ctx.new_cmd()
.args(["inspect", "-f", "{{.State.ExitCode}}", "init-sem-restart"])
.assert()
.success()
.stdout(predicate::str::contains("8"));
ctx.cleanup_box("init-sem-restart");
}
#[test]
fn test_init_semantics_failed_init_build_reports_and_recovers() {
let ctx = common::boxlite();
ctx.cleanup_boxes(&["init-sem-badwd", "init-sem-goodwd"]);
ctx.new_cmd()
.args([
"run",
"--workdir",
"/nonexistent/deep",
"--name",
"init-sem-badwd",
"alpine:latest",
"true",
])
.assert()
.failure()
.stderr(predicate::str::contains("Failed to create container"));
ctx.new_cmd()
.args(["rm", "-f", "init-sem-badwd"])
.assert()
.success();
ctx.new_cmd()
.args([
"run",
"--name",
"init-sem-goodwd",
"alpine:latest",
"sh",
"-c",
"exit 0",
])
.assert()
.success();
let stopped = wait_for_stopped(&ctx, "init-sem-goodwd", Duration::from_secs(30));
assert!(
stopped.contains("stopped") || stopped.contains("exited"),
"the recovery box must run and stop cleanly, got {stopped:?}"
);
ctx.cleanup_box("init-sem-goodwd");
}