Skip to main content

canic_testkit/pic/
mod.rs

1//! PocketIC wrapper and fixture helpers for host-side Canic tests.
2
3use candid::{Principal, encode_args, encode_one};
4use canic::{
5    Error,
6    cdk::types::TC,
7    dto::{
8        abi::v1::CanisterInitPayload,
9        env::EnvBootstrapArgs,
10        subnet::SubnetIdentity,
11        topology::{AppIndexArgs, SubnetIndexArgs},
12    },
13    ids::CanisterRole,
14};
15use pocket_ic::{
16    CanisterStatusResult, PocketIc, PocketIcBuilder, RejectResponse, common::rest::RawMessageId,
17};
18use std::time::Duration;
19
20mod baseline;
21mod calls;
22mod diagnostics;
23mod errors;
24mod lifecycle;
25mod process_lock;
26mod readiness;
27mod snapshot;
28mod standalone;
29mod startup;
30
31pub use baseline::{
32    CachedPicBaseline, CachedPicBaselineGuard, ControllerSnapshots,
33    restore_or_rebuild_cached_pic_baseline,
34};
35pub use errors::{PicInstallError, StandaloneCanisterFixtureError};
36pub use process_lock::{
37    PicSerialGuard, PicSerialGuardError, acquire_pic_serial_guard, try_acquire_pic_serial_guard,
38};
39pub use readiness::{role_pid, wait_until_ready};
40pub use startup::PicStartError;
41const INSTALL_CYCLES: u128 = 500 * TC;
42
43pub use standalone::{
44    StandaloneCanisterFixture, install_prebuilt_canister, install_prebuilt_canister_with_cycles,
45    install_standalone_canister, try_install_prebuilt_canister,
46    try_install_prebuilt_canister_with_cycles,
47};
48
49///
50/// Create a fresh PocketIC instance with the default Canic test subnet layout.
51///
52/// IMPORTANT:
53/// - Each call creates a new IC instance
54/// - WARNING: callers must hold a `PicSerialGuard` for the full `Pic` lifetime
55/// - Required to avoid PocketIC wasm chunk store exhaustion
56///
57#[must_use]
58pub fn pic() -> Pic {
59    try_pic().unwrap_or_else(|err| panic!("failed to start PocketIC: {err}"))
60}
61
62/// Create a fresh PocketIC instance without panicking on startup failures.
63pub fn try_pic() -> Result<Pic, PicStartError> {
64    PicBuilder::new().with_application_subnet().try_build()
65}
66
67///
68/// PicBuilder
69/// Thin wrapper around the PocketIC builder.
70///
71/// This builder configures one PocketIC instance before startup.
72/// It does not share or reuse a global test runtime.
73///
74/// Note: this file is test-only infrastructure; simplicity wins over abstraction.
75///
76
77pub struct PicBuilder(PocketIcBuilder);
78
79#[expect(clippy::new_without_default)]
80impl PicBuilder {
81    /// Start a new PicBuilder with sensible defaults.
82    #[must_use]
83    pub fn new() -> Self {
84        Self(PocketIcBuilder::new())
85    }
86
87    /// Include an application subnet in the PocketIC instance.
88    #[must_use]
89    pub fn with_application_subnet(mut self) -> Self {
90        self.0 = self.0.with_application_subnet();
91        self
92    }
93
94    /// Include an II subnet so threshold keys are available in the PocketIC instance.
95    #[must_use]
96    pub fn with_ii_subnet(mut self) -> Self {
97        self.0 = self.0.with_ii_subnet();
98        self
99    }
100
101    /// Include an NNS subnet in the PocketIC instance.
102    #[must_use]
103    pub fn with_nns_subnet(mut self) -> Self {
104        self.0 = self.0.with_nns_subnet();
105        self
106    }
107
108    /// Finish building the PocketIC instance and wrap it.
109    #[must_use]
110    pub fn build(self) -> Pic {
111        self.try_build()
112            .unwrap_or_else(|err| panic!("failed to start PocketIC: {err}"))
113    }
114
115    /// Finish building the PocketIC instance without panicking on startup failures.
116    pub fn try_build(self) -> Result<Pic, PicStartError> {
117        startup::try_build_pic(self.0)
118    }
119}
120/// Pic
121/// Thin wrapper around a PocketIC instance.
122///
123/// This type intentionally exposes only a minimal API surface; callers should
124/// use `pic()` to obtain an instance and then perform installs/calls.
125/// Callers must hold a `PicSerialGuard` for the full `Pic` lifetime.
126///
127
128pub struct Pic {
129    inner: PocketIc,
130}
131
132impl Pic {
133    /// Advance one execution round in the owned PocketIC instance.
134    pub fn tick(&self) {
135        self.inner.tick();
136    }
137
138    /// Advance PocketIC wall-clock time by one duration.
139    pub fn advance_time(&self, duration: Duration) {
140        self.inner.advance_time(duration);
141    }
142
143    /// Create one canister with PocketIC default settings.
144    #[must_use]
145    pub fn create_canister(&self) -> Principal {
146        self.inner.create_canister()
147    }
148
149    /// Add cycles to one existing canister.
150    pub fn add_cycles(&self, canister_id: Principal, amount: u128) {
151        let _ = self.inner.add_cycles(canister_id, amount);
152    }
153
154    /// Install one wasm module on one existing canister.
155    pub fn install_canister(
156        &self,
157        canister_id: Principal,
158        wasm_module: Vec<u8>,
159        arg: Vec<u8>,
160        sender: Option<Principal>,
161    ) {
162        self.inner
163            .install_canister(canister_id, wasm_module, arg, sender);
164    }
165
166    /// Upgrade one existing canister with a new wasm module.
167    pub fn upgrade_canister(
168        &self,
169        canister_id: Principal,
170        wasm_module: Vec<u8>,
171        arg: Vec<u8>,
172        sender: Option<Principal>,
173    ) -> Result<(), RejectResponse> {
174        self.inner
175            .upgrade_canister(canister_id, wasm_module, arg, sender)
176    }
177
178    /// Reinstall one existing canister with a new wasm module.
179    pub fn reinstall_canister(
180        &self,
181        canister_id: Principal,
182        wasm_module: Vec<u8>,
183        arg: Vec<u8>,
184        sender: Option<Principal>,
185    ) -> Result<(), RejectResponse> {
186        self.inner
187            .reinstall_canister(canister_id, wasm_module, arg, sender)
188    }
189
190    /// Submit one raw update call without executing it immediately.
191    pub fn submit_call(
192        &self,
193        canister_id: Principal,
194        sender: Principal,
195        method: &str,
196        payload: Vec<u8>,
197    ) -> Result<RawMessageId, RejectResponse> {
198        self.inner.submit_call(canister_id, sender, method, payload)
199    }
200
201    /// Await one previously submitted raw update call.
202    pub fn await_call(&self, message_id: RawMessageId) -> Result<Vec<u8>, RejectResponse> {
203        self.inner.await_call(message_id)
204    }
205
206    /// Fetch one canister status snapshot from PocketIC.
207    pub fn canister_status(
208        &self,
209        canister_id: Principal,
210        sender: Option<Principal>,
211    ) -> Result<CanisterStatusResult, RejectResponse> {
212        self.inner.canister_status(canister_id, sender)
213    }
214
215    /// Fetch one canister log stream from PocketIC.
216    pub fn fetch_canister_logs(
217        &self,
218        canister_id: Principal,
219        sender: Principal,
220    ) -> Result<Vec<pocket_ic::CanisterLogRecord>, RejectResponse> {
221        self.inner.fetch_canister_logs(canister_id, sender)
222    }
223
224    /// Capture the current PocketIC wall-clock time as nanoseconds since epoch.
225    #[must_use]
226    pub fn current_time_nanos(&self) -> u64 {
227        self.inner.get_time().as_nanos_since_unix_epoch()
228    }
229
230    /// Restore PocketIC wall-clock and certified time from a captured nanosecond value.
231    pub fn restore_time_nanos(&self, nanos_since_epoch: u64) {
232        let restored = pocket_ic::Time::from_nanos_since_unix_epoch(nanos_since_epoch);
233        self.inner.set_time(restored);
234        self.inner.set_certified_time(restored);
235    }
236}
237
238/// --------------------------------------
239/// install_args helper
240/// --------------------------------------
241///
242/// Init semantics:
243/// - Root canisters receive a `SubnetIdentity` (direct root bootstrap).
244/// - Non-root canisters receive `EnvBootstrapArgs` + optional directory snapshots.
245///
246/// Directory handling:
247/// - By default, directory views are empty for standalone installs.
248/// - Directory-dependent logic is opt-in via `install_args_with_directories`.
249/// - Root-provisioned installs will populate directories via cascade.
250///
251
252fn install_args(role: CanisterRole) -> Result<Vec<u8>, Error> {
253    if role.is_root() {
254        install_root_args()
255    } else {
256        // Non-root standalone install.
257        // Provide only what is structurally known at install time.
258        let env = EnvBootstrapArgs {
259            prime_root_pid: None,
260            subnet_role: None,
261            subnet_pid: None,
262            root_pid: None,
263            canister_role: Some(role),
264            parent_pid: None,
265        };
266
267        // Intentional: standalone installs do not require directories unless
268        // a test explicitly exercises directory-dependent behavior.
269        let payload = CanisterInitPayload {
270            env,
271            app_index: AppIndexArgs(Vec::new()),
272            subnet_index: SubnetIndexArgs(Vec::new()),
273        };
274
275        encode_args::<(CanisterInitPayload, Option<Vec<u8>>)>((payload, None))
276            .map_err(|err| Error::internal(format!("encode_args failed: {err}")))
277    }
278}
279
280fn install_root_args() -> Result<Vec<u8>, Error> {
281    encode_one(SubnetIdentity::Manual)
282        .map_err(|err| Error::internal(format!("encode_one failed: {err}")))
283}
284
285// Prefer the likely controller sender first to reduce noisy management-call failures.