Skip to main content

ic_testkit/pic/
baseline.rs

1use candid::Principal;
2use pocket_ic::PocketIc;
3use std::{
4    panic::{AssertUnwindSafe, catch_unwind, resume_unwind},
5    sync::{Mutex, MutexGuard},
6};
7
8use super::{
9    ControllerSnapshotError, ControllerSnapshots, PocketIcSnapshotExt, SnapshotRestoreFunding,
10    transport,
11};
12
13///
14/// CachedPocketIcBaseline
15///
16
17pub struct CachedPocketIcBaseline<T> {
18    pocket_ic: PocketIc,
19    snapshots: ControllerSnapshots,
20    metadata: T,
21}
22
23///
24/// CachedPocketIcBaselineGuard
25///
26
27pub struct CachedPocketIcBaselineGuard<'a, T> {
28    guard: MutexGuard<'a, Option<CachedPocketIcBaseline<T>>>,
29}
30
31enum CachedBaselineRestoreFailure {
32    DeadInstanceTransport,
33    Panic(Box<dyn std::any::Any + Send>),
34}
35
36/// Acquire one process-local cached PocketIC baseline, building it on first use.
37fn acquire_cached_pocket_ic_baseline<T, F>(
38    slot: &'static Mutex<Option<CachedPocketIcBaseline<T>>>,
39    build: F,
40) -> (CachedPocketIcBaselineGuard<'static, T>, bool)
41where
42    F: FnOnce() -> CachedPocketIcBaseline<T>,
43{
44    let mut guard = slot
45        .lock()
46        .unwrap_or_else(std::sync::PoisonError::into_inner);
47    let cache_hit = guard.is_some();
48
49    if !cache_hit {
50        *guard = Some(build());
51    }
52
53    (CachedPocketIcBaselineGuard { guard }, cache_hit)
54}
55
56/// Restore one cached PocketIC baseline, rebuilding it if the owned PocketIC
57/// instance has died between tests.
58pub fn restore_or_rebuild_cached_pocket_ic_baseline<T, B, R>(
59    slot: &'static Mutex<Option<CachedPocketIcBaseline<T>>>,
60    build: B,
61    restore: R,
62) -> (CachedPocketIcBaselineGuard<'static, T>, bool)
63where
64    B: Fn() -> CachedPocketIcBaseline<T>,
65    R: Fn(&CachedPocketIcBaseline<T>),
66{
67    let (baseline, cache_hit) = acquire_cached_pocket_ic_baseline(slot, &build);
68    if !cache_hit {
69        return (baseline, false);
70    }
71
72    match try_restore_cached_pocket_ic_baseline(
73        baseline
74            .guard
75            .as_ref()
76            .expect("cached PocketIC baseline must exist"),
77        restore,
78    ) {
79        Ok(()) => return (baseline, true),
80        Err(CachedBaselineRestoreFailure::DeadInstanceTransport) => {}
81        Err(CachedBaselineRestoreFailure::Panic(payload)) => {
82            resume_unwind(payload);
83        }
84    }
85
86    drop(baseline);
87    drop_stale_cached_pocket_ic_baseline(slot);
88
89    let (rebuilt, _cache_hit) = acquire_cached_pocket_ic_baseline(slot, build);
90    (rebuilt, false)
91}
92
93// Attempt one cached baseline restore and classify only the one recovery path
94// we intentionally swallow: a dead PocketIC transport instance.
95fn try_restore_cached_pocket_ic_baseline<T, R>(
96    baseline: &CachedPocketIcBaseline<T>,
97    restore: R,
98) -> Result<(), CachedBaselineRestoreFailure>
99where
100    R: Fn(&CachedPocketIcBaseline<T>),
101{
102    match catch_unwind(AssertUnwindSafe(|| restore(baseline))) {
103        Ok(()) => Ok(()),
104        Err(payload) => {
105            if transport::panic_is_dead_instance_transport(payload.as_ref()) {
106                Err(CachedBaselineRestoreFailure::DeadInstanceTransport)
107            } else {
108                Err(CachedBaselineRestoreFailure::Panic(payload))
109            }
110        }
111    }
112}
113
114/// Remove one dead cached baseline and swallow teardown panics from a broken
115/// PocketIC instance so callers can rebuild cleanly.
116fn drop_stale_cached_pocket_ic_baseline<T>(
117    slot: &'static Mutex<Option<CachedPocketIcBaseline<T>>>,
118) {
119    let stale = {
120        let mut slot = slot
121            .lock()
122            .unwrap_or_else(std::sync::PoisonError::into_inner);
123        slot.take()
124    };
125
126    if let Some(stale) = stale {
127        let _ = catch_unwind(AssertUnwindSafe(|| {
128            drop(stale);
129        }));
130    }
131}
132
133impl<T> CachedPocketIcBaselineGuard<'_, T> {
134    /// Borrow the owned PocketIC instance behind this cached baseline guard.
135    #[must_use]
136    pub fn pocket_ic(&self) -> &PocketIc {
137        self.guard
138            .as_ref()
139            .expect("cached PocketIC baseline must exist")
140            .pocket_ic()
141    }
142
143    /// Borrow the captured metadata behind this cached baseline guard.
144    #[must_use]
145    pub fn metadata(&self) -> &T {
146        self.guard
147            .as_ref()
148            .expect("cached PocketIC baseline must exist")
149            .metadata()
150    }
151
152    /// Mutably borrow the captured metadata behind this cached baseline guard.
153    #[must_use]
154    pub fn metadata_mut(&mut self) -> &mut T {
155        self.guard
156            .as_mut()
157            .expect("cached PocketIC baseline must exist")
158            .metadata_mut()
159    }
160
161    /// Restore the captured snapshot set without adding cycles.
162    pub fn restore(&self, controller_id: Principal) -> Result<(), ControllerSnapshotError> {
163        self.guard
164            .as_ref()
165            .expect("cached PocketIC baseline must exist")
166            .restore(controller_id)
167    }
168
169    /// Restore the captured snapshot set with an explicit cycle-funding policy.
170    pub fn restore_with_funding(
171        &self,
172        controller_id: Principal,
173        funding: SnapshotRestoreFunding,
174    ) -> Result<(), ControllerSnapshotError> {
175        self.guard
176            .as_ref()
177            .expect("cached PocketIC baseline must exist")
178            .restore_with_funding(controller_id, funding)
179    }
180}
181
182impl<T> CachedPocketIcBaseline<T> {
183    /// Capture one immutable cached baseline from the current PocketIC instance.
184    pub fn capture<I>(
185        pocket_ic: PocketIc,
186        controller_id: Principal,
187        canister_ids: I,
188        metadata: T,
189    ) -> Result<Self, ControllerSnapshotError>
190    where
191        I: IntoIterator<Item = Principal>,
192    {
193        let snapshots = pocket_ic.capture_controller_snapshots(controller_id, canister_ids)?;
194
195        Ok(Self {
196            pocket_ic,
197            snapshots,
198            metadata,
199        })
200    }
201
202    /// Restore the captured snapshot set without adding cycles.
203    pub fn restore(&self, controller_id: Principal) -> Result<(), ControllerSnapshotError> {
204        self.pocket_ic
205            .restore_controller_snapshots(controller_id, &self.snapshots)
206    }
207
208    /// Restore the captured snapshot set with an explicit cycle-funding policy.
209    pub fn restore_with_funding(
210        &self,
211        controller_id: Principal,
212        funding: SnapshotRestoreFunding,
213    ) -> Result<(), ControllerSnapshotError> {
214        self.pocket_ic.restore_controller_snapshots_with_funding(
215            controller_id,
216            &self.snapshots,
217            funding,
218        )
219    }
220
221    /// Borrow the owned PocketIC instance behind this cached baseline.
222    #[must_use]
223    pub const fn pocket_ic(&self) -> &PocketIc {
224        &self.pocket_ic
225    }
226
227    /// Borrow the captured metadata associated with this cached baseline.
228    #[must_use]
229    pub const fn metadata(&self) -> &T {
230        &self.metadata
231    }
232
233    /// Mutably borrow the captured metadata associated with this cached baseline.
234    #[must_use]
235    pub const fn metadata_mut(&mut self) -> &mut T {
236        &mut self.metadata
237    }
238}