#![no_std]
#![forbid(unsafe_code)]
#![warn(missing_docs)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct Timeout {
budget: u64,
}
impl Timeout {
pub const fn new(budget: u64) -> Self {
Self { budget }
}
pub const fn budget(&self) -> u64 {
self.budget
}
pub const fn start(&self, now: u64) -> Deadline {
Deadline::new(now, self.budget)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct Deadline {
start: u64,
budget: u64,
}
impl Deadline {
pub const fn new(start: u64, budget: u64) -> Self {
Self { start, budget }
}
pub const fn start(&self) -> u64 {
self.start
}
pub const fn budget(&self) -> u64 {
self.budget
}
pub const fn expiry(&self) -> u64 {
self.start.saturating_add(self.budget)
}
pub const fn elapsed(&self, now: u64) -> u64 {
now.saturating_sub(self.start)
}
pub const fn remaining(&self, now: u64) -> u64 {
self.expiry().saturating_sub(now)
}
pub const fn is_expired(&self, now: u64) -> bool {
now >= self.expiry()
}
pub const fn check(&self, now: u64) -> Option<u64> {
if self.is_expired(now) {
None
} else {
Some(self.remaining(now))
}
}
pub const fn allows(&self, now: u64, duration: u64) -> bool {
self.remaining(now) >= duration
}
pub const fn clamp(&self, now: u64, duration: u64) -> u64 {
let left = self.remaining(now);
if duration < left {
duration
} else {
left
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn timeout_starts_a_deadline() {
let t = Timeout::new(100);
assert_eq!(t.budget(), 100);
let d = t.start(50);
assert_eq!(d, Deadline::new(50, 100));
assert_eq!(d.start(), 50);
assert_eq!(d.budget(), 100);
assert_eq!(d.expiry(), 150);
}
#[test]
fn remaining_and_elapsed_track_time() {
let d = Deadline::new(1_000, 500);
assert_eq!(d.elapsed(1_000), 0);
assert_eq!(d.remaining(1_000), 500);
assert_eq!(d.elapsed(1_200), 200);
assert_eq!(d.remaining(1_200), 300);
assert_eq!(d.elapsed(1_500), 500);
assert_eq!(d.remaining(1_500), 0);
}
#[test]
fn expiry_boundary_is_inclusive() {
let d = Deadline::new(0, 10);
assert!(!d.is_expired(9));
assert!(d.is_expired(10)); assert!(d.is_expired(11));
assert_eq!(d.check(9), Some(1));
assert_eq!(d.check(10), None);
}
#[test]
fn zero_budget_expires_immediately() {
let d = Deadline::new(42, 0);
assert_eq!(d.expiry(), 42);
assert!(d.is_expired(42));
assert_eq!(d.remaining(42), 0);
assert_eq!(d.check(42), None);
assert!(!d.is_expired(41));
assert_eq!(d.check(41), Some(1));
}
#[test]
fn backwards_clock_does_not_panic_or_underflow() {
let d = Deadline::new(1_000, 200);
assert_eq!(d.elapsed(0), 0);
assert_eq!(d.remaining(0), 1_200);
assert!(!d.is_expired(0));
}
#[test]
fn expiry_saturates_on_overflow() {
let d = Deadline::new(u64::MAX - 5, 100);
assert_eq!(d.expiry(), u64::MAX);
assert!(!d.is_expired(u64::MAX - 1));
assert!(d.is_expired(u64::MAX));
assert_eq!(d.remaining(u64::MAX - 10), 10);
}
#[test]
fn allows_checks_fit() {
let d = Deadline::new(0, 100);
assert!(d.allows(0, 100));
assert!(d.allows(0, 99));
assert!(!d.allows(0, 101));
assert!(d.allows(60, 40));
assert!(!d.allows(60, 41));
assert!(!d.allows(100, 1)); assert!(d.allows(0, 0)); assert!(d.allows(100, 0)); }
#[test]
fn clamp_caps_duration_by_remaining() {
let d = Deadline::new(0, 1_000);
assert_eq!(d.clamp(0, 800), 800); assert_eq!(d.clamp(500, 800), 500); assert_eq!(d.clamp(1_000, 800), 0); assert_eq!(d.clamp(0, 1_000), 1_000); }
#[test]
fn defaults_are_zero() {
assert_eq!(Timeout::default(), Timeout::new(0));
assert_eq!(Deadline::default(), Deadline::new(0, 0));
assert!(Deadline::default().is_expired(0));
assert_eq!(Deadline::default().check(0), None);
}
}