use hya_core::intervals::{IntervalSet, Range};
use hya_core::sched::{greedy_concurrency, Scheduler, Source};
use proptest::prelude::*;
fn src(gamma: f64) -> Source {
Source {
gamma_est: gamma,
delta_est: 0.05,
..Default::default()
}
}
fn uniform_sched(n: usize, size: u64) -> Scheduler {
let sources: Vec<Source> = (0..n).map(|_| src(1.0e6)).collect();
Scheduler::new(size, sources, &vec![1; n]).with_stall_timeout(0.3)
}
proptest! {
#[test]
fn interval_set_matches_bitmap(
ops in prop::collection::vec((0u64..256, 0u64..256, any::<bool>()), 1..60)
) {
const N: usize = 256;
let mut set = IntervalSet::new();
let mut bitmap = [false; N];
for (a, b, is_insert) in ops {
let (lo, hi) = if a <= b { (a, b) } else { (b, a) };
if is_insert {
set.insert(Range::new(lo, hi));
for i in lo..hi { bitmap[i as usize] = true; }
} else {
set.remove(lo, hi);
for i in lo..hi { bitmap[i as usize] = false; }
}
prop_assert!(set.invariant_holds(), "coalescing invariant broken: {:?}", set);
let expect = bitmap.iter().filter(|b| **b).count() as u64;
prop_assert_eq!(set.total(), expect, "total diverged from bitmap");
}
}
#[test]
fn take_front_conserves(size in 1u64..100_000, takes in prop::collection::vec(1u64..5000, 1..40)) {
let mut set = IntervalSet::full(size);
let mut taken = 0u64;
for n in takes {
if let Some(r) = set.take_front(n) {
prop_assert!(r.len() <= n);
taken += r.len();
}
prop_assert_eq!(taken + set.total(), size);
prop_assert!(set.invariant_holds());
}
}
#[test]
fn scheduler_terminates_under_random_rates(
size in 50_000u64..1_000_000,
rates in prop::collection::vec(0u64..40_000, 2..7),
) {
let mut rates = rates;
if rates.iter().all(|r| *r == 0) { rates[0] = 10_000; }
let n = rates.len();
let sources: Vec<Source> = rates.iter().map(|r| src(*r as f64 * 100.0)).collect();
let mut s = Scheduler::new(size, sources, &vec![1; n]).with_stall_timeout(0.5);
let mut now = 0.0f64;
let dt = 0.01;
let mut steps = 0u32;
while !s.is_complete() && steps < 200_000 {
s.tick(now);
for (j, r) in rates.iter().enumerate().take(n) {
if *r > 0 {
s.on_bytes(j, *r, now, dt);
}
}
prop_assert!(s.coverage_holds(), "coverage broke at t={}", now);
prop_assert!(s.liveness_holds(), "livelocked at t={}", now);
now += dt;
steps += 1;
}
prop_assert!(s.is_complete(), "did not finish: {} of {} after {} steps", s.bytes_held(), size, steps);
}
#[test]
fn scheduler_survives_mid_transfer_collapse(
size in 100_000u64..800_000,
victim in 0usize..4,
collapse_at in 5u32..200,
) {
let n = 4usize;
let mut s = uniform_sched(n, size);
let mut now = 0.0f64;
let dt = 0.01;
let mut steps = 0u32;
while !s.is_complete() && steps < 200_000 {
s.tick(now);
for j in 0..n {
let dead = j == victim % n && steps >= collapse_at;
if !dead { s.on_bytes(j, 10_000, now, dt); }
}
prop_assert!(s.coverage_holds());
prop_assert!(s.liveness_holds(), "livelocked after collapse at t={}", now);
now += dt;
steps += 1;
}
prop_assert!(s.is_complete(), "stalled at {} of {}", s.bytes_held(), size);
}
#[test]
fn suspension_preserves_coverage_and_completion(
size in 100_000u64..600_000,
suspend_at in 1u32..80,
) {
let n = 3usize;
let mut s = uniform_sched(n, size);
let mut now = 0.0f64;
let dt = 0.01;
let mut steps = 0u32;
while !s.is_complete() && steps < 200_000 {
if steps == suspend_at { s.suspend_source(0, now + 5.0); }
s.tick(now);
for j in 1..n { s.on_bytes(j, 10_000, now, dt); }
prop_assert!(s.coverage_holds());
prop_assert!(s.liveness_holds());
now += dt;
steps += 1;
}
prop_assert!(s.is_complete());
}
#[test]
fn greedy_concurrency_is_exactly_optimal(
rho_in in prop::collection::vec(1u64..40, 1..4),
gamma_in in prop::collection::vec(1u64..20, 1..4),
cap in 1u64..120,
budget in 1usize..7,
) {
let m = rho_in.len().min(gamma_in.len());
let rho: Vec<f64> = rho_in[..m].iter().map(|v| *v as f64).collect();
let gamma: Vec<f64> = gamma_in[..m].iter().map(|v| *v as f64).collect();
let cap = cap as f64;
let g = |n: &[usize]| -> f64 {
let s: f64 = (0..m).map(|i| rho[i].min(n[i] as f64 * gamma[i])).sum();
s.min(cap)
};
let got = g(&greedy_concurrency(&rho, &gamma, cap, budget));
let mut best = 0.0f64;
let mut idx = vec![0usize; m];
loop {
if idx.iter().sum::<usize>() <= budget {
best = best.max(g(&idx));
}
let mut k = 0usize;
loop {
if k == m { break; }
idx[k] += 1;
if idx[k] <= budget { break; }
idx[k] = 0;
k += 1;
}
if k == m { break; }
}
prop_assert!((got - best).abs() < 1e-9,
"greedy {} vs optimum {} (rho={:?} gamma={:?} cap={} budget={})",
got, best, rho, gamma, cap, budget);
}
}
#[test]
fn total_collapse_keeps_both_invariants_while_never_completing() {
let n = 3usize;
let sources: Vec<Source> = (0..n).map(|_| src(1.0e6)).collect();
let mut s = Scheduler::new(400_000, sources, &vec![1; n]).with_stall_timeout(0.3);
let mut now = 0.0f64;
for _ in 0..5_000 {
s.tick(now);
assert!(
s.coverage_holds(),
"safety must survive total collapse: the bytes are unobtainable, not lost"
);
assert!(
s.liveness_holds(),
"a reclaimable stall IS an enabled transition, so liveness holds vacuously"
);
now += 0.01;
}
assert!(
!s.is_complete(),
"nothing was delivered, so the transfer cannot be complete"
);
assert_eq!(s.bytes_held(), 0, "no progress was ever made");
}
#[test]
fn buffered_reads_do_not_inflate_the_rate_estimate() {
let n = 1usize;
let sources: Vec<Source> = (0..n).map(|_| src(1.0e6)).collect();
let mut s = Scheduler::new(8 << 20, sources, &vec![1; n]).with_stall_timeout(5.0);
let mut now = 0.0f64;
s.tick(now);
let window = 64.0 * 1024.0;
let wait = window / 1.0e6 - 60e-6; for _ in 0..40 {
s.on_bytes(0, 16 << 10, now, wait);
now += wait;
for _ in 0..3 {
s.on_bytes(0, 16 << 10, now, 20e-6);
now += 20e-6;
}
s.tick(now);
}
let est = s.conn_rate(0);
assert!(
est < 3.0e6,
"rate estimate {est:.0} B/s reflects buffer drain, not the ~1 MB/s link"
);
assert!(
est > 0.3e6,
"rate estimate {est:.0} B/s is implausibly low for a ~1 MB/s link"
);
assert!(
!s.conn_health(0).is_suspect_or_worse(),
"a healthy steady link must not be graded {:?}",
s.conn_health(0)
);
}