1use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
4
5#[derive(Debug, thiserror::Error)]
7#[error("invalid duration: {0}")]
8pub struct TtlParseError(String);
9
10pub type Timestamp = i64;
12
13pub trait Clock: Send + Sync {
25 fn now(&self) -> Instant;
27
28 fn unix_now(&self) -> Timestamp;
30}
31
32#[derive(Debug, Clone, Copy, Default)]
34pub struct SystemClock;
35
36impl Clock for SystemClock {
37 fn now(&self) -> Instant {
38 Instant::now()
39 }
40
41 fn unix_now(&self) -> Timestamp {
42 SystemTime::now()
43 .duration_since(UNIX_EPOCH)
44 .map(|d| d.as_secs() as Timestamp)
45 .unwrap_or(0)
46 }
47}
48
49#[derive(Debug)]
51pub struct MockClock {
52 inner: parking_lot::Mutex<Instant>,
53 unix_inner: parking_lot::Mutex<Timestamp>,
54}
55
56impl MockClock {
57 pub fn new() -> Self {
58 Self {
59 inner: parking_lot::Mutex::new(Instant::now()),
60 unix_inner: parking_lot::Mutex::new(0),
61 }
62 }
63
64 pub fn set_unix(&self, t: Timestamp) {
66 *self.unix_inner.lock() = t;
67 }
68
69 pub fn advance_unix(&self, d: Duration) {
72 *self.unix_inner.lock() += d.as_secs() as Timestamp;
73 let now = *self.inner.lock();
74 *self.inner.lock() = now.checked_add(d).unwrap_or(now);
75 }
76}
77
78impl Default for MockClock {
79 fn default() -> Self {
80 Self::new()
81 }
82}
83
84impl Clock for MockClock {
85 fn now(&self) -> Instant {
86 *self.inner.lock()
87 }
88
89 fn unix_now(&self) -> Timestamp {
90 *self.unix_inner.lock()
91 }
92}
93
94pub fn parse_duration(s: &str) -> std::result::Result<Duration, TtlParseError> {
99 use std::str::FromStr;
100 humantime::Duration::from_str(s)
101 .map(Into::into)
102 .map_err(|e| TtlParseError(e.to_string()))
103}
104
105pub fn format_duration(d: Duration) -> String {
107 humantime::format_duration(d).to_string()
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 #[test]
115 fn system_clock_returns_sane_unix_now() {
116 let c = SystemClock;
117 let t = c.unix_now();
118 assert!(t > 1_500_000_000, "unix_now returned {}", t);
120 }
121
122 #[test]
123 fn mock_clock_advances() {
124 let c = MockClock::new();
125 c.set_unix(100);
126 assert_eq!(c.unix_now(), 100);
127 c.advance_unix(Duration::from_secs(60));
128 assert_eq!(c.unix_now(), 160);
129 }
130
131 #[test]
132 fn parse_and_format_roundtrip() {
133 let cases = ["30s", "5m", "2h", "1d"];
134 for s in cases {
135 let d = parse_duration(s).unwrap();
136 assert!(format_duration(d).contains(|c: char| c.is_ascii_digit()));
137 }
138 }
139}