blob: 46affb05531ea1a91b9005213a15d0449afa5ec3 [file] [log] [blame]
// Copyright 2021 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.
use {
crate::error::{ComponentError, EvaluationError},
crate::spec::TestCase,
itertools::Itertools,
maplit::hashmap,
std::collections::HashMap,
};
const TRIAGE_PRESENCE_TEMPLATE: &'static str = r#"
{
select: {
var: "INSPECT:{selector}",
},
act: {
value_was_missing: {
type: "Warning",
trigger: "Missing(var)",
print: "Selector `{selector}` did not match any data",
}
}
}"#;
const TRIAGE_LAMBDA_TEMPLATE: &'static str = r#"
{
select: {
var: "INSPECT:{selector}",
},
eval: {
lambda: "Fn({lambda})",
},
act: {
value_was_missing: {
type: "Warning",
trigger: "Not(Apply(lambda, [var]))",
print: "Function `{lambda}` returned false for `{selector}`",
}
}
}"#;
/// Wrapper for the context to evaluate incoming diagnostics data.
pub(crate) struct EvaluationContext {
/// The result of constructing and parsing a triage configuration for a specific test case.
config_parse_result: fuchsia_triage::ParseResult,
/// The original config text, for debugging.
config_text: String,
}
impl EvaluationContext {
pub(crate) fn run(&self, json: &str) -> Result<(), EvaluationError> {
let data_vec = vec![fuchsia_triage::DiagnosticData::new(
"inspect.json".to_string(),
fuchsia_triage::Source::Inspect,
json.to_string(),
)
.map_err(|e| EvaluationError::ParseFailure {
message: e.to_string(),
data: json.to_string(),
})?];
let result = fuchsia_triage::analyze(&data_vec, &self.config_parse_result)
.map_err(|e| EvaluationError::InternalFailure(e.to_string()))?;
let mut issues = result.all_issues().peekable();
match issues.peek() {
None => Ok(()),
Some(_) => Err(EvaluationError::Failure {
reasons: Itertools::intersperse(issues, "\n").collect(),
data: json.to_string(),
config: self.config_text.to_string(),
}),
}
}
}
impl TryFrom<&TestCase> for EvaluationContext {
type Error = ComponentError;
fn try_from(case: &TestCase) -> Result<Self, Self::Error> {
let selector = case.selector.replace("\\", "\\\\");
let case_config = match &case.expression {
None => TRIAGE_PRESENCE_TEMPLATE.replace("{selector}", &selector),
Some(expression) => TRIAGE_LAMBDA_TEMPLATE
.replace("{selector}", &selector)
// Lambda is of the form [var] <function using var>,
// we format the first ']' character as "]," so that it matches the input required
// for Fn([var], <function using var>)
//
// We additionally escape all instances of '"' and '\'
.replace(
"{lambda}",
&(expression
.replacen("]", "],", 1)
.replace("\\", "\\\\")
.replace("\"", "\\\"")),
),
};
let configs: HashMap<String, String> = hashmap! {
"test_case".to_string() => case_config.clone(),
};
let config_parse_result = fuchsia_triage::ParseResult::new(
&configs,
&fuchsia_triage::ActionTagDirective::AllowAll,
)
.map_err(|e| ComponentError::TriageConfigError {
message: e.to_string(),
config: case_config.clone(),
})?;
config_parse_result.validate().map_err(|e| ComponentError::TriageConfigError {
message: e.to_string(),
config: case_config.clone(),
})?;
Ok(EvaluationContext { config_parse_result, config_text: case_config })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn evaluation_context_run() {
// Note: Even though several cases will fail to evaluate, they pass validation of the
// triage format.
let cases = vec![
(
"a:b:c WHERE abcd",
"a",
r#"{"root": {"value": 10}}"#,
Some("Apply needs a function in its first argument"),
),
("a:b:c", "a", r#"{"root": {"value": 10}}"#, Some("Selector `a:b:c` did not match")),
("a:root:value", "a", r#"{"root": {"value": 10}}"#, None),
("a:root:value WHERE [v] v > 9", "a", r#"{"root": {"value": 10}}"#, None),
(
"a:root:value WHERE [v] v < 9",
"a",
r#"{"root": {"value": 10}}"#,
Some("returned false for `a:root:value`"),
),
("a:root:\\:value", "a", r#"{"root": {":value": 10}}"#, None),
];
for (case, moniker, payload, expected_error) in cases.into_iter() {
let context = EvaluationContext::try_from(
&TestCase::try_from(case.to_string()).expect("parsing case"),
)
.expect("create context");
let data = format!(
r#"
[
{{
"data_source": "Inspect",
"moniker": "{moniker}",
"payload": {payload},
"version": 1
}}
]
"#,
moniker = moniker,
payload = payload
);
match expected_error {
None => {
context.run(&data).expect("successful evaluation");
}
Some(expected_error) => match context.run(&data) {
Err(EvaluationError::Failure { reasons, .. }) => {
assert!(
reasons.find(expected_error).is_some(),
"Could not find substring {} in {}",
expected_error,
reasons
);
}
v => {
assert!(
false,
"Expected an internal error {}, found {:?}",
expected_error, v
);
}
},
}
}
}
}