use super::support::*;
use std::time::Duration;
#[cfg(unix)]
#[test]
fn test_headless_bind_custom_socket() {
let tmp = tempfile::TempDir::new().expect("create temp dir");
let sock_path = tmp.path().join("custom.sock");
let sock_str = sock_path.display().to_string();
let process = HeadlessProcess::spawn_with_args(&["--bind", &sock_str])
.expect("Failed to spawn headless with --bind");
assert!(
sock_path.exists(),
"custom socket file should exist at: {}",
sock_str
);
let result = process.ipc_eval("1 + 1").expect("eval should work");
assert!(result.success, "eval should succeed: {}", result.stderr);
assert!(
result.stdout.contains("[1] 2"),
"should return result: {}",
result.stdout
);
let stderr = process.stderr_output();
assert!(
stderr.contains(&sock_str),
"stderr should mention custom socket path: {}",
stderr
);
}
#[test]
fn test_headless_pid_file() {
let tmp = tempfile::TempDir::new().expect("create temp dir");
let pid_path = tmp.path().join("arf.pid");
let pid_str = pid_path.display().to_string();
let mut process = HeadlessProcess::spawn_with_args(&["--pid-file", &pid_str])
.expect("Failed to spawn headless with --pid-file");
let start = std::time::Instant::now();
let pid_content = loop {
assert!(
start.elapsed() < Duration::from_secs(5),
"PID file should appear with content at: {}",
pid_str
);
if let Ok(content) = std::fs::read_to_string(&pid_path)
&& !content.is_empty()
{
break content;
}
std::thread::sleep(Duration::from_millis(50));
};
let expected_pid = process.pid.to_string();
assert_eq!(
pid_content.trim(),
expected_pid,
"PID file should contain process PID"
);
let result = process.ipc_shutdown().expect("shutdown should run");
assert!(result.success, "shutdown should succeed");
process
.wait_for_exit(Duration::from_secs(10))
.expect("headless process should exit after shutdown");
assert!(
!pid_path.exists(),
"PID file should be removed after shutdown"
);
}
#[test]
fn test_headless_quiet_mode() {
let process = HeadlessProcess::spawn_with_args(&["--quiet"])
.expect("Failed to spawn headless with --quiet");
let result = process.ipc_eval("1 + 1").expect("eval should work");
assert!(result.success, "eval should succeed: {}", result.stderr);
assert!(
result.stdout.contains("[1] 2"),
"should return result: {}",
result.stdout
);
let stderr = process.stderr_output();
assert!(
!stderr.contains("IPC server listening on:"),
"quiet mode should suppress IPC listening message, got: {}",
stderr
);
assert!(
!stderr.contains("Headless mode ready"),
"quiet mode should suppress ready message, got: {}",
stderr
);
}
#[test]
fn test_headless_log_file() {
let tmp = tempfile::TempDir::new().expect("create temp dir");
let log_path = tmp.path().join("arf.log");
let log_str = log_path.display().to_string();
let process = HeadlessProcess::spawn_with_args(&["--log-file", &log_str])
.expect("Failed to spawn headless with --log-file");
let result = process.ipc_eval("1 + 1").expect("eval should work");
assert!(result.success, "eval should succeed: {}", result.stderr);
assert!(log_path.exists(), "log file should exist at: {}", log_str);
let log_content = std::fs::read_to_string(&log_path).unwrap_or_default();
assert!(
log_content.contains("Headless mode ready"),
"log file should contain status messages (stderr is redirected): {}",
log_content
);
let stderr = process.stderr_output();
assert!(
stderr.trim().is_empty(),
"stderr pipe should be empty when --log-file redirects stderr, but got: {}",
stderr
);
}
#[cfg(unix)]
fn assert_signal_graceful_shutdown(signal: nix::sys::signal::Signal) {
use nix::sys::signal;
use nix::unistd::Pid;
let tmp = tempfile::TempDir::new().expect("create temp dir");
let pid_path = tmp.path().join("arf.pid");
let pid_str = pid_path.display().to_string();
let mut process = HeadlessProcess::spawn_with_args(&["--pid-file", &pid_str])
.expect("Failed to spawn headless with --pid-file");
let start = std::time::Instant::now();
loop {
if start.elapsed() > Duration::from_secs(10) {
panic!(
"Headless mode should become ready.\nServer output:\n{}",
process.server_output()
);
}
if let Ok(Some(status)) = process.child.try_wait() {
panic!(
"Headless process exited early with {status}.\nServer output:\n{}",
process.server_output()
);
}
if process.stderr_output().contains("Headless mode ready") {
break;
}
std::thread::sleep(Duration::from_millis(50));
}
assert!(pid_path.exists(), "PID file should exist at: {}", pid_str);
signal::kill(Pid::from_raw(process.pid as i32), signal)
.unwrap_or_else(|e| panic!("failed to send {signal}: {e}"));
let status = process
.wait_for_exit(Duration::from_secs(10))
.unwrap_or_else(|e| panic!("headless process should exit after {signal}: {e}"));
assert!(
status.success(),
"headless process should exit cleanly after {signal}, got: {status}"
);
assert!(
!pid_path.exists(),
"PID file should be removed after {signal} shutdown"
);
}
#[cfg(unix)]
#[test]
fn test_headless_sigterm_shutdown() {
assert_signal_graceful_shutdown(nix::sys::signal::Signal::SIGTERM);
}
#[cfg(unix)]
#[test]
fn test_headless_sighup_shutdown() {
assert_signal_graceful_shutdown(nix::sys::signal::Signal::SIGHUP);
}
#[cfg(unix)]
#[test]
fn test_headless_ctrlc_shutdown() {
assert_signal_graceful_shutdown(nix::sys::signal::Signal::SIGINT);
}
#[cfg(windows)]
#[test]
fn test_headless_ctrlc_shutdown() {
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
let tmp = tempfile::TempDir::new().expect("create temp dir");
let pid_path = tmp.path().join("arf.pid");
let pid_str = pid_path.display().to_string();
let mut process = HeadlessProcess::spawn_with_creation_flags(
&["--pid-file", &pid_str],
CREATE_NEW_PROCESS_GROUP,
)
.expect("Failed to spawn headless with --pid-file");
let start = std::time::Instant::now();
loop {
if start.elapsed() > Duration::from_secs(10) {
panic!(
"Headless mode should become ready.\nServer output:\n{}",
process.server_output()
);
}
if let Ok(Some(status)) = process.child.try_wait() {
panic!(
"Headless process exited early with {status}.\nServer output:\n{}",
process.server_output()
);
}
if process.stderr_output().contains("Headless mode ready") {
break;
}
std::thread::sleep(Duration::from_millis(50));
}
assert!(pid_path.exists(), "PID file should exist");
let result = unsafe {
windows_sys::Win32::System::Console::GenerateConsoleCtrlEvent(
windows_sys::Win32::System::Console::CTRL_BREAK_EVENT,
process.pid,
)
};
assert!(
result != 0,
"GenerateConsoleCtrlEvent failed: {}",
std::io::Error::last_os_error()
);
let status = process
.wait_for_exit(Duration::from_secs(10))
.unwrap_or_else(|e| {
panic!(
"headless process should exit after CTRL_BREAK: {e}\nServer output:\n{}",
process.server_output()
)
});
assert!(
status.success(),
"headless process should exit cleanly after CTRL_BREAK, got: {status}\n\
Server output:\n{}",
process.server_output()
);
assert!(
!pid_path.exists(),
"PID file should be removed after CTRL_BREAK shutdown\nServer output:\n{}",
process.server_output()
);
}