thruput_native_exec/mock_contract.rs
1use crate::test_environment::TestEnvironment;
2
3use crate::invoke_error::invoke_wrapper;
4use sha2::{Digest, Sha256};
5use std::boxed::Box;
6use std::collections::HashMap;
7use std::panic::UnwindSafe;
8use std::rc::Rc;
9use thruput_xdr::contract::Contract;
10use thruput_xdr::contract::ContractEntrypointID;
11use thruput_xdr::types::ContractAddress;
12use thruput_xdr::types::SHA256Hash;
13use xdrs::txs;
14
15/// Backing storage for a mock contract, owned by the [`TestEnvironment`];
16/// interact with it through [`MockContract`] handles.
17pub(crate) struct MockContractInner {
18 // implicitly, this contract is Contract::kMockTest { mock_contract_id : index }
19 index: i32,
20 hash: SHA256Hash, // sha256(Contract::kMocktest(index))
21 funcs: HashMap<
22 ContractEntrypointID,
23 Box<dyn Fn() -> thruput_sys::ForeignInvokeResult + Send + Sync>,
24 >,
25 /// Run instead of a deterministic failure when an invoked method id
26 /// matches no entry in `funcs`. `None` (the default) keeps the
27 /// historical behavior; `thruput_sdk::register_mock_package` installs a
28 /// `LibraryExn::UnknownMethod` throw, so a mock standing in for a real
29 /// contract agrees with the rv32 guest dispatcher.
30 unknown: Option<
31 Box<dyn Fn() -> thruput_sys::ForeignInvokeResult + Send + Sync>,
32 >,
33}
34
35/// Handle to a mock contract registered in a [`TestEnvironment`]: a
36/// contract whose entrypoints are native closures instead of rv32 code.
37pub struct MockContract {
38 contract: *mut MockContractInner,
39 _env: Rc<TestEnvironment>,
40}
41
42fn i32_to_contract(index: i32) -> Contract {
43 Contract::kMockTest {
44 mock_contract_id: index,
45 }
46}
47
48impl MockContract {
49 /// Wrap a raw [`MockContractInner`] owned by `env`.
50 pub(crate) fn new(
51 inner: *mut MockContractInner,
52 env: Rc<TestEnvironment>,
53 ) -> Self {
54 MockContract {
55 contract: inner,
56 _env: env,
57 }
58 }
59
60 /// The `Contract::kMockTest` value identifying this mock.
61 pub fn as_contract(&self) -> Contract {
62 i32_to_contract(unsafe { (*self.contract).index })
63 }
64
65 /// The contract hash (`sha256(Contract::kMockTest(index))`).
66 pub fn as_hash(&self) -> &SHA256Hash {
67 unsafe { &(*self.contract).hash }
68 }
69
70 /// Deploy this prepared mock contract at a fixed address in test setup.
71 ///
72 /// This uses the test environment's setup path, so it should be called
73 /// before executing the transaction under test.
74 pub fn deploy_contract_to(&self, addr: &ContractAddress) {
75 self._env.setup_create_deployment(addr, &self.as_contract());
76 }
77
78 /// Register `func` as the entrypoint for method `id`. The closure runs
79 /// inside the frame's catch site, so panics and aborts out of it map to
80 /// frame outcomes.
81 ///
82 /// Panics if `id` is already registered on this mock: two methods
83 /// resolving to one dispatch ID means the second registration would
84 /// silently shadow the first, so duplicate definitions (same interface
85 /// name, major version, and method name) fail loudly at test setup
86 /// instead. There is deliberately no replace/override variant; on-chain
87 /// dispatch has no override either.
88 ///
89 /// `Send + Sync` on `func` is what keeps a mock body safe to run on the
90 /// thread the coroutine resumed on rather than the one that registered it;
91 /// see [`invoke_wrapper`]. Capturing `Rc`/`RefCell` state to record calls
92 /// is rejected here -- use `Arc`/`Mutex`, or an `AtomicUsize` for a plain
93 /// counter.
94 pub fn register_method<F>(&mut self, id: ContractEntrypointID, func: F)
95 where
96 F: Fn() + UnwindSafe + Clone + Send + Sync + 'static,
97 {
98 // Entry API so the rejected registration never touches the map: a
99 // caller that catches the panic must still observe the ORIGINAL
100 // handler on later dispatches, not the rejected replacement.
101 match unsafe { &mut (*self.contract).funcs }.entry(id) {
102 std::collections::hash_map::Entry::Occupied(_) => panic!(
103 "duplicate entrypoint registration for dispatch ID \
104 {:016X}: two contract methods share one entrypoint ID \
105 (same interface name, major version, and method name), so \
106 calls would silently route to only one of them. Remove or \
107 rename the duplicate definition.",
108 id.into_u64(),
109 ),
110 std::collections::hash_map::Entry::Vacant(slot) => {
111 slot.insert(invoke_wrapper(func));
112 }
113 }
114 }
115
116 /// Install the handler for method ids this contract does not define,
117 /// replacing any previous one.
118 ///
119 /// The registered closure runs inside the same `invoke_wrapper` catch
120 /// site as a real method, so aborting out of it (e.g. via
121 /// `thruput_sdk::exn::library_throw`) reaches the caller as an ordinary
122 /// exception rather than escaping the frame.
123 pub fn register_unknown_method<F>(&mut self, func: F)
124 where
125 F: Fn() + UnwindSafe + Clone + Send + Sync + 'static,
126 {
127 unsafe { (*self.contract).unknown = Some(invoke_wrapper(func)) };
128 }
129
130 /// Apply a registrar closure (e.g. a generated `__mock` function),
131 /// for chaining.
132 pub fn register<F: FnOnce(&mut Self)>(&mut self, f: F) -> &mut Self {
133 f(self);
134 self
135 }
136}
137
138impl MockContractInner {
139 /// Create the backing storage for mock contract number `n`.
140 pub fn new(n: i32) -> Self {
141 let mut hasher = Sha256::new();
142 let contract = i32_to_contract(n);
143 hasher.update(txs::encode(&contract).unwrap());
144 let result: [u8; 32] = hasher.finalize().into();
145 Self {
146 index: n,
147 hash: result.into(),
148 funcs: HashMap::new(),
149 unknown: None,
150 }
151 }
152
153 /// The registered entrypoint for `method`, if any.
154 pub fn get_func(
155 &self,
156 method: ContractEntrypointID,
157 ) -> Option<&(dyn Fn() -> thruput_sys::ForeignInvokeResult + Send + Sync)>
158 {
159 self.funcs.get(&method).map(|boxed| boxed.as_ref())
160 }
161
162 /// The handler for undefined method ids, if one was registered.
163 pub fn get_unknown_func(
164 &self,
165 ) -> Option<&(dyn Fn() -> thruput_sys::ForeignInvokeResult + Send + Sync)>
166 {
167 self.unknown.as_ref().map(|boxed| boxed.as_ref())
168 }
169}