diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2024-02-02 23:53:09 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2024-02-02 23:53:09 +0000 |
commit | bd89ff2a891dfb1a37aa7732a80c58637f98d71c (patch) | |
tree | 3e293be638feec77e6366d73e3e10a5773927bd9 | |
parent | 5cb64615012bfa4deba9bf061a901ce4ce389d2d (diff) | |
parent | 74aede000e0520b58e51ab6e6844cffc416ced4a (diff) | |
download | googletest-simpleperf-release.tar.gz |
Snap for 11400057 from 74aede000e0520b58e51ab6e6844cffc416ced4a to simpleperf-releasesimpleperf-release
Change-Id: Ia58e56825e6d98827a4e61bc1eea83536c69ccb1
72 files changed, 2751 insertions, 1335 deletions
diff --git a/config.toml b/.cargo/config.toml index 7bc2412..7bc2412 100644 --- a/config.toml +++ b/.cargo/config.toml diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json index ce5ec95..dae245d 100644 --- a/.cargo_vcs_info.json +++ b/.cargo_vcs_info.json @@ -1,6 +1,6 @@ { "git": { - "sha1": "6e5405db2217d37006720c101beb1b91199a3a26" + "sha1": "91a15f5eb416cbf97cd8b6d8831263f2c861b859" }, "path_in_vcs": "googletest" }
\ No newline at end of file @@ -6,7 +6,7 @@ rust_library { host_supported: true, crate_name: "googletest", cargo_env_compat: true, - cargo_pkg_version: "0.10.0", + cargo_pkg_version: "0.11.0", srcs: ["src/lib.rs"], edition: "2021", rustlibs: [ @@ -11,9 +11,9 @@ [package] edition = "2021" -rust-version = "1.59.0" +rust-version = "1.66.0" name = "googletest" -version = "0.10.0" +version = "0.11.0" authors = [ "Bradford Hovinen <hovinen@google.com>", "Bastien Jacot-Guillarmod <bjacotg@google.com>", @@ -34,24 +34,23 @@ categories = [ ] license = "Apache-2.0" repository = "https://github.com/google/googletest-rust" -resolver = "1" [dependencies.anyhow] version = "1" optional = true [dependencies.googletest_macro] -version = "0.10.0" +version = "0.11.0" [dependencies.num-traits] -version = "0.2.15" +version = "0.2.17" [dependencies.proptest] version = "1.2.0" optional = true [dependencies.regex] -version = "1.6.0" +version = "1.7.3" [dependencies.rustversion] version = "1.0.14" diff --git a/Cargo.toml.orig b/Cargo.toml.orig new file mode 100644 index 0000000..9fedcfb --- /dev/null +++ b/Cargo.toml.orig @@ -0,0 +1,44 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +[package] +name = "googletest" +version = "0.11.0" +keywords = ["unit", "matcher", "testing", "assertions"] +categories = ["development-tools", "development-tools::testing"] +description = "A rich assertion and matcher library inspired by GoogleTest for C++" +repository = "https://github.com/google/googletest-rust" +readme = "../README.md" +license = "Apache-2.0" +edition = "2021" +rust-version = "1.66.0" +authors = [ + "Bradford Hovinen <hovinen@google.com>", + "Bastien Jacot-Guillarmod <bjacotg@google.com>", + "Maciej Pietrzak <mpi@google.com>", + "Martin Geisler <mgeisler@google.com>", +] + +[dependencies] +googletest_macro = { path = "../googletest_macro", version = "0.11.0" } +anyhow = { version = "1", optional = true } +num-traits = "0.2.17" +proptest = { version = "1.2.0", optional = true } +regex = "1.7.3" +rustversion = "1.0.14" + +[dev-dependencies] +indoc = "2" +quickcheck = "1.0.3" +serial_test = "2.0.0" @@ -1,19 +1,24 @@ +# This project was upgraded with external_updater. +# Usage: tools/external_updater/updater.sh update external/rust/crates/googletest +# For more info, check https://cs.android.com/android/platform/superproject/+/main:tools/external_updater/README.md + name: "googletest" description: "A rich assertion and matcher library inspired by GoogleTest for C++" third_party { + license_type: NOTICE + last_upgrade_date { + year: 2024 + month: 2 + day: 2 + } identifier { type: "crates.io" - value: "https://crates.io/crates/googletest" + value: "https://static.crates.io/crates/googletest/googletest-0.11.0.crate" + version: "0.10.0" } identifier { type: "Archive" value: "https://static.crates.io/crates/googletest/googletest-0.10.0.crate" - } - version: "0.10.0" - license_type: NOTICE - last_upgrade_date { - year: 2023 - month: 9 - day: 1 + version: "0.11.0" } } @@ -25,11 +25,14 @@ This library brings the rich assertion types of Google's C++ testing library * A new set of assertion macros offering similar functionality to those of [GoogleTest](https://google.github.io/googletest/primer.html#assertions). -**The minimum supported Rust version is 1.59**. We recommend using at least -version 1.66 for the best developer experience. +**The minimum supported Rust version is 1.66**. > :warning: The API is not fully stable and may still be changed until we > publish version 1.0. +> +> Moreover, any items or modules starting with `__` (double underscores) must +> not be used directly. Those items or modules are only for internal uses and +> their API may change without a major version update. ## Assertions and matchers @@ -162,10 +165,10 @@ impl<T: PartialEq + Debug> Matcher for MyEqMatcher<T> { fn describe(&self, matcher_result: MatcherResult) -> String { match matcher_result { - MatcherResult::Matches => { + MatcherResult::Match => { format!("is equal to {:?} the way I define it", self.expected) } - MatcherResult::DoesNotMatch => { + MatcherResult::NoMatch => { format!("isn't equal to {:?} the way I define it", self.expected) } } diff --git a/crate_docs.md b/crate_docs.md index a00c219..8c8b47c 100644 --- a/crate_docs.md +++ b/crate_docs.md @@ -153,38 +153,48 @@ The following matchers are provided in GoogleTest Rust: | [`superset_of`] | A container containing all elements of the argument. | | [`unordered_elements_are!`] | A container whose elements the arguments match, in any order. | +[`all!`]: matchers::all +[`any!`]: matchers::any [`anything`]: matchers::anything [`approx_eq`]: matchers::approx_eq [`char_count`]: matchers::char_count [`container_eq`]: matchers::container_eq [`contains`]: matchers::contains +[`contains_each!`]: matchers::contains_each [`contains_regex`]: matchers::contains_regex [`contains_substring`]: matchers::contains_substring [`displays_as`]: matchers::displays_as [`each`]: matchers::each +[`elements_are!`]: matchers::elements_are [`empty`]: matchers::empty [`ends_with`]: matchers::ends_with [`eq`]: matchers::eq [`eq_deref_of`]: matchers::eq_deref_of [`err`]: matchers::err +[`field!`]: matchers::field [`ge`]: matchers::ge [`gt`]: matchers::gt [`has_entry`]: matchers::has_entry +[`is_contained_in!`]: matchers::is_contained_in [`is_nan`]: matchers::is_nan [`le`]: matchers::le [`len`]: matchers::len [`lt`]: matchers::lt [`matches_regex`]: matchers::matches_regex +[`matches_pattern!`]: matchers::matches_pattern [`near`]: matchers::near [`none`]: matchers::none [`not`]: matchers::not +[`pat!`]: matchers::pat [`ok`]: matchers::ok [`points_to`]: matchers::points_to +[`pointwise!`]: matchers::pointwise [`predicate`]: matchers::predicate [`some`]: matchers::some [`starts_with`]: matchers::starts_with [`subset_of`]: matchers::subset_of [`superset_of`]: matchers::superset_of +[`unordered_elements_are!`]: matchers::unordered_elements_are [`Deref`]: std::ops::Deref [`Display`]: std::fmt::Display [`HashMap`]: std::collections::HashMap @@ -199,7 +209,7 @@ a struct holding the matcher's data and have it implement the trait [`Matcher`]: ```no_run -use googletest::matcher::{Matcher, MatcherResult}; +use googletest::{description::Description, matcher::{Matcher, MatcherResult}}; use std::fmt::Debug; struct MyEqMatcher<T> { @@ -217,13 +227,13 @@ impl<T: PartialEq + Debug> Matcher for MyEqMatcher<T> { } } - fn describe(&self, matcher_result: MatcherResult) -> String { + fn describe(&self, matcher_result: MatcherResult) -> Description { match matcher_result { MatcherResult::Match => { - format!("is equal to {:?} the way I define it", self.expected) + format!("is equal to {:?} the way I define it", self.expected).into() } MatcherResult::NoMatch => { - format!("isn't equal to {:?} the way I define it", self.expected) + format!("isn't equal to {:?} the way I define it", self.expected).into() } } } @@ -233,7 +243,7 @@ impl<T: PartialEq + Debug> Matcher for MyEqMatcher<T> { It is recommended to expose a function which constructs the matcher: ```no_run - # use googletest::matcher::{Matcher, MatcherResult}; + # use googletest::{description::Description, matcher::{Matcher, MatcherResult}}; # use std::fmt::Debug; # # struct MyEqMatcher<T> { @@ -251,13 +261,13 @@ impl<T: PartialEq + Debug> Matcher for MyEqMatcher<T> { # } # } # - # fn describe(&self, matcher_result: MatcherResult) -> String { + # fn describe(&self, matcher_result: MatcherResult) -> Description { # match matcher_result { # MatcherResult::Match => { - # format!("is equal to {:?} the way I define it", self.expected) + # format!("is equal to {:?} the way I define it", self.expected).into() # } # MatcherResult::NoMatch => { - # format!("isn't equal to {:?} the way I define it", self.expected) + # format!("isn't equal to {:?} the way I define it", self.expected).into() # } # } # } @@ -272,7 +282,7 @@ impl<T: PartialEq + Debug> Matcher for MyEqMatcher<T> { ``` # use googletest::prelude::*; -# use googletest::matcher::{Matcher, MatcherResult}; +# use googletest::{description::Description, matcher::{Matcher, MatcherResult}}; # use std::fmt::Debug; # # struct MyEqMatcher<T> { @@ -290,13 +300,13 @@ impl<T: PartialEq + Debug> Matcher for MyEqMatcher<T> { # } # } # -# fn describe(&self, matcher_result: MatcherResult) -> String { +# fn describe(&self, matcher_result: MatcherResult) -> Description { # match matcher_result { # MatcherResult::Match => { -# format!("is equal to {:?} the way I define it", self.expected) +# format!("is equal to {:?} the way I define it", self.expected).into() # } # MatcherResult::NoMatch => { -# format!("isn't equal to {:?} the way I define it", self.expected) +# format!("isn't equal to {:?} the way I define it", self.expected).into() # } # } # } diff --git a/src/assertions.rs b/src/assertions.rs index 7664486..2668028 100644 --- a/src/assertions.rs +++ b/src/assertions.rs @@ -120,23 +120,23 @@ /// not supported; see [Rust by Example](https://doc.rust-lang.org/rust-by-example/primitives/tuples.html#tuples). #[macro_export] macro_rules! verify_that { - ($actual:expr, [$($expecteds:expr),+]) => { + ($actual:expr, [$($expecteds:expr),+ $(,)?]) => { $crate::assertions::internal::check_matcher( &$actual, - $crate::elements_are![$($expecteds),+], + $crate::matchers::elements_are![$($expecteds),+], stringify!($actual), $crate::internal::source_location::SourceLocation::new(file!(), line!(), column!()), ) }; - ($actual:expr, {$($expecteds:expr),+}) => { + ($actual:expr, {$($expecteds:expr),+ $(,)?}) => { $crate::assertions::internal::check_matcher( &$actual, - $crate::unordered_elements_are![$($expecteds),+], + $crate::matchers::unordered_elements_are![$($expecteds),+], stringify!($actual), $crate::internal::source_location::SourceLocation::new(file!(), line!(), column!()), ) }; - ($actual:expr, $expected:expr) => { + ($actual:expr, $expected:expr $(,)?) => { $crate::assertions::internal::check_matcher( &$actual, $expected, @@ -309,16 +309,49 @@ macro_rules! fail { /// Matches the given value against the given matcher, panicking if it does not /// match. /// +/// ```should_panic +/// # use googletest::prelude::*; +/// # fn should_fail() { +/// let value = 2; +/// assert_that!(value, eq(3)); // Fails and panics. +/// # } +/// # should_fail(); +/// ``` +/// /// This is analogous to assertions in most Rust test libraries, where a failed /// assertion causes a panic. /// +/// One may optionally add arguments which will be formatted and appended to a +/// failure message. For example: +/// +/// ```should_panic +/// # use googletest::prelude::*; +/// # fn should_fail() { +/// let value = 2; +/// let extra_information = "Some additional information"; +/// assert_that!(value, eq(3), "Test failed. Extra information: {extra_information}."); +/// # } +/// # should_fail(); +/// ``` +/// +/// This is output as follows: +/// +/// ```text +/// Value of: value +/// Expected: is equal to 3 +/// Actual: 2, +/// which isn't equal to 3 +/// at ... +/// Test failed. Extra information: Some additional information. +/// ``` +/// /// **Note for users of [GoogleTest for C++](http://google.github.io/googletest/):** /// This differs from the `ASSERT_THAT` macro in that it panics rather /// than triggering an early return from the invoking function. To get behaviour /// equivalent to `ASSERT_THAT`, use [`verify_that!`] with the `?` operator. #[macro_export] macro_rules! assert_that { - ($actual:expr, $expected:expr) => { + ($actual:expr, $expected:expr $(,)?) => { match $crate::verify_that!($actual, $expected) { Ok(_) => {} Err(e) => { @@ -328,6 +361,19 @@ macro_rules! assert_that { } } }; + + ($actual:expr, $expected:expr, $($format_args:expr),* $(,)?) => { + match $crate::verify_that!($actual, $expected) + .with_failure_message(|| format!($($format_args),*)) + { + Ok(_) => {} + Err(e) => { + // The extra newline before the assertion failure message makes the failure a + // bit easier to read when there's some generic boilerplate from the panic. + panic!("\n{}", e); + } + } + }; } /// Asserts that the given predicate applied to the given arguments returns @@ -368,12 +414,44 @@ macro_rules! assert_pred { /// ```ignore /// verify_that!(actual, expected).and_log_failure() /// ``` +/// +/// One may optionally add arguments which will be formatted and appended to a +/// failure message. For example: +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_fail() -> std::result::Result<(), googletest::internal::test_outcome::TestFailure> { +/// # googletest::internal::test_outcome::TestOutcome::init_current_test_outcome(); +/// let value = 2; +/// let extra_information = "Some additional information"; +/// expect_that!(value, eq(3), "Test failed. Extra information: {extra_information}."); +/// # googletest::internal::test_outcome::TestOutcome::close_current_test_outcome::<&str>(Ok(())) +/// # } +/// # should_fail().unwrap_err(); +/// ``` +/// +/// This is output as follows: +/// +/// ```text +/// Value of: value +/// Expected: is equal to 3 +/// Actual: 2, +/// which isn't equal to 3 +/// at ... +/// Test failed. Extra information: Some additional information. +/// ``` #[macro_export] macro_rules! expect_that { - ($actual:expr, $expected:expr) => {{ + ($actual:expr, $expected:expr $(,)?) => {{ use $crate::GoogleTestSupport; $crate::verify_that!($actual, $expected).and_log_failure(); }}; + + ($actual:expr, $expected:expr, $($format_args:expr),* $(,)?) => { + $crate::verify_that!($actual, $expected) + .with_failure_message(|| format!($($format_args),*)) + .and_log_failure() + }; } /// Asserts that the given predicate applied to the given arguments returns diff --git a/src/description.rs b/src/description.rs new file mode 100644 index 0000000..9605559 --- /dev/null +++ b/src/description.rs @@ -0,0 +1,362 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{ + borrow::Cow, + fmt::{Display, Formatter, Result}, +}; + +use crate::internal::description_renderer::{List, INDENTATION_SIZE}; + +/// A structured description, either of a (composed) matcher or of an +/// assertion failure. +/// +/// One can compose blocks of text into a `Description`. Each one appears on a +/// new line. For example: +/// +/// ``` +/// # use googletest::prelude::*; +/// # use googletest::description::Description; +/// let description = Description::new() +/// .text("A block") +/// .text("Another block"); +/// verify_that!(description, displays_as(eq("A block\nAnother block"))) +/// # .unwrap(); +/// ``` +/// +/// One can embed nested descriptions into a `Description`. The resulting +/// nested description is then rendered with an additional level of +/// indentation. For example: +/// +/// ``` +/// # use googletest::prelude::*; +/// # use googletest::description::Description; +/// let inner_description = Description::new() +/// .text("A block") +/// .text("Another block"); +/// let outer_description = Description::new() +/// .text("Header") +/// .nested(inner_description); +/// verify_that!(outer_description, displays_as(eq("\ +/// Header +/// A block +/// Another block"))) +/// # .unwrap(); +/// ``` +/// +/// One can also enumerate or bullet list the elements of a `Description`: +/// +/// ``` +/// # use googletest::prelude::*; +/// # use googletest::description::Description; +/// let description = Description::new() +/// .text("First item") +/// .text("Second item") +/// .bullet_list(); +/// verify_that!(description, displays_as(eq("\ +/// * First item +/// * Second item"))) +/// # .unwrap(); +/// ``` +/// +/// One can construct a `Description` from a [`String`] or a string slice, an +/// iterator thereof, or from an iterator over other `Description`s: +/// +/// ``` +/// # use googletest::description::Description; +/// let single_element_description: Description = +/// "A single block description".into(); +/// let two_element_description: Description = +/// ["First item", "Second item"].into_iter().collect(); +/// let two_element_description_from_strings: Description = +/// ["First item".to_string(), "Second item".to_string()].into_iter().collect(); +/// ``` +/// +/// No newline is added after the last element during rendering. This makes it +/// easier to support single-line matcher descriptions and match explanations. +#[derive(Debug, Default)] +pub struct Description { + elements: List, + initial_indentation: usize, +} + +impl Description { + /// Returns a new empty [`Description`]. + pub fn new() -> Self { + Default::default() + } + + /// Appends a block of text to this instance. + /// + /// The block is indented uniformly when this instance is rendered. + pub fn text(mut self, text: impl Into<Cow<'static, str>>) -> Self { + self.elements.push_literal(text.into()); + self + } + + /// Appends a nested [`Description`] to this instance. + /// + /// The nested [`Description`] `inner` is indented uniformly at the next + /// level of indentation when this instance is rendered. + pub fn nested(mut self, inner: Description) -> Self { + self.elements.push_nested(inner.elements); + self + } + + /// Appends all [`Description`] in the given sequence `inner` to this + /// instance. + /// + /// Each element is treated as a nested [`Description`] in the sense of + /// [`Self::nested`]. + pub fn collect(self, inner: impl IntoIterator<Item = Description>) -> Self { + inner.into_iter().fold(self, |outer, inner| outer.nested(inner)) + } + + /// Indents the lines in elements of this description. + /// + /// This operation will be performed lazily when [`self`] is displayed. + /// + /// This will indent every line inside each element. + /// + /// For example: + /// + /// ``` + /// # use googletest::prelude::*; + /// # use googletest::description::Description; + /// let description = std::iter::once("A B C\nD E F".to_string()).collect::<Description>(); + /// verify_that!(description.indent(), displays_as(eq(" A B C\n D E F"))) + /// # .unwrap(); + /// ``` + pub fn indent(self) -> Self { + Self { initial_indentation: INDENTATION_SIZE, ..self } + } + + /// Instructs this instance to render its elements as a bullet list. + /// + /// Each element (from either [`Description::text`] or + /// [`Description::nested`]) is rendered as a bullet point. If an element + /// contains multiple lines, the following lines are aligned with the first + /// one in the block. + /// + /// For instance: + /// + /// ``` + /// # use googletest::prelude::*; + /// # use googletest::description::Description; + /// let description = Description::new() + /// .text("First line\nsecond line") + /// .bullet_list(); + /// verify_that!(description, displays_as(eq("\ + /// * First line + /// second line"))) + /// # .unwrap(); + /// ``` + pub fn bullet_list(self) -> Self { + Self { elements: self.elements.bullet_list(), ..self } + } + + /// Instructs this instance to render its elements as an enumerated list. + /// + /// Each element (from either [`Description::text`] or + /// [`Description::nested`]) is rendered with its zero-based index. If an + /// element contains multiple lines, the following lines are aligned with + /// the first one in the block. + /// + /// For instance: + /// + /// ``` + /// # use googletest::prelude::*; + /// # use googletest::description::Description; + /// let description = Description::new() + /// .text("First line\nsecond line") + /// .enumerate(); + /// verify_that!(description, displays_as(eq("\ + /// 0. First line + /// second line"))) + /// # .unwrap(); + /// ``` + pub fn enumerate(self) -> Self { + Self { elements: self.elements.enumerate(), ..self } + } + + /// Returns the length of elements. + pub fn len(&self) -> usize { + self.elements.len() + } + + /// Returns whether the set of elements is empty. + pub fn is_empty(&self) -> bool { + self.elements.is_empty() + } +} + +impl Display for Description { + fn fmt(&self, f: &mut Formatter) -> Result { + self.elements.render(f, self.initial_indentation) + } +} + +impl<ElementT: Into<Cow<'static, str>>> FromIterator<ElementT> for Description { + fn from_iter<T>(iter: T) -> Self + where + T: IntoIterator<Item = ElementT>, + { + Self { elements: iter.into_iter().map(ElementT::into).collect(), ..Default::default() } + } +} + +impl FromIterator<Description> for Description { + fn from_iter<T>(iter: T) -> Self + where + T: IntoIterator<Item = Description>, + { + Self { elements: iter.into_iter().map(|s| s.elements).collect(), ..Default::default() } + } +} + +impl<T: Into<Cow<'static, str>>> From<T> for Description { + fn from(value: T) -> Self { + let mut elements = List::default(); + elements.push_literal(value.into()); + Self { elements, ..Default::default() } + } +} + +#[cfg(test)] +mod tests { + use super::Description; + use crate::prelude::*; + use indoc::indoc; + + #[test] + fn renders_single_fragment() -> Result<()> { + let description: Description = "A B C".into(); + verify_that!(description, displays_as(eq("A B C"))) + } + + #[test] + fn renders_two_fragments() -> Result<()> { + let description = + ["A B C".to_string(), "D E F".to_string()].into_iter().collect::<Description>(); + verify_that!(description, displays_as(eq("A B C\nD E F"))) + } + + #[test] + fn nested_description_is_indented() -> Result<()> { + let description = Description::new() + .text("Header") + .nested(["A B C".to_string()].into_iter().collect::<Description>()); + verify_that!(description, displays_as(eq("Header\n A B C"))) + } + + #[test] + fn nested_description_indents_two_elements() -> Result<()> { + let description = Description::new().text("Header").nested( + ["A B C".to_string(), "D E F".to_string()].into_iter().collect::<Description>(), + ); + verify_that!(description, displays_as(eq("Header\n A B C\n D E F"))) + } + + #[test] + fn nested_description_indents_one_element_on_two_lines() -> Result<()> { + let description = Description::new().text("Header").nested("A B C\nD E F".into()); + verify_that!(description, displays_as(eq("Header\n A B C\n D E F"))) + } + + #[test] + fn single_fragment_renders_with_bullet_when_bullet_list_enabled() -> Result<()> { + let description = Description::new().text("A B C").bullet_list(); + verify_that!(description, displays_as(eq("* A B C"))) + } + + #[test] + fn single_nested_fragment_renders_with_bullet_when_bullet_list_enabled() -> Result<()> { + let description = Description::new().nested("A B C".into()).bullet_list(); + verify_that!(description, displays_as(eq("* A B C"))) + } + + #[test] + fn two_fragments_render_with_bullet_when_bullet_list_enabled() -> Result<()> { + let description = Description::new().text("A B C").text("D E F").bullet_list(); + verify_that!(description, displays_as(eq("* A B C\n* D E F"))) + } + + #[test] + fn two_nested_fragments_render_with_bullet_when_bullet_list_enabled() -> Result<()> { + let description = + Description::new().nested("A B C".into()).nested("D E F".into()).bullet_list(); + verify_that!(description, displays_as(eq("* A B C\n* D E F"))) + } + + #[test] + fn single_fragment_with_more_than_one_line_renders_with_one_bullet() -> Result<()> { + let description = Description::new().text("A B C\nD E F").bullet_list(); + verify_that!(description, displays_as(eq("* A B C\n D E F"))) + } + + #[test] + fn single_fragment_renders_with_enumeration_when_enumerate_enabled() -> Result<()> { + let description = Description::new().text("A B C").enumerate(); + verify_that!(description, displays_as(eq("0. A B C"))) + } + + #[test] + fn two_fragments_render_with_enumeration_when_enumerate_enabled() -> Result<()> { + let description = Description::new().text("A B C").text("D E F").enumerate(); + verify_that!(description, displays_as(eq("0. A B C\n1. D E F"))) + } + + #[test] + fn single_fragment_with_two_lines_renders_with_one_enumeration_label() -> Result<()> { + let description = Description::new().text("A B C\nD E F").enumerate(); + verify_that!(description, displays_as(eq("0. A B C\n D E F"))) + } + + #[test] + fn multi_digit_enumeration_renders_with_correct_offset() -> Result<()> { + let description = ["A B C\nD E F"; 11] + .into_iter() + .map(str::to_string) + .collect::<Description>() + .enumerate(); + verify_that!( + description, + displays_as(eq(indoc!( + " + 0. A B C + D E F + 1. A B C + D E F + 2. A B C + D E F + 3. A B C + D E F + 4. A B C + D E F + 5. A B C + D E F + 6. A B C + D E F + 7. A B C + D E F + 8. A B C + D E F + 9. A B C + D E F + 10. A B C + D E F" + ))) + ) + } +} diff --git a/src/internal/description_renderer.rs b/src/internal/description_renderer.rs new file mode 100644 index 0000000..937f0d5 --- /dev/null +++ b/src/internal/description_renderer.rs @@ -0,0 +1,614 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{ + borrow::Cow, + fmt::{Result, Write}, +}; + +/// Number of space used to indent lines when no alignement is required. +pub(crate) const INDENTATION_SIZE: usize = 2; + +/// A list of [`Block`] possibly rendered with a [`Decoration`]. +/// +/// This is the top-level renderable component, corresponding to the description +/// or match explanation of a single matcher. +/// +/// The constituent [`Block`] of a `List` can be decorated with either bullets +/// (`* `) or enumeration (`0. `, `1. `, ...). This is controlled via the +/// methods [`List::bullet_list`] and [`List::enumerate`]. By default, there is +/// no decoration. +/// +/// A `List` can be constructed as follows: +/// +/// * [`Default::default()`] constructs an empty `List`. +/// * [`Iterator::collect()`] on an [`Iterator`] of [`Block`]. +/// * [`Iterator::collect()`] on an [`Iterator`] of `String`, which produces a +/// [`Block::Literal`] for each `String`. +/// * [`Iterator::collect()`] on an [`Iterator`] of `List`, which produces a +/// [`Block::Nested`] for each `List`. +#[derive(Debug, Default)] +pub(crate) struct List(Vec<Block>, Decoration); + +impl List { + /// Render this instance using the formatter `f`. + /// + /// Indent each line of output by `indentation` spaces. + pub(crate) fn render(&self, f: &mut dyn Write, indentation: usize) -> Result { + self.render_with_prefix(f, indentation, "".into()) + } + + /// Append a new [`Block`] containing `literal`. + /// + /// The input `literal` is split into lines so that each line will be + /// indented correctly. + pub(crate) fn push_literal(&mut self, literal: Cow<'static, str>) { + self.0.push(literal.into()); + } + + /// Append a new [`Block`] containing `inner` as a nested [`List`]. + pub(crate) fn push_nested(&mut self, inner: List) { + self.0.push(Block::Nested(inner)); + } + + /// Render each [`Block`] of this instance preceded with a bullet "* ". + pub(crate) fn bullet_list(self) -> Self { + Self(self.0, Decoration::Bullet) + } + + /// Render each [`Block`] of this instance preceded with its 0-based index. + pub(crate) fn enumerate(self) -> Self { + Self(self.0, Decoration::Enumerate) + } + + /// Return the number of [`Block`] in this instance. + pub(crate) fn len(&self) -> usize { + self.0.len() + } + + /// Return `true` if there are no [`Block`] in this instance, `false` + /// otherwise. + pub(crate) fn is_empty(&self) -> bool { + self.0.is_empty() + } + + fn render_with_prefix( + &self, + f: &mut dyn Write, + indentation: usize, + prefix: Cow<'static, str>, + ) -> Result { + if self.0.is_empty() { + return Ok(()); + } + + let enumeration_padding = self.enumeration_padding(); + + self.0[0].render( + f, + indentation, + self.full_prefix(0, enumeration_padding, &prefix).into(), + )?; + for (index, block) in self.0[1..].iter().enumerate() { + writeln!(f)?; + block.render( + f, + indentation + prefix.len(), + self.prefix(index + 1, enumeration_padding), + )?; + } + Ok(()) + } + + fn full_prefix(&self, index: usize, enumeration_padding: usize, prior_prefix: &str) -> String { + format!("{prior_prefix}{}", self.prefix(index, enumeration_padding)) + } + + fn prefix(&self, index: usize, enumeration_padding: usize) -> Cow<'static, str> { + match self.1 { + Decoration::None => "".into(), + Decoration::Bullet => "* ".into(), + Decoration::Enumerate => format!("{:>enumeration_padding$}. ", index).into(), + } + } + + fn enumeration_padding(&self) -> usize { + match self.1 { + Decoration::None => 0, + Decoration::Bullet => 0, + Decoration::Enumerate => { + if self.0.len() > 1 { + ((self.0.len() - 1) as f64).log10().floor() as usize + 1 + } else { + // Avoid negative logarithm when there is only 0 or 1 element. + 1 + } + } + } + } +} + +impl FromIterator<Block> for List { + fn from_iter<T>(iter: T) -> Self + where + T: IntoIterator<Item = Block>, + { + Self(iter.into_iter().collect(), Decoration::None) + } +} + +impl<ElementT: Into<Cow<'static, str>>> FromIterator<ElementT> for List { + fn from_iter<T>(iter: T) -> Self + where + T: IntoIterator<Item = ElementT>, + { + Self(iter.into_iter().map(|b| b.into().into()).collect(), Decoration::None) + } +} + +impl FromIterator<List> for List { + fn from_iter<T>(iter: T) -> Self + where + T: IntoIterator<Item = List>, + { + Self(iter.into_iter().map(Block::nested).collect(), Decoration::None) + } +} + +/// A sequence of [`Fragment`] or a nested [`List`]. +/// +/// This may be rendered with a prefix specified by the [`Decoration`] of the +/// containing [`List`]. In this case, all lines are indented to align with the +/// first character of the first line of the block. +#[derive(Debug)] +enum Block { + /// A block of text. + /// + /// Each constituent [`Fragment`] contains one line of text. The lines are + /// indented uniformly to the current indentation of this block when + /// rendered. + Literal(Vec<Fragment>), + + /// A nested [`List`]. + /// + /// The [`List`] is rendered recursively at the next level of indentation. + Nested(List), +} + +impl Block { + fn nested(inner: List) -> Self { + Self::Nested(inner) + } + + fn render(&self, f: &mut dyn Write, indentation: usize, prefix: Cow<'static, str>) -> Result { + match self { + Self::Literal(fragments) => { + if fragments.is_empty() { + return Ok(()); + } + + write!(f, "{:indentation$}{prefix}", "")?; + fragments[0].render(f)?; + let block_indentation = indentation + prefix.as_ref().len(); + for fragment in &fragments[1..] { + writeln!(f)?; + write!(f, "{:block_indentation$}", "")?; + fragment.render(f)?; + } + Ok(()) + } + Self::Nested(inner) => inner.render_with_prefix( + f, + indentation + INDENTATION_SIZE.saturating_sub(prefix.len()), + prefix, + ), + } + } +} + +impl From<String> for Block { + fn from(value: String) -> Self { + Block::Literal(value.lines().map(|v| Fragment(v.to_string().into())).collect()) + } +} + +impl From<&'static str> for Block { + fn from(value: &'static str) -> Self { + Block::Literal(value.lines().map(|v| Fragment(v.into())).collect()) + } +} + +impl From<Cow<'static, str>> for Block { + fn from(value: Cow<'static, str>) -> Self { + match value { + Cow::Borrowed(value) => value.into(), + Cow::Owned(value) => value.into(), + } + } +} + +/// A string representing one line of a description or match explanation. +#[derive(Debug)] +struct Fragment(Cow<'static, str>); + +impl Fragment { + fn render(&self, f: &mut dyn Write) -> Result { + write!(f, "{}", self.0) + } +} + +/// The decoration which appears on [`Block`] of a [`List`] when rendered. +#[derive(Debug, Default)] +enum Decoration { + /// No decoration on each [`Block`]. The default. + #[default] + None, + + /// Each [`Block`] is preceded by a bullet (`* `). + Bullet, + + /// Each [`Block`] is preceded by its index in the [`List`] (`0. `, `1. `, + /// ...). + Enumerate, +} + +#[cfg(test)] +mod tests { + use super::{Block, Fragment, List}; + use crate::prelude::*; + use indoc::indoc; + + #[test] + fn renders_fragment() -> Result<()> { + let fragment = Fragment("A fragment".into()); + let mut result = String::new(); + + fragment.render(&mut result)?; + + verify_that!(result, eq("A fragment")) + } + + #[test] + fn renders_empty_block() -> Result<()> { + let block = Block::Literal(vec![]); + let mut result = String::new(); + + block.render(&mut result, 0, "".into())?; + + verify_that!(result, eq("")) + } + + #[test] + fn renders_block_with_one_fragment() -> Result<()> { + let block: Block = "A fragment".into(); + let mut result = String::new(); + + block.render(&mut result, 0, "".into())?; + + verify_that!(result, eq("A fragment")) + } + + #[test] + fn renders_block_with_two_fragments() -> Result<()> { + let block: Block = "A fragment\nAnother fragment".into(); + let mut result = String::new(); + + block.render(&mut result, 0, "".into())?; + + verify_that!(result, eq("A fragment\nAnother fragment")) + } + + #[test] + fn renders_indented_block() -> Result<()> { + let block: Block = "A fragment\nAnother fragment".into(); + let mut result = String::new(); + + block.render(&mut result, 2, "".into())?; + + verify_that!(result, eq(" A fragment\n Another fragment")) + } + + #[test] + fn renders_block_with_prefix() -> Result<()> { + let block: Block = "A fragment\nAnother fragment".into(); + let mut result = String::new(); + + block.render(&mut result, 0, "* ".into())?; + + verify_that!(result, eq("* A fragment\n Another fragment")) + } + + #[test] + fn renders_indented_block_with_prefix() -> Result<()> { + let block: Block = "A fragment\nAnother fragment".into(); + let mut result = String::new(); + + block.render(&mut result, 2, "* ".into())?; + + verify_that!(result, eq(" * A fragment\n Another fragment")) + } + + #[test] + fn renders_empty_list() -> Result<()> { + let list = list(vec![]); + let mut result = String::new(); + + list.render(&mut result, 0)?; + + verify_that!(result, eq("")) + } + + #[test] + fn renders_plain_list_with_one_block() -> Result<()> { + let list = list(vec!["A fragment".into()]); + let mut result = String::new(); + + list.render(&mut result, 0)?; + + verify_that!(result, eq("A fragment")) + } + + #[test] + fn renders_plain_list_with_two_blocks() -> Result<()> { + let list = list(vec!["A fragment".into(), "A fragment in a second block".into()]); + let mut result = String::new(); + + list.render(&mut result, 0)?; + + verify_that!(result, eq("A fragment\nA fragment in a second block")) + } + + #[test] + fn renders_plain_list_with_one_block_with_two_fragments() -> Result<()> { + let list = list(vec!["A fragment\nA second fragment".into()]); + let mut result = String::new(); + + list.render(&mut result, 0)?; + + verify_that!(result, eq("A fragment\nA second fragment")) + } + + #[test] + fn renders_nested_plain_list_with_one_block() -> Result<()> { + let list = list(vec![Block::nested(list(vec!["A fragment".into()]))]); + let mut result = String::new(); + + list.render(&mut result, 0)?; + + verify_that!(result, eq(" A fragment")) + } + + #[test] + fn renders_nested_plain_list_with_two_blocks() -> Result<()> { + let list = list(vec![Block::nested(list(vec![ + "A fragment".into(), + "A fragment in a second block".into(), + ]))]); + let mut result = String::new(); + + list.render(&mut result, 0)?; + + verify_that!(result, eq(" A fragment\n A fragment in a second block")) + } + + #[test] + fn renders_nested_plain_list_with_one_block_with_two_fragments() -> Result<()> { + let list = list(vec![Block::nested(list(vec!["A fragment\nA second fragment".into()]))]); + let mut result = String::new(); + + list.render(&mut result, 0)?; + + verify_that!(result, eq(" A fragment\n A second fragment")) + } + + #[test] + fn renders_bulleted_list_with_one_block() -> Result<()> { + let list = list(vec!["A fragment".into()]).bullet_list(); + let mut result = String::new(); + + list.render(&mut result, 0)?; + + verify_that!(result, eq("* A fragment")) + } + + #[test] + fn renders_bulleted_list_with_two_blocks() -> Result<()> { + let list = + list(vec!["A fragment".into(), "A fragment in a second block".into()]).bullet_list(); + let mut result = String::new(); + + list.render(&mut result, 0)?; + + verify_that!(result, eq("* A fragment\n* A fragment in a second block")) + } + + #[test] + fn renders_bulleted_list_with_one_block_with_two_fragments() -> Result<()> { + let list = list(vec!["A fragment\nA second fragment".into()]).bullet_list(); + let mut result = String::new(); + + list.render(&mut result, 0)?; + + verify_that!(result, eq("* A fragment\n A second fragment")) + } + + #[test] + fn renders_nested_bulleted_list_with_one_block() -> Result<()> { + let list = list(vec![Block::nested(list(vec!["A fragment".into()]).bullet_list())]); + let mut result = String::new(); + + list.render(&mut result, 0)?; + + verify_that!(result, eq(" * A fragment")) + } + + #[test] + fn renders_nested_bulleted_list_with_two_blocks() -> Result<()> { + let list = list(vec![Block::nested( + list(vec!["A fragment".into(), "A fragment in a second block".into()]).bullet_list(), + )]); + let mut result = String::new(); + + list.render(&mut result, 0)?; + + verify_that!(result, eq(" * A fragment\n * A fragment in a second block")) + } + + #[test] + fn renders_nested_bulleted_list_with_one_block_with_two_fragments() -> Result<()> { + let list = list(vec![Block::nested( + list(vec!["A fragment\nA second fragment".into()]).bullet_list(), + )]); + let mut result = String::new(); + + list.render(&mut result, 0)?; + + verify_that!(result, eq(" * A fragment\n A second fragment")) + } + + #[test] + fn renders_enumerated_list_with_one_block() -> Result<()> { + let list = list(vec!["A fragment".into()]).enumerate(); + let mut result = String::new(); + + list.render(&mut result, 0)?; + + verify_that!(result, eq("0. A fragment")) + } + + #[test] + fn renders_enumerated_list_with_two_blocks() -> Result<()> { + let list = + list(vec!["A fragment".into(), "A fragment in a second block".into()]).enumerate(); + let mut result = String::new(); + + list.render(&mut result, 0)?; + + verify_that!(result, eq("0. A fragment\n1. A fragment in a second block")) + } + + #[test] + fn renders_enumerated_list_with_one_block_with_two_fragments() -> Result<()> { + let list = list(vec!["A fragment\nA second fragment".into()]).enumerate(); + let mut result = String::new(); + + list.render(&mut result, 0)?; + + verify_that!(result, eq("0. A fragment\n A second fragment")) + } + + #[test] + fn aligns_renders_enumerated_list_with_more_than_ten_blocks() -> Result<()> { + let list = + (0..11).map(|i| Block::from(format!("Fragment {i}"))).collect::<List>().enumerate(); + let mut result = String::new(); + + list.render(&mut result, 0)?; + + verify_that!( + result, + eq(indoc! {" + 0. Fragment 0 + 1. Fragment 1 + 2. Fragment 2 + 3. Fragment 3 + 4. Fragment 4 + 5. Fragment 5 + 6. Fragment 6 + 7. Fragment 7 + 8. Fragment 8 + 9. Fragment 9 + 10. Fragment 10"}) + ) + } + + #[test] + fn renders_fragment_plus_nested_plain_list_with_one_block() -> Result<()> { + let list = + list(vec!["A fragment".into(), Block::nested(list(vec!["Another fragment".into()]))]); + let mut result = String::new(); + + list.render(&mut result, 0)?; + + verify_that!(result, eq("A fragment\n Another fragment")) + } + + #[test] + fn renders_double_nested_plain_list_with_one_block() -> Result<()> { + let list = + list(vec![Block::nested(list(vec![Block::nested(list(vec!["A fragment".into()]))]))]); + let mut result = String::new(); + + list.render(&mut result, 0)?; + + verify_that!(result, eq(" A fragment")) + } + + #[test] + fn renders_headers_plus_double_nested_plain_list() -> Result<()> { + let list = list(vec![ + "First header".into(), + Block::nested(list(vec![ + "Second header".into(), + Block::nested(list(vec!["A fragment".into()])), + ])), + ]); + let mut result = String::new(); + + list.render(&mut result, 0)?; + + verify_that!(result, eq("First header\n Second header\n A fragment")) + } + + #[test] + fn renders_double_nested_bulleted_list() -> Result<()> { + let list = + list(vec![Block::nested(list(vec!["A fragment".into()]).bullet_list())]).bullet_list(); + let mut result = String::new(); + + list.render(&mut result, 0)?; + + verify_that!(result, eq("* * A fragment")) + } + + #[test] + fn renders_nested_enumeration_with_two_blocks_inside_bulleted_list() -> Result<()> { + let list = + list(vec![Block::nested(list(vec!["Block 1".into(), "Block 2".into()]).enumerate())]) + .bullet_list(); + let mut result = String::new(); + + list.render(&mut result, 0)?; + + verify_that!(result, eq("* 0. Block 1\n 1. Block 2")) + } + + #[test] + fn renders_nested_enumeration_with_block_with_two_fragments_inside_bulleted_list() -> Result<()> + { + let list = list(vec![Block::nested( + list(vec!["A fragment\nAnother fragment".into()]).enumerate(), + )]) + .bullet_list(); + let mut result = String::new(); + + list.render(&mut result, 0)?; + + verify_that!(result, eq("* 0. A fragment\n Another fragment")) + } + + fn list(blocks: Vec<Block>) -> List { + List(blocks, super::Decoration::None) + } +} diff --git a/src/internal/mod.rs b/src/internal/mod.rs index 2f37418..9bccdc3 100644 --- a/src/internal/mod.rs +++ b/src/internal/mod.rs @@ -14,5 +14,6 @@ #![doc(hidden)] +pub(crate) mod description_renderer; pub mod source_location; pub mod test_outcome; diff --git a/src/internal/test_outcome.rs b/src/internal/test_outcome.rs index 2171cc7..132ba99 100644 --- a/src/internal/test_outcome.rs +++ b/src/internal/test_outcome.rs @@ -174,7 +174,7 @@ impl TestAssertionFailure { pub(crate) fn log(&self) { TestOutcome::fail_current_test(); - print!("{}", self); + println!("{}", self); } } @@ -21,6 +21,7 @@ extern crate quickcheck; #[macro_use] pub mod assertions; +pub mod description; pub mod internal; pub mod matcher; pub mod matcher_support; @@ -49,11 +50,6 @@ pub mod prelude { pub use super::Result; // Assert macros pub use super::{assert_that, expect_pred, expect_that, fail, verify_pred, verify_that}; - // Matcher macros - pub use super::{ - all, any, contains_each, elements_are, field, is_contained_in, matches_pattern, pat, - pointwise, property, unordered_elements_are, - }; } pub use googletest_macro::test; @@ -62,7 +58,10 @@ use internal::test_outcome::{TestAssertionFailure, TestOutcome}; /// A `Result` whose `Err` variant indicates a test failure. /// -/// All test functions should return `Result<()>`. +/// The assertions [`verify_that!`][crate::verify_that], +/// [`verify_pred!`][crate::verify_pred], and [`fail!`][crate::fail] evaluate +/// to `Result<()>`. A test function may return `Result<()>` in combination with +/// those macros to abort immediately on assertion failure. /// /// This can be used with subroutines which may cause the test to fatally fail /// and which return some value needed by the caller. For example: diff --git a/src/matcher.rs b/src/matcher.rs index 83a4475..071b0d1 100644 --- a/src/matcher.rs +++ b/src/matcher.rs @@ -14,10 +14,11 @@ //! The components required to implement matchers. +use crate::description::Description; use crate::internal::source_location::SourceLocation; use crate::internal::test_outcome::TestAssertionFailure; -use crate::matchers::conjunction_matcher::ConjunctionMatcher; -use crate::matchers::disjunction_matcher::DisjunctionMatcher; +use crate::matchers::__internal_unstable_do_not_depend_on_these::ConjunctionMatcher; +use crate::matchers::__internal_unstable_do_not_depend_on_these::DisjunctionMatcher; use std::fmt::Debug; /// An interface for checking an arbitrary condition on a datum. @@ -57,11 +58,13 @@ pub trait Matcher { /// [`some`][crate::matchers::some] implements `describe` as follows: /// /// ```ignore - /// fn describe(&self, matcher_result: MatcherResult) -> String { + /// fn describe(&self, matcher_result: MatcherResult) -> Description { /// match matcher_result { /// MatcherResult::Matches => { - /// format!("has a value which {}", self.inner.describe(MatcherResult::Matches)) - /// // Inner matcher invocation: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + /// Description::new() + /// .text("has a value which") + /// .nested(self.inner.describe(MatcherResult::Matches)) + /// // Inner matcher: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ /// } /// MatcherResult::DoesNotMatch => {...} // Similar to the above /// } @@ -75,7 +78,7 @@ pub trait Matcher { /// output of `explain_match` is always used adjectivally to describe the /// actual value, while `describe` is used in contexts where a relative /// clause would not make sense. - fn describe(&self, matcher_result: MatcherResult) -> String; + fn describe(&self, matcher_result: MatcherResult) -> Description; /// Prepares a [`String`] describing how the expected value /// encoded in this instance matches or does not match the given value @@ -114,7 +117,7 @@ pub trait Matcher { /// inner matcher and appears as follows: /// /// ```ignore - /// fn explain_match(&self, actual: &Self::ActualT) -> String { + /// fn explain_match(&self, actual: &Self::ActualT) -> Description { /// self.expected.explain_match(actual.deref()) /// } /// ``` @@ -124,12 +127,14 @@ pub trait Matcher { /// inner matcher at a point where a relative clause would fit. For example: /// /// ```ignore - /// fn explain_match(&self, actual: &Self::ActualT) -> String { - /// format!("which points to a value {}", self.expected.explain_match(actual.deref())) + /// fn explain_match(&self, actual: &Self::ActualT) -> Description { + /// Description::new() + /// .text("which points to a value") + /// .nested(self.expected.explain_match(actual.deref())) /// } /// ``` - fn explain_match(&self, actual: &Self::ActualT) -> String { - format!("which {}", self.describe(self.matches(actual))) + fn explain_match(&self, actual: &Self::ActualT) -> Description { + format!("which {}", self.describe(self.matches(actual))).into() } /// Constructs a matcher that matches both `self` and `right`. @@ -222,10 +227,10 @@ pub(crate) fn create_assertion_failure<T: Debug + ?Sized>( Value of: {actual_expr} Expected: {} Actual: {actual_formatted}, - {} +{} {source_location}", matcher.describe(MatcherResult::Match), - matcher.explain_match(actual), + matcher.explain_match(actual).indent(), )) } diff --git a/src/matcher_support/description.rs b/src/matcher_support/description.rs deleted file mode 100644 index ce074e0..0000000 --- a/src/matcher_support/description.rs +++ /dev/null @@ -1,410 +0,0 @@ -// Copyright 2023 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::fmt::{Display, Formatter, Result}; - -/// Helper structure to build better output of -/// [`Matcher::describe`][crate::matcher::Matcher::describe] and -/// [`Matcher::explain_match`][crate::matcher::Matcher::explain_match]. This -/// is especially useful with composed matchers and matchers over containers. -/// -/// It provides simple operations to lazily format lists of strings. -/// -/// Usage: -/// ```ignore -/// let iter: impl Iterator<String> = ... -/// format!("{}", iter.collect::<Description>().indent().bullet_list()) -/// ``` -/// -/// To construct a [`Description`], use `Iterator<Item=String>::collect()`. -/// Each element of the collected iterator will be separated by a -/// newline when displayed. The elements may be multi-line, but they will -/// nevertheless be indented consistently. -/// -/// Note that a newline will only be added between each element, but not -/// after the last element. This makes it simpler to keep -/// [`Matcher::describe`][crate::matcher::Matcher::describe] -/// and [`Matcher::explain_match`][crate::matcher::Matcher::explain_match] -/// consistent with simpler [`Matchers`][crate::matcher::Matcher]. -/// -/// They can also be indented, enumerated and or -/// bullet listed if [`Description::indent`], [`Description::enumerate`], or -/// respectively [`Description::bullet_list`] has been called. -#[derive(Debug)] -pub struct Description { - elements: Vec<String>, - indent_mode: IndentMode, - list_style: ListStyle, -} - -#[derive(Debug)] -enum IndentMode { - NoIndent, - EveryLine, - AllExceptFirstLine, -} - -#[derive(Debug)] -enum ListStyle { - NoList, - Bullet, - Enumerate, -} - -struct IndentationSizes { - first_line_indent: usize, - first_line_of_element_indent: usize, - enumeration_padding: usize, - other_line_indent: usize, -} - -/// Number of space used to indent lines when no alignement is required. -const INDENTATION_SIZE: usize = 2; - -impl Description { - /// Indents the lines in elements of this description. - /// - /// This operation will be performed lazily when [`self`] is displayed. - /// - /// This will indent every line inside each element. - /// - /// For example: - /// - /// ``` - /// # use googletest::prelude::*; - /// # use googletest::matcher_support::description::Description; - /// let description = std::iter::once("A B C\nD E F".to_string()).collect::<Description>(); - /// verify_that!(description.indent(), displays_as(eq(" A B C\n D E F"))) - /// # .unwrap(); - /// ``` - pub fn indent(self) -> Self { - Self { indent_mode: IndentMode::EveryLine, ..self } - } - - /// Indents the lines in elements of this description except for the first - /// line. - /// - /// This is similar to [`Self::indent`] except that the first line is not - /// indented. This is useful when the first line has already been indented - /// in the output. - /// - /// For example: - /// - /// ``` - /// # use googletest::prelude::*; - /// # use googletest::matcher_support::description::Description; - /// let description = std::iter::once("A B C\nD E F".to_string()).collect::<Description>(); - /// verify_that!(description.indent_except_first_line(), displays_as(eq("A B C\n D E F"))) - /// # .unwrap(); - /// ``` - pub fn indent_except_first_line(self) -> Self { - Self { indent_mode: IndentMode::AllExceptFirstLine, ..self } - } - - /// Bullet lists the elements of [`self`]. - /// - /// This operation will be performed lazily when [`self`] is displayed. - /// - /// Note that this will only bullet list each element, not each line - /// in each element. - /// - /// For instance: - /// - /// ``` - /// # use googletest::prelude::*; - /// # use googletest::matcher_support::description::Description; - /// let description = std::iter::once("A B C\nD E F".to_string()).collect::<Description>(); - /// verify_that!(description.bullet_list(), displays_as(eq("* A B C\n D E F"))) - /// # .unwrap(); - /// ``` - pub fn bullet_list(self) -> Self { - Self { list_style: ListStyle::Bullet, ..self } - } - - /// Enumerates the elements of [`self`]. - /// - /// This operation will be performed lazily when [`self`] is displayed. - /// - /// Note that this will only enumerate each element, not each line in - /// each element. - /// - /// For instance: - /// - /// ``` - /// # use googletest::prelude::*; - /// # use googletest::matcher_support::description::Description; - /// let description = std::iter::once("A B C\nD E F".to_string()).collect::<Description>(); - /// verify_that!(description.enumerate(), displays_as(eq("0. A B C\n D E F"))) - /// # .unwrap(); - /// ``` - pub fn enumerate(self) -> Self { - Self { list_style: ListStyle::Enumerate, ..self } - } - - /// Returns the length of elements. - pub fn len(&self) -> usize { - self.elements.len() - } - - /// Returns whether the set of elements is empty. - pub fn is_empty(&self) -> bool { - self.elements.is_empty() - } - - fn indentation_sizes(&self) -> IndentationSizes { - let first_line_indent = - if matches!(self.indent_mode, IndentMode::EveryLine) { INDENTATION_SIZE } else { 0 }; - let first_line_of_element_indent = - if !matches!(self.indent_mode, IndentMode::NoIndent) { INDENTATION_SIZE } else { 0 }; - // Number of digit of the last index. For instance, an array of length 13 will - // have 12 as last index (we start at 0), which have a digit size of 2. - let enumeration_padding = if self.elements.len() > 1 { - ((self.elements.len() - 1) as f64).log10().floor() as usize + 1 - } else { - // Avoid negative logarithm when there is only 0 or 1 element. - 1 - }; - - let other_line_indent = first_line_of_element_indent - + match self.list_style { - ListStyle::NoList => 0, - ListStyle::Bullet => "* ".len(), - ListStyle::Enumerate => enumeration_padding + ". ".len(), - }; - IndentationSizes { - first_line_indent, - first_line_of_element_indent, - enumeration_padding, - other_line_indent, - } - } -} - -impl Display for Description { - fn fmt(&self, f: &mut Formatter) -> Result { - let IndentationSizes { - mut first_line_indent, - first_line_of_element_indent, - enumeration_padding, - other_line_indent, - } = self.indentation_sizes(); - - let mut first = true; - for (idx, element) in self.elements.iter().enumerate() { - let mut lines = element.lines(); - if let Some(line) = lines.next() { - if first { - first = false; - } else { - writeln!(f)?; - } - match self.list_style { - ListStyle::NoList => { - write!(f, "{:first_line_indent$}{line}", "")?; - } - ListStyle::Bullet => { - write!(f, "{:first_line_indent$}* {line}", "")?; - } - ListStyle::Enumerate => { - write!( - f, - "{:first_line_indent$}{:>enumeration_padding$}. {line}", - "", idx, - )?; - } - } - } - for line in lines { - writeln!(f)?; - write!(f, "{:other_line_indent$}{line}", "")?; - } - first_line_indent = first_line_of_element_indent; - } - Ok(()) - } -} - -impl FromIterator<String> for Description { - fn from_iter<T>(iter: T) -> Self - where - T: IntoIterator<Item = String>, - { - Self { - elements: iter.into_iter().collect(), - indent_mode: IndentMode::NoIndent, - list_style: ListStyle::NoList, - } - } -} - -#[cfg(test)] -mod tests { - use super::Description; - use crate::prelude::*; - use indoc::indoc; - - #[test] - fn description_single_element() -> Result<()> { - let description = ["A B C".to_string()].into_iter().collect::<Description>(); - verify_that!(description, displays_as(eq("A B C"))) - } - - #[test] - fn description_two_elements() -> Result<()> { - let description = - ["A B C".to_string(), "D E F".to_string()].into_iter().collect::<Description>(); - verify_that!(description, displays_as(eq("A B C\nD E F"))) - } - - #[test] - fn description_indent_single_element() -> Result<()> { - let description = ["A B C".to_string()].into_iter().collect::<Description>().indent(); - verify_that!(description, displays_as(eq(" A B C"))) - } - - #[test] - fn description_indent_two_elements() -> Result<()> { - let description = ["A B C".to_string(), "D E F".to_string()] - .into_iter() - .collect::<Description>() - .indent(); - verify_that!(description, displays_as(eq(" A B C\n D E F"))) - } - - #[test] - fn description_indent_two_elements_except_first_line() -> Result<()> { - let description = ["A B C".to_string(), "D E F".to_string()] - .into_iter() - .collect::<Description>() - .indent_except_first_line(); - verify_that!(description, displays_as(eq("A B C\n D E F"))) - } - - #[test] - fn description_indent_single_element_two_lines() -> Result<()> { - let description = - ["A B C\nD E F".to_string()].into_iter().collect::<Description>().indent(); - verify_that!(description, displays_as(eq(" A B C\n D E F"))) - } - - #[test] - fn description_indent_single_element_two_lines_except_first_line() -> Result<()> { - let description = ["A B C\nD E F".to_string()] - .into_iter() - .collect::<Description>() - .indent_except_first_line(); - verify_that!(description, displays_as(eq("A B C\n D E F"))) - } - - #[test] - fn description_bullet_single_element() -> Result<()> { - let description = ["A B C".to_string()].into_iter().collect::<Description>().bullet_list(); - verify_that!(description, displays_as(eq("* A B C"))) - } - - #[test] - fn description_bullet_two_elements() -> Result<()> { - let description = ["A B C".to_string(), "D E F".to_string()] - .into_iter() - .collect::<Description>() - .bullet_list(); - verify_that!(description, displays_as(eq("* A B C\n* D E F"))) - } - - #[test] - fn description_bullet_single_element_two_lines() -> Result<()> { - let description = - ["A B C\nD E F".to_string()].into_iter().collect::<Description>().bullet_list(); - verify_that!(description, displays_as(eq("* A B C\n D E F"))) - } - - #[test] - fn description_bullet_single_element_two_lines_indent_except_first_line() -> Result<()> { - let description = ["A B C\nD E F".to_string()] - .into_iter() - .collect::<Description>() - .bullet_list() - .indent_except_first_line(); - verify_that!(description, displays_as(eq("* A B C\n D E F"))) - } - - #[test] - fn description_bullet_two_elements_indent_except_first_line() -> Result<()> { - let description = ["A B C".to_string(), "D E F".to_string()] - .into_iter() - .collect::<Description>() - .bullet_list() - .indent_except_first_line(); - verify_that!(description, displays_as(eq("* A B C\n * D E F"))) - } - - #[test] - fn description_enumerate_single_element() -> Result<()> { - let description = ["A B C".to_string()].into_iter().collect::<Description>().enumerate(); - verify_that!(description, displays_as(eq("0. A B C"))) - } - - #[test] - fn description_enumerate_two_elements() -> Result<()> { - let description = ["A B C".to_string(), "D E F".to_string()] - .into_iter() - .collect::<Description>() - .enumerate(); - verify_that!(description, displays_as(eq("0. A B C\n1. D E F"))) - } - - #[test] - fn description_enumerate_single_element_two_lines() -> Result<()> { - let description = - ["A B C\nD E F".to_string()].into_iter().collect::<Description>().enumerate(); - verify_that!(description, displays_as(eq("0. A B C\n D E F"))) - } - - #[test] - fn description_enumerate_correct_indentation_with_large_index() -> Result<()> { - let description = ["A B C\nD E F"; 11] - .into_iter() - .map(str::to_string) - .collect::<Description>() - .enumerate(); - verify_that!( - description, - displays_as(eq(indoc!( - " - 0. A B C - D E F - 1. A B C - D E F - 2. A B C - D E F - 3. A B C - D E F - 4. A B C - D E F - 5. A B C - D E F - 6. A B C - D E F - 7. A B C - D E F - 8. A B C - D E F - 9. A B C - D E F - 10. A B C - D E F" - ))) - ) - } -} diff --git a/src/matcher_support/edit_distance.rs b/src/matcher_support/edit_distance.rs index 8469f42..8847bc9 100644 --- a/src/matcher_support/edit_distance.rs +++ b/src/matcher_support/edit_distance.rs @@ -19,7 +19,7 @@ use std::fmt::Debug; /// /// Increasing this limit increases the accuracy of [`edit_list`] while /// quadratically increasing its worst-case runtime. -const MAX_DISTANCE: i32 = 25; +const MAX_DISTANCE: i32 = 50; /// The difference between two inputs as produced by [`edit_list`]. #[derive(Debug)] @@ -533,7 +533,7 @@ mod tests { #[test] fn returns_unrelated_when_maximum_distance_exceeded() -> Result<()> { - let result = edit_list(0..=20, 20..40, Mode::Exact); + let result = edit_list(0..=50, 60..110, Mode::Exact); verify_that!(result, matches_pattern!(Difference::Unrelated)) } diff --git a/src/matcher_support/mod.rs b/src/matcher_support/mod.rs index 8c30161..8a72ba4 100644 --- a/src/matcher_support/mod.rs +++ b/src/matcher_support/mod.rs @@ -19,7 +19,6 @@ //! matchers. pub(crate) mod count_elements; -pub mod description; pub(crate) mod edit_distance; pub(crate) mod summarize_diff; pub(crate) mod zipped_iterator; diff --git a/src/matcher_support/summarize_diff.rs b/src/matcher_support/summarize_diff.rs index a045282..cd4efa7 100644 --- a/src/matcher_support/summarize_diff.rs +++ b/src/matcher_support/summarize_diff.rs @@ -17,10 +17,7 @@ use crate::matcher_support::edit_distance; #[rustversion::since(1.70)] use std::io::IsTerminal; -use std::{ - borrow::Cow, - fmt::{Display, Write}, -}; +use std::{borrow::Cow, fmt::Display}; /// Returns a string describing how the expected and actual lines differ. /// @@ -43,13 +40,10 @@ pub(crate) fn create_diff( } match edit_distance::edit_list(actual_debug.lines(), expected_debug.lines(), diff_mode) { edit_distance::Difference::Equal => "No difference found between debug strings.".into(), - edit_distance::Difference::Editable(edit_list) => format!( - "\nDifference({} / {}):{}", - LineStyle::extra_actual_style().style("actual"), - LineStyle::extra_expected_style().style("expected"), - edit_list.into_iter().collect::<BufferedSummary>(), - ) - .into(), + edit_distance::Difference::Editable(edit_list) => { + format!("\n{}{}", summary_header(), edit_list.into_iter().collect::<BufferedSummary>(),) + .into() + } edit_distance::Difference::Unrelated => "".into(), } } @@ -79,69 +73,125 @@ pub(crate) fn create_diff_reversed( edit_distance::Difference::Equal => "No difference found between debug strings.".into(), edit_distance::Difference::Editable(mut edit_list) => { edit_list.reverse(); - format!( - "\nDifference({} / {}):{}", - LineStyle::extra_actual_style().style("actual"), - LineStyle::extra_expected_style().style("expected"), - edit_list.into_iter().collect::<BufferedSummary>(), - ) - .into() + format!("\n{}{}", summary_header(), edit_list.into_iter().collect::<BufferedSummary>(),) + .into() } edit_distance::Difference::Unrelated => "".into(), } } +// Produces the header, with or without coloring depending on +// stdout_supports_color() +fn summary_header() -> Cow<'static, str> { + if stdout_supports_color() { + format!( + "Difference(-{ACTUAL_ONLY_STYLE}actual{RESET_ALL} / +{EXPECTED_ONLY_STYLE}expected{RESET_ALL}):" + ).into() + } else { + "Difference(-actual / +expected):".into() + } +} + // Aggregator collecting the lines to be printed in the difference summary. // // This is buffered in order to allow a future line to potentially impact how // the current line would be printed. +#[derive(Default)] struct BufferedSummary<'a> { - summary: String, + summary: SummaryBuilder, buffer: Buffer<'a>, } impl<'a> BufferedSummary<'a> { // Appends a new line which is common to both actual and expected. fn feed_common_lines(&mut self, common_line: &'a str) { - let Buffer::CommonLineBuffer(ref mut common_lines) = self.buffer; - common_lines.push(common_line); + if let Buffer::CommonLineBuffer(ref mut common_lines) = self.buffer { + common_lines.push(common_line); + } else { + self.flush_buffer(); + self.buffer = Buffer::CommonLineBuffer(vec![common_line]); + } } // Appends a new line which is found only in the actual string. fn feed_extra_actual(&mut self, extra_actual: &'a str) { - self.buffer.flush(&mut self.summary).unwrap(); - write!(&mut self.summary, "\n{}", LineStyle::extra_actual_style().style(extra_actual)) - .unwrap(); + if let Buffer::ExtraExpectedLineChunk(extra_expected) = self.buffer { + self.print_inline_diffs(extra_actual, extra_expected); + self.buffer = Buffer::Empty; + } else { + self.flush_buffer(); + self.buffer = Buffer::ExtraActualLineChunk(extra_actual); + } } // Appends a new line which is found only in the expected string. - fn feed_extra_expected(&mut self, extra_expected: &str) { - self.flush_buffer(); - write!(&mut self.summary, "\n{}", LineStyle::extra_expected_style().style(extra_expected)) - .unwrap(); + fn feed_extra_expected(&mut self, extra_expected: &'a str) { + if let Buffer::ExtraActualLineChunk(extra_actual) = self.buffer { + self.print_inline_diffs(extra_actual, extra_expected); + self.buffer = Buffer::Empty; + } else { + self.flush_buffer(); + self.buffer = Buffer::ExtraExpectedLineChunk(extra_expected); + } } // Appends a comment for the additional line at the start or the end of the // actual string which should be omitted. fn feed_additional_actual(&mut self) { self.flush_buffer(); - write!( - &mut self.summary, - "\n{}", - LineStyle::comment_style().style("<---- remaining lines omitted ---->") - ) - .unwrap(); + self.summary.new_line(); + self.summary.push_str_as_comment("<---- remaining lines omitted ---->"); } fn flush_buffer(&mut self) { - self.buffer.flush(&mut self.summary).unwrap(); + self.buffer.flush(&mut self.summary); + } + + fn print_inline_diffs(&mut self, actual_line: &str, expected_line: &str) { + let line_edits = edit_distance::edit_list( + actual_line.chars(), + expected_line.chars(), + edit_distance::Mode::Exact, + ); + + if let edit_distance::Difference::Editable(edit_list) = line_edits { + let mut actual_summary = SummaryBuilder::default(); + actual_summary.new_line_for_actual(); + let mut expected_summary = SummaryBuilder::default(); + expected_summary.new_line_for_expected(); + for edit in &edit_list { + match edit { + edit_distance::Edit::ExtraActual(c) => actual_summary.push_actual_only(*c), + edit_distance::Edit::ExtraExpected(c) => { + expected_summary.push_expected_only(*c) + } + edit_distance::Edit::Both(c) => { + actual_summary.push_actual_with_match(*c); + expected_summary.push_expected_with_match(*c); + } + edit_distance::Edit::AdditionalActual => { + // Calling edit_distance::edit_list(_, _, Mode::Exact) should never return + // this enum + panic!("This should not happen. This is a bug in gtest_rust") + } + } + } + actual_summary.reset_ansi(); + expected_summary.reset_ansi(); + self.summary.push_str(&actual_summary.summary); + self.summary.push_str(&expected_summary.summary); + } else { + self.summary.new_line_for_actual(); + self.summary.push_str_actual_only(actual_line); + self.summary.new_line_for_expected(); + self.summary.push_str_expected_only(expected_line); + } } } impl<'a> FromIterator<edit_distance::Edit<&'a str>> for BufferedSummary<'a> { fn from_iter<T: IntoIterator<Item = edit_distance::Edit<&'a str>>>(iter: T) -> Self { - let mut buffered_summary = - BufferedSummary { summary: String::new(), buffer: Buffer::CommonLineBuffer(vec![]) }; + let mut buffered_summary = BufferedSummary::default(); for edit in iter { match edit { edit_distance::Edit::Both(same) => { @@ -159,6 +209,7 @@ impl<'a> FromIterator<edit_distance::Edit<&'a str>> for BufferedSummary<'a> { }; } buffered_summary.flush_buffer(); + buffered_summary.summary.reset_ansi(); buffered_summary } @@ -166,120 +217,80 @@ impl<'a> FromIterator<edit_distance::Edit<&'a str>> for BufferedSummary<'a> { impl<'a> Display for BufferedSummary<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if !matches!(self.buffer, Buffer::CommonLineBuffer(ref b) if b.is_empty()) { + if !matches!(self.buffer, Buffer::Empty) { panic!("Buffer is not empty. This is a bug in gtest_rust.") } - self.summary.fmt(f) + if !self.summary.last_ansi_style.is_empty() { + panic!("ANSI style has not been reset. This is a bug in gtest_rust.") + } + self.summary.summary.fmt(f) } } -// This needs to be an enum as there will be in a follow-up PR new types of -// buffer, most likely actual and expected lines, to be compared with expected -// and actual lines for line to line comparison. enum Buffer<'a> { + Empty, CommonLineBuffer(Vec<&'a str>), + ExtraActualLineChunk(&'a str), + ExtraExpectedLineChunk(&'a str), } impl<'a> Buffer<'a> { - fn flush(&mut self, writer: impl std::fmt::Write) -> std::fmt::Result { + fn flush(&mut self, summary: &mut SummaryBuilder) { match self { + Buffer::Empty => {} Buffer::CommonLineBuffer(common_lines) => { - Self::flush_common_lines(std::mem::take(common_lines), writer)? + Self::flush_common_lines(std::mem::take(common_lines), summary); + } + Buffer::ExtraActualLineChunk(extra_actual) => { + summary.new_line_for_actual(); + summary.push_str_actual_only(extra_actual); + } + Buffer::ExtraExpectedLineChunk(extra_expected) => { + summary.new_line_for_expected(); + summary.push_str_expected_only(extra_expected); } }; - Ok(()) + *self = Buffer::Empty; } - fn flush_common_lines( - common_lines: Vec<&'a str>, - mut writer: impl std::fmt::Write, - ) -> std::fmt::Result { + fn flush_common_lines(common_lines: Vec<&'a str>, summary: &mut SummaryBuilder) { // The number of the lines kept before and after the compressed lines. const COMMON_LINES_CONTEXT_SIZE: usize = 2; if common_lines.len() <= 2 * COMMON_LINES_CONTEXT_SIZE + 1 { for line in common_lines { - write!(writer, "\n{}", LineStyle::unchanged_style().style(line))?; + summary.new_line(); + summary.push_str(line); } - return Ok(()); + return; } let start_context = &common_lines[0..COMMON_LINES_CONTEXT_SIZE]; for line in start_context { - write!(writer, "\n{}", LineStyle::unchanged_style().style(line))?; + summary.new_line(); + summary.push_str(line); } - write!( - writer, - "\n{}", - LineStyle::comment_style().style(&format!( - "<---- {} common lines omitted ---->", - common_lines.len() - 2 * COMMON_LINES_CONTEXT_SIZE - )), - )?; + summary.new_line(); + summary.push_str_as_comment(&format!( + "<---- {} common lines omitted ---->", + common_lines.len() - 2 * COMMON_LINES_CONTEXT_SIZE, + )); let end_context = &common_lines[common_lines.len() - COMMON_LINES_CONTEXT_SIZE..common_lines.len()]; for line in end_context { - write!(writer, "\n{}", LineStyle::unchanged_style().style(line))?; + summary.new_line(); + summary.push_str(line); } - Ok(()) } } -// Use ANSI code to enable styling on the summary lines. -// -// See https://en.wikipedia.org/wiki/ANSI_escape_code. -struct LineStyle { - ansi_prefix: &'static str, - ansi_suffix: &'static str, - header: char, -} - -impl LineStyle { - // Font in red and bold - fn extra_actual_style() -> Self { - Self { ansi_prefix: "\x1B[1;31m", ansi_suffix: "\x1B[0m", header: '-' } - } - - // Font in green and bold - fn extra_expected_style() -> Self { - Self { ansi_prefix: "\x1B[1;32m", ansi_suffix: "\x1B[0m", header: '+' } - } - - // Font in italic - fn comment_style() -> Self { - Self { ansi_prefix: "\x1B[3m", ansi_suffix: "\x1B[0m", header: ' ' } - } - - // No ansi styling - fn unchanged_style() -> Self { - Self { ansi_prefix: "", ansi_suffix: "", header: ' ' } - } - - fn style(self, line: &str) -> StyledLine<'_> { - StyledLine { style: self, line } - } -} - -struct StyledLine<'a> { - style: LineStyle, - line: &'a str, -} - -impl<'a> Display for StyledLine<'a> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if stdout_supports_color() { - write!( - f, - "{}{}{}{}", - self.style.header, self.style.ansi_prefix, self.line, self.style.ansi_suffix - ) - } else { - write!(f, "{}{}", self.style.header, self.line) - } +impl<'a> Default for Buffer<'a> { + fn default() -> Self { + Self::Empty } } @@ -301,11 +312,107 @@ fn is_env_var_set(var: &'static str) -> bool { std::env::var(var).map(|s| !s.is_empty()).unwrap_or(false) } +// Font in italic +const COMMENT_STYLE: &str = "\x1B[3m"; +// Font in green and bold +const EXPECTED_ONLY_STYLE: &str = "\x1B[1;32m"; +// Font in red and bold +const ACTUAL_ONLY_STYLE: &str = "\x1B[1;31m"; +// Font in green onlyh +const EXPECTED_WITH_MATCH_STYLE: &str = "\x1B[32m"; +// Font in red only +const ACTUAL_WITH_MATCH_STYLE: &str = "\x1B[31m"; +// Reset all ANSI formatting +const RESET_ALL: &str = "\x1B[0m"; + +#[derive(Default)] +struct SummaryBuilder { + summary: String, + last_ansi_style: &'static str, +} + +impl SummaryBuilder { + fn push_str(&mut self, element: &str) { + self.reset_ansi(); + self.summary.push_str(element); + } + + fn push_str_as_comment(&mut self, element: &str) { + self.set_ansi(COMMENT_STYLE); + self.summary.push_str(element); + } + + fn push_str_actual_only(&mut self, element: &str) { + self.set_ansi(ACTUAL_ONLY_STYLE); + self.summary.push_str(element); + } + + fn push_str_expected_only(&mut self, element: &str) { + self.set_ansi(EXPECTED_ONLY_STYLE); + self.summary.push_str(element); + } + + fn push_actual_only(&mut self, element: char) { + self.set_ansi(ACTUAL_ONLY_STYLE); + self.summary.push(element); + } + + fn push_expected_only(&mut self, element: char) { + self.set_ansi(EXPECTED_ONLY_STYLE); + self.summary.push(element); + } + + fn push_actual_with_match(&mut self, element: char) { + self.set_ansi(ACTUAL_WITH_MATCH_STYLE); + self.summary.push(element); + } + + fn push_expected_with_match(&mut self, element: char) { + self.set_ansi(EXPECTED_WITH_MATCH_STYLE); + self.summary.push(element); + } + + fn new_line(&mut self) { + self.reset_ansi(); + self.summary.push_str("\n "); + } + + fn new_line_for_actual(&mut self) { + self.reset_ansi(); + self.summary.push_str("\n-"); + } + + fn new_line_for_expected(&mut self) { + self.reset_ansi(); + self.summary.push_str("\n+"); + } + + fn reset_ansi(&mut self) { + if !self.last_ansi_style.is_empty() && stdout_supports_color() { + self.summary.push_str(RESET_ALL); + self.last_ansi_style = ""; + } + } + + fn set_ansi(&mut self, ansi_style: &'static str) { + if !stdout_supports_color() || self.last_ansi_style == ansi_style { + return; + } + if !self.last_ansi_style.is_empty() { + self.summary.push_str(RESET_ALL); + } + self.summary.push_str(ansi_style); + self.last_ansi_style = ansi_style; + } +} + #[cfg(test)] mod tests { use super::*; use crate::{matcher_support::edit_distance::Mode, prelude::*}; use indoc::indoc; + use serial_test::{parallel, serial}; + use std::fmt::Write; // Make a long text with each element of the iterator on one line. // `collection` must contains at least one element. @@ -320,11 +427,13 @@ mod tests { } #[test] + #[parallel] fn create_diff_smaller_than_one_line() -> Result<()> { verify_that!(create_diff("One", "Two", Mode::Exact), eq("")) } #[test] + #[parallel] fn create_diff_exact_same() -> Result<()> { let expected = indoc! {" One @@ -341,14 +450,47 @@ mod tests { } #[test] + #[parallel] + fn create_diff_multiline_diff() -> Result<()> { + let expected = indoc! {" + prefix + Actual#1 + Actual#2 + Actual#3 + suffix"}; + let actual = indoc! {" + prefix + Expected@one + Expected@two + suffix"}; + // TODO: It would be better to have all the Actual together followed by all the + // Expected together. + verify_that!( + create_diff(expected, actual, Mode::Exact), + eq(indoc!( + " + + Difference(-actual / +expected): + prefix + -Actual#1 + +Expected@one + -Actual#2 + +Expected@two + -Actual#3 + suffix" + )) + ) + } + + #[test] + #[parallel] fn create_diff_exact_unrelated() -> Result<()> { verify_that!(create_diff(&build_text(1..500), &build_text(501..1000), Mode::Exact), eq("")) } #[test] - fn create_diff_exact_small_difference_no_color() -> Result<()> { - std::env::set_var("NO_COLOR", "1"); - + #[parallel] + fn create_diff_exact_small_difference() -> Result<()> { verify_that!( create_diff(&build_text(1..50), &build_text(1..51), Mode::Exact), eq(indoc! { @@ -364,4 +506,74 @@ mod tests { }) ) } + + // Test with color enabled. + + struct ForceColor; + + fn force_color() -> ForceColor { + std::env::set_var("FORCE_COLOR", "1"); + std::env::remove_var("NO_COLOR"); + ForceColor + } + + impl Drop for ForceColor { + fn drop(&mut self) { + std::env::remove_var("FORCE_COLOR"); + std::env::set_var("NO_COLOR", "1"); + } + } + + #[test] + #[serial] + fn create_diff_exact_small_difference_with_color() -> Result<()> { + let _keep = force_color(); + + verify_that!( + create_diff(&build_text(1..50), &build_text(1..51), Mode::Exact), + eq(indoc! { + " + + Difference(-\x1B[1;31mactual\x1B[0m / +\x1B[1;32mexpected\x1B[0m): + 1 + 2 + \x1B[3m<---- 45 common lines omitted ---->\x1B[0m + 48 + 49 + +\x1B[1;32m50\x1B[0m" + }) + ) + } + + #[test] + #[serial] + fn create_diff_exact_difference_with_inline_color() -> Result<()> { + let _keep = force_color(); + let actual = indoc!( + "There is a home in Nouvelle Orleans + They say, it is the rising sons + And it has been the ruin of many a po'boy" + ); + + let expected = indoc!( + "There is a house way down in New Orleans + They call the rising sun + And it has been the ruin of many a poor boy" + ); + + verify_that!( + create_diff(actual, expected, Mode::Exact), + eq(indoc! { + " + + Difference(-\x1B[1;31mactual\x1B[0m / +\x1B[1;32mexpected\x1B[0m): + -\x1B[31mThere is a ho\x1B[0m\x1B[1;31mm\x1B[0m\x1B[31me in N\x1B[0m\x1B[1;31mouv\x1B[0m\x1B[31me\x1B[0m\x1B[1;31mlle\x1B[0m\x1B[31m Orleans\x1B[0m + +\x1B[32mThere is a ho\x1B[0m\x1B[1;32mus\x1B[0m\x1B[32me \x1B[0m\x1B[1;32mway down \x1B[0m\x1B[32min Ne\x1B[0m\x1B[1;32mw\x1B[0m\x1B[32m Orleans\x1B[0m + -\x1B[31mThey \x1B[0m\x1B[1;31ms\x1B[0m\x1B[31ma\x1B[0m\x1B[1;31my,\x1B[0m\x1B[31m \x1B[0m\x1B[1;31mi\x1B[0m\x1B[31mt\x1B[0m\x1B[1;31m is t\x1B[0m\x1B[31mhe rising s\x1B[0m\x1B[1;31mo\x1B[0m\x1B[31mn\x1B[0m\x1B[1;31ms\x1B[0m + +\x1B[32mThey \x1B[0m\x1B[1;32mc\x1B[0m\x1B[32ma\x1B[0m\x1B[1;32mll\x1B[0m\x1B[32m the rising s\x1B[0m\x1B[1;32mu\x1B[0m\x1B[32mn\x1B[0m + -\x1B[31mAnd it has been the ruin of many a po\x1B[0m\x1B[1;31m'\x1B[0m\x1B[31mboy\x1B[0m + +\x1B[32mAnd it has been the ruin of many a po\x1B[0m\x1B[1;32mor \x1B[0m\x1B[32mboy\x1B[0m" + }) + ) + } } diff --git a/src/matchers/all_matcher.rs b/src/matchers/all_matcher.rs index cc959c7..f2e6d06 100644 --- a/src/matchers/all_matcher.rs +++ b/src/matchers/all_matcher.rs @@ -13,7 +13,7 @@ // limitations under the License. // There are no visible documentation elements in this module; the declarative -// macro is documented at the top level. +// macro is documented in the matcher module. #![doc(hidden)] /// Matches a value which all of the given matchers match. @@ -51,9 +51,10 @@ /// /// Assertion failure messages are not guaranteed to be identical, however. #[macro_export] -macro_rules! all { +#[doc(hidden)] +macro_rules! __all { ($($matcher:expr),* $(,)?) => {{ - use $crate::matchers::all_matcher::internal::AllMatcher; + use $crate::matchers::__internal_unstable_do_not_depend_on_these::AllMatcher; AllMatcher::new([$(Box::new($matcher)),*]) }} } @@ -63,8 +64,8 @@ macro_rules! all { /// For internal use only. API stablility is not guaranteed! #[doc(hidden)] pub mod internal { + use crate::description::Description; use crate::matcher::{Matcher, MatcherResult}; - use crate::matcher_support::description::Description; use crate::matchers::anything; use std::fmt::Debug; @@ -101,7 +102,7 @@ pub mod internal { MatcherResult::Match } - fn explain_match(&self, actual: &Self::ActualT) -> String { + fn explain_match(&self, actual: &Self::ActualT) -> Description { match N { 0 => anything::<T>().explain_match(actual), 1 => self.components[0].explain_match(actual), @@ -110,36 +111,37 @@ pub mod internal { .components .iter() .filter(|component| component.matches(actual).is_no_match()) - .map(|component| component.explain_match(actual)) - .collect::<Description>(); + .collect::<Vec<_>>(); + if failures.len() == 1 { - format!("{}", failures) + failures[0].explain_match(actual) } else { - format!("{}", failures.bullet_list().indent_except_first_line()) + Description::new() + .collect( + failures + .into_iter() + .map(|component| component.explain_match(actual)), + ) + .bullet_list() } } } } - fn describe(&self, matcher_result: MatcherResult) -> String { + fn describe(&self, matcher_result: MatcherResult) -> Description { match N { 0 => anything::<T>().describe(matcher_result), 1 => self.components[0].describe(matcher_result), _ => { - let properties = self - .components - .iter() - .map(|m| m.describe(matcher_result)) - .collect::<Description>() - .bullet_list() - .indent(); - format!( - "{}:\n{properties}", - if matcher_result.into() { - "has all the following properties" - } else { - "has at least one of the following properties" - } + let header = if matcher_result.into() { + "has all the following properties:" + } else { + "has at least one of the following properties:" + }; + Description::new().text(header).nested( + Description::new() + .bullet_list() + .collect(self.components.iter().map(|m| m.describe(matcher_result))), ) } } @@ -162,12 +164,12 @@ mod tests { verify_that!( matcher.describe(MatcherResult::Match), - eq(indoc!( + displays_as(eq(indoc!( " has all the following properties: * starts with prefix \"A\" * ends with suffix \"string\"" - )) + ))) ) } @@ -176,7 +178,10 @@ mod tests { let first_matcher = starts_with("A"); let matcher: internal::AllMatcher<String, 1> = all!(first_matcher); - verify_that!(matcher.describe(MatcherResult::Match), eq("starts with prefix \"A\"")) + verify_that!( + matcher.describe(MatcherResult::Match), + displays_as(eq("starts with prefix \"A\"")) + ) } #[test] diff --git a/src/matchers/any_matcher.rs b/src/matchers/any_matcher.rs index 82c8910..95d53fe 100644 --- a/src/matchers/any_matcher.rs +++ b/src/matchers/any_matcher.rs @@ -13,7 +13,7 @@ // limitations under the License. // There are no visible documentation elements in this module; the declarative -// macro is documented at the top level. +// macro is documented in the matchers module. #![doc(hidden)] /// Matches a value which at least one of the given matchers match. @@ -53,9 +53,10 @@ /// /// Assertion failure messages are not guaranteed to be identical, however. #[macro_export] -macro_rules! any { +#[doc(hidden)] +macro_rules! __any { ($($matcher:expr),* $(,)?) => {{ - use $crate::matchers::any_matcher::internal::AnyMatcher; + use $crate::matchers::__internal_unstable_do_not_depend_on_these::AnyMatcher; AnyMatcher::new([$(Box::new($matcher)),*]) }} } @@ -65,8 +66,8 @@ macro_rules! any { /// For internal use only. API stablility is not guaranteed! #[doc(hidden)] pub mod internal { + use crate::description::Description; use crate::matcher::{Matcher, MatcherResult}; - use crate::matcher_support::description::Description; use crate::matchers::anything; use std::fmt::Debug; @@ -95,27 +96,33 @@ pub mod internal { MatcherResult::from(self.components.iter().any(|c| c.matches(actual).is_match())) } - fn explain_match(&self, actual: &Self::ActualT) -> String { + fn explain_match(&self, actual: &Self::ActualT) -> Description { match N { - 0 => format!("which {}", anything::<T>().describe(MatcherResult::NoMatch)), + 0 => format!("which {}", anything::<T>().describe(MatcherResult::NoMatch)).into(), 1 => self.components[0].explain_match(actual), _ => { let failures = self .components .iter() .filter(|component| component.matches(actual).is_no_match()) - .map(|component| component.explain_match(actual)) - .collect::<Description>(); + .collect::<Vec<_>>(); + if failures.len() == 1 { - format!("{}", failures) + failures[0].explain_match(actual) } else { - format!("{}", failures.bullet_list().indent_except_first_line()) + Description::new() + .collect( + failures + .into_iter() + .map(|component| component.explain_match(actual)), + ) + .bullet_list() } } } } - fn describe(&self, matcher_result: MatcherResult) -> String { + fn describe(&self, matcher_result: MatcherResult) -> Description { match N { 0 => anything::<T>().describe(matcher_result), 1 => self.components[0].describe(matcher_result), @@ -135,6 +142,7 @@ pub mod internal { "has none of the following properties" } ) + .into() } } } @@ -156,12 +164,12 @@ mod tests { verify_that!( matcher.describe(MatcherResult::Match), - eq(indoc!( + displays_as(eq(indoc!( " has at least one of the following properties: * starts with prefix \"A\" * ends with suffix \"string\"" - )) + ))) ) } @@ -170,7 +178,10 @@ mod tests { let first_matcher = starts_with("A"); let matcher: internal::AnyMatcher<String, 1> = any!(first_matcher); - verify_that!(matcher.describe(MatcherResult::Match), eq("starts with prefix \"A\"")) + verify_that!( + matcher.describe(MatcherResult::Match), + displays_as(eq("starts with prefix \"A\"")) + ) } #[test] diff --git a/src/matchers/anything_matcher.rs b/src/matchers/anything_matcher.rs index 82de460..36be478 100644 --- a/src/matchers/anything_matcher.rs +++ b/src/matchers/anything_matcher.rs @@ -12,7 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::matcher::{Matcher, MatcherResult}; +use crate::{ + description::Description, + matcher::{Matcher, MatcherResult}, +}; use std::{fmt::Debug, marker::PhantomData}; /// Matches anything. This matcher always succeeds. @@ -42,10 +45,10 @@ impl<T: Debug + ?Sized> Matcher for Anything<T> { MatcherResult::Match } - fn describe(&self, matcher_result: MatcherResult) -> String { + fn describe(&self, matcher_result: MatcherResult) -> Description { match matcher_result { - MatcherResult::Match => "is anything".to_string(), - MatcherResult::NoMatch => "never matches".to_string(), + MatcherResult::Match => "is anything".into(), + MatcherResult::NoMatch => "never matches".into(), } } } diff --git a/src/matchers/char_count_matcher.rs b/src/matchers/char_count_matcher.rs index a7765b4..70977d7 100644 --- a/src/matchers/char_count_matcher.rs +++ b/src/matchers/char_count_matcher.rs @@ -12,7 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::matcher::{Matcher, MatcherResult}; +use crate::{ + description::Description, + matcher::{Matcher, MatcherResult}, +}; use std::{fmt::Debug, marker::PhantomData}; /// Matches a string whose number of Unicode scalars matches `expected`. @@ -71,36 +74,36 @@ impl<T: Debug + ?Sized + AsRef<str>, E: Matcher<ActualT = usize>> Matcher for Ch self.expected.matches(&actual.as_ref().chars().count()) } - fn describe(&self, matcher_result: MatcherResult) -> String { + fn describe(&self, matcher_result: MatcherResult) -> Description { match matcher_result { - MatcherResult::Match => { - format!( - "has character count, which {}", - self.expected.describe(MatcherResult::Match) - ) - } - MatcherResult::NoMatch => { - format!( - "has character count, which {}", - self.expected.describe(MatcherResult::NoMatch) - ) - } + MatcherResult::Match => format!( + "has character count, which {}", + self.expected.describe(MatcherResult::Match) + ) + .into(), + MatcherResult::NoMatch => format!( + "has character count, which {}", + self.expected.describe(MatcherResult::NoMatch) + ) + .into(), } } - fn explain_match(&self, actual: &T) -> String { + fn explain_match(&self, actual: &T) -> Description { let actual_size = actual.as_ref().chars().count(); format!( "which has character count {}, {}", actual_size, self.expected.explain_match(&actual_size) ) + .into() } } #[cfg(test)] mod tests { use super::char_count; + use crate::description::Description; use crate::matcher::{Matcher, MatcherResult}; use crate::prelude::*; use indoc::indoc; @@ -135,11 +138,11 @@ mod tests { false.into() } - fn describe(&self, _: MatcherResult) -> String { + fn describe(&self, _: MatcherResult) -> Description { "called described".into() } - fn explain_match(&self, _: &T) -> String { + fn explain_match(&self, _: &T) -> Description { "called explain_match".into() } } diff --git a/src/matchers/conjunction_matcher.rs b/src/matchers/conjunction_matcher.rs index 1ba59c3..dc50833 100644 --- a/src/matchers/conjunction_matcher.rs +++ b/src/matchers/conjunction_matcher.rs @@ -15,7 +15,10 @@ // There are no visible documentation elements in this module. #![doc(hidden)] -use crate::matcher::{Matcher, MatcherResult}; +use crate::{ + description::Description, + matcher::{Matcher, MatcherResult}, +}; use std::fmt::Debug; /// Matcher created by [`Matcher::and`]. @@ -46,29 +49,24 @@ where } } - fn explain_match(&self, actual: &M1::ActualT) -> String { + fn explain_match(&self, actual: &M1::ActualT) -> Description { match (self.m1.matches(actual), self.m2.matches(actual)) { - (MatcherResult::Match, MatcherResult::Match) => { - format!( - "{} and\n {}", - self.m1.explain_match(actual), - self.m2.explain_match(actual) - ) - } + (MatcherResult::Match, MatcherResult::Match) => Description::new() + .nested(self.m1.explain_match(actual)) + .text("and") + .nested(self.m2.explain_match(actual)), (MatcherResult::NoMatch, MatcherResult::Match) => self.m1.explain_match(actual), (MatcherResult::Match, MatcherResult::NoMatch) => self.m2.explain_match(actual), - (MatcherResult::NoMatch, MatcherResult::NoMatch) => { - format!( - "{} and\n {}", - self.m1.explain_match(actual), - self.m2.explain_match(actual) - ) - } + (MatcherResult::NoMatch, MatcherResult::NoMatch) => Description::new() + .nested(self.m1.explain_match(actual)) + .text("and") + .nested(self.m2.explain_match(actual)), } } - fn describe(&self, matcher_result: MatcherResult) -> String { + fn describe(&self, matcher_result: MatcherResult) -> Description { format!("{}, and {}", self.m1.describe(matcher_result), self.m2.describe(matcher_result)) + .into() } } @@ -124,8 +122,9 @@ mod tests { Value of: 1 Expected: never matches, and never matches Actual: 1, - which is anything and - which is anything + which is anything + and + which is anything " )))) ) diff --git a/src/matchers/container_eq_matcher.rs b/src/matchers/container_eq_matcher.rs index f80cebf..d4f872c 100644 --- a/src/matchers/container_eq_matcher.rs +++ b/src/matchers/container_eq_matcher.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::description::Description; use crate::matcher::{Matcher, MatcherResult}; use std::fmt::Debug; use std::marker::PhantomData; @@ -29,9 +30,14 @@ use std::marker::PhantomData; /// Unexpected: [4] /// ``` /// -/// The type of `expected` must implement `IntoIterator` with an `Item` which -/// implements `PartialEq`. If the container type is a `Vec`, then the expected -/// type may be a slice of the same element type. For example: +/// The actual value must be a container such as a `Vec`, an array, or a +/// dereferenced slice. More precisely, a shared borrow of the actual value must +/// implement [`IntoIterator`] whose `Item` type implements +/// [`PartialEq<ExpectedT>`], where `ExpectedT` is the element type of the +/// expected value. +/// +/// If the container type is a `Vec`, then the expected type may be a slice of +/// the same element type. For example: /// /// ``` /// # use googletest::prelude::*; @@ -114,14 +120,14 @@ where (*actual == self.expected).into() } - fn explain_match(&self, actual: &ActualContainerT) -> String { - build_explanation(self.get_missing_items(actual), self.get_unexpected_items(actual)) + fn explain_match(&self, actual: &ActualContainerT) -> Description { + build_explanation(self.get_missing_items(actual), self.get_unexpected_items(actual)).into() } - fn describe(&self, matcher_result: MatcherResult) -> String { + fn describe(&self, matcher_result: MatcherResult) -> Description { match matcher_result { - MatcherResult::Match => format!("is equal to {:?}", self.expected), - MatcherResult::NoMatch => format!("isn't equal to {:?}", self.expected), + MatcherResult::Match => format!("is equal to {:?}", self.expected).into(), + MatcherResult::NoMatch => format!("isn't equal to {:?}", self.expected).into(), } } } diff --git a/src/matchers/contains_matcher.rs b/src/matchers/contains_matcher.rs index d714c88..1a27ce0 100644 --- a/src/matchers/contains_matcher.rs +++ b/src/matchers/contains_matcher.rs @@ -12,7 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::matcher::{Matcher, MatcherResult}; +use crate::{ + description::Description, + matcher::{Matcher, MatcherResult}, +}; use std::{fmt::Debug, marker::PhantomData}; /// Matches an iterable type whose elements contain a value matched by `inner`. @@ -101,33 +104,37 @@ where } } - fn explain_match(&self, actual: &Self::ActualT) -> String { + fn explain_match(&self, actual: &Self::ActualT) -> Description { let count = self.count_matches(actual); match (count, &self.count) { - (_, Some(_)) => format!("which contains {} matching elements", count), - (0, None) => "which does not contain a matching element".to_string(), - (_, None) => "which contains a matching element".to_string(), + (_, Some(_)) => format!("which contains {} matching elements", count).into(), + (0, None) => "which does not contain a matching element".into(), + (_, None) => "which contains a matching element".into(), } } - fn describe(&self, matcher_result: MatcherResult) -> String { + fn describe(&self, matcher_result: MatcherResult) -> Description { match (matcher_result, &self.count) { (MatcherResult::Match, Some(count)) => format!( "contains n elements which {}\n where n {}", self.inner.describe(MatcherResult::Match), count.describe(MatcherResult::Match) - ), + ) + .into(), (MatcherResult::NoMatch, Some(count)) => format!( "doesn't contain n elements which {}\n where n {}", self.inner.describe(MatcherResult::Match), count.describe(MatcherResult::Match) - ), + ) + .into(), (MatcherResult::Match, None) => format!( "contains at least one element which {}", self.inner.describe(MatcherResult::Match) - ), + ) + .into(), (MatcherResult::NoMatch, None) => { format!("contains no element which {}", self.inner.describe(MatcherResult::Match)) + .into() } } } @@ -193,7 +200,7 @@ mod tests { #[test] fn contains_does_not_match_empty_slice() -> Result<()> { - let matcher = contains(eq(1)); + let matcher = contains(eq::<i32, _>(1)); let result = matcher.matches(&[]); @@ -233,7 +240,7 @@ mod tests { verify_that!( Matcher::describe(&matcher, MatcherResult::Match), - eq("contains at least one element which is equal to 1") + displays_as(eq("contains at least one element which is equal to 1")) ) } @@ -243,7 +250,7 @@ mod tests { verify_that!( Matcher::describe(&matcher, MatcherResult::Match), - eq("contains n elements which is equal to 1\n where n is equal to 2") + displays_as(eq("contains n elements which is equal to 1\n where n is equal to 2")) ) } diff --git a/src/matchers/contains_regex_matcher.rs b/src/matchers/contains_regex_matcher.rs index 8cc93a7..6f68168 100644 --- a/src/matchers/contains_regex_matcher.rs +++ b/src/matchers/contains_regex_matcher.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::description::Description; use crate::matcher::{Matcher, MatcherResult}; use regex::Regex; use std::fmt::Debug; @@ -77,13 +78,13 @@ impl<ActualT: AsRef<str> + Debug + ?Sized> Matcher for ContainsRegexMatcher<Actu self.regex.is_match(actual.as_ref()).into() } - fn describe(&self, matcher_result: MatcherResult) -> String { + fn describe(&self, matcher_result: MatcherResult) -> Description { match matcher_result { MatcherResult::Match => { - format!("contains the regular expression {:#?}", self.regex.as_str()) + format!("contains the regular expression {:#?}", self.regex.as_str()).into() } MatcherResult::NoMatch => { - format!("doesn't contain the regular expression {:#?}", self.regex.as_str()) + format!("doesn't contain the regular expression {:#?}", self.regex.as_str()).into() } } } @@ -142,7 +143,7 @@ mod tests { verify_that!( Matcher::describe(&matcher, MatcherResult::Match), - eq("contains the regular expression \"\\n\"") + displays_as(eq("contains the regular expression \"\\n\"")) ) } } diff --git a/src/matchers/disjunction_matcher.rs b/src/matchers/disjunction_matcher.rs index 48bf226..dd56be2 100644 --- a/src/matchers/disjunction_matcher.rs +++ b/src/matchers/disjunction_matcher.rs @@ -15,7 +15,10 @@ // There are no visible documentation elements in this module. #![doc(hidden)] -use crate::matcher::{Matcher, MatcherResult}; +use crate::{ + description::Description, + matcher::{Matcher, MatcherResult}, +}; use std::fmt::Debug; /// Matcher created by [`Matcher::or`]. @@ -46,12 +49,16 @@ where } } - fn explain_match(&self, actual: &M1::ActualT) -> String { - format!("{} and\n {}", self.m1.explain_match(actual), self.m2.explain_match(actual)) + fn explain_match(&self, actual: &M1::ActualT) -> Description { + Description::new() + .nested(self.m1.explain_match(actual)) + .text("and") + .nested(self.m2.explain_match(actual)) } - fn describe(&self, matcher_result: MatcherResult) -> String { + fn describe(&self, matcher_result: MatcherResult) -> Description { format!("{}, or {}", self.m1.describe(matcher_result), self.m2.describe(matcher_result)) + .into() } } @@ -85,8 +92,9 @@ mod tests { Value of: 1 Expected: never matches, or never matches Actual: 1, - which is anything and - which is anything + which is anything + and + which is anything " )))) ) diff --git a/src/matchers/display_matcher.rs b/src/matchers/display_matcher.rs index 628769b..51a5bff 100644 --- a/src/matchers/display_matcher.rs +++ b/src/matchers/display_matcher.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::description::Description; use crate::matcher::{Matcher, MatcherResult}; use std::fmt::{Debug, Display}; use std::marker::PhantomData; @@ -42,21 +43,22 @@ impl<T: Debug + Display, InnerMatcher: Matcher<ActualT = String>> Matcher self.inner.matches(&format!("{actual}")) } - fn explain_match(&self, actual: &T) -> String { + fn explain_match(&self, actual: &T) -> Description { format!("which displays as a string {}", self.inner.explain_match(&format!("{actual}"))) + .into() } - fn describe(&self, matcher_result: MatcherResult) -> String { + fn describe(&self, matcher_result: MatcherResult) -> Description { match matcher_result { MatcherResult::Match => { format!("displays as a string which {}", self.inner.describe(MatcherResult::Match)) + .into() } - MatcherResult::NoMatch => { - format!( - "doesn't display as a string which {}", - self.inner.describe(MatcherResult::Match) - ) - } + MatcherResult::NoMatch => format!( + "doesn't display as a string which {}", + self.inner.describe(MatcherResult::Match) + ) + .into(), } } } @@ -107,6 +109,7 @@ mod tests { result, err(displays_as(contains_substring(indoc!( " + Actual: \"123\\n234\", which displays as a string which isn't equal to \"123\\n345\" Difference(-actual / +expected): 123 diff --git a/src/matchers/each_matcher.rs b/src/matchers/each_matcher.rs index ce61207..08a0742 100644 --- a/src/matchers/each_matcher.rs +++ b/src/matchers/each_matcher.rs @@ -12,14 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::description::Description; use crate::matcher::{Matcher, MatcherResult}; -use crate::matcher_support::description::Description; use std::{fmt::Debug, marker::PhantomData}; /// Matches a container all of whose elements are matched by the matcher /// `inner`. /// -/// `T` can be any container such that `&T` implements `IntoIterator`. +/// `T` can be any container such that `&T` implements `IntoIterator`. This +/// includes `Vec`, arrays, and (dereferenced) slices. /// /// ``` /// # use googletest::prelude::*; @@ -27,6 +28,10 @@ use std::{fmt::Debug, marker::PhantomData}; /// # fn should_pass_1() -> Result<()> { /// let value = vec![1, 2, 3]; /// verify_that!(value, each(gt(0)))?; // Passes +/// let array_value = [1, 2, 3]; +/// verify_that!(array_value, each(gt(0)))?; // Passes +/// let slice_value = &[1, 2, 3]; +/// verify_that!(*slice_value, each(gt(0)))?; // Passes /// # Ok(()) /// # } /// # fn should_fail() -> Result<()> { @@ -87,7 +92,7 @@ where MatcherResult::Match } - fn explain_match(&self, actual: &ActualT) -> String { + fn explain_match(&self, actual: &ActualT) -> Description { let mut non_matching_elements = Vec::new(); for (index, element) in actual.into_iter().enumerate() { if self.inner.matches(element).is_no_match() { @@ -95,11 +100,12 @@ where } } if non_matching_elements.is_empty() { - return format!("whose each element {}", self.inner.describe(MatcherResult::Match)); + return format!("whose each element {}", self.inner.describe(MatcherResult::Match)) + .into(); } if non_matching_elements.len() == 1 { let (idx, element, explanation) = non_matching_elements.remove(0); - return format!("whose element #{idx} is {element:?}, {explanation}"); + return format!("whose element #{idx} is {element:?}, {explanation}").into(); } let failed_indexes = non_matching_elements @@ -112,16 +118,18 @@ where .map(|&(_, element, ref explanation)| format!("{element:?}, {explanation}")) .collect::<Description>() .indent(); - format!("whose elements {failed_indexes} don't match\n{element_explanations}") + format!("whose elements {failed_indexes} don't match\n{element_explanations}").into() } - fn describe(&self, matcher_result: MatcherResult) -> String { + fn describe(&self, matcher_result: MatcherResult) -> Description { match matcher_result { MatcherResult::Match => { format!("only contains elements that {}", self.inner.describe(MatcherResult::Match)) + .into() } MatcherResult::NoMatch => { format!("contains no element that {}", self.inner.describe(MatcherResult::Match)) + .into() } } } @@ -220,8 +228,8 @@ mod tests { Expected: only contains elements that is greater than 1 Actual: [0, 1, 3], whose elements #0, #1 don't match - 0, which is less than or equal to 1 - 1, which is less than or equal to 1" + 0, which is less than or equal to 1 + 1, which is less than or equal to 1" )))) ) } @@ -233,7 +241,6 @@ mod tests { result, err(displays_as(contains_substring(indoc!( " - Value of: vec![vec! [1, 2], vec! [1]] Expected: only contains elements that only contains elements that is equal to 1 Actual: [[1, 2], [1]], whose element #0 is [1, 2], whose element #1 is 2, which isn't equal to 1" diff --git a/src/matchers/elements_are_matcher.rs b/src/matchers/elements_are_matcher.rs index 736bf47..3a4b5b2 100644 --- a/src/matchers/elements_are_matcher.rs +++ b/src/matchers/elements_are_matcher.rs @@ -13,7 +13,7 @@ // limitations under the License. // There are no visible documentation elements in this module; the declarative -// macro is documented at the top level. +// macro is documented in the matchers module. #![doc(hidden)] /// Matches a container's elements to each matcher in order. @@ -28,8 +28,9 @@ /// # .unwrap(); /// ``` /// -/// The actual value must be a container implementing [`IntoIterator`]. This -/// includes standard containers, slices (when dereferenced) and arrays. +/// The actual value must be a container such as a `Vec`, an array, or a +/// dereferenced slice. More precisely, a shared borrow of the actual value must +/// implement [`IntoIterator`]. /// /// ``` /// # use googletest::prelude::*; @@ -50,7 +51,7 @@ /// /// Note: This behavior is only possible in [`verify_that!`] macros. In any /// other cases, it is still necessary to use the -/// [`elements_are!`][crate::elements_are] macro. +/// [`elements_are!`][crate::matchers::elements_are] macro. /// /// ```compile_fail /// # use googletest::prelude::*; @@ -69,7 +70,8 @@ /// match against an iterator, use [`Iterator::collect`] to build a [`Vec`]. /// /// Do not use this with unordered containers, since that will lead to flaky -/// tests. Use [`unordered_elements_are!`][crate::unordered_elements_are] +/// tests. Use +/// [`unordered_elements_are!`][crate::matchers::unordered_elements_are] /// instead. /// /// [`IntoIterator`]: std::iter::IntoIterator @@ -77,9 +79,10 @@ /// [`Iterator::collect`]: std::iter::Iterator::collect /// [`Vec`]: std::vec::Vec #[macro_export] -macro_rules! elements_are { +#[doc(hidden)] +macro_rules! __elements_are { ($($matcher:expr),* $(,)?) => {{ - use $crate::matchers::elements_are_matcher::internal::ElementsAre; + use $crate::matchers::__internal_unstable_do_not_depend_on_these::ElementsAre; ElementsAre::new(vec![$(Box::new($matcher)),*]) }} } @@ -89,8 +92,8 @@ macro_rules! elements_are { /// **For internal use only. API stablility is not guaranteed!** #[doc(hidden)] pub mod internal { + use crate::description::Description; use crate::matcher::{Matcher, MatcherResult}; - use crate::matcher_support::description::Description; use crate::matcher_support::zipped_iterator::zip; use std::{fmt::Debug, marker::PhantomData}; @@ -133,7 +136,7 @@ pub mod internal { } } - fn explain_match(&self, actual: &ContainerT) -> String { + fn explain_match(&self, actual: &ContainerT) -> Description { let actual_iterator = actual.into_iter(); let mut zipped_iterator = zip(actual_iterator, self.elements.iter()); let mut mismatches = Vec::new(); @@ -144,20 +147,20 @@ pub mod internal { } if mismatches.is_empty() { if !zipped_iterator.has_size_mismatch() { - "whose elements all match".to_string() + "whose elements all match".into() } else { - format!("whose size is {}", zipped_iterator.left_size()) + format!("whose size is {}", zipped_iterator.left_size()).into() } } else if mismatches.len() == 1 { let mismatches = mismatches.into_iter().collect::<Description>(); - format!("where {mismatches}") + format!("where {mismatches}").into() } else { let mismatches = mismatches.into_iter().collect::<Description>(); - format!("where:\n{}", mismatches.bullet_list().indent()) + format!("where:\n{}", mismatches.bullet_list().indent()).into() } } - fn describe(&self, matcher_result: MatcherResult) -> String { + fn describe(&self, matcher_result: MatcherResult) -> Description { format!( "{} elements:\n{}", if matcher_result.into() { "has" } else { "doesn't have" }, @@ -169,6 +172,7 @@ pub mod internal { .enumerate() .indent() ) + .into() } } } diff --git a/src/matchers/empty_matcher.rs b/src/matchers/empty_matcher.rs index afefcb7..11cb675 100644 --- a/src/matchers/empty_matcher.rs +++ b/src/matchers/empty_matcher.rs @@ -12,12 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::matcher::{Matcher, MatcherResult}; +use crate::{ + description::Description, + matcher::{Matcher, MatcherResult}, +}; use std::{fmt::Debug, marker::PhantomData}; /// Matches an empty container. /// -/// `T` can be any container such that `&T` implements `IntoIterator`. +/// `T` can be any container such that `&T` implements `IntoIterator`. For +/// instance, `T` can be a common container like `Vec` and +/// [`HashSet`][std::collections::HashSet]. /// /// ``` /// # use googletest::prelude::*; @@ -66,8 +71,8 @@ where actual.into_iter().next().is_none().into() } - fn describe(&self, matcher_result: MatcherResult) -> String { - if matcher_result.into() { "is empty" } else { "isn't empty" }.to_string() + fn describe(&self, matcher_result: MatcherResult) -> Description { + if matcher_result.into() { "is empty" } else { "isn't empty" }.into() } } diff --git a/src/matchers/eq_deref_of_matcher.rs b/src/matchers/eq_deref_of_matcher.rs index 1540905..320aafb 100644 --- a/src/matchers/eq_deref_of_matcher.rs +++ b/src/matchers/eq_deref_of_matcher.rs @@ -13,6 +13,7 @@ // limitations under the License. use crate::{ + description::Description, matcher::{Matcher, MatcherResult}, matcher_support::{edit_distance, summarize_diff::create_diff}, }; @@ -76,14 +77,14 @@ where (self.expected.deref() == actual).into() } - fn describe(&self, matcher_result: MatcherResult) -> String { + fn describe(&self, matcher_result: MatcherResult) -> Description { match matcher_result { - MatcherResult::Match => format!("is equal to {:?}", self.expected), - MatcherResult::NoMatch => format!("isn't equal to {:?}", self.expected), + MatcherResult::Match => format!("is equal to {:?}", self.expected).into(), + MatcherResult::NoMatch => format!("isn't equal to {:?}", self.expected).into(), } } - fn explain_match(&self, actual: &ActualT) -> String { + fn explain_match(&self, actual: &ActualT) -> Description { format!( "which {}{}", &self.describe(self.matches(actual)), @@ -93,6 +94,7 @@ where edit_distance::Mode::Exact, ) ) + .into() } } @@ -138,13 +140,13 @@ mod tests { " Actual: Strukt { int: 123, string: \"something\" }, which isn't equal to Strukt { int: 321, string: \"someone\" } - Difference(-actual / +expected): - Strukt { - - int: 123, - + int: 321, - - string: \"something\", - + string: \"someone\", - } + Difference(-actual / +expected): + Strukt { + - int: 123, + + int: 321, + - string: \"something\", + + string: \"someone\", + } "}))) ) } diff --git a/src/matchers/eq_matcher.rs b/src/matchers/eq_matcher.rs index 42684c9..985307f 100644 --- a/src/matchers/eq_matcher.rs +++ b/src/matchers/eq_matcher.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::description::Description; use crate::matcher::{Matcher, MatcherResult}; use crate::matcher_support::edit_distance; use crate::matcher_support::summarize_diff::create_diff; @@ -82,21 +83,21 @@ pub struct EqMatcher<A: ?Sized, T> { phantom: PhantomData<A>, } -impl<A: Debug + ?Sized, T: PartialEq<A> + Debug> Matcher for EqMatcher<A, T> { +impl<T: Debug, A: Debug + ?Sized + PartialEq<T>> Matcher for EqMatcher<A, T> { type ActualT = A; fn matches(&self, actual: &A) -> MatcherResult { - (self.expected == *actual).into() + (*actual == self.expected).into() } - fn describe(&self, matcher_result: MatcherResult) -> String { + fn describe(&self, matcher_result: MatcherResult) -> Description { match matcher_result { - MatcherResult::Match => format!("is equal to {:?}", self.expected), - MatcherResult::NoMatch => format!("isn't equal to {:?}", self.expected), + MatcherResult::Match => format!("is equal to {:?}", self.expected).into(), + MatcherResult::NoMatch => format!("isn't equal to {:?}", self.expected).into(), } } - fn explain_match(&self, actual: &A) -> String { + fn explain_match(&self, actual: &A) -> Description { let expected_debug = format!("{:#?}", self.expected); let actual_debug = format!("{:#?}", actual); let description = self.describe(self.matches(actual)); @@ -117,7 +118,7 @@ impl<A: Debug + ?Sized, T: PartialEq<A> + Debug> Matcher for EqMatcher<A, T> { create_diff(&actual_debug, &expected_debug, edit_distance::Mode::Exact) }; - format!("which {description}{diff}") + format!("which {description}{diff}").into() } } @@ -178,13 +179,13 @@ mod tests { " Actual: Strukt { int: 123, string: \"something\" }, which isn't equal to Strukt { int: 321, string: \"someone\" } - Difference(-actual / +expected): - Strukt { - - int: 123, - + int: 321, - - string: \"something\", - + string: \"someone\", - } + Difference(-actual / +expected): + Strukt { + - int: 123, + + int: 321, + - string: \"something\", + + string: \"someone\", + } "}))) ) } @@ -200,13 +201,13 @@ mod tests { Expected: is equal to [1, 3, 4] Actual: [1, 2, 3], which isn't equal to [1, 3, 4] - Difference(-actual / +expected): - [ - 1, - - 2, - 3, - + 4, - ] + Difference(-actual / +expected): + [ + 1, + - 2, + 3, + + 4, + ] "}))) ) } @@ -222,14 +223,14 @@ mod tests { Expected: is equal to [1, 3, 5] Actual: [1, 2, 3, 4, 5], which isn't equal to [1, 3, 5] - Difference(-actual / +expected): - [ - 1, - - 2, - 3, - - 4, - 5, - ] + Difference(-actual / +expected): + [ + 1, + - 2, + 3, + - 4, + 5, + ] "}))) ) } @@ -241,18 +242,20 @@ mod tests { result, err(displays_as(contains_substring(indoc! { " - Difference(-actual / +expected): - [ - - 1, - - 2, - 3, - 4, - <---- 43 common lines omitted ----> - 48, - 49, - + 50, - + 51, - ]"}))) + ], + which isn't equal to [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51] + Difference(-actual / +expected): + [ + - 1, + - 2, + 3, + 4, + <---- 43 common lines omitted ----> + 48, + 49, + + 50, + + 51, + ]"}))) ) } @@ -263,18 +266,20 @@ mod tests { result, err(displays_as(contains_substring(indoc! { " - Difference(-actual / +expected): - [ - - 1, - - 2, - 3, - 4, - 5, - 6, - 7, - + 8, - + 9, - ]"}))) + Actual: [1, 2, 3, 4, 5, 6, 7], + which isn't equal to [3, 4, 5, 6, 7, 8, 9] + Difference(-actual / +expected): + [ + - 1, + - 2, + 3, + 4, + 5, + 6, + 7, + + 8, + + 9, + ]"}))) ) } @@ -285,15 +290,17 @@ mod tests { result, err(displays_as(contains_substring(indoc! { " - Difference(-actual / +expected): - [ - 1, - <---- 46 common lines omitted ----> - 48, - 49, - + 50, - + 51, - ]"}))) + ], + which isn't equal to [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51] + Difference(-actual / +expected): + [ + 1, + <---- 46 common lines omitted ----> + 48, + 49, + + 50, + + 51, + ]"}))) ) } @@ -304,15 +311,17 @@ mod tests { result, err(displays_as(contains_substring(indoc! { " - Difference(-actual / +expected): - [ - - 1, - - 2, - 3, - 4, - <---- 46 common lines omitted ----> - 51, - ]"}))) + ], + which isn't equal to [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51] + Difference(-actual / +expected): + [ + - 1, + - 2, + 3, + 4, + <---- 46 common lines omitted ----> + 51, + ]"}))) ) } @@ -354,14 +363,13 @@ mod tests { verify_that!( result, - err(displays_as(contains_substring(indoc!( - " - First line - -Second line - +Second lines - Third line - " - )))) + err(displays_as(contains_substring( + "\ + First line + -Second line + +Second lines + Third line" + ))) ) } diff --git a/src/matchers/err_matcher.rs b/src/matchers/err_matcher.rs index 022bc8c..3b10de4 100644 --- a/src/matchers/err_matcher.rs +++ b/src/matchers/err_matcher.rs @@ -12,7 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::matcher::{Matcher, MatcherResult}; +use crate::{ + description::Description, + matcher::{Matcher, MatcherResult}, +}; use std::{fmt::Debug, marker::PhantomData}; /// Matches a `Result` containing `Err` with a value matched by `inner`. @@ -56,24 +59,25 @@ impl<T: Debug, E: Debug, InnerMatcherT: Matcher<ActualT = E>> Matcher actual.as_ref().err().map(|v| self.inner.matches(v)).unwrap_or(MatcherResult::NoMatch) } - fn explain_match(&self, actual: &Self::ActualT) -> String { + fn explain_match(&self, actual: &Self::ActualT) -> Description { match actual { - Err(e) => format!("which is an error {}", self.inner.explain_match(e)), - Ok(_) => "which is a success".to_string(), + Err(e) => { + Description::new().text("which is an error").nested(self.inner.explain_match(e)) + } + Ok(_) => "which is a success".into(), } } - fn describe(&self, matcher_result: MatcherResult) -> String { + fn describe(&self, matcher_result: MatcherResult) -> Description { match matcher_result { MatcherResult::Match => { - format!("is an error which {}", self.inner.describe(MatcherResult::Match)) - } - MatcherResult::NoMatch => { - format!( - "is a success or is an error containing a value which {}", - self.inner.describe(MatcherResult::NoMatch) - ) + format!("is an error which {}", self.inner.describe(MatcherResult::Match)).into() } + MatcherResult::NoMatch => format!( + "is a success or is an error containing a value which {}", + self.inner.describe(MatcherResult::NoMatch) + ) + .into(), } } } @@ -126,7 +130,8 @@ mod tests { Value of: Err::<i32, i32>(1) Expected: is an error which is equal to 2 Actual: Err(1), - which is an error which isn't equal to 2 + which is an error + which isn't equal to 2 " )))) ) @@ -139,6 +144,9 @@ mod tests { phantom_t: Default::default(), phantom_e: Default::default(), }; - verify_that!(matcher.describe(MatcherResult::Match), eq("is an error which is equal to 1")) + verify_that!( + matcher.describe(MatcherResult::Match), + displays_as(eq("is an error which is equal to 1")) + ) } } diff --git a/src/matchers/field_matcher.rs b/src/matchers/field_matcher.rs index 0d1d4fe..fc37ce6 100644 --- a/src/matchers/field_matcher.rs +++ b/src/matchers/field_matcher.rs @@ -13,7 +13,7 @@ // limitations under the License. // There are no visible documentation elements in this module; the declarative -// macro is documented at the top level. +// macro is documented in the matchers module. #![doc(hidden)] /// Matches a structure or enum with a given field which is matched by a given @@ -105,10 +105,11 @@ /// # } /// ``` /// -/// See also the macro [`property`][crate::property] for an analogous mechanism -/// to extract a datum by invoking a method. +/// See also the macro [`property`][crate::matchers::property] for an analogous +/// mechanism to extract a datum by invoking a method. #[macro_export] -macro_rules! field { +#[doc(hidden)] +macro_rules! __field { ($($t:tt)*) => { $crate::field_internal!($($t)*) } } @@ -118,7 +119,7 @@ macro_rules! field { #[macro_export] macro_rules! field_internal { ($($t:ident)::+.$field:tt, $m:expr) => {{ - use $crate::matchers::field_matcher::internal::field_matcher; + use $crate::matchers::__internal_unstable_do_not_depend_on_these::field_matcher; field_matcher( |o| { match o { @@ -140,7 +141,10 @@ macro_rules! field_internal { /// **For internal use only. API stablility is not guaranteed!** #[doc(hidden)] pub mod internal { - use crate::matcher::{Matcher, MatcherResult}; + use crate::{ + description::Description, + matcher::{Matcher, MatcherResult}, + }; use std::fmt::Debug; /// Creates a matcher to verify a specific field of the actual struct using @@ -175,27 +179,29 @@ pub mod internal { } } - fn explain_match(&self, actual: &OuterT) -> String { + fn explain_match(&self, actual: &OuterT) -> Description { if let Some(actual) = (self.field_accessor)(actual) { format!( "which has field `{}`, {}", self.field_path, self.inner.explain_match(actual) ) + .into() } else { let formatted_actual_value = format!("{actual:?}"); let without_fields = formatted_actual_value.split('(').next().unwrap_or(""); let without_fields = without_fields.split('{').next().unwrap_or("").trim_end(); - format!("which has the wrong enum variant `{without_fields}`") + format!("which has the wrong enum variant `{without_fields}`").into() } } - fn describe(&self, matcher_result: MatcherResult) -> String { + fn describe(&self, matcher_result: MatcherResult) -> Description { format!( "has field `{}`, which {}", self.field_path, self.inner.describe(matcher_result) ) + .into() } } } diff --git a/src/matchers/ge_matcher.rs b/src/matchers/ge_matcher.rs index 33e847e..cb2a91f 100644 --- a/src/matchers/ge_matcher.rs +++ b/src/matchers/ge_matcher.rs @@ -12,7 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::matcher::{Matcher, MatcherResult}; +use crate::{ + description::Description, + matcher::{Matcher, MatcherResult}, +}; use std::{fmt::Debug, marker::PhantomData}; /// Matches a value greater than or equal to (in the sense of `>=`) `expected`. @@ -91,10 +94,12 @@ impl<ActualT: Debug + PartialOrd<ExpectedT>, ExpectedT: Debug> Matcher (*actual >= self.expected).into() } - fn describe(&self, matcher_result: MatcherResult) -> String { + fn describe(&self, matcher_result: MatcherResult) -> Description { match matcher_result { - MatcherResult::Match => format!("is greater than or equal to {:?}", self.expected), - MatcherResult::NoMatch => format!("is less than {:?}", self.expected), + MatcherResult::Match => { + format!("is greater than or equal to {:?}", self.expected).into() + } + MatcherResult::NoMatch => format!("is less than {:?}", self.expected).into(), } } } diff --git a/src/matchers/gt_matcher.rs b/src/matchers/gt_matcher.rs index 699bf2a..b52c6e3 100644 --- a/src/matchers/gt_matcher.rs +++ b/src/matchers/gt_matcher.rs @@ -12,7 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::matcher::{Matcher, MatcherResult}; +use crate::{ + description::Description, + matcher::{Matcher, MatcherResult}, +}; use std::{fmt::Debug, marker::PhantomData}; /// Matches a value greater (in the sense of `>`) than `expected`. @@ -91,10 +94,12 @@ impl<ActualT: Debug + PartialOrd<ExpectedT>, ExpectedT: Debug> Matcher (*actual > self.expected).into() } - fn describe(&self, matcher_result: MatcherResult) -> String { + fn describe(&self, matcher_result: MatcherResult) -> Description { match matcher_result { - MatcherResult::Match => format!("is greater than {:?}", self.expected), - MatcherResult::NoMatch => format!("is less than or equal to {:?}", self.expected), + MatcherResult::Match => format!("is greater than {:?}", self.expected).into(), + MatcherResult::NoMatch => { + format!("is less than or equal to {:?}", self.expected).into() + } } } } @@ -175,14 +180,17 @@ mod tests { #[test] fn gt_describe_matches() -> Result<()> { - verify_that!(gt::<i32, i32>(232).describe(MatcherResult::Match), eq("is greater than 232")) + verify_that!( + gt::<i32, i32>(232).describe(MatcherResult::Match), + displays_as(eq("is greater than 232")) + ) } #[test] fn gt_describe_does_not_match() -> Result<()> { verify_that!( gt::<i32, i32>(232).describe(MatcherResult::NoMatch), - eq("is less than or equal to 232") + displays_as(eq("is less than or equal to 232")) ) } diff --git a/src/matchers/has_entry_matcher.rs b/src/matchers/has_entry_matcher.rs index 67a44cd..7f9d2ad 100644 --- a/src/matchers/has_entry_matcher.rs +++ b/src/matchers/has_entry_matcher.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::description::Description; use crate::matcher::{Matcher, MatcherResult}; use std::collections::HashMap; use std::fmt::Debug; @@ -87,7 +88,7 @@ impl<KeyT: Debug + Eq + Hash, ValueT: Debug, MatcherT: Matcher<ActualT = ValueT> } } - fn explain_match(&self, actual: &HashMap<KeyT, ValueT>) -> String { + fn explain_match(&self, actual: &HashMap<KeyT, ValueT>) -> Description { if let Some(value) = actual.get(&self.key) { format!( "which contains key {:?}, but is mapped to value {:#?}, {}", @@ -95,24 +96,27 @@ impl<KeyT: Debug + Eq + Hash, ValueT: Debug, MatcherT: Matcher<ActualT = ValueT> value, self.inner.explain_match(value) ) + .into() } else { - format!("which doesn't contain key {:?}", self.key) + format!("which doesn't contain key {:?}", self.key).into() } } - fn describe(&self, matcher_result: MatcherResult) -> String { + fn describe(&self, matcher_result: MatcherResult) -> Description { match matcher_result { MatcherResult::Match => format!( "contains key {:?}, which value {}", self.key, self.inner.describe(MatcherResult::Match) - ), + ) + .into(), MatcherResult::NoMatch => format!( "doesn't contain key {:?} or contains key {:?}, which value {}", self.key, self.key, self.inner.describe(MatcherResult::NoMatch) - ), + ) + .into(), } } } diff --git a/src/matchers/is_encoded_string_matcher.rs b/src/matchers/is_encoded_string_matcher.rs new file mode 100644 index 0000000..d2fb259 --- /dev/null +++ b/src/matchers/is_encoded_string_matcher.rs @@ -0,0 +1,180 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::{ + description::Description, + matcher::{Matcher, MatcherResult}, +}; +use std::{fmt::Debug, marker::PhantomData}; + +/// Matches a byte sequence which is a UTF-8 encoded string matched by `inner`. +/// +/// The matcher reports no match if either the string is not UTF-8 encoded or if +/// `inner` does not match on the decoded string. +/// +/// The input may be a slice `&[u8]` or a `Vec` of bytes. +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// let bytes: &[u8] = "A string".as_bytes(); +/// verify_that!(bytes, is_utf8_string(eq("A string")))?; // Passes +/// let bytes: Vec<u8> = "A string".as_bytes().to_vec(); +/// verify_that!(bytes, is_utf8_string(eq("A string")))?; // Passes +/// # Ok(()) +/// # } +/// # fn should_fail_1() -> Result<()> { +/// # let bytes: &[u8] = "A string".as_bytes(); +/// verify_that!(bytes, is_utf8_string(eq("Another string")))?; // Fails (inner matcher does not match) +/// # Ok(()) +/// # } +/// # fn should_fail_2() -> Result<()> { +/// let bytes: Vec<u8> = vec![255, 64, 128, 32]; +/// verify_that!(bytes, is_utf8_string(anything()))?; // Fails (not UTF-8 encoded) +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// # should_fail_1().unwrap_err(); +/// # should_fail_2().unwrap_err(); +/// ``` +pub fn is_utf8_string<'a, ActualT: AsRef<[u8]> + Debug + 'a, InnerMatcherT>( + inner: InnerMatcherT, +) -> impl Matcher<ActualT = ActualT> +where + InnerMatcherT: Matcher<ActualT = String>, +{ + IsEncodedStringMatcher { inner, phantom: Default::default() } +} + +struct IsEncodedStringMatcher<ActualT, InnerMatcherT> { + inner: InnerMatcherT, + phantom: PhantomData<ActualT>, +} + +impl<'a, ActualT: AsRef<[u8]> + Debug + 'a, InnerMatcherT> Matcher + for IsEncodedStringMatcher<ActualT, InnerMatcherT> +where + InnerMatcherT: Matcher<ActualT = String>, +{ + type ActualT = ActualT; + + fn matches(&self, actual: &Self::ActualT) -> MatcherResult { + String::from_utf8(actual.as_ref().to_vec()) + .map(|s| self.inner.matches(&s)) + .unwrap_or(MatcherResult::NoMatch) + } + + fn describe(&self, matcher_result: MatcherResult) -> Description { + match matcher_result { + MatcherResult::Match => format!( + "is a UTF-8 encoded string which {}", + self.inner.describe(MatcherResult::Match) + ) + .into(), + MatcherResult::NoMatch => format!( + "is not a UTF-8 encoded string which {}", + self.inner.describe(MatcherResult::Match) + ) + .into(), + } + } + + fn explain_match(&self, actual: &Self::ActualT) -> Description { + match String::from_utf8(actual.as_ref().to_vec()) { + Ok(s) => { + format!("which is a UTF-8 encoded string {}", self.inner.explain_match(&s)).into() + } + Err(e) => format!("which is not a UTF-8 encoded string: {e}").into(), + } + } +} + +#[cfg(test)] +mod tests { + use crate::matcher::MatcherResult; + use crate::prelude::*; + + #[test] + fn matches_string_as_byte_slice() -> Result<()> { + verify_that!("A string".as_bytes(), is_utf8_string(eq("A string"))) + } + + #[test] + fn matches_string_as_byte_vec() -> Result<()> { + verify_that!("A string".as_bytes().to_vec(), is_utf8_string(eq("A string"))) + } + + #[test] + fn matches_string_with_utf_8_encoded_sequences() -> Result<()> { + verify_that!("äöüÄÖÜ".as_bytes().to_vec(), is_utf8_string(eq("äöüÄÖÜ"))) + } + + #[test] + fn does_not_match_non_equal_string() -> Result<()> { + verify_that!("äöüÄÖÜ".as_bytes().to_vec(), not(is_utf8_string(eq("A string")))) + } + + #[test] + fn does_not_match_non_utf_8_encoded_byte_sequence() -> Result<()> { + verify_that!(&[192, 64, 255, 32], not(is_utf8_string(eq("A string")))) + } + + #[test] + fn has_correct_description_in_matched_case() -> Result<()> { + let matcher = is_utf8_string::<&[u8], _>(eq("A string")); + + verify_that!( + matcher.describe(MatcherResult::Match), + displays_as(eq("is a UTF-8 encoded string which is equal to \"A string\"")) + ) + } + + #[test] + fn has_correct_description_in_not_matched_case() -> Result<()> { + let matcher = is_utf8_string::<&[u8], _>(eq("A string")); + + verify_that!( + matcher.describe(MatcherResult::NoMatch), + displays_as(eq("is not a UTF-8 encoded string which is equal to \"A string\"")) + ) + } + + #[test] + fn has_correct_explanation_in_matched_case() -> Result<()> { + let explanation = is_utf8_string(eq("A string")).explain_match(&"A string".as_bytes()); + + verify_that!( + explanation, + displays_as(eq("which is a UTF-8 encoded string which is equal to \"A string\"")) + ) + } + + #[test] + fn has_correct_explanation_when_byte_array_is_not_utf8_encoded() -> Result<()> { + let explanation = is_utf8_string(eq("A string")).explain_match(&&[192, 128, 0, 64]); + + verify_that!(explanation, displays_as(starts_with("which is not a UTF-8 encoded string: "))) + } + + #[test] + fn has_correct_explanation_when_inner_matcher_does_not_match() -> Result<()> { + let explanation = + is_utf8_string(eq("A string")).explain_match(&"Another string".as_bytes()); + + verify_that!( + explanation, + displays_as(eq("which is a UTF-8 encoded string which isn't equal to \"A string\"")) + ) + } +} diff --git a/src/matchers/is_matcher.rs b/src/matchers/is_matcher.rs index 51fd3b2..336ce53 100644 --- a/src/matchers/is_matcher.rs +++ b/src/matchers/is_matcher.rs @@ -14,7 +14,10 @@ #![doc(hidden)] -use crate::matcher::{Matcher, MatcherResult}; +use crate::{ + description::Description, + matcher::{Matcher, MatcherResult}, +}; use std::{fmt::Debug, marker::PhantomData}; /// Matches precisely values matched by `inner`. @@ -44,22 +47,24 @@ impl<'a, ActualT: Debug, InnerMatcherT: Matcher<ActualT = ActualT>> Matcher self.inner.matches(actual) } - fn describe(&self, matcher_result: MatcherResult) -> String { + fn describe(&self, matcher_result: MatcherResult) -> Description { match matcher_result { MatcherResult::Match => format!( "is {} which {}", self.description, self.inner.describe(MatcherResult::Match) - ), + ) + .into(), MatcherResult::NoMatch => format!( "is not {} which {}", self.description, self.inner.describe(MatcherResult::Match) - ), + ) + .into(), } } - fn explain_match(&self, actual: &Self::ActualT) -> String { + fn explain_match(&self, actual: &Self::ActualT) -> Description { self.inner.explain_match(actual) } } diff --git a/src/matchers/is_nan_matcher.rs b/src/matchers/is_nan_matcher.rs index 3cbe694..0af4338 100644 --- a/src/matchers/is_nan_matcher.rs +++ b/src/matchers/is_nan_matcher.rs @@ -12,7 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::matcher::{Matcher, MatcherResult}; +use crate::{ + description::Description, + matcher::{Matcher, MatcherResult}, +}; use num_traits::float::Float; use std::{fmt::Debug, marker::PhantomData}; @@ -30,8 +33,8 @@ impl<T: Float + Debug> Matcher for IsNanMatcher<T> { actual.is_nan().into() } - fn describe(&self, matcher_result: MatcherResult) -> String { - if matcher_result.into() { "is NaN" } else { "isn't NaN" }.to_string() + fn describe(&self, matcher_result: MatcherResult) -> Description { + if matcher_result.into() { "is NaN" } else { "isn't NaN" }.into() } } diff --git a/src/matchers/le_matcher.rs b/src/matchers/le_matcher.rs index 6e7a16f..cfc5781 100644 --- a/src/matchers/le_matcher.rs +++ b/src/matchers/le_matcher.rs @@ -12,7 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::matcher::{Matcher, MatcherResult}; +use crate::{ + description::Description, + matcher::{Matcher, MatcherResult}, +}; use std::{fmt::Debug, marker::PhantomData}; /// Matches a value less than or equal to (in the sense of `<=`) `expected`. @@ -91,10 +94,10 @@ impl<ActualT: Debug + PartialOrd<ExpectedT>, ExpectedT: Debug> Matcher (*actual <= self.expected).into() } - fn describe(&self, matcher_result: MatcherResult) -> String { + fn describe(&self, matcher_result: MatcherResult) -> Description { match matcher_result { - MatcherResult::Match => format!("is less than or equal to {:?}", self.expected), - MatcherResult::NoMatch => format!("is greater than {:?}", self.expected), + MatcherResult::Match => format!("is less than or equal to {:?}", self.expected).into(), + MatcherResult::NoMatch => format!("is greater than {:?}", self.expected).into(), } } } diff --git a/src/matchers/len_matcher.rs b/src/matchers/len_matcher.rs index 3a31873..be903c9 100644 --- a/src/matchers/len_matcher.rs +++ b/src/matchers/len_matcher.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::description::Description; use crate::matcher::{Matcher, MatcherResult}; use crate::matcher_support::count_elements::count_elements; use std::{fmt::Debug, marker::PhantomData}; @@ -19,7 +20,9 @@ use std::{fmt::Debug, marker::PhantomData}; /// Matches a container whose number of elements matches `expected`. /// /// This matches against a container over which one can iterate. This includes -/// the standard Rust containers, arrays, and (when dereferenced) slices. +/// the standard Rust containers, arrays, and (when dereferenced) slices. More +/// precisely, a shared borrow of the actual type must implement +/// [`IntoIterator`]. /// /// ``` /// # use googletest::prelude::*; @@ -68,26 +71,29 @@ where self.expected.matches(&count_elements(actual)) } - fn describe(&self, matcher_result: MatcherResult) -> String { + fn describe(&self, matcher_result: MatcherResult) -> Description { match matcher_result { MatcherResult::Match => { - format!("has length, which {}", self.expected.describe(MatcherResult::Match)) + format!("has length, which {}", self.expected.describe(MatcherResult::Match)).into() } MatcherResult::NoMatch => { format!("has length, which {}", self.expected.describe(MatcherResult::NoMatch)) + .into() } } } - fn explain_match(&self, actual: &T) -> String { + fn explain_match(&self, actual: &T) -> Description { let actual_size = count_elements(actual); format!("which has length {}, {}", actual_size, self.expected.explain_match(&actual_size)) + .into() } } #[cfg(test)] mod tests { use super::len; + use crate::description::Description; use crate::matcher::{Matcher, MatcherResult}; use crate::prelude::*; use indoc::indoc; @@ -180,11 +186,11 @@ mod tests { false.into() } - fn describe(&self, _: MatcherResult) -> String { + fn describe(&self, _: MatcherResult) -> Description { "called described".into() } - fn explain_match(&self, _: &T) -> String { + fn explain_match(&self, _: &T) -> Description { "called explain_match".into() } } diff --git a/src/matchers/lt_matcher.rs b/src/matchers/lt_matcher.rs index 96df00c..08bc563 100644 --- a/src/matchers/lt_matcher.rs +++ b/src/matchers/lt_matcher.rs @@ -12,7 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::matcher::{Matcher, MatcherResult}; +use crate::{ + description::Description, + matcher::{Matcher, MatcherResult}, +}; use std::{fmt::Debug, marker::PhantomData}; /// Matches a value less (in the sense of `<`) than `expected`. @@ -91,11 +94,11 @@ impl<ActualT: Debug + PartialOrd<ExpectedT>, ExpectedT: Debug> Matcher (*actual < self.expected).into() } - fn describe(&self, matcher_result: MatcherResult) -> String { + fn describe(&self, matcher_result: MatcherResult) -> Description { match matcher_result { - MatcherResult::Match => format!("is less than {:?}", self.expected), + MatcherResult::Match => format!("is less than {:?}", self.expected).into(), MatcherResult::NoMatch => { - format!("is greater than or equal to {:?}", self.expected) + format!("is greater than or equal to {:?}", self.expected).into() } } } diff --git a/src/matchers/matches_pattern.rs b/src/matchers/matches_pattern.rs index 9c252e5..106a5d7 100644 --- a/src/matchers/matches_pattern.rs +++ b/src/matchers/matches_pattern.rs @@ -13,7 +13,7 @@ // limitations under the License. // There are no visible documentation elements in this module; the declarative -// macro is documented at the top level. +// macro is documented in the matchers module. #![doc(hidden)] /// Matches a value according to a pattern of matchers. @@ -91,7 +91,8 @@ /// # .unwrap(); /// ``` /// -/// One can use the alias [`pat`][crate::pat] to make this less verbose: +/// One can use the alias [`pat`][crate::matchers::pat] to make this less +/// verbose: /// /// ``` /// # use googletest::prelude::*; @@ -162,7 +163,7 @@ /// # .unwrap(); /// ``` /// -/// If the method returns a reference, precede it with the keyword `ref`: +/// If the method returns a reference, precede it with a `*`: /// /// ``` /// # use googletest::prelude::*; @@ -177,7 +178,7 @@ /// /// # let my_struct = MyStruct { a_field: "Something to believe in".into() }; /// verify_that!(my_struct, matches_pattern!(MyStruct { -/// ref get_a_field_ref(): starts_with("Something"), +/// *get_a_field_ref(): starts_with("Something"), /// })) /// # .unwrap(); /// ``` @@ -246,7 +247,8 @@ /// # .unwrap(); /// ``` #[macro_export] -macro_rules! matches_pattern { +#[doc(hidden)] +macro_rules! __matches_pattern { ($($t:tt)*) => { $crate::matches_pattern_internal!($($t)*) } } @@ -259,7 +261,7 @@ macro_rules! matches_pattern_internal { [$($struct_name:tt)*], { $field_name:ident : $matcher:expr $(,)? } ) => { - $crate::matchers::is_matcher::is( + $crate::matchers::__internal_unstable_do_not_depend_on_these::is( stringify!($($struct_name)*), all!(field!($($struct_name)*.$field_name, $matcher)) ) @@ -269,7 +271,7 @@ macro_rules! matches_pattern_internal { [$($struct_name:tt)*], { $property_name:ident($($argument:expr),* $(,)?) : $matcher:expr $(,)? } ) => { - $crate::matchers::is_matcher::is( + $crate::matchers::__internal_unstable_do_not_depend_on_these::is( stringify!($($struct_name)*), all!(property!($($struct_name)*.$property_name($($argument),*), $matcher)) ) @@ -277,11 +279,11 @@ macro_rules! matches_pattern_internal { ( [$($struct_name:tt)*], - { ref $property_name:ident($($argument:expr),* $(,)?) : $matcher:expr $(,)? } + { * $property_name:ident($($argument:expr),* $(,)?) : $matcher:expr $(,)? } ) => { - $crate::matchers::is_matcher::is( + $crate::matchers::__internal_unstable_do_not_depend_on_these::is( stringify!($($struct_name)*), - all!(property!(ref $($struct_name)*.$property_name($($argument),*), $matcher)) + all!(property!(* $($struct_name)*.$property_name($($argument),*), $matcher)) ) }; @@ -309,10 +311,10 @@ macro_rules! matches_pattern_internal { ( [$($struct_name:tt)*], - { ref $property_name:ident($($argument:expr),* $(,)?) : $matcher:expr, $($rest:tt)* } + { * $property_name:ident($($argument:expr),* $(,)?) : $matcher:expr, $($rest:tt)* } ) => { $crate::matches_pattern_internal!( - all!(property!(ref $($struct_name)*.$property_name($($argument),*), $matcher)), + all!(property!(* $($struct_name)*.$property_name($($argument),*), $matcher)), [$($struct_name)*], { $($rest)* } ) @@ -323,7 +325,7 @@ macro_rules! matches_pattern_internal { [$($struct_name:tt)*], { $field_name:ident : $matcher:expr $(,)? } ) => { - $crate::matchers::is_matcher::is(stringify!($($struct_name)*), all!( + $crate::matchers::__internal_unstable_do_not_depend_on_these::is(stringify!($($struct_name)*), all!( $($processed)*, field!($($struct_name)*.$field_name, $matcher) )) @@ -334,7 +336,7 @@ macro_rules! matches_pattern_internal { [$($struct_name:tt)*], { $property_name:ident($($argument:expr),* $(,)?) : $matcher:expr $(,)? } ) => { - $crate::matchers::is_matcher::is(stringify!($($struct_name)*), all!( + $crate::matchers::__internal_unstable_do_not_depend_on_these::is(stringify!($($struct_name)*), all!( $($processed)*, property!($($struct_name)*.$property_name($($argument),*), $matcher) )) @@ -343,11 +345,11 @@ macro_rules! matches_pattern_internal { ( all!($($processed:tt)*), [$($struct_name:tt)*], - { ref $property_name:ident($($argument:expr),* $(,)?) : $matcher:expr $(,)? } + { * $property_name:ident($($argument:expr),* $(,)?) : $matcher:expr $(,)? } ) => { - $crate::matchers::is_matcher::is(stringify!($($struct_name)*), all!( + $crate::matchers::__internal_unstable_do_not_depend_on_these::is(stringify!($($struct_name)*), all!( $($processed)*, - property!(ref $($struct_name)*.$property_name($($argument),*), $matcher) + property!(* $($struct_name)*.$property_name($($argument),*), $matcher) )) }; @@ -384,12 +386,12 @@ macro_rules! matches_pattern_internal { ( all!($($processed:tt)*), [$($struct_name:tt)*], - { ref $property_name:ident($($argument:expr),* $(,)?) : $matcher:expr, $($rest:tt)* } + { * $property_name:ident($($argument:expr),* $(,)?) : $matcher:expr, $($rest:tt)* } ) => { $crate::matches_pattern_internal!( all!( $($processed)*, - property!(ref $($struct_name)*.$property_name($($argument),*), $matcher) + property!(* $($struct_name)*.$property_name($($argument),*), $matcher) ), [$($struct_name)*], { $($rest)* } @@ -410,7 +412,7 @@ macro_rules! matches_pattern_internal { [$($struct_name:tt)*], ($matcher:expr $(,)?) ) => { - $crate::matchers::is_matcher::is( + $crate::matchers::__internal_unstable_do_not_depend_on_these::is( stringify!($($struct_name)*), all!(field!($($struct_name)*.0, $matcher)) ) @@ -436,7 +438,7 @@ macro_rules! matches_pattern_internal { $field:tt, ($matcher:expr $(,)?) ) => { - $crate::matchers::is_matcher::is(stringify!($($struct_name)*), all!( + $crate::matchers::__internal_unstable_do_not_depend_on_these::is(stringify!($($struct_name)*), all!( $($processed)*, field!($($struct_name)*.$field, $matcher) )) @@ -587,13 +589,14 @@ macro_rules! matches_pattern_internal { ($first:tt $($rest:tt)*) => {{ #[allow(unused)] - use $crate::{all, field, property}; + use $crate::matchers::{all, field, property}; $crate::matches_pattern_internal!([$first], $($rest)*) }}; } -/// An alias for [`matches_pattern`]. +/// An alias for [`matches_pattern`][crate::matchers::matches_pattern!]. #[macro_export] -macro_rules! pat { +#[doc(hidden)] +macro_rules! __pat { ($($t:tt)*) => { $crate::matches_pattern_internal!($($t)*) } } diff --git a/src/matchers/matches_regex_matcher.rs b/src/matchers/matches_regex_matcher.rs index d0001e2..32b053b 100644 --- a/src/matchers/matches_regex_matcher.rs +++ b/src/matchers/matches_regex_matcher.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::description::Description; use crate::matcher::{Matcher, MatcherResult}; use regex::Regex; use std::fmt::Debug; @@ -94,13 +95,13 @@ where self.regex.is_match(actual.as_ref()).into() } - fn describe(&self, matcher_result: MatcherResult) -> String { + fn describe(&self, matcher_result: MatcherResult) -> Description { match matcher_result { MatcherResult::Match => { - format!("matches the regular expression {:#?}", self.pattern.deref()) + format!("matches the regular expression {:#?}", self.pattern.deref()).into() } MatcherResult::NoMatch => { - format!("doesn't match the regular expression {:#?}", self.pattern.deref()) + format!("doesn't match the regular expression {:#?}", self.pattern.deref()).into() } } } @@ -204,7 +205,7 @@ mod tests { verify_that!( Matcher::describe(&matcher, MatcherResult::Match), - eq("matches the regular expression \"\\n\"") + displays_as(eq("matches the regular expression \"\\n\"")) ) } } diff --git a/src/matchers/mod.rs b/src/matchers/mod.rs index f8aef10..1e028b9 100644 --- a/src/matchers/mod.rs +++ b/src/matchers/mod.rs @@ -14,27 +14,28 @@ //! All built-in matchers of this crate are in submodules of this module. -pub mod all_matcher; -pub mod any_matcher; +mod all_matcher; +mod any_matcher; mod anything_matcher; mod char_count_matcher; -pub mod conjunction_matcher; +mod conjunction_matcher; mod container_eq_matcher; mod contains_matcher; mod contains_regex_matcher; -pub mod disjunction_matcher; +mod disjunction_matcher; mod display_matcher; mod each_matcher; -pub mod elements_are_matcher; +mod elements_are_matcher; mod empty_matcher; mod eq_deref_of_matcher; mod eq_matcher; mod err_matcher; -pub mod field_matcher; +mod field_matcher; mod ge_matcher; mod gt_matcher; mod has_entry_matcher; -pub mod is_matcher; +mod is_encoded_string_matcher; +mod is_matcher; mod is_nan_matcher; mod le_matcher; mod len_matcher; @@ -46,15 +47,15 @@ mod none_matcher; mod not_matcher; mod ok_matcher; mod points_to_matcher; -pub mod pointwise_matcher; +mod pointwise_matcher; mod predicate_matcher; -pub mod property_matcher; +mod property_matcher; mod some_matcher; mod str_matcher; mod subset_of_matcher; mod superset_of_matcher; mod tuple_matcher; -pub mod unordered_elements_are_matcher; +mod unordered_elements_are_matcher; pub use anything_matcher::anything; pub use char_count_matcher::char_count; @@ -70,6 +71,7 @@ pub use err_matcher::err; pub use ge_matcher::ge; pub use gt_matcher::gt; pub use has_entry_matcher::has_entry; +pub use is_encoded_string_matcher::is_utf8_string; pub use is_nan_matcher::is_nan; pub use le_matcher::le; pub use len_matcher::len; @@ -87,3 +89,32 @@ pub use str_matcher::{ }; pub use subset_of_matcher::subset_of; pub use superset_of_matcher::superset_of; + +// Reexport and unmangle the macros. +#[doc(inline)] +pub use crate::{ + __all as all, __any as any, __contains_each as contains_each, __elements_are as elements_are, + __field as field, __is_contained_in as is_contained_in, __matches_pattern as matches_pattern, + __pat as pat, __pointwise as pointwise, __property as property, + __unordered_elements_are as unordered_elements_are, +}; + +// Types and functions used by macros matchers. +// Do not use directly. +// We may perform incompatible changes without major release. These elements +// should only be used through their respective macros. +#[doc(hidden)] +pub mod __internal_unstable_do_not_depend_on_these { + pub use super::all_matcher::internal::AllMatcher; + pub use super::any_matcher::internal::AnyMatcher; + pub use super::conjunction_matcher::ConjunctionMatcher; + pub use super::disjunction_matcher::DisjunctionMatcher; + pub use super::elements_are_matcher::internal::ElementsAre; + pub use super::field_matcher::internal::field_matcher; + pub use super::is_matcher::is; + pub use super::pointwise_matcher::internal::PointwiseMatcher; + pub use super::property_matcher::internal::{property_matcher, property_ref_matcher}; + pub use super::unordered_elements_are_matcher::internal::{ + Requirements, UnorderedElementsAreMatcher, UnorderedElementsOfMapAreMatcher, + }; +} diff --git a/src/matchers/near_matcher.rs b/src/matchers/near_matcher.rs index 484939c..ca7cbdf 100644 --- a/src/matchers/near_matcher.rs +++ b/src/matchers/near_matcher.rs @@ -12,7 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::matcher::{Matcher, MatcherResult}; +use crate::{ + description::Description, + matcher::{Matcher, MatcherResult}, +}; use num_traits::{Float, FloatConst}; use std::fmt::Debug; @@ -179,13 +182,13 @@ impl<T: Debug + Float> Matcher for NearMatcher<T> { } } - fn describe(&self, matcher_result: MatcherResult) -> String { + fn describe(&self, matcher_result: MatcherResult) -> Description { match matcher_result { MatcherResult::Match => { - format!("is within {:?} of {:?}", self.max_abs_error, self.expected) + format!("is within {:?} of {:?}", self.max_abs_error, self.expected).into() } MatcherResult::NoMatch => { - format!("isn't within {:?} of {:?}", self.max_abs_error, self.expected) + format!("isn't within {:?} of {:?}", self.max_abs_error, self.expected).into() } } } diff --git a/src/matchers/none_matcher.rs b/src/matchers/none_matcher.rs index e48d549..af28932 100644 --- a/src/matchers/none_matcher.rs +++ b/src/matchers/none_matcher.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::description::Description; use crate::matcher::{Matcher, MatcherResult}; use std::fmt::Debug; use std::marker::PhantomData; @@ -46,10 +47,10 @@ impl<T: Debug> Matcher for NoneMatcher<T> { (actual.is_none()).into() } - fn describe(&self, matcher_result: MatcherResult) -> String { + fn describe(&self, matcher_result: MatcherResult) -> Description { match matcher_result { - MatcherResult::Match => "is none".to_string(), - MatcherResult::NoMatch => "is some(_)".to_string(), + MatcherResult::Match => "is none".into(), + MatcherResult::NoMatch => "is some(_)".into(), } } } diff --git a/src/matchers/not_matcher.rs b/src/matchers/not_matcher.rs index 1dff791..f03d4ce 100644 --- a/src/matchers/not_matcher.rs +++ b/src/matchers/not_matcher.rs @@ -12,7 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::matcher::{Matcher, MatcherResult}; +use crate::{ + description::Description, + matcher::{Matcher, MatcherResult}, +}; use std::{fmt::Debug, marker::PhantomData}; /// Matches the actual value exactly when the inner matcher does _not_ match. @@ -51,11 +54,11 @@ impl<T: Debug, InnerMatcherT: Matcher<ActualT = T>> Matcher for NotMatcher<T, In } } - fn explain_match(&self, actual: &T) -> String { + fn explain_match(&self, actual: &T) -> Description { self.inner.explain_match(actual) } - fn describe(&self, matcher_result: MatcherResult) -> String { + fn describe(&self, matcher_result: MatcherResult) -> Description { self.inner.describe(if matcher_result.into() { MatcherResult::NoMatch } else { diff --git a/src/matchers/ok_matcher.rs b/src/matchers/ok_matcher.rs index 5c6fa51..8425b93 100644 --- a/src/matchers/ok_matcher.rs +++ b/src/matchers/ok_matcher.rs @@ -12,7 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::matcher::{Matcher, MatcherResult}; +use crate::{ + description::Description, + matcher::{Matcher, MatcherResult}, +}; use std::{fmt::Debug, marker::PhantomData}; /// Matches a `Result` containing `Ok` with a value matched by `inner`. @@ -56,27 +59,27 @@ impl<T: Debug, E: Debug, InnerMatcherT: Matcher<ActualT = T>> Matcher actual.as_ref().map(|v| self.inner.matches(v)).unwrap_or(MatcherResult::NoMatch) } - fn explain_match(&self, actual: &Self::ActualT) -> String { + fn explain_match(&self, actual: &Self::ActualT) -> Description { match actual { - Ok(o) => format!("which is a success {}", self.inner.explain_match(o)), - Err(_) => "which is an error".to_string(), + Ok(o) => { + Description::new().text("which is a success").nested(self.inner.explain_match(o)) + } + Err(_) => "which is an error".into(), } } - fn describe(&self, matcher_result: MatcherResult) -> String { + fn describe(&self, matcher_result: MatcherResult) -> Description { match matcher_result { - MatcherResult::Match => { - format!( - "is a success containing a value, which {}", - self.inner.describe(MatcherResult::Match) - ) - } - MatcherResult::NoMatch => { - format!( - "is an error or a success containing a value, which {}", - self.inner.describe(MatcherResult::NoMatch) - ) - } + MatcherResult::Match => format!( + "is a success containing a value, which {}", + self.inner.describe(MatcherResult::Match) + ) + .into(), + MatcherResult::NoMatch => format!( + "is an error or a success containing a value, which {}", + self.inner.describe(MatcherResult::NoMatch) + ) + .into(), } } } @@ -129,7 +132,8 @@ mod tests { Value of: Ok::<i32, i32>(1) Expected: is a success containing a value, which is equal to 2 Actual: Ok(1), - which is a success which isn't equal to 2 + which is a success + which isn't equal to 2 " )))) ) @@ -144,7 +148,7 @@ mod tests { }; verify_that!( matcher.describe(MatcherResult::Match), - eq("is a success containing a value, which is equal to 1") + displays_as(eq("is a success containing a value, which is equal to 1")) ) } } diff --git a/src/matchers/points_to_matcher.rs b/src/matchers/points_to_matcher.rs index 08c7343..2c516d0 100644 --- a/src/matchers/points_to_matcher.rs +++ b/src/matchers/points_to_matcher.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::description::Description; use crate::matcher::{Matcher, MatcherResult}; use std::fmt::Debug; use std::marker::PhantomData; @@ -59,11 +60,11 @@ where self.expected.matches(actual.deref()) } - fn explain_match(&self, actual: &ActualT) -> String { + fn explain_match(&self, actual: &ActualT) -> Description { self.expected.explain_match(actual.deref()) } - fn describe(&self, matcher_result: MatcherResult) -> String { + fn describe(&self, matcher_result: MatcherResult) -> Description { self.expected.describe(matcher_result) } } diff --git a/src/matchers/pointwise_matcher.rs b/src/matchers/pointwise_matcher.rs index 5ee0f22..01e70c0 100644 --- a/src/matchers/pointwise_matcher.rs +++ b/src/matchers/pointwise_matcher.rs @@ -13,7 +13,7 @@ // limitations under the License. // There are no visible documentation elements in this module; the declarative -// macro is documented at the top level. +// macro is documented in the matcher module. #![doc(hidden)] /// Generates a matcher which matches a container each of whose elements match @@ -79,8 +79,9 @@ /// that all of the containers have the same size. This matcher does not check /// whether the sizes match. /// -/// The actual value must be a container implementing [`IntoIterator`]. This -/// includes standard containers, slices (when dereferenced) and arrays. +/// The actual value must be a container such as a `Vec`, an array, or a +/// dereferenced slice. More precisely, a shared borrow of the actual value must +/// implement [`IntoIterator`]. /// /// ``` /// # use googletest::prelude::*; @@ -115,14 +116,15 @@ /// [`Pointwise`]: https://google.github.io/googletest/reference/matchers.html#container-matchers /// [`Vec`]: std::vec::Vec #[macro_export] -macro_rules! pointwise { +#[doc(hidden)] +macro_rules! __pointwise { ($matcher:expr, $container:expr) => {{ - use $crate::matchers::pointwise_matcher::internal::PointwiseMatcher; + use $crate::matchers::__internal_unstable_do_not_depend_on_these::PointwiseMatcher; PointwiseMatcher::new($container.into_iter().map($matcher).collect()) }}; ($matcher:expr, $left_container:expr, $right_container:expr) => {{ - use $crate::matchers::pointwise_matcher::internal::PointwiseMatcher; + use $crate::matchers::__internal_unstable_do_not_depend_on_these::PointwiseMatcher; PointwiseMatcher::new( $left_container .into_iter() @@ -133,7 +135,7 @@ macro_rules! pointwise { }}; ($matcher:expr, $left_container:expr, $middle_container:expr, $right_container:expr) => {{ - use $crate::matchers::pointwise_matcher::internal::PointwiseMatcher; + use $crate::matchers::__internal_unstable_do_not_depend_on_these::PointwiseMatcher; PointwiseMatcher::new( $left_container .into_iter() @@ -149,8 +151,8 @@ macro_rules! pointwise { /// **For internal use only. API stablility is not guaranteed!** #[doc(hidden)] pub mod internal { + use crate::description::Description; use crate::matcher::{Matcher, MatcherResult}; - use crate::matcher_support::description::Description; use crate::matcher_support::zipped_iterator::zip; use std::{fmt::Debug, marker::PhantomData}; @@ -190,7 +192,7 @@ pub mod internal { } } - fn explain_match(&self, actual: &ContainerT) -> String { + fn explain_match(&self, actual: &ContainerT) -> Description { // TODO(b/260819741) This code duplicates elements_are_matcher.rs. Consider // extract as a separate library. (or implement pointwise! with // elements_are) @@ -204,23 +206,24 @@ pub mod internal { } if mismatches.is_empty() { if !zipped_iterator.has_size_mismatch() { - "which matches all elements".to_string() + "which matches all elements".into() } else { format!( "which has size {} (expected {})", zipped_iterator.left_size(), self.matchers.len() ) + .into() } } else if mismatches.len() == 1 { - format!("where {}", mismatches[0]) + format!("where {}", mismatches[0]).into() } else { let mismatches = mismatches.into_iter().collect::<Description>(); - format!("where:\n{}", mismatches.bullet_list().indent()) + format!("where:\n{}", mismatches.bullet_list().indent()).into() } } - fn describe(&self, matcher_result: MatcherResult) -> String { + fn describe(&self, matcher_result: MatcherResult) -> Description { format!( "{} elements satisfying respectively:\n{}", if matcher_result.into() { "has" } else { "doesn't have" }, @@ -231,6 +234,7 @@ pub mod internal { .enumerate() .indent() ) + .into() } } } diff --git a/src/matchers/predicate_matcher.rs b/src/matchers/predicate_matcher.rs index fabd6c3..5bc067d 100644 --- a/src/matchers/predicate_matcher.rs +++ b/src/matchers/predicate_matcher.rs @@ -12,7 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::matcher::{Matcher, MatcherResult}; +use crate::{ + description::Description, + matcher::{Matcher, MatcherResult}, +}; use std::{fmt::Debug, marker::PhantomData}; /// Creates a matcher based on the predicate provided. @@ -92,18 +95,18 @@ pub struct PredicateMatcher<T: ?Sized, P, D1, D2> { /// /// See [`PredicateMatcher::with_description`] pub trait PredicateDescription { - fn to_description(&self) -> String; + fn to_description(&self) -> Description; } impl PredicateDescription for &str { - fn to_description(&self) -> String { - self.to_string() + fn to_description(&self) -> Description { + self.to_string().into() } } impl PredicateDescription for String { - fn to_description(&self) -> String { - self.clone() + fn to_description(&self) -> Description { + self.to_string().into() } } @@ -112,8 +115,8 @@ where T: Fn() -> S, S: Into<String>, { - fn to_description(&self) -> String { - self().into() + fn to_description(&self) -> Description { + self().into().into() } } @@ -131,10 +134,10 @@ where (self.predicate)(actual).into() } - fn describe(&self, result: MatcherResult) -> String { + fn describe(&self, result: MatcherResult) -> Description { match result { - MatcherResult::Match => "matches".to_string(), - MatcherResult::NoMatch => "does not match".to_string(), + MatcherResult::Match => "matches".into(), + MatcherResult::NoMatch => "does not match".into(), } } } @@ -150,7 +153,7 @@ where (self.predicate)(actual).into() } - fn describe(&self, result: MatcherResult) -> String { + fn describe(&self, result: MatcherResult) -> Description { match result { MatcherResult::Match => self.positive_description.to_description(), MatcherResult::NoMatch => self.negative_description.to_description(), diff --git a/src/matchers/property_matcher.rs b/src/matchers/property_matcher.rs index d69ba1d..19b4862 100644 --- a/src/matchers/property_matcher.rs +++ b/src/matchers/property_matcher.rs @@ -13,7 +13,7 @@ // limitations under the License. // There are no visible documentation elements in this module; the declarative -// macro is documented at the top level. +// macro is documented in the matcher module. #![doc(hidden)] /// Matches an object which, upon calling the given method on it with the given @@ -44,8 +44,7 @@ /// failure, it will be invoked a second time, with the assertion failure output /// reflecting the *second* invocation. /// -/// If the method returns a *reference*, then it must be preceded by the keyword -/// `ref`: +/// If the method returns a *reference*, then it must be preceded by a `*`: /// /// ``` /// # use googletest::prelude::*; @@ -58,7 +57,7 @@ /// } /// /// # let value = vec![MyStruct { a_field: 100 }]; -/// verify_that!(value, contains(property!(ref MyStruct.get_a_field(), eq(100)))) +/// verify_that!(value, contains(property!(*MyStruct.get_a_field(), eq(100)))) /// # .unwrap(); /// ``` /// @@ -93,17 +92,18 @@ /// } /// /// let value = MyStruct { a_string: "A string".into() }; -/// verify_that!(value, property!(ref MyStruct.get_a_string(), eq("A string"))) // Does not compile +/// verify_that!(value, property!(*MyStruct.get_a_string(), eq("A string"))) // Does not compile /// # .unwrap(); /// ``` /// -/// This macro is analogous to [`field`][crate::field], except that it extracts -/// the datum to be matched from the given object by invoking a method rather -/// than accessing a field. +/// This macro is analogous to [`field`][crate::matchers::field], except that it +/// extracts the datum to be matched from the given object by invoking a method +/// rather than accessing a field. /// /// The list of arguments may optionally have a trailing comma. #[macro_export] -macro_rules! property { +#[doc(hidden)] +macro_rules! __property { ($($t:tt)*) => { $crate::property_internal!($($t)*) } } @@ -113,15 +113,15 @@ macro_rules! property { #[macro_export] macro_rules! property_internal { ($($t:ident)::+.$method:tt($($argument:tt),* $(,)?), $m:expr) => {{ - use $crate::matchers::property_matcher::internal::property_matcher; + use $crate::matchers::__internal_unstable_do_not_depend_on_these::property_matcher; property_matcher( |o: &$($t)::+| o.$method($($argument),*), &stringify!($method($($argument),*)), $m) }}; - (ref $($t:ident)::+.$method:tt($($argument:tt),* $(,)?), $m:expr) => {{ - use $crate::matchers::property_matcher::internal::property_ref_matcher; + (* $($t:ident)::+.$method:tt($($argument:tt),* $(,)?), $m:expr) => {{ + use $crate::matchers::__internal_unstable_do_not_depend_on_these::property_ref_matcher; property_ref_matcher( |o: &$($t)::+| o.$method($($argument),*), &stringify!($method($($argument),*)), @@ -134,7 +134,10 @@ macro_rules! property_internal { /// **For internal use only. API stablility is not guaranteed!** #[doc(hidden)] pub mod internal { - use crate::matcher::{Matcher, MatcherResult}; + use crate::{ + description::Description, + matcher::{Matcher, MatcherResult}, + }; use std::{fmt::Debug, marker::PhantomData}; /// **For internal use only. API stablility is not guaranteed!** @@ -167,15 +170,16 @@ pub mod internal { self.inner.matches(&(self.extractor)(actual)) } - fn describe(&self, matcher_result: MatcherResult) -> String { + fn describe(&self, matcher_result: MatcherResult) -> Description { format!( "has property `{}`, which {}", self.property_desc, self.inner.describe(matcher_result) ) + .into() } - fn explain_match(&self, actual: &OuterT) -> String { + fn explain_match(&self, actual: &OuterT) -> Description { let actual_inner = (self.extractor)(actual); format!( "whose property `{}` is `{:#?}`, {}", @@ -183,6 +187,7 @@ pub mod internal { actual_inner, self.inner.explain_match(&actual_inner) ) + .into() } } @@ -216,15 +221,16 @@ pub mod internal { self.inner.matches((self.extractor)(actual)) } - fn describe(&self, matcher_result: MatcherResult) -> String { + fn describe(&self, matcher_result: MatcherResult) -> Description { format!( "has property `{}`, which {}", self.property_desc, self.inner.describe(matcher_result) ) + .into() } - fn explain_match(&self, actual: &OuterT) -> String { + fn explain_match(&self, actual: &OuterT) -> Description { let actual_inner = (self.extractor)(actual); format!( "whose property `{}` is `{:#?}`, {}", @@ -232,6 +238,7 @@ pub mod internal { actual_inner, self.inner.explain_match(actual_inner) ) + .into() } } } diff --git a/src/matchers/some_matcher.rs b/src/matchers/some_matcher.rs index a6ce021..905aa17 100644 --- a/src/matchers/some_matcher.rs +++ b/src/matchers/some_matcher.rs @@ -12,7 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::matcher::{Matcher, MatcherResult}; +use crate::{ + description::Description, + matcher::{Matcher, MatcherResult}, +}; use std::{fmt::Debug, marker::PhantomData}; /// Matches an `Option` containing a value matched by `inner`. @@ -51,24 +54,25 @@ impl<T: Debug, InnerMatcherT: Matcher<ActualT = T>> Matcher for SomeMatcher<T, I actual.as_ref().map(|v| self.inner.matches(v)).unwrap_or(MatcherResult::NoMatch) } - fn explain_match(&self, actual: &Option<T>) -> String { + fn explain_match(&self, actual: &Option<T>) -> Description { match (self.matches(actual), actual) { - (_, Some(t)) => format!("which has a value {}", self.inner.explain_match(t)), - (_, None) => "which is None".to_string(), + (_, Some(t)) => { + Description::new().text("which has a value").nested(self.inner.explain_match(t)) + } + (_, None) => "which is None".into(), } } - fn describe(&self, matcher_result: MatcherResult) -> String { + fn describe(&self, matcher_result: MatcherResult) -> Description { match matcher_result { MatcherResult::Match => { - format!("has a value which {}", self.inner.describe(MatcherResult::Match)) - } - MatcherResult::NoMatch => { - format!( - "is None or has a value which {}", - self.inner.describe(MatcherResult::NoMatch) - ) + format!("has a value which {}", self.inner.describe(MatcherResult::Match)).into() } + MatcherResult::NoMatch => format!( + "is None or has a value which {}", + self.inner.describe(MatcherResult::NoMatch) + ) + .into(), } } } @@ -100,7 +104,7 @@ mod tests { #[test] fn some_does_not_match_option_with_none() -> Result<()> { - let matcher = some(eq(1)); + let matcher = some(eq::<i32, _>(1)); let result = matcher.matches(&None); @@ -117,7 +121,8 @@ mod tests { Value of: Some(2) Expected: has a value which is equal to 1 Actual: Some(2), - which has a value which isn't equal to 1 + which has a value + which isn't equal to 1 " )))) ) @@ -126,29 +131,29 @@ mod tests { #[test] fn some_describe_matches() -> Result<()> { verify_that!( - some(eq(1)).describe(MatcherResult::Match), - eq("has a value which is equal to 1") + some(eq::<i32, _>(1)).describe(MatcherResult::Match), + displays_as(eq("has a value which is equal to 1")) ) } #[test] fn some_describe_does_not_match() -> Result<()> { verify_that!( - some(eq(1)).describe(MatcherResult::NoMatch), - eq("is None or has a value which isn't equal to 1") + some(eq::<i32, _>(1)).describe(MatcherResult::NoMatch), + displays_as(eq("is None or has a value which isn't equal to 1")) ) } #[test] fn some_explain_match_with_none() -> Result<()> { - verify_that!(some(eq(1)).explain_match(&None), displays_as(eq("which is None"))) + verify_that!(some(eq::<i32, _>(1)).explain_match(&None), displays_as(eq("which is None"))) } #[test] fn some_explain_match_with_some_success() -> Result<()> { verify_that!( some(eq(1)).explain_match(&Some(1)), - displays_as(eq("which has a value which is equal to 1")) + displays_as(eq("which has a value\n which is equal to 1")) ) } @@ -156,7 +161,7 @@ mod tests { fn some_explain_match_with_some_fail() -> Result<()> { verify_that!( some(eq(1)).explain_match(&Some(2)), - displays_as(eq("which has a value which isn't equal to 1")) + displays_as(eq("which has a value\n which isn't equal to 1")) ) } } diff --git a/src/matchers/str_matcher.rs b/src/matchers/str_matcher.rs index 3a4e2e9..b624d44 100644 --- a/src/matchers/str_matcher.rs +++ b/src/matchers/str_matcher.rs @@ -13,6 +13,7 @@ // limitations under the License. use crate::{ + description::Description, matcher::{Matcher, MatcherResult}, matcher_support::{ edit_distance, @@ -303,11 +304,11 @@ where self.configuration.do_strings_match(self.expected.deref(), actual.as_ref()).into() } - fn describe(&self, matcher_result: MatcherResult) -> String { + fn describe(&self, matcher_result: MatcherResult) -> Description { self.configuration.describe(matcher_result, self.expected.deref()) } - fn explain_match(&self, actual: &ActualT) -> String { + fn explain_match(&self, actual: &ActualT) -> Description { self.configuration.explain_match(self.expected.deref(), actual.as_ref()) } } @@ -467,7 +468,7 @@ impl Configuration { } // StrMatcher::describe redirects immediately to this function. - fn describe(&self, matcher_result: MatcherResult, expected: &str) -> String { + fn describe(&self, matcher_result: MatcherResult, expected: &str) -> Description { let mut addenda: Vec<Cow<'static, str>> = Vec::with_capacity(3); match (self.ignore_leading_whitespace, self.ignore_trailing_whitespace) { (true, true) => addenda.push("ignoring leading and trailing whitespace".into()), @@ -502,14 +503,15 @@ impl Configuration { MatcherResult::NoMatch => "does not end with", }, }; - format!("{match_mode_description} {expected:?}{extra}") + format!("{match_mode_description} {expected:?}{extra}").into() } - fn explain_match(&self, expected: &str, actual: &str) -> String { + fn explain_match(&self, expected: &str, actual: &str) -> Description { let default_explanation = format!( "which {}", self.describe(self.do_strings_match(expected, actual).into(), expected) - ); + ) + .into(); if !expected.contains('\n') || !actual.contains('\n') { return default_explanation; } @@ -549,7 +551,7 @@ impl Configuration { MatchMode::EndsWith => create_diff_reversed(actual, expected, self.mode.to_diff_mode()), }; - format!("{default_explanation}\n{diff}",) + format!("{default_explanation}\n{diff}").into() } fn ignoring_leading_whitespace(self) -> Self { @@ -811,7 +813,7 @@ mod tests { let matcher: StrMatcher<&str, _> = StrMatcher::with_default_config("A string"); verify_that!( Matcher::describe(&matcher, MatcherResult::Match), - eq("is equal to \"A string\"") + displays_as(eq("is equal to \"A string\"")) ) } @@ -820,7 +822,7 @@ mod tests { let matcher: StrMatcher<&str, _> = StrMatcher::with_default_config("A string"); verify_that!( Matcher::describe(&matcher, MatcherResult::NoMatch), - eq("isn't equal to \"A string\"") + displays_as(eq("isn't equal to \"A string\"")) ) } @@ -830,7 +832,7 @@ mod tests { StrMatcher::with_default_config("A string").ignoring_leading_whitespace(); verify_that!( Matcher::describe(&matcher, MatcherResult::Match), - eq("is equal to \"A string\" (ignoring leading whitespace)") + displays_as(eq("is equal to \"A string\" (ignoring leading whitespace)")) ) } @@ -840,7 +842,7 @@ mod tests { StrMatcher::with_default_config("A string").ignoring_leading_whitespace(); verify_that!( Matcher::describe(&matcher, MatcherResult::NoMatch), - eq("isn't equal to \"A string\" (ignoring leading whitespace)") + displays_as(eq("isn't equal to \"A string\" (ignoring leading whitespace)")) ) } @@ -850,7 +852,7 @@ mod tests { StrMatcher::with_default_config("A string").ignoring_trailing_whitespace(); verify_that!( Matcher::describe(&matcher, MatcherResult::Match), - eq("is equal to \"A string\" (ignoring trailing whitespace)") + displays_as(eq("is equal to \"A string\" (ignoring trailing whitespace)")) ) } @@ -861,7 +863,7 @@ mod tests { StrMatcher::with_default_config("A string").ignoring_outer_whitespace(); verify_that!( Matcher::describe(&matcher, MatcherResult::Match), - eq("is equal to \"A string\" (ignoring leading and trailing whitespace)") + displays_as(eq("is equal to \"A string\" (ignoring leading and trailing whitespace)")) ) } @@ -871,7 +873,7 @@ mod tests { StrMatcher::with_default_config("A string").ignoring_ascii_case(); verify_that!( Matcher::describe(&matcher, MatcherResult::Match), - eq("is equal to \"A string\" (ignoring ASCII case)") + displays_as(eq("is equal to \"A string\" (ignoring ASCII case)")) ) } @@ -883,7 +885,9 @@ mod tests { .ignoring_ascii_case(); verify_that!( Matcher::describe(&matcher, MatcherResult::Match), - eq("is equal to \"A string\" (ignoring leading whitespace, ignoring ASCII case)") + displays_as(eq( + "is equal to \"A string\" (ignoring leading whitespace, ignoring ASCII case)" + )) ) } @@ -892,7 +896,7 @@ mod tests { let matcher: StrMatcher<&str, _> = contains_substring("A string"); verify_that!( Matcher::describe(&matcher, MatcherResult::Match), - eq("contains a substring \"A string\"") + displays_as(eq("contains a substring \"A string\"")) ) } @@ -901,7 +905,7 @@ mod tests { let matcher: StrMatcher<&str, _> = contains_substring("A string"); verify_that!( Matcher::describe(&matcher, MatcherResult::NoMatch), - eq("does not contain a substring \"A string\"") + displays_as(eq("does not contain a substring \"A string\"")) ) } @@ -910,7 +914,7 @@ mod tests { let matcher: StrMatcher<&str, _> = contains_substring("A string").times(gt(2)); verify_that!( Matcher::describe(&matcher, MatcherResult::Match), - eq("contains a substring \"A string\" (count is greater than 2)") + displays_as(eq("contains a substring \"A string\" (count is greater than 2)")) ) } @@ -919,7 +923,7 @@ mod tests { let matcher: StrMatcher<&str, _> = starts_with("A string"); verify_that!( Matcher::describe(&matcher, MatcherResult::Match), - eq("starts with prefix \"A string\"") + displays_as(eq("starts with prefix \"A string\"")) ) } @@ -928,7 +932,7 @@ mod tests { let matcher: StrMatcher<&str, _> = starts_with("A string"); verify_that!( Matcher::describe(&matcher, MatcherResult::NoMatch), - eq("does not start with \"A string\"") + displays_as(eq("does not start with \"A string\"")) ) } @@ -937,7 +941,7 @@ mod tests { let matcher: StrMatcher<&str, _> = ends_with("A string"); verify_that!( Matcher::describe(&matcher, MatcherResult::Match), - eq("ends with suffix \"A string\"") + displays_as(eq("ends with suffix \"A string\"")) ) } @@ -946,7 +950,7 @@ mod tests { let matcher: StrMatcher<&str, _> = ends_with("A string"); verify_that!( Matcher::describe(&matcher, MatcherResult::NoMatch), - eq("does not end with \"A string\"") + displays_as(eq("does not end with \"A string\"")) ) } @@ -971,14 +975,13 @@ mod tests { verify_that!( result, - err(displays_as(contains_substring(indoc!( - " - First line - -Second line - +Second lines - Third line - " - )))) + err(displays_as(contains_substring( + "\ + First line + -Second line + +Second lines + Third line" + ))) ) } @@ -1004,15 +1007,14 @@ mod tests { verify_that!( result, - err(displays_as(contains_substring(indoc!( - " - First line - -Second line - +Second lines - Third line - <---- remaining lines omitted ----> + err(displays_as(contains_substring( " - )))) + First line + -Second line + +Second lines + Third line + <---- remaining lines omitted ---->" + ))) ) } @@ -1037,14 +1039,13 @@ mod tests { verify_that!( result, - err(displays_as(contains_substring(indoc!( - " - First line - -Second line - +Second lines - <---- remaining lines omitted ----> - " - )))) + err(displays_as(contains_substring( + "\ + First line + -Second line + +Second lines + <---- remaining lines omitted ---->" + ))) ) } @@ -1070,16 +1071,15 @@ mod tests { verify_that!( result, - err(displays_as(contains_substring(indoc!( - " - Difference(-actual / +expected): - <---- remaining lines omitted ----> - Second line - +Third lines - -Third line - Fourth line + err(displays_as(contains_substring( " - )))) + Difference(-actual / +expected): + <---- remaining lines omitted ----> + Second line + -Third line + +Third lines + Fourth line" + ))) ) } @@ -1107,16 +1107,16 @@ mod tests { verify_that!( result, - err(displays_as(contains_substring(indoc!( + err(displays_as(contains_substring( " - Difference(-actual / +expected): - <---- remaining lines omitted ----> - Second line - +Third lines - -Third line - Fourth line - <---- remaining lines omitted ---->" - )))) + Difference(-actual / +expected): + <---- remaining lines omitted ----> + Second line + -Third line + +Third lines + Fourth line + <---- remaining lines omitted ---->" + ))) ) } @@ -1144,20 +1144,19 @@ mod tests { verify_that!( result, - err(displays_as(contains_substring(indoc!( + err(displays_as(contains_substring( " - Difference(-actual / +expected): - <---- remaining lines omitted ----> - +line - -Second line - Third line - +Foorth line - -Fourth line - +Fifth - -Fifth line - <---- remaining lines omitted ----> - " - )))) + Difference(-actual / +expected): + <---- remaining lines omitted ----> + -Second line + +line + Third line + -Fourth line + +Foorth line + -Fifth line + +Fifth + <---- remaining lines omitted ---->" + ))) ) } @@ -1183,15 +1182,14 @@ mod tests { verify_that!( result, - err(displays_as(contains_substring(indoc!( - " - First line - -Second line - +Second lines - Third line - -Fourth line - " - )))) + err(displays_as(contains_substring( + "\ + First line + -Second line + +Second lines + Third line + -Fourth line" + ))) ) } diff --git a/src/matchers/subset_of_matcher.rs b/src/matchers/subset_of_matcher.rs index facee4f..24c00d8 100644 --- a/src/matchers/subset_of_matcher.rs +++ b/src/matchers/subset_of_matcher.rs @@ -12,7 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::matcher::{Matcher, MatcherResult}; +use crate::{ + description::Description, + matcher::{Matcher, MatcherResult}, +}; use std::{fmt::Debug, marker::PhantomData}; /// Matches a container all of whose items are in the given container @@ -22,7 +25,8 @@ use std::{fmt::Debug, marker::PhantomData}; /// comparison. /// /// `ActualT` and `ExpectedT` can each be any container a reference to which -/// implements `IntoIterator`. They need not be the same container type. +/// implements `IntoIterator`. For instance, `T` can be a common container like +/// `Vec` or arrays. They need not be the same container type. /// /// ``` /// # use googletest::prelude::*; @@ -30,6 +34,8 @@ use std::{fmt::Debug, marker::PhantomData}; /// # fn should_pass_1() -> Result<()> { /// let value = vec![1, 2, 3]; /// verify_that!(value, subset_of([1, 2, 3, 4]))?; // Passes +/// let array_value = [1, 2, 3]; +/// verify_that!(array_value, subset_of([1, 2, 3, 4]))?; // Passes /// # Ok(()) /// # } /// # fn should_fail() -> Result<()> { @@ -109,7 +115,7 @@ where MatcherResult::Match } - fn explain_match(&self, actual: &ActualT) -> String { + fn explain_match(&self, actual: &ActualT) -> Description { let unexpected_elements = actual .into_iter() .enumerate() @@ -118,16 +124,16 @@ where .collect::<Vec<_>>(); match unexpected_elements.len() { - 0 => "which no element is unexpected".to_string(), - 1 => format!("whose element {} is unexpected", &unexpected_elements[0]), - _ => format!("whose elements {} are unexpected", unexpected_elements.join(", ")), + 0 => "which no element is unexpected".into(), + 1 => format!("whose element {} is unexpected", &unexpected_elements[0]).into(), + _ => format!("whose elements {} are unexpected", unexpected_elements.join(", ")).into(), } } - fn describe(&self, matcher_result: MatcherResult) -> String { + fn describe(&self, matcher_result: MatcherResult) -> Description { match matcher_result { - MatcherResult::Match => format!("is a subset of {:#?}", self.superset), - MatcherResult::NoMatch => format!("isn't a subset of {:#?}", self.superset), + MatcherResult::Match => format!("is a subset of {:#?}", self.superset).into(), + MatcherResult::NoMatch => format!("isn't a subset of {:#?}", self.superset).into(), } } } diff --git a/src/matchers/superset_of_matcher.rs b/src/matchers/superset_of_matcher.rs index 8e98015..d1e9d72 100644 --- a/src/matchers/superset_of_matcher.rs +++ b/src/matchers/superset_of_matcher.rs @@ -12,7 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::matcher::{Matcher, MatcherResult}; +use crate::{ + description::Description, + matcher::{Matcher, MatcherResult}, +}; use std::{fmt::Debug, marker::PhantomData}; /// Matches a container containing all of the items in the given container @@ -22,7 +25,9 @@ use std::{fmt::Debug, marker::PhantomData}; /// comparison. /// /// `ActualT` and `ExpectedT` can each be any container a reference to which -/// implements `IntoIterator`. They need not be the same container type. +/// implements `IntoIterator`. For instance, `ActualT` and `ExpectedT` can be a +/// common container like `Vec` or arrays. They need not be the same container +/// type. /// /// ``` /// # use googletest::prelude::*; @@ -30,6 +35,8 @@ use std::{fmt::Debug, marker::PhantomData}; /// # fn should_pass_1() -> Result<()> { /// let value = vec![1, 2, 3]; /// verify_that!(value, superset_of([1, 2]))?; // Passes +/// let array_value = [1, 2, 3]; +/// verify_that!(array_value, superset_of([1, 2]))?; // Passes /// # Ok(()) /// # } /// # fn should_fail() -> Result<()> { @@ -109,7 +116,7 @@ where MatcherResult::Match } - fn explain_match(&self, actual: &ActualT) -> String { + fn explain_match(&self, actual: &ActualT) -> Description { let missing_items: Vec<_> = self .subset .into_iter() @@ -117,16 +124,16 @@ where .map(|expected_item| format!("{expected_item:#?}")) .collect(); match missing_items.len() { - 0 => "whose no element is missing".to_string(), - 1 => format!("whose element {} is missing", &missing_items[0]), - _ => format!("whose elements {} are missing", missing_items.join(", ")), + 0 => "whose no element is missing".into(), + 1 => format!("whose element {} is missing", &missing_items[0]).into(), + _ => format!("whose elements {} are missing", missing_items.join(", ")).into(), } } - fn describe(&self, matcher_result: MatcherResult) -> String { + fn describe(&self, matcher_result: MatcherResult) -> Description { match matcher_result { - MatcherResult::Match => format!("is a superset of {:#?}", self.subset), - MatcherResult::NoMatch => format!("isn't a superset of {:#?}", self.subset), + MatcherResult::Match => format!("is a superset of {:#?}", self.subset).into(), + MatcherResult::NoMatch => format!("isn't a superset of {:#?}", self.subset).into(), } } } diff --git a/src/matchers/tuple_matcher.rs b/src/matchers/tuple_matcher.rs index a2e325b..af55cbf 100644 --- a/src/matchers/tuple_matcher.rs +++ b/src/matchers/tuple_matcher.rs @@ -21,22 +21,11 @@ /// **For internal use only. API stablility is not guaranteed!** #[doc(hidden)] pub mod internal { - use crate::matcher::{Matcher, MatcherResult}; - use std::fmt::{Debug, Write}; - - /// Replaces the first expression with the second at compile time. - /// - /// This is used below in repetition sequences where the output must only - /// include the same expression repeated the same number of times as the - /// macro input. - /// - /// **For internal use only. API stablility is not guaranteed!** - #[doc(hidden)] - macro_rules! replace_expr { - ($_ignored:tt, $replacement:expr) => { - $replacement - }; - } + use crate::{ + description::Description, + matcher::{Matcher, MatcherResult}, + }; + use std::fmt::Debug; // This implementation is provided for completeness, but is completely trivial. // The only actual value which can be supplied is (), which must match. @@ -47,7 +36,7 @@ pub mod internal { MatcherResult::Match } - fn describe(&self, matcher_result: MatcherResult) -> String { + fn describe(&self, matcher_result: MatcherResult) -> Description { match matcher_result { MatcherResult::Match => "is the empty tuple".into(), MatcherResult::NoMatch => "is not the empty tuple".into(), @@ -76,41 +65,30 @@ pub mod internal { MatcherResult::Match } - fn explain_match(&self, actual: &($($field_type,)*)) -> String { - let mut explanation = format!("which {}", self.describe(self.matches(actual))); + fn explain_match(&self, actual: &($($field_type,)*)) -> Description { + let mut explanation = Description::new().text("which").nested(self.describe(self.matches(actual))); $(match self.$field_number.matches(&actual.$field_number) { MatcherResult::Match => {}, MatcherResult::NoMatch => { - writeln!( - &mut explanation, - concat!("Element #", $field_number, " is {:?}, {}"), - actual.$field_number, - self.$field_number.explain_match(&actual.$field_number) - ).unwrap(); + explanation = explanation + .text(format!(concat!("Element #", $field_number, " is {:?},"), actual.$field_number)) + .nested(self.$field_number.explain_match(&actual.$field_number)); } })* - (explanation) + explanation } - fn describe(&self, matcher_result: MatcherResult) -> String { + fn describe(&self, matcher_result: MatcherResult) -> Description { match matcher_result { MatcherResult::Match => { - format!( - concat!( - "is a tuple whose values respectively match:\n", - $(replace_expr!($field_number, " {},\n")),* - ), - $(self.$field_number.describe(matcher_result)),* - ) + let mut description = Description::new().text("is a tuple whose values respectively match:"); + $(description = description.nested(self.$field_number.describe(matcher_result));)* + description } MatcherResult::NoMatch => { - format!( - concat!( - "is a tuple whose values do not respectively match:\n", - $(replace_expr!($field_number, " {},\n")),* - ), - $(self.$field_number.describe(MatcherResult::Match)),* - ) + let mut description = Description::new().text("is a tuple whose values do not respectively match:"); + $(description = description.nested(self.$field_number.describe(MatcherResult::Match));)* + description } } } diff --git a/src/matchers/unordered_elements_are_matcher.rs b/src/matchers/unordered_elements_are_matcher.rs index 1930697..f4585a4 100644 --- a/src/matchers/unordered_elements_are_matcher.rs +++ b/src/matchers/unordered_elements_are_matcher.rs @@ -13,7 +13,7 @@ // limitations under the License. // There are no visible documentation elements in this module; the declarative -// macro is documented at the top level. +// macro is documented in the matchers module. #![doc(hidden)] /// Matches a container whose elements in any order have a 1:1 correspondence @@ -43,8 +43,9 @@ /// # should_fail_3().unwrap_err(); /// ``` /// -/// The actual value must be a container implementing [`IntoIterator`]. This -/// includes standard containers, slices (when dereferenced) and arrays. +/// The actual value must be a container such as a `Vec`, an array, or a +/// dereferenced slice. More precisely, a shared borrow of the actual value must +/// implement [`IntoIterator`]. /// /// This can also match against [`HashMap`][std::collections::HashMap] and /// similar collections. The arguments are a sequence of pairs of matchers @@ -73,7 +74,7 @@ /// /// Note: This behavior is only possible in [`verify_that!`] macros. In any /// other cases, it is still necessary to use the -/// [`unordered_elements_are!`][crate::unordered_elements_are] macro. +/// [`unordered_elements_are!`][crate::matchers::unordered_elements_are] macro. /// /// ```compile_fail /// # use googletest::prelude::*; @@ -115,9 +116,10 @@ /// [`Iterator::collect`]: std::iter::Iterator::collect /// [`Vec`]: std::vec::Vec #[macro_export] -macro_rules! unordered_elements_are { +#[doc(hidden)] +macro_rules! __unordered_elements_are { ($(,)?) => {{ - use $crate::matchers::unordered_elements_are_matcher::internal::{ + use $crate::matchers::__internal_unstable_do_not_depend_on_these::{ UnorderedElementsAreMatcher, Requirements }; UnorderedElementsAreMatcher::new([], Requirements::PerfectMatch) @@ -126,7 +128,7 @@ macro_rules! unordered_elements_are { // TODO: Consider an alternative map-like syntax here similar to that used in // https://crates.io/crates/maplit. ($(($key_matcher:expr, $value_matcher:expr)),* $(,)?) => {{ - use $crate::matchers::unordered_elements_are_matcher::internal::{ + use $crate::matchers::__internal_unstable_do_not_depend_on_these::{ UnorderedElementsOfMapAreMatcher, Requirements }; UnorderedElementsOfMapAreMatcher::new( @@ -136,7 +138,7 @@ macro_rules! unordered_elements_are { }}; ($($matcher:expr),* $(,)?) => {{ - use $crate::matchers::unordered_elements_are_matcher::internal::{ + use $crate::matchers::__internal_unstable_do_not_depend_on_these::{ UnorderedElementsAreMatcher, Requirements }; UnorderedElementsAreMatcher::new([$(Box::new($matcher)),*], Requirements::PerfectMatch) @@ -151,7 +153,9 @@ macro_rules! unordered_elements_are { /// additional elements that don't correspond to any matcher. /// /// Put another way, `contains_each!` matches if there is a subset of the actual -/// container which [`unordered_elements_are`] would match. +/// container which +/// [`unordered_elements_are`][crate::matchers::unordered_elements_are] would +/// match. /// /// ``` /// # use googletest::prelude::*; @@ -178,8 +182,9 @@ macro_rules! unordered_elements_are { /// # should_fail_3().unwrap_err(); /// ``` /// -/// The actual value must be a container implementing [`IntoIterator`]. This -/// includes standard containers, slices (when dereferenced) and arrays. +/// The actual value must be a container such as a `Vec`, an array, or a +/// dereferenced slice. More precisely, a shared borrow of the actual value must +/// implement [`IntoIterator`]. /// /// This can also match against [`HashMap`][std::collections::HashMap] and /// similar collections. The arguments are a sequence of pairs of matchers @@ -217,9 +222,10 @@ macro_rules! unordered_elements_are { /// [`Iterator::collect`]: std::iter::Iterator::collect /// [`Vec`]: std::vec::Vec #[macro_export] -macro_rules! contains_each { +#[doc(hidden)] +macro_rules! __contains_each { ($(,)?) => {{ - use $crate::matchers::unordered_elements_are_matcher::internal::{ + use $crate::matchers::__internal_unstable_do_not_depend_on_these::{ UnorderedElementsAreMatcher, Requirements }; UnorderedElementsAreMatcher::new([], Requirements::Superset) @@ -228,7 +234,7 @@ macro_rules! contains_each { // TODO: Consider an alternative map-like syntax here similar to that used in // https://crates.io/crates/maplit. ($(($key_matcher:expr, $value_matcher:expr)),* $(,)?) => {{ - use $crate::matchers::unordered_elements_are_matcher::internal::{ + use $crate::matchers::__internal_unstable_do_not_depend_on_these::{ UnorderedElementsOfMapAreMatcher, Requirements }; UnorderedElementsOfMapAreMatcher::new( @@ -238,7 +244,7 @@ macro_rules! contains_each { }}; ($($matcher:expr),* $(,)?) => {{ - use $crate::matchers::unordered_elements_are_matcher::internal::{ + use $crate::matchers::__internal_unstable_do_not_depend_on_these::{ UnorderedElementsAreMatcher, Requirements }; UnorderedElementsAreMatcher::new([$(Box::new($matcher)),*], Requirements::Superset) @@ -255,7 +261,8 @@ macro_rules! contains_each { /// container. /// /// Put another way, `is_contained_in!` matches if there is a subset of the -/// matchers which would match with [`unordered_elements_are`]. +/// matchers which would match with +/// [`unordered_elements_are`][crate::matchers::unordered_elements_are]. /// /// ``` /// # use googletest::prelude::*; @@ -282,8 +289,9 @@ macro_rules! contains_each { /// # should_fail_3().unwrap_err(); /// ``` /// -/// The actual value must be a container implementing [`IntoIterator`]. This -/// includes standard containers, slices (when dereferenced) and arrays. +/// The actual value must be a container such as a `Vec`, an array, or a +/// dereferenced slice. More precisely, a shared borrow of the actual value must +/// implement [`IntoIterator`]. /// /// This can also match against [`HashMap`][std::collections::HashMap] and /// similar collections. The arguments are a sequence of pairs of matchers @@ -323,9 +331,10 @@ macro_rules! contains_each { /// [`Iterator::collect`]: std::iter::Iterator::collect /// [`Vec`]: std::vec::Vec #[macro_export] -macro_rules! is_contained_in { +#[doc(hidden)] +macro_rules! __is_contained_in { ($(,)?) => {{ - use $crate::matchers::unordered_elements_are_matcher::internal::{ + use $crate::matchers::__internal_unstable_do_not_depend_on_these::{ UnorderedElementsAreMatcher, Requirements }; UnorderedElementsAreMatcher::new([], Requirements::Subset) @@ -334,7 +343,7 @@ macro_rules! is_contained_in { // TODO: Consider an alternative map-like syntax here similar to that used in // https://crates.io/crates/maplit. ($(($key_matcher:expr, $value_matcher:expr)),* $(,)?) => {{ - use $crate::matchers::unordered_elements_are_matcher::internal::{ + use $crate::matchers::__internal_unstable_do_not_depend_on_these::{ UnorderedElementsOfMapAreMatcher, Requirements }; UnorderedElementsOfMapAreMatcher::new( @@ -344,7 +353,7 @@ macro_rules! is_contained_in { }}; ($($matcher:expr),* $(,)?) => {{ - use $crate::matchers::unordered_elements_are_matcher::internal::{ + use $crate::matchers::__internal_unstable_do_not_depend_on_these::{ UnorderedElementsAreMatcher, Requirements }; UnorderedElementsAreMatcher::new([$(Box::new($matcher)),*], Requirements::Subset) @@ -356,9 +365,9 @@ macro_rules! is_contained_in { /// **For internal use only. API stablility is not guaranteed!** #[doc(hidden)] pub mod internal { + use crate::description::Description; use crate::matcher::{Matcher, MatcherResult}; use crate::matcher_support::count_elements::count_elements; - use crate::matcher_support::description::Description; use std::collections::HashSet; use std::fmt::{Debug, Display}; use std::marker::PhantomData; @@ -406,7 +415,7 @@ pub mod internal { match_matrix.is_match_for(self.requirements).into() } - fn explain_match(&self, actual: &ContainerT) -> String { + fn explain_match(&self, actual: &ContainerT) -> Description { if let Some(size_mismatch_explanation) = self.requirements.explain_size_mismatch(actual, N) { @@ -423,10 +432,10 @@ pub mod internal { let best_match = match_matrix.find_best_match(); best_match .get_explanation(actual, &self.elements, self.requirements) - .unwrap_or("whose elements all match".to_string()) + .unwrap_or("whose elements all match".into()) } - fn describe(&self, matcher_result: MatcherResult) -> String { + fn describe(&self, matcher_result: MatcherResult) -> Description { format!( "{} elements matching in any order:\n{}", if matcher_result.into() { "contains" } else { "doesn't contain" }, @@ -437,6 +446,7 @@ pub mod internal { .enumerate() .indent() ) + .into() } } @@ -482,7 +492,7 @@ pub mod internal { match_matrix.is_match_for(self.requirements).into() } - fn explain_match(&self, actual: &ContainerT) -> String { + fn explain_match(&self, actual: &ContainerT) -> Description { if let Some(size_mismatch_explanation) = self.requirements.explain_size_mismatch(actual, N) { @@ -500,10 +510,10 @@ pub mod internal { best_match .get_explanation_for_map(actual, &self.elements, self.requirements) - .unwrap_or("whose elements all match".to_string()) + .unwrap_or("whose elements all match".into()) } - fn describe(&self, matcher_result: MatcherResult) -> String { + fn describe(&self, matcher_result: MatcherResult) -> Description { format!( "{} elements matching in any order:\n{}", if matcher_result.into() { "contains" } else { "doesn't contain" }, @@ -517,6 +527,7 @@ pub mod internal { .collect::<Description>() .indent() ) + .into() } } @@ -545,25 +556,25 @@ pub mod internal { &self, actual: &ContainerT, expected_size: usize, - ) -> Option<String> + ) -> Option<Description> where for<'b> &'b ContainerT: IntoIterator, { let actual_size = count_elements(actual); match self { - Requirements::PerfectMatch if actual_size != expected_size => { - Some(format!("which has size {} (expected {})", actual_size, expected_size)) - } + Requirements::PerfectMatch if actual_size != expected_size => Some( + format!("which has size {} (expected {})", actual_size, expected_size).into(), + ), - Requirements::Superset if actual_size < expected_size => Some(format!( - "which has size {} (expected at least {})", - actual_size, expected_size - )), + Requirements::Superset if actual_size < expected_size => Some( + format!("which has size {} (expected at least {})", actual_size, expected_size) + .into(), + ), - Requirements::Subset if actual_size > expected_size => Some(format!( - "which has size {} (expected at most {})", - actual_size, expected_size - )), + Requirements::Subset if actual_size > expected_size => Some( + format!("which has size {} (expected at most {})", actual_size, expected_size) + .into(), + ), _ => None, } @@ -641,7 +652,7 @@ pub mod internal { } } - fn explain_unmatchable(&self, requirements: Requirements) -> Option<String> { + fn explain_unmatchable(&self, requirements: Requirements) -> Option<Description> { let unmatchable_elements = match requirements { Requirements::PerfectMatch => self.find_unmatchable_elements(), Requirements::Superset => self.find_unmatched_expected(), @@ -848,7 +859,7 @@ pub mod internal { || self.unmatchable_expected.iter().any(|b| *b) } - fn get_explanation(&self) -> Option<String> { + fn get_explanation(&self) -> Option<Description> { let unmatchable_actual = self.unmatchable_actual(); let actual_idx = unmatchable_actual .iter() @@ -864,29 +875,29 @@ pub mod internal { match (unmatchable_actual.len(), unmatchable_expected.len()) { (0, 0) => None, (1, 0) => { - Some(format!("whose element {actual_idx} does not match any expected elements")) + Some(format!("whose element {actual_idx} does not match any expected elements").into()) } (_, 0) => { - Some(format!("whose elements {actual_idx} do not match any expected elements",)) + Some(format!("whose elements {actual_idx} do not match any expected elements",).into()) } (0, 1) => Some(format!( "which has no element matching the expected element {expected_idx}" - )), + ).into()), (0, _) => Some(format!( "which has no elements matching the expected elements {expected_idx}" - )), + ).into()), (1, 1) => Some(format!( "whose element {actual_idx} does not match any expected elements and no elements match the expected element {expected_idx}" - )), + ).into()), (_, 1) => Some(format!( "whose elements {actual_idx} do not match any expected elements and no elements match the expected element {expected_idx}" - )), + ).into()), (1, _) => Some(format!( "whose element {actual_idx} does not match any expected elements and no elements match the expected elements {expected_idx}" - )), + ).into()), (_, _) => Some(format!( "whose elements {actual_idx} do not match any expected elements and no elements match the expected elements {expected_idx}" - )), + ).into()), } } @@ -953,7 +964,7 @@ pub mod internal { actual: &ContainerT, expected: &[Box<dyn Matcher<ActualT = T> + 'a>; N], requirements: Requirements, - ) -> Option<String> + ) -> Option<Description> where for<'b> &'b ContainerT: IntoIterator<Item = &'b T>, { @@ -992,7 +1003,7 @@ pub mod internal { .indent(); Some(format!( "which does not have a {requirements} match with the expected elements. The best match found was:\n{best_match}" - )) + ).into()) } fn get_explanation_for_map<'a, KeyT: Debug, ValueT: Debug, ContainerT: Debug + ?Sized>( @@ -1000,7 +1011,7 @@ pub mod internal { actual: &ContainerT, expected: &[KeyValueMatcher<'a, KeyT, ValueT>; N], requirements: Requirements, - ) -> Option<String> + ) -> Option<Description> where for<'b> &'b ContainerT: IntoIterator<Item = (&'b KeyT, &'b ValueT)>, { @@ -1050,7 +1061,7 @@ pub mod internal { .indent(); Some(format!( "which does not have a {requirements} match with the expected elements. The best match found was:\n{best_match}" - )) + ).into()) } } } @@ -1078,13 +1089,13 @@ mod tests { ]; verify_that!( Matcher::describe(&matcher, MatcherResult::Match), - eq(indoc!( + displays_as(eq(indoc!( " contains elements matching in any order: is equal to 2 => is equal to \"Two\" is equal to 1 => is equal to \"One\" is equal to 3 => is equal to \"Three\"" - )) + ))) ) } diff --git a/tests/all_matcher_test.rs b/tests/all_matcher_test.rs index 6d7e3f8..8b36fc0 100644 --- a/tests/all_matcher_test.rs +++ b/tests/all_matcher_test.rs @@ -61,9 +61,7 @@ fn admits_matchers_without_static_lifetime() -> Result<()> { fn mismatch_description_two_failed_matchers() -> Result<()> { verify_that!( all!(starts_with("One"), starts_with("Two")).explain_match("Three"), - displays_as(eq( - "* which does not start with \"One\"\n * which does not start with \"Two\"" - )) + displays_as(eq("* which does not start with \"One\"\n* which does not start with \"Two\"")) ) } @@ -91,3 +89,67 @@ fn all_multiple_failed_assertions() -> Result<()> { )))) ) } + +#[test] +fn formats_error_message_correctly_when_all_is_inside_some() -> Result<()> { + let value = Some(4); + let result = verify_that!(value, some(all![eq(1), eq(2), eq(3)])); + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + Value of: value + Expected: has a value which has all the following properties: + * is equal to 1 + * is equal to 2 + * is equal to 3 + Actual: Some(4), + which has a value + * which isn't equal to 1 + * which isn't equal to 2 + * which isn't equal to 3" + )))) + ) +} + +#[test] +fn formats_error_message_correctly_when_all_is_inside_ok() -> Result<()> { + let value: std::result::Result<i32, std::io::Error> = Ok(4); + let result = verify_that!(value, ok(all![eq(1), eq(2), eq(3)])); + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + Value of: value + Expected: is a success containing a value, which has all the following properties: + * is equal to 1 + * is equal to 2 + * is equal to 3 + Actual: Ok(4), + which is a success + * which isn't equal to 1 + * which isn't equal to 2 + * which isn't equal to 3" + )))) + ) +} + +#[test] +fn formats_error_message_correctly_when_all_is_inside_err() -> Result<()> { + let value: std::result::Result<(), &'static str> = Err("An error"); + let result = verify_that!(value, err(all![starts_with("Not"), ends_with("problem")])); + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + r#" + Value of: value + Expected: is an error which has all the following properties: + * starts with prefix "Not" + * ends with suffix "problem" + Actual: Err("An error"), + which is an error + * which does not start with "Not" + * which does not end with "problem""# + )))) + ) +} diff --git a/tests/any_matcher_test.rs b/tests/any_matcher_test.rs index 1bdd794..82ed046 100644 --- a/tests/any_matcher_test.rs +++ b/tests/any_matcher_test.rs @@ -61,9 +61,7 @@ fn admits_matchers_without_static_lifetime() -> Result<()> { fn mismatch_description_two_failed_matchers() -> Result<()> { verify_that!( any!(starts_with("One"), starts_with("Two")).explain_match("Three"), - displays_as(eq( - "* which does not start with \"One\"\n * which does not start with \"Two\"" - )) + displays_as(eq("* which does not start with \"One\"\n* which does not start with \"Two\"")) ) } @@ -91,3 +89,67 @@ fn all_multiple_failed_assertions() -> Result<()> { )))) ) } + +#[test] +fn formats_error_message_correctly_when_any_is_inside_some() -> Result<()> { + let value = Some(4); + let result = verify_that!(value, some(any![eq(1), eq(2), eq(3)])); + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + Value of: value + Expected: has a value which has at least one of the following properties: + * is equal to 1 + * is equal to 2 + * is equal to 3 + Actual: Some(4), + which has a value + * which isn't equal to 1 + * which isn't equal to 2 + * which isn't equal to 3" + )))) + ) +} + +#[test] +fn formats_error_message_correctly_when_any_is_inside_ok() -> Result<()> { + let value: std::result::Result<i32, std::io::Error> = Ok(4); + let result = verify_that!(value, ok(any![eq(1), eq(2), eq(3)])); + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + Value of: value + Expected: is a success containing a value, which has at least one of the following properties: + * is equal to 1 + * is equal to 2 + * is equal to 3 + Actual: Ok(4), + which is a success + * which isn't equal to 1 + * which isn't equal to 2 + * which isn't equal to 3" + )))) + ) +} + +#[test] +fn formats_error_message_correctly_when_any_is_inside_err() -> Result<()> { + let value: std::result::Result<(), &'static str> = Err("An error"); + let result = verify_that!(value, err(any![starts_with("Not"), ends_with("problem")])); + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + r#" + Value of: value + Expected: is an error which has at least one of the following properties: + * starts with prefix "Not" + * ends with suffix "problem" + Actual: Err("An error"), + which is an error + * which does not start with "Not" + * which does not end with "problem""# + )))) + ) +} diff --git a/tests/colorized_diff_test.rs b/tests/colorized_diff_test.rs index d1b4ecb..d056020 100644 --- a/tests/colorized_diff_test.rs +++ b/tests/colorized_diff_test.rs @@ -13,7 +13,6 @@ // limitations under the License. use googletest::prelude::*; -use indoc::indoc; use std::fmt::{Display, Write}; // Make a long text with each element of the iterator on one line. @@ -37,16 +36,15 @@ fn colors_appear_when_no_color_is_no_set_and_force_color_is_set() -> Result<()> verify_that!( result, - err(displays_as(contains_substring(indoc! { + err(displays_as(contains_substring( " - - Difference(-\x1B[1;31mactual\x1B[0m / +\x1B[1;32mexpected\x1B[0m): - 1 - 2 - \x1B[3m<---- 45 common lines omitted ---->\x1B[0m - 48 - 49 - +\x1B[1;32m50\x1B[0m" - }))) + Difference(-\x1B[1;31mactual\x1B[0m / +\x1B[1;32mexpected\x1B[0m): + 1 + 2 + \x1B[3m<---- 45 common lines omitted ---->\x1B[0m + 48 + 49 + +\x1B[1;32m50\x1B[0m" + ))) ) } diff --git a/tests/elements_are_matcher_test.rs b/tests/elements_are_matcher_test.rs index 99e9fe7..4de2314 100644 --- a/tests/elements_are_matcher_test.rs +++ b/tests/elements_are_matcher_test.rs @@ -82,7 +82,6 @@ fn elements_are_produces_correct_failure_message_nested() -> Result<()> { result, err(displays_as(contains_substring(indoc!( " - Value of: vec![vec! [0, 1], vec! [1, 2]] Expected: has elements: 0. has elements: 0. is equal to 1 @@ -92,12 +91,12 @@ fn elements_are_produces_correct_failure_message_nested() -> Result<()> { 1. is equal to 3 Actual: [[0, 1], [1, 2]], where: - * element #0 is [0, 1], where: - * element #0 is 0, which isn't equal to 1 - * element #1 is 1, which isn't equal to 2 - * element #1 is [1, 2], where: - * element #0 is 1, which isn't equal to 2 - * element #1 is 2, which isn't equal to 3" + * element #0 is [0, 1], where: + * element #0 is 0, which isn't equal to 1 + * element #1 is 1, which isn't equal to 2 + * element #1 is [1, 2], where: + * element #0 is 1, which isn't equal to 2 + * element #1 is 2, which isn't equal to 3" )))) ) } diff --git a/tests/field_matcher_test.rs b/tests/field_matcher_test.rs index f5d85c5..f585c21 100644 --- a/tests/field_matcher_test.rs +++ b/tests/field_matcher_test.rs @@ -41,7 +41,7 @@ fn field_error_message_shows_field_name_and_inner_matcher() -> Result<()> { verify_that!( matcher.describe(MatcherResult::Match), - eq("has field `int`, which is equal to 31") + displays_as(eq("has field `int`, which is equal to 31")) ) } diff --git a/tests/matches_pattern_test.rs b/tests/matches_pattern_test.rs index 4904cf5..3298e36 100644 --- a/tests/matches_pattern_test.rs +++ b/tests/matches_pattern_test.rs @@ -202,8 +202,6 @@ fn has_meaningful_assertion_failure_message_when_wrong_enum_variant_is_used() -> verify_that!( result, err(displays_as(contains_substring(indoc! {" - Value of: actual - Expected: is AnEnum :: B which has field `0`, which is equal to 123 Actual: A(123), which has the wrong enum variant `A` " @@ -399,6 +397,12 @@ fn matches_enum_without_field() -> Result<()> { verify_that!(actual, matches_pattern!(AnEnum::A)) } +#[rustversion::before(1.76)] +const ANENUM_A_REPR: &str = "AnEnum :: A"; + +#[rustversion::since(1.76)] +const ANENUM_A_REPR: &str = "AnEnum::A"; + #[test] fn generates_correct_failure_output_when_enum_variant_without_field_is_not_matched() -> Result<()> { #[derive(Debug)] @@ -411,7 +415,7 @@ fn generates_correct_failure_output_when_enum_variant_without_field_is_not_match let result = verify_that!(actual, matches_pattern!(AnEnum::A)); - verify_that!(result, err(displays_as(contains_substring("is not AnEnum :: A")))) + verify_that!(result, err(displays_as(contains_substring(format!("is not {ANENUM_A_REPR}"))))) } #[test] @@ -424,7 +428,7 @@ fn generates_correct_failure_output_when_enum_variant_without_field_is_matched() let result = verify_that!(actual, not(matches_pattern!(AnEnum::A))); - verify_that!(result, err(displays_as(contains_substring("is AnEnum :: A")))) + verify_that!(result, err(displays_as(contains_substring(format!("is {ANENUM_A_REPR}"))))) } #[test] @@ -463,7 +467,9 @@ fn includes_enum_variant_in_description_with_field() -> Result<()> { verify_that!( result, - err(displays_as(contains_substring("Expected: is AnEnum :: A which has field `0`"))) + err(displays_as(contains_substring(format!( + "Expected: is {ANENUM_A_REPR} which has field `0`" + )))) ) } @@ -479,9 +485,9 @@ fn includes_enum_variant_in_negative_description_with_field() -> Result<()> { verify_that!( result, - err(displays_as(contains_substring( - "Expected: is not AnEnum :: A which has field `0`, which is equal to" - ))) + err(displays_as(contains_substring(format!( + "Expected: is not {ANENUM_A_REPR} which has field `0`, which is equal to" + )))) ) } @@ -497,9 +503,9 @@ fn includes_enum_variant_in_description_with_two_fields() -> Result<()> { verify_that!( result, - err(displays_as(contains_substring( - "Expected: is AnEnum :: A which has all the following properties" - ))) + err(displays_as(contains_substring(format!( + "Expected: is {ANENUM_A_REPR} which has all the following properties" + )))) ) } @@ -515,9 +521,9 @@ fn includes_enum_variant_in_description_with_three_fields() -> Result<()> { verify_that!( result, - err(displays_as(contains_substring( - "Expected: is AnEnum :: A which has all the following properties" - ))) + err(displays_as(contains_substring(format!( + "Expected: is {ANENUM_A_REPR} which has all the following properties" + )))) ) } @@ -533,7 +539,9 @@ fn includes_enum_variant_in_description_with_named_field() -> Result<()> { verify_that!( result, - err(displays_as(contains_substring("Expected: is AnEnum :: A which has field `field`"))) + err(displays_as(contains_substring(format!( + "Expected: is {ANENUM_A_REPR} which has field `field`" + )))) ) } @@ -552,9 +560,9 @@ fn includes_enum_variant_in_description_with_two_named_fields() -> Result<()> { verify_that!( result, - err(displays_as(contains_substring( - "Expected: is AnEnum :: A which has all the following properties" - ))) + err(displays_as(contains_substring(format!( + "Expected: is {ANENUM_A_REPR} which has all the following properties" + )))) ) } @@ -594,7 +602,7 @@ fn includes_struct_name_in_description_with_ref_property() -> Result<()> { } let actual = AStruct { field: 123 }; - let result = verify_that!(actual, matches_pattern!(AStruct { ref get_field(): eq(234) })); + let result = verify_that!(actual, matches_pattern!(AStruct { *get_field(): eq(234) })); verify_that!( result, @@ -641,10 +649,8 @@ fn includes_struct_name_in_description_with_ref_property_after_field() -> Result } let actual = AStruct { field: 123 }; - let result = verify_that!( - actual, - matches_pattern!(AStruct { field: eq(123), ref get_field(): eq(234) }) - ); + let result = + verify_that!(actual, matches_pattern!(AStruct { field: eq(123), *get_field(): eq(234) })); verify_that!( result, @@ -781,7 +787,7 @@ fn matches_struct_with_a_method_returning_a_reference() -> Result<()> { let actual = AStruct { a_field: 123 }; - verify_that!(actual, matches_pattern!(AStruct { ref get_field_ref(): eq(123) })) + verify_that!(actual, matches_pattern!(AStruct { *get_field_ref(): eq(123) })) } #[test] @@ -799,7 +805,7 @@ fn matches_struct_with_a_method_returning_a_reference_with_trailing_comma() -> R let actual = AStruct { a_field: 123 }; - verify_that!(actual, matches_pattern!(AStruct { ref get_field_ref(): eq(123), })) + verify_that!(actual, matches_pattern!(AStruct { *get_field_ref(): eq(123), })) } #[test] @@ -817,7 +823,7 @@ fn matches_struct_with_a_method_taking_two_parameters_ret_ref() -> Result<()> { let actual = AStruct { a_field: 1 }; - verify_that!(actual, matches_pattern!(AStruct { ref get_field_ref(2, 3): eq(1) })) + verify_that!(actual, matches_pattern!(AStruct { *get_field_ref(2, 3): eq(1) })) } #[test] @@ -839,7 +845,7 @@ fn matches_struct_with_a_method_returning_reference_taking_enum_value_parameter( let actual = AStruct { a_field: 1 }; - verify_that!(actual, matches_pattern!(AStruct { ref get_field_ref(AnEnum::AVariant): eq(1) })) + verify_that!(actual, matches_pattern!(AStruct { *get_field_ref(AnEnum::AVariant): eq(1) })) } #[test] @@ -857,7 +863,7 @@ fn matches_struct_with_a_method_taking_two_parameters_with_trailing_comma_ret_re let actual = AStruct { a_field: 1 }; - verify_that!(actual, matches_pattern!(AStruct { ref get_field_ref(2, 3,): eq(1) })) + verify_that!(actual, matches_pattern!(AStruct { *get_field_ref(2, 3,): eq(1) })) } #[test] @@ -990,7 +996,7 @@ fn matches_struct_with_a_method_returning_reference_followed_by_a_field() -> Res verify_that!( actual, - matches_pattern!(AStruct { ref get_field_ref(): eq(123), another_field: eq(234) }) + matches_pattern!(AStruct { *get_field_ref(): eq(123), another_field: eq(234) }) ) } @@ -1013,7 +1019,7 @@ fn matches_struct_with_a_method_returning_reference_followed_by_a_field_with_tra verify_that!( actual, - matches_pattern!(AStruct { ref get_field_ref(): eq(123), another_field: eq(234), }) + matches_pattern!(AStruct { *get_field_ref(): eq(123), another_field: eq(234), }) ) } @@ -1035,7 +1041,7 @@ fn matches_struct_with_a_method_taking_two_parameters_ret_ref_and_field() -> Res verify_that!( actual, - matches_pattern!(AStruct { ref get_field_ref(2, 3): eq(1), another_field: eq(123) }) + matches_pattern!(AStruct { *get_field_ref(2, 3): eq(1), another_field: eq(123) }) ) } @@ -1061,7 +1067,7 @@ fn matches_struct_with_a_method_taking_enum_value_param_ret_ref_followed_by_fiel verify_that!( actual, - matches_pattern!(AStruct { ref get_field_ref(AnEnum::AVariant): eq(1), another_field: eq(2) }) + matches_pattern!(AStruct { *get_field_ref(AnEnum::AVariant): eq(1), another_field: eq(2) }) ) } @@ -1084,7 +1090,7 @@ fn matches_struct_with_a_method_taking_two_parameters_with_trailing_comma_ret_re verify_that!( actual, - matches_pattern!(AStruct { ref get_field_ref(2, 3,): eq(1), another_field: eq(123) }) + matches_pattern!(AStruct { *get_field_ref(2, 3,): eq(1), another_field: eq(123) }) ) } @@ -1217,7 +1223,7 @@ fn matches_struct_with_a_field_followed_by_a_method_returning_reference() -> Res verify_that!( actual, - matches_pattern!(AStruct { another_field: eq(234), ref get_field_ref(): eq(123) }) + matches_pattern!(AStruct { another_field: eq(234), *get_field_ref(): eq(123) }) ) } @@ -1240,7 +1246,7 @@ fn matches_struct_with_a_field_followed_by_a_method_returning_ref_and_trailing_c verify_that!( actual, - matches_pattern!(AStruct { another_field: eq(234), ref get_field_ref(): eq(123), }) + matches_pattern!(AStruct { another_field: eq(234), *get_field_ref(): eq(123), }) ) } @@ -1262,7 +1268,7 @@ fn matches_struct_with_a_field_followed_by_a_method_with_params_ret_ref() -> Res verify_that!( actual, - matches_pattern!(AStruct { another_field: eq(234), ref get_field_ref(2, 3): eq(123) }) + matches_pattern!(AStruct { another_field: eq(234), *get_field_ref(2, 3): eq(123) }) ) } @@ -1288,7 +1294,7 @@ fn matches_struct_with_field_followed_by_method_taking_enum_value_param_ret_ref( verify_that!( actual, - matches_pattern!(AStruct { another_field: eq(2), ref get_field_ref(AnEnum::AVariant): eq(1) }) + matches_pattern!(AStruct { another_field: eq(2), *get_field_ref(AnEnum::AVariant): eq(1) }) ) } @@ -1311,7 +1317,7 @@ fn matches_struct_with_a_field_followed_by_a_method_with_params_and_trailing_com verify_that!( actual, - matches_pattern!(AStruct { another_field: eq(234), ref get_field_ref(2, 3,): eq(123) }) + matches_pattern!(AStruct { another_field: eq(234), *get_field_ref(2, 3,): eq(123) }) ) } @@ -1479,7 +1485,7 @@ fn matches_struct_with_a_field_followed_by_a_method_ret_ref_followed_by_a_field( actual, matches_pattern!(AStruct { another_field: eq(234), - ref get_field_ref(): eq(123), + *get_field_ref(): eq(123), a_third_field: eq(345) }) ) @@ -1507,7 +1513,7 @@ fn matches_struct_with_a_field_followed_by_a_method_ret_ref_followed_by_a_field_ actual, matches_pattern!(AStruct { another_field: eq(234), - ref get_field_ref(): eq(123), + *get_field_ref(): eq(123), a_third_field: eq(345), }) ) @@ -1535,7 +1541,7 @@ fn matches_struct_with_a_field_followed_by_a_method_with_params_ret_ref_followed actual, matches_pattern!(AStruct { another_field: eq(234), - ref get_field_ref(2, 3): eq(123), + *get_field_ref(2, 3): eq(123), a_third_field: eq(345), }) ) @@ -1567,7 +1573,7 @@ fn matches_struct_with_field_followed_by_method_taking_enum_value_param_ret_ref_ actual, matches_pattern!(AStruct { another_field: eq(2), - ref get_field_ref(AnEnum::AVariant): eq(1), + *get_field_ref(AnEnum::AVariant): eq(1), a_third_field: eq(3), }) ) @@ -1595,7 +1601,7 @@ fn matches_struct_with_a_field_followed_by_a_method_with_params_trailing_comma_r actual, matches_pattern!(AStruct { another_field: eq(234), - ref get_field_ref(2, 3,): eq(123), + *get_field_ref(2, 3,): eq(123), a_third_field: eq(345), }) ) diff --git a/tests/pointwise_matcher_test.rs b/tests/pointwise_matcher_test.rs index 85d16ff..cb8ef3b 100644 --- a/tests/pointwise_matcher_test.rs +++ b/tests/pointwise_matcher_test.rs @@ -142,8 +142,8 @@ fn pointwise_returns_mismatch_when_actual_value_does_not_match_on_first_and_seco 2. is equal to 3 Actual: [1, 2, 3], where: - * element #0 is 1, which isn't equal to 2 - * element #1 is 2, which isn't equal to 3" + * element #0 is 1, which isn't equal to 2 + * element #1 is 2, which isn't equal to 3" )))) ) } diff --git a/tests/property_matcher_test.rs b/tests/property_matcher_test.rs index f9a88be..7092446 100644 --- a/tests/property_matcher_test.rs +++ b/tests/property_matcher_test.rs @@ -67,7 +67,7 @@ fn matches_struct_with_matching_property_with_parameters_with_trailing_comma() - #[test] fn matches_struct_with_matching_property_ref() -> Result<()> { let value = SomeStruct { a_property: 10 }; - verify_that!(value, property!(ref SomeStruct.get_property_ref(), eq(10))) + verify_that!(value, property!(*SomeStruct.get_property_ref(), eq(10))) } #[test] @@ -82,7 +82,7 @@ fn matches_struct_with_matching_string_reference_property() -> Result<()> { } } let value = StructWithString { property: "Something".into() }; - verify_that!(value, property!(ref StructWithString.get_property_ref(), eq("Something"))) + verify_that!(value, property!(*StructWithString.get_property_ref(), eq("Something"))) } #[test] @@ -97,19 +97,19 @@ fn matches_struct_with_matching_slice_property() -> Result<()> { } } let value = StructWithVec { property: vec![1, 2, 3] }; - verify_that!(value, property!(ref StructWithVec.get_property_ref(), eq([1, 2, 3]))) + verify_that!(value, property!(*StructWithVec.get_property_ref(), eq([1, 2, 3]))) } #[test] fn matches_struct_with_matching_property_ref_with_parameters() -> Result<()> { let value = SomeStruct { a_property: 10 }; - verify_that!(value, property!(ref SomeStruct.get_property_ref_with_params(2, 3), eq(10))) + verify_that!(value, property!(*SomeStruct.get_property_ref_with_params(2, 3), eq(10))) } #[test] fn matches_struct_with_matching_property_ref_with_parameters_and_trailing_comma() -> Result<()> { let value = SomeStruct { a_property: 10 }; - verify_that!(value, property!(ref SomeStruct.get_property_ref_with_params(2, 3,), eq(10))) + verify_that!(value, property!(*SomeStruct.get_property_ref_with_params(2, 3,), eq(10))) } #[test] @@ -122,7 +122,7 @@ fn does_not_match_struct_with_non_matching_property() -> Result<()> { fn describes_itself_in_matching_case() -> Result<()> { verify_that!( property!(SomeStruct.get_property(), eq(1)).describe(MatcherResult::Match), - eq("has property `get_property()`, which is equal to 1") + displays_as(eq("has property `get_property()`, which is equal to 1")) ) } @@ -130,7 +130,7 @@ fn describes_itself_in_matching_case() -> Result<()> { fn describes_itself_in_not_matching_case() -> Result<()> { verify_that!( property!(SomeStruct.get_property(), eq(1)).describe(MatcherResult::NoMatch), - eq("has property `get_property()`, which isn't equal to 1") + displays_as(eq("has property `get_property()`, which isn't equal to 1")) ) } @@ -155,16 +155,16 @@ fn explains_mismatch_referencing_explanation_of_inner_matcher() -> Result<()> { #[test] fn describes_itself_in_matching_case_for_ref() -> Result<()> { verify_that!( - property!(ref SomeStruct.get_property_ref(), eq(1)).describe(MatcherResult::Match), - eq("has property `get_property_ref()`, which is equal to 1") + property!(*SomeStruct.get_property_ref(), eq(1)).describe(MatcherResult::Match), + displays_as(eq("has property `get_property_ref()`, which is equal to 1")) ) } #[test] fn describes_itself_in_not_matching_case_for_ref() -> Result<()> { verify_that!( - property!(ref SomeStruct.get_property_ref(), eq(1)).describe(MatcherResult::NoMatch), - eq("has property `get_property_ref()`, which isn't equal to 1") + property!(*SomeStruct.get_property_ref(), eq(1)).describe(MatcherResult::NoMatch), + displays_as(eq("has property `get_property_ref()`, which isn't equal to 1")) ) } @@ -178,7 +178,7 @@ fn explains_mismatch_referencing_explanation_of_inner_matcher_for_ref() -> Resul } let value = SomeStruct { a_property: 2 }; let result = - verify_that!(value, property!(ref SomeStruct.get_a_collection_ref(), container_eq([1]))); + verify_that!(value, property!(*SomeStruct.get_a_collection_ref(), container_eq([1]))); verify_that!( result, diff --git a/tests/tuple_matcher_test.rs b/tests/tuple_matcher_test.rs index 0c1eb01..a93ffee 100644 --- a/tests/tuple_matcher_test.rs +++ b/tests/tuple_matcher_test.rs @@ -176,54 +176,50 @@ fn tuple_matcher_with_trailing_comma_matches_matching_12_tuple() -> Result<()> { #[test] fn tuple_matcher_1_has_correct_description_for_match() -> Result<()> { verify_that!( - (eq(1),).describe(MatcherResult::Match), - eq(indoc!( + (eq::<i32, _>(1),).describe(MatcherResult::Match), + displays_as(eq(indoc!( " is a tuple whose values respectively match: - is equal to 1, - " - )) + is equal to 1" + ))) ) } #[test] fn tuple_matcher_1_has_correct_description_for_mismatch() -> Result<()> { verify_that!( - (eq(1),).describe(MatcherResult::NoMatch), - eq(indoc!( + (eq::<i32, _>(1),).describe(MatcherResult::NoMatch), + displays_as(eq(indoc!( " is a tuple whose values do not respectively match: - is equal to 1, - " - )) + is equal to 1" + ))) ) } #[test] fn tuple_matcher_2_has_correct_description_for_match() -> Result<()> { verify_that!( - (eq(1), eq(2)).describe(MatcherResult::Match), - eq(indoc!( + (eq::<i32, _>(1), eq::<i32, _>(2)).describe(MatcherResult::Match), + displays_as(eq(indoc!( " is a tuple whose values respectively match: - is equal to 1, - is equal to 2, - " - )) + is equal to 1 + is equal to 2" + ))) ) } #[test] fn tuple_matcher_2_has_correct_description_for_mismatch() -> Result<()> { verify_that!( - (eq(1), eq(2)).describe(MatcherResult::NoMatch), - eq(indoc!( + (eq::<i32, _>(1), eq::<i32, _>(2)).describe(MatcherResult::NoMatch), + displays_as(eq(indoc!( " is a tuple whose values do not respectively match: - is equal to 1, - is equal to 2, - " - )) + is equal to 1 + is equal to 2" + ))) ) } @@ -233,11 +229,12 @@ fn describe_match_shows_which_tuple_element_did_not_match() -> Result<()> { (eq(1), eq(2)).explain_match(&(1, 3)), displays_as(eq(indoc!( " - which is a tuple whose values do not respectively match: - is equal to 1, - is equal to 2, - Element #1 is 3, which isn't equal to 2 - " + which + is a tuple whose values do not respectively match: + is equal to 1 + is equal to 2 + Element #1 is 3, + which isn't equal to 2" ))) ) } @@ -248,12 +245,14 @@ fn describe_match_shows_which_two_tuple_elements_did_not_match() -> Result<()> { (eq(1), eq(2)).explain_match(&(2, 3)), displays_as(eq(indoc!( " - which is a tuple whose values do not respectively match: - is equal to 1, - is equal to 2, - Element #0 is 2, which isn't equal to 1 - Element #1 is 3, which isn't equal to 2 - " + which + is a tuple whose values do not respectively match: + is equal to 1 + is equal to 2 + Element #0 is 2, + which isn't equal to 1 + Element #1 is 3, + which isn't equal to 2" ))) ) } diff --git a/tests/unordered_elements_are_matcher_test.rs b/tests/unordered_elements_are_matcher_test.rs index a105b70..bd61417 100644 --- a/tests/unordered_elements_are_matcher_test.rs +++ b/tests/unordered_elements_are_matcher_test.rs @@ -352,7 +352,7 @@ fn is_contained_in_matches_hash_map_with_trailing_comma() -> Result<()> { #[test] fn is_contained_in_matches_when_container_is_empty() -> Result<()> { - verify_that!(vec![], is_contained_in!(eq(2), eq(3), eq(4))) + verify_that!(vec![], is_contained_in!(eq::<i32, _>(2), eq(3), eq(4))) } #[test] |