1use std::{
2 collections::{BTreeMap, BTreeSet},
3 panic::{AssertUnwindSafe, catch_unwind},
4};
5
6use candid::Principal;
7use pocket_ic::{PocketIc, RejectResponse};
8
9use super::transport;
10
11#[derive(Clone, Debug, Eq, PartialEq)]
12struct ControllerSnapshot {
13 snapshot_id: Vec<u8>,
14 sender: Option<Principal>,
15}
16
17#[derive(Clone, Debug, Eq, PartialEq)]
19pub struct ControllerSnapshots(BTreeMap<Principal, ControllerSnapshot>);
20
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
23pub struct CanisterSnapshotTarget {
24 canister_id: Principal,
25 sender: Option<Principal>,
26}
27
28#[derive(Clone, Debug, Eq, PartialEq)]
30pub struct SnapshotAttemptFailure {
31 sender: Option<Principal>,
32 response: RejectResponse,
33}
34
35#[derive(Clone, Debug, Eq, PartialEq)]
37pub struct SnapshotCleanupFailure {
38 canister_id: Principal,
39 sender: Option<Principal>,
40 response: Option<Box<RejectResponse>>,
41 panic_message: Option<String>,
42}
43
44#[non_exhaustive]
46#[derive(Clone, Copy, Debug, Eq, PartialEq)]
47pub enum SnapshotRestoreFunding {
48 Preserve,
50 TopUpTo {
52 minimum_cycles: u128,
54 },
55}
56
57#[non_exhaustive]
59#[derive(Clone, Debug, Eq, PartialEq)]
60pub enum ControllerSnapshotError {
61 DuplicateCanisterId {
63 canister_id: Principal,
65 },
66 CaptureFailed {
68 canister_id: Principal,
70 attempts: Vec<SnapshotAttemptFailure>,
72 cleanup_failures: Vec<SnapshotCleanupFailure>,
74 },
75 CapturePanicked {
77 canister_id: Principal,
79 message: String,
81 cleanup_failures: Vec<SnapshotCleanupFailure>,
83 },
84 RestoreFailed {
86 canister_id: Principal,
88 attempts: Vec<SnapshotAttemptFailure>,
90 },
91 RestorePanicked {
93 canister_id: Principal,
95 message: String,
97 },
98}
99
100enum SnapshotCaptureFailure {
101 Rejected(Vec<SnapshotAttemptFailure>),
102 Panicked(String),
103}
104
105pub trait PocketIcSnapshotExt {
107 fn capture_snapshots_with_senders<I>(
113 &self,
114 targets: I,
115 ) -> Result<ControllerSnapshots, ControllerSnapshotError>
116 where
117 I: IntoIterator<Item = CanisterSnapshotTarget>;
118
119 fn capture_controller_snapshots<I>(
125 &self,
126 controller_id: Principal,
127 canister_ids: I,
128 ) -> Result<ControllerSnapshots, ControllerSnapshotError>
129 where
130 I: IntoIterator<Item = Principal>;
131
132 fn restore_controller_snapshots(
137 &self,
138 controller_id: Principal,
139 snapshots: &ControllerSnapshots,
140 ) -> Result<(), ControllerSnapshotError>;
141
142 fn restore_controller_snapshots_with_funding(
147 &self,
148 controller_id: Principal,
149 snapshots: &ControllerSnapshots,
150 funding: SnapshotRestoreFunding,
151 ) -> Result<(), ControllerSnapshotError>;
152
153 fn restore_snapshots_with_captured_senders(
159 &self,
160 snapshots: &ControllerSnapshots,
161 ) -> Result<(), ControllerSnapshotError>;
162
163 fn restore_snapshots_with_captured_senders_and_funding(
165 &self,
166 snapshots: &ControllerSnapshots,
167 funding: SnapshotRestoreFunding,
168 ) -> Result<(), ControllerSnapshotError>;
169}
170
171impl PocketIcSnapshotExt for PocketIc {
172 fn capture_snapshots_with_senders<I>(
173 &self,
174 targets: I,
175 ) -> Result<ControllerSnapshots, ControllerSnapshotError>
176 where
177 I: IntoIterator<Item = CanisterSnapshotTarget>,
178 {
179 let targets = ordered_unique_snapshot_targets(targets)?;
180 capture_snapshot_set(
181 self,
182 targets.into_iter().map(|target| {
183 (
184 target.canister_id,
185 std::iter::once(target.sender).collect::<Vec<_>>(),
186 )
187 }),
188 )
189 }
190
191 fn capture_controller_snapshots<I>(
192 &self,
193 controller_id: Principal,
194 canister_ids: I,
195 ) -> Result<ControllerSnapshots, ControllerSnapshotError>
196 where
197 I: IntoIterator<Item = Principal>,
198 {
199 let canister_ids = ordered_unique_canister_ids(canister_ids)?;
200 capture_snapshot_set(
201 self,
202 canister_ids.into_iter().map(|canister_id| {
203 (
204 canister_id,
205 controller_sender_candidates(controller_id, canister_id).to_vec(),
206 )
207 }),
208 )
209 }
210
211 fn restore_controller_snapshots(
212 &self,
213 controller_id: Principal,
214 snapshots: &ControllerSnapshots,
215 ) -> Result<(), ControllerSnapshotError> {
216 self.restore_controller_snapshots_with_funding(
217 controller_id,
218 snapshots,
219 SnapshotRestoreFunding::Preserve,
220 )
221 }
222
223 fn restore_controller_snapshots_with_funding(
224 &self,
225 controller_id: Principal,
226 snapshots: &ControllerSnapshots,
227 funding: SnapshotRestoreFunding,
228 ) -> Result<(), ControllerSnapshotError> {
229 for (canister_id, snapshot_id, sender) in snapshots.iter() {
230 restore_controller_snapshot(
231 self,
232 canister_id,
233 snapshot_id,
234 funding,
235 [
236 sender,
237 if sender.is_some() {
238 None
239 } else {
240 Some(controller_id)
241 },
242 ],
243 )?;
244 }
245 Ok(())
246 }
247
248 fn restore_snapshots_with_captured_senders(
249 &self,
250 snapshots: &ControllerSnapshots,
251 ) -> Result<(), ControllerSnapshotError> {
252 self.restore_snapshots_with_captured_senders_and_funding(
253 snapshots,
254 SnapshotRestoreFunding::Preserve,
255 )
256 }
257
258 fn restore_snapshots_with_captured_senders_and_funding(
259 &self,
260 snapshots: &ControllerSnapshots,
261 funding: SnapshotRestoreFunding,
262 ) -> Result<(), ControllerSnapshotError> {
263 for (canister_id, snapshot_id, sender) in snapshots.iter() {
264 restore_controller_snapshot(
265 self,
266 canister_id,
267 snapshot_id,
268 funding,
269 std::iter::once(sender),
270 )?;
271 }
272 Ok(())
273 }
274}
275
276impl CanisterSnapshotTarget {
277 #[must_use]
279 pub const fn new(canister_id: Principal, sender: Option<Principal>) -> Self {
280 Self {
281 canister_id,
282 sender,
283 }
284 }
285
286 #[must_use]
288 pub const fn canister_id(self) -> Principal {
289 self.canister_id
290 }
291
292 #[must_use]
294 pub const fn sender(self) -> Option<Principal> {
295 self.sender
296 }
297}
298
299fn capture_snapshot_set<I>(
300 pocket_ic: &PocketIc,
301 targets: I,
302) -> Result<ControllerSnapshots, ControllerSnapshotError>
303where
304 I: IntoIterator<Item = (Principal, Vec<Option<Principal>>)>,
305{
306 let mut snapshots = BTreeMap::new();
307 for (canister_id, senders) in targets {
308 match try_take_snapshot(pocket_ic, canister_id, senders) {
309 Ok(snapshot) => {
310 snapshots.insert(canister_id, snapshot);
311 }
312 Err(SnapshotCaptureFailure::Rejected(attempts)) => {
313 let cleanup_failures = cleanup_captured_snapshots(pocket_ic, &snapshots);
314 return Err(ControllerSnapshotError::CaptureFailed {
315 canister_id,
316 attempts,
317 cleanup_failures,
318 });
319 }
320 Err(SnapshotCaptureFailure::Panicked(message)) => {
321 let cleanup_failures = cleanup_captured_snapshots(pocket_ic, &snapshots);
322 return Err(ControllerSnapshotError::CapturePanicked {
323 canister_id,
324 message,
325 cleanup_failures,
326 });
327 }
328 }
329 }
330 Ok(ControllerSnapshots(snapshots))
331}
332
333impl ControllerSnapshots {
334 #[must_use]
336 pub fn len(&self) -> usize {
337 self.0.len()
338 }
339
340 #[must_use]
342 pub fn is_empty(&self) -> bool {
343 self.0.is_empty()
344 }
345
346 pub fn canister_ids(&self) -> impl Iterator<Item = Principal> + '_ {
348 self.0.keys().copied()
349 }
350
351 pub(super) fn iter(&self) -> impl Iterator<Item = (Principal, &[u8], Option<Principal>)> + '_ {
352 self.0.iter().map(|(canister_id, snapshot)| {
353 (
354 *canister_id,
355 snapshot.snapshot_id.as_slice(),
356 snapshot.sender,
357 )
358 })
359 }
360}
361
362impl SnapshotAttemptFailure {
363 #[must_use]
365 pub const fn sender(&self) -> Option<Principal> {
366 self.sender
367 }
368
369 #[must_use]
371 pub const fn response(&self) -> &RejectResponse {
372 &self.response
373 }
374}
375
376impl SnapshotCleanupFailure {
377 #[must_use]
379 pub const fn canister_id(&self) -> Principal {
380 self.canister_id
381 }
382
383 #[must_use]
385 pub const fn sender(&self) -> Option<Principal> {
386 self.sender
387 }
388
389 #[must_use]
391 pub fn response(&self) -> Option<&RejectResponse> {
392 self.response.as_deref()
393 }
394
395 #[must_use]
397 pub fn panic_message(&self) -> Option<&str> {
398 self.panic_message.as_deref()
399 }
400}
401
402impl std::fmt::Display for ControllerSnapshotError {
403 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
404 match self {
405 Self::DuplicateCanisterId { canister_id } => {
406 write!(f, "duplicate canister id in snapshot set: {canister_id}")
407 }
408 Self::CaptureFailed {
409 canister_id,
410 attempts,
411 cleanup_failures,
412 } => write!(
413 f,
414 "failed to capture snapshot for {canister_id} after {} sender attempts; {} partial snapshots could not be cleaned up",
415 attempts.len(),
416 cleanup_failures.len()
417 ),
418 Self::CapturePanicked {
419 canister_id,
420 message,
421 cleanup_failures,
422 } => write!(
423 f,
424 "snapshot capture panicked for {canister_id}: {message}; {} partial snapshots could not be cleaned up",
425 cleanup_failures.len()
426 ),
427 Self::RestoreFailed {
428 canister_id,
429 attempts,
430 } => write!(
431 f,
432 "failed to restore snapshot for {canister_id} after {} sender attempts",
433 attempts.len()
434 ),
435 Self::RestorePanicked {
436 canister_id,
437 message,
438 } => write!(f, "snapshot restore panicked for {canister_id}: {message}"),
439 }
440 }
441}
442
443impl std::error::Error for ControllerSnapshotError {}
444
445fn ordered_unique_canister_ids<I>(
446 canister_ids: I,
447) -> Result<Vec<Principal>, ControllerSnapshotError>
448where
449 I: IntoIterator<Item = Principal>,
450{
451 let mut unique = BTreeSet::new();
452 for canister_id in canister_ids {
453 if !unique.insert(canister_id) {
454 return Err(ControllerSnapshotError::DuplicateCanisterId { canister_id });
455 }
456 }
457 Ok(unique.into_iter().collect())
458}
459
460fn ordered_unique_snapshot_targets<I>(
461 targets: I,
462) -> Result<Vec<CanisterSnapshotTarget>, ControllerSnapshotError>
463where
464 I: IntoIterator<Item = CanisterSnapshotTarget>,
465{
466 let mut unique = BTreeMap::new();
467 for target in targets {
468 if unique.insert(target.canister_id, target).is_some() {
469 return Err(ControllerSnapshotError::DuplicateCanisterId {
470 canister_id: target.canister_id,
471 });
472 }
473 }
474 Ok(unique.into_values().collect())
475}
476
477fn try_take_snapshot(
478 pocket_ic: &PocketIc,
479 canister_id: Principal,
480 candidates: impl IntoIterator<Item = Option<Principal>>,
481) -> Result<ControllerSnapshot, SnapshotCaptureFailure> {
482 let mut attempts = Vec::new();
483
484 for sender in candidates {
485 let capture = catch_unwind(AssertUnwindSafe(|| {
486 pocket_ic.take_canister_snapshot(canister_id, sender, None)
487 }));
488 match capture {
489 Err(payload) => {
490 return Err(SnapshotCaptureFailure::Panicked(
491 transport::panic_payload_to_string(payload.as_ref()),
492 ));
493 }
494 Ok(snapshot) => match snapshot {
495 Ok(snapshot) => {
496 return Ok(ControllerSnapshot {
497 snapshot_id: snapshot.id,
498 sender,
499 });
500 }
501 Err(response) => attempts.push(SnapshotAttemptFailure { sender, response }),
502 },
503 }
504 }
505
506 Err(SnapshotCaptureFailure::Rejected(attempts))
507}
508
509fn cleanup_captured_snapshots(
510 pocket_ic: &PocketIc,
511 snapshots: &BTreeMap<Principal, ControllerSnapshot>,
512) -> Vec<SnapshotCleanupFailure> {
513 let mut failures = Vec::new();
514 for (canister_id, snapshot) in snapshots {
515 let cleanup = catch_unwind(AssertUnwindSafe(|| {
516 pocket_ic.delete_canister_snapshot(
517 *canister_id,
518 snapshot.sender,
519 snapshot.snapshot_id.clone(),
520 )
521 }));
522 match cleanup {
523 Ok(Ok(())) => {}
524 Ok(Err(response)) => failures.push(SnapshotCleanupFailure {
525 canister_id: *canister_id,
526 sender: snapshot.sender,
527 response: Some(Box::new(response)),
528 panic_message: None,
529 }),
530 Err(payload) => failures.push(SnapshotCleanupFailure {
531 canister_id: *canister_id,
532 sender: snapshot.sender,
533 response: None,
534 panic_message: Some(transport::panic_payload_to_string(payload.as_ref())),
535 }),
536 }
537 }
538 failures
539}
540
541fn restore_controller_snapshot(
542 pocket_ic: &PocketIc,
543 canister_id: Principal,
544 snapshot_id: &[u8],
545 funding: SnapshotRestoreFunding,
546 candidates: impl IntoIterator<Item = Option<Principal>>,
547) -> Result<(), ControllerSnapshotError> {
548 let mut attempts = Vec::new();
549
550 for sender in candidates {
551 let restore = catch_unwind(AssertUnwindSafe(|| {
552 apply_snapshot_restore_funding(pocket_ic, canister_id, funding);
553 pocket_ic.load_canister_snapshot(canister_id, sender, snapshot_id.to_vec())
554 }));
555 match restore {
556 Err(payload) => {
557 return Err(ControllerSnapshotError::RestorePanicked {
558 canister_id,
559 message: transport::panic_payload_to_string(payload.as_ref()),
560 });
561 }
562 Ok(Ok(())) => return Ok(()),
563 Ok(Err(response)) => attempts.push(SnapshotAttemptFailure { sender, response }),
564 }
565 }
566
567 Err(ControllerSnapshotError::RestoreFailed {
568 canister_id,
569 attempts,
570 })
571}
572
573fn apply_snapshot_restore_funding(
574 pocket_ic: &PocketIc,
575 canister_id: Principal,
576 funding: SnapshotRestoreFunding,
577) {
578 if funding == SnapshotRestoreFunding::Preserve {
579 return;
580 }
581
582 let balance = pocket_ic.cycle_balance(canister_id);
583 let top_up = snapshot_restore_top_up(balance, funding);
584 if top_up > 0 {
585 let _ = pocket_ic.add_cycles(canister_id, top_up);
586 }
587}
588
589const fn snapshot_restore_top_up(balance: u128, funding: SnapshotRestoreFunding) -> u128 {
590 match funding {
591 SnapshotRestoreFunding::Preserve => 0,
592 SnapshotRestoreFunding::TopUpTo { minimum_cycles } => {
593 minimum_cycles.saturating_sub(balance)
594 }
595 }
596}
597
598fn controller_sender_candidates(
599 controller_id: Principal,
600 canister_id: Principal,
601) -> [Option<Principal>; 2] {
602 if canister_id == controller_id {
603 [None, Some(controller_id)]
604 } else {
605 [Some(controller_id), None]
606 }
607}
608
609#[cfg(test)]
610mod tests {
611 use candid::Principal;
612
613 use super::{
614 ControllerSnapshotError, SnapshotRestoreFunding, ordered_unique_canister_ids,
615 snapshot_restore_top_up,
616 };
617
618 #[test]
619 fn duplicate_canister_ids_are_rejected_before_capture() {
620 let canister_id = Principal::from_slice(&[1]);
621 let error = ordered_unique_canister_ids([canister_id, canister_id]).unwrap_err();
622
623 assert_eq!(
624 error,
625 ControllerSnapshotError::DuplicateCanisterId { canister_id }
626 );
627 }
628
629 #[test]
630 fn canister_ids_are_sorted_deterministically() {
631 let first = Principal::from_slice(&[1]);
632 let second = Principal::from_slice(&[2]);
633
634 assert_eq!(
635 ordered_unique_canister_ids([second, first]).unwrap(),
636 vec![first, second]
637 );
638 }
639
640 #[test]
641 fn snapshot_restore_funding_is_explicit() {
642 assert_eq!(
643 snapshot_restore_top_up(10, SnapshotRestoreFunding::Preserve),
644 0
645 );
646 assert_eq!(
647 snapshot_restore_top_up(10, SnapshotRestoreFunding::TopUpTo { minimum_cycles: 25 }),
648 15
649 );
650 assert_eq!(
651 snapshot_restore_top_up(30, SnapshotRestoreFunding::TopUpTo { minimum_cycles: 25 }),
652 0
653 );
654 }
655}