Skip to main content

neo_test/
environment.rs

1// Copyright (c) 2025-2026 R3E Network
2// Licensed under the MIT License
3
4//! Test Environment
5
6use crate::assertions::{RuntimeAssertions, StorageAssertions};
7use crate::mock_runtime::{MockRuntime, MockStorageContext};
8use neo_types::NeoByteString;
9
10pub type TestResult<T = ()> = Result<T, TestError>;
11
12#[derive(Debug, Clone)]
13pub struct TestError {
14    pub message: String,
15    pub context: String,
16}
17
18impl TestError {
19    pub fn new(message: impl Into<String>) -> Self {
20        Self {
21            message: message.into(),
22            context: String::new(),
23        }
24    }
25
26    pub fn with_context(mut self, context: impl Into<String>) -> Self {
27        self.context = context.into();
28        self
29    }
30}
31
32impl std::fmt::Display for TestError {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        if self.context.is_empty() {
35            write!(f, "{}", self.message)
36        } else {
37            write!(f, "{}: {}", self.context, self.message)
38        }
39    }
40}
41
42impl std::error::Error for TestError {}
43
44/// Test environment for Neo N3 contract testing
45pub struct TestEnvironment {
46    runtime: MockRuntime,
47    deployment: Option<DeploymentState>,
48}
49
50#[derive(Debug, Clone)]
51struct DeploymentState {
52    script: Vec<u8>,
53    manifest: Vec<u8>,
54}
55
56impl TestEnvironment {
57    pub fn new() -> Self {
58        Self {
59            runtime: MockRuntime::new(),
60            deployment: None,
61        }
62    }
63
64    pub fn with_runtime(mut self, runtime: MockRuntime) -> Self {
65        self.runtime = runtime;
66        self
67    }
68
69    pub fn runtime(&self) -> &MockRuntime {
70        &self.runtime
71    }
72
73    pub fn runtime_mut(&mut self) -> &mut MockRuntime {
74        &mut self.runtime
75    }
76
77    pub fn set_storage(&mut self, key: &[u8], value: &[u8]) {
78        // D6: route to the global syscall mock so contract code reading via
79        // `NeoStorage`/`RawStorage`/`NeoVMSyscall::storage_get` sees the same
80        // store (previously this only updated a private MockRuntime map that
81        // the syscall layer never read).
82        self.runtime.storage_mut().put(key, value);
83        let _ = neo_syscalls::NeoVMSyscall::seed_storage(&[(key, value)]);
84    }
85
86    pub fn get_storage_context(&mut self) -> MockStorageContext {
87        self.runtime.get_storage_context()
88    }
89
90    pub fn get_read_only_storage_context(&mut self) -> MockStorageContext {
91        self.runtime.get_read_only_storage_context()
92    }
93
94    pub fn put_storage_with_context(
95        &mut self,
96        context: &MockStorageContext,
97        key: &[u8],
98        value: &[u8],
99    ) -> Result<(), neo_types::NeoError> {
100        self.runtime.storage_put_with_context(context, key, value)
101    }
102
103    pub fn delete_storage_with_context(
104        &mut self,
105        context: &MockStorageContext,
106        key: &[u8],
107    ) -> Result<(), neo_types::NeoError> {
108        self.runtime.storage_delete_with_context(context, key)
109    }
110
111    pub fn get_storage(&self, key: &[u8]) -> Option<Vec<u8>> {
112        self.runtime.storage_ref().get(key)
113    }
114
115    pub fn delete_storage(&mut self, key: &[u8]) {
116        self.runtime.storage_mut().delete(key);
117    }
118
119    pub fn set_trigger(&mut self, trigger: i32) {
120        self.runtime.trigger = trigger;
121    }
122
123    pub fn set_time(&mut self, time: i64) {
124        self.runtime.time = time;
125    }
126
127    pub fn set_network(&mut self, network: i64) {
128        self.runtime.network = network;
129    }
130
131    pub fn add_witness(&mut self, address: &[u8]) {
132        self.runtime
133            .witnesses_mut()
134            .push(NeoByteString::from_slice(address));
135    }
136
137    pub fn check_witness(&self, address: &[u8]) -> bool {
138        self.runtime.check_witness(address)
139    }
140
141    pub fn add_log(&mut self, message: &str) {
142        self.runtime.add_log(message);
143    }
144
145    pub fn logs(&self) -> &[String] {
146        self.runtime.logs()
147    }
148
149    pub fn clear_logs(&mut self) {
150        self.runtime.clear_logs();
151    }
152
153    pub fn assert_runtime(&self) -> RuntimeAssertions<'_> {
154        RuntimeAssertions::new(&self.runtime)
155    }
156
157    pub fn assert_storage(&self) -> StorageAssertions<'_> {
158        StorageAssertions::new(self.runtime.storage_ref())
159    }
160
161    /// Run a contract method against this mock environment under a readable
162    /// label, returning its result.
163    ///
164    /// This is the *unit / logic* testing layer: `f` invokes your contract
165    /// method natively (host execution) so it reads/writes this environment's
166    /// mock storage and runtime. `name`/`args` document the call and appear in
167    /// failure diagnostics; they do not dispatch (native Rust calls are
168    /// type-checked directly).
169    ///
170    /// To execute the *translated NeoVM bytecode* on a real VM instead — the
171    /// equivalent of Solana's `solana-program-test` — use the `neo-vm-test`
172    /// crate's `Contract::compile(..).invoke(..)`.
173    pub fn call_method<F, R>(&self, name: &str, args: &[neo_types::NeoValue], f: F) -> R
174    where
175        F: FnOnce() -> R,
176    {
177        debug_assert!(!name.is_empty(), "call_method requires a method name");
178        let _ = args;
179        f()
180    }
181
182    pub fn deploy(&mut self, script: &[u8], manifest: &[u8]) -> TestResult {
183        if self.deployment.is_some() {
184            return Err(TestError::new("contract is already deployed").with_context("deploy"));
185        }
186        if script.is_empty() {
187            return Err(TestError::new("script cannot be empty").with_context("deploy"));
188        }
189        if manifest.is_empty() {
190            return Err(TestError::new("manifest cannot be empty").with_context("deploy"));
191        }
192
193        self.deployment = Some(DeploymentState {
194            script: script.to_vec(),
195            manifest: manifest.to_vec(),
196        });
197        Ok(())
198    }
199
200    pub fn update(&mut self, script: &[u8]) -> TestResult {
201        let existing_manifest = self
202            .deployment
203            .as_ref()
204            .ok_or_else(|| TestError::new("contract is not deployed").with_context("update"))?
205            .manifest
206            .clone();
207        self.update_with_manifest(script, &existing_manifest)
208    }
209
210    pub fn update_with_manifest(&mut self, script: &[u8], manifest: &[u8]) -> TestResult {
211        if script.is_empty() {
212            return Err(TestError::new("script cannot be empty").with_context("update"));
213        }
214        if manifest.is_empty() {
215            return Err(TestError::new("manifest cannot be empty").with_context("update"));
216        }
217
218        let deployment = self
219            .deployment
220            .as_mut()
221            .ok_or_else(|| TestError::new("contract is not deployed").with_context("update"))?;
222        deployment.script.clear();
223        deployment.script.extend_from_slice(script);
224        deployment.manifest.clear();
225        deployment.manifest.extend_from_slice(manifest);
226        Ok(())
227    }
228
229    pub fn destroy(&mut self) -> TestResult {
230        if self.deployment.take().is_none() {
231            return Err(TestError::new("contract is not deployed").with_context("destroy"));
232        }
233
234        // Simulate ContractManagement.Destroy semantics by wiping contract storage state.
235        self.runtime.storage_mut().clear();
236        self.runtime.clear_storage_contexts();
237
238        Ok(())
239    }
240
241    pub fn is_deployed(&self) -> bool {
242        self.deployment.is_some()
243    }
244
245    pub fn deployed_script(&self) -> Option<&[u8]> {
246        self.deployment
247            .as_ref()
248            .map(|deployment| deployment.script.as_slice())
249    }
250
251    pub fn deployed_manifest(&self) -> Option<&[u8]> {
252        self.deployment
253            .as_ref()
254            .map(|deployment| deployment.manifest.as_slice())
255    }
256
257    pub fn reset(&mut self) {
258        self.runtime = MockRuntime::new();
259        self.deployment = None;
260    }
261}
262
263impl Default for TestEnvironment {
264    fn default() -> Self {
265        Self::new()
266    }
267}
268
269pub struct ContractTest<T> {
270    env: TestEnvironment,
271    contract: T,
272}
273
274impl<T> ContractTest<T> {
275    pub fn new(contract: T) -> Self {
276        Self {
277            env: TestEnvironment::new(),
278            contract,
279        }
280    }
281
282    pub fn with_env(mut self, env: TestEnvironment) -> Self {
283        self.env = env;
284        self
285    }
286
287    pub fn env(&self) -> &TestEnvironment {
288        &self.env
289    }
290
291    pub fn env_mut(&mut self) -> &mut TestEnvironment {
292        &mut self.env
293    }
294
295    pub fn contract(&self) -> &T {
296        &self.contract
297    }
298
299    pub fn contract_mut(&mut self) -> &mut T {
300        &mut self.contract
301    }
302}
303
304pub struct TestBuilder {
305    env: TestEnvironment,
306}
307
308impl TestBuilder {
309    pub fn new() -> Self {
310        Self {
311            env: TestEnvironment::new(),
312        }
313    }
314
315    pub fn storage(mut self, key: impl AsRef<[u8]>, value: impl AsRef<[u8]>) -> Self {
316        self.env.set_storage(key.as_ref(), value.as_ref());
317        self
318    }
319
320    pub fn time(mut self, time: i64) -> Self {
321        self.env.set_time(time);
322        self
323    }
324
325    pub fn network(mut self, network: i64) -> Self {
326        self.env.set_network(network);
327        self
328    }
329
330    pub fn witness(mut self, address: impl AsRef<[u8]>) -> Self {
331        self.env.add_witness(address.as_ref());
332        self
333    }
334
335    pub fn trigger(mut self, trigger: i32) -> Self {
336        self.env.set_trigger(trigger);
337        self
338    }
339
340    pub fn build(self) -> TestEnvironment {
341        self.env
342    }
343}
344
345impl Default for TestBuilder {
346    fn default() -> Self {
347        Self::new()
348    }
349}
350
351#[macro_export]
352macro_rules! assert_neo {
353    ($expr:expr, $expected:expr) => {
354        assert_eq!($expr.as_i32_saturating(), $expected, "Assertion failed")
355    };
356}
357
358#[macro_export]
359macro_rules! test_env {
360    () => {{
361        neo_test::TestEnvironment::new()
362    }};
363}