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