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