Skip to main content

jellyflow_runtime/runtime/conformance/
approval.rs

1use std::path::PathBuf;
2
3use serde::{Deserialize, Serialize};
4
5use super::fixtures::{
6    ConformanceFixtureDirectory, ConformanceFixtureFileError, ConformanceSuiteFile,
7};
8use super::reports::{ConformanceRunError, ConformanceTraceMismatch};
9use super::runner::run_conformance_scenario;
10use super::scenario::ConformanceSuite;
11
12impl ConformanceSuite {
13    pub fn approve_actual_traces(&self) -> ConformanceSuiteApproval {
14        let mut approved = self.clone();
15        let mut scenario_reports = Vec::new();
16        let mut errors = Vec::new();
17
18        for scenario in &mut approved.scenarios {
19            let compiled = scenario.compiled();
20            match run_conformance_scenario(scenario) {
21                Ok(report) => {
22                    let expected_event_count = compiled.expected_event_count();
23                    let actual_event_count = report.actual_trace.len();
24                    let approved_expected_trace = compiled
25                        .approval_expected_trace(&scenario.expected_trace, &report.actual_trace);
26                    let changed = scenario.expected_trace != approved_expected_trace;
27                    scenario.expected_trace = approved_expected_trace;
28                    scenario_reports.push(ConformanceScenarioApprovalReport {
29                        scenario: scenario.name.clone(),
30                        changed,
31                        expected_event_count,
32                        actual_event_count,
33                        mismatches: report.mismatches,
34                    });
35                }
36                Err(error) => errors.push(error),
37            }
38        }
39
40        ConformanceSuiteApproval {
41            suite: approved,
42            report: ConformanceSuiteApprovalReport {
43                suite: self.name.clone(),
44                scenario_reports,
45                errors,
46            },
47        }
48    }
49}
50
51impl ConformanceFixtureDirectory {
52    pub fn approve_actual_traces_to_json(
53        &self,
54    ) -> Result<ConformanceFixtureDirectoryApprovalReport, ConformanceFixtureFileError> {
55        let mut approvals = Vec::new();
56        for file in &self.files {
57            let approval = file.suite.approve_actual_traces();
58            if !approval.is_approvable() {
59                return Err(ConformanceFixtureFileError::Approve {
60                    path: file.path.display().to_string(),
61                    source: ConformanceApprovalError::from_report(approval.report),
62                });
63            }
64            approvals.push((file.path.clone(), approval));
65        }
66
67        for (path, approval) in &approvals {
68            approval.suite.save_json(path)?;
69        }
70
71        Ok(ConformanceFixtureDirectoryApprovalReport {
72            root: self.root.clone(),
73            reports: approvals
74                .into_iter()
75                .map(|(path, approval)| ConformanceSuiteFileApprovalReport {
76                    path,
77                    report: approval.report,
78                })
79                .collect(),
80        })
81    }
82}
83
84impl ConformanceSuiteFile {
85    pub fn approve_actual_traces_to_json(
86        &self,
87    ) -> Result<ConformanceSuiteApproval, ConformanceFixtureFileError> {
88        let approval = self.suite.approve_actual_traces();
89        if !approval.is_approvable() {
90            return Err(ConformanceFixtureFileError::Approve {
91                path: self.path.display().to_string(),
92                source: ConformanceApprovalError::from_report(approval.report),
93            });
94        }
95        approval.suite.save_json(&self.path)?;
96        Ok(approval)
97    }
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct ConformanceSuiteApproval {
102    pub suite: ConformanceSuite,
103    pub report: ConformanceSuiteApprovalReport,
104}
105
106impl ConformanceSuiteApproval {
107    pub fn is_approvable(&self) -> bool {
108        self.report.is_approvable()
109    }
110
111    pub fn has_changes(&self) -> bool {
112        self.report.has_changes()
113    }
114
115    pub fn changed_scenarios(&self) -> usize {
116        self.report.changed_scenarios()
117    }
118
119    pub fn error_count(&self) -> usize {
120        self.report.error_count()
121    }
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct ConformanceSuiteApprovalReport {
126    pub suite: String,
127    pub scenario_reports: Vec<ConformanceScenarioApprovalReport>,
128    #[serde(default, skip_serializing_if = "Vec::is_empty")]
129    pub errors: Vec<ConformanceRunError>,
130}
131
132impl ConformanceSuiteApprovalReport {
133    pub fn is_approvable(&self) -> bool {
134        self.errors.is_empty()
135    }
136
137    pub fn has_changes(&self) -> bool {
138        self.scenario_reports.iter().any(|report| report.changed)
139    }
140
141    pub fn changed_scenarios(&self) -> usize {
142        self.scenario_reports
143            .iter()
144            .filter(|report| report.changed)
145            .count()
146    }
147
148    pub fn error_count(&self) -> usize {
149        self.errors.len()
150    }
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct ConformanceScenarioApprovalReport {
155    pub scenario: String,
156    pub changed: bool,
157    pub expected_event_count: usize,
158    pub actual_event_count: usize,
159    #[serde(default, skip_serializing_if = "Vec::is_empty")]
160    pub mismatches: Vec<ConformanceTraceMismatch>,
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct ConformanceSuiteFileApprovalReport {
165    pub path: PathBuf,
166    pub report: ConformanceSuiteApprovalReport,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct ConformanceFixtureDirectoryApprovalReport {
171    pub root: PathBuf,
172    pub reports: Vec<ConformanceSuiteFileApprovalReport>,
173}
174
175impl ConformanceFixtureDirectoryApprovalReport {
176    pub fn is_approvable(&self) -> bool {
177        self.reports
178            .iter()
179            .all(|report| report.report.is_approvable())
180    }
181
182    pub fn has_changes(&self) -> bool {
183        self.reports
184            .iter()
185            .any(|report| report.report.has_changes())
186    }
187
188    pub fn file_count(&self) -> usize {
189        self.reports.len()
190    }
191
192    pub fn changed_files(&self) -> usize {
193        self.reports
194            .iter()
195            .filter(|report| report.report.has_changes())
196            .count()
197    }
198
199    pub fn changed_scenarios(&self) -> usize {
200        self.reports
201            .iter()
202            .map(|report| report.report.changed_scenarios())
203            .sum()
204    }
205
206    pub fn error_count(&self) -> usize {
207        self.reports
208            .iter()
209            .map(|report| report.report.error_count())
210            .sum()
211    }
212}
213
214#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
215#[error("failed to approve conformance suite `{suite}` because {error_count} scenario(s) errored")]
216pub struct ConformanceApprovalError {
217    pub suite: String,
218    pub error_count: usize,
219    pub errors: Vec<ConformanceRunError>,
220}
221
222impl ConformanceApprovalError {
223    pub fn from_report(report: ConformanceSuiteApprovalReport) -> Self {
224        Self {
225            suite: report.suite,
226            error_count: report.errors.len(),
227            errors: report.errors,
228        }
229    }
230}