#![cfg_attr(coverage_nightly, coverage(off))]
use crate::TestClock;
use crossbeam_channel::{Receiver, RecvTimeoutError, TryRecvError, bounded};
use proptest::prelude::*;
use proptest::test_runner::TestCaseResult;
use std::time::{Duration, Instant, UNIX_EPOCH};
#[derive(Clone, Debug)]
enum Operation {
At(i8),
After(Option<u8>),
Advance(u8),
AdvanceTo(u8),
SetSystemTime(i16),
Drop(u8),
TryRecv(u8),
Receive {
relative: bool,
buffered: bool,
connected: bool,
past: u8,
},
}
fn operation() -> impl Strategy<Value = Operation> {
prop_oneof![
3 => (-4i8..=8).prop_map(Operation::At),
3 => prop::option::of(0u8..=8).prop_map(Operation::After),
2 => (0u8..=8).prop_map(Operation::Advance),
2 => (0u8..=8).prop_map(Operation::AdvanceTo),
1 => (-100i16..=100).prop_map(Operation::SetSystemTime),
2 => any::<u8>().prop_map(Operation::Drop),
3 => any::<u8>().prop_map(Operation::TryRecv),
2 => (any::<bool>(), any::<bool>(), any::<bool>(), 0u8..=4)
.prop_map(|(relative, buffered, connected, past)| Operation::Receive {
relative,
buffered,
connected,
past,
}),
]
}
#[derive(Debug)]
struct Timer {
deadline: Option<i64>,
live: bool,
message: Option<i64>,
delivered: bool,
}
struct Model {
now: i64,
wall: i64,
timers: Vec<Timer>,
}
impl Model {
fn arm(&mut self, deadline: Option<i64>) {
let delivered = deadline.is_some_and(|deadline| deadline <= self.now);
self.timers.push(Timer {
deadline,
live: true,
message: deadline.filter(|_| delivered),
delivered,
});
}
fn advance(&mut self, by: u8) {
let next = self.now + i64::from(by);
for timer in &mut self.timers {
if timer.live
&& timer
.deadline
.is_some_and(|deadline| self.now < deadline && deadline <= next)
{
timer.message = timer.deadline;
timer.delivered = true;
}
}
self.now = next;
self.wall += i64::from(by);
}
fn receive(&mut self, index: usize) -> Result<i64, TryRecvError> {
self.timers[index].message.take().ok_or(TryRecvError::Empty)
}
}
fn instant(origin: Instant, millis: i64) -> Instant {
origin + Duration::from_millis(millis.try_into().unwrap())
}
fn check_operations(operations: &[Operation]) -> TestCaseResult {
let mut tester = TestClock::new();
let clock = tester.clock();
let origin = clock.now();
tester.set_system_time(UNIX_EPOCH);
tester.advance(Duration::from_millis(8));
let mut model = Model {
now: 8,
wall: 8,
timers: Vec::new(),
};
let mut receivers: Vec<Option<Receiver<Instant>>> = Vec::new();
for (step, operation) in operations.iter().enumerate() {
match *operation {
Operation::At(offset) => {
let deadline = model.now + i64::from(offset);
receivers.push(Some(clock.at(instant(origin, deadline))));
model.arm(Some(deadline));
}
Operation::After(offset) => {
let duration =
offset.map_or(Duration::MAX, |millis| Duration::from_millis(millis.into()));
receivers.push(Some(clock.after(duration)));
model.arm(offset.map(|offset| model.now + i64::from(offset)));
}
Operation::Advance(by) => {
tester.advance(Duration::from_millis(by.into()));
model.advance(by);
}
Operation::AdvanceTo(by) => {
tester.advance_to(instant(origin, model.now + i64::from(by)));
model.advance(by);
}
Operation::SetSystemTime(millis) => {
let duration = Duration::from_millis(millis.unsigned_abs().into());
tester.set_system_time(if millis < 0 {
UNIX_EPOCH - duration
} else {
UNIX_EPOCH + duration
});
model.wall = millis.into();
}
Operation::Drop(index) if !receivers.is_empty() => {
let index = usize::from(index) % receivers.len();
receivers[index] = None;
model.timers[index].live = false;
model.timers[index].message = None;
}
Operation::TryRecv(index) if !receivers.is_empty() => {
let index = usize::from(index) % receivers.len();
if let Some(receiver) = &receivers[index] {
let expected = model.receive(index).map(|millis| instant(origin, millis));
prop_assert_eq!(receiver.try_recv(), expected, "{}: {:?}", step, operation);
}
}
Operation::Receive {
relative,
buffered,
connected,
past,
} => {
let (sender, receiver) = bounded(1);
if buffered {
sender.send(7).unwrap();
}
let _sender = connected.then_some(sender);
let empty = if connected {
RecvTimeoutError::Timeout
} else {
RecvTimeoutError::Disconnected
};
for expected in [if buffered { Ok(7) } else { Err(empty) }, Err(empty)] {
let actual = if relative {
clock.recv_timeout(&receiver, Duration::ZERO)
} else {
clock.recv_deadline(&receiver, instant(origin, model.now - i64::from(past)))
};
prop_assert_eq!(actual, expected, "{}: {:?}", step, operation);
}
}
Operation::Drop(_) | Operation::TryRecv(_) => {}
}
let pending: Vec<_> = model
.timers
.iter()
.filter_map(|timer| timer.deadline)
.filter(|deadline| *deadline > model.now)
.collect();
let kept = model.timers.iter().filter(|timer| timer.delivered).count();
let next = pending.iter().min().map(|&millis| instant(origin, millis));
prop_assert_eq!(tester.next_deadline(), next, "{}: {:?}", step, operation);
prop_assert_eq!(
clock.paused.as_ref().unwrap().timer_counts(),
(pending.len(), kept),
"{}: {:?}",
step,
operation
);
prop_assert_eq!(
clock.now(),
instant(origin, model.now),
"{}: {:?}",
step,
operation
);
let wall_duration = Duration::from_millis(model.wall.unsigned_abs());
let wall = if model.wall < 0 {
UNIX_EPOCH - wall_duration
} else {
UNIX_EPOCH + wall_duration
};
prop_assert_eq!(clock.system_time(), wall, "{}: {:?}", step, operation);
for (index, receiver) in receivers.iter().enumerate() {
if let Some(receiver) = receiver {
let timer = &model.timers[index];
prop_assert_eq!(
receiver.len(),
usize::from(timer.message.is_some()),
"{}: {}",
step,
index
);
prop_assert_eq!(
receiver.capacity(),
Some(usize::from(timer.deadline.is_some())),
"{}: {}",
step,
index
);
}
}
}
for (index, receiver) in receivers.iter().enumerate() {
if let Some(receiver) = receiver {
let expected = model.receive(index).map(|millis| instant(origin, millis));
prop_assert_eq!(receiver.try_recv(), expected, "{}", index);
prop_assert_eq!(receiver.try_recv(), Err(TryRecvError::Empty), "{}", index);
}
}
Ok(())
}
proptest! {
#![proptest_config(ProptestConfig {
cases: 512,
failure_persistence: None,
..ProptestConfig::default()
})]
#[test]
fn test_timer_bookkeeping_matches_model(operations in prop::collection::vec(operation(), 1..=128)) {
check_operations(&operations)?;
}
}