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//!
130//! use std::cell::RefCell;
131//!
132//! #[ic_cdk::query]
133//! fn greet(name: String) -> String {
134//!     format!("Hello, {}!", name)
135//! }
136//!
137//! #[derive(Clone, Default)]
138//! struct CounterState {
139//!     value: u64,
140//!     increment: u64,
141//! }
142//!
143//! thread_local! {
144//!     static STATE: RefCell<CounterState> = RefCell::new(CounterState::default());
145//! }
146//!
147//! #[ic_cdk::init]
148//! fn init(init_value: u64, increment: u64) {
149//!     STATE.with(|state| {
150//!         *state.borrow_mut() = CounterState {
151//!             value: init_value,
152//!             increment,
153//!         };
154//!     });
155//! }
156//!
157//! #[ic_cdk::update]
158//! fn increment_counter() {
159//!     STATE.with(|state| {
160//!         let mut s = state.borrow_mut();
161//!         s.value += s.increment;
162//!     });
163//! }
164//!
165//! #[ic_cdk::query]
166//! fn get_counter() -> u64 {
167//!     STATE.with(|state| state.borrow().value)
168//! }
169//! ```
170//!
171//! *Update Candid file `hello-ic-test-backend.did`:*
172//!
173//! ```candid
174//! service : (nat64, nat64) -> {
175//!   "greet": (text) -> (text) query;
176//!   "get_counter": () -> (nat64) query;
177//!   "increment_counter": () -> ();
178//! }
179//! ```
180//!
181//! *Set initialization arguments in `dfx.json`:*
182//!
183//! ```json
184//! {
185//!   "canisters": {
186//!     "hello-ic-test-backend": {
187//!       "candid": "src/hello-ic-test-backend/hello-ic-test-backend.did",
188//!       "package": "hello-ic-test-backend",
189//!       "type": "rust",
190//!       "init_arg": "(50, 73)"
191//!     }
192//!   },
193//!   "defaults": {
194//!     "build": {
195//!       "args": "",
196//!       "packtool": ""
197//!     }
198//!   },
199//!   "output_env_file": ".env",
200//!   "version": 1
201//! }
202//! ```
203//!
204//! *Regenerate the bindings:*
205//!
206//! ```bash
207//! dfx build
208//!
209//! ic-test
210//! ```
211//!
212//! 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:
213//!
214//! ```rust
215//!
216//! # mod hello_ic_test_backend {
217//! #     type Caller = ic_test::IcpUser;
218//! #     type DeployMode = ic_test::DeployMode;
219//! #     type Deployer = ic_test::IcpUser;
220//! #     type DeployBuilder<C> = ic_test::DeployBuilder<C, Caller>;
221//! #     pub fn deploy(user: &str, a: u64, b: u64) -> DeployBuilder<String> {
222//! #         panic!();
223//! #     }
224//! # }
225//!   //...
226//!   # async fn dummy_fn() {
227//!   let icp_user = "";
228//!   let hello_ic_test_backend = hello_ic_test_backend::deploy(&icp_user, 50, 73)
229//!       .call()
230//!       .await;
231//!   # }
232//!   // ...
233//!
234//! ```
235//!
236//! ### New test
237//!
238//! *Add a new test in `tests.rs`:*
239//!
240//! ```rust
241//! // ...
242//! #[tokio::test]
243//! async fn test_counter() {
244//!     let test_setup::Env {
245//!         icp_test,
246//!         hello_ic_test_backend,
247//!     } = test_setup::setup(IcpTest::new().await).await;
248//!
249//!     let result = hello_ic_test_backend.get_counter().call().await;
250//!
251//!     assert_eq!(result, 50u64);
252//!
253//!     hello_ic_test_backend.increment_counter().call().await;
254//!
255//!     let result = hello_ic_test_backend.get_counter().call().await;
256//!
257//!     assert_eq!(result, 123u64); // 50 + 73
258//! }
259//! ```
260//!
261//! ### More examples
262//!
263//! For other examples, see <https://github.com/wasm-forge/ic-test-examples>.
264
265#[cfg(feature = "evm")]
266use icp::http_outcalls::handle_http_outcalls;
267#[cfg(feature = "evm")]
268use std::sync::Arc;
269#[cfg(feature = "evm")]
270use tokio::task;
271
272use candid::{decode_one, encode_one, CandidType};
273
274use serde::Deserialize;
275
276mod icp;
277
278#[cfg(feature = "evm")]
279mod evm;
280#[cfg(feature = "evm")]
281pub use crate::evm::{Evm, EvmUser};
282
283pub use crate::{
284    icp::caller::{CallBuilder, CallError, CallMode, Caller},
285    icp::deployer::{DeployBuilder, DeployError, DeployMode, Deployer},
286    icp::user::IcpUser,
287    icp::Icp,
288};
289
290/// Helper structure combining test environments
291pub struct IcpTest {
292    /// Internet Computer environment for canister interactions.
293    pub icp: Icp,
294
295    /// EVM testing environment for the EVM start contract interactions.
296    #[cfg(feature = "evm")]
297    pub evm: Evm,
298}
299
300impl IcpTest {
301    /// Create a new `IcpTest` instance.
302    ///
303    /// Initializes the IC environment and, if the `evm` feature is enabled,
304    /// also spawns a background task to handle EVM outcalls via Pocket-IC.
305    pub async fn new() -> Self {
306        let result = Self {
307            icp: Icp::new().await,
308            #[cfg(feature = "evm")]
309            evm: Evm::new(),
310        };
311
312        #[cfg(feature = "evm")]
313        let pic = Arc::downgrade(&result.icp.pic);
314
315        #[cfg(feature = "evm")]
316        task::spawn(handle_http_outcalls(
317            pic,
318            result.evm.rpc_url(),
319            vec![result.evm.rpc_url().to_string()],
320        ));
321        result
322    }
323
324    /// Advance both the IC and EVM environments.
325    ///
326    /// - For IC, triggers a single tick cycle (e.g., canister heartbeat and timer).
327    /// - For EVM (if enabled), mines a new block.
328    pub async fn tick(&self) {
329        self.icp.tick().await;
330        #[cfg(feature = "evm")]
331        self.evm.mine_block().await;
332    }
333}
334
335/// Utility function to convert between types via Candid encoding/decoding.
336pub fn convert<F, T>(value: F) -> T
337where
338    F: CandidType,
339    T: for<'a> Deserialize<'a> + CandidType,
340{
341    decode_one(&encode_one(&value).unwrap()).unwrap()
342}