use anodizer_core::DeterminismReport;
use std::fs;
use std::process::Command;
use tempfile::TempDir;
#[test]
fn check_determinism_help_lists_every_flag() {
let output = Command::new(env!("CARGO_BIN_EXE_anodizer"))
.args(["check", "determinism", "--help"])
.output()
.expect("invoking anodizer check determinism --help");
assert!(
output.status.success(),
"--help exited non-zero: stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
for flag in &[
"--runs",
"--stages",
"--report",
"--snapshot",
"--no-snapshot",
"--preserve-dist",
] {
assert!(
stdout.contains(flag),
"--help missing flag {}; full output: {}",
flag,
stdout
);
}
}
#[test]
fn check_determinism_errors_cleanly_outside_git_repo() {
let tmp = TempDir::new().unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_anodizer"))
.args(["check", "determinism", "--runs", "2"])
.current_dir(tmp.path())
.output()
.expect("invoking anodizer check determinism");
assert!(
!output.status.success(),
"expected non-zero exit outside a git repo; stdout={} stderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn check_determinism_respects_report_flag_in_error_path() {
let tmp = TempDir::new().unwrap();
let report = tmp.path().join("custom-report.json");
let output = Command::new(env!("CARGO_BIN_EXE_anodizer"))
.args(["check", "determinism", "--runs", "2", "--report"])
.arg(&report)
.current_dir(tmp.path())
.output()
.expect("invoking anodizer check determinism");
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stderr.contains("panicked"),
"binary panicked instead of erroring cleanly: {}",
stderr
);
}
#[test]
fn inject_drift_rejected_without_test_harness_env() {
let tmp = TempDir::new().unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_anodizer"))
.args([
"check",
"determinism",
"--runs",
"1",
"--inject-drift",
"archive",
])
.current_dir(tmp.path())
.env_remove("ANODIZE_TEST_HARNESS")
.output()
.expect("invoking anodizer check determinism --inject-drift");
assert!(
!output.status.success(),
"expected non-zero exit when --inject-drift is set without ANODIZE_TEST_HARNESS=1"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("--inject-drift") && stderr.contains("ANODIZE_TEST_HARNESS"),
"expected error citing both --inject-drift and ANODIZE_TEST_HARNESS; got: {}",
stderr
);
}
#[test]
fn inject_drift_hidden_from_help() {
let output = Command::new(env!("CARGO_BIN_EXE_anodizer"))
.args(["check", "determinism", "--help"])
.output()
.expect("invoking anodizer check determinism --help");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
!stdout.contains("--inject-drift"),
"--inject-drift must not appear in --help output: {}",
stdout
);
}
mod common;
use common::{bootstrap_minimal_cargo_repo, host_triple, run_git, tool_on_path};
#[test]
fn inject_drift_archive_reports_drift_on_minimal_workspace() {
assert!(
tool_on_path("cargo") && tool_on_path("git"),
"inject_drift_archive_reports_drift_on_minimal_workspace requires cargo and git on PATH"
);
let tmp = TempDir::new().unwrap();
let repo = tmp.path();
bootstrap_minimal_cargo_repo(repo, "anodizer-det-fixture");
let report_path = repo.join("det.json");
let output = Command::new(env!("CARGO_BIN_EXE_anodizer"))
.args([
"check",
"determinism",
"--runs",
"2",
"--stages",
"build,archive",
"--inject-drift",
"archive",
"--report",
])
.arg(&report_path)
.current_dir(repo)
.env("ANODIZE_TEST_HARNESS", "1")
.output()
.expect("invoking anodizer check determinism");
assert!(
!output.status.success(),
"expected non-zero exit on drift; stdout={} stderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
assert!(
report_path.exists(),
"report file missing at {}; stderr was: {}",
report_path.display(),
String::from_utf8_lossy(&output.stderr)
);
let json = fs::read_to_string(&report_path).unwrap();
let report: DeterminismReport =
serde_json::from_str(&json).unwrap_or_else(|e| panic!("parsing report JSON: {e}\n{json}"));
assert_eq!(
report.schema_version,
anodizer_core::determinism_report::CURRENT_SCHEMA_VERSION,
"the written report carries the current schema version"
);
assert_eq!(report.runs, 2, "harness ran exactly --runs=2 times");
assert!(
report.drift_count > 0,
"expected drift_count > 0 after --inject-drift=archive; report: {:?}\nstderr: {}",
report,
String::from_utf8_lossy(&output.stderr)
);
assert!(
!report.drift.is_empty(),
"drift list non-empty alongside drift_count > 0"
);
assert!(
report
.drift
.iter()
.any(|d| d.artifact.ends_with(".tar.gz") || d.artifact.ends_with(".zip")),
"at least one drift row should be an archive artifact; got: {:?}",
report.drift.iter().map(|d| &d.artifact).collect::<Vec<_>>()
);
}
#[cfg(target_os = "linux")]
#[test]
fn harness_skips_env_preflight_and_prints_header_and_config_warnings_once() {
assert!(
tool_on_path("cargo") && tool_on_path("git") && tool_on_path("nfpm"),
"harness_skips_env_preflight_and_prints_header_and_config_warnings_once requires \
cargo, git, and nfpm on PATH (the fixture drives the nfpm stage, which the \
determinism gate hard-fails on when its tool is absent)"
);
let tmp = TempDir::new().unwrap();
let repo = tmp.path();
bootstrap_minimal_cargo_repo(repo, "anodizer-preflight-fixture");
let host = host_triple();
let yaml = format!(
r#"crates:
- name: anodizer-preflight-fixture
path: .
tag_template: "v{{{{ Version }}}}"
builds:
- id: anodizer-preflight-fixture
binary: anodizer-preflight-fixture
targets:
- {host}
nfpms:
- id: default
formats:
- apk
maintainer: "Test <test@test.com>"
description: "preflight fixture"
apk:
signature:
key_file: "{{{{ .Env.APK_PRIVATE_KEY_PATH }}}}"
publish:
chocolatey:
required: true
winget:
required: true
"#,
);
fs::write(repo.join(".anodizer.yaml"), yaml).unwrap();
run_git(repo, &["add", "-A"]);
run_git(repo, &["commit", "-q", "-m", "preflight fixture config"]);
run_git(repo, &["tag", "v0.1.0"]);
let report_path = repo.join("det.json");
let output = Command::new(env!("CARGO_BIN_EXE_anodizer"))
.args([
"check",
"determinism",
"--runs",
"2",
"--stages",
"build,archive,nfpm",
"--report",
])
.arg(&report_path)
.current_dir(repo)
.env_remove("APK_PRIVATE_KEY_PATH")
.env("NO_COLOR", "1")
.output()
.expect("invoking anodizer check determinism");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"harness must not preflight-fail in a credential-less env; \
stdout={} stderr={stderr}",
String::from_utf8_lossy(&output.stdout),
);
assert!(
!stderr.contains("preflight:") && !stderr.contains("preflight check(s) failed"),
"no env-preflight failure may surface from replica children: {stderr}"
);
assert!(
!stderr.contains("no release tags at HEAD"),
"children must select the fixture crate from the tag, not no-op: {stderr}"
);
let builds = stderr
.matches("built anodizer-preflight-fixture/anodizer-preflight-fixture for ")
.count();
assert_eq!(
builds, 2,
"expected one build-result line per run (runs=2), got {builds}:\n{stderr}"
);
let header_count = stderr.matches("Checking determinism").count();
assert_eq!(
header_count, 1,
"`Checking determinism` header must print exactly once, got {header_count}:\n{stderr}"
);
for publisher in ["chocolatey", "winget"] {
let needle = format!("publisher '{publisher}' submits to an external moderation queue");
let count = stderr.matches(needle.as_str()).count();
assert_eq!(
count, 0,
"moderation-queue advisory for {publisher} must be hidden at the \
default log level, got {count}:\n{stderr}"
);
}
}
#[test]
fn check_determinism_warns_on_crate_universe_name_collision() {
assert!(
tool_on_path("git"),
"check_determinism_warns_on_crate_universe_name_collision requires git on PATH"
);
let tmp = TempDir::new().unwrap();
let repo = tmp.path();
bootstrap_minimal_cargo_repo(repo, "det-collide-fixture");
let host = host_triple();
let yaml = format!(
r#"crates:
- name: det-collide-fixture
path: .
tag_template: "v{{{{ Version }}}}"
builds:
- id: det-collide-fixture
binary: det-collide-fixture
builder: prebuilt
prebuilt:
path: "out/det-collide-fixture"
targets:
- {host}
workspaces:
- name: ws
crates:
- name: det-collide-fixture
path: elsewhere
tag_template: "v{{{{ Version }}}}"
"#,
);
fs::write(repo.join(".anodizer.yaml"), yaml).unwrap();
run_git(repo, &["add", "-A"]);
run_git(repo, &["commit", "-q", "-m", "collision fixture config"]);
let output = Command::new(env!("CARGO_BIN_EXE_anodizer"))
.args(["check", "determinism", "--runs", "2"])
.current_dir(repo)
.env("NO_COLOR", "1")
.output()
.expect("invoking anodizer check determinism");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"all-prebuilt short-circuit must exit 0; stdout={} stderr={stderr}",
String::from_utf8_lossy(&output.stdout),
);
assert!(
stderr.contains("name collision with different paths"),
"expected the crate-universe collision warning on stderr: {stderr}"
);
assert!(
stderr.contains("workspace 'ws' crate 'det-collide-fixture'"),
"warning must name the workspace and the colliding crate: {stderr}"
);
assert!(
stderr.contains("no buildable targets"),
"expected the all-prebuilt short-circuit note: {stderr}"
);
}
#[test]
fn quiet_flag_silences_harness_run_bullets_and_children() {
assert!(
tool_on_path("cargo") && tool_on_path("git"),
"quiet_flag_silences_harness_run_bullets_and_children requires cargo and git on PATH"
);
let tmp = TempDir::new().unwrap();
let repo = tmp.path();
bootstrap_minimal_cargo_repo(repo, "anodizer-quiet-fixture");
let report_path = repo.join("det.json");
let output = Command::new(env!("CARGO_BIN_EXE_anodizer"))
.args([
"-q",
"check",
"determinism",
"--runs",
"2",
"--stages",
"build",
"--report",
])
.arg(&report_path)
.current_dir(repo)
.env("NO_COLOR", "1")
.output()
.expect("invoking anodizer -q check determinism");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"quiet run must still succeed; stdout={} stderr={stderr}",
String::from_utf8_lossy(&output.stdout),
);
assert!(
report_path.exists(),
"quiet run must still write the report"
);
for needle in [
"Checking determinism",
"run 1 of 2",
"Building binaries",
"running cargo",
"wrote determinism report",
] {
assert!(
!stderr.contains(needle),
"-q must silence `{needle}`; stderr:\n{stderr}"
);
}
}