test(xy): integration test harness

This commit is contained in:
2026-05-25 12:03:38 +02:00
parent 7107977637
commit 48d63a0549
+82
View File
@@ -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<Child>,
}
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
}