1use std::{
2 num::NonZeroUsize,
3 ops::Deref,
4 panic::{AssertUnwindSafe, catch_unwind},
5 time::{Duration, Instant},
6};
7
8use super::{
9 CachedPocketIcBaseline, ControllerSnapshotError, SnapshotRestoreFunding,
10 bounded_pool::{BoundedSlotLease, BoundedSlotPool, Slot},
11};
12
13#[non_exhaustive]
15#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
16pub enum ResetDomainKind {
17 PocketIcTime,
19 ExtraCanisters,
21 PendingMessages,
23 SubnetState,
25 ExternalResources,
27}
28
29#[non_exhaustive]
31#[derive(Clone, Copy, Debug, Eq, PartialEq)]
32pub enum TimeResetPolicy {
33 PreserveCurrent,
35 RestoreBaseline,
37 RebuildOnMutation,
39}
40
41#[non_exhaustive]
43#[derive(Clone, Copy, Debug, Eq, PartialEq)]
44pub enum ExtraCanisterPolicy {
45 RequireBaselineSet,
47 RemoveTracked,
49 RebuildOnChange,
51}
52
53#[non_exhaustive]
55#[derive(Clone, Copy, Debug, Eq, PartialEq)]
56pub enum StateResetPolicy {
57 ResetByRecipe,
59 ValidateUnchanged,
61 IrrelevantByRecipeContract,
63 RebuildOnChange,
65}
66
67#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
95pub struct ResetPolicies {
96 pub pocket_ic_time: Option<TimeResetPolicy>,
98 pub extra_canisters: Option<ExtraCanisterPolicy>,
100 pub pending_messages: Option<StateResetPolicy>,
102 pub subnet_state: Option<StateResetPolicy>,
104 pub external_resources: Option<StateResetPolicy>,
106}
107
108#[derive(Clone, Copy, Debug, Eq, PartialEq)]
110pub struct ResetRequirements {
111 pub funding: SnapshotRestoreFunding,
113 pub policies: ResetPolicies,
115}
116
117#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
122pub struct ResetReceipt {
123 pub policies: ResetPolicies,
125}
126
127#[non_exhaustive]
129#[derive(Clone, Debug, Eq, PartialEq)]
130pub enum BaselinePoolContractError {
131 MissingResetDomain {
133 domain: ResetDomainKind,
135 },
136 ResetPolicyMismatch {
138 domain: ResetDomainKind,
140 requirement: ResetPolicies,
142 achievement: ResetPolicies,
144 },
145}
146
147#[non_exhaustive]
149#[derive(Clone, Debug, Eq, PartialEq)]
150pub enum PreparedBaseline {
151 Built,
153 Restored {
155 reset: ResetReceipt,
157 },
158}
159
160#[non_exhaustive]
162#[derive(Clone, Copy, Debug, Eq, PartialEq)]
163pub enum BaselinePreparationStage {
164 Build,
166 RestoreCanisters,
168 ResetNonSnapshotState,
170 DriveToReadiness,
172 ValidateBuilt,
174 ValidateRestored,
176}
177
178#[non_exhaustive]
180#[derive(Clone, Debug, Eq, PartialEq)]
181pub enum RebuildReason {
182 DeadPocketIcTransport,
184 SnapshotRestoreFailure,
186 ResetFailure,
188 ReadinessFailure,
190 ResetCoverageMismatch,
192 InvariantValidationFailure,
194 ExplicitLeaseInvalidation,
196 UnwindWhileLeased,
198 RecipeClassified {
200 code: String,
202 },
203}
204
205#[non_exhaustive]
207#[derive(Clone, Debug, Eq, PartialEq)]
208pub enum FailureDisposition {
209 Fatal,
211 Rebuild(RebuildReason),
213}
214
215#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
217pub struct BaselinePoolTimings {
218 wait: Duration,
219 build: Option<Duration>,
220 restore: Option<Duration>,
221 reset: Option<Duration>,
222 readiness: Option<Duration>,
223 validation: Option<Duration>,
224 stale_teardown: Option<Duration>,
225 total: Duration,
226}
227
228#[derive(Clone, Debug, Eq, PartialEq)]
230pub struct BaselinePoolOutcome {
231 pub slot: usize,
233 pub timings: BaselinePoolTimings,
235 pub disposition: BaselinePoolDisposition,
237}
238
239#[non_exhaustive]
241#[derive(Clone, Debug, Eq, PartialEq)]
242pub enum BaselinePoolDisposition {
243 Built,
245 Restored,
247 Rebuilt(RebuildReason),
249}
250
251#[non_exhaustive]
253#[derive(Debug)]
254pub enum BaselinePoolPreparationError<E> {
255 Recipe {
257 stage: BaselinePreparationStage,
259 source: E,
261 },
262 Restore(ControllerSnapshotError),
264 Contract(BaselinePoolContractError),
266}
267
268#[non_exhaustive]
270#[derive(Debug)]
271pub enum BaselinePoolError<E> {
272 Preparation {
274 error: BaselinePoolPreparationError<E>,
276 timings: Box<BaselinePoolTimings>,
278 },
279 RecoveryFailed {
281 original: Box<BaselinePoolPreparationError<E>>,
283 rebuild: Box<BaselinePoolPreparationError<E>>,
285 timings: Box<BaselinePoolTimings>,
287 },
288}
289
290pub trait PocketIcBaselineRecipe: Send + Sync + 'static {
292 type Metadata: Send + 'static;
294 type Error: std::error::Error + Send + Sync + 'static;
296
297 fn reset_requirements(&self) -> &ResetRequirements;
299
300 fn build(&self) -> Result<CachedPocketIcBaseline<Self::Metadata>, Self::Error>;
305
306 fn prepare_restore(
315 &self,
316 _baseline: &CachedPocketIcBaseline<Self::Metadata>,
317 ) -> Result<(), Self::Error> {
318 Ok(())
319 }
320
321 fn reset_non_snapshot_state(
326 &self,
327 baseline: &CachedPocketIcBaseline<Self::Metadata>,
328 ) -> Result<ResetReceipt, Self::Error>;
329
330 fn drive_to_readiness(
336 &self,
337 baseline: &CachedPocketIcBaseline<Self::Metadata>,
338 ) -> Result<(), Self::Error>;
339
340 fn validate(
346 &self,
347 baseline: &CachedPocketIcBaseline<Self::Metadata>,
348 preparation: &PreparedBaseline,
349 ) -> Result<(), Self::Error>;
350
351 fn classify_failure(
353 &self,
354 stage: BaselinePreparationStage,
355 _error: &(dyn std::error::Error + 'static),
356 ) -> FailureDisposition {
357 FailureDisposition::Rebuild(stage.default_rebuild_reason())
358 }
359}
360
361pub struct CachedPocketIcBaselinePool<R>
373where
374 R: PocketIcBaselineRecipe,
375{
376 recipe: R,
377 slots: BoundedSlotPool<CachedPocketIcBaseline<R::Metadata>, RebuildReason>,
378}
379
380pub struct CachedPocketIcBaselinePoolGuard<'a, R>
382where
383 R: PocketIcBaselineRecipe,
384{
385 slot: BoundedSlotLease<'a, CachedPocketIcBaseline<R::Metadata>, RebuildReason>,
386}
387
388impl ResetRequirements {
389 fn verify(&self, receipt: ResetReceipt) -> Result<(), BaselinePoolContractError> {
390 self.verify_policy(
391 receipt,
392 ResetDomainKind::PocketIcTime,
393 self.policies.pocket_ic_time,
394 receipt.policies.pocket_ic_time,
395 )?;
396 self.verify_policy(
397 receipt,
398 ResetDomainKind::ExtraCanisters,
399 self.policies.extra_canisters,
400 receipt.policies.extra_canisters,
401 )?;
402 self.verify_policy(
403 receipt,
404 ResetDomainKind::PendingMessages,
405 self.policies.pending_messages,
406 receipt.policies.pending_messages,
407 )?;
408 self.verify_policy(
409 receipt,
410 ResetDomainKind::SubnetState,
411 self.policies.subnet_state,
412 receipt.policies.subnet_state,
413 )?;
414 self.verify_policy(
415 receipt,
416 ResetDomainKind::ExternalResources,
417 self.policies.external_resources,
418 receipt.policies.external_resources,
419 )
420 }
421
422 fn verify_policy<P: PartialEq>(
423 &self,
424 receipt: ResetReceipt,
425 domain: ResetDomainKind,
426 requirement: Option<P>,
427 achievement: Option<P>,
428 ) -> Result<(), BaselinePoolContractError> {
429 let Some(requirement) = requirement else {
430 return Ok(());
431 };
432 let Some(achievement) = achievement else {
433 return Err(BaselinePoolContractError::MissingResetDomain { domain });
434 };
435 if requirement != achievement {
436 return Err(BaselinePoolContractError::ResetPolicyMismatch {
437 domain,
438 requirement: self.policies,
439 achievement: receipt.policies,
440 });
441 }
442 Ok(())
443 }
444}
445
446impl BaselinePreparationStage {
447 #[must_use]
452 pub fn default_rebuild_reason(self) -> RebuildReason {
453 match self {
454 Self::RestoreCanisters => RebuildReason::SnapshotRestoreFailure,
455 Self::ResetNonSnapshotState => RebuildReason::ResetFailure,
456 Self::DriveToReadiness => RebuildReason::ReadinessFailure,
457 Self::ValidateRestored | Self::ValidateBuilt => {
458 RebuildReason::InvariantValidationFailure
459 }
460 Self::Build => RebuildReason::RecipeClassified {
461 code: "build".to_owned(),
462 },
463 }
464 }
465}
466
467impl BaselinePoolTimings {
468 #[must_use]
470 pub const fn wait(self) -> Duration {
471 self.wait
472 }
473
474 #[must_use]
476 pub const fn build(self) -> Option<Duration> {
477 self.build
478 }
479
480 #[must_use]
482 pub const fn restore(self) -> Option<Duration> {
483 self.restore
484 }
485
486 #[must_use]
488 pub const fn reset(self) -> Option<Duration> {
489 self.reset
490 }
491
492 #[must_use]
494 pub const fn readiness(self) -> Option<Duration> {
495 self.readiness
496 }
497
498 #[must_use]
500 pub const fn validation(self) -> Option<Duration> {
501 self.validation
502 }
503
504 #[must_use]
506 pub const fn stale_teardown(self) -> Option<Duration> {
507 self.stale_teardown
508 }
509
510 #[must_use]
512 pub const fn total(self) -> Duration {
513 self.total
514 }
515}
516
517impl std::fmt::Display for BaselinePoolTimings {
518 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
519 write!(
520 formatter,
521 "total={:?} wait={:?} build={:?} restore={:?} reset={:?} readiness={:?} validation={:?} stale_teardown={:?}",
522 self.total,
523 self.wait,
524 self.build,
525 self.restore,
526 self.reset,
527 self.readiness,
528 self.validation,
529 self.stale_teardown,
530 )
531 }
532}
533
534impl std::fmt::Display for BaselinePoolOutcome {
535 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
536 match &self.disposition {
537 BaselinePoolDisposition::Built => formatter.write_str("built")?,
538 BaselinePoolDisposition::Restored => formatter.write_str("restored")?,
539 BaselinePoolDisposition::Rebuilt(reason) => {
540 write!(
541 formatter,
542 "rebuilt slot={} reason={reason:?} {}",
543 self.slot, self.timings
544 )?;
545 return Ok(());
546 }
547 }
548 write!(formatter, " slot={} {}", self.slot, self.timings)
549 }
550}
551
552impl<E> BaselinePoolError<E> {
553 #[must_use]
555 pub const fn timings(&self) -> BaselinePoolTimings {
556 match self {
557 Self::Preparation { timings, .. } | Self::RecoveryFailed { timings, .. } => **timings,
558 }
559 }
560}
561
562impl<R> CachedPocketIcBaselinePool<R>
563where
564 R: PocketIcBaselineRecipe,
565{
566 #[must_use]
571 pub const fn new(capacity: NonZeroUsize, recipe: R) -> Self {
572 Self {
573 recipe,
574 slots: BoundedSlotPool::new(capacity),
575 }
576 }
577
578 #[must_use]
580 pub const fn capacity(&self) -> NonZeroUsize {
581 self.slots.capacity()
582 }
583
584 #[expect(
596 clippy::significant_drop_tightening,
597 reason = "lease and coordinator scope deliberately cover the complete operation"
598 )]
599 pub fn acquire(
600 &self,
601 ) -> Result<
602 (CachedPocketIcBaselinePoolGuard<'_, R>, BaselinePoolOutcome),
603 BaselinePoolError<R::Error>,
604 > {
605 let total_started = Instant::now();
606 let mut slot = self.slots.acquire();
607 let mut timings = BaselinePoolTimings {
608 wait: slot.wait(),
609 ..BaselinePoolTimings::default()
610 };
611 let slot_index = slot.slot_index();
612
613 let (rebuild_reason, original) = match slot.state() {
614 Slot::Reusable(_) => match self.prepare_reused(&slot, &mut timings) {
615 Ok(()) => {
616 timings.total = total_started.elapsed();
617 return Ok((
618 CachedPocketIcBaselinePoolGuard { slot },
619 BaselinePoolOutcome {
620 slot: slot_index,
621 timings,
622 disposition: BaselinePoolDisposition::Restored,
623 },
624 ));
625 }
626 Err(original) => match self.failure_disposition(&original) {
627 FailureDisposition::Fatal => {
628 Self::discard_stale_slot(&mut slot, &mut timings);
631 timings.total = total_started.elapsed();
632 return Err(BaselinePoolError::Preparation {
633 error: original,
634 timings: Box::new(timings),
635 });
636 }
637 FailureDisposition::Rebuild(reason) => (Some(reason), Some(original)),
638 },
639 },
640 Slot::Empty => (None, None),
641 Slot::Invalidated { reason, .. } => (Some(reason.clone()), None),
642 Slot::Unwound(_) => (Some(RebuildReason::UnwindWhileLeased), None),
643 };
644
645 if slot.get().is_some() {
646 Self::discard_stale_slot(&mut slot, &mut timings);
647 }
648 let build = self.build_slot(&mut slot, &mut timings);
649 timings.total = total_started.elapsed();
650 if let Err(error) = build {
651 return Err(match original {
652 Some(original) => BaselinePoolError::RecoveryFailed {
653 original: Box::new(original),
654 rebuild: Box::new(error),
655 timings: Box::new(timings),
656 },
657 None => BaselinePoolError::Preparation {
658 error,
659 timings: Box::new(timings),
660 },
661 });
662 }
663
664 let outcome = BaselinePoolOutcome {
665 slot: slot_index,
666 timings,
667 disposition: rebuild_reason.map_or(
668 BaselinePoolDisposition::Built,
669 BaselinePoolDisposition::Rebuilt,
670 ),
671 };
672 Ok((CachedPocketIcBaselinePoolGuard { slot }, outcome))
673 }
674
675 fn prepare_reused(
676 &self,
677 slot: &BoundedSlotLease<'_, CachedPocketIcBaseline<R::Metadata>, RebuildReason>,
678 timings: &mut BaselinePoolTimings,
679 ) -> Result<(), BaselinePoolPreparationError<R::Error>> {
680 let baseline = slot
681 .get()
682 .expect("reusable baseline slot must be populated");
683
684 let started = Instant::now();
685 let restore = self
686 .recipe
687 .prepare_restore(baseline)
688 .map_err(|source| BaselinePoolPreparationError::Recipe {
689 stage: BaselinePreparationStage::RestoreCanisters,
690 source,
691 })
692 .and_then(|()| {
693 baseline
694 .restore(self.recipe.reset_requirements().funding)
695 .map_err(BaselinePoolPreparationError::Restore)
696 });
697 add_timing(&mut timings.restore, started.elapsed());
698 restore?;
699
700 let reset = timed_recipe_stage(
701 &mut timings.reset,
702 BaselinePreparationStage::ResetNonSnapshotState,
703 || self.recipe.reset_non_snapshot_state(baseline),
704 )?;
705 timed_recipe_stage(
706 &mut timings.readiness,
707 BaselinePreparationStage::DriveToReadiness,
708 || self.recipe.drive_to_readiness(baseline),
709 )?;
710 self.recipe
711 .reset_requirements()
712 .verify(reset)
713 .map_err(BaselinePoolPreparationError::Contract)?;
714
715 let preparation = PreparedBaseline::Restored { reset };
716 self.validate_baseline(
717 baseline,
718 &preparation,
719 BaselinePreparationStage::ValidateRestored,
720 timings,
721 )?;
722 Ok(())
723 }
724
725 fn build_slot(
726 &self,
727 slot: &mut BoundedSlotLease<'_, CachedPocketIcBaseline<R::Metadata>, RebuildReason>,
728 timings: &mut BaselinePoolTimings,
729 ) -> Result<(), BaselinePoolPreparationError<R::Error>> {
730 let baseline =
731 timed_recipe_stage(&mut timings.build, BaselinePreparationStage::Build, || {
732 self.recipe.build()
733 })?;
734
735 if let Err(error) = self.validate_baseline(
736 &baseline,
737 &PreparedBaseline::Built,
738 BaselinePreparationStage::ValidateBuilt,
739 timings,
740 ) {
741 drop_baseline_safely(baseline);
742 return Err(error);
743 }
744 let replaced = slot.replace(baseline);
745 debug_assert!(replaced.is_none());
746 Ok(())
747 }
748
749 fn validate_baseline(
750 &self,
751 baseline: &CachedPocketIcBaseline<R::Metadata>,
752 preparation: &PreparedBaseline,
753 stage: BaselinePreparationStage,
754 timings: &mut BaselinePoolTimings,
755 ) -> Result<(), BaselinePoolPreparationError<R::Error>> {
756 timed_recipe_stage(&mut timings.validation, stage, || {
757 self.recipe.validate(baseline, preparation)
758 })
759 }
760
761 fn failure_disposition(
762 &self,
763 error: &BaselinePoolPreparationError<R::Error>,
764 ) -> FailureDisposition {
765 match error {
766 BaselinePoolPreparationError::Recipe { stage, source } => {
767 self.recipe.classify_failure(*stage, source)
768 }
769 BaselinePoolPreparationError::Restore(source) => self
770 .recipe
771 .classify_failure(BaselinePreparationStage::RestoreCanisters, source),
772 BaselinePoolPreparationError::Contract(_) => {
773 FailureDisposition::Rebuild(RebuildReason::ResetCoverageMismatch)
774 }
775 }
776 }
777
778 fn discard_stale_slot(
779 slot: &mut BoundedSlotLease<'_, CachedPocketIcBaseline<R::Metadata>, RebuildReason>,
780 timings: &mut BaselinePoolTimings,
781 ) {
782 let started = Instant::now();
783 if let Some(stale) = slot.take() {
784 drop_baseline_safely(stale);
785 }
786 timings.stale_teardown = Some(started.elapsed());
787 }
788}
789
790impl<R> CachedPocketIcBaselinePoolGuard<'_, R>
791where
792 R: PocketIcBaselineRecipe,
793{
794 #[must_use]
796 pub const fn slot(&self) -> usize {
797 self.slot.slot_index()
798 }
799
800 pub fn invalidate(&mut self, reason: RebuildReason) {
802 self.slot.invalidate(reason);
803 }
804}
805
806impl<R> Deref for CachedPocketIcBaselinePoolGuard<'_, R>
807where
808 R: PocketIcBaselineRecipe,
809{
810 type Target = CachedPocketIcBaseline<R::Metadata>;
811
812 fn deref(&self) -> &Self::Target {
813 self.slot
814 .get()
815 .expect("leased baseline pool slot must be populated")
816 }
817}
818
819fn timed_recipe_stage<T, E>(
820 timing: &mut Option<Duration>,
821 stage: BaselinePreparationStage,
822 operation: impl FnOnce() -> Result<T, E>,
823) -> Result<T, BaselinePoolPreparationError<E>> {
824 let started = Instant::now();
825 let result = operation();
826 add_timing(timing, started.elapsed());
827 result.map_err(|source| BaselinePoolPreparationError::Recipe { stage, source })
828}
829
830const fn add_timing(total: &mut Option<Duration>, elapsed: Duration) {
831 *total = Some(match *total {
832 None => elapsed,
833 Some(previous) => previous.saturating_add(elapsed),
834 });
835}
836
837fn drop_baseline_safely<M>(baseline: CachedPocketIcBaseline<M>) {
838 let _ = catch_unwind(AssertUnwindSafe(|| drop(baseline)));
839}
840
841impl std::fmt::Display for BaselinePreparationStage {
842 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
843 formatter.write_str(match self {
844 Self::Build => "build",
845 Self::RestoreCanisters => "canister restore",
846 Self::ResetNonSnapshotState => "non-snapshot reset",
847 Self::DriveToReadiness => "readiness",
848 Self::ValidateBuilt => "built-baseline validation",
849 Self::ValidateRestored => "restored-baseline validation",
850 })
851 }
852}
853
854impl std::fmt::Display for BaselinePoolContractError {
855 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
856 match self {
857 Self::MissingResetDomain { domain } => {
858 write!(
859 formatter,
860 "required reset domain {domain:?} was not achieved"
861 )
862 }
863 Self::ResetPolicyMismatch {
864 domain,
865 requirement,
866 achievement,
867 } => write!(
868 formatter,
869 "reset achievement {achievement:?} does not satisfy {requirement:?} for {domain:?}",
870 ),
871 }
872 }
873}
874
875impl std::error::Error for BaselinePoolContractError {}
876
877impl<E> std::fmt::Display for BaselinePoolPreparationError<E>
878where
879 E: std::fmt::Display,
880{
881 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
882 match self {
883 Self::Recipe { stage, source } => {
884 write!(formatter, "baseline {stage} failed: {source}")
885 }
886 Self::Restore(source) => {
887 write!(formatter, "baseline canister restore failed: {source}")
888 }
889 Self::Contract(error) => write!(formatter, "baseline pool contract failed: {error}"),
890 }
891 }
892}
893
894impl<E> std::error::Error for BaselinePoolPreparationError<E>
895where
896 E: std::error::Error + 'static,
897{
898 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
899 match self {
900 Self::Recipe { source, .. } => Some(source),
901 Self::Restore(source) => Some(source),
902 Self::Contract(error) => Some(error),
903 }
904 }
905}
906
907impl<E> std::fmt::Display for BaselinePoolError<E>
908where
909 E: std::fmt::Display,
910{
911 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
912 match self {
913 Self::Preparation { error, .. } => error.fmt(formatter),
914 Self::RecoveryFailed {
915 original, rebuild, ..
916 } => write!(
917 formatter,
918 "baseline preparation failed ({original}); rebuilding the slot also failed: {rebuild}",
919 ),
920 }
921 }
922}
923
924impl<E> std::error::Error for BaselinePoolError<E>
925where
926 E: std::error::Error + 'static,
927{
928 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
929 match self {
930 Self::Preparation { error, .. } => Some(error),
931 Self::RecoveryFailed { original, .. } => Some(original.as_ref()),
932 }
933 }
934}
935
936#[cfg(test)]
937mod tests {
938 use super::{
939 BaselinePoolContractError, BaselinePoolTimings, ExtraCanisterPolicy, ResetDomainKind,
940 ResetPolicies, ResetReceipt, ResetRequirements, SnapshotRestoreFunding, StateResetPolicy,
941 TimeResetPolicy, add_timing,
942 };
943 use std::time::Duration;
944
945 #[test]
946 fn phase_timing_preserves_absence_zero_and_saturation() {
947 let mut timings = BaselinePoolTimings::default();
948 assert_eq!(timings.restore(), None);
949 add_timing(&mut timings.restore, Duration::ZERO);
950 assert_eq!(timings.restore(), Some(Duration::ZERO));
951 add_timing(&mut timings.restore, Duration::from_secs(2));
952 assert_eq!(timings.restore(), Some(Duration::from_secs(2)));
953 add_timing(&mut timings.restore, Duration::MAX);
954 assert_eq!(timings.restore(), Some(Duration::MAX));
955 assert_eq!(timings.build(), None);
956 }
957
958 #[test]
959 fn required_policy_must_match_achieved_policy() {
960 for (domain, requirement, achievement) in [
961 (
962 ResetDomainKind::PocketIcTime,
963 ResetPolicies {
964 pocket_ic_time: Some(TimeResetPolicy::PreserveCurrent),
965 ..ResetPolicies::default()
966 },
967 ResetPolicies {
968 pocket_ic_time: Some(TimeResetPolicy::RebuildOnMutation),
969 ..ResetPolicies::default()
970 },
971 ),
972 (
973 ResetDomainKind::ExtraCanisters,
974 ResetPolicies {
975 extra_canisters: Some(ExtraCanisterPolicy::RequireBaselineSet),
976 ..ResetPolicies::default()
977 },
978 ResetPolicies {
979 extra_canisters: Some(ExtraCanisterPolicy::RemoveTracked),
980 ..ResetPolicies::default()
981 },
982 ),
983 (
984 ResetDomainKind::PendingMessages,
985 ResetPolicies {
986 pending_messages: Some(StateResetPolicy::ValidateUnchanged),
987 ..ResetPolicies::default()
988 },
989 ResetPolicies {
990 pending_messages: Some(StateResetPolicy::IrrelevantByRecipeContract),
991 ..ResetPolicies::default()
992 },
993 ),
994 (
995 ResetDomainKind::SubnetState,
996 ResetPolicies {
997 subnet_state: Some(StateResetPolicy::ResetByRecipe),
998 ..ResetPolicies::default()
999 },
1000 ResetPolicies {
1001 subnet_state: Some(StateResetPolicy::RebuildOnChange),
1002 ..ResetPolicies::default()
1003 },
1004 ),
1005 (
1006 ResetDomainKind::ExternalResources,
1007 ResetPolicies {
1008 external_resources: Some(StateResetPolicy::ValidateUnchanged),
1009 ..ResetPolicies::default()
1010 },
1011 ResetPolicies {
1012 external_resources: Some(StateResetPolicy::ResetByRecipe),
1013 ..ResetPolicies::default()
1014 },
1015 ),
1016 ] {
1017 let requirements = ResetRequirements {
1018 funding: SnapshotRestoreFunding::Preserve,
1019 policies: requirement,
1020 };
1021 assert_eq!(
1022 requirements.verify(ResetReceipt {
1023 policies: requirement
1024 }),
1025 Ok(())
1026 );
1027 assert_eq!(
1028 requirements.verify(ResetReceipt::default()),
1029 Err(BaselinePoolContractError::MissingResetDomain { domain })
1030 );
1031 assert_eq!(
1032 requirements.verify(ResetReceipt {
1033 policies: achievement
1034 }),
1035 Err(BaselinePoolContractError::ResetPolicyMismatch {
1036 domain,
1037 requirement,
1038 achievement
1039 })
1040 );
1041 let unconstrained = ResetRequirements {
1042 funding: SnapshotRestoreFunding::Preserve,
1043 policies: ResetPolicies::default(),
1044 };
1045 assert_eq!(
1046 unconstrained.verify(ResetReceipt {
1047 policies: achievement
1048 }),
1049 Ok(())
1050 );
1051 }
1052 }
1053
1054 #[test]
1055 fn reset_coverage_preserves_absence_and_first_failure() {
1056 let requirements = ResetRequirements {
1057 funding: SnapshotRestoreFunding::Preserve,
1058 policies: ResetPolicies {
1059 pocket_ic_time: Some(TimeResetPolicy::PreserveCurrent),
1060 pending_messages: Some(StateResetPolicy::IrrelevantByRecipeContract),
1061 ..ResetPolicies::default()
1062 },
1063 };
1064 let mut receipt = ResetReceipt::default();
1065 assert_eq!(
1066 requirements.verify(receipt),
1067 Err(BaselinePoolContractError::MissingResetDomain {
1068 domain: ResetDomainKind::PocketIcTime,
1069 })
1070 );
1071 receipt.policies.pocket_ic_time = Some(TimeResetPolicy::RebuildOnMutation);
1072 assert_eq!(
1073 requirements.verify(receipt),
1074 Err(BaselinePoolContractError::ResetPolicyMismatch {
1075 domain: ResetDomainKind::PocketIcTime,
1076 requirement: requirements.policies,
1077 achievement: receipt.policies,
1078 })
1079 );
1080 receipt.policies.pocket_ic_time = Some(TimeResetPolicy::PreserveCurrent);
1081 assert_eq!(
1082 requirements.verify(receipt),
1083 Err(BaselinePoolContractError::MissingResetDomain {
1084 domain: ResetDomainKind::PendingMessages,
1085 })
1086 );
1087 receipt.policies.pending_messages = Some(StateResetPolicy::IrrelevantByRecipeContract);
1088 assert_eq!(requirements.verify(receipt), Ok(()));
1089 }
1090}