1use crate::level::Level;
2use crate::rule::{RuleResult, Violation};
3
4#[derive(Debug, Clone)]
5pub struct Report {
6 pub results: Vec<RuleResult>,
7}
8
9impl Report {
10 pub fn has_errors(&self) -> bool {
11 self.results
12 .iter()
13 .any(|r| r.level == Level::Error && !r.violations.is_empty())
14 }
15
16 pub fn has_warnings(&self) -> bool {
17 self.results
18 .iter()
19 .any(|r| r.level == Level::Warning && !r.violations.is_empty())
20 }
21
22 pub fn total_violations(&self) -> usize {
23 self.results.iter().map(|r| r.violations.len()).sum()
24 }
25
26 pub fn failing_rules(&self) -> usize {
27 self.results.iter().filter(|r| !r.passed()).count()
28 }
29
30 pub fn passing_rules(&self) -> usize {
31 self.results.iter().filter(|r| r.passed()).count()
32 }
33}
34
35#[derive(Debug, Clone)]
39pub struct FixReport {
40 pub results: Vec<FixRuleResult>,
41}
42
43#[derive(Debug, Clone)]
44pub struct FixRuleResult {
45 pub rule_id: String,
46 pub level: Level,
47 pub items: Vec<FixItem>,
48}
49
50#[derive(Debug, Clone)]
51pub struct FixItem {
52 pub violation: Violation,
53 pub status: FixStatus,
54}
55
56#[derive(Debug, Clone)]
57pub enum FixStatus {
58 Applied(String),
60 Skipped(String),
63 Unfixable,
65}
66
67impl FixReport {
68 pub fn applied(&self) -> usize {
69 self.items()
70 .filter(|i| matches!(i.status, FixStatus::Applied(_)))
71 .count()
72 }
73
74 pub fn skipped(&self) -> usize {
75 self.items()
76 .filter(|i| matches!(i.status, FixStatus::Skipped(_)))
77 .count()
78 }
79
80 pub fn unfixable(&self) -> usize {
81 self.items()
82 .filter(|i| matches!(i.status, FixStatus::Unfixable))
83 .count()
84 }
85
86 pub fn has_unfixable_errors(&self) -> bool {
88 self.results
89 .iter()
90 .any(|r| r.level == Level::Error && has_unresolved(&r.items))
91 }
92
93 pub fn has_unfixable_warnings(&self) -> bool {
94 self.results
95 .iter()
96 .any(|r| r.level == Level::Warning && has_unresolved(&r.items))
97 }
98
99 fn items(&self) -> impl Iterator<Item = &FixItem> {
100 self.results.iter().flat_map(|r| &r.items)
101 }
102}
103
104fn has_unresolved(items: &[FixItem]) -> bool {
105 items
106 .iter()
107 .any(|i| matches!(i.status, FixStatus::Skipped(_) | FixStatus::Unfixable))
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 fn rr(rule_id: &str, level: Level, n_violations: usize) -> RuleResult {
115 RuleResult {
116 rule_id: rule_id.into(),
117 level,
118 policy_url: None,
119 violations: (0..n_violations)
120 .map(|i| Violation::new(format!("v{i}")))
121 .collect(),
122 is_fixable: false,
123 }
124 }
125
126 fn frr(rule_id: &str, level: Level, statuses: Vec<FixStatus>) -> FixRuleResult {
127 FixRuleResult {
128 rule_id: rule_id.into(),
129 level,
130 items: statuses
131 .into_iter()
132 .map(|status| FixItem {
133 violation: Violation::new("v"),
134 status,
135 })
136 .collect(),
137 }
138 }
139
140 #[test]
141 fn empty_report_has_no_errors_or_warnings() {
142 let r = Report { results: vec![] };
143 assert!(!r.has_errors());
144 assert!(!r.has_warnings());
145 assert_eq!(r.total_violations(), 0);
146 assert_eq!(r.failing_rules(), 0);
147 assert_eq!(r.passing_rules(), 0);
148 }
149
150 #[test]
151 fn passing_rules_count_passing_results() {
152 let r = Report {
154 results: vec![rr("a", Level::Error, 0), rr("b", Level::Warning, 0)],
155 };
156 assert_eq!(r.passing_rules(), 2);
157 assert_eq!(r.failing_rules(), 0);
158 assert!(!r.has_errors());
159 }
160
161 #[test]
162 fn has_errors_true_when_error_level_has_violations() {
163 let r = Report {
164 results: vec![rr("a", Level::Error, 1), rr("b", Level::Warning, 5)],
165 };
166 assert!(r.has_errors());
167 assert!(r.has_warnings());
168 assert_eq!(r.total_violations(), 6);
169 assert_eq!(r.failing_rules(), 2);
170 }
171
172 #[test]
173 fn has_errors_false_when_only_warnings_have_violations() {
174 let r = Report {
175 results: vec![rr("a", Level::Error, 0), rr("b", Level::Warning, 3)],
176 };
177 assert!(!r.has_errors());
178 assert!(r.has_warnings());
179 }
180
181 #[test]
182 fn fix_report_applied_skipped_unfixable_counts_summed_across_rules() {
183 let r = FixReport {
184 results: vec![
185 frr(
186 "a",
187 Level::Error,
188 vec![
189 FixStatus::Applied("ok".into()),
190 FixStatus::Applied("ok".into()),
191 FixStatus::Skipped("nope".into()),
192 ],
193 ),
194 frr(
195 "b",
196 Level::Warning,
197 vec![FixStatus::Unfixable, FixStatus::Applied("ok".into())],
198 ),
199 ],
200 };
201 assert_eq!(r.applied(), 3);
202 assert_eq!(r.skipped(), 1);
203 assert_eq!(r.unfixable(), 1);
204 }
205
206 #[test]
207 fn has_unfixable_errors_true_when_error_rule_has_unresolved() {
208 let r = FixReport {
209 results: vec![frr("a", Level::Error, vec![FixStatus::Unfixable])],
210 };
211 assert!(r.has_unfixable_errors());
212 assert!(!r.has_unfixable_warnings());
213 }
214
215 #[test]
216 fn has_unfixable_errors_false_when_all_applied() {
217 let r = FixReport {
218 results: vec![frr(
219 "a",
220 Level::Error,
221 vec![FixStatus::Applied("done".into())],
222 )],
223 };
224 assert!(!r.has_unfixable_errors());
225 }
226
227 #[test]
228 fn has_unfixable_errors_false_when_skip_only_at_warning_level() {
229 let r = FixReport {
232 results: vec![frr(
233 "a",
234 Level::Warning,
235 vec![FixStatus::Skipped("nope".into())],
236 )],
237 };
238 assert!(!r.has_unfixable_errors());
239 assert!(r.has_unfixable_warnings());
240 }
241
242 #[test]
243 fn rule_result_passed_method_is_correct() {
244 let passing = rr("a", Level::Error, 0);
245 let failing = rr("b", Level::Error, 1);
246 assert!(passing.passed());
247 assert!(!failing.passed());
248 }
249}