thruput_native_exec/frame_result.rs
1use crate::invoke_error::PanicRecord;
2use crate::rv32_stack_usage::Rv32StackUsage;
3use thruput_event::{EmittedEvent, Event, TypedEvent};
4use thruput_exn::{Exn, RawExceptionRef};
5use thruput_trace::InvocationFrameTrace;
6use thruput_xdr::event::TxEmittedEvent;
7use thruput_xdr::transaction::InvocationFrameOutput;
8use thruput_xdr::types::ContractAddress;
9
10/// One-line trace context for assertion messages: the syscalls the frame
11/// made, ending with the one closest to the failure.
12fn describe_trace(trace: Option<&InvocationFrameTrace>) -> String {
13 let Some(trace) = trace else {
14 return String::new();
15 };
16 let count = trace.system_calls.len();
17 let last = trace.system_calls.iter().last().map(|syscall| {
18 let mut dbg = format!("{syscall:?}");
19 const MAX: usize = 160;
20 if dbg.len() > MAX {
21 let mut end = MAX;
22 while !dbg.is_char_boundary(end) {
23 end -= 1;
24 }
25 dbg.truncate(end);
26 dbg.push('…');
27 }
28 dbg
29 });
30 match last {
31 None => format!("\n trace: {count} syscalls"),
32 Some(last) => {
33 format!("\n trace: {count} syscalls, last: {last}")
34 }
35 }
36}
37
38/// Result of executing a single invocation frame.
39/// Wraps InvocationFrameOutput + optional trace + per-frame events.
40#[derive(Debug, Clone)]
41pub struct FrameResult {
42 output: InvocationFrameOutput,
43 trace: Option<InvocationFrameTrace>,
44 events: Vec<TxEmittedEvent>,
45 fault_panic: Option<PanicRecord>,
46 rv32_stack_usage: Vec<Rv32StackUsage>,
47}
48
49impl FrameResult {
50 /// Wrap a frame's output, optional trace, and emitted events.
51 pub fn new(
52 output: InvocationFrameOutput,
53 trace: Option<InvocationFrameTrace>,
54 events: Vec<TxEmittedEvent>,
55 ) -> Self {
56 Self {
57 output,
58 trace,
59 events,
60 fault_panic: None,
61 rv32_stack_usage: Vec::new(),
62 }
63 }
64
65 /// Attach the rv32 stack records read as this frame closed.
66 ///
67 /// Crate-internal, like `with_fault_panic`: only the frame executor
68 /// knows which of the transaction's records are this frame's.
69 pub(crate) fn with_rv32_stack_usage(
70 mut self,
71 records: Vec<Rv32StackUsage>,
72 ) -> Self {
73 self.rv32_stack_usage = records;
74 self
75 }
76
77 /// The stack record of the most recent rv32 call made under this frame:
78 /// the last of [`rv32_stack_usage_records`](Self::rv32_stack_usage_records).
79 /// For a mock frame whose body calls an rv32 contract through its
80 /// client, that is the last such call.
81 ///
82 /// `None` if no rv32 invocation ran under this frame, or if the test
83 /// FFI library was built without the diagnostic (the prebuilt one has
84 /// it). See [`Rv32StackUsage`] for the fields.
85 pub fn rv32_stack_usage(&self) -> Option<Rv32StackUsage> {
86 self.rv32_stack_usage.last().copied()
87 }
88
89 /// The stack records of the rv32 invocations that ran under this frame,
90 /// in the order they finished: a callee's before its caller's.
91 ///
92 /// "Under this frame" is what its body ran while the frame was open:
93 /// the rv32 contracts it called directly, and those called by a mock
94 /// contract it invoked. It is never a call that finished before this
95 /// frame opened, whichever frame of the transaction made it, and never
96 /// a call made under a child frame opened with `child_call`: those are
97 /// the child's own result's. Pick a call by its `contract_address` when
98 /// the body made several.
99 pub fn rv32_stack_usage_records(&self) -> &[Rv32StackUsage] {
100 &self.rv32_stack_usage
101 }
102
103 /// Attach the panic that faulted this frame.
104 ///
105 /// Crate-internal: the frame executor is the only thing that may set
106 /// this, which is what makes a record here always *this* frame's body.
107 pub(crate) fn with_fault_panic(
108 mut self,
109 panic: Option<PanicRecord>,
110 ) -> Self {
111 self.fault_panic = panic;
112 self
113 }
114
115 /// The panic that faulted this frame's own body.
116 ///
117 /// `None` whenever there is nothing to report rather than nothing to
118 /// find, and the two are worth telling apart:
119 ///
120 /// - the frame did not fault, or faulted with neither a string payload
121 /// nor a hook record;
122 /// - the frame faulted because a `raw_Invoke`ed callee did. That callee
123 /// ran in a frame the runtime owns, caught its own panic in
124 /// `invoke_wrapper`, and reported an error code — nothing reaches this
125 /// side. It is still printed to stderr by the panic hook.
126 ///
127 /// So a record here is always *this* frame's body panicking, never a
128 /// nested frame's.
129 pub fn fault_panic(&self) -> Option<&PanicRecord> {
130 self.fault_panic.as_ref()
131 }
132
133 /// Just the message from [`fault_panic`](Self::fault_panic), for callers
134 /// that do not want the location.
135 pub fn fault_message(&self) -> Option<&str> {
136 self.fault_panic.as_ref()?.message.as_deref()
137 }
138
139 // -- Status checks (using pattern matching for safety) --
140
141 /// True if the frame returned successfully.
142 pub fn is_success(&self) -> bool {
143 matches!(&self.output, InvocationFrameOutput::Return { .. })
144 }
145
146 /// True if the frame aborted (threw a contract exception).
147 pub fn is_abort(&self) -> bool {
148 matches!(&self.output, InvocationFrameOutput::Abort { .. })
149 }
150
151 /// True if the frame faulted (panicked or trapped, with no abort data).
152 pub fn is_fault(&self) -> bool {
153 matches!(&self.output, InvocationFrameOutput::Fault)
154 }
155
156 // -- Assertions --
157
158 /// Assert the frame returned successfully. Frame-only check; use
159 /// [`TransactResult::assert_success`] when the commit outcome matters.
160 ///
161 /// [`TransactResult::assert_success`]: crate::transact_result::TransactResult::assert_success
162 #[track_caller]
163 pub fn assert_success(&self) {
164 assert!(
165 self.is_success(),
166 "expected success, got {:?}{}",
167 self.output,
168 describe_trace(self.trace())
169 );
170 }
171
172 /// Assert the frame aborted (threw a contract exception). Frame-only
173 /// check.
174 #[track_caller]
175 pub fn assert_abort(&self) {
176 assert!(
177 self.is_abort(),
178 "expected abort, got {:?}{}",
179 self.output,
180 describe_trace(self.trace())
181 );
182 }
183
184 /// Assert the frame faulted (panicked or trapped). Frame-only check.
185 #[track_caller]
186 pub fn assert_fault(&self) {
187 assert!(
188 self.is_fault(),
189 "expected fault, got {:?}{}",
190 self.output,
191 describe_trace(self.trace())
192 );
193 }
194
195 // -- Buffer accessors --
196
197 /// Returns the return buffer. Panics if the frame did not succeed.
198 pub fn return_buffer(&self) -> &[u8] {
199 match &self.output {
200 InvocationFrameOutput::Return { return_buffer } => {
201 return_buffer.as_slice()
202 }
203 other => {
204 panic!(
205 "return_buffer() called on non-success frame: {other:?}"
206 )
207 }
208 }
209 }
210
211 /// Returns the abort buffer. Panics if the frame did not abort.
212 pub fn abort_buffer(&self) -> &[u8] {
213 match &self.output {
214 InvocationFrameOutput::Abort { abort_buffer } => {
215 abort_buffer.as_slice()
216 }
217 other => {
218 panic!("abort_buffer() called on non-abort frame: {other:?}")
219 }
220 }
221 }
222
223 // -- Typed return assertion --
224
225 /// Assert the frame succeeded and decode the return buffer as a typed value.
226 #[track_caller]
227 pub fn assert_return_with<R>(&self, check: impl FnOnce(&R))
228 where
229 R: xdrs::ReadXDROwned,
230 {
231 self.assert_success();
232 let buf = self.return_buffer();
233 let decoded: R = xdrs::cxs::decode(buf).unwrap_or_else(|e| {
234 panic!("failed to decode return buffer: {}", e)
235 });
236 check(&decoded);
237 }
238
239 /// Assert the frame succeeded and decode the return buffer, returning
240 /// the decoded value. The returning twin of `assert_return_with`.
241 #[track_caller]
242 pub fn assert_return_as<R>(&self) -> R
243 where
244 R: xdrs::ReadXDROwned,
245 {
246 self.assert_success();
247 xdrs::cxs::decode(self.return_buffer()).unwrap_or_else(|e| {
248 panic!("failed to decode return buffer: {}", e)
249 })
250 }
251
252 // -- Typed abort assertion --
253
254 /// Assert the frame aborted with a thrown `E` and return it. Panics
255 /// unless the frame aborted with a `[CXS(E::SELECTOR)][payload]` buffer
256 /// whose payload decodes as `E`.
257 #[track_caller]
258 pub fn assert_abort_as<E>(&self) -> E
259 where
260 E: Exn + xdrs::ReadXDROwned,
261 {
262 self.assert_abort();
263 let buf = self.abort_buffer();
264 let exn = RawExceptionRef::parse(buf).unwrap_or_else(|e| {
265 panic!("abort buffer is not a contract exception ({e}): {buf:?}")
266 });
267 let name = core::any::type_name::<E>();
268 match exn.try_decode::<E>() {
269 Some(Ok(e)) => e,
270 Some(Err(e)) => {
271 panic!("failed to decode abort payload as {name}: {e}")
272 }
273 None => panic!(
274 "expected exception {name} (selector {:#x}), got {exn:?}",
275 E::SELECTOR
276 ),
277 }
278 }
279
280 // -- Raw access --
281
282 /// The frame's raw [`InvocationFrameOutput`].
283 pub fn output(&self) -> &InvocationFrameOutput {
284 &self.output
285 }
286
287 /// The frame's execution trace, when tracing was enabled.
288 pub fn trace(&self) -> Option<&InvocationFrameTrace> {
289 self.trace.as_ref()
290 }
291
292 // -- Per-frame events --
293
294 /// The events this frame emitted (empty if the frame reverted).
295 pub fn events(&self) -> &[TxEmittedEvent] {
296 &self.events
297 }
298
299 /// Assert exactly `expected` events were emitted by this frame.
300 #[track_caller]
301 pub fn assert_event_count(&self, expected: usize) {
302 let actual = self.events.len();
303 assert_eq!(
304 actual, expected,
305 "expected {} events, got {}",
306 expected, actual
307 );
308 }
309
310 /// Assert this frame emitted no events.
311 #[track_caller]
312 pub fn assert_no_events(&self) {
313 assert!(
314 self.events.is_empty(),
315 "expected no events, got {}",
316 self.events.len()
317 );
318 }
319
320 /// All raw events emitted at the given address.
321 pub fn events_from(
322 &self,
323 address: &ContractAddress,
324 ) -> Vec<&TxEmittedEvent> {
325 self.events
326 .iter()
327 .filter(|e| &e.contract_address == address)
328 .collect()
329 }
330
331 /// Collect events from `address` that decode as `E` via
332 /// [`TypedEvent::try_new`]: well-formed, topic 0 matching `E`'s declared
333 /// discriminant (when [`Event::signature`] returns one), topic count
334 /// matching [`Event::num_topics`], and payload decoding as `E::Payload`.
335 /// Foreign and malformed events are skipped, never panicked on.
336 pub fn filter_events<E>(
337 &self,
338 address: &ContractAddress,
339 ) -> Vec<TypedEvent<'_, E>>
340 where
341 E: Event,
342 {
343 self.events
344 .iter()
345 .filter(|e| &e.contract_address == address)
346 .filter_map(|e| TypedEvent::try_new(e))
347 .collect()
348 }
349
350 /// Inspect a raw emitted event by index.
351 #[track_caller]
352 pub fn assert_raw_event(
353 &self,
354 index: usize,
355 check: impl FnOnce(&EmittedEvent<'_>),
356 ) {
357 let events = self.events();
358 assert!(
359 index < events.len(),
360 "event index {} out of range (have {} events)",
361 index,
362 events.len()
363 );
364 let emitted = EmittedEvent::from(&events[index]);
365 check(&emitted);
366 }
367
368 /// Decode an event as a typed event and pass it to the check closure.
369 /// Provides access to both topics and the decoded payload.
370 #[track_caller]
371 pub fn assert_event<E>(
372 &self,
373 index: usize,
374 check: impl FnOnce(&TypedEvent<'_, E>),
375 ) where
376 E: Event,
377 {
378 let events = self.events();
379 assert!(
380 index < events.len(),
381 "event index {} out of range (have {} events)",
382 index,
383 events.len()
384 );
385 let typed = TypedEvent::<E>::new(&events[index]);
386 check(&typed);
387 }
388
389 /// Assert that exactly one event from `address` decodes as `E` (per
390 /// [`TypedEvent::try_new`] — see [`filter_events`](Self::filter_events)
391 /// for the matching rules) and satisfies `predicate`. Panics if zero or
392 /// more than one event matches.
393 #[track_caller]
394 pub fn assert_emitted<E>(
395 &self,
396 address: &ContractAddress,
397 predicate: impl Fn(&TypedEvent<'_, E>) -> bool,
398 ) where
399 E: Event,
400 {
401 let match_count = self
402 .events
403 .iter()
404 .filter(|e| &e.contract_address == address)
405 .filter_map(|e| TypedEvent::try_new(e))
406 .filter(|typed| predicate(typed))
407 .count();
408 assert!(
409 match_count == 1,
410 "expected exactly 1 matching event, got {}",
411 match_count
412 );
413 }
414}