use std::collections::BTreeSet;
use std::fs;
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::TempDir;
fn write_file(dir: &TempDir, rel: &str, content: &str) -> PathBuf {
let path = dir.path().join(rel);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).unwrap();
}
let mut file = std::fs::File::create(&path).unwrap();
file.write_all(content.as_bytes()).unwrap();
path
}
fn run(dir: &TempDir) -> StructuralReport {
scan(&StructuralRulesConfig::with_root(dir.path())).expect("scan must not fail")
}
#[test]
fn clean_tree_is_green() {
let dir = TempDir::new().unwrap();
write_file(&dir, "src/lib.rs", "pub fn a() {}\n");
write_file(&dir, "src/a.rs", "pub fn a() {}\n");
let report = run(&dir);
assert!(report.is_green(), "{:?}", report);
}
#[test]
fn mod_rs_is_detected() {
let dir = TempDir::new().unwrap();
write_file(&dir, "src/lib.rs", "pub mod foo;\n");
write_file(&dir, "src/foo/mod.rs", "pub fn a() {}\n");
let report = run(&dir);
let kinds: Vec<_> = report
.new_findings
.iter()
.map(|finding| finding.rule)
.collect();
assert!(kinds.contains(&StructuralRule::NoModRs), "{:?}", report);
}
#[test]
fn six_entries_in_src_dir_fails_max_5() {
let dir = TempDir::new().unwrap();
for letter in 'a'..='f' {
write_file(
&dir,
&format!("src/foo/item_{letter}.rs"),
"pub fn a() {}\n",
);
}
let report = run(&dir);
let dir_findings: Vec<_> = report
.new_findings
.iter()
.filter(|finding| finding.rule == StructuralRule::MaxFiveEntriesPerDir)
.collect();
assert_eq!(dir_findings.len(), 1, "{:?}", report);
}
#[test]
fn max_5_ignores_tree_gen_artifacts() {
let dir = TempDir::new().unwrap();
write_file(&dir, "src/a.rs", "pub fn a() {}\n");
write_file(&dir, "src/b.rs", "pub fn a() {}\n");
write_file(&dir, "src/c.rs", "pub fn a() {}\n");
write_file(&dir, "src/d.rs", "pub fn a() {}\n");
write_file(&dir, "src/e.rs", "pub fn a() {}\n");
write_file(&dir, "src/_tree.rs", "pub mod a;\n");
write_file(&dir, "src/_deprecated_bridge.rs", "\n");
let report = run(&dir);
assert!(
!report
.new_findings
.iter()
.any(|finding| finding.rule == StructuralRule::MaxFiveEntriesPerDir),
"tree-gen files must not count toward the 5-entry limit: {:?}",
report
);
}
#[test]
fn use_super_is_detected_and_use_crate_is_not() {
let dir = TempDir::new().unwrap();
write_file(
&dir,
"src/lib.rs",
"use super::parent;\nuse self::sibling;\nuse crate::elsewhere;\nfn f() {}\n",
);
let report = run(&dir);
let count = report
.new_findings
.iter()
.filter(|finding| finding.rule == StructuralRule::NoUseSuper)
.count();
assert_eq!(count, 2, "{:?}", report);
}
#[test]
fn use_super_in_comment_is_ignored() {
let dir = TempDir::new().unwrap();
write_file(
&dir,
"src/lib.rs",
"/// Historical example: `use super::parent;` is banned.\nfn f() {}\n",
);
let report = run(&dir);
assert!(report.is_green(), "{:?}", report);
}
#[test]
fn skip_dirs_default_excludes_target_and_docs() {
let dir = TempDir::new().unwrap();
write_file(
&dir,
"target/poison/src/mod.rs",
"pub fn a() {}\n",
);
write_file(&dir, "src/good.rs", "pub fn a() {}\n");
let report = run(&dir);
assert!(report.is_green(), "{:?}", report);
}
#[test]
fn waiver_suppresses_exactly_its_rule_on_its_path() {
let dir = TempDir::new().unwrap();
write_file(&dir, "src/a/mod.rs", "pub fn a() {}\n");
write_file(
&dir,
"src/b.rs",
"use super::x;\nfn f() {}\n",
);
let config = StructuralRulesConfig {
roots: vec![dir.path().to_path_buf()],
skip_dirs: default_skip_dirs(),
waivers: [
Waiver {
path: PathBuf::from("src/a/mod.rs"),
rule: StructuralRule::NoModRs,
},
Waiver {
path: PathBuf::from("src/b.rs"),
rule: StructuralRule::NoUseSuper,
},
]
.into_iter()
.collect(),
};
let report = scan(&config).unwrap();
assert!(
report.is_green(),
"all violations should be waived: {:?}",
report
);
}
#[test]
fn stale_waiver_is_a_regression() {
let dir = TempDir::new().unwrap();
write_file(&dir, "src/ok.rs", "pub fn a() {}\n");
let config = StructuralRulesConfig {
roots: vec![dir.path().to_path_buf()],
skip_dirs: default_skip_dirs(),
waivers: [Waiver {
path: PathBuf::from("src/gone.rs"),
rule: StructuralRule::NoModRs,
}]
.into_iter()
.collect(),
};
let report = scan(&config).unwrap();
assert!(
!report.is_green(),
"unused waivers should fail the gate: {:?}",
report
);
assert_eq!(report.unused_waivers.len(), 1);
}
#[test]
fn waiver_does_not_bleed_across_rules() {
let dir = TempDir::new().unwrap();
write_file(&dir, "src/foo/mod.rs", "use super::parent;\n");
let config = StructuralRulesConfig {
roots: vec![dir.path().to_path_buf()],
skip_dirs: default_skip_dirs(),
waivers: [Waiver {
path: PathBuf::from("src/foo/mod.rs"),
rule: StructuralRule::NoModRs,
}]
.into_iter()
.collect(),
};
let report = scan(&config).unwrap();
assert_eq!(
report
.new_findings
.iter()
.filter(|finding| finding.rule == StructuralRule::NoUseSuper)
.count(),
1,
"{:?}",
report
);
}
#[test]
fn violation_count_counts_all_findings_regardless_of_waivers() {
let dir = TempDir::new().unwrap();
write_file(&dir, "src/a/mod.rs", "use super::x;\n");
let config = StructuralRulesConfig {
roots: vec![dir.path().to_path_buf()],
skip_dirs: default_skip_dirs(),
waivers: [
Waiver {
path: PathBuf::from("src/a/mod.rs"),
rule: StructuralRule::NoModRs,
},
Waiver {
path: PathBuf::from("src/a/mod.rs"),
rule: StructuralRule::NoUseSuper,
},
]
.into_iter()
.collect(),
};
let report = scan(&config).unwrap();
assert_eq!(violation_count(&report), 2);
assert!(report.is_green());
}
#[test]
fn findings_are_deterministically_sorted() {
let dir = TempDir::new().unwrap();
write_file(&dir, "src/z/mod.rs", "\n");
write_file(&dir, "src/a/mod.rs", "\n");
write_file(&dir, "src/m/mod.rs", "\n");
let report = run(&dir);
let paths: Vec<_> = report
.all_findings
.iter()
.map(|finding| finding.path.to_string_lossy().to_string())
.collect();
let mut sorted = paths.clone();
sorted.sort();
assert_eq!(paths, sorted, "{:?}", paths);
}
#[test]
fn missing_root_returns_actionable_error() {
let config = StructuralRulesConfig::with_root("/nope/does/not/exist");
let error = scan(&config).unwrap_err();
assert!(
error.contains("structural rules gate root does not exist"),
"{error}"
);
assert!(error.contains("Fix:"), "{error}");
}
#[test]
fn rule_names_are_unique() {
let mut seen = std::collections::BTreeSet::new();
for rule in [
StructuralRule::NoModRs,
StructuralRule::MaxFiveEntriesPerDir,
StructuralRule::NoUseSuper,
] {
assert!(seen.insert(rule.name()), "duplicate name {}", rule.name());
}
}
#[test]
fn display_finding_includes_rule_name_and_fix() {
let finding = StructuralFinding {
path: PathBuf::from("src/a/mod.rs"),
rule: StructuralRule::NoModRs,
line: None,
detail: "mod.rs is forbidden".to_string(),
};
let rendered = format!("{finding}");
assert!(rendered.contains("src/a/mod.rs"), "{rendered}");
assert!(rendered.contains("no_mod_rs"), "{rendered}");
}
#[test]
fn dir_rule_ignores_non_src_directories() {
let dir = TempDir::new().unwrap();
for letter in 'a'..='f' {
write_file(&dir, &format!("tests/{letter}.rs"), "\n");
}
let report = run(&dir);
assert!(report.is_green(), "{:?}", report);
}
}