ic_testkit/pic/
lifecycle.rs1use std::{num::NonZeroUsize, time::Duration};
2
3use crate::pocket_ic::{ClientError, ErrorCode, PocketIc};
4use ic_platform_types::principal::ExternalPrincipal as Principal;
5
6use super::CanisterInstallError;
7
8pub struct InstallSpec {
10 pub wasm: Vec<u8>,
12 pub init_bytes: Vec<u8>,
14 pub cycles: u128,
16 pub install_sender: Option<Principal>,
18 pub label: Option<String>,
20}
21
22#[derive(Clone, Copy, Debug, Eq, PartialEq)]
24pub struct RetryPolicy {
25 pub max_attempts: NonZeroUsize,
27 pub cooldown: Duration,
29}
30
31#[expect(
36 async_fn_in_trait,
37 reason = "extension futures may retain caller-owned non-Send callbacks, iterators or writers; pool recipes separately require Send"
38)]
39pub trait CanisterInstallExt {
40 async fn try_create_and_install(
50 &self,
51 spec: InstallSpec,
52 ) -> Result<Principal, CanisterInstallError>;
53
54 async fn wait_out_install_code_rate_limit(&self, cooldown: Duration)
60 -> Result<(), ClientError>;
61
62 async fn retry_install_code<T, F>(&self, policy: RetryPolicy, op: F) -> Result<T, ClientError>
72 where
73 F: AsyncFnMut() -> Result<T, ClientError>;
74}
75
76impl CanisterInstallExt for PocketIc {
77 async fn try_create_and_install(
79 &self,
80 spec: InstallSpec,
81 ) -> Result<Principal, CanisterInstallError> {
82 let canister_id = self.create_canister().await.map_err(|source| {
83 CanisterInstallError::CreateCanister {
84 label: spec.label.clone(),
85 source,
86 }
87 })?;
88 if spec.cycles > 0 {
89 self.add_cycles(canister_id, spec.cycles)
90 .await
91 .map_err(|source| CanisterInstallError::AddCycles {
92 canister_id,
93 label: spec.label.clone(),
94 source,
95 })?;
96 }
97
98 self.install_canister(canister_id, spec.wasm, spec.init_bytes, spec.install_sender)
100 .await
101 .map_err(|source| CanisterInstallError::InstallCode {
102 canister_id,
103 label: spec.label,
104 source,
105 })?;
106
107 Ok(canister_id)
108 }
109
110 async fn wait_out_install_code_rate_limit(
112 &self,
113 cooldown: Duration,
114 ) -> Result<(), ClientError> {
115 super::time::advance_and_tick(self, cooldown, 2).await
116 }
117
118 async fn retry_install_code<T, F>(&self, policy: RetryPolicy, op: F) -> Result<T, ClientError>
119 where
120 F: AsyncFnMut() -> Result<T, ClientError>,
121 {
122 retry_install_code_with(policy, op, async || {
123 self.wait_out_install_code_rate_limit(policy.cooldown).await
124 })
125 .await
126 }
127}
128
129async fn retry_install_code_with<T, F, W>(
130 policy: RetryPolicy,
131 mut op: F,
132 mut wait_out_cooldown: W,
133) -> Result<T, ClientError>
134where
135 F: AsyncFnMut() -> Result<T, ClientError>,
136 W: AsyncFnMut() -> Result<(), ClientError>,
137{
138 let mut retries = 1..policy.max_attempts.get();
139 loop {
140 match op().await {
141 Err(ClientError::Canister(err))
142 if err.error_code == ErrorCode::CanisterInstallCodeRateLimited
143 && retries.next().is_some() =>
144 {
145 wait_out_cooldown().await?;
146 }
147 result => return result,
148 }
149 }
150}
151
152#[cfg(test)]
153mod tests;