#![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(feature = "core")]
impl Timeout {
pub fn start_now<C: reliakit_core::Clock>(&self, clock: &C) -> Deadline {
self.start(clock.now())
}
}
#[cfg(feature = "core")]
impl Deadline {
pub fn elapsed_now<C: reliakit_core::Clock>(&self, clock: &C) -> u64 {
self.elapsed(clock.now())
}
pub fn remaining_now<C: reliakit_core::Clock>(&self, clock: &C) -> u64 {
self.remaining(clock.now())
}
pub fn is_expired_now<C: reliakit_core::Clock>(&self, clock: &C) -> bool {
self.is_expired(clock.now())
}
pub fn check_now<C: reliakit_core::Clock>(&self, clock: &C) -> Option<u64> {
self.check(clock.now())
}
pub fn allows_now<C: reliakit_core::Clock>(&self, clock: &C, duration: u64) -> bool {
self.allows(clock.now(), duration)
}
pub fn clamp_now<C: reliakit_core::Clock>(&self, clock: &C, duration: u64) -> u64 {
self.clamp(clock.now(), duration)
}
}
#[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);
}
}
#[cfg(all(test, feature = "core"))]
mod core_tests {
use super::*;
use reliakit_core::ManualClock;
#[test]
fn now_methods_match_explicit_now() {
let clock = ManualClock::new(1_000);
let deadline = Timeout::new(500).start_now(&clock);
assert_eq!(deadline, Timeout::new(500).start(1_000));
clock.set(1_200);
assert_eq!(deadline.elapsed_now(&clock), deadline.elapsed(1_200));
assert_eq!(deadline.remaining_now(&clock), deadline.remaining(1_200));
assert_eq!(deadline.is_expired_now(&clock), deadline.is_expired(1_200));
assert_eq!(deadline.check_now(&clock), deadline.check(1_200));
assert_eq!(
deadline.allows_now(&clock, 200),
deadline.allows(1_200, 200)
);
assert_eq!(deadline.clamp_now(&clock, 400), deadline.clamp(1_200, 400));
clock.set(2_000); assert!(deadline.is_expired_now(&clock));
assert_eq!(deadline.check_now(&clock), None);
}
}