use klasp::output::json;
use klasp_core::{
CheckResult, Finding, Severity, Verdict, VerdictPolicy, GATE_SCHEMA_VERSION,
KLASP_OUTPUT_SCHEMA,
};
fn make_check_result(name: &str, source: &str, verdict: Verdict) -> CheckResult {
CheckResult {
check_name: name.into(),
source_id: source.into(),
verdict,
raw_stdout: None,
raw_stderr: None,
}
}
fn load_golden(name: &str) -> String {
let path = format!(
"{}/tests/fixtures/output_json/{name}",
env!("CARGO_MANIFEST_DIR")
);
std::fs::read_to_string(&path)
.unwrap_or_else(|_| panic!("golden fixture missing: {path}"))
.replace("\r\n", "\n")
.replace("__GATE_SCHEMA_VERSION__", &GATE_SCHEMA_VERSION.to_string())
}
fn assert_matches_golden(actual: &str, golden_name: &str) {
let actual_norm = actual.replace("\r\n", "\n");
let expected = load_golden(golden_name);
if actual_norm != expected {
eprintln!("--- expected ({golden_name}) ---");
eprintln!("{expected}");
eprintln!("--- actual ---");
eprintln!("{actual_norm}");
let a_lines: Vec<&str> = expected.lines().collect();
let b_lines: Vec<&str> = actual_norm.lines().collect();
let max = a_lines.len().max(b_lines.len());
for i in 0..max {
let a = a_lines.get(i).copied().unwrap_or("<missing>");
let b = b_lines.get(i).copied().unwrap_or("<missing>");
if a != b {
eprintln!("line {}: expected {:?}", i + 1, a);
eprintln!("line {}: actual {:?}", i + 1, b);
}
}
panic!("JSON output does not match golden fixture: {golden_name}");
}
}
#[test]
fn format_json_pass_no_findings_matches_golden() {
let json = json::render(&Verdict::Pass, VerdictPolicy::AnyFail, &[]);
assert_matches_golden(&json, "gate-pass-empty.json");
}
#[test]
fn format_json_fail_with_findings_matches_golden() {
let results = vec![make_check_result(
"rustfmt",
"shell",
Verdict::Fail {
findings: vec![Finding {
rule: "fmt".into(),
message: "not formatted".into(),
file: Some("src/lib.rs".into()),
line: Some(10),
severity: Severity::Error,
}],
message: "1 check failed".into(),
},
)];
let json = json::render(
&Verdict::Fail {
findings: vec![],
message: "1 check failed".into(),
},
VerdictPolicy::AnyFail,
&results,
);
assert_matches_golden(&json, "gate-fail-with-findings.json");
}
#[test]
fn format_json_warn_mixed_matches_golden() {
let results = vec![
make_check_result("lint", "shell", Verdict::Pass),
make_check_result(
"security",
"shell",
Verdict::Warn {
findings: vec![Finding {
rule: "dep-outdated".into(),
message: "dependency is outdated".into(),
file: None,
line: None,
severity: Severity::Warn,
}],
message: None,
},
),
];
let json = json::render(
&Verdict::Warn {
findings: vec![],
message: None,
},
VerdictPolicy::AnyFail,
&results,
);
assert_matches_golden(&json, "gate-warn-mixed.json");
}
#[test]
fn format_json_includes_output_schema_version() {
let json = json::render(&Verdict::Pass, VerdictPolicy::AnyFail, &[]);
let v: serde_json::Value = serde_json::from_str(&json).expect("invalid JSON");
assert_eq!(
v["output_schema_version"],
serde_json::json!(KLASP_OUTPUT_SCHEMA),
"output_schema_version must equal KLASP_OUTPUT_SCHEMA"
);
assert_eq!(
v["output_schema_version"], 1,
"KLASP_OUTPUT_SCHEMA must be 1 in v0.3"
);
}
#[test]
fn format_json_includes_stats() {
let results = vec![
make_check_result("a", "shell", Verdict::Pass),
make_check_result("b", "shell", Verdict::Pass),
make_check_result(
"c",
"shell",
Verdict::Fail {
findings: vec![],
message: "fail".into(),
},
),
];
let merged = Verdict::Fail {
findings: vec![],
message: "fail".into(),
};
let json = json::render(&merged, VerdictPolicy::AnyFail, &results);
let v: serde_json::Value = serde_json::from_str(&json).expect("invalid JSON");
assert_eq!(v["stats"]["total_checks"], 3, "total_checks must be 3");
assert_eq!(v["stats"]["pass"], 2, "pass must be 2");
assert_eq!(v["stats"]["warn"], 0, "warn must be 0");
assert_eq!(v["stats"]["fail"], 1, "fail must be 1");
}
#[test]
fn format_json_field_order_stable() {
let json = json::render(&Verdict::Pass, VerdictPolicy::AnyFail, &[]);
assert_matches_golden(&json, "gate-pass-empty.json");
let lines: Vec<&str> = json.lines().collect();
let key_lines: Vec<&str> = lines
.iter()
.filter(|l| l.trim_start().starts_with('"'))
.copied()
.collect();
let keys: Vec<&str> = key_lines
.iter()
.filter_map(|l| {
let trimmed = l.trim();
if trimmed.starts_with('"') {
trimmed.split('"').nth(1)
} else {
None
}
})
.collect();
let top_level_keys: Vec<&str> = keys.iter().take(5).copied().collect();
assert_eq!(
top_level_keys,
vec![
"output_schema_version",
"gate_schema_version",
"verdict",
"checks",
"stats"
],
"top-level field order must match KLASP_OUTPUT_SCHEMA = 1 spec"
);
}