Skip to main content

ic_testkit/pic/
mod.rs

1//! PocketIC wrapper and fixture helpers for host-side canister tests.
2
3use candid::Principal;
4use pocket_ic::{
5    CanisterStatusResult, PocketIc, PocketIcBuilder, RejectResponse, common::rest::RawMessageId,
6};
7use std::time::Duration;
8
9mod baseline;
10mod calls;
11mod diagnostics;
12mod errors;
13mod lifecycle;
14mod process_lock;
15mod runtime;
16mod snapshot;
17mod standalone;
18mod startup;
19
20pub use baseline::{
21    CachedPicBaseline, CachedPicBaselineGuard, ControllerSnapshots,
22    restore_or_rebuild_cached_pic_baseline,
23};
24pub use errors::{PicCallError, PicInstallError, StandaloneCanisterFixtureError};
25pub use process_lock::{
26    PicSerialGuard, PicSerialGuardError, acquire_pic_serial_guard, try_acquire_pic_serial_guard,
27};
28pub use runtime::PicRuntimeConfig;
29pub use startup::PicStartError;
30
31pub use standalone::{
32    StandaloneCanisterFixture, install_prebuilt_canister, install_prebuilt_canister_with_cycles,
33    try_install_prebuilt_canister, try_install_prebuilt_canister_with_cycles,
34};
35
36///
37/// Create a fresh PocketIC instance with the default application subnet layout.
38///
39/// IMPORTANT:
40/// - Each call creates a new IC instance
41/// - WARNING: callers must hold a `PicSerialGuard` for the full `Pic` lifetime
42/// - Required to avoid PocketIC wasm chunk store exhaustion
43///
44#[must_use]
45pub fn pic() -> Pic {
46    try_pic().unwrap_or_else(|err| panic!("failed to start PocketIC: {err}"))
47}
48
49/// Create a fresh PocketIC instance without panicking on startup failures.
50pub fn try_pic() -> Result<Pic, PicStartError> {
51    PicBuilder::new().with_application_subnet().try_build()
52}
53
54/// Resolve the PocketIC server binary from environment/config and panic on failure.
55#[must_use]
56pub fn ensure_pocket_ic_bin() -> std::path::PathBuf {
57    try_ensure_pocket_ic_bin()
58        .unwrap_or_else(|err| panic!("failed to resolve PocketIC server binary: {err}"))
59}
60
61/// Resolve the PocketIC server binary from environment/config without panicking.
62pub fn try_ensure_pocket_ic_bin() -> Result<std::path::PathBuf, PicStartError> {
63    runtime::ensure_pocket_ic_bin_from_env()
64}
65
66///
67/// PicBuilder
68/// Thin wrapper around the PocketIC builder.
69///
70/// This builder configures one PocketIC instance before startup.
71/// It does not share or reuse a global test runtime.
72///
73/// Note: this file is test-only infrastructure; simplicity wins over abstraction.
74///
75
76pub struct PicBuilder {
77    inner: PocketIcBuilder,
78    runtime_config: PicRuntimeConfig,
79}
80
81#[expect(clippy::new_without_default)]
82impl PicBuilder {
83    /// Start a new PicBuilder with sensible defaults.
84    #[must_use]
85    pub fn new() -> Self {
86        Self {
87            inner: PocketIcBuilder::new(),
88            runtime_config: PicRuntimeConfig::from_env(),
89        }
90    }
91
92    /// Override the runtime policy used to resolve the PocketIC server binary.
93    #[must_use]
94    pub fn with_runtime_config(mut self, runtime_config: PicRuntimeConfig) -> Self {
95        self.runtime_config = runtime_config;
96        self
97    }
98
99    /// Use one explicit PocketIC server binary path.
100    #[must_use]
101    pub fn with_server_binary(mut self, server_binary: impl Into<std::path::PathBuf>) -> Self {
102        self.runtime_config = self.runtime_config.pocket_ic_bin(server_binary);
103        self
104    }
105
106    /// Include an application subnet in the PocketIC instance.
107    #[must_use]
108    pub fn with_application_subnet(mut self) -> Self {
109        self.inner = self.inner.with_application_subnet();
110        self
111    }
112
113    /// Include an II subnet so threshold keys are available in the PocketIC instance.
114    #[must_use]
115    pub fn with_ii_subnet(mut self) -> Self {
116        self.inner = self.inner.with_ii_subnet();
117        self
118    }
119
120    /// Include an NNS subnet in the PocketIC instance.
121    #[must_use]
122    pub fn with_nns_subnet(mut self) -> Self {
123        self.inner = self.inner.with_nns_subnet();
124        self
125    }
126
127    /// Finish building the PocketIC instance and wrap it.
128    #[must_use]
129    pub fn build(self) -> Pic {
130        self.try_build()
131            .unwrap_or_else(|err| panic!("failed to start PocketIC: {err}"))
132    }
133
134    /// Finish building the PocketIC instance without panicking on startup failures.
135    pub fn try_build(self) -> Result<Pic, PicStartError> {
136        let server_binary = self.runtime_config.ensure_binary()?;
137        startup::try_build_pic(self.inner.with_server_binary(server_binary))
138    }
139}
140/// Pic
141/// Thin wrapper around a PocketIC instance.
142///
143/// This type intentionally exposes only a minimal API surface; callers should
144/// use `pic()` to obtain an instance and then perform installs/calls.
145/// Callers must hold a `PicSerialGuard` for the full `Pic` lifetime.
146///
147
148pub struct Pic {
149    inner: PocketIc,
150}
151
152impl Pic {
153    /// Advance one execution round in the owned PocketIC instance.
154    pub fn tick(&self) {
155        self.inner.tick();
156    }
157
158    /// Advance PocketIC wall-clock time by one duration.
159    pub fn advance_time(&self, duration: Duration) {
160        self.inner.advance_time(duration);
161    }
162
163    /// Create one canister with PocketIC default settings.
164    #[must_use]
165    pub fn create_canister(&self) -> Principal {
166        self.inner.create_canister()
167    }
168
169    /// Add cycles to one existing canister.
170    pub fn add_cycles(&self, canister_id: Principal, amount: u128) {
171        let _ = self.inner.add_cycles(canister_id, amount);
172    }
173
174    /// Install one wasm module on one existing canister.
175    pub fn install_canister(
176        &self,
177        canister_id: Principal,
178        wasm_module: Vec<u8>,
179        arg: Vec<u8>,
180        sender: Option<Principal>,
181    ) {
182        self.inner
183            .install_canister(canister_id, wasm_module, arg, sender);
184    }
185
186    /// Upgrade one existing canister with a new wasm module.
187    pub fn upgrade_canister(
188        &self,
189        canister_id: Principal,
190        wasm_module: Vec<u8>,
191        arg: Vec<u8>,
192        sender: Option<Principal>,
193    ) -> Result<(), RejectResponse> {
194        self.inner
195            .upgrade_canister(canister_id, wasm_module, arg, sender)
196    }
197
198    /// Reinstall one existing canister with a new wasm module.
199    pub fn reinstall_canister(
200        &self,
201        canister_id: Principal,
202        wasm_module: Vec<u8>,
203        arg: Vec<u8>,
204        sender: Option<Principal>,
205    ) -> Result<(), RejectResponse> {
206        self.inner
207            .reinstall_canister(canister_id, wasm_module, arg, sender)
208    }
209
210    /// Submit one raw update call without executing it immediately.
211    pub fn submit_call(
212        &self,
213        canister_id: Principal,
214        sender: Principal,
215        method: &str,
216        payload: Vec<u8>,
217    ) -> Result<RawMessageId, RejectResponse> {
218        self.inner.submit_call(canister_id, sender, method, payload)
219    }
220
221    /// Await one previously submitted raw update call.
222    pub fn await_call(&self, message_id: RawMessageId) -> Result<Vec<u8>, RejectResponse> {
223        self.inner.await_call(message_id)
224    }
225
226    /// Fetch one canister status snapshot from PocketIC.
227    pub fn canister_status(
228        &self,
229        canister_id: Principal,
230        sender: Option<Principal>,
231    ) -> Result<CanisterStatusResult, RejectResponse> {
232        self.inner.canister_status(canister_id, sender)
233    }
234
235    /// Fetch one canister log stream from PocketIC.
236    pub fn fetch_canister_logs(
237        &self,
238        canister_id: Principal,
239        sender: Principal,
240    ) -> Result<Vec<pocket_ic::CanisterLogRecord>, RejectResponse> {
241        self.inner.fetch_canister_logs(canister_id, sender)
242    }
243
244    /// Capture the current PocketIC wall-clock time as nanoseconds since epoch.
245    #[must_use]
246    pub fn current_time_nanos(&self) -> u64 {
247        self.inner.get_time().as_nanos_since_unix_epoch()
248    }
249
250    /// Restore PocketIC wall-clock and certified time from a captured nanosecond value.
251    pub fn restore_time_nanos(&self, nanos_since_epoch: u64) {
252        let restored = pocket_ic::Time::from_nanos_since_unix_epoch(nanos_since_epoch);
253        self.inner.set_time(restored);
254        self.inner.set_certified_time(restored);
255    }
256}