ic_test/
lib.rs

1//! # ic-test
2//!
3//! **ic-test** is a command-line tool that helps to set up and manage Rust canister tests on the Internet Computer (IC) using.
4//! It makes it easier to create a test project and includes the basic files and setup needed for both IC canisters and optionally EVM (Ethereum Virtual Machine) smart contracts.
5//!
6//! The tool reads the `dfx.json` (must exist) and the `foundry.toml` (may exist) files in order to build the test environment automatically. It uses `pocket-ic` and `alloy` (Foundry) to run tests.
7//! The generated code and helpers provide:
8//!
9//! - A simple way to start a test project.
10//! - A single, easy-to-use interface for testing both IC and EVM parts.  
11//! - Type checking and auto-completion support.
12//! - Easy functions for deploying and calling canisters or contracts.
13//!
14//! ## Overview
15//!
16//! **ic-test** will:
17//!
18//! - Read `dfx.json` to get canister details.  
19//! - Read `foundry.toml` to get contract details.  
20//! - Generate Rust types from Candid (`.did`) files.  
21//! - Generate contract interfaces from Solidity (`.sol`) files.  
22//! - Provide API to work with `.wasm` canisters and `.json` contract files in tests.
23//!
24//! ## Requirements
25//!
26//! - [Rust](https://www.rust-lang.org/tools/install)
27//! - [DFX](https://internetcomputer.org/docs/building-apps/getting-started/install#installing-dfx-via-dfxvm) – to build and locally deploy canisters.
28//! - [Foundry](https://book.getfoundry.sh/getting-started/installation) – optional, if you want to test EVM contract's interaction with canisters.
29//!
30//! ## Installation
31//!
32//! ```bash
33//! cargo install ic-test
34//! ```
35//!
36//! ## Tool usage
37//!
38//! ```bash
39//! ic-test <COMMAND> [OPTIONS]
40//! ```
41//!
42//! Without arguments it starts in interactive mode to create a new test project. If an `ic-test.json` config file exists already, the "update" mode will regenerate the existing test project bindings.
43//!
44//! ### Create a new test project
45//!
46//! ```bash
47//! ic-test new tests
48//! ```
49//!
50//! - Creates a new test project in the `tests` folder.
51//! - Looks for canisters and contracts, generates API bindings and a sample test.
52//! - Generates an `ic-test.json` configuration file.
53//! - Fails if the `tests` folder already exists, the user would need to choose a different name.
54//!
55//! ### Update/regenerate an existing test project
56//!
57//! ```bash
58//! ic-test update
59//! ```
60//!
61//! Regenerates bindings using the configuration in `ic-test.json`.
62//!
63//! ## "Hello world" tutorial
64//!
65//! *Create a "Hello, World!" canister:*
66//!
67//! ```bash
68//! dfx new hello-ic-test --type rust --no-frontend
69//! ```
70//!
71//! *Compile the project:*
72//!
73//! ```bash
74//! dfx start --clean --background
75//!
76//! dfx canister create --all
77//!
78//! dfx build
79//! ```
80//!
81//! *Generate test bindings*
82//!
83//! If there are uncommitted changes, either commit them before generating or use the `--force` flag:
84//!
85//! ```bash
86//! ic-test new tests --force
87//! ```
88//!
89//! This creates a tests package with:
90//!
91//! * Canister API bindings in `tests/src/bindings`
92//! * Test environment setup logic in `test_setup.rs`
93//! * A test template in `tests.rs`
94//!
95//! ### Example test
96//!
97//! *Edit `tests.rs`:*
98//!
99//! ```rust
100//! use ic_test::IcpTest;
101//!
102//! #[tokio::test]
103//! async fn test_greet() {
104//!     let test_setup::Env {
105//!         icp_test,
106//!         hello_ic_test_backend,
107//!     } = test_setup::setup(IcpTest::new().await).await;
108//!
109//!     let result = hello_ic_test_backend
110//!         .greet("ic-test".to_string())
111//!         .call()
112//!         .await;
113//!
114//!     assert_eq!(result, "Hello, ic-test!");
115//! }
116//! ```
117//!
118//! *Run tests:*
119//!
120//! ```bash
121//! cargo test
122//! ```
123//!
124//! ### Adding a counter
125//!
126//! *Update the canister backend:*
127//!
128//! ```rust
129//! use std::cell::RefCell;
130//!
131//! #[ic_cdk::query]
132//! fn greet(name: String) -> String {
133//!     format!("Hello, {}!", name)
134//! }
135//!
136//! #[derive(Clone, Default)]
137//! struct CounterState {
138//!     value: u64,
139//!     increment: u64,
140//! }
141//!
142//! thread_local! {
143//!     static STATE: RefCell<CounterState> = RefCell::new(CounterState::default());
144//! }
145//!
146//! #[ic_cdk::init]
147//! fn init(init_value: u64, increment: u64) {
148//!     STATE.with(|state| {
149//!         *state.borrow_mut() = CounterState {
150//!             value: init_value,
151//!             increment,
152//!         };
153//!     });
154//! }
155//!
156//! #[ic_cdk::update]
157//! fn increment_counter() {
158//!     STATE.with(|state| {
159//!         let mut s = state.borrow_mut();
160//!         s.value += s.increment;
161//!     });
162//! }
163//!
164//! #[ic_cdk::query]
165//! fn get_counter() -> u64 {
166//!     STATE.with(|state| state.borrow().value)
167//! }
168//! ```
169//!
170//! *Update Candid file `hello-ic-test-backend.did`:*
171//!
172//! ```candid
173//! service : (nat64, nat64) -> {
174//!   "greet": (text) -> (text) query;
175//!   "get_counter": () -> (nat64) query;
176//!   "increment_counter": () -> ();
177//! }
178//! ```
179//!
180//! *Set initialization arguments in `dfx.json`:*
181//!
182//! ```json
183//! {
184//!   "canisters": {
185//!     "hello-ic-test-backend": {
186//!       "candid": "src/hello-ic-test-backend/hello-ic-test-backend.did",
187//!       "package": "hello-ic-test-backend",
188//!       "type": "rust",
189//!       "init_arg": "(50, 73)"
190//!     }
191//!   },
192//!   "defaults": {
193//!     "build": {
194//!       "args": "",
195//!       "packtool": ""
196//!     }
197//!   },
198//!   "output_env_file": ".env",
199//!   "version": 1
200//! }
201//! ```
202//!
203//! *Regenerate the bindings:*
204//!
205//! ```bash
206//! dfx build
207//!
208//! ic-test
209//! ```
210//!
211//! The `ic-test` will enter interactive mode and prompt user to allow overwriting the `test_setup.rs` file. Upon confirmation the the `test_setup.rs` is regenerated with the initialization parameters:
212//!
213//! ```rust
214//!
215//! # mod hello_ic_test_backend {
216//! #     type Caller = ic_test::IcpUser;
217//! #     type DeployMode = ic_test::DeployMode;
218//! #     type Deployer = ic_test::IcpUser;
219//! #     type DeployBuilder<C> = ic_test::DeployBuilder<C, Caller>;
220//! #     pub fn deploy(user: &str, a: u64, b: u64) -> DeployBuilder<String> {
221//! #         panic!();
222//! #     }
223//! # }
224//!   //...
225//!   # async fn dummy_fn() {
226//!   let icp_user = "";
227//!   let hello_ic_test_backend = hello_ic_test_backend::deploy(&icp_user, 50, 73)
228//!       .call()
229//!       .await;
230//!   # }
231//!   // ...
232//!
233//! ```
234//!
235//! ### New test
236//!
237//! *Add a new test in `tests.rs`:*
238//!
239//! ```rust
240//! // ...
241//! #[tokio::test]
242//! async fn test_counter() {
243//!     let test_setup::Env {
244//!         icp_test,
245//!         hello_ic_test_backend,
246//!     } = test_setup::setup(IcpTest::new().await).await;
247//!
248//!     let result = hello_ic_test_backend.get_counter().call().await;
249//!
250//!     assert_eq!(result, 50u64);
251//!
252//!     hello_ic_test_backend.increment_counter().call().await;
253//!
254//!     let result = hello_ic_test_backend.get_counter().call().await;
255//!
256//!     assert_eq!(result, 123u64); // 50 + 73
257//! }
258//! ```
259//!
260//! ### More examples
261//!
262//! For other examples, see <https://github.com/wasm-forge/ic-test-examples>.
263
264#[cfg(feature = "evm")]
265use icp::http_outcalls::handle_http_outcalls;
266#[cfg(feature = "evm")]
267use std::sync::Arc;
268#[cfg(feature = "evm")]
269use tokio::task;
270
271use candid::{decode_one, encode_one, CandidType};
272
273use serde::Deserialize;
274
275mod icp;
276
277#[cfg(feature = "evm")]
278mod evm;
279#[cfg(feature = "evm")]
280pub use crate::evm::{Evm, EvmUser};
281
282pub use crate::{
283    icp::caller::{CallBuilder, CallError, CallMode, Caller},
284    icp::deployer::{DeployBuilder, DeployError, DeployMode, Deployer},
285    icp::user::IcpUser,
286    icp::Icp,
287};
288
289/// Helper structure combining test environments
290pub struct IcpTest {
291    /// Internet Computer environment for canister interactions.
292    pub icp: Icp,
293
294    /// EVM testing environment for the EVM start contract interactions.
295    #[cfg(feature = "evm")]
296    pub evm: Evm,
297}
298
299impl IcpTest {
300    /// Create a new `IcpTest` instance.
301    ///
302    /// Initializes the IC environment and, if the `evm` feature is enabled,
303    /// also spawns a background task to handle EVM outcalls via Pocket-IC.
304    pub async fn new() -> Self {
305        let result = Self {
306            icp: Icp::new().await,
307            #[cfg(feature = "evm")]
308            evm: Evm::new(),
309        };
310
311        #[cfg(feature = "evm")]
312        let pic = Arc::downgrade(&result.icp.pic);
313
314        #[cfg(feature = "evm")]
315        task::spawn(handle_http_outcalls(
316            pic,
317            result.evm.rpc_url(),
318            vec![result.evm.rpc_url().to_string()],
319        ));
320        result
321    }
322
323    /// Advance both the IC and EVM environments.
324    ///
325    /// - For IC, triggers a single tick cycle (e.g., canister heartbeat and timer).
326    /// - For EVM (if enabled), mines a new block.
327    pub async fn tick(&self) {
328        self.icp.tick().await;
329        #[cfg(feature = "evm")]
330        self.evm.mine_block().await;
331    }
332}
333
334/// Utility function to convert between types via Candid encoding/decoding.
335pub fn convert<F, T>(value: F) -> T
336where
337    F: CandidType,
338    T: for<'a> Deserialize<'a> + CandidType,
339{
340    decode_one(&encode_one(&value).unwrap()).unwrap()
341}