use std::path::Path;
use std::process::Stdio;
use std::time::{Duration, Instant};
use tempfile::TempDir;
use tokio::process::Command;
use mati_core::store::derive_slug;
const READY_TIMEOUT: Duration = Duration::from_secs(20);
const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(15);
const POLL: Duration = Duration::from_millis(100);
#[tokio::test]
#[ignore]
async fn daemon_start_writes_lifecycle_events_and_exits_cleanly_on_sigterm() {
let project_temp = TempDir::new().expect("project tempdir");
let project = std::fs::canonicalize(project_temp.path()).expect("canonicalize project");
let bin = env!("CARGO_BIN_EXE_mati");
let stderr_path = project.join("daemon.stderr");
let stderr_file = std::fs::File::create(&stderr_path).unwrap();
let mut child = Command::new(bin)
.arg("daemon")
.arg("start")
.current_dir(&project)
.env("RUST_LOG", "info")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::from(stderr_file))
.kill_on_drop(true) .spawn()
.expect("failed to spawn `mati daemon start`");
let pid = child.id().expect("child pid available pre-wait");
let slug = derive_slug(&project);
let mati_root = dirs::home_dir().unwrap().join(".mati").join(&slug);
let lifecycle_log = mati_root.join("lifecycle.log");
let sock = mati_root.join("mati.sock");
if wait_for_path(&sock, READY_TIMEOUT).is_err() {
let _ = child.kill().await;
let stderr = std::fs::read_to_string(&stderr_path).unwrap_or_default();
panic!("mati.sock never appeared.\nstderr:\n{stderr}");
}
let start_seen = wait_for_log_event(&lifecycle_log, "serve_start", READY_TIMEOUT);
assert!(
start_seen,
"lifecycle.log should contain serve_start; contents:\n{:?}",
std::fs::read_to_string(&lifecycle_log).ok()
);
unsafe {
libc::kill(pid as libc::pid_t, libc::SIGTERM);
}
let exit = tokio::time::timeout(SHUTDOWN_TIMEOUT, child.wait()).await;
match exit {
Ok(Ok(status)) => {
let _ = status;
}
Ok(Err(e)) => {
let _ = child.kill().await;
panic!("child wait error: {e}");
}
Err(_) => {
let _ = child.kill().await;
let stderr = std::fs::read_to_string(&stderr_path).unwrap_or_default();
panic!(
"daemon did not exit within {SHUTDOWN_TIMEOUT:?} after SIGTERM\n\
stderr:\n{stderr}"
);
}
}
let shutdown_seen =
wait_for_log_event(&lifecycle_log, "serve_shutdown", Duration::from_secs(2));
assert!(
shutdown_seen,
"lifecycle.log should contain serve_shutdown after SIGTERM; contents:\n{:?}",
std::fs::read_to_string(&lifecycle_log).ok()
);
let log_contents = std::fs::read_to_string(&lifecycle_log).unwrap();
assert!(
log_contents.contains("\tserve_shutdown\tsignal_sigterm"),
"expected serve_shutdown reason 'signal_sigterm'; got:\n{log_contents}"
);
assert!(!sock.exists(), "mati.sock should be removed on shutdown");
assert!(
!mati_root.join("mati.pid").exists(),
"mati.pid should be removed on shutdown"
);
}
#[tokio::test]
#[ignore]
async fn serve_exits_cleanly_on_sigterm_after_client_disconnect() {
let project_temp = TempDir::new().expect("project tempdir");
let project = std::fs::canonicalize(project_temp.path()).expect("canonicalize project");
let bin = env!("CARGO_BIN_EXE_mati");
let stderr_path = project.join("serve.stderr");
let stderr_file = std::fs::File::create(&stderr_path).unwrap();
let mut child = Command::new(bin)
.arg("serve")
.current_dir(&project)
.env("RUST_LOG", "info")
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::from(stderr_file))
.kill_on_drop(true)
.spawn()
.expect("failed to spawn `mati serve`");
let pid = child.id().expect("child pid available pre-wait");
drop(child.stdin.take());
let slug = derive_slug(&project);
let mati_root = dirs::home_dir().unwrap().join(".mati").join(&slug);
let lifecycle_log = mati_root.join("lifecycle.log");
let sock = mati_root.join("mati.sock");
if wait_for_path(&sock, READY_TIMEOUT).is_err() {
let _ = child.kill().await;
let stderr = std::fs::read_to_string(&stderr_path).unwrap_or_default();
panic!("mati.sock never appeared after stdin close.\nstderr:\n{stderr}");
}
let start_seen = wait_for_log_event(&lifecycle_log, "serve_start", Duration::from_secs(5));
assert!(
start_seen,
"lifecycle.log should contain serve_start; contents:\n{:?}",
std::fs::read_to_string(&lifecycle_log).ok()
);
unsafe {
libc::kill(pid as libc::pid_t, libc::SIGTERM);
}
let exit = tokio::time::timeout(SHUTDOWN_TIMEOUT, child.wait()).await;
match exit {
Ok(Ok(_status)) => { }
Ok(Err(e)) => {
let _ = child.kill().await;
panic!("child wait error: {e}");
}
Err(_) => {
let _ = child.kill().await;
let stderr = std::fs::read_to_string(&stderr_path).unwrap_or_default();
panic!(
"mati serve did not exit within {SHUTDOWN_TIMEOUT:?} after SIGTERM\n\
stderr:\n{stderr}"
);
}
}
let shutdown_seen =
wait_for_log_event(&lifecycle_log, "serve_shutdown", Duration::from_secs(2));
assert!(
shutdown_seen,
"lifecycle.log should contain serve_shutdown after SIGTERM; contents:\n{:?}",
std::fs::read_to_string(&lifecycle_log).ok()
);
let log_contents = std::fs::read_to_string(&lifecycle_log).unwrap();
assert!(
log_contents.contains("\tserve_shutdown\tsignal_shutdown"),
"expected serve_shutdown reason 'signal_shutdown'; got:\n{log_contents}"
);
assert!(
!sock.exists(),
"mati.sock should be removed on SIGTERM shutdown"
);
assert!(
!mati_root.join("mati.pid").exists(),
"mati.pid should be removed on SIGTERM shutdown"
);
}
fn wait_for_path(path: &Path, timeout: Duration) -> Result<(), &'static str> {
let start = Instant::now();
while start.elapsed() < timeout {
if path.exists() {
return Ok(());
}
std::thread::sleep(POLL);
}
Err("timeout")
}
fn wait_for_log_event(log_path: &Path, event: &str, timeout: Duration) -> bool {
let needle = format!("\t{event}\t");
let start = Instant::now();
while start.elapsed() < timeout {
if let Ok(contents) = std::fs::read_to_string(log_path) {
if contents.contains(&needle) {
return true;
}
}
std::thread::sleep(POLL);
}
false
}