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