use std::time::Duration;
#[derive(Debug, Default, thiserror::Error)]
#[error("the audit record was not stored before the exchange's time was up")]
#[non_exhaustive]
pub struct Late;
impl Late {
#[must_use]
pub const fn new() -> Self {
Self
}
}
#[derive(Debug)]
pub enum Recorded<E> {
Accepted,
Refused(E),
Late,
}
#[must_use]
pub fn deadline(timeout: Duration) -> Option<tokio::time::Instant> {
tokio::time::Instant::now().checked_add(timeout)
}
pub async fn within<E>(
deadline: Option<tokio::time::Instant>,
record: impl Future<Output = Result<(), E>>,
) -> Recorded<E> {
let outcome = match deadline {
Some(deadline) => match tokio::time::timeout_at(deadline, record).await {
Ok(outcome) => outcome,
Err(_elapsed) => return Recorded::Late,
},
None => record.await,
};
match outcome {
Ok(()) => Recorded::Accepted,
Err(error) => Recorded::Refused(error),
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::{Recorded, deadline, within};
#[tokio::test]
async fn a_record_accepted_in_time_is_accepted() {
let recorded = within(deadline(Duration::from_secs(5)), async { Ok::<(), ()>(()) }).await;
assert!(matches!(recorded, Recorded::Accepted), "{recorded:?}");
}
#[tokio::test]
async fn a_refusal_in_time_is_the_recorders_error() {
let recorded = within(deadline(Duration::from_secs(5)), async {
Err::<(), _>("full")
})
.await;
assert!(
matches!(recorded, Recorded::Refused("full")),
"{recorded:?}"
);
}
#[tokio::test]
async fn a_record_not_accepted_when_the_time_is_up_is_late() {
let recorded = within(deadline(Duration::from_millis(20)), async {
tokio::time::sleep(Duration::from_secs(5)).await;
Ok::<(), ()>(())
})
.await;
assert!(matches!(recorded, Recorded::Late), "{recorded:?}");
}
#[tokio::test]
async fn with_no_deadline_the_record_is_awaited() {
let recorded = within(None, async {
tokio::time::sleep(Duration::from_millis(20)).await;
Ok::<(), ()>(())
})
.await;
assert!(matches!(recorded, Recorded::Accepted), "{recorded:?}");
}
#[test]
fn a_late_record_is_made_by_the_caller_that_reports_it() {
assert_eq!(
super::Late::new().to_string(),
"the audit record was not stored before the exchange's time was up"
);
}
#[test]
fn a_timeout_past_the_clock_has_no_deadline() {
assert!(
deadline(Duration::MAX).is_none(),
"no deadline past the clock"
);
}
}