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