diff --git a/src/assertions/iterator.rs b/src/assertions/iterator.rs index af647e0..fb3e440 100644 --- a/src/assertions/iterator.rs +++ b/src/assertions/iterator.rs @@ -306,23 +306,16 @@ where expected_iter.clone(), SequenceOrderComparison::Strict, ); - if comparison.contains_exactly() && comparison.order_preserved { - self.new_result().do_ok() - } else if comparison.contains_exactly() && !comparison.order_preserved { - self.new_result() - .add_simple_fact("contents match, but order was wrong") - .add_splitter() - .add_formatted_values_fact("expected", expected_iter.collect()) - .add_formatted_values_fact("actual", self.actual().clone().collect()) - .do_fail() + let (result, ok) = check_contains_exactly_in_order( + comparison, + self.actual().clone(), + expected_iter, + self.new_result(), + ); + if ok { + result.do_ok() } else { - feed_facts_about_item_diff( - self.new_result(), - &comparison, - self.actual().clone(), - expected_iter, - ) - .do_fail() + result.do_fail() } } @@ -393,30 +386,16 @@ where expected_iter.clone(), SequenceOrderComparison::Relative, ); - if comparison.contains_all() && comparison.order_preserved { - self.new_result().do_ok() - } else if comparison.contains_all() { - self.new_result() - .add_simple_fact("required elements were all found, but order was wrong") - .add_formatted_values_fact( - "expected order for required elements", - expected_iter.clone().collect(), - ) - .add_formatted_values_fact("but was", self.actual().clone().collect()) - .do_fail() + let (result, ok) = check_contains_all_of_in_order( + comparison, + self.actual().clone(), + expected_iter, + self.new_result(), + ); + if ok { + result.do_ok() } else { - let missing = comparison.missing; - self.new_result() - .add_fact( - format!("missing ({})", missing.len()), - format!("{:?}", missing), - ) - // Idea: implement near_miss_obj - // .add_fact("tough it did contain", format!("{:?}", near_miss_obj)) - .add_splitter() - .add_formatted_values_fact("expected to contain at least", expected_iter.collect()) - .add_formatted_values_fact("but was", self.actual().clone().collect()) - .do_fail() + result.do_fail() } } @@ -523,6 +502,80 @@ where } } +pub(crate) fn check_contains_exactly_in_order( + comparison: SequenceComparison, + actual: I, + expected_iter: EI, + assertion_result: AssertionResult, +) -> (AssertionResult, bool) +where + AssertionResult: AssertionStrategy, + T: PartialEq + Debug, + EI: Iterator + Clone, + I: Iterator + Clone, +{ + if comparison.contains_exactly() && comparison.order_preserved { + (assertion_result, true) + } else if comparison.contains_exactly() && !comparison.order_preserved { + ( + assertion_result + .add_simple_fact("contents match, but order was wrong") + .add_splitter() + .add_formatted_values_fact("expected", expected_iter.collect()) + .add_formatted_values_fact("actual", actual.collect()), + false, + ) + } else { + ( + feed_facts_about_item_diff(assertion_result, &comparison, actual, expected_iter), + false, + ) + } +} + +pub(crate) fn check_contains_all_of_in_order( + comparison: SequenceComparison, + actual: I, + expected_iter: EI, + assertion_result: AssertionResult, +) -> (AssertionResult, bool) +where + AssertionResult: AssertionStrategy, + T: PartialEq + Debug, + EI: Iterator + Clone, + I: Iterator + Clone, +{ + if comparison.contains_all() && comparison.order_preserved { + (assertion_result, true) + } else if comparison.contains_all() { + ( + assertion_result + .add_simple_fact("required elements were all found, but order was wrong") + .add_formatted_values_fact( + "expected order for required elements", + expected_iter.clone().collect(), + ) + .add_formatted_values_fact("but was", actual.collect()), + false, + ) + } else { + let missing = comparison.missing; + ( + assertion_result + .add_fact( + format!("missing ({})", missing.len()), + format!("{:?}", missing), + ) + // Idea: implement near_miss_obj + // .add_fact("tough it did contain", format!("{:?}", near_miss_obj)) + .add_splitter() + .add_formatted_values_fact("expected to contain at least", expected_iter.collect()) + .add_formatted_values_fact("but was", actual.collect()), + false, + ) + } +} + pub(crate) fn feed_facts_about_item_diff< T: Debug + PartialEq, A: Debug, @@ -681,7 +734,7 @@ mod tests { } #[test] - fn contains_at_least_in_order() { + fn contains_all_of_in_order() { assert_that!(vec![1, 2, 3].iter()).contains_all_of_in_order(vec![].iter()); assert_that!(vec![1, 2, 3].iter()).contains_all_of_in_order(vec![1, 2].iter()); assert_that!(vec![1, 2, 3].iter()).contains_all_of_in_order(vec![2, 3].iter()); diff --git a/src/assertions/map.rs b/src/assertions/map.rs index 36437b4..2165b30 100644 --- a/src/assertions/map.rs +++ b/src/assertions/map.rs @@ -21,7 +21,8 @@ use crate::assertions::iterator::{ check_contains, check_does_not_contain, check_is_empty, check_is_not_empty, }; use crate::base::{AssertionApi, AssertionResult, AssertionStrategy, Subject}; -use crate::diff::map::{MapComparison, MapLike, MapValueDiff}; +use crate::diff::iter::SequenceOrderComparison; +use crate::diff::map::{MapComparison, MapLike, MapValueDiff, OrderedMapLike}; /// Trait for map assertion. /// @@ -133,6 +134,48 @@ where K: 'b; } +/// Trait for ordered map assertion. +/// +/// # Example +/// ``` +/// use std::collections::BTreeMap; +/// use assertor::*; +/// +/// let mut map = BTreeMap::new(); +/// assert_that!(map).is_empty(); +/// +/// map.insert("one", 1); +/// map.insert("two", 2); +/// map.insert("three", 3); +/// +/// assert_that!(map).has_length(3); +/// assert_that!(map).contains_key("one"); +/// assert_that!(map).key_set().contains_exactly(vec!["three","two","one"].iter()); +/// assert_that!(map).contains_all_of_in_order(BTreeMap::from([("one", 1), ("three", 3)])); +/// ``` +pub trait OrderedMapAssertion<'a, K: 'a + Ord + Eq, V, ML, R>: + MapAssertion<'a, K, V, ML, R> +where + AssertionResult: AssertionStrategy, + ML: OrderedMapLike, +{ + /// Checks that the subject exactly contains `expected` in the same order. + fn contains_exactly_in_order(&self, expected: BM) -> R + where + K: Eq + Ord + Debug, + V: Eq + Debug, + OML: OrderedMapLike + 'a, + BM: Borrow + 'a; + + /// Checks that the subject contains at least all elements of `expected` in the same order. + fn contains_all_of_in_order(&self, expected: BM) -> R + where + K: Eq + Ord + Debug, + V: Eq + Debug, + OML: OrderedMapLike + 'a, + BM: Borrow + 'a; +} + impl<'a, K, V, ML, R> MapAssertion<'a, K, V, ML, R> for Subject<'a, ML, (), R> where AssertionResult: AssertionStrategy, @@ -338,6 +381,71 @@ where } } +impl<'a, K, V, ML, R> OrderedMapAssertion<'a, K, V, ML, R> for Subject<'a, ML, (), R> +where + AssertionResult: AssertionStrategy, + K: 'a + Eq + Ord, + ML: OrderedMapLike, +{ + fn contains_exactly_in_order(&self, expected: BM) -> R + where + K: Eq + Ord + Debug, + V: Eq + Debug, + OML: OrderedMapLike + 'a, + BM: Borrow + 'a, + { + let map_diff = MapComparison::from_map_like( + self.actual(), + expected.borrow(), + Some(SequenceOrderComparison::Strict), + ); + let (values_assertion_result, values_different) = + feed_different_values_facts(self.new_result(), &map_diff, false); + let key_order_comparison = map_diff.key_order_comparison.unwrap(); + let (order_assertion_result, order_ok) = super::iterator::check_contains_exactly_in_order( + key_order_comparison, + self.actual().keys().into_iter(), + expected.borrow().keys().into_iter(), + values_assertion_result, + ); + + if order_ok && !values_different { + order_assertion_result.do_ok() + } else { + order_assertion_result.do_fail() + } + } + + fn contains_all_of_in_order(&self, expected: BM) -> R + where + K: Eq + Ord + Debug, + V: Eq + Debug, + OML: OrderedMapLike + 'a, + BM: Borrow + 'a, + { + let map_diff = MapComparison::from_map_like( + self.actual(), + expected.borrow(), + Some(SequenceOrderComparison::Relative), + ); + let (values_assertion_result, values_different) = + feed_different_values_facts(self.new_result(), &map_diff, false); + let key_order_comparison = map_diff.key_order_comparison.unwrap(); + let (order_assertion_result, order_ok) = super::iterator::check_contains_all_of_in_order( + key_order_comparison, + self.actual().keys().into_iter(), + expected.borrow().keys().into_iter(), + values_assertion_result, + ); + + if order_ok && !values_different { + order_assertion_result.do_ok() + } else { + order_assertion_result.do_fail() + } + } +} + fn pluralize<'a>(count: usize, single: &'a str, plural: &'a str) -> &'a str { if count == 1 { single @@ -346,7 +454,7 @@ fn pluralize<'a>(count: usize, single: &'a str, plural: &'a str) -> &'a str { } } -fn feed_different_values_facts( +fn feed_different_values_facts( mut result: AssertionResult, diff: &MapComparison<&K, &V>, splitter: bool, @@ -384,7 +492,7 @@ fn feed_different_values_facts( (result, has_diffs) } -fn feed_missing_entries_facts( +fn feed_missing_entries_facts( containment_spec: &str, mut result: AssertionResult, diff: &MapComparison<&K, &V>, @@ -425,7 +533,7 @@ fn feed_missing_entries_facts( (result, has_diffs) } -fn feed_extra_entries_facts( +fn feed_extra_entries_facts( mut result: AssertionResult, diff: &MapComparison<&K, &V>, splitter: bool, @@ -437,7 +545,7 @@ fn feed_extra_entries_facts( } result = result .add_fact( - format!("expected to not contain additional entries"), + "expected to not contain additional entries".to_string(), format!( "but {} additional {} found", diff.extra.len(), @@ -476,7 +584,7 @@ impl<'a, K: Debug, V: Debug> Debug for MapEntry<'a, K, V> { } } -impl Debug for MapValueDiff<&K, &V> { +impl Debug for MapValueDiff<&K, &V> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_str( format!( @@ -858,4 +966,84 @@ mod tests { assert_that!(tree_map).does_not_contain_any(BTreeMap::from([("world", "nope")])); assert_that!(tree_map).does_not_contain_any(HashMap::from([("world", "nope")])); } + + #[test] + fn contains_exactly_in_order() { + let tree_map = BTreeMap::from([("hello", "sorted_map"), ("world", "in")]); + assert_that!(tree_map) + .contains_exactly_in_order(BTreeMap::from([("hello", "sorted_map"), ("world", "in")])); + + // Wrong value + let result = check_that!(tree_map) + .contains_exactly_in_order(BTreeMap::from([("hello", "wrong"), ("world", "in")])); + assert_that!(result).facts_are_at_least(vec![ + Fact::new( + "expected to contain the same entries", + "but found 1 entry that is different", + ), + Fact::new_splitter(), + Fact::new_multi_value_fact( + r#"key was mapped to unexpected value"#, + vec![r#"{ key: "hello", expected: "sorted_map", actual: "wrong" }"#], + ), + ]); + + // Extra key + let result = check_that!(tree_map) + .contains_exactly_in_order(BTreeMap::from([("hello", "sorted_map"), ("was", "at")])); + assert_that!(result).facts_are(vec![ + Fact::new("missing (1)", r#"["was"]"#), + Fact::new("unexpected (1)", r#"["world"]"#), + Fact::new_splitter(), + Fact::new_multi_value_fact("expected", vec![r#""hello""#, r#""was""#]), + Fact::new_multi_value_fact("actual", vec![r#""hello""#, r#""world""#]), + ]); + + // Extra key and wrong value + let result = check_that!(tree_map) + .contains_exactly_in_order(BTreeMap::from([("hello", "wrong"), ("was", "at")])); + assert_that!(result).facts_are(vec![ + Fact::new( + "expected to contain the same entries", + "but found 1 entry that is different", + ), + Fact::new_splitter(), + Fact::new_multi_value_fact( + r#"key was mapped to unexpected value"#, + vec![r#"{ key: "hello", expected: "sorted_map", actual: "wrong" }"#], + ), + Fact::new("missing (1)", r#"["was"]"#), + Fact::new("unexpected (1)", r#"["world"]"#), + Fact::new_splitter(), + Fact::new_multi_value_fact("expected", vec![r#""hello""#, r#""was""#]), + Fact::new_multi_value_fact("actual", vec![r#""hello""#, r#""world""#]), + ]); + } + + #[test] + fn contains_all_of_in_order() { + let tree_map = BTreeMap::from([("hello", "sorted_map"), ("lang", "en"), ("world", "in")]); + assert_that!(tree_map) + .contains_all_of_in_order(BTreeMap::from([("hello", "sorted_map"), ("world", "in")])); + + // Extra key and wrong value + let result = check_that!(tree_map) + .contains_exactly_in_order(BTreeMap::from([("hello", "wrong"), ("ww", "w")])); + assert_that!(result).facts_are(vec![ + Fact::new( + "expected to contain the same entries", + "but found 1 entry that is different", + ), + Fact::new_splitter(), + Fact::new_multi_value_fact( + r#"key was mapped to unexpected value"#, + vec![r#"{ key: "hello", expected: "sorted_map", actual: "wrong" }"#], + ), + Fact::new("missing (1)", r#"["ww"]"#), + Fact::new("unexpected (2)", r#"["lang", "world"]"#), + Fact::new_splitter(), + Fact::new_multi_value_fact("expected", vec![r#""hello""#, r#""ww""#]), + Fact::new_multi_value_fact("actual", vec![r#""hello""#, r#""lang""#, r#""world""#]), + ]); + } } diff --git a/src/assertions/set.rs b/src/assertions/set.rs index ce55c8c..d4362b5 100644 --- a/src/assertions/set.rs +++ b/src/assertions/set.rs @@ -123,7 +123,98 @@ where } } -trait SetLike { +/// Trait for sorted set assertions. +/// +/// # Example +/// ``` +/// use assertor::*; +/// use std::collections::BTreeSet; +/// +/// let mut set = BTreeSet::new(); +/// assert_that!(set).is_empty(); +/// +/// set.insert("a"); +/// set.insert("b"); +/// set.insert("c"); +/// +/// assert_that!(set).contains("a"); +/// assert_that!(set).has_length(3); +/// assert_that!(set).contains_all_of_in_order(BTreeSet::from(["b", "c"])); +/// ``` +/// ```should_panic +/// use assertor::*; +/// use std::collections::BTreeSet; +/// +/// let mut set = BTreeSet::new(); +/// set.insert("a"); +/// assert_that!(set).contains("z"); +/// // expected to contain : "z" +/// // but did not +/// // though it did contain: ["a"] +/// ``` +pub trait OrderedSetAssertion<'a, ST, T, R>: SetAssertion<'a, ST, T, R> +where + AssertionResult: AssertionStrategy, + T: PartialOrd + Eq + Debug, + ST: OrderedSetLike, +{ + /// Checks that the subject contains at least all elements of `expected` in the same order. + /// + /// # Example + /// ``` + /// use assertor::*; + /// use std::collections::BTreeSet; + /// assert_that!(BTreeSet::from([1, 2, 3])).contains_all_of_in_order(BTreeSet::from([1, 2, 3])); + /// ``` + fn contains_all_of_in_order(&self, expected: OSA) -> R + where + T: PartialOrd + Eq + Debug, + OS: OrderedSetLike, + OSA: Borrow; + + /// Checks that the subject exactly contains elements of `expected` in the same order. + /// + /// # Example + /// ``` + /// use assertor::*; + /// use std::collections::BTreeSet; + /// assert_that!(BTreeSet::from([1, 2, 3])).contains_exactly_in_order(BTreeSet::from([1, 2, 3])); + /// ``` + fn contains_exactly_in_order(&self, expected: OSA) -> R + where + T: PartialOrd + Eq + Debug, + OS: OrderedSetLike, + OSA: Borrow; +} + +impl<'a, T, R, ST> OrderedSetAssertion<'a, ST, T, R> for Subject<'a, ST, (), R> +where + AssertionResult: AssertionStrategy, + T: Eq + PartialOrd + Debug, + ST: OrderedSetLike, +{ + fn contains_all_of_in_order(&self, expected: OSA) -> R + where + T: PartialOrd + Eq + Debug, + OS: OrderedSetLike, + OSA: Borrow, + { + self.new_owned_subject(self.actual().iter(), None, ()) + .contains_all_of_in_order(expected.borrow().iter()) + } + + fn contains_exactly_in_order(&self, expected: OSA) -> R + where + T: PartialOrd + Eq + Debug, + OS: OrderedSetLike, + OSA: Borrow, + { + self.new_owned_subject(self.actual().iter(), None, ()) + .contains_exactly_in_order(expected.borrow().iter()) + } +} + +pub trait SetLike { type It<'a>: Iterator + Clone where T: 'a, @@ -135,6 +226,8 @@ trait SetLike { } } +pub trait OrderedSetLike: SetLike {} + impl SetLike for HashSet { type It<'a> = std::collections::hash_set::Iter<'a, T> where T: 'a, Self: 'a; @@ -151,6 +244,8 @@ impl SetLike for BTreeSet { } } +impl OrderedSetLike for BTreeSet {} + #[cfg(test)] mod tests { use std::iter::FromIterator; @@ -208,4 +303,19 @@ mod tests { assert_that!(btree_set).does_not_contain("nope"); assert_that!(btree_set).does_not_contain_any(vec!["one", "two"]); } + + #[test] + fn contains_all_of_in_order() { + assert_that!(BTreeSet::from([1, 2, 3])).contains_all_of_in_order(BTreeSet::from([])); + assert_that!(BTreeSet::from([1, 2, 3])).contains_all_of_in_order(BTreeSet::from([1, 2])); + assert_that!(BTreeSet::from([1, 2, 3])).contains_all_of_in_order(BTreeSet::from([2, 3])); + assert_that!(BTreeSet::from([1, 2, 3])).contains_all_of_in_order(BTreeSet::from([1, 3])); + assert_that!(BTreeSet::from([1, 2, 3])).contains_all_of_in_order(BTreeSet::from([1, 2, 3])); + } + + #[test] + fn contains_exactly_in_order() { + assert_that!(BTreeSet::from([1, 2, 3])) + .contains_exactly_in_order(BTreeSet::from([1, 2, 3])); + } } diff --git a/src/diff.rs b/src/diff.rs index da5c327..7a335e9 100644 --- a/src/diff.rs +++ b/src/diff.rs @@ -19,14 +19,14 @@ pub(crate) mod map { use std::hash::Hash; /// Difference for a single key in a Map-like data structure. - pub(crate) struct MapValueDiff { + pub(crate) struct MapValueDiff { pub(crate) key: K, pub(crate) actual_value: V, pub(crate) expected_value: V, } /// Disjoint and commonalities representation between two Map-like data structures. - pub(crate) struct MapComparison { + pub(crate) struct MapComparison { pub(crate) extra: Vec<(K, V)>, pub(crate) missing: Vec<(K, V)>, pub(crate) different_values: Vec>, @@ -67,6 +67,8 @@ pub(crate) mod map { fn entries(&self) -> Vec<(&K, &V)>; } + pub trait OrderedMapLike: MapLike {} + impl MapLike for BTreeMap { type It<'a> = std::collections::btree_map::Keys<'a, K, V> where K: 'a, V: 'a; @@ -91,6 +93,8 @@ pub(crate) mod map { } } + impl OrderedMapLike for BTreeMap {} + impl MapLike for HashMap { type It<'a> = std::collections::hash_map::Keys<'a, K, V> where K: 'a, V: 'a; @@ -115,7 +119,7 @@ pub(crate) mod map { } } - impl MapComparison { + impl MapComparison { pub(crate) fn from_map_like<'a, M1, M2>( actual: &'a M1, expected: &'a M2, diff --git a/src/lib.rs b/src/lib.rs index 3c65175..b2adf41 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -54,8 +54,10 @@ pub use assertions::boolean::BooleanAssertion; pub use assertions::float::FloatAssertion; pub use assertions::iterator::IteratorAssertion; pub use assertions::map::MapAssertion; +pub use assertions::map::OrderedMapAssertion; pub use assertions::option::OptionAssertion; pub use assertions::result::ResultAssertion; +pub use assertions::set::OrderedSetAssertion; pub use assertions::set::SetAssertion; pub use assertions::string::StringAssertion; pub use assertions::vec::VecAssertion;