use super::{RuleStatus, Snapshot};
#[derive(Debug, Clone, PartialEq)]
pub enum Event {
BreachStarted {
target: String,
cell: String,
message: String,
},
BreachCleared { target: String, cell: String },
WarnStarted {
target: String,
cell: String,
message: String,
},
WarnCleared { target: String, cell: String },
}
impl Event {
pub fn banner(&self) -> String {
match self {
Event::BreachStarted {
target, message, ..
}
| Event::WarnStarted {
target, message, ..
} => format!("{target}: {message}"),
Event::BreachCleared { target, cell } => format!("{target}: {cell} passes again"),
Event::WarnCleared { target, cell } => format!("{target}: {cell} no longer warns"),
}
}
}
pub fn diff(prev: &Snapshot, cur: &Snapshot) -> Vec<Event> {
let before = |target: &str, cell: &str| {
prev.targets
.iter()
.find(|t| t.target == target)
.and_then(|t| t.cells.iter().find(|c| c.label == cell))
.map(|c| &c.status)
};
let mut events = Vec::new();
for t in &cur.targets {
for c in &t.cells {
let old = before(&t.target, &c.label);
let was_fail = matches!(old, Some(RuleStatus::Fail(_)));
let was_warn = matches!(old, Some(RuleStatus::Warn(_)));
let target = t.target.clone();
let cell = c.label.clone();
match &c.status {
RuleStatus::Fail(message) if !was_fail => events.push(Event::BreachStarted {
target,
cell,
message: message.clone(),
}),
RuleStatus::Warn(message) if !was_warn => events.push(Event::WarnStarted {
target,
cell,
message: message.clone(),
}),
RuleStatus::Fail(_) | RuleStatus::Warn(_) => {}
RuleStatus::Error(_) => {}
RuleStatus::Pass | RuleStatus::NotApplicable(_) => {
if was_fail {
events.push(Event::BreachCleared { target, cell });
} else if was_warn {
events.push(Event::WarnCleared { target, cell });
}
}
}
if was_warn && matches!(c.status, RuleStatus::Fail(_)) {
events.push(Event::WarnCleared {
target: t.target.clone(),
cell: c.label.clone(),
});
}
}
}
events
}
#[cfg(test)]
mod tests {
use super::*;
use crate::watch::{Cell, TargetSnapshot};
fn snap(status: RuleStatus) -> Snapshot {
Snapshot {
targets: vec![TargetSnapshot::new(
"USDC@binance",
vec![Cell {
label: "Spread".into(),
rule: None,
value: None,
status,
detail: None,
}],
)],
}
}
fn fail() -> RuleStatus {
RuleStatus::Fail("spread 3.4".into())
}
#[test]
fn first_tick_compares_with_empty() {
let e = diff(&Snapshot::default(), &snap(fail()));
assert_eq!(
e,
vec![Event::BreachStarted {
target: "USDC@binance".into(),
cell: "Spread".into(),
message: "spread 3.4".into()
}]
);
assert_eq!(e[0].banner(), "USDC@binance: spread 3.4");
assert!(diff(&Snapshot::default(), &snap(RuleStatus::Pass)).is_empty());
}
#[test]
fn no_event_when_nothing_changes() {
assert!(diff(&snap(fail()), &snap(fail())).is_empty());
assert!(diff(&snap(RuleStatus::Pass), &snap(RuleStatus::Pass)).is_empty());
}
#[test]
fn breach_clears_on_pass_and_on_na_but_not_on_error() {
assert!(matches!(
diff(&snap(fail()), &snap(RuleStatus::Pass))[..],
[Event::BreachCleared { .. }]
));
assert!(matches!(
diff(&snap(fail()), &snap(RuleStatus::NotApplicable("n".into())))[..],
[Event::BreachCleared { .. }]
));
assert!(diff(&snap(fail()), &snap(RuleStatus::Error("e".into()))).is_empty());
}
#[test]
fn warnings_start_clear_and_turn_into_breaches() {
let warn = RuleStatus::Warn("ratio".into());
assert!(matches!(
diff(&snap(RuleStatus::Pass), &snap(warn.clone()))[..],
[Event::WarnStarted { .. }]
));
assert!(matches!(
diff(&snap(warn.clone()), &snap(RuleStatus::Pass))[..],
[Event::WarnCleared { .. }]
));
assert!(matches!(
diff(&snap(warn), &snap(fail()))[..],
[Event::BreachStarted { .. }, Event::WarnCleared { .. }]
));
}
}