diff --git a/crates/xy/tests/common/mod.rs b/crates/xy/tests/common/mod.rs new file mode 100644 index 0000000..d85b375 --- /dev/null +++ b/crates/xy/tests/common/mod.rs @@ -0,0 +1,82 @@ +#![allow(dead_code)] // not all helpers are used by every test file + +use std::path::PathBuf; +use std::process::Stdio; +use std::time::Duration; +use tokio::process::{Child, Command}; +use tempfile::TempDir; + +pub struct Harness { + pub tmp: TempDir, + pub config_dir: PathBuf, + pub state_dir: PathBuf, + pub socket: PathBuf, + pub daemon: Option, +} + +impl Harness { + pub fn new() -> Self { + let tmp = tempfile::tempdir().expect("tempdir"); + std::fs::create_dir_all(tmp.path().join("config")).unwrap(); + std::fs::create_dir_all(tmp.path().join("state")).unwrap(); + std::fs::create_dir_all(tmp.path().join("run")).unwrap(); + let config_dir = tmp.path().join("config/xy/servers"); + let state_dir = tmp.path().join("state/xy"); + std::fs::create_dir_all(&config_dir).unwrap(); + std::fs::create_dir_all(&state_dir).unwrap(); + let socket = tmp.path().join("run/xy.sock"); + Self { tmp, config_dir, state_dir, socket, daemon: None } + } + + pub fn write_server(&self, name: &str, command: &str, port: u16, restart_policy: &str) { + let body = format!( + "command \"{command}\"\nport {port}\nrestart {{ policy \"{restart_policy}\" backoff-initial \"10ms\" backoff-max \"50ms\" max-retries-per-minute 3 }}\nstop {{ grace \"500ms\" }}\n" + ); + std::fs::write(self.config_dir.join(format!("{name}.kdl")), body).unwrap(); + } + + pub async fn start_daemon(&mut self, xy_bin: &PathBuf) { + let child = Command::new(xy_bin) + .arg("daemon") + .env("XDG_CONFIG_HOME", self.tmp.path().join("config")) + .env("XDG_STATE_HOME", self.tmp.path().join("state")) + .env("XDG_RUNTIME_DIR", self.tmp.path().join("run")) + .stdout(Stdio::null()) + .stderr(Stdio::inherit()) + .kill_on_drop(true) + .spawn() + .expect("spawn daemon"); + self.daemon = Some(child); + let deadline = std::time::Instant::now() + Duration::from_secs(5); + while !self.socket.exists() { + if std::time::Instant::now() > deadline { panic!("daemon socket never appeared"); } + tokio::time::sleep(Duration::from_millis(25)).await; + } + } + + pub async fn run_cli(&self, xy_bin: &PathBuf, args: &[&str]) -> (i32, String, String) { + let out = Command::new(xy_bin) + .args(args) + .env("XDG_CONFIG_HOME", self.tmp.path().join("config")) + .env("XDG_STATE_HOME", self.tmp.path().join("state")) + .env("XDG_RUNTIME_DIR", self.tmp.path().join("run")) + .output().await.expect("run cli"); + let code = out.status.code().unwrap_or(-1); + let stdout = String::from_utf8_lossy(&out.stdout).to_string(); + let stderr = String::from_utf8_lossy(&out.stderr).to_string(); + (code, stdout, stderr) + } +} + +pub fn xy_bin() -> PathBuf { artifact("xy") } +pub fn sleep_server_bin() -> PathBuf { artifact("xy-test-sleep-server") } +pub fn exit_failure_bin() -> PathBuf { artifact("xy-test-exit-failure") } + +fn artifact(name: &str) -> PathBuf { + let mut p = std::env::current_exe().unwrap(); + p.pop(); + if p.ends_with("deps") { p.pop(); } + p.push(name); + if !p.exists() { panic!("artifact `{}` not found at {}", name, p.display()); } + p +}