| // 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![], |
| }, |
| ], |
| } |
| } |
| } |