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 is_fixable: false,
125 }
126 }
127
128 fn frr(rule_id: &str, level: Level, statuses: Vec<FixStatus>) -> FixRuleResult {
129 FixRuleResult {
130 rule_id: rule_id.into(),
131 level,
132 items: statuses
133 .into_iter()
134 .map(|status| FixItem {
135 violation: Violation::new("v"),
136 status,
137 })
138 .collect(),
139 }
140 }
141
142 #[test]
143 fn empty_report_has_no_errors_or_warnings() {
144 let r = Report { results: vec![] };
145 assert!(!r.has_errors());
146 assert!(!r.has_warnings());
147 assert_eq!(r.total_violations(), 0);
148 assert_eq!(r.failing_rules(), 0);
149 assert_eq!(r.passing_rules(), 0);
150 }
151
152 #[test]
153 fn passing_rules_count_passing_results() {
154 let r = Report {
156 results: vec![rr("a", Level::Error, 0), rr("b", Level::Warning, 0)],
157 };
158 assert_eq!(r.passing_rules(), 2);
159 assert_eq!(r.failing_rules(), 0);
160 assert!(!r.has_errors());
161 }
162
163 #[test]
164 fn has_errors_true_when_error_level_has_violations() {
165 let r = Report {
166 results: vec![rr("a", Level::Error, 1), rr("b", Level::Warning, 5)],
167 };
168 assert!(r.has_errors());
169 assert!(r.has_warnings());
170 assert_eq!(r.total_violations(), 6);
171 assert_eq!(r.failing_rules(), 2);
172 }
173
174 #[test]
175 fn has_errors_false_when_only_warnings_have_violations() {
176 let r = Report {
177 results: vec![rr("a", Level::Error, 0), rr("b", Level::Warning, 3)],
178 };
179 assert!(!r.has_errors());
180 assert!(r.has_warnings());
181 }
182
183 #[test]
184 fn fix_report_applied_skipped_unfixable_counts_summed_across_rules() {
185 let r = FixReport {
186 results: vec![
187 frr(
188 "a",
189 Level::Error,
190 vec![
191 FixStatus::Applied("ok".into()),
192 FixStatus::Applied("ok".into()),
193 FixStatus::Skipped("nope".into()),
194 ],
195 ),
196 frr(
197 "b",
198 Level::Warning,
199 vec![FixStatus::Unfixable, FixStatus::Applied("ok".into())],
200 ),
201 ],
202 };
203 assert_eq!(r.applied(), 3);
204 assert_eq!(r.skipped(), 1);
205 assert_eq!(r.unfixable(), 1);
206 }
207
208 #[test]
209 fn has_unfixable_errors_true_when_error_rule_has_unresolved() {
210 let r = FixReport {
211 results: vec![frr("a", Level::Error, vec![FixStatus::Unfixable])],
212 };
213 assert!(r.has_unfixable_errors());
214 assert!(!r.has_unfixable_warnings());
215 }
216
217 #[test]
218 fn has_unfixable_errors_false_when_all_applied() {
219 let r = FixReport {
220 results: vec![frr(
221 "a",
222 Level::Error,
223 vec![FixStatus::Applied("done".into())],
224 )],
225 };
226 assert!(!r.has_unfixable_errors());
227 }
228
229 #[test]
230 fn has_unfixable_errors_false_when_skip_only_at_warning_level() {
231 let r = FixReport {
234 results: vec![frr(
235 "a",
236 Level::Warning,
237 vec![FixStatus::Skipped("nope".into())],
238 )],
239 };
240 assert!(!r.has_unfixable_errors());
241 assert!(r.has_unfixable_warnings());
242 }
243
244 #[test]
245 fn rule_result_passed_method_is_correct() {
246 let passing = rr("a", Level::Error, 0);
247 let failing = rr("b", Level::Error, 1);
248 assert!(passing.passed());
249 assert!(!failing.passed());
250 }
251}