ic_testkit/pic/
lifecycle.rs1use std::panic::{AssertUnwindSafe, catch_unwind};
2use std::time::Duration;
3
4use candid::Principal;
5
6use super::{Pic, PicInstallError, startup};
7
8#[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 #[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 #[must_use]
36 pub const fn install_sender(mut self, sender: Principal) -> Self {
37 self.install_sender = Some(sender);
38 self
39 }
40
41 #[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 #[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 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 #[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 pub fn try_create_and_install(&self, spec: InstallSpec) -> Result<Principal, PicInstallError> {
84 self.try_create_funded_and_install(spec)
85 }
86
87 #[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 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 pub fn wait_out_install_code_rate_limit(&self, cooldown: Duration) {
123 self.advance_time(cooldown);
124 self.tick_n(2);
125 }
126
127 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 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 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}