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