Skip to main content

ic_testkit/pic/
lifecycle.rs

1use std::panic::{AssertUnwindSafe, catch_unwind};
2use std::time::Duration;
3
4use candid::Principal;
5
6use super::{Pic, PicInstallError, startup};
7
8///
9/// InstallSpec
10///
11
12#[non_exhaustive]
13pub struct InstallSpec {
14    pub wasm: Vec<u8>,
15    pub init_bytes: Vec<u8>,
16    pub cycles: u128,
17    pub install_sender: Option<Principal>,
18    pub label: Option<String>,
19}
20
21impl InstallSpec {
22    /// Build one generic canister install specification.
23    #[must_use]
24    pub const fn new(wasm: Vec<u8>, init_bytes: Vec<u8>, cycles: u128) -> Self {
25        Self {
26            wasm,
27            init_bytes,
28            cycles,
29            install_sender: None,
30            label: None,
31        }
32    }
33
34    /// Set the management-call sender used for `install_canister`.
35    #[must_use]
36    pub const fn install_sender(mut self, sender: Principal) -> Self {
37        self.install_sender = Some(sender);
38        self
39    }
40
41    /// Set a diagnostic label for install failures.
42    #[must_use]
43    pub fn label(mut self, label: impl Into<String>) -> Self {
44        self.label = Some(label.into());
45        self
46    }
47}
48
49impl Pic {
50    /// Install one arbitrary wasm module with caller-provided init bytes.
51    ///
52    /// This is the generic install path for downstreams that use `ic-testkit`
53    /// without depending on application-specific init payload conventions.
54    #[must_use]
55    pub fn create_and_install_with_args(
56        &self,
57        wasm: Vec<u8>,
58        init_bytes: Vec<u8>,
59        install_cycles: u128,
60    ) -> Principal {
61        self.try_create_and_install_with_args(wasm, init_bytes, install_cycles)
62            .unwrap_or_else(|err| panic!("{err}"))
63    }
64
65    /// Install one arbitrary wasm module with caller-provided init bytes.
66    pub fn try_create_and_install_with_args(
67        &self,
68        wasm: Vec<u8>,
69        init_bytes: Vec<u8>,
70        install_cycles: u128,
71    ) -> Result<Principal, PicInstallError> {
72        self.try_create_and_install(InstallSpec::new(wasm, init_bytes, install_cycles))
73    }
74
75    /// Install one arbitrary wasm module from a generic install specification.
76    #[must_use]
77    pub fn create_and_install(&self, spec: InstallSpec) -> Principal {
78        self.try_create_and_install(spec)
79            .unwrap_or_else(|err| panic!("{err}"))
80    }
81
82    /// Install one arbitrary wasm module from a generic install specification.
83    pub fn try_create_and_install(&self, spec: InstallSpec) -> Result<Principal, PicInstallError> {
84        self.try_create_funded_and_install(spec)
85    }
86
87    /// Sequentially install multiple arbitrary wasm modules into this `Pic`.
88    ///
89    /// Installs are attempted in iterator order. If one install fails, earlier
90    /// installs remain in the PocketIC instance, the failed canister may exist
91    /// with the id exposed by `PicInstallError::canister_id()`, and later
92    /// installs are not attempted.
93    #[must_use]
94    pub fn create_and_install_many<I>(&self, specs: I) -> Vec<Principal>
95    where
96        I: IntoIterator<Item = InstallSpec>,
97    {
98        self.try_create_and_install_many(specs)
99            .unwrap_or_else(|err| panic!("{err}"))
100    }
101
102    /// Sequentially install multiple arbitrary wasm modules into this `Pic`.
103    ///
104    /// Installs are attempted in iterator order. If one install fails, earlier
105    /// installs remain in the PocketIC instance, the failed canister may exist
106    /// with the id exposed by `PicInstallError::canister_id()`, and later
107    /// installs are not attempted.
108    pub fn try_create_and_install_many<I>(
109        &self,
110        specs: I,
111    ) -> Result<Vec<Principal>, PicInstallError>
112    where
113        I: IntoIterator<Item = InstallSpec>,
114    {
115        specs
116            .into_iter()
117            .map(|spec| self.try_create_and_install(spec))
118            .collect()
119    }
120
121    /// Wait out the PocketIC `install_code` cooldown window inside the same instance.
122    pub fn wait_out_install_code_rate_limit(&self, cooldown: Duration) {
123        self.advance_time(cooldown);
124        self.tick_n(2);
125    }
126
127    /// Retry one install_code-like operation while PocketIC still reports rate limiting.
128    pub fn retry_install_code_ok<T, F>(
129        &self,
130        retry_limit: usize,
131        cooldown: Duration,
132        mut op: F,
133    ) -> Result<T, String>
134    where
135        F: FnMut() -> Result<T, String>,
136    {
137        let mut last_err = None;
138
139        for _ in 0..retry_limit {
140            match op() {
141                Ok(value) => return Ok(value),
142                Err(err) if is_install_code_rate_limited(&err) => {
143                    last_err = Some(err);
144                    self.wait_out_install_code_rate_limit(cooldown);
145                }
146                Err(err) => return Err(err),
147            }
148        }
149
150        Err(last_err.unwrap_or_else(|| "install_code retry loop exhausted".to_string()))
151    }
152
153    /// Retry one install_code-like failure path while PocketIC still reports rate limiting.
154    pub fn retry_install_code_err<F>(
155        &self,
156        retry_limit: usize,
157        cooldown: Duration,
158        first: Result<(), String>,
159        mut op: F,
160    ) -> Result<(), String>
161    where
162        F: FnMut() -> Result<(), String>,
163    {
164        match first {
165            Ok(()) => return Ok(()),
166            Err(err) if !is_install_code_rate_limited(&err) => return Err(err),
167            Err(_) => {}
168        }
169
170        self.wait_out_install_code_rate_limit(cooldown);
171
172        for _ in 1..retry_limit {
173            match op() {
174                Ok(()) => return Ok(()),
175                Err(err) if is_install_code_rate_limited(&err) => {
176                    self.wait_out_install_code_rate_limit(cooldown);
177                }
178                Err(err) => return Err(err),
179            }
180        }
181
182        op()
183    }
184
185    // Install a canister after creating it and optionally adding extra cycles.
186    fn try_create_funded_and_install(
187        &self,
188        spec: InstallSpec,
189    ) -> Result<Principal, PicInstallError> {
190        let canister_id = self.create_canister();
191        if spec.cycles > 0 {
192            self.add_cycles(canister_id, spec.cycles);
193        }
194
195        let install = catch_unwind(AssertUnwindSafe(|| {
196            self.inner.install_canister(
197                canister_id,
198                spec.wasm,
199                spec.init_bytes,
200                spec.install_sender,
201            );
202        }));
203        if let Err(payload) = install {
204            if let Some(label) = &spec.label {
205                eprintln!("install_canister trapped for {canister_id} ({label})");
206            } else {
207                eprintln!("install_canister trapped for {canister_id}");
208            }
209            if let Ok(status) = self.inner.canister_status(canister_id, None) {
210                eprintln!("canister_status for {canister_id}: {status:?}");
211            }
212            if let Ok(logs) = self
213                .inner
214                .fetch_canister_logs(canister_id, Principal::anonymous())
215            {
216                for record in logs {
217                    eprintln!("canister_log {canister_id}: {record:?}");
218                }
219            }
220            let message = startup::panic_payload_to_string(payload.as_ref());
221            return if let Some(label) = spec.label {
222                Err(PicInstallError::labeled(canister_id, label, message))
223            } else {
224                Err(PicInstallError::new(canister_id, message))
225            };
226        }
227
228        Ok(canister_id)
229    }
230}
231
232fn is_install_code_rate_limited(message: &str) -> bool {
233    message.contains("CanisterInstallCodeRateLimited")
234}