use std::fmt;
use std::time::{Instant, SystemTime, UNIX_EPOCH};
const NOTHING_REMEMBERED: &str = "the system clock reads before the Unix epoch and no earlier reading is remembered; barks are stamped 0 until it recovers";
const LAST_GOOD_FALLBACK: &str = "the system clock reads before the Unix epoch; barks are stamped from the last good reading until it recovers";
const CLOCK_RECOVERED: &str = "the system clock reads normally again; barks are stamped from it";
pub(super) struct Clock {
source: Box<dyn FnMut() -> SystemTime + Send>,
lines: Box<dyn FnMut(&str) + Send>,
last_good: Option<(u64, Instant)>,
failure_reported: bool,
}
impl fmt::Debug for Clock {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Clock")
.field("last_good_ms", &self.last_good.map(|(ms, _)| ms))
.field("failure_reported", &self.failure_reported)
.finish()
}
}
impl Clock {
pub(super) fn new(
source: impl FnMut() -> SystemTime + Send + 'static,
lines: impl FnMut(&str) + Send + 'static,
) -> Self {
Self {
source: Box::new(source),
lines: Box::new(lines),
last_good: None,
failure_reported: false,
}
}
pub(super) fn system() -> Self {
Self::new(SystemTime::now, |line| eprintln!("shep dog bark: {line}"))
}
#[must_use]
pub(super) fn now_ms(&mut self) -> u64 {
match (self.source)().duration_since(UNIX_EPOCH) {
Ok(elapsed) => {
let stamp = elapsed.as_millis() as u64;
self.last_good = Some((stamp, Instant::now()));
if self.failure_reported {
self.failure_reported = false;
(self.lines)(CLOCK_RECOVERED);
}
stamp
}
Err(_) => {
let stamp = match self.last_good {
Some((last, at)) => last.saturating_add(at.elapsed().as_millis() as u64),
None => 0,
};
if !self.failure_reported {
self.failure_reported = true;
(self.lines)(match self.last_good {
Some(_) => LAST_GOOD_FALLBACK,
None => NOTHING_REMEMBERED,
});
}
stamp
}
}
}
}
#[cfg(test)]
mod tests {
use std::sync::{Arc, Mutex};
use std::time::{Duration, UNIX_EPOCH};
use super::*;
type Lines = Arc<Mutex<Vec<String>>>;
fn clock_with(source: impl FnMut() -> SystemTime + Send + 'static) -> (Clock, Lines) {
let lines = Lines::default();
let reported = Arc::clone(&lines);
let sink = move |line: &str| reported.lock().unwrap().push(line.to_owned());
(Clock::new(source, sink), lines)
}
const GOOD_MS: u64 = 1_731_691_999_123;
#[test]
fn a_good_read_returns_milliseconds_since_the_epoch() {
let (mut clock, _lines) = clock_with(|| UNIX_EPOCH + Duration::from_millis(GOOD_MS));
assert_eq!(clock.now_ms(), GOOD_MS);
}
#[test]
fn a_pre_epoch_read_after_a_good_one_keeps_the_last_stamp_moving() {
let mut good_first = true;
let (mut clock, _lines) = clock_with(move || {
if good_first {
good_first = false;
UNIX_EPOCH + Duration::from_millis(GOOD_MS)
} else {
UNIX_EPOCH - Duration::from_secs(3_600)
}
});
assert_eq!(clock.now_ms(), GOOD_MS, "the good read is remembered");
std::thread::sleep(Duration::from_millis(20));
let stamp = clock.now_ms();
assert!(
stamp >= GOOD_MS + 20,
"a pre-epoch read must carry the last good stamp forward by the time since it, got {stamp}"
);
assert!(
clock.now_ms() >= stamp,
"and the next failed read must not fall back to the last good one"
);
}
#[test]
fn a_pre_epoch_read_with_no_good_one_returns_zero_and_says_so() {
let (mut clock, lines) = clock_with(|| UNIX_EPOCH - Duration::from_secs(3_600));
assert_eq!(clock.now_ms(), 0);
assert_eq!(
lines.lock().unwrap().clone(),
[
"the system clock reads before the Unix epoch and no earlier reading is remembered; barks are stamped 0 until it recovers"
],
"a stamp of 0 must be reported as the clock's doing"
);
}
#[test]
fn a_run_of_failed_reads_prints_one_line_and_recovery_prints_one_more() {
let mut reads = 0;
let (mut clock, lines) = clock_with(move || {
reads += 1;
if reads <= 3 {
UNIX_EPOCH - Duration::from_secs(3_600)
} else {
UNIX_EPOCH + Duration::from_millis(GOOD_MS)
}
});
let stamps: Vec<u64> = (0..4).map(|_| clock.now_ms()).collect();
assert_eq!(stamps, [0, 0, 0, GOOD_MS]);
assert_eq!(
lines.lock().unwrap().clone(),
[
"the system clock reads before the Unix epoch and no earlier reading is remembered; barks are stamped 0 until it recovers",
"the system clock reads normally again; barks are stamped from it",
],
"one line for the episode, one for its end"
);
}
#[test]
fn a_second_failure_after_recovery_prints_again() {
let mut reads = 0;
let (mut clock, lines) = clock_with(move || {
reads += 1;
if reads % 2 == 1 {
UNIX_EPOCH + Duration::from_millis(GOOD_MS)
} else {
UNIX_EPOCH - Duration::from_secs(3_600)
}
});
for read in 0..4 {
assert!(
clock.now_ms() >= GOOD_MS,
"read {read} must carry a stamp, whether the clock answered it or the fallback did"
);
}
assert_eq!(
lines.lock().unwrap().clone(),
[
"the system clock reads before the Unix epoch; barks are stamped from the last good reading until it recovers",
"the system clock reads normally again; barks are stamped from it",
"the system clock reads before the Unix epoch; barks are stamped from the last good reading until it recovers",
],
"a fault that comes back is reported again"
);
}
#[test]
fn the_clock_debug_names_its_state_and_not_its_closures() {
let (clock, _lines) = clock_with(|| UNIX_EPOCH + Duration::from_millis(GOOD_MS));
assert_eq!(
format!("{clock:?}"),
"Clock { last_good_ms: None, failure_reported: false }"
);
}
}