1use std::fmt;
8
9use serde::{Deserialize, Serialize};
10
11use crate::report::{Comparison, Report, Verdict};
12
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
15#[serde(tag = "kind", rename_all = "snake_case")]
16pub enum CaseSummary {
17 Gap {
19 case: String,
21 metrics: usize,
23 refusal: String,
25 },
26 Predicted {
28 case: String,
30 metrics: usize,
32 within_target: usize,
34 largest: Option<(String, f64)>,
36 },
37 Scored {
39 case: String,
41 metrics: usize,
43 worst_scored: f64,
45 not_scored: Vec<String>,
47 failed: usize,
49 },
50}
51
52impl CaseSummary {
53 pub fn case(&self) -> &str {
55 match self {
56 Self::Gap { case, .. } | Self::Predicted { case, .. } | Self::Scored { case, .. } => {
57 case
58 }
59 }
60 }
61}
62
63impl fmt::Display for CaseSummary {
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 match self {
66 Self::Gap {
68 case,
69 metrics,
70 refusal,
71 } => write!(
72 f,
73 "{case}: known gap, {metrics} metric(s) not scored: hpr {refusal}"
74 ),
75 Self::Predicted {
78 case,
79 metrics,
80 within_target,
81 largest,
82 } => {
83 write!(
84 f,
85 "{case}: predicted, {metrics} metric(s) reported, {within_target} within target"
86 )?;
87 if let Some((metric, relative)) = largest {
88 write!(f, ", largest {metric} {:+.2}%", 100.0 * relative)?;
89 }
90 Ok(())
91 }
92 Self::Scored {
93 case,
94 metrics,
95 worst_scored,
96 not_scored,
97 failed,
98 } => {
99 write!(
100 f,
101 "{case}: {metrics} metric(s), worst scored {:+.2}%",
102 100.0 * worst_scored
103 )?;
104 if !not_scored.is_empty() {
105 write!(f, ", not scored: {}", not_scored.join(", "))?;
106 }
107 if *failed != 0 {
108 write!(f, ", {failed} OUT OF TOLERANCE")?;
109 }
110 Ok(())
111 }
112 }
113 }
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
118pub struct Totals {
119 pub cases: usize,
121 pub metrics: usize,
123 pub not_scored: usize,
125 pub predicted: usize,
127 pub outside_target: usize,
129 pub failed: usize,
131}
132
133impl Totals {
134 pub fn passed(&self) -> bool {
136 self.failed == 0
137 }
138}
139
140impl fmt::Display for Totals {
141 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142 let predicted = format!(
143 "predicted, against a target, {} outside it",
144 self.outside_target
145 );
146 let aside: Vec<String> = [
147 (self.not_scored, "not scored"),
148 (self.predicted, predicted.as_str()),
149 ]
150 .into_iter()
151 .filter(|(count, _)| *count > 0)
152 .map(|(count, what)| format!("{count} {what}"))
153 .collect();
154 write!(
155 f,
156 "validate: {} case(s), {} metric(s){}, {}",
157 self.cases,
158 self.metrics,
159 if aside.is_empty() {
160 String::new()
161 } else {
162 format!(" ({})", aside.join(", "))
163 },
164 if self.passed() { "ok" } else { "FAILED" }
165 )
166 }
167}
168
169pub fn cases(report: &Report) -> Vec<CaseSummary> {
171 report
172 .cases
173 .iter()
174 .map(|case| {
175 if let Some(gap) = report.gaps.iter().find(|gap| gap.case == *case) {
176 return CaseSummary::Gap {
177 case: case.clone(),
178 metrics: gap.metric_count,
179 refusal: gap.refusal.clone(),
180 };
181 }
182 let metrics: Vec<&Comparison> = report
183 .comparisons
184 .iter()
185 .filter(|comparison| comparison.case == *case)
186 .collect();
187 if metrics.iter().any(|comparison| comparison.targeted_row()) {
188 return CaseSummary::Predicted {
189 case: case.clone(),
190 metrics: metrics.len(),
191 within_target: metrics
192 .iter()
193 .filter(|comparison| comparison.verdict == Verdict::WithinTarget)
194 .count(),
195 largest: metrics
196 .iter()
197 .filter_map(|comparison| {
198 comparison.relative.map(|r| (comparison.metric.clone(), r))
199 })
200 .max_by(|(_, a), (_, b)| a.abs().total_cmp(&b.abs())),
201 };
202 }
203 CaseSummary::Scored {
204 case: case.clone(),
205 metrics: metrics.len(),
206 worst_scored: metrics
207 .iter()
208 .filter(|comparison| comparison.scored())
209 .filter_map(|comparison| comparison.relative)
210 .fold(0.0_f64, |worst, relative| worst.max(relative.abs())),
211 not_scored: metrics
212 .iter()
213 .filter(|comparison| !comparison.scored())
214 .map(|comparison| comparison.metric.clone())
215 .collect(),
216 failed: metrics
217 .iter()
218 .filter(|comparison| comparison.verdict == Verdict::Fail)
219 .count(),
220 }
221 })
222 .collect()
223}
224
225pub fn totals(report: &Report) -> Totals {
227 let count = |verdict| {
228 report
229 .comparisons
230 .iter()
231 .filter(|comparison| comparison.verdict == verdict)
232 .count()
233 };
234 Totals {
235 cases: report.cases.len(),
236 metrics: report.comparisons.len(),
237 not_scored: report.not_scored().len(),
238 predicted: report
239 .comparisons
240 .iter()
241 .filter(|comparison| comparison.targeted_row())
242 .count(),
243 outside_target: count(Verdict::OutsideTarget),
244 failed: report.failures().len(),
245 }
246}