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