ic_testkit/pic/
standalone_recipe.rs1use std::io;
2
3use candid::Principal;
4
5use super::{
6 CachedPocketIcBaseline, CanisterRestoreReceipt, CycleResetPolicy, FailureDisposition,
7 FixtureRecipeId, PocketIcBaselineRecipe, PreparedBaseline, ReadinessReceipt, RebuildReason,
8 ResetReceipt, ResetRequirements, SnapshotRestoreFunding, StandaloneCanisterFixture,
9 ValidationReceipt, is_dead_pocket_ic_transport_error,
10};
11
12pub struct StandaloneCanisterRecipe<B> {
25 id: FixtureRecipeId,
26 requirements: ResetRequirements,
27 funding: SnapshotRestoreFunding,
28 build: B,
29}
30
31impl<B> StandaloneCanisterRecipe<B> {
32 #[must_use]
36 pub fn new(id: FixtureRecipeId, funding: SnapshotRestoreFunding, build: B) -> Self {
37 let cycle_policy = match funding {
38 SnapshotRestoreFunding::Preserve => CycleResetPolicy::PreserveCurrent,
39 SnapshotRestoreFunding::TopUpTo { minimum_cycles } => {
40 CycleResetPolicy::TopUpTo(minimum_cycles)
41 }
42 };
43 Self {
44 id,
45 requirements: ResetRequirements::try_new(cycle_policy, [])
46 .expect("empty non-snapshot requirements are valid"),
47 funding,
48 build,
49 }
50 }
51}
52
53impl<B> PocketIcBaselineRecipe for StandaloneCanisterRecipe<B>
54where
55 B: Fn() -> StandaloneCanisterFixture + Send + Sync + 'static,
56{
57 type Metadata = Principal;
58 type Error = io::Error;
60
61 fn id(&self) -> &FixtureRecipeId {
62 &self.id
63 }
64
65 fn reset_requirements(&self) -> &ResetRequirements {
66 &self.requirements
67 }
68
69 fn build(&self) -> Result<CachedPocketIcBaseline<Principal>, io::Error> {
70 let (pocket_ic, canister_id) = (self.build)().into_parts();
71 CachedPocketIcBaseline::capture(pocket_ic, canister_id, [canister_id], canister_id)
72 .map_err(io::Error::other)
73 }
74
75 fn restore_canisters(
76 &self,
77 baseline: &CachedPocketIcBaseline<Principal>,
78 ) -> Result<CanisterRestoreReceipt, io::Error> {
79 baseline
80 .restore_with_captured_senders_and_funding(self.funding)
81 .map_err(io::Error::other)?;
82 CanisterRestoreReceipt::try_from_baseline(baseline, self.requirements.cycle_policy())
83 .map_err(io::Error::other)
84 }
85
86 fn reset_non_snapshot_state(
87 &self,
88 _baseline: &CachedPocketIcBaseline<Principal>,
89 ) -> Result<ResetReceipt, io::Error> {
90 Ok(ResetReceipt::empty())
91 }
92
93 fn drive_to_readiness(
94 &self,
95 _baseline: &CachedPocketIcBaseline<Principal>,
96 ) -> Result<ReadinessReceipt, io::Error> {
97 ReadinessReceipt::try_new("snapshot-operation-completed").map_err(io::Error::other)
98 }
99
100 fn validate(
101 &self,
102 _baseline: &CachedPocketIcBaseline<Principal>,
103 _preparation: &PreparedBaseline,
104 ) -> Result<ValidationReceipt, io::Error> {
105 ValidationReceipt::try_new(self.id.clone(), "single-canister-snapshot-completed")
106 .map_err(io::Error::other)
107 }
108
109 fn classify_failure(
110 &self,
111 _stage: super::BaselinePreparationStage,
112 error: &io::Error,
113 ) -> FailureDisposition {
114 if is_dead_pocket_ic_transport_error(error) {
115 FailureDisposition::Rebuild(RebuildReason::DeadPocketIcTransport)
116 } else {
117 FailureDisposition::Fatal
118 }
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use std::num::NonZeroUsize;
125
126 use super::*;
127 use crate::pic::CachedPocketIcBaselinePool;
128
129 fn build_fixture() -> StandaloneCanisterFixture {
130 panic!("constructing an empty pool must not invoke its builder")
131 }
132
133 #[test]
134 fn runtime_capacity_preserves_lazy_fixture_construction() {
135 for capacity in [1, 2, 4, 8].map(|value| NonZeroUsize::new(value).unwrap()) {
136 let recipe = StandaloneCanisterRecipe::new(
137 FixtureRecipeId::try_new("lazy-single-canister").unwrap(),
138 SnapshotRestoreFunding::TopUpTo { minimum_cycles: 1 },
139 build_fixture,
140 );
141 let pool = CachedPocketIcBaselinePool::new(capacity, recipe);
142 assert_eq!(pool.capacity(), capacity);
143 }
144 }
145}