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}