blob: 6c870ed7fcf7b160358955ca2e12a703ec4049ec [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.
use super::{
reader::{NodeHierarchy, Property},
Inspector,
};
use failure::{bail, Error};
use std::borrow::Cow;
use std::collections::HashSet;
/// Macro to simplify tree matching in tests. The first argument is the actual tree and can be
/// a NodeHierarchy or an Inspector. The second argument is an matcher/assertion expression to
/// compare against the tree.
///
/// Each leaf value must be a type that implements `PropertyAssertion`.
///
/// Example:
/// ```
/// // Actual tree
/// let node_hierarchy = NodeHierarchy {
/// name: "key".to_string(),
/// properties: vec![
/// Property::String("sub".to_string(), "sub_value".to_string()),
/// Property::String("sub2".to_string(), "sub2_value".to_string()),
/// ],
/// children: vec![
/// NodeHierarchy {
/// name: "child1".to_string(),
/// properties: vec![
/// Property::Int("child1_sub".to_string(), 10i64),
/// ],
/// children: vec![],
/// },
/// NodeHierarchy {
/// name: "child2".to_string(),
/// properties: vec![
/// Property::Uint("child2_sub".to_string(), 20u64),
/// ],
/// children: vec![],
/// },
/// ],
/// };
///
/// assert_inspect_tree!(node_hierarchy, key: {
/// sub: AnyProperty, // only verify that `sub` is a property of `key`
/// sub2: "sub2_value",
/// child1: {
/// child1_sub: 10i64,
/// },
/// child2: {
/// child2_sub: 20u64,
/// },
/// });
/// ```
///
/// In order to do partial match on a tree, use the `contains` keyword:
/// ```
/// assert_inspect_tree!(node_hierarchy, key: contains {
/// sub: "sub_value",
/// child1: contains {},
/// });
/// ```
///
/// The first argument can be an Inspector, in which case the whole tree is read from the VMO and
/// matched against:
/// ```
/// let inspector = Inspector::new().unwrap();
/// assert_inspect_tree!(inspector, root: {});
/// ```
#[macro_export]
macro_rules! assert_inspect_tree {
(@build $tree_assertion:expr,) => {};
// Exact match of tree
(@build $tree_assertion:expr, var $key:ident: { $($sub:tt)* }) => {{
#[allow(unused_mut)]
let mut child_tree_assertion = TreeAssertion::new($key, true);
assert_inspect_tree!(@build child_tree_assertion, $($sub)*);
$tree_assertion.add_child_assertion(child_tree_assertion);
}};
(@build $tree_assertion:expr, var $key:ident: { $($sub:tt)* }, $($rest:tt)*) => {{
assert_inspect_tree!(@build $tree_assertion, var $key: { $($sub)* });
assert_inspect_tree!(@build $tree_assertion, $($rest)*);
}};
// Partial match of tree
(@build $tree_assertion:expr, var $key:ident: contains { $($sub:tt)* }) => {{
#[allow(unused_mut)]
let mut child_tree_assertion = TreeAssertion::new($key, false);
assert_inspect_tree!(@build child_tree_assertion, $($sub)*);
$tree_assertion.add_child_assertion(child_tree_assertion);
}};
(@build $tree_assertion:expr, var $key:ident: contains { $($sub:tt)* }, $($rest:tt)*) => {{
assert_inspect_tree!(@build $tree_assertion, var $key: contains { $($sub)* });
assert_inspect_tree!(@build $tree_assertion, $($rest)*);
}};
// Matching properties of a tree
(@build $tree_assertion:expr, var $key:ident: $assertion:expr) => {{
$tree_assertion.add_property_assertion($key, Box::new($assertion))
}};
(@build $tree_assertion:expr, var $key:ident: $assertion:expr, $($rest:tt)*) => {{
assert_inspect_tree!(@build $tree_assertion, var $key: $assertion);
assert_inspect_tree!(@build $tree_assertion, $($rest)*);
}};
// Key identifier format
(@build $tree_assertion:expr, $key:ident: $($rest:tt)+) => {{
let key = stringify!($key);
assert_inspect_tree!(@build $tree_assertion, var key: $($rest)+);
}};
// Allows string literal for key
(@build $tree_assertion:expr, $key:tt: $($rest:tt)+) => {{
let key: &'static str = $key;
assert_inspect_tree!(@build $tree_assertion, var key: $($rest)+);
}};
// Entry points
($node_hierarchy:expr, var $key:ident: { $($sub:tt)* }) => {{
use $crate::testing::{NodeHierarchyGetter, TreeAssertion};
#[allow(unused_mut)]
let mut tree_assertion = TreeAssertion::new($key, true);
assert_inspect_tree!(@build tree_assertion, $($sub)*);
if let Err(e) = tree_assertion.run($node_hierarchy.get_node_hierarchy().as_ref()) {
panic!("tree assertion fails: {}", e);
}
}};
($node_hierarchy:expr, var $key:ident: contains { $($sub:tt)* }) => {{
use $crate::testing::{NodeHierarchyGetter, TreeAssertion};
#[allow(unused_mut)]
let mut tree_assertion = TreeAssertion::new($key, false);
assert_inspect_tree!(@build tree_assertion, $($sub)*);
if let Err(e) = tree_assertion.run($node_hierarchy.get_node_hierarchy().as_ref()) {
panic!("tree assertion fails: {}", e);
}
}};
($node_hierarchy:expr, $key:ident: $($rest:tt)+) => {{
let key = stringify!($key);
assert_inspect_tree!($node_hierarchy, var key: $($rest)+);
}};
($node_hierarchy:expr, $key:tt: $($rest:tt)+) => {{
let key: &'static str = $key;
assert_inspect_tree!($node_hierarchy, var key: $($rest)+);
}};
}
pub trait NodeHierarchyGetter {
fn get_node_hierarchy(&self) -> Cow<NodeHierarchy>;
}
impl NodeHierarchyGetter for NodeHierarchy {
fn get_node_hierarchy(&self) -> Cow<NodeHierarchy> {
Cow::Borrowed(self)
}
}
impl NodeHierarchyGetter for Inspector {
fn get_node_hierarchy(&self) -> Cow<NodeHierarchy> {
use std::convert::TryFrom;
Cow::Owned(NodeHierarchy::try_from(self).unwrap())
}
}
macro_rules! eq_or_bail {
($expected:expr, $actual:expr) => {{
if $expected != $actual {
bail!("\n Expected: {:?}\n Got: {:?}", $expected, $actual);
}
}};
($expected:expr, $actual:expr, $($args:tt)+) => {{
if $expected != $actual {
bail!("{}:\n Expected: {:?}\n Got: {:?}", format!($($args)+), $expected, $actual);
}
}}
}
/// Struct for matching against an Inspect tree (NodeHierarchy).
pub struct TreeAssertion {
/// Expected name of the node being compared against
name: String,
/// Friendly name that includes path from ancestors. Mainly used to indicate which node fails
/// in error message
path: String,
/// Expected property names and assertions to match the actual properties against
properties: Vec<(String, Box<dyn PropertyAssertion>)>,
/// Assertions to match against child trees
children: Vec<TreeAssertion>,
/// Whether all properties and children of the tree should be checked
exact_match: bool,
}
impl TreeAssertion {
/// Create a new `TreeAssertion`. The |name| argument is the expected name of the tree to be
/// compared against. Set |exact_match| to true to specify that all properties and children of
/// the tree should be checked. To perform partial matching of the tree, set it to false.
pub fn new(name: &str, exact_match: bool) -> Self {
Self {
name: name.to_string(),
path: name.to_string(),
properties: vec![],
children: vec![],
exact_match,
}
}
pub fn add_property_assertion(&mut self, key: &str, assertion: Box<dyn PropertyAssertion>) {
self.properties.push((key.to_string(), assertion));
}
pub fn add_child_assertion(&mut self, mut assertion: TreeAssertion) {
assertion.path = format!("{}.{}", self.path, assertion.name);
self.children.push(assertion);
}
/// Check whether |actual| tree satisfies criteria defined by `TreeAssertion`. Return `Ok` if
/// assertion passes and `Error` if assertion fails.
pub fn run(&self, actual: &NodeHierarchy) -> Result<(), Error> {
eq_or_bail!(self.name, actual.name, "node `{}` - expected node name != actual", self.path);
if self.exact_match {
let properties_names = self.properties.iter().map(|p| p.0.clone());
let children_names = self.children.iter().map(|c| c.name.clone());
let keys: HashSet<String> = properties_names.chain(children_names).collect();
let actual_props = actual.properties.iter().map(|p| p.name().to_string());
let actual_children = actual.children.iter().map(|c| c.name.clone());
let actual_keys: HashSet<String> = actual_props.chain(actual_children).collect();
eq_or_bail!(keys, actual_keys, "node `{}` - expected keys != actual", self.path);
}
for (name, assertion) in self.properties.iter() {
match actual.properties.iter().find(|p| p.name() == name) {
Some(property) => {
if let Err(e) = assertion.run(property) {
bail!(
"node `{}` - assertion fails for property `{}`. Reason: {}",
self.path,
name,
e
);
}
}
None => bail!("node `{}` - no property named `{}`", self.path, name),
}
}
for assertion in self.children.iter() {
match actual.children.iter().find(|c| c.name == assertion.name) {
Some(child) => assertion.run(&child)?,
None => bail!("node `{}` - no child named `{}`", self.path, assertion.name),
}
}
Ok(())
}
}
pub trait PropertyAssertion {
/// Check whether |actual| property satisfies criteria. Return `Ok` if assertion passes and
/// `Error` if assertion fails.
fn run(&self, actual: &Property) -> Result<(), Error>;
}
macro_rules! impl_property_assertion {
($prop_variant:ident, $($ty:ty),+) => {
$(
impl PropertyAssertion for $ty {
fn run(&self, actual: &Property) -> Result<(), Error> {
if let Property::$prop_variant(_key, value, ..) = actual {
eq_or_bail!(self, value);
} else {
bail!("expected {}, found {}", stringify!($prop_variant), property_type_name(actual));
}
Ok(())
}
}
)+
}
}
fn property_type_name(property: &Property) -> &str {
match property {
Property::String(_, _) => "String",
Property::Bytes(_, _) => "Bytes",
Property::Int(_, _) => "Int",
Property::IntArray(_, _, _) => "IntArray",
Property::Uint(_, _) => "Uint",
Property::UintArray(_, _, _) => "UintArray",
Property::Double(_, _) => "Double",
Property::DoubleArray(_, _, _) => "DoubleArray",
}
}
impl_property_assertion!(String, &str, String);
impl_property_assertion!(Bytes, Vec<u8>);
impl_property_assertion!(Uint, u64);
impl_property_assertion!(Int, i64);
impl_property_assertion!(Double, f64);
impl_property_assertion!(DoubleArray, Vec<f64>);
impl_property_assertion!(IntArray, Vec<i64>);
impl_property_assertion!(UintArray, Vec<u64>);
/// A PropertyAssertion that always passes
pub struct AnyProperty;
impl PropertyAssertion for AnyProperty {
fn run(&self, _actual: &Property) -> Result<(), Error> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use {super::*, crate::reader::ArrayFormat};
#[test]
fn test_exact_match_simple() {
let node_hierarchy = simple_tree();
assert_inspect_tree!(node_hierarchy, key: {
sub: "sub_value",
sub2: "sub2_value",
});
}
#[test]
fn test_exact_match_complex() {
let node_hierarchy = complex_tree();
assert_inspect_tree!(node_hierarchy, key: {
sub: "sub_value",
sub2: "sub2_value",
child1: {
child1_sub: 10i64,
},
child2: {
child2_sub: 20u64,
},
});
}
#[test]
fn test_exact_match_mismatched_property_name() {
let node_hierarchy = simple_tree();
assert_fail(|| {
assert_inspect_tree!(node_hierarchy, key: {
sub: "sub_value",
sub3: "sub2_value",
});
});
}
#[test]
fn test_exact_match_mismatched_child_name() {
let node_hierarchy = complex_tree();
assert_fail(|| {
assert_inspect_tree!(node_hierarchy, key: {
sub: "sub_value",
sub2: "sub2_value",
child1: {
child1_sub: 10i64,
},
child3: {
child2_sub: 20u64,
},
});
});
}
#[test]
fn test_exact_match_mismatched_property_name_in_child() {
let node_hierarchy = complex_tree();
assert_fail(|| {
assert_inspect_tree!(node_hierarchy, key: {
sub: "sub_value",
sub2: "sub2_value",
child1: {
child2_sub: 10i64,
},
child2: {
child2_sub: 20u64,
},
});
});
}
#[test]
fn test_exact_match_mismatched_property_value() {
let node_hierarchy = simple_tree();
assert_fail(|| {
assert_inspect_tree!(node_hierarchy, key: {
sub: "sub2_value",
sub2: "sub2_value",
});
});
}
#[test]
fn test_exact_match_missing_property() {
let node_hierarchy = simple_tree();
assert_fail(|| {
assert_inspect_tree!(node_hierarchy, key: {
sub: "sub_value",
});
});
}
#[test]
fn test_exact_match_missing_child() {
let node_hierarchy = complex_tree();
assert_fail(|| {
assert_inspect_tree!(node_hierarchy, key: {
sub: "sub_value",
sub2: "sub2_value",
child1: {
child1_sub: 10i64,
},
});
});
}
#[test]
fn test_partial_match_success() {
let node_hierarchy = complex_tree();
// only verify the top tree name
assert_inspect_tree!(node_hierarchy, key: contains {});
// verify parts of the tree
assert_inspect_tree!(node_hierarchy, key: contains {
sub: "sub_value",
child1: contains {},
});
}
#[test]
fn test_partial_match_nonexistent_property() {
let node_hierarchy = simple_tree();
assert_fail(|| {
assert_inspect_tree!(node_hierarchy, key: contains {
sub3: AnyProperty,
});
});
}
#[test]
fn test_ignore_property_value() {
let node_hierarchy = simple_tree();
assert_inspect_tree!(node_hierarchy, key: {
sub: AnyProperty,
sub2: "sub2_value",
});
}
#[test]
fn test_ignore_property_value_property_name_is_still_checked() {
let node_hierarchy = simple_tree();
assert_fail(|| {
assert_inspect_tree!(node_hierarchy, key: {
sub1: AnyProperty,
sub2: "sub2_value",
})
})
}
#[test]
fn test_expr_key_syntax() {
let node_hierarchy = NodeHierarchy {
name: "key".to_string(),
properties: vec![Property::String("@time".to_string(), "1.000".to_string())],
children: vec![],
};
assert_inspect_tree!(node_hierarchy, key: {
"@time": "1.000"
});
}
#[test]
fn test_var_key_syntax() {
let node_hierarchy = NodeHierarchy {
name: "key".to_string(),
properties: vec![Property::String("@time".to_string(), "1.000".to_string())],
children: vec![],
};
let time_key = "@time";
assert_inspect_tree!(node_hierarchy, key: {
var time_key: "1.000"
});
}
#[test]
fn test_arrays() {
let node_hierarchy = NodeHierarchy {
name: "key".to_string(),
children: vec![],
properties: vec![
Property::UintArray("@uints".to_string(), vec![1, 2, 3], ArrayFormat::Default),
Property::IntArray("@ints".to_string(), vec![-2, -4, 0], ArrayFormat::Default),
Property::DoubleArray(
"@doubles".to_string(),
vec![1.3, 2.5, -3.6],
ArrayFormat::Default,
),
],
};
assert_inspect_tree!(node_hierarchy, key: {
"@uints": vec![1u64, 2, 3],
"@ints": vec![-2i64, -4, 0],
"@doubles": vec![1.3, 2.5, -3.6]
});
}
#[test]
fn test_histograms() {
let node_hierarchy = NodeHierarchy {
name: "key".to_string(),
children: vec![],
properties: vec![
Property::UintArray(
"@linear-uints".to_string(),
vec![1, 2, 3, 4, 5],
ArrayFormat::LinearHistogram,
),
Property::IntArray(
"@linear-ints".to_string(),
vec![6, 7, 8, 9],
ArrayFormat::LinearHistogram,
),
Property::DoubleArray(
"@linear-doubles".to_string(),
vec![1.0, 2.0, 4.0, 5.0],
ArrayFormat::LinearHistogram,
),
Property::UintArray(
"@exp-uints".to_string(),
vec![2, 4, 6, 8, 10],
ArrayFormat::ExponentialHistogram,
),
Property::IntArray(
"@exp-ints".to_string(),
vec![1, 3, 5, 7, 9],
ArrayFormat::ExponentialHistogram,
),
Property::DoubleArray(
"@exp-doubles".to_string(),
vec![1.0, 2.0, 3.0, 4.0, 5.0],
ArrayFormat::ExponentialHistogram,
),
],
};
assert_inspect_tree!(node_hierarchy, key: {
"@linear-uints": vec![1u64, 2, 3, 4, 5],
"@linear-ints": vec![6i64, 7, 8, 9],
"@linear-doubles": vec![1.0, 2.0, 4.0, 5.0],
"@exp-uints": vec![2u64, 4, 6, 8, 10],
"@exp-ints": vec![1i64, 3, 5, 7, 9],
"@exp-doubles": vec![1.0, 2.0, 3.0, 4.0, 5.0]
});
}
#[test]
fn test_matching_with_inspector() {
let inspector = Inspector::new();
assert_inspect_tree!(inspector, root: {});
}
fn assert_fail<F: FnOnce() + std::panic::UnwindSafe>(f: F) {
if std::panic::catch_unwind(f).is_ok() {
panic!("expect test to fail");
}
}
fn simple_tree() -> NodeHierarchy {
NodeHierarchy {
name: "key".to_string(),
properties: vec![
Property::String("sub".to_string(), "sub_value".to_string()),
Property::String("sub2".to_string(), "sub2_value".to_string()),
],
children: vec![],
}
}
fn complex_tree() -> NodeHierarchy {
NodeHierarchy {
name: "key".to_string(),
properties: vec![
Property::String("sub".to_string(), "sub_value".to_string()),
Property::String("sub2".to_string(), "sub2_value".to_string()),
],
children: vec![
NodeHierarchy {
name: "child1".to_string(),
properties: vec![Property::Int("child1_sub".to_string(), 10i64)],
children: vec![],
},
NodeHierarchy {
name: "child2".to_string(),
properties: vec![Property::Uint("child2_sub".to_string(), 20u64)],
children: vec![],
},
],
}
}
}