1use std::collections::BTreeMap;
2
3use candid::Principal;
4use pocket_ic::{PocketIc, RejectResponse};
5
6use super::{PocketIcOperationError, transport};
7
8#[derive(Clone, Debug, Eq, PartialEq)]
9struct ControllerSnapshot {
10 snapshot_id: Vec<u8>,
11 sender: Option<Principal>,
12}
13
14#[derive(Clone, Debug, Eq, PartialEq)]
16pub struct ControllerSnapshots(BTreeMap<Principal, ControllerSnapshot>);
17
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20pub struct CanisterSnapshotTarget {
21 canister_id: Principal,
22 sender: Option<Principal>,
23}
24
25#[derive(Clone, Debug, Eq, PartialEq)]
27pub struct SnapshotCleanupFailure {
28 canister_id: Principal,
29 sender: Option<Principal>,
30 response: Option<Box<RejectResponse>>,
31 panic_message: Option<String>,
32}
33
34#[non_exhaustive]
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
37pub enum SnapshotRestoreFunding {
38 Preserve,
40 TopUpTo {
42 minimum_cycles: u128,
44 },
45}
46
47#[non_exhaustive]
49#[derive(Clone, Debug, Eq, PartialEq)]
50pub enum ControllerSnapshotError {
51 DuplicateCanisterId {
53 canister_id: Principal,
55 },
56 CaptureFailed {
58 canister_id: Principal,
60 sender: Option<Principal>,
62 response: Box<RejectResponse>,
64 cleanup_failures: Vec<SnapshotCleanupFailure>,
66 },
67 CapturePanicked {
69 canister_id: Principal,
71 source: PocketIcOperationError,
73 cleanup_failures: Vec<SnapshotCleanupFailure>,
75 },
76 RestoreFailed {
78 canister_id: Principal,
80 sender: Option<Principal>,
82 response: Box<RejectResponse>,
84 },
85 RestorePanicked {
87 canister_id: Principal,
89 source: PocketIcOperationError,
91 },
92}
93
94enum SnapshotCaptureFailure {
95 Rejected(RejectResponse),
96 Panicked(PocketIcOperationError),
97}
98
99pub trait PocketIcSnapshotExt {
101 fn capture_snapshots<I>(
107 &self,
108 targets: I,
109 ) -> Result<ControllerSnapshots, ControllerSnapshotError>
110 where
111 I: IntoIterator<Item = CanisterSnapshotTarget>;
112
113 fn restore_snapshots(
119 &self,
120 snapshots: &ControllerSnapshots,
121 funding: SnapshotRestoreFunding,
122 ) -> Result<(), ControllerSnapshotError>;
123}
124
125impl PocketIcSnapshotExt for PocketIc {
126 fn capture_snapshots<I>(
127 &self,
128 targets: I,
129 ) -> Result<ControllerSnapshots, ControllerSnapshotError>
130 where
131 I: IntoIterator<Item = CanisterSnapshotTarget>,
132 {
133 capture_snapshot_set(self, targets)
134 }
135
136 fn restore_snapshots(
137 &self,
138 snapshots: &ControllerSnapshots,
139 funding: SnapshotRestoreFunding,
140 ) -> Result<(), ControllerSnapshotError> {
141 for (canister_id, snapshot_id, sender) in snapshots.iter() {
142 restore_controller_snapshot(self, canister_id, snapshot_id, funding, sender)?;
143 }
144 Ok(())
145 }
146}
147
148impl CanisterSnapshotTarget {
149 #[must_use]
151 pub const fn new(canister_id: Principal, sender: Option<Principal>) -> Self {
152 Self {
153 canister_id,
154 sender,
155 }
156 }
157
158 #[must_use]
160 pub const fn canister_id(self) -> Principal {
161 self.canister_id
162 }
163
164 #[must_use]
166 pub const fn sender(self) -> Option<Principal> {
167 self.sender
168 }
169}
170
171fn capture_snapshot_set<I>(
172 pocket_ic: &PocketIc,
173 targets: I,
174) -> Result<ControllerSnapshots, ControllerSnapshotError>
175where
176 I: IntoIterator<Item = CanisterSnapshotTarget>,
177{
178 let mut ordered_targets = BTreeMap::new();
181 for target in targets {
182 if ordered_targets
183 .insert(target.canister_id, target.sender)
184 .is_some()
185 {
186 return Err(ControllerSnapshotError::DuplicateCanisterId {
187 canister_id: target.canister_id,
188 });
189 }
190 }
191 let mut snapshots = BTreeMap::new();
192 for (canister_id, sender) in ordered_targets {
193 let failure = match try_take_snapshot(pocket_ic, canister_id, sender) {
194 Ok(snapshot) => {
195 snapshots.insert(canister_id, snapshot);
196 continue;
197 }
198 Err(failure) => failure,
199 };
200 let cleanup_failures = cleanup_captured_snapshots(pocket_ic, snapshots);
201 return Err(match failure {
202 SnapshotCaptureFailure::Rejected(response) => ControllerSnapshotError::CaptureFailed {
203 canister_id,
204 sender,
205 response: Box::new(response),
206 cleanup_failures,
207 },
208 SnapshotCaptureFailure::Panicked(source) => ControllerSnapshotError::CapturePanicked {
209 canister_id,
210 source,
211 cleanup_failures,
212 },
213 });
214 }
215 Ok(ControllerSnapshots(snapshots))
216}
217
218impl ControllerSnapshots {
219 #[must_use]
221 pub fn len(&self) -> usize {
222 self.0.len()
223 }
224
225 #[must_use]
227 pub fn is_empty(&self) -> bool {
228 self.0.is_empty()
229 }
230
231 pub fn canister_ids(&self) -> impl Iterator<Item = Principal> + '_ {
233 self.0.keys().copied()
234 }
235
236 pub(super) fn iter(&self) -> impl Iterator<Item = (Principal, &[u8], Option<Principal>)> + '_ {
237 self.0.iter().map(|(canister_id, snapshot)| {
238 (
239 *canister_id,
240 snapshot.snapshot_id.as_slice(),
241 snapshot.sender,
242 )
243 })
244 }
245}
246
247impl SnapshotCleanupFailure {
248 #[must_use]
250 pub const fn canister_id(&self) -> Principal {
251 self.canister_id
252 }
253
254 #[must_use]
256 pub const fn sender(&self) -> Option<Principal> {
257 self.sender
258 }
259
260 #[must_use]
262 pub fn response(&self) -> Option<&RejectResponse> {
263 self.response.as_deref()
264 }
265
266 #[must_use]
268 pub fn panic_message(&self) -> Option<&str> {
269 self.panic_message.as_deref()
270 }
271}
272
273impl std::fmt::Display for ControllerSnapshotError {
274 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
275 match self {
276 Self::DuplicateCanisterId { canister_id } => {
277 write!(f, "duplicate canister id in snapshot set: {canister_id}")
278 }
279 Self::CaptureFailed {
280 canister_id,
281 sender,
282 response,
283 cleanup_failures,
284 } => write!(
285 f,
286 "failed to capture snapshot for {canister_id} with sender {sender:?}: {response:?}; {} partial snapshots could not be cleaned up",
287 cleanup_failures.len()
288 ),
289 Self::CapturePanicked {
290 canister_id,
291 source,
292 cleanup_failures,
293 } => write!(
294 f,
295 "snapshot capture panicked for {canister_id}: {source}; {} partial snapshots could not be cleaned up",
296 cleanup_failures.len()
297 ),
298 Self::RestoreFailed {
299 canister_id,
300 sender,
301 response,
302 } => write!(
303 f,
304 "failed to restore snapshot for {canister_id} with sender {sender:?}: {response:?}",
305 ),
306 Self::RestorePanicked {
307 canister_id,
308 source,
309 } => write!(f, "snapshot restore panicked for {canister_id}: {source}"),
310 }
311 }
312}
313
314impl std::error::Error for ControllerSnapshotError {
315 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
316 match self {
317 Self::CapturePanicked { source, .. } | Self::RestorePanicked { source, .. } => {
318 Some(source)
319 }
320 _ => None,
321 }
322 }
323}
324
325fn try_take_snapshot(
326 pocket_ic: &PocketIc,
327 canister_id: Principal,
328 sender: Option<Principal>,
329) -> Result<ControllerSnapshot, SnapshotCaptureFailure> {
330 let snapshot = transport::capture_operation(|| {
331 pocket_ic.take_canister_snapshot(canister_id, sender, None)
332 })
333 .map_err(SnapshotCaptureFailure::Panicked)?
334 .map_err(SnapshotCaptureFailure::Rejected)?;
335 Ok(ControllerSnapshot {
336 snapshot_id: snapshot.id,
337 sender,
338 })
339}
340
341fn cleanup_captured_snapshots(
342 pocket_ic: &PocketIc,
343 snapshots: BTreeMap<Principal, ControllerSnapshot>,
344) -> Vec<SnapshotCleanupFailure> {
345 let mut failures = Vec::new();
346 for (canister_id, snapshot) in snapshots {
347 let cleanup = transport::capture_operation(|| {
348 pocket_ic.delete_canister_snapshot(canister_id, snapshot.sender, snapshot.snapshot_id)
349 });
350 match cleanup {
351 Ok(Ok(())) => {}
352 Ok(Err(response)) => failures.push(SnapshotCleanupFailure {
353 canister_id,
354 sender: snapshot.sender,
355 response: Some(Box::new(response)),
356 panic_message: None,
357 }),
358 Err(source) => failures.push(SnapshotCleanupFailure {
359 canister_id,
360 sender: snapshot.sender,
361 response: None,
362 panic_message: Some(source.to_string()),
363 }),
364 }
365 }
366 failures
367}
368
369fn restore_controller_snapshot(
370 pocket_ic: &PocketIc,
371 canister_id: Principal,
372 snapshot_id: &[u8],
373 funding: SnapshotRestoreFunding,
374 sender: Option<Principal>,
375) -> Result<(), ControllerSnapshotError> {
376 transport::capture_operation(|| {
377 apply_snapshot_restore_funding(pocket_ic, canister_id, funding);
378 pocket_ic.load_canister_snapshot(canister_id, sender, snapshot_id.to_vec())
379 })
380 .map_err(|source| ControllerSnapshotError::RestorePanicked {
381 canister_id,
382 source,
383 })?
384 .map_err(|response| ControllerSnapshotError::RestoreFailed {
385 canister_id,
386 sender,
387 response: Box::new(response),
388 })
389}
390
391fn apply_snapshot_restore_funding(
392 pocket_ic: &PocketIc,
393 canister_id: Principal,
394 funding: SnapshotRestoreFunding,
395) {
396 if funding == SnapshotRestoreFunding::Preserve {
397 return;
398 }
399
400 let balance = pocket_ic.cycle_balance(canister_id);
401 let top_up = snapshot_restore_top_up(balance, funding);
402 if top_up > 0 {
403 let _ = pocket_ic.add_cycles(canister_id, top_up);
404 }
405}
406
407const fn snapshot_restore_top_up(balance: u128, funding: SnapshotRestoreFunding) -> u128 {
408 match funding {
409 SnapshotRestoreFunding::Preserve => 0,
410 SnapshotRestoreFunding::TopUpTo { minimum_cycles } => {
411 minimum_cycles.saturating_sub(balance)
412 }
413 }
414}
415
416#[cfg(test)]
417mod tests {
418 use super::{SnapshotRestoreFunding, snapshot_restore_top_up};
419
420 #[test]
421 fn snapshot_restore_funding_is_explicit() {
422 assert_eq!(
423 snapshot_restore_top_up(10, SnapshotRestoreFunding::Preserve),
424 0
425 );
426 assert_eq!(
427 snapshot_restore_top_up(10, SnapshotRestoreFunding::TopUpTo { minimum_cycles: 25 }),
428 15
429 );
430 assert_eq!(
431 snapshot_restore_top_up(30, SnapshotRestoreFunding::TopUpTo { minimum_cycles: 25 }),
432 0
433 );
434 }
435}