1use super::{RuleStatus, Snapshot};
4
5#[derive(Debug, Clone, PartialEq)]
7pub enum Event {
8 BreachStarted {
10 target: String,
11 cell: String,
12 message: String,
13 },
14 BreachCleared { target: String, cell: String },
16 WarnStarted {
18 target: String,
19 cell: String,
20 message: String,
21 },
22 WarnCleared { target: String, cell: String },
24}
25
26impl Event {
27 pub fn banner(&self) -> String {
29 match self {
30 Event::BreachStarted {
31 target, message, ..
32 }
33 | Event::WarnStarted {
34 target, message, ..
35 } => format!("{target}: {message}"),
36 Event::BreachCleared { target, cell } => format!("{target}: {cell} passes again"),
37 Event::WarnCleared { target, cell } => format!("{target}: {cell} no longer warns"),
38 }
39 }
40}
41
42pub fn diff(prev: &Snapshot, cur: &Snapshot) -> Vec<Event> {
46 let before = |target: &str, cell: &str| {
47 prev.targets
48 .iter()
49 .find(|t| t.target == target)
50 .and_then(|t| t.cells.iter().find(|c| c.label == cell))
51 .map(|c| &c.status)
52 };
53 let mut events = Vec::new();
54 for t in &cur.targets {
55 for c in &t.cells {
56 let old = before(&t.target, &c.label);
57 let was_fail = matches!(old, Some(RuleStatus::Fail(_)));
58 let was_warn = matches!(old, Some(RuleStatus::Warn(_)));
59 let target = t.target.clone();
60 let cell = c.label.clone();
61 match &c.status {
62 RuleStatus::Fail(message) if !was_fail => events.push(Event::BreachStarted {
63 target,
64 cell,
65 message: message.clone(),
66 }),
67 RuleStatus::Warn(message) if !was_warn => events.push(Event::WarnStarted {
68 target,
69 cell,
70 message: message.clone(),
71 }),
72 RuleStatus::Fail(_) | RuleStatus::Warn(_) => {}
73 RuleStatus::Error(_) => {}
76 RuleStatus::Pass | RuleStatus::NotApplicable(_) => {
77 if was_fail {
78 events.push(Event::BreachCleared { target, cell });
79 } else if was_warn {
80 events.push(Event::WarnCleared { target, cell });
81 }
82 }
83 }
84 if was_warn && matches!(c.status, RuleStatus::Fail(_)) {
86 events.push(Event::WarnCleared {
87 target: t.target.clone(),
88 cell: c.label.clone(),
89 });
90 }
91 }
92 }
93 events
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99 use crate::watch::{Cell, TargetSnapshot};
100
101 fn snap(status: RuleStatus) -> Snapshot {
102 Snapshot {
103 targets: vec![TargetSnapshot::new(
104 "USDC@binance",
105 vec![Cell {
106 label: "Spread".into(),
107 rule: None,
108 value: None,
109 status,
110 detail: None,
111 }],
112 )],
113 }
114 }
115
116 fn fail() -> RuleStatus {
117 RuleStatus::Fail("spread 3.4".into())
118 }
119
120 #[test]
121 fn first_tick_compares_with_empty() {
122 let e = diff(&Snapshot::default(), &snap(fail()));
123 assert_eq!(
124 e,
125 vec![Event::BreachStarted {
126 target: "USDC@binance".into(),
127 cell: "Spread".into(),
128 message: "spread 3.4".into()
129 }]
130 );
131 assert_eq!(e[0].banner(), "USDC@binance: spread 3.4");
132 assert!(diff(&Snapshot::default(), &snap(RuleStatus::Pass)).is_empty());
133 }
134
135 #[test]
136 fn no_event_when_nothing_changes() {
137 assert!(diff(&snap(fail()), &snap(fail())).is_empty());
138 assert!(diff(&snap(RuleStatus::Pass), &snap(RuleStatus::Pass)).is_empty());
139 }
140
141 #[test]
142 fn breach_clears_on_pass_and_on_na_but_not_on_error() {
143 assert!(matches!(
144 diff(&snap(fail()), &snap(RuleStatus::Pass))[..],
145 [Event::BreachCleared { .. }]
146 ));
147 assert!(matches!(
148 diff(&snap(fail()), &snap(RuleStatus::NotApplicable("n".into())))[..],
149 [Event::BreachCleared { .. }]
150 ));
151 assert!(diff(&snap(fail()), &snap(RuleStatus::Error("e".into()))).is_empty());
152 }
153
154 #[test]
155 fn warnings_start_clear_and_turn_into_breaches() {
156 let warn = RuleStatus::Warn("ratio".into());
157 assert!(matches!(
158 diff(&snap(RuleStatus::Pass), &snap(warn.clone()))[..],
159 [Event::WarnStarted { .. }]
160 ));
161 assert!(matches!(
162 diff(&snap(warn.clone()), &snap(RuleStatus::Pass))[..],
163 [Event::WarnCleared { .. }]
164 ));
165 assert!(matches!(
166 diff(&snap(warn), &snap(fail()))[..],
167 [Event::BreachStarted { .. }, Event::WarnCleared { .. }]
168 ));
169 }
170}