1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
use trellis_core::{
GraphError, OutputFrameTrace, ResourceCommandTrace, ResourceKey, Revision, TraceMismatch,
TransactionId, TransactionResult, TransactionTrace, assert_transaction_traces_match,
};
use crate::{FullRecomputeOracle, OracleCheck, OracleMismatch, assert_incremental_equals_full};
/// Named transaction trace captured by a scenario test.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ScenarioStep {
/// Human-readable step name.
pub name: String,
/// Structural transaction trace for this step.
pub trace: TransactionTrace,
}
/// Deterministic scenario recorder for transaction scripts.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Scenario {
steps: Vec<ScenarioStep>,
}
/// Scenario assertion failure.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ScenarioError {
/// The replay trace sequence differed.
ReplayMismatch(TraceMismatch),
/// The final deterministic graph dump differed after replay.
ReplayFinalStateMismatch {
/// Expected final graph dump.
expected: String,
/// Actual final graph dump.
actual: String,
},
/// The replayed typed ledger state differed.
ReplayLedgerMismatch {
/// Ledger field whose value differed.
field: &'static str,
/// Expected typed ledger state.
expected: String,
/// Actual typed ledger state.
actual: String,
},
/// A named step was not found.
MissingStep(String),
/// A named step had different structural data.
StepMismatch {
/// Step whose assertion failed.
step: String,
/// Transaction that produced the mismatched structural value.
transaction_id: TransactionId,
/// Graph revision at the mismatched step.
revision: Revision,
/// Trace field whose value differed.
field: &'static str,
/// Expected structural value.
expected: String,
/// Actual structural value.
actual: String,
},
/// A scenario step failed while staging or committing a transaction.
StepCommitFailed {
/// Step whose transaction failed.
step: String,
/// Graph error returned by core.
error: GraphError,
},
/// A step-level invariant hook failed.
InvariantFailed {
/// Step whose invariant failed.
step: String,
/// Stable invariant name.
invariant: String,
/// Transaction that produced the failure.
transaction_id: TransactionId,
/// Graph revision at the failed invariant.
revision: Revision,
},
}
/// Redaction hook for scenario debug and snapshot output.
pub trait TraceRedactor {
/// Redacts a scenario step name.
fn step_name(&self, name: &str) -> String {
name.to_owned()
}
/// Redacts a resource key.
fn resource_key(&self, key: &ResourceKey) -> ResourceKey {
key.clone()
}
/// Redacts an invariant name.
fn invariant_name(&self, name: &str) -> String {
name.to_owned()
}
}
/// Redactor that preserves all trace data.
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub struct NoRedaction;
impl TraceRedactor for NoRedaction {}
impl Scenario {
/// Creates an empty scenario recorder.
pub fn new() -> Self {
Self::default()
}
/// Records a committed transaction result under a stable step name.
pub fn record<C, O>(&mut self, name: impl Into<String>, result: &TransactionResult<C, O>) {
self.record_trace(name, result.trace());
}
/// Records an already-built structural transaction trace under a step name.
pub fn record_trace(&mut self, name: impl Into<String>, trace: TransactionTrace) {
self.steps.push(ScenarioStep {
name: name.into(),
trace,
});
}
/// Returns all recorded steps in commit order.
pub fn steps(&self) -> &[ScenarioStep] {
&self.steps
}
/// Returns a named step.
pub fn step(&self, name: &str) -> Result<&ScenarioStep, ScenarioError> {
self.steps
.iter()
.find(|step| step.name == name)
.ok_or_else(|| ScenarioError::MissingStep(name.to_owned()))
}
/// Compares two scenario trace sequences structurally.
pub fn assert_replay_matches(&self, other: &Scenario) -> Result<(), ScenarioError> {
assert_transaction_traces_match(&self.traces(), &other.traces())
.map_err(ScenarioError::ReplayMismatch)
}
/// Returns all transaction traces in commit order.
pub fn traces(&self) -> Vec<TransactionTrace> {
self.steps
.iter()
.map(|step| step.trace.clone())
.collect::<Vec<_>>()
}
/// Returns all resource command traces in commit order.
pub fn resource_commands(&self) -> Vec<ResourceCommandTrace> {
self.steps
.iter()
.flat_map(|step| step.trace.resource_commands.iter().cloned())
.collect()
}
/// Returns all output frame traces in commit order.
pub fn output_frames(&self) -> Vec<OutputFrameTrace> {
self.steps
.iter()
.flat_map(|step| step.trace.output_frames.iter().cloned())
.collect()
}
/// Asserts a named step emitted the expected resource command trace.
pub fn assert_step_resource_commands(
&self,
name: &str,
expected: &[ResourceCommandTrace],
) -> Result<(), ScenarioError> {
let step = self.step(name)?;
if step.trace.resource_commands == expected {
Ok(())
} else {
Err(ScenarioError::StepMismatch {
step: name.to_owned(),
transaction_id: step.trace.transaction_id,
revision: step.trace.revision,
field: "resource_commands",
expected: format!("{expected:#?}"),
actual: format!("{:#?}", step.trace.resource_commands),
})
}
}
/// Asserts a named step emitted the expected output frame trace.
pub fn assert_step_output_frames(
&self,
name: &str,
expected: &[OutputFrameTrace],
) -> Result<(), ScenarioError> {
let step = self.step(name)?;
if step.trace.output_frames == expected {
Ok(())
} else {
Err(ScenarioError::StepMismatch {
step: name.to_owned(),
transaction_id: step.trace.transaction_id,
revision: step.trace.revision,
field: "output_frames",
expected: format!("{expected:#?}"),
actual: format!("{:#?}", step.trace.output_frames),
})
}
}
/// Runs an app-owned oracle check through the scenario harness.
pub fn assert_oracle<G, O>(
&self,
graph: &G,
inputs: &O::CanonicalInputs,
) -> Result<OracleCheck<O::ExpectedState>, OracleMismatch<O::ExpectedState>>
where
O: FullRecomputeOracle<G>,
{
assert_incremental_equals_full::<G, O>(graph, inputs)
}
/// Returns a redacted copy of the scenario for snapshot/debug output.
pub fn redacted(&self, redactor: &impl TraceRedactor) -> Self {
let steps = self
.steps
.iter()
.map(|step| ScenarioStep {
name: redactor.step_name(&step.name),
trace: redact_trace(&step.trace, redactor),
})
.collect();
Self { steps }
}
/// Returns deterministic redacted debug output for snapshots.
pub fn to_redacted_debug_string(&self, redactor: &impl TraceRedactor) -> String {
format!("{:#?}", self.redacted(redactor))
}
}
fn redact_trace(trace: &TransactionTrace, redactor: &impl TraceRedactor) -> TransactionTrace {
let mut trace = trace.clone();
for command in &mut trace.resource_commands {
command.key = redactor.resource_key(&command.key);
}
for result in &mut trace.invariant_results {
result.name = redactor.invariant_name(&result.name);
}
trace
}