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    CanisterSnapshotTarget, ControllerSnapshotError, ControllerSnapshots,
10    PocketIcCapturedSnapshotExt, PocketIcSnapshotExt, SnapshotRestoreFunding, transport,
11};
12
13/// One owned PocketIC instance with captured snapshots and caller metadata.
14///
15/// The value contains no global synchronization. Callers choose the specific
16/// [`Mutex`] slot passed to [`restore_or_rebuild_cached_pocket_ic_baseline`].
17pub struct CachedPocketIcBaseline<T> {
18    pocket_ic: PocketIc,
19    snapshots: ControllerSnapshots,
20    metadata: T,
21}
22
23/// Exclusive access to one caller-provided cached-baseline slot.
24///
25/// The slot remains locked for this guard's lifetime. Other slots and fresh
26/// PocketIC instances remain independent.
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.
58///
59/// On the first call, `build` creates the baseline and `restore` is not run.
60/// On a cache hit, `restore` runs while the slot is locked. Only a recognized
61/// dead-instance transport panic causes eviction and rebuilding; unrelated
62/// panics resume unwinding.
63///
64/// The returned boolean is `true` only when the existing baseline was restored
65/// successfully.
66pub fn restore_or_rebuild_cached_pocket_ic_baseline<T, B, R>(
67    slot: &'static Mutex<Option<CachedPocketIcBaseline<T>>>,
68    build: B,
69    restore: R,
70) -> (CachedPocketIcBaselineGuard<'static, T>, bool)
71where
72    B: Fn() -> CachedPocketIcBaseline<T>,
73    R: Fn(&CachedPocketIcBaseline<T>),
74{
75    let (baseline, cache_hit) = acquire_cached_pocket_ic_baseline(slot, &build);
76    if !cache_hit {
77        return (baseline, false);
78    }
79
80    match try_restore_cached_pocket_ic_baseline(
81        baseline
82            .guard
83            .as_ref()
84            .expect("cached PocketIC baseline must exist"),
85        restore,
86    ) {
87        Ok(()) => return (baseline, true),
88        Err(CachedBaselineRestoreFailure::DeadInstanceTransport) => {}
89        Err(CachedBaselineRestoreFailure::Panic(payload)) => {
90            resume_unwind(payload);
91        }
92    }
93
94    drop(baseline);
95    drop_stale_cached_pocket_ic_baseline(slot);
96
97    let (rebuilt, _cache_hit) = acquire_cached_pocket_ic_baseline(slot, build);
98    (rebuilt, false)
99}
100
101// Attempt one cached baseline restore and classify only the one recovery path
102// we intentionally swallow: a dead PocketIC transport instance.
103fn try_restore_cached_pocket_ic_baseline<T, R>(
104    baseline: &CachedPocketIcBaseline<T>,
105    restore: R,
106) -> Result<(), CachedBaselineRestoreFailure>
107where
108    R: Fn(&CachedPocketIcBaseline<T>),
109{
110    match catch_unwind(AssertUnwindSafe(|| restore(baseline))) {
111        Ok(()) => Ok(()),
112        Err(payload) => {
113            if transport::panic_is_dead_instance_transport(payload.as_ref()) {
114                Err(CachedBaselineRestoreFailure::DeadInstanceTransport)
115            } else {
116                Err(CachedBaselineRestoreFailure::Panic(payload))
117            }
118        }
119    }
120}
121
122/// Remove one dead cached baseline and swallow teardown panics from a broken
123/// PocketIC instance so callers can rebuild cleanly.
124fn drop_stale_cached_pocket_ic_baseline<T>(
125    slot: &'static Mutex<Option<CachedPocketIcBaseline<T>>>,
126) {
127    let stale = {
128        let mut slot = slot
129            .lock()
130            .unwrap_or_else(std::sync::PoisonError::into_inner);
131        slot.take()
132    };
133
134    if let Some(stale) = stale {
135        let _ = catch_unwind(AssertUnwindSafe(|| {
136            drop(stale);
137        }));
138    }
139}
140
141impl<T> CachedPocketIcBaselineGuard<'_, T> {
142    /// Borrow the owned PocketIC instance behind this cached baseline guard.
143    #[must_use]
144    pub fn pocket_ic(&self) -> &PocketIc {
145        self.guard
146            .as_ref()
147            .expect("cached PocketIC baseline must exist")
148            .pocket_ic()
149    }
150
151    /// Borrow the captured metadata behind this cached baseline guard.
152    #[must_use]
153    pub fn metadata(&self) -> &T {
154        self.guard
155            .as_ref()
156            .expect("cached PocketIC baseline must exist")
157            .metadata()
158    }
159
160    /// Mutably borrow the captured metadata behind this cached baseline guard.
161    #[must_use]
162    pub fn metadata_mut(&mut self) -> &mut T {
163        self.guard
164            .as_mut()
165            .expect("cached PocketIC baseline must exist")
166            .metadata_mut()
167    }
168
169    /// Restore the captured snapshot set without adding cycles.
170    pub fn restore(&self, controller_id: Principal) -> Result<(), ControllerSnapshotError> {
171        self.guard
172            .as_ref()
173            .expect("cached PocketIC baseline must exist")
174            .restore(controller_id)
175    }
176
177    /// Restore the captured snapshot set with an explicit cycle-funding policy.
178    pub fn restore_with_funding(
179        &self,
180        controller_id: Principal,
181        funding: SnapshotRestoreFunding,
182    ) -> Result<(), ControllerSnapshotError> {
183        self.guard
184            .as_ref()
185            .expect("cached PocketIC baseline must exist")
186            .restore_with_funding(controller_id, funding)
187    }
188
189    /// Restore with exactly the senders retained during snapshot capture.
190    pub fn restore_with_captured_senders(&self) -> Result<(), ControllerSnapshotError> {
191        self.guard
192            .as_ref()
193            .expect("cached PocketIC baseline must exist")
194            .restore_with_captured_senders()
195    }
196
197    /// Restore with captured senders and an explicit cycle-funding policy.
198    pub fn restore_with_captured_senders_and_funding(
199        &self,
200        funding: SnapshotRestoreFunding,
201    ) -> Result<(), ControllerSnapshotError> {
202        self.guard
203            .as_ref()
204            .expect("cached PocketIC baseline must exist")
205            .restore_with_captured_senders_and_funding(funding)
206    }
207}
208
209impl<T> CachedPocketIcBaseline<T> {
210    /// Capture one cached baseline from the current PocketIC instance.
211    ///
212    /// Snapshot capture is ordered and transactional as documented by
213    /// [`PocketIcSnapshotExt::capture_controller_snapshots`].
214    pub fn capture<I>(
215        pocket_ic: PocketIc,
216        controller_id: Principal,
217        canister_ids: I,
218        metadata: T,
219    ) -> Result<Self, ControllerSnapshotError>
220    where
221        I: IntoIterator<Item = Principal>,
222    {
223        let snapshots = pocket_ic.capture_controller_snapshots(controller_id, canister_ids)?;
224
225        Ok(Self {
226            pocket_ic,
227            snapshots,
228            metadata,
229        })
230    }
231
232    /// Capture one cached baseline with an explicit sender for every canister.
233    ///
234    /// This avoids fallback rejections in mixed-controller topologies.
235    pub fn capture_with_senders<I>(
236        pocket_ic: PocketIc,
237        targets: I,
238        metadata: T,
239    ) -> Result<Self, ControllerSnapshotError>
240    where
241        I: IntoIterator<Item = CanisterSnapshotTarget>,
242    {
243        let snapshots = pocket_ic.capture_snapshots_with_senders(targets)?;
244
245        Ok(Self {
246            pocket_ic,
247            snapshots,
248            metadata,
249        })
250    }
251
252    /// Restore the captured snapshot set without adding cycles.
253    pub fn restore(&self, controller_id: Principal) -> Result<(), ControllerSnapshotError> {
254        self.pocket_ic
255            .restore_controller_snapshots(controller_id, &self.snapshots)
256    }
257
258    /// Restore the captured snapshot set with an explicit cycle-funding policy.
259    pub fn restore_with_funding(
260        &self,
261        controller_id: Principal,
262        funding: SnapshotRestoreFunding,
263    ) -> Result<(), ControllerSnapshotError> {
264        self.pocket_ic.restore_controller_snapshots_with_funding(
265            controller_id,
266            &self.snapshots,
267            funding,
268        )
269    }
270
271    /// Restore with exactly the senders retained during snapshot capture.
272    pub fn restore_with_captured_senders(&self) -> Result<(), ControllerSnapshotError> {
273        self.pocket_ic
274            .restore_snapshots_with_captured_senders(&self.snapshots)
275    }
276
277    /// Restore with captured senders and an explicit cycle-funding policy.
278    pub fn restore_with_captured_senders_and_funding(
279        &self,
280        funding: SnapshotRestoreFunding,
281    ) -> Result<(), ControllerSnapshotError> {
282        self.pocket_ic
283            .restore_snapshots_with_captured_senders_and_funding(&self.snapshots, funding)
284    }
285
286    /// Borrow the owned PocketIC instance behind this cached baseline.
287    #[must_use]
288    pub const fn pocket_ic(&self) -> &PocketIc {
289        &self.pocket_ic
290    }
291
292    /// Return the number of canisters captured by this baseline.
293    #[must_use]
294    pub fn snapshot_count(&self) -> usize {
295        self.snapshots.len()
296    }
297
298    /// Iterate over captured canister ids in deterministic principal order.
299    pub fn snapshot_canister_ids(&self) -> impl Iterator<Item = Principal> + '_ {
300        self.snapshots.canister_ids()
301    }
302
303    /// Borrow the captured metadata associated with this cached baseline.
304    #[must_use]
305    pub const fn metadata(&self) -> &T {
306        &self.metadata
307    }
308
309    /// Mutably borrow the captured metadata associated with this cached baseline.
310    #[must_use]
311    pub const fn metadata_mut(&mut self) -> &mut T {
312        &mut self.metadata
313    }
314}