ppoppo-clock 0.1.0

Universal Clock + Timer port for ppoppo workspace — chat-as-infrastructure primitive
Documentation
use crate::{Clock, Timer, Tz, ZonedDateTime};
use futures::channel::oneshot;
use futures::future::BoxFuture;
use std::sync::{Arc, Mutex};
use time::{Date, Duration, Month, OffsetDateTime};

// ── FrozenClock ────────────────────────────────────────────────────────────

/// A clock that always returns the same instant. ~80% of test scenarios.
pub struct FrozenClock {
    instant: OffsetDateTime,
}

impl FrozenClock {
    /// Parse an RFC 3339 string into a frozen instant.
    pub fn at(rfc3339: &str) -> Self {
        let fmt = time::format_description::well_known::Rfc3339;
        let instant = OffsetDateTime::parse(rfc3339, &fmt)
            .unwrap_or(OffsetDateTime::UNIX_EPOCH);
        Self { instant }
    }

    pub fn at_instant(instant: OffsetDateTime) -> Self {
        Self { instant }
    }
}

impl Clock for FrozenClock {
    fn now_utc(&self) -> OffsetDateTime {
        self.instant
    }

    fn now_in(&self, tz: &Tz) -> ZonedDateTime {
        ZonedDateTime::new(self.instant, tz.clone())
    }

    fn today_in(&self, tz: &Tz) -> Date {
        let p = self.now_in(tz).local_parts();
        Month::try_from(p.month_1)
            .ok()
            .and_then(|m| Date::from_calendar_date(p.year, m, p.day).ok())
            .unwrap_or_else(|| self.instant.date())
    }

    fn now_unix_millis(&self) -> i64 {
        (self.instant.unix_timestamp_nanos() / 1_000_000) as i64
    }
}

// ── MockClock ──────────────────────────────────────────────────────────────

type PendingSleeps = Vec<(OffsetDateTime, oneshot::Sender<()>)>;

/// A settable, advanceable clock for time-travel tests.
pub struct MockClock {
    inner: Arc<Mutex<OffsetDateTime>>,
    pending: Arc<Mutex<PendingSleeps>>,
}

impl MockClock {
    pub fn new(start: OffsetDateTime) -> Self {
        Self {
            inner: Arc::new(Mutex::new(start)),
            pending: Arc::new(Mutex::new(Vec::new())),
        }
    }

    pub fn set_now(&self, instant: OffsetDateTime) {
        let mut guard = self.inner.lock().unwrap_or_else(|e| e.into_inner());
        *guard = instant;
        drop(guard);
        self.fire_pending();
    }

    pub fn advance_by(&self, dur: Duration) {
        let new_now = {
            let mut guard = self.inner.lock().unwrap_or_else(|e| e.into_inner());
            *guard += dur;
            *guard
        };
        self.fire_ready(new_now);
    }

    /// Advance time and fire all sleeps whose deadline has passed.
    /// Equivalent to `tokio::time::advance` for mock-aware code.
    pub fn advance_and_fire(&self, dur: Duration) {
        self.advance_by(dur);
    }

    fn current(&self) -> OffsetDateTime {
        *self.inner.lock().unwrap_or_else(|e| e.into_inner())
    }

    fn fire_pending(&self) {
        let now = self.current();
        self.fire_ready(now);
    }

    fn fire_ready(&self, now: OffsetDateTime) {
        // Drain matching entries while holding the lock; drop lock before firing.
        let to_fire: Vec<oneshot::Sender<()>> = {
            let mut pending = self.pending.lock().unwrap_or_else(|e| e.into_inner());
            let mut fired = Vec::new();
            pending.retain(|(wake_at, _)| *wake_at > now);
            // collect the ones we removed
            let mut remaining: PendingSleeps = Vec::new();
            let mut original: PendingSleeps = Vec::new();
            std::mem::swap(&mut *pending, &mut original);
            for (wake_at, sender) in original {
                if wake_at <= now {
                    fired.push(sender);
                } else {
                    remaining.push((wake_at, sender));
                }
            }
            *pending = remaining;
            fired
        };
        for sender in to_fire {
            // ignore send errors — receiver may have been dropped
            let _ = sender.send(());
        }
    }

    pub(crate) fn register_sleep(
        &self,
        wake_at: OffsetDateTime,
    ) -> oneshot::Receiver<()> {
        let (tx, rx) = oneshot::channel();
        let now = self.current();
        if wake_at <= now {
            // Already past — fire immediately.
            let _ = tx.send(());
        } else {
            let mut pending = self.pending.lock().unwrap_or_else(|e| e.into_inner());
            pending.push((wake_at, tx));
        }
        rx
    }
}

impl Clock for MockClock {
    fn now_utc(&self) -> OffsetDateTime {
        self.current()
    }

    fn now_in(&self, tz: &Tz) -> ZonedDateTime {
        ZonedDateTime::new(self.current(), tz.clone())
    }

    fn today_in(&self, tz: &Tz) -> Date {
        let p = self.now_in(tz).local_parts();
        Month::try_from(p.month_1)
            .ok()
            .and_then(|m| Date::from_calendar_date(p.year, m, p.day).ok())
            .unwrap_or_else(|| self.current().date())
    }

    fn now_unix_millis(&self) -> i64 {
        (self.current().unix_timestamp_nanos() / 1_000_000) as i64
    }
}

// ── AdvanceableTimer ───────────────────────────────────────────────────────

/// A timer whose sleeps resolve only when `MockClock::advance_and_fire` is called.
/// Pair with `MockClock` for deterministic time-travel tests.
pub struct AdvanceableTimer {
    clock: Arc<MockClock>,
}

impl AdvanceableTimer {
    pub fn new(clock: Arc<MockClock>) -> Self {
        Self { clock }
    }
}

impl Timer for AdvanceableTimer {
    fn sleep(&self, dur: Duration) -> BoxFuture<'static, ()> {
        let now = self.clock.current();
        let wake_at = now + dur;
        let rx = self.clock.register_sleep(wake_at);
        Box::pin(async move {
            // Discard errors — if sender dropped, treat as woken up.
            let _ = rx.await;
        })
    }

    fn next_tick(&self) -> BoxFuture<'static, ()> {
        Box::pin(futures::future::ready(()))
    }
}

// ── Test ergonomics helpers ────────────────────────────────────────────────

use std::sync::Arc as StdArc;

/// Returns an `Arc<dyn Clock>` frozen at the given RFC 3339 string.
pub fn frozen_at(rfc3339: &str) -> StdArc<dyn Clock> {
    StdArc::new(FrozenClock::at(rfc3339))
}

/// Returns a `(Arc<MockClock>, Arc<AdvanceableTimer>)` pair wired together.
pub fn mock_pair(start: OffsetDateTime) -> (StdArc<MockClock>, StdArc<AdvanceableTimer>) {
    let clock = StdArc::new(MockClock::new(start));
    let timer = StdArc::new(AdvanceableTimer::new(StdArc::clone(&clock)));
    (clock, timer)
}

#[cfg(test)]
mod tests {
    use super::*;
    use time::macros::datetime;

    fn t0() -> OffsetDateTime {
        datetime!(2026-05-10 00:00:00 UTC)
    }

    // ── FrozenClock tests ──────────────────────────────────────────────

    #[test]
    fn frozen_at_parses_rfc3339() {
        let clock = FrozenClock::at("2026-05-10T00:00:00Z");
        assert_eq!(clock.now_utc(), t0());
    }

    #[test]
    fn frozen_clock_at_instant() {
        let clock = FrozenClock::at_instant(t0());
        assert_eq!(clock.now_utc(), t0());
    }

    #[test]
    fn frozen_clock_never_advances() {
        let clock = FrozenClock::at_instant(t0());
        let a = clock.now_utc();
        let b = clock.now_utc();
        assert_eq!(a, b);
    }

    #[test]
    fn frozen_clock_unix_millis() {
        let clock = FrozenClock::at("1970-01-01T00:00:01Z");
        assert_eq!(clock.now_unix_millis(), 1_000);
    }

    #[test]
    fn frozen_at_helper_trait_dispatch() {
        let c: StdArc<dyn Clock> = frozen_at("2026-05-10T00:00:00Z");
        assert_eq!(c.now_utc(), t0());
    }

    // ── MockClock tests ────────────────────────────────────────────────

    #[test]
    fn mock_clock_starts_at_given_instant() {
        let clock = MockClock::new(t0());
        assert_eq!(clock.now_utc(), t0());
    }

    #[test]
    fn mock_clock_advance_by_increases_time() {
        let clock = MockClock::new(t0());
        clock.advance_by(Duration::seconds(1));
        assert_eq!(clock.now_utc(), t0() + Duration::seconds(1));
    }

    #[test]
    fn mock_clock_set_now_updates_instant() {
        let clock = MockClock::new(t0());
        let new_now = t0() + Duration::hours(5);
        clock.set_now(new_now);
        assert_eq!(clock.now_utc(), new_now);
    }

    #[test]
    fn mock_clock_trait_dispatch() {
        let c: StdArc<dyn Clock> = StdArc::new(MockClock::new(t0()));
        assert_eq!(c.now_utc(), t0());
    }

    // ── AdvanceableTimer tests ─────────────────────────────────────────

    #[tokio::test]
    async fn advanceable_timer_sleep_resolves_after_advance() {
        let (clock, timer) = mock_pair(t0());
        let sleep_fut = timer.sleep(Duration::seconds(60));
        // Advance past the deadline in a concurrent task
        let clock2 = StdArc::clone(&clock);
        tokio::spawn(async move {
            clock2.advance_and_fire(Duration::seconds(60));
        })
        .await
        .ok();
        sleep_fut.await; // should complete
    }

    #[tokio::test]
    async fn advanceable_timer_partial_advance_fires_only_due_sleeps() {
        let (clock, timer) = mock_pair(t0());
        let sleep_30 = timer.sleep(Duration::seconds(30));
        let sleep_60 = timer.sleep(Duration::seconds(60));

        clock.advance_and_fire(Duration::seconds(45));

        // 30s sleep should be done
        sleep_30.await;

        // 60s sleep should NOT yet be done — fire another advance
        clock.advance_and_fire(Duration::seconds(15));
        sleep_60.await;
    }

    #[tokio::test]
    async fn advanceable_timer_next_tick_resolves_immediately() {
        let (_, timer) = mock_pair(t0());
        timer.next_tick().await;
    }

    #[test]
    fn mock_pair_helper_returns_linked_pair() {
        let (clock, timer) = mock_pair(t0());
        let _: StdArc<MockClock> = clock;
        let _: StdArc<AdvanceableTimer> = timer;
    }

    #[test]
    fn frozen_clock_send_sync() {
        fn assert_send_sync<T: Send + Sync>() {}
        assert_send_sync::<FrozenClock>();
    }

    #[test]
    fn mock_clock_send_sync() {
        fn assert_send_sync<T: Send + Sync>() {}
        assert_send_sync::<MockClock>();
        assert_send_sync::<AdvanceableTimer>();
    }
}