pub mod handle;
pub mod launch;
pub mod log_runner;
pub mod multi_runner;
pub mod runner;
pub mod stats;
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct GapWindow {
pub every: Duration,
pub duration: Duration,
}
#[derive(Debug, Clone)]
pub struct BurstWindow {
pub every: Duration,
pub duration: Duration,
pub multiplier: f64,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct Schedule {
pub rate: f64,
pub duration: Option<Duration>,
pub gap: Option<GapWindow>,
pub burst: Option<BurstWindow>,
}
pub fn is_in_burst(elapsed: Duration, burst: &BurstWindow) -> Option<f64> {
let every_secs = burst.every.as_secs_f64();
let duration_secs = burst.duration.as_secs_f64();
let cycle_pos = elapsed.as_secs_f64() % every_secs;
if cycle_pos < duration_secs {
Some(burst.multiplier)
} else {
None
}
}
pub fn time_until_burst_end(elapsed: Duration, burst: &BurstWindow) -> Duration {
let every_secs = burst.every.as_secs_f64();
let duration_secs = burst.duration.as_secs_f64();
let cycle_pos = elapsed.as_secs_f64() % every_secs;
let remaining_secs = duration_secs - cycle_pos;
if remaining_secs <= 0.0 {
Duration::ZERO
} else {
Duration::from_secs_f64(remaining_secs)
}
}
pub fn is_in_gap(elapsed: Duration, gap: &GapWindow) -> bool {
let every_secs = gap.every.as_secs_f64();
let duration_secs = gap.duration.as_secs_f64();
let cycle_pos = elapsed.as_secs_f64() % every_secs;
cycle_pos >= every_secs - duration_secs
}
pub fn time_until_gap_end(elapsed: Duration, gap: &GapWindow) -> Duration {
let every_secs = gap.every.as_secs_f64();
let cycle_pos = elapsed.as_secs_f64() % every_secs;
let remaining_secs = every_secs - cycle_pos;
if remaining_secs <= 0.0 {
Duration::ZERO
} else {
Duration::from_secs_f64(remaining_secs)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn gap(every_secs: u64, duration_secs: u64) -> GapWindow {
GapWindow {
every: Duration::from_secs(every_secs),
duration: Duration::from_secs(duration_secs),
}
}
#[test]
fn is_in_gap_at_zero_is_false() {
let g = gap(10, 2);
assert!(!is_in_gap(Duration::from_secs(0), &g));
}
#[test]
fn is_in_gap_at_8_5s_is_true() {
let g = gap(10, 2);
assert!(is_in_gap(Duration::from_millis(8500), &g));
}
#[test]
fn is_in_gap_at_exact_gap_start_is_true() {
let g = gap(10, 2);
assert!(is_in_gap(Duration::from_secs(8), &g));
}
#[test]
fn is_in_gap_at_10s_new_cycle_is_false() {
let g = gap(10, 2);
assert!(!is_in_gap(Duration::from_secs(10), &g));
}
#[test]
fn is_in_gap_at_18_5s_second_cycle_is_true() {
let g = gap(10, 2);
assert!(is_in_gap(Duration::from_millis(18500), &g));
}
#[test]
fn is_in_gap_at_20s_third_cycle_start_is_false() {
let g = gap(10, 2);
assert!(!is_in_gap(Duration::from_secs(20), &g));
}
#[test]
fn is_in_gap_at_5s_is_false() {
let g = gap(10, 2);
assert!(!is_in_gap(Duration::from_secs(5), &g));
}
#[test]
fn is_in_gap_sub_millisecond_gap_duration() {
let g = GapWindow {
every: Duration::from_secs(10),
duration: Duration::from_millis(1),
};
assert!(is_in_gap(Duration::from_millis(9999), &g));
assert!(!is_in_gap(Duration::from_secs(5), &g));
}
#[test]
fn is_in_gap_minute_scale_cycle() {
let g = GapWindow {
every: Duration::from_secs(120),
duration: Duration::from_secs(20),
};
assert!(!is_in_gap(Duration::from_secs(0), &g));
assert!(!is_in_gap(Duration::from_secs(50), &g));
assert!(!is_in_gap(Duration::from_secs(99), &g));
assert!(is_in_gap(Duration::from_secs(100), &g));
assert!(is_in_gap(Duration::from_secs(110), &g));
assert!(is_in_gap(Duration::from_secs(119), &g));
assert!(!is_in_gap(Duration::from_secs(120), &g));
}
#[test]
fn time_until_gap_end_at_9s_returns_1s() {
let g = gap(10, 2);
let remaining = time_until_gap_end(Duration::from_secs(9), &g);
let diff = (remaining.as_secs_f64() - 1.0).abs();
assert!(
diff < 0.001,
"expected ~1s remaining, got {remaining:?} (diff={diff})"
);
}
#[test]
fn time_until_gap_end_at_gap_start_returns_gap_duration() {
let g = gap(10, 2);
let remaining = time_until_gap_end(Duration::from_secs(8), &g);
let diff = (remaining.as_secs_f64() - 2.0).abs();
assert!(
diff < 0.001,
"expected ~2s remaining, got {remaining:?} (diff={diff})"
);
}
#[test]
fn time_until_gap_end_near_cycle_boundary_is_non_negative() {
let g = gap(10, 2);
let remaining = time_until_gap_end(Duration::from_millis(9999), &g);
assert!(
remaining >= Duration::ZERO,
"remaining must never be negative"
);
assert!(
remaining.as_millis() <= 2,
"expected ~1ms, got {remaining:?}"
);
}
#[test]
fn time_until_gap_end_second_cycle_at_18s() {
let g = gap(10, 2);
let remaining = time_until_gap_end(Duration::from_secs(18), &g);
let diff = (remaining.as_secs_f64() - 2.0).abs();
assert!(
diff < 0.001,
"expected ~2s remaining in second cycle, got {remaining:?}"
);
}
#[test]
fn rate_1000_yields_1ms_interval() {
let interval = Duration::from_secs_f64(1.0 / 1000.0);
assert_eq!(interval.as_millis(), 1);
}
#[test]
fn rate_1_yields_1s_interval() {
let interval = Duration::from_secs_f64(1.0 / 1.0);
assert_eq!(interval.as_secs(), 1);
}
#[test]
fn rate_0_5_yields_2s_interval() {
let interval = Duration::from_secs_f64(1.0 / 0.5);
assert_eq!(interval.as_secs(), 2);
}
#[test]
fn gap_window_is_cloneable() {
let g = gap(10, 2);
let cloned = g.clone();
assert_eq!(cloned.every, Duration::from_secs(10));
assert_eq!(cloned.duration, Duration::from_secs(2));
}
#[test]
fn gap_window_is_debuggable() {
let g = gap(10, 2);
let s = format!("{g:?}");
assert!(s.contains("GapWindow"), "Debug output must name the struct");
}
#[test]
fn schedule_is_cloneable_and_debuggable() {
let sched = Schedule {
rate: 100.0,
duration: Some(Duration::from_secs(30)),
gap: Some(gap(10, 2)),
burst: None,
};
let cloned = sched.clone();
assert_eq!(cloned.rate, 100.0);
let s = format!("{sched:?}");
assert!(s.contains("Schedule"));
}
#[test]
fn burst_window_is_cloneable() {
let b = BurstWindow {
every: Duration::from_secs(10),
duration: Duration::from_secs(2),
multiplier: 5.0,
};
let cloned = b.clone();
assert_eq!(cloned.every, Duration::from_secs(10));
assert_eq!(cloned.duration, Duration::from_secs(2));
assert_eq!(cloned.multiplier, 5.0);
}
#[test]
fn burst_window_is_debuggable() {
let b = BurstWindow {
every: Duration::from_secs(10),
duration: Duration::from_secs(2),
multiplier: 5.0,
};
let s = format!("{b:?}");
assert!(
s.contains("BurstWindow"),
"Debug output must name the struct"
);
}
fn burst(every_secs: u64, duration_secs: u64, multiplier: f64) -> BurstWindow {
BurstWindow {
every: Duration::from_secs(every_secs),
duration: Duration::from_secs(duration_secs),
multiplier,
}
}
#[test]
fn is_in_burst_at_zero_is_some_multiplier() {
let b = burst(10, 2, 5.0);
let result = is_in_burst(Duration::ZERO, &b);
assert_eq!(
result,
Some(5.0),
"at elapsed=0s the burst occupies [0, duration) so should be Some"
);
}
#[test]
fn is_in_burst_at_0_5s_returns_some_multiplier() {
let b = burst(10, 2, 5.0);
let result = is_in_burst(Duration::from_millis(500), &b);
assert_eq!(
result,
Some(5.0),
"at 0.5s (cycle_pos=0.5 < duration=2) burst must be active"
);
}
#[test]
fn is_in_burst_at_burst_end_boundary_returns_none() {
let b = burst(10, 2, 5.0);
let result = is_in_burst(Duration::from_secs(2), &b);
assert!(
result.is_none(),
"at elapsed=2.0s (cycle_pos=2.0 == duration) burst must be None"
);
}
#[test]
fn is_in_burst_at_2_5s_returns_none() {
let b = burst(10, 2, 5.0);
let result = is_in_burst(Duration::from_millis(2500), &b);
assert!(
result.is_none(),
"at 2.5s (cycle_pos=2.5 > duration=2) burst must be None"
);
}
#[test]
fn is_in_burst_at_5s_is_none() {
let b = burst(10, 2, 5.0);
assert!(is_in_burst(Duration::from_secs(5), &b).is_none());
}
#[test]
fn is_in_burst_at_9_5s_is_none() {
let b = burst(10, 2, 5.0);
assert!(is_in_burst(Duration::from_millis(9500), &b).is_none());
}
#[test]
fn is_in_burst_at_10s_second_cycle_start_is_some() {
let b = burst(10, 2, 5.0);
let result = is_in_burst(Duration::from_secs(10), &b);
assert_eq!(
result,
Some(5.0),
"at 10s (start of cycle 2) burst must be active again"
);
}
#[test]
fn is_in_burst_at_10_5s_second_cycle_is_some() {
let b = burst(10, 2, 5.0);
let result = is_in_burst(Duration::from_millis(10500), &b);
assert_eq!(result, Some(5.0));
}
#[test]
fn is_in_burst_at_12_5s_second_cycle_is_none() {
let b = burst(10, 2, 5.0);
let result = is_in_burst(Duration::from_millis(12500), &b);
assert!(result.is_none());
}
#[test]
fn is_in_burst_returns_correct_multiplier_value() {
let b = burst(10, 2, 10.0);
let result = is_in_burst(Duration::from_millis(500), &b);
assert_eq!(result, Some(10.0), "multiplier must equal configured value");
}
#[test]
fn is_in_burst_with_multiplier_one_returns_some() {
let b = burst(10, 2, 1.0);
let result = is_in_burst(Duration::from_millis(500), &b);
assert_eq!(result, Some(1.0));
}
#[test]
fn time_until_burst_end_at_zero_returns_burst_duration() {
let b = burst(10, 2, 5.0);
let remaining = time_until_burst_end(Duration::ZERO, &b);
let diff = (remaining.as_secs_f64() - 2.0).abs();
assert!(
diff < 0.001,
"at elapsed=0 expected ~2s remaining, got {remaining:?}"
);
}
#[test]
fn time_until_burst_end_at_0_5s_returns_1_5s() {
let b = burst(10, 2, 5.0);
let remaining = time_until_burst_end(Duration::from_millis(500), &b);
let diff = (remaining.as_secs_f64() - 1.5).abs();
assert!(
diff < 0.001,
"at 0.5s expected ~1.5s remaining, got {remaining:?}"
);
}
#[test]
fn time_until_burst_end_at_1_9s_returns_0_1s() {
let b = burst(10, 2, 5.0);
let remaining = time_until_burst_end(Duration::from_millis(1900), &b);
let diff = (remaining.as_secs_f64() - 0.1).abs();
assert!(
diff < 0.005,
"at 1.9s expected ~0.1s remaining, got {remaining:?}"
);
}
#[test]
fn time_until_burst_end_at_exact_boundary_is_non_negative() {
let b = burst(10, 2, 5.0);
let remaining = time_until_burst_end(Duration::from_secs(2), &b);
assert!(
remaining >= Duration::ZERO,
"remaining must never be negative, got {remaining:?}"
);
}
#[test]
fn time_until_burst_end_second_cycle_at_10_5s_returns_1_5s() {
let b = burst(10, 2, 5.0);
let remaining = time_until_burst_end(Duration::from_millis(10500), &b);
let diff = (remaining.as_secs_f64() - 1.5).abs();
assert!(
diff < 0.001,
"in second cycle at 10.5s expected ~1.5s remaining, got {remaining:?}"
);
}
}