stern4rust/rules/testing/
test_file_structure_rule.rs1use crate::finding::model::import_path::ImportPath;
6use crate::finding::model::section::Section;
7use crate::finding::model::test_file_item::TestFileItem;
8use crate::finding::parsing::test_file_parser::TestFileParser;
9use crate::reporting::offence::Offence;
10use crate::rule::Rule;
11use crate::source_file::SourceFile;
12
13pub struct TestFileStructureRule;
23
24impl TestFileStructureRule {
25 pub fn new() -> Self {
26 Self
27 }
28
29 fn applies_to(file: &SourceFile) -> bool {
31 file.relative_path().starts_with("tests/") && !Self::is_registry(file)
32 }
33
34 fn is_registry(file: &SourceFile) -> bool {
39 matches!(
40 file.relative_path().rsplit('/').next(),
41 Some("all_tests.rs") | Some("mod.rs")
42 )
43 }
44
45 fn section_order(&self, previous: &TestFileItem, item: &TestFileItem) -> Option<Offence> {
46 if item.section >= previous.section {
47 return None;
48 }
49 Some(self.offence(
50 item,
51 format!(
52 "a {} follows a {}, but every {} belongs above them",
53 item.section.label(),
54 previous.section.label(),
55 item.section.label()
56 ),
57 format!(
58 "move `{}` up above the {}s",
59 item.name,
60 previous.section.label()
61 ),
62 ))
63 }
64
65 fn alphabetic_order(&self, previous: &TestFileItem, item: &TestFileItem) -> Option<Offence> {
66 if previous.section != item.section || item.sort_key() >= previous.sort_key() {
67 return None;
68 }
69 if Self::ordered_by_rustfmt(previous, item) {
70 return None;
71 }
72 Some(self.offence(
73 item,
74 format!(
75 "{} is out of alphabetic order; it follows {}",
76 item.name, previous.name
77 ),
78 format!("move `{}` above `{}`", item.name, previous.name),
79 ))
80 }
81
82 fn ordered_by_rustfmt(previous: &TestFileItem, item: &TestFileItem) -> bool {
86 item.section == Section::Imports && ImportPath::decides_order(&previous.name, &item.name)
87 }
88
89 fn spacing(
90 &self,
91 file: &SourceFile,
92 previous: &TestFileItem,
93 item: &TestFileItem,
94 ) -> Option<Offence> {
95 if previous.section != item.section {
96 return None;
97 }
98 let expected = item.section.blank_lines_between_entries();
99 let found = Self::blank_lines_between(file, previous.last_line, item.first_line);
100 if found == expected {
101 return None;
102 }
103 Some(self.offence(
104 item,
105 format!(
106 "expected {expected} blank line(s) before {} but found {found}",
107 item.name
108 ),
109 format!(
110 "leave exactly {expected} blank line(s) between `{}` and `{}`",
111 previous.name, item.name
112 ),
113 ))
114 }
115
116 fn blank_lines_between(file: &SourceFile, after: usize, before: usize) -> usize {
117 file.lines()
118 .iter()
119 .skip(after)
120 .take(before.saturating_sub(after).saturating_sub(1))
121 .filter(|line| line.trim().is_empty())
122 .count()
123 }
124
125 fn offence(&self, item: &TestFileItem, description: String, correction: String) -> Offence {
126 Offence::new("", item.first_line, self.name(), description, correction)
127 .with_subject(&item.name)
128 }
129}
130
131impl Default for TestFileStructureRule {
132 fn default() -> Self {
133 Self::new()
134 }
135}
136
137impl Rule for TestFileStructureRule {
138 fn name(&self) -> &'static str {
139 "test-file-structure"
140 }
141
142 fn check(&self, file: &SourceFile) -> Vec<Offence> {
147 if !Self::applies_to(file) {
148 return Vec::new();
149 }
150 let Some(items) = TestFileParser::parse(file) else {
151 return Vec::new();
152 };
153 let mut offences = Vec::new();
154 for pair in items.windows(2) {
155 let (previous, item) = (&pair[0], &pair[1]);
156 offences.extend(self.section_order(previous, item));
157 offences.extend(self.alphabetic_order(previous, item));
158 offences.extend(self.spacing(file, previous, item));
159 }
160 offences
161 .into_iter()
162 .map(|offence| Offence {
163 file: file.relative_path().to_string(),
164 ..offence
165 })
166 .collect()
167 }
168
169 fn check_workspace(&self, _files: &[SourceFile]) -> Vec<Offence> {
170 Vec::new()
171 }
172
173 fn requirement(&self) -> Option<&'static str> {
174 None
175 }
176
177 fn is_configured(&self) -> bool {
178 true
179 }
180}