use super::artifacts::{
HEAD_SAMPLE_BYTES, TAIL_SAMPLE_BYTES, infer_stage_from_path, should_capture_full,
};
use super::*;
use anodizer_core::AllowListEntry;
#[test]
fn stage_id_from_token_round_trips_every_variant() {
for s in StageId::iter() {
assert_eq!(
StageId::from_token(s.as_str()),
Some(s),
"from_token is not the inverse of as_str for {s:?}"
);
}
assert_eq!(StageId::from_token("not-a-stage"), None);
assert_eq!(StageId::from_token(""), None);
}
#[test]
fn require_c_toolchain_errors_on_msvc_target_without_clang_cl() {
let targets = vec!["x86_64-pc-windows-msvc".to_string()];
let err = require_c_toolchain(&targets, false, |_| false).unwrap_err();
assert!(
err.to_string().contains("clang-cl"),
"error must name clang-cl as the missing tool: {err}"
);
}
#[test]
fn require_c_toolchain_ok_on_msvc_target_with_clang_cl_present() {
let targets = vec!["aarch64-pc-windows-msvc".to_string()];
require_c_toolchain(&targets, false, |_| true).unwrap();
}
#[test]
fn require_c_toolchain_ok_on_non_msvc_target_without_clang_cl() {
let targets = vec!["x86_64-unknown-linux-gnu".to_string()];
require_c_toolchain(&targets, false, |_| false).unwrap();
}
#[test]
fn require_c_toolchain_errors_on_empty_targets_when_host_is_msvc() {
require_c_toolchain(&[], true, |_| false).unwrap_err();
}
#[test]
fn require_c_toolchain_ok_on_empty_targets_when_host_is_not_msvc() {
require_c_toolchain(&[], false, |_| false).unwrap();
}
#[test]
fn require_c_toolchain_ok_on_mixed_targets_with_clang_cl_present() {
let targets = vec![
"x86_64-pc-windows-msvc".to_string(),
"x86_64-unknown-linux-gnu".to_string(),
];
require_c_toolchain(&targets, false, |_| true).unwrap();
}
fn empty_harness() -> Harness {
Harness {
repo_root: PathBuf::from("/tmp/unused"),
commit: "deadbeef".into(),
stages: vec![StageId::Archive, StageId::Checksum],
explicit_stages: vec![StageId::Archive, StageId::Checksum],
require_tools: false,
runs: 2,
sde: 1_715_000_000,
allowlist: AllowList::default(),
report_path: PathBuf::from("/tmp/unused/report.json"),
inject_drift: None,
targets: None,
preserve_dist: None,
version_hint: String::new(),
child_snapshot: true,
docker_backend_hint: None,
docker_configs: Vec::new(),
docker_declared: false,
crate_name: None,
verbosity: Verbosity::Normal,
config_tools: BTreeMap::new(),
disk_abs_floor_bytes: anodizer_core::disk::DEFAULT_ABS_FLOOR_BYTES,
disk_safety_factor: anodizer_core::disk::DEFAULT_SAFETY_FACTOR,
}
}
fn harness_with_allow(patterns: &[&str]) -> Harness {
let mut h = empty_harness();
h.allowlist = AllowList {
compile_time: patterns
.iter()
.map(|p| AllowListEntry {
artifact: (*p).to_string(),
reason: format!("test: {p} is intrinsically non-deterministic"),
})
.collect(),
runtime: Vec::new(),
};
h
}
fn run_with_files(
h: &Harness,
runs: Vec<Vec<(&str, &[u8])>>,
) -> Vec<BTreeMap<String, ArtifactInfo>> {
let _ = h;
runs.into_iter()
.map(|files| {
let mut map = BTreeMap::new();
for (name, bytes) in files {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(bytes);
let digest = format!("sha256:{:x}", hasher.finalize());
let head_len = bytes.len().min(HEAD_SAMPLE_BYTES);
let tail_sample = if bytes.len() > HEAD_SAMPLE_BYTES + TAIL_SAMPLE_BYTES {
bytes[bytes.len() - TAIL_SAMPLE_BYTES..].to_vec()
} else {
Vec::new()
};
let full = if should_capture_full(name, bytes) {
Some(bytes.to_vec())
} else {
None
};
map.insert(
name.into(),
ArtifactInfo {
hash: digest,
size_bytes: bytes.len() as u64,
relative_path: format!("dist/{}", name),
stage: infer_stage_from_path(name),
head_sample: bytes[..head_len].to_vec(),
tail_sample,
full,
},
);
}
map
})
.collect()
}
#[test]
fn harness_report_shape_serializes_correctly() {
let h = empty_harness();
let runs = run_with_files(
&h,
vec![
vec![("anodizer_0.2.1.tar.gz", b"hello")],
vec![("anodizer_0.2.1.tar.gz", b"hello")],
],
);
let report = h.build_report(runs);
assert_eq!(report.schema_version, 1);
assert_eq!(report.runs, 2);
assert_eq!(report.commit, "deadbeef");
assert_eq!(report.stages_under_test, vec!["archive", "checksum"]);
assert_eq!(report.drift_count, 0);
assert_eq!(report.artifacts.len(), 1);
assert!(report.artifacts[0].deterministic);
assert!(report.artifacts[0].hash.is_some());
assert!(report.artifacts[0].hashes.is_empty());
let s = serde_json::to_string_pretty(&report).unwrap();
let back: DeterminismReport = serde_json::from_str(&s).unwrap();
assert_eq!(back, report);
}
#[test]
fn harness_diffs_artifacts_by_sha256() {
let h = empty_harness();
let runs = run_with_files(
&h,
vec![
vec![("stable.tar.gz", b"hello"), ("drifting.tar.gz", b"first")],
vec![("stable.tar.gz", b"hello"), ("drifting.tar.gz", b"second")],
],
);
let report = h.build_report(runs);
assert_eq!(report.drift_count, 1);
assert_eq!(report.drift.len(), 1);
assert_eq!(report.drift[0].artifact, "drifting.tar.gz");
assert_eq!(report.drift[0].hashes.len(), 2);
assert_ne!(report.drift[0].hashes[0], report.drift[0].hashes[1]);
let summary = report.drift[0]
.differing_bytes_summary
.as_deref()
.expect("drift row must populate differing_bytes_summary");
assert!(
summary.contains("offset 0x0"),
"summary should point at byte 0 for diverging single-byte prefixes. got={summary}"
);
let stable = report
.artifacts
.iter()
.find(|a| a.name == "stable.tar.gz")
.unwrap();
let drifting = report
.artifacts
.iter()
.find(|a| a.name == "drifting.tar.gz")
.unwrap();
assert!(stable.deterministic);
assert!(!drifting.deterministic);
assert!(drifting.hash.is_none());
assert_eq!(drifting.hashes.len(), 2);
}
#[test]
fn aggregate_excused_when_only_allowlisted_member_drifts() {
let h = harness_with_allow(&["*.deb"]);
let run0 = b"hashA bar.tar.gz\ndeb000 app_1.0_amd64.deb\n" as &[u8];
let run1 = b"hashA bar.tar.gz\ndeb111 app_1.0_amd64.deb\n" as &[u8];
let runs = run_with_files(
&h,
vec![
vec![("app_checksums.txt", run0), ("bar.tar.gz", b"stable")],
vec![("app_checksums.txt", run1), ("bar.tar.gz", b"stable")],
],
);
let report = h.build_report(runs);
assert_eq!(
report.drift_count, 0,
"aggregate drift caused only by an allow-listed member must not fail"
);
let agg = report
.artifacts
.iter()
.find(|a| a.name == "app_checksums.txt")
.expect("checksums row present");
assert!(!agg.deterministic);
assert!(
agg.nondeterministic_reason
.as_deref()
.is_some_and(|r| r.contains("app_1.0_amd64.deb")),
"excuse must name the differing allow-listed member: {:?}",
agg.nondeterministic_reason
);
}
#[test]
fn aggregate_fails_when_gated_member_drifts_even_if_member_row_suppressed() {
let h = harness_with_allow(&["*.deb"]);
let run0 = b"t000 bar.tar.gz\ndeb000 app_1.0_amd64.deb\n" as &[u8];
let run1 = b"t111 bar.tar.gz\ndeb111 app_1.0_amd64.deb\n" as &[u8];
let runs = run_with_files(
&h,
vec![
vec![("app_checksums.txt", run0)],
vec![("app_checksums.txt", run1)],
],
);
let report = h.build_report(runs);
assert_eq!(
report.drift_count, 1,
"a gated member drifting inside the aggregate must surface as drift"
);
assert!(
report
.drift
.iter()
.any(|d| d.artifact.contains("bar.tar.gz")),
"the offending gated member must be named: {:?}",
report.drift
);
assert!(
!report.drift.iter().any(|d| d.artifact.contains(".deb")),
"allow-listed member must not be reported as a regression"
);
}
#[test]
fn aggregate_judges_additions_and_removals_by_member_status() {
let h = harness_with_allow(&["*.deb"]);
let add0 = b"a000 a.tar.gz\ndeb000 x_1.0_amd64.deb\n" as &[u8];
let add1 = b"a000 a.tar.gz\ndeb000 x_1.0_amd64.deb\nb000 b.tar.gz\n" as &[u8];
let runs = run_with_files(
&h,
vec![
vec![("c_checksums.txt", add0)],
vec![("c_checksums.txt", add1)],
],
);
let report = h.build_report(runs);
assert_eq!(report.drift_count, 1, "added gated member must fail");
assert!(report.drift.iter().any(|d| d.artifact.contains("b.tar.gz")));
let rem0 = b"a000 a.tar.gz\ndeb000 x_1.0_amd64.deb\n" as &[u8];
let rem1 = b"a000 a.tar.gz\n" as &[u8];
let runs = run_with_files(
&h,
vec![
vec![("c_checksums.txt", rem0)],
vec![("c_checksums.txt", rem1)],
],
);
let report = h.build_report(runs);
assert_eq!(
report.drift_count, 0,
"removing an allow-listed member must be excused"
);
}
#[test]
fn aggregate_fails_closed_on_unparseable_or_structural_drift() {
let h = harness_with_allow(&["*.deb"]);
let s0 = b"a a.tar.gz\nd x_1.0_amd64.deb\n" as &[u8];
let s1 = b"d x_1.0_amd64.deb\na a.tar.gz\nz z\n" as &[u8];
let runs = run_with_files(
&h,
vec![vec![("s_checksums.txt", s0)], vec![("s_checksums.txt", s1)]],
);
let report = h.build_report(runs);
assert_eq!(
report.drift_count, 1,
"an aggregate whose drift cannot be attributed must fail closed"
);
}
#[test]
fn artifacts_manifest_transitive_rule() {
let h = harness_with_allow(&["*.deb"]);
let gated0 = br#"[
{"kind":"archive","path":"./dist/a.tar.gz","name":"a.tar.gz","metadata":{"sha256":"aaaa"}},
{"kind":"linux_package","path":"./dist/a_1.0_amd64.deb","name":"a_1.0_amd64.deb","metadata":{"sha256":"dddd"}}
]"# as &[u8];
let gated1 = br#"[
{"kind":"archive","path":"./dist/a.tar.gz","name":"a.tar.gz","metadata":{"sha256":"bbbb"}},
{"kind":"linux_package","path":"./dist/a_1.0_amd64.deb","name":"a_1.0_amd64.deb","metadata":{"sha256":"dddd"}}
]"#;
let runs = run_with_files(
&h,
vec![
vec![("artifacts.json", gated0)],
vec![("artifacts.json", gated1)],
],
);
let report = h.build_report(runs);
assert_eq!(
report.drift_count, 1,
"gated archive digest drift must fail"
);
assert!(report.drift.iter().any(|d| d.artifact.contains("a.tar.gz")));
let deb1 = br#"[
{"kind":"archive","path":"./dist/a.tar.gz","name":"a.tar.gz","metadata":{"sha256":"aaaa"}},
{"kind":"linux_package","path":"./dist/a_1.0_amd64.deb","name":"a_1.0_amd64.deb","metadata":{"sha256":"eeee"}}
]"#;
let runs = run_with_files(
&h,
vec![
vec![("artifacts.json", gated0)],
vec![("artifacts.json", deb1)],
],
);
let report = h.build_report(runs);
assert_eq!(
report.drift_count, 0,
"deb-only digest drift must be excused"
);
}
#[test]
fn marker_named_combined_file_obeys_transitive_rule() {
let h = harness_with_allow(&["*.deb"]);
let manifest = br#"[
{"kind":"archive","path":"./dist/bar.tar.gz","name":"bar.tar.gz","metadata":{"sha256":"barbar"}},
{"kind":"linux_package","path":"./dist/app_1.0_amd64.deb","name":"app_1.0_amd64.deb","metadata":{"sha256":"debdeb"}},
{"kind":"checksum","path":"./dist/SHA512SUMS","name":"SHA512SUMS","metadata":{"combined":"true"}}
]"# as &[u8];
assert!(anodizer_core::determinism::aggregate_kind_for("SHA512SUMS").is_none());
let sums0 = b"barbar bar.tar.gz\ndeb000 app_1.0_amd64.deb\n" as &[u8];
let sums1 = b"barbar bar.tar.gz\ndeb111 app_1.0_amd64.deb\n" as &[u8];
let runs = run_with_files(
&h,
vec![
vec![
("artifacts.json", manifest),
("SHA512SUMS", sums0),
("bar.tar.gz", b"stable"),
],
vec![
("artifacts.json", manifest),
("SHA512SUMS", sums1),
("bar.tar.gz", b"stable"),
],
],
);
let report = h.build_report(runs);
assert_eq!(
report.drift_count, 0,
"marker-named combined file drift from an allow-listed member must be excused: {:?}",
report.drift
);
let agg = report
.artifacts
.iter()
.find(|a| a.name == "SHA512SUMS")
.expect("SHA512SUMS classified as an aggregate row");
assert!(!agg.deterministic);
assert!(
agg.nondeterministic_reason
.as_deref()
.is_some_and(|r| r.contains("app_1.0_amd64.deb")),
"excuse must name the drifting allow-listed member: {:?}",
agg.nondeterministic_reason
);
let g0 = b"bar000 bar.tar.gz\ndeb000 app_1.0_amd64.deb\n" as &[u8];
let g1 = b"bar111 bar.tar.gz\ndeb000 app_1.0_amd64.deb\n" as &[u8];
let runs = run_with_files(
&h,
vec![
vec![
("artifacts.json", manifest),
("SHA512SUMS", g0),
("bar.tar.gz", b"stable"),
],
vec![
("artifacts.json", manifest),
("SHA512SUMS", g1),
("bar.tar.gz", b"stable"),
],
],
);
let report = h.build_report(runs);
assert_eq!(
report.drift_count, 1,
"a gated member drifting inside the renamed combined file must fail"
);
assert!(
report
.drift
.iter()
.any(|d| d.artifact.contains("bar.tar.gz")),
"the gated member must be named: {:?}",
report.drift
);
}
#[test]
fn artifacts_manifest_recut_excuses_only_nondeterministic_members() {
let mut h = empty_harness();
h.allowlist = AllowList {
compile_time: vec![
AllowListEntry {
artifact: "*.cdx.json".into(),
reason: "CycloneDX SBOM carries a random serial UUID".into(),
},
AllowListEntry {
artifact: "*.deb".into(),
reason: "GPG-signed nfpm deb".into(),
},
],
runtime: vec![
AllowListEntry {
artifact: "*.cosign.bundle".into(),
reason: "cosign ECDSA random nonce".into(),
},
AllowListEntry {
artifact: "*.sig".into(),
reason: "cosign detached signature".into(),
},
],
};
let manifest = |arch: &str, sbom: &str, bundle: &str, sig: &str| {
format!(
r#"[
{{"kind":"archive","path":"./dist/app.tar.gz","name":"app.tar.gz","metadata":{{"sha256":"{arch}"}}}},
{{"kind":"sbom","path":"./dist/app.cdx.json","name":"app.cdx.json","metadata":{{"sha256":"{sbom}"}}}},
{{"kind":"signature","path":"./dist/app.tar.gz.cosign.bundle","name":"app.tar.gz.cosign.bundle","metadata":{{"sha256":"{bundle}"}}}},
{{"kind":"signature","path":"./dist/app.tar.gz.sig","name":"app.tar.gz.sig","metadata":{{"sha256":"{sig}"}}}},
{{"kind":"checksum","path":"./dist/checksums.txt","name":"checksums.txt","metadata":{{"combined":"true"}}}},
{{"kind":"metadata","path":"./dist/metadata.json","name":"metadata.json","metadata":{{}}}},
{{"kind":"uploadable_file","path":"./dist/install.sh","name":"install.sh","metadata":{{"sha256":"inst"}}}}
]"#
)
.into_bytes()
};
let checksums = b"arch app.tar.gz\n" as &[u8];
let files = |m: &[u8], sbom: &[u8], bundle: &[u8], sig: &[u8]| -> Vec<(String, Vec<u8>)> {
vec![
("artifacts.json".into(), m.to_vec()),
("app.tar.gz".into(), b"archive-stable".to_vec()),
("app.cdx.json".into(), sbom.to_vec()),
("app.tar.gz.cosign.bundle".into(), bundle.to_vec()),
("app.tar.gz.sig".into(), sig.to_vec()),
("checksums.txt".into(), checksums.to_vec()),
("metadata.json".into(), b"meta-stable".to_vec()),
("install.sh".into(), b"#!/bin/sh\n".to_vec()),
]
};
fn borrow(v: &[(String, Vec<u8>)]) -> Vec<(&str, &[u8])> {
v.iter().map(|(n, b)| (n.as_str(), b.as_slice())).collect()
}
let r0 = files(
&manifest("ARCH", "SB0", "BUN0", "SIG0"),
b"sbom-0",
b"bundle-0",
b"sig-0",
);
let r1 = files(
&manifest("ARCH", "SB1", "BUN1", "SIG1"),
b"sbom-1",
b"bundle-1",
b"sig-1",
);
let runs = run_with_files(&h, vec![borrow(&r0), borrow(&r1)]);
let report = h.build_report(runs);
assert_eq!(
report.drift_count, 0,
"a recut that only moves SBOM/cosign-bundle/sig must be fully excused: {:?}",
report.drift
);
let g0 = files(
&manifest("ARCH0", "SB0", "BUN0", "SIG0"),
b"sbom-0",
b"bundle-0",
b"sig-0",
);
let mut g1 = files(
&manifest("ARCH1", "SB0", "BUN0", "SIG0"),
b"sbom-0",
b"bundle-0",
b"sig-0",
);
for entry in &mut g1 {
if entry.0 == "app.tar.gz" {
entry.1 = b"archive-DRIFTED".to_vec();
}
}
let runs = run_with_files(&h, vec![borrow(&g0), borrow(&g1)]);
let report = h.build_report(runs);
assert!(
report.drift_count >= 1,
"a gated archive regression must surface"
);
assert!(
report
.drift
.iter()
.any(|d| d.artifact.contains("app.tar.gz")),
"the gated archive must be named in drift: {:?}",
report.drift
);
}
#[test]
fn nested_aggregate_recursion_judges_inner_members() {
let h = harness_with_allow(&["*.cdx.json"]);
let manifest = |size: u32| {
format!(
r#"[
{{"kind":"archive","path":"./dist/app.tar.gz","name":"app.tar.gz","metadata":{{"sha256":"AAAA"}}}},
{{"kind":"sbom","path":"./dist/app.cdx.json","name":"app.cdx.json","metadata":{{"sha256":"SB{size}"}}}},
{{"kind":"checksum","path":"./dist/checksums.txt","name":"checksums.txt","metadata":{{"combined":"true"}},"size":{size}}}
]"#
)
.into_bytes()
};
let ck0 = b"arch app.tar.gz\nsbom0 app.cdx.json\n" as &[u8];
let ck1 = b"arch app.tar.gz\nsbom1 app.cdx.json\n" as &[u8];
let m0 = manifest(100);
let m1 = manifest(101);
let runs = run_with_files(
&h,
vec![
vec![("artifacts.json", m0.as_slice()), ("checksums.txt", ck0)],
vec![("artifacts.json", m1.as_slice()), ("checksums.txt", ck1)],
],
);
let report = h.build_report(runs);
assert_eq!(
report.drift_count, 0,
"nested aggregate excused when inner drift is allow-listed: {:?}",
report.drift
);
let bad0 = b"arch0 app.tar.gz\nsbom0 app.cdx.json\n" as &[u8];
let bad1 = b"arch1 app.tar.gz\nsbom0 app.cdx.json\n" as &[u8];
let runs = run_with_files(
&h,
vec![
vec![("artifacts.json", m0.as_slice()), ("checksums.txt", bad0)],
vec![("artifacts.json", m1.as_slice()), ("checksums.txt", bad1)],
],
);
let report = h.build_report(runs);
assert!(
report.drift_count >= 1,
"nested aggregate must fail when an inner gated member drifts"
);
assert!(
report
.drift
.iter()
.any(|d| d.artifact.contains("checksums.txt") || d.artifact.contains("app.tar.gz")),
"the failing nested member chain must be named: {:?}",
report.drift
);
}
#[test]
fn unclassified_gates_on_byte_drift_not_on_classification() {
let h = harness_with_allow(&["*.flatpak"]);
let manifest = br#"[
{"kind":"archive","path":"./dist/foo.tar.gz","name":"foo.tar.gz","metadata":{"sha256":"aaaa"}},
{"kind":"uploadable_file","path":"./dist/install.sh","name":"install.sh","metadata":{"sha256":"bbbb"}},
{"kind":"metadata","path":"./dist/metadata.json","name":"metadata.json","metadata":{}}
]"# as &[u8];
let checksums = b"aaaa foo.tar.gz\n" as &[u8];
let files: Vec<(&str, &[u8])> = vec![
("foo.tar.gz", b"archive-bytes"), ("foo.tar.gz.sig", b"sig-bytes"), ("app_1.0_amd64.deb", b"deb-bytes"), ("app_1.0_amd64.flatpak", b"fp-bytes"), ("anodizer.1", b"man-bytes"), ("install.sh", b"sh-bytes"), ("metadata.json", b"meta-bytes"), ("app_checksums.txt", checksums), ("artifacts.json", manifest), ];
let runs = run_with_files(&h, vec![files.clone(), files]);
let report = h.build_report(runs);
assert_eq!(
report.drift_count, 0,
"a fully-classified, byte-stable run must not fail. drift={:?}",
report.drift
);
assert!(
!report.drift.iter().any(|d| d
.differing_bytes_summary
.as_deref()
.is_some_and(|s| s.contains("unclassified"))),
"no file should be unclassified: {:?}",
report.drift
);
let stable: Vec<(&str, &[u8])> = vec![("mystery.xyz", b"same")];
let report = h.build_report(run_with_files(&h, vec![stable.clone(), stable]));
assert_eq!(
report.drift_count, 0,
"a byte-stable unclassified file must NOT fail: {:?}",
report.drift
);
let drifting: Vec<Vec<(&str, &[u8])>> =
vec![vec![("mystery.xyz", b"one")], vec![("mystery.xyz", b"two")]];
let report = h.build_report(run_with_files(&h, drifting));
assert_eq!(
report.drift_count, 1,
"a drifting unclassified file must hard-fail: {:?}",
report.drift
);
assert!(
report.drift[0]
.differing_bytes_summary
.as_deref()
.is_some_and(|s| s.contains("unclassified")),
"the drift reason must flag the unclassified file: {:?}",
report.drift
);
}
#[test]
fn harness_excludes_allowlisted_artifacts_from_drift() {
let mut h = empty_harness();
h.allowlist.compile_time.push(AllowListEntry {
artifact: "*.flatpak".into(),
reason: "flatpak build-bundle OSTree commit metadata not byte-stable".into(),
});
let runs = run_with_files(
&h,
vec![
vec![("anodizer_0.2.1_linux_amd64.flatpak", b"flatpak-bytes-A")],
vec![("anodizer_0.2.1_linux_amd64.flatpak", b"flatpak-bytes-B")],
],
);
let report = h.build_report(runs);
assert_eq!(
report.drift_count, 0,
"allowlisted artifact must not bump drift_count"
);
let row = &report.artifacts[0];
assert_eq!(row.name, "anodizer_0.2.1_linux_amd64.flatpak");
assert!(!row.deterministic);
assert_eq!(
row.nondeterministic_reason.as_deref(),
Some("flatpak build-bundle OSTree commit metadata not byte-stable")
);
assert_eq!(row.hashes.len(), 2);
}
#[test]
fn harness_treats_missing_artifact_in_one_run_as_drift() {
let h = empty_harness();
let runs = run_with_files(&h, vec![vec![("only-in-run-1.tar.gz", b"present")], vec![]]);
let report = h.build_report(runs);
assert_eq!(report.drift_count, 1);
assert_eq!(report.drift[0].artifact, "only-in-run-1.tar.gz");
assert!(report.drift[0].hashes.iter().any(|h| h == "<missing>"));
}
#[test]
fn matches_artifact_pattern_handles_glob_and_exact() {
assert!(matches_artifact_pattern("*.crate", "foo.crate"));
assert!(!matches_artifact_pattern("*.crate", "foo.tar.gz"));
assert!(matches_artifact_pattern("exact.bin", "exact.bin"));
assert!(!matches_artifact_pattern("exact.bin", "other.bin"));
}
#[test]
fn stage_id_round_trips_to_string() {
assert_eq!(StageId::Build.as_str(), "build");
assert_eq!(StageId::Archive.as_str(), "archive");
assert_eq!(StageId::Sbom.as_str(), "sbom");
assert_eq!(StageId::Sign.as_str(), "sign");
assert_eq!(StageId::Checksum.as_str(), "checksum");
}
#[test]
fn harness_extra_skip_with_default_stages_includes_nfpm() {
let stages = vec![
StageId::Build,
StageId::Archive,
StageId::Sbom,
StageId::Sign,
StageId::Checksum,
];
let extra = compute_extra_skip(&stages);
for name in [
"nfpm",
"nsis",
"msi",
"dmg",
"pkg",
"snapcraft",
"source",
"flatpak",
"appbundle",
"srpm",
"upx",
"makeself",
] {
assert!(
extra.iter().any(|s| s == name),
"compute_extra_skip(default-stages) missing `{name}`: {extra:?}"
);
}
assert!(
!extra.iter().any(|s| s == "notarize"),
"notarize must not appear in the complement set: {extra:?}"
);
}
#[test]
fn harness_extra_skip_omits_preserve_set() {
let stages = vec![StageId::Build, StageId::Archive];
let extra = compute_extra_skip(&stages);
for name in PRESERVE_SET {
assert!(
!extra.iter().any(|s| s == name),
"compute_extra_skip emitted PRESERVE_SET stage `{name}`: {extra:?}"
);
}
}
#[test]
fn harness_extra_skip_includes_changelog() {
let stages = vec![StageId::Build, StageId::Archive];
let extra = compute_extra_skip(&stages);
assert!(
extra.iter().any(|s| s == "changelog"),
"compute_extra_skip missing `changelog`: {extra:?}"
);
}
#[test]
fn harness_extra_skip_omits_requested_stages() {
let stages = vec![StageId::Build, StageId::Archive, StageId::Sign];
let extra = compute_extra_skip(&stages);
for name in ["build", "archive", "sign"] {
assert!(
!extra.iter().any(|s| s == name),
"compute_extra_skip dropped requested stage `{name}`: {extra:?}"
);
}
}
#[test]
fn harness_extra_skip_retains_build_for_binary_consuming_subset() {
for stages in [
vec![StageId::Appimage, StageId::Flatpak],
vec![StageId::Flatpak],
vec![StageId::Nfpm],
vec![StageId::Archive],
] {
let extra = compute_extra_skip(&stages);
assert!(
!extra.iter().any(|s| s == "build"),
"compute_extra_skip skipped `build` for binary-consuming subset {stages:?}: {extra:?}"
);
}
}
#[test]
fn harness_extra_skip_skips_build_for_source_only_subset() {
for stages in [
vec![StageId::Source],
vec![StageId::CargoPackage],
vec![StageId::Srpm],
] {
let extra = compute_extra_skip(&stages);
assert!(
extra.iter().any(|s| s == "build"),
"compute_extra_skip kept `build` for source-only subset {stages:?}: {extra:?}"
);
}
}
#[test]
fn harness_extra_skip_excludes_side_effect_stages() {
use anodizer_core::determinism_runner::SIDE_EFFECT_STAGES;
let stages = vec![StageId::Build];
let extra = compute_extra_skip(&stages);
for &name in SIDE_EFFECT_STAGES.iter() {
assert!(
!extra.iter().any(|s| s == name),
"compute_extra_skip double-listed side-effect stage `{name}`: {extra:?}"
);
}
}
#[test]
fn report_drift_count_matches_drift_array_len() {
let h = empty_harness();
let runs = run_with_files(
&h,
vec![
vec![("a.tar.gz", b"x"), ("b.tar.gz", b"y"), ("c.tar.gz", b"z")],
vec![
("a.tar.gz", b"x"),
("b.tar.gz", b"y-different"),
("c.tar.gz", b"z-different"),
],
],
);
let report = h.build_report(runs);
assert_eq!(report.drift.len() as u32, report.drift_count);
assert_eq!(report.drift_count, 2);
}
#[test]
fn docker_stage_no_config_is_ok_even_when_explicit() {
let tmp = tempfile::TempDir::new().unwrap();
std::fs::write(tmp.path().join("Dockerfile"), "FROM scratch\n").unwrap();
let h = empty_harness();
assert!(h.docker_configs.is_empty());
let env = HashMap::new();
assert!(
h.run_docker_stage(tmp.path(), &env, true).is_ok(),
"no dockers_v2 config must be a harmless no-op regardless of intent"
);
}
#[test]
fn docker_stage_declared_but_all_skipped_warns_not_errors_even_when_explicit() {
let tmp = tempfile::TempDir::new().unwrap();
let mut h = empty_harness();
h.docker_declared = true; h.docker_configs = Vec::new(); let env = HashMap::new();
assert!(
h.run_docker_stage(tmp.path(), &env, true).is_ok(),
"declared-but-all-skipped docker must warn-and-skip, not error, even under an \
explicit request (production parity)"
);
assert!(
h.run_docker_stage(tmp.path(), &env, false).is_ok(),
"declared-but-all-skipped docker must warn-and-skip on a host-default run too"
);
}
#[test]
fn docker_stage_podman_explicit_request_is_hard_error() {
let tmp = tempfile::TempDir::new().unwrap();
std::fs::write(tmp.path().join("Dockerfile"), "FROM scratch\n").unwrap();
let mut h = empty_harness();
h.docker_backend_hint = Some("podman".into());
h.docker_configs = vec![ResolvedDockerConfig {
dockerfile: "Dockerfile".into(),
extra_files: Vec::new(),
build_args: Vec::new(),
}];
let env = HashMap::new();
let err = h
.run_docker_stage(tmp.path(), &env, true)
.expect_err("explicit docker request under podman must fail the run, not skip");
let msg = err.to_string();
assert!(
msg.contains("podman") && msg.contains("Refusing"),
"error must explain the false-coverage refusal: {msg}"
);
}
#[test]
fn installer_explicit_request_missing_tool_is_hard_error() {
let mut h = empty_harness();
h.stages = vec![StageId::Build, StageId::Nsis];
h.explicit_stages = h.stages.clone();
let err = h
.gate_installer_stages(&h.stages.clone(), |_tool| false)
.expect_err("explicit installer request with a missing tool must fail the run, not skip");
let msg = err.to_string();
assert!(
msg.contains("nsis") && msg.contains("makensis"),
"error must name the missing stage and its tool: {msg}"
);
assert!(
msg.contains("--stages"),
"error must tell the operator how to opt out: {msg}"
);
}
#[test]
fn installer_non_explicit_missing_tool_warns_and_drops() {
let h = empty_harness();
let effective = vec![StageId::Build, StageId::Nsis];
let available = h
.gate_installer_stages(&effective, |_tool| false)
.expect("non-explicit missing tool must warn-and-drop, not error");
assert_eq!(
available,
vec![StageId::Build],
"missing-tool installer must be dropped; non-installer stages pass through"
);
}
#[test]
fn require_tools_hard_fails_host_default_missing_tool() {
let mut h = empty_harness();
h.explicit_stages = Vec::new();
h.require_tools = true;
let effective = vec![StageId::Build, StageId::Nsis];
let err = h
.gate_installer_stages(&effective, |_tool| false)
.expect_err("--require-tools must hard-fail a host-default missing tool");
let msg = err.to_string();
assert!(
msg.contains("nsis") && msg.contains("makensis"),
"error must name the missing host-default stage and its tool: {msg}"
);
}
#[test]
fn require_tools_keeps_host_default_when_tool_present() {
let mut h = empty_harness();
h.explicit_stages = Vec::new();
h.require_tools = true;
let effective = vec![StageId::Build, StageId::Nsis];
let available = h
.gate_installer_stages(&effective, |_tool| true)
.expect("present tool must pass even under --require-tools");
assert_eq!(available, vec![StageId::Build, StageId::Nsis]);
}
#[test]
fn require_tools_hard_fails_host_default_missing_upx() {
let mut h = empty_harness();
h.explicit_stages = Vec::new();
h.require_tools = true;
h.config_tools.insert(StageId::Upx, vec!["upx".to_string()]);
let effective = vec![StageId::Build, StageId::Upx];
let err = h
.gate_installer_stages(&effective, |_tool| false)
.expect_err("--require-tools must hard-fail a host-default missing upx");
let msg = err.to_string();
assert!(
msg.contains("upx"),
"error must name the missing upx stage and its tool: {msg}"
);
}
#[test]
fn require_tools_keeps_host_default_upx_when_tool_present() {
let mut h = empty_harness();
h.explicit_stages = Vec::new();
h.require_tools = true;
h.config_tools.insert(StageId::Upx, vec!["upx".to_string()]);
let effective = vec![StageId::Build, StageId::Upx];
let available = h
.gate_installer_stages(&effective, |_tool| true)
.expect("present upx must pass even under --require-tools");
assert_eq!(available, vec![StageId::Build, StageId::Upx]);
}
#[test]
fn dev_mode_warn_skips_host_default_upx_when_tool_absent() {
let mut h = empty_harness();
h.explicit_stages = Vec::new();
h.require_tools = false;
h.config_tools.insert(StageId::Upx, vec!["upx".to_string()]);
let effective = vec![StageId::Build, StageId::Upx];
let available = h
.gate_installer_stages(&effective, |_tool| false)
.expect("dev-mode host-default missing upx must warn-and-drop, not error");
assert_eq!(
available,
vec![StageId::Build],
"missing-tool upx must be dropped in dev mode; non-gated stages pass through"
);
}
#[test]
fn msi_v3_gate_passes_when_candle_and_light_present() {
let mut h = empty_harness();
h.stages = vec![StageId::Build, StageId::Msi];
h.config_tools.insert(
StageId::Msi,
vec!["candle".to_string(), "light".to_string()],
);
let available = h
.gate_installer_stages(&h.stages.clone(), |tool| matches!(tool, "candle" | "light"))
.expect("v3 msi with candle+light present must pass the gate");
assert_eq!(
available,
vec![StageId::Build, StageId::Msi],
"msi must stay in the effective set when its resolved tools are present"
);
}
#[test]
fn msi_v3_gate_hard_fails_when_a_resolved_tool_absent() {
let mut h = empty_harness();
h.stages = vec![StageId::Build, StageId::Msi];
h.explicit_stages = h.stages.clone();
h.config_tools.insert(
StageId::Msi,
vec!["candle".to_string(), "light".to_string()],
);
let err = h
.gate_installer_stages(&h.stages.clone(), |tool| tool == "candle")
.expect_err("v3 msi missing `light` must hard-fail the run");
let msg = err.to_string();
assert!(
msg.contains("msi") && msg.contains("light"),
"error must name msi and the first missing tool: {msg}"
);
}
#[test]
fn docker_context_staging_lays_out_os_arch_bin_and_dockerfile() {
let tmp = tempfile::TempDir::new().unwrap();
let worktree = tmp.path();
for triple in ["x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu"] {
let release = worktree
.join(".det-tmp")
.join("target")
.join(triple)
.join("release");
std::fs::create_dir_all(&release).unwrap();
std::fs::write(release.join("anodizer"), b"fake-binary").unwrap();
}
let host_release = worktree.join(".det-tmp").join("target").join("release");
std::fs::create_dir_all(&host_release).unwrap();
std::fs::write(host_release.join("anodizer"), b"host-byproduct").unwrap();
std::fs::write(worktree.join("Dockerfile"), "FROM scratch\nCOPY x x\n").unwrap();
let cfg = ResolvedDockerConfig {
dockerfile: "Dockerfile".into(),
extra_files: Vec::new(),
build_args: Vec::new(),
};
let context_dir = worktree.join(".det-tmp").join("docker-context");
let log = StageLogger::new("test", Verbosity::Quiet);
let staged = stage_docker_context(worktree, &context_dir, &cfg, &log).unwrap();
assert_eq!(staged, 2, "only per-triple binaries should be staged");
assert!(
context_dir
.join("linux")
.join("amd64")
.join("anodizer")
.is_file(),
"amd64 binary must land at <context>/linux/amd64/anodizer"
);
assert!(
context_dir
.join("linux")
.join("arm64")
.join("anodizer")
.is_file(),
"arm64 binary must land at <context>/linux/arm64/anodizer"
);
assert!(
context_dir.join("Dockerfile").is_file(),
"configured dockerfile must be copied to the staging root"
);
std::fs::write(context_dir.join("stale.txt"), b"old").unwrap();
let staged2 = stage_docker_context(worktree, &context_dir, &cfg, &log).unwrap();
assert_eq!(staged2, 2);
assert!(
!context_dir.join("stale.txt").exists(),
"re-run must wipe the prior staging dir so no bytes carry over"
);
}
#[test]
fn docker_context_staging_thin_dockerfile_and_extra_files() {
let tmp = tempfile::TempDir::new().unwrap();
let worktree = tmp.path();
let release = worktree
.join(".det-tmp")
.join("target")
.join("x86_64-unknown-linux-gnu")
.join("release");
std::fs::create_dir_all(&release).unwrap();
std::fs::write(release.join("cfgd"), b"fake-binary").unwrap();
std::fs::write(
worktree.join("Dockerfile"),
"FROM rust\nCOPY Cargo.lock .\n",
)
.unwrap();
std::fs::write(
worktree.join("Dockerfile.agent.release"),
"FROM debian\nARG TARGETOS=linux\nARG TARGETARCH\n\
COPY ${TARGETOS}/${TARGETARCH}/cfgd /usr/local/bin/cfgd\n\
COPY entrypoint.sh /entrypoint.sh\n",
)
.unwrap();
std::fs::write(worktree.join("entrypoint.sh"), b"#!/bin/sh\n").unwrap();
let cfg = ResolvedDockerConfig {
dockerfile: "Dockerfile.agent.release".into(),
extra_files: vec!["entrypoint.sh".into()],
build_args: vec![("VERSION".into(), "1.2.3".into())],
};
let context_dir = worktree.join(".det-tmp").join("docker-context-0");
let log = StageLogger::new("test", Verbosity::Quiet);
let staged = stage_docker_context(worktree, &context_dir, &cfg, &log).unwrap();
assert_eq!(staged, 1, "the single per-triple binary must be staged");
assert!(
context_dir
.join("linux")
.join("amd64")
.join("cfgd")
.is_file(),
"binary must land at <context>/linux/amd64/cfgd"
);
let staged_dockerfile = std::fs::read_to_string(context_dir.join("Dockerfile")).unwrap();
assert!(
staged_dockerfile.contains("COPY ${TARGETOS}/${TARGETARCH}/cfgd"),
"the configured thin dockerfile must be staged, not the fat repo-root one: \
{staged_dockerfile}"
);
assert!(
!staged_dockerfile.contains("COPY Cargo.lock"),
"the fat repo-root Dockerfile must never be staged: {staged_dockerfile}"
);
assert!(
context_dir.join("entrypoint.sh").is_file(),
"extra_files must be staged into the context"
);
}
#[test]
fn docker_stage_podman_auto_included_warns_and_skips() {
let tmp = tempfile::TempDir::new().unwrap();
std::fs::write(tmp.path().join("Dockerfile"), "FROM scratch\n").unwrap();
let mut h = empty_harness();
h.docker_backend_hint = Some("podman".into());
h.docker_configs = vec![ResolvedDockerConfig {
dockerfile: "Dockerfile".into(),
extra_files: Vec::new(),
build_args: Vec::new(),
}];
let env = HashMap::new();
assert!(
h.run_docker_stage(tmp.path(), &env, false).is_ok(),
"auto-included docker under podman must warn-and-skip, not error"
);
}
#[test]
fn headroom_guard_aborts_below_floor_with_actionable_message() {
const GIB: u64 = 1024 * 1024 * 1024;
let mut h = empty_harness();
h.disk_abs_floor_bytes = 45 * GIB;
let log = StageLogger::new("test", Verbosity::Quiet);
let vol = std::path::Path::new("/Volumes/scratch");
let err = h
.guard_run_headroom(&log, 0, vol, Some(30 * GIB), None)
.expect_err("below-floor free space must abort the run");
let msg = err.to_string();
assert!(msg.contains("determinism run 1"), "1-based run: {msg}");
assert!(
msg.contains(&format!("{}", 45 * GIB)),
"exact required: {msg}"
);
assert!(
msg.contains(&format!("{}", 30 * GIB)),
"exact available: {msg}"
);
assert!(msg.contains("/Volumes/scratch"), "volume: {msg}");
assert!(msg.contains("reclaim-disk"), "remedy hint: {msg}");
assert!(
msg.contains("absolute floor"),
"run-0 must state the floor basis, not a peak guarantee: {msg}"
);
}
#[test]
fn headroom_guard_proceeds_with_ample_space_and_gates_on_measured_peak() {
const GIB: u64 = 1024 * 1024 * 1024;
let mut h = empty_harness();
h.disk_abs_floor_bytes = 45 * GIB;
h.disk_safety_factor = 1.3;
let log = StageLogger::new("test", Verbosity::Quiet);
let vol = std::path::Path::new("/scratch");
assert!(
h.guard_run_headroom(&log, 0, vol, Some(60 * GIB), None)
.is_ok(),
"ample free space must proceed"
);
let prior_peak = Some(70 * GIB);
assert!(
h.guard_run_headroom(&log, 1, vol, Some(71 * GIB), prior_peak)
.is_err(),
"71 GiB free under a 91 GiB peak-projected requirement must abort"
);
assert!(
h.guard_run_headroom(&log, 1, vol, Some(95 * GIB), prior_peak)
.is_ok(),
"95 GiB free clears the 91 GiB peak-projected requirement"
);
}
#[test]
fn headroom_guard_unknown_free_space_is_noop() {
let h = empty_harness();
let log = StageLogger::new("test", Verbosity::Quiet);
let vol = std::path::Path::new("/scratch");
assert!(
h.guard_run_headroom(&log, 1, vol, None, None).is_ok(),
"unknown free space must proceed (no manufactured abort)"
);
}