Skip to main content

ic_testkit/pic/
standalone_recipe.rs

1use std::io;
2
3use ic_platform_types::principal::ExternalPrincipal as Principal;
4
5use super::{
6    CachedPocketIcBaseline, CanisterSnapshotTarget, FailureDisposition, PocketIcBaselineRecipe,
7    PreparedBaseline, RebuildReason, ResetPolicies, ResetReceipt, ResetRequirements,
8    SnapshotRestoreFunding, StandaloneCanisterFixture, is_dead_pocket_ic_transport_error,
9};
10
11/// Snapshot-only recipe for one caller-built, installed canister.
12///
13/// Use with [`super::CachedPocketIcBaselinePool`]. The baseline metadata is the
14/// installed canister ID; calls go through the baseline's borrowed `PocketIC`.
15/// The builder owns initialization and readiness before capture and must produce
16/// the same topology and seeded state for every slot of this recipe. It returns
17/// `io::Result<StandaloneCanisterFixture>`; build errors retain their original
18/// cause and are reported at the pool's build stage, without invoking capture.
19///
20/// Only the captured canister is restored. Time, other canisters, pending work
21/// and external resources are not reset. Use a custom recipe or a fresh instance
22/// when those mutations matter. Funding is applied immediately before restore.
23/// This recipe stops/drains its single canister before capture and warm restore,
24/// then starts it at readiness. Automatic progress must remain disabled.
25/// Non-transport restore failures are fatal: the pool discards the affected
26/// baseline before returning the error. Dead transport permits one recovery build.
27pub struct StandaloneCanisterRecipe<B> {
28    requirements: ResetRequirements,
29    build: B,
30}
31
32impl<B> StandaloneCanisterRecipe<B> {
33    /// Describe a single-canister recipe without invoking its builder.
34    ///
35    /// Static suites can initialize the recipe and pool with [`std::sync::LazyLock`].
36    #[must_use]
37    pub fn new(funding: SnapshotRestoreFunding, build: B) -> Self {
38        Self {
39            requirements: ResetRequirements {
40                funding,
41                policies: ResetPolicies::default(),
42            },
43            build,
44        }
45    }
46}
47
48impl<B, Fut> PocketIcBaselineRecipe for StandaloneCanisterRecipe<B>
49where
50    B: Fn() -> Fut + Send + Sync + 'static,
51    Fut: std::future::Future<Output = io::Result<StandaloneCanisterFixture>> + Send,
52{
53    type Metadata = Principal;
54    // Retain the typed snapshot/contract cause without another error vocabulary.
55    type Error = io::Error;
56
57    fn reset_requirements(&self) -> &ResetRequirements {
58        &self.requirements
59    }
60
61    async fn build(&self) -> Result<CachedPocketIcBaseline<Principal>, io::Error> {
62        let (pocket_ic, canister_id) = (self.build)().await?.into_parts();
63        if let Err(source) = pocket_ic.instance().stop_canister(canister_id, None).await {
64            let error = io::Error::other(super::ControllerSnapshotError::CaptureFailed {
65                canister_id,
66                sender: None,
67                source: Box::new(source),
68                cleanup_failures: Vec::new(),
69            });
70            if let Err(cleanup) = pocket_ic.close().await {
71                tracing::warn!("PocketIC deletion after stop failure failed: {cleanup}");
72            }
73            return Err(error);
74        }
75        let baseline = CachedPocketIcBaseline::capture(
76            pocket_ic,
77            [CanisterSnapshotTarget {
78                canister_id,
79                sender: None,
80            }],
81            canister_id,
82        )
83        .await
84        .map_err(io::Error::other)?;
85        if let Err(error) = self.drive_to_readiness(&baseline).await {
86            if let Err(cleanup) = baseline.close().await {
87                tracing::warn!("PocketIC deletion after readiness failure failed: {cleanup}");
88            }
89            return Err(error);
90        }
91        Ok(baseline)
92    }
93
94    fn reset_non_snapshot_state(
95        &self,
96        _baseline: &CachedPocketIcBaseline<Principal>,
97    ) -> impl std::future::Future<Output = Result<ResetReceipt, io::Error>> + Send {
98        std::future::ready(Ok(ResetReceipt::default()))
99    }
100
101    async fn prepare_restore(
102        &self,
103        baseline: &CachedPocketIcBaseline<Principal>,
104    ) -> Result<(), io::Error> {
105        baseline
106            .pocket_ic()
107            .stop_canister(*baseline.metadata(), None)
108            .await
109            .map_err(io::Error::other)
110    }
111
112    async fn drive_to_readiness(
113        &self,
114        baseline: &CachedPocketIcBaseline<Principal>,
115    ) -> Result<(), io::Error> {
116        baseline
117            .pocket_ic()
118            .start_canister(*baseline.metadata(), None)
119            .await
120            .map_err(io::Error::other)
121    }
122
123    fn validate(
124        &self,
125        _baseline: &CachedPocketIcBaseline<Principal>,
126        _preparation: &PreparedBaseline,
127    ) -> impl std::future::Future<Output = Result<(), io::Error>> + Send {
128        std::future::ready(Ok(()))
129    }
130
131    fn classify_failure(
132        &self,
133        _stage: super::BaselinePreparationStage,
134        error: &(dyn std::error::Error + 'static),
135    ) -> FailureDisposition {
136        if is_dead_pocket_ic_transport_error(error) {
137            FailureDisposition::Rebuild(RebuildReason::DeadPocketIcTransport)
138        } else {
139            FailureDisposition::Fatal
140        }
141    }
142}
143
144#[cfg(test)]
145mod tests;