#[path = "common/mod.rs"]
mod common;
use common::{parse_json, run_fallow_raw};
use std::fs;
use std::process::Command;
use tempfile::TempDir;
fn create_audit_fixture(_suffix: &str) -> TempDir {
let tmp = TempDir::new().expect("failed to create temp dir");
let dir = tmp.path();
fs::create_dir_all(dir.join("src")).unwrap();
fs::write(
dir.join("package.json"),
r#"{"name": "audit-test", "main": "src/index.ts", "dependencies": {"unused-pkg": "1.0.0"}}"#,
)
.unwrap();
fs::write(
dir.join("src/index.ts"),
"import { used } from './utils';\nused();\n",
)
.unwrap();
fs::write(
dir.join("src/utils.ts"),
"export const used = () => 42;\nexport const unused = () => 0;\n",
)
.unwrap();
fs::write(
dir.join("src/orphan.ts"),
"export const orphaned = 'nobody';\n",
)
.unwrap();
let git = |args: &[&str]| {
Command::new("git")
.args(args)
.current_dir(dir)
.env_remove("GIT_DIR")
.env_remove("GIT_WORK_TREE")
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_SYSTEM", "/dev/null")
.env("GIT_AUTHOR_NAME", "test")
.env("GIT_AUTHOR_EMAIL", "test@test.com")
.env("GIT_COMMITTER_NAME", "test")
.env("GIT_COMMITTER_EMAIL", "test@test.com")
.output()
.expect("git command failed")
};
git(&["init", "-b", "main"]);
git(&["add", "."]);
git(&["-c", "commit.gpgsign=false", "commit", "-m", "initial"]);
tmp
}
#[test]
fn audit_json_has_verdict_and_schema() {
let dir = create_audit_fixture("verdict");
let output = run_fallow_raw(&[
"audit",
"--root",
dir.path().to_str().unwrap(),
"--base",
"HEAD",
"--format",
"json",
"--quiet",
]);
assert_eq!(
output.code, 0,
"audit with no changes should exit 0. stderr: {}",
output.stderr
);
let json = parse_json(&output);
assert_eq!(
json["verdict"].as_str(),
Some("pass"),
"no changes should give pass verdict"
);
assert_eq!(
json["command"].as_str(),
Some("audit"),
"command should be 'audit'"
);
assert!(
json.get("schema_version").is_some(),
"audit JSON should have schema_version"
);
}
#[test]
fn audit_pass_verdict_when_no_changes() {
let dir = create_audit_fixture("nochanges");
let output = run_fallow_raw(&[
"audit",
"--root",
dir.path().to_str().unwrap(),
"--base",
"HEAD",
"--format",
"json",
"--quiet",
]);
assert_eq!(output.code, 0, "no changes should give exit 0");
let json = parse_json(&output);
assert_eq!(
json["verdict"].as_str(),
Some("pass"),
"no changes should give pass verdict"
);
assert_eq!(
json["changed_files_count"].as_u64(),
Some(0),
"should report 0 changed files"
);
}
#[test]
fn audit_json_has_summary_with_changes() {
let dir = create_audit_fixture("summary");
fs::write(
dir.path().join("src/new.ts"),
"export const newThing = 'added';\n",
)
.unwrap();
Command::new("git")
.args(["add", "."])
.current_dir(dir.path())
.output()
.unwrap();
Command::new("git")
.args(["-c", "commit.gpgsign=false", "commit", "-m", "add new file"])
.current_dir(dir.path())
.env("GIT_AUTHOR_NAME", "test")
.env("GIT_AUTHOR_EMAIL", "test@test.com")
.env("GIT_COMMITTER_NAME", "test")
.env("GIT_COMMITTER_EMAIL", "test@test.com")
.output()
.unwrap();
let output = run_fallow_raw(&[
"audit",
"--root",
dir.path().to_str().unwrap(),
"--base",
"HEAD~1",
"--format",
"json",
"--quiet",
]);
assert!(
output.code == 0 || output.code == 1,
"audit should not crash, got exit {}. stderr: {}",
output.code,
output.stderr
);
let json = parse_json(&output);
assert!(
json.get("summary").is_some(),
"audit JSON should have summary"
);
let summary = &json["summary"];
assert!(
summary.get("dead_code_issues").is_some(),
"summary should have dead_code_issues"
);
}
fn create_audit_baseline_fixture() -> TempDir {
let tmp = TempDir::new().expect("failed to create temp dir");
let dir = tmp.path();
fs::create_dir_all(dir.join("src")).unwrap();
fs::write(
dir.join("package.json"),
r#"{"name": "audit-baseline-test", "main": "src/index.ts"}"#,
)
.unwrap();
fs::write(
dir.join("tsconfig.json"),
r#"{"compilerOptions":{"target":"ES2022","module":"ESNext","moduleResolution":"bundler"},"include":["src"]}"#,
)
.unwrap();
fs::write(
dir.join("src/legacy.ts"),
"export const used = 1;\n\
export const unusedA = 'a';\n\
export const unusedB = 'b';\n\
export const unusedC = 'c';\n\
export const unusedD = 'd';\n\
export const unusedE = 'e';\n",
)
.unwrap();
fs::write(
dir.join("src/index.ts"),
"import { used } from './legacy';\nconsole.log(used);\n",
)
.unwrap();
let git = |args: &[&str]| {
Command::new("git")
.args(args)
.current_dir(dir)
.env_remove("GIT_DIR")
.env_remove("GIT_WORK_TREE")
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_SYSTEM", "/dev/null")
.env("GIT_AUTHOR_NAME", "test")
.env("GIT_AUTHOR_EMAIL", "test@test.com")
.env("GIT_COMMITTER_NAME", "test")
.env("GIT_COMMITTER_EMAIL", "test@test.com")
.output()
.expect("git command failed")
};
git(&["init", "-b", "main"]);
git(&["add", "."]);
git(&["-c", "commit.gpgsign=false", "commit", "-m", "initial"]);
git(&["checkout", "-b", "feature"]);
let legacy = fs::read_to_string(dir.join("src/legacy.ts")).unwrap();
fs::write(dir.join("src/legacy.ts"), format!("{legacy}// touched\n")).unwrap();
git(&["add", "."]);
git(&["-c", "commit.gpgsign=false", "commit", "-m", "touch legacy"]);
tmp
}
#[test]
fn audit_without_baseline_reports_preexisting_issues() {
let tmp = create_audit_baseline_fixture();
let output = run_fallow_raw(&[
"audit",
"--root",
tmp.path().to_str().unwrap(),
"--base",
"main",
"--format",
"json",
"--quiet",
]);
assert_eq!(
output.code, 1,
"audit should fail when touched file has pre-existing issues. stderr: {}",
output.stderr
);
let json = parse_json(&output);
assert_eq!(json["verdict"].as_str(), Some("fail"));
let dead_code_issues = json["summary"]["dead_code_issues"]
.as_u64()
.expect("summary.dead_code_issues should be present");
assert!(
dead_code_issues >= 5,
"expected at least 5 pre-existing unused exports, got {dead_code_issues}"
);
}
#[test]
fn audit_with_dead_code_baseline_filters_preexisting_issues() {
let tmp = create_audit_baseline_fixture();
let dir = tmp.path();
let baseline_path = dir.join(".fallow-dead-code-baseline.json");
let git = |args: &[&str]| {
Command::new("git")
.args(args)
.current_dir(dir)
.env_remove("GIT_DIR")
.env_remove("GIT_WORK_TREE")
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_SYSTEM", "/dev/null")
.env("GIT_AUTHOR_NAME", "test")
.env("GIT_AUTHOR_EMAIL", "test@test.com")
.env("GIT_COMMITTER_NAME", "test")
.env("GIT_COMMITTER_EMAIL", "test@test.com")
.output()
.expect("git command failed")
};
git(&["checkout", "main"]);
let save = run_fallow_raw(&[
"dead-code",
"--root",
dir.to_str().unwrap(),
"--save-baseline",
baseline_path.to_str().unwrap(),
"--format",
"json",
"--quiet",
]);
assert!(
save.code == 0 || save.code == 1,
"save-baseline should not crash, got {}: {}",
save.code,
save.stderr
);
assert!(
baseline_path.exists(),
"baseline file should have been written"
);
git(&["checkout", "feature"]);
let output = run_fallow_raw(&[
"audit",
"--root",
dir.to_str().unwrap(),
"--base",
"main",
"--dead-code-baseline",
baseline_path.to_str().unwrap(),
"--format",
"json",
"--quiet",
]);
assert_eq!(
output.code, 0,
"audit with dead-code baseline should pass (no new issues). stdout: {}\nstderr: {}",
output.stdout, output.stderr
);
let json = parse_json(&output);
assert_eq!(
json["verdict"].as_str(),
Some("pass"),
"verdict should be pass when all pre-existing issues are baselined"
);
assert_eq!(
json["summary"]["dead_code_issues"].as_u64(),
Some(0),
"baseline should filter all pre-existing unused exports"
);
}
#[test]
fn audit_rejects_global_baseline_flag() {
let tmp = create_audit_baseline_fixture();
let output = run_fallow_raw(&[
"--baseline",
"anything.json",
"audit",
"--root",
tmp.path().to_str().unwrap(),
"--base",
"main",
"--format",
"json",
"--quiet",
]);
assert_eq!(
output.code, 2,
"global --baseline on audit should exit 2. stderr: {}",
output.stderr
);
let combined = format!("{}{}", output.stdout, output.stderr);
assert!(
combined.contains("--dead-code-baseline")
|| combined.contains("--health-baseline")
|| combined.contains("--dupes-baseline"),
"error should point users at per-analysis flags, got: {combined}"
);
}
#[test]
fn audit_rejects_global_save_baseline_flag() {
let tmp = create_audit_baseline_fixture();
let output = run_fallow_raw(&[
"--save-baseline",
"anywhere.json",
"audit",
"--root",
tmp.path().to_str().unwrap(),
"--base",
"main",
"--format",
"json",
"--quiet",
]);
assert_eq!(
output.code, 2,
"global --save-baseline on audit should exit 2. stderr: {}",
output.stderr
);
let combined = format!("{}{}", output.stdout, output.stderr);
assert!(
combined.contains("--dead-code-baseline")
|| combined.contains("--health-baseline")
|| combined.contains("--dupes-baseline"),
"error should point users at per-analysis flags, got: {combined}"
);
}
#[test]
fn audit_badge_format_exits_2() {
let dir = create_audit_fixture("badge");
let output = run_fallow_raw(&[
"audit",
"--root",
dir.path().to_str().unwrap(),
"--base",
"HEAD",
"--format",
"badge",
"--quiet",
]);
assert_eq!(
output.code, 2,
"audit with --format badge should exit 2 (unsupported)"
);
}
#[test]
fn audit_max_crap_flag_fails_when_threshold_crossed() {
let dir = create_audit_fixture("crap");
fs::write(
dir.path().join("src/branchy.ts"),
"export function branchy(n: number): number {\n\
if (n < 0) return -1;\n\
if (n === 0) return 0;\n\
if (n < 10) return 1;\n\
if (n < 100) return 2;\n\
if (n < 1000) return 3;\n\
if (n < 10000) return 4;\n\
return 5;\n\
}\n\
import { used } from './legacy';\nbranchy(used);\n",
)
.unwrap();
Command::new("git")
.args(["add", "."])
.current_dir(dir.path())
.output()
.unwrap();
Command::new("git")
.args(["-c", "commit.gpgsign=false", "commit", "-m", "add branchy"])
.current_dir(dir.path())
.env("GIT_AUTHOR_NAME", "test")
.env("GIT_AUTHOR_EMAIL", "test@test.com")
.env("GIT_COMMITTER_NAME", "test")
.env("GIT_COMMITTER_EMAIL", "test@test.com")
.output()
.unwrap();
let output = run_fallow_raw(&[
"audit",
"--root",
dir.path().to_str().unwrap(),
"--base",
"HEAD~1",
"--max-crap",
"1",
"--format",
"json",
"--quiet",
]);
assert_eq!(
output.code, 1,
"audit should fail when --max-crap is crossed. stderr: {}",
output.stderr
);
let json = parse_json(&output);
assert_eq!(
json["verdict"].as_str(),
Some("fail"),
"verdict should be fail when CRAP threshold is crossed"
);
}