1use crate::{Evaluation, ExecutionError, Outcome, Status};
2
3pub struct CheckReport {
22 pub check: String,
23 pub result: Result<CheckRun, ExecutionError>,
24}
25
26#[derive(Debug)]
28pub struct CheckRun {
29 pub summary: Summary,
30 pub evaluations: Vec<Evaluation>,
31}
32
33impl CheckRun {
34 #[must_use]
36 pub fn non_passing_evaluations(&self) -> Vec<&Evaluation> {
37 self.evaluations.iter().filter(|e| !e.is_pass()).collect()
38 }
39}
40
41#[derive(Debug)]
43pub struct Summary {
44 pub evaluated: u64,
45 pub passed: u64,
46 pub warned: u64,
47 pub failed: u64,
48 pub errored: u64,
49}
50
51impl CheckReport {
52 #[must_use]
54 pub fn new(check_name: &str, result: Result<Vec<Evaluation>, ExecutionError>) -> Self {
55 Self {
56 check: check_name.into(),
57 result: result.map(|evals| {
58 let summary = summarize(&evals);
59 CheckRun {
60 summary,
61 evaluations: evals,
62 }
63 }),
64 }
65 }
66
67 #[must_use]
69 pub fn has_failures(&self) -> bool {
70 self.result.as_ref().is_ok_and(|run| run.summary.failed > 0)
71 }
72
73 #[must_use]
75 pub fn has_errors(&self) -> bool {
76 self.result.is_err()
77 || self
78 .result
79 .as_ref()
80 .is_ok_and(|run| run.summary.errored > 0)
81 }
82}
83
84fn summarize(evaluations: &[Evaluation]) -> Summary {
85 let mut passed = 0u64;
86 let mut warned = 0u64;
87 let mut failed = 0u64;
88 let mut errored = 0u64;
89
90 for eval in evaluations {
91 match &eval.outcome {
92 Outcome::Completed { status, .. } => match status {
93 Status::Pass => passed += 1,
94 Status::Warn => warned += 1,
95 Status::Fail => failed += 1,
96 },
97 Outcome::Errored(_) => errored += 1,
98 }
99 }
100
101 Summary {
102 evaluated: passed + warned + failed + errored,
103 passed,
104 warned,
105 failed,
106 errored,
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use crate::{Evidence, Thresholds};
114
115 #[test]
116 fn empty_evaluations_produces_zero_summary() {
117 let report = CheckReport::new("test-check", Ok(vec![]));
118
119 let run = report.result.as_ref().unwrap();
120 assert_eq!(run.summary.evaluated, 0);
121 assert_eq!(run.summary.passed, 0);
122 assert!(!report.has_failures());
123 assert!(!report.has_errors());
124 }
125
126 #[test]
127 fn summary_counts_match_evaluations() {
128 let evals = vec![
129 passing_eval("a"),
130 failing_eval("b"),
131 warned_eval("c"),
132 errored_eval("d"),
133 ];
134
135 let report = CheckReport::new("test-check", Ok(evals));
136
137 let run = report.result.as_ref().unwrap();
138 assert_eq!(run.summary.evaluated, 4);
139 assert_eq!(run.summary.passed, 1);
140 assert_eq!(run.summary.failed, 1);
141 assert_eq!(run.summary.warned, 1);
142 assert_eq!(run.summary.errored, 1);
143 }
144
145 #[test]
146 fn check_level_error_has_no_summary() {
147 let err = ExecutionError {
148 code: "missing_tool".into(),
149 message: "not installed".into(),
150 recovery: "install it".into(),
151 };
152
153 let report = CheckReport::new("test-check", Err(err));
154
155 assert!(report.result.is_err());
156 assert_eq!(report.result.unwrap_err().code, "missing_tool");
157 }
158
159 #[test]
160 fn has_failures_true_when_any_fail() {
161 let evals = vec![passing_eval("a"), failing_eval("b")];
162
163 let report = CheckReport::new("test-check", Ok(evals));
164
165 assert!(report.has_failures());
166 }
167
168 #[test]
169 fn has_failures_false_when_all_pass() {
170 let report = CheckReport::new("test-check", Ok(vec![passing_eval("a")]));
171
172 assert!(!report.has_failures());
173 }
174
175 #[test]
176 fn has_errors_true_for_check_level_error() {
177 let err = ExecutionError {
178 code: "boom".into(),
179 message: "it broke".into(),
180 recovery: "fix it".into(),
181 };
182
183 let report = CheckReport::new("test-check", Err(err));
184
185 assert!(report.has_errors());
186 }
187
188 #[test]
189 fn has_errors_true_for_errored_evaluation() {
190 let evals = vec![Evaluation::errored(
191 "x",
192 ExecutionError {
193 code: "eval_err".into(),
194 message: "bad".into(),
195 recovery: "retry".into(),
196 },
197 )];
198
199 let report = CheckReport::new("test-check", Ok(evals));
200
201 assert!(report.has_errors());
202 }
203
204 #[test]
205 fn preserves_all_evaluations_in_run() {
206 let evals = vec![passing_eval("a"), failing_eval("b"), passing_eval("c")];
207
208 let report = CheckReport::new("test-check", Ok(evals));
209
210 let run = report.result.unwrap();
211 assert_eq!(run.evaluations.len(), 3);
212 assert_eq!(run.evaluations[0].target, "a");
213 assert_eq!(run.evaluations[1].target, "b");
214 assert_eq!(run.evaluations[2].target, "c");
215 }
216
217 #[test]
218 fn non_passing_evaluations_excludes_passing() {
219 let run = run_with(vec![passing_eval("a")]);
220
221 assert!(run.non_passing_evaluations().is_empty());
222 }
223
224 #[test]
225 fn non_passing_evaluations_includes_warned() {
226 let run = run_with(vec![warned_eval("a")]);
227
228 assert_eq!(run.non_passing_evaluations().len(), 1);
229 }
230
231 #[test]
232 fn non_passing_evaluations_includes_failed() {
233 let run = run_with(vec![failing_eval("a")]);
234
235 assert_eq!(run.non_passing_evaluations().len(), 1);
236 }
237
238 #[test]
239 fn non_passing_evaluations_includes_errored() {
240 let run = run_with(vec![errored_eval("a")]);
241
242 assert_eq!(run.non_passing_evaluations().len(), 1);
243 }
244
245 fn run_with(evals: Vec<Evaluation>) -> CheckRun {
246 CheckReport::new("test-check", Ok(evals)).result.unwrap()
247 }
248
249 fn passing_eval(target: &str) -> Evaluation {
250 Evaluation::completed(
251 target,
252 0,
253 Thresholds {
254 warn: None,
255 fail: Some(0),
256 },
257 vec![],
258 )
259 }
260
261 fn warned_eval(target: &str) -> Evaluation {
262 Evaluation::completed(
263 target,
264 3,
265 Thresholds {
266 warn: Some(2),
267 fail: Some(5),
268 },
269 vec![],
270 )
271 }
272
273 fn failing_eval(target: &str) -> Evaluation {
274 Evaluation::completed(
275 target,
276 1,
277 Thresholds {
278 warn: None,
279 fail: Some(0),
280 },
281 vec![Evidence::new("violation", "found")],
282 )
283 }
284
285 fn errored_eval(target: &str) -> Evaluation {
286 Evaluation::errored(
287 target,
288 ExecutionError {
289 code: "boom".into(),
290 message: "broke".into(),
291 recovery: "fix".into(),
292 },
293 )
294 }
295}