ic_testkit/pic/
standalone_recipe.rs1use std::io;
2
3use candid::Principal;
4
5use super::{
6 CachedPocketIcBaseline, CanisterRestoreReceipt, CanisterSnapshotTarget, CycleResetPolicy,
7 FailureDisposition, FixtureRecipeId, PocketIcBaselineRecipe, PreparedBaseline,
8 ReadinessReceipt, RebuildReason, ResetReceipt, ResetRequirements, SnapshotRestoreFunding,
9 StandaloneCanisterFixture, ValidationReceipt, is_dead_pocket_ic_transport_error,
10};
11
12pub struct StandaloneCanisterRecipe<B> {
25 id: FixtureRecipeId,
26 requirements: ResetRequirements,
27 funding: SnapshotRestoreFunding,
28 build: B,
29}
30
31impl<B> StandaloneCanisterRecipe<B> {
32 #[must_use]
36 pub fn new(id: FixtureRecipeId, funding: SnapshotRestoreFunding, build: B) -> Self {
37 let cycle_policy = match funding {
38 SnapshotRestoreFunding::Preserve => CycleResetPolicy::PreserveCurrent,
39 SnapshotRestoreFunding::TopUpTo { minimum_cycles } => {
40 CycleResetPolicy::TopUpTo(minimum_cycles)
41 }
42 };
43 Self {
44 id,
45 requirements: ResetRequirements::try_new(cycle_policy, [])
46 .expect("empty non-snapshot requirements are valid"),
47 funding,
48 build,
49 }
50 }
51}
52
53impl<B> PocketIcBaselineRecipe for StandaloneCanisterRecipe<B>
54where
55 B: Fn() -> StandaloneCanisterFixture + Send + Sync + 'static,
56{
57 type Metadata = Principal;
58 type Error = io::Error;
60
61 fn id(&self) -> &FixtureRecipeId {
62 &self.id
63 }
64
65 fn reset_requirements(&self) -> &ResetRequirements {
66 &self.requirements
67 }
68
69 fn build(&self) -> Result<CachedPocketIcBaseline<Principal>, io::Error> {
70 let (pocket_ic, canister_id) = (self.build)().into_parts();
71 CachedPocketIcBaseline::capture(
72 pocket_ic,
73 [CanisterSnapshotTarget::new(canister_id, None)],
74 canister_id,
75 )
76 .map_err(io::Error::other)
77 }
78
79 fn restore_canisters(
80 &self,
81 baseline: &CachedPocketIcBaseline<Principal>,
82 ) -> Result<CanisterRestoreReceipt, io::Error> {
83 baseline.restore(self.funding).map_err(io::Error::other)?;
84 CanisterRestoreReceipt::try_from_baseline(baseline, self.requirements.cycle_policy())
85 .map_err(io::Error::other)
86 }
87
88 fn reset_non_snapshot_state(
89 &self,
90 _baseline: &CachedPocketIcBaseline<Principal>,
91 ) -> Result<ResetReceipt, io::Error> {
92 Ok(ResetReceipt::empty())
93 }
94
95 fn drive_to_readiness(
96 &self,
97 _baseline: &CachedPocketIcBaseline<Principal>,
98 ) -> Result<ReadinessReceipt, io::Error> {
99 ReadinessReceipt::try_new("snapshot-operation-completed").map_err(io::Error::other)
100 }
101
102 fn validate(
103 &self,
104 _baseline: &CachedPocketIcBaseline<Principal>,
105 _preparation: &PreparedBaseline,
106 ) -> Result<ValidationReceipt, io::Error> {
107 ValidationReceipt::try_new(self.id.clone(), "single-canister-snapshot-completed")
108 .map_err(io::Error::other)
109 }
110
111 fn classify_failure(
112 &self,
113 _stage: super::BaselinePreparationStage,
114 error: &io::Error,
115 ) -> FailureDisposition {
116 if is_dead_pocket_ic_transport_error(error) {
117 FailureDisposition::Rebuild(RebuildReason::DeadPocketIcTransport)
118 } else {
119 FailureDisposition::Fatal
120 }
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use std::num::NonZeroUsize;
127
128 use super::*;
129 use crate::pic::CachedPocketIcBaselinePool;
130
131 fn build_fixture() -> StandaloneCanisterFixture {
132 panic!("constructing an empty pool must not invoke its builder")
133 }
134
135 #[test]
136 fn runtime_capacity_preserves_lazy_fixture_construction() {
137 for capacity in [1, 2, 4, 8].map(|value| NonZeroUsize::new(value).unwrap()) {
138 let recipe = StandaloneCanisterRecipe::new(
139 FixtureRecipeId::try_new("lazy-single-canister").unwrap(),
140 SnapshotRestoreFunding::TopUpTo { minimum_cycles: 1 },
141 build_fixture,
142 );
143 let pool = CachedPocketIcBaselinePool::new(capacity, recipe);
144 assert_eq!(pool.capacity(), capacity);
145 }
146 }
147}