ic_testkit/pic/
standalone_recipe.rs1use std::io;
2
3use candid::Principal;
4
5use super::{
6 CachedPocketIcBaseline, CanisterRestoreReceipt, CanisterSnapshotTarget, CycleResetPolicy,
7 FailureDisposition, FixtureRecipeId, PocketIcBaselineRecipe, PreparedBaseline,
8 ReadinessReceipt, RebuildReason, ResetReceipt, ResetRequirements, SnapshotRestoreFunding,
9 StandaloneCanisterFixture, ValidationReceipt, is_dead_pocket_ic_transport_error,
10};
11
12pub struct StandaloneCanisterRecipe<B> {
27 id: FixtureRecipeId,
28 requirements: ResetRequirements,
29 funding: SnapshotRestoreFunding,
30 build: B,
31}
32
33impl<B> StandaloneCanisterRecipe<B> {
34 #[must_use]
38 pub fn new(id: FixtureRecipeId, funding: SnapshotRestoreFunding, build: B) -> Self {
39 let cycle_policy = match funding {
40 SnapshotRestoreFunding::Preserve => CycleResetPolicy::PreserveCurrent,
41 SnapshotRestoreFunding::TopUpTo { minimum_cycles } => {
42 CycleResetPolicy::TopUpTo(minimum_cycles)
43 }
44 };
45 Self {
46 id,
47 requirements: ResetRequirements::try_new(cycle_policy, [])
48 .expect("empty non-snapshot requirements are valid"),
49 funding,
50 build,
51 }
52 }
53}
54
55impl<B> PocketIcBaselineRecipe for StandaloneCanisterRecipe<B>
56where
57 B: Fn() -> io::Result<StandaloneCanisterFixture> + Send + Sync + 'static,
58{
59 type Metadata = Principal;
60 type Error = io::Error;
62
63 fn id(&self) -> &FixtureRecipeId {
64 &self.id
65 }
66
67 fn reset_requirements(&self) -> &ResetRequirements {
68 &self.requirements
69 }
70
71 fn build(&self) -> Result<CachedPocketIcBaseline<Principal>, io::Error> {
72 let (pocket_ic, canister_id) = (self.build)()?.into_parts();
73 CachedPocketIcBaseline::capture(
74 pocket_ic,
75 [CanisterSnapshotTarget::new(canister_id, None)],
76 canister_id,
77 )
78 .map_err(io::Error::other)
79 }
80
81 fn restore_canisters(
82 &self,
83 baseline: &CachedPocketIcBaseline<Principal>,
84 ) -> Result<CanisterRestoreReceipt, io::Error> {
85 baseline.restore(self.funding).map_err(io::Error::other)?;
86 CanisterRestoreReceipt::try_from_baseline(baseline, self.requirements.cycle_policy())
87 .map_err(io::Error::other)
88 }
89
90 fn reset_non_snapshot_state(
91 &self,
92 _baseline: &CachedPocketIcBaseline<Principal>,
93 ) -> Result<ResetReceipt, io::Error> {
94 Ok(ResetReceipt::empty())
95 }
96
97 fn drive_to_readiness(
98 &self,
99 _baseline: &CachedPocketIcBaseline<Principal>,
100 ) -> Result<ReadinessReceipt, io::Error> {
101 ReadinessReceipt::try_new("snapshot-operation-completed").map_err(io::Error::other)
102 }
103
104 fn validate(
105 &self,
106 _baseline: &CachedPocketIcBaseline<Principal>,
107 _preparation: &PreparedBaseline,
108 ) -> Result<ValidationReceipt, io::Error> {
109 ValidationReceipt::try_new(self.id.clone(), "single-canister-snapshot-completed")
110 .map_err(io::Error::other)
111 }
112
113 fn classify_failure(
114 &self,
115 _stage: super::BaselinePreparationStage,
116 error: &io::Error,
117 ) -> FailureDisposition {
118 if is_dead_pocket_ic_transport_error(error) {
119 FailureDisposition::Rebuild(RebuildReason::DeadPocketIcTransport)
120 } else {
121 FailureDisposition::Fatal
122 }
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use std::num::NonZeroUsize;
129
130 use super::*;
131 use crate::pic::CachedPocketIcBaselinePool;
132
133 fn build_fixture() -> io::Result<StandaloneCanisterFixture> {
134 panic!("constructing an empty pool must not invoke its builder")
135 }
136
137 #[test]
138 fn runtime_capacity_preserves_lazy_fixture_construction() {
139 for capacity in [1, 2, 4, 8].map(|value| NonZeroUsize::new(value).unwrap()) {
140 let recipe = StandaloneCanisterRecipe::new(
141 FixtureRecipeId::try_new("lazy-single-canister").unwrap(),
142 SnapshotRestoreFunding::TopUpTo { minimum_cycles: 1 },
143 build_fixture,
144 );
145 let pool = CachedPocketIcBaselinePool::new(capacity, recipe);
146 assert_eq!(pool.capacity(), capacity);
147 }
148 }
149
150 #[test]
151 fn builder_errors_preserve_the_cause_and_release_the_slot_for_another_attempt() {
152 let recipe = StandaloneCanisterRecipe::new(
153 FixtureRecipeId::try_new("failed-single-canister").unwrap(),
154 SnapshotRestoreFunding::Preserve,
155 || {
156 Err(io::Error::new(
157 io::ErrorKind::PermissionDenied,
158 std::fmt::Error,
159 ))
160 },
161 );
162 let pool = CachedPocketIcBaselinePool::new(NonZeroUsize::new(1).unwrap(), recipe);
163 for _ in 0..2 {
164 let Err(super::super::BaselinePoolError::Preparation {
165 error:
166 super::super::BaselinePoolPreparationError::Recipe {
167 stage: super::super::BaselinePreparationStage::Build,
168 source,
169 },
170 timings,
171 }) = pool.acquire()
172 else {
173 panic!("builder failure must return at the build stage");
174 };
175 assert_eq!(source.kind(), io::ErrorKind::PermissionDenied);
176 assert!(source.get_ref().unwrap().is::<std::fmt::Error>());
177 assert!(timings.build().is_some());
178 assert!(timings.restore().is_none());
179 assert!(timings.total() >= timings.build().unwrap());
180 }
181 }
182}