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
use crate::test_environment::TestEnvironment;
use crate::invoke_error::invoke_wrapper;
use sha2::{Digest, Sha256};
use std::boxed::Box;
use std::collections::HashMap;
use std::panic::UnwindSafe;
use std::rc::Rc;
use thruput_xdr::contract::Contract;
use thruput_xdr::contract::ContractEntrypointID;
use thruput_xdr::types::ContractAddress;
use thruput_xdr::types::SHA256Hash;
use xdrs::txs;
/// Backing storage for a mock contract, owned by the [`TestEnvironment`];
/// interact with it through [`MockContract`] handles.
pub(crate) struct MockContractInner {
// implicitly, this contract is Contract::kMockTest { mock_contract_id : index }
index: i32,
hash: SHA256Hash, // sha256(Contract::kMocktest(index))
funcs: HashMap<
ContractEntrypointID,
Box<dyn Fn() -> thruput_sys::ForeignInvokeResult + Send + Sync>,
>,
/// Run instead of a deterministic failure when an invoked method id
/// matches no entry in `funcs`. `None` (the default) keeps the
/// historical behavior; `thruput_sdk::register_mock_package` installs a
/// `LibraryExn::UnknownMethod` throw, so a mock standing in for a real
/// contract agrees with the rv32 guest dispatcher.
unknown: Option<
Box<dyn Fn() -> thruput_sys::ForeignInvokeResult + Send + Sync>,
>,
}
/// Handle to a mock contract registered in a [`TestEnvironment`]: a
/// contract whose entrypoints are native closures instead of rv32 code.
pub struct MockContract {
contract: *mut MockContractInner,
_env: Rc<TestEnvironment>,
}
fn i32_to_contract(index: i32) -> Contract {
Contract::kMockTest {
mock_contract_id: index,
}
}
impl MockContract {
/// Wrap a raw [`MockContractInner`] owned by `env`.
pub(crate) fn new(
inner: *mut MockContractInner,
env: Rc<TestEnvironment>,
) -> Self {
MockContract {
contract: inner,
_env: env,
}
}
/// The `Contract::kMockTest` value identifying this mock.
pub fn as_contract(&self) -> Contract {
i32_to_contract(unsafe { (*self.contract).index })
}
/// The contract hash (`sha256(Contract::kMockTest(index))`).
pub fn as_hash(&self) -> &SHA256Hash {
unsafe { &(*self.contract).hash }
}
/// Deploy this prepared mock contract at a fixed address in test setup.
///
/// This uses the test environment's setup path, so it should be called
/// before executing the transaction under test.
pub fn deploy_contract_to(&self, addr: &ContractAddress) {
self._env.setup_create_deployment(addr, &self.as_contract());
}
/// Register `func` as the entrypoint for method `id`. The closure runs
/// inside the frame's catch site, so panics and aborts out of it map to
/// frame outcomes.
///
/// Panics if `id` is already registered on this mock: two methods
/// resolving to one dispatch ID means the second registration would
/// silently shadow the first, so duplicate definitions (same interface
/// name, major version, and method name) fail loudly at test setup
/// instead. There is deliberately no replace/override variant; on-chain
/// dispatch has no override either.
///
/// `Send + Sync` on `func` is what keeps a mock body safe to run on the
/// thread the coroutine resumed on rather than the one that registered it;
/// see [`invoke_wrapper`]. Capturing `Rc`/`RefCell` state to record calls
/// is rejected here -- use `Arc`/`Mutex`, or an `AtomicUsize` for a plain
/// counter.
pub fn register_method<F>(&mut self, id: ContractEntrypointID, func: F)
where
F: Fn() + UnwindSafe + Clone + Send + Sync + 'static,
{
// Entry API so the rejected registration never touches the map: a
// caller that catches the panic must still observe the ORIGINAL
// handler on later dispatches, not the rejected replacement.
match unsafe { &mut (*self.contract).funcs }.entry(id) {
std::collections::hash_map::Entry::Occupied(_) => panic!(
"duplicate entrypoint registration for dispatch ID \
{:016X}: two contract methods share one entrypoint ID \
(same interface name, major version, and method name), so \
calls would silently route to only one of them. Remove or \
rename the duplicate definition.",
id.into_u64(),
),
std::collections::hash_map::Entry::Vacant(slot) => {
slot.insert(invoke_wrapper(func));
}
}
}
/// Install the handler for method ids this contract does not define,
/// replacing any previous one.
///
/// The registered closure runs inside the same `invoke_wrapper` catch
/// site as a real method, so aborting out of it (e.g. via
/// `thruput_sdk::exn::library_throw`) reaches the caller as an ordinary
/// exception rather than escaping the frame.
pub fn register_unknown_method<F>(&mut self, func: F)
where
F: Fn() + UnwindSafe + Clone + Send + Sync + 'static,
{
unsafe { (*self.contract).unknown = Some(invoke_wrapper(func)) };
}
/// Apply a registrar closure (e.g. a generated `__mock` function),
/// for chaining.
pub fn register<F: FnOnce(&mut Self)>(&mut self, f: F) -> &mut Self {
f(self);
self
}
}
impl MockContractInner {
/// Create the backing storage for mock contract number `n`.
pub fn new(n: i32) -> Self {
let mut hasher = Sha256::new();
let contract = i32_to_contract(n);
hasher.update(txs::encode(&contract).unwrap());
let result: [u8; 32] = hasher.finalize().into();
Self {
index: n,
hash: result.into(),
funcs: HashMap::new(),
unknown: None,
}
}
/// The registered entrypoint for `method`, if any.
pub fn get_func(
&self,
method: ContractEntrypointID,
) -> Option<&(dyn Fn() -> thruput_sys::ForeignInvokeResult + Send + Sync)>
{
self.funcs.get(&method).map(|boxed| boxed.as_ref())
}
/// The handler for undefined method ids, if one was registered.
pub fn get_unknown_func(
&self,
) -> Option<&(dyn Fn() -> thruput_sys::ForeignInvokeResult + Send + Sync)>
{
self.unknown.as_ref().map(|boxed| boxed.as_ref())
}
}