use std::time::Duration;
use chrono::{DateTime, Utc};
use super::due::due_after;
use crate::app::Wanted;
pub(crate) type Arming = Box<dyn Fn(&crate::config::Config) -> Vec<Armed>>;
pub(crate) struct Armed {
project: String,
every: Option<Duration>,
at: Option<DateTime<Utc>>,
}
impl Armed {
pub(crate) fn polling(project: String, every: Option<Duration>) -> Self {
Self {
project,
every,
at: None,
}
}
pub(super) fn project(&self) -> &str {
&self.project
}
pub(super) fn still_due(self, named: Armed) -> Armed {
Armed {
at: named.every.and(self.at),
..named
}
}
pub(super) fn asks(&mut self, now: DateTime<Utc>) -> Option<Wanted> {
if !self.at.is_some_and(|at| at <= now) {
return None;
}
self.at = None;
Some(Wanted::Project(self.project.clone()))
}
pub(super) fn asks_in(&self, now: DateTime<Utc>) -> Option<Duration> {
self.at
.map(|at| (at - now).to_std().unwrap_or(Duration::ZERO))
}
pub(super) fn came_back(&mut self, wanted: &Wanted, at: DateTime<Utc>) {
if wanted.names(&self.project) {
self.at = self.every.and_then(|every| due_after(at, every));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tui::fixtures::{arkham, ferry};
const EVERY: Duration = Duration::from_secs(30);
fn at(seconds: i64) -> DateTime<Utc> {
DateTime::from_timestamp(seconds, 0).expect("an instant inside the epoch")
}
fn polling() -> Armed {
Armed::polling("arkham".to_string(), Some(EVERY))
}
fn seconds_to_the_end_of_time(from: DateTime<Utc>) -> u64 {
(DateTime::<Utc>::MAX_UTC - from)
.to_std()
.expect("the end of time is after the epoch")
.as_secs()
}
#[test]
fn a_project_whose_interval_reaches_the_end_of_time_still_asks() {
let every = Duration::from_secs(seconds_to_the_end_of_time(at(100)));
let mut armed = Armed::polling("arkham".to_string(), Some(every));
armed.came_back(&arkham(), at(100));
assert_eq!(armed.asks_in(at(100)), Some(every));
}
#[test]
fn a_project_whose_interval_outruns_time_asks_no_more() {
let every = Duration::from_secs(seconds_to_the_end_of_time(at(100)) + 1);
let mut armed = Armed::polling("arkham".to_string(), Some(every));
armed.came_back(&arkham(), at(100));
assert_eq!(armed.asks_in(at(100)), None);
assert_eq!(armed.asks(at(1_000_000)), None);
}
#[test]
fn a_project_whose_interval_fills_the_key_asks_no_more() {
let mut armed = Armed::polling("arkham".to_string(), Some(Duration::from_secs(u64::MAX)));
armed.came_back(&arkham(), at(100));
assert_eq!(armed.asks_in(at(100)), None);
assert_eq!(armed.asks(at(1_000_000)), None);
}
#[test]
fn a_project_asks_again_one_interval_after_the_read_that_answered_it() {
let mut armed = polling();
armed.came_back(&arkham(), at(100));
assert_eq!(armed.asks(at(129)), None, "the interval was not out");
assert_eq!(armed.asks(at(130)), Some(arkham()));
}
#[test]
fn a_project_whose_first_read_is_still_coming_asks_for_nothing() {
let mut armed = polling();
assert_eq!(armed.asks(at(1_000_000)), None);
assert_eq!(armed.asks_in(at(0)), None);
}
#[test]
fn a_project_that_does_not_poll_never_asks_for_itself() {
let mut armed = Armed::polling("arkham".to_string(), None);
armed.came_back(&arkham(), at(100));
assert_eq!(armed.asks_in(at(100)), None);
assert_eq!(armed.asks(at(1_000_000)), None);
}
#[test]
fn asking_disarms_until_another_read_comes_back() {
let mut armed = polling();
armed.came_back(&arkham(), at(100));
assert_eq!(armed.asks(at(130)), Some(arkham()));
assert_eq!(armed.asks(at(200)), None, "nothing has answered the ask");
assert_eq!(armed.asks_in(at(200)), None);
armed.came_back(&arkham(), at(210));
assert_eq!(armed.asks(at(240)), Some(arkham()));
}
#[test]
fn a_project_whose_read_never_comes_back_asks_no_more() {
let mut armed = polling();
armed.came_back(&arkham(), at(100));
armed.asks(at(130));
assert_eq!(armed.asks(at(1_000_000)), None);
}
#[test]
fn a_project_something_keeps_reporting_for_is_not_also_polled() {
let mut armed = polling();
for reported_at in [100, 120, 140, 160] {
armed.came_back(&arkham(), at(reported_at));
assert_eq!(
armed.asks(at(reported_at + 20)),
None,
"reported again at {}, so the poll is still out at {}",
reported_at + 20,
reported_at + 20
);
}
assert_eq!(
armed.asks(at(190)),
Some(arkham()),
"the reports stopped at 160, so the poll comes due one interval after"
);
}
#[test]
fn a_project_with_a_producer_is_left_out_of_the_poll_its_neighbour_needs() {
let mut has_one = Armed::polling("arkham".to_string(), None);
let mut has_none = Armed::polling("ferry".to_string(), Some(EVERY));
has_one.came_back(&Wanted::Everything, at(100));
has_none.came_back(&Wanted::Everything, at(100));
assert_eq!(has_one.asks(at(130)), None);
assert_eq!(has_none.asks(at(130)), Some(ferry()));
}
#[test]
fn a_project_the_channel_stops_covering_is_polled_again() {
let mut armed = polling();
armed.came_back(&arkham(), at(100));
assert_eq!(armed.asks(at(130)), Some(arkham()));
}
#[test]
fn a_read_of_every_project_arms_this_one() {
let mut armed = polling();
armed.came_back(&Wanted::Everything, at(100));
assert_eq!(armed.asks(at(130)), Some(arkham()));
}
#[test]
fn a_read_of_another_project_leaves_this_one_as_it_was() {
let mut armed = polling();
armed.came_back(&arkham(), at(100));
armed.came_back(&ferry(), at(120));
assert_eq!(
armed.asks(at(130)),
Some(arkham()),
"arkham asks 30 after its own read, not 30 after ferry's"
);
}
#[test]
fn projects_read_at_different_instants_stay_apart() {
let mut first = Armed::polling("arkham".to_string(), Some(EVERY));
let mut second = Armed::polling("ferry".to_string(), Some(EVERY));
first.came_back(&arkham(), at(100));
second.came_back(&ferry(), at(112));
assert_eq!(first.asks_in(at(100)), Some(EVERY));
assert_eq!(second.asks_in(at(100)), Some(Duration::from_secs(42)));
assert_eq!(first.asks(at(130)), Some(arkham()));
assert_eq!(second.asks(at(130)), None, "ferry's own read was later");
}
#[test]
fn an_ask_already_overdue_says_it_waits_no_longer() {
let mut armed = polling();
armed.came_back(&arkham(), at(100));
assert_eq!(armed.asks_in(at(500)), Some(Duration::ZERO));
}
#[test]
fn a_project_answers_to_its_own_name() {
assert_eq!(polling().project(), "arkham");
}
}