Skip to main content

canic_testkit/pic/
lifecycle.rs

1use std::panic::{AssertUnwindSafe, catch_unwind};
2use std::time::Duration;
3
4use candid::Principal;
5use canic::{Error, ids::CanisterRole};
6
7use super::{INSTALL_CYCLES, Pic, PicInstallError, install_args, install_root_args, startup};
8
9impl Pic {
10    /// Install a root canister with the default root init arguments.
11    pub fn create_and_install_root_canister(&self, wasm: Vec<u8>) -> Result<Principal, Error> {
12        let init_bytes = install_root_args()?;
13
14        Ok(self.create_and_install_with_args(wasm, init_bytes, INSTALL_CYCLES))
15    }
16
17    /// Install a canister with the given type and wasm bytes.
18    ///
19    /// Install failures are treated as fatal in tests.
20    pub fn create_and_install_canister(
21        &self,
22        role: CanisterRole,
23        wasm: Vec<u8>,
24    ) -> Result<Principal, Error> {
25        let init_bytes = install_args(role)?;
26
27        Ok(self.create_and_install_with_args(wasm, init_bytes, INSTALL_CYCLES))
28    }
29
30    /// Install one arbitrary wasm module with caller-provided init bytes.
31    ///
32    /// This is the generic install path for downstreams that use `canic-testkit`
33    /// without depending on Canic canister init payload conventions.
34    #[must_use]
35    pub fn create_and_install_with_args(
36        &self,
37        wasm: Vec<u8>,
38        init_bytes: Vec<u8>,
39        install_cycles: u128,
40    ) -> Principal {
41        self.try_create_and_install_with_args(wasm, init_bytes, install_cycles)
42            .unwrap_or_else(|err| panic!("{err}"))
43    }
44
45    /// Install one arbitrary wasm module with caller-provided init bytes.
46    pub fn try_create_and_install_with_args(
47        &self,
48        wasm: Vec<u8>,
49        init_bytes: Vec<u8>,
50        install_cycles: u128,
51    ) -> Result<Principal, PicInstallError> {
52        self.try_create_funded_and_install(wasm, init_bytes, install_cycles)
53    }
54
55    /// Wait until one canister reports `canic_ready`.
56    pub fn wait_for_ready(&self, canister_id: Principal, tick_limit: usize, context: &str) {
57        for _ in 0..tick_limit {
58            self.tick();
59            if self.fetch_ready(canister_id) {
60                return;
61            }
62        }
63
64        self.dump_canister_debug(canister_id, context);
65        panic!("{context}: canister {canister_id} did not become ready after {tick_limit} ticks");
66    }
67
68    /// Wait until all provided canisters report `canic_ready`.
69    pub fn wait_for_all_ready<I>(&self, canister_ids: I, tick_limit: usize, context: &str)
70    where
71        I: IntoIterator<Item = Principal>,
72    {
73        let canister_ids = canister_ids.into_iter().collect::<Vec<_>>();
74
75        for _ in 0..tick_limit {
76            self.tick();
77            if canister_ids
78                .iter()
79                .copied()
80                .all(|canister_id| self.fetch_ready(canister_id))
81            {
82                return;
83            }
84        }
85
86        for canister_id in &canister_ids {
87            self.dump_canister_debug(*canister_id, context);
88        }
89        panic!("{context}: canisters did not become ready after {tick_limit} ticks");
90    }
91
92    /// Wait out the PocketIC `install_code` cooldown window inside the same instance.
93    pub fn wait_out_install_code_rate_limit(&self, cooldown: Duration) {
94        self.advance_time(cooldown);
95        self.tick_n(2);
96    }
97
98    /// Retry one install_code-like operation while PocketIC still reports rate limiting.
99    pub fn retry_install_code_ok<T, F>(
100        &self,
101        retry_limit: usize,
102        cooldown: Duration,
103        mut op: F,
104    ) -> Result<T, String>
105    where
106        F: FnMut() -> Result<T, String>,
107    {
108        let mut last_err = None;
109
110        for _ in 0..retry_limit {
111            match op() {
112                Ok(value) => return Ok(value),
113                Err(err) if is_install_code_rate_limited(&err) => {
114                    last_err = Some(err);
115                    self.wait_out_install_code_rate_limit(cooldown);
116                }
117                Err(err) => return Err(err),
118            }
119        }
120
121        Err(last_err.unwrap_or_else(|| "install_code retry loop exhausted".to_string()))
122    }
123
124    /// Retry one install_code-like failure path while PocketIC still reports rate limiting.
125    pub fn retry_install_code_err<F>(
126        &self,
127        retry_limit: usize,
128        cooldown: Duration,
129        first: Result<(), String>,
130        mut op: F,
131    ) -> Result<(), String>
132    where
133        F: FnMut() -> Result<(), String>,
134    {
135        match first {
136            Ok(()) => return Ok(()),
137            Err(err) if !is_install_code_rate_limited(&err) => return Err(err),
138            Err(_) => {}
139        }
140
141        self.wait_out_install_code_rate_limit(cooldown);
142
143        for _ in 1..retry_limit {
144            match op() {
145                Ok(()) => return Ok(()),
146                Err(err) if is_install_code_rate_limited(&err) => {
147                    self.wait_out_install_code_rate_limit(cooldown);
148                }
149                Err(err) => return Err(err),
150            }
151        }
152
153        op()
154    }
155
156    // Install a canister after creating it and funding it with cycles.
157    fn try_create_funded_and_install(
158        &self,
159        wasm: Vec<u8>,
160        init_bytes: Vec<u8>,
161        install_cycles: u128,
162    ) -> Result<Principal, PicInstallError> {
163        let canister_id = self.create_canister();
164        self.add_cycles(canister_id, install_cycles);
165
166        let install = catch_unwind(AssertUnwindSafe(|| {
167            self.inner
168                .install_canister(canister_id, wasm, init_bytes, None);
169        }));
170        if let Err(payload) = install {
171            eprintln!("install_canister trapped for {canister_id}");
172            if let Ok(status) = self.inner.canister_status(canister_id, None) {
173                eprintln!("canister_status for {canister_id}: {status:?}");
174            }
175            if let Ok(logs) = self
176                .inner
177                .fetch_canister_logs(canister_id, Principal::anonymous())
178            {
179                for record in logs {
180                    eprintln!("canister_log {canister_id}: {record:?}");
181                }
182            }
183            return Err(PicInstallError::new(
184                canister_id,
185                startup::panic_payload_to_string(payload.as_ref()),
186            ));
187        }
188
189        Ok(canister_id)
190    }
191}
192
193fn is_install_code_rate_limited(message: &str) -> bool {
194    message.contains("CanisterInstallCodeRateLimited")
195}