use crate::cancel::symbol_cancel::{CancelListener, SymbolCancelToken};
use crate::types::{Budget, CancelKind, CancelReason, ObjectId, Time};
use crate::util::DetRng;
use proptest::prelude::*;
use std::sync::Arc;
const EPSILON_NANOS: u64 = 1000;
#[derive(Clone)]
struct TestListener {
notifications: Arc<std::sync::Mutex<Vec<(CancelReason, Time)>>>,
panic_on_notify: bool,
}
impl TestListener {
fn new() -> Self {
Self {
notifications: Arc::new(std::sync::Mutex::new(Vec::new())),
panic_on_notify: false,
}
}
fn with_panic() -> Self {
Self {
notifications: Arc::new(std::sync::Mutex::new(Vec::new())),
panic_on_notify: true,
}
}
fn notifications(&self) -> Vec<(CancelReason, Time)> {
self.notifications.lock().unwrap().clone()
}
fn notification_count(&self) -> usize {
self.notifications.lock().unwrap().len()
}
}
impl CancelListener for TestListener {
fn on_cancel(&self, reason: &CancelReason, at: Time) {
assert!(!self.panic_on_notify, "Test listener panic");
self.notifications
.lock()
.unwrap()
.push((reason.clone(), at));
}
}
fn create_reason(kind: CancelKind, _message: &str) -> CancelReason {
CancelReason::new(kind)
}
fn object_id_from_u64(value: u64) -> ObjectId {
ObjectId::from_u128(u128::from(value))
}
fn cancel_kind_strategy() -> impl Strategy<Value = CancelKind> {
prop_oneof![
Just(CancelKind::User),
Just(CancelKind::Timeout),
Just(CancelKind::Deadline),
Just(CancelKind::Shutdown),
Just(CancelKind::ParentCancelled),
Just(CancelKind::ResourceUnavailable),
]
}
#[test]
fn mr1_hierarchical_propagation() {
proptest!(|(
parent_id in 1u64..1000,
child_ids in prop::collection::vec(1u64..1000, 1..=10),
cancel_kind in prop_oneof![
Just(CancelKind::User),
Just(CancelKind::Timeout),
Just(CancelKind::Shutdown),
Just(CancelKind::ParentCancelled),
]
)| {
prop_assume!(child_ids.iter().all(|&id| id != parent_id));
let mut rng = DetRng::new(42);
let now = Time::from_nanos(1_000_000_000);
let parent = SymbolCancelToken::new(object_id_from_u64(parent_id), &mut rng);
let mut children = Vec::new();
for _child_id in &child_ids {
let child = parent.child(&mut rng);
children.push(child);
}
prop_assert!(!parent.is_cancelled(), "Parent should start uncancelled");
for (i, child) in children.iter().enumerate() {
prop_assert!(!child.is_cancelled(),
"Child {} should start uncancelled", i);
}
let reason = create_reason(cancel_kind, "test cancellation");
let was_first_cancel = parent.cancel(&reason, now);
prop_assert!(was_first_cancel, "Should be first cancellation");
prop_assert!(parent.is_cancelled(), "Parent should be cancelled");
for (i, child) in children.iter().enumerate() {
prop_assert!(child.is_cancelled(),
"Child {} must be cancelled when parent is cancelled", i);
if let Some(child_reason) = child.reason() {
prop_assert!(
child_reason.kind == CancelKind::ParentCancelled ||
child_reason.kind.severity() >= CancelKind::ParentCancelled.severity(),
"Child {} should have ParentCancelled or stronger reason, got {:?}",
i, child_reason.kind
);
}
}
});
}
#[test]
fn mr2_reason_strengthening_monotonicity() {
proptest!(|(
object_id in 1u64..1000,
reasons in prop::collection::vec((cancel_kind_strategy(), ".*"), 1..=8)
)| {
prop_assume!(!reasons.is_empty() && reasons.len() <= 8);
let mut rng = DetRng::new(42);
let token = SymbolCancelToken::new(object_id_from_u64(object_id), &mut rng);
let base_time = Time::from_nanos(1_000_000_000);
let mut previous_severity = 0u8;
let mut time_offset = 0;
for (i, (kind, message)) in reasons.iter().enumerate() {
let reason = create_reason(*kind, message);
let now = Time::from_nanos(base_time.as_nanos() + time_offset);
time_offset += 1_000_000;
let _ = token.cancel(&reason, now);
prop_assert!(token.is_cancelled(),
"Token should be cancelled after attempt {}", i);
if let Some(stored_reason) = token.reason() {
let current_severity = stored_reason.kind.severity();
prop_assert!(current_severity >= previous_severity,
"Reason severity should be monotonic: attempt {} had severity {}, got {}",
i, previous_severity, current_severity);
previous_severity = current_severity;
}
}
});
}
#[test]
fn mr3_timing_monotonicity() {
proptest!(|(
object_id in 1u64..1000,
timestamps in prop::collection::vec(1u64..(u64::MAX / 2), 1..=5)
)| {
let mut rng = DetRng::new(42);
let token = SymbolCancelToken::new(object_id_from_u64(object_id), &mut rng);
let mut first_cancel_time = None;
for (i, ×tamp) in timestamps.iter().enumerate() {
let time = Time::from_nanos(timestamp);
let reason = create_reason(CancelKind::User, &format!("cancel {}", i));
let was_first = token.cancel(&reason, time);
if was_first {
first_cancel_time = Some(time);
}
if let Some(recorded_time) = token.cancelled_at() {
if let Some(first_time) = first_cancel_time {
let time_diff = if recorded_time.as_nanos() >= first_time.as_nanos() {
recorded_time.as_nanos() - first_time.as_nanos()
} else {
first_time.as_nanos() - recorded_time.as_nanos()
};
prop_assert!(time_diff <= EPSILON_NANOS,
"Cancellation time should be stable: first={}, recorded={}, diff={}",
first_time.as_nanos(), recorded_time.as_nanos(), time_diff);
}
}
}
});
}
#[test]
fn mr4_token_identity_invariance() {
proptest!(|(
object_id in 1u64..1000,
operations in prop::collection::vec(any::<u8>(), 1..=10)
)| {
let mut rng = DetRng::new(object_id);
let token = SymbolCancelToken::new(object_id_from_u64(object_id), &mut rng);
let initial_token_id = token.token_id();
let initial_object_id = token.object_id();
let now = Time::from_nanos(1_000_000_000);
for (i, &op) in operations.iter().enumerate() {
match op % 4 {
0 => {
let reason = create_reason(CancelKind::User, &format!("op {}", i));
let _ = token.cancel(&reason, now);
}
1 => {
let _ = token.is_cancelled();
}
2 => {
let _child = token.child(&mut rng);
}
3 => {
let listener = TestListener::new();
token.add_listener(listener);
}
_ => unreachable!(),
}
prop_assert_eq!(token.token_id(), initial_token_id,
"Token ID should never change after operation {}", i);
prop_assert_eq!(token.object_id(), initial_object_id,
"Object ID should never change after operation {}", i);
}
});
}
#[test]
fn mr5_listener_notification_completeness() {
proptest!(|(
object_id in 1u64..1000,
listener_count in 1usize..8
)| {
let mut rng = DetRng::new(42);
let token = SymbolCancelToken::new(object_id_from_u64(object_id), &mut rng);
let mut listeners = Vec::new();
for _i in 0..listener_count {
let listener = TestListener::new();
let listener_clone = listener.clone();
token.add_listener(listener_clone);
listeners.push(listener);
}
for (i, listener) in listeners.iter().enumerate() {
prop_assert_eq!(listener.notification_count(), 0,
"Listener {} should have no initial notifications", i);
}
let reason = create_reason(CancelKind::Timeout, "test timeout");
let now = Time::from_nanos(1_000_000_000);
let was_first = token.cancel(&reason, now);
prop_assert!(was_first, "Should be first cancellation");
for (i, listener) in listeners.iter().enumerate() {
prop_assert!(listener.notification_count() > 0,
"Listener {} should be notified after cancellation", i);
let notifications = listener.notifications();
if let Some((notified_reason, notified_time)) = notifications.first() {
prop_assert_eq!(notified_reason.kind, reason.kind,
"Listener {} should receive correct cancel reason", i);
let time_diff = if notified_time.as_nanos() >= now.as_nanos() {
notified_time.as_nanos() - now.as_nanos()
} else {
now.as_nanos() - notified_time.as_nanos()
};
prop_assert!(time_diff <= EPSILON_NANOS,
"Listener {} should receive correct time", i);
}
}
});
}
#[test]
fn mr6_cancellation_idempotence() {
proptest!(|(
object_id in 1u64..1000,
repeat_count in 2usize..10
)| {
let mut rng = DetRng::new(42);
let token_a = SymbolCancelToken::new(object_id_from_u64(object_id), &mut rng);
let listener_a = TestListener::new();
token_a.add_listener(listener_a.clone());
let token_b = SymbolCancelToken::new(object_id_from_u64(object_id), &mut rng);
let listener_b = TestListener::new();
token_b.add_listener(listener_b.clone());
let reason = create_reason(CancelKind::Deadline, "test deadline");
let now = Time::from_nanos(1_000_000_000);
let was_first_a = token_a.cancel(&reason, now);
let mut was_first_b = false;
for i in 0..repeat_count {
let result = token_b.cancel(&reason, now);
if i == 0 {
was_first_b = result;
}
}
prop_assert_eq!(was_first_a, was_first_b,
"Both tokens should report same first-cancel status");
prop_assert_eq!(token_a.is_cancelled(), token_b.is_cancelled(),
"Both tokens should have same cancellation state");
if let (Some(reason_a), Some(reason_b)) = (token_a.reason(), token_b.reason()) {
prop_assert_eq!(reason_a.kind, reason_b.kind,
"Both tokens should have same cancel reason");
}
prop_assert_eq!(listener_a.notification_count(), listener_b.notification_count(),
"Listener notification counts should be identical");
});
}
#[test]
fn mr7_child_independence() {
proptest!(|(
parent_id in 1u64..100,
child_ids in prop::collection::vec(1u64..100, 2..=6)
)| {
prop_assume!(child_ids.iter().all(|&id| id != parent_id));
let mut rng = DetRng::new(42);
let parent = SymbolCancelToken::new(object_id_from_u64(parent_id), &mut rng);
let mut children = Vec::new();
for _child_id in &child_ids {
let child = parent.child(&mut rng);
children.push(child);
}
let initial_states: Vec<bool> = children.iter()
.map(|child| child.is_cancelled())
.collect();
if let Some(first_child) = children.first() {
let reason = create_reason(CancelKind::User, "direct child cancel");
let now = Time::from_nanos(1_000_000_000);
let _ = first_child.cancel(&reason, now);
prop_assert!(first_child.is_cancelled(),
"Directly cancelled child should be cancelled");
for (i, child) in children.iter().enumerate().skip(1) {
prop_assert_eq!(child.is_cancelled(), initial_states[i],
"Sibling {} should be unaffected by sibling 0 cancellation", i);
}
prop_assert!(!parent.is_cancelled(),
"Parent should not be affected by child cancellation");
}
});
}
#[test]
fn mr8_budget_preservation() {
proptest!(|(
object_id in 1u64..1000,
initial_budget in 1u64..1000000
)| {
let mut rng = DetRng::new(42);
let budget = Budget::new().with_cost_quota(initial_budget);
let token =
SymbolCancelToken::with_budget(object_id_from_u64(object_id), budget, &mut rng);
let initial_budget_value = token.cleanup_budget().remaining_cost();
let reason1 = create_reason(CancelKind::User, "first cancel");
let reason2 = create_reason(CancelKind::Timeout, "second cancel");
let now = Time::from_nanos(1_000_000_000);
let _ = token.cancel(&reason1, now);
let _ = token.cancel(&reason2, now);
let _child = token.child(&mut rng);
let listener = TestListener::new();
token.add_listener(listener);
let final_budget_value = token.cleanup_budget().remaining_cost();
prop_assert_eq!(initial_budget_value, final_budget_value,
"Cleanup budget should be preserved across operations");
});
}
#[cfg(test)]
mod integration_tests {
use super::*;
#[test]
fn mr_composition_hierarchical_with_strengthening() {
let mut rng = DetRng::new(42);
let parent = SymbolCancelToken::new(object_id_from_u64(1), &mut rng);
let child = parent.child(&mut rng);
let weak_reason = create_reason(CancelKind::User, "weak");
let strong_reason = create_reason(CancelKind::Shutdown, "strong");
let now = Time::from_nanos(1_000_000_000);
parent.cancel(&weak_reason, now);
assert!(child.is_cancelled());
parent.cancel(&strong_reason, now);
if let Some(child_reason) = child.reason() {
assert!(child_reason.kind.severity() >= CancelKind::ParentCancelled.severity());
}
}
#[test]
fn mr_validation_catches_listener_panics() {
let mut rng = DetRng::new(42);
let token = SymbolCancelToken::new(object_id_from_u64(1), &mut rng);
let panic_listener = TestListener::with_panic();
token.add_listener(panic_listener);
let initial_panic_count = token.listener_panic_count();
let reason = create_reason(CancelKind::User, "test");
let now = Time::from_nanos(1_000_000_000);
token.cancel(&reason, now);
assert!(
token.listener_panic_count() > initial_panic_count,
"Panic count should increase when listener panics"
);
}
}