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 '{namespace}::{name}': (need boolean trigger) {reason:?}",
413                ));
414            }
415            MetricValue::Problem(problem) => {
416                self.action_results.errors.push(format!(
417                    "[ERROR] In config '{namespace}::{name}': (need boolean trigger): {problem:?}",
418                ));
419            }
420            other => {
421                self.action_results.errors.push(format!(
422                    "[DEBUG: BAD CONFIG] Unexpected value type in config '{namespace}::{name}' (need boolean trigger): {other}",
423                ));
424            }
425        };
426    }
427
428    /// Update snapshots if condition is met.
429    fn update_snapshots(&mut self, action: &Snapshot, namespace: &str, name: &str) {
430        match self.metric_state.eval_action_metric(namespace, &action.trigger) {
431            MetricValue::Bool(true) => {
432                let repeat_value = self.metric_state.eval_action_metric(namespace, &action.repeat);
433                let interval = metric_value_to_int(repeat_value);
434                match interval {
435                    Ok(interval) => {
436                        let signature = action.signature.clone();
437                        let output = SnapshotTrigger { interval, signature };
438                        self.action_results.snapshots.push(output);
439                    }
440                    Err(ref bad_type) => {
441                        self.action_results.errors.push(format!(
442                            "Bad interval in config '{namespace}::{name}': {bad_type:?}",
443                        ));
444                        inspect_logger::log_error(
445                            "Bad interval",
446                            namespace,
447                            name,
448                            &format!("{interval:?}"),
449                        );
450                    }
451                }
452            }
453            MetricValue::Bool(false) => (),
454            MetricValue::Problem(Problem::Ignore(_)) => (),
455            MetricValue::Problem(reason) => {
456                inspect_logger::log_warn(
457                    "Snapshot trigger not boolean",
458                    namespace,
459                    name,
460                    &format!("{reason:?}"),
461                );
462                self.action_results
463                    .infos
464                    .push(format!("[MISSING] In config '{namespace}::{name}': {reason:?}",));
465            }
466            other => {
467                inspect_logger::log_error(
468                    "Bad config: Unexpected value type (need boolean)",
469                    namespace,
470                    name,
471                    &format!("{other}"),
472                );
473                self.action_results.errors.push(format!(
474                    "Bad config: Unexpected value type in config '{namespace}::{name}' (need boolean): {other}",
475                ));
476            }
477        };
478    }
479
480    /// Update gauges.
481    fn update_gauges(&mut self, action: &Gauge, namespace: &str, name: &str) {
482        let value = self.metric_state.eval_action_metric(namespace, &action.value);
483        match value {
484            MetricValue::Problem(Problem::Ignore(_)) => {
485                self.action_results.broken_gauges.push(format!("{name}: N/A"));
486            }
487            MetricValue::Problem(problem) => {
488                self.action_results.broken_gauges.push(format!("{name}: {problem:?}"));
489            }
490            value => {
491                self.action_results.gauges.push(format!(
492                    "{}: {}",
493                    name,
494                    action.get_formatted_value(value)
495                ));
496            }
497        }
498    }
499}
500
501#[cfg(test)]
502mod test {
503    use super::*;
504    use crate::config::Source;
505    use crate::make_metrics;
506
507    /// Tells whether any of the stored values include a substring.
508    fn includes(values: &Vec<String>, substring: &str) -> bool {
509        for value in values {
510            if value.contains(substring) {
511                return true;
512            }
513        }
514        false
515    }
516
517    #[fuchsia::test]
518    fn actions_fire_correctly() {
519        let metrics = make_metrics!({
520            "file":{
521                eval: {
522                    "true": "0 == 0",
523                    "false": "0 == 1",
524                    "true_array": "[0 == 0]",
525                    "false_array": "[0 == 1]"
526                }
527            }
528        });
529        let mut actions = Actions::new();
530        let mut action_file = ActionsSchema::new();
531        action_file.insert(
532            "do_true".to_string(),
533            Action::Alert(Alert {
534                trigger: ValueSource::try_from_expression_with_namespace("true", "file").unwrap(),
535                print: "True was fired".to_string(),
536                file_bug: Some("Some>Monorail>Component".to_string()),
537                tag: None,
538                severity: Severity::Warning,
539            }),
540        );
541        action_file.insert(
542            "do_false".to_string(),
543            Action::Alert(Alert {
544                trigger: ValueSource::try_from_expression_with_namespace("false", "file").unwrap(),
545                print: "False was fired".to_string(),
546                file_bug: None,
547                tag: None,
548                severity: Severity::Warning,
549            }),
550        );
551        action_file.insert(
552            "do_true_array".to_string(),
553            Action::Alert(Alert {
554                trigger: ValueSource::try_from_expression_with_namespace("true_array", "file")
555                    .unwrap(),
556                print: "True array was fired".to_string(),
557                file_bug: None,
558                tag: None,
559                severity: Severity::Warning,
560            }),
561        );
562        action_file.insert(
563            "do_false_array".to_string(),
564            Action::Alert(Alert {
565                trigger: ValueSource::try_from_expression_with_namespace("false_array", "file")
566                    .unwrap(),
567                print: "False array was fired".to_string(),
568                file_bug: None,
569                tag: None,
570                severity: Severity::Warning,
571            }),
572        );
573
574        action_file.insert(
575            "do_operation".to_string(),
576            Action::Alert(Alert {
577                trigger: ValueSource::try_from_expression_with_namespace("0 < 10", "file").unwrap(),
578                print: "Inequality triggered".to_string(),
579                file_bug: None,
580                tag: None,
581                severity: Severity::Warning,
582            }),
583        );
584        actions.insert("file".to_string(), action_file);
585        let no_data = Vec::new();
586        let mut context = ActionContext::new(&metrics, &actions, &no_data, None);
587        let results = context.process();
588        assert!(includes(&results.errors, "[BUG:Some>Monorail>Component] True was fired."));
589        assert!(includes(&results.warnings, "[WARNING] Inequality triggered."));
590        assert!(includes(&results.warnings, "[WARNING] True array was fired"));
591        assert!(!includes(&results.warnings, "False was fired"));
592        assert!(!includes(&results.warnings, "False array was fired"));
593    }
594
595    #[fuchsia::test]
596    fn gauges_fire_correctly() {
597        let metrics = make_metrics!({
598            "file":{
599                eval: {
600                    "gauge_f1": "2 / 5",
601                    "gauge_f2": "4 / 5",
602                    "gauge_f3": "6 / 5",
603                    "gauge_i4": "9 // 2",
604                    "gauge_i5": "11 // 2",
605                    "gauge_i6": "13 // 2",
606                    "gauge_b7": "2 == 2",
607                    "gauge_b8": "2 > 2",
608                    "gauge_s9": "'foo'"
609                }
610            }
611        });
612        let mut actions = Actions::new();
613        let mut action_file = ActionsSchema::new();
614        macro_rules! insert_gauge {
615            ($name:expr, $format:expr) => {
616                action_file.insert(
617                    $name.to_string(),
618                    Action::Gauge(Gauge {
619                        value: ValueSource::try_from_expression_with_namespace($name, "file")
620                            .unwrap(),
621                        format: $format,
622                        tag: None,
623                    }),
624                );
625            };
626        }
627        insert_gauge!("gauge_f1", None);
628        insert_gauge!("gauge_f2", Some("percentage".to_string()));
629        insert_gauge!("gauge_f3", Some("unknown".to_string()));
630        insert_gauge!("gauge_i4", None);
631        insert_gauge!("gauge_i5", Some("percentage".to_string()));
632        insert_gauge!("gauge_i6", Some("unknown".to_string()));
633        insert_gauge!("gauge_b7", None);
634        insert_gauge!("gauge_b8", None);
635        insert_gauge!("gauge_s9", None);
636        actions.insert("file".to_string(), action_file);
637        let no_data = Vec::new();
638        let mut context = ActionContext::new(&metrics, &actions, &no_data, None);
639
640        let results = context.process();
641
642        assert!(includes(&results.gauges, "gauge_f1: 0.4"));
643        assert!(includes(&results.gauges, "gauge_f2: 80.00%"));
644        assert!(includes(&results.gauges, "gauge_f3: 1.2"));
645        assert!(includes(&results.gauges, "gauge_i4: 4"));
646        assert!(includes(&results.gauges, "gauge_i5: 500%"));
647        assert!(includes(&results.gauges, "gauge_i6: 6"));
648        assert!(includes(&results.gauges, "gauge_b7: Bool(true)"));
649        assert!(includes(&results.gauges, "gauge_b8: Bool(false)"));
650        assert!(includes(&results.gauges, "gauge_s9: String(\"foo\")"));
651    }
652
653    #[fuchsia::test]
654    fn action_context_errors() {
655        let metrics = Metrics::new();
656        let actions = Actions::new();
657        let data = vec![DiagnosticData::new(
658            "inspect.json".to_string(),
659            Source::Inspect,
660            r#"
661            [
662                {
663                    "moniker": "abcd",
664                    "metadata": {},
665                    "payload": {"root": {"val": 10}}
666                },
667                {
668                    "moniker": "abcd2",
669                    "metadata": {},
670                    "payload": ["a", "b"]
671                },
672                {
673                    "moniker": "abcd3",
674                    "metadata": {},
675                    "payload": null
676                }
677            ]
678            "#
679            .to_string(),
680        )
681        .expect("create data")];
682        let action_context = ActionContext::new(&metrics, &actions, &data, None);
683        // Caution - test footgun! This error will show up without calling process() but
684        // most get_warnings() results will not.
685        assert_eq!(
686            vec!["[DEBUG: BAD DATA] Unable to deserialize Inspect contents for abcd2 to node hierarchy"
687                .to_string()],
688            action_context.action_results.errors
689        );
690    }
691
692    #[fuchsia::test]
693    fn time_propagates_correctly() {
694        let metrics = Metrics::new();
695        let mut actions = Actions::new();
696        let mut action_file = ActionsSchema::new();
697        action_file.insert(
698            "time_1234".to_string(),
699            Action::Alert(Alert {
700                trigger: ValueSource::try_from_expression_with_namespace("Now() == 1234", "file")
701                    .unwrap(),
702                print: "1234".to_string(),
703                tag: None,
704                file_bug: None,
705                severity: Severity::Warning,
706            }),
707        );
708        action_file.insert(
709            "time_missing".to_string(),
710            Action::Alert(Alert {
711                trigger: ValueSource::try_from_expression_with_namespace("Problem(Now())", "file")
712                    .unwrap(),
713                print: "missing".to_string(),
714                tag: None,
715                file_bug: None,
716                severity: Severity::Warning,
717            }),
718        );
719        actions.insert("file".to_string(), action_file);
720        let data = vec![];
721        let actions_missing = actions.clone();
722        let mut context_1234 = ActionContext::new(&metrics, &actions, &data, Some(1234));
723        let results_1234 = context_1234.process();
724        let mut context_missing = ActionContext::new(&metrics, &actions_missing, &data, None);
725        let results_no_time = context_missing.process();
726
727        assert_eq!(vec!["[WARNING] 1234.".to_string()], results_1234.warnings);
728        assert!(results_no_time
729            .infos
730            .contains(&"[MISSING] In config \'file::time_1234\': (need boolean trigger) \"No valid time available\"".to_string()));
731        assert!(results_no_time.warnings.contains(&"[WARNING] missing.".to_string()));
732    }
733
734    #[fuchsia::test]
735    fn snapshots_update_correctly() -> Result<(), Error> {
736        let metrics = Metrics::new();
737        let actions = Actions::new();
738        let data = vec![];
739        let mut action_context = ActionContext::new(&metrics, &actions, &data, None);
740        let true_value = ValueSource::try_from_expression_with_default_namespace("1==1")?;
741        let false_value = ValueSource::try_from_expression_with_default_namespace("1==2")?;
742        let five_value = ValueSource {
743            metric: Metric::Eval(ExpressionContext::try_from_expression_with_default_namespace(
744                "5",
745            )?),
746            cached_value: RefCell::new(Some(MetricValue::Int(5))),
747        };
748        let foo_value = ValueSource::try_from_expression_with_default_namespace("'foo'")?;
749        let missing_value = ValueSource::try_from_expression_with_default_namespace("foo")?;
750        let snapshot_5_sig = SnapshotTrigger { interval: 5, signature: "signature".to_string() };
751        // Tester re-uses the same action_context, so results will accumulate.
752        macro_rules! tester {
753            ($trigger:expr, $repeat:expr, $func:expr) => {
754                let selector_interval_action = Snapshot {
755                    trigger: $trigger.clone(),
756                    repeat: $repeat.clone(),
757                    signature: "signature".to_string(),
758                };
759                action_context.update_snapshots(&selector_interval_action, "", "");
760                assert!($func(&action_context.action_results.snapshots));
761            };
762        }
763        type VT = Vec<SnapshotTrigger>;
764
765        // Verify it doesn't crash on bad inputs
766        tester!(true_value, foo_value, |s: &VT| s.is_empty());
767        tester!(true_value, missing_value, |s: &VT| s.is_empty());
768        tester!(foo_value, five_value, |s: &VT| s.is_empty());
769        tester!(five_value, five_value, |s: &VT| s.is_empty());
770        tester!(missing_value, five_value, |s: &VT| s.is_empty());
771        // Problem::Missing shows up in infos, not warnings
772        assert_eq!(action_context.action_results.infos.len(), 1);
773        assert_eq!(action_context.action_results.warnings.len(), 0);
774        assert_eq!(action_context.action_results.errors.len(), 4);
775        // False trigger shouldn't add a result
776        tester!(false_value, five_value, |s: &VT| s.is_empty());
777        tester!(true_value, five_value, |s| s == &vec![snapshot_5_sig.clone()]);
778        // We can have more than one of the same trigger in the results.
779        tester!(true_value, five_value, |s| s
780            == &vec![snapshot_5_sig.clone(), snapshot_5_sig.clone()]);
781        assert_eq!(action_context.action_results.infos.len(), 1);
782        assert_eq!(action_context.action_results.warnings.len(), 0);
783        assert_eq!(action_context.action_results.errors.len(), 4);
784        let (snapshots, warnings) = action_context.into_snapshots();
785        assert_eq!(snapshots.len(), 2);
786        assert_eq!(warnings.len(), 5);
787        Ok(())
788    }
789
790    #[fuchsia::test]
791    fn actions_cache_correctly() {
792        let metrics = make_metrics!({
793            "file":{
794                eval: {
795                    "true": "0 == 0",
796                    "false": "0 == 1",
797                    "five": "5"
798                }
799            }
800        });
801        let mut actions = Actions::new();
802        let mut action_file = ActionsSchema::new();
803        action_file.insert(
804            "true_warning".to_string(),
805            Action::Alert(Alert {
806                trigger: ValueSource::try_from_expression_with_namespace("true", "file").unwrap(),
807                print: "True was fired".to_string(),
808                file_bug: None,
809                tag: None,
810                severity: Severity::Warning,
811            }),
812        );
813        action_file.insert(
814            "false_gauge".to_string(),
815            Action::Gauge(Gauge {
816                value: ValueSource::try_from_expression_with_namespace("false", "file").unwrap(),
817                format: None,
818                tag: None,
819            }),
820        );
821        action_file.insert(
822            "true_snapshot".to_string(),
823            Action::Snapshot(Snapshot {
824                trigger: ValueSource::try_from_expression_with_namespace("true", "file").unwrap(),
825                repeat: ValueSource {
826                    metric: Metric::Eval(
827                        ExpressionContext::try_from_expression_with_namespace("five", "file")
828                            .unwrap(),
829                    ),
830                    cached_value: RefCell::new(Some(MetricValue::Int(5))),
831                },
832                signature: "signature".to_string(),
833            }),
834        );
835        action_file.insert(
836            "test_snapshot".to_string(),
837            Action::Snapshot(Snapshot {
838                trigger: ValueSource::try_from_expression_with_namespace("true", "file").unwrap(),
839                repeat: ValueSource::try_from_expression_with_namespace("five", "file").unwrap(),
840                signature: "signature".to_string(),
841            }),
842        );
843        actions.insert("file".to_string(), action_file);
844        let no_data = Vec::new();
845        let mut context = ActionContext::new(&metrics, &actions, &no_data, None);
846        context.process();
847
848        // Ensure Alert caches correctly
849        if let Action::Alert(warning) = actions.get("file").unwrap().get("true_warning").unwrap() {
850            assert_eq!(*warning.trigger.cached_value.borrow(), Some(MetricValue::Bool(true)));
851        } else {
852            unreachable!("'true_warning' must be an Action::Alert")
853        }
854
855        // Ensure Gauge caches correctly
856        if let Action::Gauge(gauge) = actions.get("file").unwrap().get("false_gauge").unwrap() {
857            assert_eq!(*gauge.value.cached_value.borrow(), Some(MetricValue::Bool(false)));
858        } else {
859            unreachable!("'false_gauge' must be an Action::Gauge")
860        }
861
862        // Ensure Snapshot caches correctly
863        if let Action::Snapshot(snapshot) =
864            actions.get("file").unwrap().get("true_snapshot").unwrap()
865        {
866            assert_eq!(*snapshot.trigger.cached_value.borrow(), Some(MetricValue::Bool(true)));
867            assert_eq!(*snapshot.repeat.cached_value.borrow(), Some(MetricValue::Int(5)));
868        } else {
869            unreachable!("'true_snapshot' must be an Action::Snapshot")
870        }
871
872        // Ensure value-calculation does not fail for a Snapshot with an empty cache.
873        // The cached value for 'repeat' is expected to be pre-calculated during deserialization
874        // however, an empty cached value should still be supported.
875        if let Action::Snapshot(snapshot) =
876            actions.get("file").unwrap().get("test_snapshot").unwrap()
877        {
878            assert_eq!(*snapshot.trigger.cached_value.borrow(), Some(MetricValue::Bool(true)));
879            assert_eq!(*snapshot.repeat.cached_value.borrow(), Some(MetricValue::Int(5)));
880        } else {
881            unreachable!("'true_snapshot' must be an Action::Snapshot")
882        }
883    }
884}