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/// 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
190impl<T> CachedPocketIcBaseline<T> {
191    /// Capture one cached baseline from the current PocketIC instance.
192    ///
193    /// Snapshot capture is ordered and transactional as documented by
194    /// [`PocketIcSnapshotExt::capture_controller_snapshots`].
195    pub fn capture<I>(
196        pocket_ic: PocketIc,
197        controller_id: Principal,
198        canister_ids: I,
199        metadata: T,
200    ) -> Result<Self, ControllerSnapshotError>
201    where
202        I: IntoIterator<Item = Principal>,
203    {
204        let snapshots = pocket_ic.capture_controller_snapshots(controller_id, canister_ids)?;
205
206        Ok(Self {
207            pocket_ic,
208            snapshots,
209            metadata,
210        })
211    }
212
213    /// Restore the captured snapshot set without adding cycles.
214    pub fn restore(&self, controller_id: Principal) -> Result<(), ControllerSnapshotError> {
215        self.pocket_ic
216            .restore_controller_snapshots(controller_id, &self.snapshots)
217    }
218
219    /// Restore the captured snapshot set with an explicit cycle-funding policy.
220    pub fn restore_with_funding(
221        &self,
222        controller_id: Principal,
223        funding: SnapshotRestoreFunding,
224    ) -> Result<(), ControllerSnapshotError> {
225        self.pocket_ic.restore_controller_snapshots_with_funding(
226            controller_id,
227            &self.snapshots,
228            funding,
229        )
230    }
231
232    /// Borrow the owned PocketIC instance behind this cached baseline.
233    #[must_use]
234    pub const fn pocket_ic(&self) -> &PocketIc {
235        &self.pocket_ic
236    }
237
238    /// Borrow the captured metadata associated with this cached baseline.
239    #[must_use]
240    pub const fn metadata(&self) -> &T {
241        &self.metadata
242    }
243
244    /// Mutably borrow the captured metadata associated with this cached baseline.
245    #[must_use]
246    pub const fn metadata_mut(&mut self) -> &mut T {
247        &mut self.metadata
248    }
249}