use std::time::Duration;
use serde::Serialize;
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct RegionWatch {
pub index: usize,
pub label: String,
pub monitor: usize,
pub score: f64,
pub matching: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Condition {
Match,
Change,
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum WaitError {
#[error("--interval 0 would poll as fast as capture allows — pass a nonzero interval")]
ZeroInterval,
#[error(
"--interval {interval:?} is longer than --timeout {timeout:?}, so nothing \
would be polled twice — shorten the interval or lengthen the timeout"
)]
IntervalExceedsTimeout {
interval: Duration,
timeout: Duration,
},
}
pub fn poll_budget(timeout: Duration, interval: Duration) -> Result<u32, WaitError> {
if interval.is_zero() {
return Err(WaitError::ZeroInterval);
}
if interval > timeout {
return Err(WaitError::IntervalExceedsTimeout { interval, timeout });
}
let spans = timeout.as_millis() / interval.as_millis();
Ok(u32::try_from(spans).unwrap_or(u32::MAX).saturating_add(1))
}
#[must_use]
pub fn satisfied(condition: Condition, scores: &[f64], min_score: f64) -> bool {
if scores.is_empty() {
return false;
}
match condition {
Condition::Match => scores.iter().all(|s| *s >= min_score),
Condition::Change => scores.iter().any(|s| *s < min_score),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_documented_budget_is_the_one_computed() {
assert_eq!(
poll_budget(Duration::from_secs(30), Duration::from_millis(500)).unwrap(),
61,
"one immediate poll, then sixty"
);
}
#[test]
fn an_interval_equal_to_the_timeout_still_polls_twice() {
assert_eq!(
poll_budget(Duration::from_secs(5), Duration::from_secs(5)).unwrap(),
2,
"once now, once at the deadline"
);
}
#[test]
fn a_ragged_division_keeps_the_polls_that_fit() {
assert_eq!(
poll_budget(Duration::from_secs(1), Duration::from_millis(300)).unwrap(),
4
);
}
#[test]
fn a_zero_interval_is_refused_rather_than_spinning() {
assert_eq!(
poll_budget(Duration::from_secs(1), Duration::ZERO).unwrap_err(),
WaitError::ZeroInterval
);
}
#[test]
fn an_interval_past_the_timeout_is_refused_as_a_mistake() {
let err = poll_budget(Duration::from_secs(1), Duration::from_secs(2)).unwrap_err();
assert_eq!(
err,
WaitError::IntervalExceedsTimeout {
interval: Duration::from_secs(2),
timeout: Duration::from_secs(1),
},
"a single poll makes the timeout meaningless — say so"
);
assert!(err.to_string().contains("polled twice"));
}
#[test]
fn match_needs_every_region_and_change_needs_one() {
let all_high = [0.99, 0.95];
let one_low = [0.99, 0.10];
assert!(satisfied(Condition::Match, &all_high, 0.9));
assert!(!satisfied(Condition::Match, &one_low, 0.9), "match is all");
assert!(satisfied(Condition::Change, &one_low, 0.9), "change is any");
assert!(!satisfied(Condition::Change, &all_high, 0.9));
}
#[test]
fn the_floor_is_inclusive_on_both_verbs() {
assert!(satisfied(Condition::Match, &[0.9], 0.9));
assert!(!satisfied(Condition::Change, &[0.9], 0.9));
}
#[test]
fn nothing_to_watch_satisfies_neither() {
assert!(!satisfied(Condition::Match, &[], 0.9));
assert!(
!satisfied(Condition::Change, &[], 0.9),
"a wait that verified nothing has not succeeded"
);
}
}