#![cfg(unix)]
mod common;
use common::sqry_bin;
use std::os::unix::net::UnixStream;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant};
fn find_sqryd_binary() -> Option<PathBuf> {
if let Ok(path) = std::env::var("CARGO_BIN_EXE_sqryd") {
let p = PathBuf::from(path);
if p.is_file() {
return Some(p);
}
}
let binary_name = format!("sqryd{}", std::env::consts::EXE_SUFFIX);
let exe = std::env::current_exe().ok()?;
let parent = exe.parent()?; let candidate = parent.join(&binary_name);
if candidate.is_file() {
return Some(candidate);
}
let grandparent = parent.parent()?; let candidate = grandparent.join(&binary_name);
if candidate.is_file() {
return Some(candidate);
}
None
}
fn find_sqry_binary() -> PathBuf {
sqry_bin()
}
fn write_daemon_config(config_path: &Path, socket_path: &Path, runtime_dir: &Path) {
let contents = format!(
"[socket]\npath = {:?}\n",
socket_path.to_string_lossy().as_ref()
);
std::fs::write(config_path, &contents)
.unwrap_or_else(|e| panic!("write daemon config TOML to {}: {e}", config_path.display()));
let sqry_runtime = runtime_dir.join("sqry");
std::fs::create_dir_all(&sqry_runtime)
.unwrap_or_else(|e| panic!("create runtime dir {}: {e}", sqry_runtime.display()));
}
fn wait_for_socket_connectable(socket_path: &Path, timeout: Duration) -> bool {
let deadline = Instant::now() + timeout;
loop {
if UnixStream::connect(socket_path).is_ok() {
return true;
}
if Instant::now() >= deadline {
return false;
}
std::thread::sleep(Duration::from_millis(50));
}
}
fn wait_for_socket_gone(socket_path: &Path, timeout: Duration) -> bool {
let deadline = Instant::now() + timeout;
loop {
if UnixStream::connect(socket_path).is_err() {
return true;
}
if Instant::now() >= deadline {
return false;
}
std::thread::sleep(Duration::from_millis(50));
}
}
fn send_sigterm(child: &Child) {
let pid = child.id();
unsafe {
libc::kill(pid as libc::pid_t, libc::SIGTERM);
}
}
struct DaemonGuard {
child: Child,
_tmp: tempfile::TempDir,
}
impl DaemonGuard {
fn new(child: Child, tmp: tempfile::TempDir) -> Self {
Self { child, _tmp: tmp }
}
fn wait_exit(&mut self, timeout: Duration) -> std::process::ExitStatus {
let deadline = Instant::now() + timeout;
loop {
match self.child.try_wait().expect("try_wait on daemon child") {
Some(status) => return status,
None => {
if Instant::now() >= deadline {
panic!("sqryd did not exit within {}s; killing", timeout.as_secs());
}
std::thread::sleep(Duration::from_millis(50));
}
}
}
}
}
impl Drop for DaemonGuard {
fn drop(&mut self) {
send_sigterm(&self.child);
std::thread::sleep(Duration::from_millis(200));
let _ = self.child.kill();
let _ = self.child.wait();
}
}
struct DetachedDaemonGuard {
pid: Option<u32>,
_tmp: tempfile::TempDir,
}
impl DetachedDaemonGuard {
fn new(pid: u32, tmp: tempfile::TempDir) -> Self {
Self {
pid: Some(pid),
_tmp: tmp,
}
}
fn disarm(&mut self) {
self.pid = None;
}
}
impl Drop for DetachedDaemonGuard {
fn drop(&mut self) {
if let Some(pid) = self.pid.take() {
unsafe {
libc::kill(pid as libc::pid_t, libc::SIGTERM);
}
std::thread::sleep(Duration::from_millis(200));
unsafe {
libc::kill(pid as libc::pid_t, libc::SIGKILL);
}
}
}
}
fn read_pid_from_file(path: &Path) -> Option<u32> {
let contents = std::fs::read_to_string(path).ok()?;
contents.trim().parse::<u32>().ok()
}
fn wait_for_pidfile(pidfile_path: &Path, timeout: Duration) -> Option<u32> {
let deadline = Instant::now() + timeout;
loop {
if let Some(pid) = read_pid_from_file(pidfile_path) {
return Some(pid);
}
if Instant::now() >= deadline {
return None;
}
std::thread::sleep(Duration::from_millis(50));
}
}
#[test]
fn daemon_status_when_not_running_exits_nonzero() {
let tmp = tempfile::TempDir::new().expect("create tempdir");
let socket_path = tmp.path().join("sqryd-status-test.sock");
let config_path = tmp.path().join("daemon.toml");
write_daemon_config(&config_path, &socket_path, tmp.path());
let sqry = find_sqry_binary();
let output = Command::new(&sqry)
.args(["daemon", "status"])
.env("SQRY_DAEMON_CONFIG", &config_path)
.env("XDG_RUNTIME_DIR", tmp.path())
.stdin(Stdio::null())
.output()
.unwrap_or_else(|e| panic!("failed to spawn sqry daemon status: {e}"));
assert!(
!output.status.success(),
"sqry daemon status must exit nonzero when no daemon is running; \
got exit code {:?}\nstdout: {}\nstderr: {}",
output.status.code(),
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("not running") || stderr.contains("daemon"),
"stderr must contain diagnostic message; got:\n{stderr}"
);
}
#[test]
fn daemon_start_locates_sqryd_binary() {
let tmp = tempfile::TempDir::new().expect("create tempdir");
let socket_path = tmp.path().join("sqryd-start-locate-test.sock");
let config_path = tmp.path().join("daemon.toml");
write_daemon_config(&config_path, &socket_path, tmp.path());
let sqry = find_sqry_binary();
let nonexistent = tmp.path().join("no-such-sqryd");
let output = Command::new(&sqry)
.args([
"daemon",
"start",
"--sqryd-path",
nonexistent.to_str().expect("valid UTF-8 path"),
])
.env("SQRY_DAEMON_CONFIG", &config_path)
.env("XDG_RUNTIME_DIR", tmp.path())
.stdin(Stdio::null())
.output()
.unwrap_or_else(|e| panic!("failed to spawn sqry daemon start: {e}"));
assert!(
!output.status.success(),
"sqry daemon start with nonexistent --sqryd-path must exit nonzero; \
got exit code {:?}\nstdout: {}\nstderr: {}",
output.status.code(),
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
let combined = format!("{stderr}{stdout}");
assert!(
combined.contains("does not exist")
|| combined.contains("not found")
|| combined.contains("no-such-sqryd"),
"error output must mention missing binary; got:\nstderr: {stderr}\nstdout: {stdout}"
);
}
#[test]
fn daemon_logs_without_daemon_prints_error() {
let tmp = tempfile::TempDir::new().expect("create tempdir");
let socket_path = tmp.path().join("sqryd-logs-test.sock");
let config_path = tmp.path().join("daemon.toml");
let missing_log = tmp.path().join("missing.log");
let contents = format!(
"[socket]\npath = {:?}\nlog_file = {:?}\n",
socket_path.to_string_lossy().as_ref(),
missing_log.to_string_lossy().as_ref(),
);
std::fs::write(&config_path, &contents)
.unwrap_or_else(|e| panic!("write daemon config TOML: {e}"));
let sqry_runtime = tmp.path().join("sqry");
std::fs::create_dir_all(&sqry_runtime).unwrap_or_else(|e| panic!("create runtime dir: {e}"));
let sqry = find_sqry_binary();
let output = Command::new(&sqry)
.args(["daemon", "logs"])
.env("SQRY_DAEMON_CONFIG", &config_path)
.env("XDG_RUNTIME_DIR", tmp.path())
.stdin(Stdio::null())
.output()
.unwrap_or_else(|e| panic!("failed to spawn sqry daemon logs: {e}"));
assert!(
!output.status.success(),
"sqry daemon logs must exit nonzero when log_file does not exist; \
got exit code {:?}\nstdout: {}\nstderr: {}",
output.status.code(),
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("missing.log")
|| stderr.contains("not found")
|| stderr.contains("log file"),
"error message must mention missing log file; got:\n{stderr}"
);
}
#[test]
fn daemon_start_idempotent() {
let sqryd = match find_sqryd_binary() {
Some(b) => b,
None => {
eprintln!(
"SKIP daemon_start_idempotent: sqryd binary not found. \
Build with `cargo build -p sqry-daemon` first."
);
return;
}
};
let tmp = tempfile::TempDir::new().expect("create tempdir");
let socket_path = tmp.path().join("sqryd-idempotent.sock");
let config_path = tmp.path().join("daemon.toml");
write_daemon_config(&config_path, &socket_path, tmp.path());
let child = Command::new(&sqryd)
.args(["foreground"])
.env("SQRY_DAEMON_CONFIG", &config_path)
.env("XDG_RUNTIME_DIR", tmp.path())
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.unwrap_or_else(|e| panic!("failed to spawn sqryd foreground: {e}"));
let mut guard = DaemonGuard::new(child, tmp);
assert!(
wait_for_socket_connectable(&socket_path, Duration::from_secs(15)),
"sqryd foreground socket never became connectable at {} within 15s",
socket_path.display()
);
let sqry = find_sqry_binary();
let config_path = guard._tmp.path().join("daemon.toml");
let status1 = Command::new(&sqry)
.args(["daemon", "start", "--sqryd-path", sqryd.to_str().unwrap()])
.env("SQRY_DAEMON_CONFIG", &config_path)
.env("XDG_RUNTIME_DIR", guard._tmp.path())
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("spawn sqry daemon start (1st)");
assert!(
status1.success(),
"first `sqry daemon start` when daemon already running must exit 0; \
got {status1}"
);
let status2 = Command::new(&sqry)
.args(["daemon", "start", "--sqryd-path", sqryd.to_str().unwrap()])
.env("SQRY_DAEMON_CONFIG", &config_path)
.env("XDG_RUNTIME_DIR", guard._tmp.path())
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("spawn sqry daemon start (2nd)");
assert!(
status2.success(),
"second `sqry daemon start` when daemon already running must exit 0; \
got {status2}"
);
send_sigterm(&guard.child);
guard.wait_exit(Duration::from_secs(5));
}
fn stop_daemon_best_effort(sqry: &Path, config_path: &Path, runtime_dir: &Path) {
let _ = Command::new(sqry)
.args(["daemon", "stop", "--timeout", "5"])
.env("SQRY_DAEMON_CONFIG", config_path)
.env("XDG_RUNTIME_DIR", runtime_dir)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
}
#[test]
fn daemon_start_stop_round_trip() {
let sqryd = match find_sqryd_binary() {
Some(b) => b,
None => {
eprintln!(
"SKIP daemon_start_stop_round_trip: sqryd binary not found. \
Build with `cargo build -p sqry-daemon` first."
);
return;
}
};
let tmp = tempfile::TempDir::new().expect("create tempdir");
let socket_path = tmp.path().join("sqryd-round-trip.sock");
let config_path = tmp.path().join("daemon.toml");
let pidfile_path = tmp.path().join("sqry").join("sqryd.pid");
write_daemon_config(&config_path, &socket_path, tmp.path());
let sqry = find_sqry_binary();
let start_status = Command::new(&sqry)
.args([
"daemon",
"start",
"--sqryd-path",
sqryd.to_str().expect("sqryd path is valid UTF-8"),
"--timeout",
"20",
])
.env("SQRY_DAEMON_CONFIG", &config_path)
.env("XDG_RUNTIME_DIR", tmp.path())
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("spawn sqry daemon start");
assert!(
start_status.success(),
"sqry daemon start must exit 0 when daemon starts successfully; \
got {start_status}"
);
let grandchild_pid =
wait_for_pidfile(&pidfile_path, Duration::from_secs(5)).unwrap_or_else(|| {
stop_daemon_best_effort(&sqry, &config_path, tmp.path());
panic!(
"daemon pidfile never appeared at {} within 5s after sqry daemon start",
pidfile_path.display()
)
});
let mut daemon_guard = DetachedDaemonGuard::new(grandchild_pid, tmp);
assert!(
wait_for_socket_connectable(&socket_path, Duration::from_secs(5)),
"socket {} not connectable after sqry daemon start succeeded",
socket_path.display()
);
let config_path = daemon_guard._tmp.path().join("daemon.toml");
let status_output = Command::new(&sqry)
.args(["daemon", "status"])
.env("SQRY_DAEMON_CONFIG", &config_path)
.env("XDG_RUNTIME_DIR", daemon_guard._tmp.path())
.stdin(Stdio::null())
.output()
.expect("spawn sqry daemon status (running)");
assert!(
status_output.status.success(),
"sqry daemon status must exit 0 when daemon is running; \
got {:?}\nstdout: {}\nstderr: {}",
status_output.status.code(),
String::from_utf8_lossy(&status_output.stdout),
String::from_utf8_lossy(&status_output.stderr),
);
let stdout = String::from_utf8_lossy(&status_output.stdout);
assert!(
stdout.contains("sqryd") || stdout.contains("v8"),
"sqry daemon status output must contain daemon info; got:\n{stdout}"
);
let stop_status = Command::new(&sqry)
.args(["daemon", "stop", "--timeout", "10"])
.env("SQRY_DAEMON_CONFIG", &config_path)
.env("XDG_RUNTIME_DIR", daemon_guard._tmp.path())
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("spawn sqry daemon stop");
assert!(
stop_status.success(),
"sqry daemon stop must exit 0 after stopping running daemon; \
got {stop_status}"
);
assert!(
wait_for_socket_gone(&socket_path, Duration::from_secs(10)),
"socket {} still connectable 10s after sqry daemon stop",
socket_path.display()
);
daemon_guard.disarm();
let status_after = Command::new(&sqry)
.args(["daemon", "status"])
.env("SQRY_DAEMON_CONFIG", &config_path)
.env("XDG_RUNTIME_DIR", daemon_guard._tmp.path())
.stdin(Stdio::null())
.output()
.expect("spawn sqry daemon status (stopped)");
assert!(
!status_after.status.success(),
"sqry daemon status must exit nonzero after daemon is stopped; \
got exit code {:?}\nstdout: {}\nstderr: {}",
status_after.status.code(),
String::from_utf8_lossy(&status_after.stdout),
String::from_utf8_lossy(&status_after.stderr),
);
}