1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
13pub enum Standing {
14 Unknown,
15 Refused,
16 Admitted,
17 Planned,
18 Executed,
19 Impossible,
20}
21
22impl Default for Standing {
23 fn default() -> Self {
24 Standing::Unknown
25 }
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct DfCmAxis {
33 pub name: String,
34 pub description: Option<String>,
35 pub variants: Vec<String>,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct DfCmCell {
43 pub coords: Vec<String>,
44 pub expected_standing: Standing,
45 pub actual_standing: Standing,
46 pub fixture: Option<String>,
47 pub is_impossible: bool,
48 pub refusal_reason: Option<String>,
49 pub manufacture_witness: Option<String>,
50}
51
52impl DfCmCell {
53 pub fn new(coords: Vec<String>) -> Self {
55 Self {
56 coords,
57 expected_standing: Standing::Unknown,
58 actual_standing: Standing::Unknown,
59 fixture: None,
60 is_impossible: false,
61 refusal_reason: None,
62 manufacture_witness: None,
63 }
64 }
65
66 pub fn impossible(coords: Vec<String>) -> Self {
68 Self {
69 coords,
70 expected_standing: Standing::Impossible,
71 actual_standing: Standing::Refused,
72 fixture: None,
73 is_impossible: true,
74 refusal_reason: None,
75 manufacture_witness: None,
76 }
77 }
78
79 pub fn passes(&self) -> bool {
81 self.expected_standing == self.actual_standing
82 || (self.is_impossible && self.actual_standing == Standing::Refused)
83 }
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct DfCmMatrix {
91 pub name: String,
92 pub axes: Vec<DfCmAxis>,
93 pub cells: Vec<DfCmCell>,
94}
95
96impl DfCmMatrix {
97 pub fn new(name: impl Into<String>, axes: Vec<DfCmAxis>) -> Self {
99 Self {
100 name: name.into(),
101 axes,
102 cells: Vec::new(),
103 }
104 }
105
106 pub fn expand_cartesian(&mut self) {
109 let variant_slices: Vec<&[String]> =
110 self.axes.iter().map(|a| a.variants.as_slice()).collect();
111 let combos = cartesian_product(&variant_slices);
112 self.cells = combos.into_iter().map(DfCmCell::new).collect();
113 }
114
115 pub fn total(&self) -> usize {
117 self.cells.len()
118 }
119
120 pub fn passing(&self) -> usize {
128 self.cells
129 .iter()
130 .filter(|c| c.actual_standing != Standing::Unknown && c.passes())
131 .count()
132 }
133
134 pub fn evaluated(&self) -> usize {
136 self.cells
137 .iter()
138 .filter(|c| c.actual_standing != Standing::Unknown)
139 .count()
140 }
141
142 pub fn coverage(&self) -> f64 {
144 let t = self.total();
145 if t == 0 {
146 return 0.0;
147 }
148 self.evaluated() as f64 / t as f64
149 }
150
151 pub fn pass_rate(&self) -> f64 {
154 let e = self.evaluated();
155 if e == 0 {
156 return 0.0;
157 }
158 self.passing() as f64 / e as f64
159 }
160
161 pub fn find_cell(&self, coords: &[&str]) -> Option<&DfCmCell> {
163 self.cells.iter().find(|c| {
164 c.coords.len() == coords.len()
165 && c.coords.iter().zip(coords.iter()).all(|(a, b)| a == b)
166 })
167 }
168
169 pub fn find_cell_mut(&mut self, coords: &[&str]) -> Option<&mut DfCmCell> {
171 self.cells.iter_mut().find(|c| {
172 c.coords.len() == coords.len()
173 && c.coords.iter().zip(coords.iter()).all(|(a, b)| a == b)
174 })
175 }
176
177 pub fn validate(&self) -> Vec<String> {
184 let mut errors = Vec::new();
185 let axis_count = self.axes.len();
186
187 for (idx, cell) in self.cells.iter().enumerate() {
188 if cell.coords.len() != axis_count {
189 errors.push(format!(
190 "cell[{}]: expected {} coordinates, got {}",
191 idx,
192 axis_count,
193 cell.coords.len()
194 ));
195 continue;
196 }
197 for (dim, (coord, axis)) in cell.coords.iter().zip(self.axes.iter()).enumerate() {
198 if !axis.variants.contains(coord) {
199 errors.push(format!(
200 "cell[{}] dim {}: coordinate {:?} not in axis {:?} variants",
201 idx, dim, coord, axis.name
202 ));
203 }
204 }
205 }
206
207 for i in 0..self.cells.len() {
209 for j in (i + 1)..self.cells.len() {
210 if self.cells[i].coords == self.cells[j].coords {
211 errors.push(format!(
212 "cells[{}] and cells[{}] share duplicate coords {:?}",
213 i, j, self.cells[i].coords
214 ));
215 }
216 }
217 }
218
219 errors
220 }
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize)]
227pub struct DfCmFailure {
228 pub coords: Vec<String>,
229 pub expected: Standing,
230 pub actual: Standing,
231 pub reason: Option<String>,
232}
233
234#[derive(Debug, Clone, Serialize, Deserialize)]
236pub struct DfCmReport {
237 pub matrix_name: String,
238 pub total: usize,
239 pub evaluated: usize,
240 pub passing: usize,
241 pub coverage: f64,
242 pub pass_rate: f64,
243 pub failures: Vec<DfCmFailure>,
244}
245
246impl DfCmReport {
247 pub fn from_matrix(matrix: &DfCmMatrix) -> Self {
249 let failures = matrix
250 .cells
251 .iter()
252 .filter(|c| !c.passes() && c.actual_standing != Standing::Unknown)
253 .map(|c| DfCmFailure {
254 coords: c.coords.clone(),
255 expected: c.expected_standing,
256 actual: c.actual_standing,
257 reason: c.refusal_reason.clone(),
258 })
259 .collect();
260
261 Self {
262 matrix_name: matrix.name.clone(),
263 total: matrix.total(),
264 evaluated: matrix.evaluated(),
265 passing: matrix.passing(),
266 coverage: matrix.coverage(),
267 pass_rate: matrix.pass_rate(),
268 failures,
269 }
270 }
271}
272
273fn cartesian_product(axes: &[&[String]]) -> Vec<Vec<String>> {
276 if axes.is_empty() {
277 return vec![vec![]];
278 }
279 let mut result = vec![vec![]];
280 for axis in axes {
281 let mut next = Vec::with_capacity(result.len() * axis.len());
282 for existing in &result {
283 for variant in *axis {
284 let mut combo = existing.clone();
285 combo.push(variant.clone());
286 next.push(combo);
287 }
288 }
289 result = next;
290 }
291 result
292}
293
294#[cfg(test)]
297mod tests {
298 use super::*;
299
300 fn two_axis_matrix() -> DfCmMatrix {
301 let axes = vec![
302 DfCmAxis {
303 name: "format".into(),
304 description: None,
305 variants: vec!["xes".into(), "ocel".into()],
306 },
307 DfCmAxis {
308 name: "size".into(),
309 description: None,
310 variants: vec!["small".into(), "large".into()],
311 },
312 ];
313 let mut m = DfCmMatrix::new("test", axes);
314 m.expand_cartesian();
315 m
316 }
317
318 #[test]
319 fn cartesian_expands_correctly() {
320 let m = two_axis_matrix();
321 assert_eq!(m.total(), 4);
322 assert!(m.find_cell(&["xes", "small"]).is_some());
323 assert!(m.find_cell(&["ocel", "large"]).is_some());
324 }
325
326 #[test]
327 fn validate_clean_matrix() {
328 let m = two_axis_matrix();
329 assert!(m.validate().is_empty());
330 }
331
332 #[test]
333 fn coverage_and_pass_rate() {
334 let mut m = two_axis_matrix();
335 {
336 let c = m.find_cell_mut(&["xes", "small"]).unwrap();
337 c.expected_standing = Standing::Admitted;
338 c.actual_standing = Standing::Admitted;
339 }
340 {
341 let c = m.find_cell_mut(&["xes", "large"]).unwrap();
342 c.expected_standing = Standing::Admitted;
343 c.actual_standing = Standing::Refused;
344 }
345 assert_eq!(m.evaluated(), 2);
346 assert_eq!(m.passing(), 1);
347 assert!((m.coverage() - 0.5).abs() < f64::EPSILON);
348 assert!((m.pass_rate() - 0.5).abs() < f64::EPSILON);
349 }
350
351 #[test]
352 fn impossible_cell_passes() {
353 let c = DfCmCell::impossible(vec!["xes".into(), "small".into()]);
354 assert!(c.passes());
355 }
356
357 #[test]
358 fn report_captures_failures() {
359 let mut m = two_axis_matrix();
360 {
361 let c = m.find_cell_mut(&["xes", "large"]).unwrap();
362 c.expected_standing = Standing::Admitted;
363 c.actual_standing = Standing::Refused;
364 }
365 let r = DfCmReport::from_matrix(&m);
366 assert_eq!(r.failures.len(), 1);
367 assert_eq!(r.failures[0].coords, vec!["xes", "large"]);
368 }
369}