#![allow(dead_code)]
use std::path::PathBuf;
use std::process::{Command, Output, Stdio};
pub fn binary() -> PathBuf {
let mut p = std::env::current_exe().expect("current exe");
p.pop(); if p.ends_with("deps") {
p.pop();
}
p.push("agent-exec");
if cfg!(windows) {
p.set_extension("exe");
}
p
}
pub struct TestHarness {
_tmp: tempfile::TempDir,
root: String,
}
impl TestHarness {
pub fn new() -> Self {
let tmp = tempfile::tempdir().expect("create tempdir");
let root = tmp
.path()
.to_str()
.expect("tempdir path is valid UTF-8")
.to_string();
Self { _tmp: tmp, root }
}
pub fn root(&self) -> &str {
&self.root
}
pub fn run(&self, args: &[&str]) -> serde_json::Value {
run_cmd_with_root(args, Some(&self.root))
}
}
impl Default for TestHarness {
fn default() -> Self {
Self::new()
}
}
pub fn run_raw_with_root_and_stdin(
args: &[&str],
root: Option<&str>,
stdin_bytes: Option<&[u8]>,
) -> Output {
run_raw_with_root_cwd_and_stdin(args, root, None, stdin_bytes)
}
pub fn run_cmd_with_root(args: &[&str], root: Option<&str>) -> serde_json::Value {
let output = run_raw_with_root_and_stdin(args, root, None);
parse_json_stdout(&output, args)
}
pub fn run_cmd_with_root_and_stdin(
args: &[&str],
root: Option<&str>,
stdin_bytes: &[u8],
) -> serde_json::Value {
let output = run_raw_with_root_and_stdin(args, root, Some(stdin_bytes));
parse_json_stdout(&output, args)
}
pub fn run_cmd_with_root_and_cwd(
args: &[&str],
root: Option<&str>,
cwd: Option<&std::path::Path>,
) -> (serde_json::Value, std::process::ExitStatus) {
let output = run_raw_with_root_cwd_and_stdin(args, root, cwd, None);
let value = if output.stdout.is_empty() {
serde_json::json!({})
} else {
parse_json_stdout(&output, args)
};
(value, output.status)
}
pub fn run_raw_with_root_cwd_and_stdin(
args: &[&str],
root: Option<&str>,
cwd: Option<&std::path::Path>,
stdin_bytes: Option<&[u8]>,
) -> Output {
let bin = binary();
let mut cmd = Command::new(&bin);
cmd.args(args);
if let Some(r) = root {
cmd.env("AGENT_EXEC_ROOT", r);
}
if let Some(d) = cwd {
cmd.current_dir(d);
}
if stdin_bytes.is_some() {
cmd.stdin(Stdio::piped());
} else {
cmd.stdin(Stdio::null());
}
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
let mut child = cmd.spawn().expect("spawn binary");
if let Some(bytes) = stdin_bytes {
use std::io::Write;
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(bytes).expect("write stdin bytes");
}
}
child.wait_with_output().expect("wait binary output")
}
pub fn assert_usage_error(args: &[&str], root: Option<&str>) {
let bin = binary();
let mut cmd = Command::new(&bin);
cmd.args(args);
if let Some(r) = root {
cmd.env("AGENT_EXEC_ROOT", r);
}
let output = cmd.output().expect("run binary");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert_eq!(
output.status.code(),
Some(2),
"expected exit code 2 (usage error)\nstdout: {stdout}\nstderr: {stderr}\nargs: {args:?}"
);
assert!(
stdout.trim().is_empty(),
"expected empty stdout for usage error\nstdout: {stdout}\nstderr: {stderr}\nargs: {args:?}"
);
}
pub fn run_cmd_with_global_root_flag(root: &str, args: &[&str]) -> serde_json::Value {
let bin = binary();
let mut cmd = Command::new(&bin);
cmd.arg("--root").arg(root);
cmd.args(args);
cmd.env_remove("AGENT_EXEC_ROOT");
let output = cmd.output().expect("run binary");
parse_json_stdout(&output, args)
}
pub fn run_cmd_with_subcommand_root_flag(
subcommand: &str,
root: &str,
extra_args: &[&str],
) -> serde_json::Value {
let bin = binary();
let mut cmd = Command::new(&bin);
cmd.arg(subcommand);
cmd.arg("--root").arg(root);
cmd.args(extra_args);
cmd.env_remove("AGENT_EXEC_ROOT");
let output = cmd.output().expect("run binary");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stdout.trim().is_empty(),
"stdout is empty (stderr: {stderr})\nsubcommand: {subcommand}, root: {root}, extra: {extra_args:?}"
);
serde_json::from_str(stdout.trim()).unwrap_or_else(|e| {
panic!(
"stdout is not valid JSON: {e}\nstdout: {stdout}\nstderr: {stderr}\nsubcommand: {subcommand}"
)
})
}
pub fn assert_envelope(v: &serde_json::Value, expected_type: &str, expected_ok: bool) {
assert_eq!(
v["schema_version"].as_str().unwrap_or(""),
"0.1",
"schema_version mismatch: {v}"
);
assert_eq!(
v["ok"].as_bool().unwrap_or(!expected_ok),
expected_ok,
"ok mismatch: {v}"
);
assert_eq!(
v["type"].as_str().unwrap_or(""),
expected_type,
"type mismatch: {v}"
);
}
pub fn assert_common_fields(json: &serde_json::Value) {
assert!(
json.get("schema_version").is_some(),
"missing schema_version in: {json}"
);
assert!(json.get("ok").is_some(), "missing ok in: {json}");
assert!(json.get("type").is_some(), "missing type in: {json}");
}
fn parse_json_stdout(output: &Output, args: &[&str]) -> serde_json::Value {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stdout.trim().is_empty(),
"stdout is empty (stderr: {stderr})\nargs: {args:?}"
);
serde_json::from_str(stdout.trim()).unwrap_or_else(|e| {
panic!("stdout is not valid JSON: {e}\nstdout: {stdout}\nstderr: {stderr}\nargs: {args:?}")
})
}