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},
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) = if slot.is_reusable() {
614 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 } else {
641 let reason = if slot.invalidated_by_unwind() {
642 Some(RebuildReason::UnwindWhileLeased)
643 } else {
644 slot.invalidation_reason().cloned()
645 };
646 (reason, None)
647 };
648
649 if slot.is_populated() {
650 Self::discard_stale_slot(&mut slot, &mut timings);
651 }
652 let build = self.build_slot(&mut slot, &mut timings);
653 timings.total = total_started.elapsed();
654 if let Err(error) = build {
655 return Err(match original {
656 Some(original) => BaselinePoolError::RecoveryFailed {
657 original: Box::new(original),
658 rebuild: Box::new(error),
659 timings: Box::new(timings),
660 },
661 None => BaselinePoolError::Preparation {
662 error,
663 timings: Box::new(timings),
664 },
665 });
666 }
667
668 let outcome = BaselinePoolOutcome {
669 slot: slot_index,
670 timings,
671 disposition: rebuild_reason.map_or(
672 BaselinePoolDisposition::Built,
673 BaselinePoolDisposition::Rebuilt,
674 ),
675 };
676 Ok((CachedPocketIcBaselinePoolGuard { slot }, outcome))
677 }
678
679 fn prepare_reused(
680 &self,
681 slot: &BoundedSlotLease<'_, CachedPocketIcBaseline<R::Metadata>, RebuildReason>,
682 timings: &mut BaselinePoolTimings,
683 ) -> Result<(), BaselinePoolPreparationError<R::Error>> {
684 let baseline = slot
685 .get()
686 .expect("reusable baseline slot must be populated");
687
688 let started = Instant::now();
689 let restore = self
690 .recipe
691 .prepare_restore(baseline)
692 .map_err(|source| BaselinePoolPreparationError::Recipe {
693 stage: BaselinePreparationStage::RestoreCanisters,
694 source,
695 })
696 .and_then(|()| {
697 baseline
698 .restore(self.recipe.reset_requirements().funding)
699 .map_err(BaselinePoolPreparationError::Restore)
700 });
701 add_timing(&mut timings.restore, started.elapsed());
702 restore?;
703
704 let reset = timed_recipe_stage(
705 &mut timings.reset,
706 BaselinePreparationStage::ResetNonSnapshotState,
707 || self.recipe.reset_non_snapshot_state(baseline),
708 )?;
709 timed_recipe_stage(
710 &mut timings.readiness,
711 BaselinePreparationStage::DriveToReadiness,
712 || self.recipe.drive_to_readiness(baseline),
713 )?;
714 self.recipe
715 .reset_requirements()
716 .verify(reset)
717 .map_err(BaselinePoolPreparationError::Contract)?;
718
719 let preparation = PreparedBaseline::Restored { reset };
720 self.validate_baseline(
721 baseline,
722 &preparation,
723 BaselinePreparationStage::ValidateRestored,
724 timings,
725 )?;
726 Ok(())
727 }
728
729 fn build_slot(
730 &self,
731 slot: &mut BoundedSlotLease<'_, CachedPocketIcBaseline<R::Metadata>, RebuildReason>,
732 timings: &mut BaselinePoolTimings,
733 ) -> Result<(), BaselinePoolPreparationError<R::Error>> {
734 let baseline =
735 timed_recipe_stage(&mut timings.build, BaselinePreparationStage::Build, || {
736 self.recipe.build()
737 })?;
738
739 if let Err(error) = self.validate_baseline(
740 &baseline,
741 &PreparedBaseline::Built,
742 BaselinePreparationStage::ValidateBuilt,
743 timings,
744 ) {
745 drop_baseline_safely(baseline);
746 return Err(error);
747 }
748 let replaced = slot.replace(baseline);
749 debug_assert!(replaced.is_none());
750 Ok(())
751 }
752
753 fn validate_baseline(
754 &self,
755 baseline: &CachedPocketIcBaseline<R::Metadata>,
756 preparation: &PreparedBaseline,
757 stage: BaselinePreparationStage,
758 timings: &mut BaselinePoolTimings,
759 ) -> Result<(), BaselinePoolPreparationError<R::Error>> {
760 timed_recipe_stage(&mut timings.validation, stage, || {
761 self.recipe.validate(baseline, preparation)
762 })
763 }
764
765 fn failure_disposition(
766 &self,
767 error: &BaselinePoolPreparationError<R::Error>,
768 ) -> FailureDisposition {
769 match error {
770 BaselinePoolPreparationError::Recipe { stage, source } => {
771 self.recipe.classify_failure(*stage, source)
772 }
773 BaselinePoolPreparationError::Restore(source) => self
774 .recipe
775 .classify_failure(BaselinePreparationStage::RestoreCanisters, source),
776 BaselinePoolPreparationError::Contract(_) => {
777 FailureDisposition::Rebuild(RebuildReason::ResetCoverageMismatch)
778 }
779 }
780 }
781
782 fn discard_stale_slot(
783 slot: &mut BoundedSlotLease<'_, CachedPocketIcBaseline<R::Metadata>, RebuildReason>,
784 timings: &mut BaselinePoolTimings,
785 ) {
786 let started = Instant::now();
787 if let Some(stale) = slot.take() {
788 drop_baseline_safely(stale);
789 }
790 timings.stale_teardown = Some(started.elapsed());
791 }
792}
793
794impl<R> CachedPocketIcBaselinePoolGuard<'_, R>
795where
796 R: PocketIcBaselineRecipe,
797{
798 #[must_use]
800 pub const fn slot(&self) -> usize {
801 self.slot.slot_index()
802 }
803
804 pub fn invalidate(&mut self, reason: RebuildReason) {
806 self.slot.invalidate(reason);
807 }
808}
809
810impl<R> Deref for CachedPocketIcBaselinePoolGuard<'_, R>
811where
812 R: PocketIcBaselineRecipe,
813{
814 type Target = CachedPocketIcBaseline<R::Metadata>;
815
816 fn deref(&self) -> &Self::Target {
817 self.slot
818 .get()
819 .expect("leased baseline pool slot must be populated")
820 }
821}
822
823fn timed_recipe_stage<T, E>(
824 timing: &mut Option<Duration>,
825 stage: BaselinePreparationStage,
826 operation: impl FnOnce() -> Result<T, E>,
827) -> Result<T, BaselinePoolPreparationError<E>> {
828 let started = Instant::now();
829 let result = operation();
830 add_timing(timing, started.elapsed());
831 result.map_err(|source| BaselinePoolPreparationError::Recipe { stage, source })
832}
833
834const fn add_timing(total: &mut Option<Duration>, elapsed: Duration) {
835 *total = Some(match *total {
836 None => elapsed,
837 Some(previous) => previous.saturating_add(elapsed),
838 });
839}
840
841fn drop_baseline_safely<M>(baseline: CachedPocketIcBaseline<M>) {
842 let _ = catch_unwind(AssertUnwindSafe(|| drop(baseline)));
843}
844
845impl std::fmt::Display for BaselinePreparationStage {
846 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
847 formatter.write_str(match self {
848 Self::Build => "build",
849 Self::RestoreCanisters => "canister restore",
850 Self::ResetNonSnapshotState => "non-snapshot reset",
851 Self::DriveToReadiness => "readiness",
852 Self::ValidateBuilt => "built-baseline validation",
853 Self::ValidateRestored => "restored-baseline validation",
854 })
855 }
856}
857
858impl std::fmt::Display for BaselinePoolContractError {
859 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
860 match self {
861 Self::MissingResetDomain { domain } => {
862 write!(
863 formatter,
864 "required reset domain {domain:?} was not achieved"
865 )
866 }
867 Self::ResetPolicyMismatch {
868 domain,
869 requirement,
870 achievement,
871 } => write!(
872 formatter,
873 "reset achievement {achievement:?} does not satisfy {requirement:?} for {domain:?}",
874 ),
875 }
876 }
877}
878
879impl std::error::Error for BaselinePoolContractError {}
880
881impl<E> std::fmt::Display for BaselinePoolPreparationError<E>
882where
883 E: std::fmt::Display,
884{
885 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
886 match self {
887 Self::Recipe { stage, source } => {
888 write!(formatter, "baseline {stage} failed: {source}")
889 }
890 Self::Restore(source) => {
891 write!(formatter, "baseline canister restore failed: {source}")
892 }
893 Self::Contract(error) => write!(formatter, "baseline pool contract failed: {error}"),
894 }
895 }
896}
897
898impl<E> std::error::Error for BaselinePoolPreparationError<E>
899where
900 E: std::error::Error + 'static,
901{
902 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
903 match self {
904 Self::Recipe { source, .. } => Some(source),
905 Self::Restore(source) => Some(source),
906 Self::Contract(error) => Some(error),
907 }
908 }
909}
910
911impl<E> std::fmt::Display for BaselinePoolError<E>
912where
913 E: std::fmt::Display,
914{
915 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
916 match self {
917 Self::Preparation { error, .. } => error.fmt(formatter),
918 Self::RecoveryFailed {
919 original, rebuild, ..
920 } => write!(
921 formatter,
922 "baseline preparation failed ({original}); rebuilding the slot also failed: {rebuild}",
923 ),
924 }
925 }
926}
927
928impl<E> std::error::Error for BaselinePoolError<E>
929where
930 E: std::error::Error + 'static,
931{
932 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
933 match self {
934 Self::Preparation { error, .. } => Some(error),
935 Self::RecoveryFailed { original, .. } => Some(original.as_ref()),
936 }
937 }
938}
939
940#[cfg(test)]
941mod tests {
942 use super::{
943 BaselinePoolContractError, BaselinePoolTimings, ExtraCanisterPolicy, ResetDomainKind,
944 ResetPolicies, ResetReceipt, ResetRequirements, SnapshotRestoreFunding, StateResetPolicy,
945 TimeResetPolicy, add_timing,
946 };
947 use std::time::Duration;
948
949 #[test]
950 fn phase_timing_preserves_absence_zero_and_saturation() {
951 let mut timings = BaselinePoolTimings::default();
952 assert_eq!(timings.restore(), None);
953 add_timing(&mut timings.restore, Duration::ZERO);
954 assert_eq!(timings.restore(), Some(Duration::ZERO));
955 add_timing(&mut timings.restore, Duration::from_secs(2));
956 assert_eq!(timings.restore(), Some(Duration::from_secs(2)));
957 add_timing(&mut timings.restore, Duration::MAX);
958 assert_eq!(timings.restore(), Some(Duration::MAX));
959 assert_eq!(timings.build(), None);
960 }
961
962 #[test]
963 fn required_policy_must_match_achieved_policy() {
964 for (domain, requirement, achievement) in [
965 (
966 ResetDomainKind::PocketIcTime,
967 ResetPolicies {
968 pocket_ic_time: Some(TimeResetPolicy::PreserveCurrent),
969 ..ResetPolicies::default()
970 },
971 ResetPolicies {
972 pocket_ic_time: Some(TimeResetPolicy::RebuildOnMutation),
973 ..ResetPolicies::default()
974 },
975 ),
976 (
977 ResetDomainKind::ExtraCanisters,
978 ResetPolicies {
979 extra_canisters: Some(ExtraCanisterPolicy::RequireBaselineSet),
980 ..ResetPolicies::default()
981 },
982 ResetPolicies {
983 extra_canisters: Some(ExtraCanisterPolicy::RemoveTracked),
984 ..ResetPolicies::default()
985 },
986 ),
987 (
988 ResetDomainKind::PendingMessages,
989 ResetPolicies {
990 pending_messages: Some(StateResetPolicy::ValidateUnchanged),
991 ..ResetPolicies::default()
992 },
993 ResetPolicies {
994 pending_messages: Some(StateResetPolicy::IrrelevantByRecipeContract),
995 ..ResetPolicies::default()
996 },
997 ),
998 (
999 ResetDomainKind::SubnetState,
1000 ResetPolicies {
1001 subnet_state: Some(StateResetPolicy::ResetByRecipe),
1002 ..ResetPolicies::default()
1003 },
1004 ResetPolicies {
1005 subnet_state: Some(StateResetPolicy::RebuildOnChange),
1006 ..ResetPolicies::default()
1007 },
1008 ),
1009 (
1010 ResetDomainKind::ExternalResources,
1011 ResetPolicies {
1012 external_resources: Some(StateResetPolicy::ValidateUnchanged),
1013 ..ResetPolicies::default()
1014 },
1015 ResetPolicies {
1016 external_resources: Some(StateResetPolicy::ResetByRecipe),
1017 ..ResetPolicies::default()
1018 },
1019 ),
1020 ] {
1021 let requirements = ResetRequirements {
1022 funding: SnapshotRestoreFunding::Preserve,
1023 policies: requirement,
1024 };
1025 assert_eq!(
1026 requirements.verify(ResetReceipt {
1027 policies: requirement
1028 }),
1029 Ok(())
1030 );
1031 assert_eq!(
1032 requirements.verify(ResetReceipt::default()),
1033 Err(BaselinePoolContractError::MissingResetDomain { domain })
1034 );
1035 assert_eq!(
1036 requirements.verify(ResetReceipt {
1037 policies: achievement
1038 }),
1039 Err(BaselinePoolContractError::ResetPolicyMismatch {
1040 domain,
1041 requirement,
1042 achievement
1043 })
1044 );
1045 let unconstrained = ResetRequirements {
1046 funding: SnapshotRestoreFunding::Preserve,
1047 policies: ResetPolicies::default(),
1048 };
1049 assert_eq!(
1050 unconstrained.verify(ResetReceipt {
1051 policies: achievement
1052 }),
1053 Ok(())
1054 );
1055 }
1056 }
1057
1058 #[test]
1059 fn reset_coverage_preserves_absence_and_first_failure() {
1060 let requirements = ResetRequirements {
1061 funding: SnapshotRestoreFunding::Preserve,
1062 policies: ResetPolicies {
1063 pocket_ic_time: Some(TimeResetPolicy::PreserveCurrent),
1064 pending_messages: Some(StateResetPolicy::IrrelevantByRecipeContract),
1065 ..ResetPolicies::default()
1066 },
1067 };
1068 let mut receipt = ResetReceipt::default();
1069 assert_eq!(
1070 requirements.verify(receipt),
1071 Err(BaselinePoolContractError::MissingResetDomain {
1072 domain: ResetDomainKind::PocketIcTime,
1073 })
1074 );
1075 receipt.policies.pocket_ic_time = Some(TimeResetPolicy::RebuildOnMutation);
1076 assert_eq!(
1077 requirements.verify(receipt),
1078 Err(BaselinePoolContractError::ResetPolicyMismatch {
1079 domain: ResetDomainKind::PocketIcTime,
1080 requirement: requirements.policies,
1081 achievement: receipt.policies,
1082 })
1083 );
1084 receipt.policies.pocket_ic_time = Some(TimeResetPolicy::PreserveCurrent);
1085 assert_eq!(
1086 requirements.verify(receipt),
1087 Err(BaselinePoolContractError::MissingResetDomain {
1088 domain: ResetDomainKind::PendingMessages,
1089 })
1090 );
1091 receipt.policies.pending_messages = Some(StateResetPolicy::IrrelevantByRecipeContract);
1092 assert_eq!(requirements.verify(receipt), Ok(()));
1093 }
1094}