use std::path::PathBuf;
use std::process::{Child, Command, ExitStatus};
use std::time::{Duration, Instant};
fn example_bin() -> PathBuf {
let mut cmd = Command::new("cargo");
cmd.args([
"build",
"--example",
"auto_exit_demo",
"--quiet",
"--features",
"auto-signal,periodic",
]);
if !cfg!(debug_assertions) {
cmd.arg("--release");
}
let status = cmd
.status()
.expect("failed to build auto_exit_demo example");
assert!(
status.success(),
"cargo build --example auto_exit_demo failed"
);
let profile = if cfg!(debug_assertions) {
"debug"
} else {
"release"
};
PathBuf::from(format!("target/{profile}/examples/auto_exit_demo"))
}
fn spawn_demo(mode: &str) -> (Child, PathBuf) {
let bin = example_bin();
let out_dir =
std::env::temp_dir().join(format!("memscope_e2e_{}_{}", std::process::id(), mode));
let _ = std::fs::remove_dir_all(&out_dir);
let child = Command::new(&bin)
.arg(mode)
.env("MEMSCOPE_E2E_OUTPUT", &out_dir)
.spawn()
.unwrap_or_else(|e| panic!("failed to spawn auto_exit_demo {mode}: {e}"));
(child, out_dir)
}
fn wait_with_timeout(child: &mut Child, timeout: Duration) -> ExitStatus {
let deadline = Instant::now() + timeout;
loop {
match child.try_wait() {
Ok(Some(status)) => return status,
Ok(None) => {
if Instant::now() >= deadline {
let _ = child.kill();
let _ = child.wait();
panic!("child process did not exit within {timeout:?}");
}
std::thread::sleep(Duration::from_millis(50));
}
Err(e) => panic!("error waiting for child: {e}"),
}
}
}
fn assert_report_exists(dir: &std::path::Path) {
let html = dir.join("dashboard_unified_dashboard.html");
let json = dir.join("memory_analysis.json");
assert!(html.exists(), "expected HTML report at {}", html.display());
assert!(json.exists(), "expected JSON report at {}", json.display());
assert!(
std::fs::metadata(&html).unwrap().len() > 100,
"HTML report is suspiciously small"
);
assert!(
std::fs::metadata(&json).unwrap().len() > 50,
"JSON report is suspiciously small"
);
}
#[test]
fn e2e_normal_exit() {
let (mut child, out_dir) = spawn_demo("normal");
let status = wait_with_timeout(&mut child, Duration::from_secs(30));
assert!(status.success(), "normal exit should succeed");
assert_report_exists(&out_dir);
let _ = std::fs::remove_dir_all(&out_dir);
}
#[test]
fn e2e_panic_exit() {
let (mut child, out_dir) = spawn_demo("panic");
let status = wait_with_timeout(&mut child, Duration::from_secs(30));
assert!(!status.success(), "panic exit should report failure");
assert_report_exists(&out_dir);
let _ = std::fs::remove_dir_all(&out_dir);
}
#[test]
#[cfg(unix)]
fn e2e_ctrlc_exit() {
use std::os::unix::process::ExitStatusExt;
let (mut child, out_dir) = spawn_demo("sleep");
std::thread::sleep(Duration::from_secs(2));
let pid = child.id() as libc::pid_t;
let ret = unsafe { libc::kill(pid, libc::SIGINT) };
assert_eq!(ret, 0, "libc::kill failed to send SIGINT to pid {pid}");
let status = wait_with_timeout(&mut child, Duration::from_secs(10));
assert_eq!(
status.code(),
Some(130),
"ctrlc handler should exit with code 130 (128+SIGINT), got {status:?}"
);
assert!(!status.core_dumped(), "child should not have core-dumped");
assert_report_exists(&out_dir);
let _ = std::fs::remove_dir_all(&out_dir);
}