Files
TheRock/rocm-systems/emulation/mirage/tests/e2e.rs
T
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

352 lines
10 KiB
Rust

//! End-to-end tests for the `mirage` CLI.
//!
//! Each test:
//!
//! * Creates a fresh tempdir and points `XDG_CONFIG_HOME`,
//! `XDG_RUNTIME_DIR`, and `XDG_STATE_HOME` at it.
//! * Drives the unified `mirage` binary as a subprocess via
//! `assert_cmd`. The CLI re-execs itself with the `host` subcommand
//! to spawn per-session hosts - no separate host binary needed.
use std::path::PathBuf;
use std::process::Command;
use assert_cmd::prelude::*;
use predicates::str;
use tempfile::TempDir;
struct Env {
_dir: TempDir,
config: PathBuf,
runtime: PathBuf,
mirage_bin: PathBuf,
}
impl Env {
fn new() -> Self {
let dir = tempfile::tempdir().unwrap();
let config = dir.path().join("config");
let runtime = dir.path().join("runtime");
let mirage_bin = PathBuf::from(env!("CARGO_BIN_EXE_mirage"));
Self {
_dir: dir,
config,
runtime,
mirage_bin,
}
}
fn mirage(&self) -> Command {
let mut c = Command::new(&self.mirage_bin);
c.env("XDG_CONFIG_HOME", &self.config)
.env("XDG_RUNTIME_DIR", &self.runtime)
.env("XDG_STATE_HOME", self._dir.path().join("state"))
// ensure the CLI re-execs the same binary when spawning
// hosts (covers the case where it lives in a non-standard
// location during testing).
.env("MIRAGE_BIN", &self.mirage_bin)
// make sure user env doesn't leak through.
.env_remove("MIRAGE_LOG");
c
}
}
#[test]
fn paths_prints_overridden_dirs() {
let env = Env::new();
env.mirage()
.arg("paths")
.assert()
.success()
.stdout(str::contains(env.config.display().to_string()))
.stdout(str::contains(env.runtime.display().to_string()));
}
#[test]
fn profile_create_list_show_delete() {
let env = Env::new();
env.mirage()
.args(["profile", "create", "p1", "--description", "the test"])
.assert()
.success();
env.mirage()
.args(["profile", "list"])
.assert()
.success()
.stdout(str::contains("p1"));
env.mirage()
.args(["profile", "list", "-l"])
.assert()
.success()
.stdout(str::contains("the test"));
env.mirage()
.args(["profile", "show", "p1"])
.assert()
.success()
.stdout(str::contains("\"name\": \"p1\""));
env.mirage()
.args(["profile", "delete", "p1", "-f"])
.assert()
.success();
env.mirage()
.args(["profile", "show", "p1"])
.assert()
.failure();
}
#[test]
fn run_command_streams_stdout_stderr_and_exit_code() {
let env = Env::new();
env.mirage()
.args(["profile", "create", "p"])
.assert()
.success();
let out = env
.mirage()
.args([
"run",
"--profile",
"p",
"--",
"/bin/sh",
"-c",
"echo hello; echo oops 1>&2; exit 7",
])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
// The exec runs under a PTY, so stdout and stderr are merged onto the
// terminal (the stdout stream), exactly as in a real terminal.
let combined = format!("{stdout}{stderr}");
assert!(combined.contains("hello"), "output was: {combined:?}");
assert!(combined.contains("oops"), "output was: {combined:?}");
assert_eq!(out.status.code(), Some(7));
}
#[test]
fn run_cleans_up_transient_session() {
let env = Env::new();
env.mirage()
.args(["profile", "create", "p"])
.assert()
.success();
env.mirage()
.args(["run", "--profile", "p", "--", "/bin/true"])
.assert()
.success();
let out = env.mirage().args(["session", "list"]).output().unwrap();
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("(no sessions)"),
"expected no sessions, got stderr={stderr:?} stdout={:?}",
String::from_utf8_lossy(&out.stdout)
);
}
#[test]
fn session_start_detach_exec_then_stop() {
let env = Env::new();
env.mirage()
.args(["profile", "create", "p"])
.assert()
.success();
env.mirage()
.args(["session", "start", "--profile", "p", "--id", "s1"])
.assert()
.success();
// submit an exec without attaching
env.mirage()
.args([
"exec",
"start",
"s1",
"--keep",
"--detach",
"--",
"/bin/echo",
"hi",
])
.assert()
.success()
.stdout(str::contains("e-000000"));
// wait for it to finish
for _ in 0..100 {
let out = env
.mirage()
.args(["exec", "show", "s1", "e-000000"])
.output()
.unwrap();
if String::from_utf8_lossy(&out.stdout).contains("\"ended\": true") {
break;
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
// logs (non-follow) should contain "hi"
let out = env
.mirage()
.args(["logs", "s1", "e-000000"])
.output()
.unwrap();
assert!(
String::from_utf8_lossy(&out.stdout).contains("hi"),
"stdout={:?}",
String::from_utf8_lossy(&out.stdout)
);
// stop the session
env.mirage()
.args(["session", "stop", "s1"])
.assert()
.success();
// verify the session dir is gone
assert!(!env.runtime.join("mirage/session/s1").exists());
}
#[test]
fn stopped_session_stays_gone_past_heartbeat() {
// Regression: `session stop` reported success but the orphaned host
// process kept re-stamping its heartbeat, which recreated the session
// directory (its parent is created on demand) and made the "stopped"
// session silently reappear in `session list`. The host must detect
// that its session directory was removed and exit instead.
let env = Env::new();
env.mirage()
.args(["profile", "create", "p"])
.assert()
.success();
env.mirage()
.args(["session", "start", "--profile", "p", "--id", "s9"])
.assert()
.success();
env.mirage()
.args(["session", "stop", "s9"])
.assert()
.success();
// Wait well past the host's heartbeat interval (2s): a live orphan
// would recreate the directory within one beat.
std::thread::sleep(std::time::Duration::from_millis(3000));
assert!(
!env.runtime.join("mirage/session/s9").exists(),
"session directory reappeared after stop"
);
let out = env.mirage().args(["session", "list"]).output().unwrap();
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("(no sessions)"),
"stopped session reappeared in list: stderr={stderr:?} stdout={:?}",
String::from_utf8_lossy(&out.stdout)
);
}
#[test]
fn attach_to_long_running_exec_then_signal() {
let env = Env::new();
env.mirage()
.args(["profile", "create", "p"])
.assert()
.success();
env.mirage()
.args(["session", "start", "--profile", "p", "--id", "s2"])
.assert()
.success();
env.mirage()
.args([
"exec", "start", "s2", "--keep", "--detach", "--", "/bin/sh", "-c", "sleep 30",
])
.assert()
.success();
// wait for it to start
for _ in 0..100 {
let out = env
.mirage()
.args(["exec", "show", "s2", "e-000000"])
.output()
.unwrap();
if String::from_utf8_lossy(&out.stdout).contains("\"started\": true") {
break;
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
// signal it
env.mirage()
.args(["exec", "signal", "s2", "e-000000", "KILL"])
.assert()
.success();
// it should end soon
let mut ended = false;
for _ in 0..100 {
let out = env
.mirage()
.args(["exec", "show", "s2", "e-000000"])
.output()
.unwrap();
if String::from_utf8_lossy(&out.stdout).contains("\"ended\": true") {
ended = true;
break;
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
assert!(ended, "exec did not end after SIGKILL");
env.mirage()
.args(["session", "stop", "s2"])
.assert()
.success();
}
#[test]
fn json_output_is_parseable() {
let env = Env::new();
env.mirage()
.args(["profile", "create", "p"])
.assert()
.success();
let out = env
.mirage()
.args(["--json", "profile", "list"])
.output()
.unwrap();
let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
// The CLI auto-seeds builtin profiles on first run, so the list also
// contains those; just assert it parses as an array that includes the
// profile we created.
let names = v.as_array().expect("profile list should be a JSON array");
assert!(
names.contains(&serde_json::json!("p")),
"expected created profile in list, got {v}"
);
}
#[test]
fn invalid_session_id_is_rejected() {
let env = Env::new();
env.mirage()
.args(["profile", "create", "p"])
.assert()
.success();
env.mirage()
.args(["session", "start", "--profile", "p", "--id", "../oops"])
.assert()
.failure();
}
#[test]
fn duplicate_session_id_fails() {
let env = Env::new();
env.mirage()
.args(["profile", "create", "p"])
.assert()
.success();
env.mirage()
.args(["session", "start", "--profile", "p", "--id", "dup"])
.assert()
.success();
env.mirage()
.args(["session", "start", "--profile", "p", "--id", "dup"])
.assert()
.failure();
env.mirage()
.args(["session", "stop", "dup"])
.assert()
.success();
}