Skip to main content

scute_core/
report.rs

1use crate::{Evaluation, ExecutionError, Outcome, Status};
2
3/// Aggregated result of running a check.
4///
5/// Wraps the raw check output (`Result<Vec<Evaluation>, ExecutionError>`)
6/// with summary counts and the check name.
7///
8/// ```
9/// use scute_core::report::CheckReport;
10/// use scute_core::{Evaluation, Thresholds};
11///
12/// let evals = vec![Evaluation::completed(
13///     "feat: add login",
14///     0,
15///     Thresholds { warn: None, fail: Some(0) },
16///     vec![],
17/// )];
18/// let report = CheckReport::new("commit-message", Ok(evals));
19/// assert!(!report.has_failures());
20/// ```
21pub struct CheckReport {
22    pub check: String,
23    pub result: Result<CheckRun, ExecutionError>,
24}
25
26/// Successful check execution with summary and all evaluations.
27#[derive(Debug)]
28pub struct CheckRun {
29    pub summary: Summary,
30    pub evaluations: Vec<Evaluation>,
31}
32
33impl CheckRun {
34    /// Evaluations that did not pass (warnings, failures, errors).
35    #[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/// Counts of evaluation outcomes.
42#[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    /// Create a report from raw check output, computing the [`Summary`].
53    #[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    /// True when any evaluation resolved to [`Status::Fail`].
68    #[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    /// True when the check itself failed to run, or any evaluation errored.
74    #[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}