diff --git a/.ide-settings/jetbrains/runConfigurations/Doc.run.xml b/.ide-settings/jetbrains/runConfigurations/Doc.run.xml new file mode 100644 index 0000000..62fb27d --- /dev/null +++ b/.ide-settings/jetbrains/runConfigurations/Doc.run.xml @@ -0,0 +1,19 @@ + + + + \ No newline at end of file diff --git a/README.md b/README.md index 9d311aa..1f9f57a 100644 --- a/README.md +++ b/README.md @@ -457,9 +457,9 @@ for iterators that yield items in a well-defined order. | contains_all_in_order | verify that an iterator/collection contains all the given values and in the given order, possibly with other values between them | | starts_with | verify that an iterator/collection contains the given values as the first elements in order | | ends_with | verify that an iterator/collection contains the given values as the last elements in order | -| first_element | verfiy that an iterator/collection contains at least one element and return a `Spec` containing the first element | -| last_element | verfiy that an iterator/collection contains at least one element and return a `Spec` containing the last element | -| nth_element | verfiy that an iterator/collection contains at least one element and return a `Spec` containing the nth element | +| first_element | verify that an iterator/collection contains at least one element and return a `Spec` containing the first element | +| last_element | verify that an iterator/collection contains at least one element and return a `Spec` containing the last element | +| nth_element | verify that an iterator/collection contains at least one element and return a `Spec` containing the nth element | | elements_at | pick the elements of an iterator/collection at the given positions and return a `Spec` containing the selected elements | ### Maps diff --git a/src/assertions.rs b/src/assertions.rs index e8644f2..2fc626e 100644 --- a/src/assertions.rs +++ b/src/assertions.rs @@ -10,6 +10,7 @@ //! assertions. #![allow(clippy::wrong_self_convention, clippy::return_self_not_must_use)] +use crate::spec::{CollectFailures, GetFailures, Spec}; use crate::std::fmt::Debug; use crate::std::ops::RangeBounds; use crate::std::string::String; @@ -947,7 +948,7 @@ pub trait AssertDecimalNumber { /// # } /// ``` /// - /// Note: `rust_decimal::Decimal` is fixed precision decimal number. The + /// Note: `rust_decimal::Decimal` is a fixed precision decimal number. The /// actual precision is always 29. #[track_caller] fn has_precision_of(self, expected_precision: u64) -> Self; @@ -3969,6 +3970,118 @@ pub trait AssertMapContainsValue { fn does_not_contain_values(self, expected_values: impl IntoIterator) -> Self; } +/// Execute assertions on every element of a collection or iterator. +/// +/// Iterators over the elements of a collection or an iterator and executes one +/// or multiple assertions on each of those elements. The failure report +/// contains detailed information for each element for which one or multiple +/// assertions failed. +pub trait AssertElements<'a, I> +where + I: IntoIterator, +{ + /// A spec-like type that is returned by the methods of this trait. + type Output; + + /// Iterates over the elements of a collection or an iterator and executes + /// the given assertions for each of those elements. If all elements are + /// asserted successfully, the whole assertion succeeds. + /// + /// It iterates over all elements of the collection or iterator and collects + /// the failure messages for those elements where the assertion fails. In + /// other words, it does not stop iterating when the assertion for one + /// element fails. + /// + /// The failure messages contain the position of the element within the + /// collection or iterator. The position is 0-based. So a failure message + /// for the first element contains `[0]`, the second `[1]`, and so on. + /// + /// # Example + /// + /// The following assertion: + /// + /// ```should_panic + /// use asserting::prelude::*; + /// + /// let numbers = [2, 4, 6, 8, 10]; + /// + /// assert_that!(numbers).each_element(|e| + /// e.is_greater_than(2) + /// .is_at_most(7) + /// ); + /// ``` + /// + /// will print: + /// + /// ```console + /// expected numbers [0] to be greater than 2 + /// but was: 2 + /// expected: > 2 + /// + /// expected numbers [3] to be at most 7 + /// but was: 8 + /// expected: <= 7 + /// + /// expected numbers [4] to be at most 7 + /// but was: 10 + /// expected: <= 7 + /// ``` + #[allow(clippy::return_self_not_must_use)] + #[track_caller] + fn each_element(self, assert: A) -> Self::Output + where + A: Fn(Spec<'a, ::Item, CollectFailures>) -> B, + B: GetFailures; + + /// Iterates over the elements of a collection or an iterator and executes + /// the given assertions for each of those elements. If the assertion of any + /// element is successful, the iteration stops and the whole assertion + /// succeeds. + /// + /// If the assertion fails for all elements, the failures of the assertion + /// for all elements are collected. + /// + /// The failure messages contain the position of the element within the + /// collection or iterator. The position is 0-based. So a failure message + /// for the first element contains `[0]`, the second `[1]`, and so on. + /// + /// # Example + /// + /// The following assertion: + /// + /// ```should_panic + /// use asserting::prelude::*; + /// + /// let digit_names = ["one", "two", "three"]; + /// + /// assert_that!(digit_names).any_element(|e| + /// e.contains('x') + /// ); + /// ``` + /// + /// will print: + /// + /// ```console + /// expected digit_names [0] to contain 'x' + /// but was: "one" + /// expected: 'x' + /// + /// expected digit_names [1] to contain 'x' + /// but was: "two" + /// expected: 'x' + /// + /// expected digit_names [2] to contain 'x' + /// but was: "three" + /// expected: 'x' + /// ``` + #[allow(clippy::return_self_not_must_use)] + #[track_caller] + fn any_element(self, assert: A) -> Self::Output + where + A: Fn(Spec<'a, ::Item, CollectFailures>) -> B, + B: GetFailures; +} + /// Filter assertions for elements of a collection or an iterator. /// /// Filtering is used to target the assertions on specific elements of a @@ -4027,8 +4140,6 @@ pub trait AssertFilteredElements { /// /// assert_that!(subject).single_element().is_equal_to("single"); /// ``` - /// - /// [`Spec`]: crate::spec::Spec #[track_caller] fn single_element(self) -> Self::SingleElement; @@ -4051,8 +4162,6 @@ pub trait AssertFilteredElements { /// .single_element() /// .is_equal_to("three"); /// ``` - /// - /// [`Spec`]: crate::spec::Spec #[track_caller] fn filtered_on(self, condition: C) -> Self::MultipleElements where @@ -4107,25 +4216,27 @@ pub trait AssertFilteredElements { P: FnMut(&T) -> bool; } -/// Filter assertions for elements of a collection or an iterator that yields -/// its elements in a defined order. +/// Extract one or multiple elements of a collection or an iterator that yields +/// its elements in a defined order to call assertions on the extracted +/// elements. /// -/// Filtering is used to target the assertions on specific elements of a +/// Extraction is used to target the assertions on specific elements of a /// collection or an iterator, such as the first or last element. /// +/// See also the [`AssertOrderedElementsRef`] trait for extracting multiple +/// elements from the same ordered collection or iterator for individual +/// assertions. +/// /// # Examples /// /// ``` /// use asserting::prelude::*; /// -/// let subject = ["first", "second", "third", "four", "five"]; +/// let subject = ["one", "two", "three", "four", "five"]; /// -/// assert_that!(subject).first_element().is_equal_to("first"); +/// assert_that!(subject).first_element().is_equal_to("one"); /// assert_that!(subject).last_element().is_equal_to("five"); /// assert_that!(subject).nth_element(3).is_equal_to("four"); -/// -/// let subject = ["one", "two", "three", "four", "five"]; -/// /// assert_that!(subject) /// .elements_at([0, 2, 4]) /// .contains_exactly(["one", "three", "five"]); @@ -4150,12 +4261,10 @@ pub trait AssertOrderedElements { /// ``` /// use asserting::prelude::*; /// - /// let subject = ["first", "second", "third"]; + /// let subject = ["one", "two", "three"]; /// - /// assert_that!(subject).first_element().is_equal_to("first"); + /// assert_that!(subject).first_element().is_equal_to("one"); /// ``` - /// - /// [`Spec`]: crate::spec::Spec #[track_caller] fn first_element(self) -> Self::SingleElement; @@ -4167,12 +4276,10 @@ pub trait AssertOrderedElements { /// ``` /// use asserting::prelude::*; /// - /// let subject = ["first", "second", "third"]; + /// let subject = ["one", "two", "three"]; /// - /// assert_that!(subject).last_element().is_equal_to("third"); + /// assert_that!(subject).last_element().is_equal_to("three"); /// ``` - /// - /// [`Spec`]: crate::spec::Spec #[track_caller] fn last_element(self) -> Self::SingleElement; @@ -4186,14 +4293,12 @@ pub trait AssertOrderedElements { /// ``` /// use asserting::prelude::*; /// - /// let subject = ["first", "second", "third"]; + /// let subject = ["one", "two", "three"]; /// - /// assert_that!(subject).nth_element(0).is_equal_to("first"); - /// assert_that!(subject).nth_element(1).is_equal_to("second"); - /// assert_that!(subject).nth_element(2).is_equal_to("third"); + /// assert_that!(subject).nth_element(0).is_equal_to("one"); + /// assert_that!(subject).nth_element(1).is_equal_to("two"); + /// assert_that!(subject).nth_element(2).is_equal_to("three"); /// ``` - /// - /// [`Spec`]: crate::spec::Spec #[track_caller] fn nth_element(self, n: usize) -> Self::SingleElement; @@ -4211,8 +4316,128 @@ pub trait AssertOrderedElements { /// .elements_at([0, 2, 4]) /// .contains_exactly(["one", "three", "five"]); /// ``` - /// - /// [`Spec`]: crate::spec::Spec #[track_caller] fn elements_at(self, indices: impl IntoIterator) -> Self::MultipleElements; } + +/// Extract one or multiple elements of a collection or an iterator that yields +/// its elements in a defined order to call assertions on the extracted +/// elements. +/// +/// Extraction is used to target the assertions on specific elements of a +/// collection or an iterator, such as the first or last element. +/// +/// The methods of this trait can be used in combination with the `and` method +/// to extract multiple elements from a collection for individual assertions. +/// The downside is that the elements of the collection or iterator must +/// implement the `ToOwned` trait. +/// +/// If you only want to call a single extraction method on the same collection +/// or iterator, you can use the methods of the [`AssertOrderedElements`] trait. +/// These methods do not require that the elements of the collection implement +/// `ToOwned` or `Clone`. +/// +/// # Examples +/// +/// ``` +/// use asserting::prelude::*; +/// +/// let subject = ["one", "two", "three", "four", "five"]; +/// +/// assert_that!(subject) +/// .first_element_ref().is_equal_to("one") +/// .and() +/// .last_element_ref().is_equal_to("five") +/// .and() +/// .nth_element_ref(3).is_equal_to("four") +/// .and() +/// .elements_ref_at([0, 2, 4]).contains_exactly(["one", "three", "five"]); +/// ``` +pub trait AssertOrderedElementsRef { + /// A spec-like type that contains a single element as the subject that is + /// extracted from the iterator. + /// + /// Usually this is a `Spec<'a, T, R>`. + type SingleElement; + /// A spec-like type that contains multiple or all elements of an iterator + /// as the subject. + /// + /// Usually this is a `Spec<'a, Vec, R>`. + type MultipleElements; + + /// Verify that a collection or an iterator contains at least one element + /// and return a [`Spec`] for the first element. + /// + /// # Examples + /// + /// ``` + /// use asserting::prelude::*; + /// + /// let subject = ["one", "two", "three"]; + /// + /// assert_that!(subject) + /// .first_element_ref().is_equal_to("one") + /// .and() + /// .last_element_ref().is_equal_to("three"); + /// ``` + #[track_caller] + fn first_element_ref(self) -> Self::SingleElement; + + /// Verify that a collection or an iterator contains at least one element + /// and return a [`Spec`] for the last element. + /// + /// # Examples + /// + /// ``` + /// use asserting::prelude::*; + /// + /// let subject = ["one", "two", "three"]; + /// + /// assert_that!(subject) + /// .last_element_ref().is_equal_to("three") + /// .and() + /// .first_element_ref().is_equal_to("one"); + /// ``` + #[track_caller] + fn last_element_ref(self) -> Self::SingleElement; + + /// Verify that a collection or an iterator contains at least n + 1 elements + /// and return a [`Spec`] for the nth element. + /// + /// The index n is zero-based (similar to the `nth` method of iterators). + /// + /// # Examples + /// + /// ``` + /// use asserting::prelude::*; + /// + /// let subject = ["one", "two", "three"]; + /// + /// assert_that!(subject) + /// .nth_element_ref(0).is_equal_to("one") + /// .and() + /// .nth_element_ref(1).is_equal_to("two") + /// .and() + /// .nth_element_ref(2).is_equal_to("three"); + /// ``` + #[track_caller] + fn nth_element_ref(self, n: usize) -> Self::SingleElement; + + /// Pick the elements of a collection or an iterator at the given positions + /// and return a [`Spec`] only containing the selected elements. + /// + /// # Examples + /// + /// ``` + /// use asserting::prelude::*; + /// + /// let subject = ["one", "two", "three", "four", "five"]; + /// + /// assert_that!(subject) + /// .elements_ref_at([0, 2, 4]).contains_exactly(["one", "three", "five"]) + /// .and() + /// .elements_ref_at([1, 3]).contains_exactly(["two", "four"]); + /// ``` + #[track_caller] + fn elements_ref_at(self, indices: impl IntoIterator) -> Self::MultipleElements; +} diff --git a/src/boolean/mod.rs b/src/boolean/mod.rs index 480420b..6f91481 100644 --- a/src/boolean/mod.rs +++ b/src/boolean/mod.rs @@ -3,7 +3,9 @@ use crate::assertions::AssertBoolean; use crate::colored::{mark_missing, mark_unexpected}; use crate::expectations::{IsFalse, IsTrue, is_false, is_true}; -use crate::spec::{DiffFormat, Expectation, Expression, FailingStrategy, Invertible, Spec}; +use crate::spec::{ + DiffFormat, Expectation, Expecting, Expression, FailingStrategy, Invertible, Spec, +}; use crate::std::format; use crate::std::string::String; diff --git a/src/char/mod.rs b/src/char/mod.rs index a4fedab..907151c 100644 --- a/src/char/mod.rs +++ b/src/char/mod.rs @@ -5,7 +5,9 @@ use crate::expectations::{ IsWhitespace, is_alphabetic, is_alphanumeric, is_ascii, is_control_char, is_digit, is_lower_case, is_upper_case, is_whitespace, }; -use crate::spec::{DiffFormat, Expectation, Expression, FailingStrategy, Invertible, Spec}; +use crate::spec::{ + DiffFormat, Expectation, Expecting, Expression, FailingStrategy, Invertible, Spec, +}; use crate::std::format; use crate::std::string::{String, ToString}; diff --git a/src/char_count.rs b/src/char_count.rs index 8196ba2..1029e0e 100644 --- a/src/char_count.rs +++ b/src/char_count.rs @@ -8,7 +8,7 @@ use crate::expectations::{ has_char_count, has_char_count_greater_than, has_char_count_in_range, has_char_count_less_than, }; use crate::properties::CharCountProperty; -use crate::spec::{DiffFormat, Expectation, Expression, FailingStrategy, Spec}; +use crate::spec::{DiffFormat, Expectation, Expecting, Expression, FailingStrategy, Spec}; use crate::std::fmt::Debug; use crate::std::format; use crate::std::ops::RangeBounds; diff --git a/src/derived_spec/mod.rs b/src/derived_spec/mod.rs new file mode 100644 index 0000000..4dddb4a --- /dev/null +++ b/src/derived_spec/mod.rs @@ -0,0 +1,1776 @@ +//! Defines the [`DerivedSpec`], which keeps track of the original subject while doing assertions +//! on a derived subject. + +use crate::assertions::{ + AssertBoolean, AssertChar, AssertDebugString, AssertDecimalNumber, AssertDisplayString, + AssertElements, AssertEmptiness, AssertEquality, AssertErrorHasSource, AssertHasCharCount, + AssertHasDebugString, AssertHasDisplayString, AssertHasError, AssertHasErrorMessage, + AssertHasLength, AssertHasValue, AssertInRange, AssertInfinity, AssertIteratorContains, + AssertIteratorContainsInAnyOrder, AssertIteratorContainsInOrder, AssertMapContainsKey, + AssertMapContainsValue, AssertNotANumber, AssertNumericIdentity, AssertOption, + AssertOptionValue, AssertOrder, AssertOrderedElements, AssertOrderedElementsRef, AssertResult, + AssertResultValue, AssertSameAs, AssertSignum, AssertStringContainsAnyOf, AssertStringPattern, +}; +use crate::expectations::{ + error_has_source, error_has_source_message, has_at_least_char_count, has_at_least_length, + has_at_least_number_of_elements, has_at_most_char_count, has_at_most_length, has_char_count, + has_char_count_greater_than, has_char_count_in_range, has_char_count_less_than, + has_debug_string, has_display_string, has_error, has_length, has_length_greater_than, + has_length_in_range, has_length_less_than, has_precision_of, has_scale_of, has_value, + is_a_number, is_after, is_alphabetic, is_alphanumeric, is_ascii, is_at_least, is_at_most, + is_before, is_between, is_control_char, is_digit, is_empty, is_equal_to, is_err, is_false, + is_finite, is_greater_than, is_in_range, is_infinite, is_integer, is_less_than, is_lower_case, + is_negative, is_none, is_ok, is_one, is_positive, is_same_as, is_some, is_true, is_upper_case, + is_whitespace, is_zero, iterator_contains, iterator_contains_all_in_order, + iterator_contains_all_of, iterator_contains_any_of, iterator_contains_exactly, + iterator_contains_exactly_in_any_order, iterator_contains_only, iterator_contains_only_once, + iterator_contains_sequence, iterator_ends_with, iterator_starts_with, + map_contains_exactly_keys, map_contains_key, map_contains_keys, map_contains_value, + map_contains_values, map_does_not_contain_keys, map_does_not_contain_values, not, satisfies, + string_contains, string_contains_any_of, string_ends_with, string_starts_with, +}; +use crate::properties::{ + AdditiveIdentityProperty, CharCountProperty, DecimalProperties, DefinedOrderProperty, + InfinityProperty, IsEmptyProperty, IsNanProperty, LengthProperty, MapProperties, + MultiplicativeIdentityProperty, SignumProperty, +}; +use crate::spec::{ + And, AssertFailure, CollectFailures, DiffFormat, DoFail, Expectation, Expecting, Expression, + FailingStrategy, GetFailures, GetLocation, Location, PanicOnFail, Satisfies, SoftPanic, Spec, +}; +use crate::std::borrow::{Cow, ToOwned}; +use crate::std::error::Error; +use crate::std::fmt::{Debug, Display}; +use crate::std::format; +use crate::std::ops::RangeBounds; +use crate::std::slice; +use crate::std::string::{String, ToString}; +use crate::std::vec::Vec; +use hashbrown::HashSet; + +/// A `DerivedSpec` does assertions on a derived subject while keeping track +/// of the original subject. +/// +/// It has similar functionality to a [`Spec`], but additionally holds the +/// original subject. Calling the `and` method switches the subject back to the +/// original subject. +/// +/// The derived subject can have its own name and diff format in failure +/// reports. +/// +/// [`Spec`]: Spec +pub struct DerivedSpec<'a, O, S> { + original: O, + subject: S, + expression: Expression<'a>, + diff_format: DiffFormat, +} + +impl DerivedSpec<'_, O, S> { + /// Returns the expression (or subject name) if one has been set. + pub fn expression(&self) -> &Expression<'_> { + &self.expression + } + + /// Returns the diff format used with this assertion. + pub const fn diff_format(&self) -> &DiffFormat { + &self.diff_format + } +} + +impl<'a, O, S> DerivedSpec<'a, O, S> { + #[must_use = "a derived spec does nothing unless an assertion method is called"] + pub(crate) fn new( + original: O, + derived_subject: S, + expression: Expression<'a>, + diff_format: DiffFormat, + ) -> Self { + Self { + original, + subject: derived_subject, + expression, + diff_format, + } + } + + /// Sets the subject name or expression for this assertion. + #[must_use = "a derived spec does nothing unless an assertion method is called"] + pub fn named(mut self, subject_name: impl Into>) -> Self { + self.expression = Expression(subject_name.into()); + self + } + + /// Sets the diff format used to highlight differences between the actual + /// value and the expected value. + /// + /// Note: This method must be called before an assertion method is called to + /// affect the failure message of the assertion as failure messages are + /// formatted immediately when an assertion is executed. + #[must_use = "a spec does nothing unless an assertion method is called"] + pub const fn with_diff_format(mut self, diff_format: DiffFormat) -> Self { + self.diff_format = diff_format; + self + } +} + +impl<'a, O, S> GetLocation<'a> for DerivedSpec<'a, O, S> +where + O: GetLocation<'a>, +{ + fn location(&self) -> Option> { + self.original.location() + } +} + +impl GetFailures for DerivedSpec<'_, O, S> +where + O: GetFailures, +{ + fn has_failures(&self) -> bool { + self.original.has_failures() + } + + fn failures(&self) -> Vec { + self.original.failures() + } + + fn display_failures(&self) -> Vec { + self.original.display_failures() + } +} + +impl DoFail for DerivedSpec<'_, O, S> +where + O: DoFail, +{ + fn do_fail_with(&mut self, failures: impl IntoIterator) { + self.original.do_fail_with(failures); + } + + fn do_fail_with_message(&mut self, message: impl Into) { + self.original.do_fail_with_message(message); + } +} + +impl SoftPanic for DerivedSpec<'_, O, S> +where + O: SoftPanic, +{ + fn soft_panic(&self) { + self.original.soft_panic(); + } +} + +impl And for DerivedSpec<'_, O, S> { + type Output = O; + + fn and(self) -> Self::Output { + self.original + } +} + +impl<'a, O, S> DerivedSpec<'a, O, S> { + /// Extracts a property from the current subject. + /// + /// The extracting closure gets a reference to the current subject as an + /// argument and should return a reference to the extracted property. The + /// given property name is used in failure reports for referencing the + /// property for which an assertion fails. + /// + /// Use this method if you want to extract multiple properties from the + /// same subject for individual assertions on each of these properties. + /// To extract another property from the previous subject, call the `and` + /// method to switch back to the previous subject before calling + /// `extracting_ref` for the other property. + /// + /// # Arguments + /// + /// * `property_name` - A name describing the extracted property used for + /// referencing that property in failure reports. + /// * `extract` - A closure that returns a reference to the property to be + /// extracted. + /// + /// # Examples + /// + /// ``` + /// use asserting::prelude::*; + /// + /// #[derive(Debug, Clone)] + /// struct Item { + /// name: String, + /// price: f32, + /// quantity: u32, + /// } + /// + /// struct Order { + /// id: String, + /// items: Vec, + /// } + /// + /// let my_order = Order { + /// id: "O261234".into(), + /// items: vec![ + /// Item { + /// name: "Apple".into(), + /// price: 0.99, + /// quantity: 6, + /// }, + /// Item { + /// name: "Orange".into(), + /// price: 1.99, + /// quantity: 4, + /// }, + /// ], + /// }; + /// + /// assert_that!(my_order) + /// .extracting_ref("items", |o| &o.items) + /// .extracting_ref("[0].name", |i| &i[0].name) + /// .is_equal_to("Apple") + /// .and() + /// .extracting_ref("[1].name", |i| &i[1].name) + /// .is_equal_to("Orange") + /// .and() + /// .extracting_ref("[1].quantity", |i| &i[1].quantity) + /// .is_equal_to(4) + /// .and() // switches back to `my_order.items` + /// .and() // second call to `and()` switches back to `my_order` + /// .extracting_ref("id", |o| &o.id) + /// .is_equal_to("O261234"); + /// ``` + /// + /// Hint: To avoid having to call the `and()` method two or more times, it + /// is recommended to first extract all properties from the higher level + /// subject and then extract fields from deeper down in the hierarchy. + /// + /// ``` + /// # use asserting::prelude::*; + /// # + /// # #[derive(Debug, Clone)] + /// # struct Item { + /// # name: String, + /// # price: f32, + /// # quantity: u32, + /// # } + /// # + /// # struct Order { + /// # id: String, + /// # items: Vec, + /// # } + /// # + /// # let my_order = Order { + /// # id: "O261234".into(), + /// # items: vec![ + /// # Item { + /// # name: "Apple".into(), + /// # price: 0.99, + /// # quantity: 6, + /// # }, + /// # Item { + /// # name: "Orange".into(), + /// # price: 1.99, + /// # quantity: 4, + /// # }, + /// # ], + /// # }; + /// # + /// assert_that!(my_order) + /// .extracting_ref("id", |o| &o.id) + /// .is_equal_to("O261234") + /// .and() + /// .extracting_ref("items", |o| &o.items) + /// .extracting_ref("[0].name", |i| &i[0].name) + /// .is_equal_to("Apple") + /// .and() + /// .extracting_ref("[1].name", |i| &i[1].name) + /// .is_equal_to("Orange") + /// .and() + /// .extracting_ref("[1].quantity", |i| &i[1].quantity) + /// .is_equal_to(4); + /// ``` + #[must_use = "a derived spec does nothing unless an assertion method is called"] + pub fn extracting_ref( + self, + property_name: impl Into>, + extract: F, + ) -> DerivedSpec<'a, Self, U> + where + F: FnOnce(&S) -> &B, + B: ToOwned + ?Sized, + { + let extracted = extract(&self.subject).to_owned(); + let expression = Expression(property_name.into()); + let diff_format = self.diff_format.clone(); + DerivedSpec { + original: self, + subject: extracted, + expression, + diff_format, + } + } + + /// Maps the current subject to some other value. + /// + /// It takes a closure that maps the current subject to a new subject and + /// returns a new `DerivedSpec` with the value returned by the closure as + /// the new subject. The new subject may have a different type than the + /// original subject. All other data like description, location, and diff + /// format are taken over from this `DerivedSpec` into the returned + /// `DerivedSpec`. + /// + /// This method is useful when having a custom type, and one specific + /// property of this type shall be asserted only. If you want to assert + /// multiple properties of the same subject, use the [`extracting_ref`] + /// method instead. + /// + /// This method is similar to the [`mapping`] method. In contrast to + /// [`mapping`], this method does not copy the subject's name + /// (or expression) but resets it to the default "subject". The idea is + /// that the "extracted" property is most likely a different subject than + /// the original one. + /// + /// It is recommended to give the extracted property a specific name by + /// calling the `named` method. This helps with spotting the cause of a + /// failing assertion. + /// + /// This method does not memorize the current subject. Calling `and` on the + /// extracted property switches back to the original subject of this + /// `DerivedSpec`. The current subject is omitted. So, `and` always switches + /// back to the subject before the last `extracting_ref` call. + /// + /// # Example + /// + /// ``` + /// use asserting::prelude::*; + /// + /// #[derive(Debug, Clone)] + /// struct Item { + /// name: String, + /// price: f32, + /// quantity: u32, + /// } + /// + /// struct Order { + /// id: String, + /// items: Vec, + /// } + /// + /// let my_order = Order { + /// id: "O261234".into(), + /// items: vec![ + /// Item { + /// name: "Apple".into(), + /// price: 0.99, + /// quantity: 6, + /// }, + /// Item { + /// name: "Orange".into(), + /// price: 1.99, + /// quantity: 4, + /// }, + /// ], + /// }; + /// + /// assert_that!(my_order) + /// .extracting_ref("items", |o| &o.items) + /// .extracting(|i| i[0].name.clone()) + /// .is_equal_to("Apple") + /// .and() // switches back to `my_order` not `my_order.items` + /// .extracting(|o| o.id) + /// .is_equal_to("O261234"); + /// ``` + /// + /// [`extracting_ref`]: Self::extracting_ref + /// [`mapping`]: Self::mapping + #[must_use = "a derived spec does nothing unless an assertion method is called"] + pub fn extracting(self, extract: F) -> DerivedSpec<'a, O, U> + where + F: FnOnce(S) -> U, + { + let extracted = extract(self.subject); + let diff_format = self.diff_format.clone(); + DerivedSpec { + original: self.original, + subject: extracted, + expression: Expression::default(), + diff_format, + } + } + + /// Maps the current subject to some other value. + /// + /// It takes a closure that maps the current subject to a new subject and + /// returns a new `DerivedSpec` with the value returned by the closure as + /// the new subject. The new subject may have a different type than the + /// original subject. All other data like expression, description, and + /// location are taken over from this `DerivedSpec` into the returned + /// `DerivedSpec`. + /// + /// This method is useful if some type does not implement a trait required + /// for an assertion. + /// + /// `DerivedSpec` also provides the [`extracting()`](DerivedSpec::extracting) + /// method, which is similar to this method. In contrast to this method, + /// [`extracting()`](DerivedSpec::extracting) does not copy the subject's + /// name (or expression) but resets it to the default "subject". + /// + /// # Example + /// + /// ``` + /// use asserting::prelude::*; + /// + /// #[derive(Clone, Copy)] + /// struct Point { + /// x: i64, + /// y: i64, + /// } + /// + /// struct Line { + /// a: Point, + /// b: Point, + /// } + /// + /// let line = Line { + /// a: Point { x: 12, y: -64 }, + /// b: Point { x: -28, y: 17 }, + /// }; + /// + /// assert_that!(line) + /// .extracting_ref("a", |l| &l.a) + /// .mapping(|p| (p.x, p.y)) + /// .is_equal_to((12, -64)) + /// .and() + /// .extracting_ref("b", |l| &l.b) + /// .mapping(|p| (p.x, p.y)) + /// .is_equal_to((-28, 17)); + /// ``` + /// + /// The custom type `Point` does not implement the `PartialEq` trait nor + /// the `Debug` trait, which are both required for an `is_equal_to` + /// assertion. So we map the subject of the type `Point` to a tuple of its + /// fields. + #[must_use = "a derived spec does nothing unless an assertion method is called"] + pub fn mapping(self, map: F) -> DerivedSpec<'a, O, U> + where + F: FnOnce(S) -> U, + { + let mapped = map(self.subject); + DerivedSpec { + original: self.original, + subject: mapped, + expression: self.expression, + diff_format: self.diff_format, + } + } +} + +impl<'a, O, I> DerivedSpec<'a, O, I> +where + I: IntoIterator, +{ + pub(crate) fn extracting_ref_iter( + self, + property_name: impl Into>, + extract: F, + ) -> DerivedSpec<'a, DerivedSpec<'a, O, Vec<::Item>>, Vec> + where + for<'b> F: Fn(slice::Iter<'b, ::Item>) -> Vec, + { + let property_name = Expression(property_name.into()); + let diff_format = self.diff_format.clone(); + let orig_spec = self.mapping(Vec::from_iter); + let new_subject = extract(orig_spec.subject.iter()); + DerivedSpec { + original: orig_spec, + subject: new_subject, + expression: property_name, + diff_format, + } + } +} + +impl Satisfies for DerivedSpec<'_, O, S> +where + O: DoFail, +{ + fn satisfies

(self, predicate: P) -> Self + where + P: Fn(&S) -> bool, + { + self.expecting(satisfies(predicate)) + } + + fn satisfies_with_message

(self, message: impl Into, predicate: P) -> Self + where + P: Fn(&S) -> bool, + { + self.expecting(satisfies(predicate).with_message(message)) + } +} + +impl Expecting for DerivedSpec<'_, O, S> +where + O: DoFail, +{ + fn expecting(mut self, mut expectation: impl Expectation) -> Self { + if !expectation.test(&self.subject) { + let message = + expectation.message(&self.expression, &self.subject, false, &self.diff_format); + self.do_fail_with_message(message); + } + self + } +} + +impl AssertEquality for DerivedSpec<'_, O, S> +where + S: PartialEq + Debug, + E: Debug, + O: DoFail, +{ + fn is_equal_to(self, expected: E) -> Self { + self.expecting(is_equal_to(expected)) + } + + fn is_not_equal_to(self, expected: E) -> Self { + self.expecting(not(is_equal_to(expected))) + } +} + +impl AssertSameAs for DerivedSpec<'_, O, S> +where + S: PartialEq + Debug, + O: DoFail, +{ + fn is_same_as(self, expected: S) -> Self { + self.expecting(is_same_as(expected)) + } + + fn is_not_same_as(self, expected: S) -> Self { + self.expecting(not(is_same_as(expected))) + } +} + +#[cfg(feature = "float-cmp")] +mod float_cmp { + use super::DerivedSpec; + use crate::assertions::{AssertIsCloseToWithDefaultMargin, AssertIsCloseToWithinMargin}; + use crate::expectations::{is_close_to, not}; + use crate::spec::{DoFail, Expecting}; + use float_cmp::{F32Margin, F64Margin}; + + impl AssertIsCloseToWithinMargin for DerivedSpec<'_, O, f32> + where + O: DoFail, + { + fn is_close_to_with_margin(self, expected: f32, margin: impl Into) -> Self { + self.expecting(is_close_to(expected).within_margin(margin)) + } + + fn is_not_close_to_with_margin(self, expected: f32, margin: impl Into) -> Self { + self.expecting(not(is_close_to(expected).within_margin(margin))) + } + } + + impl AssertIsCloseToWithDefaultMargin for DerivedSpec<'_, O, f32> + where + O: DoFail, + { + fn is_close_to(self, expected: f32) -> Self { + self.expecting(is_close_to(expected).within_margin((4. * f32::EPSILON, 4))) + } + + fn is_not_close_to(self, expected: f32) -> Self { + self.expecting(not( + is_close_to(expected).within_margin((4. * f32::EPSILON, 4)) + )) + } + } + + impl AssertIsCloseToWithinMargin for DerivedSpec<'_, O, f64> + where + O: DoFail, + { + fn is_close_to_with_margin(self, expected: f64, margin: impl Into) -> Self { + self.expecting(is_close_to(expected).within_margin(margin)) + } + + fn is_not_close_to_with_margin(self, expected: f64, margin: impl Into) -> Self { + self.expecting(not(is_close_to(expected).within_margin(margin))) + } + } + + impl AssertIsCloseToWithDefaultMargin for DerivedSpec<'_, O, f64> + where + O: DoFail, + { + fn is_close_to(self, expected: f64) -> Self { + self.expecting(is_close_to(expected).within_margin((4. * f64::EPSILON, 4))) + } + + fn is_not_close_to(self, expected: f64) -> Self { + self.expecting(not( + is_close_to(expected).within_margin((4. * f64::EPSILON, 4)) + )) + } + } +} + +impl AssertOrder for DerivedSpec<'_, O, S> +where + S: PartialOrd + Debug, + E: Debug, + O: DoFail, +{ + fn is_less_than(self, expected: E) -> Self { + self.expecting(is_less_than(expected)) + } + + fn is_greater_than(self, expected: E) -> Self { + self.expecting(is_greater_than(expected)) + } + + fn is_at_most(self, expected: E) -> Self { + self.expecting(is_at_most(expected)) + } + + fn is_at_least(self, expected: E) -> Self { + self.expecting(is_at_least(expected)) + } + + fn is_before(self, expected: E) -> Self { + self.expecting(is_before(expected)) + } + + fn is_after(self, expected: E) -> Self { + self.expecting(is_after(expected)) + } + + fn is_between(self, min: E, max: E) -> Self { + self.expecting(is_between(min, max)) + } +} + +impl AssertInRange for DerivedSpec<'_, O, S> +where + S: PartialOrd + Debug, + E: PartialOrd + Debug, + O: DoFail, +{ + fn is_in_range(self, range: R) -> Self + where + R: RangeBounds + Debug, + { + self.expecting(is_in_range(range)) + } + + fn is_not_in_range(self, range: R) -> Self + where + R: RangeBounds + Debug, + { + self.expecting(not(is_in_range(range))) + } +} + +impl AssertNumericIdentity for DerivedSpec<'_, O, S> +where + S: AdditiveIdentityProperty + MultiplicativeIdentityProperty + PartialEq + Debug, + O: DoFail, +{ + fn is_zero(self) -> Self { + self.expecting(is_zero()) + } + + fn is_one(self) -> Self { + self.expecting(is_one()) + } +} + +impl AssertSignum for DerivedSpec<'_, O, S> +where + S: SignumProperty + Debug, + O: DoFail, +{ + fn is_negative(self) -> Self { + self.expecting(is_negative()) + } + + fn is_not_negative(self) -> Self { + self.expecting(not(is_negative())) + } + + fn is_positive(self) -> Self { + self.expecting(is_positive()) + } + + fn is_not_positive(self) -> Self { + self.expecting(not(is_positive())) + } +} + +impl AssertInfinity for DerivedSpec<'_, O, S> +where + S: InfinityProperty + Debug, + O: DoFail, +{ + fn is_infinite(self) -> Self { + self.expecting(is_infinite()) + } + + fn is_finite(self) -> Self { + self.expecting(is_finite()) + } +} + +impl AssertNotANumber for DerivedSpec<'_, O, S> +where + S: IsNanProperty + Debug, + O: DoFail, +{ + fn is_not_a_number(self) -> Self { + self.expecting(not(is_a_number())) + } + + fn is_a_number(self) -> Self { + self.expecting(is_a_number()) + } +} + +impl AssertDecimalNumber for DerivedSpec<'_, O, S> +where + S: DecimalProperties + Debug, + O: DoFail, +{ + fn has_scale_of(self, expected_scale: i64) -> Self { + self.expecting(has_scale_of(expected_scale)) + } + + fn has_precision_of(self, expected_precision: u64) -> Self { + self.expecting(has_precision_of(expected_precision)) + } + + fn is_integer(self) -> Self { + self.expecting(is_integer()) + } +} + +impl AssertBoolean for DerivedSpec<'_, O, bool> +where + O: DoFail, +{ + fn is_true(self) -> Self { + self.expecting(is_true()) + } + + fn is_false(self) -> Self { + self.expecting(is_false()) + } +} + +impl AssertChar for DerivedSpec<'_, O, char> +where + O: DoFail, +{ + fn is_lowercase(self) -> Self { + self.expecting(is_lower_case()) + } + + fn is_uppercase(self) -> Self { + self.expecting(is_upper_case()) + } + + fn is_ascii(self) -> Self { + self.expecting(is_ascii()) + } + + fn is_alphabetic(self) -> Self { + self.expecting(is_alphabetic()) + } + + fn is_alphanumeric(self) -> Self { + self.expecting(is_alphanumeric()) + } + + fn is_control_char(self) -> Self { + self.expecting(is_control_char()) + } + + fn is_digit(self, radix: u32) -> Self { + self.expecting(is_digit(radix)) + } + + fn is_whitespace(self) -> Self { + self.expecting(is_whitespace()) + } +} + +impl AssertChar for DerivedSpec<'_, O, &char> +where + O: DoFail, +{ + fn is_lowercase(self) -> Self { + self.expecting(is_lower_case()) + } + + fn is_uppercase(self) -> Self { + self.expecting(is_upper_case()) + } + + fn is_ascii(self) -> Self { + self.expecting(is_ascii()) + } + + fn is_alphabetic(self) -> Self { + self.expecting(is_alphabetic()) + } + + fn is_alphanumeric(self) -> Self { + self.expecting(is_alphanumeric()) + } + + fn is_control_char(self) -> Self { + self.expecting(is_control_char()) + } + + fn is_digit(self, radix: u32) -> Self { + self.expecting(is_digit(radix)) + } + + fn is_whitespace(self) -> Self { + self.expecting(is_whitespace()) + } +} + +impl AssertEmptiness for DerivedSpec<'_, O, S> +where + S: IsEmptyProperty + Debug, + O: DoFail, +{ + fn is_empty(self) -> Self { + self.expecting(is_empty()) + } + + fn is_not_empty(self) -> Self { + self.expecting(not(is_empty())) + } +} + +impl AssertHasLength for DerivedSpec<'_, O, S> +where + S: LengthProperty + Debug, + O: DoFail, +{ + fn has_length(self, expected_length: usize) -> Self { + self.expecting(has_length(expected_length)) + } + + fn has_length_in_range(self, expected_range: R) -> Self + where + R: RangeBounds + Debug, + { + self.expecting(has_length_in_range(expected_range)) + } + + fn has_length_less_than(self, expected_length: usize) -> Self { + self.expecting(has_length_less_than(expected_length)) + } + + fn has_length_greater_than(self, expected_length: usize) -> Self { + self.expecting(has_length_greater_than(expected_length)) + } + + fn has_at_most_length(self, expected_length: usize) -> Self { + self.expecting(has_at_most_length(expected_length)) + } + + fn has_at_least_length(self, expected_length: usize) -> Self { + self.expecting(has_at_least_length(expected_length)) + } +} + +impl AssertHasCharCount for DerivedSpec<'_, O, S> +where + S: CharCountProperty + Debug, + O: DoFail, +{ + fn has_char_count(self, expected_char_count: usize) -> Self { + self.expecting(has_char_count(expected_char_count)) + } + + fn has_char_count_in_range(self, expected_range: U) -> Self + where + U: RangeBounds + Debug, + { + self.expecting(has_char_count_in_range(expected_range)) + } + + fn has_char_count_less_than(self, expected_char_count: usize) -> Self { + self.expecting(has_char_count_less_than(expected_char_count)) + } + + fn has_char_count_greater_than(self, expected_char_count: usize) -> Self { + self.expecting(has_char_count_greater_than(expected_char_count)) + } + + fn has_at_most_char_count(self, expected_char_count: usize) -> Self { + self.expecting(has_at_most_char_count(expected_char_count)) + } + + fn has_at_least_char_count(self, expected_char_count: usize) -> Self { + self.expecting(has_at_least_char_count(expected_char_count)) + } +} + +impl AssertOption for DerivedSpec<'_, O, Option> +where + S: Debug, + O: DoFail, +{ + fn is_some(self) -> Self { + self.expecting(is_some()) + } + + fn is_none(self) -> Self { + self.expecting(is_none()) + } +} + +impl<'a, O, T> AssertOptionValue for DerivedSpec<'a, O, Option> +where + O: DoFail, +{ + type Some = DerivedSpec<'a, O, T>; + + fn some(self) -> Self::Some { + self.mapping(|subject| match subject { + None => { + panic!("expected the subject to be `Some(_)`, but was `None`") + }, + Some(value) => value, + }) + } +} + +impl<'a, O, T> AssertOptionValue for DerivedSpec<'a, O, &'a Option> +where + T: 'a, + O: DoFail, +{ + type Some = DerivedSpec<'a, O, &'a T>; + + fn some(self) -> Self::Some { + self.mapping(|subject| match subject { + None => { + panic!("expected the subject to be `Some(_)`, but was `None`") + }, + Some(value) => value, + }) + } +} + +impl AssertHasValue for DerivedSpec<'_, O, Option> +where + T: PartialEq + Debug, + E: Debug, + O: DoFail, +{ + fn has_value(self, expected: E) -> Self { + self.expecting(has_value(expected)) + } +} + +impl AssertHasValue for DerivedSpec<'_, O, &Option> +where + T: PartialEq + Debug, + E: Debug, + O: DoFail, +{ + fn has_value(self, expected: E) -> Self { + self.expecting(has_value(expected)) + } +} + +impl AssertResult for DerivedSpec<'_, O, Result> +where + T: Debug, + E: Debug, + O: DoFail, +{ + fn is_ok(self) -> Self { + self.expecting(is_ok()) + } + + fn is_err(self) -> Self { + self.expecting(is_err()) + } +} + +impl AssertResult for DerivedSpec<'_, O, &Result> +where + T: Debug, + E: Debug, + O: DoFail, +{ + fn is_ok(self) -> Self { + self.expecting(is_ok()) + } + + fn is_err(self) -> Self { + self.expecting(is_err()) + } +} + +impl<'a, O, T, E> AssertResultValue for DerivedSpec<'a, O, Result> +where + T: Debug, + E: Debug, + O: DoFail, +{ + type Ok = DerivedSpec<'a, O, T>; + type Err = DerivedSpec<'a, O, E>; + + fn ok(self) -> Self::Ok { + self.mapping(|subject| match subject { + Ok(value) => value, + Err(error) => { + panic!("expected the subject to be `Ok(_)`, but was `Err({error:?})`") + }, + }) + } + + fn err(self) -> Self::Err { + self.mapping(|subject| match subject { + Ok(value) => { + panic!("expected the subject to be `Err(_)`, but was `Ok({value:?})`") + }, + Err(error) => error, + }) + } +} + +impl<'a, O, T, E> AssertResultValue for DerivedSpec<'a, O, &'a Result> +where + T: Debug, + E: Debug, + O: DoFail, +{ + type Ok = DerivedSpec<'a, O, &'a T>; + type Err = DerivedSpec<'a, O, &'a E>; + + fn ok(self) -> Self::Ok { + self.mapping(|subject| match subject { + Ok(value) => value, + Err(error) => { + panic!("expected the subject to be `Ok(_)`, but was `Err({error:?})`") + }, + }) + } + + fn err(self) -> Self::Err { + self.mapping(|subject| match subject { + Ok(value) => { + panic!("expected the subject to be `Err(_)`, but was `Ok({value:?})`") + }, + Err(error) => error, + }) + } +} + +impl AssertHasValue for DerivedSpec<'_, O, Result> +where + T: PartialEq + Debug, + E: Debug, + X: Debug, + O: DoFail, +{ + fn has_value(self, expected: X) -> Self { + self.expecting(has_value(expected)) + } +} + +impl AssertHasValue for DerivedSpec<'_, O, &Result> +where + T: PartialEq + Debug, + E: Debug, + X: Debug, + O: DoFail, +{ + fn has_value(self, expected: X) -> Self { + self.expecting(has_value(expected)) + } +} + +impl AssertHasError for DerivedSpec<'_, O, Result> +where + T: Debug, + E: PartialEq + Debug, + X: Debug, + O: DoFail, +{ + fn has_error(self, expected: X) -> Self { + self.expecting(has_error(expected)) + } +} + +impl AssertHasError for DerivedSpec<'_, O, &Result> +where + T: Debug, + E: PartialEq + Debug, + X: Debug, + O: DoFail, +{ + fn has_error(self, expected: X) -> Self { + self.expecting(has_error(expected)) + } +} + +impl<'a, O, T, E, X> AssertHasErrorMessage for DerivedSpec<'a, O, Result> +where + T: Debug, + E: Display, + X: Debug, + String: PartialEq, + O: DoFail, +{ + type ErrorMessage = DerivedSpec<'a, O, String>; + + fn has_error_message(self, expected: X) -> Self::ErrorMessage { + self.mapping(|result| match result { + Ok(value) => panic!("expected the subject to be `Err(_)` with message {expected:?}, but was `Ok({value:?})`"), + Err(error) => error.to_string(), + }).expecting(is_equal_to(expected)) + } +} + +impl<'a, O, T, E, X> AssertHasErrorMessage for DerivedSpec<'a, O, &Result> +where + T: Debug, + E: Display, + X: Debug, + String: PartialEq, + O: DoFail, +{ + type ErrorMessage = DerivedSpec<'a, O, String>; + + fn has_error_message(self, expected: X) -> Self::ErrorMessage { + self.mapping(|result| match result { + Ok(value) => panic!("expected the subject to be `Err(_)` with message {expected:?}, but was `Ok({value:?})`"), + Err(error) => error.to_string(), + }).expecting(is_equal_to(expected)) + } +} + +impl<'a, O, S> AssertErrorHasSource for DerivedSpec<'a, O, S> +where + S: Error, + O: DoFail, +{ + type SourceMessage = DerivedSpec<'a, O, Option>; + + fn has_no_source(self) -> Self { + self.expecting(not(error_has_source())) + } + + fn has_source(self) -> Self { + self.expecting(error_has_source()) + } + + fn has_source_message(self, expected_source_message: impl Into) -> Self::SourceMessage { + let expected_source_message = expected_source_message.into(); + self.expecting(error_has_source_message(expected_source_message)) + .mapping(|err| err.source().map(ToString::to_string)) + } +} + +impl AssertHasDebugString for DerivedSpec<'_, O, S> +where + S: Debug, + E: AsRef, + O: DoFail, +{ + fn has_debug_string(self, expected: E) -> Self { + self.expecting(has_debug_string(expected)) + } + + fn does_not_have_debug_string(self, expected: E) -> Self { + self.expecting(not(has_debug_string(expected))) + } +} + +impl<'a, O, S> AssertDebugString for DerivedSpec<'a, O, S> +where + S: Debug, + O: DoFail, +{ + type DebugString = DerivedSpec<'a, O, String>; + + fn debug_string(self) -> Self::DebugString { + let expression_debug_string = format!("{}'s debug string", self.expression); + self.mapping(|subject| format!("{subject:?}")) + .named(expression_debug_string) + } +} + +impl AssertHasDisplayString for DerivedSpec<'_, O, S> +where + S: Display, + E: AsRef, + O: DoFail, +{ + fn has_display_string(self, expected: E) -> Self { + self.expecting(has_display_string(expected)) + } + + fn does_not_have_display_string(self, expected: E) -> Self { + self.expecting(not(has_display_string(expected))) + } +} + +impl<'a, O, S> AssertDisplayString for DerivedSpec<'a, O, S> +where + S: Display, + O: DoFail, +{ + type DisplayString = DerivedSpec<'a, O, String>; + + fn display_string(self) -> Self::DisplayString { + let expression_display_string = format!("{}'s display string", self.expression); + self.mapping(|subject| subject.to_string()) + .named(expression_display_string) + } +} + +impl<'a, O, S> AssertStringPattern<&'a str> for DerivedSpec<'a, O, S> +where + S: 'a + AsRef + Debug, + O: DoFail, +{ + fn contains(self, pattern: &'a str) -> Self { + self.expecting(string_contains(pattern)) + } + + fn does_not_contain(self, pattern: &'a str) -> Self { + self.expecting(not(string_contains(pattern))) + } + + fn starts_with(self, pattern: &'a str) -> Self { + self.expecting(string_starts_with(pattern)) + } + + fn does_not_start_with(self, pattern: &'a str) -> Self { + self.expecting(not(string_starts_with(pattern))) + } + + fn ends_with(self, pattern: &'a str) -> Self { + self.expecting(string_ends_with(pattern)) + } + + fn does_not_end_with(self, pattern: &'a str) -> Self { + self.expecting(not(string_ends_with(pattern))) + } +} + +impl<'a, O, S> AssertStringPattern for DerivedSpec<'a, O, S> +where + S: 'a + AsRef + Debug, + O: DoFail, +{ + fn contains(self, pattern: String) -> Self { + self.expecting(string_contains(pattern)) + } + + fn does_not_contain(self, pattern: String) -> Self { + self.expecting(not(string_contains(pattern))) + } + + fn starts_with(self, pattern: String) -> Self { + self.expecting(string_starts_with(pattern)) + } + + fn does_not_start_with(self, pattern: String) -> Self { + self.expecting(not(string_starts_with(pattern))) + } + + fn ends_with(self, pattern: String) -> Self { + self.expecting(string_ends_with(pattern)) + } + + fn does_not_end_with(self, pattern: String) -> Self { + self.expecting(not(string_ends_with(pattern))) + } +} + +impl<'a, O, S> AssertStringPattern for DerivedSpec<'a, O, S> +where + S: 'a + AsRef + Debug, + O: DoFail, +{ + fn contains(self, pattern: char) -> Self { + self.expecting(string_contains(pattern)) + } + + fn does_not_contain(self, pattern: char) -> Self { + self.expecting(not(string_contains(pattern))) + } + + fn starts_with(self, pattern: char) -> Self { + self.expecting(string_starts_with(pattern)) + } + + fn does_not_start_with(self, pattern: char) -> Self { + self.expecting(not(string_starts_with(pattern))) + } + + fn ends_with(self, pattern: char) -> Self { + self.expecting(string_ends_with(pattern)) + } + + fn does_not_end_with(self, pattern: char) -> Self { + self.expecting(not(string_ends_with(pattern))) + } +} + +impl<'a, O, S> AssertStringContainsAnyOf<&'a [char]> for DerivedSpec<'a, O, S> +where + S: 'a + AsRef + Debug, + O: DoFail, +{ + fn contains_any_of(self, expected: &'a [char]) -> Self { + self.expecting(string_contains_any_of(expected)) + } + + fn does_not_contain_any_of(self, expected: &'a [char]) -> Self { + self.expecting(not(string_contains_any_of(expected))) + } +} + +impl<'a, O, S, const N: usize> AssertStringContainsAnyOf<[char; N]> for DerivedSpec<'a, O, S> +where + S: 'a + AsRef + Debug, + O: DoFail, +{ + fn contains_any_of(self, expected: [char; N]) -> Self { + self.expecting(string_contains_any_of(expected)) + } + + fn does_not_contain_any_of(self, expected: [char; N]) -> Self { + self.expecting(not(string_contains_any_of(expected))) + } +} + +impl<'a, O, S, const N: usize> AssertStringContainsAnyOf<&'a [char; N]> for DerivedSpec<'a, O, S> +where + S: 'a + AsRef + Debug, + O: DoFail, +{ + fn contains_any_of(self, expected: &'a [char; N]) -> Self { + self.expecting(string_contains_any_of(expected)) + } + + fn does_not_contain_any_of(self, expected: &'a [char; N]) -> Self { + self.expecting(not(string_contains_any_of(expected))) + } +} + +#[cfg(feature = "regex")] +mod regex { + use crate::assertions::AssertStringMatches; + use crate::derived_spec::DerivedSpec; + use crate::expectations::{not, string_matches}; + use crate::spec::{DoFail, Expecting}; + use crate::std::fmt::Debug; + + impl AssertStringMatches for DerivedSpec<'_, O, S> + where + S: AsRef + Debug, + O: DoFail, + { + fn matches(self, regex_pattern: &str) -> Self { + self.expecting(string_matches(regex_pattern)) + } + + fn does_not_match(self, regex_pattern: &str) -> Self { + self.expecting(not(string_matches(regex_pattern))) + } + } +} + +impl<'a, O, S, T, E> AssertIteratorContains for DerivedSpec<'a, O, S> +where + S: IntoIterator, + T: PartialEq + Debug, + E: Debug, + O: DoFail, +{ + type Sequence = DerivedSpec<'a, O, Vec>; + + fn contains(self, element: E) -> Self::Sequence { + self.mapping(Vec::from_iter) + .expecting(iterator_contains(element)) + } + + fn does_not_contain(self, element: E) -> Self::Sequence { + self.mapping(Vec::from_iter) + .expecting(not(iterator_contains(element))) + } +} + +impl<'a, O, S, T, E> AssertIteratorContainsInAnyOrder for DerivedSpec<'a, O, S> +where + S: IntoIterator, + T: PartialEq<::Item> + Debug, + E: IntoIterator, + ::Item: Debug, + O: DoFail, +{ + type Sequence = DerivedSpec<'a, O, Vec>; + + fn contains_exactly_in_any_order(self, expected: E) -> Self::Sequence { + self.mapping(Vec::from_iter) + .expecting(iterator_contains_exactly_in_any_order(expected)) + } + + fn contains_any_of(self, expected: E) -> Self::Sequence { + self.mapping(Vec::from_iter) + .expecting(iterator_contains_any_of(expected)) + } + + fn does_not_contain_any_of(self, expected: E) -> Self::Sequence { + self.mapping(Vec::from_iter) + .expecting(not(iterator_contains_any_of(expected))) + } + + fn contains_all_of(self, expected: E) -> Self::Sequence { + self.mapping(Vec::from_iter) + .expecting(iterator_contains_all_of(expected)) + } + + fn contains_only(self, expected: E) -> Self::Sequence { + self.mapping(Vec::from_iter) + .expecting(iterator_contains_only(expected)) + } + + fn contains_only_once(self, expected: E) -> Self::Sequence { + self.mapping(Vec::from_iter) + .expecting(iterator_contains_only_once(expected)) + } +} + +impl<'a, O, S, T, E> AssertIteratorContainsInOrder for DerivedSpec<'a, O, S> +where + S: IntoIterator, + ::IntoIter: DefinedOrderProperty, + E: IntoIterator, + ::IntoIter: DefinedOrderProperty, + ::Item: Debug, + T: PartialEq<::Item> + Debug, + O: DoFail, +{ + type Sequence = DerivedSpec<'a, O, Vec>; + + fn contains_exactly(self, expected: E) -> Self::Sequence { + self.mapping(Vec::from_iter) + .expecting(iterator_contains_exactly(expected)) + } + + fn contains_sequence(self, expected: E) -> Self::Sequence { + self.mapping(Vec::from_iter) + .expecting(iterator_contains_sequence(expected)) + } + + fn contains_all_in_order(self, expected: E) -> Self::Sequence { + self.mapping(Vec::from_iter) + .expecting(iterator_contains_all_in_order(expected)) + } + + fn starts_with(self, expected: E) -> Self::Sequence { + self.mapping(Vec::from_iter) + .expecting(iterator_starts_with(expected)) + } + + fn ends_with(self, expected: E) -> Self::Sequence { + self.mapping(Vec::from_iter) + .expecting(iterator_ends_with(expected)) + } +} + +impl AssertMapContainsKey for DerivedSpec<'_, O, S> +where + S: MapProperties + Debug, + ::Key: PartialEq + Debug, + ::Value: Debug, + E: Debug, + O: DoFail, +{ + fn contains_key(self, expected_key: E) -> Self { + self.expecting(map_contains_key(expected_key)) + } + + fn does_not_contain_key(self, expected_key: E) -> Self { + self.expecting(not(map_contains_key(expected_key))) + } + + fn contains_keys(self, expected_keys: impl IntoIterator) -> Self { + self.expecting(map_contains_keys(expected_keys)) + } + + fn does_not_contain_keys(self, expected_keys: impl IntoIterator) -> Self { + self.expecting(map_does_not_contain_keys(expected_keys)) + } + + fn contains_exactly_keys(self, expected_keys: impl IntoIterator) -> Self { + self.expecting(map_contains_exactly_keys(expected_keys)) + } +} + +impl AssertMapContainsValue for DerivedSpec<'_, O, S> +where + S: MapProperties + Debug, + ::Key: Debug, + ::Value: PartialEq + Debug, + E: Debug, + O: DoFail, +{ + fn contains_value(self, expected_value: E) -> Self { + self.expecting(map_contains_value(expected_value)) + } + + fn does_not_contain_value(self, expected_value: E) -> Self { + self.expecting(not(map_contains_value(expected_value))) + } + + fn contains_values(self, expected_values: impl IntoIterator) -> Self { + self.expecting(map_contains_values(expected_values)) + } + + fn does_not_contain_values(self, expected_values: impl IntoIterator) -> Self { + self.expecting(map_does_not_contain_values(expected_values)) + } +} + +impl<'a, O, S, T> AssertOrderedElements for DerivedSpec<'a, O, S> +where + S: IntoIterator, + ::IntoIter: DefinedOrderProperty, + T: Debug, + O: DoFail + GetFailures, +{ + type SingleElement = DerivedSpec<'a, O, T>; + type MultipleElements = DerivedSpec<'a, O, Vec>; + + fn first_element(self) -> Self::SingleElement { + let spec = self + .mapping(Vec::from_iter) + .expecting(has_at_least_number_of_elements(1)); + if spec.has_failures() { + PanicOnFail.do_fail_with(&spec.failures()); + unreachable!("Assertion failed and should have panicked! Please report a bug.") + } + spec.extracting(|mut collection| collection.remove(0)) + } + + fn last_element(self) -> Self::SingleElement { + let spec = self + .mapping(Vec::from_iter) + .expecting(has_at_least_number_of_elements(1)); + if spec.has_failures() { + PanicOnFail.do_fail_with(&spec.failures()); + unreachable!("Assertion failed and should have panicked! Please report a bug.") + } + spec.extracting(|mut collection| { + collection.pop().unwrap_or_else(|| { + unreachable!("Assertion failed and should have panicked! Please report a bug.") + }) + }) + } + + fn nth_element(self, n: usize) -> Self::SingleElement { + let min_len = n + 1; + let spec = self + .mapping(Vec::from_iter) + .expecting(has_at_least_number_of_elements(min_len)); + if spec.has_failures() { + PanicOnFail.do_fail_with(&spec.failures()); + unreachable!("Assertion failed and should have panicked! Please report a bug.") + } + spec.extracting(|mut collection| collection.remove(n)) + } + + fn elements_at(self, indices: impl IntoIterator) -> Self::MultipleElements { + let indices = HashSet::<_>::from_iter(indices); + self.mapping(|subject| { + subject + .into_iter() + .enumerate() + .filter_map(|(i, v)| if indices.contains(&i) { Some(v) } else { None }) + .collect() + }) + } +} + +impl<'a, O, I> AssertElements<'a, I> for DerivedSpec<'a, O, I> +where + I: 'a + IntoIterator, + O: DoFail + GetLocation<'a>, +{ + type Output = DerivedSpec<'a, O, ()>; + + fn each_element(mut self, assert: A) -> Self::Output + where + A: Fn(Spec<'a, ::Item, CollectFailures>) -> B, + B: GetFailures, + { + let root_expression = &self.expression; + let diff_format = self.diff_format().clone(); + let location = self.location(); + let mut collected_failures = Vec::new(); + let mut position = -1; + for item in self.subject { + position += 1; + let mut element_spec = Spec::new(item, CollectFailures) + .named(format!("{root_expression}[{position}]")) + .with_diff_format(diff_format.clone()); + if let Some(location) = location { + element_spec = element_spec.located_at(location); + } + let failures = assert(element_spec).failures(); + collected_failures.extend(failures); + } + if !collected_failures.is_empty() { + self.original.do_fail_with(collected_failures); + } + DerivedSpec { + original: self.original, + subject: (), + expression: self.expression, + diff_format: self.diff_format, + } + } + + fn any_element(mut self, assert: A) -> Self::Output + where + A: Fn(Spec<'a, ::Item, CollectFailures>) -> B, + B: GetFailures, + { + let root_expression = &self.expression; + let diff_format = self.diff_format().clone(); + let location = self.location(); + let mut collected_failures = Vec::new(); + let mut any_success = false; + let mut position = -1; + for item in self.subject { + position += 1; + let mut element_spec = Spec::new(item, CollectFailures) + .named(format!("{root_expression}[{position}]")) + .with_diff_format(diff_format.clone()); + if let Some(location) = location { + element_spec = element_spec.located_at(location); + } + let failures = assert(element_spec).failures(); + if failures.is_empty() { + any_success = true; + break; + } + collected_failures.extend(failures); + } + if !any_success { + self.original.do_fail_with(collected_failures); + } + DerivedSpec { + original: self.original, + subject: (), + expression: self.expression, + diff_format: self.diff_format, + } + } +} + +impl<'a, O, S, T, U> AssertOrderedElementsRef for DerivedSpec<'a, O, S> +where + S: IntoIterator, + ::IntoIter: DefinedOrderProperty, + T: ToOwned + Debug, + O: DoFail + GetFailures, +{ + type SingleElement = DerivedSpec<'a, DerivedSpec<'a, O, Vec>, U>; + type MultipleElements = DerivedSpec<'a, DerivedSpec<'a, O, Vec>, Vec>; + + fn first_element_ref(self) -> Self::SingleElement { + let original_spec = self + .mapping(Vec::from_iter) + .expecting(has_at_least_number_of_elements(1)); + if original_spec.has_failures() { + PanicOnFail.do_fail_with(&original_spec.failures()); + unreachable!("Assertion failed and should have panicked! Please report a bug.") + } + let orig_subject_name = original_spec.expression(); + let new_subject_name = format!("the first element of {orig_subject_name}"); + original_spec.extracting_ref(new_subject_name, |collection| + collection.first() + .unwrap_or_else(|| + unreachable!("We should have asserted before, that there is at least one element in the collection/iterator. Please file a bug.") + ) + ) + } + + fn last_element_ref(self) -> Self::SingleElement { + let original_spec = self + .mapping(Vec::from_iter) + .expecting(has_at_least_number_of_elements(1)); + if original_spec.has_failures() { + PanicOnFail.do_fail_with(&original_spec.failures()); + unreachable!("Assertion failed and should have panicked! Please report a bug.") + } + let orig_subject_name = original_spec.expression(); + let new_subject_name = format!("the last element of {orig_subject_name}"); + original_spec.extracting_ref(new_subject_name, |collection| + collection.last() + .unwrap_or_else(|| + unreachable!("We should have asserted before, that there is at least one element in the collection/iterator. Please file a bug.") + ) + ) + } + + fn nth_element_ref(self, n: usize) -> Self::SingleElement { + let min_len = n + 1; + let original_spec = self + .mapping(Vec::from_iter) + .expecting(has_at_least_number_of_elements(min_len)); + if original_spec.has_failures() { + PanicOnFail.do_fail_with(&original_spec.failures()); + unreachable!("Assertion failed and should have panicked! Please report a bug.") + } + let orig_subject_name = original_spec.expression(); + let new_subject_name = format!("{orig_subject_name}[{n}]"); + original_spec.extracting_ref(new_subject_name, |collection| + collection.get(n) + .unwrap_or_else(|| + unreachable!("We should have asserted before, that there is at least one element in the collection/iterator. Please file a bug.") + ) + ) + } + + fn elements_ref_at(self, indices: impl IntoIterator) -> Self::MultipleElements { + let indices = Vec::from_iter(indices); + let orig_subject_name = self.expression(); + let new_subject_name = format!("{orig_subject_name} at positions {indices:?}"); + let indices = HashSet::<_>::from_iter(indices); + let original_spec = self.mapping(Vec::from_iter); + original_spec.extracting_ref_iter(new_subject_name, |collection| { + collection + .enumerate() + .filter_map(|(i, e)| { + if indices.contains(&i) { + Some(e.to_owned()) + } else { + None + } + }) + .collect() + }) + } +} + +#[cfg(test)] +mod tests; diff --git a/src/derived_spec/tests.rs b/src/derived_spec/tests.rs new file mode 100644 index 0000000..3be30fb --- /dev/null +++ b/src/derived_spec/tests.rs @@ -0,0 +1,1490 @@ +use crate::prelude::*; +use crate::std::string::{String, ToString}; +use crate::std::vec; +use crate::std::vec::Vec; +#[cfg(feature = "bigdecimal")] +use bigdecimal::BigDecimal; +#[cfg(feature = "float-cmp")] +use time::OffsetDateTime; +#[cfg(feature = "float-cmp")] +use time::macros::datetime; + +#[cfg(feature = "float-cmp")] +#[derive(Debug, Clone, PartialEq)] +struct Item { + name: String, + price: f32, + quantity: u32, +} + +#[cfg(feature = "float-cmp")] +struct Order { + id: String, + purchased_at: OffsetDateTime, + items: Vec, + vat: f32, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +enum Gender { + Male, + Female, + NonBinary, + PreferNotToSay, +} + +struct Person { + name: String, + age: u8, + gender: Gender, +} + +impl Person { + fn name(&self) -> &str { + &self.name + } +} + +struct Answer(i32); + +#[test] +fn mapping_person_name_starts_with_alex() { + let person = Person { + name: "Alexander".to_string(), + age: 31, + gender: Gender::Male, + }; + + assert_that(person).mapping(|p| p.name).starts_with("Alex"); +} + +#[test] +fn extracting_person_name_contains_i() { + let person = Person { + name: "Silvia".to_string(), + age: 27, + gender: Gender::Female, + }; + + assert_that(person).extracting(|p| p.name).contains('i'); +} + +#[test] +fn extracting_ref_person_name_via_accessor_contains_via() { + let person = Person { + name: "Silvia".to_string(), + age: 27, + gender: Gender::Female, + }; + + assert_that(person) + .extracting_ref("name", Person::name) + .contains("via"); +} + +#[test] +fn extracting_ref_to_assert_all_person_fields() { + let person = Person { + name: "Silvia".to_string(), + age: 27, + gender: Gender::PreferNotToSay, + }; + + assert_that(person) + .extracting_ref("name", |p| &p.name) + .is_equal_to("Silvia") + .and() + .extracting_ref("age", |p| &p.age) + .is_at_least(18) + .and() + .extracting_ref("gender", |p| &p.gender) + .is_equal_to(Gender::PreferNotToSay); +} + +#[test] +fn verify_extracting_ref_to_assert_all_fields_fails_with_all_failures() { + let person = Person { + name: "silvia".to_string(), + age: 17, + gender: Gender::NonBinary, + }; + + let failures = verify_that(person) + .named("person") + .extracting_ref("name", Person::name) + .is_equal_to("Silvia") + .and() + .extracting_ref("age", |p| &p.age) + .is_at_least(18) + .and() + .extracting_ref("gender", |p| &p.gender) + .is_equal_to(Gender::PreferNotToSay) + .display_failures(); + + assert_eq!( + failures, + &[ + r#"expected person.name to be equal to "Silvia" + but was: "silvia" + expected: "Silvia" +"#, + r"expected person.age to be at least 18 + but was: 17 + expected: >= 18 +", + r"expected person.gender to be equal to PreferNotToSay + but was: NonBinary + expected: PreferNotToSay +", + ] + ); +} + +#[cfg(feature = "float-cmp")] +#[test] +fn extracting_ref_to_assert_all_order_item_fields() { + let order = Order { + id: "019d359f-d2f1-7d64-826e-c111ae12dd24".to_string(), + purchased_at: datetime!(2026-03-28 14:20:33 +01:00), + items: vec![ + Item { + name: "Apple".to_string(), + price: 1.99, + quantity: 6, + }, + Item { + name: "Orange".to_string(), + price: 2.99, + quantity: 3, + }, + ], + vat: 0.15, + }; + + assert_that(order) + .extracting_ref("id", |o| &o.id) + .is_not_empty() + .and() + .extracting_ref("purchased_at", |o| &o.purchased_at) + .is_between( + datetime!(2026-03-28 14:00 +01:00), + datetime!(2026-03-28 15:00 +01:00), + ) + .and() + .extracting_ref("items", |o| &o.items) + .has_length(2) + .extracting_ref("[0]", |items| &items[0]) + .extracting_ref("name", |i| &i.name) + .is_equal_to("Apple") + .and() + .extracting_ref("price", |i| &i.price) + .is_close_to(1.99) + .and() + .extracting_ref("quantity", |i| &i.quantity) + .is_equal_to(6) + .and() + .and() + .contains_exactly([ + Item { + name: "Apple".to_string(), + price: 1.99, + quantity: 6, + }, + Item { + name: "Orange".to_string(), + price: 2.99, + quantity: 3, + }, + ]) + .and() + .extracting_ref("vat", |o| &o.vat) + .is_close_to(0.15); +} + +#[test] +fn assert_that_extracted_ref_satisfies_predicate() { + let answer = Answer(42); + + assert_that(answer) + .named("answer") + .extracting_ref("val", |answer| &answer.0) + .satisfies(|actual| *actual == 42) + .is_at_least(42); +} + +#[test] +fn verify_that_subject_satisfies_predicate_fails() { + let subject = Answer(51); + + let failures = verify_that(subject) + .named("answer") + .extracting_ref("val", |answer| &answer.0) + .satisfies(|actual| *actual == 42) + .display_failures(); + + assert_eq!( + failures, + &["expected answer.val to satisfy the given predicate, but returned false\n"] + ); +} + +#[test] +fn verify_that_subject_satisfies_predicate_fails_with_custom_message() { + let subject = Answer(51); + + let failures = verify_that(subject) + .named("answer") + .extracting_ref("val", |answer| &answer.0) + .satisfies_with_message("the answer to all important questions is 42", |actual| { + *actual == 42 + }) + .display_failures(); + + assert_eq!(failures, &["the answer to all important questions is 42\n"]); +} + +#[test] +fn extracting_ref_string_is_equal_to() { + struct Name(String); + + let name = Name("Alexander".to_string()); + + assert_that(name) + .extracting_ref("0", |n| &n.0) + .is_equal_to("Alexander"); +} + +#[test] +fn extracting_ref_string_is_same_as() { + struct Name(String); + + let name = Name("Alexander".to_string()); + + assert_that(name) + .extracting_ref("0", |n| &n.0) + .is_same_as("Alexander".to_string()); +} + +#[test] +fn extracting_ref_i32_is_zero() { + struct Int(i32); + + let number = Int(0); + + assert_that(number).extracting_ref("0", |n| &n.0).is_zero(); +} + +#[test] +fn extracting_ref_i32_is_one() { + struct Int(i32); + + let number = Int(1); + + assert_that(number).extracting_ref("0", |n| &n.0).is_one(); +} + +#[test] +fn extracting_ref_i32_is_positive() { + struct Int(i32); + + let number = Int(1); + + assert_that(number) + .extracting_ref("0", |n| &n.0) + .is_positive(); +} + +#[test] +fn extracting_ref_i32_is_negative() { + struct Int(i32); + + let number = Int(-1); + + assert_that(number) + .extracting_ref("0", |n| &n.0) + .is_negative(); +} + +#[test] +fn extracting_ref_i32_is_not_positive_and_is_not_negative() { + struct Int(i32); + + let number = Int(0); + + assert_that(number) + .extracting_ref("0", |n| &n.0) + .is_not_positive() + .is_not_negative(); +} + +#[test] +fn extracting_ref_i32_is_in_range() { + struct Int(i32); + + let number = Int(9); + + assert_that(number) + .extracting_ref("0", |n| &n.0) + .is_in_range(1..=9); +} + +#[cfg(feature = "float-cmp")] +#[test] +fn extracting_ref_f32_is_close_to() { + struct Float(f32); + + let value = Float(1.99); + + assert_that(value) + .extracting_ref("0", |f| &f.0) + .is_close_to(1.99); +} + +#[test] +fn extracting_ref_f32_is_zero() { + struct Float(f32); + + let value = Float(0.); + + assert_that(value).extracting_ref("0", |f| &f.0).is_zero(); +} + +#[test] +fn extracting_ref_f32_is_one() { + struct Float(f32); + + let value = Float(1.); + + assert_that(value).extracting_ref("0", |f| &f.0).is_one(); +} + +#[test] +fn extracting_ref_f32_is_positive() { + struct Float(f32); + + let value = Float(1.); + + assert_that(value) + .extracting_ref("0", |f| &f.0) + .is_positive(); +} + +#[test] +fn extracting_ref_f32_is_negative() { + struct Float(f32); + + let value = Float(-1.); + + assert_that(value) + .extracting_ref("0", |f| &f.0) + .is_negative(); +} + +#[test] +fn extracting_ref_f32_is_not_positive_and_is_not_negative() { + struct Float(f32); + + let value = Float(0.); + + assert_that(value) + .extracting_ref("0", |f| &f.0) + .is_not_positive() + .is_not_negative(); +} + +#[test] +fn extracting_ref_f32_is_infinite() { + struct Float(f32); + + let value = Float(f32::INFINITY); + + assert_that(value) + .extracting_ref("0", |f| &f.0) + .is_infinite(); +} + +#[test] +fn extracting_ref_f32_is_not_a_number() { + struct Float(f32); + + let value = Float(f32::NAN); + + assert_that(value) + .extracting_ref("0", |f| &f.0) + .is_not_a_number(); +} + +#[cfg(feature = "float-cmp")] +#[test] +fn extracting_ref_f64_is_close_to_within_margin() { + struct Float(f64); + + let value = Float(1.99); + + assert_that(value) + .extracting_ref("0", |f| &f.0) + .is_close_to_with_margin(1.99, (0.001, 2)); +} + +#[test] +fn extracting_ref_f64_is_zero() { + struct Float(f64); + + let value = Float(0.); + + assert_that(value).extracting_ref("0", |f| &f.0).is_zero(); +} + +#[test] +fn extracting_ref_f64_is_one() { + struct Float(f64); + + let value = Float(1.); + + assert_that(value).extracting_ref("0", |f| &f.0).is_one(); +} + +#[test] +fn extracting_ref_f64_is_positive() { + struct Float(f64); + + let value = Float(1.); + + assert_that(value) + .extracting_ref("0", |f| &f.0) + .is_positive(); +} + +#[test] +fn extracting_ref_f64_is_negative() { + struct Float(f64); + + let value = Float(-1.); + + assert_that(value) + .extracting_ref("0", |f| &f.0) + .is_negative(); +} + +#[test] +fn extracting_ref_f64_is_not_positive_and_is_not_negative() { + struct Float(f64); + + let value = Float(0.); + + assert_that(value) + .extracting_ref("0", |f| &f.0) + .is_not_positive() + .is_not_negative(); +} + +#[test] +fn extracting_ref_f64_is_infinite() { + struct Float(f64); + + let value = Float(f64::INFINITY); + + assert_that(value) + .extracting_ref("0", |f| &f.0) + .is_infinite(); +} + +#[test] +fn extracting_ref_f64_is_not_a_number() { + struct Float(f64); + + let value = Float(f64::NAN); + + assert_that(value) + .extracting_ref("0", |f| &f.0) + .is_not_a_number(); +} + +#[cfg(feature = "bigdecimal")] +#[test] +fn extracting_ref_bigdecimal_has_scale_of() { + struct DecimalNumber(BigDecimal); + + let number = DecimalNumber( + "23.99182405" + .parse() + .unwrap_or_else(|err| panic!("{}", err)), + ); + + assert_that(number) + .extracting_ref("0", |n| &n.0) + .has_scale_of(8); +} + +#[cfg(feature = "bigdecimal")] +#[test] +fn extracting_ref_bigdecimal_has_precision_of() { + struct DecimalNumber(BigDecimal); + + let number = DecimalNumber( + "4123.99182405" + .parse() + .unwrap_or_else(|err| panic!("{}", err)), + ); + + assert_that(number) + .extracting_ref("0", |n| &n.0) + .has_precision_of(12); +} + +#[cfg(feature = "bigdecimal")] +#[test] +fn extracting_ref_bigdecimal_is_integer() { + struct DecimalNumber(BigDecimal); + + let number = DecimalNumber("123.0".parse().unwrap_or_else(|err| panic!("{}", err))); + + assert_that(number) + .extracting_ref("0", |n| &n.0) + .is_integer(); +} + +#[test] +fn extracting_ref_bool_is_true() { + struct Flag(bool); + + assert_that(Flag(true)) + .extracting_ref("0", |f| &f.0) + .is_true(); +} + +#[test] +fn extracting_ref_bool_is_false() { + struct Flag(bool); + + assert_that(Flag(false)) + .extracting_ref("0", |f| &f.0) + .is_false(); +} + +#[test] +fn extracting_ref_char_is_lowercase() { + struct Character(char); + + assert_that(Character('r')) + .extracting_ref("0", |c| &c.0) + .is_lowercase(); +} + +#[test] +fn extracting_ref_char_is_uppercase() { + struct Character(char); + + assert_that(Character('R')) + .extracting_ref("0", |c| &c.0) + .is_uppercase(); +} + +#[test] +fn extracting_ref_char_is_ascii() { + struct Character(char); + + assert_that(Character('@')) + .extracting_ref("0", |c| &c.0) + .is_ascii(); +} + +#[test] +fn extracting_ref_char_is_alphabetic() { + struct Character(char); + + assert_that(Character('Z')) + .extracting_ref("0", |c| &c.0) + .is_alphabetic(); +} + +#[test] +fn extracting_ref_char_is_alphanumeric() { + struct Character(char); + + assert_that(Character('Z')) + .extracting_ref("0", |c| &c.0) + .is_alphanumeric(); + + assert_that(Character('5')) + .extracting_ref("0", |c| &c.0) + .is_alphanumeric(); +} + +#[test] +fn extracting_ref_char_is_control_char() { + struct Character(char); + + assert_that(Character('\t')) + .extracting_ref("0", |c| &c.0) + .is_control_char(); + + assert_that(Character('\u{1b}')) + .extracting_ref("0", |c| &c.0) + .is_control_char(); +} + +#[test] +fn extracting_ref_char_is_digit() { + struct Character(char); + + assert_that(Character('0')) + .extracting_ref("0", |c| &c.0) + .is_digit(10); +} + +#[test] +fn extracting_ref_char_is_whitespace() { + struct Character(char); + + assert_that(Character(' ')) + .extracting_ref("0", |c| &c.0) + .is_whitespace(); + assert_that(Character('\n')) + .extracting_ref("0", |c| &c.0) + .is_whitespace(); +} + +#[test] +fn extracting_ref_string_is_empty() { + struct Name(String); + + let name = Name(String::new()); + + assert_that(name).extracting_ref("0", |n| &n.0).is_empty(); +} + +#[test] +fn extracting_ref_string_is_not_empty() { + struct Name(String); + + let name = Name(" ".to_string()); + + assert_that(name) + .extracting_ref("0", |n| &n.0) + .is_not_empty(); +} + +#[test] +fn extracting_ref_vec_is_empty() { + struct Bytes(Vec); + + let name = Bytes(vec![]); + + assert_that(name).extracting_ref("0", |n| &n.0).is_empty(); +} + +#[test] +fn extracting_ref_vec_is_not_empty() { + struct Bytes(Vec); + + let name = Bytes(vec![48, 65]); + + assert_that(name) + .extracting_ref("0", |n| &n.0) + .is_not_empty(); +} + +#[test] +fn extracting_ref_string_has_length() { + struct Name(String); + + let name = Name("Alex".to_string()); + + assert_that(name) + .extracting_ref("0", |n| &n.0) + .has_length(4); +} + +#[test] +fn extracting_ref_string_has_char_count() { + struct Text(String); + + let name = Text("imper \u{0180} diet al \u{02AA} \u{01AF} zzril".to_string()); + + assert_that(name) + .extracting_ref("0", |n| &n.0) + .has_char_count(25); +} + +#[test] +fn extracting_ref_option_some() { + struct Optional(Option); + + let note = Optional(Some("note".to_string())); + + assert_that(note).extracting_ref("0", |n| &n.0).is_some(); +} + +#[test] +fn extracting_ref_option_none() { + struct Optional(Option); + + let note = Optional(None); + + assert_that(note).extracting_ref("0", |n| &n.0).is_none(); +} + +#[test] +fn extracting_ref_option_some_is_equal_to() { + struct Optional(Option); + + let note = Optional(Some("a note".to_string())); + + assert_that(note) + .extracting_ref("0", |n| &n.0) + .some() + .is_equal_to("a note"); +} + +#[test] +fn extracting_ref_option_has_value() { + struct Optional(Option); + + let note = Optional(Some("a note".to_string())); + + assert_that(note) + .extracting_ref("0", |n| &n.0) + .has_value("a note"); +} + +#[test] +fn extracting_ref_result_is_ok() { + struct Response(Result); + + let response = Response(Ok(-123)); + + assert_that(response).extracting_ref("0", |r| &r.0).is_ok(); +} + +#[test] +fn extracting_ref_result_is_err() { + struct Response(Result); + + let response = Response(Err("not found".to_string())); + + assert_that(response).extracting_ref("0", |r| &r.0).is_err(); +} + +#[test] +fn extracting_ref_result_ok_is_negative() { + struct Response(Result); + + let response = Response(Ok(-123)); + + assert_that(response) + .extracting_ref("0", |r| &r.0) + .ok() + .is_negative(); +} + +#[test] +fn extracting_ref_result_err_is_equal_to() { + struct Response(Result); + + let response = Response(Err("not found".to_string())); + + assert_that(response) + .extracting_ref("0", |r| &r.0) + .err() + .is_equal_to("not found"); +} + +#[test] +fn extracting_ref_result_has_value() { + struct Response(Result); + + let response = Response(Ok(-123)); + + assert_that(response) + .extracting_ref("0", |r| &r.0) + .has_value(-123); +} + +#[test] +fn extracting_ref_result_has_error() { + struct Response(Result); + + let response = Response(Err("not found".to_string())); + + assert_that(response) + .extracting_ref("0", |r| &r.0) + .has_error("not found"); +} + +#[test] +fn extracting_ref_string_contains_char() { + struct Name(String); + + let name = Name("Alexander is here".to_string()); + + assert_that(name) + .extracting_ref("0", |n| &n.0) + .contains('x'); +} + +#[test] +fn extracting_ref_string_contains_any_of_chars() { + struct Name(String); + + let name = Name("Alexander is here".to_string()); + + assert_that(name) + .extracting_ref("0", |n| &n.0) + .contains_any_of(['a', 'e', 'i', 'o', 'u']); +} + +#[test] +fn extracting_ref_vec_has_length() { + struct Bytes(Vec); + + let bytes = Bytes(vec![1, 2, 3, 4, 5]); + + assert_that(bytes) + .extracting_ref("0", |b| &b.0) + .has_length(5); +} + +#[test] +fn extracting_ref_vec_contains() { + struct Names(Vec); + + let names = Names(vec![ + "Silvia".to_string(), + "Alexander".to_string(), + "Robert".to_string(), + ]); + + assert_that(names) + .extracting_ref("0", |n| &n.0) + .contains("Alexander"); +} + +#[test] +fn extracting_ref_vec_contains_exactly() { + struct Names(Vec); + + let names = Names(vec![ + "Silvia".to_string(), + "Alexander".to_string(), + "Robert".to_string(), + ]); + + assert_that(names) + .extracting_ref("0", |n| &n.0) + .contains_exactly(["Silvia", "Alexander", "Robert"]); +} + +#[test] +fn extracting_ref_vec_contains_only() { + struct Names(Vec); + + let names = Names(vec![ + "Silvia".to_string(), + "Alexander".to_string(), + "Robert".to_string(), + ]); + + assert_that(names) + .extracting_ref("0", |n| &n.0) + .contains_only(["Silvia", "Robert", "Philipp", "Alexander"]); +} + +#[test] +fn extracting_ref_vec_contains_any() { + struct Names(Vec); + + let names = Names(vec![ + "Silvia".to_string(), + "Alexander".to_string(), + "Robert".to_string(), + ]); + + assert_that(names) + .extracting_ref("0", |n| &n.0) + .contains_any_of(["Robert", "Philipp", "Peter"]); +} + +#[test] +fn extracting_ref_vec_contains_all() { + struct Names(Vec); + + let names = Names(vec![ + "Silvia".to_string(), + "Alexander".to_string(), + "Robert".to_string(), + ]); + + assert_that(names) + .extracting_ref("0", |n| &n.0) + .contains_all_of(["Robert", "Silvia"]); +} + +#[test] +fn extracting_ref_vec_contains_all_in_order() { + struct Names(Vec); + + let names = Names(vec![ + "Silvia".to_string(), + "Alexander".to_string(), + "Robert".to_string(), + ]); + + assert_that(names) + .extracting_ref("0", |n| &n.0) + .contains_all_in_order(["Silvia", "Robert"]); +} + +mod iterator_all_elements { + use super::*; + + #[derive(Debug, Clone)] + struct Person { + name: String, + age: u8, + } + + struct People(Vec); + + struct Numbers(Vec); + + struct Words(Vec<&'static str>); + + #[test] + fn assert_each_element_of_an_iterator_of_integer() { + let subject = Numbers(vec![2, 4, 6, 8, 10]); + + assert_that(subject) + .extracting_ref("0", |numbers| &numbers.0) + .is_not_empty() + .each_element(|e| e.is_positive().is_at_most(20)); + } + + #[test] + fn assert_each_element_of_an_iterator_of_person() { + let subject = People(vec![ + Person { + name: "John".into(), + age: 42, + }, + Person { + name: "Jane".into(), + age: 20, + }, + ]); + + assert_that(subject) + .extracting_ref("0", |people| &people.0) + .is_not_empty() + .each_element(|person| { + person + .extracting_ref("name", |p| &p.name) + .starts_with('J') + .and() + .extracting_ref("age", |p| &p.age) + .is_at_most(42) + }); + } + + #[test] + #[should_panic = "expected numbers.val[1] to be not equal to 4\n but was: 4\n expected: not 4\n"] + fn assert_each_element_of_an_iterator_panics_if_one_assertion_fails() { + let subject = Numbers(vec![2, 4, 6, 8, 10]); + + assert_that(subject) + .named("numbers") + .extracting_ref("val", |numbers| &numbers.0) + .is_not_empty() + .each_element(|e| e.is_not_equal_to(4)); + } + + #[test] + fn verify_assert_each_element_of_an_iterator_fails() { + let subject = Numbers(vec![2, 4, 6, 8, 10]); + + let failures = verify_that(&subject) + .named("numbers") + .extracting_ref("val", |numbers| &numbers.0) + .each_element(|e| e.is_greater_than(2).is_at_most(7)) + .display_failures(); + + assert_eq!( + failures, + &[ + r"expected numbers.val[0] to be greater than 2 + but was: 2 + expected: > 2 +", + r"expected numbers.val[3] to be at most 7 + but was: 8 + expected: <= 7 +", + r"expected numbers.val[4] to be at most 7 + but was: 10 + expected: <= 7 +", + ] + ); + } + + #[test] + fn assert_any_element_of_an_iterator_of_str() { + let subject = Words(vec!["one", "two", "three", "four", "five"]); + + assert_that(subject) + .extracting_ref("0", |words| &words.0) + .is_not_empty() + .any_element(|e| e.contains("ee")); + } + + #[test] + fn assert_any_element_of_an_iterator_of_person() { + let subject = People(vec![ + Person { + name: "John".into(), + age: 42, + }, + Person { + name: "Jane".into(), + age: 20, + }, + ]); + + assert_that(subject) + .extracting_ref("0", |people| &people.0) + .is_not_empty() + .any_element(|person| { + person + .extracting_ref("name", |p| &p.name) + .is_equal_to("John") + .and() + .extracting_ref("age", |p| &p.age) + .is_at_least(42) + }); + } + + #[test] + fn verify_any_element_of_an_iterator_asserting_two_properties_fails() { + let subject = People(vec![ + Person { + name: "John".into(), + age: 42, + }, + Person { + name: "Jane".into(), + age: 20, + }, + ]); + + let failures = verify_that(subject) + .named("people") + .extracting_ref("0", |people| &people.0) + .any_element(|person| { + person + .extracting_ref("name", |p| &p.name) + .is_equal_to("John") + .and() + .extracting_ref("age", |p| &p.age) + .is_at_most(20) + }) + .display_failures(); + + assert_eq!( + failures, + &[ + r"expected people.0[0].age to be at most 20 + but was: 42 + expected: <= 20 +", + r#"expected people.0[1].name to be equal to "John" + but was: "Jane" + expected: "John" +"# + ] + ); + } + + #[test] + fn verify_any_element_of_an_iterator_assertion_for_elements_fails() { + let subject = Words(vec!["one", "two", "three", "four", "five"]); + + let failures = verify_that(subject) + .named("words") + .extracting_ref("0", |words| &words.0) + .any_element(|e| e.starts_with("fu")) + .display_failures(); + + assert_eq!( + failures, + &[ + r#"expected words.0[0] to start with "fu" + but was: "one" + expected: "fu" +"#, + r#"expected words.0[1] to start with "fu" + but was: "two" + expected: "fu" +"#, + r#"expected words.0[2] to start with "fu" + but was: "three" + expected: "fu" +"#, + r#"expected words.0[3] to start with "fu" + but was: "four" + expected: "fu" +"#, + r#"expected words.0[4] to start with "fu" + but was: "five" + expected: "fu" +"#, + ] + ); + } +} + +mod iterator_extracted_elements_ref { + use super::*; + + #[allow(dead_code)] + struct Order { + id: u64, + items: Vec<&'static str>, + } + + #[test] + fn first_element_of_iterator_with_one_element() { + let order = Order { + id: 55, + items: vec!["Apple"], + }; + + assert_that(order) + .extracting_ref("items", |o| &o.items) + .first_element_ref() + .is_equal_to("Apple") + .has_length(5) + .starts_with("App") + .and() + .last_element_ref() + .is_equal_to("Apple"); + } + + #[test] + fn first_element_of_iterator_with_several_elements() { + let order = Order { + id: 55, + items: vec!["Apple", "Banana", "Cherry", "Grapes", "Orange"], + }; + + assert_that(order) + .extracting_ref("items", |o| &o.items) + .first_element_ref() + .is_equal_to("Apple") + .has_length(5) + .starts_with('A') + .and() + .last_element_ref() + .is_equal_to("Orange"); + } + + #[cfg(feature = "panic")] + #[test] + fn first_element_of_iterator_with_no_elements_fails() { + let order = Order { + id: 55, + items: vec![], + }; + + assert_that_code(|| { + assert_that(order) + .named("order") + .with_diff_format(DIFF_FORMAT_NO_HIGHLIGHT) + .extracting_ref("items", |o| &o.items) + .first_element_ref() + .is_equal_to("Apple"); + }) + .panics_with_message( + r"expected order.items to have at least one element, but has no elements + actual: [] +", + ); + } + + #[test] + fn verify_first_element_of_iterator_assertion_fails() { + let order = Order { + id: 55, + items: vec!["Melon", "Banana", "Cherry", "Grapes", "Orange"], + }; + + let failures = verify_that(order) + .named("order") + .extracting_ref("items", |o| &o.items) + .first_element_ref() + .is_equal_to("Apple") + .display_failures(); + + assert_eq!( + failures, + &[ + r#"expected the first element of order.items to be equal to "Apple" + but was: "Melon" + expected: "Apple" +"# + ] + ); + } + + #[test] + fn last_element_of_iterator_with_one_element() { + let order = Order { + id: 55, + items: vec!["Apple"], + }; + + assert_that(order) + .extracting_ref("items", |o| &o.items) + .last_element_ref() + .is_equal_to("Apple") + .has_length(5) + .starts_with("Ap") + .and() + .first_element_ref() + .is_equal_to("Apple"); + } + + #[test] + fn last_element_of_iterator_with_several_elements() { + let order = Order { + id: 55, + items: vec!["Apple", "Banana", "Cherry", "Grapes", "Orange"], + }; + + assert_that(order) + .extracting_ref("items", |o| &o.items) + .last_element_ref() + .is_equal_to("Orange") + .has_length(6) + .starts_with("Oran") + .and() + .first_element_ref() + .is_equal_to("Apple"); + } + + #[cfg(feature = "panic")] + #[test] + fn last_element_of_iterator_with_no_elements_fails() { + let order = Order { + id: 55, + items: vec![], + }; + + assert_that_code(|| { + assert_that(order) + .named("order") + .with_diff_format(DIFF_FORMAT_NO_HIGHLIGHT) + .extracting_ref("items", |o| &o.items) + .last_element_ref() + .is_equal_to("Grapes"); + }) + .panics_with_message( + r"expected order.items to have at least one element, but has no elements + actual: [] +", + ); + } + + #[test] + fn verify_last_element_of_iterator_assertion_fails() { + let order = Order { + id: 55, + items: vec!["Apple", "Banana", "Melon"], + }; + + let failures = verify_that(order) + .named("order") + .extracting_ref("items", |o| &o.items) + .last_element_ref() + .is_equal_to("Cherry") + .display_failures(); + + assert_eq!( + failures, + &[ + r#"expected the last element of order.items to be equal to "Cherry" + but was: "Melon" + expected: "Cherry" +"# + ] + ); + } + + #[test] + fn nth_element_of_iterator_with_one_element() { + let order = Order { + id: 55, + items: vec!["Apple"], + }; + + assert_that(order) + .extracting_ref("items", |o| &o.items) + .nth_element_ref(0) + .is_equal_to("Apple") + .has_length(5) + .starts_with("App") + .and() + .first_element_ref() + .is_equal_to("Apple"); + } + + #[test] + fn nth_element_of_iterator_with_several_elements_second_element() { + let order = Order { + id: 55, + items: vec!["Apple", "Banana", "Cherry", "Grapes", "Orange"], + }; + + assert_that(order) + .extracting_ref("items", |o| &o.items) + .nth_element_ref(1) + .is_equal_to("Banana") + .has_length(6) + .starts_with("Ban") + .and() + .nth_element_ref(3) + .is_equal_to("Grapes"); + } + + #[test] + fn nth_element_of_iterator_with_several_elements_fifth_element() { + let order = Order { + id: 55, + items: vec!["Apple", "Banana", "Cherry", "Grapes", "Orange"], + }; + + assert_that(order) + .extracting_ref("items", |o| &o.items) + .nth_element_ref(4) + .is_equal_to("Orange") + .has_length(6) + .starts_with("Or") + .and() + .nth_element_ref(0) + .is_equal_to("Apple"); + } + + #[cfg(feature = "panic")] + #[test] + fn nth_element_of_iterator_with_five_elements_6th_element_fails() { + let order = Order { + id: 55, + items: vec!["Apple", "Banana", "Cherry", "Grapes", "Orange"], + }; + + assert_that_code(|| { + assert_that(order) + .named("order") + .with_diff_format(DIFF_FORMAT_NO_HIGHLIGHT) + .extracting_ref("items", |o| &o.items) + .nth_element_ref(5) + .is_equal_to("Melon"); + }) + .panics_with_message( + r#"expected order.items to have at least 6 elements, but has 5 elements + actual: ["Apple", "Banana", "Cherry", "Grapes", "Orange"] +"#, + ); + } + + #[test] + fn verify_nth_element_of_iterator_assertion_fails() { + let order = Order { + id: 55, + items: vec!["Apple", "Melon", "Cherry", "Grapes", "Orange"], + }; + + let failures = verify_that(order) + .named("order") + .extracting_ref("items", |o| &o.items) + .nth_element_ref(1) + .is_equal_to("Banana") + .display_failures(); + + assert_eq!( + failures, + &[r#"expected order.items[1] to be equal to "Banana" + but was: "Melon" + expected: "Banana" +"#] + ); + } + + #[test] + fn elements_at_positions_of_iterator() { + let order = Order { + id: 55, + items: vec!["Apple", "Banana", "Cherry", "Grapes", "Orange"], + }; + + assert_that(order) + .extracting_ref("items", |o| &o.items) + .elements_ref_at([0, 2, 4]) + .contains_exactly(["Apple", "Cherry", "Orange"]); + } + + #[test] + fn verify_elements_at_positions_of_empty_iterator_fails() { + let order = Order { + id: 55, + items: vec![], + }; + + let failures = verify_that(order) + .named("order") + .extracting_ref("items", |o| &o.items) + .elements_ref_at([0, 1]) + .contains_exactly(["Apple", "Banana"]) + .display_failures(); + + assert_eq!( + failures, + &[ + r#"expected order.items at positions [0, 1] to contain exactly in order ["Apple", "Banana"] + but was: [] + expected: ["Apple", "Banana"] + missing: ["Apple", "Banana"] + extra: [] + out-of-order: [] +"# + ] + ); + } + + #[test] + fn verify_elements_at_out_of_bounds_position_fails() { + let order = Order { + id: 55, + items: vec!["Apple", "Banana", "Cherry"], + }; + + let failures = verify_that(order) + .named("order") + .extracting_ref("items", |o| &o.items) + .elements_ref_at([0, 3]) + .contains_exactly(["Apple", "Grapes"]) + .display_failures(); + + assert_eq!( + failures, + &[ + r#"expected order.items at positions [0, 3] to contain exactly in order ["Apple", "Grapes"] + but was: ["Apple"] + expected: ["Apple", "Grapes"] + missing: ["Grapes"] + extra: [] + out-of-order: [] +"# + ] + ); + } +} diff --git a/src/equality.rs b/src/equality.rs index 3564fdb..eae0280 100644 --- a/src/equality.rs +++ b/src/equality.rs @@ -8,7 +8,9 @@ use crate::expectations::{ HasDebugString, HasDisplayString, IsEqualTo, IsSameAs, has_debug_string, has_display_string, is_equal_to, is_same_as, not, }; -use crate::spec::{DiffFormat, Expectation, Expression, FailingStrategy, Invertible, Spec}; +use crate::spec::{ + DiffFormat, Expectation, Expecting, Expression, FailingStrategy, Invertible, Spec, +}; use crate::std::fmt::{Debug, Display}; use crate::std::format; use crate::std::string::{String, ToString}; diff --git a/src/error/mod.rs b/src/error/mod.rs index 9e54ba7..b2aa34f 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -3,7 +3,9 @@ use crate::colored::{mark_missing, mark_missing_string, mark_unexpected, mark_un use crate::expectations::{ ErrorHasSource, ErrorHasSourceMessage, error_has_source, error_has_source_message, not, }; -use crate::spec::{DiffFormat, Expectation, Expression, FailingStrategy, Invertible, Spec}; +use crate::spec::{ + DiffFormat, Expectation, Expecting, Expression, FailingStrategy, Invertible, Spec, +}; use crate::std::error::Error; use crate::std::format; use crate::std::string::{String, ToString}; diff --git a/src/float/mod.rs b/src/float/mod.rs index 5b98edc..69f77a6 100644 --- a/src/float/mod.rs +++ b/src/float/mod.rs @@ -98,7 +98,9 @@ mod cmp { use crate::assertions::{AssertIsCloseToWithDefaultMargin, AssertIsCloseToWithinMargin}; use crate::colored::mark_diff; use crate::expectations::{IsCloseTo, is_close_to, not}; - use crate::spec::{DiffFormat, Expectation, Expression, FailingStrategy, Invertible, Spec}; + use crate::spec::{ + DiffFormat, Expectation, Expecting, Expression, FailingStrategy, Invertible, Spec, + }; use crate::std::{format, string::String}; use float_cmp::{ApproxEq, F32Margin, F64Margin}; diff --git a/src/iterator/mod.rs b/src/iterator/mod.rs index 6a0a178..9e8136a 100644 --- a/src/iterator/mod.rs +++ b/src/iterator/mod.rs @@ -2,12 +2,13 @@ use crate::assertions::{ AssertFilteredElements, AssertIteratorContains, AssertIteratorContainsInAnyOrder, - AssertIteratorContainsInOrder, AssertOrderedElements, + AssertIteratorContainsInOrder, AssertOrderedElements, AssertOrderedElementsRef, }; use crate::colored::{ mark_all_items_in_collection, mark_missing, mark_missing_string, mark_selected_items_in_collection, mark_unexpected, mark_unexpected_string, }; +use crate::derived_spec::DerivedSpec; use crate::expectations::{ AllSatisfy, AnySatisfies, HasAtLeastNumberOfElements, HasSingleElement, IteratorContains, IteratorContainsAllInOrder, IteratorContainsAllOf, IteratorContainsAnyOf, @@ -21,9 +22,10 @@ use crate::expectations::{ }; use crate::properties::DefinedOrderProperty; use crate::spec::{ - DiffFormat, Expectation, Expression, FailingStrategy, GetFailures, Invertible, PanicOnFail, - Spec, + DiffFormat, Expectation, Expecting, Expression, FailingStrategy, GetFailures, Invertible, + PanicOnFail, Spec, }; +use crate::std::borrow::ToOwned; use crate::std::cmp::Ordering; use crate::std::fmt::Debug; use crate::std::mem; @@ -1004,6 +1006,9 @@ where } fn elements_at(self, indices: impl IntoIterator) -> Self::MultipleElements { + let indices = Vec::from_iter(indices); + let orig_subject_name = self.expression(); + let new_subject_name = format!("{orig_subject_name} at positions {indices:?}"); let indices = HashSet::<_>::from_iter(indices); self.mapping(|subject| { subject @@ -1012,6 +1017,94 @@ where .filter_map(|(i, v)| if indices.contains(&i) { Some(v) } else { None }) .collect() }) + .named(new_subject_name) + } +} + +impl<'a, S, T, U, R> AssertOrderedElementsRef for Spec<'a, S, R> +where + S: IntoIterator, + ::IntoIter: DefinedOrderProperty, + T: 'a + ToOwned + Debug, + U: Clone, + R: FailingStrategy, +{ + type SingleElement = DerivedSpec<'a, Spec<'a, Vec, R>, U>; + type MultipleElements = DerivedSpec<'a, Spec<'a, Vec, R>, Vec>; + + fn first_element_ref(self) -> Self::SingleElement { + let original_spec = self + .mapping(Vec::from_iter) + .expecting(has_at_least_number_of_elements(1)); + if original_spec.has_failures() { + PanicOnFail.do_fail_with(&original_spec.failures()); + unreachable!("Assertion failed and should have panicked! Please report a bug.") + } + let orig_subject_name = original_spec.expression(); + let new_subject_name = format!("the first element of {orig_subject_name}"); + original_spec.extracting_ref("", |collection| + collection.first() + .unwrap_or_else(|| + unreachable!("We should have asserted before, that there is at least one element in the collection/iterator. Please file a bug.") + ) + ).named(new_subject_name) + } + + fn last_element_ref(self) -> Self::SingleElement { + let original_spec = self + .mapping(Vec::from_iter) + .expecting(has_at_least_number_of_elements(1)); + if original_spec.has_failures() { + PanicOnFail.do_fail_with(&original_spec.failures()); + unreachable!("Assertion failed and should have panicked! Please report a bug.") + } + let orig_subject_name = original_spec.expression(); + let new_subject_name = format!("the last element of {orig_subject_name}"); + original_spec.extracting_ref("", |collection| + collection.last() + .unwrap_or_else(|| + unreachable!("We should have asserted before, that there is at least one element in the collection/iterator. Please file a bug.") + ) + ).named(new_subject_name) + } + + fn nth_element_ref(self, n: usize) -> Self::SingleElement { + let min_len = n + 1; + let original_spec = self + .mapping(Vec::from_iter) + .expecting(has_at_least_number_of_elements(min_len)); + if original_spec.has_failures() { + PanicOnFail.do_fail_with(&original_spec.failures()); + unreachable!("Assertion failed and should have panicked! Please report a bug.") + } + let orig_subject_name = original_spec.expression(); + let new_subject_name = format!("{orig_subject_name}[{n}]"); + original_spec.extracting_ref("", |collection| + collection.get(n) + .unwrap_or_else(|| + unreachable!("We should have asserted before, that there is at least one element in the collection/iterator. Please file a bug.") + ) + ).named(new_subject_name) + } + + fn elements_ref_at(self, indices: impl IntoIterator) -> Self::MultipleElements { + let indices = Vec::from_iter(indices); + let orig_subject_name = self.expression(); + let new_subject_name = format!("{orig_subject_name} at positions {indices:?}"); + let indices = HashSet::<_>::from_iter(indices); + let original_spec = self.mapping(Vec::from_iter); + original_spec.extracting_ref_iter(new_subject_name, |collection| { + collection + .enumerate() + .filter_map(|(i, e)| { + if indices.contains(&i) { + Some(e.to_owned()) + } else { + None + } + }) + .collect() + }) } } diff --git a/src/iterator/tests.rs b/src/iterator/tests.rs index 8256dc1..78593af 100644 --- a/src/iterator/tests.rs +++ b/src/iterator/tests.rs @@ -1,4 +1,5 @@ use crate::prelude::*; +use crate::std::string::String; use crate::std::{vec, vec::Vec}; #[derive(Debug)] @@ -231,7 +232,200 @@ fn verify_custom_iterator_does_not_contain_fails() { ); } -mod element_filters { +mod all_elements { + use super::*; + + #[derive(Debug)] + struct TestPerson { + name: String, + age: u8, + } + + #[test] + fn assert_each_element_of_an_iterator_of_integer() { + let subject = [2, 4, 6, 8, 10]; + + assert_that(subject) + .is_not_empty() + .each_element(|e| e.is_positive().is_at_most(20)); + } + + #[test] + fn assert_each_element_of_an_iterator_of_person() { + let subject = vec![ + TestPerson { + name: "John".into(), + age: 42, + }, + TestPerson { + name: "Jane".into(), + age: 20, + }, + ]; + + assert_that(subject) + .is_not_empty() + .each_element(|person| person.extracting(|p| p.age).is_at_most(42)); + } + + #[test] + fn assert_each_element_of_a_borrowed_iterator_of_integer() { + let subject = vec![2, 4, 6, 8, 10]; + + assert_that(&subject) + .is_not_empty() + .each_element(|e| e.is_positive().is_at_most(&20)); + } + + #[test] + fn assert_each_element_of_a_borrowed_iterator_of_person() { + let subject = vec![ + TestPerson { + name: "John".into(), + age: 42, + }, + TestPerson { + name: "Jane".into(), + age: 20, + }, + ]; + + assert_that(&subject) + .is_not_empty() + .each_element(|person| person.extracting(|p| &p.name).starts_with('J')); + } + + #[test] + #[should_panic = "expected numbers [1] to be not equal to 4\n but was: 4\n expected: not 4\n"] + fn assert_each_element_of_an_iterator_panics_if_one_assertion_fails() { + let subject = [2, 4, 6, 8, 10]; + + assert_that(subject) + .named("numbers") + .is_not_empty() + .each_element(|e| e.is_not_equal_to(4)); + } + + #[test] + fn verify_assert_each_element_of_an_iterator_fails() { + let subject = [2, 4, 6, 8, 10]; + + let failures = verify_that(&subject) + .named("numbers") + .each_element(|e| e.is_greater_than(&2).is_at_most(&7)) + .display_failures(); + + assert_eq!( + failures, + &[ + r"expected numbers [0] to be greater than 2 + but was: 2 + expected: > 2 +", + r"expected numbers [3] to be at most 7 + but was: 8 + expected: <= 7 +", + r"expected numbers [4] to be at most 7 + but was: 10 + expected: <= 7 +", + ] + ); + } + + #[test] + fn assert_any_element_of_an_iterator_of_str() { + let subject = ["one", "two", "three", "four", "five"]; + + assert_that(subject) + .is_not_empty() + .any_element(|e| e.contains("ee")); + } + + #[test] + fn assert_any_element_of_an_iterator_of_person() { + let subject = vec![ + TestPerson { + name: "John".into(), + age: 42, + }, + TestPerson { + name: "Jane".into(), + age: 20, + }, + ]; + + assert_that(subject) + .is_not_empty() + .any_element(|person| person.extracting(|p| p.age).is_at_most(20)); + } + + #[test] + fn assert_any_element_of_a_borrowed_iterator_of_str() { + let subject = vec!["one", "two", "three", "four", "five"]; + + assert_that(&subject) + .is_not_empty() + .any_element(|e| e.starts_with("fi")); + } + + #[test] + fn assert_any_element_of_a_borrowed_iterator_of_person() { + let subject = vec![ + TestPerson { + name: "John".into(), + age: 42, + }, + TestPerson { + name: "Jane".into(), + age: 20, + }, + ]; + + assert_that(&subject) + .is_not_empty() + .any_element(|person| person.extracting(|p| &p.name).ends_with('n')); + } + + #[test] + fn verify_assert_any_element_of_an_iterator_fails() { + let subject = ["one", "two", "three", "four", "five"]; + + let failures = verify_that(subject) + .named("words") + .any_element(|e| e.starts_with("fu")) + .display_failures(); + + assert_eq!( + failures, + &[ + r#"expected words [0] to start with "fu" + but was: "one" + expected: "fu" +"#, + r#"expected words [1] to start with "fu" + but was: "two" + expected: "fu" +"#, + r#"expected words [2] to start with "fu" + but was: "three" + expected: "fu" +"#, + r#"expected words [3] to start with "fu" + but was: "four" + expected: "fu" +"#, + r#"expected words [4] to start with "fu" + but was: "five" + expected: "fu" +"#, + ] + ); + } +} + +mod filtered_elements { use super::*; #[test] @@ -287,6 +481,173 @@ mod element_filters { ); } + #[test] + fn filtered_on_elements_of_iterator_even_elements() { + let subject = CustomCollection { + inner: vec![1, 2, 3, 4, 5], + }; + + assert_that(subject) + .filtered_on(|e| e & 1 == 0) + .contains_exactly_in_any_order([2, 4]); + } + + #[test] + fn elements_at_positions_of_iterator() { + let subject = CustomOrderedCollection { + inner: vec!["one", "two", "three", "four", "five"], + }; + + assert_that(subject) + .elements_at([0, 2, 4]) + .contains_exactly(["one", "three", "five"]); + } + + #[test] + fn any_satisfies_on_elements_of_iterator_value_is_equal_to_42() { + let subject = CustomCollection { + inner: vec![1, 41, 43, 42, 5], + }; + + assert_that(subject).any_satisfies(|e| *e == 42); + } + + #[test] + fn verify_any_satisfies_on_elements_of_iterator_value_is_equal_to_42_fails() { + let subject = CustomCollection { + inner: vec![1, 2, 43, 41, 5], + }; + + let failures = verify_that(subject) + .named("my_numbers") + .any_satisfies(|e| *e == 42) + .display_failures(); + + assert_eq!( + failures, + &[ + r"expected any element of my_numbers to satisfy the predicate, but none did + actual: [1, 2, 43, 41, 5] +" + ] + ); + } + + #[test] + fn all_satisfy_on_elements_of_iterator_value_is_greater_than_42() { + let subject = CustomCollection { + inner: vec![47, 46, 45, 44, 43], + }; + + assert_that(subject).all_satisfy(|e| *e > 42); + } + + #[test] + fn verify_all_satisfy_on_elements_of_iterator_value_is_greater_than_42_fails() { + let subject = CustomCollection { + inner: vec![43, 44, 45, 42, 47], + }; + + let failures = verify_that(subject) + .named("my_numbers") + .all_satisfy(|e| *e > 42) + .display_failures(); + + assert_eq!( + failures, + &[ + r"expected all elements of my_numbers to satisfy the predicate, but 1 did not + actual: [43, 44, 45, 42, 47] + failing: [42] +" + ] + ); + } + + #[test] + fn none_satisfies_on_elements_of_iterator_value_is_greater_than_42() { + let subject = CustomCollection { + inner: vec![42, 41, 40, 39, 38], + }; + + assert_that(subject).none_satisfies(|e| *e > 42); + } + + #[test] + fn verify_none_satisfies_on_elements_of_iterator_value_is_greater_than_42_fails() { + let subject = CustomCollection { + inner: vec![41, 43, 45, 42, 47], + }; + + let failures = verify_that(subject) + .named("my_numbers") + .none_satisfies(|e| *e > 42) + .display_failures(); + + assert_eq!( + failures, + &[ + r"expected none of the elements of my_numbers to satisfy the predicate, but 3 did + actual: [41, 43, 45, 42, 47] + failing: [43, 45, 47] +" + ] + ); + } + + #[cfg(feature = "colored")] + mod colored { + use super::*; + + #[test] + fn highlight_all_satisfy_on_elements_of_iterator() { + let subject = CustomCollection { + inner: vec![43, 44, 45, 42, 47], + }; + + let failures = verify_that(subject) + .named("my_numbers") + .with_diff_format(DIFF_FORMAT_RED_YELLOW) + .all_satisfy(|e| *e > 42) + .display_failures(); + + assert_eq!( + failures, + &[ + "expected all elements of my_numbers to satisfy the predicate, but 1 did not\n \ + actual: [43, 44, 45, \u{1b}[31m42\u{1b}[0m, 47]\n \ + failing: [42]\n" + ] + ); + } + + #[test] + fn highlight_none_satisfies_on_elements_of_iterator() { + let subject = CustomCollection { + inner: vec![41, 43, 45, 42, 47], + }; + + let failures = verify_that(subject) + .named("my_numbers") + .with_diff_format(DIFF_FORMAT_RED_YELLOW) + .none_satisfies(|e| *e > 42) + .display_failures(); + + assert_eq!( + failures, + &[ + "expected none of the elements of my_numbers to satisfy the predicate, but 3 did\n \ + actual: [41, \u{1b}[31m43\u{1b}[0m, \u{1b}[31m45\u{1b}[0m, 42, \u{1b}[31m47\u{1b}[0m]\n \ + failing: [43, 45, 47]\n" + ] + ); + } + } +} + +mod extracted_elements { + use super::*; + #[test] fn first_element_of_iterator_with_one_element() { let subject = CustomOrderedCollection { @@ -437,17 +798,6 @@ mod element_filters { ); } - #[test] - fn filtered_on_elements_of_iterator_even_elements() { - let subject = CustomCollection { - inner: vec![1, 2, 3, 4, 5], - }; - - assert_that(subject) - .filtered_on(|e| e & 1 == 0) - .contains_exactly_in_any_order([2, 4]); - } - #[test] fn elements_at_positions_of_iterator() { let subject = CustomOrderedCollection { @@ -460,143 +810,308 @@ mod element_filters { } #[test] - fn any_satisfies_on_elements_of_iterator_value_is_equal_to_42() { - let subject = CustomCollection { - inner: vec![1, 41, 43, 42, 5], - }; + fn verify_elements_at_positions_of_empty_iterator_fails() { + let subject: CustomOrderedCollection<&str> = CustomOrderedCollection { inner: vec![] }; - assert_that(subject).any_satisfies(|e| *e == 42); + let failures = verify_that(subject) + .named("my_custom_collection") + .elements_at([0, 1]) + .contains_exactly(["one", "two"]) + .display_failures(); + + assert_eq!( + failures, + &[ + r#"expected my_custom_collection at positions [0, 1] to contain exactly in order ["one", "two"] + but was: [] + expected: ["one", "two"] + missing: ["one", "two"] + extra: [] + out-of-order: [] +"# + ] + ); } #[test] - fn verify_any_satisfies_on_elements_of_iterator_value_is_equal_to_42_fails() { - let subject = CustomCollection { - inner: vec![1, 2, 43, 41, 5], + fn verify_elements_at_out_of_bounds_position_fails() { + let subject = CustomOrderedCollection { + inner: vec!["one", "two", "three"], }; let failures = verify_that(subject) - .named("my_numbers") - .any_satisfies(|e| *e == 42) + .named("my_custom_collection") + .elements_at([0, 3]) + .contains_exactly(["one", "four"]) .display_failures(); assert_eq!( failures, &[ - r"expected any element of my_numbers to satisfy the predicate, but none did - actual: [1, 2, 43, 41, 5] -" + r#"expected my_custom_collection at positions [0, 3] to contain exactly in order ["one", "four"] + but was: ["one"] + expected: ["one", "four"] + missing: ["four"] + extra: [] + out-of-order: [] +"# ] ); } +} + +mod extracted_elements_ref { + use super::*; #[test] - fn all_satisfy_on_elements_of_iterator_value_is_greater_than_42() { - let subject = CustomCollection { - inner: vec![47, 46, 45, 44, 43], - }; + fn first_element_of_iterator_with_one_element() { + let subject = vec!["single"]; - assert_that(subject).all_satisfy(|e| *e > 42); + assert_that(subject) + .first_element_ref() + .is_equal_to("single") + .has_length(6) + .starts_with("si"); } #[test] - fn verify_all_satisfy_on_elements_of_iterator_value_is_greater_than_42_fails() { - let subject = CustomCollection { - inner: vec![43, 44, 45, 42, 47], - }; + fn first_element_of_iterator_with_several_elements() { + let subject = vec!["one", "two", "three", "four", "five"]; + + assert_that(subject) + .first_element_ref() + .is_equal_to("one") + .has_length(3) + .starts_with('o'); + } + + #[cfg(feature = "panic")] + #[test] + fn first_element_of_iterator_with_no_elements_fails() { + let subject: Vec = vec![]; + + assert_that_code(|| { + assert_that(subject) + .named("my_custom_collection") + .with_diff_format(DIFF_FORMAT_NO_HIGHLIGHT) + .first_element_ref() + .is_equal_to(42); + }) + .panics_with_message( + r"expected my_custom_collection to have at least one element, but has no elements + actual: [] +", + ); + } + + #[test] + fn verify_first_element_of_iterator_assertion_fails() { + let subject = vec!["four", "two", "three"]; let failures = verify_that(subject) - .named("my_numbers") - .all_satisfy(|e| *e > 42) + .named("my_collection") + .first_element_ref() + .is_equal_to("one") .display_failures(); assert_eq!( failures, &[ - r"expected all elements of my_numbers to satisfy the predicate, but 1 did not - actual: [43, 44, 45, 42, 47] - failing: [42] -" + r#"expected the first element of my_collection to be equal to "one" + but was: "four" + expected: "one" +"# ] ); } #[test] - fn none_satisfies_on_elements_of_iterator_value_is_greater_than_42() { - let subject = CustomCollection { - inner: vec![42, 41, 40, 39, 38], - }; + fn last_element_of_iterator_with_one_element() { + let subject = vec!["single"]; - assert_that(subject).none_satisfies(|e| *e > 42); + assert_that(subject) + .last_element_ref() + .is_equal_to("single") + .has_length(6) + .starts_with("si"); } #[test] - fn verify_none_satisfies_on_elements_of_iterator_value_is_greater_than_42_fails() { - let subject = CustomCollection { - inner: vec![41, 43, 45, 42, 47], - }; + fn last_element_of_iterator_with_several_elements() { + let subject = vec!["one", "two", "three", "four", "five"]; + + assert_that(subject) + .last_element_ref() + .is_equal_to("five") + .has_length(4) + .starts_with("fi"); + } + + #[cfg(feature = "panic")] + #[test] + fn last_element_of_iterator_with_no_elements_fails() { + let subject: Vec = vec![]; + + assert_that_code(|| { + assert_that(subject) + .named("my_custom_collection") + .with_diff_format(DIFF_FORMAT_NO_HIGHLIGHT) + .last_element_ref() + .is_equal_to(42); + }) + .panics_with_message( + r"expected my_custom_collection to have at least one element, but has no elements + actual: [] +", + ); + } + + #[test] + fn verify_last_element_of_iterator_assertion_fails() { + let subject = vec!["one", "two", "four"]; let failures = verify_that(subject) - .named("my_numbers") - .none_satisfies(|e| *e > 42) + .named("my_collection") + .last_element_ref() + .is_equal_to("three") .display_failures(); assert_eq!( failures, &[ - r"expected none of the elements of my_numbers to satisfy the predicate, but 3 did - actual: [41, 43, 45, 42, 47] - failing: [43, 45, 47] -" + r#"expected the last element of my_collection to be equal to "three" + but was: "four" + expected: "three" +"# ] ); } - #[cfg(feature = "colored")] - mod colored { - use super::*; + #[test] + fn nth_element_of_iterator_with_one_element() { + let subject = vec!["single"]; - #[test] - fn highlight_all_satisfy_on_elements_of_iterator() { - let subject = CustomCollection { - inner: vec![43, 44, 45, 42, 47], - }; + assert_that(subject) + .nth_element_ref(0) + .is_equal_to("single") + .has_length(6) + .starts_with("si"); + } - let failures = verify_that(subject) - .named("my_numbers") - .with_diff_format(DIFF_FORMAT_RED_YELLOW) - .all_satisfy(|e| *e > 42) - .display_failures(); + #[test] + fn nth_element_of_iterator_with_several_elements_second_element() { + let subject = vec!["one", "two", "three", "four", "five"]; - assert_eq!( - failures, - &[ - "expected all elements of my_numbers to satisfy the predicate, but 1 did not\n \ - actual: [43, 44, 45, \u{1b}[31m42\u{1b}[0m, 47]\n \ - failing: [42]\n" - ] - ); - } + assert_that(subject) + .nth_element_ref(1) + .is_equal_to("two") + .has_length(3) + .starts_with("tw"); + } - #[test] - fn highlight_none_satisfies_on_elements_of_iterator() { - let subject = CustomCollection { - inner: vec![41, 43, 45, 42, 47], - }; + #[test] + fn nth_element_of_iterator_with_several_elements_fifth_element() { + let subject = vec!["one", "two", "three", "four", "five"]; - let failures = verify_that(subject) - .named("my_numbers") - .with_diff_format(DIFF_FORMAT_RED_YELLOW) - .none_satisfies(|e| *e > 42) - .display_failures(); + assert_that(subject) + .nth_element_ref(4) + .is_equal_to("five") + .has_length(4) + .starts_with("fi"); + } - assert_eq!( - failures, - &[ - "expected none of the elements of my_numbers to satisfy the predicate, but 3 did\n \ - actual: [41, \u{1b}[31m43\u{1b}[0m, \u{1b}[31m45\u{1b}[0m, 42, \u{1b}[31m47\u{1b}[0m]\n \ - failing: [43, 45, 47]\n" - ] - ); - } + #[cfg(feature = "panic")] + #[test] + fn nth_element_of_iterator_with_five_elements_6th_element_fails() { + let subject = vec!["one", "two", "three", "four", "five"]; + + assert_that_code(|| { + assert_that(subject) + .named("my_custom_collection") + .with_diff_format(DIFF_FORMAT_NO_HIGHLIGHT) + .nth_element_ref(5) + .is_equal_to("five"); + }) + .panics_with_message( + r#"expected my_custom_collection to have at least 6 elements, but has 5 elements + actual: ["one", "two", "three", "four", "five"] +"#, + ); + } + + #[test] + fn verify_nth_element_of_iterator_assertion_fails() { + let subject = vec!["one", "four", "three"]; + + let failures = verify_that(subject) + .named("my_collection") + .nth_element_ref(1) + .is_equal_to("two") + .display_failures(); + + assert_eq!( + failures, + &[r#"expected my_collection[1] to be equal to "two" + but was: "four" + expected: "two" +"#] + ); + } + + #[test] + fn elements_at_positions_of_iterator() { + let subject = vec!["one", "two", "three", "four", "five"]; + + assert_that(subject) + .elements_ref_at([0, 2, 4]) + .contains_exactly(["one", "three", "five"]); + } + + #[test] + fn verify_elements_at_positions_of_empty_iterator_fails() { + let subject: Vec<&str> = vec![]; + + let failures = verify_that(subject) + .named("my_custom_collection") + .elements_ref_at([0, 1]) + .contains_exactly(["one", "two"]) + .display_failures(); + + assert_eq!( + failures, + &[ + r#"expected my_custom_collection at positions [0, 1] to contain exactly in order ["one", "two"] + but was: [] + expected: ["one", "two"] + missing: ["one", "two"] + extra: [] + out-of-order: [] +"# + ] + ); + } + + #[test] + fn verify_elements_at_out_of_bounds_position_fails() { + let subject = vec!["one", "two", "three"]; + + let failures = verify_that(subject) + .named("my_custom_collection") + .elements_ref_at([0, 3]) + .contains_exactly(["one", "four"]) + .display_failures(); + + assert_eq!( + failures, + &[ + r#"expected my_custom_collection at positions [0, 3] to contain exactly in order ["one", "four"] + but was: ["one"] + expected: ["one", "four"] + missing: ["four"] + extra: [] + out-of-order: [] +"# + ] + ); } } diff --git a/src/length.rs b/src/length.rs index 845b7d0..bb2f9fd 100644 --- a/src/length.rs +++ b/src/length.rs @@ -8,7 +8,9 @@ use crate::expectations::{ has_length_greater_than, has_length_in_range, has_length_less_than, is_empty, not, }; use crate::properties::{IsEmptyProperty, LengthProperty}; -use crate::spec::{DiffFormat, Expectation, Expression, FailingStrategy, Invertible, Spec}; +use crate::spec::{ + DiffFormat, Expectation, Expecting, Expression, FailingStrategy, Invertible, Spec, +}; use crate::std::fmt::Debug; use crate::std::ops::RangeBounds; use crate::std::{format, string::String}; diff --git a/src/lib.rs b/src/lib.rs index c62e896..8a92bc1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -144,7 +144,7 @@ //! ); //! ``` //! -//! See [`Spec::each_element()`] for more details. +//! See [`AssertElements`] for more details. //! //! Assert some elements of a collection or an iterator to satisfy a predicate: //! @@ -161,12 +161,12 @@ //! assert_that!(subject).none_satisfies(|e| *e < 42); //! ``` //! -//! See [`AssertElements`] for more details. +//! See [`AssertFilteredElements`] for more details. //! //! ## Asserting specific elements of a collection or an iterator //! -//! Filter assertions are handy to assert specific elements of a collection or -//! an iterator. +//! We can extract one or multiple elements of a collection or an itertor to +//! assert specific elements only. //! //! Assert the only element of a collection or an iterator: //! @@ -178,7 +178,27 @@ //! assert_that!(subject).single_element().is_equal_to("single"); //! ``` //! -//! Assert the first, the last, or the nth element of a collection or an iterator: +//! We can filter the elements to be asserted on a condition: +//! +//! ``` +//! use asserting::prelude::*; +//! +//! let subject = [1, 2, 3, 4, 5]; +//! +//! assert_that!(subject) +//! .filtered_on(|e| e & 1 == 0) +//! .contains_exactly_in_any_order([2, 4]); +//! +//! let subject = ["one", "two", "three", "four"]; +//! +//! assert_that!(subject) +//! .filtered_on(|e| e.len() == 5) +//! .single_element() +//! .is_equal_to("three"); +//! ``` +//! +//! If the collection or iterator yields its elements in a defined order, we can +//! assert the first, last, or nth element of the iterator: //! //! ``` //! use asserting::prelude::*; @@ -190,23 +210,20 @@ //! assert_that!(numbers).nth_element(3).is_equal_to(4); //! ``` //! -//! Filter the elements to be asserted on a condition: +//! We can also chain the extraction of several elements each for individual +//! assertions. //! //! ``` //! use asserting::prelude::*; //! -//! let subject = [1, 2, 3, 4, 5]; -//! -//! assert_that!(subject) -//! .filtered_on(|e| e & 1 == 0) -//! .contains_exactly_in_any_order([2, 4]); -//! -//! let subject = ["one", "two", "three", "four"]; +//! let subject = ["one", "two", "three", "four", "five"]; //! //! assert_that!(subject) -//! .filtered_on(|e| e.len() == 5) -//! .single_element() -//! .is_equal_to("three"); +//! .first_element_ref().is_equal_to("one") +//! .and() +//! .last_element_ref().is_equal_to("five") +//! .and() +//! .nth_element_ref(2).is_equal_to("three"); //! ``` //! //! Pick the elements of a collection or an iterator at given positions: @@ -252,27 +269,57 @@ //! We can extract a property of a custom type and assert its value: //! //! ``` -//! # use asserting::prelude::*; -//! struct MyStruct { -//! important_property: String, -//! other_property: f64, +//! use asserting::prelude::*; +//! +//! struct Person { +//! name: String, +//! age: u8, //! } //! -//! let some_thing = MyStruct { -//! important_property: "imperdiet aliqua zzril eiusmod".into(), -//! other_property: 99.9, +//! let person = Person { +//! name: "Silvia".into(), +//! age: 25, //! }; //! -//! assert_that!(some_thing).extracting(|s| s.important_property) -//! .is_equal_to("imperdiet aliqua zzril eiusmod"); +//! assert_that!(person).extracting(|s| s.name) +//! .is_equal_to("Silvia"); +//! ``` +//! +//! We can also chain the extraction of multiple properties of the same subject. +//! To do so, we use combinations of the methods [`extracting_ref`] and [`and`]: +//! +//! ``` +//! use asserting::prelude::*; +//! +//! struct Person { +//! name: String, +//! age: u8, +//! } //! +//! let person = Person { +//! name: "Silvia".into(), +//! age: 25 +//! }; +//! +//! assert_that!(person) +//! .extracting_ref("name", |p| &p.name) +//! .is_equal_to("Silvia") +//! .and() +//! .extracting_ref("age", |p| &p.age) +//! .is_at_least(18); //! ``` //! -//! Or we can map a custom type that does not implement a required trait to some -//! supported type, e.g., a tuple in this example: +//! Note: The [`extracting_ref`] method can only be used if the type of the +//! extracted value implements `ToOwned`. +//! +//! In some cases we might want to map a custom type that does not implement a +//! required trait to some other type that implements the required traits. For +//! example, we map a value of custom type `Point` to a tuple of two integers +//! as `Point` does not implement `PartialEq` and `Debug`: //! //! ``` -//! # use asserting::prelude::*; +//! use asserting::prelude::*; +//! //! struct Point { //! x: i64, //! y: i64, @@ -464,7 +511,7 @@ //! ).is_equal_to(42); //! ``` //! -//! When using the `verfiy_*` variants of the macros or functions for each +//! When using the `verify_*` variants of the macros or functions for each //! failing assertion, a failure of type [`AssertFailure`] is added to the //! [`Spec`]. We can read the failures collected by calling the [`failures()`] //! method, like so: @@ -515,8 +562,8 @@ //! //! [`Expectation`]s enable us to write specialized assertions by combining //! several basic expectations. In case a custom assertion cannot be composed -//! out of the provided expectations but writing a custom [`Expectation`] is too -//! cumbersome, we can write a custom assertion method directly without any +//! out of the provided expectations, but writing a custom [`Expectation`] is +//! too cumbersome, we can write a custom assertion method directly without any //! custom [`Expectation`]. See the //! [Writing custom assertions without writing an expectation](#writing-custom-assertions-without-writing-an-expectation) //! chapter below for an example. @@ -698,7 +745,7 @@ //! # ) //! # } //! # } -//! use asserting::spec::{FailingStrategy, Spec}; +//! use asserting::spec::{Expecting, FailingStrategy, Spec}; //! use std::fmt::Debug; //! //! pub trait AssertEither { @@ -822,14 +869,14 @@ //! assert_that!(person).is_over_18(); //! ``` //! -//! [`AssertElements`]: assertions::AssertFilteredElements +//! [`AssertElements`]: assertions::AssertElements +//! [`AssertFilteredElements`]: assertions::AssertFilteredElements //! [`AssertFailure`]: spec::AssertFailure //! [`Expectation`]: spec::Expectation //! [`LengthProperty`]: properties::LengthProperty //! [`Spec`]: spec::Spec -//! [`Spec::each_element()`]: spec::Spec::each_element -//! [`Spec::expecting()`]: spec::Spec::expecting -//! [`Spec::satisfies()`]: spec::Spec::satisfies +//! [`Spec::expecting()`]: spec::Expecting::expecting +//! [`Spec::satisfies()`]: spec::Satisfies::satisfies //! [`SoftPanic::soft_panic()`]: spec::SoftPanic::soft_panic //! [`assert_that`]: spec::assert_that //! [`assert_that_code`]: spec::assert_that_code @@ -839,6 +886,8 @@ //! [`failures()`]: spec::GetFailures::failures //! [`named()`]: spec::Spec::named //! [`located_at()`]: spec::Spec::located_at +//! [`extracting_ref`]: spec::Spec::extracting_ref +//! [`and`]: spec::And::and //! [`serde::Serialize`]: serde_core::Serialize #![doc(html_root_url = "https://docs.rs/asserting/0.14.0")] @@ -905,6 +954,7 @@ pub mod __private { pub mod assertions; pub mod colored; +pub mod derived_spec; pub mod expectations; pub mod prelude; pub mod properties; diff --git a/src/map/mod.rs b/src/map/mod.rs index 3f9e592..4975a20 100644 --- a/src/map/mod.rs +++ b/src/map/mod.rs @@ -11,7 +11,9 @@ use crate::expectations::{ }; use crate::iterator::collect_selected_values; use crate::properties::MapProperties; -use crate::spec::{DiffFormat, Expectation, Expression, FailingStrategy, Invertible, Spec}; +use crate::spec::{ + DiffFormat, Expectation, Expecting, Expression, FailingStrategy, Invertible, Spec, +}; use crate::std::fmt::Debug; use crate::std::format; use crate::std::string::String; diff --git a/src/number.rs b/src/number.rs index f38dbf7..15e0365 100644 --- a/src/number.rs +++ b/src/number.rs @@ -13,7 +13,9 @@ use crate::properties::{ AdditiveIdentityProperty, DecimalProperties, InfinityProperty, IsNanProperty, MultiplicativeIdentityProperty, SignumProperty, }; -use crate::spec::{DiffFormat, Expectation, Expression, FailingStrategy, Invertible, Spec}; +use crate::spec::{ + DiffFormat, Expectation, Expecting, Expression, FailingStrategy, Invertible, Spec, +}; use crate::std::fmt::Debug; use crate::std::format; use crate::std::string::String; diff --git a/src/option/mod.rs b/src/option/mod.rs index 5c2d938..5f66877 100644 --- a/src/option/mod.rs +++ b/src/option/mod.rs @@ -1,12 +1,10 @@ //! Implementation of assertions for `Option` values. -use crate::assertions::{ - AssertBorrowedOptionValue, AssertHasValue, AssertOption, AssertOptionValue, -}; +use crate::assertions::{AssertHasValue, AssertOption, AssertOptionValue}; use crate::colored::{mark_missing, mark_unexpected}; use crate::expectations::{HasValue, IsNone, IsSome, has_value, is_none, is_some}; use crate::spec::{ - DiffFormat, Expectation, Expression, FailingStrategy, Invertible, Spec, Unknown, + DiffFormat, Expectation, Expecting, Expression, FailingStrategy, Invertible, Spec, Unknown, }; use crate::std::fmt::Debug; use crate::std::{format, string::String}; @@ -55,7 +53,7 @@ where } } -impl<'a, T, R> AssertBorrowedOptionValue for Spec<'a, &'a Option, R> +impl<'a, T, R> AssertOptionValue for Spec<'a, &'a Option, R> where R: FailingStrategy, { diff --git a/src/order/mod.rs b/src/order/mod.rs index 63c906d..c907704 100644 --- a/src/order/mod.rs +++ b/src/order/mod.rs @@ -6,7 +6,9 @@ use crate::expectations::{ IsAfter, IsAtLeast, IsAtMost, IsBefore, IsBetween, IsGreaterThan, IsLessThan, is_after, is_at_least, is_at_most, is_before, is_between, is_greater_than, is_less_than, }; -use crate::spec::{DiffFormat, Expectation, Expression, FailingStrategy, Invertible, Spec}; +use crate::spec::{ + DiffFormat, Expectation, Expecting, Expression, FailingStrategy, Invertible, Spec, +}; use crate::std::fmt::Debug; use crate::std::{format, string::String}; diff --git a/src/panic/mod.rs b/src/panic/mod.rs index ff8df1b..3c908c6 100644 --- a/src/panic/mod.rs +++ b/src/panic/mod.rs @@ -3,7 +3,7 @@ use crate::assertions::AssertCodePanics; use crate::colored::{mark_missing_string, mark_unexpected_string}; use crate::expectations::{DoesNotPanic, DoesPanic, does_not_panic, does_panic}; -use crate::spec::{Code, DiffFormat, Expectation, Expression, FailingStrategy, Spec}; +use crate::spec::{Code, DiffFormat, Expectation, Expecting, Expression, FailingStrategy, Spec}; use crate::std::any::Any; use crate::std::panic; diff --git a/src/prelude.rs b/src/prelude.rs index 88a8464..de8d260 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -20,8 +20,8 @@ pub use super::{ colored::{DEFAULT_DIFF_FORMAT, DIFF_FORMAT_NO_HIGHLIGHT}, properties::*, spec::{ - CollectFailures, DoFail, GetFailures, Location, PanicOnFail, SoftPanic, assert_that, - verify_that, + And, CollectFailures, DoFail, Expecting, GetFailures, Location, PanicOnFail, Satisfies, + SoftPanic, assert_that, verify_that, }, verify_that, }; diff --git a/src/range/mod.rs b/src/range/mod.rs index 22b5727..d564001 100644 --- a/src/range/mod.rs +++ b/src/range/mod.rs @@ -4,7 +4,9 @@ use crate::assertions::AssertInRange; use crate::colored::{mark_missing, mark_missing_string, mark_unexpected}; use crate::expectations::{IsInRange, is_in_range, not}; use crate::properties::IsEmptyProperty; -use crate::spec::{DiffFormat, Expectation, Expression, FailingStrategy, Invertible, Spec}; +use crate::spec::{ + DiffFormat, Expectation, Expecting, Expression, FailingStrategy, Invertible, Spec, +}; use crate::std::fmt::Debug; use crate::std::format; use crate::std::ops::{Bound, Range, RangeBounds, RangeInclusive}; diff --git a/src/result/mod.rs b/src/result/mod.rs index bdc3d59..057a060 100644 --- a/src/result/mod.rs +++ b/src/result/mod.rs @@ -1,15 +1,14 @@ //! Implementation of assertions for `Result` values. use crate::assertions::{ - AssertBorrowedResultValue, AssertHasError, AssertHasErrorMessage, AssertHasValue, AssertResult, - AssertResultValue, + AssertHasError, AssertHasErrorMessage, AssertHasValue, AssertResult, AssertResultValue, }; use crate::colored::{mark_missing, mark_unexpected}; use crate::expectations::{ HasError, HasValue, IsErr, IsOk, has_error, has_value, is_equal_to, is_err, is_ok, }; use crate::spec::{ - DiffFormat, Expectation, Expression, FailingStrategy, Invertible, Spec, Unknown, + DiffFormat, Expectation, Expecting, Expression, FailingStrategy, Invertible, Spec, Unknown, }; use crate::std::fmt::{Debug, Display}; use crate::std::{ @@ -74,7 +73,7 @@ where } } -impl<'a, T, E, R> AssertBorrowedResultValue for Spec<'a, &'a Result, R> +impl<'a, T, E, R> AssertResultValue for Spec<'a, &'a Result, R> where T: Debug, E: Debug, diff --git a/src/spec/mod.rs b/src/spec/mod.rs index 2c95f93..554fe26 100644 --- a/src/spec/mod.rs +++ b/src/spec/mod.rs @@ -1,16 +1,18 @@ //! This is the core of the `asserting` crate. +use crate::assertions::AssertElements; use crate::colored; +use crate::derived_spec::DerivedSpec; use crate::expectations::satisfies; #[cfg(feature = "recursive")] use crate::recursive_comparison::RecursiveComparison; use crate::std::any; -use crate::std::borrow::Borrow; -use crate::std::borrow::Cow; +use crate::std::borrow::{Borrow, Cow, ToOwned}; use crate::std::error::Error as StdError; use crate::std::fmt::{self, Debug, Display}; use crate::std::format; use crate::std::ops::Deref; +use crate::std::slice; use crate::std::string::{String, ToString}; use crate::std::vec; use crate::std::vec::Vec; @@ -658,11 +660,6 @@ impl Spec<'_, S, R> { &self.expression } - /// Returns the location in source code or test code if it has been set. - pub fn location(&self) -> Option> { - self.location - } - /// Returns the description or the assertion if it has been set. pub fn description(&self) -> Option<&str> { self.description.as_deref() @@ -724,8 +721,8 @@ impl<'a, S, R> Spec<'a, S, R> { /// value and the expected value. /// /// Note: This method must be called before an assertion method is called to - /// have an effect on the failure message of the assertion as failure - /// messages are formatted immediately when an assertion is executed. + /// affect the failure message of the assertion as failure messages are + /// formatted immediately when an assertion is executed. #[must_use = "a spec does nothing unless an assertion method is called"] pub const fn with_diff_format(mut self, diff_format: DiffFormat) -> Self { self.diff_format = diff_format; @@ -775,21 +772,106 @@ impl<'a, S, R> Spec<'a, S, R> { RecursiveComparison::new(self) } + /// Extracts a property from the current subject. + /// + /// The extracting closure gets a reference to the current subject as an + /// argument and should return a reference to the extracted property. The + /// given property name is used in failure reports for referencing the + /// property for which an assertion fails. + /// + /// Use this method if you want to extract multiple properties from the + /// same subject for individual assertions on each of these properties. + /// To extract another property from the original subject, call the `and` + /// method to switch back to the original subject before calling + /// `extracting_ref` for the other property. + /// + /// # Arguments + /// + /// * `property_name` - A name describing the extracted property used for + /// referencing that property in failure reports. + /// * `extract` - A closure that returns a reference to the property to be + /// extracted. + /// + /// # Examples + /// + /// ``` + /// use asserting::prelude::*; + /// + /// #[derive(Debug, Clone, Copy, PartialEq)] + /// enum Gender { + /// Male, + /// Female, + /// NonBinary, + /// PreferNotToSay, + /// } + /// + /// struct Person { + /// name: String, + /// age: u8, + /// gender: Gender, + /// } + /// + /// impl Person { + /// fn name(&self) -> &str { + /// &self.name + /// } + /// } + /// + /// let my_friend = Person { + /// name: "Silvia".into(), + /// age: 27, + /// gender: Gender::Female, + /// }; + /// + /// assert_that!(my_friend) + /// .extracting_ref("name", Person::name) + /// .is_equal_to("Silvia") + /// .and() + /// .extracting_ref("age", |p| &p.age) + /// .is_at_least(18) + /// .and() + /// .extracting_ref("gender", |p| &p.gender) + /// .is_equal_to(Gender::Female); + /// ``` + pub fn extracting_ref( + self, + property_name: impl Into>, + extract: F, + ) -> DerivedSpec<'a, Self, U> + where + F: FnOnce(&S) -> &B, + B: ToOwned + ?Sized, + { + let derived_subject = extract(&self.subject).to_owned(); + let orig_subject_name = &self.expression; + let property_name = property_name.into(); + let expression = Expression(format!("{orig_subject_name}.{property_name}").into()); + let diff_format = self.diff_format.clone(); + DerivedSpec::new(self, derived_subject, expression, diff_format) + } + /// Maps the current subject to some other value. /// /// It takes a closure that maps the current subject to a new subject and /// returns a new `Spec` with the value returned by the closure as the new /// subject. The new subject may have a different type than the original - /// subject. All other data like expression, description, and location are + /// subject. All other data like description, location, and diff format are /// taken over from this `Spec` into the returned `Spec`. /// - /// This function is useful when having a custom type, and a specific - /// property of this type shall be asserted only. + /// This method is useful when having a custom type, and one specific + /// property of this type shall be asserted only. If you want to assert + /// multiple properties of the same subject, use the [`extracting_ref`] + /// method instead. + /// + /// This method is similar to the [`mapping`] method. In contrast to + /// [`mapping`], this method does not copy the subject's name + /// (or expression) but resets it to the default "subject". The idea is + /// that the "extracted" property is most likely a different subject than + /// the original one. /// - /// This is an alias function to the [`mapping()`](Spec::mapping) function. - /// Both functions do exactly the same. The idea is to provide different - /// names to be able to express the intent more clearly when used in - /// assertions. + /// It is recommended to give the extracted property a specific name by + /// calling the `named` method. This helps with spotting the cause of a + /// failing assertion. /// /// # Example /// @@ -806,16 +888,29 @@ impl<'a, S, R> Spec<'a, S, R> { /// other_property: 99.9, /// }; /// - /// assert_that!(some_thing).extracting(|s| s.important_property) + /// assert_that!(some_thing) + /// .extracting(|s| s.important_property) + /// .named("some_thing.important_property") /// .is_equal_to("imperdiet aliqua zzril eiusmod"); /// /// ``` + /// + /// [`extracting_ref`]: Self::extracting_ref + /// [`mapping`]: Self::mapping #[must_use = "a spec does nothing unless an assertion method is called"] - pub fn extracting(self, extractor: F) -> Spec<'a, U, R> + pub fn extracting(self, extract: F) -> Spec<'a, U, R> where F: FnOnce(S) -> U, { - self.mapping(extractor) + Spec { + subject: extract(self.subject), + expression: Expression::default(), + description: self.description, + location: self.location, + failures: self.failures, + diff_format: self.diff_format, + failing_strategy: self.failing_strategy, + } } /// Maps the current subject to some other value. @@ -826,13 +921,13 @@ impl<'a, S, R> Spec<'a, S, R> { /// subject. All other data like expression, description, and location are /// taken over from this `Spec` into the returned `Spec`. /// - /// This function is useful if some type does not implement a trait - /// required for an assertion. + /// This method is useful if some type does not implement a trait required + /// for an assertion. /// - /// `Spec` also provides the [`extracting()`](Spec::extracting) function, - /// which is an alias to this function. Both functions do exactly the same. - /// Choose that function of which its name expresses the intent more - /// clearly. + /// `Spec` also provides the [`extracting()`](Spec::extracting) method, + /// which is similar to this method. In contrast to this method, + /// [`extracting()`](Spec::extracting) does not copy the subject's name + /// (or expression) but resets it to the default "subject". /// /// # Example /// @@ -854,12 +949,12 @@ impl<'a, S, R> Spec<'a, S, R> { /// assertion. So we map the subject of the type `Point` to a tuple of its /// fields. #[must_use = "a spec does nothing unless an assertion method is called"] - pub fn mapping(self, mapper: F) -> Spec<'a, U, R> + pub fn mapping(self, map: F) -> Spec<'a, U, R> where F: FnOnce(S) -> U, { Spec { - subject: mapper(self.subject), + subject: map(self.subject), expression: self.expression, description: self.description, location: self.location, @@ -870,173 +965,16 @@ impl<'a, S, R> Spec<'a, S, R> { } } -impl Spec<'_, S, R> +impl<'a, I, R> AssertElements<'a, I> for Spec<'a, I, R> where - R: FailingStrategy, + I: IntoIterator, { - /// Asserts the given expectation. - /// - /// In case the expectation is not meet, the assertion fails according to - /// the current failing strategy of this `Spec`. - /// - /// This method is called from the implementations of the assertion traits - /// defined in the [`assertions`](crate::assertions) module. Implementations - /// of custom assertions will call this method with a proper expectation. - /// - /// # Examples - /// - /// ``` - /// use asserting::expectations::{IsEmpty, IsEqualTo}; - /// use asserting::prelude::*; - /// - /// assert_that!(7 * 6).expecting(IsEqualTo {expected: 42 }); - /// - /// assert_that!("").expecting(IsEmpty); - /// ``` - #[allow(clippy::needless_pass_by_value, clippy::return_self_not_must_use)] - #[track_caller] - pub fn expecting(mut self, mut expectation: impl Expectation) -> Self { - if !expectation.test(&self.subject) { - let message = - expectation.message(&self.expression, &self.subject, false, &self.diff_format); - self.do_fail_with_message(message); - } - self - } + type Output = Spec<'a, (), R>; - /// Asserts whether the given predicate is meet. - /// - /// This method takes a predicate function and calls it as an expectation. - /// In case the predicate function returns false, it does fail with a - /// generic failure message and according to the current failing strategy of - /// this `Spec`. - /// - /// This method can be used to do simple custom assertions without - /// implementing an [`Expectation`] and an assertion trait. - /// - /// # Examples - /// - /// ``` - /// use asserting::prelude::*; - /// - /// fn is_odd(value: &i32) -> bool { - /// value & 1 == 1 - /// } - /// - /// assert_that!(37).satisfies(is_odd); - /// - /// let failures = verify_that!(22).satisfies(is_odd).display_failures(); - /// - /// assert_that!(failures).contains_exactly([ - /// "expected 22 to satisfy the given predicate, but returned false\n" - /// ]); - /// ``` - /// - /// To assert a predicate with a custom failure message instead of the - /// generic one, use the method - /// [`satisfies_with_message`](Spec::satisfies_with_message). - #[allow(clippy::return_self_not_must_use)] - #[track_caller] - pub fn satisfies

(self, predicate: P) -> Self + fn each_element(mut self, assert: A) -> Self::Output where - P: Fn(&S) -> bool, - { - self.expecting(satisfies(predicate)) - } - - /// Asserts whether the given predicate is meet. - /// - /// This method takes a predicate function and calls it as an expectation. - /// In case the predicate function returns false, it does fail with the - /// provided failure message and according to the current failing strategy - /// of this `Spec`. - /// - /// This method can be used to do simple custom assertions without - /// implementing an [`Expectation`] and an assertion trait. - /// - /// # Examples - /// - /// ``` - /// use asserting::prelude::*; - /// - /// fn is_odd(value: &i32) -> bool { - /// value & 1 == 1 - /// } - /// - /// assert_that!(37).satisfies_with_message("expected my number to be odd", is_odd); - /// - /// let failures = verify_that!(22) - /// .satisfies_with_message("expected my number to be odd", is_odd) - /// .display_failures(); - /// - /// assert_that!(failures).contains_exactly([ - /// "expected my number to be odd\n" - /// ]); - /// ``` - /// - /// To assert a predicate with a generic failure message instead of - /// providing one use the method - /// [`satisfies`](Spec::satisfies). - #[allow(clippy::return_self_not_must_use)] - #[track_caller] - pub fn satisfies_with_message

(self, message: impl Into, predicate: P) -> Self - where - P: Fn(&S) -> bool, - { - self.expecting(satisfies(predicate).with_message(message)) - } -} - -impl<'a, I, R> Spec<'a, I, R> { - /// Iterates over the elements of a collection or an iterator and executes - /// the given assertions for each of those elements. If all elements are - /// asserted successfully, the whole assertion succeeds. - /// - /// It iterates over all elements of the collection or iterator and collects - /// the failure messages for those elements where the assertion fails. In - /// other words, it does not stop iterating when the assertion for one - /// element fails. - /// - /// The failure messages contain the position of the element within the - /// collection or iterator. The position is 0-based. So a failure message - /// for the first element contains `[0]`, the second `[1]`, and so on. - /// - /// # Example - /// - /// The following assertion: - /// - /// ```should_panic - /// use asserting::prelude::*; - /// - /// let numbers = [2, 4, 6, 8, 10]; - /// - /// assert_that!(numbers).each_element(|e| - /// e.is_greater_than(2) - /// .is_at_most(7) - /// ); - /// ``` - /// - /// will print: - /// - /// ```console - /// expected numbers [0] to be greater than 2 - /// but was: 2 - /// expected: > 2 - /// - /// expected numbers [3] to be at most 7 - /// but was: 8 - /// expected: <= 7 - /// - /// expected numbers [4] to be at most 7 - /// but was: 10 - /// expected: <= 7 - /// ``` - #[allow(clippy::return_self_not_must_use)] - #[track_caller] - pub fn each_element(mut self, assert: A) -> Spec<'a, (), R> - where - I: IntoIterator, - A: Fn(Spec<'a, T, CollectFailures>) -> Spec<'a, B, CollectFailures>, + A: Fn(Spec<'a, ::Item, CollectFailures>) -> B, + B: GetFailures, { let root_expression = &self.expression; let mut position = -1; @@ -1051,7 +989,7 @@ impl<'a, I, R> Spec<'a, I, R> { diff_format: self.diff_format.clone(), failing_strategy: CollectFailures, }; - let failures = assert(element_spec).failures; + let failures = assert(element_spec).failures(); self.failures.extend(failures); } if !self.failures.is_empty() @@ -1070,52 +1008,10 @@ impl<'a, I, R> Spec<'a, I, R> { } } - /// Iterates over the elements of a collection or an iterator and executes - /// the given assertions for each of those elements. If the assertion of any - /// element is successful, the iteration stops and the whole assertion - /// succeeds. - /// - /// If the assertion fails for all elements, the failures of the assertion - /// for all elements are collected. - /// - /// The failure messages contain the position of the element within the - /// collection or iterator. The position is 0-based. So a failure message - /// for the first element contains `[0]`, the second `[1]`, and so on. - /// - /// # Example - /// - /// The following assertion: - /// - /// ```should_panic - /// use asserting::prelude::*; - /// - /// let digit_names = ["one", "two", "three"]; - /// - /// assert_that!(digit_names).any_element(|e| - /// e.contains('x') - /// ); - /// ``` - /// - /// will print: - /// - /// ```console - /// expected digit_names [0] to contain 'x' - /// but was: "one" - /// expected: 'x' - /// - /// expected digit_names [1] to contain 'x' - /// but was: "two" - /// expected: 'x' - /// - /// expected digit_names [2] to contain 'x' - /// but was: "three" - /// expected: 'x' - /// ``` - #[track_caller] - pub fn any_element(mut self, assert: A) -> Spec<'a, (), R> + fn any_element(mut self, assert: A) -> Self::Output where - I: IntoIterator, - A: Fn(Spec<'a, T, CollectFailures>) -> Spec<'a, B, CollectFailures>, + A: Fn(Spec<'a, ::Item, CollectFailures>) -> B, + B: GetFailures, { let root_expression = &self.expression; let mut any_success = false; @@ -1131,7 +1027,7 @@ impl<'a, I, R> Spec<'a, I, R> { diff_format: self.diff_format.clone(), failing_strategy: CollectFailures, }; - let failures = assert(element_spec).failures; + let failures = assert(element_spec).failures(); if failures.is_empty() { any_success = true; break; @@ -1155,6 +1051,26 @@ impl<'a, I, R> Spec<'a, I, R> { } } +impl<'a, I, R> Spec<'a, I, R> +where + I: IntoIterator, +{ + pub(crate) fn extracting_ref_iter( + self, + property_name: impl Into>, + extract: F, + ) -> DerivedSpec<'a, Spec<'a, Vec<::Item>, R>, Vec> + where + for<'b> F: Fn(slice::Iter<'b, ::Item>) -> Vec, + { + let property_name = Expression(property_name.into()); + let diff_format = self.diff_format.clone(); + let orig_spec = self.mapping(Vec::from_iter); + let new_subject = extract(orig_spec.subject.iter()); + DerivedSpec::new(orig_spec, new_subject, property_name, diff_format) + } +} + /// Trigger failing of an assertion according to the failing strategy of its /// implementing spec-like struct. pub trait DoFail { @@ -1264,6 +1180,264 @@ impl SoftPanic for Spec<'_, S, CollectFailures> { } } +/// Chaining another assertion. +/// +/// Both the previous assertion and the next assertion must be met to pass the +/// overall assertion. +pub trait And { + /// The return type of the `and()` method. + type Output; + + /// Express explicitly that another assertion must be met to pass the + /// overall assertion. + /// + /// Note: assertions can be changed anyway without calling this `and` + /// method. So in most cases, this method does nothing and just offers a + /// different style of writing assertions. + /// + /// In combination with the [`Spec::extracting_ref`] the `and` method can be + /// used to chain multiple assertions on the original subject, instead of + /// the extracted one. + /// + /// # Examples + /// + /// Calling the `and` method on the original subject is optional and just + /// a question of style how one wants to write assertions. + /// + /// ``` + /// use asserting::prelude::*; + /// + /// let subject = "the answer to all important questions in the universe is 42"; + /// + /// assert_that(subject).is_not_empty() + /// .and().contains("answer to all important questions") + /// .and().ends_with("42"); + /// + /// // the same assertions can be written without calling `and()`: + /// + /// assert_that(subject).is_not_empty() + /// .contains("answer to all important questions") + /// .ends_with("42"); + /// ``` + /// + /// In combination with the [`Spec::extracting_ref`] method, the `and` method + /// switches back to the original subject to chain multiple assertions on + /// different extracted fields. + /// + /// ``` + /// use asserting::prelude::*; + /// + /// #[derive(Debug, Clone, Copy, PartialEq)] + /// enum Gender { + /// Male, + /// Female, + /// NonBinary, + /// PreferNotToSay, + /// } + /// + /// struct Person { + /// name: String, + /// age: u8, + /// gender: Gender, + /// } + /// + /// impl Person { + /// fn name(&self) -> &str { + /// &self.name + /// } + /// } + /// + /// let my_friend = Person { + /// name: "Silvia".into(), + /// age: 27, + /// gender: Gender::Female, + /// }; + /// + /// assert_that!(my_friend) + /// .extracting_ref("name", Person::name) + /// .is_equal_to("Silvia") + /// .and() + /// .extracting_ref("age", |p| &p.age) + /// .is_at_least(18) + /// .and() + /// .extracting_ref("gender", |p| &p.gender) + /// .is_equal_to(Gender::Female); + /// ``` + /// + /// The specified property name helps in case of a failing assertion as the + /// failure report references the more detailed name, such as + /// `my_friend.name` or `my_friend.age`, instead of just `subject`. + #[must_use = "calling the `and` method without calling another assertion method is useless"] + fn and(self) -> Self::Output; +} + +impl And for Spec<'_, S, R> { + type Output = Self; + + fn and(self) -> Self::Output { + self + } +} + +/// Verify whether a subject satisfies a given predicate. +/// +/// A predicate is a function that takes the subject and returns true if the +/// subject meets certain criteria. +pub trait Satisfies { + /// Asserts whether the given predicate is meet. + /// + /// A predicate is a function that takes the subject and returns true if the + /// subject meets certain criteria. + /// + /// This method takes a predicate function and calls it as an expectation. + /// In case the predicate function returns false, it does fail with a + /// generic failure message and according to the current failing strategy of + /// this `Spec`. + /// + /// This method can be used to do simple custom assertions without + /// implementing an [`Expectation`] and an assertion trait. + /// + /// # Examples + /// + /// ``` + /// use asserting::prelude::*; + /// + /// fn is_odd(value: &i32) -> bool { + /// value & 1 == 1 + /// } + /// + /// assert_that!(37).satisfies(is_odd); + /// + /// let failures = verify_that!(22).satisfies(is_odd).display_failures(); + /// + /// assert_that!(failures).contains_exactly([ + /// "expected 22 to satisfy the given predicate, but returned false\n" + /// ]); + /// ``` + /// + /// To assert a predicate with a custom failure message instead of the + /// generic one, use the method + /// [`satisfies_with_message`](Spec::satisfies_with_message). + #[allow(clippy::return_self_not_must_use)] + #[track_caller] + fn satisfies

(self, predicate: P) -> Self + where + P: Fn(&S) -> bool; + + /// Asserts whether the given predicate is meet. + /// + /// A predicate is a function that takes the subject and returns true if the + /// subject meets certain criteria. + /// + /// This method takes a predicate function and calls it as an expectation. + /// In case the predicate function returns false, it does fail with the + /// provided failure message and according to the current failing strategy + /// of this `Spec`. + /// + /// This method can be used to do simple custom assertions without + /// implementing an [`Expectation`] and an assertion trait. + /// + /// # Examples + /// + /// ``` + /// use asserting::prelude::*; + /// + /// fn is_odd(value: &i32) -> bool { + /// value & 1 == 1 + /// } + /// + /// assert_that!(37).satisfies_with_message("expected my number to be odd", is_odd); + /// + /// let failures = verify_that!(22) + /// .satisfies_with_message("expected my number to be odd", is_odd) + /// .display_failures(); + /// + /// assert_that!(failures).contains_exactly([ + /// "expected my number to be odd\n" + /// ]); + /// ``` + /// + /// To assert a predicate with a generic failure message instead of + /// providing one, use the method [`satisfies`](Satisfies::satisfies). + #[allow(clippy::return_self_not_must_use)] + #[track_caller] + fn satisfies_with_message

(self, message: impl Into, predicate: P) -> Self + where + P: Fn(&S) -> bool; +} + +impl Satisfies for Spec<'_, S, R> +where + R: FailingStrategy, +{ + fn satisfies

(self, predicate: P) -> Self + where + P: Fn(&S) -> bool, + { + self.expecting(satisfies(predicate)) + } + + fn satisfies_with_message

(self, message: impl Into, predicate: P) -> Self + where + P: Fn(&S) -> bool, + { + self.expecting(satisfies(predicate).with_message(message)) + } +} + +/// Verify whether a subject meets the given expectation (impl of +/// [`Expectation`]) and record a failure if it is not met. +pub trait Expecting { + /// Asserts the given expectation. + /// + /// In case the expectation is not meet, the assertion fails, according to + /// the current failing strategy of this `Spec`. + /// + /// This method is called from the implementations of the assertion traits + /// defined in the [`assertions`](crate::assertions) module. Implementations + /// of custom assertions will call this method with a proper expectation. + /// + /// # Examples + /// + /// ``` + /// use asserting::expectations::{IsEmpty, IsEqualTo}; + /// use asserting::prelude::*; + /// + /// assert_that!(7 * 6).expecting(IsEqualTo {expected: 42 }); + /// + /// assert_that!("").expecting(IsEmpty); + /// ``` + #[allow(clippy::needless_pass_by_value, clippy::return_self_not_must_use)] + #[track_caller] + fn expecting(self, expectation: impl Expectation) -> Self; +} + +impl Expecting for Spec<'_, S, R> +where + R: FailingStrategy, +{ + fn expecting(mut self, mut expectation: impl Expectation) -> Self { + if !expectation.test(&self.subject) { + let message = + expectation.message(&self.expression, &self.subject, false, &self.diff_format); + self.do_fail_with_message(message); + } + self + } +} + +/// Access the location of an assertion in the source code or test code. +pub trait GetLocation<'a> { + /// Returns the location in source code or test code if it has been set. + fn location(&self) -> Option>; +} + +impl<'a, S, R> GetLocation<'a> for Spec<'a, S, R> { + fn location(&self) -> Option> { + self.location + } +} + /// Access the assertion-failures collected by a `Spec` or spec-like struct. pub trait GetFailures { /// Returns whether there are assertion failures collected so far. diff --git a/src/spec/tests.rs b/src/spec/tests.rs index 54ddc64..d8fcc64 100644 --- a/src/spec/tests.rs +++ b/src/spec/tests.rs @@ -1,9 +1,10 @@ use crate::prelude::*; use crate::spec::{AssertFailure, Expression, OwnedLocation}; +#[cfg(feature = "colored")] +use crate::std::any::type_name_of_val; use crate::std::{ format, string::{String, ToString}, - vec, }; #[test] @@ -230,193 +231,27 @@ fn soft_assertions_panic_once_with_multiple_failure_messages() { .soft_panic(); } -#[derive(Debug)] -struct TestPerson { - name: String, - age: u8, -} - -#[test] -fn assert_each_element_of_an_iterator_of_integer() { - let subject = [2, 4, 6, 8, 10]; - - assert_that(subject) - .is_not_empty() - .each_element(|e| e.is_positive().is_at_most(20)); -} - -#[test] -fn assert_each_element_of_an_iterator_of_person() { - let subject = vec![ - TestPerson { - name: "John".into(), - age: 42, - }, - TestPerson { - name: "Jane".into(), - age: 20, - }, - ]; - - assert_that(subject) - .is_not_empty() - .each_element(|person| person.extracting(|p| p.age).is_at_most(42)); -} - -#[test] -fn assert_each_element_of_a_borrowed_iterator_of_integer() { - let subject = vec![2, 4, 6, 8, 10]; - - assert_that(&subject) - .is_not_empty() - .each_element(|e| e.is_positive().is_at_most(&20)); -} - -#[test] -fn assert_each_element_of_a_borrowed_iterator_of_person() { - let subject = vec![ - TestPerson { - name: "John".into(), - age: 42, - }, - TestPerson { - name: "Jane".into(), - age: 20, - }, - ]; - - assert_that(&subject) - .is_not_empty() - .each_element(|person| person.extracting(|p| &p.name).starts_with('J')); -} - -#[test] -#[should_panic = "expected numbers [1] to be not equal to 4\n but was: 4\n expected: not 4\n"] -fn assert_each_element_of_an_iterator_panics_if_one_assertion_fails() { - let subject = [2, 4, 6, 8, 10]; - - assert_that(subject) - .named("numbers") - .is_not_empty() - .each_element(|e| e.is_not_equal_to(4)); -} - -#[test] -fn verify_assert_each_element_of_an_iterator_fails() { - let subject = [2, 4, 6, 8, 10]; - - let failures = verify_that(&subject) - .named("numbers") - .each_element(|e| e.is_greater_than(&2).is_at_most(&7)) - .display_failures(); - - assert_eq!( - failures, - &[ - r"expected numbers [0] to be greater than 2 - but was: 2 - expected: > 2 -", - r"expected numbers [3] to be at most 7 - but was: 8 - expected: <= 7 -", - r"expected numbers [4] to be at most 7 - but was: 10 - expected: <= 7 -", - ] - ); -} - -#[test] -fn assert_any_element_of_an_iterator_of_str() { - let subject = ["one", "two", "three", "four", "five"]; - - assert_that(subject) - .is_not_empty() - .any_element(|e| e.contains("ee")); -} - -#[test] -fn assert_any_element_of_an_iterator_of_person() { - let subject = vec![ - TestPerson { - name: "John".into(), - age: 42, - }, - TestPerson { - name: "Jane".into(), - age: 20, - }, - ]; - - assert_that(subject) - .is_not_empty() - .any_element(|person| person.extracting(|p| p.age).is_at_most(20)); -} - -#[test] -fn assert_any_element_of_a_borrowed_iterator_of_str() { - let subject = vec!["one", "two", "three", "four", "five"]; - - assert_that(&subject) - .is_not_empty() - .any_element(|e| e.starts_with("fi")); -} - -#[test] -fn assert_any_element_of_a_borrowed_iterator_of_person() { - let subject = vec![ - TestPerson { - name: "John".into(), - age: 42, - }, - TestPerson { - name: "Jane".into(), - age: 20, - }, - ]; - - assert_that(&subject) - .is_not_empty() - .any_element(|person| person.extracting(|p| &p.name).ends_with('n')); -} - +#[cfg(feature = "colored")] #[test] -fn verify_assert_any_element_of_an_iterator_fails() { - let subject = ["one", "two", "three", "four", "five"]; - - let failures = verify_that(subject) - .named("words") - .any_element(|e| e.starts_with("fu")) - .display_failures(); +fn and_called_on_spec_does_nothing() { + let subject = "the answer to all important questions is 42".to_string(); - assert_eq!( - failures, - &[ - r#"expected words [0] to start with "fu" - but was: "one" - expected: "fu" -"#, - r#"expected words [1] to start with "fu" - but was: "two" - expected: "fu" -"#, - r#"expected words [2] to start with "fu" - but was: "three" - expected: "fu" -"#, - r#"expected words [3] to start with "fu" - but was: "four" - expected: "fu" -"#, - r#"expected words [4] to start with "fu" - but was: "five" - expected: "fu" -"#, - ] - ); + let original_spec = verify_that(subject) + .named("answer") + .with_diff_format(DIFF_FORMAT_RED_BLUE) + .is_empty(); + let original_spec_type = type_name_of_val(&original_spec); + let original_subject = original_spec.subject().clone(); + let original_diff_format = original_spec.diff_format().clone(); + let original_failures = original_spec.failures(); + assert!(!original_failures.is_empty()); + + let returned_spec = original_spec.and(); + + assert_eq!(type_name_of_val(&returned_spec), original_spec_type); + assert_eq!(returned_spec.subject(), &original_subject); + assert_eq!(returned_spec.diff_format(), &original_diff_format); + assert_eq!(returned_spec.failures(), original_failures); } #[cfg(feature = "colored")] diff --git a/src/string/mod.rs b/src/string/mod.rs index dca10b7..f77ed67 100644 --- a/src/string/mod.rs +++ b/src/string/mod.rs @@ -11,7 +11,9 @@ use crate::expectations::{ string_contains_any_of, string_ends_with, string_starts_with, }; use crate::properties::{CharCountProperty, DefinedOrderProperty, IsEmptyProperty, LengthProperty}; -use crate::spec::{DiffFormat, Expectation, Expression, FailingStrategy, Invertible, Spec}; +use crate::spec::{ + DiffFormat, Expectation, Expecting, Expression, FailingStrategy, Invertible, Spec, +}; use crate::std::fmt::Debug; use crate::std::str::Chars; use crate::std::{ @@ -703,7 +705,9 @@ mod regex { use crate::assertions::AssertStringMatches; use crate::colored::{mark_missing_string, mark_unexpected_string}; use crate::expectations::{StringMatches, not, string_matches}; - use crate::spec::{DiffFormat, Expectation, Expression, FailingStrategy, Invertible, Spec}; + use crate::spec::{ + DiffFormat, Expectation, Expecting, Expression, FailingStrategy, Invertible, Spec, + }; use crate::std::fmt::Debug; use crate::std::format; use crate::std::string::String;