Skip to main content

jellyflow_runtime/runtime/conformance/
reports.rs

1use std::fmt;
2
3use serde::{Deserialize, Serialize};
4
5use super::scenario::ConformanceTraceEvent;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct ConformanceRunReport {
9    pub scenario: String,
10    pub actual_trace: Vec<ConformanceTraceEvent>,
11    #[serde(default, skip_serializing_if = "Vec::is_empty")]
12    pub mismatches: Vec<ConformanceTraceMismatch>,
13}
14
15impl ConformanceRunReport {
16    pub fn new(
17        scenario: impl Into<String>,
18        actual_trace: Vec<ConformanceTraceEvent>,
19        expected_trace: &[ConformanceTraceEvent],
20    ) -> Self {
21        let mismatches = trace_mismatches(expected_trace, &actual_trace);
22        Self {
23            scenario: scenario.into(),
24            actual_trace,
25            mismatches,
26        }
27    }
28
29    pub fn is_match(&self) -> bool {
30        self.mismatches.is_empty()
31    }
32
33    pub fn actual_trace(&self) -> &[ConformanceTraceEvent] {
34        &self.actual_trace
35    }
36
37    pub fn mismatches(&self) -> &[ConformanceTraceMismatch] {
38        &self.mismatches
39    }
40}
41
42impl fmt::Display for ConformanceRunReport {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        if self.is_match() {
45            return write!(
46                f,
47                "conformance scenario `{}` matched {} trace events",
48                self.scenario,
49                self.actual_trace.len()
50            );
51        }
52
53        writeln!(
54            f,
55            "conformance trace mismatch for scenario `{}` ({} mismatch(es))",
56            self.scenario,
57            self.mismatches.len()
58        )?;
59        for mismatch in self.mismatches.iter().take(8) {
60            writeln!(
61                f,
62                "  [{}] expected: {:?}; actual: {:?}",
63                mismatch.index, mismatch.expected, mismatch.actual
64            )?;
65        }
66        if self.mismatches.len() > 8 {
67            writeln!(f, "  ... {} more", self.mismatches.len() - 8)?;
68        }
69        Ok(())
70    }
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct ConformanceSuiteReport {
75    pub suite: String,
76    pub scenario_reports: Vec<ConformanceRunReport>,
77    #[serde(default, skip_serializing_if = "Vec::is_empty")]
78    pub errors: Vec<ConformanceRunError>,
79}
80
81impl ConformanceSuiteReport {
82    pub fn is_match(&self) -> bool {
83        self.errors.is_empty()
84            && self
85                .scenario_reports
86                .iter()
87                .all(ConformanceRunReport::is_match)
88    }
89
90    pub fn failed_scenarios(&self) -> usize {
91        self.errors.len()
92            + self
93                .scenario_reports
94                .iter()
95                .filter(|report| !report.is_match())
96                .count()
97    }
98
99    pub fn scenario_count(&self) -> usize {
100        self.scenario_reports.len() + self.errors.len()
101    }
102}
103
104impl fmt::Display for ConformanceSuiteReport {
105    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106        if self.is_match() {
107            return write!(
108                f,
109                "conformance suite `{}` matched {} scenario(s)",
110                self.suite,
111                self.scenario_count()
112            );
113        }
114
115        writeln!(
116            f,
117            "conformance suite `{}` failed: {} scenario(s), {} execution error(s)",
118            self.suite,
119            self.failed_scenarios(),
120            self.errors.len()
121        )?;
122        for report in self
123            .scenario_reports
124            .iter()
125            .filter(|report| !report.is_match())
126            .take(8)
127        {
128            writeln!(
129                f,
130                "  scenario `{}` mismatched {} trace event(s)",
131                report.scenario,
132                report.mismatches.len()
133            )?;
134        }
135        for error in self.errors.iter().take(8) {
136            writeln!(
137                f,
138                "  scenario `{}` errored at action {} ({}): {}",
139                error.scenario, error.action_index, error.action_kind, error.message
140            )?;
141        }
142        Ok(())
143    }
144}
145
146#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
147pub struct ConformanceTraceMismatch {
148    pub index: usize,
149    pub expected: Option<ConformanceTraceEvent>,
150    pub actual: Option<ConformanceTraceEvent>,
151}
152
153#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
154#[error(
155    "conformance scenario `{scenario}` failed at action {action_index} ({action_kind}): {message}"
156)]
157pub struct ConformanceRunError {
158    pub scenario: String,
159    pub action_index: usize,
160    pub action_kind: String,
161    pub message: String,
162}
163
164fn trace_mismatches(
165    expected: &[ConformanceTraceEvent],
166    actual: &[ConformanceTraceEvent],
167) -> Vec<ConformanceTraceMismatch> {
168    let len = expected.len().max(actual.len());
169    (0..len)
170        .filter_map(|index| {
171            let expected = expected.get(index);
172            let actual = actual.get(index);
173            (expected != actual).then(|| ConformanceTraceMismatch {
174                index,
175                expected: expected.cloned(),
176                actual: actual.cloned(),
177            })
178        })
179        .collect()
180}