Skip to main content

canic_testkit/pic/
mod.rs

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