use serde::{Deserialize, Serialize};
use super::property::{PropertyOutcome, Severity, Violation};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum EnforcementMode {
Disabled,
#[default]
Shadow,
Enforce,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ConformanceAction {
Nothing,
Report(Violation),
WouldRemove(Violation),
Remove(Violation),
}
impl ConformanceAction {
pub fn removes(&self) -> bool {
matches!(self, ConformanceAction::Remove(_))
}
pub fn violation(&self) -> Option<&Violation> {
match self {
ConformanceAction::Report(v)
| ConformanceAction::WouldRemove(v)
| ConformanceAction::Remove(v) => Some(v),
ConformanceAction::Nothing => None,
}
}
}
pub fn decide(mode: EnforcementMode, outcome: &PropertyOutcome) -> ConformanceAction {
let violation = match outcome {
PropertyOutcome::Violated(v) => v.clone(),
PropertyOutcome::Holds | PropertyOutcome::Inconclusive(_) => {
return ConformanceAction::Nothing;
}
};
match (mode, violation.property.severity()) {
(EnforcementMode::Disabled, _) => ConformanceAction::Nothing,
(_, Severity::Diagnostic) => ConformanceAction::Report(violation),
(EnforcementMode::Shadow, Severity::Violation) => ConformanceAction::WouldRemove(violation),
(EnforcementMode::Enforce, Severity::Violation) => ConformanceAction::Remove(violation),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::conformance::property::{ConformanceProperty, Inconclusive, OutputDigest};
fn violation(property: ConformanceProperty) -> PropertyOutcome {
PropertyOutcome::Violated(Violation {
property,
severity: property.severity(),
left: OutputDigest::of(b"left"),
right: OutputDigest::of(b"right"),
detail: "test".to_string(),
})
}
#[test]
fn a_forged_severity_cannot_make_a_diagnostic_removable() {
let forged = PropertyOutcome::Violated(Violation {
property: ConformanceProperty::DeltaIdempotence,
severity: Severity::Violation,
left: OutputDigest::of(b"left"),
right: OutputDigest::of(b"right"),
detail: "claims to be enforceable".to_string(),
});
assert!(
!forged.is_enforceable_violation(),
"severity must come from the property, not from the carried field"
);
assert!(
!decide(EnforcementMode::Enforce, &forged).removes(),
"a diagnostic property must not be removable even under Enforce, and even \
when the struct says otherwise"
);
}
#[test]
fn shadow_mode_never_removes_anything() {
for property in ConformanceProperty::ALL {
let action = decide(EnforcementMode::Shadow, &violation(*property));
assert!(
!action.removes(),
"shadow mode proposed a removal for {property}"
);
}
assert!(!decide(EnforcementMode::Shadow, &PropertyOutcome::Holds).removes());
assert!(
!decide(
EnforcementMode::Shadow,
&PropertyOutcome::Inconclusive(Inconclusive::RoundLimit)
)
.removes()
);
}
#[test]
fn diagnostics_never_propose_removal_even_under_enforcement() {
for property in ConformanceProperty::ALL {
if property.severity() != Severity::Diagnostic {
continue;
}
let action = decide(EnforcementMode::Enforce, &violation(*property));
assert!(
matches!(action, ConformanceAction::Report(_)),
"diagnostic {property} produced {action:?} under enforcement"
);
}
}
#[test]
fn a_merge_law_break_would_be_removed_in_shadow_and_is_under_enforcement() {
let outcome = violation(ConformanceProperty::StateCommutativity);
assert!(matches!(
decide(EnforcementMode::Shadow, &outcome),
ConformanceAction::WouldRemove(_)
));
assert!(decide(EnforcementMode::Enforce, &outcome).removes());
}
#[test]
fn disabled_mode_does_nothing_at_all() {
for property in ConformanceProperty::ALL {
assert_eq!(
decide(EnforcementMode::Disabled, &violation(*property)),
ConformanceAction::Nothing
);
}
}
#[test]
fn the_default_mode_is_shadow() {
assert_eq!(EnforcementMode::default(), EnforcementMode::Shadow);
}
}