Skip to main content

canic_testkit/pic/
mod.rs

1use candid::{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::{PocketIc, PocketIcBuilder};
14use std::ops::{Deref, DerefMut};
15
16mod baseline;
17mod calls;
18mod diagnostics;
19mod errors;
20mod lifecycle;
21mod process_lock;
22mod readiness;
23mod snapshot;
24mod standalone;
25mod startup;
26
27pub use baseline::{
28    CachedPicBaseline, CachedPicBaselineGuard, ControllerSnapshots,
29    restore_or_rebuild_cached_pic_baseline,
30};
31pub use errors::{PicInstallError, StandaloneCanisterFixtureError};
32pub use process_lock::{
33    PicSerialGuard, PicSerialGuardError, acquire_pic_serial_guard, try_acquire_pic_serial_guard,
34};
35pub use readiness::{role_pid, wait_until_ready};
36pub use startup::PicStartError;
37const INSTALL_CYCLES: u128 = 500 * TC;
38
39pub use standalone::{
40    StandaloneCanisterFixture, install_prebuilt_canister, install_prebuilt_canister_with_cycles,
41    install_standalone_canister, try_install_prebuilt_canister,
42    try_install_prebuilt_canister_with_cycles,
43};
44
45///
46/// Create a fresh PocketIC universe.
47///
48/// IMPORTANT:
49/// - Each call creates a new IC instance
50/// - WARNING: callers must hold a `PicSerialGuard` for the full `Pic` lifetime
51/// - Required to avoid PocketIC wasm chunk store exhaustion
52///
53#[must_use]
54pub fn pic() -> Pic {
55    try_pic().unwrap_or_else(|err| panic!("failed to start PocketIC: {err}"))
56}
57
58/// Create a fresh PocketIC universe without panicking on startup failures.
59pub fn try_pic() -> Result<Pic, PicStartError> {
60    PicBuilder::new().with_application_subnet().try_build()
61}
62
63///
64/// PicBuilder
65/// Thin wrapper around the PocketIC builder.
66///
67/// This builder is only used to configure the singleton. It does not create
68/// additional IC instances beyond the global `Pic`.
69///
70/// Note: this file is test-only infrastructure; simplicity wins over abstraction.
71///
72
73pub struct PicBuilder(PocketIcBuilder);
74
75#[expect(clippy::new_without_default)]
76impl PicBuilder {
77    /// Start a new PicBuilder with sensible defaults.
78    #[must_use]
79    pub fn new() -> Self {
80        Self(PocketIcBuilder::new())
81    }
82
83    /// Include an application subnet in the PocketIC universe.
84    #[must_use]
85    pub fn with_application_subnet(mut self) -> Self {
86        self.0 = self.0.with_application_subnet();
87        self
88    }
89
90    /// Include an II subnet so threshold keys are available in the PocketIC universe.
91    #[must_use]
92    pub fn with_ii_subnet(mut self) -> Self {
93        self.0 = self.0.with_ii_subnet();
94        self
95    }
96
97    /// Include an NNS subnet in the PocketIC universe.
98    #[must_use]
99    pub fn with_nns_subnet(mut self) -> Self {
100        self.0 = self.0.with_nns_subnet();
101        self
102    }
103
104    /// Finish building the PocketIC instance and wrap it.
105    #[must_use]
106    pub fn build(self) -> Pic {
107        self.try_build()
108            .unwrap_or_else(|err| panic!("failed to start PocketIC: {err}"))
109    }
110
111    /// Finish building the PocketIC instance without panicking on startup failures.
112    pub fn try_build(self) -> Result<Pic, PicStartError> {
113        startup::try_build_pic(self.0)
114    }
115}
116/// Pic
117/// Thin wrapper around a PocketIC instance.
118///
119/// This type intentionally exposes only a minimal API surface; callers should
120/// use `pic()` to obtain an instance and then perform installs/calls.
121/// Callers must hold a `PicSerialGuard` for the full `Pic` lifetime.
122///
123
124pub struct Pic {
125    inner: PocketIc,
126}
127
128impl Pic {
129    /// Capture the current PocketIC wall-clock time as nanoseconds since epoch.
130    #[must_use]
131    pub fn current_time_nanos(&self) -> u64 {
132        self.inner.get_time().as_nanos_since_unix_epoch()
133    }
134
135    /// Restore PocketIC wall-clock and certified time from a captured nanosecond value.
136    pub fn restore_time_nanos(&self, nanos_since_epoch: u64) {
137        let restored = pocket_ic::Time::from_nanos_since_unix_epoch(nanos_since_epoch);
138        self.inner.set_time(restored);
139        self.inner.set_certified_time(restored);
140    }
141}
142
143impl Deref for Pic {
144    type Target = PocketIc;
145
146    fn deref(&self) -> &Self::Target {
147        &self.inner
148    }
149}
150
151impl DerefMut for Pic {
152    fn deref_mut(&mut self) -> &mut Self::Target {
153        &mut self.inner
154    }
155}
156
157/// --------------------------------------
158/// install_args helper
159/// --------------------------------------
160///
161/// Init semantics:
162/// - Root canisters receive a `SubnetIdentity` (direct root bootstrap).
163/// - Non-root canisters receive `EnvBootstrapArgs` + optional directory snapshots.
164///
165/// Directory handling:
166/// - By default, directory views are empty for standalone installs.
167/// - Directory-dependent logic is opt-in via `install_args_with_directories`.
168/// - Root-provisioned installs will populate directories via cascade.
169///
170
171fn install_args(role: CanisterRole) -> Result<Vec<u8>, Error> {
172    if role.is_root() {
173        install_root_args()
174    } else {
175        // Non-root standalone install.
176        // Provide only what is structurally known at install time.
177        let env = EnvBootstrapArgs {
178            prime_root_pid: None,
179            subnet_role: None,
180            subnet_pid: None,
181            root_pid: None,
182            canister_role: Some(role),
183            parent_pid: None,
184        };
185
186        // Intentional: standalone installs do not require directories unless
187        // a test explicitly exercises directory-dependent behavior.
188        let payload = CanisterInitPayload {
189            env,
190            app_directory: AppDirectoryArgs(Vec::new()),
191            subnet_directory: SubnetDirectoryArgs(Vec::new()),
192        };
193
194        encode_args::<(CanisterInitPayload, Option<Vec<u8>>)>((payload, None))
195            .map_err(|err| Error::internal(format!("encode_args failed: {err}")))
196    }
197}
198
199fn install_root_args() -> Result<Vec<u8>, Error> {
200    encode_one(SubnetIdentity::Manual)
201        .map_err(|err| Error::internal(format!("encode_one failed: {err}")))
202}
203
204// Prefer the likely controller sender first to reduce noisy management-call failures.