use midstreamer_scheduler::{
Deadline, Priority, RealtimeScheduler, SchedulerConfig, SchedulingPolicy,
};
use proptest::prelude::*;
use std::time::Duration;
fn priority(idx: u8) -> Priority {
match idx % 5 {
0 => Priority::Critical,
1 => Priority::High,
2 => Priority::Medium,
3 => Priority::Low,
_ => Priority::Background,
}
}
fn task_specs() -> impl Strategy<Value = Vec<(u8, u64, u32)>> {
proptest::collection::vec((0u8..=4, 1u64..=1_000_000, any::<u32>()), 0..=32)
}
fn fresh_scheduler(max_queue_size: usize) -> RealtimeScheduler<u32> {
RealtimeScheduler::new(SchedulerConfig {
policy: SchedulingPolicy::FixedPriority,
max_queue_size,
enable_rt_scheduling: false,
cpu_affinity: None,
})
}
proptest! {
#[test]
fn queue_size_equals_schedule_count(specs in task_specs()) {
let s = fresh_scheduler(specs.len().max(1));
for (p, micros, payload) in &specs {
s.schedule(*payload, Deadline::from_micros(*micros), priority(*p)).unwrap();
}
prop_assert_eq!(s.queue_size(), specs.len());
}
#[test]
fn clear_empties_queue(specs in task_specs()) {
let s = fresh_scheduler(specs.len().max(1));
for (p, micros, payload) in &specs {
s.schedule(*payload, Deadline::from_micros(*micros), priority(*p)).unwrap();
}
s.clear();
prop_assert_eq!(s.queue_size(), 0);
}
}
proptest! {
#[test]
fn queue_full_at_capacity(cap in 1usize..=8) {
let s = fresh_scheduler(cap);
for i in 0..cap {
s.schedule(i as u32, Deadline::from_micros(1_000), Priority::Medium)
.expect("under-cap schedule should not fail");
}
prop_assert_eq!(s.queue_size(), cap);
let err = s.schedule(99, Deadline::from_micros(1_000), Priority::Medium);
prop_assert!(err.is_err(), "over-cap schedule unexpectedly succeeded");
}
}
proptest! {
#[test]
fn next_task_decreases_size_monotonically(specs in task_specs()) {
let s = fresh_scheduler(specs.len().max(1));
for (p, micros, payload) in &specs {
s.schedule(*payload, Deadline::from_micros(*micros), priority(*p)).unwrap();
}
let mut remaining = s.queue_size();
while remaining > 0 {
let popped = s.next_task();
prop_assert!(popped.is_some(), "pop returned None with non-empty queue");
remaining -= 1;
prop_assert_eq!(s.queue_size(), remaining);
}
prop_assert!(s.next_task().is_none(), "empty queue must return None");
}
#[test]
fn next_task_pops_every_scheduled_exactly_once(specs in task_specs()) {
let s = fresh_scheduler(specs.len().max(1));
let expected = specs.len();
for (p, micros, payload) in &specs {
s.schedule(*payload, Deadline::from_micros(*micros), priority(*p)).unwrap();
}
let mut popped = 0;
while s.next_task().is_some() {
popped += 1;
prop_assert!(popped <= expected, "popped more tasks than scheduled");
}
prop_assert_eq!(popped, expected);
}
#[test]
fn next_task_emits_priority_desc_then_deadline_asc(specs in task_specs()) {
let s = fresh_scheduler(specs.len().max(1));
for (p, micros, payload) in &specs {
s.schedule(*payload, Deadline::from_micros(*micros), priority(*p)).unwrap();
}
let mut last: Option<(i32, std::time::Instant)> = None;
while let Some(task) = s.next_task() {
let curr = (task.priority.as_i32(), task.deadline.absolute_time);
if let Some(prev) = last {
prop_assert!(
prev.0 >= curr.0,
"priority order violated: prev={} curr={}", prev.0, curr.0
);
if prev.0 == curr.0 {
prop_assert!(
prev.1 <= curr.1,
"deadline order violated within priority {}", curr.0
);
}
}
last = Some(curr);
}
}
#[test]
fn empty_queue_yields_none(_unit in proptest::strategy::Just(())) {
let s = fresh_scheduler(8);
prop_assert!(s.next_task().is_none());
}
}
proptest! {
#[test]
fn deadline_from_now_is_future(micros in 1u64..=1_000_000) {
let d = Deadline::from_now(Duration::from_micros(micros));
prop_assert!(!d.is_passed(), "freshly-created deadline already passed");
}
#[test]
fn deadline_zero_is_passed(_unit in proptest::strategy::Just(())) {
let d = Deadline::from_now(Duration::from_micros(0));
prop_assert!(d.is_passed() || d.time_until() == Some(Duration::ZERO));
}
}