ic_testkit/pic/
standalone_recipe.rs1use std::io;
2
3use candid::Principal;
4
5use super::{
6 CachedPocketIcBaseline, CanisterRestoreReceipt, CanisterSnapshotTarget, FailureDisposition,
7 FixtureRecipeId, PocketIcBaselineRecipe, PreparedBaseline, RebuildReason, ResetReceipt,
8 ResetRequirements, SnapshotRestoreFunding, StandaloneCanisterFixture,
9 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 {
70 canister_id,
71 sender: None,
72 }],
73 canister_id,
74 )
75 .map_err(io::Error::other)
76 }
77
78 fn restore_canisters(
79 &self,
80 baseline: &CachedPocketIcBaseline<Principal>,
81 ) -> Result<CanisterRestoreReceipt, io::Error> {
82 baseline.restore(self.funding).map_err(io::Error::other)
83 }
84
85 fn reset_non_snapshot_state(
86 &self,
87 _baseline: &CachedPocketIcBaseline<Principal>,
88 ) -> Result<ResetReceipt, io::Error> {
89 Ok(ResetReceipt::empty())
90 }
91
92 fn drive_to_readiness(
93 &self,
94 _baseline: &CachedPocketIcBaseline<Principal>,
95 ) -> Result<(), io::Error> {
96 Ok(())
97 }
98
99 fn validate(
100 &self,
101 _baseline: &CachedPocketIcBaseline<Principal>,
102 _preparation: &PreparedBaseline,
103 ) -> Result<(), io::Error> {
104 Ok(())
105 }
106
107 fn classify_failure(
108 &self,
109 _stage: super::BaselinePreparationStage,
110 error: &io::Error,
111 ) -> FailureDisposition {
112 if is_dead_pocket_ic_transport_error(error) {
113 FailureDisposition::Rebuild(RebuildReason::DeadPocketIcTransport)
114 } else {
115 FailureDisposition::Fatal
116 }
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use std::num::NonZeroUsize;
123
124 use super::*;
125 use crate::pic::CachedPocketIcBaselinePool;
126
127 fn build_fixture() -> io::Result<StandaloneCanisterFixture> {
128 panic!("constructing an empty pool must not invoke its builder")
129 }
130
131 #[test]
132 fn runtime_capacity_preserves_lazy_fixture_construction() {
133 for capacity in [1, 2, 4, 8].map(|value| NonZeroUsize::new(value).unwrap()) {
134 let recipe = StandaloneCanisterRecipe::new(
135 FixtureRecipeId::try_new("lazy-single-canister").unwrap(),
136 SnapshotRestoreFunding::TopUpTo { minimum_cycles: 1 },
137 build_fixture,
138 );
139 let pool = CachedPocketIcBaselinePool::new(capacity, recipe);
140 assert_eq!(pool.capacity(), capacity);
141 }
142 }
143
144 #[test]
145 fn builder_errors_preserve_the_cause_and_release_the_slot_for_another_attempt() {
146 let recipe = StandaloneCanisterRecipe::new(
147 FixtureRecipeId::try_new("failed-single-canister").unwrap(),
148 SnapshotRestoreFunding::Preserve,
149 || {
150 Err(io::Error::new(
151 io::ErrorKind::PermissionDenied,
152 std::fmt::Error,
153 ))
154 },
155 );
156 let pool = CachedPocketIcBaselinePool::new(NonZeroUsize::new(1).unwrap(), recipe);
157 for _ in 0..2 {
158 let Err(super::super::BaselinePoolError::Preparation {
159 error:
160 super::super::BaselinePoolPreparationError::Recipe {
161 stage: super::super::BaselinePreparationStage::Build,
162 source,
163 },
164 timings,
165 }) = pool.acquire()
166 else {
167 panic!("builder failure must return at the build stage");
168 };
169 assert_eq!(source.kind(), io::ErrorKind::PermissionDenied);
170 assert!(source.get_ref().unwrap().is::<std::fmt::Error>());
171 assert!(timings.build().is_some());
172 assert!(timings.restore().is_none());
173 assert!(timings.total() >= timings.build().unwrap());
174 }
175 }
176}