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