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        self.runtime.storage_mut().put(key, value);
79    }
80
81    pub fn get_storage_context(&mut self) -> MockStorageContext {
82        self.runtime.get_storage_context()
83    }
84
85    pub fn get_read_only_storage_context(&mut self) -> MockStorageContext {
86        self.runtime.get_read_only_storage_context()
87    }
88
89    pub fn put_storage_with_context(
90        &mut self,
91        context: &MockStorageContext,
92        key: &[u8],
93        value: &[u8],
94    ) -> Result<(), neo_types::NeoError> {
95        self.runtime.storage_put_with_context(context, key, value)
96    }
97
98    pub fn delete_storage_with_context(
99        &mut self,
100        context: &MockStorageContext,
101        key: &[u8],
102    ) -> Result<(), neo_types::NeoError> {
103        self.runtime.storage_delete_with_context(context, key)
104    }
105
106    pub fn get_storage(&self, key: &[u8]) -> Option<Vec<u8>> {
107        self.runtime.storage_ref().get(key)
108    }
109
110    pub fn delete_storage(&mut self, key: &[u8]) {
111        self.runtime.storage_mut().delete(key);
112    }
113
114    pub fn set_trigger(&mut self, trigger: i32) {
115        self.runtime.trigger = trigger;
116    }
117
118    pub fn set_time(&mut self, time: i64) {
119        self.runtime.time = time;
120    }
121
122    pub fn set_network(&mut self, network: i64) {
123        self.runtime.network = network;
124    }
125
126    pub fn add_witness(&mut self, address: &[u8]) {
127        self.runtime
128            .witnesses_mut()
129            .push(NeoByteString::from_slice(address));
130    }
131
132    pub fn check_witness(&self, address: &[u8]) -> bool {
133        self.runtime.check_witness(address)
134    }
135
136    pub fn add_log(&mut self, message: &str) {
137        self.runtime.add_log(message);
138    }
139
140    pub fn logs(&self) -> &[String] {
141        self.runtime.logs()
142    }
143
144    pub fn clear_logs(&mut self) {
145        self.runtime.clear_logs();
146    }
147
148    pub fn assert_runtime(&self) -> RuntimeAssertions<'_> {
149        RuntimeAssertions::new(&self.runtime)
150    }
151
152    pub fn assert_storage(&self) -> StorageAssertions<'_> {
153        StorageAssertions::new(self.runtime.storage_ref())
154    }
155
156    pub fn call_method<F, R>(&self, _name: &str, _args: &[neo_types::NeoValue], _f: F) -> R
157    where
158        F: FnOnce() -> R,
159    {
160        _f()
161    }
162
163    pub fn deploy(&mut self, script: &[u8], manifest: &[u8]) -> TestResult {
164        if self.deployment.is_some() {
165            return Err(TestError::new("contract is already deployed").with_context("deploy"));
166        }
167        if script.is_empty() {
168            return Err(TestError::new("script cannot be empty").with_context("deploy"));
169        }
170        if manifest.is_empty() {
171            return Err(TestError::new("manifest cannot be empty").with_context("deploy"));
172        }
173
174        self.deployment = Some(DeploymentState {
175            script: script.to_vec(),
176            manifest: manifest.to_vec(),
177        });
178        Ok(())
179    }
180
181    pub fn update(&mut self, script: &[u8]) -> TestResult {
182        let existing_manifest = self
183            .deployment
184            .as_ref()
185            .ok_or_else(|| TestError::new("contract is not deployed").with_context("update"))?
186            .manifest
187            .clone();
188        self.update_with_manifest(script, &existing_manifest)
189    }
190
191    pub fn update_with_manifest(&mut self, script: &[u8], manifest: &[u8]) -> TestResult {
192        if script.is_empty() {
193            return Err(TestError::new("script cannot be empty").with_context("update"));
194        }
195        if manifest.is_empty() {
196            return Err(TestError::new("manifest cannot be empty").with_context("update"));
197        }
198
199        let deployment = self
200            .deployment
201            .as_mut()
202            .ok_or_else(|| TestError::new("contract is not deployed").with_context("update"))?;
203        deployment.script.clear();
204        deployment.script.extend_from_slice(script);
205        deployment.manifest.clear();
206        deployment.manifest.extend_from_slice(manifest);
207        Ok(())
208    }
209
210    pub fn destroy(&mut self) -> TestResult {
211        if self.deployment.take().is_none() {
212            return Err(TestError::new("contract is not deployed").with_context("destroy"));
213        }
214
215        // Simulate ContractManagement.Destroy semantics by wiping contract storage state.
216        self.runtime.storage_mut().clear();
217        self.runtime.clear_storage_contexts();
218
219        Ok(())
220    }
221
222    pub fn is_deployed(&self) -> bool {
223        self.deployment.is_some()
224    }
225
226    pub fn deployed_script(&self) -> Option<&[u8]> {
227        self.deployment
228            .as_ref()
229            .map(|deployment| deployment.script.as_slice())
230    }
231
232    pub fn deployed_manifest(&self) -> Option<&[u8]> {
233        self.deployment
234            .as_ref()
235            .map(|deployment| deployment.manifest.as_slice())
236    }
237
238    pub fn reset(&mut self) {
239        self.runtime = MockRuntime::new();
240        self.deployment = None;
241    }
242}
243
244impl Default for TestEnvironment {
245    fn default() -> Self {
246        Self::new()
247    }
248}
249
250pub struct ContractTest<T> {
251    env: TestEnvironment,
252    contract: T,
253}
254
255impl<T> ContractTest<T> {
256    pub fn new(contract: T) -> Self {
257        Self {
258            env: TestEnvironment::new(),
259            contract,
260        }
261    }
262
263    pub fn with_env(mut self, env: TestEnvironment) -> Self {
264        self.env = env;
265        self
266    }
267
268    pub fn env(&self) -> &TestEnvironment {
269        &self.env
270    }
271
272    pub fn env_mut(&mut self) -> &mut TestEnvironment {
273        &mut self.env
274    }
275
276    pub fn contract(&self) -> &T {
277        &self.contract
278    }
279
280    pub fn contract_mut(&mut self) -> &mut T {
281        &mut self.contract
282    }
283}
284
285pub struct TestBuilder {
286    env: TestEnvironment,
287}
288
289impl TestBuilder {
290    pub fn new() -> Self {
291        Self {
292            env: TestEnvironment::new(),
293        }
294    }
295
296    pub fn storage(mut self, key: impl AsRef<[u8]>, value: impl AsRef<[u8]>) -> Self {
297        self.env.set_storage(key.as_ref(), value.as_ref());
298        self
299    }
300
301    pub fn time(mut self, time: i64) -> Self {
302        self.env.set_time(time);
303        self
304    }
305
306    pub fn network(mut self, network: i64) -> Self {
307        self.env.set_network(network);
308        self
309    }
310
311    pub fn witness(mut self, address: impl AsRef<[u8]>) -> Self {
312        self.env.add_witness(address.as_ref());
313        self
314    }
315
316    pub fn trigger(mut self, trigger: i32) -> Self {
317        self.env.set_trigger(trigger);
318        self
319    }
320
321    pub fn build(self) -> TestEnvironment {
322        self.env
323    }
324}
325
326impl Default for TestBuilder {
327    fn default() -> Self {
328        Self::new()
329    }
330}
331
332#[macro_export]
333macro_rules! assert_neo {
334    ($expr:expr, $expected:expr) => {
335        assert_eq!($expr.as_i32_saturating(), $expected, "Assertion failed")
336    };
337}
338
339#[macro_export]
340macro_rules! test_env {
341    () => {{
342        neo_test::TestEnvironment::new()
343    }};
344}