fuchsia_triage/
validate.rs

1// Copyright 2019 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use 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
27// Outer String is namespace, inner String is trial name
28pub 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
114// Returns true iff the trial did NOT get the expected result.
115fn 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    // Correct operation of the klog, syslog, and bootlog fields of TrialDataFetcher are tested
165    // in the integration test via log_tests.triage.
166
167    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        // Make sure it objects if a trial that should fire doesn't.
241        // Also, make sure it signals failure if there's both a good and a bad trial.
242        let bad_trial = Trial {
243            yes: Some(vec!["fires".to_string(), "no_fire".to_string()]),
244            no: None, // Test that None doesn't crash
245            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        // Make sure it objects if a trial fires when it shouldn't.
271        let bad_trial = Trial {
272            yes: Some(vec![]), // Test that empty vec works right
273            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}