use std::collections::BTreeSet;
use crate::finding::registry_parser::RegistryParser;
use crate::finding::registry_policy::RegistryPolicy;
use crate::reporting::offence::Offence;
use crate::rule::Rule;
use crate::source_file::SourceFile;
pub struct TestsLayoutRule;
impl TestsLayoutRule {
pub const ROOT: &'static str = "tests/";
pub fn new() -> Self {
Self
}
fn in_tests(files: &[SourceFile]) -> Vec<&SourceFile> {
files
.iter()
.filter(|file| file.relative_path().starts_with(Self::ROOT))
.collect()
}
fn missing_door(&self, present: &[&SourceFile]) -> Vec<Offence> {
if present
.iter()
.any(|file| file.relative_path() == "tests/all_tests.rs")
{
return Vec::new();
}
vec![
Offence::new(
"tests/all_tests.rs",
1,
self.name(),
"a tests folder is present but has no all_tests.rs, so nothing in it \
is compiled"
.to_string(),
"create tests/all_tests.rs with the header and one `pub mod` line per \
file in tests/"
.to_string(),
)
.with_subject("tests/all_tests.rs"),
]
}
fn stray_doors(&self, present: &[&SourceFile]) -> Vec<Offence> {
present
.iter()
.filter(|file| {
file.relative_path().ends_with("/all_tests.rs")
&& file.relative_path() != "tests/all_tests.rs"
})
.map(|file| {
Offence::new(
file.relative_path(),
1,
self.name(),
"only tests/all_tests.rs is a registry; this one is never \
reached"
.to_string(),
"rename it to mod.rs, or delete it and declare its contents from \
tests/all_tests.rs"
.to_string(),
)
.with_subject(file.relative_path())
})
.collect()
}
fn missing_mod_files(&self, present: &[&SourceFile]) -> Vec<Offence> {
let existing: BTreeSet<&str> = present.iter().map(|file| file.relative_path()).collect();
Self::subfolders(present)
.into_iter()
.map(|folder| format!("{folder}/mod.rs"))
.filter(|expected| !existing.contains(expected.as_str()))
.map(|expected| {
Offence::new(
&expected,
1,
self.name(),
"a tests subfolder has no mod.rs, so nothing in it is compiled".to_string(),
format!(
"create {expected} with the header and one `pub mod` line per \
file in that folder"
),
)
.with_subject(&expected)
})
.collect()
}
fn subfolders(present: &[&SourceFile]) -> BTreeSet<String> {
let mut folders = BTreeSet::new();
for file in present {
let mut parts: Vec<&str> = file.relative_path().split('/').collect();
parts.pop();
for depth in 2..=parts.len() {
folders.insert(parts[..depth].join("/"));
}
}
folders
}
fn registry_contents(&self, present: &[&SourceFile]) -> Vec<Offence> {
present
.iter()
.filter(|file| Self::is_registry(file))
.flat_map(|file| self.declarations_only(file))
.collect()
}
fn is_registry(file: &SourceFile) -> bool {
let path = file.relative_path();
path == "tests/all_tests.rs" || path.ends_with("/mod.rs")
}
fn declarations_only(&self, file: &SourceFile) -> Vec<Offence> {
RegistryParser::strays(file, RegistryPolicy::tests())
.unwrap_or_default()
.into_iter()
.map(|stray| {
Offence::new(
file.relative_path(),
stray.line,
self.name(),
format!(
"{} does not belong in a registry, which holds the header \
and pub mod declarations only",
stray.label
),
format!(
"move {} out of the registry into the file that needs it",
stray.label
),
)
.with_subject(&stray.label)
})
.collect()
}
}
impl Default for TestsLayoutRule {
fn default() -> Self {
Self::new()
}
}
impl Rule for TestsLayoutRule {
fn name(&self) -> &'static str {
"tests-layout"
}
fn check_workspace(&self, files: &[SourceFile]) -> Vec<Offence> {
let present = Self::in_tests(files);
if present.is_empty() {
return Vec::new();
}
let mut offences = self.missing_door(&present);
offences.extend(self.stray_doors(&present));
offences.extend(self.missing_mod_files(&present));
offences.extend(self.registry_contents(&present));
offences
}
}