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
352 lines
10 KiB
Rust
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();
|
|
}
|