jellyflow_runtime/runtime/conformance/
reports.rs1use 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}