1use trellis_core::{
2 GraphError, OutputFrameTrace, ResourceCommandTrace, ResourceKey, Revision, TraceMismatch,
3 TransactionId, TransactionResult, TransactionTrace, assert_transaction_traces_match,
4};
5
6use crate::{
7 FullRecomputeOracle, OracleCheck, OracleMismatch, ResourceLedgerError,
8 assert_incremental_equals_full,
9};
10
11#[derive(Clone, Debug, Eq, PartialEq)]
13pub struct ScenarioStep {
14 pub name: String,
16 pub trace: TransactionTrace,
18}
19
20#[derive(Clone, Debug, Default, Eq, PartialEq)]
22pub struct Scenario {
23 steps: Vec<ScenarioStep>,
24}
25
26#[derive(Clone, Debug, Eq, PartialEq)]
28pub enum ScenarioError {
29 ReplayMismatch(TraceMismatch),
31 TraceFormatVersionMismatch {
33 expected: u32,
35 actual: u32,
37 },
38 ReplayFinalStateMismatch {
40 expected: String,
42 actual: String,
44 },
45 ReplayLedgerMismatch {
47 field: &'static str,
49 expected: String,
51 actual: String,
53 },
54 MissingStep(String),
56 DuplicateStep {
58 step: String,
60 },
61 StepMismatch {
63 step: String,
65 transaction_id: TransactionId,
67 revision: Revision,
69 field: &'static str,
71 expected: String,
73 actual: String,
75 },
76 StepCommitFailed {
78 step: String,
80 error: GraphError,
82 },
83 ResourceLedgerInvariantFailed {
85 step: String,
87 error: Box<ResourceLedgerError>,
89 },
90 InvariantFailed {
92 step: String,
94 invariant: String,
96 transaction_id: TransactionId,
98 revision: Revision,
100 },
101}
102
103pub trait TraceRedactor {
105 fn step_name(&self, name: &str) -> String {
107 name.to_owned()
108 }
109
110 fn resource_key(&self, key: &ResourceKey) -> ResourceKey {
112 key.clone()
113 }
114
115 fn invariant_name(&self, name: &str) -> String {
117 name.to_owned()
118 }
119}
120
121#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
123pub struct NoRedaction;
124
125impl TraceRedactor for NoRedaction {}
126
127impl Scenario {
128 pub fn new() -> Self {
130 Self::default()
131 }
132
133 pub fn record<C>(
135 &mut self,
136 name: impl Into<String>,
137 result: &TransactionResult<C>,
138 ) -> Result<(), ScenarioError> {
139 self.record_trace(name, result.trace())
140 }
141
142 pub fn record_trace(
144 &mut self,
145 name: impl Into<String>,
146 trace: TransactionTrace,
147 ) -> Result<(), ScenarioError> {
148 let name = name.into();
149 self.ensure_step_name_available(&name)?;
150 self.steps.push(ScenarioStep { name, trace });
151 Ok(())
152 }
153
154 pub fn steps(&self) -> &[ScenarioStep] {
156 &self.steps
157 }
158
159 pub fn step(&self, name: &str) -> Result<&ScenarioStep, ScenarioError> {
161 self.steps
162 .iter()
163 .find(|step| step.name == name)
164 .ok_or_else(|| ScenarioError::MissingStep(name.to_owned()))
165 }
166
167 pub(crate) fn ensure_step_name_available(&self, name: &str) -> Result<(), ScenarioError> {
168 if self.steps.iter().any(|step| step.name == name) {
169 Err(ScenarioError::DuplicateStep {
170 step: name.to_owned(),
171 })
172 } else {
173 Ok(())
174 }
175 }
176
177 pub fn assert_replay_matches(&self, other: &Scenario) -> Result<(), ScenarioError> {
179 assert_transaction_traces_match(&self.traces(), &other.traces())
180 .map_err(ScenarioError::ReplayMismatch)
181 }
182
183 pub fn traces(&self) -> Vec<TransactionTrace> {
185 self.steps
186 .iter()
187 .map(|step| step.trace.clone())
188 .collect::<Vec<_>>()
189 }
190
191 pub fn resource_commands(&self) -> Vec<ResourceCommandTrace> {
193 self.steps
194 .iter()
195 .flat_map(|step| step.trace.resource_commands.iter().cloned())
196 .collect()
197 }
198
199 pub fn output_frames(&self) -> Vec<OutputFrameTrace> {
201 self.steps
202 .iter()
203 .flat_map(|step| step.trace.output_frames.iter().cloned())
204 .collect()
205 }
206
207 pub fn assert_step_resource_commands(
209 &self,
210 name: &str,
211 expected: &[ResourceCommandTrace],
212 ) -> Result<(), ScenarioError> {
213 let step = self.step(name)?;
214 if step.trace.resource_commands == expected {
215 Ok(())
216 } else {
217 Err(ScenarioError::StepMismatch {
218 step: name.to_owned(),
219 transaction_id: step.trace.transaction_id,
220 revision: step.trace.revision,
221 field: "resource_commands",
222 expected: format!("{expected:#?}"),
223 actual: format!("{:#?}", step.trace.resource_commands),
224 })
225 }
226 }
227
228 pub fn assert_step_output_frames(
230 &self,
231 name: &str,
232 expected: &[OutputFrameTrace],
233 ) -> Result<(), ScenarioError> {
234 let step = self.step(name)?;
235 if step.trace.output_frames == expected {
236 Ok(())
237 } else {
238 Err(ScenarioError::StepMismatch {
239 step: name.to_owned(),
240 transaction_id: step.trace.transaction_id,
241 revision: step.trace.revision,
242 field: "output_frames",
243 expected: format!("{expected:#?}"),
244 actual: format!("{:#?}", step.trace.output_frames),
245 })
246 }
247 }
248
249 pub fn assert_oracle<G, O>(
251 &self,
252 graph: &G,
253 inputs: &O::CanonicalInputs,
254 ) -> Result<OracleCheck<O::ExpectedState>, OracleMismatch<O::ExpectedState>>
255 where
256 O: FullRecomputeOracle<G>,
257 {
258 assert_incremental_equals_full::<G, O>(graph, inputs)
259 }
260
261 pub fn redacted(&self, redactor: &impl TraceRedactor) -> Self {
263 let steps = self
264 .steps
265 .iter()
266 .map(|step| ScenarioStep {
267 name: redactor.step_name(&step.name),
268 trace: redact_trace(&step.trace, redactor),
269 })
270 .collect();
271 Self { steps }
272 }
273
274 pub fn to_redacted_debug_string(&self, redactor: &impl TraceRedactor) -> String {
276 format!("{:#?}", self.redacted(redactor))
277 }
278}
279
280fn redact_trace(trace: &TransactionTrace, redactor: &impl TraceRedactor) -> TransactionTrace {
281 let mut trace = trace.clone();
282 for command in &mut trace.resource_commands {
283 command.key = redactor.resource_key(&command.key);
284 }
285 for coalesced in &mut trace.resource_coalescences {
286 coalesced.key = redactor.resource_key(&coalesced.key);
287 }
288 for result in &mut trace.invariant_results {
289 result.name = redactor.invariant_name(&result.name);
290 }
291 trace
292}