1use super::act::{Action, Actions};
6use super::config::ParseResult;
7use super::metrics::MetricState;
8use super::metrics::fetch::{Fetcher, KeyValueFetcher, TextFetcher, TrialDataFetcher};
9use super::metrics::metric_value::MetricValue;
10use anyhow::{Error, bail, format_err};
11use serde::Deserialize;
12use serde_json as json;
13use std::collections::HashMap;
14
15#[derive(Clone, Deserialize, Debug)]
16pub struct Trial {
17 yes: Option<Vec<String>>,
18 no: Option<Vec<String>>,
19 now: Option<String>,
20 values: Option<HashMap<String, json::Value>>,
21 klog: Option<String>,
22 syslog: Option<String>,
23 bootlog: Option<String>,
24 annotations: Option<json::map::Map<String, json::Value>>,
25}
26
27pub type Trials = HashMap<String, TrialsSchema>;
29pub type TrialsSchema = HashMap<String, Trial>;
30
31pub fn validate(parse_result: &ParseResult) -> Result<(), Error> {
32 let ParseResult { metrics, actions, tests } = parse_result;
33 let mut failed = false;
34 for (namespace, trial_map) in tests {
35 for (trial_name, trial) in trial_map {
36 if trial.yes.is_none() && trial.no.is_none() {
37 bail!("Trial {} in {} needs a yes: or no: entry", trial_name, namespace);
38 }
39 let klog_fetcher;
40 let syslog_fetcher;
41 let bootlog_fetcher;
42 let annotations_fetcher;
43 let mut fetcher = match &trial.values {
44 Some(values) => Fetcher::TrialData(TrialDataFetcher::new(values)),
45 None => Fetcher::TrialData(TrialDataFetcher::new_empty()),
46 };
47 if let Fetcher::TrialData(fetcher) = &mut fetcher {
48 if let Some(klog) = &trial.klog {
49 klog_fetcher = TextFetcher::from(&**klog);
50 fetcher.set_klog(&klog_fetcher);
51 }
52 if let Some(syslog) = &trial.syslog {
53 syslog_fetcher = TextFetcher::from(&**syslog);
54 fetcher.set_syslog(&syslog_fetcher);
55 }
56 if let Some(bootlog) = &trial.bootlog {
57 bootlog_fetcher = TextFetcher::from(&**bootlog);
58 fetcher.set_bootlog(&bootlog_fetcher);
59 }
60 if let Some(annotations) = &trial.annotations {
61 annotations_fetcher = KeyValueFetcher::try_from(annotations)?;
62 fetcher.set_annotations(&annotations_fetcher);
63 }
64 }
65 let now = if let Some(now) = &trial.now {
66 match MetricState::evaluate_math(now) {
67 MetricValue::Int(time) => Some(time),
68 oops => bail!(
69 "Trial {} in {}: 'now: {}' was not integer, it was {:?}",
70 trial_name,
71 namespace,
72 now,
73 oops
74 ),
75 }
76 } else {
77 None
78 };
79 if let Some(action_names) = &trial.yes {
80 for action_name in action_names.iter() {
81 let original_metrics = &(metrics.clone());
82 let original_actions = &(actions.clone());
83 let state = MetricState::new(original_metrics, fetcher.clone(), now);
84 failed = check_failure(
85 namespace,
86 trial_name,
87 action_name,
88 original_actions,
89 &state,
90 true,
91 ) || failed;
92 }
93 }
94 if let Some(action_names) = &trial.no {
95 for action_name in action_names.iter() {
96 let original_metrics = &(metrics.clone());
97 let original_actions = &(actions.clone());
98 let state = MetricState::new(original_metrics, fetcher.clone(), now);
99 failed = check_failure(
100 namespace,
101 trial_name,
102 action_name,
103 original_actions,
104 &state,
105 false,
106 ) || failed;
107 }
108 }
109 }
110 }
111 if failed { Err(format_err!("Config validation test failed")) } else { Ok(()) }
112}
113
114fn check_failure(
116 namespace: &String,
117 trial_name: &String,
118 action_name: &String,
119 actions: &Actions,
120 metric_state: &MetricState<'_>,
121 expected: bool,
122) -> bool {
123 match actions.get(namespace) {
124 None => {
125 println!("Namespace {action_name} not found in trial {trial_name}");
126 true
127 }
128 Some(action_map) => match action_map.get(action_name) {
129 None => {
130 println!("Action {action_name} not found in trial {trial_name}");
131 true
132 }
133 Some(action) => {
134 let trigger = match action {
135 Action::Alert(properties) => &properties.trigger,
136 Action::Snapshot(properties) => &properties.trigger,
137 _ => {
138 println!("Action {action:?} cannot be tested");
139 return true;
140 }
141 };
142 match metric_state.eval_action_metric(namespace, trigger) {
143 MetricValue::Bool(actual) if actual == expected => false,
144 other => {
145 println!(
146 "Test {trial_name} failed: trigger '{trigger}' of action \
147 {action_name} returned {other:?}, expected {expected}",
148 );
149 true
150 }
151 }
152 }
153 },
154 }
155}
156
157#[cfg(test)]
158mod test {
159 use super::*;
160 use crate::act::{Alert, Severity};
161 use crate::make_metrics;
162 use crate::metrics::ValueSource;
163
164 macro_rules! build_map {($($tuple:expr),*) => ({
168 let mut map = HashMap::new();
169 $(
170 let (key, value) = $tuple;
171 map.insert(key.to_string(), value);
172 )*
173 map
174 })}
175
176 macro_rules! create_parse_result {
177 (metrics: $metrics:expr, actions: $actions:expr, tests: $tests:expr) => {
178 ParseResult {
179 metrics: $metrics.clone(),
180 actions: $actions.clone(),
181 tests: $tests.clone(),
182 }
183 };
184 }
185
186 #[fuchsia::test]
187 fn validate_works() -> Result<(), Error> {
188 let metrics = make_metrics!({
189 "foo":{
190 eval: {
191 "true": "1 == 1",
192 "false": "1 == 0"
193 }
194 }
195 });
196
197 let actions = build_map!((
198 "foo",
199 build_map!(
200 (
201 "fires",
202 Action::Alert(Alert {
203 trigger: ValueSource::try_from_expression_with_namespace("true", "foo")?,
204 print: "good".to_string(),
205 tag: None,
206 file_bug: None,
207 severity: Severity::Warning,
208 })
209 ),
210 (
211 "no_fire",
212 Action::Alert(Alert {
213 trigger: ValueSource::try_from_expression_with_namespace("false", "foo")?,
214 print: "what?!?".to_string(),
215 tag: None,
216 file_bug: None,
217 severity: Severity::Warning,
218 })
219 )
220 )
221 ));
222 let good_trial = Trial {
223 yes: Some(vec!["fires".to_string()]),
224 no: Some(vec!["no_fire".to_string()]),
225 values: Some(HashMap::new()),
226 klog: None,
227 syslog: None,
228 bootlog: None,
229 annotations: None,
230 now: None,
231 };
232 assert!(
233 validate(&create_parse_result!(
234 metrics: metrics,
235 actions: actions,
236 tests: build_map!(("foo", build_map!(("good", good_trial))))
237 ))
238 .is_ok()
239 );
240 let bad_trial = Trial {
243 yes: Some(vec!["fires".to_string(), "no_fire".to_string()]),
244 no: None, values: None,
246 klog: None,
247 syslog: None,
248 bootlog: None,
249 annotations: None,
250 now: None,
251 };
252 let good_trial = Trial {
253 yes: Some(vec!["fires".to_string()]),
254 no: Some(vec!["no_fire".to_string()]),
255 values: Some(HashMap::new()),
256 klog: None,
257 syslog: None,
258 bootlog: None,
259 annotations: None,
260 now: None,
261 };
262 assert!(
263 validate(&create_parse_result!(
264 metrics: metrics,
265 actions: actions,
266 tests: build_map!(("foo", build_map!(("good", good_trial), ("bad", bad_trial))))
267 ))
268 .is_err()
269 );
270 let bad_trial = Trial {
272 yes: Some(vec![]), no: Some(vec!["fires".to_string(), "no_fire".to_string()]),
274 values: None,
275 klog: None,
276 syslog: None,
277 bootlog: None,
278 annotations: None,
279 now: None,
280 };
281 assert!(
282 validate(&create_parse_result!(
283 metrics: metrics,
284 actions: actions,
285 tests: build_map!(("foo", build_map!(("bad", bad_trial))))
286 ))
287 .is_err()
288 );
289 Ok(())
290 }
291
292 #[fuchsia::test]
293 fn validate_time() -> Result<(), Error> {
294 let metrics = HashMap::new();
295 let actions = build_map!((
296 "foo",
297 build_map!(
298 (
299 "time_quarter",
300 Action::Alert(Alert {
301 trigger: ValueSource::try_from_expression_with_namespace(
302 "Now()==250000000",
303 "foo"
304 )?,
305 print: "time_billion".to_string(),
306 tag: None,
307 file_bug: None,
308 severity: Severity::Warning,
309 })
310 ),
311 (
312 "time_missing",
313 Action::Alert(Alert {
314 trigger: ValueSource::try_from_expression_with_namespace(
315 "Missing(Now())",
316 "foo"
317 )?,
318 print: "time_missing".to_string(),
319 tag: None,
320 file_bug: None,
321 severity: Severity::Warning,
322 })
323 )
324 )
325 ));
326 let time_trial = Trial {
327 yes: Some(vec!["time_quarter".to_string()]),
328 no: Some(vec!["time_missing".to_string()]),
329 values: Some(HashMap::new()),
330 klog: None,
331 syslog: None,
332 bootlog: None,
333 annotations: None,
334 now: Some("Seconds(0.25)".to_string()),
335 };
336 assert!(
337 validate(&create_parse_result!(
338 metrics: metrics,
339 actions: actions,
340 tests: build_map!(("foo", build_map!(("good", time_trial))))
341 ))
342 .is_ok()
343 );
344 let missing_trial = Trial {
345 yes: Some(vec!["time_missing".to_string()]),
346 no: Some(vec![]),
347 values: Some(HashMap::new()),
348 klog: None,
349 syslog: None,
350 bootlog: None,
351 annotations: None,
352 now: None,
353 };
354 assert!(
355 validate(&create_parse_result!(
356 metrics: metrics,
357 actions: actions,
358 tests: build_map!(("foo", build_map!(("good", missing_trial))))
359 ))
360 .is_ok()
361 );
362 let bad_trial = Trial {
363 yes: Some(vec!["time_missing".to_string()]),
364 no: Some(vec![]),
365 values: Some(HashMap::new()),
366 klog: None,
367 syslog: None,
368 bootlog: None,
369 annotations: None,
370 now: Some("this won't parse".to_string()),
371 };
372 assert!(
373 validate(&create_parse_result!(
374 metrics: metrics,
375 actions: actions,
376 tests: build_map!(("foo", build_map!(("good", bad_trial))))
377 ))
378 .is_err()
379 );
380 Ok(())
381 }
382}