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