use std::{
num::NonZeroUsize,
ops::Deref,
panic::{AssertUnwindSafe, catch_unwind},
time::{Duration, Instant},
};
use super::{
CachedPocketIcBaseline, ControllerSnapshotError, SnapshotRestoreFunding,
bounded_pool::{BoundedSlotLease, BoundedSlotPool},
};
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum ResetDomainKind {
PocketIcTime,
ExtraCanisters,
PendingMessages,
SubnetState,
ExternalResources,
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TimeResetPolicy {
PreserveCurrent,
RestoreBaseline,
RebuildOnMutation,
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ExtraCanisterPolicy {
RequireBaselineSet,
RemoveTracked,
RebuildOnChange,
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum StateResetPolicy {
ResetByRecipe,
ValidateUnchanged,
IrrelevantByRecipeContract,
RebuildOnChange,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct ResetPolicies {
pub pocket_ic_time: Option<TimeResetPolicy>,
pub extra_canisters: Option<ExtraCanisterPolicy>,
pub pending_messages: Option<StateResetPolicy>,
pub subnet_state: Option<StateResetPolicy>,
pub external_resources: Option<StateResetPolicy>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ResetRequirements {
pub funding: SnapshotRestoreFunding,
pub policies: ResetPolicies,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct ResetReceipt {
pub policies: ResetPolicies,
}
#[non_exhaustive]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum BaselinePoolContractError {
MissingResetDomain { domain: ResetDomainKind },
ResetPolicyMismatch {
domain: ResetDomainKind,
requirement: ResetPolicies,
achievement: ResetPolicies,
},
}
#[non_exhaustive]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PreparedBaseline {
Built,
Restored {
reset: ResetReceipt,
},
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum BaselinePreparationStage {
Build,
RestoreCanisters,
ResetNonSnapshotState,
DriveToReadiness,
ValidateBuilt,
ValidateRestored,
}
#[non_exhaustive]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum RebuildReason {
DeadPocketIcTransport,
SnapshotRestoreFailure,
ResetFailure,
ReadinessFailure,
ResetCoverageMismatch,
InvariantValidationFailure,
ExplicitLeaseInvalidation,
UnwindWhileLeased,
RecipeClassified { code: String },
}
#[non_exhaustive]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum FailureDisposition {
Fatal,
Rebuild(RebuildReason),
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct BaselinePoolTimings {
wait: Duration,
build: Option<Duration>,
restore: Option<Duration>,
reset: Option<Duration>,
readiness: Option<Duration>,
validation: Option<Duration>,
stale_teardown: Option<Duration>,
total: Duration,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BaselinePoolOutcome {
pub slot: usize,
pub timings: BaselinePoolTimings,
pub disposition: BaselinePoolDisposition,
}
#[non_exhaustive]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum BaselinePoolDisposition {
Built,
Restored,
Rebuilt(RebuildReason),
}
#[non_exhaustive]
#[derive(Debug)]
pub enum BaselinePoolPreparationError<E> {
Recipe {
stage: BaselinePreparationStage,
source: E,
},
Restore(ControllerSnapshotError),
Contract(BaselinePoolContractError),
}
#[non_exhaustive]
#[derive(Debug)]
pub enum BaselinePoolError<E> {
Preparation {
error: BaselinePoolPreparationError<E>,
timings: Box<BaselinePoolTimings>,
},
RecoveryFailed {
original: Box<BaselinePoolPreparationError<E>>,
rebuild: Box<BaselinePoolPreparationError<E>>,
timings: Box<BaselinePoolTimings>,
},
}
pub trait PocketIcBaselineRecipe: Send + Sync + 'static {
type Metadata: Send + 'static;
type Error: std::error::Error + Send + Sync + 'static;
fn reset_requirements(&self) -> &ResetRequirements;
fn build(&self) -> Result<CachedPocketIcBaseline<Self::Metadata>, Self::Error>;
fn prepare_restore(
&self,
_baseline: &CachedPocketIcBaseline<Self::Metadata>,
) -> Result<(), Self::Error> {
Ok(())
}
fn reset_non_snapshot_state(
&self,
baseline: &CachedPocketIcBaseline<Self::Metadata>,
) -> Result<ResetReceipt, Self::Error>;
fn drive_to_readiness(
&self,
baseline: &CachedPocketIcBaseline<Self::Metadata>,
) -> Result<(), Self::Error>;
fn validate(
&self,
baseline: &CachedPocketIcBaseline<Self::Metadata>,
preparation: &PreparedBaseline,
) -> Result<(), Self::Error>;
fn classify_failure(
&self,
stage: BaselinePreparationStage,
_error: &(dyn std::error::Error + 'static),
) -> FailureDisposition {
FailureDisposition::Rebuild(stage.default_rebuild_reason())
}
}
pub struct CachedPocketIcBaselinePool<R>
where
R: PocketIcBaselineRecipe,
{
recipe: R,
slots: BoundedSlotPool<CachedPocketIcBaseline<R::Metadata>, RebuildReason>,
}
pub struct CachedPocketIcBaselinePoolGuard<'a, R>
where
R: PocketIcBaselineRecipe,
{
slot: BoundedSlotLease<'a, CachedPocketIcBaseline<R::Metadata>, RebuildReason>,
}
impl ResetRequirements {
fn verify(&self, receipt: ResetReceipt) -> Result<(), BaselinePoolContractError> {
self.verify_policy(
receipt,
ResetDomainKind::PocketIcTime,
self.policies.pocket_ic_time,
receipt.policies.pocket_ic_time,
)?;
self.verify_policy(
receipt,
ResetDomainKind::ExtraCanisters,
self.policies.extra_canisters,
receipt.policies.extra_canisters,
)?;
self.verify_policy(
receipt,
ResetDomainKind::PendingMessages,
self.policies.pending_messages,
receipt.policies.pending_messages,
)?;
self.verify_policy(
receipt,
ResetDomainKind::SubnetState,
self.policies.subnet_state,
receipt.policies.subnet_state,
)?;
self.verify_policy(
receipt,
ResetDomainKind::ExternalResources,
self.policies.external_resources,
receipt.policies.external_resources,
)
}
fn verify_policy<P: PartialEq>(
&self,
receipt: ResetReceipt,
domain: ResetDomainKind,
requirement: Option<P>,
achievement: Option<P>,
) -> Result<(), BaselinePoolContractError> {
let Some(requirement) = requirement else {
return Ok(());
};
let Some(achievement) = achievement else {
return Err(BaselinePoolContractError::MissingResetDomain { domain });
};
if requirement != achievement {
return Err(BaselinePoolContractError::ResetPolicyMismatch {
domain,
requirement: self.policies,
achievement: receipt.policies,
});
}
Ok(())
}
}
impl BaselinePreparationStage {
#[must_use]
pub fn default_rebuild_reason(self) -> RebuildReason {
match self {
Self::RestoreCanisters => RebuildReason::SnapshotRestoreFailure,
Self::ResetNonSnapshotState => RebuildReason::ResetFailure,
Self::DriveToReadiness => RebuildReason::ReadinessFailure,
Self::ValidateRestored | Self::ValidateBuilt => {
RebuildReason::InvariantValidationFailure
}
Self::Build => RebuildReason::RecipeClassified {
code: "build".to_owned(),
},
}
}
}
impl BaselinePoolTimings {
#[must_use]
pub const fn wait(self) -> Duration {
self.wait
}
#[must_use]
pub const fn build(self) -> Option<Duration> {
self.build
}
#[must_use]
pub const fn restore(self) -> Option<Duration> {
self.restore
}
#[must_use]
pub const fn reset(self) -> Option<Duration> {
self.reset
}
#[must_use]
pub const fn readiness(self) -> Option<Duration> {
self.readiness
}
#[must_use]
pub const fn validation(self) -> Option<Duration> {
self.validation
}
#[must_use]
pub const fn stale_teardown(self) -> Option<Duration> {
self.stale_teardown
}
#[must_use]
pub const fn total(self) -> Duration {
self.total
}
}
impl std::fmt::Display for BaselinePoolTimings {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
formatter,
"total={:?} wait={:?} build={:?} restore={:?} reset={:?} readiness={:?} validation={:?} stale_teardown={:?}",
self.total,
self.wait,
self.build,
self.restore,
self.reset,
self.readiness,
self.validation,
self.stale_teardown,
)
}
}
impl std::fmt::Display for BaselinePoolOutcome {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.disposition {
BaselinePoolDisposition::Built => formatter.write_str("built")?,
BaselinePoolDisposition::Restored => formatter.write_str("restored")?,
BaselinePoolDisposition::Rebuilt(reason) => {
write!(
formatter,
"rebuilt slot={} reason={reason:?} {}",
self.slot, self.timings
)?;
return Ok(());
}
}
write!(formatter, " slot={} {}", self.slot, self.timings)
}
}
impl<E> BaselinePoolError<E> {
#[must_use]
pub const fn timings(&self) -> BaselinePoolTimings {
match self {
Self::Preparation { timings, .. } | Self::RecoveryFailed { timings, .. } => **timings,
}
}
}
impl<R> CachedPocketIcBaselinePool<R>
where
R: PocketIcBaselineRecipe,
{
#[must_use]
pub const fn new(capacity: NonZeroUsize, recipe: R) -> Self {
Self {
recipe,
slots: BoundedSlotPool::new(capacity),
}
}
#[must_use]
pub const fn capacity(&self) -> NonZeroUsize {
self.slots.capacity()
}
pub fn acquire(
&self,
) -> Result<
(CachedPocketIcBaselinePoolGuard<'_, R>, BaselinePoolOutcome),
BaselinePoolError<R::Error>,
> {
let total_started = Instant::now();
let mut slot = self.slots.acquire();
let mut timings = BaselinePoolTimings {
wait: slot.wait(),
..BaselinePoolTimings::default()
};
let slot_index = slot.slot_index();
let (rebuild_reason, original) = if slot.is_reusable() {
match self.prepare_reused(&slot, &mut timings) {
Ok(()) => {
timings.total = total_started.elapsed();
return Ok((
CachedPocketIcBaselinePoolGuard { slot },
BaselinePoolOutcome {
slot: slot_index,
timings,
disposition: BaselinePoolDisposition::Restored,
},
));
}
Err(original) => match self.failure_disposition(&original) {
FailureDisposition::Fatal => {
Self::discard_stale_slot(&mut slot, &mut timings);
timings.total = total_started.elapsed();
return Err(BaselinePoolError::Preparation {
error: original,
timings: Box::new(timings),
});
}
FailureDisposition::Rebuild(reason) => (Some(reason), Some(original)),
},
}
} else {
let reason = if slot.invalidated_by_unwind() {
Some(RebuildReason::UnwindWhileLeased)
} else {
slot.invalidation_reason().cloned()
};
(reason, None)
};
if slot.is_populated() {
Self::discard_stale_slot(&mut slot, &mut timings);
}
let build = self.build_slot(&mut slot, &mut timings);
timings.total = total_started.elapsed();
if let Err(error) = build {
return Err(match original {
Some(original) => BaselinePoolError::RecoveryFailed {
original: Box::new(original),
rebuild: Box::new(error),
timings: Box::new(timings),
},
None => BaselinePoolError::Preparation {
error,
timings: Box::new(timings),
},
});
}
let outcome = BaselinePoolOutcome {
slot: slot_index,
timings,
disposition: rebuild_reason.map_or(
BaselinePoolDisposition::Built,
BaselinePoolDisposition::Rebuilt,
),
};
Ok((CachedPocketIcBaselinePoolGuard { slot }, outcome))
}
fn prepare_reused(
&self,
slot: &BoundedSlotLease<'_, CachedPocketIcBaseline<R::Metadata>, RebuildReason>,
timings: &mut BaselinePoolTimings,
) -> Result<(), BaselinePoolPreparationError<R::Error>> {
let baseline = slot
.get()
.expect("reusable baseline slot must be populated");
let started = Instant::now();
let restore = self
.recipe
.prepare_restore(baseline)
.map_err(|source| BaselinePoolPreparationError::Recipe {
stage: BaselinePreparationStage::RestoreCanisters,
source,
})
.and_then(|()| {
baseline
.restore(self.recipe.reset_requirements().funding)
.map_err(BaselinePoolPreparationError::Restore)
});
add_timing(&mut timings.restore, started.elapsed());
restore?;
let started = Instant::now();
let reset_result = self.recipe.reset_non_snapshot_state(baseline);
add_timing(&mut timings.reset, started.elapsed());
let reset = reset_result.map_err(|source| BaselinePoolPreparationError::Recipe {
stage: BaselinePreparationStage::ResetNonSnapshotState,
source,
})?;
let started = Instant::now();
let readiness_result = self.recipe.drive_to_readiness(baseline);
add_timing(&mut timings.readiness, started.elapsed());
readiness_result.map_err(|source| BaselinePoolPreparationError::Recipe {
stage: BaselinePreparationStage::DriveToReadiness,
source,
})?;
self.recipe
.reset_requirements()
.verify(reset)
.map_err(BaselinePoolPreparationError::Contract)?;
let preparation = PreparedBaseline::Restored { reset };
self.validate_baseline(
baseline,
&preparation,
BaselinePreparationStage::ValidateRestored,
timings,
)?;
Ok(())
}
fn build_slot(
&self,
slot: &mut BoundedSlotLease<'_, CachedPocketIcBaseline<R::Metadata>, RebuildReason>,
timings: &mut BaselinePoolTimings,
) -> Result<(), BaselinePoolPreparationError<R::Error>> {
let started = Instant::now();
let build = self.recipe.build();
add_timing(&mut timings.build, started.elapsed());
let baseline = build.map_err(|source| BaselinePoolPreparationError::Recipe {
stage: BaselinePreparationStage::Build,
source,
})?;
if let Err(error) = self.validate_baseline(
&baseline,
&PreparedBaseline::Built,
BaselinePreparationStage::ValidateBuilt,
timings,
) {
drop_baseline_safely(baseline);
return Err(error);
}
let replaced = slot.replace(baseline);
debug_assert!(replaced.is_none());
Ok(())
}
fn validate_baseline(
&self,
baseline: &CachedPocketIcBaseline<R::Metadata>,
preparation: &PreparedBaseline,
stage: BaselinePreparationStage,
timings: &mut BaselinePoolTimings,
) -> Result<(), BaselinePoolPreparationError<R::Error>> {
let started = Instant::now();
let validation = self.recipe.validate(baseline, preparation);
add_timing(&mut timings.validation, started.elapsed());
validation.map_err(|source| BaselinePoolPreparationError::Recipe { stage, source })
}
fn failure_disposition(
&self,
error: &BaselinePoolPreparationError<R::Error>,
) -> FailureDisposition {
match error {
BaselinePoolPreparationError::Recipe { stage, source } => {
self.recipe.classify_failure(*stage, source)
}
BaselinePoolPreparationError::Restore(source) => self
.recipe
.classify_failure(BaselinePreparationStage::RestoreCanisters, source),
BaselinePoolPreparationError::Contract(_) => {
FailureDisposition::Rebuild(RebuildReason::ResetCoverageMismatch)
}
}
}
fn discard_stale_slot(
slot: &mut BoundedSlotLease<'_, CachedPocketIcBaseline<R::Metadata>, RebuildReason>,
timings: &mut BaselinePoolTimings,
) {
let started = Instant::now();
if let Some(stale) = slot.take() {
drop_baseline_safely(stale);
}
timings.stale_teardown = Some(started.elapsed());
}
}
impl<R> CachedPocketIcBaselinePoolGuard<'_, R>
where
R: PocketIcBaselineRecipe,
{
#[must_use]
pub const fn slot(&self) -> usize {
self.slot.slot_index()
}
pub fn invalidate(&mut self, reason: RebuildReason) {
self.slot.invalidate(reason);
}
}
impl<R> Deref for CachedPocketIcBaselinePoolGuard<'_, R>
where
R: PocketIcBaselineRecipe,
{
type Target = CachedPocketIcBaseline<R::Metadata>;
fn deref(&self) -> &Self::Target {
self.slot
.get()
.expect("leased baseline pool slot must be populated")
}
}
const fn add_timing(total: &mut Option<Duration>, elapsed: Duration) {
*total = Some(match *total {
None => elapsed,
Some(previous) => previous.saturating_add(elapsed),
});
}
fn drop_baseline_safely<M>(baseline: CachedPocketIcBaseline<M>) {
let _ = catch_unwind(AssertUnwindSafe(|| drop(baseline)));
}
impl std::fmt::Display for BaselinePreparationStage {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(match self {
Self::Build => "build",
Self::RestoreCanisters => "canister restore",
Self::ResetNonSnapshotState => "non-snapshot reset",
Self::DriveToReadiness => "readiness",
Self::ValidateBuilt => "built-baseline validation",
Self::ValidateRestored => "restored-baseline validation",
})
}
}
impl std::fmt::Display for BaselinePoolContractError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MissingResetDomain { domain } => {
write!(
formatter,
"required reset domain {domain:?} was not achieved"
)
}
Self::ResetPolicyMismatch {
domain,
requirement,
achievement,
} => write!(
formatter,
"reset achievement {achievement:?} does not satisfy {requirement:?} for {domain:?}",
),
}
}
}
impl std::error::Error for BaselinePoolContractError {}
impl<E> std::fmt::Display for BaselinePoolPreparationError<E>
where
E: std::fmt::Display,
{
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Recipe { stage, source } => {
write!(formatter, "baseline {stage} failed: {source}")
}
Self::Restore(source) => {
write!(formatter, "baseline canister restore failed: {source}")
}
Self::Contract(error) => write!(formatter, "baseline pool contract failed: {error}"),
}
}
}
impl<E> std::error::Error for BaselinePoolPreparationError<E>
where
E: std::error::Error + 'static,
{
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Recipe { source, .. } => Some(source),
Self::Restore(source) => Some(source),
Self::Contract(error) => Some(error),
}
}
}
impl<E> std::fmt::Display for BaselinePoolError<E>
where
E: std::fmt::Display,
{
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Preparation { error, .. } => error.fmt(formatter),
Self::RecoveryFailed {
original, rebuild, ..
} => write!(
formatter,
"baseline preparation failed ({original}); rebuilding the slot also failed: {rebuild}",
),
}
}
}
impl<E> std::error::Error for BaselinePoolError<E>
where
E: std::error::Error + 'static,
{
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Preparation { error, .. } => Some(error),
Self::RecoveryFailed { original, .. } => Some(original.as_ref()),
}
}
}
#[cfg(test)]
mod tests {
use super::{
BaselinePoolContractError, BaselinePoolTimings, ExtraCanisterPolicy, ResetDomainKind,
ResetPolicies, ResetReceipt, ResetRequirements, SnapshotRestoreFunding, StateResetPolicy,
TimeResetPolicy, add_timing,
};
use std::time::Duration;
#[test]
fn phase_timing_preserves_absence_zero_and_saturation() {
let mut timings = BaselinePoolTimings::default();
assert_eq!(timings.restore(), None);
add_timing(&mut timings.restore, Duration::ZERO);
assert_eq!(timings.restore(), Some(Duration::ZERO));
add_timing(&mut timings.restore, Duration::from_secs(2));
assert_eq!(timings.restore(), Some(Duration::from_secs(2)));
add_timing(&mut timings.restore, Duration::MAX);
assert_eq!(timings.restore(), Some(Duration::MAX));
assert_eq!(timings.build(), None);
}
#[test]
fn required_policy_must_match_achieved_policy() {
for (domain, requirement, achievement) in [
(
ResetDomainKind::PocketIcTime,
ResetPolicies {
pocket_ic_time: Some(TimeResetPolicy::PreserveCurrent),
..ResetPolicies::default()
},
ResetPolicies {
pocket_ic_time: Some(TimeResetPolicy::RebuildOnMutation),
..ResetPolicies::default()
},
),
(
ResetDomainKind::ExtraCanisters,
ResetPolicies {
extra_canisters: Some(ExtraCanisterPolicy::RequireBaselineSet),
..ResetPolicies::default()
},
ResetPolicies {
extra_canisters: Some(ExtraCanisterPolicy::RemoveTracked),
..ResetPolicies::default()
},
),
(
ResetDomainKind::PendingMessages,
ResetPolicies {
pending_messages: Some(StateResetPolicy::ValidateUnchanged),
..ResetPolicies::default()
},
ResetPolicies {
pending_messages: Some(StateResetPolicy::IrrelevantByRecipeContract),
..ResetPolicies::default()
},
),
(
ResetDomainKind::SubnetState,
ResetPolicies {
subnet_state: Some(StateResetPolicy::ResetByRecipe),
..ResetPolicies::default()
},
ResetPolicies {
subnet_state: Some(StateResetPolicy::RebuildOnChange),
..ResetPolicies::default()
},
),
(
ResetDomainKind::ExternalResources,
ResetPolicies {
external_resources: Some(StateResetPolicy::ValidateUnchanged),
..ResetPolicies::default()
},
ResetPolicies {
external_resources: Some(StateResetPolicy::ResetByRecipe),
..ResetPolicies::default()
},
),
] {
let requirements = ResetRequirements {
funding: SnapshotRestoreFunding::Preserve,
policies: requirement,
};
assert_eq!(
requirements.verify(ResetReceipt {
policies: requirement
}),
Ok(())
);
assert_eq!(
requirements.verify(ResetReceipt::default()),
Err(BaselinePoolContractError::MissingResetDomain { domain })
);
assert_eq!(
requirements.verify(ResetReceipt {
policies: achievement
}),
Err(BaselinePoolContractError::ResetPolicyMismatch {
domain,
requirement,
achievement
})
);
let unconstrained = ResetRequirements {
funding: SnapshotRestoreFunding::Preserve,
policies: ResetPolicies::default(),
};
assert_eq!(
unconstrained.verify(ResetReceipt {
policies: achievement
}),
Ok(())
);
}
}
#[test]
fn reset_coverage_preserves_absence_and_first_failure() {
let requirements = ResetRequirements {
funding: SnapshotRestoreFunding::Preserve,
policies: ResetPolicies {
pocket_ic_time: Some(TimeResetPolicy::PreserveCurrent),
pending_messages: Some(StateResetPolicy::IrrelevantByRecipeContract),
..ResetPolicies::default()
},
};
let mut receipt = ResetReceipt::default();
assert_eq!(
requirements.verify(receipt),
Err(BaselinePoolContractError::MissingResetDomain {
domain: ResetDomainKind::PocketIcTime,
})
);
receipt.policies.pocket_ic_time = Some(TimeResetPolicy::RebuildOnMutation);
assert_eq!(
requirements.verify(receipt),
Err(BaselinePoolContractError::ResetPolicyMismatch {
domain: ResetDomainKind::PocketIcTime,
requirement: requirements.policies,
achievement: receipt.policies,
})
);
receipt.policies.pocket_ic_time = Some(TimeResetPolicy::PreserveCurrent);
assert_eq!(
requirements.verify(receipt),
Err(BaselinePoolContractError::MissingResetDomain {
domain: ResetDomainKind::PendingMessages,
})
);
receipt.policies.pending_messages = Some(StateResetPolicy::IrrelevantByRecipeContract);
assert_eq!(requirements.verify(receipt), Ok(()));
}
}