use core::num::{NonZeroU32, NonZeroU64};
use proptest::prelude::*;
use proptest::test_runner::RngSeed;
use renew_frame::{FrameLoop, FrameStats, StepBudget, Timestamp, Timestep};
#[allow(clippy::expect_used)]
fn timestep(nanos: u64) -> Timestep {
Timestep::from_nanos(NonZeroU64::new(nanos).expect("non-zero"))
}
#[allow(clippy::expect_used)]
fn budget(steps: u32) -> StepBudget {
StepBudget::new(NonZeroU32::new(steps).expect("non-zero"))
}
proptest! {
#![proptest_config(ProptestConfig {
rng_seed: RngSeed::Fixed(0x0000_2601),
cases: 64,
..ProptestConfig::default()
})]
#[test]
fn every_submitted_nanosecond_is_executed_dropped_or_banked(
dt in 1u64..=1_000_000_000,
allowance in 1u32..=64,
deltas in prop::collection::vec(0u64..500_000_000, 1..24),
) {
let mut frame = FrameLoop::new(timestep(dt), budget(allowance), Timestamp::from_nanos(0));
let mut now = 0u64;
let mut submitted = 0u64;
let mut due_total = 0u64;
let mut executed = 0u64;
for delta in deltas {
now += delta;
submitted += delta;
let plan = frame.begin_frame(Timestamp::from_nanos(now));
prop_assert!(plan.step_count() <= allowance);
prop_assert!(plan.remainder().get() < dt);
prop_assert_eq!(plan.first_tick(), executed);
prop_assert_eq!(
u32::try_from(plan.steps().len()).expect("step count is a u32"),
plan.step_count()
);
for (offset, step) in plan.steps().enumerate() {
let tick = executed + u64::try_from(offset).expect("64-bit offset");
prop_assert_eq!(step.tick, tick);
prop_assert_eq!(step.dt.get(), dt);
prop_assert_eq!(step.sim_time.get(), tick * dt);
}
executed += u64::from(plan.step_count());
due_total += u64::from(plan.step_count()) + plan.dropped();
prop_assert_eq!(frame.tick(), executed);
prop_assert_eq!(frame.remainder(), plan.remainder());
prop_assert!(plan.remainder().get() < plan.timestep().nanos().get());
}
prop_assert_eq!(submitted, due_total * dt + frame.remainder().get());
prop_assert_eq!(frame.simulated().get(), executed * dt);
}
#[test]
fn the_plan_stays_bounded_over_the_whole_domain(
dt in 1u64..=u64::MAX,
allowance in 1u32..=u32::MAX,
start in 0u64..=u64::MAX,
first in 0u64..=u64::MAX,
second in 0u64..=u64::MAX,
) {
let mut frame = FrameLoop::new(
timestep(dt),
budget(allowance),
Timestamp::from_nanos(start),
);
for now in [first, second] {
let before = frame.tick();
let plan = frame.begin_frame(Timestamp::from_nanos(now));
prop_assert!(plan.step_count() <= allowance);
prop_assert!(plan.remainder().get() < dt);
prop_assert_eq!(plan.first_tick(), before);
prop_assert_eq!(frame.tick(), before + u64::from(plan.step_count()));
prop_assert!(plan.remainder().get() < plan.timestep().nanos().get());
}
}
#[test]
fn a_backwards_timestamp_advances_nothing(
dt in 1u64..=1_000_000_000,
start in 0u64..=u64::MAX,
back in 0u64..=u64::MAX,
) {
let mut frame = FrameLoop::new(
timestep(dt),
StepBudget::DEFAULT,
Timestamp::from_nanos(start),
);
let plan = frame.begin_frame(Timestamp::from_nanos(start.saturating_sub(back)));
prop_assert_eq!(plan.step_count(), 0);
prop_assert_eq!(plan.dropped(), 0);
prop_assert_eq!(plan.remainder().get(), 0);
}
#[test]
fn the_remainder_is_always_a_proper_fraction_of_the_timestep(
dt in 1u64..=u64::MAX,
fraction in 0u64..=u64::MAX,
) {
let remainder = fraction % dt;
let mut frame = FrameLoop::new(
timestep(dt),
StepBudget::DEFAULT,
Timestamp::from_nanos(0),
);
let plan = frame.begin_frame(Timestamp::from_nanos(remainder));
prop_assert_eq!(plan.step_count(), 0);
prop_assert_eq!(plan.remainder().get(), remainder);
prop_assert!(plan.remainder().get() < plan.timestep().nanos().get());
}
#[test]
fn the_presented_pair_is_a_function_of_the_remainder_and_timestep_alone(
dt in 2u64..=1_000_000_000,
fraction in 0u64..=u64::MAX,
) {
let remainder = fraction % dt;
let mut quiet = FrameLoop::new(timestep(dt), budget(1), Timestamp::from_nanos(0));
let calm = quiet.begin_frame(Timestamp::from_nanos(remainder));
let mut busy = FrameLoop::new(timestep(dt), budget(1), Timestamp::from_nanos(0));
let _ = busy.begin_frame(Timestamp::from_nanos(2 * dt));
let stalled = busy.begin_frame(Timestamp::from_nanos(6 * dt + remainder));
prop_assert_eq!(stalled.remainder(), calm.remainder());
prop_assert_ne!(stalled.first_tick(), calm.first_tick());
prop_assert_ne!(stalled.step_count(), calm.step_count());
prop_assert!(stalled.dropped() > 0);
prop_assert_eq!(stalled.timestep().nanos(), calm.timestep().nanos());
}
#[test]
fn identical_traces_produce_identical_schedules(
dt in 1u64..=1_000_000_000,
allowance in 1u32..=8,
deltas in prop::collection::vec(0u64..2_000_000_000, 1..16),
) {
let run = || {
let mut frame = FrameLoop::new(
timestep(dt),
budget(allowance),
Timestamp::from_nanos(0),
);
let mut stats = FrameStats::new();
let mut now = 0u64;
let plans: Vec<_> = deltas
.iter()
.map(|delta| {
now += delta;
let plan = frame.begin_frame(Timestamp::from_nanos(now));
stats.absorb(&plan);
plan
})
.collect();
(plans, stats, frame)
};
prop_assert_eq!(run(), run());
}
}