blob: 3a69fcf93616b9db643e90e94e4995a4236c0ffb [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
pub(crate) mod arithmetic;
pub(crate) mod fetch;
pub(crate) mod metric_value;
pub(crate) mod parse;
pub(crate) mod variable;
use {
fetch::{Fetcher, FileDataFetcher, SelectorString, TrialDataFetcher},
metric_value::MetricValue,
serde::{Deserialize, Deserializer},
std::{clone::Clone, cmp::min, collections::HashMap, convert::TryFrom},
variable::VariableName,
};
/// The contents of a single Metric. Metrics produce a value for use in Actions or other Metrics.
#[derive(Clone, Debug)]
pub enum Metric {
/// Selector tells where to find a value in the Inspect data.
// Note: This can't be a fidl_fuchsia_diagnostics::Selector because it's not deserializable or
// cloneable.
Selector(SelectorString),
/// Eval contains an arithmetic expression,
// TODO(cphoenix): Parse and validate this at load-time.
Eval(String),
}
impl<'de> Deserialize<'de> for Metric {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
if SelectorString::is_selector(&value) {
Ok(Metric::Selector(SelectorString::try_from(value).map_err(serde::de::Error::custom)?))
} else {
Ok(Metric::Eval(value))
}
}
}
impl std::fmt::Display for Metric {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Metric::Selector(s) => write!(f, "{:?}", s),
Metric::Eval(s) => write!(f, "{}", s),
}
}
}
/// [Metrics] are a map from namespaces to the named [Metric]s stored within that namespace.
pub type Metrics = HashMap<String, HashMap<String, Metric>>;
/// Contains all the information needed to look up and evaluate a Metric - other
/// [Metric]s that may be referred to, and a source of input values to calculate on.
///
/// Note: MetricState uses a single Now() value for all evaluations. If a MetricState is
/// retained and used for multiple evaluations at different times, provide a way to update
/// the `now` field.
pub struct MetricState<'a> {
pub metrics: &'a Metrics,
pub fetcher: Fetcher<'a>,
now: Option<i64>,
}
#[derive(Deserialize, Debug, Clone, PartialEq)]
pub enum MathFunction {
Add,
Sub,
Mul,
FloatDiv,
IntDiv,
Greater,
Less,
GreaterEq,
LessEq,
Max,
Min,
}
#[derive(Deserialize, Debug, Clone, PartialEq)]
pub enum Function {
Math(MathFunction),
// Equals and NotEq can apply to bools and strings, and handle int/float without needing
// the mechanisms in mod arithmetic.
Equals,
NotEq,
And,
Or,
Not,
KlogHas,
SyslogHas,
BootlogHas,
Missing,
Annotation,
Lambda,
Apply,
Map,
Fold,
Filter,
Count,
Nanos,
Micros,
Millis,
Seconds,
Minutes,
Hours,
Days,
Now,
OptionF,
}
/// Lambda stores a function; its parameters and body are evaluated lazily.
/// Lambda's are created by evaluating the "Fn()" expression.
#[derive(Deserialize, Debug, Clone)]
pub struct Lambda {
parameters: Vec<String>,
body: Expression,
}
impl Lambda {
fn valid_parameters(parameters: &Expression) -> Result<Vec<String>, MetricValue> {
match parameters {
Expression::Vector(parameters) => parameters
.iter()
.map(|param| match param {
Expression::Variable(name) => {
if name.includes_namespace() {
Err(MetricValue::Missing(
"Namespaces not allowed in function params".to_string(),
))
} else {
Ok(name.original_name().to_string())
}
}
_ => Err(MetricValue::Missing(
"Function params must be valid identifier names".to_string(),
)),
})
.collect::<Result<Vec<_>, _>>(),
_ => {
return Err(MetricValue::Missing(
"Function params must be a vector of names".to_string(),
))
}
}
}
fn as_metric_value(definition: &Vec<Expression>) -> MetricValue {
if definition.len() != 2 {
return MetricValue::Missing(
"Function needs two parameters, list of params and expression".to_string(),
);
}
let parameters = match Self::valid_parameters(&definition[0]) {
Ok(names) => names,
Err(problem) => return problem,
};
let body = definition[1].clone();
MetricValue::Lambda(Box::new(Lambda { parameters, body }))
}
}
// Behavior for short circuiting execution when applying operands.
#[derive(Copy, Clone, Debug)]
enum ShortCircuitBehavior {
// Short circuit when the first true value is found.
True,
// Short circuit when the first false value is found.
False,
}
/// Expression represents the parsed body of an Eval Metric. It applies
/// a function to sub-expressions, or stores a Missing error, the name of a
/// Metric, a vector of expressions, or a basic Value.
#[derive(Deserialize, Debug, Clone, PartialEq)]
pub enum Expression {
// Some operators have arity 1 or 2, some have arity N.
// For symmetry/readability, I use the same operand-spec Vec<Expression> for all.
// TODO(cphoenix): Check on load that all operators have a legal number of operands.
Function(Function, Vec<Expression>),
Vector(Vec<Expression>),
Variable(VariableName),
Value(MetricValue),
}
// Selectors return a vec of values. Typically they will select a single
// value which we want to use for math without lots of boilerplate.
// So we "promote" a 1-entry vector into the value it contains.
// Other vectors will be passed through unchanged (and cause an error later).
fn unwrap_for_math<'b>(value: &'b MetricValue) -> &'b MetricValue {
match value {
MetricValue::Vector(v) if v.len() == 1 => &v[0],
v => v,
}
}
// Condense the visual clutter of iter() and collect::<Vec<_>>().
fn map_vec<T, U, F>(vec: &Vec<T>, f: F) -> Vec<U>
where
F: FnMut(&T) -> U,
{
vec.iter().map(f).collect::<Vec<U>>()
}
// Condense visual clutter of iterating over vec of references.
fn map_vec_r<'a, T, U, F>(vec: &'a [T], f: F) -> Vec<&'a U>
where
F: FnMut(&'a T) -> &'a U,
{
vec.iter().map(f).collect::<Vec<&'a U>>()
}
// Construct Missing() metric from a message
fn missing(message: &str) -> MetricValue {
MetricValue::Missing(message.to_string())
}
pub fn safe_float_to_int(float: f64) -> Option<i64> {
if !float.is_finite() {
return None;
}
if float > i64::MAX as f64 {
return Some(i64::MAX);
}
if float < i64::MIN as f64 {
return Some(i64::MIN);
}
Some(float as i64)
}
impl<'a> MetricState<'a> {
/// Create an initialized MetricState.
pub fn new(metrics: &'a Metrics, fetcher: Fetcher<'a>, now: Option<i64>) -> MetricState<'a> {
MetricState { metrics, fetcher, now }
}
/// Any [name] found in the trial's "values" uses the corresponding value, regardless of
/// whether it is a Selector or Eval Metric, and regardless of whether it includes
/// a namespace; the string match must be exact.
/// If not found in "values" the name must be an Eval metric from the current file.
fn metric_value_for_trial(
&self,
fetcher: &TrialDataFetcher<'_>,
namespace: &str,
variable: &VariableName,
) -> MetricValue {
let name = variable.original_name();
if fetcher.has_entry(name) {
return fetcher.fetch(name);
}
if variable.includes_namespace() {
return MetricValue::Missing(format!(
"Name {} not in test values and refers outside the file",
name
));
}
match self.metrics.get(namespace) {
None => return MetricValue::Missing(format!("BUG! Bad namespace '{}'", namespace)),
Some(metric_map) => match metric_map.get(name) {
None => {
return MetricValue::Missing(format!(
"Metric '{}' Not Found in '{}'",
name, namespace
))
}
Some(metric) => match metric {
Metric::Selector(_) => MetricValue::Missing(format!(
"Selector {} can't be used in tests; please supply a value",
name
)),
Metric::Eval(expression) => self.evaluate_value(namespace, &expression),
},
},
}
}
/// If [name] is of the form "namespace::name" then [namespace] is ignored.
/// If [name] is just "name" then [namespace] is used to look up the Metric.
fn metric_value_for_file(
&self,
fetcher: &FileDataFetcher<'_>,
namespace: &str,
name: &VariableName,
) -> MetricValue {
if let Some((real_namespace, real_name)) = name.name_parts(namespace) {
match self.metrics.get(real_namespace) {
None => return MetricValue::Missing(format!("Bad namespace '{}'", real_namespace)),
Some(metric_map) => match metric_map.get(real_name) {
None => {
return MetricValue::Missing(format!(
"Metric '{}' Not Found in '{}'",
real_name, real_namespace
))
}
Some(metric) => match metric {
Metric::Selector(selector) => fetcher.fetch(selector),
Metric::Eval(expression) => {
self.evaluate_value(real_namespace, &expression)
}
},
},
}
} else {
return MetricValue::Missing(format!("Bad name '{}'", name.original_name()));
}
}
/// Calculate the value of a Metric specified by name and namespace.
fn evaluate_variable(&self, namespace: &str, name: &VariableName) -> MetricValue {
// TODO(cphoenix): When historical metrics are added, change semantics to refresh()
// TODO(cphoenix): cache values
// TODO(cphoenix): Detect infinite cycles/depth.
// TODO(cphoenix): Improve the data structure on Metric names. Probably fill in
// namespace during parse.
match &self.fetcher {
Fetcher::FileData(fetcher) => self.metric_value_for_file(fetcher, namespace, name),
Fetcher::TrialData(fetcher) => self.metric_value_for_trial(fetcher, namespace, name),
}
}
/// Fetch or compute the value of a Metric expression from an action.
pub fn eval_action_metric(&self, namespace: &str, metric: &Metric) -> MetricValue {
match metric {
Metric::Selector(_) => {
MetricValue::Missing("Selectors aren't allowed in action triggers".to_owned())
}
Metric::Eval(string) => {
unwrap_for_math(&self.evaluate_value(namespace, string)).clone()
}
}
}
fn evaluate_value(&self, namespace: &str, expression: &str) -> MetricValue {
match parse::parse_expression(expression) {
Ok(expr) => self.evaluate(namespace, &expr),
Err(e) => MetricValue::Missing(format!("Expression parse error\n{}", e)),
}
}
/// Evaluate an Expression which contains only base values, not referring to other Metrics.
pub fn evaluate_math(expr: &str) -> MetricValue {
let parsed = match parse::parse_expression(expr) {
Ok(p) => p,
Err(err) => {
return MetricValue::Missing(format!("Failed to parse '{}': {}", expr, err))
}
};
let values = HashMap::new();
let fetcher = Fetcher::TrialData(TrialDataFetcher::new(&values));
let files = HashMap::new();
let metric_state = MetricState::new(&files, fetcher, None);
metric_state.evaluate(&"".to_string(), &parsed)
}
#[cfg(test)]
pub fn evaluate_expression(&self, e: &Expression) -> MetricValue {
self.evaluate(&"".to_string(), e)
}
fn evaluate_function(
&self,
namespace: &str,
function: &Function,
operands: &Vec<Expression>,
) -> MetricValue {
match function {
Function::Math(operation) => arithmetic::calculate(
operation,
&map_vec(operands, |o| self.evaluate(namespace, o)),
),
Function::Equals => self.apply_boolean_function(namespace, &|a, b| a == b, operands),
Function::NotEq => self.apply_boolean_function(namespace, &|a, b| a != b, operands),
Function::And => {
self.fold_bool(namespace, &|a, b| a && b, operands, ShortCircuitBehavior::False)
}
Function::Or => {
self.fold_bool(namespace, &|a, b| a || b, operands, ShortCircuitBehavior::True)
}
Function::Not => self.not_bool(namespace, operands),
Function::KlogHas | Function::SyslogHas | Function::BootlogHas => {
self.log_contains(function, namespace, operands)
}
Function::Missing => self.is_missing(namespace, operands),
Function::Annotation => self.annotation(namespace, operands),
Function::Lambda => Lambda::as_metric_value(operands),
Function::Apply => self.apply(namespace, operands),
Function::Map => self.map(namespace, operands),
Function::Fold => self.fold(namespace, operands),
Function::Filter => self.filter(namespace, operands),
Function::Count => self.count(namespace, operands),
Function::Nanos => self.time(namespace, operands, 1),
Function::Micros => self.time(namespace, operands, 1_000),
Function::Millis => self.time(namespace, operands, 1_000_000),
Function::Seconds => self.time(namespace, operands, 1_000_000_000),
Function::Minutes => self.time(namespace, operands, 1_000_000_000 * 60),
Function::Hours => self.time(namespace, operands, 1_000_000_000 * 60 * 60),
Function::Days => self.time(namespace, operands, 1_000_000_000 * 60 * 60 * 24),
Function::Now => self.now(operands),
Function::OptionF => self.option(namespace, operands),
}
}
fn option(&self, namespace: &str, operands: &Vec<Expression>) -> MetricValue {
let mut found_empty = false;
for op in operands.iter() {
match self.evaluate(namespace, op) {
MetricValue::Missing(_) => {}
MetricValue::Vector(v) if v.len() == 0 => found_empty = true,
value => return value,
}
}
if found_empty {
return MetricValue::Vector(vec![]);
}
// This will be improved when we get structured output and structured errors.
return missing("Every value was missing");
}
fn now(&self, operands: &'a [Expression]) -> MetricValue {
if !operands.is_empty() {
return missing("Now() requires no operands.");
}
match self.now {
Some(time) => MetricValue::Int(time),
None => missing("No valid time available"),
}
}
fn apply_lambda(&self, namespace: &str, lambda: &Lambda, args: &[&MetricValue]) -> MetricValue {
fn substitute_all(
expressions: &[Expression],
bindings: &HashMap<String, &MetricValue>,
) -> Vec<Expression> {
expressions.iter().map(|e| substitute(e, bindings)).collect::<Vec<_>>()
}
fn substitute(
expression: &Expression,
bindings: &HashMap<String, &MetricValue>,
) -> Expression {
match expression {
Expression::Function(function, expressions) => {
Expression::Function(function.clone(), substitute_all(expressions, bindings))
}
Expression::Vector(expressions) => {
Expression::Vector(substitute_all(expressions, bindings))
}
Expression::Variable(name) => {
if let Some(value) = bindings.get(name.original_name()) {
Expression::Value((*value).clone())
} else {
Expression::Variable(name.clone())
}
}
Expression::Value(value) => Expression::Value(value.clone()),
}
}
let parameters = &lambda.parameters;
if parameters.len() != args.len() {
return MetricValue::Missing(format!(
"Function has {} parameters and needs {} arguments, but has {}.",
parameters.len(),
parameters.len(),
args.len()
));
}
let mut bindings = HashMap::new();
for (name, value) in parameters.iter().zip(args.iter()) {
bindings.insert(name.clone(), value.clone());
}
let expression = substitute(&lambda.body, &bindings);
self.evaluate(namespace, &expression)
}
fn unpack_lambda(
&self,
namespace: &str,
operands: &'a [Expression],
) -> Result<(Box<Lambda>, Vec<MetricValue>), ()> {
if operands.len() == 0 {
return Err(());
}
let lambda = match self.evaluate(namespace, &operands[0]) {
MetricValue::Lambda(lambda) => lambda,
_ => return Err(()),
};
let arguments =
operands[1..].iter().map(|expr| self.evaluate(namespace, expr)).collect::<Vec<_>>();
Ok((lambda, arguments))
}
/// This implements the Apply() function.
fn apply(&self, namespace: &str, operands: &[Expression]) -> MetricValue {
let (lambda, arguments) = match self.unpack_lambda(namespace, operands) {
Ok((lambda, arguments)) => (lambda, arguments),
Err(()) => return missing("Apply needs a function in its first argument."),
};
self.apply_lambda(namespace, &lambda, &arguments.iter().collect::<Vec<_>>())
}
/// This implements the Map() function.
fn map(&self, namespace: &str, operands: &[Expression]) -> MetricValue {
let (lambda, arguments) = match self.unpack_lambda(namespace, operands) {
Ok((lambda, arguments)) => (lambda, arguments),
Err(()) => return missing("Map needs a function in its first argument."),
};
let vector_args = arguments
.iter()
.filter(|item| match item {
MetricValue::Vector(_) => true,
_ => false,
})
.collect::<Vec<_>>();
let result_length = match vector_args.len() {
0 => 0,
_ => {
let start = match vector_args[0] {
MetricValue::Vector(vec) => vec.len(),
_ => unreachable!(),
};
vector_args.iter().fold(start, |accum, item| {
min(
accum,
match item {
MetricValue::Vector(items) => items.len(),
_ => 0,
},
)
})
}
};
let mut result = Vec::new();
for index in 0..result_length {
let call_args = arguments
.iter()
.map(|arg| match arg {
MetricValue::Vector(vec) => &vec[index],
other => other,
})
.collect::<Vec<_>>();
result.push(self.apply_lambda(namespace, &lambda, &call_args));
}
MetricValue::Vector(result)
}
/// This implements the Fold() function.
fn fold(&self, namespace: &str, operands: &[Expression]) -> MetricValue {
let (lambda, arguments) = match self.unpack_lambda(namespace, operands) {
Ok((lambda, arguments)) => (lambda, arguments),
Err(()) => return missing("Fold needs a function as the first argument."),
};
if arguments.is_empty() {
return missing("Fold needs a second argument, a vector");
}
let vector = match &arguments[0] {
MetricValue::Vector(items) => items,
_ => return missing("Second argument of Fold must be a vector"),
};
let (first, rest) = match arguments.len() {
1 => match vector.split_first() {
Some(first_rest) => first_rest,
None => return missing("Fold needs at least one value"),
},
2 => (&arguments[1], &vector[..]),
_ => return missing("Fold needs (function, vec) or (function, vec, start)"),
};
let mut result = first.clone();
for item in rest {
result = self.apply_lambda(namespace, &lambda, &[&result, item]);
}
result
}
/// This implements the Filter() function.
fn filter(&self, namespace: &str, operands: &[Expression]) -> MetricValue {
let (lambda, arguments) = match self.unpack_lambda(namespace, operands) {
Ok((lambda, arguments)) => (lambda, arguments),
Err(()) => return missing("Filter needs a function"),
};
if arguments.len() != 1 {
return missing("Filter needs (function, vector)");
}
let result = match &arguments[0] {
MetricValue::Vector(items) => items
.iter()
.filter_map(|item| match self.apply_lambda(namespace, &lambda, &[&item]) {
MetricValue::Bool(true) => Some(item.clone()),
MetricValue::Bool(false) => None,
MetricValue::Missing(message) => Some(MetricValue::Missing(message.clone())),
bad_type => Some(MetricValue::Missing(format!(
"Bad value {:?} from filter function should be true, false, or Missing",
bad_type
))),
})
.collect(),
_ => return missing("Filter second argument must be a vector"),
};
MetricValue::Vector(result)
}
/// This implements the Count() function.
fn count(&self, namespace: &str, operands: &[Expression]) -> MetricValue {
if operands.len() != 1 {
return missing("Count requires one argument, a vector");
}
match self.evaluate(namespace, &operands[0]) {
MetricValue::Vector(items) => MetricValue::Int(items.len() as i64),
bad => MetricValue::Missing(format!("Count only works on vectors, not {}", bad)),
}
}
/// This implements the time-conversion functions.
fn time(&self, namespace: &str, operands: &[Expression], multiplier: i64) -> MetricValue {
if operands.len() != 1 {
return missing("Time conversion needs 1 numeric argument");
}
match self.evaluate(namespace, &operands[0]) {
MetricValue::Int(value) => MetricValue::Int(value * multiplier),
MetricValue::Float(value) => match safe_float_to_int(value * (multiplier as f64)) {
None => missing("Time conversion needs 1 numeric argument"),
Some(value) => MetricValue::Int(value),
},
_ => missing("Time conversion needs 1 numeric argument"),
}
}
fn evaluate(&self, namespace: &str, e: &Expression) -> MetricValue {
match e {
Expression::Function(f, operands) => self.evaluate_function(namespace, f, operands),
Expression::Variable(name) => self.evaluate_variable(namespace, name),
Expression::Value(value) => value.clone(),
Expression::Vector(values) => {
MetricValue::Vector(map_vec(values, |value| self.evaluate(namespace, value)))
}
}
}
fn annotation(&self, namespace: &str, operands: &Vec<Expression>) -> MetricValue {
if operands.len() != 1 {
return MetricValue::Missing("Annotation() needs 1 string argument".to_string());
}
match self.evaluate(namespace, &operands[0]) {
MetricValue::String(string) => match &self.fetcher {
Fetcher::TrialData(fetcher) => fetcher.annotations,
Fetcher::FileData(fetcher) => fetcher.annotations,
}
.fetch(&string),
_ => MetricValue::Missing("Annotation() needs a string argument".to_string()),
}
}
fn log_contains(
&self,
log_type: &Function,
namespace: &str,
operands: &Vec<Expression>,
) -> MetricValue {
let log_data = match &self.fetcher {
Fetcher::TrialData(fetcher) => match log_type {
Function::KlogHas => fetcher.klog,
Function::SyslogHas => fetcher.syslog,
Function::BootlogHas => fetcher.bootlog,
_ => {
return MetricValue::Missing(
"Internal error, log_contains with non-log function".to_string(),
)
}
},
Fetcher::FileData(fetcher) => match log_type {
Function::KlogHas => fetcher.klog,
Function::SyslogHas => fetcher.syslog,
Function::BootlogHas => fetcher.bootlog,
_ => {
return MetricValue::Missing(
"Internal error, log_contains with non-log function".to_string(),
)
}
},
};
if operands.len() != 1 {
return MetricValue::Missing(
"Log matcher must use exactly 1 argument, an RE string.".into(),
);
}
match self.evaluate(namespace, &operands[0]) {
MetricValue::String(re) => MetricValue::Bool(log_data.contains(&re)),
_ => MetricValue::Missing("Log matcher needs a string (RE).".into()),
}
}
fn apply_boolean_function(
&self,
namespace: &str,
function: &dyn (Fn(&MetricValue, &MetricValue) -> bool),
operands: &Vec<Expression>,
) -> MetricValue {
if operands.len() != 2 {
return MetricValue::Missing(format!(
"Bad arg list {:?} for binary operator",
operands
));
}
let operand_values = map_vec(&operands, |operand| self.evaluate(namespace, operand));
let args = map_vec_r(&operand_values, |operand| unwrap_for_math(operand));
match (args[0], args[1]) {
// We forward ::Missing for better error messaging.
(MetricValue::Missing(reason), _) => MetricValue::Missing(reason.to_string()),
(_, MetricValue::Missing(reason)) => MetricValue::Missing(reason.to_string()),
_ => MetricValue::Bool(function(args[0], args[1])),
}
}
fn fold_bool(
&self,
namespace: &str,
function: &dyn (Fn(bool, bool) -> bool),
operands: &Vec<Expression>,
short_circuit_behavior: ShortCircuitBehavior,
) -> MetricValue {
if operands.len() == 0 {
return MetricValue::Missing("No operands in boolean expression".into());
}
let first = self.evaluate(namespace, &operands[0]);
let mut result: bool = match unwrap_for_math(&first) {
MetricValue::Bool(value) => *value,
MetricValue::Missing(reason) => {
return MetricValue::Missing(reason.to_string());
}
bad => return MetricValue::Missing(format!("{:?} is not boolean", bad)),
};
for operand in operands[1..].iter() {
match (result, short_circuit_behavior) {
(true, ShortCircuitBehavior::True) => {
break;
}
(false, ShortCircuitBehavior::False) => {
break;
}
_ => {}
};
let nth = self.evaluate(namespace, operand);
result = match unwrap_for_math(&nth) {
MetricValue::Bool(value) => function(result, *value),
MetricValue::Missing(reason) => {
return MetricValue::Missing(reason.to_string());
}
bad => return MetricValue::Missing(format!("{:?} is not boolean", bad)),
}
}
MetricValue::Bool(result)
}
fn not_bool(&self, namespace: &str, operands: &Vec<Expression>) -> MetricValue {
if operands.len() != 1 {
return MetricValue::Missing(format!(
"Wrong number of arguments ({}) for unary bool operator",
operands.len()
));
}
match unwrap_for_math(&self.evaluate(namespace, &operands[0])) {
MetricValue::Bool(true) => MetricValue::Bool(false),
MetricValue::Bool(false) => MetricValue::Bool(true),
MetricValue::Missing(reason) => {
return MetricValue::Missing(reason.to_string());
}
bad => return MetricValue::Missing(format!("{:?} not boolean", bad)),
}
}
// Returns Bool true if the given metric is Missing, false if the metric has a value.
fn is_missing(&self, namespace: &str, operands: &Vec<Expression>) -> MetricValue {
if operands.len() != 1 {
return MetricValue::Missing(format!("Bad operand"));
}
MetricValue::Bool(match self.evaluate(namespace, &operands[0]) {
MetricValue::Missing(_) => true,
// TODO(fxbug.dev/58922): Well-designed errors and special cases, not hacks
MetricValue::Vector(contents) if contents.len() == 0 => true,
MetricValue::Vector(contents) if contents.len() == 1 => match contents[0] {
MetricValue::Missing(_) => true,
_ => false,
},
_ => false,
})
}
}
// The evaluation of math expressions is tested pretty exhaustively in parse.rs unit tests.
// The use of metric names in expressions and actions, with and without namespaces, is tested in
// the integration test.
// $ fx test triage_lib_test
#[cfg(test)]
pub(crate) mod test {
use super::*;
use {
crate::config::{DiagnosticData, Source},
anyhow::Error,
lazy_static::lazy_static,
};
/// Missing should never equal anything, even an identical Missing. Code (tests) can use
/// assert_missing!(MetricValue::Missing("foo".to_string()), "foo") to test error messages.
#[macro_export]
macro_rules! assert_missing {
($missing:expr, $message:expr) => {
match $missing {
MetricValue::Missing(actual_message) => assert_eq!(&actual_message, $message),
_ => assert!(false, "Non-Missing type"),
}
};
}
lazy_static! {
static ref EMPTY_F: Vec<DiagnosticData> = {
let s = r#"[]"#;
vec![DiagnosticData::new("i".to_string(), Source::Inspect, s.to_string()).unwrap()]
};
static ref NO_PAYLOAD_F: Vec<DiagnosticData> = {
let s = r#"[{"moniker": "abcd", "payload": null}]"#;
vec![DiagnosticData::new("i".to_string(), Source::Inspect, s.to_string()).unwrap()]
};
static ref EMPTY_FILE_FETCHER: FileDataFetcher<'static> = FileDataFetcher::new(&EMPTY_F);
static ref NO_PAYLOAD_FETCHER: FileDataFetcher<'static> =
FileDataFetcher::new(&NO_PAYLOAD_F);
}
#[test]
fn logs_work() -> Result<(), Error> {
let syslog_text = "line 1\nline 2\nsyslog".to_string();
let klog_text = "first line\nsecond line\nklog\n".to_string();
let bootlog_text = "Yes there's a bootlog with one long line".to_string();
let syslog = DiagnosticData::new("sys".to_string(), Source::Syslog, syslog_text)?;
let klog = DiagnosticData::new("k".to_string(), Source::Klog, klog_text)?;
let bootlog = DiagnosticData::new("boot".to_string(), Source::Bootlog, bootlog_text)?;
let metrics = HashMap::new();
let mut data = vec![klog, syslog, bootlog];
let fetcher = FileDataFetcher::new(&data);
let state = MetricState::new(&metrics, Fetcher::FileData(fetcher), None);
assert_eq!(state.evaluate_value("", r#"KlogHas("lin")"#), MetricValue::Bool(true));
assert_eq!(state.evaluate_value("", r#"KlogHas("l.ne")"#), MetricValue::Bool(true));
assert_eq!(state.evaluate_value("", r#"KlogHas("fi.*ne")"#), MetricValue::Bool(true));
assert_eq!(state.evaluate_value("", r#"KlogHas("fi.*sec")"#), MetricValue::Bool(false));
assert_eq!(state.evaluate_value("", r#"KlogHas("first line")"#), MetricValue::Bool(true));
// Full regex; even capture groups are allowed but the values can't be extracted.
assert_eq!(
state.evaluate_value("", r#"KlogHas("f(.)rst \bline")"#),
MetricValue::Bool(true)
);
// Backreferences don't work; this is regex, not fancy_regex.
assert_eq!(
state.evaluate_value("", r#"KlogHas("f(.)rst \bl\1ne")"#),
MetricValue::Bool(false)
);
assert_eq!(state.evaluate_value("", r#"KlogHas("second line")"#), MetricValue::Bool(true));
assert_eq!(
state.evaluate_value("", "KlogHas(\"second line\n\")"),
MetricValue::Bool(false)
);
assert_eq!(state.evaluate_value("", r#"KlogHas("klog")"#), MetricValue::Bool(true));
assert_eq!(state.evaluate_value("", r#"KlogHas("line 2")"#), MetricValue::Bool(false));
assert_eq!(state.evaluate_value("", r#"SyslogHas("line 2")"#), MetricValue::Bool(true));
assert_eq!(state.evaluate_value("", r#"SyslogHas("syslog")"#), MetricValue::Bool(true));
assert_eq!(state.evaluate_value("", r#"BootlogHas("bootlog")"#), MetricValue::Bool(true));
assert_eq!(state.evaluate_value("", r#"BootlogHas("syslog")"#), MetricValue::Bool(false));
data.pop();
let fetcher = FileDataFetcher::new(&data);
let state = MetricState::new(&metrics, Fetcher::FileData(fetcher), None);
assert_eq!(state.evaluate_value("", r#"SyslogHas("syslog")"#), MetricValue::Bool(true));
assert_eq!(state.evaluate_value("", r#"BootlogHas("bootlog")"#), MetricValue::Bool(false));
assert_eq!(state.evaluate_value("", r#"BootlogHas("syslog")"#), MetricValue::Bool(false));
Ok(())
}
#[test]
fn annotations_work() -> Result<(), Error> {
let annotation_text = r#"{ "build.board": "chromebook-x64", "answer": 42 }"#.to_string();
let annotations =
DiagnosticData::new("a".to_string(), Source::Annotations, annotation_text)?;
let metrics = HashMap::new();
let data = vec![annotations];
let fetcher = FileDataFetcher::new(&data);
let state = MetricState::new(&metrics, Fetcher::FileData(fetcher), None);
assert_eq!(
state.evaluate_value("", "Annotation('build.board')"),
MetricValue::String("chromebook-x64".to_string())
);
assert_eq!(state.evaluate_value("", "Annotation('answer')"), MetricValue::Int(42));
assert_missing!(
state.evaluate_value("", "Annotation('bogus')"),
"Key 'bogus' not found in annotations"
);
assert_missing!(
state.evaluate_value("", "Annotation('bogus', 'Double bogus')"),
"Annotation() needs 1 string argument"
);
assert_missing!(
state.evaluate_value("", "Annotation(42)"),
"Annotation() needs a string argument"
);
Ok(())
}
#[test]
fn test_fetch_errors() {
assert_eq!(1, NO_PAYLOAD_FETCHER.errors().len());
}
// Correct operation of the klog, syslog, and bootlog fields of TrialDataFetcher are tested
// in the integration test via log_tests.triage.
// Test evaluation on static values.
#[test]
fn test_evaluation() {
let metrics: Metrics = [(
"root".to_string(),
[
("is42".to_string(), Metric::Eval("42".to_string())),
("isOk".to_string(), Metric::Eval("'OK'".to_string())),
]
.iter()
.cloned()
.collect(),
)]
.iter()
.cloned()
.collect();
let state = MetricState::new(&metrics, Fetcher::FileData(EMPTY_FILE_FETCHER.clone()), None);
// Can read a value.
assert_eq!(state.evaluate_value("root", "is42"), MetricValue::Int(42));
// Basic arithmetic
assert_eq!(state.evaluate_value("root", "is42 + 1"), MetricValue::Int(43));
assert_eq!(state.evaluate_value("root", "is42 - 1"), MetricValue::Int(41));
assert_eq!(state.evaluate_value("root", "is42 * 2"), MetricValue::Int(84));
// Automatic float conversion and truncating divide.
assert_eq!(state.evaluate_value("root", "is42 / 4"), MetricValue::Float(10.5));
assert_eq!(state.evaluate_value("root", "is42 // 4"), MetricValue::Int(10));
// Order of operations
assert_eq!(
state.evaluate_value("root", "is42 + 10 / 2 * 10 - 2 "),
MetricValue::Float(90.0)
);
assert_eq!(state.evaluate_value("root", "is42 + 10 // 2 * 10 - 2 "), MetricValue::Int(90));
// Boolean
assert_eq!(
state.evaluate_value("root", "And(is42 == 42, is42 < 100)"),
MetricValue::Bool(true)
);
assert_eq!(
state.evaluate_value("root", "And(is42 == 42, is42 > 100)"),
MetricValue::Bool(false)
);
assert_eq!(
state.evaluate_value("root", "Or(is42 == 42, is42 > 100)"),
MetricValue::Bool(true)
);
assert_eq!(
state.evaluate_value("root", "Or(is42 != 42, is42 < 100)"),
MetricValue::Bool(true)
);
assert_eq!(
state.evaluate_value("root", "Or(is42 != 42, is42 > 100)"),
MetricValue::Bool(false)
);
assert_eq!(state.evaluate_value("root", "Not(is42 == 42)"), MetricValue::Bool(false));
// Read strings
assert_eq!(state.evaluate_value("root", "isOk"), MetricValue::String("OK".to_string()));
// Missing value
assert_missing!(
state.evaluate_value("root", "missing"),
"Metric 'missing' Not Found in 'root'"
);
// Booleans short circuit
assert_missing!(
state.evaluate_value("root", "Or(is42 != 42, missing)"),
"Metric 'missing' Not Found in 'root'"
);
assert_eq!(
state.evaluate_value("root", "Or(is42 == 42, missing)"),
MetricValue::Bool(true)
);
assert_missing!(
state.evaluate_value("root", "And(is42 == 42, missing)"),
"Metric 'missing' Not Found in 'root'"
);
assert_eq!(
state.evaluate_value("root", "And(is42 != 42, missing)"),
MetricValue::Bool(false)
);
// Missing checks
assert_eq!(state.evaluate_value("root", "Missing(is42)"), MetricValue::Bool(false));
assert_eq!(state.evaluate_value("root", "Missing(missing)"), MetricValue::Bool(true));
assert_eq!(
state.evaluate_value("root", "And(Not(Missing(is42)), is42 == 42)"),
MetricValue::Bool(true)
);
assert_eq!(
state.evaluate_value("root", "And(Not(Missing(missing)), missing == 'Hello')"),
MetricValue::Bool(false)
);
assert_eq!(
state.evaluate_value("root", "Or(Missing(is42), is42 < 42)"),
MetricValue::Bool(false)
);
assert_eq!(
state.evaluate_value("root", "Or(Missing(missing), missing == 'Hello')"),
MetricValue::Bool(true)
);
// Ensure evaluation for action converts vector values.
assert_eq!(
state.evaluate_value("root", "[0==0]"),
MetricValue::Vector(vec![MetricValue::Bool(true)])
);
assert_eq!(
state.eval_action_metric("root", &Metric::Eval("[0==0]".to_string())),
MetricValue::Bool(true)
);
assert_eq!(
state.evaluate_value("root", "[0==0, 0==0]"),
MetricValue::Vector(vec![MetricValue::Bool(true), MetricValue::Bool(true)])
);
assert_eq!(
state.eval_action_metric("root", &Metric::Eval("[0==0, 0==0]".to_string())),
MetricValue::Vector(vec![MetricValue::Bool(true), MetricValue::Bool(true)])
);
}
// TODO(fxbug.dev/58922): Modify or probably delete this function after better error design.
#[test]
fn test_missing_hacks() -> Result<(), Error> {
macro_rules! eval {
($e:expr) => {
MetricState::evaluate_math($e)
};
}
assert_eq!(eval!("Missing(2>'a')"), MetricValue::Bool(true));
assert_eq!(eval!("Missing([])"), MetricValue::Bool(true));
assert_eq!(eval!("Missing([2>'a'])"), MetricValue::Bool(true));
assert_eq!(eval!("Missing([2>'a', 2>'a'])"), MetricValue::Bool(false));
assert_eq!(eval!("Missing([2>1])"), MetricValue::Bool(false));
assert_eq!(eval!("Or(Missing(2>'a'), 2>'a')"), MetricValue::Bool(true));
Ok(())
}
#[test]
fn test_time() -> Result<(), Error> {
let metrics = Metrics::new();
let files = vec![];
let state_1234 =
MetricState::new(&metrics, Fetcher::FileData(FileDataFetcher::new(&files)), Some(1234));
let state_missing =
MetricState::new(&metrics, Fetcher::FileData(FileDataFetcher::new(&files)), None);
let now_expression = parse::parse_expression("Now()").unwrap();
assert_missing!(MetricState::evaluate_math("Now()"), "No valid time available");
assert_eq!(state_1234.evaluate_expression(&now_expression), MetricValue::Int(1234));
assert_missing!(
state_missing.evaluate_expression(&now_expression),
"No valid time available"
);
Ok(())
}
// Correct operation of annotations is tested via annotation_tests.triage.
}