#![allow(dead_code, reason = "shared harness included by multiple test crates")]
use std::path::{Path, PathBuf};
use std::process::Command;
pub struct CommandOutput {
pub stdout: String,
pub stderr: String,
pub code: i32,
}
pub fn fallow_bin() -> PathBuf {
std::env::var_os("CARGO_BIN_EXE_fallow").map_or_else(
|| {
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.pop(); path.pop(); path.push("target/debug/fallow");
if cfg!(windows) {
path.set_extension("exe");
}
path
},
PathBuf::from,
)
}
pub fn fixture_path(name: &str) -> PathBuf {
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.pop(); path.pop(); path.push("tests/fixtures");
path.push(name);
path
}
pub fn scrub_coverage_env(cmd: &mut Command) {
cmd.env_remove("FALLOW_COVERAGE")
.env_remove("FALLOW_COVERAGE_ROOT");
}
pub fn run_fallow(subcommand: &str, fixture: &str, args: &[&str]) -> CommandOutput {
let root = fixture_path(fixture);
run_fallow_in_root(subcommand, &root, args)
}
pub fn run_fallow_in_root(subcommand: &str, root: &Path, args: &[&str]) -> CommandOutput {
let bin = fallow_bin();
let mut cmd = Command::new(&bin);
cmd.arg(subcommand)
.arg("--root")
.arg(root)
.env("RUST_LOG", "")
.env("NO_COLOR", "1");
for arg in args {
cmd.arg(arg);
}
let output = cmd.output().expect("failed to run fallow binary");
CommandOutput {
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
code: output.status.code().unwrap_or(-1),
}
}
pub fn run_fallow_combined(fixture: &str, args: &[&str]) -> CommandOutput {
let bin = fallow_bin();
let root = fixture_path(fixture);
let mut cmd = Command::new(&bin);
cmd.arg("--root")
.arg(&root)
.env("RUST_LOG", "")
.env("NO_COLOR", "1");
for arg in args {
cmd.arg(arg);
}
let output = cmd.output().expect("failed to run fallow binary");
CommandOutput {
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
code: output.status.code().unwrap_or(-1),
}
}
pub fn run_fallow_raw(args: &[&str]) -> CommandOutput {
let bin = fallow_bin();
let mut cmd = Command::new(&bin);
cmd.env("RUST_LOG", "").env("NO_COLOR", "1");
scrub_coverage_env(&mut cmd);
for arg in args {
cmd.arg(arg);
}
let output = cmd.output().expect("failed to run fallow binary");
CommandOutput {
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
code: output.status.code().unwrap_or(-1),
}
}
pub fn run_fallow_raw_with_env(args: &[&str], env: &[(&str, &str)]) -> CommandOutput {
let bin = fallow_bin();
let mut cmd = Command::new(&bin);
cmd.env("RUST_LOG", "").env("NO_COLOR", "1");
scrub_coverage_env(&mut cmd);
for (key, value) in env {
cmd.env(key, value);
}
for arg in args {
cmd.arg(arg);
}
let output = cmd.output().expect("failed to run fallow binary");
CommandOutput {
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
code: output.status.code().unwrap_or(-1),
}
}
pub fn configure_type_aware_sidecar(cmd: &mut Command) {
let mut sidecar = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
sidecar.pop(); sidecar.pop(); sidecar.push("tools/type-aware-sidecar/fallow-type-aware.mjs");
#[cfg(windows)]
let sidecar_bin = {
let path = std::env::var_os("PATH").expect("PATH must contain the Node.js runtime");
std::env::split_paths(&path)
.map(|entry| entry.join("node.exe"))
.find(|candidate| candidate.is_file())
.expect("Node.js executable must be available for type-aware CLI tests")
};
#[cfg(not(windows))]
let sidecar_bin = sidecar.clone();
cmd.env("FALLOW_TYPE_AWARE_BIN", sidecar_bin);
#[cfg(windows)]
cmd.env("FALLOW_TYPE_AWARE_SCRIPT", sidecar);
}
pub fn run_fallow_raw_with_type_aware_sidecar(args: &[&str]) -> CommandOutput {
let bin = fallow_bin();
let mut cmd = Command::new(&bin);
cmd.env("RUST_LOG", "").env("NO_COLOR", "1");
configure_type_aware_sidecar(&mut cmd);
for arg in args {
cmd.arg(arg);
}
let output = cmd.output().expect("failed to run fallow binary");
CommandOutput {
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
code: output.status.code().unwrap_or(-1),
}
}
pub fn parse_json(output: &CommandOutput) -> serde_json::Value {
serde_json::from_str(&output.stdout).unwrap_or_else(|e| {
panic!(
"failed to parse JSON: {e}\nstdout was:\n{}\nstderr was:\n{}",
output.stdout, output.stderr
)
})
}
pub const VOLATILE_REPORT_FIELDS: &[&str] = &["elapsed_ms", "head_sha"];
pub fn strip_volatile_fields(value: &mut serde_json::Value) {
match value {
serde_json::Value::Object(map) => {
for field in VOLATILE_REPORT_FIELDS {
map.remove(*field);
}
if let Some(telemetry) = map
.get_mut("_meta")
.and_then(|meta| meta.get_mut("telemetry"))
.and_then(|telemetry| telemetry.as_object_mut())
{
telemetry.remove("analysis_run_id");
}
for nested in map.values_mut() {
strip_volatile_fields(nested);
}
}
serde_json::Value::Array(items) => {
for nested in items {
strip_volatile_fields(nested);
}
}
_ => {}
}
}
pub fn canonical_report(output: &CommandOutput) -> String {
let mut value = parse_json(output);
strip_volatile_fields(&mut value);
serde_json::to_string(&value).expect("re-serialize canonical report")
}
pub fn redact_paths(s: &str, root: &Path) -> String {
let root_str = root.to_string_lossy();
s.replace(root_str.as_ref(), "[ROOT]").replace('\\', "/")
}
pub fn redact_version(s: &str) -> String {
s.replace(env!("CARGO_PKG_VERSION"), "[VERSION]")
}
pub fn redact_all(s: &str, root: &Path) -> String {
let s = redact_paths(s, root);
redact_version(&s)
}