fuchsia_triage/
act.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 crate::config::ActionConfig;
6
7use super::config::DiagnosticData;
8use super::metrics::fetch::{Fetcher, FileDataFetcher};
9use super::metrics::metric_value::{MetricValue, Problem};
10use super::metrics::{
11    ExpressionContext, ExpressionTree, Function, Metric, MetricState, Metrics, ValueSource,
12};
13use super::plugins::{register_plugins, Plugin};
14use crate::{inspect_logger, metric_value_to_int};
15use anyhow::{bail, Error};
16use fidl_fuchsia_feedback::MAX_CRASH_SIGNATURE_LENGTH;
17use serde::{Deserialize, Serialize};
18use std::cell::RefCell;
19use std::collections::HashMap;
20
21/// Provides the [metric_state] context to evaluate [Action]s and results of the [actions].
22pub struct ActionContext<'a> {
23    actions: &'a Actions,
24    metric_state: MetricState<'a>,
25    action_results: ActionResults,
26    plugins: Vec<Box<dyn Plugin>>,
27}
28
29impl<'a> ActionContext<'a> {
30    pub(crate) fn new(
31        metrics: &'a Metrics,
32        actions: &'a Actions,
33        diagnostic_data: &'a [DiagnosticData],
34        now: Option<i64>,
35    ) -> ActionContext<'a> {
36        let fetcher = FileDataFetcher::new(diagnostic_data);
37        let mut action_results = ActionResults::new();
38        fetcher.errors().iter().for_each(|e| {
39            action_results.errors.push(format!("[DEBUG: BAD DATA] {}", e));
40        });
41        ActionContext {
42            actions,
43            metric_state: MetricState::new(metrics, Fetcher::FileData(fetcher), now),
44            action_results,
45            plugins: register_plugins(),
46        }
47    }
48}
49
50/// Stores the results of each [Action] specified in [source] and
51/// the [warnings] and [gauges] that are generated.
52#[derive(Clone, Debug)]
53pub struct ActionResults {
54    pub infos: Vec<String>,
55    pub warnings: Vec<String>,
56    pub errors: Vec<String>,
57    pub gauges: Vec<String>,
58    pub broken_gauges: Vec<String>,
59    pub snapshots: Vec<SnapshotTrigger>,
60    pub sort_gauges: bool,
61    pub verbose: bool,
62    pub sub_results: Vec<(String, Box<ActionResults>)>,
63}
64
65impl Default for ActionResults {
66    fn default() -> Self {
67        ActionResults {
68            infos: Vec::new(),
69            warnings: Vec::new(),
70            errors: Vec::new(),
71            gauges: Vec::new(),
72            broken_gauges: Vec::new(),
73            snapshots: Vec::new(),
74            sort_gauges: true,
75            verbose: false,
76            sub_results: Vec::new(),
77        }
78    }
79}
80
81impl ActionResults {
82    pub fn new() -> ActionResults {
83        ActionResults::default()
84    }
85
86    pub fn all_issues(&self) -> impl Iterator<Item = &str> {
87        self.infos.iter().chain(self.warnings.iter()).chain(self.errors.iter()).map(|s| s.as_ref())
88    }
89}
90
91/// [SnapshotTrigger] is the information needed to generate a request for a crash report.
92/// It can be returned from the library as part of ActionResults.
93#[derive(Debug, Clone, PartialEq)]
94pub struct SnapshotTrigger {
95    pub interval: i64, // zx::MonotonicDuration but this library has to run on host.
96    pub signature: String,
97}
98
99/// [Actions] are stored as a map of maps, both with string keys. The outer key
100/// is the namespace for the inner key, which is the name of the [Action].
101pub(crate) type Actions = HashMap<String, ActionsSchema>;
102
103/// [ActionsSchema] stores the [Action]s from a single config file / namespace.
104///
105/// This struct is used to deserialize the [Action]s from the JSON-formatted
106/// config file.
107pub(crate) type ActionsSchema = HashMap<String, Action>;
108
109/// Action represent actions that can be taken using an evaluated value(s).
110#[derive(Clone, Debug, Serialize, PartialEq)]
111#[serde(tag = "type")]
112pub enum Action {
113    Alert(Alert),
114    Gauge(Gauge),
115    Snapshot(Snapshot),
116}
117
118impl Action {
119    pub fn from_config_with_namespace(
120        action_config: ActionConfig,
121        namespace: &str,
122    ) -> Result<Action, anyhow::Error> {
123        let action = match action_config {
124            ActionConfig::Alert { trigger, print, file_bug, tag, severity } => {
125                Action::Alert(Alert {
126                    trigger: ValueSource::try_from_expression_with_namespace(&trigger, namespace)?,
127                    print,
128                    file_bug,
129                    tag,
130                    severity,
131                })
132            }
133            ActionConfig::Warning { trigger, print, file_bug, tag } => Action::Alert(Alert {
134                trigger: ValueSource::try_from_expression_with_namespace(&trigger, namespace)?,
135                print,
136                file_bug,
137                tag,
138                // TODO(https://fxbug.dev/42153014): `Warning` will be deprecated once all config files use `Alert`
139                severity: Severity::Warning,
140            }),
141            ActionConfig::Gauge { value, format, tag } => Action::Gauge(Gauge {
142                value: ValueSource::try_from_expression_with_namespace(&value, namespace)?,
143                format,
144                tag,
145            }),
146            ActionConfig::Snapshot { trigger, repeat, signature } => Action::Snapshot(Snapshot {
147                trigger: ValueSource::try_from_expression_with_namespace(&trigger, namespace)?,
148                repeat: ValueSource::try_from_expression_with_namespace(&repeat, namespace)?,
149                signature,
150            }),
151        };
152        Ok(action)
153    }
154}
155
156#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
157//#[serde(tag = "severity")]
158pub enum Severity {
159    Info,
160    Warning,
161    Error,
162}
163
164pub(crate) fn validate_action(
165    action_name: &str,
166    action_config: &ActionConfig,
167    namespace: &str,
168) -> Result<(), Error> {
169    match action_config {
170        // Make sure the snapshot signature isn't too long.
171        ActionConfig::Snapshot { signature, repeat, .. } => {
172            if signature.len() > MAX_CRASH_SIGNATURE_LENGTH as usize {
173                bail!("Signature too long in {}", action_name);
174            }
175            let repeat = ValueSource::try_from_expression_with_namespace(repeat, namespace)?;
176            // Make sure repeat is a const int expression (cache the value if so)
177            match repeat.metric {
178                Metric::Eval(repeat_expression) => {
179                    let repeat_value = MetricState::evaluate_const_expression(
180                        &repeat_expression.parsed_expression,
181                    );
182                    if let MetricValue::Int(repeat_int) = repeat_value {
183                        repeat.cached_value.borrow_mut().replace(MetricValue::Int(repeat_int));
184                    } else {
185                        bail!(
186                            "Snapshot {} repeat expression '{}' must evaluate to int, not {:?}",
187                            action_name,
188                            repeat_expression.raw_expression,
189                            repeat_value
190                        );
191                    }
192                }
193                _ => unreachable!("ValueSource::try_from() only produces an Eval"),
194            }
195        }
196        // Make sure Error-level alerts have a file_bug field.
197        ActionConfig::Alert { severity, file_bug, .. } => {
198            if *severity == Severity::Error && file_bug.is_none() {
199                bail!("Error severity requires file_bug field in {}", action_name);
200            }
201        }
202        _ => {}
203    }
204    Ok(())
205}
206
207/// Action that is triggered if a predicate is met.
208#[derive(Clone, Debug, Serialize, PartialEq)]
209pub struct Alert {
210    /// A wrapped expression to evaluate which determines if this action triggers.
211    pub trigger: ValueSource,
212    /// What to print if trigger is true.
213    pub print: String,
214    /// Describes where bugs should be filed if this action triggers.
215    pub file_bug: Option<String>,
216    /// An optional tag to associate with this Action.
217    pub tag: Option<String>,
218    /// Info, Warning, Error, with the same meanings as the log types. Error must have a file_bug:
219    /// field but that field is optional for Info and Warning.
220    pub severity: Severity,
221}
222
223/// Action that displays percentage of value.
224#[derive(Clone, Debug, Serialize, PartialEq)]
225pub struct Gauge {
226    /// Value to surface.
227    pub value: ValueSource,
228    /// Opaque type that determines how value should be formatted (e.g. percentage).
229    pub format: Option<String>,
230    /// An optional tag to associate with this Action.
231    pub tag: Option<String>,
232}
233
234/// Action that displays percentage of value.
235#[derive(Clone, Debug, Serialize, PartialEq)]
236pub struct Snapshot {
237    /// Take snapshot when this is true.
238    pub trigger: ValueSource,
239    /// A wrapped expression evaluating to time delay before repeated triggers.
240    pub repeat: ValueSource,
241    /// Sent in the crash report.
242    pub signature: String,
243    // There's no tag option because snapshot conditions are always news worth seeing.
244}
245
246impl Gauge {
247    pub fn get_formatted_value(&self, metric_value: MetricValue) -> String {
248        match metric_value {
249            MetricValue::Float(value) => match &self.format {
250                Some(format) if format.as_str() == "percentage" => {
251                    format!("{:.2}%", value * 100.0f64)
252                }
253                _ => format!("{}", value),
254            },
255            MetricValue::Int(value) => match &self.format {
256                Some(format) if format.as_str() == "percentage" => format!("{}%", value * 100),
257                _ => format!("{}", value),
258            },
259            MetricValue::Problem(Problem::Ignore(_)) => "N/A".to_string(),
260            value => format!("{:?}", value),
261        }
262    }
263}
264
265impl Action {
266    pub fn get_tag(&self) -> Option<String> {
267        match self {
268            Action::Alert(action) => action.tag.clone(),
269            Action::Gauge(action) => action.tag.clone(),
270            Action::Snapshot(_) => None,
271        }
272    }
273
274    /// Creates a [Warning] with a trigger evaluating to Bool(true) and its cache pre-populated.
275    pub fn new_synthetic_warning(print: String) -> Action {
276        let trigger_true = get_trigger_true();
277        Action::Alert(Alert {
278            trigger: trigger_true,
279            print,
280            file_bug: None,
281            tag: None,
282            severity: Severity::Warning,
283        })
284    }
285
286    pub fn new_synthetic_error(print: String, file_bug: String) -> Action {
287        let trigger_true = get_trigger_true();
288        Action::Alert(Alert {
289            trigger: trigger_true,
290            print,
291            file_bug: Some(file_bug),
292            tag: None,
293            severity: Severity::Error,
294        })
295    }
296
297    /// Creates a [Gauge] with the cache value pre-populated.
298    /// This only supports string values.
299    pub fn new_synthetic_string_gauge(
300        raw_value: String,
301        format: Option<String>,
302        tag: Option<String>,
303    ) -> Action {
304        let value = ValueSource {
305            metric: Metric::Eval(ExpressionContext {
306                raw_expression: format!("'{}'", raw_value),
307                parsed_expression: ExpressionTree::Value(MetricValue::String(raw_value.clone())),
308            }),
309            cached_value: RefCell::new(Some(MetricValue::String(raw_value))),
310        };
311        Action::Gauge(Gauge { value, format, tag })
312    }
313
314    /// Returns true if any significant problem or notification is found.
315    /// If the trigger or value hasnn't been evaluated, returns false
316    pub(crate) fn has_reportable_issue(&self) -> bool {
317        let value = match self {
318            Action::Alert(alert) => &alert.trigger.cached_value,
319            Action::Snapshot(snapshot) => &snapshot.trigger.cached_value,
320            Action::Gauge(gauge) => &gauge.value.cached_value,
321        };
322        let reportable_on_true = match self {
323            Action::Gauge(_) => false,
324            Action::Snapshot(_) => true,
325            Action::Alert(alert) if alert.severity == Severity::Info => false,
326            Action::Alert(_) => true,
327        };
328        let result = match *value.borrow() {
329            Some(MetricValue::Bool(true)) if reportable_on_true => true,
330            Some(MetricValue::Problem(Problem::Missing(_))) => false,
331            Some(MetricValue::Problem(Problem::Ignore(_))) => false,
332            Some(MetricValue::Problem(_)) => true,
333            _ => false,
334        };
335        result
336    }
337}
338
339fn get_trigger_true() -> ValueSource {
340    ValueSource {
341        metric: Metric::Eval(ExpressionContext {
342            raw_expression: "True()".to_string(),
343            parsed_expression: ExpressionTree::Function(Function::True, vec![]),
344        }),
345        cached_value: RefCell::new(Some(MetricValue::Bool(true))),
346    }
347}
348
349/// Contains all Error, Warning, and Info generated while computing snapshots.
350pub type WarningVec = Vec<String>;
351
352impl ActionContext<'_> {
353    /// Processes all actions, acting on the ones that trigger.
354    pub fn process(&mut self) -> &ActionResults {
355        if let Fetcher::FileData(file_data) = &self.metric_state.fetcher {
356            for plugin in &self.plugins {
357                self.action_results
358                    .sub_results
359                    .push((plugin.display_name().to_string(), Box::new(plugin.run(file_data))));
360            }
361        }
362
363        for (namespace, actions) in self.actions.iter() {
364            for (name, action) in actions.iter() {
365                match action {
366                    Action::Alert(alert) => self.update_alerts(alert, namespace, name),
367                    Action::Gauge(gauge) => self.update_gauges(gauge, namespace, name),
368                    Action::Snapshot(snapshot) => self.update_snapshots(snapshot, namespace, name),
369                };
370            }
371        }
372
373        &self.action_results
374    }
375
376    pub(crate) fn set_verbose(&mut self, verbose: bool) {
377        self.action_results.verbose = verbose;
378    }
379
380    /// Evaluate and return snapshots. Consume self.
381    pub fn into_snapshots(mut self) -> (Vec<SnapshotTrigger>, WarningVec) {
382        for (namespace, actions) in self.actions.iter() {
383            for (name, action) in actions.iter() {
384                if let Action::Snapshot(snapshot) = action {
385                    self.update_snapshots(snapshot, namespace, name)
386                }
387            }
388        }
389        let mut alerts = vec![];
390        alerts.extend(self.action_results.errors);
391        alerts.extend(self.action_results.warnings);
392        alerts.extend(self.action_results.infos);
393        (self.action_results.snapshots, alerts)
394    }
395
396    /// Update warnings if condition is met.
397    fn update_alerts(&mut self, action: &Alert, namespace: &String, name: &String) {
398        match self.metric_state.eval_action_metric(namespace, &action.trigger) {
399            MetricValue::Bool(true) => {
400                if let Some(file_bug) = &action.file_bug {
401                    self.action_results
402                        .errors
403                        .push(format!("[BUG:{}] {}.", file_bug, action.print));
404                } else {
405                    self.action_results.warnings.push(format!("[WARNING] {}.", action.print));
406                }
407            }
408            MetricValue::Bool(false) => (),
409            MetricValue::Problem(Problem::Ignore(_)) => (),
410            MetricValue::Problem(Problem::Missing(reason)) => {
411                self.action_results.infos.push(format!(
412                    "[MISSING] In config '{}::{}': (need boolean trigger) {:?}",
413                    namespace, name, reason,
414                ));
415            }
416            MetricValue::Problem(problem) => {
417                self.action_results.errors.push(format!(
418                    "[ERROR] In config '{}::{}': (need boolean trigger): {:?}",
419                    namespace, name, problem,
420                ));
421            }
422            other => {
423                self.action_results.errors.push(format!(
424                    "[DEBUG: BAD CONFIG] Unexpected value type in config '{}::{}' (need boolean trigger): {}",
425                    namespace,
426                    name,
427                    other,
428                ));
429            }
430        };
431    }
432
433    /// Update snapshots if condition is met.
434    fn update_snapshots(&mut self, action: &Snapshot, namespace: &str, name: &str) {
435        match self.metric_state.eval_action_metric(namespace, &action.trigger) {
436            MetricValue::Bool(true) => {
437                let repeat_value = self.metric_state.eval_action_metric(namespace, &action.repeat);
438                let interval = metric_value_to_int(repeat_value);
439                match interval {
440                    Ok(interval) => {
441                        let signature = action.signature.clone();
442                        let output = SnapshotTrigger { interval, signature };
443                        self.action_results.snapshots.push(output);
444                    }
445                    Err(ref bad_type) => {
446                        self.action_results.errors.push(format!(
447                            "Bad interval in config '{}::{}': {:?}",
448                            namespace, name, bad_type,
449                        ));
450                        inspect_logger::log_error(
451                            "Bad interval",
452                            namespace,
453                            name,
454                            &format!("{:?}", interval),
455                        );
456                    }
457                }
458            }
459            MetricValue::Bool(false) => (),
460            MetricValue::Problem(Problem::Ignore(_)) => (),
461            MetricValue::Problem(reason) => {
462                inspect_logger::log_warn(
463                    "Snapshot trigger not boolean",
464                    namespace,
465                    name,
466                    &format!("{:?}", reason),
467                );
468                self.action_results
469                    .infos
470                    .push(format!("[MISSING] In config '{}::{}': {:?}", namespace, name, reason,));
471            }
472            other => {
473                inspect_logger::log_error(
474                    "Bad config: Unexpected value type (need boolean)",
475                    namespace,
476                    name,
477                    &format!("{}", other),
478                );
479                self.action_results.errors.push(format!(
480                    "Bad config: Unexpected value type in config '{}::{}' (need boolean): {}",
481                    namespace, name, other,
482                ));
483            }
484        };
485    }
486
487    /// Update gauges.
488    fn update_gauges(&mut self, action: &Gauge, namespace: &str, name: &str) {
489        let value = self.metric_state.eval_action_metric(namespace, &action.value);
490        match value {
491            MetricValue::Problem(Problem::Ignore(_)) => {
492                self.action_results.broken_gauges.push(format!("{}: N/A", name));
493            }
494            MetricValue::Problem(problem) => {
495                self.action_results.broken_gauges.push(format!("{}: {:?}", name, problem));
496            }
497            value => {
498                self.action_results.gauges.push(format!(
499                    "{}: {}",
500                    name,
501                    action.get_formatted_value(value)
502                ));
503            }
504        }
505    }
506}
507
508#[cfg(test)]
509mod test {
510    use super::*;
511    use crate::config::Source;
512    use crate::make_metrics;
513
514    /// Tells whether any of the stored values include a substring.
515    fn includes(values: &Vec<String>, substring: &str) -> bool {
516        for value in values {
517            if value.contains(substring) {
518                return true;
519            }
520        }
521        false
522    }
523
524    #[fuchsia::test]
525    fn actions_fire_correctly() {
526        let metrics = make_metrics!({
527            "file":{
528                eval: {
529                    "true": "0 == 0",
530                    "false": "0 == 1",
531                    "true_array": "[0 == 0]",
532                    "false_array": "[0 == 1]"
533                }
534            }
535        });
536        let mut actions = Actions::new();
537        let mut action_file = ActionsSchema::new();
538        action_file.insert(
539            "do_true".to_string(),
540            Action::Alert(Alert {
541                trigger: ValueSource::try_from_expression_with_namespace("true", "file").unwrap(),
542                print: "True was fired".to_string(),
543                file_bug: Some("Some>Monorail>Component".to_string()),
544                tag: None,
545                severity: Severity::Warning,
546            }),
547        );
548        action_file.insert(
549            "do_false".to_string(),
550            Action::Alert(Alert {
551                trigger: ValueSource::try_from_expression_with_namespace("false", "file").unwrap(),
552                print: "False was fired".to_string(),
553                file_bug: None,
554                tag: None,
555                severity: Severity::Warning,
556            }),
557        );
558        action_file.insert(
559            "do_true_array".to_string(),
560            Action::Alert(Alert {
561                trigger: ValueSource::try_from_expression_with_namespace("true_array", "file")
562                    .unwrap(),
563                print: "True array was fired".to_string(),
564                file_bug: None,
565                tag: None,
566                severity: Severity::Warning,
567            }),
568        );
569        action_file.insert(
570            "do_false_array".to_string(),
571            Action::Alert(Alert {
572                trigger: ValueSource::try_from_expression_with_namespace("false_array", "file")
573                    .unwrap(),
574                print: "False array was fired".to_string(),
575                file_bug: None,
576                tag: None,
577                severity: Severity::Warning,
578            }),
579        );
580
581        action_file.insert(
582            "do_operation".to_string(),
583            Action::Alert(Alert {
584                trigger: ValueSource::try_from_expression_with_namespace("0 < 10", "file").unwrap(),
585                print: "Inequality triggered".to_string(),
586                file_bug: None,
587                tag: None,
588                severity: Severity::Warning,
589            }),
590        );
591        actions.insert("file".to_string(), action_file);
592        let no_data = Vec::new();
593        let mut context = ActionContext::new(&metrics, &actions, &no_data, None);
594        let results = context.process();
595        assert!(includes(&results.errors, "[BUG:Some>Monorail>Component] True was fired."));
596        assert!(includes(&results.warnings, "[WARNING] Inequality triggered."));
597        assert!(includes(&results.warnings, "[WARNING] True array was fired"));
598        assert!(!includes(&results.warnings, "False was fired"));
599        assert!(!includes(&results.warnings, "False array was fired"));
600    }
601
602    #[fuchsia::test]
603    fn gauges_fire_correctly() {
604        let metrics = make_metrics!({
605            "file":{
606                eval: {
607                    "gauge_f1": "2 / 5",
608                    "gauge_f2": "4 / 5",
609                    "gauge_f3": "6 / 5",
610                    "gauge_i4": "9 // 2",
611                    "gauge_i5": "11 // 2",
612                    "gauge_i6": "13 // 2",
613                    "gauge_b7": "2 == 2",
614                    "gauge_b8": "2 > 2",
615                    "gauge_s9": "'foo'"
616                }
617            }
618        });
619        let mut actions = Actions::new();
620        let mut action_file = ActionsSchema::new();
621        macro_rules! insert_gauge {
622            ($name:expr, $format:expr) => {
623                action_file.insert(
624                    $name.to_string(),
625                    Action::Gauge(Gauge {
626                        value: ValueSource::try_from_expression_with_namespace($name, "file")
627                            .unwrap(),
628                        format: $format,
629                        tag: None,
630                    }),
631                );
632            };
633        }
634        insert_gauge!("gauge_f1", None);
635        insert_gauge!("gauge_f2", Some("percentage".to_string()));
636        insert_gauge!("gauge_f3", Some("unknown".to_string()));
637        insert_gauge!("gauge_i4", None);
638        insert_gauge!("gauge_i5", Some("percentage".to_string()));
639        insert_gauge!("gauge_i6", Some("unknown".to_string()));
640        insert_gauge!("gauge_b7", None);
641        insert_gauge!("gauge_b8", None);
642        insert_gauge!("gauge_s9", None);
643        actions.insert("file".to_string(), action_file);
644        let no_data = Vec::new();
645        let mut context = ActionContext::new(&metrics, &actions, &no_data, None);
646
647        let results = context.process();
648
649        assert!(includes(&results.gauges, "gauge_f1: 0.4"));
650        assert!(includes(&results.gauges, "gauge_f2: 80.00%"));
651        assert!(includes(&results.gauges, "gauge_f3: 1.2"));
652        assert!(includes(&results.gauges, "gauge_i4: 4"));
653        assert!(includes(&results.gauges, "gauge_i5: 500%"));
654        assert!(includes(&results.gauges, "gauge_i6: 6"));
655        assert!(includes(&results.gauges, "gauge_b7: Bool(true)"));
656        assert!(includes(&results.gauges, "gauge_b8: Bool(false)"));
657        assert!(includes(&results.gauges, "gauge_s9: String(\"foo\")"));
658    }
659
660    #[fuchsia::test]
661    fn action_context_errors() {
662        let metrics = Metrics::new();
663        let actions = Actions::new();
664        let data = vec![DiagnosticData::new(
665            "inspect.json".to_string(),
666            Source::Inspect,
667            r#"
668            [
669                {
670                    "moniker": "abcd",
671                    "metadata": {},
672                    "payload": {"root": {"val": 10}}
673                },
674                {
675                    "moniker": "abcd2",
676                    "metadata": {},
677                    "payload": ["a", "b"]
678                },
679                {
680                    "moniker": "abcd3",
681                    "metadata": {},
682                    "payload": null
683                }
684            ]
685            "#
686            .to_string(),
687        )
688        .expect("create data")];
689        let action_context = ActionContext::new(&metrics, &actions, &data, None);
690        // Caution - test footgun! This error will show up without calling process() but
691        // most get_warnings() results will not.
692        assert_eq!(
693            vec!["[DEBUG: BAD DATA] Unable to deserialize Inspect contents for abcd2 to node hierarchy"
694                .to_string()],
695            action_context.action_results.errors
696        );
697    }
698
699    #[fuchsia::test]
700    fn time_propagates_correctly() {
701        let metrics = Metrics::new();
702        let mut actions = Actions::new();
703        let mut action_file = ActionsSchema::new();
704        action_file.insert(
705            "time_1234".to_string(),
706            Action::Alert(Alert {
707                trigger: ValueSource::try_from_expression_with_namespace("Now() == 1234", "file")
708                    .unwrap(),
709                print: "1234".to_string(),
710                tag: None,
711                file_bug: None,
712                severity: Severity::Warning,
713            }),
714        );
715        action_file.insert(
716            "time_missing".to_string(),
717            Action::Alert(Alert {
718                trigger: ValueSource::try_from_expression_with_namespace("Problem(Now())", "file")
719                    .unwrap(),
720                print: "missing".to_string(),
721                tag: None,
722                file_bug: None,
723                severity: Severity::Warning,
724            }),
725        );
726        actions.insert("file".to_string(), action_file);
727        let data = vec![];
728        let actions_missing = actions.clone();
729        let mut context_1234 = ActionContext::new(&metrics, &actions, &data, Some(1234));
730        let results_1234 = context_1234.process();
731        let mut context_missing = ActionContext::new(&metrics, &actions_missing, &data, None);
732        let results_no_time = context_missing.process();
733
734        assert_eq!(vec!["[WARNING] 1234.".to_string()], results_1234.warnings);
735        assert!(results_no_time
736            .infos
737            .contains(&"[MISSING] In config \'file::time_1234\': (need boolean trigger) \"No valid time available\"".to_string()));
738        assert!(results_no_time.warnings.contains(&"[WARNING] missing.".to_string()));
739    }
740
741    #[fuchsia::test]
742    fn snapshots_update_correctly() -> Result<(), Error> {
743        let metrics = Metrics::new();
744        let actions = Actions::new();
745        let data = vec![];
746        let mut action_context = ActionContext::new(&metrics, &actions, &data, None);
747        let true_value = ValueSource::try_from_expression_with_default_namespace("1==1")?;
748        let false_value = ValueSource::try_from_expression_with_default_namespace("1==2")?;
749        let five_value = ValueSource {
750            metric: Metric::Eval(ExpressionContext::try_from_expression_with_default_namespace(
751                "5",
752            )?),
753            cached_value: RefCell::new(Some(MetricValue::Int(5))),
754        };
755        let foo_value = ValueSource::try_from_expression_with_default_namespace("'foo'")?;
756        let missing_value = ValueSource::try_from_expression_with_default_namespace("foo")?;
757        let snapshot_5_sig = SnapshotTrigger { interval: 5, signature: "signature".to_string() };
758        // Tester re-uses the same action_context, so results will accumulate.
759        macro_rules! tester {
760            ($trigger:expr, $repeat:expr, $func:expr) => {
761                let selector_interval_action = Snapshot {
762                    trigger: $trigger.clone(),
763                    repeat: $repeat.clone(),
764                    signature: "signature".to_string(),
765                };
766                action_context.update_snapshots(&selector_interval_action, "", "");
767                assert!($func(&action_context.action_results.snapshots));
768            };
769        }
770        type VT = Vec<SnapshotTrigger>;
771
772        // Verify it doesn't crash on bad inputs
773        tester!(true_value, foo_value, |s: &VT| s.is_empty());
774        tester!(true_value, missing_value, |s: &VT| s.is_empty());
775        tester!(foo_value, five_value, |s: &VT| s.is_empty());
776        tester!(five_value, five_value, |s: &VT| s.is_empty());
777        tester!(missing_value, five_value, |s: &VT| s.is_empty());
778        // Problem::Missing shows up in infos, not warnings
779        assert_eq!(action_context.action_results.infos.len(), 1);
780        assert_eq!(action_context.action_results.warnings.len(), 0);
781        assert_eq!(action_context.action_results.errors.len(), 4);
782        // False trigger shouldn't add a result
783        tester!(false_value, five_value, |s: &VT| s.is_empty());
784        tester!(true_value, five_value, |s| s == &vec![snapshot_5_sig.clone()]);
785        // We can have more than one of the same trigger in the results.
786        tester!(true_value, five_value, |s| s
787            == &vec![snapshot_5_sig.clone(), snapshot_5_sig.clone()]);
788        assert_eq!(action_context.action_results.infos.len(), 1);
789        assert_eq!(action_context.action_results.warnings.len(), 0);
790        assert_eq!(action_context.action_results.errors.len(), 4);
791        let (snapshots, warnings) = action_context.into_snapshots();
792        assert_eq!(snapshots.len(), 2);
793        assert_eq!(warnings.len(), 5);
794        Ok(())
795    }
796
797    #[fuchsia::test]
798    fn actions_cache_correctly() {
799        let metrics = make_metrics!({
800            "file":{
801                eval: {
802                    "true": "0 == 0",
803                    "false": "0 == 1",
804                    "five": "5"
805                }
806            }
807        });
808        let mut actions = Actions::new();
809        let mut action_file = ActionsSchema::new();
810        action_file.insert(
811            "true_warning".to_string(),
812            Action::Alert(Alert {
813                trigger: ValueSource::try_from_expression_with_namespace("true", "file").unwrap(),
814                print: "True was fired".to_string(),
815                file_bug: None,
816                tag: None,
817                severity: Severity::Warning,
818            }),
819        );
820        action_file.insert(
821            "false_gauge".to_string(),
822            Action::Gauge(Gauge {
823                value: ValueSource::try_from_expression_with_namespace("false", "file").unwrap(),
824                format: None,
825                tag: None,
826            }),
827        );
828        action_file.insert(
829            "true_snapshot".to_string(),
830            Action::Snapshot(Snapshot {
831                trigger: ValueSource::try_from_expression_with_namespace("true", "file").unwrap(),
832                repeat: ValueSource {
833                    metric: Metric::Eval(
834                        ExpressionContext::try_from_expression_with_namespace("five", "file")
835                            .unwrap(),
836                    ),
837                    cached_value: RefCell::new(Some(MetricValue::Int(5))),
838                },
839                signature: "signature".to_string(),
840            }),
841        );
842        action_file.insert(
843            "test_snapshot".to_string(),
844            Action::Snapshot(Snapshot {
845                trigger: ValueSource::try_from_expression_with_namespace("true", "file").unwrap(),
846                repeat: ValueSource::try_from_expression_with_namespace("five", "file").unwrap(),
847                signature: "signature".to_string(),
848            }),
849        );
850        actions.insert("file".to_string(), action_file);
851        let no_data = Vec::new();
852        let mut context = ActionContext::new(&metrics, &actions, &no_data, None);
853        context.process();
854
855        // Ensure Alert caches correctly
856        if let Action::Alert(warning) = actions.get("file").unwrap().get("true_warning").unwrap() {
857            assert_eq!(*warning.trigger.cached_value.borrow(), Some(MetricValue::Bool(true)));
858        } else {
859            unreachable!("'true_warning' must be an Action::Alert")
860        }
861
862        // Ensure Gauge caches correctly
863        if let Action::Gauge(gauge) = actions.get("file").unwrap().get("false_gauge").unwrap() {
864            assert_eq!(*gauge.value.cached_value.borrow(), Some(MetricValue::Bool(false)));
865        } else {
866            unreachable!("'false_gauge' must be an Action::Gauge")
867        }
868
869        // Ensure Snapshot caches correctly
870        if let Action::Snapshot(snapshot) =
871            actions.get("file").unwrap().get("true_snapshot").unwrap()
872        {
873            assert_eq!(*snapshot.trigger.cached_value.borrow(), Some(MetricValue::Bool(true)));
874            assert_eq!(*snapshot.repeat.cached_value.borrow(), Some(MetricValue::Int(5)));
875        } else {
876            unreachable!("'true_snapshot' must be an Action::Snapshot")
877        }
878
879        // Ensure value-calculation does not fail for a Snapshot with an empty cache.
880        // The cached value for 'repeat' is expected to be pre-calculated during deserialization
881        // however, an empty cached value should still be supported.
882        if let Action::Snapshot(snapshot) =
883            actions.get("file").unwrap().get("test_snapshot").unwrap()
884        {
885            assert_eq!(*snapshot.trigger.cached_value.borrow(), Some(MetricValue::Bool(true)));
886            assert_eq!(*snapshot.repeat.cached_value.borrow(), Some(MetricValue::Int(5)));
887        } else {
888            unreachable!("'true_snapshot' must be an Action::Snapshot")
889        }
890    }
891}