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