use crate::model_pick::grammar::entry_field;
use std::time::Duration;
pub const CADENCE_YAML: &str = "cadence.yaml";
pub const BLOCK: &str = "cadence";
pub const WATCHER: &str = "watcher";
pub const DEBOUNCE_MS: &str = "debounce_ms";
pub const CHEAP_SWEEP_MS: &str = "cheap_sweep_ms";
pub const FULL_SWEEP_MS: &str = "full_sweep_ms";
pub const DEBOUNCE_BOUNDS: (u64, u64) = (0, 10_000);
pub const CHEAP_SWEEP_BOUNDS: (u64, u64) = (100, 600_000);
pub const FULL_SWEEP_BOUNDS: (u64, u64) = (1_000, 3_600_000);
pub const TEMPLATE: &str = "\
# yog's clock: watcher-cycle periods, in milliseconds.
# Delete this file to restore the defaults below.
cadence:
watcher:
debounce_ms: 100
cheap_sweep_ms: 2000
full_sweep_ms: 15000
";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Cadence {
pub debounce: Duration,
pub cheap_sweep: Duration,
pub full_sweep: Duration,
}
impl Default for Cadence {
fn default() -> Self {
Self {
debounce: super::dirty::DEBOUNCE,
cheap_sweep: super::dirty::CHEAP_SWEEP,
full_sweep: super::dirty::FULL_SWEEP,
}
}
}
impl Cadence {
pub fn wound_grace(&self) -> Duration {
self.cheap_sweep
.saturating_add(self.debounce)
.saturating_add(self.late_pass(super::dirty::Sweep::Full))
.saturating_add(crate::wire::ASK_PERIOD)
}
pub fn late_pass(&self, sweep: super::dirty::Sweep) -> Duration {
match sweep {
super::dirty::Sweep::Full => self.full_sweep,
super::dirty::Sweep::Cheap | super::dirty::Sweep::None => self.cheap_sweep,
}
}
pub fn stale_after(&self) -> Duration {
self.full_sweep.saturating_mul(2)
}
}
pub fn parse(text: &str) -> Cadence {
let defaults = Cadence::default();
Cadence {
debounce: field_ms(text, DEBOUNCE_MS, DEBOUNCE_BOUNDS, defaults.debounce),
cheap_sweep: field_ms(
text,
CHEAP_SWEEP_MS,
CHEAP_SWEEP_BOUNDS,
defaults.cheap_sweep,
),
full_sweep: field_ms(text, FULL_SWEEP_MS, FULL_SWEEP_BOUNDS, defaults.full_sweep),
}
}
fn field_ms(text: &str, name: &str, (min, max): (u64, u64), default: Duration) -> Duration {
match entry_field(text, BLOCK, WATCHER, name).map(|v| v.parse::<u64>()) {
Some(Ok(ms)) => Duration::from_millis(ms.clamp(min, max)),
_ => default,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_absent_or_broken_file_is_the_default() {
assert_eq!(parse(""), Cadence::default(), "no file is the default");
assert_eq!(
parse("cadence:\n watcher:\n debounce_ms: nonsense\n"),
Cadence::default(),
"an unparseable field falls to its default, not to an error"
);
}
#[test]
fn the_template_spells_the_default() {
assert_eq!(parse(TEMPLATE), Cadence::default());
}
#[test]
fn a_set_field_lands_and_an_out_of_range_one_clamps() {
let text = "cadence:\n watcher:\n cheap_sweep_ms: 5000\n full_sweep_ms: 1\n";
let parsed = parse(text);
assert_eq!(parsed.cheap_sweep, Duration::from_secs(5));
assert_eq!(
parsed.full_sweep,
Duration::from_millis(FULL_SWEEP_BOUNDS.0),
"below the floor clamps to it — the §9.5 control's own rule"
);
assert_eq!(
parsed.debounce,
Cadence::default().debounce,
"absent field keeps its default"
);
}
#[test]
fn derived_periods_follow_the_bases() {
let c = Cadence {
debounce: Duration::from_millis(200),
cheap_sweep: Duration::from_secs(4),
full_sweep: Duration::from_secs(30),
};
assert_eq!(c.wound_grace(), Duration::from_millis(34_700));
assert_eq!(
c.late_pass(super::super::dirty::Sweep::Cheap),
Duration::from_secs(4)
);
assert_eq!(
c.late_pass(super::super::dirty::Sweep::Full),
Duration::from_secs(30)
);
assert_eq!(c.stale_after(), Duration::from_mins(1));
let d = Cadence::default();
assert_eq!(d.wound_grace(), Duration::from_millis(17_600));
assert_eq!(d.stale_after(), Duration::from_secs(30));
}
}