use crate::state::ReactorState;
use evorule_tcb::JsonValue;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InvariantViolation {
IoCountMismatch {
count: usize,
requests_len: usize,
timestamps_len: usize,
},
IoRecoveryWithoutResult,
VersionDecreased {
current: u64,
previous: u64,
},
ResultWithoutIoRecovery,
RecoveryWhileAwaitingIo,
}
impl InvariantViolation {
pub fn as_str(&self) -> &'static str {
match self {
Self::IoCountMismatch { .. } => "io_count_mismatch",
Self::IoRecoveryWithoutResult => "io_recovery_without_result",
Self::VersionDecreased { .. } => "version_decreased",
Self::ResultWithoutIoRecovery => "result_without_io_recovery",
Self::RecoveryWhileAwaitingIo => "recovery_while_awaiting_io",
}
}
}
impl std::fmt::Display for InvariantViolation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::IoCountMismatch {
count,
requests_len,
timestamps_len,
} => write!(
f,
"IoCountMismatch: count={}, requests_len={}, timestamps_len={}",
count, requests_len, timestamps_len
),
Self::IoRecoveryWithoutResult => write!(
f,
"IoRecoveryWithoutResult: io_recovery=true but __io_result__ missing"
),
Self::VersionDecreased { current, previous } => {
write!(
f,
"VersionDecreased: current={} < previous={}",
current, previous
)
}
Self::ResultWithoutIoRecovery => write!(
f,
"ResultWithoutIoRecovery: __io_result__ exists but io_recovery=false"
),
Self::RecoveryWhileAwaitingIo => write!(
f,
"RecoveryWhileAwaitingIo: pending_io>0, queue empty, io_recovery=true"
),
}
}
}
pub(crate) fn check_invariants(state: &ReactorState, _steps: usize) -> Vec<InvariantViolation> {
let mut violations = Vec::new();
check_io_count_consistency(state, &mut violations);
check_io_recovery_consistency(state, &mut violations);
check_version_monotonic(state, &mut violations);
check_no_recovery_conflict(state, &mut violations);
violations
}
fn check_io_count_consistency(state: &ReactorState, violations: &mut Vec<InvariantViolation>) {
let req_len = state.pending_requests.len();
let ts_len = state.pending_io_timestamps.len();
let mismatch = if cfg!(kani) {
state.pending_io_count != req_len
} else {
state.pending_io_count != req_len || state.pending_io_count != ts_len
};
if mismatch {
violations.push(InvariantViolation::IoCountMismatch {
count: state.pending_io_count,
requests_len: req_len,
timestamps_len: ts_len,
});
}
}
fn check_io_recovery_consistency(state: &ReactorState, violations: &mut Vec<InvariantViolation>) {
let has_io_result = has_io_result(&state.payload);
if state.io_recovery && !has_io_result {
violations.push(InvariantViolation::IoRecoveryWithoutResult);
}
if has_io_result && !state.io_recovery {
violations.push(InvariantViolation::ResultWithoutIoRecovery);
}
}
fn check_version_monotonic(state: &ReactorState, violations: &mut Vec<InvariantViolation>) {
if state.version < state.prev_version {
violations.push(InvariantViolation::VersionDecreased {
current: state.version,
previous: state.prev_version,
});
}
}
fn check_no_recovery_conflict(state: &ReactorState, violations: &mut Vec<InvariantViolation>) {
if state.pending_io_count > 0
&& state.queue.is_empty()
&& state.io_recovery
&& !has_io_result(&state.payload)
{
violations.push(InvariantViolation::RecoveryWhileAwaitingIo);
}
}
fn has_io_result(payload: &JsonValue) -> bool {
matches!(payload, JsonValue::Object(map) if map.contains_key("__io_result__"))
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
use crate::fact::FactId;
#[test]
fn test_fresh_state_passes_all_invariants() {
let state = ReactorState::new();
let violations = check_invariants(&state, 0);
assert!(
violations.is_empty(),
"Fresh state should pass all invariants, got: {:?}",
violations
);
}
#[test]
fn test_invariant_1_io_count_mismatch_requests() {
let mut state = ReactorState::new();
state.pending_io_count = 2;
state.pending_requests.insert(FactId(1));
let violations = check_invariants(&state, 0);
assert!(violations
.iter()
.any(|v| matches!(v, InvariantViolation::IoCountMismatch { .. })));
}
#[test]
fn test_invariant_1_io_count_mismatch_timestamps() {
let mut state = ReactorState::new();
state.pending_io_count = 1;
state.pending_requests.insert(FactId(1));
state
.pending_io_timestamps
.insert(FactId(1), std::time::Instant::now());
state
.pending_io_timestamps
.insert(FactId(2), std::time::Instant::now());
let violations = check_invariants(&state, 0);
assert!(violations
.iter()
.any(|v| matches!(v, InvariantViolation::IoCountMismatch { .. })));
}
#[test]
fn test_invariant_1_all_three_consistent() {
let mut state = ReactorState::new();
state.pending_io_count = 2;
state.pending_requests.insert(FactId(1));
state.pending_requests.insert(FactId(2));
state
.pending_io_timestamps
.insert(FactId(1), std::time::Instant::now());
state
.pending_io_timestamps
.insert(FactId(2), std::time::Instant::now());
let violations = check_invariants(&state, 0);
assert!(violations
.iter()
.all(|v| !matches!(v, InvariantViolation::IoCountMismatch { .. })));
}
#[test]
fn test_invariant_2_io_recovery_without_result() {
let mut state = ReactorState::new();
state.io_recovery = true;
let violations = check_invariants(&state, 0);
assert!(violations
.iter()
.any(|v| matches!(v, InvariantViolation::IoRecoveryWithoutResult)));
}
#[test]
fn test_invariant_3_version_decreased() {
let mut state = ReactorState::new();
state.version = 5;
state.prev_version = 10;
let violations = check_invariants(&state, 0);
assert!(violations
.iter()
.any(|v| matches!(v, InvariantViolation::VersionDecreased { .. })));
}
#[test]
fn test_invariant_3_version_equal_passes() {
let mut state = ReactorState::new();
state.version = 5;
state.prev_version = 5;
let violations = check_invariants(&state, 0);
assert!(violations
.iter()
.all(|v| !matches!(v, InvariantViolation::VersionDecreased { .. })));
}
#[test]
fn test_invariant_4_result_without_io_recovery() {
let mut state = ReactorState::new();
if let JsonValue::Object(map) = &mut state.payload {
map.insert("__io_result__".to_string(), JsonValue::string("test"));
}
state.io_recovery = false;
let violations = check_invariants(&state, 0);
assert!(violations
.iter()
.any(|v| matches!(v, InvariantViolation::ResultWithoutIoRecovery)));
}
#[test]
fn test_invariant_2_4_consistent_both_true() {
let mut state = ReactorState::new();
state.io_recovery = true;
if let JsonValue::Object(map) = &mut state.payload {
map.insert("__io_result__".to_string(), JsonValue::string("test"));
}
let violations = check_invariants(&state, 0);
assert!(violations.iter().all(|v| !matches!(
v,
InvariantViolation::IoRecoveryWithoutResult
| InvariantViolation::ResultWithoutIoRecovery
)));
}
#[test]
fn test_invariant_2_4_consistent_both_false() {
let state = ReactorState::new();
let violations = check_invariants(&state, 0);
assert!(violations.iter().all(|v| !matches!(
v,
InvariantViolation::IoRecoveryWithoutResult
| InvariantViolation::ResultWithoutIoRecovery
)));
}
#[test]
fn test_invariant_5_recovery_conflict_violation() {
let mut state = ReactorState::new();
state.pending_io_count = 1;
state.pending_requests.insert(FactId(1));
state
.pending_io_timestamps
.insert(FactId(1), std::time::Instant::now());
state.io_recovery = true;
let violations = check_invariants(&state, 0);
assert!(violations
.iter()
.any(|v| matches!(v, InvariantViolation::RecoveryWhileAwaitingIo)));
}
#[test]
fn test_invariant_5_no_conflict_when_result_present() {
let mut state = ReactorState::new();
state.pending_io_count = 1;
state.pending_requests.insert(FactId(1));
state
.pending_io_timestamps
.insert(FactId(1), std::time::Instant::now());
state.io_recovery = true;
if let JsonValue::Object(map) = &mut state.payload {
map.insert("__io_result__".to_string(), JsonValue::string("x"));
}
let violations = check_invariants(&state, 0);
assert!(violations
.iter()
.all(|v| !matches!(v, InvariantViolation::RecoveryWhileAwaitingIo)));
}
#[test]
fn test_invariant_5_no_conflict_when_queue_nonempty() {
let mut state = ReactorState::new();
state.pending_io_count = 1;
state.pending_requests.insert(FactId(1));
state
.pending_io_timestamps
.insert(FactId(1), std::time::Instant::now());
state.io_recovery = true;
state.push_back(JsonValue::string("work"), FactId(1));
let violations = check_invariants(&state, 0);
assert!(violations
.iter()
.all(|v| !matches!(v, InvariantViolation::RecoveryWhileAwaitingIo)));
}
#[test]
fn test_invariant_5_no_conflict_when_no_pending_io() {
let mut state = ReactorState::new();
state.io_recovery = true;
let violations = check_invariants(&state, 0);
assert!(violations
.iter()
.all(|v| !matches!(v, InvariantViolation::RecoveryWhileAwaitingIo)));
}
#[test]
fn test_violation_as_str() {
assert_eq!(
InvariantViolation::IoRecoveryWithoutResult.as_str(),
"io_recovery_without_result"
);
assert_eq!(
InvariantViolation::ResultWithoutIoRecovery.as_str(),
"result_without_io_recovery"
);
assert_eq!(
InvariantViolation::VersionDecreased {
current: 1,
previous: 2
}
.as_str(),
"version_decreased"
);
assert_eq!(
InvariantViolation::RecoveryWhileAwaitingIo.as_str(),
"recovery_while_awaiting_io"
);
}
#[test]
fn test_violation_display() {
let v = InvariantViolation::IoCountMismatch {
count: 2,
requests_len: 1,
timestamps_len: 1,
};
let s = format!("{}", v);
assert!(s.contains("IoCountMismatch"));
assert!(s.contains("count=2"));
}
}