stern4rust/rules/
tests_layout_rule.rs1use std::collections::BTreeSet;
6
7use crate::offence::Offence;
8use crate::registry_parser::RegistryParser;
9use crate::rule::Rule;
10use crate::source_file::SourceFile;
11
12pub struct TestsLayoutRule;
24
25impl TestsLayoutRule {
26 pub const ROOT: &'static str = "tests/";
27
28 pub fn new() -> Self {
29 Self
30 }
31
32 fn in_tests(files: &[SourceFile]) -> Vec<&SourceFile> {
33 files
34 .iter()
35 .filter(|file| file.relative_path().starts_with(Self::ROOT))
36 .collect()
37 }
38
39 fn missing_door(&self, present: &[&SourceFile]) -> Vec<Offence> {
40 if present
41 .iter()
42 .any(|file| file.relative_path() == "tests/all_tests.rs")
43 {
44 return Vec::new();
45 }
46 vec![
47 Offence::new(
48 "tests/all_tests.rs",
49 1,
50 self.name(),
51 "a tests folder is present but has no all_tests.rs, so nothing in it \
52 is compiled"
53 .to_string(),
54 "create tests/all_tests.rs with the header and one `pub mod` line per \
55 file in tests/"
56 .to_string(),
57 )
58 .with_subject("tests/all_tests.rs"),
59 ]
60 }
61
62 fn stray_doors(&self, present: &[&SourceFile]) -> Vec<Offence> {
65 present
66 .iter()
67 .filter(|file| {
68 file.relative_path().ends_with("/all_tests.rs")
69 && file.relative_path() != "tests/all_tests.rs"
70 })
71 .map(|file| {
72 Offence::new(
73 file.relative_path(),
74 1,
75 self.name(),
76 "only tests/all_tests.rs is a registry; this one is never \
77 reached"
78 .to_string(),
79 "rename it to mod.rs, or delete it and declare its contents from \
80 tests/all_tests.rs"
81 .to_string(),
82 )
83 .with_subject(file.relative_path())
84 })
85 .collect()
86 }
87
88 fn missing_mod_files(&self, present: &[&SourceFile]) -> Vec<Offence> {
89 let existing: BTreeSet<&str> = present.iter().map(|file| file.relative_path()).collect();
90 Self::subfolders(present)
91 .into_iter()
92 .map(|folder| format!("{folder}/mod.rs"))
93 .filter(|expected| !existing.contains(expected.as_str()))
94 .map(|expected| {
95 Offence::new(
96 &expected,
97 1,
98 self.name(),
99 "a tests subfolder has no mod.rs, so nothing in it is compiled".to_string(),
100 format!(
101 "create {expected} with the header and one `pub mod` line per \
102 file in that folder"
103 ),
104 )
105 .with_subject(&expected)
106 })
107 .collect()
108 }
109
110 fn subfolders(present: &[&SourceFile]) -> BTreeSet<String> {
114 let mut folders = BTreeSet::new();
115 for file in present {
116 let mut parts: Vec<&str> = file.relative_path().split('/').collect();
117 parts.pop();
118 for depth in 2..=parts.len() {
119 folders.insert(parts[..depth].join("/"));
120 }
121 }
122 folders
123 }
124
125 fn registry_contents(&self, present: &[&SourceFile]) -> Vec<Offence> {
126 present
127 .iter()
128 .filter(|file| Self::is_registry(file))
129 .flat_map(|file| self.declarations_only(file))
130 .collect()
131 }
132
133 fn is_registry(file: &SourceFile) -> bool {
134 let path = file.relative_path();
135 path == "tests/all_tests.rs" || path.ends_with("/mod.rs")
136 }
137
138 fn declarations_only(&self, file: &SourceFile) -> Vec<Offence> {
141 RegistryParser::strays(file)
142 .unwrap_or_default()
143 .into_iter()
144 .map(|stray| {
145 Offence::new(
146 file.relative_path(),
147 stray.line,
148 self.name(),
149 format!(
150 "{} does not belong in a registry, which holds the header \
151 and pub mod declarations only",
152 stray.label
153 ),
154 format!(
155 "move {} out of the registry into the file that needs it",
156 stray.label
157 ),
158 )
159 .with_subject(&stray.label)
160 })
161 .collect()
162 }
163}
164
165impl Default for TestsLayoutRule {
166 fn default() -> Self {
167 Self::new()
168 }
169}
170
171impl Rule for TestsLayoutRule {
172 fn name(&self) -> &'static str {
173 "tests-layout"
174 }
175
176 fn check_workspace(&self, files: &[SourceFile]) -> Vec<Offence> {
177 let present = Self::in_tests(files);
178 if present.is_empty() {
179 return Vec::new();
180 }
181 let mut offences = self.missing_door(&present);
182 offences.extend(self.stray_doors(&present));
183 offences.extend(self.missing_mod_files(&present));
184 offences.extend(self.registry_contents(&present));
185 offences
186 }
187}