#![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 exe = std::env::current_exe().expect("test binary path");
let dir = exe.parent().expect("test binary directory");
let profile_dir = if dir.ends_with("deps") {
dir.parent().expect("profile directory")
} else {
dir
};
let mut path = profile_dir.join("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 copy_fixture(name: &str) -> tempfile::TempDir {
fn copy(from: &Path, to: &Path) {
std::fs::create_dir_all(to).expect("create fixture copy dir");
for entry in std::fs::read_dir(from).expect("read fixture dir") {
let entry = entry.expect("fixture entry");
let target = to.join(entry.file_name());
if entry.file_type().expect("fixture entry type").is_dir() {
copy(&entry.path(), &target);
} else {
std::fs::copy(entry.path(), &target).expect("copy fixture file");
}
}
}
let dir = tempfile::tempdir().expect("create fixture copy");
copy(&fixture_path(name), dir.path());
dir
}
pub fn scrub_coverage_env(cmd: &mut Command) {
cmd.env_remove("FALLOW_COVERAGE")
.env_remove("FALLOW_COVERAGE_ROOT");
}
fn fallow_command() -> Command {
let mut cmd = Command::new(fallow_bin());
cmd.env("RUST_LOG", "").env("NO_COLOR", "1");
scrub_coverage_env(&mut cmd);
cmd
}
fn execute(mut cmd: Command) -> CommandOutput {
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(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 mut cmd = fallow_command();
cmd.arg(subcommand).arg("--root").arg(root).args(args);
execute(cmd)
}
pub fn run_fallow_combined(fixture: &str, args: &[&str]) -> CommandOutput {
let mut cmd = fallow_command();
cmd.arg("--root").arg(fixture_path(fixture)).args(args);
execute(cmd)
}
pub fn run_fallow_raw(args: &[&str]) -> CommandOutput {
run_fallow_raw_with_env(args, &[])
}
pub fn run_fallow_raw_with_env(args: &[&str], env: &[(&str, &str)]) -> CommandOutput {
let mut cmd = fallow_command();
cmd.envs(env.iter().copied()).args(args);
execute(cmd)
}
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 canonical_report_without_gate_outcomes(output: &CommandOutput) -> String {
let mut value = parse_json(output);
strip_volatile_fields(&mut value);
strip_gate_outcomes(&mut value);
serde_json::to_string(&value).expect("re-serialize canonical report")
}
fn strip_gate_outcomes(value: &mut serde_json::Value) {
match value {
serde_json::Value::Object(map) => {
map.remove("gate_outcomes");
for nested in map.values_mut() {
strip_gate_outcomes(nested);
}
}
serde_json::Value::Array(items) => {
for item in items {
strip_gate_outcomes(item);
}
}
_ => {}
}
}
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)
}
const GIT_NULL_CONFIG: &str = "/dev/null";
const GIT_TEST_NAME: &str = "test";
const GIT_TEST_EMAIL: &str = "test@test.com";
pub fn git_command(dir: &Path) -> Command {
let mut cmd = Command::new("git");
fallow_engine::changed_files::clear_ambient_git_env(&mut cmd);
cmd.current_dir(dir)
.env("GIT_CONFIG_GLOBAL", GIT_NULL_CONFIG)
.env("GIT_CONFIG_SYSTEM", GIT_NULL_CONFIG)
.env("GIT_AUTHOR_NAME", GIT_TEST_NAME)
.env("GIT_AUTHOR_EMAIL", GIT_TEST_EMAIL)
.env("GIT_COMMITTER_NAME", GIT_TEST_NAME)
.env("GIT_COMMITTER_EMAIL", GIT_TEST_EMAIL);
cmd
}
pub fn git_capture(dir: &Path, args: &[&str]) -> String {
let output = git_command(dir)
.args(args)
.output()
.expect("git command failed");
assert!(
output.status.success(),
"git {args:?} failed\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8_lossy(&output.stdout).trim().to_owned()
}
pub fn git(dir: &Path, args: &[&str]) {
git_capture(dir, args);
}
pub fn commit_all(dir: &Path, message: &str) {
git(dir, &["add", "."]);
git(
dir,
&["-c", "commit.gpgsign=false", "commit", "-m", message],
);
}