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