Skip to main content

canic_testkit/pic/
standalone.rs

1use candid::encode_args;
2use canic::{
3    dto::{
4        abi::v1::CanisterInitPayload,
5        env::EnvBootstrapArgs,
6        topology::{AppIndexArgs, SubnetIndexArgs},
7    },
8    ids::{CanisterRole, SubnetRole},
9};
10use std::{
11    path::{Path, PathBuf},
12    sync::Mutex,
13};
14
15use crate::{
16    Fake,
17    artifacts::{
18        WasmBuildProfile, build_internal_test_wasm_canisters, read_wasm, test_target_dir,
19        workspace_root_for,
20    },
21};
22
23use super::{
24    Pic, PicSerialGuard, StandaloneCanisterFixtureError, try_acquire_pic_serial_guard, try_pic,
25};
26
27const STANDALONE_INSTALL_CYCLES: u128 = 1_000_000_000_000;
28const STANDALONE_READY_TICK_LIMIT: usize = 60;
29static STANDALONE_BUILD_SERIAL: Mutex<()> = Mutex::new(());
30
31///
32/// StandaloneCanisterFixture
33///
34
35pub struct StandaloneCanisterFixture {
36    pic: Pic,
37    canister_id: canic::cdk::types::Principal,
38    _serial_guard: PicSerialGuard,
39}
40
41impl StandaloneCanisterFixture {
42    /// Borrow the PocketIC instance that owns this standalone fixture.
43    #[must_use]
44    pub const fn pic(&self) -> &Pic {
45        &self.pic
46    }
47
48    /// Mutably borrow the PocketIC instance that owns this standalone fixture.
49    #[must_use]
50    pub const fn pic_mut(&mut self) -> &mut Pic {
51        &mut self.pic
52    }
53
54    /// Read the installed canister id for this standalone fixture.
55    #[must_use]
56    pub const fn canister_id(&self) -> canic::cdk::types::Principal {
57        self.canister_id
58    }
59
60    /// Consume the fixture and return the owned PocketIC instance and canister id.
61    #[must_use]
62    pub fn into_parts(self) -> (Pic, canic::cdk::types::Principal) {
63        (self.pic, self.canister_id)
64    }
65}
66
67// Install one already-built wasm module into a fresh PocketIC instance with
68// caller-provided init args and no Canic-specific bootstrap assumptions.
69#[must_use]
70pub fn install_prebuilt_canister(wasm: Vec<u8>, init_bytes: Vec<u8>) -> StandaloneCanisterFixture {
71    try_install_prebuilt_canister(wasm, init_bytes)
72        .unwrap_or_else(|err| panic!("failed to install prebuilt canister fixture: {err}"))
73}
74
75// Install one already-built wasm module into a fresh PocketIC instance with
76// caller-provided init args and no Canic-specific bootstrap assumptions.
77pub fn try_install_prebuilt_canister(
78    wasm: Vec<u8>,
79    init_bytes: Vec<u8>,
80) -> Result<StandaloneCanisterFixture, StandaloneCanisterFixtureError> {
81    try_install_prebuilt_canister_with_cycles(wasm, init_bytes, STANDALONE_INSTALL_CYCLES)
82}
83
84// Install one already-built wasm module into a fresh PocketIC instance with
85// caller-provided init args and explicit install cycles.
86#[must_use]
87pub fn install_prebuilt_canister_with_cycles(
88    wasm: Vec<u8>,
89    init_bytes: Vec<u8>,
90    install_cycles: u128,
91) -> StandaloneCanisterFixture {
92    try_install_prebuilt_canister_with_cycles(wasm, init_bytes, install_cycles)
93        .unwrap_or_else(|err| panic!("failed to install prebuilt canister fixture: {err}"))
94}
95
96// Install one already-built wasm module into a fresh PocketIC instance with
97// caller-provided init args and explicit install cycles.
98pub fn try_install_prebuilt_canister_with_cycles(
99    wasm: Vec<u8>,
100    init_bytes: Vec<u8>,
101    install_cycles: u128,
102) -> Result<StandaloneCanisterFixture, StandaloneCanisterFixtureError> {
103    let serial_guard =
104        try_acquire_pic_serial_guard().map_err(StandaloneCanisterFixtureError::SerialGuard)?;
105    let pic = try_pic().map_err(StandaloneCanisterFixtureError::Start)?;
106    let canister_id = pic
107        .try_create_and_install_with_args(wasm, init_bytes, install_cycles)
108        .map_err(StandaloneCanisterFixtureError::Install)?;
109
110    Ok(StandaloneCanisterFixture {
111        pic,
112        canister_id,
113        _serial_guard: serial_guard,
114    })
115}
116
117// Install one non-root Canic canister into a fresh PocketIC instance with
118// explicit local env bootstrap fields, empty topology indexes, and the
119// internal test endpoint surface enabled for that test build.
120#[must_use]
121pub fn install_standalone_canister(
122    crate_name: &str,
123    role: CanisterRole,
124    profile: WasmBuildProfile,
125) -> StandaloneCanisterFixture {
126    assert!(
127        !role.is_root(),
128        "standalone helper is for non-root canisters"
129    );
130
131    let workspace_root = workspace_root();
132    let target_name = format!("standalone-{crate_name}");
133    let target_dir = test_target_dir(&workspace_root, &target_name);
134    ensure_canister_wasm_ready(&workspace_root, &target_dir, crate_name, profile);
135
136    let wasm = read_wasm(&target_dir, crate_name, profile);
137    let fixture = install_prebuilt_canister(wasm, standalone_init_args(role));
138    let canister_id = fixture.canister_id();
139    let pic = fixture.pic();
140    pic.wait_for_ready(
141        canister_id,
142        STANDALONE_READY_TICK_LIMIT,
143        "standalone canister bootstrap",
144    );
145
146    fixture
147}
148
149// Build the requested wasm artifact once per process for the shared standalone
150// target directory instead of trusting stale on-disk artifacts, and compile it
151// with the internal test endpoint surface enabled.
152fn ensure_canister_wasm_ready(
153    workspace_root: &Path,
154    target_dir: &Path,
155    crate_name: &str,
156    profile: WasmBuildProfile,
157) {
158    let _build_guard = STANDALONE_BUILD_SERIAL
159        .lock()
160        .unwrap_or_else(std::sync::PoisonError::into_inner);
161
162    build_internal_test_wasm_canisters(workspace_root, target_dir, &[crate_name], profile);
163}
164
165// Encode one explicit local non-root init payload without any preloaded
166// topology index snapshots.
167fn standalone_init_args(role: CanisterRole) -> Vec<u8> {
168    let root_pid = Fake::principal(1);
169    let payload = CanisterInitPayload {
170        env: EnvBootstrapArgs {
171            prime_root_pid: Some(root_pid),
172            subnet_role: Some(SubnetRole::PRIME),
173            subnet_pid: Some(Fake::principal(2)),
174            root_pid: Some(root_pid),
175            canister_role: Some(role),
176            parent_pid: Some(root_pid),
177        },
178        app_index: AppIndexArgs(Vec::new()),
179        subnet_index: SubnetIndexArgs(Vec::new()),
180    };
181
182    encode_args::<(CanisterInitPayload, Option<Vec<u8>>)>((payload, None))
183        .expect("encode standalone init args")
184}
185
186fn workspace_root() -> PathBuf {
187    workspace_root_for(env!("CARGO_MANIFEST_DIR"))
188}