use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use camel_cli::commands::lint::production_engine;
type CodeSev = (String, String);
type EmittedMap = BTreeMap<String, BTreeSet<CodeSev>>;
fn workspace_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..")
.canonicalize()
.expect("workspace root canonicalizes")
}
fn collect(pattern: &str, out: &mut BTreeSet<PathBuf>) {
for entry in glob::glob(pattern).expect("glob pattern compiles") {
match entry {
Ok(p) => {
out.insert(p);
}
Err(e) => panic!("glob iteration error on `{pattern}`: {e}"),
}
}
}
fn discover_corpus() -> Vec<(String, PathBuf)> {
let root = workspace_root();
let mut found = BTreeSet::new();
for ext in ["yaml", "yml", "json"] {
let pat = root
.join("examples")
.join("**")
.join(format!("*.{ext}"))
.to_string_lossy()
.into_owned();
collect(&pat, &mut found);
let pat = root
.join("crates")
.join("**")
.join("tests")
.join("fixtures")
.join("**")
.join(format!("*.{ext}"))
.to_string_lossy()
.into_owned();
collect(&pat, &mut found);
}
let excluded = root.join("examples").join("validator").join("schemas");
found
.into_iter()
.filter(|p| !p.starts_with(&excluded))
.map(|p| {
let rel = p
.strip_prefix(&root)
.unwrap_or_else(|_| panic!("corpus path {p:?} not under workspace root {root:?}"))
.to_string_lossy()
.into_owned();
(rel, p)
})
.collect()
}
async fn run_corpus() -> EmittedMap {
let engine = production_engine()
.await
.expect("production engine builds for corpus gate");
let corpus = discover_corpus();
assert!(
!corpus.is_empty(),
"corpus glob found zero files — discovery is broken"
);
let mut emitted: EmittedMap = BTreeMap::new();
for (rel, full) in &corpus {
let source = std::fs::read_to_string(full)
.unwrap_or_else(|e| panic!("read corpus file {full:?}: {e}"));
for diag in engine.lint(&source) {
emitted
.entry(rel.clone())
.or_default()
.insert((diag.code.to_string(), diag.severity.to_string()));
}
}
emitted
}
fn load_baseline() -> EmittedMap {
let path =
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/lint-corpus-baseline.ron");
let text =
std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read baseline {path:?}: {e}"));
let list: Vec<(String, Vec<CodeSev>)> =
ron::from_str(&text).unwrap_or_else(|e| panic!("parse baseline RON: {e}"));
let mut map: EmittedMap = BTreeMap::new();
for (file, codes) in list {
for cs in codes {
map.entry(file.clone()).or_default().insert(cs);
}
}
map
}
fn compare(emitted: &EmittedMap, baseline: &EmittedMap) -> Vec<String> {
let mut failures = Vec::new();
for (file, codes) in emitted {
let base = baseline.get(file);
for cs in codes {
if !base.is_some_and(|b| b.contains(cs)) {
failures.push(format!(
"FALSE-POSITIVE: {file} emits {}({}) not in baseline",
cs.0, cs.1
));
}
}
}
for (file, codes) in baseline {
let got = emitted.get(file);
for cs in codes {
if !got.is_some_and(|g| g.contains(cs)) {
failures.push(format!(
"MISSING-REGRESSION: {file} baseline {}({}) not emitted",
cs.0, cs.1
));
}
}
}
failures
}
#[tokio::test]
async fn corpus_zero_false_positives() {
let emitted = run_corpus().await;
let baseline = load_baseline();
let failures = compare(&emitted, &baseline);
if !failures.is_empty() {
panic!(
"corpus gate failed ({} mismatch(es)):\n - {}\n\n\
If these are real defects, add them to \
tests/fixtures/lint-corpus-baseline.ron with a justification.\n\
If they are suspected false positives, report them — do NOT \
baseline.",
failures.len(),
failures.join("\n - ")
);
}
}
#[tokio::test]
async fn corpus_gate_detects_false_positive() {
let mut emitted = run_corpus().await;
let baseline = load_baseline();
let probe_file = emitted
.keys()
.next()
.cloned()
.expect("corpus non-empty for FP probe");
emitted
.entry(probe_file.clone())
.or_default()
.insert(("LINT-GATE-PROBE".to_string(), "error".to_string()));
let failures = compare(&emitted, &baseline);
assert!(
!failures.is_empty(),
"gate must FAIL when an unbasetined diagnostic is emitted"
);
let named = failures
.iter()
.any(|f| f.contains(&probe_file) && f.contains("LINT-GATE-PROBE"));
assert!(
named,
"gate failure must name the file + code; got:\n - {}",
failures.join("\n - ")
);
}