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, thiserror::Error)]
130pub enum BaselinePoolContractError {
131 #[error("required reset domain {domain:?} was not achieved")]
133 MissingResetDomain {
134 domain: ResetDomainKind,
136 },
137 #[error("reset achievement {achievement:?} does not satisfy {requirement:?} for {domain:?}")]
139 ResetPolicyMismatch {
140 domain: ResetDomainKind,
142 requirement: ResetPolicies,
144 achievement: ResetPolicies,
146 },
147}
148
149#[non_exhaustive]
151#[derive(Clone, Debug, Eq, PartialEq)]
152pub enum PreparedBaseline {
153 Built,
155 Restored {
157 reset: ResetReceipt,
159 },
160}
161
162#[non_exhaustive]
164#[derive(Clone, Copy, Debug, Eq, PartialEq)]
165pub enum BaselinePreparationStage {
166 Build,
168 RestoreCanisters,
170 ResetNonSnapshotState,
172 DriveToReadiness,
174 ValidateBuilt,
176 ValidateRestored,
178}
179
180#[non_exhaustive]
182#[derive(Clone, Debug, Eq, PartialEq)]
183pub enum RebuildReason {
184 DeadPocketIcTransport,
186 SnapshotRestoreFailure,
188 ResetFailure,
190 ReadinessFailure,
192 ResetCoverageMismatch,
194 InvariantValidationFailure,
196 ExplicitLeaseInvalidation,
198 UnwindWhileLeased,
200 RecipeClassified {
202 code: String,
204 },
205}
206
207#[non_exhaustive]
209#[derive(Clone, Debug, Eq, PartialEq)]
210pub enum FailureDisposition {
211 Fatal,
213 Rebuild(RebuildReason),
215}
216
217#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
219pub struct BaselinePoolTimings {
220 wait: Duration,
221 build: Option<Duration>,
222 restore: Option<Duration>,
223 reset: Option<Duration>,
224 readiness: Option<Duration>,
225 validation: Option<Duration>,
226 stale_teardown: Option<Duration>,
227 total: Duration,
228}
229
230#[derive(Clone, Debug, Eq, PartialEq)]
232pub struct BaselinePoolOutcome {
233 pub slot: usize,
235 pub timings: BaselinePoolTimings,
237 pub disposition: BaselinePoolDisposition,
239}
240
241#[non_exhaustive]
243#[derive(Clone, Debug, Eq, PartialEq)]
244pub enum BaselinePoolDisposition {
245 Built,
247 Restored,
249 Rebuilt(RebuildReason),
251}
252
253#[non_exhaustive]
255#[derive(Debug, thiserror::Error)]
256pub enum BaselinePoolPreparationError<E> {
257 #[error("baseline {stage} failed: {source}")]
259 Recipe {
260 stage: BaselinePreparationStage,
262 source: E,
264 },
265 #[error("baseline canister restore failed: {0}")]
267 Restore(#[source] ControllerSnapshotError),
268 #[error("baseline pool contract failed: {0}")]
270 Contract(#[source] BaselinePoolContractError),
271}
272
273#[non_exhaustive]
275#[derive(Debug)]
276pub enum BaselinePoolError<E> {
277 Preparation {
279 error: BaselinePoolPreparationError<E>,
281 timings: Box<BaselinePoolTimings>,
283 },
284 RecoveryFailed {
286 original: Box<BaselinePoolPreparationError<E>>,
288 rebuild: Box<BaselinePoolPreparationError<E>>,
290 timings: Box<BaselinePoolTimings>,
292 },
293}
294
295pub trait PocketIcBaselineRecipe: Send + Sync + 'static {
297 type Metadata: Send + 'static;
299 type Error: std::error::Error + Send + Sync + 'static;
301
302 fn reset_requirements(&self) -> &ResetRequirements;
304
305 fn build(&self) -> Result<CachedPocketIcBaseline<Self::Metadata>, Self::Error>;
310
311 fn prepare_restore(
320 &self,
321 _baseline: &CachedPocketIcBaseline<Self::Metadata>,
322 ) -> Result<(), Self::Error> {
323 Ok(())
324 }
325
326 fn reset_non_snapshot_state(
331 &self,
332 baseline: &CachedPocketIcBaseline<Self::Metadata>,
333 ) -> Result<ResetReceipt, Self::Error>;
334
335 fn drive_to_readiness(
341 &self,
342 baseline: &CachedPocketIcBaseline<Self::Metadata>,
343 ) -> Result<(), Self::Error>;
344
345 fn validate(
351 &self,
352 baseline: &CachedPocketIcBaseline<Self::Metadata>,
353 preparation: &PreparedBaseline,
354 ) -> Result<(), Self::Error>;
355
356 fn classify_failure(
358 &self,
359 stage: BaselinePreparationStage,
360 _error: &(dyn std::error::Error + 'static),
361 ) -> FailureDisposition {
362 FailureDisposition::Rebuild(stage.default_rebuild_reason())
363 }
364}
365
366pub struct CachedPocketIcBaselinePool<R>
378where
379 R: PocketIcBaselineRecipe,
380{
381 recipe: R,
382 slots: BoundedSlotPool<CachedPocketIcBaseline<R::Metadata>, RebuildReason>,
383}
384
385pub struct CachedPocketIcBaselinePoolGuard<'a, R>
387where
388 R: PocketIcBaselineRecipe,
389{
390 slot: BoundedSlotLease<'a, CachedPocketIcBaseline<R::Metadata>, RebuildReason>,
391}
392
393impl ResetRequirements {
394 fn verify(&self, receipt: ResetReceipt) -> Result<(), BaselinePoolContractError> {
395 self.verify_policy(
396 receipt,
397 ResetDomainKind::PocketIcTime,
398 self.policies.pocket_ic_time,
399 receipt.policies.pocket_ic_time,
400 )?;
401 self.verify_policy(
402 receipt,
403 ResetDomainKind::ExtraCanisters,
404 self.policies.extra_canisters,
405 receipt.policies.extra_canisters,
406 )?;
407 self.verify_policy(
408 receipt,
409 ResetDomainKind::PendingMessages,
410 self.policies.pending_messages,
411 receipt.policies.pending_messages,
412 )?;
413 self.verify_policy(
414 receipt,
415 ResetDomainKind::SubnetState,
416 self.policies.subnet_state,
417 receipt.policies.subnet_state,
418 )?;
419 self.verify_policy(
420 receipt,
421 ResetDomainKind::ExternalResources,
422 self.policies.external_resources,
423 receipt.policies.external_resources,
424 )
425 }
426
427 fn verify_policy<P: PartialEq>(
428 &self,
429 receipt: ResetReceipt,
430 domain: ResetDomainKind,
431 requirement: Option<P>,
432 achievement: Option<P>,
433 ) -> Result<(), BaselinePoolContractError> {
434 let Some(requirement) = requirement else {
435 return Ok(());
436 };
437 let Some(achievement) = achievement else {
438 return Err(BaselinePoolContractError::MissingResetDomain { domain });
439 };
440 if requirement != achievement {
441 return Err(BaselinePoolContractError::ResetPolicyMismatch {
442 domain,
443 requirement: self.policies,
444 achievement: receipt.policies,
445 });
446 }
447 Ok(())
448 }
449}
450
451impl BaselinePreparationStage {
452 #[must_use]
457 pub fn default_rebuild_reason(self) -> RebuildReason {
458 match self {
459 Self::RestoreCanisters => RebuildReason::SnapshotRestoreFailure,
460 Self::ResetNonSnapshotState => RebuildReason::ResetFailure,
461 Self::DriveToReadiness => RebuildReason::ReadinessFailure,
462 Self::ValidateRestored | Self::ValidateBuilt => {
463 RebuildReason::InvariantValidationFailure
464 }
465 Self::Build => RebuildReason::RecipeClassified {
466 code: "build".to_owned(),
467 },
468 }
469 }
470}
471
472impl BaselinePoolTimings {
473 #[must_use]
475 pub const fn wait(self) -> Duration {
476 self.wait
477 }
478
479 #[must_use]
481 pub const fn build(self) -> Option<Duration> {
482 self.build
483 }
484
485 #[must_use]
487 pub const fn restore(self) -> Option<Duration> {
488 self.restore
489 }
490
491 #[must_use]
493 pub const fn reset(self) -> Option<Duration> {
494 self.reset
495 }
496
497 #[must_use]
499 pub const fn readiness(self) -> Option<Duration> {
500 self.readiness
501 }
502
503 #[must_use]
505 pub const fn validation(self) -> Option<Duration> {
506 self.validation
507 }
508
509 #[must_use]
511 pub const fn stale_teardown(self) -> Option<Duration> {
512 self.stale_teardown
513 }
514
515 #[must_use]
517 pub const fn total(self) -> Duration {
518 self.total
519 }
520}
521
522impl std::fmt::Display for BaselinePoolTimings {
523 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
524 write!(
525 formatter,
526 "total={:?} wait={:?} build={:?} restore={:?} reset={:?} readiness={:?} validation={:?} stale_teardown={:?}",
527 self.total,
528 self.wait,
529 self.build,
530 self.restore,
531 self.reset,
532 self.readiness,
533 self.validation,
534 self.stale_teardown,
535 )
536 }
537}
538
539impl std::fmt::Display for BaselinePoolOutcome {
540 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
541 match &self.disposition {
542 BaselinePoolDisposition::Built => formatter.write_str("built")?,
543 BaselinePoolDisposition::Restored => formatter.write_str("restored")?,
544 BaselinePoolDisposition::Rebuilt(reason) => {
545 write!(
546 formatter,
547 "rebuilt slot={} reason={reason:?} {}",
548 self.slot, self.timings
549 )?;
550 return Ok(());
551 }
552 }
553 write!(formatter, " slot={} {}", self.slot, self.timings)
554 }
555}
556
557impl<E> BaselinePoolError<E> {
558 #[must_use]
560 pub const fn timings(&self) -> BaselinePoolTimings {
561 match self {
562 Self::Preparation { timings, .. } | Self::RecoveryFailed { timings, .. } => **timings,
563 }
564 }
565}
566
567impl<R> CachedPocketIcBaselinePool<R>
568where
569 R: PocketIcBaselineRecipe,
570{
571 #[must_use]
576 pub const fn new(capacity: NonZeroUsize, recipe: R) -> Self {
577 Self {
578 recipe,
579 slots: BoundedSlotPool::new(capacity),
580 }
581 }
582
583 #[must_use]
585 pub const fn capacity(&self) -> NonZeroUsize {
586 self.slots.capacity()
587 }
588
589 #[expect(
601 clippy::significant_drop_tightening,
602 reason = "lease and coordinator scope deliberately cover the complete operation"
603 )]
604 pub fn acquire(
605 &self,
606 ) -> Result<
607 (CachedPocketIcBaselinePoolGuard<'_, R>, BaselinePoolOutcome),
608 BaselinePoolError<R::Error>,
609 > {
610 let total_started = Instant::now();
611 let mut slot = self.slots.acquire();
612 let mut timings = BaselinePoolTimings {
613 wait: slot.wait(),
614 ..BaselinePoolTimings::default()
615 };
616 let slot_index = slot.slot_index();
617
618 let result = self.prepare_slot(&mut slot, &mut timings);
619 timings.total = total_started.elapsed();
620 let disposition = result.map_err(|mut error| {
621 match &mut error {
622 BaselinePoolError::Preparation { timings: saved, .. }
623 | BaselinePoolError::RecoveryFailed { timings: saved, .. } => **saved = timings,
624 }
625 error
626 })?;
627 Ok((
628 CachedPocketIcBaselinePoolGuard { slot },
629 BaselinePoolOutcome {
630 slot: slot_index,
631 timings,
632 disposition,
633 },
634 ))
635 }
636
637 fn prepare_slot(
638 &self,
639 slot: &mut BoundedSlotLease<'_, CachedPocketIcBaseline<R::Metadata>, RebuildReason>,
640 timings: &mut BaselinePoolTimings,
641 ) -> Result<BaselinePoolDisposition, BaselinePoolError<R::Error>> {
642 let (disposition, original) = match slot.state() {
643 Slot::Reusable(_) => match self.prepare_reused(slot, timings) {
644 Ok(()) => {
645 return Ok(BaselinePoolDisposition::Restored);
646 }
647 Err(original) => match self.failure_disposition(&original) {
648 FailureDisposition::Fatal => {
649 Self::discard_stale_slot(slot, timings);
652 return Err(BaselinePoolError::Preparation {
653 error: original,
654 timings: Box::new(*timings),
655 });
656 }
657 FailureDisposition::Rebuild(reason) => {
658 (BaselinePoolDisposition::Rebuilt(reason), Some(original))
659 }
660 },
661 },
662 Slot::Empty => (BaselinePoolDisposition::Built, None),
663 Slot::Invalidated { reason, .. } => {
664 (BaselinePoolDisposition::Rebuilt(reason.clone()), None)
665 }
666 Slot::Unwound(_) => (
667 BaselinePoolDisposition::Rebuilt(RebuildReason::UnwindWhileLeased),
668 None,
669 ),
670 };
671
672 if slot.get().is_some() {
673 Self::discard_stale_slot(slot, timings);
674 }
675 let build = self.build_slot(slot, timings);
676 if let Err(error) = build {
677 return Err(match original {
678 Some(original) => BaselinePoolError::RecoveryFailed {
679 original: Box::new(original),
680 rebuild: Box::new(error),
681 timings: Box::new(*timings),
682 },
683 None => BaselinePoolError::Preparation {
684 error,
685 timings: Box::new(*timings),
686 },
687 });
688 }
689
690 Ok(disposition)
691 }
692
693 fn prepare_reused(
694 &self,
695 slot: &BoundedSlotLease<'_, CachedPocketIcBaseline<R::Metadata>, RebuildReason>,
696 timings: &mut BaselinePoolTimings,
697 ) -> Result<(), BaselinePoolPreparationError<R::Error>> {
698 let baseline = slot
699 .get()
700 .expect("reusable baseline slot must be populated");
701
702 let started = Instant::now();
703 let restore = self
704 .recipe
705 .prepare_restore(baseline)
706 .map_err(|source| BaselinePoolPreparationError::Recipe {
707 stage: BaselinePreparationStage::RestoreCanisters,
708 source,
709 })
710 .and_then(|()| {
711 baseline
712 .restore(self.recipe.reset_requirements().funding)
713 .map_err(BaselinePoolPreparationError::Restore)
714 });
715 add_timing(&mut timings.restore, started.elapsed());
716 restore?;
717
718 let reset = timed_recipe_stage(
719 &mut timings.reset,
720 BaselinePreparationStage::ResetNonSnapshotState,
721 || self.recipe.reset_non_snapshot_state(baseline),
722 )?;
723 timed_recipe_stage(
724 &mut timings.readiness,
725 BaselinePreparationStage::DriveToReadiness,
726 || self.recipe.drive_to_readiness(baseline),
727 )?;
728 self.recipe
729 .reset_requirements()
730 .verify(reset)
731 .map_err(BaselinePoolPreparationError::Contract)?;
732
733 let preparation = PreparedBaseline::Restored { reset };
734 self.validate_baseline(baseline, &preparation, timings)?;
735 Ok(())
736 }
737
738 fn build_slot(
739 &self,
740 slot: &mut BoundedSlotLease<'_, CachedPocketIcBaseline<R::Metadata>, RebuildReason>,
741 timings: &mut BaselinePoolTimings,
742 ) -> Result<(), BaselinePoolPreparationError<R::Error>> {
743 let baseline =
744 timed_recipe_stage(&mut timings.build, BaselinePreparationStage::Build, || {
745 self.recipe.build()
746 })?;
747
748 if let Err(error) = self.validate_baseline(&baseline, &PreparedBaseline::Built, timings) {
749 drop_baseline_safely(baseline);
750 return Err(error);
751 }
752 let replaced = slot.replace(baseline);
753 debug_assert!(replaced.is_none());
754 Ok(())
755 }
756
757 fn validate_baseline(
758 &self,
759 baseline: &CachedPocketIcBaseline<R::Metadata>,
760 preparation: &PreparedBaseline,
761 timings: &mut BaselinePoolTimings,
762 ) -> Result<(), BaselinePoolPreparationError<R::Error>> {
763 let stage = match preparation {
764 PreparedBaseline::Built => BaselinePreparationStage::ValidateBuilt,
765 PreparedBaseline::Restored { .. } => BaselinePreparationStage::ValidateRestored,
766 };
767 timed_recipe_stage(&mut timings.validation, stage, || {
768 self.recipe.validate(baseline, preparation)
769 })
770 }
771
772 fn failure_disposition(
773 &self,
774 error: &BaselinePoolPreparationError<R::Error>,
775 ) -> FailureDisposition {
776 match error {
777 BaselinePoolPreparationError::Recipe { stage, source } => {
778 self.recipe.classify_failure(*stage, source)
779 }
780 BaselinePoolPreparationError::Restore(source) => self
781 .recipe
782 .classify_failure(BaselinePreparationStage::RestoreCanisters, source),
783 BaselinePoolPreparationError::Contract(_) => {
784 FailureDisposition::Rebuild(RebuildReason::ResetCoverageMismatch)
785 }
786 }
787 }
788
789 fn discard_stale_slot(
790 slot: &mut BoundedSlotLease<'_, CachedPocketIcBaseline<R::Metadata>, RebuildReason>,
791 timings: &mut BaselinePoolTimings,
792 ) {
793 let started = Instant::now();
794 if let Some(stale) = slot.take() {
795 drop_baseline_safely(stale);
796 }
797 timings.stale_teardown = Some(started.elapsed());
798 }
799}
800
801impl<R> CachedPocketIcBaselinePoolGuard<'_, R>
802where
803 R: PocketIcBaselineRecipe,
804{
805 #[must_use]
807 pub const fn slot(&self) -> usize {
808 self.slot.slot_index()
809 }
810
811 pub fn invalidate(&mut self, reason: RebuildReason) {
813 self.slot.invalidate(reason);
814 }
815}
816
817impl<R> Deref for CachedPocketIcBaselinePoolGuard<'_, R>
818where
819 R: PocketIcBaselineRecipe,
820{
821 type Target = CachedPocketIcBaseline<R::Metadata>;
822
823 fn deref(&self) -> &Self::Target {
824 self.slot
825 .get()
826 .expect("leased baseline pool slot must be populated")
827 }
828}
829
830fn timed_recipe_stage<T, E>(
831 timing: &mut Option<Duration>,
832 stage: BaselinePreparationStage,
833 operation: impl FnOnce() -> Result<T, E>,
834) -> Result<T, BaselinePoolPreparationError<E>> {
835 let started = Instant::now();
836 let result = operation();
837 add_timing(timing, started.elapsed());
838 result.map_err(|source| BaselinePoolPreparationError::Recipe { stage, source })
839}
840
841const fn add_timing(total: &mut Option<Duration>, elapsed: Duration) {
842 *total = Some(match *total {
843 None => elapsed,
844 Some(previous) => previous.saturating_add(elapsed),
845 });
846}
847
848fn drop_baseline_safely<M>(baseline: CachedPocketIcBaseline<M>) {
849 let _ = catch_unwind(AssertUnwindSafe(|| drop(baseline)));
850}
851
852impl std::fmt::Display for BaselinePreparationStage {
853 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
854 formatter.write_str(match self {
855 Self::Build => "build",
856 Self::RestoreCanisters => "canister restore",
857 Self::ResetNonSnapshotState => "non-snapshot reset",
858 Self::DriveToReadiness => "readiness",
859 Self::ValidateBuilt => "built-baseline validation",
860 Self::ValidateRestored => "restored-baseline validation",
861 })
862 }
863}
864
865impl<E> std::fmt::Display for BaselinePoolError<E>
866where
867 E: std::fmt::Display,
868{
869 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
870 match self {
871 Self::Preparation { error, .. } => error.fmt(formatter),
872 Self::RecoveryFailed {
873 original, rebuild, ..
874 } => write!(
875 formatter,
876 "baseline preparation failed ({original}); rebuilding the slot also failed: {rebuild}",
877 ),
878 }
879 }
880}
881
882impl<E> std::error::Error for BaselinePoolError<E>
883where
884 E: std::error::Error + 'static,
885{
886 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
887 match self {
888 Self::Preparation { error, .. } => Some(error),
889 Self::RecoveryFailed { original, .. } => Some(original.as_ref()),
890 }
891 }
892}
893
894#[cfg(test)]
895mod tests {
896 use super::{
897 BaselinePoolContractError, BaselinePoolTimings, ExtraCanisterPolicy, ResetDomainKind,
898 ResetPolicies, ResetReceipt, ResetRequirements, SnapshotRestoreFunding, StateResetPolicy,
899 TimeResetPolicy, add_timing,
900 };
901 use std::time::Duration;
902
903 #[test]
904 fn preparation_and_recovery_keep_display_and_unboxed_primary_sources() {
905 use std::{error::Error as _, io};
906 let preparation = |message| super::BaselinePoolPreparationError::Recipe {
907 stage: super::BaselinePreparationStage::Build,
908 source: io::Error::other(message),
909 };
910 let original = preparation("original");
911 assert_eq!(original.to_string(), "baseline build failed: original");
912 assert!(original.source().unwrap().is::<io::Error>());
913 let error = super::BaselinePoolError::RecoveryFailed {
914 original: Box::new(original),
915 rebuild: Box::new(preparation("replacement")),
916 timings: Box::default(),
917 };
918 assert_eq!(
919 error.to_string(),
920 "baseline preparation failed (baseline build failed: original); rebuilding the slot also failed: baseline build failed: replacement"
921 );
922 assert!(
923 error
924 .source()
925 .unwrap()
926 .is::<super::BaselinePoolPreparationError<io::Error>>()
927 );
928 }
929
930 #[test]
931 fn phase_timing_preserves_absence_zero_and_saturation() {
932 let mut timings = BaselinePoolTimings::default();
933 assert_eq!(timings.restore(), None);
934 add_timing(&mut timings.restore, Duration::ZERO);
935 assert_eq!(timings.restore(), Some(Duration::ZERO));
936 add_timing(&mut timings.restore, Duration::from_secs(2));
937 assert_eq!(timings.restore(), Some(Duration::from_secs(2)));
938 add_timing(&mut timings.restore, Duration::MAX);
939 assert_eq!(timings.restore(), Some(Duration::MAX));
940 assert_eq!(timings.build(), None);
941 }
942
943 #[test]
944 fn required_policy_must_match_achieved_policy() {
945 for (domain, requirement, achievement) in [
946 (
947 ResetDomainKind::PocketIcTime,
948 ResetPolicies {
949 pocket_ic_time: Some(TimeResetPolicy::PreserveCurrent),
950 ..ResetPolicies::default()
951 },
952 ResetPolicies {
953 pocket_ic_time: Some(TimeResetPolicy::RebuildOnMutation),
954 ..ResetPolicies::default()
955 },
956 ),
957 (
958 ResetDomainKind::ExtraCanisters,
959 ResetPolicies {
960 extra_canisters: Some(ExtraCanisterPolicy::RequireBaselineSet),
961 ..ResetPolicies::default()
962 },
963 ResetPolicies {
964 extra_canisters: Some(ExtraCanisterPolicy::RemoveTracked),
965 ..ResetPolicies::default()
966 },
967 ),
968 (
969 ResetDomainKind::PendingMessages,
970 ResetPolicies {
971 pending_messages: Some(StateResetPolicy::ValidateUnchanged),
972 ..ResetPolicies::default()
973 },
974 ResetPolicies {
975 pending_messages: Some(StateResetPolicy::IrrelevantByRecipeContract),
976 ..ResetPolicies::default()
977 },
978 ),
979 (
980 ResetDomainKind::SubnetState,
981 ResetPolicies {
982 subnet_state: Some(StateResetPolicy::ResetByRecipe),
983 ..ResetPolicies::default()
984 },
985 ResetPolicies {
986 subnet_state: Some(StateResetPolicy::RebuildOnChange),
987 ..ResetPolicies::default()
988 },
989 ),
990 (
991 ResetDomainKind::ExternalResources,
992 ResetPolicies {
993 external_resources: Some(StateResetPolicy::ValidateUnchanged),
994 ..ResetPolicies::default()
995 },
996 ResetPolicies {
997 external_resources: Some(StateResetPolicy::ResetByRecipe),
998 ..ResetPolicies::default()
999 },
1000 ),
1001 ] {
1002 let requirements = ResetRequirements {
1003 funding: SnapshotRestoreFunding::Preserve,
1004 policies: requirement,
1005 };
1006 assert_eq!(
1007 requirements.verify(ResetReceipt {
1008 policies: requirement
1009 }),
1010 Ok(())
1011 );
1012 assert_eq!(
1013 requirements.verify(ResetReceipt::default()),
1014 Err(BaselinePoolContractError::MissingResetDomain { domain })
1015 );
1016 assert_eq!(
1017 requirements.verify(ResetReceipt {
1018 policies: achievement
1019 }),
1020 Err(BaselinePoolContractError::ResetPolicyMismatch {
1021 domain,
1022 requirement,
1023 achievement
1024 })
1025 );
1026 let unconstrained = ResetRequirements {
1027 funding: SnapshotRestoreFunding::Preserve,
1028 policies: ResetPolicies::default(),
1029 };
1030 assert_eq!(
1031 unconstrained.verify(ResetReceipt {
1032 policies: achievement
1033 }),
1034 Ok(())
1035 );
1036 }
1037 }
1038
1039 #[test]
1040 fn reset_coverage_preserves_absence_and_first_failure() {
1041 let requirements = ResetRequirements {
1042 funding: SnapshotRestoreFunding::Preserve,
1043 policies: ResetPolicies {
1044 pocket_ic_time: Some(TimeResetPolicy::PreserveCurrent),
1045 pending_messages: Some(StateResetPolicy::IrrelevantByRecipeContract),
1046 ..ResetPolicies::default()
1047 },
1048 };
1049 let mut receipt = ResetReceipt::default();
1050 assert_eq!(
1051 requirements.verify(receipt),
1052 Err(BaselinePoolContractError::MissingResetDomain {
1053 domain: ResetDomainKind::PocketIcTime,
1054 })
1055 );
1056 receipt.policies.pocket_ic_time = Some(TimeResetPolicy::RebuildOnMutation);
1057 assert_eq!(
1058 requirements.verify(receipt),
1059 Err(BaselinePoolContractError::ResetPolicyMismatch {
1060 domain: ResetDomainKind::PocketIcTime,
1061 requirement: requirements.policies,
1062 achievement: receipt.policies,
1063 })
1064 );
1065 receipt.policies.pocket_ic_time = Some(TimeResetPolicy::PreserveCurrent);
1066 assert_eq!(
1067 requirements.verify(receipt),
1068 Err(BaselinePoolContractError::MissingResetDomain {
1069 domain: ResetDomainKind::PendingMessages,
1070 })
1071 );
1072 receipt.policies.pending_messages = Some(StateResetPolicy::IrrelevantByRecipeContract);
1073 assert_eq!(requirements.verify(receipt), Ok(()));
1074 }
1075}