cargo-allow 0.1.11

Source-tree exception ledger and policy scanner for Rust repositories.
use super::*;
use crate::{OutputFormat, RootArgs};
use allow_core::CargoAllowError;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::atomic::{AtomicUsize, Ordering};

#[test]
fn cmd_diff_with_explicit_head_reports_missing_default_policy_config_with_exact_error() {
    let root = diff_fixture_dir();
    init_git_repo_without_policy(&root);

    let err = cmd_diff(&DiffArgs {
        root: RootArgs {
            root: Some(root.clone()),
        },
        config: None,
        kind: None,
        include_untracked: false,
        format: OutputFormat::Human,
        output: None,
        base: "HEAD~1".to_string(),
        head: Some("HEAD".to_string()),
        require_change_note: false,
        revisions_dir: std::path::PathBuf::from(".allow/revisions"),
        write_change_note_template: None,
    })
    .expect_err("diff without policy in compared revisions should fail");

    assert_eq!(
        err,
        CargoAllowError::new("no policy config found in compared revisions; pass --config")
    );

    fs::remove_dir_all(&root)
        .unwrap_or_else(|err| std::panic::panic_any(format!("remove fixture dir: {err}")));
}

#[test]
fn cmd_diff_with_explicit_head_rejects_missing_explicit_config_path_with_exact_error() {
    let root = diff_fixture_dir();
    init_git_repo_with_policy(&root);

    let err = cmd_diff(&DiffArgs {
        root: RootArgs {
            root: Some(root.clone()),
        },
        config: Some(PathBuf::from("missing-policy.toml")),
        kind: None,
        include_untracked: false,
        format: OutputFormat::Human,
        output: None,
        base: "HEAD~1".to_string(),
        head: Some("HEAD".to_string()),
        require_change_note: false,
        revisions_dir: std::path::PathBuf::from(".allow/revisions"),
        write_change_note_template: None,
    })
    .expect_err("diff with missing explicit --config in compared revisions should fail");

    assert_eq!(
        err,
        CargoAllowError::new("policy config missing-policy.toml not found in compared revisions")
    );

    fs::remove_dir_all(&root)
        .unwrap_or_else(|err| std::panic::panic_any(format!("remove fixture dir: {err}")));
}

static NEXT_DIFF_FIXTURE: AtomicUsize = AtomicUsize::new(0);

fn diff_fixture_dir() -> PathBuf {
    let id = NEXT_DIFF_FIXTURE.fetch_add(1, Ordering::Relaxed);
    let stamp = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|duration| duration.as_nanos())
        .unwrap_or(0);
    let dir = std::env::temp_dir().join(format!(
        "cargo-allow-cli-diff-{}-{stamp}-{id}",
        std::process::id()
    ));
    fs::create_dir_all(&dir)
        .unwrap_or_else(|err| std::panic::panic_any(format!("fixture dir: {err}")));
    dir
}

fn init_git_repo_without_policy(root: &Path) {
    fs::create_dir_all(root.join("src"))
        .unwrap_or_else(|err| std::panic::panic_any(format!("create src dir: {err}")));
    fs::write(root.join("src/lib.rs"), "fn base() {}\n")
        .unwrap_or_else(|err| std::panic::panic_any(format!("write base source: {err}")));
    git(root, &["init"]);
    git(
        root,
        &["config", "user.email", "cargo-allow@example.invalid"],
    );
    git(root, &["config", "user.name", "cargo-allow test"]);
    git(root, &["add", "."]);
    git(root, &["commit", "-m", "base"]);
    fs::write(root.join("src/lib.rs"), "fn head() {}\n")
        .unwrap_or_else(|err| std::panic::panic_any(format!("write head source: {err}")));
    git(root, &["add", "."]);
    git(root, &["commit", "-m", "head"]);
}

fn init_git_repo_with_policy(root: &Path) {
    fs::create_dir_all(root.join("policy"))
        .unwrap_or_else(|err| std::panic::panic_any(format!("create policy dir: {err}")));
    fs::create_dir_all(root.join("src"))
        .unwrap_or_else(|err| std::panic::panic_any(format!("create src dir: {err}")));
    fs::write(root.join("src/lib.rs"), "fn base() {}\n")
        .unwrap_or_else(|err| std::panic::panic_any(format!("write base source: {err}")));
    fs::write(
        root.join("policy/allow.toml"),
        r#"schema_version = "0.1"
policy = "cargo-allow"
owner = "core/policy"
status = "active"

[workspace]
root = "."
inventory = "git-tracked"
default_mode = "no-new"
ignored = ["policy/**", "target/**"]
generated = ["target/**", "vendor/**"]

[requirements]
owner_required = true
reason_required = true
classification_required = true
evidence_required = false
expires_or_review_after_required = true
allow_bare_allow_attributes = false
lint_policy_id_required = false
stale_entries_fail = false

[[allow]]
id = "allow-base"
kind = "non_rust_file"
family = "configuration"
path = "policy/allow.toml"
owner = "core"
classification = "fixture"
reason = "fixture policy file"
review_after = "2026-11-01"

[allow.selector]
ast_kind = "tracked_file"
"#,
    )
    .unwrap_or_else(|err| std::panic::panic_any(format!("write policy: {err}")));
    git(root, &["init"]);
    git(
        root,
        &["config", "user.email", "cargo-allow@example.invalid"],
    );
    git(root, &["config", "user.name", "cargo-allow test"]);
    git(root, &["add", "."]);
    git(root, &["commit", "-m", "base"]);
    fs::write(root.join("src/lib.rs"), "fn head() {}\n")
        .unwrap_or_else(|err| std::panic::panic_any(format!("write head source: {err}")));
    git(root, &["add", "."]);
    git(root, &["commit", "-m", "head"]);
}

fn git(root: &Path, args: &[&str]) {
    let output = Command::new("git")
        .arg("-C")
        .arg(root)
        .args(args)
        .output()
        .unwrap_or_else(|err| std::panic::panic_any(format!("git {args:?}: {err}")));
    if !output.status.success() {
        std::panic::panic_any(format!(
            "git {args:?} failed: stdout=`{}` stderr=`{}`",
            String::from_utf8_lossy(&output.stdout),
            String::from_utf8_lossy(&output.stderr)
        ));
    }
}