Skip to main content

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}