use std::time::{Duration, SystemTime, UNIX_EPOCH};
use crate::git::ProbeError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct Generation(u64);
impl Generation {
pub(crate) fn new(value: u64) -> Self {
Generation(value)
}
pub(crate) fn value(self) -> u64 {
self.0
}
#[cfg(test)]
pub(crate) fn successor(self) -> Self {
Generation(self.0 + 1)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Timestamp(SystemTime);
impl Timestamp {
pub fn now() -> Self {
Timestamp(SystemTime::now())
}
#[cfg(any(test, feature = "test-util"))]
pub fn at(instant: SystemTime) -> Self {
Timestamp(instant)
}
pub fn elapsed(&self) -> Duration {
SystemTime::now()
.duration_since(self.0)
.unwrap_or(Duration::ZERO)
}
}
impl std::fmt::Display for Timestamp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let secs = self
.0
.duration_since(UNIX_EPOCH)
.unwrap_or(Duration::ZERO)
.as_secs() as i64;
let days = secs.div_euclid(86_400);
let secs_of_day = secs.rem_euclid(86_400);
let (year, month, day) = civil_from_days(days);
let hour = secs_of_day / 3_600;
let minute = (secs_of_day % 3_600) / 60;
let second = secs_of_day % 60;
write!(
f,
"{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}Z"
)
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for Timestamp {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.collect_str(self)
}
}
fn civil_from_days(days: i64) -> (i64, u32, u32) {
let z = days + 719_468;
let era = (if z >= 0 { z } else { z - 146_096 }) / 146_097;
let day_of_era = z - era * 146_097; let year_of_era =
(day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365; let year = year_of_era + era * 400;
let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100); let month_prime = (5 * day_of_year + 2) / 153; let day = (day_of_year - (153 * month_prime + 2) / 5 + 1) as u32; let month = (if month_prime < 10 {
month_prime + 3
} else {
month_prime - 9
}) as u32; let year = if month <= 2 { year + 1 } else { year };
(year, month, day)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub enum Unknown {
TimedOut,
NoDefaultBranch,
SubmoduleUninitialized,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub enum Settled<T> {
Unknown(Unknown),
Known {
value: T,
at: Timestamp,
stale: bool,
},
Failed(ProbeError),
NotApplicable,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct Cell<T> {
settled: Option<Settled<T>>,
in_flight: bool,
#[cfg_attr(feature = "serde", serde(skip))]
#[allow(dead_code)] generation: Generation,
}
impl<T> Default for Cell<T> {
fn default() -> Self {
Cell {
settled: None,
in_flight: false,
generation: Generation::default(),
}
}
}
impl<T> Cell<T> {
#[cfg(any(test, feature = "test-util"))]
pub fn already_settled(settled: Settled<T>) -> Self {
Cell {
settled: Some(settled),
in_flight: false,
generation: Generation::default(),
}
}
#[cfg(any(test, feature = "test-util"))]
pub fn already_settled_and_in_flight(settled: Settled<T>) -> Self {
Cell {
settled: Some(settled),
in_flight: true,
generation: Generation::default(),
}
}
pub fn settled(&self) -> Option<&Settled<T>> {
self.settled.as_ref()
}
pub fn is_in_flight(&self) -> bool {
self.in_flight
}
pub(crate) fn begin_probe(&mut self) {
self.in_flight = true;
}
pub(crate) fn settle(&mut self, generation: Generation, settled: Settled<T>) -> bool {
if generation < self.generation {
return false;
}
self.generation = generation;
self.settled = Some(settled);
self.in_flight = false;
true
}
pub(crate) fn force_stale(&mut self) {
if let Some(Settled::Known {
stale,
value: _,
at: _,
}) = &mut self.settled
{
*stale = true;
}
}
pub(crate) fn age_into_stale(&mut self, threshold: Duration) {
if let Some(Settled::Known {
at,
stale,
value: _,
}) = &mut self.settled
&& at.elapsed() >= threshold
{
*stale = true;
}
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
#[test]
fn unknown_reasons_match_this_documents_own_table() {
let spec_path =
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../docs/spec/core-api.md");
let spec = std::fs::read_to_string(&spec_path)
.unwrap_or_else(|error| panic!("read {}: {error}", spec_path.display()));
let declaration = spec
.lines()
.find(|line| line.starts_with("pub enum Unknown {"))
.unwrap_or_else(|| panic!("no `pub enum Unknown` line in {}", spec_path.display()));
let documented: Vec<&str> = declaration
.trim_start_matches("pub enum Unknown {")
.trim_end_matches('}')
.split(',')
.map(str::trim)
.filter(|name| !name.is_empty())
.collect();
let in_code: Vec<&str> = [
Unknown::TimedOut,
Unknown::NoDefaultBranch,
Unknown::SubmoduleUninitialized,
]
.iter()
.map(|reason| match reason {
Unknown::TimedOut => "TimedOut",
Unknown::NoDefaultBranch => "NoDefaultBranch",
Unknown::SubmoduleUninitialized => "SubmoduleUninitialized",
})
.collect();
assert_eq!(
in_code, documented,
"`Unknown`'s variants and core-api.md's own enum line disagree; amend the \
document's table and its closed-set sentence in the same change as the enum"
);
for reason in &in_code {
assert!(
spec.contains(&format!("| `{reason}` |")),
"core-api.md's reason table has no row for `{reason}`"
);
}
}
#[test]
fn re_probing_keeps_the_previous_value_instead_of_blanking() {
let mut cell: Cell<u32> = Cell::default();
cell.settle(
Generation::new(1),
Settled::Known {
value: 7,
at: Timestamp::now(),
stale: false,
},
);
cell.begin_probe();
match cell.settled() {
Some(Settled::Known {
value,
at: _,
stale: _,
}) => assert_eq!(*value, 7),
other => {
panic!("expected the previous Known value to survive a re-probe, got {other:?}")
}
}
}
#[test]
fn absent_before_any_probe_is_distinct_from_absent_while_loading() {
let never_probed: Cell<u32> = Cell::default();
assert!(never_probed.settled().is_none());
assert!(!never_probed.in_flight);
let mut loading: Cell<u32> = Cell::default();
loading.begin_probe();
assert!(loading.settled().is_none());
assert!(loading.in_flight);
}
#[test]
fn a_lower_generation_write_does_not_overwrite_a_higher_one() {
let mut cell: Cell<u32> = Cell::default();
cell.settle(
Generation::new(2),
Settled::Known {
value: 9,
at: Timestamp::now(),
stale: false,
},
);
cell.settle(
Generation::new(1),
Settled::Known {
value: 1,
at: Timestamp::now(),
stale: false,
},
);
match cell.settled() {
Some(Settled::Known {
value,
at: _,
stale: _,
}) => assert_eq!(*value, 9),
other => panic!("expected the higher Generation's value to survive, got {other:?}"),
}
}
#[test]
fn every_settled_shape_round_trips_through_settle_and_settled() {
let mut unknown_cell: Cell<u32> = Cell::default();
unknown_cell.settle(Generation::new(1), Settled::Unknown(Unknown::TimedOut));
assert!(matches!(
unknown_cell.settled(),
Some(Settled::Unknown(Unknown::TimedOut))
));
let mut failed_cell: Cell<u32> = Cell::default();
failed_cell.settle(
Generation::new(1),
Settled::Failed(ProbeError::Open(Arc::from("boom"))),
);
assert!(matches!(
failed_cell.settled(),
Some(Settled::Failed(ProbeError::Open(_)))
));
let mut not_applicable_cell: Cell<u32> = Cell::default();
not_applicable_cell.settle(Generation::new(1), Settled::NotApplicable);
assert!(matches!(
not_applicable_cell.settled(),
Some(Settled::NotApplicable)
));
}
#[test]
fn force_stale_marks_a_known_value_stale_without_changing_it() {
let mut cell: Cell<u32> = Cell::default();
cell.settle(
Generation::new(1),
Settled::Known {
value: 42,
at: Timestamp::now(),
stale: false,
},
);
cell.force_stale();
match cell.settled() {
Some(Settled::Known {
value,
stale,
at: _,
}) => {
assert_eq!(*value, 42, "the value must survive being forced stale");
assert!(*stale, "the cell must be marked stale");
}
other => panic!("expected the Known value to survive, got {other:?}"),
}
}
#[test]
fn age_into_stale_marks_a_known_value_stale_once_it_is_old_enough() {
let mut cell: Cell<u32> = Cell::default();
cell.settle(
Generation::new(1),
Settled::Known {
value: 42,
at: Timestamp::at(SystemTime::now() - Duration::from_secs(3600)),
stale: false,
},
);
cell.age_into_stale(Duration::from_secs(300));
match cell.settled() {
Some(Settled::Known {
value,
stale,
at: _,
}) => {
assert_eq!(*value, 42, "the value must survive ageing into stale");
assert!(
*stale,
"an hour-old value past a five-minute threshold must go stale"
);
}
other => panic!("expected the Known value to survive, got {other:?}"),
}
}
#[test]
fn age_into_stale_leaves_a_known_value_fresh_before_the_threshold() {
let mut cell: Cell<u32> = Cell::default();
cell.settle(
Generation::new(1),
Settled::Known {
value: 7,
at: Timestamp::now(),
stale: false,
},
);
cell.age_into_stale(Duration::from_secs(300));
match cell.settled() {
Some(Settled::Known {
stale,
value: _,
at: _,
}) => {
assert!(
!*stale,
"a value settled moments ago must not age into stale yet"
)
}
other => panic!("expected a fresh Known value, got {other:?}"),
}
}
#[test]
fn age_into_stale_on_a_cell_with_no_known_value_is_a_no_op() {
let mut unknown_cell: Cell<u32> = Cell::default();
unknown_cell.settle(Generation::new(1), Settled::Unknown(Unknown::TimedOut));
unknown_cell.age_into_stale(Duration::ZERO);
assert!(matches!(
unknown_cell.settled(),
Some(Settled::Unknown(Unknown::TimedOut))
));
let mut never_probed: Cell<u32> = Cell::default();
never_probed.age_into_stale(Duration::ZERO);
assert!(never_probed.settled().is_none());
}
#[test]
fn force_stale_on_a_cell_with_no_known_value_is_a_no_op() {
let mut unknown_cell: Cell<u32> = Cell::default();
unknown_cell.settle(Generation::new(1), Settled::Unknown(Unknown::TimedOut));
unknown_cell.force_stale();
assert!(matches!(
unknown_cell.settled(),
Some(Settled::Unknown(Unknown::TimedOut))
));
let mut never_probed: Cell<u32> = Cell::default();
never_probed.force_stale();
assert!(never_probed.settled().is_none());
}
#[test]
fn a_settled_cell_clones() {
let mut cell: Cell<u32> = Cell::default();
cell.settle(
Generation::new(1),
Settled::Known {
value: 3,
at: Timestamp::now(),
stale: false,
},
);
let cloned = cell.clone();
match cloned.settled() {
Some(Settled::Known {
value,
at: _,
stale: _,
}) => assert_eq!(*value, 3),
other => panic!("expected the clone to carry the same Known value, got {other:?}"),
}
}
#[test]
fn elapsed_reads_zero_for_a_timestamp_in_the_future_rather_than_a_negative_duration() {
let future = Timestamp::at(SystemTime::now() + Duration::from_secs(3600));
assert_eq!(future.elapsed(), Duration::ZERO);
}
#[test]
fn elapsed_reads_a_positive_duration_for_a_timestamp_in_the_past() {
let past = Timestamp::at(SystemTime::now() - Duration::from_secs(90));
assert!(past.elapsed() >= Duration::from_secs(90));
}
#[test]
fn timestamp_formats_as_rfc3339() {
let cases: [(u64, &str); 6] = [
(0, "1970-01-01T00:00:00Z"),
(1, "1970-01-01T00:00:01Z"),
(86_399, "1970-01-01T23:59:59Z"),
(86_400, "1970-01-02T00:00:00Z"),
(951_782_400, "2000-02-29T00:00:00Z"),
(1_700_000_000, "2023-11-14T22:13:20Z"),
];
for (epoch_secs, expected) in cases {
let timestamp = Timestamp(UNIX_EPOCH + Duration::from_secs(epoch_secs));
assert_eq!(timestamp.to_string(), expected);
}
}
}