pub(crate) mod arithmetic;
pub(crate) mod context;
pub(crate) mod fetch;
pub(crate) mod metric_value;
pub(crate) mod parse;
pub(crate) mod variable;
use fetch::{Fetcher, FileDataFetcher, SelectorString, TrialDataFetcher};
use metric_value::{MetricValue, Problem};
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::cell::RefCell;
use std::clone::Clone;
use std::cmp::min;
use std::collections::{HashMap, HashSet};
use variable::VariableName;
#[derive(Clone, Debug, PartialEq, Serialize)]
pub(crate) enum Metric {
Selector(Vec<SelectorString>),
Eval(ExpressionContext),
#[cfg(test)]
Hardcoded(MetricValue),
}
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),
#[cfg(test)]
Metric::Hardcoded(value) => write!(f, "{:?}", value),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct ValueSource {
pub(crate) metric: Metric,
pub cached_value: RefCell<Option<MetricValue>>,
}
impl ValueSource {
pub(crate) fn new(metric: Metric) -> Self {
Self { metric, cached_value: RefCell::new(None) }
}
pub(crate) fn try_from_expression_with_namespace(
expr: &str,
namespace: &str,
) -> Result<Self, anyhow::Error> {
Ok(ValueSource::new(Metric::Eval(ExpressionContext::try_from_expression_with_namespace(
expr, namespace,
)?)))
}
#[cfg(test)]
pub(crate) fn try_from_expression_with_default_namespace(
expr: &str,
) -> Result<Self, anyhow::Error> {
ValueSource::try_from_expression_with_namespace(expr, "")
}
}
impl std::fmt::Display for ValueSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self.metric)
}
}
pub type Metrics = HashMap<String, HashMap<String, ValueSource>>;
pub struct MetricState<'a> {
pub metrics: &'a Metrics,
pub fetcher: Fetcher<'a>,
now: Option<i64>,
stack: RefCell<HashSet<String>>,
}
#[derive(Deserialize, Debug, Clone, PartialEq, Serialize)]
pub enum MathFunction {
Add,
Sub,
Mul,
FloatDiv,
IntDiv,
FloatDivChecked,
IntDivChecked,
Greater,
Less,
GreaterEq,
LessEq,
Max,
Min,
Abs,
}
#[derive(Deserialize, Debug, Clone, PartialEq, Serialize)]
pub enum Function {
Math(MathFunction),
Equals,
NotEq,
And,
Or,
Not,
KlogHas,
SyslogHas,
BootlogHas,
Missing,
UnhandledType,
Problem,
Annotation,
Lambda,
Apply,
Map,
Fold,
All,
Any,
Filter,
Count,
CountChildren,
CountProperties,
Nanos,
Micros,
Millis,
Seconds,
Minutes,
Hours,
Days,
Now,
OptionF,
StringMatches,
True,
False,
}
#[derive(Deserialize, Debug, Clone, Serialize)]
pub struct Lambda {
parameters: Vec<String>,
body: ExpressionTree,
}
impl Lambda {
fn valid_parameters(parameters: &ExpressionTree) -> Result<Vec<String>, MetricValue> {
match parameters {
ExpressionTree::Vector(parameters) => parameters
.iter()
.map(|param| match param {
ExpressionTree::Variable(name) => {
if name.includes_namespace() {
Err(syntax_error("Namespaces not allowed in function params"))
} else {
Ok(name.original_name().to_string())
}
}
_ => Err(syntax_error("Function params must be valid identifier names")),
})
.collect::<Result<Vec<_>, _>>(),
_ => Err(syntax_error("Function params must be a vector of names")),
}
}
fn as_metric_value(definition: &[ExpressionTree]) -> MetricValue {
if definition.len() != 2 {
return syntax_error("Function needs two parameters, list of params and expression");
}
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 }))
}
}
#[derive(Copy, Clone, Debug)]
enum ShortCircuitBehavior {
True,
False,
}
struct MapFoldBoolArgs<'a> {
pub namespace: &'a str,
pub operands: &'a [ExpressionTree],
pub function_name: &'a str,
pub default_when_empty: bool,
pub function: &'a dyn Fn(bool, bool) -> bool,
pub short_circuit_behavior: ShortCircuitBehavior,
}
#[derive(Deserialize, Debug, Clone, PartialEq, Serialize)]
pub(crate) enum ExpressionTree {
Function(Function, Vec<ExpressionTree>),
Vector(Vec<ExpressionTree>),
Variable(VariableName),
Value(MetricValue),
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub(crate) struct ExpressionContext {
pub(crate) raw_expression: String,
pub(crate) parsed_expression: ExpressionTree,
}
impl ExpressionContext {
pub fn try_from_expression_with_namespace(
raw_expression: &str,
namespace: &str,
) -> Result<Self, anyhow::Error> {
let parsed_expression = parse::parse_expression(raw_expression, namespace)?;
Ok(Self { raw_expression: raw_expression.to_string(), parsed_expression })
}
pub fn try_from_expression_with_default_namespace(
raw_expression: &str,
) -> Result<Self, anyhow::Error> {
ExpressionContext::try_from_expression_with_namespace(
raw_expression,
"",
)
}
}
impl std::fmt::Display for ExpressionContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self.raw_expression)
}
}
fn unwrap_for_math(value: &MetricValue) -> &MetricValue {
match value {
MetricValue::Vector(v) if v.len() == 1 => &v[0],
v => v,
}
}
fn missing(message: impl AsRef<str>) -> MetricValue {
MetricValue::Problem(Problem::Missing(message.as_ref().to_string()))
}
fn syntax_error(message: impl AsRef<str>) -> MetricValue {
MetricValue::Problem(Problem::SyntaxError(message.as_ref().to_string()))
}
fn value_error(message: impl AsRef<str>) -> MetricValue {
MetricValue::Problem(Problem::ValueError(message.as_ref().to_string()))
}
fn internal_bug(message: impl AsRef<str>) -> MetricValue {
MetricValue::Problem(Problem::InternalBug(message.as_ref().to_string()))
}
fn unhandled_type(message: impl AsRef<str>) -> MetricValue {
MetricValue::Problem(Problem::UnhandledType(message.as_ref().to_string()))
}
fn evaluation_error(message: impl AsRef<str>) -> MetricValue {
MetricValue::Problem(Problem::EvaluationError(message.as_ref().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)
}
fn first_usable_value(values: impl Iterator<Item = MetricValue>) -> MetricValue {
let mut found_empty = false;
for value in values {
match value {
MetricValue::Problem(Problem::Missing(_)) => {}
MetricValue::Vector(ref v)
if v.len() == 1 && matches!(v[0], MetricValue::Problem(Problem::Missing(_))) => {}
MetricValue::Vector(v) if v.is_empty() => found_empty = true,
value => return value,
}
}
if found_empty {
return MetricValue::Vector(vec![]);
}
missing("Every value was missing")
}
impl<'a> MetricState<'a> {
pub fn new(metrics: &'a Metrics, fetcher: Fetcher<'a>, now: Option<i64>) -> MetricState<'a> {
MetricState { metrics, fetcher, now, stack: RefCell::new(HashSet::new()) }
}
pub fn evaluate_all_metrics(&self) {
for (namespace, metrics) in self.metrics.iter() {
for (name, _value_source) in metrics.iter() {
self.evaluate_variable(namespace, &VariableName::new(name.clone()));
}
}
}
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 syntax_error(format!(
"Name {} not in test values and refers outside the file",
name
));
}
match self.metrics.get(namespace) {
None => internal_bug(format!("BUG! Bad namespace '{}'", namespace)),
Some(metric_map) => match metric_map.get(name) {
None => syntax_error(format!("Metric '{}' Not Found in '{}'", name, namespace)),
Some(value_source) => {
let resolved_value: MetricValue;
{
let cached_value_cell = value_source.cached_value.borrow();
match &*cached_value_cell {
None => {
resolved_value = match &value_source.metric {
Metric::Selector(_) => missing(format!(
"Selector {} can't be used in tests; please supply a value",
name
)),
Metric::Eval(expression) => {
self.evaluate(namespace, &expression.parsed_expression)
}
#[cfg(test)]
Metric::Hardcoded(value) => value.clone(),
};
}
Some(cached_value) => return cached_value.clone(),
}
}
let mut cached_value_cell = value_source.cached_value.borrow_mut();
*cached_value_cell = Some(resolved_value.clone());
resolved_value
}
},
}
}
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 => syntax_error(format!("Bad namespace '{}'", real_namespace)),
Some(metric_map) => match metric_map.get(real_name) {
None => syntax_error(format!(
"Metric '{}' Not Found in '{}'",
real_name, real_namespace
)),
Some(value_source) => {
if let Some(cached_value) = value_source.cached_value.borrow().as_ref() {
return cached_value.clone();
}
let resolved_value = match &value_source.metric {
Metric::Selector(selectors) => first_usable_value(
selectors.iter().map(|selector| fetcher.fetch(selector)),
),
Metric::Eval(expression) => {
self.evaluate(real_namespace, &expression.parsed_expression)
}
#[cfg(test)]
Metric::Hardcoded(value) => value.clone(),
};
let mut cached_value_cell = value_source.cached_value.borrow_mut();
*cached_value_cell = Some(resolved_value.clone());
resolved_value
}
},
}
} else {
syntax_error(format!("Bad name '{}'", name.original_name()))
}
}
fn evaluate_variable(&self, namespace: &str, name: &VariableName) -> MetricValue {
if self.stack.borrow().contains(&name.full_name(namespace)) {
let _ = self.stack.replace(HashSet::new());
return evaluation_error(format!(
"Cycle encountered while evaluating variable {} in the expression",
name.name
));
}
self.stack.borrow_mut().insert(name.full_name(namespace));
let value = 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),
};
self.stack.borrow_mut().remove(&name.full_name(namespace));
value
}
pub(crate) fn eval_action_metric(
&self,
namespace: &str,
value_source: &ValueSource,
) -> MetricValue {
if let Some(cached_value) = &*value_source.cached_value.borrow() {
return cached_value.clone();
}
let resolved_value = match &value_source.metric {
Metric::Selector(_) => syntax_error("Selectors aren't allowed in action triggers"),
Metric::Eval(expression) => {
unwrap_for_math(&self.evaluate(namespace, &expression.parsed_expression)).clone()
}
#[cfg(test)]
Metric::Hardcoded(value) => value.clone(),
};
let mut cached_value_cell = value_source.cached_value.borrow_mut();
*cached_value_cell = Some(resolved_value.clone());
resolved_value
}
#[cfg(test)]
fn evaluate_value(&self, namespace: &str, expression: &str) -> MetricValue {
match parse::parse_expression(expression, namespace) {
Ok(expr) => self.evaluate(namespace, &expr),
Err(e) => syntax_error(format!("Expression parse error\n{}", e)),
}
}
pub(crate) fn evaluate_math(expr: &str) -> MetricValue {
let tree = match ExpressionContext::try_from_expression_with_default_namespace(expr) {
Ok(expr_context) => expr_context.parsed_expression,
Err(err) => return syntax_error(format!("Failed to parse '{}': {}", expr, err)),
};
Self::evaluate_const_expression(&tree)
}
pub(crate) fn evaluate_const_expression(tree: &ExpressionTree) -> MetricValue {
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("", tree)
}
#[cfg(test)]
pub(crate) fn evaluate_expression(&self, e: &ExpressionTree) -> MetricValue {
self.evaluate("", e)
}
fn evaluate_function(
&self,
namespace: &str,
function: &Function,
operands: &[ExpressionTree],
) -> MetricValue {
match function {
Function::Math(operation) => arithmetic::calculate(
operation,
&operands.iter().map(|o| self.evaluate(namespace, o)).collect::<Vec<MetricValue>>(),
),
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::UnhandledType => self.is_unhandled_type(namespace, operands),
Function::Problem => self.is_problem(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::All => self.map_fold_bool(MapFoldBoolArgs {
namespace,
operands,
function_name: "All",
default_when_empty: true,
function: &|a, b| a && b,
short_circuit_behavior: ShortCircuitBehavior::False,
}),
Function::Any => self.map_fold_bool(MapFoldBoolArgs {
namespace,
operands,
function_name: "Any",
default_when_empty: false,
function: &|a, b| a || b,
short_circuit_behavior: ShortCircuitBehavior::True,
}),
Function::Filter => self.filter(namespace, operands),
Function::Count | Function::CountProperties => {
self.count_properties(namespace, operands)
}
Function::CountChildren => self.count_children(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),
Function::StringMatches => self.regex(namespace, operands),
Function::True => self.boolean(operands, true),
Function::False => self.boolean(operands, false),
}
}
fn regex(&self, namespace: &str, operands: &[ExpressionTree]) -> MetricValue {
if operands.len() != 2 {
return syntax_error(
"StringMatches(metric, regex) needs one string metric and one string regex",
);
}
let (value, regex) = match (
self.evaluate(namespace, &operands[0]),
self.evaluate(namespace, &operands[1]),
) {
(MetricValue::String(value), MetricValue::String(regex)) => (value, regex),
_ => {
return syntax_error("Arguments to StringMatches must be strings");
}
};
let regex = match Regex::new(®ex) {
Ok(v) => v,
Err(_) => {
return syntax_error(format!("Could not parse `{}` as regex", regex));
}
};
MetricValue::Bool(regex.is_match(&value))
}
fn option(&self, namespace: &str, operands: &[ExpressionTree]) -> MetricValue {
first_usable_value(operands.iter().map(|expression| self.evaluate(namespace, expression)))
}
fn now(&self, operands: &'a [ExpressionTree]) -> MetricValue {
if !operands.is_empty() {
return syntax_error("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: &[ExpressionTree],
bindings: &HashMap<&str, &MetricValue>,
) -> Vec<ExpressionTree> {
expressions.iter().map(|e| substitute(e, bindings)).collect::<Vec<_>>()
}
fn substitute(
expression: &ExpressionTree,
bindings: &HashMap<&str, &MetricValue>,
) -> ExpressionTree {
match expression {
ExpressionTree::Function(function, expressions) => ExpressionTree::Function(
function.clone(),
substitute_all(expressions, bindings),
),
ExpressionTree::Vector(expressions) => {
ExpressionTree::Vector(substitute_all(expressions, bindings))
}
ExpressionTree::Variable(name) => {
let original_name = name.original_name();
if let Some(value) = bindings.get(original_name) {
ExpressionTree::Value((*value).clone())
} else {
ExpressionTree::Variable(name.clone())
}
}
ExpressionTree::Value(value) => ExpressionTree::Value(value.clone()),
}
}
let parameters = &lambda.parameters;
if parameters.len() != args.len() {
return syntax_error(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 as &str, *value);
}
let expression = substitute(&lambda.body, &bindings);
self.evaluate(namespace, &expression)
}
fn unpack_lambda(
&self,
namespace: &str,
operands: &'a [ExpressionTree],
function_name: &str,
) -> Result<(Box<Lambda>, Vec<MetricValue>), MetricValue> {
if operands.is_empty() {
return Err(syntax_error(format!(
"{} needs a function in its first argument",
function_name
)));
}
let lambda = match self.evaluate(namespace, &operands[0]) {
MetricValue::Lambda(lambda) => lambda,
_ => {
return Err(syntax_error(format!(
"{} needs a function in its first argument",
function_name
)))
}
};
let arguments =
operands[1..].iter().map(|expr| self.evaluate(namespace, expr)).collect::<Vec<_>>();
Ok((lambda, arguments))
}
fn apply(&self, namespace: &str, operands: &[ExpressionTree]) -> MetricValue {
let (lambda, arguments) = match self.unpack_lambda(namespace, operands, "Apply") {
Ok((lambda, arguments)) => (lambda, arguments),
Err(problem) => return problem,
};
if arguments.is_empty() {
return syntax_error("Apply needs a second argument (a vector).");
}
if arguments.len() > 1 {
return syntax_error("Apply only accepts one vector argument.");
}
match &arguments[0] {
MetricValue::Vector(apply_args) => {
self.apply_lambda(namespace, &lambda, &apply_args.iter().collect::<Vec<_>>())
}
_ => value_error("Apply only accepts a vector as an argument."),
}
}
fn map(&self, namespace: &str, operands: &[ExpressionTree]) -> MetricValue {
let (lambda, arguments) = match self.unpack_lambda(namespace, operands, "Map") {
Ok((lambda, arguments)) => (lambda, arguments),
Err(problem) => return problem,
};
let vector_args = arguments
.iter()
.filter(|item| matches!(item, MetricValue::Vector(_)))
.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)
}
fn fold(&self, namespace: &str, operands: &[ExpressionTree]) -> MetricValue {
let (lambda, arguments) = match self.unpack_lambda(namespace, operands, "Fold") {
Ok((lambda, arguments)) => (lambda, arguments),
Err(problem) => return problem,
};
if arguments.is_empty() {
return syntax_error("Fold needs a second argument, a vector");
}
let vector = match &arguments[0] {
MetricValue::Vector(items) => items,
_ => return value_error("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 value_error("Fold needs at least one value"),
},
2 => (&arguments[1], &vector[..]),
_ => return syntax_error("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
}
fn map_fold_bool(&self, args: MapFoldBoolArgs<'_>) -> MetricValue {
let (lambda, arguments) =
match self.unpack_lambda(args.namespace, args.operands, args.function_name) {
Ok((lambda, arguments)) => (lambda, arguments),
Err(problem) => return problem,
};
if arguments.len() != 1 {
return syntax_error(format!(
"{} needs two arguments (function, vector)",
args.function_name
));
}
let MetricValue::Vector(v) = &arguments[0] else {
return syntax_error(format!(
"The second argument passed to {} must be a vector",
args.function_name
));
};
if v.is_empty() {
return MetricValue::Bool(args.default_when_empty);
}
let operands: Vec<_> = v
.iter()
.map(|item| {
let metric_value = self.apply_lambda(args.namespace, &lambda, &[item]);
ExpressionTree::Value(metric_value)
})
.collect();
self.fold_bool(args.namespace, args.function, &operands, args.short_circuit_behavior)
}
fn filter(&self, namespace: &str, operands: &[ExpressionTree]) -> MetricValue {
let (lambda, arguments) = match self.unpack_lambda(namespace, operands, "Filter") {
Ok((lambda, arguments)) => (lambda, arguments),
Err(problem) => return problem,
};
if arguments.len() != 1 {
return syntax_error("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::Problem(problem) => Some(MetricValue::Problem(problem)),
bad_type => Some(value_error(format!(
"Bad value {:?} from filter function should be Boolean",
bad_type
))),
})
.collect(),
_ => return syntax_error("Filter second argument must be a vector"),
};
MetricValue::Vector(result)
}
fn count_properties(&self, namespace: &str, operands: &[ExpressionTree]) -> MetricValue {
if operands.len() != 1 {
return syntax_error("CountProperties requires one argument, a vector");
}
match self.evaluate(namespace, &operands[0]) {
MetricValue::Vector(items) => {
let errors = items
.iter()
.filter_map(|item| match item {
MetricValue::Problem(problem) => Some(problem),
_ => None,
})
.collect::<Vec<_>>();
match errors.len() {
0 => MetricValue::Int(
items.iter().filter(|metric| !matches!(metric, MetricValue::Node)).count()
as i64,
),
_ => MetricValue::Problem(Self::important_problem(errors)),
}
}
bad => value_error(format!("CountProperties only works on vectors, not {}", bad)),
}
}
fn count_children(&self, namespace: &str, operands: &[ExpressionTree]) -> MetricValue {
if operands.len() != 1 {
return syntax_error("CountChildren requires one argument, a vector");
}
match self.evaluate(namespace, &operands[0]) {
MetricValue::Vector(items) => {
let errors = items
.iter()
.filter_map(|item| match item {
MetricValue::Problem(problem) => Some(problem),
_ => None,
})
.collect::<Vec<_>>();
match errors.len() {
0 => MetricValue::Int(
items.iter().filter(|metric| matches!(metric, MetricValue::Node)).count()
as i64,
),
_ => MetricValue::Problem(Self::important_problem(errors)),
}
}
bad => value_error(format!("CountChildren only works on vectors, not {}", bad)),
}
}
fn boolean(&self, operands: &[ExpressionTree], value: bool) -> MetricValue {
if !operands.is_empty() {
return syntax_error("Boolean functions don't take any arguments");
}
MetricValue::Bool(value)
}
fn time(&self, namespace: &str, operands: &[ExpressionTree], multiplier: i64) -> MetricValue {
if operands.len() != 1 {
return syntax_error("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 => value_error(format!(
"Time conversion needs 1 numeric argument; couldn't convert {}",
value
)),
Some(value) => MetricValue::Int(value),
},
MetricValue::Problem(oops) => MetricValue::Problem(oops),
bad => value_error(format!("Time conversion needs 1 numeric argument, not {}", bad)),
}
}
fn evaluate(&self, namespace: &str, e: &ExpressionTree) -> MetricValue {
match e {
ExpressionTree::Function(f, operands) => self.evaluate_function(namespace, f, operands),
ExpressionTree::Variable(name) => self.evaluate_variable(namespace, name),
ExpressionTree::Value(value) => value.clone(),
ExpressionTree::Vector(values) => MetricValue::Vector(
values.iter().map(|value| self.evaluate(namespace, value)).collect(),
),
}
}
fn annotation(&self, namespace: &str, operands: &[ExpressionTree]) -> MetricValue {
if operands.len() != 1 {
return syntax_error("Annotation() needs 1 string argument");
}
match self.evaluate(namespace, &operands[0]) {
MetricValue::String(string) => match &self.fetcher {
Fetcher::TrialData(fetcher) => fetcher.annotations,
Fetcher::FileData(fetcher) => fetcher.annotations,
}
.fetch(&string),
_ => value_error("Annotation() needs a string argument"),
}
}
fn log_contains(
&self,
log_type: &Function,
namespace: &str,
operands: &[ExpressionTree],
) -> 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 internal_bug("Internal error, log_contains with non-log function"),
},
Fetcher::FileData(fetcher) => match log_type {
Function::KlogHas => fetcher.klog,
Function::SyslogHas => fetcher.syslog,
Function::BootlogHas => fetcher.bootlog,
_ => return internal_bug("Internal error, log_contains with non-log function"),
},
};
if operands.len() != 1 {
return syntax_error("Log matcher must use exactly 1 argument, an RE string.");
}
match self.evaluate(namespace, &operands[0]) {
MetricValue::String(re) => MetricValue::Bool(log_data.contains(&re)),
_ => value_error("Log matcher needs a string (RE)."),
}
}
fn important_problem(problems: Vec<&Problem>) -> Problem {
match problems.len() {
0 => Problem::InternalBug("Didn't find a Problem".to_string()),
len => {
let mut p = problems.clone();
p.sort_by_cached_key(|p| p.severity());
p[len - 1].clone()
}
}
}
fn apply_boolean_function(
&self,
namespace: &str,
function: &dyn (Fn(&MetricValue, &MetricValue) -> bool),
operands: &[ExpressionTree],
) -> MetricValue {
if operands.len() != 2 {
return syntax_error(format!("Bad arg list {:?} for binary operator", operands));
}
let operand_values =
operands.iter().map(|operand| self.evaluate(namespace, operand)).collect::<Vec<_>>();
let args = operand_values.iter().map(unwrap_for_math).collect::<Vec<_>>();
match (args[0], args[1]) {
(MetricValue::Problem(p), MetricValue::Problem(q)) => {
MetricValue::Problem(Self::important_problem(vec![p, q]))
}
(MetricValue::Problem(p), _) => MetricValue::Problem(p.clone()),
(_, MetricValue::Problem(p)) => MetricValue::Problem(p.clone()),
_ => MetricValue::Bool(function(args[0], args[1])),
}
}
fn fold_bool(
&self,
namespace: &str,
function: &dyn (Fn(bool, bool) -> bool),
operands: &[ExpressionTree],
short_circuit_behavior: ShortCircuitBehavior,
) -> MetricValue {
if operands.is_empty() {
return syntax_error("No operands in boolean expression");
}
let first = self.evaluate(namespace, &operands[0]);
let mut result: bool = match unwrap_for_math(&first) {
MetricValue::Bool(value) => *value,
MetricValue::Problem(p) => return MetricValue::Problem(p.clone()),
bad => return value_error(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::Problem(p) => return MetricValue::Problem(p.clone()),
bad => return value_error(format!("{:?} is not boolean", bad)),
}
}
MetricValue::Bool(result)
}
fn not_bool(&self, namespace: &str, operands: &[ExpressionTree]) -> MetricValue {
if operands.len() != 1 {
return syntax_error(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::Problem(p) => MetricValue::Problem(p.clone()),
bad => value_error(format!("{:?} not boolean", bad)),
}
}
fn is_unhandled_type(&self, namespace: &str, operands: &[ExpressionTree]) -> MetricValue {
if operands.len() != 1 {
return syntax_error(format!(
"Wrong number of operands for UnhandledType(): {}",
operands.len()
));
}
let value = self.evaluate(namespace, &operands[0]);
MetricValue::Bool(match value {
MetricValue::Problem(Problem::UnhandledType(_)) => true,
MetricValue::Vector(contents) if contents.len() == 1 => match contents[0] {
MetricValue::Problem(Problem::UnhandledType(_)) => true,
MetricValue::Problem(ref problem) => return MetricValue::Problem(problem.clone()),
_ => false,
},
MetricValue::Problem(problem) => return MetricValue::Problem(problem),
_ => false,
})
}
fn is_missing(&self, namespace: &str, operands: &[ExpressionTree]) -> MetricValue {
if operands.len() != 1 {
return syntax_error(format!(
"Wrong number of operands for Missing(): {}",
operands.len()
));
}
let value = self.evaluate(namespace, &operands[0]);
MetricValue::Bool(match value {
MetricValue::Problem(Problem::Missing(_)) => true,
MetricValue::Vector(contents) if contents.is_empty() => true,
MetricValue::Vector(contents) if contents.len() == 1 => match contents[0] {
MetricValue::Problem(Problem::Missing(_)) => true,
MetricValue::Problem(ref problem) => return MetricValue::Problem(problem.clone()),
_ => false,
},
MetricValue::Problem(problem) => return MetricValue::Problem(problem),
_ => false,
})
}
fn is_problem(&self, namespace: &str, operands: &[ExpressionTree]) -> MetricValue {
if operands.len() != 1 {
return syntax_error(format!(
"Wrong number of operands for Problem(): {}",
operands.len()
));
}
let value = self.evaluate(namespace, &operands[0]);
MetricValue::Bool(matches!(value, MetricValue::Problem(_)))
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::config::{DiagnosticData, Source};
use anyhow::Error;
use std::sync::LazyLock;
#[macro_export]
macro_rules! make_metrics {
({$($namespace: literal: {
$(eval: {$($ke: literal: $ve: expr),+ $(,)?})?
$(hardcoded: {$($kh: literal: $vh: expr),+ $(,)?})?
$(select: {$($ks: literal: [$($vs: expr),+ $(,)?]),+ $(,)?})?
}),+ $(,)?}) => {{
[$((
$namespace.to_string(),
[
$(
$(
($ke.to_string(),
ValueSource::try_from_expression_with_namespace($ve, $namespace)
.expect("Unable to parse expression as value source.")),
)+
)?
$(
$(
($kh.to_string(),
ValueSource::new(Metric::Hardcoded($vh))),
)+
)?
$(
$(
($ks.to_string(),
ValueSource::new(Metric::Selector(
[$($vs.clone(),)+].to_vec()))),
)+
)?
]
.into_iter().collect::<HashMap<String, ValueSource>>()
)),+]
.into_iter()
.collect::<HashMap<String, HashMap<String, ValueSource>>>()
}};
}
#[macro_export]
macro_rules! assert_problem {
($missing:expr, $message:expr) => {
match $missing {
MetricValue::Problem(problem) => assert_eq!(format!("{:?}", problem), $message),
oops => {
println!("Non problem type {:?}", oops);
assert!(false, "Non-Problem type");
}
}
};
}
#[macro_export]
macro_rules! assert_not_missing {
($not_missing:expr) => {
match $not_missing {
MetricValue::Problem(Problem::Missing(message)) => {
assert!(false, "Expected not missing, was: {}", &message)
}
_ => {}
}
};
}
static EMPTY_F: LazyLock<Vec<DiagnosticData>> = LazyLock::new(|| {
let s = r#"[]"#;
vec![DiagnosticData::new("i".to_string(), Source::Inspect, s.to_string()).unwrap()]
});
static NO_PAYLOAD_F: LazyLock<Vec<DiagnosticData>> = LazyLock::new(|| {
let s = r#"[{"moniker": "abcd", "metadata": {}, "payload": null}]"#;
vec![DiagnosticData::new("i".to_string(), Source::Inspect, s.to_string()).unwrap()]
});
static BAD_PAYLOAD_F: LazyLock<Vec<DiagnosticData>> = LazyLock::new(|| {
let s = r#"[{"moniker": "abcd", "metadata": {}, "payload": ["a", "b"]}]"#;
vec![DiagnosticData::new("i".to_string(), Source::Inspect, s.to_string()).unwrap()]
});
static EMPTY_FILE_FETCHER: LazyLock<FileDataFetcher<'static>> =
LazyLock::new(|| FileDataFetcher::new(&EMPTY_F));
static EMPTY_TRIAL_FETCHER: LazyLock<TrialDataFetcher<'static>> =
LazyLock::new(TrialDataFetcher::new_empty);
static NO_PAYLOAD_FETCHER: LazyLock<FileDataFetcher<'static>> =
LazyLock::new(|| FileDataFetcher::new(&NO_PAYLOAD_F));
static BAD_PAYLOAD_FETCHER: LazyLock<FileDataFetcher<'static>> =
LazyLock::new(|| FileDataFetcher::new(&BAD_PAYLOAD_F));
#[fuchsia::test]
fn focus_on_important_errors() {
let metrics = HashMap::new();
let state = MetricState::new(&metrics, Fetcher::FileData(EMPTY_FILE_FETCHER.clone()), None);
let major = Problem::SyntaxError("Bad".to_string());
let minor = Problem::Ignore(vec![Problem::Missing("Not a big deal".to_string())]);
let major_arg = ExpressionTree::Value(MetricValue::Problem(major.clone()));
let minor_arg = ExpressionTree::Value(MetricValue::Problem(minor));
assert_problem!(
state.apply_boolean_function(
"",
&|a, b| a == b,
&[minor_arg.clone(), major_arg.clone()]
),
"SyntaxError: Bad"
);
assert_problem!(
state.apply_boolean_function("", &|a, b| a == b, &[major_arg, minor_arg]),
"SyntaxError: Bad"
);
}
#[fuchsia::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));
assert_eq!(
state.evaluate_value("", r#"KlogHas("f(.)rst \bline")"#),
MetricValue::Bool(true)
);
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(())
}
#[fuchsia::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_problem!(
state.evaluate_value("", "Annotation('bogus')"),
"Missing: Key 'bogus' not found in annotations"
);
assert_problem!(
state.evaluate_value("", "Annotation('bogus', 'Double bogus')"),
"SyntaxError: Annotation() needs 1 string argument"
);
assert_problem!(
state.evaluate_value("", "Annotation(42)"),
"ValueError: Annotation() needs a string argument"
);
Ok(())
}
#[fuchsia::test]
fn test_fetch_errors() {
assert_eq!(0, NO_PAYLOAD_FETCHER.errors().len());
assert_eq!(1, BAD_PAYLOAD_FETCHER.errors().len());
}
#[fuchsia::test]
fn test_evaluation() {
let metrics = make_metrics!({
"root":{
eval: {"is42": "42", "isOk": "'OK'"}
hardcoded: {"unhandled": unhandled_type("Unhandled")}
}
});
let state = MetricState::new(&metrics, Fetcher::FileData(EMPTY_FILE_FETCHER.clone()), None);
assert_eq!(state.evaluate_value("root", "is42"), MetricValue::Int(42));
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));
assert_eq!(state.evaluate_value("root", "is42 / 4"), MetricValue::Float(10.5));
assert_eq!(state.evaluate_value("root", "is42 // 4"), MetricValue::Int(10));
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));
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));
assert_eq!(state.evaluate_value("root", "isOk"), MetricValue::String("OK".to_string()));
assert_problem!(
state.evaluate_value("root", "missing"),
"SyntaxError: Metric 'missing' Not Found in 'root'"
);
assert_problem!(
state.evaluate_value("root", "Or(is42 != 42, missing)"),
"SyntaxError: Metric 'missing' Not Found in 'root'"
);
assert_eq!(
state.evaluate_value("root", "Or(is42 == 42, missing)"),
MetricValue::Bool(true)
);
assert_problem!(
state.evaluate_value("root", "And(is42 == 42, missing)"),
"SyntaxError: Metric 'missing' Not Found in 'root'"
);
assert_eq!(
state.evaluate_value("root", "And(is42 != 42, missing)"),
MetricValue::Bool(false)
);
assert_eq!(state.evaluate_value("root", "Missing(is42)"), MetricValue::Bool(false));
assert_problem!(
state.evaluate_value("root", "Missing(not_found)"),
"SyntaxError: Metric 'not_found' Not Found in 'root'"
);
assert_eq!(
state.evaluate_value("root", "And(Not(Missing(is42)), is42 == 42)"),
MetricValue::Bool(true)
);
assert_problem!(
state.evaluate_value("root", "And(Not(Missing(not_found)), not_found == 'Hello')"),
"SyntaxError: Metric 'not_found' Not Found in 'root'"
);
assert_eq!(
state.evaluate_value("root", "Or(Missing(is42), is42 < 42)"),
MetricValue::Bool(false)
);
assert_problem!(
state.evaluate_value("root", "Or(Missing(not_found), not_found == 'Hello')"),
"SyntaxError: Metric 'not_found' Not Found in 'root'"
);
assert_eq!(state.evaluate_value("root", "Missing([])"), MetricValue::Bool(true));
assert_eq!(state.evaluate_value("root", "UnhandledType(is42)"), MetricValue::Bool(false));
assert_problem!(
state.evaluate_value("root", "UnhandledType(not_found)"),
"SyntaxError: Metric 'not_found' Not Found in 'root'"
);
assert_eq!(
state.evaluate_value("root", "And(Not(UnhandledType(is42)), is42 == 42)"),
MetricValue::Bool(true)
);
assert_eq!(
state.evaluate_value("root", "UnhandledType(unhandled)"),
MetricValue::Bool(true)
);
assert_eq!(state.evaluate_value("root", "UnhandledType([])"), MetricValue::Bool(false));
assert_eq!(state.evaluate_value("root", "Problem(is42)"), MetricValue::Bool(false));
assert_eq!(state.evaluate_value("root", "Problem(not_found)"), MetricValue::Bool(true));
assert_eq!(
state.evaluate_value("root", "And(Not(Problem(is42)), is42 == 42)"),
MetricValue::Bool(true)
);
assert_eq!(
state.evaluate_value("root", "And(Not(Problem(not_found)), not_found == 'Hello')"),
MetricValue::Bool(false)
);
assert_eq!(
state.evaluate_value("root", "Or(Problem(is42), is42 < 42)"),
MetricValue::Bool(false)
);
assert_eq!(
state.evaluate_value("root", "Or(Problem(not_found), not_found == 'Hello')"),
MetricValue::Bool(true)
);
assert_problem!(
state.evaluate_value("root", "Or(not_found == 'Hello', Problem(not_found))"),
"SyntaxError: Metric 'not_found' Not Found in 'root'"
);
assert_eq!(
state.evaluate_value("root", "[0==0]"),
MetricValue::Vector(vec![MetricValue::Bool(true)])
);
assert_eq!(
state.eval_action_metric(
"root",
&ValueSource::try_from_expression_with_namespace("[0==0]", "root").unwrap()
),
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",
&ValueSource::try_from_expression_with_namespace("[0==0, 0==0]", "root").unwrap()
),
MetricValue::Vector(vec![MetricValue::Bool(true), MetricValue::Bool(true)])
);
assert_eq!(
state.eval_action_metric(
"root",
&ValueSource::try_from_expression_with_namespace(
"StringMatches('abcd', '^a.c')",
"root"
)
.unwrap()
),
MetricValue::Bool(true)
);
assert_eq!(
state.eval_action_metric(
"root",
&ValueSource::try_from_expression_with_namespace(
"StringMatches('abcd', 'a.c$')",
"root"
)
.unwrap()
),
MetricValue::Bool(false)
);
assert_problem!(
state.eval_action_metric(
"root",
&ValueSource::try_from_expression_with_namespace(
"StringMatches('abcd', '[[')",
"root"
)
.unwrap()
),
"SyntaxError: Could not parse `[[` as regex"
);
}
#[fuchsia::test]
fn test_caching_after_evaluation() {
let metrics = make_metrics!({
"root":{
eval: {"is42": "42", "is43": "is42 + 1"}
hardcoded: {"unhandled": unhandled_type("Unhandled")}
}
});
let state = MetricState::new(&metrics, Fetcher::FileData(EMPTY_FILE_FETCHER.clone()), None);
let trial_state =
MetricState::new(&metrics, Fetcher::TrialData(EMPTY_TRIAL_FETCHER.clone()), None);
assert_eq!(
*state.metrics.get("root").unwrap().get("is42").unwrap().cached_value.borrow(),
None
);
assert_eq!(
*state.metrics.get("root").unwrap().get("is43").unwrap().cached_value.borrow(),
None
);
assert_eq!(
*state.metrics.get("root").unwrap().get("unhandled").unwrap().cached_value.borrow(),
None
);
assert_eq!(
*trial_state.metrics.get("root").unwrap().get("is42").unwrap().cached_value.borrow(),
None
);
assert_eq!(
*trial_state.metrics.get("root").unwrap().get("is43").unwrap().cached_value.borrow(),
None
);
assert_eq!(
*trial_state
.metrics
.get("root")
.unwrap()
.get("unhandled")
.unwrap()
.cached_value
.borrow(),
None
);
assert_eq!(state.evaluate_value("root", "is42"), MetricValue::Int(42));
assert_eq!(
*state.metrics.get("root").unwrap().get("is42").unwrap().cached_value.borrow(),
Some(MetricValue::Int(42))
);
assert_eq!(trial_state.evaluate_value("root", "is42"), MetricValue::Int(42));
assert_eq!(
*trial_state.metrics.get("root").unwrap().get("is42").unwrap().cached_value.borrow(),
Some(MetricValue::Int(42))
);
assert_eq!(state.evaluate_value("root", "is43"), MetricValue::Int(43));
assert_eq!(
*state.metrics.get("root").unwrap().get("is42").unwrap().cached_value.borrow(),
Some(MetricValue::Int(42))
);
assert_eq!(
*state.metrics.get("root").unwrap().get("is43").unwrap().cached_value.borrow(),
Some(MetricValue::Int(43))
);
assert_eq!(trial_state.evaluate_value("root", "is43"), MetricValue::Int(43));
assert_eq!(
*trial_state.metrics.get("root").unwrap().get("is42").unwrap().cached_value.borrow(),
Some(MetricValue::Int(42))
);
assert_eq!(
*trial_state.metrics.get("root").unwrap().get("is43").unwrap().cached_value.borrow(),
Some(MetricValue::Int(43))
);
state.evaluate_value("root", "unhandled");
assert_problem!(
(*state.metrics.get("root").unwrap().get("unhandled").unwrap().cached_value.borrow())
.as_ref()
.unwrap(),
"UnhandledType: Unhandled"
);
trial_state.evaluate_value("root", "unhandled");
assert_problem!(
(*trial_state
.metrics
.get("root")
.unwrap()
.get("unhandled")
.unwrap()
.cached_value
.borrow())
.as_ref()
.unwrap(),
"UnhandledType: Unhandled"
);
}
macro_rules! eval {
($e:expr) => {
MetricState::evaluate_math($e)
};
}
#[fuchsia::test]
fn test_missing_hacks() -> Result<(), Error> {
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(())
}
#[fuchsia::test]
fn test_ignores_in_expressions() {
let dbz = "ValueError: Division by zero";
assert_problem!(eval!("CountProperties([1, 2, 3/0])"), dbz);
assert_problem!(eval!("CountProperties([1/0, 2, 3/0])"), dbz);
assert_problem!(eval!("CountProperties([1/?0, 2, 3/?0])"), format!("Ignore: {}", dbz));
assert_problem!(eval!("CountProperties([1/0, 2, 3/?0])"), dbz);
assert_problem!(eval!("And(1 > 0, 3/0 > 0)"), dbz);
assert_problem!(eval!("And(1/0 > 0, 3/0 > 0)"), dbz);
assert_problem!(eval!("And(1/?0 > 0, 3/?0 > 0)"), format!("Ignore: {}", dbz));
assert_problem!(eval!("And(1/?0 > 0, 3/0 > 0)"), format!("Ignore: {}", dbz));
assert_problem!(eval!("1 == 3/0"), dbz);
assert_problem!(eval!("1/0 == 3/0"), dbz);
assert_problem!(eval!("1/?0 == 3/?0"), format!("Ignore: {}", dbz));
assert_problem!(eval!("1/?0 == 3/0"), dbz);
assert_problem!(eval!("1 + 3/0"), dbz);
assert_problem!(eval!("1/0 + 3/0"), dbz);
assert_problem!(eval!("1/?0 + 3/?0"), format!("Ignore: {}", dbz));
assert_problem!(eval!("1/?0 + 3/0"), dbz);
}
#[fuchsia::test]
fn test_checked_divide_preserves_errors() {
let san = "Missing: String(a) not numeric";
assert_problem!(eval!("(1+'a')/1"), san);
assert_problem!(eval!("(1+'a')/?1"), san);
assert_problem!(eval!("1/?(1+'a')"), san);
assert_problem!(eval!("(1+'a')/?(1+'a')"), san);
assert_problem!(eval!("(1+'a')/0"), san);
assert_problem!(eval!("(1+'a')/?0"), san);
assert_problem!(eval!("(1+'a')//1"), san);
assert_problem!(eval!("(1+'a')//?1"), san);
assert_problem!(eval!("1//?(1+'a')"), san);
assert_problem!(eval!("(1+'a')//?(1+'a')"), san);
assert_problem!(eval!("(1+'a')//0"), san);
assert_problem!(eval!("(1+'a')//?0"), san);
}
#[fuchsia::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_problem!(MetricState::evaluate_math("Now()"), "Missing: No valid time available");
assert_eq!(state_1234.evaluate_expression(&now_expression), MetricValue::Int(1234));
assert_problem!(
state_missing.evaluate_expression(&now_expression),
"Missing: No valid time available"
);
Ok(())
}
#[fuchsia::test]
fn test_expression_context() {
let selector_expr = "INSPECT:foo:bar:baz";
assert_eq!(
format!(
"{:?}",
ExpressionContext::try_from_expression_with_default_namespace(selector_expr)
.err()
.unwrap()
),
"Expression Error: \n0: at line 1, in Eof:\nINSPECT:foo:bar:baz\n ^\n\n"
);
let invalid_expr = "1 *";
assert_eq!(
format!(
"{:?}",
ExpressionContext::try_from_expression_with_default_namespace(invalid_expr)
.err()
.unwrap()
),
concat!("Expression Error: \n0: at line 1, in Eof:\n1 *\n ^\n\n")
);
let valid_expr = "42 + 1";
let parsed_expression = parse::parse_expression(valid_expr, "").unwrap();
assert_eq!(
ExpressionContext::try_from_expression_with_default_namespace(valid_expr).unwrap(),
ExpressionContext { raw_expression: valid_expr.to_string(), parsed_expression }
);
}
#[fuchsia::test]
fn test_not_valid_cycle_same_variable() {
let metrics = make_metrics!({
"root":{
eval: {
"is42": "42",
"shouldBe42": "is42 + 0",
}
},
"n2":{
eval: {
"is42": "root::shouldBe42"
}
}
});
let state = MetricState::new(&metrics, Fetcher::FileData(EMPTY_FILE_FETCHER.clone()), None);
assert_eq!(state.evaluate_value("n2", "is42"), MetricValue::Int(42));
}
#[fuchsia::test]
fn test_cycle_detected_correctly() {
let metrics = make_metrics!({
"root":{
eval: {
"is42": "42",
"shouldBe62": "is42 + 1 + n2::is19",
"b": "is42 + n2::a"
}
},
"n2":{
eval: {
"is19": "19",
"shouldBe44": "root::is42 + 2",
"a": "is19 + c",
"c": "root::b + root::is42"
}
}
});
let state = MetricState::new(&metrics, Fetcher::FileData(EMPTY_FILE_FETCHER.clone()), None);
assert_eq!(state.evaluate_value("root", "shouldBe62 + 1"), MetricValue::Int(63));
assert_problem!(
state.evaluate_value("root", "n2::a"),
"EvaluationError: Cycle encountered while evaluating variable n2::a in the expression"
);
assert_eq!(state.evaluate_value("root", "n2::shouldBe44"), MetricValue::Int(44));
assert_problem!(
state.evaluate_value("root", "b"),
"EvaluationError: Cycle encountered while evaluating variable n2::a in the expression"
);
assert_problem!(
state.evaluate_value("root", "n2::c"),
"EvaluationError: Cycle encountered while evaluating variable n2::a in the expression"
);
assert_eq!(state.evaluate_value("root", "shouldBe62"), MetricValue::Int(62));
}
}