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