diff options
author | Martin Geisler <mgeisler@google.com> | 2023-09-07 08:26:12 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2023-09-07 08:26:12 +0000 |
commit | 8a97854a1cd062cbfbf0981abae19746f65e2177 (patch) | |
tree | bf32c7b6a23a37fa66e7cbb1e627cf30b4376daf | |
parent | 501a385ccace04c1f79ee7c42f546256ea8be258 (diff) | |
parent | feface21b7006ea632c1d1b405c58bbcf83c2c54 (diff) | |
download | googletest-8a97854a1cd062cbfbf0981abae19746f65e2177.tar.gz |
Import googletest 0.10.0 crate am: feface21b7
Original change: https://android-review.googlesource.com/c/platform/external/rust/crates/googletest/+/2737240
Change-Id: I02e827e4a9f1aecaf15907509bb96b367b6fc2f2
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
79 files changed, 17617 insertions, 0 deletions
diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json new file mode 100644 index 0000000..ce5ec95 --- /dev/null +++ b/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "6e5405db2217d37006720c101beb1b91199a3a26" + }, + "path_in_vcs": "googletest" +}
\ No newline at end of file diff --git a/Android.bp b/Android.bp new file mode 100644 index 0000000..9f2e45e --- /dev/null +++ b/Android.bp @@ -0,0 +1,28 @@ +// This file is generated by cargo2android.py --config cargo2android.json. +// Do not modify this file as changes will be overridden on upgrade. + + + +rust_library { + name: "libgoogletest_rust", + host_supported: true, + crate_name: "googletest", + cargo_env_compat: true, + cargo_pkg_version: "0.10.0", + srcs: ["src/lib.rs"], + edition: "2021", + rustlibs: [ + "libnum_traits", + "libregex", + ], + proc_macros: [ + "libgoogletest_macro", + "librustversion", + ], + apex_available: [ + "//apex_available:platform", + "//apex_available:anyapex", + ], + product_available: true, + vendor_available: true, +} diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..3e898d2 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,66 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +rust-version = "1.59.0" +name = "googletest" +version = "0.10.0" +authors = [ + "Bradford Hovinen <hovinen@google.com>", + "Bastien Jacot-Guillarmod <bjacotg@google.com>", + "Maciej Pietrzak <mpi@google.com>", + "Martin Geisler <mgeisler@google.com>", +] +description = "A rich assertion and matcher library inspired by GoogleTest for C++" +readme = "README.md" +keywords = [ + "unit", + "matcher", + "testing", + "assertions", +] +categories = [ + "development-tools", + "development-tools::testing", +] +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" + +[dependencies.num-traits] +version = "0.2.15" + +[dependencies.proptest] +version = "1.2.0" +optional = true + +[dependencies.regex] +version = "1.6.0" + +[dependencies.rustversion] +version = "1.0.14" + +[dev-dependencies.indoc] +version = "2" + +[dev-dependencies.quickcheck] +version = "1.0.3" + +[dev-dependencies.serial_test] +version = "2.0.0" @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/METADATA b/METADATA new file mode 100644 index 0000000..6c2060b --- /dev/null +++ b/METADATA @@ -0,0 +1,19 @@ +name: "googletest" +description: "A rich assertion and matcher library inspired by GoogleTest for C++" +third_party { + identifier { + type: "crates.io" + value: "https://crates.io/crates/googletest" + } + 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 + } +} diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/MODULE_LICENSE_APACHE2 @@ -0,0 +1 @@ +include platform/prebuilts/rust:master:/OWNERS diff --git a/README.md b/README.md new file mode 100644 index 0000000..2a0f918 --- /dev/null +++ b/README.md @@ -0,0 +1,354 @@ +# GoogleTest Rust + +[![crates.io][crates-badge]][crates-url] +[![docs.rs][docs-badge]][docs-url] +[![Apache licensed][license-badge]][license-url] +[![Build Status][actions-badge]][actions-url] + +[crates-badge]: https://img.shields.io/crates/v/googletest.svg +[crates-url]: https://crates.io/crates/googletest +[docs-badge]: https://img.shields.io/badge/docs.rs-googletest-66c2a5 +[docs-url]: https://docs.rs/googletest/*/googletest/ +[license-badge]: https://img.shields.io/badge/license-Apache-blue.svg +[license-url]: https://github.com/google/googletest-rust/blob/main/LICENSE +[actions-badge]: https://github.com/google/googletest-rust/workflows/CI/badge.svg +[actions-url]: https://github.com/google/googletest-rust/actions?query=workflow%3ACI+branch%3Amain + +This library brings the rich assertion types of Google's C++ testing library +[GoogleTest](https://github.com/google/googletest) to Rust. It provides: + + * A framework for writing matchers which can be combined to make a wide range + of assertions on data, + * A rich set of matchers providing similar functionality to those included in + [GoogleTest](https://google.github.io/googletest/reference/matchers.html), + and + * 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. + +> :warning: The API is not fully stable and may still be changed until we +> publish version 1.0. + +## Assertions and matchers + +The core of GoogleTest is its *matchers*. Matchers indicate what aspect of an +actual value one is asserting: (in-)equality, containment, regular expression +matching, and so on. + +To make an assertion using a matcher, GoogleTest offers three macros: + + * [`assert_that!`] panics if the assertion fails, aborting the test. + * [`expect_that!`] logs an assertion failure, marking the test as having + failed, but allows the test to continue running (called a _non-fatal + assertion_). It requires the use of the [`googletest::test`] attribute macro + on the test itself. + * [`verify_that!`] has no side effects and evaluates to a [`Result<()>`] whose + `Err` variant describes the assertion failure, if there is one. In + combination with the + [`?` operator](https://doc.rust-lang.org/reference/expressions/operator-expr.html#the-question-mark-operator), + this can be used to abort the test on assertion failure without panicking. It + is also the building block for the other two macros above. + +For example: + +```rust +use googletest::prelude::*; + +#[test] +fn fails_and_panics() { + let value = 2; + assert_that!(value, eq(4)); +} + +#[googletest::test] +fn two_logged_failures() { + let value = 2; + expect_that!(value, eq(4)); // Test now failed, but continues executing. + expect_that!(value, eq(5)); // Second failure is also logged. +} + +#[test] +fn fails_immediately_without_panic() -> Result<()> { + let value = 2; + verify_that!(value, eq(4))?; // Test fails and aborts. + verify_that!(value, eq(2))?; // Never executes. + Ok(()) +} + +#[test] +fn simple_assertion() -> Result<()> { + let value = 2; + verify_that!(value, eq(4)) // One can also just return the last assertion. +} +``` + +This library includes a rich set of matchers, covering: + + * Equality, numeric inequality, and approximate equality; + * Strings and regular expressions; + * Containers and set-theoretic matching. + +Matchers are composable: + +```rust +use googletest::prelude::*; + +#[googletest::test] +fn contains_at_least_one_item_at_least_3() { + let value = vec![1, 2, 3]; + expect_that!(value, contains(ge(3))); +} +``` + +They can also be logically combined: + +```rust +use googletest::prelude::*; + +#[googletest::test] +fn strictly_between_9_and_11() { + let value = 10; + expect_that!(value, gt(9).and(not(ge(11)))); +} +``` + +## Pattern-matching + +One can use the macro [`matches_pattern!`] to create a composite matcher for a +struct or enum that matches fields with other matchers: + +```rust +use googletest::prelude::*; + +struct AStruct { + a_field: i32, + another_field: i32, + a_third_field: &'static str, +} + +#[test] +fn struct_has_expected_values() { + let value = AStruct { + a_field: 10, + another_field: 100, + a_third_field: "A correct value", + }; + expect_that!(value, matches_pattern!(AStruct { + a_field: eq(10), + another_field: gt(50), + a_third_field: contains_substring("correct"), + })); +} +``` + +## Writing matchers + +One can extend the library by writing additional matchers. To do so, create a +struct holding the matcher's data and have it implement the trait [`Matcher`]: + +```rust +struct MyEqMatcher<T> { + expected: T, +} + +impl<T: PartialEq + Debug> Matcher for MyEqMatcher<T> { + type ActualT = T; + + fn matches(&self, actual: &Self::ActualT) -> MatcherResult { + (self.expected == *actual).into() + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + match matcher_result { + MatcherResult::Matches => { + format!("is equal to {:?} the way I define it", self.expected) + } + MatcherResult::DoesNotMatch => { + format!("isn't equal to {:?} the way I define it", self.expected) + } + } + } +} +``` + +It is recommended to expose a function which constructs the matcher: + +```rust +pub fn eq_my_way<T: PartialEq + Debug>(expected: T) -> impl Matcher<ActualT = T> { + MyEqMatcher { expected } +} +``` + +The new matcher can then be used in the assertion macros: + +```rust +#[googletest::test] +fn should_be_equal_by_my_definition() { + expect_that!(10, eq_my_way(10)); +} +``` + +## Non-fatal assertions + +Using non-fatal assertions, a single test is able to log multiple assertion +failures. Any single assertion failure causes the test to be considered having +failed, but execution continues until the test completes or otherwise aborts. + +This is analogous to the `EXPECT_*` family of macros in GoogleTest. + +To make a non-fatal assertion, use the macro [`expect_that!`]. The test must +also be marked with [`googletest::test`] instead of the Rust-standard `#[test]`. + +```rust +use googletest::prelude::*; + +#[googletest::test] +fn three_non_fatal_assertions() { + let value = 2; + expect_that!(value, eq(2)); // Passes; test still considered passing. + expect_that!(value, eq(3)); // Fails; logs failure and marks the test failed. + expect_that!(value, eq(4)); // A second failure, also logged. +} +``` + +This can be used in the same tests as `verify_that!`, in which case the test +function must also return [`Result<()>`]: + +```rust +use googletest::prelude::*; + +#[googletest::test] +fn failing_non_fatal_assertion() -> Result<()> { + let value = 2; + expect_that!(value, eq(3)); // Just marks the test as having failed. + verify_that!(value, eq(2))?; // Passes, so does not abort the test. + Ok(()) // Because of the failing expect_that! call above, the + // test fails despite returning Ok(()) +} +``` + +```rust +use googletest::prelude::*; + +#[googletest::test] +fn failing_fatal_assertion_after_non_fatal_assertion() -> Result<()> { + let value = 2; + verify_that!(value, eq(3))?; // Fails and aborts the test. + expect_that!(value, eq(3)); // Never executes, since the test already aborted. + Ok(()) +} +``` + +### Interoperability + +You can use the `#[googletest::test]` macro together with many other libraries +such as [rstest](https://crates.io/crates/rstest). Just apply both attribute +macros to the test: + +```rust +#[googletest::test] +#[rstest] +#[case(1)] +#[case(2)] +#[case(3)] +fn rstest_works_with_google_test(#[case] value: u32) -> Result<()> { + verify_that!(value, gt(0)) +} +``` + +Make sure to put `#[googletest::test]` *before* `#[rstest]`. Otherwise the +annotated test will run twice, since both macros will attempt to register a test +with the Rust test harness. + +The macro also works together with +[async tests with Tokio](https://docs.rs/tokio/latest/tokio/attr.test.html) in +the same way: + +```rust +#[googletest::test] +#[tokio::test] +async fn should_work_with_tokio() -> Result<()> { + verify_that!(3, gt(0)) +} +``` + +There is one caveat when running async tests: test failure reporting through +`and_log_failure` will not work properly if the assertion occurs on a different +thread than runs the test. + +## Predicate assertions + +The macro [`verify_pred!`] provides predicate assertions analogous to +GoogleTest's `EXPECT_PRED` family of macros. Wrap an invocation of a predicate +in a `verify_pred!` invocation to turn that into a test assertion which passes +precisely when the predicate returns `true`: + +```rust +fn stuff_is_correct(x: i32, y: i32) -> bool { + x == y +} + +let x = 3; +let y = 4; +verify_pred!(stuff_is_correct(x, y))?; +``` + +The assertion failure message shows the arguments and the values to which they +evaluate: + +``` +stuff_is_correct(x, y) was false with + x = 3, + y = 4 +``` + +The `verify_pred!` invocation evaluates to a [`Result<()>`] just like +[`verify_that!`]. There is also a macro [`expect_pred!`] to make a non-fatal +predicaticate assertion. + +## Unconditionally generating a test failure + +The macro [`fail!`] unconditionally evaluates to a `Result` indicating a test +failure. It can be used analogously to [`verify_that!`] and [`verify_pred!`] to +cause a test to fail, with an optional formatted message: + +```rust +#[test] +fn always_fails() -> Result<()> { + fail!("This test must fail with {}", "today") +} +``` + +## Configuration + +This library is configurable through environment variables. Since the +configuration does not impact whether a test fails or not but how a failure is +displayed, we recommend setting those variables in the personal +`~/.cargo/config.toml` instead of in the project-scoped `Cargo.toml`. + +### Configuration variable list + +| Variable name | Description | +| ------------- | ------------------------------------------------------- | +| NO_COLOR | Disables colored output. See <https://no-color.org/>. | +| FORCE_COLOR | Forces colors even when the output is piped to a file. | + +## Contributing Changes + +Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute +to this project. + +[`and_log_failure()`]: https://docs.rs/googletest/*/googletest/trait.GoogleTestSupport.html#tymethod.and_log_failure +[`assert_that!`]: https://docs.rs/googletest/*/googletest/macro.assert_that.html +[`expect_pred!`]: https://docs.rs/googletest/*/googletest/macro.expect_pred.html +[`expect_that!`]: https://docs.rs/googletest/*/googletest/macro.expect_that.html +[`fail!`]: https://docs.rs/googletest/*/googletest/macro.fail.html +[`googletest::test`]: https://docs.rs/googletest/*/googletest/attr.test.html +[`matches_pattern!`]: https://docs.rs/googletest/*/googletest/macro.matches_pattern.html +[`verify_pred!`]: https://docs.rs/googletest/*/googletest/macro.verify_pred.html +[`verify_that!`]: https://docs.rs/googletest/*/googletest/macro.verify_that.html +[`Describe`]: https://docs.rs/googletest/*/googletest/matcher/trait.Describe.html +[`Matcher`]: https://docs.rs/googletest/*/googletest/matcher/trait.Matcher.html +[`Result<()>`]: https://docs.rs/googletest/*/googletest/type.Result.html diff --git a/cargo2android.json b/cargo2android.json new file mode 100644 index 0000000..c76e391 --- /dev/null +++ b/cargo2android.json @@ -0,0 +1,5 @@ +{ + "device": true, + "run": true, + "suffix": "_rust" +}
\ No newline at end of file diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..7bc2412 --- /dev/null +++ b/config.toml @@ -0,0 +1,2 @@ +[env] +NO_COLOR = "1" diff --git a/crate_docs.md b/crate_docs.md new file mode 100644 index 0000000..a00c219 --- /dev/null +++ b/crate_docs.md @@ -0,0 +1,485 @@ +A rich test assertion library for Rust. + +This library provides: + + * A framework for writing matchers which can be combined to make a wide + range of assertions on data, + * A rich set of matchers, and + * A new set of test assertion macros. + +## Assertions and matchers + +The core of GoogleTest is its *matchers*. Matchers indicate what aspect of an +actual value one is asserting: (in-)equality, containment, regular expression +matching, and so on. + +To make an assertion using a matcher, GoogleTest offers three macros: + + * [`assert_that!`] panics if the assertion fails, aborting the test. + * [`expect_that!`] logs an assertion failure, marking the test as having + failed, but allows the test to continue running (called a _non-fatal + assertion_). It requires the use of the [`googletest::test`][crate::test] + attribute macro on the test itself. + * [`verify_that!`] has no side effects and evaluates to a [`Result`] whose + `Err` variant describes the assertion failure, if there is one. In + combination with the + [`?` operator](https://doc.rust-lang.org/reference/expressions/operator-expr.html#the-question-mark-operator), + this can be used to abort the test on assertion failure without panicking. It + is also the building block for the other two macros above. + +For example: + +``` +use googletest::prelude::*; + +# /* The attribute macro would prevent the function from being compiled in a doctest. +#[test] +# */ +fn fails_and_panics() { + let value = 2; + assert_that!(value, eq(4)); +} + +# /* The attribute macro would prevent the function from being compiled in a doctest. +#[googletest::test] +# */ +fn two_logged_failures() { + let value = 2; + expect_that!(value, eq(4)); // Test now failed, but continues executing. + expect_that!(value, eq(5)); // Second failure is also logged. +} + +# /* The attribute macro would prevent the function from being compiled in a doctest. +#[test] +# */ +fn fails_immediately_without_panic() -> Result<()> { + let value = 2; + verify_that!(value, eq(4))?; // Test fails and aborts. + verify_that!(value, eq(2))?; // Never executes. + Ok(()) +} + +# /* The attribute macro would prevent the function from being compiled in a doctest. +#[test] +# */ +fn simple_assertion() -> Result<()> { + let value = 2; + verify_that!(value, eq(4)) // One can also just return the last assertion. +} +``` + +Matchers are composable: + +``` +use googletest::prelude::*; + +# /* The attribute macro would prevent the function from being compiled in a doctest. +#[googletest::test] +# */ +fn contains_at_least_one_item_at_least_3() { +# googletest::internal::test_outcome::TestOutcome::init_current_test_outcome(); + let value = vec![1, 2, 3]; + expect_that!(value, contains(ge(3))); +# googletest::internal::test_outcome::TestOutcome::close_current_test_outcome::<&str>(Ok(())) +# .unwrap(); +} +# contains_at_least_one_item_at_least_3(); +``` + +They can also be logically combined: + +``` +use googletest::prelude::*; + +# /* The attribute macro would prevent the function from being compiled in a doctest. +#[googletest::test] +# */ +fn strictly_between_9_and_11() { +# googletest::internal::test_outcome::TestOutcome::init_current_test_outcome(); + let value = 10; + expect_that!(value, gt(9).and(not(ge(11)))); +# googletest::internal::test_outcome::TestOutcome::close_current_test_outcome::<&str>(Ok(())) +# .unwrap(); +} +# strictly_between_9_and_11(); +``` + +## Available matchers + +The following matchers are provided in GoogleTest Rust: + +| Matcher | What it matches | +|----------------------|--------------------------------------------------------------------------| +| [`all!`] | Anything matched by all given matchers. | +| [`any!`] | Anything matched by at least one of the given matchers. | +| [`anything`] | Any input. | +| [`approx_eq`] | A floating point number within a standard tolerance of the argument. | +| [`char_count`] | A string with a Unicode scalar count matching the argument. | +| [`container_eq`] | Same as [`eq`], but for containers (with a better mismatch description). | +| [`contains`] | A container containing an element matched by the given matcher. | +| [`contains_each!`] | A container containing distinct elements each of the arguments match. | +| [`contains_regex`] | A string containing a substring matching the given regular expression. | +| [`contains_substring`] | A string containing the given substring. | +| [`displays_as`] | A [`Display`] value whose formatted string is matched by the argument. | +| [`each`] | A container all of whose elements the given argument matches. | +| [`elements_are!`] | A container whose elements the arguments match, in order. | +| [`empty`] | An empty collection. | +| [`ends_with`] | A string ending with the given suffix. | +| [`eq`] | A value equal to the argument, in the sense of the [`PartialEq`] trait. | +| [`eq_deref_of`] | A value equal to the dereferenced value of the argument. | +| [`err`] | A [`Result`][std::result::Result] containing an `Err` variant the argument matches. | +| [`field!`] | A struct or enum with a given field whose value the argument matches. | +| [`ge`] | A [`PartialOrd`] value greater than or equal to the given value. | +| [`gt`] | A [`PartialOrd`] value strictly greater than the given value. | +| [`has_entry`] | A [`HashMap`] containing a given key whose value the argument matches. | +| [`is_contained_in!`] | A container each of whose elements is matched by some given matcher. | +| [`is_nan`] | A floating point number which is NaN. | +| [`le`] | A [`PartialOrd`] value less than or equal to the given value. | +| [`len`] | A container whose number of elements the argument matches. | +| [`lt`] | A [`PartialOrd`] value strictly less than the given value. | +| [`matches_pattern!`] | A struct or enum whose fields are matched according to the arguments. | +| [`matches_regex`] | A string matched by the given regular expression. | +| [`near`] | A floating point number within a given tolerance of the argument. | +| [`none`] | An [`Option`] containing `None`. | +| [`not`] | Any value the argument does not match. | +| [`ok`] | A [`Result`][std::result::Result] containing an `Ok` variant the argument matches. | +| [`pat!`] | Alias for [`matches_pattern!`]. | +| [`points_to`] | Any [`Deref`] such as `&`, `Rc`, etc. whose value the argument matches. | +| [`pointwise!`] | A container whose contents the arguments match in a pointwise fashion. | +| [`predicate`] | A value on which the given predicate returns true. | +| [`some`] | An [`Option`] containing `Some` whose value the argument matches. | +| [`starts_with`] | A string starting with the given prefix. | +| [`subset_of`] | A container all of whose elements are contained in the argument. | +| [`superset_of`] | A container containing all elements of the argument. | +| [`unordered_elements_are!`] | A container whose elements the arguments match, in any order. | + +[`anything`]: matchers::anything +[`approx_eq`]: matchers::approx_eq +[`char_count`]: matchers::char_count +[`container_eq`]: matchers::container_eq +[`contains`]: matchers::contains +[`contains_regex`]: matchers::contains_regex +[`contains_substring`]: matchers::contains_substring +[`displays_as`]: matchers::displays_as +[`each`]: matchers::each +[`empty`]: matchers::empty +[`ends_with`]: matchers::ends_with +[`eq`]: matchers::eq +[`eq_deref_of`]: matchers::eq_deref_of +[`err`]: matchers::err +[`ge`]: matchers::ge +[`gt`]: matchers::gt +[`has_entry`]: matchers::has_entry +[`is_nan`]: matchers::is_nan +[`le`]: matchers::le +[`len`]: matchers::len +[`lt`]: matchers::lt +[`matches_regex`]: matchers::matches_regex +[`near`]: matchers::near +[`none`]: matchers::none +[`not`]: matchers::not +[`ok`]: matchers::ok +[`points_to`]: matchers::points_to +[`predicate`]: matchers::predicate +[`some`]: matchers::some +[`starts_with`]: matchers::starts_with +[`subset_of`]: matchers::subset_of +[`superset_of`]: matchers::superset_of +[`Deref`]: std::ops::Deref +[`Display`]: std::fmt::Display +[`HashMap`]: std::collections::HashMap +[`Option`]: std::option::Option +[`PartialEq`]: std::cmp::PartialEq +[`PartialOrd`]: std::cmp::PartialOrd + +## Writing matchers + +One can extend the library by writing additional matchers. To do so, create +a struct holding the matcher's data and have it implement the trait +[`Matcher`]: + +```no_run +use googletest::matcher::{Matcher, MatcherResult}; +use std::fmt::Debug; + +struct MyEqMatcher<T> { + expected: T, +} + +impl<T: PartialEq + Debug> Matcher for MyEqMatcher<T> { + type ActualT = T; + + fn matches(&self, actual: &Self::ActualT) -> MatcherResult { + if self.expected == *actual { + MatcherResult::Match + } else { + MatcherResult::NoMatch + } + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + match matcher_result { + MatcherResult::Match => { + format!("is equal to {:?} the way I define it", self.expected) + } + MatcherResult::NoMatch => { + format!("isn't equal to {:?} the way I define it", self.expected) + } + } + } +} +``` + + It is recommended to expose a function which constructs the matcher: + + ```no_run + # use googletest::matcher::{Matcher, MatcherResult}; + # use std::fmt::Debug; + # + # struct MyEqMatcher<T> { + # expected: T, + # } + # + # impl<T: PartialEq + Debug> Matcher for MyEqMatcher<T> { + # type ActualT = T; + # + # fn matches(&self, actual: &Self::ActualT) -> MatcherResult { + # if self.expected == *actual { + # MatcherResult::Match + # } else { + # MatcherResult::NoMatch + # } + # } + # + # fn describe(&self, matcher_result: MatcherResult) -> String { + # match matcher_result { + # MatcherResult::Match => { + # format!("is equal to {:?} the way I define it", self.expected) + # } + # MatcherResult::NoMatch => { + # format!("isn't equal to {:?} the way I define it", self.expected) + # } + # } + # } + # } + # + pub fn eq_my_way<T: PartialEq + Debug>(expected: T) -> impl Matcher<ActualT = T> { + MyEqMatcher { expected } + } + ``` + + The new matcher can then be used in the assertion macros: + +``` +# use googletest::prelude::*; +# use googletest::matcher::{Matcher, MatcherResult}; +# use std::fmt::Debug; +# +# struct MyEqMatcher<T> { +# expected: T, +# } +# +# impl<T: PartialEq + Debug> Matcher for MyEqMatcher<T> { +# type ActualT = T; +# +# fn matches(&self, actual: &Self::ActualT) -> MatcherResult { +# if self.expected == *actual { +# MatcherResult::Match +# } else { +# MatcherResult::NoMatch +# } +# } +# +# fn describe(&self, matcher_result: MatcherResult) -> String { +# match matcher_result { +# MatcherResult::Match => { +# format!("is equal to {:?} the way I define it", self.expected) +# } +# MatcherResult::NoMatch => { +# format!("isn't equal to {:?} the way I define it", self.expected) +# } +# } +# } +# } +# +# pub fn eq_my_way<T: PartialEq + Debug>(expected: T) -> impl Matcher<ActualT = T> { +# MyEqMatcher { expected } +# } +# /* The attribute macro would prevent the function from being compiled in a doctest. +#[googletest::test] +# */ +fn should_be_equal_by_my_definition() { +# googletest::internal::test_outcome::TestOutcome::init_current_test_outcome(); + expect_that!(10, eq_my_way(10)); +# googletest::internal::test_outcome::TestOutcome::close_current_test_outcome::<&str>(Ok(())) +# .unwrap(); +} +# should_be_equal_by_my_definition(); +``` + +## Non-fatal assertions + +Using non-fatal assertions, a single test is able to log multiple assertion +failures. Any single assertion failure causes the test to be considered +having failed, but execution continues until the test completes or otherwise +aborts. + +To make a non-fatal assertion, use the macro [`expect_that!`]. The test must +also be marked with [`googletest::test`][crate::test] instead of the +Rust-standard `#[test]`. + +```no_run +use googletest::prelude::*; + +#[googletest::test] +fn three_non_fatal_assertions() { + let value = 2; + expect_that!(value, eq(2)); // Passes; test still considered passing. + expect_that!(value, eq(3)); // Fails; logs failure and marks the test failed. + expect_that!(value, eq(4)); // A second failure, also logged. +} +``` + +This can be used in the same tests as `verify_that!`, in which case the test +function must also return [`Result<()>`]: + +```no_run +use googletest::prelude::*; + +# /* Make sure this also compiles as a doctest. +#[googletest::test] +# */ +fn failing_non_fatal_assertion() -> Result<()> { + let value = 2; + expect_that!(value, eq(3)); // Just marks the test as having failed. + verify_that!(value, eq(2))?; // Passes, so does not abort the test. + Ok(()) // Because of the failing expect_that! call above, the + // test fails despite returning Ok(()) +} +``` + +```no_run +use googletest::prelude::*; + +#[googletest::test] +fn failing_fatal_assertion_after_non_fatal_assertion() -> Result<()> { + let value = 2; + expect_that!(value, eq(2)); // Passes; test still considered passing. + verify_that!(value, eq(3))?; // Fails and aborts the test. + expect_that!(value, eq(3)); // Never executes, since the test already aborted. + Ok(()) +} +``` + +## Predicate assertions + +The macro [`verify_pred!`] provides predicate assertions analogous to +GoogleTest's `EXPECT_PRED` family of macros. Wrap an invocation of a +predicate in a `verify_pred!` invocation to turn that into a test assertion +which passes precisely when the predicate returns `true`: + +``` +# use googletest::prelude::*; +fn stuff_is_correct(x: i32, y: i32) -> bool { + x == y +} + +# fn run_test() -> Result<()> { +let x = 3; +let y = 4; +verify_pred!(stuff_is_correct(x, y))?; +# Ok(()) +# } +# run_test().unwrap_err(); +``` + +The assertion failure message shows the arguments and the values to which +they evaluate: + +```text +stuff_is_correct(x, y) was false with + x = 3, + y = 4 +``` + +The `verify_pred!` invocation evaluates to a [`Result<()>`] just like +[`verify_that!`]. There is also a macro [`expect_pred!`] to make a non-fatal +predicaticate assertion. + +## Unconditionally generating a test failure + +The macro [`fail!`] unconditionally evaluates to a `Result` indicating a +test failure. It can be used analogously to [`verify_that!`] and +[`verify_pred!`] to cause a test to fail, with an optional formatted +message: + +``` +# use googletest::prelude::*; +# /* The attribute macro would prevent the function from being compiled in a doctest. +#[test] +# */ +fn always_fails() -> Result<()> { + fail!("This test must fail with {}", "today") +} +# always_fails().unwrap_err(); +``` + +## Integrations with other crates + +GoogleTest Rust includes integrations with the +[Anyhow](https://crates.io/crates/anyhow) and +[Proptest](https://crates.io/crates/proptest) crates to simplify turning +errors from those crates into test failures. + +To use this, activate the `anyhow`, respectively `proptest` feature in +GoogleTest Rust and invoke the extension method [`into_test_result()`] on a +`Result` value in your test. For example: + +``` +# use googletest::prelude::*; +# #[cfg(feature = "anyhow")] +# use anyhow::anyhow; +# #[cfg(feature = "anyhow")] +# /* The attribute macro would prevent the function from being compiled in a doctest. +#[test] +# */ +fn has_anyhow_failure() -> Result<()> { + Ok(just_return_error().into_test_result()?) +} + +# #[cfg(feature = "anyhow")] +fn just_return_error() -> anyhow::Result<()> { + anyhow::Result::Err(anyhow!("This is an error")) +} +# #[cfg(feature = "anyhow")] +# has_anyhow_failure().unwrap_err(); +``` + +One can convert Proptest test failures into GoogleTest test failures when the +test is invoked with +[`TestRunner::run`](https://docs.rs/proptest/latest/proptest/test_runner/struct.TestRunner.html#method.run): + +``` +# use googletest::prelude::*; +# #[cfg(feature = "proptest")] +# use proptest::test_runner::{Config, TestRunner}; +# #[cfg(feature = "proptest")] +# /* The attribute macro would prevent the function from being compiled in a doctest. +#[test] +# */ +fn numbers_are_greater_than_zero() -> Result<()> { + let mut runner = TestRunner::new(Config::default()); + runner.run(&(1..100i32), |v| Ok(verify_that!(v, gt(0))?)).into_test_result() +} +# #[cfg(feature = "proptest")] +# numbers_are_greater_than_zero().unwrap(); +``` + +Similarly, when the `proptest` feature is enabled, GoogleTest assertion failures +can automatically be converted into Proptest +[`TestCaseError`](https://docs.rs/proptest/latest/proptest/test_runner/enum.TestCaseError.html) +through the `?` operator as the example above shows. + +[`and_log_failure()`]: GoogleTestSupport::and_log_failure +[`into_test_result()`]: IntoTestResult::into_test_result +[`Matcher`]: matcher::Matcher diff --git a/src/assertions.rs b/src/assertions.rs new file mode 100644 index 0000000..7664486 --- /dev/null +++ b/src/assertions.rs @@ -0,0 +1,455 @@ +// 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. + +// There are no visible documentation elements in this module; the declarative +// macros are documented at the top level. +#![doc(hidden)] + +/// Checks whether the `Matcher` given by the second argument matches the first +/// argument. +/// +/// Evaluates to `Result::Ok(())` if the matcher matches and +/// `Result::Err(TestAssertionFailure)` if it does not. The caller must then +/// decide how to handle the `Err` variant. It has a few options: +/// +/// * Abort the current function with the `?` operator. This requires that the +/// function return a suitable `Result`. +/// * Log the test failure and continue by calling the method +/// `and_log_failure`. +/// +/// Of course, one can also use all other standard methods on `Result`. +/// +/// **Invoking this macro by itself does not cause a test failure to be recorded +/// or output.** The resulting `Result` must be handled as described above to +/// cause the test to be recorded as a failure. +/// +/// Example: +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// verify_that!(42, eq(42))?; // This will pass. +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// # fn should_fail() -> Result<()> { +/// # googletest::internal::test_outcome::TestOutcome::init_current_test_outcome(); +/// verify_that!(42, eq(123)).and_log_failure(); +/// // This will log a test failure and allow execution to continue. +/// let _ = verify_that!(42, eq(123)); // This will do nothing. +/// verify_that!(42, eq(123))?; // This will fail, returning immediately. +/// verify_that!(42, eq(0))?; // This will not run. +/// # googletest::internal::test_outcome::TestOutcome::close_current_test_outcome::<&str>(Ok(())) +/// # .unwrap_err(); +/// # Ok(()) +/// # } +/// # verify_that!(should_fail(), err(displays_as(contains_substring("Expected: is equal to 123")))) +/// # .unwrap(); +/// ``` +/// +/// This macro has special support for matching against container. Namely: +/// * `verify_that!(actual, [m1, m2, ...])` is equivalent to +/// `verify_that!(actual, elements_are![m1, m2, ...])` +/// * `verify_that!(actual, {m1, m2, ...})` is equivalent to +/// `verify_that!(actual, unordered_elements_are![m1, m2, ...])` +/// +/// ## Matching against tuples +/// +/// One can match against a tuple by constructing a tuple of matchers as +/// follows: +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// verify_that!((123, 456), (eq(123), eq(456)))?; // Passes +/// # Ok(()) +/// # } +/// # fn should_fail() -> Result<()> { +/// verify_that!((123, 456), (eq(123), eq(0)))?; // Fails: second matcher does not match +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// # should_fail().unwrap_err(); +/// ``` +/// +/// This also works with composed matchers: +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// verify_that!((123, 456), not((eq(456), eq(123))))?; // Passes +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// ``` +/// +/// Matchers must correspond to the actual tuple in count and type. Otherwise +/// the test will fail to compile. +/// +/// ```compile_fail +/// # use googletest::prelude::*; +/// # fn should_not_compile() -> Result<()> { +/// verify_that!((123, 456), (eq(123),))?; // Does not compile: wrong tuple size +/// verify_that!((123, "A string"), (eq(123), eq(456)))?; // Does not compile: wrong type +/// # Ok(()) +/// # } +/// ``` +/// +/// All fields must be covered by matchers. Use +/// [`anything`][crate::matchers::anything] for fields which are not relevant +/// for the test. +/// +/// ``` +/// # use googletest::prelude::*; +/// verify_that!((123, 456), (eq(123), anything())) +/// # .unwrap(); +/// ``` +/// +/// This supports tuples of up to 12 elements. Tuples longer than that do not +/// automatically inherit the `Debug` trait from their members, so are generally +/// 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),+]) => { + $crate::assertions::internal::check_matcher( + &$actual, + $crate::elements_are![$($expecteds),+], + stringify!($actual), + $crate::internal::source_location::SourceLocation::new(file!(), line!(), column!()), + ) + }; + ($actual:expr, {$($expecteds:expr),+}) => { + $crate::assertions::internal::check_matcher( + &$actual, + $crate::unordered_elements_are![$($expecteds),+], + stringify!($actual), + $crate::internal::source_location::SourceLocation::new(file!(), line!(), column!()), + ) + }; + ($actual:expr, $expected:expr) => { + $crate::assertions::internal::check_matcher( + &$actual, + $expected, + stringify!($actual), + $crate::internal::source_location::SourceLocation::new(file!(), line!(), column!()), + ) + }; +} + +/// Asserts that the given predicate applied to the given arguments returns +/// true. +/// +/// Similarly to [`verify_that`], this evaluates to a `Result` whose `Ok` +/// variant indicates that the given predicate returned true and whose `Err` +/// variant indicates that it returned false. +/// +/// The failure message contains detailed information about the arguments. For +/// example: +/// +/// ``` +/// # use googletest::prelude::*; +/// fn equals_modulo(a: i32, b: i32, n: i32) -> bool { a % n == b % n } +/// +/// # /* The attribute macro would prevent the function from being compiled in a doctest. +/// #[test] +/// # */ +/// fn test() -> Result<()> { +/// let a = 1; +/// let b = 7; +/// let n = 5; +/// verify_pred!(equals_modulo(a, b, n))?; +/// Ok(()) +/// } +/// # verify_that!( +/// # test(), +/// # err(displays_as(contains_substring("equals_modulo(a, b, n) was false with"))) +/// # ).unwrap(); +/// ``` +/// +/// This results in the following message: +/// +/// ```text +/// equals_modulo(a, b, n) was false with +/// a = 1, +/// b = 7, +/// n = 5 +/// ``` +/// +/// The function passed to this macro must return `bool`. Each of the arguments +/// must evaluate to a type implementing [`std::fmt::Debug`]. The debug output +/// is used to construct the failure message. +/// +/// The predicate can also be a method on a struct, e.g.: +/// +/// ```ignore +/// struct AStruct {}; +/// +/// impl AStruct { +/// fn equals_modulo(...) {...} +/// } +/// +/// verify_pred!((AStruct {}).equals_modulo(a, b, n))?; +/// ``` +/// +/// **Warning:** This macro assumes that the arguments passed to the predicate +/// are either *variables* or *calls to pure functions*. If two subsequent +/// invocations to any of the expresssions passed as arguments result in +/// different values, then the output message of a test failure will deviate +/// from the values actually passed to the predicate. For this reason, *always +/// assign the outputs of non-pure functions to variables before using them in +/// this macro. For example: +/// +/// ```ignore +/// let output = generate_random_number(); // Assigned outside of verify_pred. +/// verify_pred!(is_sufficiently_random(output))?; +/// ``` +#[macro_export] +macro_rules! verify_pred { + ([$($predicate:tt)*]($($arg:tt),* $(,)?)) => { + if !$($predicate)*($($arg),*) { + $crate::assertions::internal::report_failed_predicate( + concat!(stringify!($($predicate)*), stringify!(($($arg),*))), + vec![$((format!(concat!(stringify!($arg), " = {:?}"), $arg))),*], + $crate::internal::source_location::SourceLocation::new( + file!(), + line!(), + column!(), + ), + ) + } else { + Ok(()) + } + }; + + ([$($predicate:tt)*] $first:tt $($rest:tt)*) => { + $crate::verify_pred!([$($predicate)* $first] $($rest)*) + }; + + ($first:tt $($rest:tt)*) => { + $crate::verify_pred!([$first] $($rest)*) + }; +} + +/// Evaluates to a `Result` which contains an `Err` variant with the given test +/// failure message. +/// +/// This can be used to force the test to fail if its execution reaches a +/// particular point. For example: +/// +/// ```ignore +/// match some_value { +/// ExpectedVariant => {...} +/// UnwantedVaraint => { +/// fail!("This thing which should not happen happened anyway")?; +/// } +/// } +/// ``` +/// +/// One may include formatted arguments in the failure message: +/// +/// ```ignore +/// match some_value { +/// ExpectedVariant => {...} +/// UnwantedVaraint => { +/// fail!("This thing which should not happen happened anyway: {}", some_value)?; +/// } +/// } +/// ``` +/// +/// One may also omit the message, in which case the test failure message will +/// be generic: +/// +/// ```ignore +/// match some_value { +/// ExpectedVariant => {...} +/// UnwantedVaraint => { +/// fail!()?; +/// } +/// } +/// ``` +/// +/// Unlike `panic!` but analogously to [`verify_that`] and [`verify_pred`], this +/// macro has no effect on the flow of control but instead returns a `Result` +/// which must be handled by the invoking function. This can be done with the +/// question mark operator (as above) or the method +/// [`and_log_failure`](crate::GoogleTestSupport::and_log_failure). +#[macro_export] +macro_rules! fail { + ($($message:expr),+) => {{ + // We wrap this in a function so that we can annotate it with the must_use attribute. + // must_use on expressions is still experimental. + #[must_use = "The assertion result must be evaluated to affect the test result."] + fn create_fail_result(message: String) -> $crate::Result<()> { + Err($crate::internal::test_outcome::TestAssertionFailure::create(format!( + "{}\n{}", + message, + $crate::internal::source_location::SourceLocation::new( + file!(), + line!(), + column!(), + ), + ))) + } + create_fail_result(format!($($message),*)) + }}; + + () => { fail!("Test failed") }; +} + +/// Matches the given value against the given matcher, panicking if it does not +/// match. +/// +/// This is analogous to assertions in most Rust test libraries, where a failed +/// assertion causes a panic. +/// +/// **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) => { + match $crate::verify_that!($actual, $expected) { + 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 +/// true, panicking if it does not. +/// +/// **Note for users of [GoogleTest for C++](http://google.github.io/googletest/):** +/// This differs from the `ASSERT_PRED*` family of macros in that it panics +/// rather than triggering an early return from the invoking function. To get +/// behaviour equivalent to `ASSERT_PRED*`, use [`verify_pred!`] with the `?` +/// operator. +#[macro_export] +macro_rules! assert_pred { + ($($content:tt)*) => { + match $crate::verify_pred!($($content)*) { + 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); + } + } + }; +} + +/// Matches the given value against the given matcher, marking the test as +/// failed but continuing execution if it does not match. +/// +/// This is a *non-fatal* assertion: the test continues +/// execution in the event of assertion failure. +/// +/// This can only be invoked inside tests with the +/// [`googletest::test`][crate::test] attribute. The assertion must +/// occur in the same thread as that running the test itself. +/// +/// Invoking this macro is equivalent to using +/// [`and_log_failure`](crate::GoogleTestSupport::and_log_failure) as follows: +/// +/// ```ignore +/// verify_that!(actual, expected).and_log_failure() +/// ``` +#[macro_export] +macro_rules! expect_that { + ($actual:expr, $expected:expr) => {{ + use $crate::GoogleTestSupport; + $crate::verify_that!($actual, $expected).and_log_failure(); + }}; +} + +/// Asserts that the given predicate applied to the given arguments returns +/// true, failing the test but continuing execution if not. +/// +/// This is a *non-fatal* predicate assertion: the test +/// continues execution in the event of assertion failure. +/// +/// This can only be invoked inside tests with the +/// [`googletest::test`][crate::test] attribute. The assertion must +/// occur in the same thread as that running the test itself. +/// +/// Invoking this macro is equivalent to using +/// [`and_log_failure`](crate::GoogleTestSupport::and_log_failure) as follows: +/// +/// ```ignore +/// verify_pred!(predicate(...)).and_log_failure() +/// ``` +#[macro_export] +macro_rules! expect_pred { + ($($content:tt)*) => {{ + use $crate::GoogleTestSupport; + $crate::verify_pred!($($content)*).and_log_failure(); + }}; +} + +/// Functions for use only by the procedural macros in this module. +/// +/// **For internal use only. API stablility is not guaranteed!** +#[doc(hidden)] +pub mod internal { + use crate::{ + internal::{source_location::SourceLocation, test_outcome::TestAssertionFailure}, + matcher::{create_assertion_failure, Matcher, MatcherResult}, + }; + use std::fmt::Debug; + + /// Checks whether the matcher `expected` matches the value `actual`, adding + /// a test failure report if it does not match. + /// + /// Returns `Ok(())` if the value matches and `Err(())` if it does not + /// match. + /// + /// **For internal use only. API stablility is not guaranteed!** + #[must_use = "The assertion result must be evaluated to affect the test result."] + pub fn check_matcher<T: Debug + ?Sized>( + actual: &T, + expected: impl Matcher<ActualT = T>, + actual_expr: &'static str, + source_location: SourceLocation, + ) -> Result<(), TestAssertionFailure> { + match expected.matches(actual) { + MatcherResult::Match => Ok(()), + MatcherResult::NoMatch => { + Err(create_assertion_failure(&expected, actual, actual_expr, source_location)) + } + } + } + + /// Constructs a `Result::Err(TestAssertionFailure)` for a predicate failure + /// as produced by the macro [`crate::verify_pred`]. + /// + /// This intended only for use by the macro [`crate::verify_pred`]. + /// + /// **For internal use only. API stablility is not guaranteed!** + #[must_use = "The assertion result must be evaluated to affect the test result."] + pub fn report_failed_predicate( + actual_expr: &'static str, + formatted_arguments: Vec<String>, + source_location: SourceLocation, + ) -> Result<(), TestAssertionFailure> { + Err(TestAssertionFailure::create(format!( + "{} was false with\n {}\n{}", + actual_expr, + formatted_arguments.join(",\n "), + source_location, + ))) + } +} diff --git a/src/internal/mod.rs b/src/internal/mod.rs new file mode 100644 index 0000000..2f37418 --- /dev/null +++ b/src/internal/mod.rs @@ -0,0 +1,18 @@ +// 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. + +#![doc(hidden)] + +pub mod source_location; +pub mod test_outcome; diff --git a/src/internal/source_location.rs b/src/internal/source_location.rs new file mode 100644 index 0000000..4520c92 --- /dev/null +++ b/src/internal/source_location.rs @@ -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. + +use std::fmt::{Display, Error, Formatter}; + +/// Encapsulates a location in source code. +/// +/// This is intended to report the location of an assertion which failed to +/// stdout. +/// +/// **For internal use only. API stablility is not guaranteed!** +#[doc(hidden)] +pub struct SourceLocation { + file: &'static str, + line: u32, + column: u32, +} + +impl SourceLocation { + /// Constructs a new [`SourceLocation`]. + /// + /// **For internal use only. API stablility is not guaranteed!** + #[doc(hidden)] + pub fn new(file: &'static str, line: u32, column: u32) -> Self { + Self { file, line, column } + } +} + +impl Display for SourceLocation { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { + write!(f, " at {}:{}:{}", self.file, self.line, self.column) + } +} diff --git a/src/internal/test_outcome.rs b/src/internal/test_outcome.rs new file mode 100644 index 0000000..2171cc7 --- /dev/null +++ b/src/internal/test_outcome.rs @@ -0,0 +1,211 @@ +// 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. + +use std::cell::{RefCell, RefMut}; +use std::fmt::{Debug, Display, Error, Formatter}; +use std::thread_local; + +/// The outcome hitherto of running a test. +/// +/// This is kept as a running record as the test progresses. One can access it +/// with `TestOutcome::with_current_test_outcome`. +/// +/// **For internal use only. API stablility is not guaranteed!** +#[doc(hidden)] +pub enum TestOutcome { + /// The test ran or is currently running and no assertions have failed. + Success, + /// The test ran or is currently running and at least one assertion has + /// failed. + Failure, +} + +thread_local! { + static CURRENT_TEST_OUTCOME: RefCell<Option<TestOutcome>> = RefCell::new(None); +} + +impl TestOutcome { + /// Resets the current test's [`TestOutcome`]. + /// + /// This is intended only for use by the attribute macro + /// `#[googletest::test]`. + /// + /// **For internal use only. API stablility is not guaranteed!** + #[doc(hidden)] + pub fn init_current_test_outcome() { + Self::with_current_test_outcome(|mut current_test_outcome| { + *current_test_outcome = Some(TestOutcome::Success); + }) + } + + /// Evaluates the current test's [`TestOutcome`], producing a suitable + /// `Result`. + /// + /// The parameter `result` is the value returned by the test function + /// itself. This returns `Result::Err` with a `Display`-formatted string of + /// the error if `result` is `Result::Err`. + /// + /// Otherwise, this returns `Result::Err` precisely when a test failure has + /// been recorded with + /// [`and_log_failure`](crate::GoogleTestSupport::and_log_failure). + /// + /// **For internal use only. API stablility is not guaranteed!** + #[doc(hidden)] + pub fn close_current_test_outcome<E: Display>( + inner_result: Result<(), E>, + ) -> Result<(), TestFailure> { + TestOutcome::with_current_test_outcome(|mut outcome| { + let outer_result = match &*outcome { + Some(TestOutcome::Success) => match inner_result { + Ok(()) => Ok(()), + Err(_) => Err(TestFailure), + }, + Some(TestOutcome::Failure) => Err(TestFailure), + None => { + panic!("No test context found. This indicates a bug in GoogleTest.") + } + }; + if let Err(fatal_assertion_failure) = inner_result { + println!("{fatal_assertion_failure}"); + } + *outcome = None; + outer_result + }) + } + + /// Returns a `Result` corresponding to the outcome of the currently running + /// test. + pub(crate) fn get_current_test_outcome() -> Result<(), TestAssertionFailure> { + TestOutcome::with_current_test_outcome(|mut outcome| { + let outcome = outcome + .as_mut() + .expect("No test context found. This indicates a bug in GoogleTest."); + match outcome { + TestOutcome::Success => Ok(()), + TestOutcome::Failure => Err(TestAssertionFailure::create("Test failed".into())), + } + }) + } + + /// Records that the currently running test has failed. + fn fail_current_test() { + TestOutcome::with_current_test_outcome(|mut outcome| { + let outcome = outcome + .as_mut() + .expect("No test context found. This indicates a bug in GoogleTest."); + *outcome = TestOutcome::Failure; + }) + } + + /// Runs `action` with the [`TestOutcome`] for the currently running test. + /// + /// This is primarily intended for use by assertion macros like + /// `expect_that!`. + fn with_current_test_outcome<T>(action: impl FnOnce(RefMut<Option<TestOutcome>>) -> T) -> T { + CURRENT_TEST_OUTCOME.with(|current_test_outcome| action(current_test_outcome.borrow_mut())) + } + + /// Ensure that there is a test context present and panic if there is not. + pub(crate) fn ensure_text_context_present() { + TestOutcome::with_current_test_outcome(|outcome| { + outcome.as_ref().expect( + " +No test context found. + * Did you annotate the test with googletest::test? + * Is the assertion running in the original test thread? +", + ); + }) + } +} + +/// A marking struct indicating that a test has failed. +/// +/// This exists to implement the [Error][std::error::Error] trait. It displays +/// to a message indicating that the actual test assertion failure messages are +/// in the text above. +pub struct TestFailure; + +impl std::error::Error for TestFailure {} + +impl std::fmt::Debug for TestFailure { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { + writeln!(f, "See failure output above")?; + Ok(()) + } +} + +impl std::fmt::Display for TestFailure { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { + writeln!(f, "See failure output above")?; + Ok(()) + } +} + +/// A report that a single test assertion failed. +/// +/// **For internal use only. API stablility is not guaranteed!** +#[doc(hidden)] +#[derive(Clone)] +pub struct TestAssertionFailure { + /// A human-readable formatted string describing the error. + pub description: String, + pub custom_message: Option<String>, +} + +impl TestAssertionFailure { + /// Creates a new instance with the given `description`. + /// + /// **For internal use only. API stablility is not guaranteed!** + pub fn create(description: String) -> Self { + Self { description, custom_message: None } + } + + pub(crate) fn log(&self) { + TestOutcome::fail_current_test(); + print!("{}", self); + } +} + +impl Display for TestAssertionFailure { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { + writeln!(f, "{}", self.description)?; + if let Some(custom_message) = &self.custom_message { + writeln!(f, "{}", custom_message)?; + } + Ok(()) + } +} + +// The standard Rust test harness outputs the TestAssertionFailure with the +// Debug trait. We want the output to be formatted, so we use a custom Debug +// implementation which defers to Display. +impl Debug for TestAssertionFailure { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { + Display::fmt(self, f) + } +} + +impl<T: std::error::Error> From<T> for TestAssertionFailure { + fn from(value: T) -> Self { + TestAssertionFailure::create(format!("{value}")) + } +} + +#[cfg(feature = "proptest")] +impl From<TestAssertionFailure> for proptest::test_runner::TestCaseError { + fn from(value: TestAssertionFailure) -> Self { + proptest::test_runner::TestCaseError::Fail(format!("{value}").into()) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..cec6da0 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,273 @@ +// 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. + +#![doc = include_str!("../crate_docs.md")] + +extern crate googletest_macro; + +#[cfg(test)] +extern crate quickcheck; + +#[macro_use] +pub mod assertions; +pub mod internal; +pub mod matcher; +pub mod matcher_support; +pub mod matchers; + +/// Re-exports of the symbols in this crate which are most likely to be used. +/// +/// This includes: +/// * All assertion macros, +/// * Traits and type definitions normally used by tests, and +/// * All built-in matchers. +/// +/// Typically, one imports everything in the prelude in one's test module: +/// +/// ``` +/// mod tests { +/// use googletest::prelude::*; +/// } +/// ``` +pub mod prelude { + pub use super::matcher::Matcher; + pub use super::matchers::*; + pub use super::verify_current_test_outcome; + pub use super::GoogleTestSupport; + pub use super::IntoTestResult; + 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; + +use internal::test_outcome::{TestAssertionFailure, TestOutcome}; + +/// A `Result` whose `Err` variant indicates a test failure. +/// +/// All test functions should return `Result<()>`. +/// +/// 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: +/// +/// ```ignore +/// fn load_file_content_as_string() -> Result<String> { +/// let file_stream = load_file().err_to_test_failure()?; +/// Ok(file_stream.to_string()) +/// } +/// ``` +/// +/// The `Err` variant contains a [`TestAssertionFailure`] which carries the data +/// of the (fatal) assertion failure which generated this result. Non-fatal +/// assertion failures, which log the failure and report the test as having +/// failed but allow it to continue running, are not encoded in this type. +pub type Result<T> = std::result::Result<T, TestAssertionFailure>; + +/// Returns a [`Result`] corresponding to the outcome of the currently running +/// test. +/// +/// This returns `Result::Err` precisely if the current test has recorded at +/// least one test assertion failure via [`expect_that!`][crate::expect_that], +/// [`expect_pred!`][crate::expect_pred], or +/// [`GoogleTestSupport::and_log_failure`]. It can be used in concert with the +/// `?` operator to continue execution of the test conditionally on there not +/// having been any failure yet. +/// +/// This requires the use of the [`#[googletest::test]`][crate::test] attribute +/// macro. +/// +/// ``` +/// # use googletest::prelude::*; +/// # /* Make sure this also compiles as a doctest. +/// #[googletest::test] +/// # */ +/// # fn foo() -> u32 { 1 } +/// # fn bar() -> u32 { 2 } +/// fn should_fail_and_not_execute_last_assertion() -> Result<()> { +/// # googletest::internal::test_outcome::TestOutcome::init_current_test_outcome(); +/// expect_that!(foo(), eq(2)); // May fail, but will not abort the test. +/// expect_that!(bar(), gt(1)); // May fail, but will not abort the test. +/// verify_current_test_outcome()?; // Aborts the test if one of the previous assertions failed. +/// verify_that!(foo(), gt(0)) // Does not execute if the line above aborts. +/// } +/// # verify_that!(should_fail_and_not_execute_last_assertion(), err(displays_as(contains_substring("Test failed")))).unwrap(); +/// ``` +pub fn verify_current_test_outcome() -> Result<()> { + TestOutcome::get_current_test_outcome() +} + +/// Adds to `Result` support for GoogleTest Rust functionality. +pub trait GoogleTestSupport { + /// If `self` is a `Result::Err`, writes to `stdout` a failure report + /// and marks the test failed. Otherwise, does nothing. + /// + /// This can be used for non-fatal test assertions, for example: + /// + /// ``` + /// # use googletest::prelude::*; + /// # use googletest::internal::test_outcome::TestOutcome; + /// # TestOutcome::init_current_test_outcome(); + /// let actual = 42; + /// verify_that!(actual, eq(42)).and_log_failure(); + /// // Test still passing; nothing happens + /// verify_that!(actual, eq(10)).and_log_failure(); + /// // Test now fails and failure output to stdout + /// verify_that!(actual, eq(100)).and_log_failure(); + /// // Test still fails and new failure also output to stdout + /// # TestOutcome::close_current_test_outcome::<&str>(Ok(())).unwrap_err(); + /// ``` + fn and_log_failure(self); + + /// Adds `message` to the logged failure message if `self` is a + /// `Result::Err`. Otherwise, does nothing. + /// + /// If this method is called more than once, only `message` from the last + /// invocation is output. + /// + /// For example: + /// + /// ``` + /// # use googletest::prelude::*; + /// # fn should_fail() -> Result<()> { + /// let actual = 0; + /// verify_that!(actual, eq(42)).failure_message("Actual was wrong!")?; + /// # Ok(()) + /// # } + /// # verify_that!(should_fail(), err(displays_as(contains_substring("Actual was wrong")))) + /// # .unwrap(); + /// ``` + /// + /// results in the following failure message: + /// + /// ```text + /// Expected: actual equal to 42 + /// but was: 0 + /// Actual was wrong! + /// ``` + /// + /// One can pass a `String` too: + /// + /// ``` + /// # use googletest::prelude::*; + /// # fn should_fail() -> Result<()> { + /// let actual = 0; + /// verify_that!(actual, eq(42)) + /// .failure_message(format!("Actual {} was wrong!", actual))?; + /// # Ok(()) + /// # } + /// # verify_that!(should_fail(), err(displays_as(contains_substring("Actual 0 was wrong")))) + /// # .unwrap(); + /// ``` + /// + /// However, consider using [`GoogleTestSupport::with_failure_message`] + /// instead in that case to avoid unnecessary memory allocation when the + /// message is not needed. + fn failure_message(self, message: impl Into<String>) -> Self; + + /// Adds the output of the closure `provider` to the logged failure message + /// if `self` is a `Result::Err`. Otherwise, does nothing. + /// + /// This is analogous to [`GoogleTestSupport::failure_message`] but + /// only executes the closure `provider` if it actually produces the + /// message, thus saving possible memory allocation. + /// + /// ``` + /// # use googletest::prelude::*; + /// # fn should_fail() -> Result<()> { + /// let actual = 0; + /// verify_that!(actual, eq(42)) + /// .with_failure_message(|| format!("Actual {} was wrong!", actual))?; + /// # Ok(()) + /// # } + /// # verify_that!(should_fail(), err(displays_as(contains_substring("Actual 0 was wrong")))) + /// # .unwrap(); + /// ``` + fn with_failure_message(self, provider: impl FnOnce() -> String) -> Self; +} + +impl<T> GoogleTestSupport for std::result::Result<T, TestAssertionFailure> { + fn and_log_failure(self) { + TestOutcome::ensure_text_context_present(); + if let Err(failure) = self { + failure.log(); + } + } + + fn failure_message(mut self, message: impl Into<String>) -> Self { + if let Err(ref mut failure) = self { + failure.custom_message = Some(message.into()); + } + self + } + + fn with_failure_message(mut self, provider: impl FnOnce() -> String) -> Self { + if let Err(ref mut failure) = self { + failure.custom_message = Some(provider()); + } + self + } +} + +/// Provides an extension method for converting an arbitrary type into a +/// [`Result`]. +/// +/// A type can implement this trait to provide an easy way to return immediately +/// from a test in conjunction with the `?` operator. This is useful for +/// [`Result`][std::result::Result] types whose `Result::Err` variant does not +/// implement [`std::error::Error`]. +/// +/// There is an implementation of this trait for [`anyhow::Error`] (which does +/// not implement `std::error::Error`) when the `anyhow` feature is enabled. +/// Importing this trait allows one to easily map [`anyhow::Error`] to a test +/// failure: +/// +/// ```ignore +/// #[test] +/// fn should_work() -> Result<()> { +/// let value = something_which_can_fail().into_test_result()?; +/// ... +/// } +/// +/// fn something_which_can_fail() -> anyhow::Result<...> { ... } +/// ``` +pub trait IntoTestResult<T> { + /// Converts this instance into a [`Result`]. + /// + /// Typically, the `Self` type is itself a [`std::result::Result`]. This + /// method should then map the `Err` variant to a [`TestAssertionFailure`] + /// and leave the `Ok` variant unchanged. + fn into_test_result(self) -> Result<T>; +} + +#[cfg(feature = "anyhow")] +impl<T> IntoTestResult<T> for std::result::Result<T, anyhow::Error> { + fn into_test_result(self) -> std::result::Result<T, TestAssertionFailure> { + self.map_err(|e| TestAssertionFailure::create(format!("{e}"))) + } +} + +#[cfg(feature = "proptest")] +impl<OkT, CaseT: std::fmt::Debug> IntoTestResult<OkT> + for std::result::Result<OkT, proptest::test_runner::TestError<CaseT>> +{ + fn into_test_result(self) -> std::result::Result<OkT, TestAssertionFailure> { + self.map_err(|e| TestAssertionFailure::create(format!("{e}"))) + } +} diff --git a/src/matcher.rs b/src/matcher.rs new file mode 100644 index 0000000..83a4475 --- /dev/null +++ b/src/matcher.rs @@ -0,0 +1,265 @@ +// 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. + +//! The components required to implement matchers. + +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 std::fmt::Debug; + +/// An interface for checking an arbitrary condition on a datum. +pub trait Matcher { + /// The type against which this matcher matches. + type ActualT: Debug + ?Sized; + + /// Returns whether the condition matches the datum `actual`. + /// + /// The trait implementation defines what it means to "match". Often the + /// matching condition is based on data stored in the matcher. For example, + /// `eq` matches when its stored expected value is equal (in the sense of + /// the `==` operator) to the value `actual`. + fn matches(&self, actual: &Self::ActualT) -> MatcherResult; + + /// Returns a description of `self` or a negative description if + /// `matcher_result` is `DoesNotMatch`. + /// + /// The function should print a verb phrase that describes the property a + /// value matching, respectively not matching, this matcher should have. + /// The subject of the verb phrase is the value being matched. + /// + /// The output appears next to `Expected` in an assertion failure message. + /// For example: + /// + /// ```text + /// Value of: ... + /// Expected: is equal to 7 + /// ^^^^^^^^^^^^^ + /// Actual: ... + /// ``` + /// + /// When the matcher contains one or more inner matchers, the implementation + /// should invoke [`Self::describe`] on the inner matchers to complete the + /// description. It should place the inner description at a point where a + /// verb phrase would fit. For example, the matcher + /// [`some`][crate::matchers::some] implements `describe` as follows: + /// + /// ```ignore + /// fn describe(&self, matcher_result: MatcherResult) -> String { + /// match matcher_result { + /// MatcherResult::Matches => { + /// format!("has a value which {}", self.inner.describe(MatcherResult::Matches)) + /// // Inner matcher invocation: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + /// } + /// MatcherResult::DoesNotMatch => {...} // Similar to the above + /// } + /// } + /// ``` + /// + /// The output expectation differs from that of + /// [`explain_match`][Self::explain_match] in that it is a verb phrase + /// (beginning with a verb like "is") rather than a relative clause + /// (beginning with "which" or "whose"). This difference is because the + /// 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; + + /// Prepares a [`String`] describing how the expected value + /// encoded in this instance matches or does not match the given value + /// `actual`. + /// + /// This should be in the form of a relative clause, i.e. something starting + /// with a relative pronoun such as "which" or "whose". It will appear next + /// to the actual value in an assertion failure. For example: + /// + /// ```text + /// Value of: ... + /// Expected: ... + /// Actual: ["Something"], which does not contain "Something else" + /// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + /// ``` + /// + /// The default implementation relies on [`describe`][Self::describe]. Thus + /// it does not make any use of the actual value itself, but rather only + /// whether the value is matched. + /// + /// Override the default implementation to provide additional context on why + /// a particular value matched or did not match. For example, the + /// [`container_eq`][crate::matchers::container_eq] matcher displays + /// information on which elements of the actual value were not present in + /// the expected value and vice versa. + /// + /// This implementation should be overridden in any matcher which contains + /// one or more inner matchers. The implementation should invoke + /// `explain_match` on the inner matchers, so that the generated match + /// explanation also reflects their implementation. Without this, the match + /// explanation of the inner matchers will not be able to make use of the + /// actual value at all. + /// + /// For example, the `explain_match` implementation of the matcher + /// [`points_to`][crate::matchers::points_to] defers immediately to the + /// inner matcher and appears as follows: + /// + /// ```ignore + /// fn explain_match(&self, actual: &Self::ActualT) -> String { + /// self.expected.explain_match(actual.deref()) + /// } + /// ``` + /// + /// The matcher can also provide some additional context before deferring to + /// an inner matcher. In that case it should invoke `explain_match` on the + /// 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) -> String { + format!("which {}", self.describe(self.matches(actual))) + } + + /// Constructs a matcher that matches both `self` and `right`. + /// + /// ``` + /// # use googletest::prelude::*; + /// # fn should_pass() -> Result<()> { + /// verify_that!("A string", starts_with("A").and(ends_with("string")))?; // Passes + /// # Ok(()) + /// # } + /// # fn should_fail_1() -> Result<()> { + /// verify_that!("A string", starts_with("Another").and(ends_with("string")))?; // Fails + /// # Ok(()) + /// # } + /// # fn should_fail_2() -> Result<()> { + /// verify_that!("A string", starts_with("A").and(ends_with("non-string")))?; // Fails + /// # Ok(()) + /// # } + /// # should_pass().unwrap(); + /// # should_fail_1().unwrap_err(); + /// # should_fail_2().unwrap_err(); + /// ``` + // TODO(b/264518763): Replace the return type with impl Matcher and reduce + // visibility of ConjunctionMatcher once impl in return position in trait + // methods is stable. + fn and<Right: Matcher<ActualT = Self::ActualT>>( + self, + right: Right, + ) -> ConjunctionMatcher<Self, Right> + where + Self: Sized, + { + ConjunctionMatcher::new(self, right) + } + + /// Constructs a matcher that matches when at least one of `self` or `right` + /// matches the input. + /// + /// ``` + /// # use googletest::prelude::*; + /// # fn should_pass() -> Result<()> { + /// verify_that!(10, eq(2).or(ge(5)))?; // Passes + /// verify_that!(10, eq(2).or(eq(5)).or(ge(9)))?; // Passes + /// # Ok(()) + /// # } + /// # fn should_fail() -> Result<()> { + /// verify_that!(10, eq(2).or(ge(15)))?; // Fails + /// # Ok(()) + /// # } + /// # should_pass().unwrap(); + /// # should_fail().unwrap_err(); + /// ``` + // TODO(b/264518763): Replace the return type with impl Matcher and reduce + // visibility of DisjunctionMatcher once impl in return position in trait + // methods is stable. + fn or<Right: Matcher<ActualT = Self::ActualT>>( + self, + right: Right, + ) -> DisjunctionMatcher<Self, Right> + where + Self: Sized, + { + DisjunctionMatcher::new(self, right) + } +} + +/// Any actual value whose debug length is greater than this value will be +/// pretty-printed. Otherwise, it will have normal debug output formatting. +const PRETTY_PRINT_LENGTH_THRESHOLD: usize = 60; + +/// Constructs a [`TestAssertionFailure`] reporting that the given `matcher` +/// does not match the value `actual`. +/// +/// The parameter `actual_expr` contains the expression which was evaluated to +/// obtain `actual`. +pub(crate) fn create_assertion_failure<T: Debug + ?Sized>( + matcher: &impl Matcher<ActualT = T>, + actual: &T, + actual_expr: &'static str, + source_location: SourceLocation, +) -> TestAssertionFailure { + let actual_formatted = format!("{actual:?}"); + let actual_formatted = if actual_formatted.len() > PRETTY_PRINT_LENGTH_THRESHOLD { + format!("{actual:#?}") + } else { + actual_formatted + }; + TestAssertionFailure::create(format!( + "\ +Value of: {actual_expr} +Expected: {} +Actual: {actual_formatted}, + {} +{source_location}", + matcher.describe(MatcherResult::Match), + matcher.explain_match(actual), + )) +} + +/// The result of applying a [`Matcher`] on an actual value. +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum MatcherResult { + /// The actual value matches according to the [`Matcher`] definition. + Match, + /// The actual value does not match according to the [`Matcher`] definition. + NoMatch, +} + +impl From<bool> for MatcherResult { + fn from(b: bool) -> Self { + if b { MatcherResult::Match } else { MatcherResult::NoMatch } + } +} + +impl From<MatcherResult> for bool { + fn from(matcher_result: MatcherResult) -> Self { + matcher_result.is_match() + } +} + +impl MatcherResult { + /// Returns `true` if `self` is [`MatcherResult::Match`], otherwise + /// `false`. + pub fn is_match(self) -> bool { + matches!(self, MatcherResult::Match) + } + + /// Returns `true` if `self` is [`MatcherResult::NoMatch`], otherwise + /// `false`. + pub fn is_no_match(self) -> bool { + matches!(self, MatcherResult::NoMatch) + } +} diff --git a/src/matcher_support/count_elements.rs b/src/matcher_support/count_elements.rs new file mode 100644 index 0000000..662dcc9 --- /dev/null +++ b/src/matcher_support/count_elements.rs @@ -0,0 +1,32 @@ +// 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. + +/// Counts the number of elements in `value`. +/// +/// This uses [`Iterator::size_hint`] when that function returns an +/// unambiguous answer, i.e., the upper bound exists and the lower and upper +/// bounds agree. Otherwise it iterates through `value` and counts the +/// elements. +pub(crate) fn count_elements<ContainerT: ?Sized>(value: &ContainerT) -> usize +where + for<'b> &'b ContainerT: IntoIterator, +{ + let iterator = value.into_iter(); + if let (lower, Some(higher)) = iterator.size_hint() { + if lower == higher { + return lower; + } + } + iterator.count() +} diff --git a/src/matcher_support/description.rs b/src/matcher_support/description.rs new file mode 100644 index 0000000..ce074e0 --- /dev/null +++ b/src/matcher_support/description.rs @@ -0,0 +1,410 @@ +// 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 new file mode 100644 index 0000000..8469f42 --- /dev/null +++ b/src/matcher_support/edit_distance.rs @@ -0,0 +1,648 @@ +// 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::Debug; + +/// Maximum number of edits which can exist before [`edit_list`] falls back to a +/// complete rewrite to produce the edit list. +/// +/// Increasing this limit increases the accuracy of [`edit_list`] while +/// quadratically increasing its worst-case runtime. +const MAX_DISTANCE: i32 = 25; + +/// The difference between two inputs as produced by [`edit_list`]. +#[derive(Debug)] +pub(crate) enum Difference<T> { + /// No differences were detected at all. + Equal, + + /// At most [`MAX_DISTANCE`] edits are required to convert one input to the + /// other. + /// + /// Contains the list of [`Edit`] to perform the transformation. + Editable(Vec<Edit<T>>), + + /// More than [`MAX_DISTANCE`] edits are required to convert one input to + /// the other. + /// + /// The inputs are therefore considered unrelated and no edit list is + /// provided. + Unrelated, +} + +/// An edit operation on two sequences of `T`. +#[derive(Debug, Clone)] +pub(crate) enum Edit<T> { + /// An extra `T` was added to the actual sequence. + ExtraActual(T), + + /// An extra `T` was added to the expected sequence. + ExtraExpected(T), + + /// An element was added to each sequence. + Both(T), + + /// Additional (unlisted) elements are present in the actual sequence. + /// + /// This is only output in the mode [`Mode::Prefix`]. Its presence precludes + /// reconstructing the actual sequence from the expected sequence. + AdditionalActual, +} + +/// Controls the termination condition of [`edit_list`]. +#[derive(Clone, Copy)] +pub(crate) enum Mode { + /// Indicates that the two arguments are intended to be equal. + /// + /// The entire edit list to transform between `actual` and `expected` is + /// returned. + Exact, + + /// Indicates that `expected` is inteded to be a prefix of `actual`. + /// + /// Any additional parts of `actual` after the prefix `expected` are omitted + /// from the output. + Prefix, + + /// Similar to [`Mode::Prefix`], except it is also assumed that `actual` has + /// some number of initial lines which should not be in the output. + /// + /// Any initial [`Edit::ExtraActual`] entries are replaced with + /// [`Edit::AdditionalActual`] in the edit list. If the first entry which is + /// not an [`Edit::ExtraActual`] is [`Edit::ExtraExpected`], then the last + /// [`Edit::ExtraActual`] is actual in the output. + Contains, +} + +/// Computes the edit list of `actual` and `expected`. +/// +/// If `actual` and `expected` are equal, then this returns +/// [`Difference::Equal`]. If they are different but have an +/// [edit distance](https://en.wikipedia.org/wiki/Edit_distance) +/// of at most [`MAX_DISTANCE`], this returns [`Difference::Editable`] with the +/// sequence of [`Edit`] which can be applied to `actual` to obtain `expected`. +/// Otherwise this returns [`Difference::Unrelated`]. +/// +/// This uses [Myers Algorithm](https://neil.fraser.name/writing/diff/myers.pdf) +/// with a maximum edit distance of [`MAX_DISTANCE`]. Thus the worst-case +/// runtime is linear in both the input length and [`MAX_DISTANCE`]. +pub(crate) fn edit_list<T: PartialEq + Copy>( + actual: impl IntoIterator<Item = T>, + expected: impl IntoIterator<Item = T>, + mode: Mode, +) -> Difference<T> { + let actual: Vec<_> = actual.into_iter().collect(); + let expected: Vec<_> = expected.into_iter().collect(); + + let mut paths_last: Vec<Path<T>> = Vec::new(); + + for distance in 0..=MAX_DISTANCE { + let mut paths_current = Vec::new(); + for k in (-distance..=distance).step_by(2) { + // The following will be None when k is at the edges of the range, + // since no paths have been created for k outside the range in the + // previous iteration. + let path_k_minus_1 = paths_last.get(index_of_k(k - 1, -distance + 1)); + let path_k_plus_1 = paths_last.get(index_of_k(k + 1, -distance + 1)); + + let (mut path, edit) = match (path_k_minus_1, path_k_plus_1) { + // This the first (outer) iteration. + (None, None) => (Path::default(), None), + + // k = -distance. There is no previous parent path yet. + (None, Some(path_k_plus_1)) => ( + path_k_plus_1.clone(), + expected.get(path_k_plus_1.expected_endpoint).copied().map(Edit::ExtraExpected), + ), + + // k = distance. There is no next parent path yet. + (Some(path_k_minus_1), None) => ( + path_k_minus_1.extend_actual_endpoint(), + actual.get(path_k_minus_1.actual_endpoint).copied().map(Edit::ExtraActual), + ), + + // k is strictly between -distance and distance. Both parent paths were set in the + // last iteration. + (Some(path_k_minus_1), Some(path_k_plus_1)) => { + // This decides whether the algorithm prefers to add an edit + // from the actual or from the expected when the rows differ. We + // alternate so that the elements of differing blocks + // interleave rather than all elements of each respective + // side being output in a single block. + if (distance % 2 == 0 + && path_k_plus_1.actual_endpoint > path_k_minus_1.actual_endpoint) + || (distance % 2 == 1 + && path_k_plus_1.expected_endpoint > path_k_minus_1.expected_endpoint) + { + ( + path_k_plus_1.clone(), + expected + .get(path_k_plus_1.expected_endpoint) + .copied() + .map(Edit::ExtraExpected), + ) + } else { + ( + path_k_minus_1.extend_actual_endpoint(), + actual + .get(path_k_minus_1.actual_endpoint) + .copied() + .map(Edit::ExtraActual), + ) + } + } + }; + path.edits.extend(edit); + + // Advance through any common elements starting at the current path. + let (mut actual_endpoint, mut expected_endpoint) = + (path.actual_endpoint, (path.actual_endpoint as i32 - k) as usize); + while actual_endpoint < actual.len() + && expected_endpoint < expected.len() + && actual[actual_endpoint] == expected[expected_endpoint] + { + path.edits.push(Edit::Both(actual[actual_endpoint])); + (actual_endpoint, expected_endpoint) = (actual_endpoint + 1, expected_endpoint + 1); + } + + // If we have exhausted both inputs, we are done. + if actual_endpoint == actual.len() && expected_endpoint == expected.len() { + return if path.edits.iter().any(|v| !matches!(v, Edit::Both(_))) { + if matches!(mode, Mode::Contains) { + compress_prefix_and_suffix(&mut path.edits); + } + Difference::Editable(path.edits) + } else { + Difference::Equal + }; + } + + path.actual_endpoint = actual_endpoint; + path.expected_endpoint = expected_endpoint; + paths_current.push(path); + } + + if matches!(mode, Mode::Prefix) { + if let Some(path) = paths_current + .iter_mut() + .filter(|p| p.expected_endpoint == expected.len()) + .max_by(|p1, p2| p1.edits.len().cmp(&p2.edits.len())) + { + // We've reached the end of the expected side but there could still be a + // corresponding line on the actual which we haven't picked up into the edit + // list. We'll just add it manually to the edit list. There's no + // real harm doing so -- worst case is that there's an + // additional line when there didn't have to be. + if let Some(Edit::ExtraExpected(_)) = path.edits.last() { + if path.actual_endpoint < actual.len() { + // The edits from the actual should come before the corresponding one from + // the expected, so we insert rather than push. + path.edits.insert( + path.edits.len() - 1, + Edit::ExtraActual(actual[path.actual_endpoint]), + ); + } + } + path.edits.push(Edit::AdditionalActual); + return if path.edits.iter().any(|v| !matches!(v, Edit::Both(_))) { + Difference::Editable(std::mem::take(&mut path.edits)) + } else { + Difference::Equal + }; + } + } + + paths_last = paths_current; + } + + Difference::Unrelated +} + +fn index_of_k(k: i32, k_min: i32) -> usize { + ((k - k_min) / 2) as usize +} + +fn compress_prefix_and_suffix<T>(edits: &mut Vec<Edit<T>>) { + if let Some(mut first_non_extra_actual_edit) = + edits.iter().position(|e| !matches!(e, Edit::ExtraActual(_))) + { + if first_non_extra_actual_edit > 1 + && matches!(edits[first_non_extra_actual_edit], Edit::ExtraExpected(_)) + { + first_non_extra_actual_edit -= 1; + } + edits.splice(..first_non_extra_actual_edit, [Edit::AdditionalActual]); + } + + if let Some(mut last_non_extra_actual_edit) = + edits.iter().rposition(|e| !matches!(e, Edit::ExtraActual(_))) + { + if last_non_extra_actual_edit < edits.len() - 1 + && matches!(edits[last_non_extra_actual_edit], Edit::ExtraExpected(_)) + { + last_non_extra_actual_edit += 1; + } + edits.splice(last_non_extra_actual_edit + 1.., [Edit::AdditionalActual]); + } +} + +#[derive(Clone)] +struct Path<T: Clone> { + actual_endpoint: usize, + expected_endpoint: usize, + edits: Vec<Edit<T>>, +} + +impl<T: Clone> Default for Path<T> { + fn default() -> Self { + Self { actual_endpoint: 0, expected_endpoint: 0, edits: vec![] } + } +} + +impl<T: Clone> Path<T> { + fn extend_actual_endpoint(&self) -> Self { + Self { + actual_endpoint: self.actual_endpoint + 1, + expected_endpoint: self.expected_endpoint, + edits: self.edits.clone(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::prelude::*; + use quickcheck::{quickcheck, Arbitrary, TestResult}; + + #[test] + fn returns_equal_when_strings_are_equal() -> Result<()> { + let result = edit_list(["A string"], ["A string"], Mode::Exact); + verify_that!(result, matches_pattern!(Difference::Equal)) + } + + #[test] + fn returns_sequence_of_two_common_parts() -> Result<()> { + let result = edit_list( + ["A string (1)", "A string (2)"], + ["A string (1)", "A string (2)"], + Mode::Exact, + ); + verify_that!(result, matches_pattern!(Difference::Equal)) + } + + #[test] + fn returns_extra_actual_when_only_actual_has_content() -> Result<()> { + let result = edit_list(["A string"], [], Mode::Exact); + verify_that!( + result, + matches_pattern!(Difference::Editable(elements_are![matches_pattern!( + Edit::ExtraActual(eq("A string")) + )])) + ) + } + + #[test] + fn returns_extra_expected_when_only_expected_has_content() -> Result<()> { + let result = edit_list([], ["A string"], Mode::Exact); + verify_that!( + result, + matches_pattern!(Difference::Editable(elements_are![matches_pattern!( + Edit::ExtraExpected(eq("A string")) + )])) + ) + } + + #[test] + fn returns_extra_actual_followed_by_extra_expected_with_two_unequal_strings() -> Result<()> { + let result = edit_list(["A string"], ["Another string"], Mode::Exact); + verify_that!( + result, + matches_pattern!(Difference::Editable(elements_are![ + matches_pattern!(Edit::ExtraActual(eq("A string"))), + matches_pattern!(Edit::ExtraExpected(eq("Another string"))), + ])) + ) + } + + #[test] + fn interleaves_extra_actual_and_extra_expected_when_multiple_lines_differ() -> Result<()> { + let result = + edit_list(["A string", "A string"], ["Another string", "Another string"], Mode::Exact); + verify_that!( + result, + matches_pattern!(Difference::Editable(elements_are![ + matches_pattern!(Edit::ExtraActual(eq("A string"))), + matches_pattern!(Edit::ExtraExpected(eq("Another string"))), + matches_pattern!(Edit::ExtraActual(eq("A string"))), + matches_pattern!(Edit::ExtraExpected(eq("Another string"))), + ])) + ) + } + + #[test] + fn returns_common_part_plus_difference_when_there_is_common_prefix() -> Result<()> { + let result = edit_list( + ["Common part", "Actual only"], + ["Common part", "Expected only"], + Mode::Exact, + ); + verify_that!( + result, + matches_pattern!(Difference::Editable(elements_are![ + matches_pattern!(Edit::Both(eq("Common part"))), + matches_pattern!(Edit::ExtraActual(eq("Actual only"))), + matches_pattern!(Edit::ExtraExpected(eq("Expected only"))), + ])) + ) + } + + #[test] + fn returns_common_part_plus_extra_actual_when_actual_has_extra_suffix() -> Result<()> { + let result = edit_list(["Common part", "Actual only"], ["Common part"], Mode::Exact); + verify_that!( + result, + matches_pattern!(Difference::Editable(elements_are![ + matches_pattern!(Edit::Both(eq("Common part"))), + matches_pattern!(Edit::ExtraActual(eq("Actual only"))), + ])) + ) + } + + #[test] + fn returns_common_part_plus_extra_expected_when_expected_has_extra_suffix() -> Result<()> { + let result = edit_list(["Common part"], ["Common part", "Expected only"], Mode::Exact); + verify_that!( + result, + matches_pattern!(Difference::Editable(elements_are![ + matches_pattern!(Edit::Both(eq("Common part"))), + matches_pattern!(Edit::ExtraExpected(eq("Expected only"))), + ])) + ) + } + + #[test] + fn returns_difference_plus_common_part_when_there_is_common_suffix() -> Result<()> { + let result = edit_list( + ["Actual only", "Common part"], + ["Expected only", "Common part"], + Mode::Exact, + ); + verify_that!( + result, + matches_pattern!(Difference::Editable(elements_are![ + matches_pattern!(Edit::ExtraActual(eq("Actual only"))), + matches_pattern!(Edit::ExtraExpected(eq("Expected only"))), + matches_pattern!(Edit::Both(eq("Common part"))), + ])) + ) + } + + #[test] + fn returns_difference_plus_common_part_plus_difference_when_there_is_common_infix() -> Result<()> + { + let result = edit_list( + ["Actual only (1)", "Common part", "Actual only (2)"], + ["Expected only (1)", "Common part", "Expected only (2)"], + Mode::Exact, + ); + verify_that!( + result, + matches_pattern!(Difference::Editable(elements_are![ + matches_pattern!(Edit::ExtraActual(eq("Actual only (1)"))), + matches_pattern!(Edit::ExtraExpected(eq("Expected only (1)"))), + matches_pattern!(Edit::Both(eq("Common part"))), + matches_pattern!(Edit::ExtraActual(eq("Actual only (2)"))), + matches_pattern!(Edit::ExtraExpected(eq("Expected only (2)"))), + ])) + ) + } + + #[test] + fn returns_common_part_plus_difference_plus_common_part_when_there_is_common_prefix_and_suffix() + -> Result<()> { + let result = edit_list( + ["Common part (1)", "Actual only", "Common part (2)"], + ["Common part (1)", "Expected only", "Common part (2)"], + Mode::Exact, + ); + verify_that!( + result, + matches_pattern!(Difference::Editable(elements_are![ + matches_pattern!(Edit::Both(eq("Common part (1)"))), + matches_pattern!(Edit::ExtraActual(eq("Actual only"))), + matches_pattern!(Edit::ExtraExpected(eq("Expected only"))), + matches_pattern!(Edit::Both(eq("Common part (2)"))), + ])) + ) + } + + #[test] + fn returns_common_part_plus_extra_actual_plus_common_part_when_there_is_common_prefix_and_suffix() + -> Result<()> { + let result = edit_list( + ["Common part (1)", "Actual only", "Common part (2)"], + ["Common part (1)", "Common part (2)"], + Mode::Exact, + ); + verify_that!( + result, + matches_pattern!(Difference::Editable(elements_are![ + matches_pattern!(Edit::Both(eq("Common part (1)"))), + matches_pattern!(Edit::ExtraActual(eq("Actual only"))), + matches_pattern!(Edit::Both(eq("Common part (2)"))), + ])) + ) + } + + #[test] + fn returns_common_part_plus_extra_expected_plus_common_part_when_there_is_common_prefix_and_suffix() + -> Result<()> { + let result = edit_list( + ["Common part (1)", "Common part (2)"], + ["Common part (1)", "Expected only", "Common part (2)"], + Mode::Exact, + ); + verify_that!( + result, + matches_pattern!(Difference::Editable(elements_are![ + matches_pattern!(Edit::Both(eq("Common part (1)"))), + matches_pattern!(Edit::ExtraExpected(eq("Expected only"))), + matches_pattern!(Edit::Both(eq("Common part (2)"))), + ])) + ) + } + + #[test] + fn skips_extra_parts_on_actual_at_end_in_prefix_mode() -> Result<()> { + let result = edit_list( + ["Common part", "Actual only"], + ["Expected only", "Common part"], + Mode::Prefix, + ); + verify_that!( + result, + matches_pattern!(Difference::Editable(not(contains(matches_pattern!( + Edit::ExtraActual(eq("Actual only")) + ))))) + ) + } + + #[test] + fn does_not_skip_extra_parts_on_actual_in_prefix_mode_at_end_when_they_are_in_common() + -> Result<()> { + let result = edit_list( + ["Actual only", "Common part"], + ["Expected only", "Common part"], + Mode::Prefix, + ); + verify_that!( + result, + matches_pattern!(Difference::Editable(elements_are![ + matches_pattern!(Edit::ExtraActual(eq("Actual only"))), + matches_pattern!(Edit::ExtraExpected(eq("Expected only"))), + matches_pattern!(Edit::Both(eq("Common part"))), + ])) + ) + } + + #[test] + fn does_not_skip_corresponding_line_on_actual_when_actual_and_expected_differ_in_prefix_mode() + -> Result<()> { + let result = edit_list(["Actual only"], ["Expected only"], Mode::Prefix); + verify_that!( + result, + matches_pattern!(Difference::Editable(elements_are![ + matches_pattern!(Edit::ExtraActual(eq("Actual only"))), + matches_pattern!(Edit::ExtraExpected(eq("Expected only"))), + matches_pattern!(Edit::AdditionalActual), + ])) + ) + } + + #[test] + fn returns_unrelated_when_maximum_distance_exceeded() -> Result<()> { + let result = edit_list(0..=20, 20..40, Mode::Exact); + verify_that!(result, matches_pattern!(Difference::Unrelated)) + } + + quickcheck! { + fn edit_list_edits_actual_to_expected( + actual: Vec<Alphabet>, + expected: Vec<Alphabet> + ) -> TestResult { + match edit_list(actual.clone(), expected.clone(), Mode::Exact) { + Difference::Equal => TestResult::from_bool(actual == expected), + Difference::Editable(edit_list) => { + TestResult::from_bool(apply_edits_to_actual(&edit_list, &actual) == expected) + } + Difference::Unrelated => { + if actual == expected { + TestResult::failed() + } else { + TestResult::discard() + } + } + } + } + } + + quickcheck! { + fn edit_list_edits_expected_to_actual( + actual: Vec<Alphabet>, + expected: Vec<Alphabet> + ) -> TestResult { + match edit_list(actual.clone(), expected.clone(), Mode::Exact) { + Difference::Equal => TestResult::from_bool(actual == expected), + Difference::Editable(edit_list) => { + TestResult::from_bool(apply_edits_to_expected(&edit_list, &expected) == actual) + } + Difference::Unrelated => { + if actual == expected { + TestResult::failed() + } else { + TestResult::discard() + } + } + } + } + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + enum Alphabet { + A, + B, + C, + } + + impl Arbitrary for Alphabet { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + g.choose(&[Alphabet::A, Alphabet::B, Alphabet::C]).copied().unwrap() + } + } + + fn apply_edits_to_actual<T: PartialEq + Debug + Copy>( + edit_list: &[Edit<T>], + actual: &[T], + ) -> Vec<T> { + let mut result = Vec::new(); + let mut actual_iter = actual.iter(); + for edit in edit_list { + match edit { + Edit::ExtraActual(value) => { + assert_that!(actual_iter.next(), some(eq(value))); + } + Edit::ExtraExpected(value) => { + result.push(*value); + } + Edit::Both(value) => { + assert_that!(actual_iter.next(), some(eq(value))); + result.push(*value); + } + Edit::AdditionalActual => { + fail!("Unexpected Edit::AdditionalActual").unwrap(); + } + } + } + assert_that!(actual_iter.next(), none()); + result + } + + fn apply_edits_to_expected<T: PartialEq + Debug + Copy>( + edit_list: &[Edit<T>], + expected: &[T], + ) -> Vec<T> { + let mut result = Vec::new(); + let mut expected_iter = expected.iter(); + for edit in edit_list { + match edit { + Edit::ExtraActual(value) => { + result.push(*value); + } + Edit::ExtraExpected(value) => { + assert_that!(expected_iter.next(), some(eq(value))); + } + Edit::Both(value) => { + assert_that!(expected_iter.next(), some(eq(value))); + result.push(*value); + } + Edit::AdditionalActual => { + fail!("Unexpected Edit::AdditionalActual").unwrap(); + } + } + } + assert_that!(expected_iter.next(), none()); + result + } +} diff --git a/src/matcher_support/mod.rs b/src/matcher_support/mod.rs new file mode 100644 index 0000000..8c30161 --- /dev/null +++ b/src/matcher_support/mod.rs @@ -0,0 +1,25 @@ +// 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. + +//! Utilities to facilitate writing matchers. +//! +//! Tests normally do not need to import anything from this module. Some of +//! these facilities could be useful to downstream users writing custom +//! 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 new file mode 100644 index 0000000..a045282 --- /dev/null +++ b/src/matcher_support/summarize_diff.rs @@ -0,0 +1,367 @@ +// 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. + +#![doc(hidden)] + +use crate::matcher_support::edit_distance; +#[rustversion::since(1.70)] +use std::io::IsTerminal; +use std::{ + borrow::Cow, + fmt::{Display, Write}, +}; + +/// Returns a string describing how the expected and actual lines differ. +/// +/// This is included in a match explanation for [`EqMatcher`] and +/// [`crate::matchers::str_matcher::StrMatcher`]. +/// +/// If the actual value has less than two lines, or the two differ by more than +/// the maximum edit distance, then this returns the empty string. If the two +/// are equal, it returns a simple statement that they are equal. Otherwise, +/// this constructs a unified diff view of the actual and expected values. +pub(crate) fn create_diff( + actual_debug: &str, + expected_debug: &str, + diff_mode: edit_distance::Mode, +) -> Cow<'static, str> { + if actual_debug.lines().count() < 2 { + // If the actual debug is only one line, then there is no point in doing a + // line-by-line diff. + return "".into(); + } + 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::Unrelated => "".into(), + } +} + +/// Returns a string describing how the expected and actual differ after +/// reversing the lines in each. +/// +/// This is similar to [`create_diff`] except that it first reverses the lines +/// in both the expected and actual values, then reverses the constructed edit +/// list. When `diff_mode` is [`edit_distance::Mode::Prefix`], this becomes a +/// diff of the suffix for use by [`ends_with`][crate::matchers::ends_with]. +pub(crate) fn create_diff_reversed( + actual_debug: &str, + expected_debug: &str, + diff_mode: edit_distance::Mode, +) -> Cow<'static, str> { + if actual_debug.lines().count() < 2 { + // If the actual debug is only one line, then there is no point in doing a + // line-by-line diff. + return "".into(); + } + let mut actual_lines_reversed = actual_debug.lines().collect::<Vec<_>>(); + let mut expected_lines_reversed = expected_debug.lines().collect::<Vec<_>>(); + actual_lines_reversed.reverse(); + expected_lines_reversed.reverse(); + match edit_distance::edit_list(actual_lines_reversed, expected_lines_reversed, diff_mode) { + 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() + } + edit_distance::Difference::Unrelated => "".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. +struct BufferedSummary<'a> { + summary: String, + 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); + } + + // 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(); + } + + // 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(); + } + + // 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(); + } + + fn flush_buffer(&mut self) { + self.buffer.flush(&mut self.summary).unwrap(); + } +} + +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![]) }; + for edit in iter { + match edit { + edit_distance::Edit::Both(same) => { + buffered_summary.feed_common_lines(same); + } + edit_distance::Edit::ExtraActual(actual) => { + buffered_summary.feed_extra_actual(actual); + } + edit_distance::Edit::ExtraExpected(expected) => { + buffered_summary.feed_extra_expected(expected); + } + edit_distance::Edit::AdditionalActual => { + buffered_summary.feed_additional_actual(); + } + }; + } + buffered_summary.flush_buffer(); + + buffered_summary + } +} + +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()) { + panic!("Buffer is not empty. This is a bug in gtest_rust.") + } + self.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> { + CommonLineBuffer(Vec<&'a str>), +} + +impl<'a> Buffer<'a> { + fn flush(&mut self, writer: impl std::fmt::Write) -> std::fmt::Result { + match self { + Buffer::CommonLineBuffer(common_lines) => { + Self::flush_common_lines(std::mem::take(common_lines), writer)? + } + }; + Ok(()) + } + + fn flush_common_lines( + common_lines: Vec<&'a str>, + mut writer: impl std::fmt::Write, + ) -> std::fmt::Result { + // 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))?; + } + return Ok(()); + } + + let start_context = &common_lines[0..COMMON_LINES_CONTEXT_SIZE]; + + for line in start_context { + write!(writer, "\n{}", LineStyle::unchanged_style().style(line))?; + } + + write!( + writer, + "\n{}", + LineStyle::comment_style().style(&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))?; + } + 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) + } + } +} + +#[rustversion::since(1.70)] +fn stdout_supports_color() -> bool { + match (is_env_var_set("NO_COLOR"), is_env_var_set("FORCE_COLOR")) { + (true, _) => false, + (false, true) => true, + (false, false) => std::io::stdout().is_terminal(), + } +} + +#[rustversion::not(since(1.70))] +fn stdout_supports_color() -> bool { + is_env_var_set("FORCE_COLOR") +} + +fn is_env_var_set(var: &'static str) -> bool { + std::env::var(var).map(|s| !s.is_empty()).unwrap_or(false) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{matcher_support::edit_distance::Mode, prelude::*}; + use indoc::indoc; + + // Make a long text with each element of the iterator on one line. + // `collection` must contains at least one element. + fn build_text<T: Display>(mut collection: impl Iterator<Item = T>) -> String { + let mut text = String::new(); + write!(&mut text, "{}", collection.next().expect("Provided collection without elements")) + .unwrap(); + for item in collection { + write!(&mut text, "\n{}", item).unwrap(); + } + text + } + + #[test] + fn create_diff_smaller_than_one_line() -> Result<()> { + verify_that!(create_diff("One", "Two", Mode::Exact), eq("")) + } + + #[test] + fn create_diff_exact_same() -> Result<()> { + let expected = indoc! {" + One + Two + "}; + let actual = indoc! {" + One + Two + "}; + verify_that!( + create_diff(expected, actual, Mode::Exact), + eq("No difference found between debug strings.") + ) + } + + #[test] + 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"); + + verify_that!( + create_diff(&build_text(1..50), &build_text(1..51), Mode::Exact), + eq(indoc! { + " + + Difference(-actual / +expected): + 1 + 2 + <---- 45 common lines omitted ----> + 48 + 49 + +50" + }) + ) + } +} diff --git a/src/matcher_support/zipped_iterator.rs b/src/matcher_support/zipped_iterator.rs new file mode 100644 index 0000000..d1cbaf3 --- /dev/null +++ b/src/matcher_support/zipped_iterator.rs @@ -0,0 +1,85 @@ +// 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. + +/// Zips up two iterators into a single iterator of pairs. +/// +/// This is identical to [`Iterator::zip`] except that this version allows the +/// caller to determine whether the two iterators had mismatching sizes using +/// the method [`ZippedIterator::has_size_mismatch`]. +/// +/// [`Iterator::zip`]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.zip +pub(crate) fn zip<I1, I2>(left: I1, right: I2) -> ZippedIterator<I1, I2> { + ZippedIterator { left, right, has_size_mismatch: false, consumed_elements: 0 } +} + +/// An iterator over pairs of the elements of two constituent iterators, which +/// keeps track of whether the two iterators have the same size. +/// +/// This is identical to [`Zip`] except that it allows the caller to determine +/// whether the two iterators had mismatching sizes using the method +/// [`ZippedIterator::has_size_mismatch`]. +/// +/// [`Zip`]: https://doc.rust-lang.org/std/iter/struct.Zip.html +pub(crate) struct ZippedIterator<I1, I2> { + left: I1, + right: I2, + has_size_mismatch: bool, + consumed_elements: usize, +} + +impl<I1: Iterator, I2> ZippedIterator<I1, I2> { + /// Returns whether a mismatch in the two sizes of the two iterators was + /// detected during iteration. + /// + /// This returns `true` if and only if, at some previous call to + /// [`Iterator::next`] on this instance, one of the constituent iterators + /// had a next element and the other did not. + /// + /// [`Iterator::next`]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#tymethod.next + pub(crate) fn has_size_mismatch(&self) -> bool { + self.has_size_mismatch + } + + /// Returns the number of elements in the left iterator. + /// + /// This iterates through the remainder of the left iterator if necessary in + /// order to get the true number of elements. It therefore consumes `self`. + pub(crate) fn left_size(mut self) -> usize { + self.consumed_elements + self.left.by_ref().count() + } +} + +impl<I1: Iterator, I2: Iterator> Iterator for ZippedIterator<I1, I2> { + type Item = (I1::Item, I2::Item); + + fn next(&mut self) -> Option<(I1::Item, I2::Item)> { + match (self.left.next(), self.right.next()) { + (Some(v1), Some(v2)) => { + self.consumed_elements += 1; + Some((v1, v2)) + } + (Some(_), None) => { + // Consumed elements counts only elements from self.left + self.consumed_elements += 1; + self.has_size_mismatch = true; + None + } + (None, Some(_)) => { + self.has_size_mismatch = true; + None + } + (None, None) => None, + } + } +} diff --git a/src/matchers/all_matcher.rs b/src/matchers/all_matcher.rs new file mode 100644 index 0000000..cc959c7 --- /dev/null +++ b/src/matchers/all_matcher.rs @@ -0,0 +1,205 @@ +// 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. + +// There are no visible documentation elements in this module; the declarative +// macro is documented at the top level. +#![doc(hidden)] + +/// Matches a value which all of the given matchers match. +/// +/// Each argument is a [`Matcher`][crate::matcher::Matcher] which matches +/// against the actual value. +/// +/// For example: +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// verify_that!("A string", all!(starts_with("A"), ends_with("string")))?; // Passes +/// # Ok(()) +/// # } +/// # fn should_fail() -> Result<()> { +/// verify_that!("A string", all!(starts_with("A"), ends_with("not a string")))?; // Fails +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// # should_fail().unwrap_err(); +/// ``` +/// +/// Using this macro is equivalent to using the +/// [`and`][crate::matcher::Matcher::and] method: +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// verify_that!(10, gt(9).and(lt(11)))?; // Also passes +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// ``` +/// +/// Assertion failure messages are not guaranteed to be identical, however. +#[macro_export] +macro_rules! all { + ($($matcher:expr),* $(,)?) => {{ + use $crate::matchers::all_matcher::internal::AllMatcher; + AllMatcher::new([$(Box::new($matcher)),*]) + }} +} + +/// Functionality needed by the [`all`] macro. +/// +/// For internal use only. API stablility is not guaranteed! +#[doc(hidden)] +pub mod internal { + use crate::matcher::{Matcher, MatcherResult}; + use crate::matcher_support::description::Description; + use crate::matchers::anything; + use std::fmt::Debug; + + /// A matcher which matches an input value matched by all matchers in the + /// array `components`. + /// + /// For internal use only. API stablility is not guaranteed! + #[doc(hidden)] + pub struct AllMatcher<'a, T: Debug + ?Sized, const N: usize> { + components: [Box<dyn Matcher<ActualT = T> + 'a>; N], + } + + impl<'a, T: Debug + ?Sized, const N: usize> AllMatcher<'a, T, N> { + /// Constructs an [`AllMatcher`] with the given component matchers. + /// + /// Intended for use only by the [`all`] macro. + pub fn new(components: [Box<dyn Matcher<ActualT = T> + 'a>; N]) -> Self { + Self { components } + } + } + + impl<'a, T: Debug + ?Sized, const N: usize> Matcher for AllMatcher<'a, T, N> { + type ActualT = T; + + fn matches(&self, actual: &Self::ActualT) -> MatcherResult { + for component in &self.components { + match component.matches(actual) { + MatcherResult::NoMatch => { + return MatcherResult::NoMatch; + } + MatcherResult::Match => {} + } + } + MatcherResult::Match + } + + fn explain_match(&self, actual: &Self::ActualT) -> String { + match N { + 0 => anything::<T>().explain_match(actual), + 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>(); + if failures.len() == 1 { + format!("{}", failures) + } else { + format!("{}", failures.bullet_list().indent_except_first_line()) + } + } + } + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + 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" + } + ) + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::internal; + use crate::matcher::{Matcher, MatcherResult}; + use crate::prelude::*; + use indoc::indoc; + + #[test] + fn description_shows_more_than_one_matcher() -> Result<()> { + let first_matcher = starts_with("A"); + let second_matcher = ends_with("string"); + let matcher: internal::AllMatcher<String, 2> = all!(first_matcher, second_matcher); + + verify_that!( + matcher.describe(MatcherResult::Match), + eq(indoc!( + " + has all the following properties: + * starts with prefix \"A\" + * ends with suffix \"string\"" + )) + ) + } + + #[test] + fn description_shows_one_matcher_directly() -> Result<()> { + 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\"")) + } + + #[test] + fn mismatch_description_shows_which_matcher_failed_if_more_than_one_constituent() -> Result<()> + { + let first_matcher = starts_with("Another"); + let second_matcher = ends_with("string"); + let matcher: internal::AllMatcher<str, 2> = all!(first_matcher, second_matcher); + + verify_that!( + matcher.explain_match("A string"), + displays_as(eq("which does not start with \"Another\"")) + ) + } + + #[test] + fn mismatch_description_is_simple_when_only_one_consistuent() -> Result<()> { + let first_matcher = starts_with("Another"); + let matcher: internal::AllMatcher<str, 1> = all!(first_matcher); + + verify_that!( + matcher.explain_match("A string"), + displays_as(eq("which does not start with \"Another\"")) + ) + } +} diff --git a/src/matchers/any_matcher.rs b/src/matchers/any_matcher.rs new file mode 100644 index 0000000..82c8910 --- /dev/null +++ b/src/matchers/any_matcher.rs @@ -0,0 +1,199 @@ +// 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. + +// There are no visible documentation elements in this module; the declarative +// macro is documented at the top level. +#![doc(hidden)] + +/// Matches a value which at least one of the given matchers match. +/// +/// Each argument is a [`Matcher`][crate::matcher::Matcher] which matches +/// against the actual value. +/// +/// For example: +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// verify_that!("A string", any!(starts_with("A"), ends_with("string")))?; // Passes +/// verify_that!("A string", any!(starts_with("A"), starts_with("string")))?; // Passes +/// verify_that!("A string", any!(ends_with("A"), ends_with("string")))?; // Passes +/// # Ok(()) +/// # } +/// # fn should_fail() -> Result<()> { +/// verify_that!("A string", any!(starts_with("An"), ends_with("not a string")))?; // Fails +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// # should_fail().unwrap_err(); +/// ``` +/// +/// Using this macro is equivalent to using the +/// [`or`][crate::matcher::Matcher::or] method: +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// verify_that!(10, gt(9).or(lt(8)))?; // Also passes +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// ``` +/// +/// Assertion failure messages are not guaranteed to be identical, however. +#[macro_export] +macro_rules! any { + ($($matcher:expr),* $(,)?) => {{ + use $crate::matchers::any_matcher::internal::AnyMatcher; + AnyMatcher::new([$(Box::new($matcher)),*]) + }} +} + +/// Functionality needed by the [`any`] macro. +/// +/// For internal use only. API stablility is not guaranteed! +#[doc(hidden)] +pub mod internal { + use crate::matcher::{Matcher, MatcherResult}; + use crate::matcher_support::description::Description; + use crate::matchers::anything; + use std::fmt::Debug; + + /// A matcher which matches an input value matched by all matchers in the + /// array `components`. + /// + /// For internal use only. API stablility is not guaranteed! + #[doc(hidden)] + pub struct AnyMatcher<'a, T: Debug + ?Sized, const N: usize> { + components: [Box<dyn Matcher<ActualT = T> + 'a>; N], + } + + impl<'a, T: Debug + ?Sized, const N: usize> AnyMatcher<'a, T, N> { + /// Constructs an [`AnyMatcher`] with the given component matchers. + /// + /// Intended for use only by the [`all`] macro. + pub fn new(components: [Box<dyn Matcher<ActualT = T> + 'a>; N]) -> Self { + Self { components } + } + } + + impl<'a, T: Debug + ?Sized, const N: usize> Matcher for AnyMatcher<'a, T, N> { + type ActualT = T; + + fn matches(&self, actual: &Self::ActualT) -> MatcherResult { + MatcherResult::from(self.components.iter().any(|c| c.matches(actual).is_match())) + } + + fn explain_match(&self, actual: &Self::ActualT) -> String { + match N { + 0 => format!("which {}", anything::<T>().describe(MatcherResult::NoMatch)), + 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>(); + if failures.len() == 1 { + format!("{}", failures) + } else { + format!("{}", failures.bullet_list().indent_except_first_line()) + } + } + } + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + 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 at least one of the following properties" + } else { + "has none of the following properties" + } + ) + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::internal; + use crate::matcher::{Matcher, MatcherResult}; + use crate::prelude::*; + use indoc::indoc; + + #[test] + fn description_shows_more_than_one_matcher() -> Result<()> { + let first_matcher = starts_with("A"); + let second_matcher = ends_with("string"); + let matcher: internal::AnyMatcher<String, 2> = any!(first_matcher, second_matcher); + + verify_that!( + matcher.describe(MatcherResult::Match), + eq(indoc!( + " + has at least one of the following properties: + * starts with prefix \"A\" + * ends with suffix \"string\"" + )) + ) + } + + #[test] + fn description_shows_one_matcher_directly() -> Result<()> { + 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\"")) + } + + #[test] + fn mismatch_description_shows_which_matcher_failed_if_more_than_one_constituent() -> Result<()> + { + let first_matcher = starts_with("Another"); + let second_matcher = ends_with("string"); + let matcher: internal::AnyMatcher<str, 2> = any!(first_matcher, second_matcher); + + verify_that!( + matcher.explain_match("A string"), + displays_as(eq("which does not start with \"Another\"")) + ) + } + + #[test] + fn mismatch_description_is_simple_when_only_one_constituent() -> Result<()> { + let first_matcher = starts_with("Another"); + let matcher: internal::AnyMatcher<str, 1> = any!(first_matcher); + + verify_that!( + matcher.explain_match("A string"), + displays_as(eq("which does not start with \"Another\"")) + ) + } +} diff --git a/src/matchers/anything_matcher.rs b/src/matchers/anything_matcher.rs new file mode 100644 index 0000000..82de460 --- /dev/null +++ b/src/matchers/anything_matcher.rs @@ -0,0 +1,78 @@ +// 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. + +use crate::matcher::{Matcher, MatcherResult}; +use std::{fmt::Debug, marker::PhantomData}; + +/// Matches anything. This matcher always succeeds. +/// +/// This is useful to check if `actual` matches the specific structure (like +/// `Some(...)`) but without caring about the internal value. +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// let option = Some("Some value"); +/// verify_that!(option, some(anything()))?; +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// ``` +pub fn anything<T: Debug + ?Sized>() -> impl Matcher<ActualT = T> { + Anything::<T>(Default::default()) +} + +struct Anything<T: ?Sized>(PhantomData<T>); + +impl<T: Debug + ?Sized> Matcher for Anything<T> { + type ActualT = T; + + fn matches(&self, _: &T) -> MatcherResult { + MatcherResult::Match + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + match matcher_result { + MatcherResult::Match => "is anything".to_string(), + MatcherResult::NoMatch => "never matches".to_string(), + } + } +} + +#[cfg(test)] +mod tests { + use super::anything; + use crate::prelude::*; + + #[test] + fn anything_matches_i32() -> Result<()> { + let value = 32; + verify_that!(value, anything())?; + Ok(()) + } + + #[test] + fn anything_matches_str() -> Result<()> { + let value = "32"; + verify_that!(value, anything())?; + Ok(()) + } + + #[test] + fn anything_matches_option() -> Result<()> { + let value = Some(32); + verify_that!(value, some(anything()))?; + Ok(()) + } +} diff --git a/src/matchers/char_count_matcher.rs b/src/matchers/char_count_matcher.rs new file mode 100644 index 0000000..a7765b4 --- /dev/null +++ b/src/matchers/char_count_matcher.rs @@ -0,0 +1,166 @@ +// 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::matcher::{Matcher, MatcherResult}; +use std::{fmt::Debug, marker::PhantomData}; + +/// Matches a string whose number of Unicode scalars matches `expected`. +/// +/// In other words, the argument must match the output of +/// [`actual_string.chars().count()`][std::str::Chars]. +/// +/// This can have surprising effects when what appears to be a single character +/// is composed of multiple Unicode scalars. See [Rust documentation on +/// character +/// representation](https://doc.rust-lang.org/std/primitive.char.html#representation) +/// for more information. +/// +/// This matches against owned strings and string slices. +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// let string_slice = "A string"; +/// verify_that!(string_slice, char_count(eq(8)))?; +/// let non_ascii_string_slice = "Ä ſtřiɲğ"; +/// verify_that!(non_ascii_string_slice, char_count(eq(8)))?; +/// let owned_string = String::from("A string"); +/// verify_that!(owned_string, char_count(eq(8)))?; +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// ``` +/// +/// The parameter `expected` can be any integer numeric matcher. +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// let string_slice = "A string"; +/// verify_that!(string_slice, char_count(gt(4)))?; +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// ``` +pub fn char_count<T: Debug + ?Sized + AsRef<str>, E: Matcher<ActualT = usize>>( + expected: E, +) -> impl Matcher<ActualT = T> { + CharLenMatcher { expected, phantom: Default::default() } +} + +struct CharLenMatcher<T: ?Sized, E> { + expected: E, + phantom: PhantomData<T>, +} + +impl<T: Debug + ?Sized + AsRef<str>, E: Matcher<ActualT = usize>> Matcher for CharLenMatcher<T, E> { + type ActualT = T; + + fn matches(&self, actual: &T) -> MatcherResult { + self.expected.matches(&actual.as_ref().chars().count()) + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + 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) + ) + } + } + } + + fn explain_match(&self, actual: &T) -> String { + let actual_size = actual.as_ref().chars().count(); + format!( + "which has character count {}, {}", + actual_size, + self.expected.explain_match(&actual_size) + ) + } +} + +#[cfg(test)] +mod tests { + use super::char_count; + use crate::matcher::{Matcher, MatcherResult}; + use crate::prelude::*; + use indoc::indoc; + use std::fmt::Debug; + use std::marker::PhantomData; + + #[test] + fn char_count_matches_string_slice() -> Result<()> { + let value = "abcd"; + verify_that!(value, char_count(eq(4))) + } + + #[test] + fn char_count_matches_owned_string() -> Result<()> { + let value = String::from("abcd"); + verify_that!(value, char_count(eq(4))) + } + + #[test] + fn char_count_counts_non_ascii_characters_correctly() -> Result<()> { + let value = "äöüß"; + verify_that!(value, char_count(eq(4))) + } + + #[test] + fn char_count_explains_match() -> Result<()> { + struct TestMatcher<T>(PhantomData<T>); + impl<T: Debug> Matcher for TestMatcher<T> { + type ActualT = T; + + fn matches(&self, _: &T) -> MatcherResult { + false.into() + } + + fn describe(&self, _: MatcherResult) -> String { + "called described".into() + } + + fn explain_match(&self, _: &T) -> String { + "called explain_match".into() + } + } + verify_that!( + char_count(TestMatcher(Default::default())).explain_match(&"A string"), + displays_as(eq("which has character count 8, called explain_match")) + ) + } + + #[test] + fn char_count_has_correct_failure_message() -> Result<()> { + let result = verify_that!("äöüß", char_count(eq(3))); + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + r#" + Value of: "äöüß" + Expected: has character count, which is equal to 3 + Actual: "äöüß", + which has character count 4, which isn't equal to 3"# + )))) + ) + } +} diff --git a/src/matchers/conjunction_matcher.rs b/src/matchers/conjunction_matcher.rs new file mode 100644 index 0000000..1ba59c3 --- /dev/null +++ b/src/matchers/conjunction_matcher.rs @@ -0,0 +1,157 @@ +// 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. + +// There are no visible documentation elements in this module. +#![doc(hidden)] + +use crate::matcher::{Matcher, MatcherResult}; +use std::fmt::Debug; + +/// Matcher created by [`Matcher::and`]. +/// +/// **For internal use only. API stablility is not guaranteed!** +#[doc(hidden)] +pub struct ConjunctionMatcher<M1, M2> { + m1: M1, + m2: M2, +} + +impl<M1, M2> ConjunctionMatcher<M1, M2> { + pub(crate) fn new(m1: M1, m2: M2) -> Self { + Self { m1, m2 } + } +} + +impl<M1: Matcher, M2: Matcher<ActualT = M1::ActualT>> Matcher for ConjunctionMatcher<M1, M2> +where + M1::ActualT: Debug, +{ + type ActualT = M1::ActualT; + + fn matches(&self, actual: &M1::ActualT) -> MatcherResult { + match (self.m1.matches(actual), self.m2.matches(actual)) { + (MatcherResult::Match, MatcherResult::Match) => MatcherResult::Match, + _ => MatcherResult::NoMatch, + } + } + + fn explain_match(&self, actual: &M1::ActualT) -> String { + 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::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) + ) + } + } + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + format!("{}, and {}", self.m1.describe(matcher_result), self.m2.describe(matcher_result)) + } +} + +#[cfg(test)] +mod tests { + use crate::prelude::*; + use indoc::indoc; + + #[test] + fn and_true_true_matches() -> Result<()> { + verify_that!(1, anything().and(anything())) + } + + #[test] + fn and_true_false_does_not_match() -> Result<()> { + let result = verify_that!(1, anything().and(not(anything()))); + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + Value of: 1 + Expected: is anything, and never matches + Actual: 1, + which is anything + " + )))) + ) + } + + #[test] + fn and_false_true_does_not_match() -> Result<()> { + let result = verify_that!(1, not(anything()).and(anything())); + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + Value of: 1 + Expected: never matches, and is anything + Actual: 1, + which is anything + " + )))) + ) + } + + #[test] + fn and_false_false_does_not_match() -> Result<()> { + let result = verify_that!(1, not(anything()).and(not(anything()))); + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + Value of: 1 + Expected: never matches, and never matches + Actual: 1, + which is anything and + which is anything + " + )))) + ) + } + + #[test] + fn chained_and_matches() -> Result<()> { + #[derive(Debug)] + struct Struct { + a: i32, + b: i32, + c: i32, + } + verify_that!( + Struct { a: 1, b: 2, c: 3 }, + field!(Struct.a, eq(1)).and(field!(Struct.b, eq(2))).and(field!(Struct.c, eq(3))) + ) + } + + #[test] + fn works_with_str_slices() -> Result<()> { + verify_that!("A string", starts_with("A").and(ends_with("string"))) + } + + #[test] + fn works_with_owned_strings() -> Result<()> { + verify_that!("A string".to_string(), starts_with("A").and(ends_with("string"))) + } +} diff --git a/src/matchers/container_eq_matcher.rs b/src/matchers/container_eq_matcher.rs new file mode 100644 index 0000000..f80cebf --- /dev/null +++ b/src/matchers/container_eq_matcher.rs @@ -0,0 +1,312 @@ +// 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. + +use crate::matcher::{Matcher, MatcherResult}; +use std::fmt::Debug; +use std::marker::PhantomData; + +/// Matches a container equal (in the sense of `==`) to `expected`. +/// +/// This is similar to [`crate::matchers::eq`] except that an assertion failure +/// message generated from this matcher will include the missing and unexpected +/// items in the actual value, e.g.: +/// +/// ```text +/// Expected container to equal [1, 2, 3] +/// but was: [1, 2, 4] +/// Missing: [3] +/// 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: +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// let vec = vec![1, 2, 3]; +/// verify_that!(vec, container_eq([1, 2, 3]))?; +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// ``` +/// +/// As an exception, if the actual type is a `Vec<String>`, the expected type +/// may be a slice of `&str`: +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// let vec: Vec<String> = vec!["A string".into(), "Another string".into()]; +/// verify_that!(vec, container_eq(["A string", "Another string"]))?; +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// ``` +/// +/// These exceptions allow one to avoid unnecessary allocations in test +/// assertions. +/// +/// One can also check container equality of a slice with an array. To do so, +/// dereference the slice: +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// let value = &[1, 2, 3]; +/// verify_that!(*value, container_eq([1, 2, 3]))?; +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// ``` +/// +/// Otherwise, the actual and expected types must be identical. +/// +/// *Performance note*: In the event of a mismatch leading to an assertion +/// failure, the construction of the lists of missing and unexpected values +/// uses a naive algorithm requiring time proportional to the product of the +/// sizes of the expected and actual values. This should therefore only be used +/// when the containers are small enough that this is not a problem. +// This returns ContainerEqMatcher and not impl Matcher because +// ContainerEqMatcher has some specialisations for slice types (see +// documentation above). Returning impl Matcher would hide those from the +// compiler. +pub fn container_eq<ActualContainerT, ExpectedContainerT>( + expected: ExpectedContainerT, +) -> ContainerEqMatcher<ActualContainerT, ExpectedContainerT> +where + ActualContainerT: PartialEq<ExpectedContainerT> + Debug + ?Sized, + ExpectedContainerT: Debug, +{ + ContainerEqMatcher { expected, phantom: Default::default() } +} + +pub struct ContainerEqMatcher<ActualContainerT: ?Sized, ExpectedContainerT> { + expected: ExpectedContainerT, + phantom: PhantomData<ActualContainerT>, +} + +impl<ActualElementT, ActualContainerT, ExpectedElementT, ExpectedContainerT> Matcher + for ContainerEqMatcher<ActualContainerT, ExpectedContainerT> +where + ActualElementT: PartialEq<ExpectedElementT> + Debug + ?Sized, + ActualContainerT: PartialEq<ExpectedContainerT> + Debug + ?Sized, + ExpectedElementT: Debug, + ExpectedContainerT: Debug, + for<'a> &'a ActualContainerT: IntoIterator<Item = &'a ActualElementT>, + for<'a> &'a ExpectedContainerT: IntoIterator<Item = &'a ExpectedElementT>, +{ + type ActualT = ActualContainerT; + + fn matches(&self, actual: &ActualContainerT) -> MatcherResult { + (*actual == self.expected).into() + } + + fn explain_match(&self, actual: &ActualContainerT) -> String { + build_explanation(self.get_missing_items(actual), self.get_unexpected_items(actual)) + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + match matcher_result { + MatcherResult::Match => format!("is equal to {:?}", self.expected), + MatcherResult::NoMatch => format!("isn't equal to {:?}", self.expected), + } + } +} + +impl<ActualElementT, ActualContainerT, ExpectedElementT, ExpectedContainerT> + ContainerEqMatcher<ActualContainerT, ExpectedContainerT> +where + ActualElementT: PartialEq<ExpectedElementT> + ?Sized, + ActualContainerT: PartialEq<ExpectedContainerT> + ?Sized, + for<'a> &'a ActualContainerT: IntoIterator<Item = &'a ActualElementT>, + for<'a> &'a ExpectedContainerT: IntoIterator<Item = &'a ExpectedElementT>, +{ + fn get_missing_items(&self, actual: &ActualContainerT) -> Vec<&ExpectedElementT> { + self.expected.into_iter().filter(|&i| !actual.into_iter().any(|j| j == i)).collect() + } + + fn get_unexpected_items<'a>(&self, actual: &'a ActualContainerT) -> Vec<&'a ActualElementT> { + actual.into_iter().filter(|&i| !self.expected.into_iter().any(|j| i == j)).collect() + } +} + +fn build_explanation<T: Debug, U: Debug>(missing: Vec<T>, unexpected: Vec<U>) -> String { + match (missing.len(), unexpected.len()) { + // TODO(b/261175849) add more data here (out of order elements, duplicated elements, etc...) + (0, 0) => "which contains all the elements".to_string(), + (0, 1) => format!("which contains the unexpected element {:?}", unexpected[0]), + (0, _) => format!("which contains the unexpected elements {unexpected:?}",), + (1, 0) => format!("which is missing the element {:?}", missing[0]), + (1, 1) => { + format!( + "which is missing the element {:?} and contains the unexpected element {:?}", + missing[0], unexpected[0] + ) + } + (1, _) => { + format!( + "which is missing the element {:?} and contains the unexpected elements {unexpected:?}", + missing[0] + ) + } + (_, 0) => format!("which is missing the elements {missing:?}"), + (_, 1) => { + format!( + "which is missing the elements {missing:?} and contains the unexpected element {:?}", + unexpected[0] + ) + } + (_, _) => { + format!( + "which is missing the elements {missing:?} and contains the unexpected elements {unexpected:?}", + ) + } + } +} + +#[cfg(test)] +mod tests { + use super::container_eq; + use crate::matcher::{Matcher, MatcherResult}; + use crate::prelude::*; + use indoc::indoc; + use std::collections::HashSet; + + #[test] + fn container_eq_returns_match_when_containers_match() -> Result<()> { + verify_that!(vec![1, 2, 3], container_eq(vec![1, 2, 3])) + } + + #[test] + fn container_eq_matches_array_with_slice() -> Result<()> { + let value = &[1, 2, 3]; + verify_that!(*value, container_eq([1, 2, 3])) + } + + #[test] + fn container_eq_matches_hash_set() -> Result<()> { + let value: HashSet<i32> = [1, 2, 3].into(); + verify_that!(value, container_eq([1, 2, 3].into())) + } + + #[test] + fn container_eq_full_error_message() -> Result<()> { + let result = verify_that!(vec![1, 3, 2], container_eq(vec![1, 2, 3])); + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + Value of: vec![1, 3, 2] + Expected: is equal to [1, 2, 3] + Actual: [1, 3, 2], + which contains all the elements + " + )))) + ) + } + + #[test] + fn container_eq_returns_mismatch_when_elements_out_of_order() -> Result<()> { + verify_that!( + container_eq(vec![1, 2, 3]).explain_match(&vec![1, 3, 2]), + displays_as(eq("which contains all the elements")) + ) + } + + #[test] + fn container_eq_mismatch_shows_missing_elements_in_container() -> Result<()> { + verify_that!( + container_eq(vec![1, 2, 3]).explain_match(&vec![1, 2]), + displays_as(eq("which is missing the element 3")) + ) + } + + #[test] + fn container_eq_mismatch_shows_surplus_elements_in_container() -> Result<()> { + verify_that!( + container_eq(vec![1, 2]).explain_match(&vec![1, 2, 3]), + displays_as(eq("which contains the unexpected element 3")) + ) + } + + #[test] + fn container_eq_mismatch_shows_missing_and_surplus_elements_in_container() -> Result<()> { + verify_that!( + container_eq(vec![1, 2, 3]).explain_match(&vec![1, 2, 4]), + displays_as(eq("which is missing the element 3 and contains the unexpected element 4")) + ) + } + + #[test] + fn container_eq_mismatch_does_not_show_duplicated_element() -> Result<()> { + verify_that!( + container_eq(vec![1, 2, 3]).explain_match(&vec![1, 2, 3, 3]), + displays_as(eq("which contains all the elements")) + ) + } + + #[test] + fn container_eq_matches_owned_vec_with_array() -> Result<()> { + let vector = vec![123, 234]; + verify_that!(vector, container_eq([123, 234])) + } + + #[test] + fn container_eq_matches_owned_vec_of_owned_strings_with_slice_of_string_references() + -> Result<()> { + let vector = vec!["A string".to_string(), "Another string".to_string()]; + verify_that!(vector, container_eq(["A string", "Another string"])) + } + + #[test] + fn container_eq_matches_owned_vec_of_owned_strings_with_shorter_slice_of_string_references() + -> Result<()> { + let actual = vec!["A string".to_string(), "Another string".to_string()]; + let matcher = container_eq(["A string"]); + + let result = matcher.matches(&actual); + + verify_that!(result, eq(MatcherResult::NoMatch)) + } + + #[test] + fn container_eq_mismatch_with_slice_shows_missing_elements_in_container() -> Result<()> { + verify_that!( + container_eq([1, 2, 3]).explain_match(&vec![1, 2]), + displays_as(eq("which is missing the element 3")) + ) + } + + #[test] + fn container_eq_mismatch_with_str_slice_shows_missing_elements_in_container() -> Result<()> { + verify_that!( + container_eq(["A", "B", "C"]).explain_match(&vec!["A".to_string(), "B".to_string()]), + displays_as(eq("which is missing the element \"C\"")) + ) + } + + #[test] + fn container_eq_mismatch_with_str_slice_shows_surplus_elements_in_container() -> Result<()> { + verify_that!( + container_eq(["A", "B"]).explain_match(&vec![ + "A".to_string(), + "B".to_string(), + "C".to_string() + ]), + displays_as(eq("which contains the unexpected element \"C\"")) + ) + } +} diff --git a/src/matchers/contains_matcher.rs b/src/matchers/contains_matcher.rs new file mode 100644 index 0000000..d714c88 --- /dev/null +++ b/src/matchers/contains_matcher.rs @@ -0,0 +1,273 @@ +// 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. + +use crate::matcher::{Matcher, MatcherResult}; +use std::{fmt::Debug, marker::PhantomData}; + +/// Matches an iterable type whose elements contain a value matched by `inner`. +/// +/// By default, this matches a container with any number of elements matched +/// by `inner`. Use the method [`ContainsMatcher::times`] to constrain the +/// matched containers to a specific number of matching elements. +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// verify_that!(["Some value"], contains(eq("Some value")))?; // Passes +/// verify_that!(vec!["Some value"], contains(eq("Some value")))?; // Passes +/// # Ok(()) +/// # } +/// # fn should_fail_1() -> Result<()> { +/// verify_that!([] as [String; 0], contains(eq("Some value")))?; // Fails +/// # Ok(()) +/// # } +/// # fn should_fail_2() -> Result<()> { +/// verify_that!(["Some value"], contains(eq("Some other value")))?; // Fails +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// # should_fail_1().unwrap_err(); +/// # should_fail_2().unwrap_err(); +/// ``` +pub fn contains<T, InnerMatcherT>(inner: InnerMatcherT) -> ContainsMatcher<T, InnerMatcherT> { + ContainsMatcher { inner, count: None, phantom: Default::default() } +} + +/// A matcher which matches a container containing one or more elements a given +/// inner [`Matcher`] matches. +pub struct ContainsMatcher<T, InnerMatcherT> { + inner: InnerMatcherT, + count: Option<Box<dyn Matcher<ActualT = usize>>>, + phantom: PhantomData<T>, +} + +impl<T, InnerMatcherT> ContainsMatcher<T, InnerMatcherT> { + /// Configures this instance to match containers which contain a number of + /// matching items matched by `count`. + /// + /// For example, to assert that exactly three matching items must be + /// present, use: + /// + /// ```ignore + /// contains(...).times(eq(3)) + /// ``` + /// + /// One can also use `times(eq(0))` to test for the *absence* of an item + /// matching the expected value. + pub fn times(mut self, count: impl Matcher<ActualT = usize> + 'static) -> Self { + self.count = Some(Box::new(count)); + self + } +} + +// TODO(hovinen): Revisit the trait bounds to see whether this can be made more +// flexible. Namely, the following doesn't compile currently: +// +// let matcher = contains(eq(&42)); +// let val = 42; +// let _ = matcher.matches(&vec![&val]); +// +// because val is dropped before matcher but the trait bound requires that +// the argument to matches outlive the matcher. It works fine if one defines +// val before matcher. +impl<T: Debug, InnerMatcherT: Matcher<ActualT = T>, ContainerT: Debug> Matcher + for ContainsMatcher<ContainerT, InnerMatcherT> +where + for<'a> &'a ContainerT: IntoIterator<Item = &'a T>, +{ + type ActualT = ContainerT; + + fn matches(&self, actual: &Self::ActualT) -> MatcherResult { + if let Some(count) = &self.count { + count.matches(&self.count_matches(actual)) + } else { + for v in actual.into_iter() { + if self.inner.matches(v).into() { + return MatcherResult::Match; + } + } + MatcherResult::NoMatch + } + } + + fn explain_match(&self, actual: &Self::ActualT) -> String { + 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(), + } + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + 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) + ), + (MatcherResult::NoMatch, Some(count)) => format!( + "doesn't contain n elements which {}\n where n {}", + self.inner.describe(MatcherResult::Match), + count.describe(MatcherResult::Match) + ), + (MatcherResult::Match, None) => format!( + "contains at least one element which {}", + self.inner.describe(MatcherResult::Match) + ), + (MatcherResult::NoMatch, None) => { + format!("contains no element which {}", self.inner.describe(MatcherResult::Match)) + } + } + } +} + +impl<ActualT, InnerMatcherT> ContainsMatcher<ActualT, InnerMatcherT> { + fn count_matches<T: Debug, ContainerT>(&self, actual: &ContainerT) -> usize + where + for<'b> &'b ContainerT: IntoIterator<Item = &'b T>, + InnerMatcherT: Matcher<ActualT = T>, + { + let mut count = 0; + for v in actual.into_iter() { + if self.inner.matches(v).into() { + count += 1; + } + } + count + } +} + +#[cfg(test)] +mod tests { + use super::{contains, ContainsMatcher}; + use crate::matcher::{Matcher, MatcherResult}; + use crate::prelude::*; + + #[test] + fn contains_matches_singleton_slice_with_value() -> Result<()> { + let matcher = contains(eq(1)); + + let result = matcher.matches(&vec![1]); + + verify_that!(result, eq(MatcherResult::Match)) + } + + #[test] + fn contains_matches_singleton_vec_with_value() -> Result<()> { + let matcher = contains(eq(1)); + + let result = matcher.matches(&vec![1]); + + verify_that!(result, eq(MatcherResult::Match)) + } + + #[test] + fn contains_matches_two_element_slice_with_value() -> Result<()> { + let matcher = contains(eq(1)); + + let result = matcher.matches(&[0, 1]); + + verify_that!(result, eq(MatcherResult::Match)) + } + + #[test] + fn contains_does_not_match_singleton_slice_with_wrong_value() -> Result<()> { + let matcher = contains(eq(1)); + + let result = matcher.matches(&[0]); + + verify_that!(result, eq(MatcherResult::NoMatch)) + } + + #[test] + fn contains_does_not_match_empty_slice() -> Result<()> { + let matcher = contains(eq(1)); + + let result = matcher.matches(&[]); + + verify_that!(result, eq(MatcherResult::NoMatch)) + } + + #[test] + fn contains_matches_slice_with_repeated_value() -> Result<()> { + let matcher = contains(eq(1)).times(eq(2)); + + let result = matcher.matches(&[1, 1]); + + verify_that!(result, eq(MatcherResult::Match)) + } + + #[test] + fn contains_does_not_match_slice_with_too_few_of_value() -> Result<()> { + let matcher = contains(eq(1)).times(eq(2)); + + let result = matcher.matches(&[0, 1]); + + verify_that!(result, eq(MatcherResult::NoMatch)) + } + + #[test] + fn contains_does_not_match_slice_with_too_many_of_value() -> Result<()> { + let matcher = contains(eq(1)).times(eq(1)); + + let result = matcher.matches(&[1, 1]); + + verify_that!(result, eq(MatcherResult::NoMatch)) + } + + #[test] + fn contains_formats_without_multiplicity_by_default() -> Result<()> { + let matcher: ContainsMatcher<Vec<i32>, _> = contains(eq(1)); + + verify_that!( + Matcher::describe(&matcher, MatcherResult::Match), + eq("contains at least one element which is equal to 1") + ) + } + + #[test] + fn contains_formats_with_multiplicity_when_specified() -> Result<()> { + let matcher: ContainsMatcher<Vec<i32>, _> = contains(eq(1)).times(eq(2)); + + verify_that!( + Matcher::describe(&matcher, MatcherResult::Match), + eq("contains n elements which is equal to 1\n where n is equal to 2") + ) + } + + #[test] + fn contains_mismatch_shows_number_of_times_element_was_found() -> Result<()> { + verify_that!( + contains(eq(3)).times(eq(1)).explain_match(&vec![1, 2, 3, 3]), + displays_as(eq("which contains 2 matching elements")) + ) + } + + #[test] + fn contains_mismatch_shows_when_matches() -> Result<()> { + verify_that!( + contains(eq(3)).explain_match(&vec![1, 2, 3, 3]), + displays_as(eq("which contains a matching element")) + ) + } + + #[test] + fn contains_mismatch_shows_when_no_matches() -> Result<()> { + verify_that!( + contains(eq(3)).explain_match(&vec![1, 2]), + displays_as(eq("which does not contain a matching element")) + ) + } +} diff --git a/src/matchers/contains_regex_matcher.rs b/src/matchers/contains_regex_matcher.rs new file mode 100644 index 0000000..8cc93a7 --- /dev/null +++ b/src/matchers/contains_regex_matcher.rs @@ -0,0 +1,148 @@ +// 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. + +use crate::matcher::{Matcher, MatcherResult}; +use regex::Regex; +use std::fmt::Debug; +use std::marker::PhantomData; +use std::ops::Deref; + +/// Matches a string containing a substring which matches the given regular +/// expression. +/// +/// Both the actual value and the expected regular expression may be either a +/// `String` or a string reference. +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass_1() -> Result<()> { +/// verify_that!("Some value", contains_regex("S.*e"))?; // Passes +/// # Ok(()) +/// # } +/// # fn should_fail() -> Result<()> { +/// verify_that!("Another value", contains_regex("Some"))?; // Fails +/// # Ok(()) +/// # } +/// # fn should_pass_2() -> Result<()> { +/// verify_that!("Some value".to_string(), contains_regex("v.*e"))?; // Passes +/// verify_that!("Some value", contains_regex("v.*e".to_string()))?; // Passes +/// # Ok(()) +/// # } +/// # should_pass_1().unwrap(); +/// # should_fail().unwrap_err(); +/// # should_pass_2().unwrap(); +/// ``` +/// +/// Panics if the given `pattern` is not a syntactically valid regular +/// expression. +// N.B. This returns the concrete type rather than an impl Matcher so that it +// can act simultaneously as a Matcher<str> and a Matcher<String>. Otherwise the +// compiler treats it as a Matcher<str> only and the code +// verify_that!("Some value".to_string(), contains_regex(".*value"))?; +// doesn't compile. +pub fn contains_regex<ActualT: ?Sized, PatternT: Deref<Target = str>>( + pattern: PatternT, +) -> ContainsRegexMatcher<ActualT> { + ContainsRegexMatcher { + regex: Regex::new(pattern.deref()).unwrap(), + phantom: Default::default(), + } +} + +/// A matcher matching a string-like type containing a substring matching a +/// given regular expression. +/// +/// Intended only to be used from the function [`contains_regex`] only. +/// Should not be referenced by code outside this library. +pub struct ContainsRegexMatcher<ActualT: ?Sized> { + regex: Regex, + phantom: PhantomData<ActualT>, +} + +impl<ActualT: AsRef<str> + Debug + ?Sized> Matcher for ContainsRegexMatcher<ActualT> { + type ActualT = ActualT; + + fn matches(&self, actual: &ActualT) -> MatcherResult { + self.regex.is_match(actual.as_ref()).into() + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + match matcher_result { + MatcherResult::Match => { + format!("contains the regular expression {:#?}", self.regex.as_str()) + } + MatcherResult::NoMatch => { + format!("doesn't contain the regular expression {:#?}", self.regex.as_str()) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::{contains_regex, ContainsRegexMatcher}; + use crate::matcher::{Matcher, MatcherResult}; + use crate::prelude::*; + + #[test] + fn contains_regex_matches_string_reference_with_pattern() -> Result<()> { + let matcher = contains_regex("S.*val"); + + let result = matcher.matches("Some value"); + + verify_that!(result, eq(MatcherResult::Match)) + } + + #[test] + fn contains_regex_does_not_match_string_without_pattern() -> Result<()> { + let matcher = contains_regex("Another"); + + let result = matcher.matches("Some value"); + + verify_that!(result, eq(MatcherResult::NoMatch)) + } + + #[test] + fn contains_regex_matches_owned_string_with_pattern() -> Result<()> { + let matcher = contains_regex("value"); + + let result = matcher.matches(&"Some value".to_string()); + + verify_that!(result, eq(MatcherResult::Match)) + } + + #[test] + fn contains_regex_matches_string_reference_with_owned_string() -> Result<()> { + let matcher = contains_regex("value"); + + let result = matcher.matches("Some value"); + + verify_that!(result, eq(MatcherResult::Match)) + } + + #[test] + fn verify_that_works_with_owned_string() -> Result<()> { + verify_that!("Some value".to_string(), contains_regex("value")) + } + + #[test] + fn contains_regex_displays_quoted_debug_of_pattern() -> Result<()> { + let matcher: ContainsRegexMatcher<&str> = contains_regex("\n"); + + verify_that!( + Matcher::describe(&matcher, MatcherResult::Match), + eq("contains the regular expression \"\\n\"") + ) + } +} diff --git a/src/matchers/disjunction_matcher.rs b/src/matchers/disjunction_matcher.rs new file mode 100644 index 0000000..48bf226 --- /dev/null +++ b/src/matchers/disjunction_matcher.rs @@ -0,0 +1,109 @@ +// 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. + +// There are no visible documentation elements in this module. +#![doc(hidden)] + +use crate::matcher::{Matcher, MatcherResult}; +use std::fmt::Debug; + +/// Matcher created by [`Matcher::or`]. +/// +/// **For internal use only. API stablility is not guaranteed!** +#[doc(hidden)] +pub struct DisjunctionMatcher<M1, M2> { + m1: M1, + m2: M2, +} + +impl<M1, M2> DisjunctionMatcher<M1, M2> { + pub(crate) fn new(m1: M1, m2: M2) -> Self { + Self { m1, m2 } + } +} + +impl<M1: Matcher, M2: Matcher<ActualT = M1::ActualT>> Matcher for DisjunctionMatcher<M1, M2> +where + M1::ActualT: Debug, +{ + type ActualT = M1::ActualT; + + fn matches(&self, actual: &M1::ActualT) -> MatcherResult { + match (self.m1.matches(actual), self.m2.matches(actual)) { + (MatcherResult::NoMatch, MatcherResult::NoMatch) => MatcherResult::NoMatch, + _ => MatcherResult::Match, + } + } + + fn explain_match(&self, actual: &M1::ActualT) -> String { + format!("{} and\n {}", self.m1.explain_match(actual), self.m2.explain_match(actual)) + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + format!("{}, or {}", self.m1.describe(matcher_result), self.m2.describe(matcher_result)) + } +} + +#[cfg(test)] +mod tests { + use crate::prelude::*; + use indoc::indoc; + + #[test] + fn or_true_true_matches() -> Result<()> { + verify_that!(1, anything().or(anything())) + } + + #[test] + fn or_true_false_matches() -> Result<()> { + verify_that!(1, anything().or(not(anything()))) + } + + #[test] + fn or_false_true_matches() -> Result<()> { + verify_that!(1, not(anything()).or(anything())) + } + + #[test] + fn or_false_false_does_not_match() -> Result<()> { + let result = verify_that!(1, not(anything()).or(not(anything()))); + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + Value of: 1 + Expected: never matches, or never matches + Actual: 1, + which is anything and + which is anything + " + )))) + ) + } + + #[test] + fn chained_or_matches() -> Result<()> { + verify_that!(10, eq(1).or(eq(5)).or(ge(9))) + } + + #[test] + fn works_with_str_slices() -> Result<()> { + verify_that!("A string", ends_with("A").or(ends_with("string"))) + } + + #[test] + fn works_with_owned_strings() -> Result<()> { + verify_that!("A string".to_string(), ends_with("A").or(ends_with("string"))) + } +} diff --git a/src/matchers/display_matcher.rs b/src/matchers/display_matcher.rs new file mode 100644 index 0000000..628769b --- /dev/null +++ b/src/matchers/display_matcher.rs @@ -0,0 +1,119 @@ +// 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. + +use crate::matcher::{Matcher, MatcherResult}; +use std::fmt::{Debug, Display}; +use std::marker::PhantomData; + +/// Matches the string representation of types that implement `Display`. +/// +/// ```ignore +/// let result: impl Display = ...; +/// verify_that!(result, displays_as(eq(format!("{}", result))))?; +/// ``` +pub fn displays_as<T: Debug + Display, InnerMatcher: Matcher<ActualT = String>>( + inner: InnerMatcher, +) -> impl Matcher<ActualT = T> { + DisplayMatcher::<T, _> { inner, phantom: Default::default() } +} + +struct DisplayMatcher<T, InnerMatcher: Matcher> { + inner: InnerMatcher, + phantom: PhantomData<T>, +} + +impl<T: Debug + Display, InnerMatcher: Matcher<ActualT = String>> Matcher + for DisplayMatcher<T, InnerMatcher> +{ + type ActualT = T; + + fn matches(&self, actual: &T) -> MatcherResult { + self.inner.matches(&format!("{actual}")) + } + + fn explain_match(&self, actual: &T) -> String { + format!("which displays as a string {}", self.inner.explain_match(&format!("{actual}"))) + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + match matcher_result { + MatcherResult::Match => { + format!("displays as a string which {}", self.inner.describe(MatcherResult::Match)) + } + MatcherResult::NoMatch => { + format!( + "doesn't display as a string which {}", + self.inner.describe(MatcherResult::Match) + ) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::displays_as; + use crate::prelude::*; + use indoc::indoc; + use std::fmt::{Debug, Display, Error, Formatter}; + + #[test] + fn display_matches_i32() -> Result<()> { + let value = 32; + verify_that!(value, displays_as(eq("32")))?; + Ok(()) + } + + #[test] + fn display_matches_str() -> Result<()> { + let value = "32"; + verify_that!(value, displays_as(eq("32")))?; + Ok(()) + } + + #[test] + fn display_matches_struct() -> Result<()> { + #[allow(dead_code)] + #[derive(Debug)] + struct Struct { + a: i32, + b: i64, + } + impl Display for Struct { + fn fmt(&self, f: &mut Formatter<'_>) -> std::result::Result<(), Error> { + write!(f, "{:?}", self) + } + } + verify_that!(Struct { a: 123, b: 321 }, displays_as(eq("Struct { a: 123, b: 321 }")))?; + Ok(()) + } + + #[test] + fn display_displays_error_message_with_explanation_from_inner_matcher() -> Result<()> { + let result = verify_that!("123\n234", displays_as(eq("123\n345"))); + + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + which displays as a string which isn't equal to \"123\\n345\" + Difference(-actual / +expected): + 123 + -234 + +345 + " + )))) + ) + } +} diff --git a/src/matchers/each_matcher.rs b/src/matchers/each_matcher.rs new file mode 100644 index 0000000..ce61207 --- /dev/null +++ b/src/matchers/each_matcher.rs @@ -0,0 +1,243 @@ +// 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. + +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`. +/// +/// ``` +/// # use googletest::prelude::*; +/// # use std::collections::HashSet; +/// # fn should_pass_1() -> Result<()> { +/// let value = vec![1, 2, 3]; +/// verify_that!(value, each(gt(0)))?; // Passes +/// # Ok(()) +/// # } +/// # fn should_fail() -> Result<()> { +/// # let value = vec![1, 2, 3]; +/// verify_that!(value, each(lt(2)))?; // Fails: 2 and 3 are not less than 2 +/// # Ok(()) +/// # } +/// +/// # fn should_pass_2() -> Result<()> { +/// let value: HashSet<i32> = [1, 2, 3].into(); +/// verify_that!(value, each(gt(0)))?; // Passes +/// # Ok(()) +/// # } +/// # should_pass_1().unwrap(); +/// # should_fail().unwrap_err(); +/// # should_pass_2().unwrap(); +/// ``` +/// +/// One can also verify the contents of a slice by dereferencing it: +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// let value = &[1, 2, 3]; +/// verify_that!(*value, each(gt(0)))?; +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// ``` +pub fn each<ElementT: Debug, ActualT: Debug + ?Sized, MatcherT>( + inner: MatcherT, +) -> impl Matcher<ActualT = ActualT> +where + for<'a> &'a ActualT: IntoIterator<Item = &'a ElementT>, + MatcherT: Matcher<ActualT = ElementT>, +{ + EachMatcher { inner, phantom: Default::default() } +} + +struct EachMatcher<ActualT: ?Sized, MatcherT> { + inner: MatcherT, + phantom: PhantomData<ActualT>, +} + +impl<ElementT: Debug, ActualT: Debug + ?Sized, MatcherT> Matcher for EachMatcher<ActualT, MatcherT> +where + for<'a> &'a ActualT: IntoIterator<Item = &'a ElementT>, + MatcherT: Matcher<ActualT = ElementT>, +{ + type ActualT = ActualT; + + fn matches(&self, actual: &ActualT) -> MatcherResult { + for element in actual { + if self.inner.matches(element).is_no_match() { + return MatcherResult::NoMatch; + } + } + MatcherResult::Match + } + + fn explain_match(&self, actual: &ActualT) -> String { + let mut non_matching_elements = Vec::new(); + for (index, element) in actual.into_iter().enumerate() { + if self.inner.matches(element).is_no_match() { + non_matching_elements.push((index, element, self.inner.explain_match(element))); + } + } + if non_matching_elements.is_empty() { + return format!("whose each element {}", self.inner.describe(MatcherResult::Match)); + } + if non_matching_elements.len() == 1 { + let (idx, element, explanation) = non_matching_elements.remove(0); + return format!("whose element #{idx} is {element:?}, {explanation}"); + } + + let failed_indexes = non_matching_elements + .iter() + .map(|&(idx, _, _)| format!("#{idx}")) + .collect::<Vec<_>>() + .join(", "); + let element_explanations = non_matching_elements + .iter() + .map(|&(_, element, ref explanation)| format!("{element:?}, {explanation}")) + .collect::<Description>() + .indent(); + format!("whose elements {failed_indexes} don't match\n{element_explanations}") + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + match matcher_result { + MatcherResult::Match => { + format!("only contains elements that {}", self.inner.describe(MatcherResult::Match)) + } + MatcherResult::NoMatch => { + format!("contains no element that {}", self.inner.describe(MatcherResult::Match)) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::each; + use crate::prelude::*; + use indoc::indoc; + use std::collections::HashSet; + + #[test] + fn each_matches_empty_vec() -> Result<()> { + let value: Vec<i32> = vec![]; + verify_that!(value, each(gt(0))) + } + + #[test] + fn each_matches_vec_with_one_element() -> Result<()> { + let value = vec![1]; + verify_that!(value, each(gt(0))) + } + + #[test] + fn each_matches_vec_with_two_elements() -> Result<()> { + let value = vec![1, 2]; + verify_that!(value, each(gt(0))) + } + + #[test] + fn each_matches_slice_with_one_element() -> Result<()> { + let value = &[1]; + verify_that!(*value, each(gt(0))) + } + + #[test] + fn each_matches_hash_set_with_one_element() -> Result<()> { + let value: HashSet<i32> = [1].into(); + verify_that!(value, each(gt(0))) + } + + #[test] + fn each_does_not_match_when_first_element_does_not_match() -> Result<()> { + let value = vec![0]; + verify_that!(value, not(each(gt(1)))) + } + + #[test] + fn each_does_not_match_when_second_element_does_not_match() -> Result<()> { + let value = vec![2, 0]; + verify_that!(value, not(each(gt(1)))) + } + + #[test] + fn each_shows_correct_message_when_first_item_does_not_match() -> Result<()> { + let result = verify_that!(vec![0, 2, 3], each(gt(0))); + + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + Value of: vec![0, 2, 3] + Expected: only contains elements that is greater than 0 + Actual: [0, 2, 3], + whose element #0 is 0, which is less than or equal to 0" + )))) + ) + } + + #[test] + fn each_shows_correct_message_when_second_item_does_not_match() -> Result<()> { + let result = verify_that!(vec![1, 0, 3], each(gt(0))); + + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + Value of: vec![1, 0, 3] + Expected: only contains elements that is greater than 0 + Actual: [1, 0, 3], + whose element #1 is 0, which is less than or equal to 0" + )))) + ) + } + + #[test] + fn each_shows_correct_message_when_first_two_items_do_not_match() -> Result<()> { + let result = verify_that!(vec![0, 1, 3], each(gt(1))); + + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + Value of: vec![0, 1, 3] + 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" + )))) + ) + } + #[test] + fn each_shows_inner_explanation() -> Result<()> { + let result = verify_that!(vec![vec![1, 2], vec![1]], each(each(eq(1)))); + + verify_that!( + 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 new file mode 100644 index 0000000..736bf47 --- /dev/null +++ b/src/matchers/elements_are_matcher.rs @@ -0,0 +1,174 @@ +// 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. + +// There are no visible documentation elements in this module; the declarative +// macro is documented at the top level. +#![doc(hidden)] + +/// Matches a container's elements to each matcher in order. +/// +/// This macro produces a matcher against a container. It takes as arguments a +/// sequence of matchers each of which should respectively match the +/// corresponding element of the actual value. +/// +/// ``` +/// # use googletest::prelude::*; +/// verify_that!(vec![1, 2, 3], elements_are![eq(1), anything(), gt(0).and(lt(123))]) +/// # .unwrap(); +/// ``` +/// +/// The actual value must be a container implementing [`IntoIterator`]. This +/// includes standard containers, slices (when dereferenced) and arrays. +/// +/// ``` +/// # use googletest::prelude::*; +/// let vector = vec![1, 2, 3]; +/// let slice = vector.as_slice(); +/// verify_that!(*slice, elements_are![eq(1), anything(), gt(0).and(lt(123))]) +/// # .unwrap(); +/// ``` +/// +/// This can also be omitted in [`verify_that!`] macros and replaced with square +/// brackets. +/// +/// ``` +/// # use googletest::prelude::*; +/// verify_that!(vec![1, 2], [eq(1), eq(2)]) +/// # .unwrap(); +/// ``` +/// +/// 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. +/// +/// ```compile_fail +/// # use googletest::prelude::*; +/// verify_that!(vec![vec![1,2], vec![3]], [[eq(1), eq(2)], [eq(3)]]) +/// # .unwrap(); +/// ``` +/// +/// Use this instead: +/// ``` +/// # use googletest::prelude::*; +/// verify_that!(vec![vec![1,2], vec![3]], [elements_are![eq(1), eq(2)], elements_are![eq(3)]]) +/// # .unwrap(); +/// ``` +/// +/// This matcher does not support matching directly against an [`Iterator`]. To +/// 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] +/// instead. +/// +/// [`IntoIterator`]: std::iter::IntoIterator +/// [`Iterator`]: std::iter::Iterator +/// [`Iterator::collect`]: std::iter::Iterator::collect +/// [`Vec`]: std::vec::Vec +#[macro_export] +macro_rules! elements_are { + ($($matcher:expr),* $(,)?) => {{ + use $crate::matchers::elements_are_matcher::internal::ElementsAre; + ElementsAre::new(vec![$(Box::new($matcher)),*]) + }} +} + +/// Module for use only by the procedural macros in this module. +/// +/// **For internal use only. API stablility is not guaranteed!** +#[doc(hidden)] +pub mod internal { + use crate::matcher::{Matcher, MatcherResult}; + use crate::matcher_support::description::Description; + use crate::matcher_support::zipped_iterator::zip; + use std::{fmt::Debug, marker::PhantomData}; + + /// This struct is meant to be used only by the macro `elements_are!`. + /// + /// **For internal use only. API stablility is not guaranteed!** + #[doc(hidden)] + pub struct ElementsAre<'a, ContainerT: ?Sized, T: Debug> { + elements: Vec<Box<dyn Matcher<ActualT = T> + 'a>>, + phantom: PhantomData<ContainerT>, + } + + impl<'a, ContainerT: ?Sized, T: Debug> ElementsAre<'a, ContainerT, T> { + /// Factory only intended for use in the macro `elements_are!`. + /// + /// **For internal use only. API stablility is not guaranteed!** + #[doc(hidden)] + pub fn new(elements: Vec<Box<dyn Matcher<ActualT = T> + 'a>>) -> Self { + Self { elements, phantom: Default::default() } + } + } + + impl<'a, T: Debug, ContainerT: Debug + ?Sized> Matcher for ElementsAre<'a, ContainerT, T> + where + for<'b> &'b ContainerT: IntoIterator<Item = &'b T>, + { + type ActualT = ContainerT; + + fn matches(&self, actual: &ContainerT) -> MatcherResult { + let mut zipped_iterator = zip(actual.into_iter(), self.elements.iter()); + for (a, e) in zipped_iterator.by_ref() { + if e.matches(a).is_no_match() { + return MatcherResult::NoMatch; + } + } + if !zipped_iterator.has_size_mismatch() { + MatcherResult::Match + } else { + MatcherResult::NoMatch + } + } + + fn explain_match(&self, actual: &ContainerT) -> String { + let actual_iterator = actual.into_iter(); + let mut zipped_iterator = zip(actual_iterator, self.elements.iter()); + let mut mismatches = Vec::new(); + for (idx, (a, e)) in zipped_iterator.by_ref().enumerate() { + if e.matches(a).is_no_match() { + mismatches.push(format!("element #{idx} is {a:?}, {}", e.explain_match(a))); + } + } + if mismatches.is_empty() { + if !zipped_iterator.has_size_mismatch() { + "whose elements all match".to_string() + } else { + format!("whose size is {}", zipped_iterator.left_size()) + } + } else if mismatches.len() == 1 { + let mismatches = mismatches.into_iter().collect::<Description>(); + format!("where {mismatches}") + } else { + let mismatches = mismatches.into_iter().collect::<Description>(); + format!("where:\n{}", mismatches.bullet_list().indent()) + } + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + format!( + "{} elements:\n{}", + if matcher_result.into() { "has" } else { "doesn't have" }, + &self + .elements + .iter() + .map(|matcher| matcher.describe(MatcherResult::Match)) + .collect::<Description>() + .enumerate() + .indent() + ) + } + } +} diff --git a/src/matchers/empty_matcher.rs b/src/matchers/empty_matcher.rs new file mode 100644 index 0000000..afefcb7 --- /dev/null +++ b/src/matchers/empty_matcher.rs @@ -0,0 +1,103 @@ +// 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. + +use crate::matcher::{Matcher, MatcherResult}; +use std::{fmt::Debug, marker::PhantomData}; + +/// Matches an empty container. +/// +/// `T` can be any container such that `&T` implements `IntoIterator`. +/// +/// ``` +/// # use googletest::prelude::*; +/// # use std::collections::HashSet; +/// # fn should_pass() -> Result<()> { +/// let value: Vec<i32> = vec![]; +/// verify_that!(value, empty())?; +/// let value: HashSet<i32> = HashSet::new(); +/// verify_that!(value, empty())?; +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// ``` +/// +/// One can also check whether a slice is empty by dereferencing it: +/// +/// ``` +/// # use googletest::prelude::*; +/// # use std::collections::HashSet; +/// # fn should_pass() -> Result<()> { +/// let value: &[u32] = &[]; +/// verify_that!(*value, empty())?; +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// ``` + +pub fn empty<T: Debug + ?Sized>() -> impl Matcher<ActualT = T> +where + for<'a> &'a T: IntoIterator, +{ + EmptyMatcher { phantom: Default::default() } +} + +struct EmptyMatcher<T: ?Sized> { + phantom: PhantomData<T>, +} + +impl<T: Debug + ?Sized> Matcher for EmptyMatcher<T> +where + for<'a> &'a T: IntoIterator, +{ + type ActualT = T; + + fn matches(&self, actual: &T) -> MatcherResult { + 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() + } +} + +#[cfg(test)] +mod tests { + use super::empty; + use crate::prelude::*; + use std::collections::HashSet; + + #[test] + fn empty_matcher_match_empty_vec() -> Result<()> { + let value: Vec<i32> = vec![]; + verify_that!(value, empty()) + } + + #[test] + fn empty_matcher_does_not_match_empty_vec() -> Result<()> { + let value = vec![1, 2, 3]; + verify_that!(value, not(empty())) + } + + #[test] + fn empty_matcher_matches_empty_slice() -> Result<()> { + let value: &[i32] = &[]; + verify_that!(*value, empty()) + } + + #[test] + fn empty_matcher_matches_empty_hash_set() -> Result<()> { + let value: HashSet<i32> = HashSet::new(); + verify_that!(value, empty()) + } +} diff --git a/src/matchers/eq_deref_of_matcher.rs b/src/matchers/eq_deref_of_matcher.rs new file mode 100644 index 0000000..1540905 --- /dev/null +++ b/src/matchers/eq_deref_of_matcher.rs @@ -0,0 +1,151 @@ +// 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::{ + matcher::{Matcher, MatcherResult}, + matcher_support::{edit_distance, summarize_diff::create_diff}, +}; +use std::{fmt::Debug, marker::PhantomData, ops::Deref}; + +/// Matches a value equal (in the sense of `==`) to the dereferenced value of +/// `expected`. +/// +/// This is similar to [`eq`][crate::matchers::eq] but takes a reference or +/// smart pointer to the expected value rather than consuming it. This is useful +/// when: +/// +/// * one has only a reference to the expected value, and +/// * the expected value cannot or should not be copied or cloned to create an +/// owned value from it. +/// +/// ``` +/// # use googletest::{matchers::eq_deref_of, verify_that}; +/// #[derive(Debug, PartialEq)] +/// struct NonCloneableStruct(i32); +/// let expected = NonCloneableStruct(123); +/// verify_that!(NonCloneableStruct(123), eq_deref_of(&expected)) +/// # .unwrap() +/// ``` +/// +/// **Note**: while one can use `eq_deref_of` with the configuration methods of +/// [`StrMatcherConfigurator`][crate::matchers::str_matcher::StrMatcherConfigurator] +/// to configure string equality, it is not possible to do so when the input is +/// a smart pointer to a string. +/// +/// ```compile_fail +/// # use googletest::{matchers::{eq_deref_of, str_matcher::StrMatcherConfigurator}, verify_that}; +/// verify_that!("A string", eq_deref_of(Box::new("A STRING")).ignoring_ascii_case()) // Does not compile +/// # .unwrap() +/// ``` +/// +/// Otherwise, this has the same behaviour as [`eq`][crate::matchers::eq]. +pub fn eq_deref_of<ActualT: ?Sized, ExpectedRefT>( + expected: ExpectedRefT, +) -> EqDerefOfMatcher<ActualT, ExpectedRefT> { + EqDerefOfMatcher { expected, phantom: Default::default() } +} + +/// A matcher which matches a value equal to the derefenced value of `expected`. +/// +/// See [`eq_deref_of`]. +pub struct EqDerefOfMatcher<ActualT: ?Sized, ExpectedRefT> { + pub(crate) expected: ExpectedRefT, + phantom: PhantomData<ActualT>, +} + +impl<ActualT, ExpectedRefT, ExpectedT> Matcher for EqDerefOfMatcher<ActualT, ExpectedRefT> +where + ActualT: Debug + ?Sized, + ExpectedRefT: Deref<Target = ExpectedT> + Debug, + ExpectedT: PartialEq<ActualT> + Debug, +{ + type ActualT = ActualT; + + fn matches(&self, actual: &ActualT) -> MatcherResult { + (self.expected.deref() == actual).into() + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + match matcher_result { + MatcherResult::Match => format!("is equal to {:?}", self.expected), + MatcherResult::NoMatch => format!("isn't equal to {:?}", self.expected), + } + } + + fn explain_match(&self, actual: &ActualT) -> String { + format!( + "which {}{}", + &self.describe(self.matches(actual)), + create_diff( + &format!("{:#?}", actual), + &format!("{:#?}", self.expected.deref()), + edit_distance::Mode::Exact, + ) + ) + } +} + +#[cfg(test)] +mod tests { + use super::eq_deref_of; + use crate::prelude::*; + use indoc::indoc; + + #[derive(Debug, PartialEq)] + struct NonCloneNonCopyStruct(i32); + + #[test] + fn matches_value_with_ref_to_equal_value() -> Result<()> { + verify_that!(NonCloneNonCopyStruct(123), eq_deref_of(&NonCloneNonCopyStruct(123))) + } + + #[test] + fn matches_value_with_box_of_equal_value() -> Result<()> { + verify_that!(NonCloneNonCopyStruct(123), eq_deref_of(Box::new(NonCloneNonCopyStruct(123)))) + } + + #[test] + fn does_not_match_value_with_non_equal_value() -> Result<()> { + verify_that!(NonCloneNonCopyStruct(123), not(eq_deref_of(&NonCloneNonCopyStruct(234)))) + } + + #[test] + fn shows_structured_diff() -> Result<()> { + #[derive(Debug, PartialEq)] + struct Strukt { + int: i32, + string: String, + } + + let result = verify_that!( + Strukt { int: 123, string: "something".into() }, + eq_deref_of(Box::new(Strukt { int: 321, string: "someone".into() })) + ); + verify_that!( + result, + err(displays_as(contains_substring(indoc! { + " + 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\", + } + "}))) + ) + } +} diff --git a/src/matchers/eq_matcher.rs b/src/matchers/eq_matcher.rs new file mode 100644 index 0000000..42684c9 --- /dev/null +++ b/src/matchers/eq_matcher.rs @@ -0,0 +1,405 @@ +// 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. + +use crate::matcher::{Matcher, MatcherResult}; +use crate::matcher_support::edit_distance; +use crate::matcher_support::summarize_diff::create_diff; + +use std::{fmt::Debug, marker::PhantomData}; + +/// Matches a value equal (in the sense of `==`) to `expected`. +/// +/// The type of `expected` must implement the [`PartialEq`] trait so that the +/// expected and actual values can be compared. +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// verify_that!(123, eq(123))?; // Passes +/// # Ok(()) +/// # } +/// # fn should_fail() -> Result<()> { +/// verify_that!(123, eq(234))?; // Fails +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// # should_fail().unwrap_err(); +/// ``` +/// +/// `expected` to `actual` must be comparable with one another via the +/// [`PartialEq`] trait. In most cases, this means that they must be of the same +/// type. However, there are a few cases where different but closely related +/// types are comparable, for example `String` with `&str`. +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// verify_that!(String::from("Some value"), eq("Some value"))?; // Passes +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// ``` +/// +/// In most cases however, one must convert one of the arguments explicitly. +/// This can be surprising when comparing integer types or references. +/// +/// ```compile_fail +/// verify_that!(123u32, eq(123u64))?; // Does not compile +/// verify_that!(123u32 as u64, eq(123u64))?; // Passes +/// ``` +/// +/// ```ignore +/// let actual: &T = ...; +/// let expected: T = T{...}; +/// verify_that(actual, eq(expected))?; // Does not compile +/// verify_that(actual, eq(&expected))?; // Compiles +/// ``` +/// +/// When matching with string types (`&str` and `String`), one can set more +/// options on how equality is checked through the +/// [`StrMatcherConfigurator`][crate::matchers::str_matcher::StrMatcherConfigurator] +/// extension trait, which is implemented for this matcher. +pub fn eq<A: ?Sized, T>(expected: T) -> EqMatcher<A, T> { + EqMatcher { expected, phantom: Default::default() } +} + +/// A matcher which matches a value equal to `expected`. +/// +/// See [`eq`]. +pub struct EqMatcher<A: ?Sized, T> { + pub(crate) expected: T, + phantom: PhantomData<A>, +} + +impl<A: Debug + ?Sized, T: PartialEq<A> + Debug> Matcher for EqMatcher<A, T> { + type ActualT = A; + + fn matches(&self, actual: &A) -> MatcherResult { + (self.expected == *actual).into() + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + match matcher_result { + MatcherResult::Match => format!("is equal to {:?}", self.expected), + MatcherResult::NoMatch => format!("isn't equal to {:?}", self.expected), + } + } + + fn explain_match(&self, actual: &A) -> String { + let expected_debug = format!("{:#?}", self.expected); + let actual_debug = format!("{:#?}", actual); + let description = self.describe(self.matches(actual)); + + let diff = if is_multiline_string_debug(&actual_debug) + && is_multiline_string_debug(&expected_debug) + { + create_diff( + // The two calls below return None if and only if the strings expected_debug + // respectively actual_debug are not enclosed in ". The calls to + // is_multiline_string_debug above ensure that they are. So the calls cannot + // actually return None and unwrap() should not panic. + &to_display_output(&actual_debug).unwrap(), + &to_display_output(&expected_debug).unwrap(), + edit_distance::Mode::Exact, + ) + } else { + create_diff(&actual_debug, &expected_debug, edit_distance::Mode::Exact) + }; + + format!("which {description}{diff}") + } +} + +fn is_multiline_string_debug(string: &str) -> bool { + string.starts_with('"') + && string.ends_with('"') + && !string.contains('\n') + && string.contains("\\n") +} + +fn to_display_output(string: &str) -> Option<String> { + Some(string.strip_prefix('"')?.strip_suffix('"')?.split("\\n").collect::<Vec<_>>().join("\n")) +} + +#[cfg(test)] +mod tests { + use super::eq; + use crate::prelude::*; + use indoc::indoc; + + #[test] + fn eq_matches_string_reference_with_string_reference() -> Result<()> { + verify_that!("A string", eq("A string")) + } + + #[test] + fn eq_matches_owned_string_with_string_reference() -> Result<()> { + let value = "A string".to_string(); + verify_that!(value, eq("A string")) + } + + #[test] + fn eq_matches_owned_string_reference_with_string_reference() -> Result<()> { + let value = "A string".to_string(); + verify_that!(&value, eq("A string")) + } + + #[test] + fn eq_matches_i32_with_i32() -> Result<()> { + verify_that!(123, eq(123)) + } + + #[test] + fn eq_struct_debug_diff() -> Result<()> { + #[derive(Debug, PartialEq)] + struct Strukt { + int: i32, + string: String, + } + + let result = verify_that!( + Strukt { int: 123, string: "something".into() }, + eq(Strukt { int: 321, string: "someone".into() }) + ); + verify_that!( + result, + err(displays_as(contains_substring(indoc! { + " + 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\", + } + "}))) + ) + } + + #[test] + fn eq_vec_debug_diff() -> Result<()> { + let result = verify_that!(vec![1, 2, 3], eq(vec![1, 3, 4])); + verify_that!( + result, + err(displays_as(contains_substring(indoc! { + " + Value of: vec![1, 2, 3] + 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, + ] + "}))) + ) + } + + #[test] + fn eq_vec_debug_diff_length_mismatch() -> Result<()> { + let result = verify_that!(vec![1, 2, 3, 4, 5], eq(vec![1, 3, 5])); + verify_that!( + result, + err(displays_as(contains_substring(indoc! { + " + Value of: vec![1, 2, 3, 4, 5] + 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, + ] + "}))) + ) + } + + #[test] + fn eq_debug_diff_common_lines_omitted() -> Result<()> { + let result = verify_that!((1..50).collect::<Vec<_>>(), eq((3..52).collect::<Vec<_>>())); + verify_that!( + result, + err(displays_as(contains_substring(indoc! { + " + Difference(-actual / +expected): + [ + - 1, + - 2, + 3, + 4, + <---- 43 common lines omitted ----> + 48, + 49, + + 50, + + 51, + ]"}))) + ) + } + + #[test] + fn eq_debug_diff_5_common_lines_not_omitted() -> Result<()> { + let result = verify_that!((1..8).collect::<Vec<_>>(), eq((3..10).collect::<Vec<_>>())); + verify_that!( + result, + err(displays_as(contains_substring(indoc! { + " + Difference(-actual / +expected): + [ + - 1, + - 2, + 3, + 4, + 5, + 6, + 7, + + 8, + + 9, + ]"}))) + ) + } + + #[test] + fn eq_debug_diff_start_common_lines_omitted() -> Result<()> { + let result = verify_that!((1..50).collect::<Vec<_>>(), eq((1..52).collect::<Vec<_>>())); + verify_that!( + result, + err(displays_as(contains_substring(indoc! { + " + Difference(-actual / +expected): + [ + 1, + <---- 46 common lines omitted ----> + 48, + 49, + + 50, + + 51, + ]"}))) + ) + } + + #[test] + fn eq_debug_diff_end_common_lines_omitted() -> Result<()> { + let result = verify_that!((1..52).collect::<Vec<_>>(), eq((3..52).collect::<Vec<_>>())); + verify_that!( + result, + err(displays_as(contains_substring(indoc! { + " + Difference(-actual / +expected): + [ + - 1, + - 2, + 3, + 4, + <---- 46 common lines omitted ----> + 51, + ]"}))) + ) + } + + #[test] + fn eq_multi_line_string_debug_diff() -> Result<()> { + let result = verify_that!("One\nTwo\nThree", eq("One\nSix\nThree")); + // TODO: b/257454450 - Make this more useful, by potentially unescaping the + // line return. + verify_that!( + result, + err(displays_as(contains_substring(indoc! { + r#" + Value of: "One\nTwo\nThree" + Expected: is equal to "One\nSix\nThree" + Actual: "One\nTwo\nThree", + which isn't equal to "One\nSix\nThree" + "#}))) + ) + } + + #[test] + fn match_explanation_contains_diff_of_strings_if_more_than_one_line() -> Result<()> { + let result = verify_that!( + indoc!( + " + First line + Second line + Third line + " + ), + eq(indoc!( + " + First line + Second lines + Third line + " + )) + ); + + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + First line + -Second line + +Second lines + Third line + " + )))) + ) + } + + #[test] + fn match_explanation_does_not_show_diff_if_actual_value_is_single_line() -> Result<()> { + let result = verify_that!( + "First line", + eq(indoc!( + " + First line + Second line + Third line + " + )) + ); + + verify_that!( + result, + err(displays_as(not(contains_substring("Difference(-actual / +expected):")))) + ) + } + + #[test] + fn match_explanation_does_not_show_diff_if_expected_value_is_single_line() -> Result<()> { + let result = verify_that!( + indoc!( + " + First line + Second line + Third line + " + ), + eq("First line") + ); + + verify_that!( + result, + err(displays_as(not(contains_substring("Difference(-actual / +expected):")))) + ) + } +} diff --git a/src/matchers/err_matcher.rs b/src/matchers/err_matcher.rs new file mode 100644 index 0000000..022bc8c --- /dev/null +++ b/src/matchers/err_matcher.rs @@ -0,0 +1,144 @@ +// 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. + +use crate::matcher::{Matcher, MatcherResult}; +use std::{fmt::Debug, marker::PhantomData}; + +/// Matches a `Result` containing `Err` with a value matched by `inner`. +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> googletest::Result<()> { +/// verify_that!(Err::<(), _>("Some error"), err(eq("Some error")))?; // Passes +/// # Ok(()) +/// # } +/// # fn should_fail_1() -> googletest::Result<()> { +/// verify_that!(Ok::<_, &str>("A value"), err(eq("A value")))?; // Fails +/// # Ok(()) +/// # } +/// # fn should_fail_2() -> googletest::Result<()> { +/// verify_that!(Err::<(), _>("Some error"), err(eq("Some other error")))?; // Fails +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// # should_fail_1().unwrap_err(); +/// # should_fail_2().unwrap_err(); +/// ``` +pub fn err<T: Debug, E: Debug>( + inner: impl Matcher<ActualT = E>, +) -> impl Matcher<ActualT = std::result::Result<T, E>> { + ErrMatcher::<T, E, _> { inner, phantom_t: Default::default(), phantom_e: Default::default() } +} + +struct ErrMatcher<T, E, InnerMatcherT> { + inner: InnerMatcherT, + phantom_t: PhantomData<T>, + phantom_e: PhantomData<E>, +} + +impl<T: Debug, E: Debug, InnerMatcherT: Matcher<ActualT = E>> Matcher + for ErrMatcher<T, E, InnerMatcherT> +{ + type ActualT = std::result::Result<T, E>; + + fn matches(&self, actual: &Self::ActualT) -> MatcherResult { + actual.as_ref().err().map(|v| self.inner.matches(v)).unwrap_or(MatcherResult::NoMatch) + } + + fn explain_match(&self, actual: &Self::ActualT) -> String { + match actual { + Err(e) => format!("which is an error {}", self.inner.explain_match(e)), + Ok(_) => "which is a success".to_string(), + } + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + 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) + ) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::err; + use crate::matcher::{Matcher, MatcherResult}; + use crate::prelude::*; + use indoc::indoc; + + #[test] + fn err_matches_result_with_err_value() -> Result<()> { + let matcher = err(eq(1)); + let value: std::result::Result<i32, i32> = Err(1); + + let result = matcher.matches(&value); + + verify_that!(result, eq(MatcherResult::Match)) + } + + #[test] + fn err_does_not_match_result_with_wrong_err_value() -> Result<()> { + let matcher = err(eq(1)); + let value: std::result::Result<i32, i32> = Err(0); + + let result = matcher.matches(&value); + + verify_that!(result, eq(MatcherResult::NoMatch)) + } + + #[test] + fn err_does_not_match_result_with_ok() -> Result<()> { + let matcher = err(eq(1)); + let value: std::result::Result<i32, i32> = Ok(1); + + let result = matcher.matches(&value); + + verify_that!(result, eq(MatcherResult::NoMatch)) + } + + #[test] + fn err_full_error_message() -> Result<()> { + let result = verify_that!(Err::<i32, i32>(1), err(eq(2))); + + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + 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 + " + )))) + ) + } + + #[test] + fn err_describe_matches() -> Result<()> { + let matcher = super::ErrMatcher::<i32, i32, _> { + inner: eq(1), + phantom_t: Default::default(), + phantom_e: Default::default(), + }; + verify_that!(matcher.describe(MatcherResult::Match), eq("is an error which is equal to 1")) + } +} diff --git a/src/matchers/field_matcher.rs b/src/matchers/field_matcher.rs new file mode 100644 index 0000000..0d1d4fe --- /dev/null +++ b/src/matchers/field_matcher.rs @@ -0,0 +1,201 @@ +// 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. + +// There are no visible documentation elements in this module; the declarative +// macro is documented at the top level. +#![doc(hidden)] + +/// Matches a structure or enum with a given field which is matched by a given +/// matcher. +/// +/// This takes two arguments: +/// +/// * a specification of the field against which to match, and +/// * an inner [`Matcher`][crate::matcher::Matcher] to apply to that field. +/// +/// For example: +/// +/// ``` +/// # use googletest::prelude::*; +/// #[derive(Debug)] +/// struct IntField { +/// int: i32 +/// } +/// # fn should_pass() -> Result<()> { +/// verify_that!(IntField{int: 32}, field!(IntField.int, eq(32)))?; +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// ``` +/// +/// Tuple structs are also supported via the index syntax: +/// +/// ``` +/// # use googletest::prelude::*; +/// #[derive(Debug)] +/// struct IntField(i32); +/// # fn should_pass() -> Result<()> { +/// verify_that!(IntField(32), field!(IntField.0, eq(32)))?; +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// ``` +/// +/// Enums are also supported, in which case only the specified variant is +/// matched: +/// +/// ``` +/// # use googletest::prelude::*; +/// #[derive(Debug)] +/// enum MyEnum { +/// A(i32), +/// B, +/// } +/// # fn should_pass() -> Result<()> { +/// verify_that!(MyEnum::A(32), field!(MyEnum::A.0, eq(32)))?; // Passes +/// # Ok(()) +/// # } +/// # fn should_fail() -> Result<()> { +/// verify_that!(MyEnum::B, field!(MyEnum::A.0, eq(32)))?; // Fails: wrong enum variant +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// # should_fail().unwrap_err(); +/// ``` +/// +/// The structure or enum may also be referenced from a separate module: +/// +/// ``` +/// # use googletest::prelude::*; +/// mod a_module { +/// #[derive(Debug)] +/// pub struct AStruct(pub i32); +/// } +/// # fn should_pass() -> Result<()> { +/// verify_that!(a_module::AStruct(32), field!(a_module::AStruct.0, eq(32)))?; +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// ``` +/// +/// Nested structures are *not supported*, however: +/// +/// ```compile_fail +/// # use googletest::prelude::*; +/// #[derive(Debug)] +/// struct InnerStruct(i32); +/// #[derive(Debug)] +/// struct OuterStruct { +/// inner: InnerStruct, +/// } +/// # fn should_not_compile() -> Result<()> { +/// verify_that!(value, field!(OuterStruct.inner.0, eq(32)))?; // Does not compile +/// # Ok(()) +/// # } +/// ``` +/// +/// See also the macro [`property`][crate::property] for an analogous mechanism +/// to extract a datum by invoking a method. +#[macro_export] +macro_rules! field { + ($($t:tt)*) => { $crate::field_internal!($($t)*) } +} + +// Internal-only macro created so that the macro definition does not appear in +// generated documentation. +#[doc(hidden)] +#[macro_export] +macro_rules! field_internal { + ($($t:ident)::+.$field:tt, $m:expr) => {{ + use $crate::matchers::field_matcher::internal::field_matcher; + field_matcher( + |o| { + match o { + $($t)::* { $field: value, .. } => Some(value), + // The pattern below is unreachable if the type is a struct (as opposed to an + // enum). Since the macro can't know which it is, we always include it and just + // tell the compiler not to complain. + #[allow(unreachable_patterns)] + _ => None, + } + }, + &stringify!($field), + $m) + }}; +} + +/// Functions for use only by the declarative macros in this module. +/// +/// **For internal use only. API stablility is not guaranteed!** +#[doc(hidden)] +pub mod internal { + use crate::matcher::{Matcher, MatcherResult}; + use std::fmt::Debug; + + /// Creates a matcher to verify a specific field of the actual struct using + /// the provided inner matcher. + /// + /// **For internal use only. API stablility is not guaranteed!** + #[doc(hidden)] + pub fn field_matcher<OuterT: Debug, InnerT: Debug, InnerMatcher: Matcher<ActualT = InnerT>>( + field_accessor: fn(&OuterT) -> Option<&InnerT>, + field_path: &'static str, + inner: InnerMatcher, + ) -> impl Matcher<ActualT = OuterT> { + FieldMatcher { field_accessor, field_path, inner } + } + + struct FieldMatcher<OuterT, InnerT, InnerMatcher> { + field_accessor: fn(&OuterT) -> Option<&InnerT>, + field_path: &'static str, + inner: InnerMatcher, + } + + impl<OuterT: Debug, InnerT: Debug, InnerMatcher: Matcher<ActualT = InnerT>> Matcher + for FieldMatcher<OuterT, InnerT, InnerMatcher> + { + type ActualT = OuterT; + + fn matches(&self, actual: &OuterT) -> MatcherResult { + if let Some(value) = (self.field_accessor)(actual) { + self.inner.matches(value) + } else { + MatcherResult::NoMatch + } + } + + fn explain_match(&self, actual: &OuterT) -> String { + if let Some(actual) = (self.field_accessor)(actual) { + format!( + "which has field `{}`, {}", + self.field_path, + self.inner.explain_match(actual) + ) + } 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}`") + } + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + format!( + "has field `{}`, which {}", + self.field_path, + self.inner.describe(matcher_result) + ) + } + } +} diff --git a/src/matchers/ge_matcher.rs b/src/matchers/ge_matcher.rs new file mode 100644 index 0000000..33e847e --- /dev/null +++ b/src/matchers/ge_matcher.rs @@ -0,0 +1,218 @@ +// 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. + +use crate::matcher::{Matcher, MatcherResult}; +use std::{fmt::Debug, marker::PhantomData}; + +/// Matches a value greater than or equal to (in the sense of `>=`) `expected`. +/// +/// The types of `ActualT` of `actual` and `ExpectedT` of `expected` must be +/// comparable via the `PartialOrd` trait. Namely, `ActualT` must implement +/// `PartialOrd<ExpectedT>`. +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// verify_that!(1, ge(0))?; // Passes +/// # Ok(()) +/// # } +/// # fn should_fail() -> Result<()> { +/// verify_that!(0, ge(1))?; // Fails +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// # should_fail().unwrap_err(); +/// ``` +/// +/// In most cases the params neeed to be the same type or they need to be cast +/// explicitly. This can be surprising when comparing integer types or +/// references: +/// +/// ```compile_fail +/// # use googletest::prelude::*; +/// # fn should_not_compile() -> Result<()> { +/// verify_that!(123u32, ge(0u64))?; // Does not compile +/// verify_that!(123u32 as u64, ge(0u64))?; // Passes +/// # Ok(()) +/// # } +/// ``` +/// +/// ```compile_fail +/// # use googletest::prelude::*; +/// # fn should_not_compile() -> Result<()> { +/// let actual: &u32 = &2; +/// let expected: u32 = 0; +/// verify_that!(actual, ge(expected))?; // Does not compile +/// # Ok(()) +/// # } +/// ``` +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// let actual: &u32 = &2; +/// let expected: u32 = 0; +/// verify_that!(actual, ge(&expected))?; // Compiles and passes +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// ``` +/// +/// You can find the standard library `PartialOrd` implementation in +/// <https://doc.rust-lang.org/core/cmp/trait.PartialOrd.html#implementors> +pub fn ge<ActualT: Debug + PartialOrd<ExpectedT>, ExpectedT: Debug>( + expected: ExpectedT, +) -> impl Matcher<ActualT = ActualT> { + GeMatcher::<ActualT, _> { expected, phantom: Default::default() } +} + +struct GeMatcher<ActualT, ExpectedT> { + expected: ExpectedT, + phantom: PhantomData<ActualT>, +} + +impl<ActualT: Debug + PartialOrd<ExpectedT>, ExpectedT: Debug> Matcher + for GeMatcher<ActualT, ExpectedT> +{ + type ActualT = ActualT; + + fn matches(&self, actual: &ActualT) -> MatcherResult { + (*actual >= self.expected).into() + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + match matcher_result { + MatcherResult::Match => format!("is greater than or equal to {:?}", self.expected), + MatcherResult::NoMatch => format!("is less than {:?}", self.expected), + } + } +} + +#[cfg(test)] +mod tests { + use super::ge; + use crate::matcher::{Matcher, MatcherResult}; + use crate::prelude::*; + use indoc::indoc; + use std::ffi::OsString; + + #[test] + fn ge_matches_i32_with_i32() -> Result<()> { + let actual: i32 = 0; + let expected: i32 = 0; + verify_that!(actual, ge(expected)) + } + + #[test] + fn ge_does_not_match_smaller_i32() -> Result<()> { + let matcher = ge(10); + let result = matcher.matches(&9); + verify_that!(result, eq(MatcherResult::NoMatch)) + } + + #[test] + fn ge_matches_bigger_str() -> Result<()> { + verify_that!("B", ge("A")) + } + + #[test] + fn ge_does_not_match_lesser_str() -> Result<()> { + let matcher = ge("z"); + let result = matcher.matches(&"a"); + verify_that!(result, eq(MatcherResult::NoMatch)) + } + + #[test] + fn ge_mismatch_contains_actual_and_expected() -> Result<()> { + let result = verify_that!(591, ge(927)); + + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + Value of: 591 + Expected: is greater than or equal to 927 + Actual: 591, + which is less than 927 + " + )))) + ) + } + + // Test `ge` matcher where actual is `&OsString` and expected is `&str`. + // Note that stdlib is a little bit inconsistent: `PartialOrd` exists for + // `OsString` and `str`, but only in one direction: it's only possible to + // compare `OsString` with `str` if `OsString` is on the left side of the + // ">=" operator (`impl PartialOrd<str> for OsString`). + // + // The comparison in the other direction is not defined. + // + // This means that the test case bellow effectively ensures that + // `verify_that(actual, ge(expected))` works if `actual >= expected` works + // (regardless whether the `expected >= actual` works`). + #[test] + fn ge_matches_owned_osstring_reference_with_string_reference() -> Result<()> { + let expected = "A"; + let actual: OsString = "B".to_string().into(); + verify_that!(&actual, ge(expected)) + } + + #[test] + fn ge_matches_ipv6addr_with_ipaddr() -> Result<()> { + use std::net::IpAddr; + use std::net::Ipv6Addr; + let actual: Ipv6Addr = "2001:4860:4860::8844".parse().unwrap(); + let expected: IpAddr = "127.0.0.1".parse().unwrap(); + verify_that!(actual, ge(expected)) + } + + #[test] + fn ge_matches_with_custom_partial_ord() -> Result<()> { + /// A custom "number" that is lower than all other numbers. The only + /// things we define about this "special" number is `PartialOrd` and + /// `PartialEq` against `u32`. + #[derive(Debug)] + struct VeryLowNumber {} + + impl std::cmp::PartialEq<u32> for VeryLowNumber { + fn eq(&self, _other: &u32) -> bool { + false + } + } + + // PartialOrd (required for >) requires PartialEq. + impl std::cmp::PartialOrd<u32> for VeryLowNumber { + fn partial_cmp(&self, _other: &u32) -> Option<std::cmp::Ordering> { + Some(std::cmp::Ordering::Less) + } + } + + impl std::cmp::PartialEq<VeryLowNumber> for u32 { + fn eq(&self, _other: &VeryLowNumber) -> bool { + false + } + } + + impl std::cmp::PartialOrd<VeryLowNumber> for u32 { + fn partial_cmp(&self, _other: &VeryLowNumber) -> Option<std::cmp::Ordering> { + Some(std::cmp::Ordering::Greater) + } + } + + let actual: u32 = 42; + let expected = VeryLowNumber {}; + + verify_that!(actual, ge(expected)) + } +} diff --git a/src/matchers/gt_matcher.rs b/src/matchers/gt_matcher.rs new file mode 100644 index 0000000..699bf2a --- /dev/null +++ b/src/matchers/gt_matcher.rs @@ -0,0 +1,254 @@ +// 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. + +use crate::matcher::{Matcher, MatcherResult}; +use std::{fmt::Debug, marker::PhantomData}; + +/// Matches a value greater (in the sense of `>`) than `expected`. +/// +/// The types of `ActualT` of `actual` and `ExpectedT` of `expected` must be +/// comparable via the `PartialOrd` trait. Namely, `ActualT` must implement +/// `PartialOrd<ExpectedT>`. +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// verify_that!(38, gt(1))?; // Passes +/// # Ok(()) +/// # } +/// # fn should_fail() -> Result<()> { +/// verify_that!(234, gt(234))?; // Fails +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// # should_fail().unwrap_err(); +/// ``` +/// +/// In most cases the params neeed to be the same type or they need to be cast +/// explicitly. This can be surprising when comparing integer types or +/// references: +/// +/// ```compile_fail +/// # use googletest::prelude::*; +/// # fn should_not_compile() -> Result<()> { +/// verify_that!(123u32, gt(0u64))?; // Does not compile +/// verify_that!(123u32 as u64, gt(0u64))?; // Passes +/// # Ok(()) +/// # } +/// ``` +/// +/// ```compile_fail +/// # use googletest::prelude::*; +/// # fn should_not_compile() -> Result<()> { +/// let actual: &u32 = &2; +/// let expected: u32 = 1; +/// verify_that!(actual, gt(expected))?; // Does not compile +/// # Ok(()) +/// # } +/// ``` +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// let actual: &u32 = &2; +/// let expected: u32 = 1; +/// verify_that!(actual, gt(&expected))?; // Compiles and passes +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// ``` +/// +/// You can find the standard library `PartialOrd` implementation in +/// <https://doc.rust-lang.org/core/cmp/trait.PartialOrd.html#implementors> +pub fn gt<ActualT: Debug + PartialOrd<ExpectedT>, ExpectedT: Debug>( + expected: ExpectedT, +) -> impl Matcher<ActualT = ActualT> { + GtMatcher::<ActualT, _> { expected, phantom: Default::default() } +} + +struct GtMatcher<ActualT, ExpectedT> { + expected: ExpectedT, + phantom: PhantomData<ActualT>, +} + +impl<ActualT: Debug + PartialOrd<ExpectedT>, ExpectedT: Debug> Matcher + for GtMatcher<ActualT, ExpectedT> +{ + type ActualT = ActualT; + + fn matches(&self, actual: &ActualT) -> MatcherResult { + (*actual > self.expected).into() + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + match matcher_result { + MatcherResult::Match => format!("is greater than {:?}", self.expected), + MatcherResult::NoMatch => format!("is less than or equal to {:?}", self.expected), + } + } +} + +#[cfg(test)] +mod tests { + use super::gt; + use crate::matcher::{Matcher, MatcherResult}; + use crate::prelude::*; + use indoc::indoc; + use std::ffi::OsString; + + #[test] + fn gt_matches_i32_with_i32() -> Result<()> { + let actual: i32 = 321; + let expected: i32 = 123; + verify_that!(actual, gt(expected)) + } + + #[test] + fn gt_does_not_match_equal_i32() -> Result<()> { + let matcher = gt(10); + let result = matcher.matches(&10); + verify_that!(result, eq(MatcherResult::NoMatch)) + } + + #[test] + fn gt_does_not_match_lower_i32() -> Result<()> { + let matcher = gt(-50); + let result = matcher.matches(&-51); + verify_that!(result, eq(MatcherResult::NoMatch)) + } + + #[test] + fn gt_matches_greater_str() -> Result<()> { + verify_that!("B", gt("A")) + } + + #[test] + fn gt_does_not_match_lesser_str() -> Result<()> { + let matcher = gt("B"); + let result = matcher.matches(&"A"); + verify_that!(result, eq(MatcherResult::NoMatch)) + } + + #[test] + fn gt_mismatch_contains_actual_and_expected() -> Result<()> { + let result = verify_that!(481, gt(632)); + + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + Value of: 481 + Expected: is greater than 632 + Actual: 481, + " + )))) + ) + } + + #[test] + fn gt_mismatch_combined_with_each() -> Result<()> { + let result = verify_that!(vec![19, 23, 11], each(gt(15))); + + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + Value of: vec![19, 23, 11] + Expected: only contains elements that is greater than 15 + Actual: [19, 23, 11], + whose element #2 is 11, which is less than or equal to 15 + " + )))) + ) + } + + #[test] + fn gt_describe_matches() -> Result<()> { + verify_that!(gt::<i32, i32>(232).describe(MatcherResult::Match), 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") + ) + } + + // Test `gt` matcher where actual is `&OsString` and expected is `&str`. + // Note that stdlib is a little bit inconsistent: `PartialOrd` exists for + // `OsString` and `str`, but only in one direction: it's only possible to + // compare `OsString` with `str` if `OsString` is on the left side of the + // ">" operator (`impl PartialOrd<str> for OsString`). + // + // The comparison in the other direction is not defined. + // + // This means that the test case bellow effectively ensures that + // `verify_that(actual, gt(expected))` works if `actual > expected` works + // (regardless whether the `expected > actual` works`). + #[test] + fn gt_matches_owned_osstring_reference_with_string_reference() -> Result<()> { + let expected = "A"; + let actual: OsString = "B".to_string().into(); + verify_that!(&actual, gt(expected)) + } + + #[test] + fn gt_matches_ipv6addr_with_ipaddr() -> Result<()> { + use std::net::IpAddr; + use std::net::Ipv6Addr; + let actual: Ipv6Addr = "2001:4860:4860::8888".parse().unwrap(); + let expected: IpAddr = "127.0.0.1".parse().unwrap(); + verify_that!(actual, gt(expected)) + } + + #[test] + fn gt_matches_with_custom_partial_ord() -> Result<()> { + /// A custom "number" that is smaller than all other numbers. The only + /// things we define about this "special" number is `PartialOrd` and + /// `PartialEq` against `u32`. + #[derive(Debug)] + struct VeryLowNumber {} + + impl std::cmp::PartialEq<u32> for VeryLowNumber { + fn eq(&self, _other: &u32) -> bool { + false + } + } + + // PartialOrd (required for >) requires PartialEq. + impl std::cmp::PartialOrd<u32> for VeryLowNumber { + fn partial_cmp(&self, _other: &u32) -> Option<std::cmp::Ordering> { + Some(std::cmp::Ordering::Less) + } + } + + impl std::cmp::PartialEq<VeryLowNumber> for u32 { + fn eq(&self, _other: &VeryLowNumber) -> bool { + false + } + } + + impl std::cmp::PartialOrd<VeryLowNumber> for u32 { + fn partial_cmp(&self, _other: &VeryLowNumber) -> Option<std::cmp::Ordering> { + Some(std::cmp::Ordering::Greater) + } + } + + let actual: u32 = 42; + let expected = VeryLowNumber {}; + + verify_that!(actual, gt(expected)) + } +} diff --git a/src/matchers/has_entry_matcher.rs b/src/matchers/has_entry_matcher.rs new file mode 100644 index 0000000..67a44cd --- /dev/null +++ b/src/matchers/has_entry_matcher.rs @@ -0,0 +1,184 @@ +// 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. + +use crate::matcher::{Matcher, MatcherResult}; +use std::collections::HashMap; +use std::fmt::Debug; +use std::hash::Hash; +use std::marker::PhantomData; + +/// Matches a HashMap containing the given `key` whose value is matched by the +/// matcher `inner`. +/// +/// ``` +/// # use googletest::prelude::*; +/// # use std::collections::HashMap; +/// # fn should_pass() -> Result<()> { +/// let value = HashMap::from([(0, 1), (1, -1)]); +/// verify_that!(value, has_entry(0, eq(1)))?; // Passes +/// # Ok(()) +/// # } +/// # fn should_fail_1() -> Result<()> { +/// # let value = HashMap::from([(0, 1), (1, -1)]); +/// verify_that!(value, has_entry(1, gt(0)))?; // Fails: value not matched +/// # Ok(()) +/// # } +/// # fn should_fail_2() -> Result<()> { +/// # let value = HashMap::from([(0, 1), (1, -1)]); +/// verify_that!(value, has_entry(2, eq(0)))?; // Fails: key not present +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// # should_fail_1().unwrap_err(); +/// # should_fail_2().unwrap_err(); +/// ``` +/// +/// Note: One could obtain the same effect by collecting entries into a `Vec` +/// and using `contains`: +/// +/// ``` +/// # use googletest::prelude::*; +/// # use std::collections::HashMap; +/// # fn should_pass() -> Result<()> { +/// let value = HashMap::from([(0, 1), (1, -1)]); +/// verify_that!(value.into_iter().collect::<Vec<_>>(), contains(eq((0, 1))))?; +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// ``` +/// +/// However, `has_entry` will offer somewhat better diagnostic messages in the +/// case of assertion failure. And it avoid the extra allocation hidden in the +/// code above. +pub fn has_entry<KeyT: Debug + Eq + Hash, ValueT: Debug, MatcherT: Matcher<ActualT = ValueT>>( + key: KeyT, + inner: MatcherT, +) -> impl Matcher<ActualT = HashMap<KeyT, ValueT>> { + HasEntryMatcher { key, inner, phantom: Default::default() } +} + +struct HasEntryMatcher<KeyT, ValueT, MatcherT> { + key: KeyT, + inner: MatcherT, + phantom: PhantomData<ValueT>, +} + +impl<KeyT: Debug + Eq + Hash, ValueT: Debug, MatcherT: Matcher<ActualT = ValueT>> Matcher + for HasEntryMatcher<KeyT, ValueT, MatcherT> +{ + type ActualT = HashMap<KeyT, ValueT>; + + fn matches(&self, actual: &HashMap<KeyT, ValueT>) -> MatcherResult { + if let Some(value) = actual.get(&self.key) { + self.inner.matches(value) + } else { + MatcherResult::NoMatch + } + } + + fn explain_match(&self, actual: &HashMap<KeyT, ValueT>) -> String { + if let Some(value) = actual.get(&self.key) { + format!( + "which contains key {:?}, but is mapped to value {:#?}, {}", + self.key, + value, + self.inner.explain_match(value) + ) + } else { + format!("which doesn't contain key {:?}", self.key) + } + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + match matcher_result { + MatcherResult::Match => format!( + "contains key {:?}, which value {}", + self.key, + self.inner.describe(MatcherResult::Match) + ), + MatcherResult::NoMatch => format!( + "doesn't contain key {:?} or contains key {:?}, which value {}", + self.key, + self.key, + self.inner.describe(MatcherResult::NoMatch) + ), + } + } +} + +#[cfg(test)] +mod tests { + use super::has_entry; + use crate::prelude::*; + use indoc::indoc; + use std::collections::HashMap; + + #[test] + fn has_entry_does_not_match_empty_hash_map() -> Result<()> { + let value: HashMap<i32, i32> = HashMap::new(); + verify_that!(value, not(has_entry(0, eq(0)))) + } + + #[test] + fn has_entry_matches_hash_map_with_value() -> Result<()> { + let value: HashMap<i32, i32> = HashMap::from([(0, 0)]); + verify_that!(value, has_entry(0, eq(0))) + } + + #[test] + fn has_entry_does_not_match_hash_map_with_wrong_value() -> Result<()> { + let value: HashMap<i32, i32> = HashMap::from([(0, 1)]); + verify_that!(value, not(has_entry(0, eq(0)))) + } + + #[test] + fn has_entry_does_not_match_hash_map_with_wrong_key() -> Result<()> { + let value: HashMap<i32, i32> = HashMap::from([(1, 0)]); + verify_that!(value, not(has_entry(0, eq(0)))) + } + + #[test] + fn has_entry_shows_correct_message_when_key_is_not_present() -> Result<()> { + let result = verify_that!(HashMap::from([(0, 0)]), has_entry(1, eq(0))); + + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + Value of: HashMap::from([(0, 0)]) + Expected: contains key 1, which value is equal to 0 + Actual: {0: 0}, + which doesn't contain key 1 + " + )))) + ) + } + + #[test] + fn has_entry_shows_correct_message_when_key_has_non_matching_value() -> Result<()> { + let result = verify_that!(HashMap::from([(0, 0)]), has_entry(0, eq(1))); + + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + Value of: HashMap::from([(0, 0)]) + Expected: contains key 0, which value is equal to 1 + Actual: {0: 0}, + which contains key 0, but is mapped to value 0, which isn't equal to 1 + " + )))) + ) + } +} diff --git a/src/matchers/is_matcher.rs b/src/matchers/is_matcher.rs new file mode 100644 index 0000000..51fd3b2 --- /dev/null +++ b/src/matchers/is_matcher.rs @@ -0,0 +1,65 @@ +// 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. + +#![doc(hidden)] + +use crate::matcher::{Matcher, MatcherResult}; +use std::{fmt::Debug, marker::PhantomData}; + +/// Matches precisely values matched by `inner`. +/// +/// The returned matcher produces a description prefixed by the string +/// `description`. This is useful in contexts where the test assertion failure +/// output must include the additional description. +pub fn is<'a, ActualT: Debug + 'a, InnerMatcherT: Matcher<ActualT = ActualT> + 'a>( + description: &'a str, + inner: InnerMatcherT, +) -> impl Matcher<ActualT = ActualT> + 'a { + IsMatcher { description, inner, phantom: Default::default() } +} + +struct IsMatcher<'a, ActualT, InnerMatcherT> { + description: &'a str, + inner: InnerMatcherT, + phantom: PhantomData<ActualT>, +} + +impl<'a, ActualT: Debug, InnerMatcherT: Matcher<ActualT = ActualT>> Matcher + for IsMatcher<'a, ActualT, InnerMatcherT> +{ + type ActualT = ActualT; + + fn matches(&self, actual: &Self::ActualT) -> MatcherResult { + self.inner.matches(actual) + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + match matcher_result { + MatcherResult::Match => format!( + "is {} which {}", + self.description, + self.inner.describe(MatcherResult::Match) + ), + MatcherResult::NoMatch => format!( + "is not {} which {}", + self.description, + self.inner.describe(MatcherResult::Match) + ), + } + } + + fn explain_match(&self, actual: &Self::ActualT) -> String { + self.inner.explain_match(actual) + } +} diff --git a/src/matchers/is_nan_matcher.rs b/src/matchers/is_nan_matcher.rs new file mode 100644 index 0000000..3cbe694 --- /dev/null +++ b/src/matchers/is_nan_matcher.rs @@ -0,0 +1,62 @@ +// 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. + +use crate::matcher::{Matcher, MatcherResult}; +use num_traits::float::Float; +use std::{fmt::Debug, marker::PhantomData}; + +/// Matches a floating point value which is NaN. +pub fn is_nan<T: Float + Debug>() -> impl Matcher<ActualT = T> { + IsNanMatcher::<T>(Default::default()) +} + +struct IsNanMatcher<T>(PhantomData<T>); + +impl<T: Float + Debug> Matcher for IsNanMatcher<T> { + type ActualT = T; + + fn matches(&self, actual: &T) -> MatcherResult { + actual.is_nan().into() + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + if matcher_result.into() { "is NaN" } else { "isn't NaN" }.to_string() + } +} + +#[cfg(test)] +mod tests { + use super::is_nan; + use crate::prelude::*; + + #[test] + fn matches_f32_nan() -> Result<()> { + verify_that!(f32::NAN, is_nan()) + } + + #[test] + fn does_not_match_f32_number() -> Result<()> { + verify_that!(0.0f32, not(is_nan())) + } + + #[test] + fn matches_f64_nan() -> Result<()> { + verify_that!(f64::NAN, is_nan()) + } + + #[test] + fn does_not_match_f64_number() -> Result<()> { + verify_that!(0.0f64, not(is_nan())) + } +} diff --git a/src/matchers/le_matcher.rs b/src/matchers/le_matcher.rs new file mode 100644 index 0000000..6e7a16f --- /dev/null +++ b/src/matchers/le_matcher.rs @@ -0,0 +1,219 @@ +// 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. + +use crate::matcher::{Matcher, MatcherResult}; +use std::{fmt::Debug, marker::PhantomData}; + +/// Matches a value less than or equal to (in the sense of `<=`) `expected`. +/// +/// The types of `ActualT` of `actual` and `ExpectedT` of `expected` must be +/// comparable via the `PartialOrd` trait. Namely, `ActualT` must implement +/// `PartialOrd<ExpectedT>`. +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// verify_that!(0, le(0))?; // Passes +/// # Ok(()) +/// # } +/// # fn should_fail() -> Result<()> { +/// verify_that!(1, le(0))?; // Fails +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// # should_fail().unwrap_err(); +/// ``` +/// +/// In most cases the params neeed to be the same type or they need to be cast +/// explicitly. This can be surprising when comparing integer types or +/// references: +/// +/// ```compile_fail +/// # use googletest::prelude::*; +/// # fn should_not_compile() -> Result<()> { +/// verify_that!(1u32, le(2u64))?; // Does not compile +/// verify_that!(1u32 as u64, le(2u64))?; // Passes +/// # Ok(()) +/// # } +/// ``` +/// +/// ```compile_fail +/// # use googletest::prelude::*; +/// # fn should_not_compile() -> Result<()> { +/// let actual: &u32 = &1; +/// let expected: u32 = 2; +/// verify_that!(actual, le(expected))?; // Does not compile +/// # Ok(()) +/// # } +/// ``` +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// let actual: &u32 = &1; +/// let expected: u32 = 2; +/// verify_that!(actual, le(&expected))?; // Compiles and passes +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// ``` +/// +/// You can find the standard library `PartialOrd` implementation in +/// <https://doc.rust-lang.org/core/cmp/trait.PartialOrd.html#implementors> +pub fn le<ActualT: Debug + PartialOrd<ExpectedT>, ExpectedT: Debug>( + expected: ExpectedT, +) -> impl Matcher<ActualT = ActualT> { + LeMatcher::<ActualT, _> { expected, phantom: Default::default() } +} + +struct LeMatcher<ActualT, ExpectedT> { + expected: ExpectedT, + phantom: PhantomData<ActualT>, +} + +impl<ActualT: Debug + PartialOrd<ExpectedT>, ExpectedT: Debug> Matcher + for LeMatcher<ActualT, ExpectedT> +{ + type ActualT = ActualT; + + fn matches(&self, actual: &ActualT) -> MatcherResult { + (*actual <= self.expected).into() + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + match matcher_result { + MatcherResult::Match => format!("is less than or equal to {:?}", self.expected), + MatcherResult::NoMatch => format!("is greater than {:?}", self.expected), + } + } +} + +#[cfg(test)] +mod tests { + use super::le; + use crate::matcher::{Matcher, MatcherResult}; + use crate::prelude::*; + use indoc::indoc; + use std::ffi::OsString; + + #[test] + fn le_matches_i32_with_i32() -> Result<()> { + let actual: i32 = 0; + let expected: i32 = 0; + verify_that!(actual, le(expected)) + } + + #[test] + fn le_does_not_match_bigger_i32() -> Result<()> { + let matcher = le(0); + let result = matcher.matches(&1); + verify_that!(result, eq(MatcherResult::NoMatch)) + } + + #[test] + fn le_matches_smaller_str() -> Result<()> { + verify_that!("A", le("B")) + } + + #[test] + fn le_does_not_match_bigger_str() -> Result<()> { + let matcher = le("a"); + let result = matcher.matches(&"z"); + verify_that!(result, eq(MatcherResult::NoMatch)) + } + + #[test] + fn le_mismatch_contains_actual_and_expected() -> Result<()> { + let result = verify_that!(489, le(294)); + let formatted_message = format!("{}", result.unwrap_err()); + + verify_that!( + formatted_message.as_str(), + contains_substring(indoc!( + " + Value of: 489 + Expected: is less than or equal to 294 + Actual: 489, + which is greater than 294 + " + )) + ) + } + + // Test `le` matcher where actual is `&OsString` and expected is `&str`. + // Note that stdlib is a little bit inconsistent: `PartialOrd` exists for + // `OsString` and `str`, but only in one direction: it's only possible to + // compare `OsString` with `str` if `OsString` is on the left side of the + // "<=" operator (`impl PartialOrd<str> for OsString`). + // + // The comparison in the other direction is not defined. + // + // This means that the test case bellow effectively ensures that + // `verify_that(actual, le(expected))` works if `actual <= expected` works + // (regardless whether the `expected <= actual` works`). + #[test] + fn le_matches_owned_osstring_reference_with_string_reference() -> Result<()> { + let expected = "B"; + let actual: OsString = "A".into(); + verify_that!(&actual, le(expected)) + } + + #[test] + fn le_matches_ipv6addr_with_ipaddr() -> Result<()> { + use std::net::IpAddr; + use std::net::Ipv6Addr; + let actual: IpAddr = "127.0.0.1".parse().unwrap(); + let expected: Ipv6Addr = "2001:4860:4860::8844".parse().unwrap(); + verify_that!(actual, le(expected)) + } + + #[test] + fn le_matches_with_custom_partial_ord() -> Result<()> { + /// A custom "number" that is lower than all other numbers. The only + /// things we define about this "special" number is `PartialOrd` and + /// `PartialEq` against `u32`. + #[derive(Debug)] + struct VeryLowNumber {} + + impl std::cmp::PartialEq<u32> for VeryLowNumber { + fn eq(&self, _other: &u32) -> bool { + false + } + } + + // PartialOrd (required for >) requires PartialEq. + impl std::cmp::PartialOrd<u32> for VeryLowNumber { + fn partial_cmp(&self, _other: &u32) -> Option<std::cmp::Ordering> { + Some(std::cmp::Ordering::Less) + } + } + + impl std::cmp::PartialEq<VeryLowNumber> for u32 { + fn eq(&self, _other: &VeryLowNumber) -> bool { + false + } + } + + impl std::cmp::PartialOrd<VeryLowNumber> for u32 { + fn partial_cmp(&self, _other: &VeryLowNumber) -> Option<std::cmp::Ordering> { + Some(std::cmp::Ordering::Greater) + } + } + + let actual = VeryLowNumber {}; + let expected: u32 = 42; + + verify_that!(actual, le(expected)) + } +} diff --git a/src/matchers/len_matcher.rs b/src/matchers/len_matcher.rs new file mode 100644 index 0000000..3a31873 --- /dev/null +++ b/src/matchers/len_matcher.rs @@ -0,0 +1,211 @@ +// 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. + +use crate::matcher::{Matcher, MatcherResult}; +use crate::matcher_support::count_elements::count_elements; +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. +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// let array = [1,2,3]; +/// verify_that!(array, len(eq(3)))?; +/// let vec = vec![1,2,3]; +/// verify_that!(vec, len(eq(3)))?; +/// let slice = vec.as_slice(); +/// verify_that!(*slice, len(eq(3)))?; +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// ``` +/// +/// The parameter `expected` can be any integer numeric matcher. +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// let vec = vec![1,2,3]; +/// verify_that!(vec, len(gt(1)))?; +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// ``` +pub fn len<T: Debug + ?Sized, E: Matcher<ActualT = usize>>(expected: E) -> impl Matcher<ActualT = T> +where + for<'a> &'a T: IntoIterator, +{ + LenMatcher { expected, phantom: Default::default() } +} + +struct LenMatcher<T: ?Sized, E> { + expected: E, + phantom: PhantomData<T>, +} + +impl<T: Debug + ?Sized, E: Matcher<ActualT = usize>> Matcher for LenMatcher<T, E> +where + for<'a> &'a T: IntoIterator, +{ + type ActualT = T; + + fn matches(&self, actual: &T) -> MatcherResult { + self.expected.matches(&count_elements(actual)) + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + match matcher_result { + MatcherResult::Match => { + format!("has length, which {}", self.expected.describe(MatcherResult::Match)) + } + MatcherResult::NoMatch => { + format!("has length, which {}", self.expected.describe(MatcherResult::NoMatch)) + } + } + } + + fn explain_match(&self, actual: &T) -> String { + let actual_size = count_elements(actual); + format!("which has length {}, {}", actual_size, self.expected.explain_match(&actual_size)) + } +} + +#[cfg(test)] +mod tests { + use super::len; + use crate::matcher::{Matcher, MatcherResult}; + use crate::prelude::*; + use indoc::indoc; + use std::collections::{ + BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet, LinkedList, VecDeque, + }; + use std::fmt::Debug; + use std::marker::PhantomData; + + #[test] + fn len_matcher_match_vec() -> Result<()> { + let value = vec![1, 2, 3]; + verify_that!(value, len(eq(3))) + } + + #[test] + fn len_matcher_match_array_reference() -> Result<()> { + let value = &[1, 2, 3]; + verify_that!(*value, len(eq(3))) + } + + #[test] + fn len_matcher_match_slice_of_array() -> Result<()> { + let value = &[1, 2, 3]; + verify_that!(value[0..1], len(eq(1))) + } + + #[test] + fn len_matcher_match_slice_of_vec() -> Result<()> { + let value = vec![1, 2, 3]; + let slice = value.as_slice(); + verify_that!(*slice, len(eq(3))) + } + + #[test] + fn len_matcher_match_sized_slice() -> Result<()> { + let value = [1, 2, 3]; + verify_that!(value, len(eq(3))) + } + + #[test] + fn len_matcher_match_btreemap() -> Result<()> { + let value = BTreeMap::from([(1, 2), (2, 3), (3, 4)]); + verify_that!(value, len(eq(3))) + } + + #[test] + fn len_matcher_match_btreeset() -> Result<()> { + let value = BTreeSet::from([1, 2, 3]); + verify_that!(value, len(eq(3))) + } + + #[test] + fn len_matcher_match_binaryheap() -> Result<()> { + let value = BinaryHeap::from([1, 2, 3]); + verify_that!(value, len(eq(3))) + } + + #[test] + fn len_matcher_match_hashmap() -> Result<()> { + let value = HashMap::from([(1, 2), (2, 3), (3, 4)]); + verify_that!(value, len(eq(3))) + } + + #[test] + fn len_matcher_match_hashset() -> Result<()> { + let value = HashSet::from([1, 2, 3]); + verify_that!(value, len(eq(3))) + } + + #[test] + fn len_matcher_match_linkedlist() -> Result<()> { + let value = LinkedList::from([1, 2, 3]); + verify_that!(value, len(eq(3))) + } + + #[test] + fn len_matcher_match_vecdeque() -> Result<()> { + let value = VecDeque::from([1, 2, 3]); + verify_that!(value, len(eq(3))) + } + + #[test] + fn len_matcher_explain_match() -> Result<()> { + struct TestMatcher<T>(PhantomData<T>); + impl<T: Debug> Matcher for TestMatcher<T> { + type ActualT = T; + + fn matches(&self, _: &T) -> MatcherResult { + false.into() + } + + fn describe(&self, _: MatcherResult) -> String { + "called described".into() + } + + fn explain_match(&self, _: &T) -> String { + "called explain_match".into() + } + } + verify_that!( + len(TestMatcher(Default::default())).explain_match(&[1, 2, 3]), + displays_as(eq("which has length 3, called explain_match")) + ) + } + + #[test] + fn len_matcher_error_message() -> Result<()> { + let result = verify_that!(vec![1, 2, 3, 4], len(eq(3))); + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + Value of: vec![1, 2, 3, 4] + Expected: has length, which is equal to 3 + Actual: [1, 2, 3, 4], + which has length 4, which isn't equal to 3" + )))) + ) + } +} diff --git a/src/matchers/lt_matcher.rs b/src/matchers/lt_matcher.rs new file mode 100644 index 0000000..96df00c --- /dev/null +++ b/src/matchers/lt_matcher.rs @@ -0,0 +1,228 @@ +// 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. + +use crate::matcher::{Matcher, MatcherResult}; +use std::{fmt::Debug, marker::PhantomData}; + +/// Matches a value less (in the sense of `<`) than `expected`. +/// +/// The types of `ActualT` of `actual` and `ExpectedT` of `expected` must be +/// comparable via the `PartialOrd` trait. Namely, `ActualT` must implement +/// `PartialOrd<ExpectedT>`. +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// verify_that!(1, lt(2))?; // Passes +/// # Ok(()) +/// # } +/// # fn should_fail() -> Result<()> { +/// verify_that!(2, lt(2))?; // Fails +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// # should_fail().unwrap_err(); +/// ``` +/// +/// In most cases the params neeed to be the same type or they need to be cast +/// explicitly. This can be surprising when comparing integer types or +/// references: +/// +/// ```compile_fail +/// # use googletest::prelude::*; +/// # fn should_not_compile() -> Result<()> { +/// verify_that!(123u32, lt(0u64))?; // Does not compile +/// verify_that!(123u32 as u64, lt(100000000u64))?; // Passes +/// # Ok(()) +/// # } +/// ``` +/// +/// ```compile_fail +/// # use googletest::prelude::*; +/// # fn should_not_compile() -> Result<()> { +/// let actual: &u32 = &2; +/// let expected: u32 = 70; +/// verify_that!(actual, lt(expected))?; // Does not compile +/// # Ok(()) +/// # } +/// ``` +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// let actual: &u32 = &2; +/// let expected: u32 = 70; +/// verify_that!(actual, lt(&expected))?; // Compiles and passes +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// ``` +/// +/// You can find the standard library `PartialOrd` implementation in +/// <https://doc.rust-lang.org/core/cmp/trait.PartialOrd.html#implementors> +pub fn lt<ActualT: Debug + PartialOrd<ExpectedT>, ExpectedT: Debug>( + expected: ExpectedT, +) -> impl Matcher<ActualT = ActualT> { + LtMatcher::<ActualT, _> { expected, phantom: Default::default() } +} + +struct LtMatcher<ActualT, ExpectedT> { + expected: ExpectedT, + phantom: PhantomData<ActualT>, +} + +impl<ActualT: Debug + PartialOrd<ExpectedT>, ExpectedT: Debug> Matcher + for LtMatcher<ActualT, ExpectedT> +{ + type ActualT = ActualT; + + fn matches(&self, actual: &ActualT) -> MatcherResult { + (*actual < self.expected).into() + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + match matcher_result { + MatcherResult::Match => format!("is less than {:?}", self.expected), + MatcherResult::NoMatch => { + format!("is greater than or equal to {:?}", self.expected) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::lt; + use crate::matcher::{Matcher, MatcherResult}; + use crate::prelude::*; + use indoc::indoc; + use std::ffi::OsString; + + #[test] + fn lt_matches_i32_with_i32() -> Result<()> { + let actual: i32 = 10000; + let expected: i32 = 20000; + verify_that!(actual, lt(expected)) + } + + #[test] + fn lt_does_not_match_equal_i32() -> Result<()> { + let matcher = lt(10); + let result = matcher.matches(&10); + verify_that!(result, eq(MatcherResult::NoMatch)) + } + + #[test] + fn lt_does_not_match_lower_i32() -> Result<()> { + let matcher = lt(-50); + let result = matcher.matches(&50); + verify_that!(result, eq(MatcherResult::NoMatch)) + } + + #[test] + fn lt_matches_lesser_str() -> Result<()> { + verify_that!("A", lt("B")) + } + + #[test] + fn lt_does_not_match_bigger_str() -> Result<()> { + let matcher = lt("ab"); + let result = matcher.matches(&"az"); + verify_that!(result, eq(MatcherResult::NoMatch)) + } + + #[test] + fn lt_mismatch_contains_actual_and_expected() -> Result<()> { + let result = verify_that!(481, lt(45)); + let formatted_message = format!("{}", result.unwrap_err()); + + verify_that!( + formatted_message.as_str(), + contains_substring(indoc!( + " + Value of: 481 + Expected: is less than 45 + Actual: 481, + which is greater than or equal to 45 + " + )) + ) + } + + // Test `lt` matcher where actual is `&OsString` and expected is `&str`. + // Note that stdlib is a little bit inconsistent: `PartialOrd` exists for + // `OsString` and `str`, but only in one direction: it's only possible to + // compare `OsString` with `str` if `OsString` is on the left side of the + // "<" operator (`impl PartialOrd<str> for OsString`). + // + // The comparison in the other direction is not defined. + // + // This means that the test case bellow effectively ensures that + // `verify_that(actual, lt(expected))` works if `actual < expected` works + // (regardless whether the `expected < actual` works`). + #[test] + fn lt_matches_owned_osstring_reference_with_string_reference() -> Result<()> { + let expected = "C"; + let actual: OsString = "B".to_string().into(); + verify_that!(&actual, lt(expected)) + } + + #[test] + fn lt_matches_ipv6addr_with_ipaddr() -> Result<()> { + use std::net::IpAddr; + use std::net::Ipv6Addr; + let actual: IpAddr = "127.0.0.1".parse().unwrap(); + let expected: Ipv6Addr = "2001:4860:4860::8844".parse().unwrap(); + verify_that!(actual, lt(expected)) + } + + #[test] + fn lt_matches_with_custom_partial_ord() -> Result<()> { + /// A custom "number" that is smaller than all other numbers. The only + /// things we define about this "special" number is `PartialOrd` and + /// `PartialEq` against `u32`. + #[derive(Debug)] + struct VeryLowNumber {} + + impl std::cmp::PartialEq<u32> for VeryLowNumber { + fn eq(&self, _other: &u32) -> bool { + false + } + } + + // PartialOrd (required for >) requires PartialEq. + impl std::cmp::PartialOrd<u32> for VeryLowNumber { + fn partial_cmp(&self, _other: &u32) -> Option<std::cmp::Ordering> { + Some(std::cmp::Ordering::Less) + } + } + + impl std::cmp::PartialEq<VeryLowNumber> for u32 { + fn eq(&self, _other: &VeryLowNumber) -> bool { + false + } + } + + impl std::cmp::PartialOrd<VeryLowNumber> for u32 { + fn partial_cmp(&self, _other: &VeryLowNumber) -> Option<std::cmp::Ordering> { + Some(std::cmp::Ordering::Greater) + } + } + + let actual = VeryLowNumber {}; + let expected: u32 = 42; + + verify_that!(actual, lt(expected)) + } +} diff --git a/src/matchers/matches_pattern.rs b/src/matchers/matches_pattern.rs new file mode 100644 index 0000000..9c252e5 --- /dev/null +++ b/src/matchers/matches_pattern.rs @@ -0,0 +1,599 @@ +// 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. + +// There are no visible documentation elements in this module; the declarative +// macro is documented at the top level. +#![doc(hidden)] + +/// Matches a value according to a pattern of matchers. +/// +/// This takes as an argument a specification similar to a struct or enum +/// initialiser, where each value is a [`Matcher`][crate::matcher::Matcher] +/// which is applied to the corresponding field. +/// +/// This can be used to match arbitrary combinations of fields on structures +/// using arbitrary matchers: +/// +/// ``` +/// # use googletest::prelude::*; +/// #[derive(Debug)] +/// struct MyStruct { +/// a_field: String, +/// another_field: String, +/// } +/// +/// let my_struct = MyStruct { +/// a_field: "Something to believe in".into(), +/// another_field: "Something else".into() +/// }; +/// verify_that!(my_struct, matches_pattern!(MyStruct { +/// a_field: starts_with("Something"), +/// another_field: ends_with("else"), +/// })) +/// # .unwrap(); +/// ``` +/// +/// It is not required to include all named fields in the specification. Omitted +/// fields have no effect on the output of the matcher. +/// +/// ``` +/// # use googletest::prelude::*; +/// # #[derive(Debug)] +/// # struct MyStruct { +/// # a_field: String, +/// # another_field: String, +/// # } +/// # +/// # let my_struct = MyStruct { +/// # a_field: "Something to believe in".into(), +/// # another_field: "Something else".into() +/// # }; +/// verify_that!(my_struct, matches_pattern!(MyStruct { +/// a_field: starts_with("Something"), +/// // another_field is missing, so it may be anything. +/// })) +/// # .unwrap(); +/// ``` +/// +/// One can use it recursively to match nested structures: +/// +/// ``` +/// # use googletest::prelude::*; +/// #[derive(Debug)] +/// struct MyStruct { +/// a_nested_struct: MyInnerStruct, +/// } +/// +/// #[derive(Debug)] +/// struct MyInnerStruct { +/// a_field: String, +/// } +/// +/// let my_struct = MyStruct { +/// a_nested_struct: MyInnerStruct { a_field: "Something to believe in".into() }, +/// }; +/// verify_that!(my_struct, matches_pattern!(MyStruct { +/// a_nested_struct: matches_pattern!(MyInnerStruct { +/// a_field: starts_with("Something"), +/// }), +/// })) +/// # .unwrap(); +/// ``` +/// +/// One can use the alias [`pat`][crate::pat] to make this less verbose: +/// +/// ``` +/// # use googletest::prelude::*; +/// # #[derive(Debug)] +/// # struct MyStruct { +/// # a_nested_struct: MyInnerStruct, +/// # } +/// # +/// # #[derive(Debug)] +/// # struct MyInnerStruct { +/// # a_field: String, +/// # } +/// # +/// # let my_struct = MyStruct { +/// # a_nested_struct: MyInnerStruct { a_field: "Something to believe in".into() }, +/// # }; +/// verify_that!(my_struct, matches_pattern!(MyStruct { +/// a_nested_struct: pat!(MyInnerStruct { +/// a_field: starts_with("Something"), +/// }), +/// })) +/// # .unwrap(); +/// ``` +/// +/// In addition to fields, one can match on the outputs of methods +/// ("properties"): +/// +/// ``` +/// # use googletest::prelude::*; +/// #[derive(Debug)] +/// struct MyStruct { +/// a_field: String, +/// } +/// +/// impl MyStruct { +/// fn get_a_field(&self) -> String { self.a_field.clone() } +/// } +/// +/// let my_struct = MyStruct { a_field: "Something to believe in".into() }; +/// verify_that!(my_struct, matches_pattern!(MyStruct { +/// get_a_field(): starts_with("Something"), +/// })) +/// # .unwrap(); +/// ``` +/// +/// **Important**: The method should be pure function with a deterministic +/// output and no side effects. In particular, in the event of an assertion +/// failure, it will be invoked a second time, with the assertion failure output +/// reflecting the *second* invocation. +/// +/// These may also include extra parameters you pass in: +/// +/// ``` +/// # use googletest::prelude::*; +/// # #[derive(Debug)] +/// # struct MyStruct { +/// # a_field: String, +/// # } +/// # +/// impl MyStruct { +/// fn append_to_a_field(&self, suffix: &str) -> String { self.a_field.clone() + suffix } +/// } +/// +/// # let my_struct = MyStruct { a_field: "Something to believe in".into() }; +/// verify_that!(my_struct, matches_pattern!(MyStruct { +/// append_to_a_field("a suffix"): ends_with("a suffix"), +/// })) +/// # .unwrap(); +/// ``` +/// +/// If the method returns a reference, precede it with the keyword `ref`: +/// +/// ``` +/// # use googletest::prelude::*; +/// # #[derive(Debug)] +/// # struct MyStruct { +/// # a_field: String, +/// # } +/// # +/// impl MyStruct { +/// fn get_a_field_ref(&self) -> &String { &self.a_field } +/// } +/// +/// # 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"), +/// })) +/// # .unwrap(); +/// ``` +/// +/// One can also match tuple structs with up to 10 fields. In this case, all +/// fields must have matchers: +/// +/// ``` +/// # use googletest::prelude::*; +/// #[derive(Debug)] +/// struct MyTupleStruct(String, String); +/// +/// let my_struct = MyTupleStruct("Something".into(), "Some other thing".into()); +/// verify_that!( +/// my_struct, +/// matches_pattern!(MyTupleStruct(eq("Something"), eq("Some other thing"))) +/// ) +/// # .unwrap(); +/// ``` +/// +/// One can also match enum values: +/// +/// ``` +/// # use googletest::prelude::*; +/// #[derive(Debug)] +/// enum MyEnum { +/// A(u32), +/// B, +/// } +/// +/// # fn should_pass() -> Result<()> { +/// verify_that!(MyEnum::A(123), matches_pattern!(MyEnum::A(eq(123))))?; // Passes +/// # Ok(()) +/// # } +/// # fn should_fail() -> Result<()> { +/// verify_that!(MyEnum::B, matches_pattern!(MyEnum::A(eq(123))))?; // Fails - wrong enum variant +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// # should_fail().unwrap_err(); +/// ``` +/// +/// This macro does not support plain (non-struct) tuples. Use the macro +/// [`tuple`] for that purpose. +/// +/// Trailing commas are allowed (but not required) in both ordinary and tuple +/// structs. +/// +/// Unfortunately, this matcher does *not* work with methods returning string +/// slices: +/// +/// ```compile_fail +/// # use googletest::prelude::*; +/// # #[derive(Debug)] +/// pub struct MyStruct { +/// a_string: String, +/// } +/// impl MyStruct { +/// pub fn get_a_string(&self) -> &str { &self.a_string } +/// } +/// +/// let value = MyStruct { a_string: "A string".into() }; +/// verify_that!(value, matches_pattern!( MyStruct { +/// get_a_string(): eq("A string"), // Does not compile +/// })) +/// # .unwrap(); +/// ``` +#[macro_export] +macro_rules! matches_pattern { + ($($t:tt)*) => { $crate::matches_pattern_internal!($($t)*) } +} + +// Internal-only macro created so that the macro definition does not appear in +// generated documentation. +#[doc(hidden)] +#[macro_export] +macro_rules! matches_pattern_internal { + ( + [$($struct_name:tt)*], + { $field_name:ident : $matcher:expr $(,)? } + ) => { + $crate::matchers::is_matcher::is( + stringify!($($struct_name)*), + all!(field!($($struct_name)*.$field_name, $matcher)) + ) + }; + + ( + [$($struct_name:tt)*], + { $property_name:ident($($argument:expr),* $(,)?) : $matcher:expr $(,)? } + ) => { + $crate::matchers::is_matcher::is( + stringify!($($struct_name)*), + all!(property!($($struct_name)*.$property_name($($argument),*), $matcher)) + ) + }; + + ( + [$($struct_name:tt)*], + { ref $property_name:ident($($argument:expr),* $(,)?) : $matcher:expr $(,)? } + ) => { + $crate::matchers::is_matcher::is( + stringify!($($struct_name)*), + all!(property!(ref $($struct_name)*.$property_name($($argument),*), $matcher)) + ) + }; + + ( + [$($struct_name:tt)*], + { $field_name:ident : $matcher:expr, $($rest:tt)* } + ) => { + $crate::matches_pattern_internal!( + all!(field!($($struct_name)*.$field_name, $matcher)), + [$($struct_name)*], + { $($rest)* } + ) + }; + + ( + [$($struct_name:tt)*], + { $property_name:ident($($argument:expr),* $(,)?) : $matcher:expr, $($rest:tt)* } + ) => { + $crate::matches_pattern_internal!( + all!(property!($($struct_name)*.$property_name($($argument),*), $matcher)), + [$($struct_name)*], + { $($rest)* } + ) + }; + + ( + [$($struct_name:tt)*], + { ref $property_name:ident($($argument:expr),* $(,)?) : $matcher:expr, $($rest:tt)* } + ) => { + $crate::matches_pattern_internal!( + all!(property!(ref $($struct_name)*.$property_name($($argument),*), $matcher)), + [$($struct_name)*], + { $($rest)* } + ) + }; + + ( + all!($($processed:tt)*), + [$($struct_name:tt)*], + { $field_name:ident : $matcher:expr $(,)? } + ) => { + $crate::matchers::is_matcher::is(stringify!($($struct_name)*), all!( + $($processed)*, + field!($($struct_name)*.$field_name, $matcher) + )) + }; + + ( + all!($($processed:tt)*), + [$($struct_name:tt)*], + { $property_name:ident($($argument:expr),* $(,)?) : $matcher:expr $(,)? } + ) => { + $crate::matchers::is_matcher::is(stringify!($($struct_name)*), all!( + $($processed)*, + property!($($struct_name)*.$property_name($($argument),*), $matcher) + )) + }; + + ( + all!($($processed:tt)*), + [$($struct_name:tt)*], + { ref $property_name:ident($($argument:expr),* $(,)?) : $matcher:expr $(,)? } + ) => { + $crate::matchers::is_matcher::is(stringify!($($struct_name)*), all!( + $($processed)*, + property!(ref $($struct_name)*.$property_name($($argument),*), $matcher) + )) + }; + + ( + all!($($processed:tt)*), + [$($struct_name:tt)*], + { $field_name:ident : $matcher:expr, $($rest:tt)* } + ) => { + $crate::matches_pattern_internal!( + all!( + $($processed)*, + field!($($struct_name)*.$field_name, $matcher) + ), + [$($struct_name)*], + { $($rest)* } + ) + }; + + ( + all!($($processed:tt)*), + [$($struct_name:tt)*], + { $property_name:ident($($argument:expr),* $(,)?) : $matcher:expr, $($rest:tt)* } + ) => { + $crate::matches_pattern_internal!( + all!( + $($processed)*, + property!($($struct_name)*.$property_name($($argument),*), $matcher) + ), + [$($struct_name)*], + { $($rest)* } + ) + }; + + ( + all!($($processed:tt)*), + [$($struct_name:tt)*], + { ref $property_name:ident($($argument:expr),* $(,)?) : $matcher:expr, $($rest:tt)* } + ) => { + $crate::matches_pattern_internal!( + all!( + $($processed)*, + property!(ref $($struct_name)*.$property_name($($argument),*), $matcher) + ), + [$($struct_name)*], + { $($rest)* } + ) + }; + + ( + [$($struct_name:tt)*], + ) => { + $crate::matchers::predicate(|v| matches!(v, $($struct_name)*)) + .with_description( + concat!("is ", stringify!($($struct_name)*)), + concat!("is not ", stringify!($($struct_name)*)), + ) + }; + + ( + [$($struct_name:tt)*], + ($matcher:expr $(,)?) + ) => { + $crate::matchers::is_matcher::is( + stringify!($($struct_name)*), + all!(field!($($struct_name)*.0, $matcher)) + ) + }; + + ( + [$($struct_name:tt)*], + ($matcher:expr, $($rest:tt)*) + ) => { + $crate::matches_pattern_internal!( + all!( + field!($($struct_name)*.0, $matcher) + ), + [$($struct_name)*], + 1, + ($($rest)*) + ) + }; + + ( + all!($($processed:tt)*), + [$($struct_name:tt)*], + $field:tt, + ($matcher:expr $(,)?) + ) => { + $crate::matchers::is_matcher::is(stringify!($($struct_name)*), all!( + $($processed)*, + field!($($struct_name)*.$field, $matcher) + )) + }; + + // We need to repeat this once for every supported field position, unfortunately. There appears + // to be no way in declarative macros to compute $field + 1 and have the result evaluated to a + // token which can be used as a tuple index. + ( + all!($($processed:tt)*), + [$($struct_name:tt)*], + 1, + ($matcher:expr, $($rest:tt)*) + ) => { + $crate::matches_pattern_internal!( + all!( + $($processed)*, + field!($($struct_name)*.1, $matcher) + ), + [$($struct_name)*], + 2, + ($($rest)*) + ) + }; + + ( + all!($($processed:tt)*), + [$($struct_name:tt)*], + 2, + ($matcher:expr, $($rest:tt)*) + ) => { + $crate::matches_pattern_internal!( + all!( + $($processed)*, + field!($($struct_name)*.2, $matcher) + ), + [$($struct_name)*], + 3, + ($($rest)*) + ) + }; + + ( + all!($($processed:tt)*), + [$($struct_name:tt)*], + 3, + ($matcher:expr, $($rest:tt)*) + ) => { + $crate::matches_pattern_internal!( + all!( + $($processed)*, + field!($($struct_name)*.3, $matcher) + ), + [$($struct_name)*], + 4, + ($($rest)*) + ) + }; + + ( + all!($($processed:tt)*), + [$($struct_name:tt)*], + 4, + ($matcher:expr, $($rest:tt)*) + ) => { + $crate::matches_pattern_internal!( + all!( + $($processed)*, + field!($($struct_name)*.4, $matcher) + ), + [$($struct_name)*], + 5, + ($($rest)*) + ) + }; + + ( + all!($($processed:tt)*), + [$($struct_name:tt)*], + 5, + ($matcher:expr, $($rest:tt)*) + ) => { + $crate::matches_pattern_internal!( + all!( + $($processed)*, + field!($($struct_name)*.5, $matcher) + ), + [$($struct_name)*], + 6, + ($($rest)*) + ) + }; + + ( + all!($($processed:tt)*), + [$($struct_name:tt)*], + 6, + ($matcher:expr, $($rest:tt)*) + ) => { + $crate::matches_pattern_internal!( + all!( + $($processed)*, + field!($($struct_name)*.6, $matcher) + ), + [$($struct_name)*], + 7, + ($($rest)*) + ) + }; + + ( + all!($($processed:tt)*), + [$($struct_name:tt)*], + 7, + ($matcher:expr, $($rest:tt)*) + ) => { + $crate::matches_pattern_internal!( + all!( + $($processed)*, + field!($($struct_name)*.7, $matcher) + ), + [$($struct_name)*], + 8, + ($($rest)*) + ) + }; + + ( + all!($($processed:tt)*), + [$($struct_name:tt)*], + 8, + ($matcher:expr, $($rest:tt)*) + ) => { + $crate::matches_pattern_internal!( + all!( + $($processed)*, + field!($($struct_name)*.8, $matcher) + ), + [$($struct_name)*], + 9, + ($($rest)*) + ) + }; + + ([$($struct_name:tt)*], $first:tt $($rest:tt)*) => { + $crate::matches_pattern_internal!([$($struct_name)* $first], $($rest)*) + }; + + ($first:tt $($rest:tt)*) => {{ + #[allow(unused)] + use $crate::{all, field, property}; + $crate::matches_pattern_internal!([$first], $($rest)*) + }}; +} + +/// An alias for [`matches_pattern`]. +#[macro_export] +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 new file mode 100644 index 0000000..d0001e2 --- /dev/null +++ b/src/matchers/matches_regex_matcher.rs @@ -0,0 +1,210 @@ +// 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. + +use crate::matcher::{Matcher, MatcherResult}; +use regex::Regex; +use std::fmt::Debug; +use std::marker::PhantomData; +use std::ops::Deref; + +/// Matches a string the entirety of which which matches the given regular +/// expression. +/// +/// This is similar to [`contains_regex`][crate::matchers::contains_regex], +/// except that the match must cover the whole string and not a substring. +/// +/// Both the actual value and the expected regular expression may be either a +/// `String` or a string reference. +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass_1() -> Result<()> { +/// verify_that!("Some value", matches_regex("S.*e"))?; // Passes +/// # Ok(()) +/// # } +/// # fn should_fail_1() -> Result<()> { +/// verify_that!("Another value", matches_regex("Some"))?; // Fails +/// # Ok(()) +/// # } +/// # fn should_fail_2() -> Result<()> { +/// verify_that!("Some value", matches_regex("Some"))?; // Fails +/// # Ok(()) +/// # } +/// # fn should_pass_2() -> Result<()> { +/// verify_that!("Some value".to_string(), matches_regex(".*v.*e"))?; // Passes +/// verify_that!("Some value", matches_regex(".*v.*e".to_string()))?; // Passes +/// # Ok(()) +/// # } +/// # should_pass_1().unwrap(); +/// # should_fail_1().unwrap_err(); +/// # should_fail_2().unwrap_err(); +/// # should_pass_2().unwrap(); +/// ``` +/// +/// Panics if the given `pattern` is not a syntactically valid regular +/// expression. +// N.B. This returns the concrete type rather than an impl Matcher so that it +// can act simultaneously as a Matcher<str> and a Matcher<String>. Otherwise the +// compiler treats it as a Matcher<str> only and the code +// verify_that!("Some value".to_string(), matches_regex(".*value"))?; +// doesn't compile. +pub fn matches_regex<ActualT: ?Sized, PatternT: Deref<Target = str>>( + pattern: PatternT, +) -> MatchesRegexMatcher<ActualT, PatternT> { + let adjusted_pattern = format!("^{}$", pattern.deref()); + let regex = Regex::new(adjusted_pattern.as_str()).unwrap(); + MatchesRegexMatcher { + regex, + pattern, + _adjusted_pattern: adjusted_pattern, + phantom: Default::default(), + } +} + +/// A matcher matching a string-like type matching a given regular expression. +/// +/// Intended only to be used from the function [`matches_regex`] only. +/// Should not be referenced by code outside this library. +pub struct MatchesRegexMatcher<ActualT: ?Sized, PatternT: Deref<Target = str>> { + regex: Regex, + pattern: PatternT, + _adjusted_pattern: String, + phantom: PhantomData<ActualT>, +} + +impl<PatternT, ActualT> Matcher for MatchesRegexMatcher<ActualT, PatternT> +where + PatternT: Deref<Target = str>, + ActualT: AsRef<str> + Debug + ?Sized, +{ + type ActualT = ActualT; + + fn matches(&self, actual: &Self::ActualT) -> MatcherResult { + self.regex.is_match(actual.as_ref()).into() + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + match matcher_result { + MatcherResult::Match => { + format!("matches the regular expression {:#?}", self.pattern.deref()) + } + MatcherResult::NoMatch => { + format!("doesn't match the regular expression {:#?}", self.pattern.deref()) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::{matches_regex, MatchesRegexMatcher}; + use crate::matcher::{Matcher, MatcherResult}; + use crate::prelude::*; + + #[test] + fn matches_regex_matches_string_reference_with_pattern() -> Result<()> { + let matcher = matches_regex("S.*e"); + + let result = matcher.matches("Some value"); + + verify_that!(result, eq(MatcherResult::Match)) + } + + #[test] + fn matches_regex_does_not_match_string_without_pattern() -> Result<()> { + let matcher = matches_regex("Another"); + + let result = matcher.matches("Some value"); + + verify_that!(result, eq(MatcherResult::NoMatch)) + } + + #[test] + fn matches_regex_does_not_match_string_only_beginning_of_which_matches() -> Result<()> { + let matcher = matches_regex("Some"); + + let result = matcher.matches("Some value"); + + verify_that!(result, eq(MatcherResult::NoMatch)) + } + + #[test] + fn matches_regex_does_not_match_string_only_end_of_which_matches() -> Result<()> { + let matcher = matches_regex("value"); + + let result = matcher.matches("Some value"); + + verify_that!(result, eq(MatcherResult::NoMatch)) + } + + #[test] + fn matches_regex_matches_owned_string_with_pattern() -> Result<()> { + let matcher = matches_regex(".*value"); + + let result = matcher.matches(&"Some value".to_string()); + + verify_that!(result, eq(MatcherResult::Match)) + } + + #[test] + fn matches_regex_matches_string_when_regex_has_beginning_of_string_marker() -> Result<()> { + let matcher = matches_regex("^Some value"); + + let result = matcher.matches("Some value"); + + verify_that!(result, eq(MatcherResult::Match)) + } + + #[test] + fn matches_regex_matches_string_when_regex_has_end_of_string_marker() -> Result<()> { + let matcher = matches_regex("Some value$"); + + let result = matcher.matches("Some value"); + + verify_that!(result, eq(MatcherResult::Match)) + } + + #[test] + fn matches_regex_matches_string_when_regex_has_both_end_markers() -> Result<()> { + let matcher = matches_regex("^Some value$"); + + let result = matcher.matches("Some value"); + + verify_that!(result, eq(MatcherResult::Match)) + } + + #[test] + fn matches_regex_matches_string_reference_with_owned_string() -> Result<()> { + let matcher = matches_regex(".*value".to_string()); + + let result = matcher.matches("Some value"); + + verify_that!(result, eq(MatcherResult::Match)) + } + + #[test] + fn verify_that_works_with_owned_string() -> Result<()> { + verify_that!("Some value".to_string(), matches_regex(".*value")) + } + + #[test] + fn matches_regex_displays_quoted_debug_of_pattern() -> Result<()> { + let matcher: MatchesRegexMatcher<&str, _> = matches_regex("\n"); + + verify_that!( + Matcher::describe(&matcher, MatcherResult::Match), + eq("matches the regular expression \"\\n\"") + ) + } +} diff --git a/src/matchers/mod.rs b/src/matchers/mod.rs new file mode 100644 index 0000000..f8aef10 --- /dev/null +++ b/src/matchers/mod.rs @@ -0,0 +1,89 @@ +// 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. + +//! All built-in matchers of this crate are in submodules of this module. + +pub mod all_matcher; +pub mod any_matcher; +mod anything_matcher; +mod char_count_matcher; +pub mod conjunction_matcher; +mod container_eq_matcher; +mod contains_matcher; +mod contains_regex_matcher; +pub mod disjunction_matcher; +mod display_matcher; +mod each_matcher; +pub mod elements_are_matcher; +mod empty_matcher; +mod eq_deref_of_matcher; +mod eq_matcher; +mod err_matcher; +pub mod field_matcher; +mod ge_matcher; +mod gt_matcher; +mod has_entry_matcher; +pub mod is_matcher; +mod is_nan_matcher; +mod le_matcher; +mod len_matcher; +mod lt_matcher; +mod matches_pattern; +mod matches_regex_matcher; +mod near_matcher; +mod none_matcher; +mod not_matcher; +mod ok_matcher; +mod points_to_matcher; +pub mod pointwise_matcher; +mod predicate_matcher; +pub 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; + +pub use anything_matcher::anything; +pub use char_count_matcher::char_count; +pub use container_eq_matcher::container_eq; +pub use contains_matcher::{contains, ContainsMatcher}; +pub use contains_regex_matcher::contains_regex; +pub use display_matcher::displays_as; +pub use each_matcher::each; +pub use empty_matcher::empty; +pub use eq_deref_of_matcher::eq_deref_of; +pub use eq_matcher::{eq, EqMatcher}; +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_nan_matcher::is_nan; +pub use le_matcher::le; +pub use len_matcher::len; +pub use lt_matcher::lt; +pub use matches_regex_matcher::matches_regex; +pub use near_matcher::{approx_eq, near, NearMatcher}; +pub use none_matcher::none; +pub use not_matcher::not; +pub use ok_matcher::ok; +pub use points_to_matcher::points_to; +pub use predicate_matcher::{predicate, PredicateMatcher}; +pub use some_matcher::some; +pub use str_matcher::{ + contains_substring, ends_with, starts_with, StrMatcher, StrMatcherConfigurator, +}; +pub use subset_of_matcher::subset_of; +pub use superset_of_matcher::superset_of; diff --git a/src/matchers/near_matcher.rs b/src/matchers/near_matcher.rs new file mode 100644 index 0000000..484939c --- /dev/null +++ b/src/matchers/near_matcher.rs @@ -0,0 +1,347 @@ +// 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. + +use crate::matcher::{Matcher, MatcherResult}; +use num_traits::{Float, FloatConst}; +use std::fmt::Debug; + +/// Matches a value equal within `max_abs_error` of `expected`. +/// +/// The type `T` of the actual, `expected`, and `max_abs_error` values must +/// implement [`Float`]. +/// +/// The values `expected` and `max_abs_error` may not be NaN. The value +/// `max_abs_error` must be non-negative. The matcher panics on construction +/// otherwise. +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass_1() -> Result<()> { +/// verify_that!(1.0, near(1.0, 0.1))?; // Passes +/// verify_that!(1.01, near(1.0, 0.1))?; // Passes +/// verify_that!(1.25, near(1.0, 0.25))?; // Passes +/// verify_that!(0.75, near(1.0, 0.25))?; // Passes +/// # Ok(()) +/// # } +/// # fn should_fail_1() -> Result<()> { +/// verify_that!(1.101, near(1.0, 0.1))?; // Fails +/// # Ok(()) +/// # } +/// # fn should_fail_2() -> Result<()> { +/// verify_that!(0.899, near(1.0, 0.1))?; // Fails +/// # Ok(()) +/// # } +/// # fn should_pass_2() -> Result<()> { +/// verify_that!(100.25, near(100.0, 0.25))?; // Passes +/// # Ok(()) +/// # } +/// # should_pass_1().unwrap(); +/// # should_fail_1().unwrap_err(); +/// # should_fail_2().unwrap_err(); +/// # should_pass_2().unwrap(); +/// ``` +/// +/// The default behaviour for special values is consistent with the IEEE +/// floating point standard. Thus infinity is infinitely far away from any +/// floating point value: +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_fail_1() -> Result<()> { +/// verify_that!(f64::INFINITY, near(0.0, f64::MAX))?; // Fails +/// # Ok(()) +/// # } +/// # fn should_fail_2() -> Result<()> { +/// verify_that!(0.0, near(f64::INFINITY, f64::MAX))?; // Fails +/// # Ok(()) +/// # } +/// # fn should_fail_3() -> Result<()> { +/// verify_that!(f64::INFINITY, near(f64::INFINITY, f64::MAX))?; // Fails +/// # Ok(()) +/// # } +/// # should_fail_1().unwrap_err(); +/// # should_fail_2().unwrap_err(); +/// # should_fail_3().unwrap_err(); +/// ``` +/// +/// Similarly, by default, `NaN` is infinitely far away from any value: +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_fail_1() -> Result<()> { +/// verify_that!(f64::NAN, near(0.0, f64::MAX))?; // Fails +/// # Ok(()) +/// # } +/// # fn should_fail_2() -> Result<()> { +/// verify_that!(0.0, near(f64::NAN, f64::MAX))?; // Fails +/// # Ok(()) +/// # } +/// # fn should_fail_3() -> Result<()> { +/// verify_that!(f64::NAN, near(f64::NAN, f64::MAX))?; // Fails +/// # Ok(()) +/// # } +/// # should_fail_1().unwrap_err(); +/// # should_fail_2().unwrap_err(); +/// # should_fail_3().unwrap_err(); +/// ``` +/// +/// To treat two `NaN` values as equal, use the method +/// [`NearMatcher::nans_are_equal`]. +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// verify_that!(f64::NAN, near(f64::NAN, f64::MAX).nans_are_equal())?; // Passes +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// ``` +pub fn near<T: Debug + Float + Copy>(expected: T, max_abs_error: T) -> NearMatcher<T> { + if max_abs_error.is_nan() { + panic!("max_abs_error must not be NaN"); + } + if max_abs_error < T::zero() { + panic!("max_abs_error must be non-negative"); + } + NearMatcher { expected, max_abs_error, nans_are_equal: false } +} + +/// Matches a value approximately equal to `expected`. +/// +/// This automatically computes a tolerance from the magnitude of `expected` and +/// matches any actual value within this tolerance of the expected value. The +/// tolerance is chosen to account for the inaccuracies in most ordinary +/// floating point calculations. +/// +/// Otherwise this works analogously to [`near`]; see its documentation for +/// further notes. +pub fn approx_eq<T: Debug + Float + FloatConst + Copy>(expected: T) -> NearMatcher<T> { + // The FloatConst trait doesn't offer 2 as a constant but does offer 1. + let five_bits_of_mantissa = (T::one() + T::one()).powi(5); + let abs_tolerance = five_bits_of_mantissa * T::epsilon(); + let max_abs_error = T::max(expected.abs() * abs_tolerance, abs_tolerance); + NearMatcher { expected, max_abs_error, nans_are_equal: false } +} + +/// A matcher which matches floating-point numbers approximately equal to its +/// expected value. +pub struct NearMatcher<T: Debug> { + expected: T, + max_abs_error: T, + nans_are_equal: bool, +} + +impl<T: Debug> NearMatcher<T> { + /// Configures this instance to treat two NaNs as equal. + /// + /// This behaviour differs from the IEEE standad for floating point which + /// treats two NaNs as infinitely far apart. + pub fn nans_are_equal(mut self) -> Self { + self.nans_are_equal = true; + self + } + + /// Configures this instance to treat two NaNs as not equal. + /// + /// This behaviour complies with the IEEE standad for floating point. It is + /// the default behaviour for this matcher, so invoking this method is + /// usually redunant. + pub fn nans_are_not_equal(mut self) -> Self { + self.nans_are_equal = false; + self + } +} + +impl<T: Debug + Float> Matcher for NearMatcher<T> { + type ActualT = T; + + fn matches(&self, actual: &T) -> MatcherResult { + if self.nans_are_equal && self.expected.is_nan() && actual.is_nan() { + return MatcherResult::Match; + } + + let delta = *actual - self.expected; + if delta >= -self.max_abs_error && delta <= self.max_abs_error { + MatcherResult::Match + } else { + MatcherResult::NoMatch + } + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + match matcher_result { + MatcherResult::Match => { + format!("is within {:?} of {:?}", self.max_abs_error, self.expected) + } + MatcherResult::NoMatch => { + format!("isn't within {:?} of {:?}", self.max_abs_error, self.expected) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::{approx_eq, near}; + use crate::matcher::{Matcher, MatcherResult}; + use crate::prelude::*; + + #[test] + fn matches_value_inside_range() -> Result<()> { + let matcher = near(1.0f64, 0.1f64); + + let result = matcher.matches(&1.0f64); + + verify_that!(result, eq(MatcherResult::Match)) + } + + #[test] + fn matches_value_at_low_end_of_range() -> Result<()> { + let matcher = near(1.0f64, 0.1f64); + + let result = matcher.matches(&0.9f64); + + verify_that!(result, eq(MatcherResult::Match)) + } + + #[test] + fn matches_value_at_high_end_of_range() -> Result<()> { + let matcher = near(1.0f64, 0.25f64); + + let result = matcher.matches(&1.25f64); + + verify_that!(result, eq(MatcherResult::Match)) + } + + #[test] + fn does_not_match_value_below_low_end_of_range() -> Result<()> { + let matcher = near(1.0f64, 0.1f64); + + let result = matcher.matches(&0.899999f64); + + verify_that!(result, eq(MatcherResult::NoMatch)) + } + + #[test] + fn does_not_match_value_above_high_end_of_range() -> Result<()> { + let matcher = near(1.0f64, 0.1f64); + + let result = matcher.matches(&1.100001f64); + + verify_that!(result, eq(MatcherResult::NoMatch)) + } + + #[test] + fn nan_is_not_near_a_number() -> Result<()> { + let matcher = near(0.0f64, f64::MAX); + + let result = matcher.matches(&f64::NAN); + + verify_that!(result, eq(MatcherResult::NoMatch)) + } + + #[test] + fn nan_is_not_near_nan_by_default() -> Result<()> { + verify_that!(f64::NAN, not(near(f64::NAN, f64::MAX))) + } + + #[test] + fn nan_is_not_near_nan_when_explicitly_configured() -> Result<()> { + verify_that!(f64::NAN, not(near(f64::NAN, f64::MAX).nans_are_not_equal())) + } + + #[test] + fn nan_is_near_nan_if_nans_are_equal() -> Result<()> { + verify_that!(f64::NAN, near(f64::NAN, f64::MAX).nans_are_equal()) + } + + #[test] + fn nan_is_not_near_number_when_nans_are_equal() -> Result<()> { + verify_that!(f64::NAN, not(near(0.0, f64::MAX).nans_are_equal())) + } + + #[test] + fn number_is_not_near_nan_when_nans_are_equal() -> Result<()> { + verify_that!(0.0, not(near(f64::NAN, f64::MAX).nans_are_equal())) + } + + #[test] + fn inf_is_not_near_inf() -> Result<()> { + let matcher = near(f64::INFINITY, f64::MAX); + + let result = matcher.matches(&f64::INFINITY); + + verify_that!(result, eq(MatcherResult::NoMatch)) + } + + #[test] + fn inf_is_not_near_a_number() -> Result<()> { + let matcher = near(f64::INFINITY, f64::MAX); + + let result = matcher.matches(&f64::MIN); + + verify_that!(result, eq(MatcherResult::NoMatch)) + } + + #[test] + fn any_two_numbers_are_within_inf_of_each_other() -> Result<()> { + let matcher = near(f64::MIN, f64::INFINITY); + + let result = matcher.matches(&f64::MAX); + + verify_that!(result, eq(MatcherResult::Match)) + } + + #[::core::prelude::v1::test] + #[should_panic] + fn panics_if_max_abs_error_is_nan() { + near(0.0, f64::NAN); + } + + #[::core::prelude::v1::test] + #[should_panic] + fn panics_if_tolerance_is_negative() { + near(0.0, -1.0); + } + + #[test] + fn approx_eq_matches_equal_number() -> Result<()> { + verify_that!(1.0f64, approx_eq(1.0f64)) + } + + #[test] + fn approx_eq_matches_really_close_f64_number() -> Result<()> { + verify_that!(1.0f64, approx_eq(1.0 + 16.0 * f64::EPSILON)) + } + + #[test] + fn approx_eq_matches_really_close_f64_number_to_large_number() -> Result<()> { + verify_that!(1000f64, approx_eq(1000.0 + 16000.0 * f64::EPSILON)) + } + + #[test] + fn approx_eq_matches_really_close_f64_number_to_zero() -> Result<()> { + verify_that!(16.0 * f64::EPSILON, approx_eq(0.0)) + } + + #[test] + fn approx_eq_matches_really_close_f32_number() -> Result<()> { + verify_that!(1.0f32, approx_eq(1.0 + 16.0 * f32::EPSILON)) + } + + #[test] + fn approx_eq_does_not_match_distant_number() -> Result<()> { + verify_that!(0.0f64, not(approx_eq(1.0f64))) + } +} diff --git a/src/matchers/none_matcher.rs b/src/matchers/none_matcher.rs new file mode 100644 index 0000000..e48d549 --- /dev/null +++ b/src/matchers/none_matcher.rs @@ -0,0 +1,80 @@ +// 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. + +use crate::matcher::{Matcher, MatcherResult}; +use std::fmt::Debug; +use std::marker::PhantomData; + +/// Matches an `Option` containing `None`. +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// verify_that!(None::<()>, none())?; // Passes +/// # Ok(()) +/// # } +/// # fn should_fail() -> Result<()> { +/// verify_that!(Some("Some value"), none())?; // Fails +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// # should_fail().unwrap_err(); +/// ``` +pub fn none<T: Debug>() -> impl Matcher<ActualT = Option<T>> { + NoneMatcher::<T> { phantom: Default::default() } +} + +struct NoneMatcher<T> { + phantom: PhantomData<T>, +} + +impl<T: Debug> Matcher for NoneMatcher<T> { + type ActualT = Option<T>; + + fn matches(&self, actual: &Option<T>) -> MatcherResult { + (actual.is_none()).into() + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + match matcher_result { + MatcherResult::Match => "is none".to_string(), + MatcherResult::NoMatch => "is some(_)".to_string(), + } + } +} + +#[cfg(test)] +mod tests { + use super::none; + use crate::matcher::{Matcher, MatcherResult}; + use crate::prelude::*; + + #[test] + fn none_matches_option_with_none() -> Result<()> { + let matcher = none::<i32>(); + + let result = matcher.matches(&None); + + verify_that!(result, eq(MatcherResult::Match)) + } + + #[test] + fn none_does_not_match_option_with_value() -> Result<()> { + let matcher = none(); + + let result = matcher.matches(&Some(0)); + + verify_that!(result, eq(MatcherResult::NoMatch)) + } +} diff --git a/src/matchers/not_matcher.rs b/src/matchers/not_matcher.rs new file mode 100644 index 0000000..1dff791 --- /dev/null +++ b/src/matchers/not_matcher.rs @@ -0,0 +1,106 @@ +// 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. + +use crate::matcher::{Matcher, MatcherResult}; +use std::{fmt::Debug, marker::PhantomData}; + +/// Matches the actual value exactly when the inner matcher does _not_ match. +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// verify_that!(0, not(eq(1)))?; // Passes +/// # Ok(()) +/// # } +/// # fn should_fail() -> Result<()> { +/// verify_that!(0, not(eq(0)))?; // Fails +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// # should_fail().unwrap_err(); +/// ``` +pub fn not<T: Debug, InnerMatcherT: Matcher<ActualT = T>>( + inner: InnerMatcherT, +) -> impl Matcher<ActualT = T> { + NotMatcher::<T, _> { inner, phantom: Default::default() } +} + +struct NotMatcher<T, InnerMatcherT> { + inner: InnerMatcherT, + phantom: PhantomData<T>, +} + +impl<T: Debug, InnerMatcherT: Matcher<ActualT = T>> Matcher for NotMatcher<T, InnerMatcherT> { + type ActualT = T; + + fn matches(&self, actual: &T) -> MatcherResult { + match self.inner.matches(actual) { + MatcherResult::Match => MatcherResult::NoMatch, + MatcherResult::NoMatch => MatcherResult::Match, + } + } + + fn explain_match(&self, actual: &T) -> String { + self.inner.explain_match(actual) + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + self.inner.describe(if matcher_result.into() { + MatcherResult::NoMatch + } else { + MatcherResult::Match + }) + } +} + +#[cfg(test)] +mod tests { + use super::not; + use crate::matcher::{Matcher, MatcherResult}; + use crate::prelude::*; + use indoc::indoc; + + #[test] + fn matches_when_inner_matcher_does_not_match() -> Result<()> { + let matcher = not(eq(1)); + + let result = matcher.matches(&0); + + verify_that!(result, eq(MatcherResult::Match)) + } + + #[test] + fn does_not_match_when_inner_matcher_matches() -> Result<()> { + let matcher = not(eq(1)); + + let result = matcher.matches(&1); + + verify_that!(result, eq(MatcherResult::NoMatch)) + } + + #[test] + fn match_explanation_references_actual_value() -> Result<()> { + let result = verify_that!([1], not(container_eq([1]))); + + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + Actual: [1], + which contains all the elements + " + )))) + ) + } +} diff --git a/src/matchers/ok_matcher.rs b/src/matchers/ok_matcher.rs new file mode 100644 index 0000000..5c6fa51 --- /dev/null +++ b/src/matchers/ok_matcher.rs @@ -0,0 +1,150 @@ +// 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. + +use crate::matcher::{Matcher, MatcherResult}; +use std::{fmt::Debug, marker::PhantomData}; + +/// Matches a `Result` containing `Ok` with a value matched by `inner`. +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> googletest::Result<()> { +/// verify_that!(Ok::<_, ()>("Some value"), ok(eq("Some value")))?; // Passes +/// # Ok(()) +/// # } +/// # fn should_fail_1() -> googletest::Result<()> { +/// verify_that!(Err::<&str, _>("An error"), ok(eq("An error")))?; // Fails +/// # Ok(()) +/// # } +/// # fn should_fail_2() -> googletest::Result<()> { +/// verify_that!(Ok::<_, ()>("Some value"), ok(eq("Some other value")))?; // Fails +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// # should_fail_1().unwrap_err(); +/// # should_fail_2().unwrap_err(); +/// ``` +pub fn ok<T: Debug, E: Debug>( + inner: impl Matcher<ActualT = T>, +) -> impl Matcher<ActualT = std::result::Result<T, E>> { + OkMatcher::<T, E, _> { inner, phantom_t: Default::default(), phantom_e: Default::default() } +} + +struct OkMatcher<T, E, InnerMatcherT> { + inner: InnerMatcherT, + phantom_t: PhantomData<T>, + phantom_e: PhantomData<E>, +} + +impl<T: Debug, E: Debug, InnerMatcherT: Matcher<ActualT = T>> Matcher + for OkMatcher<T, E, InnerMatcherT> +{ + type ActualT = std::result::Result<T, E>; + + fn matches(&self, actual: &Self::ActualT) -> MatcherResult { + actual.as_ref().map(|v| self.inner.matches(v)).unwrap_or(MatcherResult::NoMatch) + } + + fn explain_match(&self, actual: &Self::ActualT) -> String { + match actual { + Ok(o) => format!("which is a success {}", self.inner.explain_match(o)), + Err(_) => "which is an error".to_string(), + } + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + 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) + ) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::ok; + use crate::matcher::{Matcher, MatcherResult}; + use crate::prelude::*; + use indoc::indoc; + + #[test] + fn ok_matches_result_with_value() -> Result<()> { + let matcher = ok(eq(1)); + let value: std::result::Result<i32, i32> = Ok(1); + + let result = matcher.matches(&value); + + verify_that!(result, eq(MatcherResult::Match)) + } + + #[test] + fn ok_does_not_match_result_with_wrong_value() -> Result<()> { + let matcher = ok(eq(1)); + let value: std::result::Result<i32, i32> = Ok(0); + + let result = matcher.matches(&value); + + verify_that!(result, eq(MatcherResult::NoMatch)) + } + + #[test] + fn ok_does_not_match_result_with_err() -> Result<()> { + let matcher = ok(eq(1)); + let value: std::result::Result<i32, i32> = Err(1); + + let result = matcher.matches(&value); + + verify_that!(result, eq(MatcherResult::NoMatch)) + } + + #[test] + fn ok_full_error_message() -> Result<()> { + let result = verify_that!(Ok::<i32, i32>(1), ok(eq(2))); + + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + 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 + " + )))) + ) + } + + #[test] + fn ok_describe_matches() -> Result<()> { + let matcher = super::OkMatcher::<i32, i32, _> { + inner: eq(1), + phantom_t: Default::default(), + phantom_e: Default::default(), + }; + verify_that!( + matcher.describe(MatcherResult::Match), + 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 new file mode 100644 index 0000000..08c7343 --- /dev/null +++ b/src/matchers/points_to_matcher.rs @@ -0,0 +1,107 @@ +// 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. + +use crate::matcher::{Matcher, MatcherResult}; +use std::fmt::Debug; +use std::marker::PhantomData; +use std::ops::Deref; + +/// Matches a (smart) pointer pointing to a value matched by the [`Matcher`] +/// `expected`. +/// +/// This allows easily matching smart pointers such as `Box`, `Rc`, and `Arc`. +/// For example: +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// verify_that!(Box::new(123), points_to(eq(123)))?; +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// ``` +pub fn points_to<ExpectedT, MatcherT, ActualT>( + expected: MatcherT, +) -> impl Matcher<ActualT = ActualT> +where + ExpectedT: Debug, + MatcherT: Matcher<ActualT = ExpectedT>, + ActualT: Deref<Target = ExpectedT> + Debug + ?Sized, +{ + PointsToMatcher { expected, phantom: Default::default() } +} + +struct PointsToMatcher<ActualT: ?Sized, MatcherT> { + expected: MatcherT, + phantom: PhantomData<ActualT>, +} + +impl<ExpectedT, MatcherT, ActualT> Matcher for PointsToMatcher<ActualT, MatcherT> +where + ExpectedT: Debug, + MatcherT: Matcher<ActualT = ExpectedT>, + ActualT: Deref<Target = ExpectedT> + Debug + ?Sized, +{ + type ActualT = ActualT; + + fn matches(&self, actual: &ActualT) -> MatcherResult { + self.expected.matches(actual.deref()) + } + + fn explain_match(&self, actual: &ActualT) -> String { + self.expected.explain_match(actual.deref()) + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + self.expected.describe(matcher_result) + } +} + +#[cfg(test)] +mod tests { + use super::points_to; + use crate::prelude::*; + use indoc::indoc; + use std::rc::Rc; + + #[test] + fn points_to_matches_box_of_int_with_int() -> Result<()> { + verify_that!(Box::new(123), points_to(eq(123))) + } + + #[test] + fn points_to_matches_rc_of_int_with_int() -> Result<()> { + verify_that!(Rc::new(123), points_to(eq(123))) + } + + #[test] + fn points_to_matches_box_of_owned_string_with_string_reference() -> Result<()> { + verify_that!(Rc::new("A string".to_string()), points_to(eq("A string"))) + } + + #[test] + fn match_explanation_references_actual_value() -> Result<()> { + let result = verify_that!(&vec![1], points_to(container_eq([]))); + + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + Actual: [1], + which contains the unexpected element 1 + " + )))) + ) + } +} diff --git a/src/matchers/pointwise_matcher.rs b/src/matchers/pointwise_matcher.rs new file mode 100644 index 0000000..5ee0f22 --- /dev/null +++ b/src/matchers/pointwise_matcher.rs @@ -0,0 +1,236 @@ +// 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. + +// There are no visible documentation elements in this module; the declarative +// macro is documented at the top level. +#![doc(hidden)] + +/// Generates a matcher which matches a container each of whose elements match +/// the given matcher name applied respectively to each element of the given +/// container. +/// +/// For example, the following matches a container of integers each of which +/// does not exceed the given upper bounds: +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// let value = vec![1, 2, 3]; +/// verify_that!(value, pointwise!(le, [1, 3, 3]))?; // Passes +/// verify_that!(value, pointwise!(le, vec![1, 3, 3]))?; // Passes +/// # Ok(()) +/// # } +/// # fn should_fail() -> Result<()> { +/// # let value = vec![1, 2, 3]; +/// verify_that!(value, pointwise!(le, [1, 1, 3]))?; // Fails +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// # should_fail().unwrap_err(); +/// ``` +/// +/// One can also use a closure which returns a matcher: +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// let value = vec![1.00001, 2.000001, 3.00001]; +/// verify_that!(value, pointwise!(|v| near(v, 0.001), [1.0, 2.0, 3.0]))?; +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// ``` +/// +/// One can pass up to three containers to supply arguments to the function +/// creating the matcher: +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// let value = vec![1.00001, 2.000001, 3.00001]; +/// verify_that!(value, pointwise!(|v, t| near(v, t), [1.0, 2.0, 3.0], [0.001, 0.0001, 0.01]))?; +/// verify_that!(value, pointwise!(near, [1.0, 2.0, 3.0], [0.001, 0.0001, 0.01]))?; // Same as above +/// verify_that!( +/// value, +/// pointwise!( +/// |v, t, u| near(v, t * u), +/// [1.0, 2.0, 3.0], +/// [0.001, 0.0001, 0.01], +/// [0.5, 0.5, 1.0] +/// ) +/// )?; +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// ``` +/// +/// When using `pointwise!` with multiple containers, the caller must ensure +/// 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. +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// let value = vec![1, 2, 3]; +/// verify_that!(*value.as_slice(), pointwise!(le, [1, 3, 3]))?; // Passes +/// verify_that!([1, 2, 3], pointwise!(le, [1, 3, 3]))?; // Passes +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// ``` +/// +/// This matcher does not support matching directly against an [`Iterator`]. To +/// match against an iterator, use [`Iterator::collect`] to build a [`Vec`] +/// first. +/// +/// The second argument can be any value implementing `IntoIterator`, such as a +/// `Vec` or an array. The container does not have to have the same type as the +/// actual value, but the value type must be the same. +/// +/// **Note for users of the [`Pointwise`] matcher in C++ GoogleTest:** +/// +/// This macro differs from `Pointwise` in that the first parameter is not a +/// matcher which matches a pair but rather the name of a function of one +/// argument whose output is a matcher. This means that one can use standard +/// matchers like `eq`, `le`, and so on with `pointwise!` but certain C++ tests +/// using `Pointwise` will require some extra work to port. +/// +/// [`IntoIterator`]: std::iter::IntoIterator +/// [`Iterator`]: std::iter::Iterator +/// [`Iterator::collect`]: std::iter::Iterator::collect +/// [`Pointwise`]: https://google.github.io/googletest/reference/matchers.html#container-matchers +/// [`Vec`]: std::vec::Vec +#[macro_export] +macro_rules! pointwise { + ($matcher:expr, $container:expr) => {{ + use $crate::matchers::pointwise_matcher::internal::PointwiseMatcher; + PointwiseMatcher::new($container.into_iter().map($matcher).collect()) + }}; + + ($matcher:expr, $left_container:expr, $right_container:expr) => {{ + use $crate::matchers::pointwise_matcher::internal::PointwiseMatcher; + PointwiseMatcher::new( + $left_container + .into_iter() + .zip($right_container.into_iter()) + .map(|(l, r)| $matcher(l, r)) + .collect(), + ) + }}; + + ($matcher:expr, $left_container:expr, $middle_container:expr, $right_container:expr) => {{ + use $crate::matchers::pointwise_matcher::internal::PointwiseMatcher; + PointwiseMatcher::new( + $left_container + .into_iter() + .zip($right_container.into_iter().zip($middle_container.into_iter())) + .map(|(l, (m, r))| $matcher(l, m, r)) + .collect(), + ) + }}; +} + +/// Module for use only by the procedural macros in this module. +/// +/// **For internal use only. API stablility is not guaranteed!** +#[doc(hidden)] +pub mod internal { + use crate::matcher::{Matcher, MatcherResult}; + use crate::matcher_support::description::Description; + use crate::matcher_support::zipped_iterator::zip; + use std::{fmt::Debug, marker::PhantomData}; + + /// This struct is meant to be used only through the `pointwise` macro. + /// + /// **For internal use only. API stablility is not guaranteed!** + #[doc(hidden)] + pub struct PointwiseMatcher<ContainerT: ?Sized, MatcherT> { + matchers: Vec<MatcherT>, + phantom: PhantomData<ContainerT>, + } + + impl<ContainerT: ?Sized, MatcherT> PointwiseMatcher<ContainerT, MatcherT> { + pub fn new(matchers: Vec<MatcherT>) -> Self { + Self { matchers, phantom: Default::default() } + } + } + + impl<T: Debug, MatcherT: Matcher<ActualT = T>, ContainerT: ?Sized + Debug> Matcher + for PointwiseMatcher<ContainerT, MatcherT> + where + for<'b> &'b ContainerT: IntoIterator<Item = &'b T>, + { + type ActualT = ContainerT; + + fn matches(&self, actual: &ContainerT) -> MatcherResult { + let mut zipped_iterator = zip(actual.into_iter(), self.matchers.iter()); + for (element, matcher) in zipped_iterator.by_ref() { + if matcher.matches(element).is_no_match() { + return MatcherResult::NoMatch; + } + } + if zipped_iterator.has_size_mismatch() { + MatcherResult::NoMatch + } else { + MatcherResult::Match + } + } + + fn explain_match(&self, actual: &ContainerT) -> String { + // TODO(b/260819741) This code duplicates elements_are_matcher.rs. Consider + // extract as a separate library. (or implement pointwise! with + // elements_are) + let actual_iterator = actual.into_iter(); + let mut zipped_iterator = zip(actual_iterator, self.matchers.iter()); + let mut mismatches = Vec::new(); + for (idx, (a, e)) in zipped_iterator.by_ref().enumerate() { + if e.matches(a).is_no_match() { + mismatches.push(format!("element #{idx} is {a:?}, {}", e.explain_match(a))); + } + } + if mismatches.is_empty() { + if !zipped_iterator.has_size_mismatch() { + "which matches all elements".to_string() + } else { + format!( + "which has size {} (expected {})", + zipped_iterator.left_size(), + self.matchers.len() + ) + } + } else if mismatches.len() == 1 { + format!("where {}", mismatches[0]) + } else { + let mismatches = mismatches.into_iter().collect::<Description>(); + format!("where:\n{}", mismatches.bullet_list().indent()) + } + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + format!( + "{} elements satisfying respectively:\n{}", + if matcher_result.into() { "has" } else { "doesn't have" }, + self.matchers + .iter() + .map(|m| m.describe(MatcherResult::Match)) + .collect::<Description>() + .enumerate() + .indent() + ) + } + } +} diff --git a/src/matchers/predicate_matcher.rs b/src/matchers/predicate_matcher.rs new file mode 100644 index 0000000..fabd6c3 --- /dev/null +++ b/src/matchers/predicate_matcher.rs @@ -0,0 +1,228 @@ +// 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::matcher::{Matcher, MatcherResult}; +use std::{fmt::Debug, marker::PhantomData}; + +/// Creates a matcher based on the predicate provided. +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// verify_that!(3, predicate(|x: &i32| x % 2 == 1))?; // Passes +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// ``` +/// +/// The predicate should take the subject type by reference and return a +/// boolean. +/// +/// Note: even if the Rust compiler should be able to infer the type of +/// the closure argument, it is likely that it won't. +/// See <https://github.com/rust-lang/rust/issues/12679> for update on this issue. +/// This is easily fixed by explicitly declaring the type of the argument +pub fn predicate<T: Debug + ?Sized, P>( + predicate: P, +) -> PredicateMatcher<T, P, NoDescription, NoDescription> +where + for<'a> P: Fn(&'a T) -> bool, +{ + PredicateMatcher { + predicate, + positive_description: NoDescription, + negative_description: NoDescription, + phantom: Default::default(), + } +} + +impl<T, P> PredicateMatcher<T, P, NoDescription, NoDescription> { + /// Configures this instance to provide a more meaningful description. + /// + /// For example, to make sure the error message is more useful + /// + /// ``` + /// # use googletest::matchers::{predicate, PredicateMatcher}; + /// # let _ = + /// predicate(|x: &i32| x % 2 == 1) + /// .with_description("is odd", "is even") + /// # ; + /// ``` + /// + /// This is optional as it only provides value when the test fails. + /// + /// Description can be passed by `&str`, `String` or `Fn() -> Into<String>`. + pub fn with_description<D1: PredicateDescription, D2: PredicateDescription>( + self, + positive_description: D1, + negative_description: D2, + ) -> PredicateMatcher<T, P, D1, D2> { + PredicateMatcher { + predicate: self.predicate, + positive_description, + negative_description, + phantom: Default::default(), + } + } +} + +/// A matcher which applies `predicate` on the value. +/// +/// See [`predicate`]. +pub struct PredicateMatcher<T: ?Sized, P, D1, D2> { + predicate: P, + positive_description: D1, + negative_description: D2, + phantom: PhantomData<T>, +} + +/// A trait to allow [`PredicateMatcher::with_description`] to accept multiple +/// types. +/// +/// See [`PredicateMatcher::with_description`] +pub trait PredicateDescription { + fn to_description(&self) -> String; +} + +impl PredicateDescription for &str { + fn to_description(&self) -> String { + self.to_string() + } +} + +impl PredicateDescription for String { + fn to_description(&self) -> String { + self.clone() + } +} + +impl<T, S> PredicateDescription for T +where + T: Fn() -> S, + S: Into<String>, +{ + fn to_description(&self) -> String { + self().into() + } +} + +// Sentinel type to tag a MatcherBuilder as without a description. +#[doc(hidden)] +pub struct NoDescription; + +impl<T: Debug, P> Matcher for PredicateMatcher<T, P, NoDescription, NoDescription> +where + for<'a> P: Fn(&'a T) -> bool, +{ + type ActualT = T; + + fn matches(&self, actual: &T) -> MatcherResult { + (self.predicate)(actual).into() + } + + fn describe(&self, result: MatcherResult) -> String { + match result { + MatcherResult::Match => "matches".to_string(), + MatcherResult::NoMatch => "does not match".to_string(), + } + } +} + +impl<T: Debug, P, D1: PredicateDescription, D2: PredicateDescription> Matcher + for PredicateMatcher<T, P, D1, D2> +where + for<'a> P: Fn(&'a T) -> bool, +{ + type ActualT = T; + + fn matches(&self, actual: &T) -> MatcherResult { + (self.predicate)(actual).into() + } + + fn describe(&self, result: MatcherResult) -> String { + match result { + MatcherResult::Match => self.positive_description.to_description(), + MatcherResult::NoMatch => self.negative_description.to_description(), + } + } +} + +#[cfg(test)] +mod tests { + use super::predicate; + use crate::matcher::Matcher; + use crate::prelude::*; + + // Simple matcher with a description + fn is_odd() -> impl Matcher<ActualT = i32> { + predicate(|x| x % 2 == 1).with_description("is odd", "is even") + } + + #[test] + fn predicate_matcher_odd() -> Result<()> { + verify_that!(1, is_odd()) + } + + #[test] + fn predicate_matcher_odd_explain_match_matches() -> Result<()> { + verify_that!(is_odd().explain_match(&1), displays_as(eq("which is odd"))) + } + + #[test] + fn predicate_matcher_odd_explain_match_does_not_match() -> Result<()> { + verify_that!(is_odd().explain_match(&2), displays_as(eq("which is even"))) + } + + // Simple Matcher without description + fn is_even() -> impl Matcher<ActualT = i32> { + predicate(|x| x % 2 == 0) + } + + #[test] + fn predicate_matcher_even() -> Result<()> { + verify_that!(2, is_even()) + } + + #[test] + fn predicate_matcher_even_explain_match_matches() -> Result<()> { + verify_that!(is_even().explain_match(&2), displays_as(eq("which matches"))) + } + + #[test] + fn predicate_matcher_even_explain_match_does_not_match() -> Result<()> { + verify_that!(is_even().explain_match(&1), displays_as(eq("which does not match"))) + } + + #[test] + fn predicate_matcher_generator_lambda() -> Result<()> { + let is_divisible_by = |quotient| { + predicate(move |x: &i32| x % quotient == 0).with_description( + move || format!("is divisible by {quotient}"), + move || format!("is not divisible by {quotient}"), + ) + }; + verify_that!(49, is_divisible_by(7)) + } + + #[test] + fn predicate_matcher_inline() -> Result<()> { + verify_that!(2048, predicate(|x: &i32| x.count_ones() == 1)) + } + + #[test] + fn predicate_matcher_function_pointer() -> Result<()> { + use std::time::Duration; + verify_that!(Duration::new(0, 0), predicate(Duration::is_zero)) + } +} diff --git a/src/matchers/property_matcher.rs b/src/matchers/property_matcher.rs new file mode 100644 index 0000000..d69ba1d --- /dev/null +++ b/src/matchers/property_matcher.rs @@ -0,0 +1,237 @@ +// 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. + +// There are no visible documentation elements in this module; the declarative +// macro is documented at the top level. +#![doc(hidden)] + +/// Matches an object which, upon calling the given method on it with the given +/// arguments, produces a value matched by the given inner matcher. +/// +/// This is particularly useful as a nested matcher when the desired +/// property cannot be accessed through a field and must instead be +/// extracted through a method call. For example: +/// +/// ``` +/// # use googletest::prelude::*; +/// #[derive(Debug)] +/// pub struct MyStruct { +/// a_field: u32, +/// } +/// +/// impl MyStruct { +/// pub fn get_a_field(&self) -> u32 { self.a_field } +/// } +/// +/// let value = vec![MyStruct { a_field: 100 }]; +/// verify_that!(value, contains(property!(MyStruct.get_a_field(), eq(100)))) +/// # .unwrap(); +/// ``` +/// +/// **Important**: The method should be pure function with a deterministic +/// output and no side effects. In particular, in the event of an assertion +/// 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`: +/// +/// ``` +/// # use googletest::prelude::*; +/// # #[derive(Debug)] +/// # pub struct MyStruct { +/// # a_field: u32, +/// # } +/// impl MyStruct { +/// pub fn get_a_field(&self) -> &u32 { &self.a_field } +/// } +/// +/// # let value = vec![MyStruct { a_field: 100 }]; +/// verify_that!(value, contains(property!(ref MyStruct.get_a_field(), eq(100)))) +/// # .unwrap(); +/// ``` +/// +/// The method may also take additional arguments: +/// +/// ``` +/// # use googletest::prelude::*; +/// # #[derive(Debug)] +/// # pub struct MyStruct { +/// # a_field: u32, +/// # } +/// impl MyStruct { +/// pub fn add_to_a_field(&self, a: u32) -> u32 { self.a_field + a } +/// } +/// +/// # let value = vec![MyStruct { a_field: 100 }]; +/// verify_that!(value, contains(property!(MyStruct.add_to_a_field(50), eq(150)))) +/// # .unwrap(); +/// ``` +/// +/// Unfortunately, this matcher does *not* work with methods returning string +/// slices: +/// +/// ```compile_fail +/// # use googletest::prelude::*; +/// #[derive(Debug)] +/// pub struct MyStruct { +/// a_string: String, +/// } +/// impl MyStruct { +/// pub fn get_a_string(&self) -> &str { &self.a_string } +/// } +/// +/// let value = MyStruct { a_string: "A string".into() }; +/// verify_that!(value, property!(ref 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. +/// +/// The list of arguments may optionally have a trailing comma. +#[macro_export] +macro_rules! property { + ($($t:tt)*) => { $crate::property_internal!($($t)*) } +} + +// Internal-only macro created so that the macro definition does not appear in +// generated documentation. +#[doc(hidden)] +#[macro_export] +macro_rules! property_internal { + ($($t:ident)::+.$method:tt($($argument:tt),* $(,)?), $m:expr) => {{ + use $crate::matchers::property_matcher::internal::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; + property_ref_matcher( + |o: &$($t)::+| o.$method($($argument),*), + &stringify!($method($($argument),*)), + $m) + }}; +} + +/// Items for use only by the declarative macros in this module. +/// +/// **For internal use only. API stablility is not guaranteed!** +#[doc(hidden)] +pub mod internal { + use crate::matcher::{Matcher, MatcherResult}; + use std::{fmt::Debug, marker::PhantomData}; + + /// **For internal use only. API stablility is not guaranteed!** + #[doc(hidden)] + pub fn property_matcher<OuterT: Debug, InnerT: Debug, MatcherT: Matcher<ActualT = InnerT>>( + extractor: impl Fn(&OuterT) -> InnerT, + property_desc: &'static str, + inner: MatcherT, + ) -> impl Matcher<ActualT = OuterT> { + PropertyMatcher { extractor, property_desc, inner, phantom: Default::default() } + } + + struct PropertyMatcher<OuterT, ExtractorT, MatcherT> { + extractor: ExtractorT, + property_desc: &'static str, + inner: MatcherT, + phantom: PhantomData<OuterT>, + } + + impl<InnerT, OuterT, ExtractorT, MatcherT> Matcher for PropertyMatcher<OuterT, ExtractorT, MatcherT> + where + InnerT: Debug, + OuterT: Debug, + ExtractorT: Fn(&OuterT) -> InnerT, + MatcherT: Matcher<ActualT = InnerT>, + { + type ActualT = OuterT; + + fn matches(&self, actual: &OuterT) -> MatcherResult { + self.inner.matches(&(self.extractor)(actual)) + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + format!( + "has property `{}`, which {}", + self.property_desc, + self.inner.describe(matcher_result) + ) + } + + fn explain_match(&self, actual: &OuterT) -> String { + let actual_inner = (self.extractor)(actual); + format!( + "whose property `{}` is `{:#?}`, {}", + self.property_desc, + actual_inner, + self.inner.explain_match(&actual_inner) + ) + } + } + + /// **For internal use only. API stablility is not guaranteed!** + #[doc(hidden)] + pub fn property_ref_matcher<OuterT, InnerT, MatcherT>( + extractor: fn(&OuterT) -> &InnerT, + property_desc: &'static str, + inner: MatcherT, + ) -> impl Matcher<ActualT = OuterT> + where + OuterT: Debug, + InnerT: Debug + ?Sized, + MatcherT: Matcher<ActualT = InnerT>, + { + PropertyRefMatcher { extractor, property_desc, inner } + } + + struct PropertyRefMatcher<InnerT: ?Sized, OuterT, MatcherT> { + extractor: fn(&OuterT) -> &InnerT, + property_desc: &'static str, + inner: MatcherT, + } + + impl<InnerT: Debug + ?Sized, OuterT: Debug, MatcherT: Matcher<ActualT = InnerT>> Matcher + for PropertyRefMatcher<InnerT, OuterT, MatcherT> + { + type ActualT = OuterT; + + fn matches(&self, actual: &OuterT) -> MatcherResult { + self.inner.matches((self.extractor)(actual)) + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + format!( + "has property `{}`, which {}", + self.property_desc, + self.inner.describe(matcher_result) + ) + } + + fn explain_match(&self, actual: &OuterT) -> String { + let actual_inner = (self.extractor)(actual); + format!( + "whose property `{}` is `{:#?}`, {}", + self.property_desc, + actual_inner, + self.inner.explain_match(actual_inner) + ) + } + } +} diff --git a/src/matchers/some_matcher.rs b/src/matchers/some_matcher.rs new file mode 100644 index 0000000..a6ce021 --- /dev/null +++ b/src/matchers/some_matcher.rs @@ -0,0 +1,162 @@ +// 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. + +use crate::matcher::{Matcher, MatcherResult}; +use std::{fmt::Debug, marker::PhantomData}; + +/// Matches an `Option` containing a value matched by `inner`. +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// verify_that!(Some("Some value"), some(eq("Some value")))?; // Passes +/// # Ok(()) +/// # } +/// # fn should_fail_1() -> Result<()> { +/// verify_that!(None::<&str>, some(eq("Some value")))?; // Fails +/// # Ok(()) +/// # } +/// # fn should_fail_2() -> Result<()> { +/// verify_that!(Some("Some value"), some(eq("Some other value")))?; // Fails +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// # should_fail_1().unwrap_err(); +/// # should_fail_2().unwrap_err(); +/// ``` +pub fn some<T: Debug>(inner: impl Matcher<ActualT = T>) -> impl Matcher<ActualT = Option<T>> { + SomeMatcher { inner, phantom: Default::default() } +} + +struct SomeMatcher<T, InnerMatcherT> { + inner: InnerMatcherT, + phantom: PhantomData<T>, +} + +impl<T: Debug, InnerMatcherT: Matcher<ActualT = T>> Matcher for SomeMatcher<T, InnerMatcherT> { + type ActualT = Option<T>; + + fn matches(&self, actual: &Option<T>) -> MatcherResult { + actual.as_ref().map(|v| self.inner.matches(v)).unwrap_or(MatcherResult::NoMatch) + } + + fn explain_match(&self, actual: &Option<T>) -> String { + match (self.matches(actual), actual) { + (_, Some(t)) => format!("which has a value {}", self.inner.explain_match(t)), + (_, None) => "which is None".to_string(), + } + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + 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) + ) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::some; + use crate::matcher::{Matcher, MatcherResult}; + use crate::prelude::*; + use indoc::indoc; + + #[test] + fn some_matches_option_with_value() -> Result<()> { + let matcher = some(eq(1)); + + let result = matcher.matches(&Some(1)); + + verify_that!(result, eq(MatcherResult::Match)) + } + + #[test] + fn some_does_not_match_option_with_wrong_value() -> Result<()> { + let matcher = some(eq(1)); + + let result = matcher.matches(&Some(0)); + + verify_that!(result, eq(MatcherResult::NoMatch)) + } + + #[test] + fn some_does_not_match_option_with_none() -> Result<()> { + let matcher = some(eq(1)); + + let result = matcher.matches(&None); + + verify_that!(result, eq(MatcherResult::NoMatch)) + } + + #[test] + fn some_full_error_message() -> Result<()> { + let result = verify_that!(Some(2), some(eq(1))); + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + 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 + " + )))) + ) + } + + #[test] + fn some_describe_matches() -> Result<()> { + verify_that!( + some(eq(1)).describe(MatcherResult::Match), + 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") + ) + } + + #[test] + fn some_explain_match_with_none() -> Result<()> { + verify_that!(some(eq(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")) + ) + } + + #[test] + 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")) + ) + } +} diff --git a/src/matchers/str_matcher.rs b/src/matchers/str_matcher.rs new file mode 100644 index 0000000..3a4e2e9 --- /dev/null +++ b/src/matchers/str_matcher.rs @@ -0,0 +1,1234 @@ +// 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::{ + matcher::{Matcher, MatcherResult}, + matcher_support::{ + edit_distance, + summarize_diff::{create_diff, create_diff_reversed}, + }, + matchers::{eq_deref_of_matcher::EqDerefOfMatcher, eq_matcher::EqMatcher}, +}; +use std::borrow::Cow; +use std::fmt::Debug; +use std::marker::PhantomData; +use std::ops::Deref; + +/// Matches a string containing a given substring. +/// +/// Both the actual value and the expected substring may be either a `String` or +/// a string reference. +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass_1() -> Result<()> { +/// verify_that!("Some value", contains_substring("Some"))?; // Passes +/// # Ok(()) +/// # } +/// # fn should_fail() -> Result<()> { +/// verify_that!("Another value", contains_substring("Some"))?; // Fails +/// # Ok(()) +/// # } +/// # fn should_pass_2() -> Result<()> { +/// verify_that!("Some value".to_string(), contains_substring("value"))?; // Passes +/// verify_that!("Some value", contains_substring("value".to_string()))?; // Passes +/// # Ok(()) +/// # } +/// # should_pass_1().unwrap(); +/// # should_fail().unwrap_err(); +/// # should_pass_2().unwrap(); +/// ``` +/// +/// See the [`StrMatcherConfigurator`] extension trait for more options on how +/// the string is matched. +/// +/// > Note on memory use: In most cases, this matcher does not allocate memory +/// > when matching strings. However, it must allocate copies of both the actual +/// > and expected values when matching strings while +/// > [`ignoring_ascii_case`][StrMatcherConfigurator::ignoring_ascii_case] is +/// > set. +pub fn contains_substring<A: ?Sized, T>(expected: T) -> StrMatcher<A, T> { + StrMatcher { + configuration: Configuration { mode: MatchMode::Contains, ..Default::default() }, + expected, + phantom: Default::default(), + } +} + +/// Matches a string which starts with the given prefix. +/// +/// Both the actual value and the expected prefix may be either a `String` or +/// a string reference. +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass_1() -> Result<()> { +/// verify_that!("Some value", starts_with("Some"))?; // Passes +/// # Ok(()) +/// # } +/// # fn should_fail_1() -> Result<()> { +/// verify_that!("Another value", starts_with("Some"))?; // Fails +/// # Ok(()) +/// # } +/// # fn should_fail_2() -> Result<()> { +/// verify_that!("Some value", starts_with("value"))?; // Fails +/// # Ok(()) +/// # } +/// # fn should_pass_2() -> Result<()> { +/// verify_that!("Some value".to_string(), starts_with("Some"))?; // Passes +/// verify_that!("Some value", starts_with("Some".to_string()))?; // Passes +/// # Ok(()) +/// # } +/// # should_pass_1().unwrap(); +/// # should_fail_1().unwrap_err(); +/// # should_fail_2().unwrap_err(); +/// # should_pass_2().unwrap(); +/// ``` +/// +/// See the [`StrMatcherConfigurator`] extension trait for more options on how +/// the string is matched. +pub fn starts_with<A: ?Sized, T>(expected: T) -> StrMatcher<A, T> { + StrMatcher { + configuration: Configuration { mode: MatchMode::StartsWith, ..Default::default() }, + expected, + phantom: Default::default(), + } +} + +/// Matches a string which ends with the given suffix. +/// +/// Both the actual value and the expected suffix may be either a `String` or +/// a string reference. +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass_1() -> Result<()> { +/// verify_that!("Some value", ends_with("value"))?; // Passes +/// # Ok(()) +/// # } +/// # fn should_fail_1() -> Result<()> { +/// verify_that!("Some value", ends_with("other value"))?; // Fails +/// # Ok(()) +/// # } +/// # fn should_fail_2() -> Result<()> { +/// verify_that!("Some value", ends_with("Some"))?; // Fails +/// # Ok(()) +/// # } +/// # fn should_pass_2() -> Result<()> { +/// verify_that!("Some value".to_string(), ends_with("value"))?; // Passes +/// verify_that!("Some value", ends_with("value".to_string()))?; // Passes +/// # Ok(()) +/// # } +/// # should_pass_1().unwrap(); +/// # should_fail_1().unwrap_err(); +/// # should_fail_2().unwrap_err(); +/// # should_pass_2().unwrap(); +/// ``` +/// +/// See the [`StrMatcherConfigurator`] extension trait for more options on how +/// the string is matched. +pub fn ends_with<A: ?Sized, T>(expected: T) -> StrMatcher<A, T> { + StrMatcher { + configuration: Configuration { mode: MatchMode::EndsWith, ..Default::default() }, + expected, + phantom: Default::default(), + } +} + +/// Extension trait to configure [`StrMatcher`]. +/// +/// Matchers which match against string values and, through configuration, +/// specialise to [`StrMatcher`] implement this trait. That includes +/// [`EqMatcher`] and [`StrMatcher`]. +pub trait StrMatcherConfigurator<ActualT: ?Sized, ExpectedT> { + /// Configures the matcher to ignore any leading whitespace in either the + /// actual or the expected value. + /// + /// Whitespace is defined as in [`str::trim_start`]. + /// + /// ``` + /// # use googletest::prelude::*; + /// # fn should_pass() -> Result<()> { + /// verify_that!("A string", eq(" A string").ignoring_leading_whitespace())?; // Passes + /// verify_that!(" A string", eq("A string").ignoring_leading_whitespace())?; // Passes + /// # Ok(()) + /// # } + /// # should_pass().unwrap(); + /// ``` + /// + /// When all other configuration options are left as the defaults, this is + /// equivalent to invoking [`str::trim_start`] on both the expected and + /// actual value. + fn ignoring_leading_whitespace(self) -> StrMatcher<ActualT, ExpectedT>; + + /// Configures the matcher to ignore any trailing whitespace in either the + /// actual or the expected value. + /// + /// Whitespace is defined as in [`str::trim_end`]. + /// + /// ``` + /// # use googletest::prelude::*; + /// # fn should_pass() -> Result<()> { + /// verify_that!("A string", eq("A string ").ignoring_trailing_whitespace())?; // Passes + /// verify_that!("A string ", eq("A string").ignoring_trailing_whitespace())?; // Passes + /// # Ok(()) + /// # } + /// # should_pass().unwrap(); + /// ``` + /// + /// When all other configuration options are left as the defaults, this is + /// equivalent to invoking [`str::trim_end`] on both the expected and + /// actual value. + fn ignoring_trailing_whitespace(self) -> StrMatcher<ActualT, ExpectedT>; + + /// Configures the matcher to ignore both leading and trailing whitespace in + /// either the actual or the expected value. + /// + /// Whitespace is defined as in [`str::trim`]. + /// + /// ``` + /// # use googletest::prelude::*; + /// # fn should_pass() -> Result<()> { + /// verify_that!("A string", eq(" A string ").ignoring_outer_whitespace())?; // Passes + /// verify_that!(" A string ", eq("A string").ignoring_outer_whitespace())?; // Passes + /// # Ok(()) + /// # } + /// # should_pass().unwrap(); + /// ``` + /// + /// This is equivalent to invoking both + /// [`ignoring_leading_whitespace`][StrMatcherConfigurator::ignoring_leading_whitespace] and + /// [`ignoring_trailing_whitespace`][StrMatcherConfigurator::ignoring_trailing_whitespace]. + /// + /// When all other configuration options are left as the defaults, this is + /// equivalent to invoking [`str::trim`] on both the expected and actual + /// value. + fn ignoring_outer_whitespace(self) -> StrMatcher<ActualT, ExpectedT>; + + /// Configures the matcher to ignore ASCII case when comparing values. + /// + /// This uses the same rules for case as [`str::eq_ignore_ascii_case`]. + /// + /// ``` + /// # use googletest::prelude::*; + /// # fn should_pass() -> Result<()> { + /// verify_that!("Some value", eq("SOME VALUE").ignoring_ascii_case())?; // Passes + /// # Ok(()) + /// # } + /// # fn should_fail() -> Result<()> { + /// verify_that!("Another value", eq("Some value").ignoring_ascii_case())?; // Fails + /// # Ok(()) + /// # } + /// # should_pass().unwrap(); + /// # should_fail().unwrap_err(); + /// ``` + /// + /// This is **not guaranteed** to match strings with differing upper/lower + /// case characters outside of the codepoints 0-127 covered by ASCII. + fn ignoring_ascii_case(self) -> StrMatcher<ActualT, ExpectedT>; + + /// Configures the matcher to match only strings which otherwise satisfy the + /// conditions a number times matched by the matcher `times`. + /// + /// ``` + /// # use googletest::prelude::*; + /// # fn should_pass() -> Result<()> { + /// verify_that!("Some value\nSome value", contains_substring("value").times(eq(2)))?; // Passes + /// # Ok(()) + /// # } + /// # fn should_fail() -> Result<()> { + /// verify_that!("Some value", contains_substring("value").times(eq(2)))?; // Fails + /// # Ok(()) + /// # } + /// # should_pass().unwrap(); + /// # should_fail().unwrap_err(); + /// ``` + /// + /// The matched substrings must be disjoint from one another to be counted. + /// For example: + /// + /// ``` + /// # use googletest::prelude::*; + /// # fn should_fail() -> Result<()> { + /// // Fails: substrings distinct but not disjoint! + /// verify_that!("ababab", contains_substring("abab").times(eq(2)))?; + /// # Ok(()) + /// # } + /// # should_fail().unwrap_err(); + /// ``` + /// + /// This is only meaningful when the matcher was constructed with + /// [`contains_substring`]. This method will panic when it is used with any + /// other matcher construction. + fn times( + self, + times: impl Matcher<ActualT = usize> + 'static, + ) -> StrMatcher<ActualT, ExpectedT>; +} + +/// A matcher which matches equality or containment of a string-like value in a +/// configurable way. +/// +/// The following matcher methods instantiate this: +/// +/// * [`eq`][crate::matchers::eq_matcher::eq], +/// * [`contains_substring`], +/// * [`starts_with`], +/// * [`ends_with`]. +pub struct StrMatcher<ActualT: ?Sized, ExpectedT> { + expected: ExpectedT, + configuration: Configuration, + phantom: PhantomData<ActualT>, +} + +impl<ExpectedT, ActualT> Matcher for StrMatcher<ActualT, ExpectedT> +where + ExpectedT: Deref<Target = str> + Debug, + ActualT: AsRef<str> + Debug + ?Sized, +{ + type ActualT = ActualT; + + fn matches(&self, actual: &ActualT) -> MatcherResult { + self.configuration.do_strings_match(self.expected.deref(), actual.as_ref()).into() + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + self.configuration.describe(matcher_result, self.expected.deref()) + } + + fn explain_match(&self, actual: &ActualT) -> String { + self.configuration.explain_match(self.expected.deref(), actual.as_ref()) + } +} + +impl<ActualT: ?Sized, ExpectedT, MatcherT: Into<StrMatcher<ActualT, ExpectedT>>> + StrMatcherConfigurator<ActualT, ExpectedT> for MatcherT +{ + fn ignoring_leading_whitespace(self) -> StrMatcher<ActualT, ExpectedT> { + let existing = self.into(); + StrMatcher { + configuration: existing.configuration.ignoring_leading_whitespace(), + ..existing + } + } + + fn ignoring_trailing_whitespace(self) -> StrMatcher<ActualT, ExpectedT> { + let existing = self.into(); + StrMatcher { + configuration: existing.configuration.ignoring_trailing_whitespace(), + ..existing + } + } + + fn ignoring_outer_whitespace(self) -> StrMatcher<ActualT, ExpectedT> { + let existing = self.into(); + StrMatcher { configuration: existing.configuration.ignoring_outer_whitespace(), ..existing } + } + + fn ignoring_ascii_case(self) -> StrMatcher<ActualT, ExpectedT> { + let existing = self.into(); + StrMatcher { configuration: existing.configuration.ignoring_ascii_case(), ..existing } + } + + fn times( + self, + times: impl Matcher<ActualT = usize> + 'static, + ) -> StrMatcher<ActualT, ExpectedT> { + let existing = self.into(); + if !matches!(existing.configuration.mode, MatchMode::Contains) { + panic!("The times() configurator is only meaningful with contains_substring()."); + } + StrMatcher { configuration: existing.configuration.times(times), ..existing } + } +} + +impl<A: ?Sized, T: Deref<Target = str>> From<EqMatcher<A, T>> for StrMatcher<A, T> { + fn from(value: EqMatcher<A, T>) -> Self { + Self::with_default_config(value.expected) + } +} + +impl<A: ?Sized, T: Deref<Target = str>> From<EqDerefOfMatcher<A, T>> for StrMatcher<A, T> { + fn from(value: EqDerefOfMatcher<A, T>) -> Self { + Self::with_default_config(value.expected) + } +} + +impl<A: ?Sized, T> StrMatcher<A, T> { + /// Returns a [`StrMatcher`] with a default configuration to match against + /// the given expected value. + /// + /// This default configuration is sensitive to whitespace and case. + fn with_default_config(expected: T) -> Self { + Self { expected, configuration: Default::default(), phantom: Default::default() } + } +} + +// Holds all the information on how the expected and actual strings are to be +// compared. Its associated functions perform the actual matching operations +// on string references. The struct and comparison methods therefore need not be +// parameterised, saving compilation time and binary size on monomorphisation. +// +// The default value represents exact equality of the strings. +struct Configuration { + mode: MatchMode, + ignore_leading_whitespace: bool, + ignore_trailing_whitespace: bool, + case_policy: CasePolicy, + times: Option<Box<dyn Matcher<ActualT = usize>>>, +} + +#[derive(Clone)] +enum MatchMode { + Equals, + Contains, + StartsWith, + EndsWith, +} + +impl MatchMode { + fn to_diff_mode(&self) -> edit_distance::Mode { + match self { + MatchMode::StartsWith | MatchMode::EndsWith => edit_distance::Mode::Prefix, + MatchMode::Contains => edit_distance::Mode::Contains, + MatchMode::Equals => edit_distance::Mode::Exact, + } + } +} + +#[derive(Clone)] +enum CasePolicy { + Respect, + IgnoreAscii, +} + +impl Configuration { + // The entry point for all string matching. StrMatcher::matches redirects + // immediately to this function. + fn do_strings_match(&self, expected: &str, actual: &str) -> bool { + let (expected, actual) = + match (self.ignore_leading_whitespace, self.ignore_trailing_whitespace) { + (true, true) => (expected.trim(), actual.trim()), + (true, false) => (expected.trim_start(), actual.trim_start()), + (false, true) => (expected.trim_end(), actual.trim_end()), + (false, false) => (expected, actual), + }; + match self.mode { + MatchMode::Equals => match self.case_policy { + CasePolicy::Respect => expected == actual, + CasePolicy::IgnoreAscii => expected.eq_ignore_ascii_case(actual), + }, + MatchMode::Contains => match self.case_policy { + CasePolicy::Respect => self.does_containment_match(actual, expected), + CasePolicy::IgnoreAscii => self.does_containment_match( + actual.to_ascii_lowercase().as_str(), + expected.to_ascii_lowercase().as_str(), + ), + }, + MatchMode::StartsWith => match self.case_policy { + CasePolicy::Respect => actual.starts_with(expected), + CasePolicy::IgnoreAscii => { + actual.len() >= expected.len() + && actual[..expected.len()].eq_ignore_ascii_case(expected) + } + }, + MatchMode::EndsWith => match self.case_policy { + CasePolicy::Respect => actual.ends_with(expected), + CasePolicy::IgnoreAscii => { + actual.len() >= expected.len() + && actual[actual.len() - expected.len()..].eq_ignore_ascii_case(expected) + } + }, + } + } + + // Returns whether actual contains expected a number of times matched by the + // matcher self.times. Does not take other configuration into account. + fn does_containment_match(&self, actual: &str, expected: &str) -> bool { + if let Some(times) = self.times.as_ref() { + // Split returns an iterator over the "boundaries" left and right of the + // substring to be matched, of which there is one more than the number of + // substrings. + matches!(times.matches(&(actual.split(expected).count() - 1)), MatcherResult::Match) + } else { + actual.contains(expected) + } + } + + // StrMatcher::describe redirects immediately to this function. + fn describe(&self, matcher_result: MatcherResult, expected: &str) -> String { + 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()), + (true, false) => addenda.push("ignoring leading whitespace".into()), + (false, true) => addenda.push("ignoring trailing whitespace".into()), + (false, false) => {} + } + match self.case_policy { + CasePolicy::Respect => {} + CasePolicy::IgnoreAscii => addenda.push("ignoring ASCII case".into()), + } + if let Some(times) = self.times.as_ref() { + addenda.push(format!("count {}", times.describe(matcher_result)).into()); + } + let extra = + if !addenda.is_empty() { format!(" ({})", addenda.join(", ")) } else { "".into() }; + let match_mode_description = match self.mode { + MatchMode::Equals => match matcher_result { + MatcherResult::Match => "is equal to", + MatcherResult::NoMatch => "isn't equal to", + }, + MatchMode::Contains => match matcher_result { + MatcherResult::Match => "contains a substring", + MatcherResult::NoMatch => "does not contain a substring", + }, + MatchMode::StartsWith => match matcher_result { + MatcherResult::Match => "starts with prefix", + MatcherResult::NoMatch => "does not start with", + }, + MatchMode::EndsWith => match matcher_result { + MatcherResult::Match => "ends with suffix", + MatcherResult::NoMatch => "does not end with", + }, + }; + format!("{match_mode_description} {expected:?}{extra}") + } + + fn explain_match(&self, expected: &str, actual: &str) -> String { + let default_explanation = format!( + "which {}", + self.describe(self.do_strings_match(expected, actual).into(), expected) + ); + if !expected.contains('\n') || !actual.contains('\n') { + return default_explanation; + } + + if self.ignore_leading_whitespace { + // TODO - b/283448414 : Support StrMatcher with ignore_leading_whitespace. + return default_explanation; + } + + if self.ignore_trailing_whitespace { + // TODO - b/283448414 : Support StrMatcher with ignore_trailing_whitespace. + return default_explanation; + } + + if self.times.is_some() { + // TODO - b/283448414 : Support StrMatcher with times. + return default_explanation; + } + if matches!(self.case_policy, CasePolicy::IgnoreAscii) { + // TODO - b/283448414 : Support StrMatcher with ignore ascii case policy. + return default_explanation; + } + if self.do_strings_match(expected, actual) { + // TODO - b/283448414 : Consider supporting debug difference if the + // strings match. This can be useful when a small contains is found + // in a long string. + return default_explanation; + } + + let diff = match self.mode { + MatchMode::Equals | MatchMode::StartsWith | MatchMode::Contains => { + // TODO(b/287632452): Also consider improving the output in MatchMode::Contains + // when the substring begins or ends in the middle of a line of the actual + // value. + create_diff(actual, expected, self.mode.to_diff_mode()) + } + MatchMode::EndsWith => create_diff_reversed(actual, expected, self.mode.to_diff_mode()), + }; + + format!("{default_explanation}\n{diff}",) + } + + fn ignoring_leading_whitespace(self) -> Self { + Self { ignore_leading_whitespace: true, ..self } + } + + fn ignoring_trailing_whitespace(self) -> Self { + Self { ignore_trailing_whitespace: true, ..self } + } + + fn ignoring_outer_whitespace(self) -> Self { + Self { ignore_leading_whitespace: true, ignore_trailing_whitespace: true, ..self } + } + + fn ignoring_ascii_case(self) -> Self { + Self { case_policy: CasePolicy::IgnoreAscii, ..self } + } + + fn times(self, times: impl Matcher<ActualT = usize> + 'static) -> Self { + Self { times: Some(Box::new(times)), ..self } + } +} + +impl Default for Configuration { + fn default() -> Self { + Self { + mode: MatchMode::Equals, + ignore_leading_whitespace: false, + ignore_trailing_whitespace: false, + case_policy: CasePolicy::Respect, + times: None, + } + } +} + +#[cfg(test)] +mod tests { + use super::{contains_substring, ends_with, starts_with, StrMatcher, StrMatcherConfigurator}; + use crate::matcher::{Matcher, MatcherResult}; + use crate::prelude::*; + use indoc::indoc; + + #[test] + fn matches_string_reference_with_equal_string_reference() -> Result<()> { + let matcher = StrMatcher::with_default_config("A string"); + verify_that!("A string", matcher) + } + + #[test] + fn does_not_match_string_reference_with_non_equal_string_reference() -> Result<()> { + let matcher = StrMatcher::with_default_config("Another string"); + verify_that!("A string", not(matcher)) + } + + #[test] + fn matches_owned_string_with_string_reference() -> Result<()> { + let matcher = StrMatcher::with_default_config("A string"); + let value = "A string".to_string(); + verify_that!(value, matcher) + } + + #[test] + fn matches_owned_string_reference_with_string_reference() -> Result<()> { + let matcher = StrMatcher::with_default_config("A string"); + let value = "A string".to_string(); + verify_that!(&value, matcher) + } + + #[test] + fn ignores_leading_whitespace_in_expected_when_requested() -> Result<()> { + let matcher = StrMatcher::with_default_config(" \n\tA string"); + verify_that!("A string", matcher.ignoring_leading_whitespace()) + } + + #[test] + fn ignores_leading_whitespace_in_actual_when_requested() -> Result<()> { + let matcher = StrMatcher::with_default_config("A string"); + verify_that!(" \n\tA string", matcher.ignoring_leading_whitespace()) + } + + #[test] + fn does_not_match_unequal_remaining_string_when_ignoring_leading_whitespace() -> Result<()> { + let matcher = StrMatcher::with_default_config(" \n\tAnother string"); + verify_that!("A string", not(matcher.ignoring_leading_whitespace())) + } + + #[test] + fn remains_sensitive_to_trailing_whitespace_when_ignoring_leading_whitespace() -> Result<()> { + let matcher = StrMatcher::with_default_config("A string \n\t"); + verify_that!("A string", not(matcher.ignoring_leading_whitespace())) + } + + #[test] + fn ignores_trailing_whitespace_in_expected_when_requested() -> Result<()> { + let matcher = StrMatcher::with_default_config("A string \n\t"); + verify_that!("A string", matcher.ignoring_trailing_whitespace()) + } + + #[test] + fn ignores_trailing_whitespace_in_actual_when_requested() -> Result<()> { + let matcher = StrMatcher::with_default_config("A string"); + verify_that!("A string \n\t", matcher.ignoring_trailing_whitespace()) + } + + #[test] + fn does_not_match_unequal_remaining_string_when_ignoring_trailing_whitespace() -> Result<()> { + let matcher = StrMatcher::with_default_config("Another string \n\t"); + verify_that!("A string", not(matcher.ignoring_trailing_whitespace())) + } + + #[test] + fn remains_sensitive_to_leading_whitespace_when_ignoring_trailing_whitespace() -> Result<()> { + let matcher = StrMatcher::with_default_config(" \n\tA string"); + verify_that!("A string", not(matcher.ignoring_trailing_whitespace())) + } + + #[test] + fn ignores_leading_and_trailing_whitespace_in_expected_when_requested() -> Result<()> { + let matcher = StrMatcher::with_default_config(" \n\tA string \n\t"); + verify_that!("A string", matcher.ignoring_outer_whitespace()) + } + + #[test] + fn ignores_leading_and_trailing_whitespace_in_actual_when_requested() -> Result<()> { + let matcher = StrMatcher::with_default_config("A string"); + verify_that!(" \n\tA string \n\t", matcher.ignoring_outer_whitespace()) + } + + #[test] + fn respects_ascii_case_by_default() -> Result<()> { + let matcher = StrMatcher::with_default_config("A string"); + verify_that!("A STRING", not(matcher)) + } + + #[test] + fn ignores_ascii_case_when_requested() -> Result<()> { + let matcher = StrMatcher::with_default_config("A string"); + verify_that!("A STRING", matcher.ignoring_ascii_case()) + } + + #[test] + fn allows_ignoring_leading_whitespace_from_eq() -> Result<()> { + verify_that!("A string", eq(" \n\tA string").ignoring_leading_whitespace()) + } + + #[test] + fn allows_ignoring_trailing_whitespace_from_eq() -> Result<()> { + verify_that!("A string", eq("A string \n\t").ignoring_trailing_whitespace()) + } + + #[test] + fn allows_ignoring_outer_whitespace_from_eq() -> Result<()> { + verify_that!("A string", eq(" \n\tA string \n\t").ignoring_outer_whitespace()) + } + + #[test] + fn allows_ignoring_ascii_case_from_eq() -> Result<()> { + verify_that!("A string", eq("A STRING").ignoring_ascii_case()) + } + + #[test] + fn allows_ignoring_ascii_case_from_eq_deref_of_str_slice() -> Result<()> { + verify_that!("A string", eq_deref_of("A STRING").ignoring_ascii_case()) + } + + #[test] + fn allows_ignoring_ascii_case_from_eq_deref_of_owned_string() -> Result<()> { + verify_that!("A string", eq_deref_of("A STRING".to_string()).ignoring_ascii_case()) + } + + #[test] + fn matches_string_containing_expected_value_in_contains_mode() -> Result<()> { + verify_that!("Some string", contains_substring("str")) + } + + #[test] + fn matches_string_containing_expected_value_in_contains_mode_while_ignoring_ascii_case() + -> Result<()> { + verify_that!("Some string", contains_substring("STR").ignoring_ascii_case()) + } + + #[test] + fn contains_substring_matches_correct_number_of_substrings() -> Result<()> { + verify_that!("Some string", contains_substring("str").times(eq(1))) + } + + #[test] + fn contains_substring_does_not_match_incorrect_number_of_substrings() -> Result<()> { + verify_that!("Some string\nSome string", not(contains_substring("string").times(eq(1)))) + } + + #[test] + fn contains_substring_does_not_match_when_substrings_overlap() -> Result<()> { + verify_that!("ababab", not(contains_substring("abab").times(eq(2)))) + } + + #[test] + fn starts_with_matches_string_reference_with_prefix() -> Result<()> { + verify_that!("Some value", starts_with("Some")) + } + + #[test] + fn starts_with_matches_string_reference_with_prefix_ignoring_ascii_case() -> Result<()> { + verify_that!("Some value", starts_with("SOME").ignoring_ascii_case()) + } + + #[test] + fn starts_with_does_not_match_wrong_prefix_ignoring_ascii_case() -> Result<()> { + verify_that!("Some value", not(starts_with("OTHER").ignoring_ascii_case())) + } + + #[test] + fn ends_with_does_not_match_short_string_ignoring_ascii_case() -> Result<()> { + verify_that!("Some", not(starts_with("OTHER").ignoring_ascii_case())) + } + + #[test] + fn starts_with_does_not_match_string_without_prefix() -> Result<()> { + verify_that!("Some value", not(starts_with("Another"))) + } + + #[test] + fn starts_with_does_not_match_string_with_substring_not_at_beginning() -> Result<()> { + verify_that!("Some value", not(starts_with("value"))) + } + + #[test] + fn ends_with_matches_string_reference_with_suffix() -> Result<()> { + verify_that!("Some value", ends_with("value")) + } + + #[test] + fn ends_with_matches_string_reference_with_suffix_ignoring_ascii_case() -> Result<()> { + verify_that!("Some value", ends_with("VALUE").ignoring_ascii_case()) + } + + #[test] + fn ends_with_does_not_match_wrong_suffix_ignoring_ascii_case() -> Result<()> { + verify_that!("Some value", not(ends_with("OTHER").ignoring_ascii_case())) + } + + #[test] + fn ends_with_does_not_match_too_short_string_ignoring_ascii_case() -> Result<()> { + verify_that!("Some", not(ends_with("OTHER").ignoring_ascii_case())) + } + + #[test] + fn ends_with_does_not_match_string_without_suffix() -> Result<()> { + verify_that!("Some value", not(ends_with("other value"))) + } + + #[test] + fn ends_with_does_not_match_string_with_substring_not_at_end() -> Result<()> { + verify_that!("Some value", not(ends_with("Some"))) + } + + #[test] + fn describes_itself_for_matching_result() -> Result<()> { + let matcher: StrMatcher<&str, _> = StrMatcher::with_default_config("A string"); + verify_that!( + Matcher::describe(&matcher, MatcherResult::Match), + eq("is equal to \"A string\"") + ) + } + + #[test] + fn describes_itself_for_non_matching_result() -> Result<()> { + let matcher: StrMatcher<&str, _> = StrMatcher::with_default_config("A string"); + verify_that!( + Matcher::describe(&matcher, MatcherResult::NoMatch), + eq("isn't equal to \"A string\"") + ) + } + + #[test] + fn describes_itself_for_matching_result_ignoring_leading_whitespace() -> Result<()> { + let matcher: StrMatcher<&str, _> = + 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)") + ) + } + + #[test] + fn describes_itself_for_non_matching_result_ignoring_leading_whitespace() -> Result<()> { + let matcher: StrMatcher<&str, _> = + 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)") + ) + } + + #[test] + fn describes_itself_for_matching_result_ignoring_trailing_whitespace() -> Result<()> { + let matcher: StrMatcher<&str, _> = + 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)") + ) + } + + #[test] + fn describes_itself_for_matching_result_ignoring_leading_and_trailing_whitespace() -> Result<()> + { + let matcher: StrMatcher<&str, _> = + 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)") + ) + } + + #[test] + fn describes_itself_for_matching_result_ignoring_ascii_case() -> Result<()> { + let matcher: StrMatcher<&str, _> = + 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)") + ) + } + + #[test] + fn describes_itself_for_matching_result_ignoring_ascii_case_and_leading_whitespace() + -> Result<()> { + let matcher: StrMatcher<&str, _> = StrMatcher::with_default_config("A string") + .ignoring_leading_whitespace() + .ignoring_ascii_case(); + verify_that!( + Matcher::describe(&matcher, MatcherResult::Match), + eq("is equal to \"A string\" (ignoring leading whitespace, ignoring ASCII case)") + ) + } + + #[test] + fn describes_itself_for_matching_result_in_contains_mode() -> Result<()> { + let matcher: StrMatcher<&str, _> = contains_substring("A string"); + verify_that!( + Matcher::describe(&matcher, MatcherResult::Match), + eq("contains a substring \"A string\"") + ) + } + + #[test] + fn describes_itself_for_non_matching_result_in_contains_mode() -> Result<()> { + let matcher: StrMatcher<&str, _> = contains_substring("A string"); + verify_that!( + Matcher::describe(&matcher, MatcherResult::NoMatch), + eq("does not contain a substring \"A string\"") + ) + } + + #[test] + fn describes_itself_with_count_number() -> Result<()> { + 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)") + ) + } + + #[test] + fn describes_itself_for_matching_result_in_starts_with_mode() -> Result<()> { + let matcher: StrMatcher<&str, _> = starts_with("A string"); + verify_that!( + Matcher::describe(&matcher, MatcherResult::Match), + eq("starts with prefix \"A string\"") + ) + } + + #[test] + fn describes_itself_for_non_matching_result_in_starts_with_mode() -> Result<()> { + let matcher: StrMatcher<&str, _> = starts_with("A string"); + verify_that!( + Matcher::describe(&matcher, MatcherResult::NoMatch), + eq("does not start with \"A string\"") + ) + } + + #[test] + fn describes_itself_for_matching_result_in_ends_with_mode() -> Result<()> { + let matcher: StrMatcher<&str, _> = ends_with("A string"); + verify_that!( + Matcher::describe(&matcher, MatcherResult::Match), + eq("ends with suffix \"A string\"") + ) + } + + #[test] + fn describes_itself_for_non_matching_result_in_ends_with_mode() -> Result<()> { + let matcher: StrMatcher<&str, _> = ends_with("A string"); + verify_that!( + Matcher::describe(&matcher, MatcherResult::NoMatch), + eq("does not end with \"A string\"") + ) + } + + #[test] + fn match_explanation_contains_diff_of_strings_if_more_than_one_line() -> Result<()> { + let result = verify_that!( + indoc!( + " + First line + Second line + Third line + " + ), + starts_with(indoc!( + " + First line + Second lines + Third line + " + )) + ); + + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + First line + -Second line + +Second lines + Third line + " + )))) + ) + } + + #[test] + fn match_explanation_for_starts_with_ignores_trailing_lines_in_actual_string() -> Result<()> { + let result = verify_that!( + indoc!( + " + First line + Second line + Third line + Fourth line + " + ), + starts_with(indoc!( + " + First line + Second lines + Third line + " + )) + ); + + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + First line + -Second line + +Second lines + Third line + <---- remaining lines omitted ----> + " + )))) + ) + } + + #[test] + fn match_explanation_for_starts_with_includes_both_versions_of_differing_last_line() + -> Result<()> { + let result = verify_that!( + indoc!( + " + First line + Second line + Third line + " + ), + starts_with(indoc!( + " + First line + Second lines + " + )) + ); + + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + First line + -Second line + +Second lines + <---- remaining lines omitted ----> + " + )))) + ) + } + + #[test] + fn match_explanation_for_ends_with_ignores_leading_lines_in_actual_string() -> Result<()> { + let result = verify_that!( + indoc!( + " + First line + Second line + Third line + Fourth line + " + ), + ends_with(indoc!( + " + Second line + Third lines + Fourth line + " + )) + ); + + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + Difference(-actual / +expected): + <---- remaining lines omitted ----> + Second line + +Third lines + -Third line + Fourth line + " + )))) + ) + } + + #[test] + fn match_explanation_for_contains_substring_ignores_outer_lines_in_actual_string() -> Result<()> + { + let result = verify_that!( + indoc!( + " + First line + Second line + Third line + Fourth line + Fifth line + " + ), + contains_substring(indoc!( + " + Second line + Third lines + Fourth line + " + )) + ); + + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + Difference(-actual / +expected): + <---- remaining lines omitted ----> + Second line + +Third lines + -Third line + Fourth line + <---- remaining lines omitted ---->" + )))) + ) + } + + #[test] + fn match_explanation_for_contains_substring_shows_diff_when_first_and_last_line_are_incomplete() + -> Result<()> { + let result = verify_that!( + indoc!( + " + First line + Second line + Third line + Fourth line + Fifth line + " + ), + contains_substring(indoc!( + " + line + Third line + Foorth line + Fifth" + )) + ); + + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + Difference(-actual / +expected): + <---- remaining lines omitted ----> + +line + -Second line + Third line + +Foorth line + -Fourth line + +Fifth + -Fifth line + <---- remaining lines omitted ----> + " + )))) + ) + } + + #[test] + fn match_explanation_for_eq_does_not_ignore_trailing_lines_in_actual_string() -> Result<()> { + let result = verify_that!( + indoc!( + " + First line + Second line + Third line + Fourth line + " + ), + eq(indoc!( + " + First line + Second lines + Third line + " + )) + ); + + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + First line + -Second line + +Second lines + Third line + -Fourth line + " + )))) + ) + } + + #[test] + fn match_explanation_does_not_show_diff_if_actual_value_is_single_line() -> Result<()> { + let result = verify_that!( + "First line", + starts_with(indoc!( + " + Second line + Third line + " + )) + ); + + verify_that!( + result, + err(displays_as(not(contains_substring("Difference(-actual / +expected):")))) + ) + } + + #[test] + fn match_explanation_does_not_show_diff_if_expected_value_is_single_line() -> Result<()> { + let result = verify_that!( + indoc!( + " + First line + Second line + Third line + " + ), + starts_with("Second line") + ); + + verify_that!( + result, + err(displays_as(not(contains_substring("Difference(-actual / +expected):")))) + ) + } +} diff --git a/src/matchers/subset_of_matcher.rs b/src/matchers/subset_of_matcher.rs new file mode 100644 index 0000000..facee4f --- /dev/null +++ b/src/matchers/subset_of_matcher.rs @@ -0,0 +1,267 @@ +// 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. + +use crate::matcher::{Matcher, MatcherResult}; +use std::{fmt::Debug, marker::PhantomData}; + +/// Matches a container all of whose items are in the given container +/// `superset`. +/// +/// The element type `ElementT` must implement `PartialEq` to allow element +/// comparison. +/// +/// `ActualT` and `ExpectedT` can each be any container a reference to which +/// implements `IntoIterator`. They need not be the same container type. +/// +/// ``` +/// # use googletest::prelude::*; +/// # use std::collections::HashSet; +/// # fn should_pass_1() -> Result<()> { +/// let value = vec![1, 2, 3]; +/// verify_that!(value, subset_of([1, 2, 3, 4]))?; // Passes +/// # Ok(()) +/// # } +/// # fn should_fail() -> Result<()> { +/// # let value = vec![1, 2, 3]; +/// verify_that!(value, subset_of([1, 2]))?; // Fails: 3 is not in the superset +/// # Ok(()) +/// # } +/// # should_pass_1().unwrap(); +/// # should_fail().unwrap_err(); +/// +/// # fn should_pass_2() -> Result<()> { +/// let value: HashSet<i32> = [1, 2, 3].into(); +/// verify_that!(value, subset_of([1, 2, 3]))?; // Passes +/// # Ok(()) +/// # } +/// # should_pass_2().unwrap(); +/// ``` +/// +/// Item multiplicity in both the actual and expected containers is ignored: +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// let value: Vec<i32> = vec![0, 0, 1]; +/// verify_that!(value, subset_of([0, 1]))?; // Passes +/// verify_that!(value, subset_of([0, 1, 1]))?; // Passes +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// ``` +/// +/// One can also verify the contents of a slice by dereferencing it: +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// let value = &[1, 2, 3]; +/// verify_that!(*value, subset_of([1, 2, 3]))?; +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// ``` +/// +/// A note on performance: This matcher uses a naive algorithm with a worst-case +/// runtime proportional to the *product* of the sizes of the actual and +/// expected containers as well as the time to check equality of each pair of +/// items. It should not be used on especially large containers. +pub fn subset_of<ElementT: Debug + PartialEq, ActualT: Debug + ?Sized, ExpectedT: Debug>( + superset: ExpectedT, +) -> impl Matcher<ActualT = ActualT> +where + for<'a> &'a ActualT: IntoIterator<Item = &'a ElementT>, + for<'a> &'a ExpectedT: IntoIterator<Item = &'a ElementT>, +{ + SubsetOfMatcher::<ActualT, _> { superset, phantom: Default::default() } +} + +struct SubsetOfMatcher<ActualT: ?Sized, ExpectedT> { + superset: ExpectedT, + phantom: PhantomData<ActualT>, +} + +impl<ElementT: Debug + PartialEq, ActualT: Debug + ?Sized, ExpectedT: Debug> Matcher + for SubsetOfMatcher<ActualT, ExpectedT> +where + for<'a> &'a ActualT: IntoIterator<Item = &'a ElementT>, + for<'a> &'a ExpectedT: IntoIterator<Item = &'a ElementT>, +{ + type ActualT = ActualT; + + fn matches(&self, actual: &ActualT) -> MatcherResult { + for actual_item in actual { + if self.expected_is_missing(actual_item) { + return MatcherResult::NoMatch; + } + } + MatcherResult::Match + } + + fn explain_match(&self, actual: &ActualT) -> String { + let unexpected_elements = actual + .into_iter() + .enumerate() + .filter(|&(_, actual_item)| self.expected_is_missing(actual_item)) + .map(|(idx, actual_item)| format!("{actual_item:#?} at #{idx}")) + .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(", ")), + } + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + match matcher_result { + MatcherResult::Match => format!("is a subset of {:#?}", self.superset), + MatcherResult::NoMatch => format!("isn't a subset of {:#?}", self.superset), + } + } +} + +impl<ActualT: ?Sized, ElementT: PartialEq, ExpectedT> SubsetOfMatcher<ActualT, ExpectedT> +where + for<'a> &'a ExpectedT: IntoIterator<Item = &'a ElementT>, +{ + fn expected_is_missing(&self, needle: &ElementT) -> bool { + !self.superset.into_iter().any(|item| *item == *needle) + } +} + +#[cfg(test)] +mod tests { + use super::subset_of; + use crate::prelude::*; + use indoc::indoc; + use std::collections::HashSet; + + #[test] + fn subset_of_matches_empty_vec() -> Result<()> { + let value: Vec<i32> = vec![]; + verify_that!(value, subset_of([])) + } + + #[test] + fn subset_of_matches_vec_with_one_element() -> Result<()> { + let value = vec![1]; + verify_that!(value, subset_of([1])) + } + + #[test] + fn subset_of_matches_vec_with_two_elements() -> Result<()> { + let value = vec![1, 2]; + verify_that!(value, subset_of([1, 2])) + } + + #[test] + fn subset_of_matches_vec_when_expected_has_excess_element() -> Result<()> { + let value = vec![1, 2]; + verify_that!(value, subset_of([1, 2, 3])) + } + + #[test] + fn subset_of_matches_vec_when_expected_has_excess_element_first() -> Result<()> { + let value = vec![1, 2]; + verify_that!(value, subset_of([3, 1, 2])) + } + + #[test] + fn subset_of_matches_slice_with_one_element() -> Result<()> { + let value = &[1]; + verify_that!(*value, subset_of([1])) + } + + #[test] + fn subset_of_matches_hash_set_with_one_element() -> Result<()> { + let value: HashSet<i32> = [1].into(); + verify_that!(value, subset_of([1])) + } + + #[test] + fn subset_of_does_not_match_when_first_element_does_not_match() -> Result<()> { + let value = vec![0]; + verify_that!(value, not(subset_of([1]))) + } + + #[test] + fn subset_of_does_not_match_when_second_element_does_not_match() -> Result<()> { + let value = vec![2, 0]; + verify_that!(value, not(subset_of([2]))) + } + + #[test] + fn subset_of_shows_correct_message_when_first_item_does_not_match() -> Result<()> { + let result = verify_that!(vec![0, 2, 3], subset_of([1, 2, 3])); + + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + Value of: vec![0, 2, 3] + Expected: is a subset of [ + 1, + 2, + 3, + ] + Actual: [0, 2, 3], + whose element 0 at #0 is unexpected + " + )))) + ) + } + + #[test] + fn subset_of_shows_correct_message_when_second_item_does_not_match() -> Result<()> { + let result = verify_that!(vec![1, 0, 3], subset_of([1, 2, 3])); + + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + Value of: vec![1, 0, 3] + Expected: is a subset of [ + 1, + 2, + 3, + ] + Actual: [1, 0, 3], + whose element 0 at #1 is unexpected + " + )))) + ) + } + + #[test] + fn subset_of_shows_correct_message_when_first_two_items_do_not_match() -> Result<()> { + let result = verify_that!(vec![0, 0, 3], subset_of([1, 2, 3])); + + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + Value of: vec![0, 0, 3] + Expected: is a subset of [ + 1, + 2, + 3, + ] + Actual: [0, 0, 3], + whose elements 0 at #0, 0 at #1 are unexpected + " + )))) + ) + } +} diff --git a/src/matchers/superset_of_matcher.rs b/src/matchers/superset_of_matcher.rs new file mode 100644 index 0000000..8e98015 --- /dev/null +++ b/src/matchers/superset_of_matcher.rs @@ -0,0 +1,267 @@ +// 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. + +use crate::matcher::{Matcher, MatcherResult}; +use std::{fmt::Debug, marker::PhantomData}; + +/// Matches a container containing all of the items in the given container +/// `subset`. +/// +/// The element type `ElementT` must implement `PartialEq` to allow element +/// comparison. +/// +/// `ActualT` and `ExpectedT` can each be any container a reference to which +/// implements `IntoIterator`. They need not be the same container type. +/// +/// ``` +/// # use googletest::prelude::*; +/// # use std::collections::HashSet; +/// # fn should_pass_1() -> Result<()> { +/// let value = vec![1, 2, 3]; +/// verify_that!(value, superset_of([1, 2]))?; // Passes +/// # Ok(()) +/// # } +/// # fn should_fail() -> Result<()> { +/// # let value = vec![1, 2, 3]; +/// verify_that!(value, superset_of([1, 2, 4]))?; // Fails: 4 is not in the subset +/// # Ok(()) +/// # } +/// # should_pass_1().unwrap(); +/// # should_fail().unwrap_err(); +/// +/// # fn should_pass_2() -> Result<()> { +/// let value: HashSet<i32> = [1, 2, 3].into(); +/// verify_that!(value, superset_of([1, 2, 3]))?; // Passes +/// # Ok(()) +/// # } +/// # should_pass_2().unwrap(); +/// ``` +/// +/// Item multiplicity in both the actual and expected containers is ignored: +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// let value: Vec<i32> = vec![0, 0, 1]; +/// verify_that!(value, superset_of([0, 1]))?; // Passes +/// verify_that!(value, superset_of([0, 1, 1]))?; // Passes +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// ``` +/// +/// One can also verify the contents of a slice by dereferencing it: +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// let value = &[1, 2, 3]; +/// verify_that!(*value, superset_of([1, 2, 3]))?; +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// ``` +/// +/// A note on performance: This matcher uses a naive algorithm with a worst-case +/// runtime proportional to the *product* of the sizes of the actual and +/// expected containers as well as the time to check equality of each pair of +/// items. It should not be used on especially large containers. +pub fn superset_of<ElementT: Debug + PartialEq, ActualT: Debug + ?Sized, ExpectedT: Debug>( + subset: ExpectedT, +) -> impl Matcher<ActualT = ActualT> +where + for<'a> &'a ActualT: IntoIterator<Item = &'a ElementT>, + for<'a> &'a ExpectedT: IntoIterator<Item = &'a ElementT>, +{ + SupersetOfMatcher::<ActualT, _> { subset, phantom: Default::default() } +} + +struct SupersetOfMatcher<ActualT: ?Sized, ExpectedT> { + subset: ExpectedT, + phantom: PhantomData<ActualT>, +} + +impl<ElementT: Debug + PartialEq, ActualT: Debug + ?Sized, ExpectedT: Debug> Matcher + for SupersetOfMatcher<ActualT, ExpectedT> +where + for<'a> &'a ActualT: IntoIterator<Item = &'a ElementT>, + for<'a> &'a ExpectedT: IntoIterator<Item = &'a ElementT>, +{ + type ActualT = ActualT; + + fn matches(&self, actual: &ActualT) -> MatcherResult { + for expected_item in &self.subset { + if actual_is_missing(actual, expected_item) { + return MatcherResult::NoMatch; + } + } + MatcherResult::Match + } + + fn explain_match(&self, actual: &ActualT) -> String { + let missing_items: Vec<_> = self + .subset + .into_iter() + .filter(|expected_item| actual_is_missing(actual, expected_item)) + .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(", ")), + } + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + match matcher_result { + MatcherResult::Match => format!("is a superset of {:#?}", self.subset), + MatcherResult::NoMatch => format!("isn't a superset of {:#?}", self.subset), + } + } +} + +fn actual_is_missing<ElementT: PartialEq, ActualT: ?Sized>( + actual: &ActualT, + needle: &ElementT, +) -> bool +where + for<'a> &'a ActualT: IntoIterator<Item = &'a ElementT>, +{ + !actual.into_iter().any(|item| *item == *needle) +} + +#[cfg(test)] +mod tests { + use super::superset_of; + use crate::prelude::*; + use indoc::indoc; + use std::collections::HashSet; + + #[test] + fn superset_of_matches_empty_vec() -> Result<()> { + let value: Vec<i32> = vec![]; + verify_that!(value, superset_of([])) + } + + #[test] + fn superset_of_matches_vec_with_one_element() -> Result<()> { + let value = vec![1]; + verify_that!(value, superset_of([1])) + } + + #[test] + fn superset_of_matches_vec_with_two_items() -> Result<()> { + let value = vec![1, 2]; + verify_that!(value, superset_of([1, 2])) + } + + #[test] + fn superset_of_matches_vec_when_actual_has_excess_element() -> Result<()> { + let value = vec![1, 2, 3]; + verify_that!(value, superset_of([1, 2])) + } + + #[test] + fn superset_of_matches_vec_when_actual_has_excess_element_first() -> Result<()> { + let value = vec![3, 1, 2]; + verify_that!(value, superset_of([1, 2])) + } + + #[test] + fn superset_of_matches_slice_with_one_element() -> Result<()> { + let value = &[1]; + verify_that!(*value, superset_of([1])) + } + + #[test] + fn superset_of_matches_hash_set_with_one_element() -> Result<()> { + let value: HashSet<i32> = [1].into(); + verify_that!(value, superset_of([1])) + } + + #[test] + fn superset_of_does_not_match_when_first_element_does_not_match() -> Result<()> { + let value = vec![0]; + verify_that!(value, not(superset_of([1]))) + } + + #[test] + fn superset_of_does_not_match_when_second_element_does_not_match() -> Result<()> { + let value = vec![2]; + verify_that!(value, not(superset_of([2, 0]))) + } + + #[test] + fn superset_of_shows_correct_message_when_first_item_does_not_match() -> Result<()> { + let result = verify_that!(vec![0, 2, 3], superset_of([1, 2, 3])); + + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + Value of: vec![0, 2, 3] + Expected: is a superset of [ + 1, + 2, + 3, + ] + Actual: [0, 2, 3], + whose element 1 is missing + " + )))) + ) + } + + #[test] + fn superset_of_shows_correct_message_when_second_item_does_not_match() -> Result<()> { + let result = verify_that!(vec![1, 0, 3], superset_of([1, 2, 3])); + + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + Value of: vec![1, 0, 3] + Expected: is a superset of [ + 1, + 2, + 3, + ] + Actual: [1, 0, 3], + whose element 2 is missing + " + )))) + ) + } + + #[test] + fn superset_of_shows_correct_message_when_first_two_items_do_not_match() -> Result<()> { + let result = verify_that!(vec![0, 0, 3], superset_of([1, 2, 3])); + + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + Value of: vec![0, 0, 3] + Expected: is a superset of [ + 1, + 2, + 3, + ] + Actual: [0, 0, 3], + whose elements 1, 2 are missing + " + )))) + ) + } +} diff --git a/src/matchers/tuple_matcher.rs b/src/matchers/tuple_matcher.rs new file mode 100644 index 0000000..a2e325b --- /dev/null +++ b/src/matchers/tuple_matcher.rs @@ -0,0 +1,207 @@ +// 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. + +// There are no visible documentation elements in this module; the declarative +// macro is documented at the top level. +#![doc(hidden)] + +/// Functions for use only by the declarative macros in this module. +/// +/// **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 + }; + } + + // This implementation is provided for completeness, but is completely trivial. + // The only actual value which can be supplied is (), which must match. + impl Matcher for () { + type ActualT = (); + + fn matches(&self, _: &Self::ActualT) -> MatcherResult { + MatcherResult::Match + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + match matcher_result { + MatcherResult::Match => "is the empty tuple".into(), + MatcherResult::NoMatch => "is not the empty tuple".into(), + } + } + } + + /// Generates a tuple matcher for tuples of a specific length. + /// + /// **For internal use only. API stablility is not guaranteed!** + #[doc(hidden)] + macro_rules! tuple_matcher_n { + ($([$field_number:tt, $matcher_type:ident, $field_type:ident]),*) => { + impl<$($field_type: Debug, $matcher_type: Matcher<ActualT = $field_type>),*> + Matcher for ($($matcher_type,)*) + { + type ActualT = ($($field_type,)*); + + fn matches(&self, actual: &($($field_type,)*)) -> MatcherResult { + $(match self.$field_number.matches(&actual.$field_number) { + MatcherResult::Match => {}, + MatcherResult::NoMatch => { + return MatcherResult::NoMatch; + } + })* + MatcherResult::Match + } + + fn explain_match(&self, actual: &($($field_type,)*)) -> String { + let mut explanation = format!("which {}", 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) + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + 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)),* + ) + } + 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)),* + ) + } + } + } + } + }; + } + + tuple_matcher_n!([0, I0, T0]); + + tuple_matcher_n!([0, I0, T0], [1, I1, T1]); + + tuple_matcher_n!([0, I0, T0], [1, I1, T1], [2, I2, T2]); + + tuple_matcher_n!([0, I0, T0], [1, I1, T1], [2, I2, T2], [3, I3, T3]); + + tuple_matcher_n!([0, I0, T0], [1, I1, T1], [2, I2, T2], [3, I3, T3], [4, I4, T4]); + + tuple_matcher_n!([0, I0, T0], [1, I1, T1], [2, I2, T2], [3, I3, T3], [4, I4, T4], [5, I5, T5]); + + tuple_matcher_n!( + [0, I0, T0], + [1, I1, T1], + [2, I2, T2], + [3, I3, T3], + [4, I4, T4], + [5, I5, T5], + [6, I6, T6] + ); + + tuple_matcher_n!( + [0, I0, T0], + [1, I1, T1], + [2, I2, T2], + [3, I3, T3], + [4, I4, T4], + [5, I5, T5], + [6, I6, T6], + [7, I7, T7] + ); + + tuple_matcher_n!( + [0, I0, T0], + [1, I1, T1], + [2, I2, T2], + [3, I3, T3], + [4, I4, T4], + [5, I5, T5], + [6, I6, T6], + [7, I7, T7], + [8, I8, T8] + ); + + tuple_matcher_n!( + [0, I0, T0], + [1, I1, T1], + [2, I2, T2], + [3, I3, T3], + [4, I4, T4], + [5, I5, T5], + [6, I6, T6], + [7, I7, T7], + [8, I8, T8], + [9, I9, T9] + ); + + tuple_matcher_n!( + [0, I0, T0], + [1, I1, T1], + [2, I2, T2], + [3, I3, T3], + [4, I4, T4], + [5, I5, T5], + [6, I6, T6], + [7, I7, T7], + [8, I8, T8], + [9, I9, T9], + [10, I10, T10] + ); + + tuple_matcher_n!( + [0, I0, T0], + [1, I1, T1], + [2, I2, T2], + [3, I3, T3], + [4, I4, T4], + [5, I5, T5], + [6, I6, T6], + [7, I7, T7], + [8, I8, T8], + [9, I9, T9], + [10, I10, T10], + [11, I11, T11] + ); +} diff --git a/src/matchers/unordered_elements_are_matcher.rs b/src/matchers/unordered_elements_are_matcher.rs new file mode 100644 index 0000000..1930697 --- /dev/null +++ b/src/matchers/unordered_elements_are_matcher.rs @@ -0,0 +1,1116 @@ +// 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. + +// There are no visible documentation elements in this module; the declarative +// macro is documented at the top level. +#![doc(hidden)] + +/// Matches a container whose elements in any order have a 1:1 correspondence +/// with the provided element matchers. +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// verify_that!(vec![3, 2, 1], unordered_elements_are![eq(1), ge(2), anything()])?; // Passes +/// # Ok(()) +/// # } +/// # fn should_fail_1() -> Result<()> { +/// verify_that!(vec![1], unordered_elements_are![eq(1), ge(2)])?; // Fails: container has wrong size +/// # Ok(()) +/// # } +/// # fn should_fail_2() -> Result<()> { +/// verify_that!(vec![3, 2, 1], unordered_elements_are![eq(1), ge(4), eq(2)])?; // Fails: second matcher not matched +/// # Ok(()) +/// # } +/// # fn should_fail_3() -> Result<()> { +/// verify_that!(vec![3, 2, 1], unordered_elements_are![ge(3), ge(3), ge(3)])?; // Fails: no 1:1 correspondence +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// # should_fail_1().unwrap_err(); +/// # should_fail_2().unwrap_err(); +/// # should_fail_3().unwrap_err(); +/// ``` +/// +/// The actual value must be a container implementing [`IntoIterator`]. This +/// includes standard containers, slices (when dereferenced) and arrays. +/// +/// This can also match against [`HashMap`][std::collections::HashMap] and +/// similar collections. The arguments are a sequence of pairs of matchers +/// corresponding to the keys and their respective values. +/// +/// ``` +/// # use googletest::prelude::*; +/// # use std::collections::HashMap; +/// let value: HashMap<u32, &'static str> = +/// HashMap::from_iter([(1, "One"), (2, "Two"), (3, "Three")]); +/// verify_that!( +/// value, +/// unordered_elements_are![(eq(2), eq("Two")), (eq(1), eq("One")), (eq(3), eq("Three"))] +/// ) +/// # .unwrap(); +/// ``` +/// +/// This can also be omitted in [`verify_that!`] macros and replaced with curly +/// brackets. +/// +/// ``` +/// # use googletest::prelude::*; +/// verify_that!(vec![1, 2], {eq(2), eq(1)}) +/// # .unwrap(); +/// ``` +/// +/// 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. +/// +/// ```compile_fail +/// # use googletest::prelude::*; +/// verify_that!(vec![vec![1,2], vec![3]], {{eq(2), eq(1)}, {eq(3)}}) +/// # .unwrap(); +/// ``` +/// +/// Use this instead: +/// ``` +/// # use googletest::prelude::*; +/// verify_that!(vec![vec![1,2], vec![3]], +/// {unordered_elements_are![eq(2), eq(1)], unordered_elements_are![eq(3)]}) +/// # .unwrap(); +/// ``` +/// +/// This matcher does not support matching directly against an [`Iterator`]. To +/// match against an iterator, use [`Iterator::collect`] to build a [`Vec`]. +/// +/// The matcher proceeds in three stages: +/// +/// 1. It first checks whether the actual value is of the right size to possibly +/// be matched by each of the given matchers. If not, then it immediately +/// fails explaining that the size is incorrect. +/// +/// 2. It then checks whether each matcher matches at least one corresponding +/// element in the actual container and each element in the actual container +/// is matched by at least one matcher. If not, it fails with a message +/// indicating which matcher respectively container elements had no +/// counterparts. +/// +/// 3. Finally, it checks whether the mapping of matchers to corresponding +/// actual elements is a 1-1 correspondence and fails if that is not the +/// case. The failure message then shows the best matching it could find, +/// including which matchers did not have corresponding unique elements in +/// the container and which container elements had no corresponding matchers. +/// +/// [`IntoIterator`]: std::iter::IntoIterator +/// [`Iterator`]: std::iter::Iterator +/// [`Iterator::collect`]: std::iter::Iterator::collect +/// [`Vec`]: std::vec::Vec +#[macro_export] +macro_rules! unordered_elements_are { + ($(,)?) => {{ + use $crate::matchers::unordered_elements_are_matcher::internal::{ + UnorderedElementsAreMatcher, Requirements + }; + UnorderedElementsAreMatcher::new([], Requirements::PerfectMatch) + }}; + + // 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::{ + UnorderedElementsOfMapAreMatcher, Requirements + }; + UnorderedElementsOfMapAreMatcher::new( + [$((Box::new($key_matcher), Box::new($value_matcher))),*], + Requirements::PerfectMatch + ) + }}; + + ($($matcher:expr),* $(,)?) => {{ + use $crate::matchers::unordered_elements_are_matcher::internal::{ + UnorderedElementsAreMatcher, Requirements + }; + UnorderedElementsAreMatcher::new([$(Box::new($matcher)),*], Requirements::PerfectMatch) + }}; +} + +/// Matches a container containing elements matched by the given matchers. +/// +/// To match, each given matcher must have a corresponding element in the +/// container which it matches. There must be a mapping uniquely matching each +/// matcher to a container element. The container can, however, contain +/// 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. +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// verify_that!(vec![3, 2, 1], contains_each![eq(2), ge(3)])?; // Passes +/// verify_that!(vec![3, 2, 1], contains_each![ge(2), ge(2)])?; // Passes +/// # Ok(()) +/// # } +/// # fn should_fail_1() -> Result<()> { +/// verify_that!(vec![1], contains_each![eq(1), ge(2)])?; // Fails: container too small +/// # Ok(()) +/// # } +/// # fn should_fail_2() -> Result<()> { +/// verify_that!(vec![3, 2, 1], contains_each![eq(1), ge(4)])?; // Fails: second matcher unmatched +/// # Ok(()) +/// # } +/// # fn should_fail_3() -> Result<()> { +/// verify_that!(vec![3, 2, 1], contains_each![ge(3), ge(3), ge(3)])?; // Fails: no matching +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// # should_fail_1().unwrap_err(); +/// # should_fail_2().unwrap_err(); +/// # should_fail_3().unwrap_err(); +/// ``` +/// +/// The actual value must be a container implementing [`IntoIterator`]. This +/// includes standard containers, slices (when dereferenced) and arrays. +/// +/// This can also match against [`HashMap`][std::collections::HashMap] and +/// similar collections. The arguments are a sequence of pairs of matchers +/// corresponding to the keys and their respective values. +/// +/// ``` +/// # use googletest::prelude::*; +/// # use std::collections::HashMap; +/// let value: HashMap<u32, &'static str> = +/// HashMap::from_iter([(1, "One"), (2, "Two"), (3, "Three")]); +/// verify_that!(value, contains_each![(eq(2), eq("Two")), (eq(1), eq("One"))]) +/// # .unwrap(); +/// ``` +/// +/// This matcher does not support matching directly against an [`Iterator`]. To +/// match against an iterator, use [`Iterator::collect`] to build a [`Vec`]. +/// +/// The matcher proceeds in three stages: +/// +/// 1. It first checks whether the actual value is large enough to possibly be +/// matched by each of the given matchers. If not, then it immediately fails +/// explaining that the size is too small. +/// +/// 2. It then checks whether each matcher matches at least one corresponding +/// element in the actual container and fails if that is not the case. The +/// failure message indicates which matcher had no corresponding element. +/// +/// 3. Finally, it checks whether the mapping of matchers to corresponding +/// actual elements is 1-1 and fails if that is not the case. The failure +/// message then shows the best matching it could find, including which +/// matchers did not have corresponding unique elements in the container. +/// +/// [`IntoIterator`]: std::iter::IntoIterator +/// [`Iterator`]: std::iter::Iterator +/// [`Iterator::collect`]: std::iter::Iterator::collect +/// [`Vec`]: std::vec::Vec +#[macro_export] +macro_rules! contains_each { + ($(,)?) => {{ + use $crate::matchers::unordered_elements_are_matcher::internal::{ + UnorderedElementsAreMatcher, Requirements + }; + UnorderedElementsAreMatcher::new([], Requirements::Superset) + }}; + + // 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::{ + UnorderedElementsOfMapAreMatcher, Requirements + }; + UnorderedElementsOfMapAreMatcher::new( + [$((Box::new($key_matcher), Box::new($value_matcher))),*], + Requirements::Superset + ) + }}; + + ($($matcher:expr),* $(,)?) => {{ + use $crate::matchers::unordered_elements_are_matcher::internal::{ + UnorderedElementsAreMatcher, Requirements + }; + UnorderedElementsAreMatcher::new([$(Box::new($matcher)),*], Requirements::Superset) + }} +} + +/// Matches a container all of whose elements are matched by the given matchers. +/// +/// To match, each element in the container must have a corresponding matcher +/// which matches it. There must be a 1-1 mapping from container elements to +/// matchers, so that no matcher has more than one corresponding element. +/// +/// There may, however, be matchers not corresponding to any elements in the +/// container. +/// +/// Put another way, `is_contained_in!` matches if there is a subset of the +/// matchers which would match with [`unordered_elements_are`]. +/// +/// ``` +/// # use googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// verify_that!(vec![2, 1], is_contained_in![eq(1), ge(2)])?; // Passes +/// verify_that!(vec![2, 1], is_contained_in![ge(1), ge(1)])?; // Passes +/// # Ok(()) +/// # } +/// # fn should_fail_1() -> Result<()> { +/// verify_that!(vec![1, 2, 3], is_contained_in![eq(1), ge(2)])?; // Fails: container too large +/// # Ok(()) +/// # } +/// # fn should_fail_2() -> Result<()> { +/// verify_that!(vec![2, 1], is_contained_in![eq(1), ge(4)])?; // Fails: second matcher unmatched +/// # Ok(()) +/// # } +/// # fn should_fail_3() -> Result<()> { +/// verify_that!(vec![3, 1], is_contained_in![ge(3), ge(3), ge(3)])?; // Fails: no matching +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// # should_fail_1().unwrap_err(); +/// # should_fail_2().unwrap_err(); +/// # should_fail_3().unwrap_err(); +/// ``` +/// +/// The actual value must be a container implementing [`IntoIterator`]. This +/// includes standard containers, slices (when dereferenced) and arrays. +/// +/// This can also match against [`HashMap`][std::collections::HashMap] and +/// similar collections. The arguments are a sequence of pairs of matchers +/// corresponding to the keys and their respective values. +/// +/// ``` +/// # use googletest::prelude::*; +/// # use std::collections::HashMap; +/// let value: HashMap<u32, &'static str> = HashMap::from_iter([(1, "One"), (2, "Two")]); +/// verify_that!( +/// value, +/// is_contained_in![(eq(2), eq("Two")), (eq(1), eq("One")), (eq(3), eq("Three"))] +/// ) +/// # .unwrap(); +/// ``` +/// +/// This matcher does not support matching directly against an [`Iterator`]. To +/// match against an iterator, use [`Iterator::collect`] to build a [`Vec`]. +/// +/// The matcher proceeds in three stages: +/// +/// 1. It first checks whether the actual value is too large to possibly be +/// matched by each of the given matchers. If so, it immediately fails +/// explaining that the size is too large. +/// +/// 2. It then checks whether each actual container element is matched by at +/// least one matcher and fails if that is not the case. The failure message +/// indicates which element had no corresponding matcher. +/// +/// 3. Finally, it checks whether the mapping of elements to corresponding +/// matchers is 1-1 and fails if that is not the case. The failure message +/// then shows the best matching it could find, including which container +/// elements did not have corresponding matchers. +/// +/// [`IntoIterator`]: std::iter::IntoIterator +/// [`Iterator`]: std::iter::Iterator +/// [`Iterator::collect`]: std::iter::Iterator::collect +/// [`Vec`]: std::vec::Vec +#[macro_export] +macro_rules! is_contained_in { + ($(,)?) => {{ + use $crate::matchers::unordered_elements_are_matcher::internal::{ + UnorderedElementsAreMatcher, Requirements + }; + UnorderedElementsAreMatcher::new([], Requirements::Subset) + }}; + + // 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::{ + UnorderedElementsOfMapAreMatcher, Requirements + }; + UnorderedElementsOfMapAreMatcher::new( + [$((Box::new($key_matcher), Box::new($value_matcher))),*], + Requirements::Subset + ) + }}; + + ($($matcher:expr),* $(,)?) => {{ + use $crate::matchers::unordered_elements_are_matcher::internal::{ + UnorderedElementsAreMatcher, Requirements + }; + UnorderedElementsAreMatcher::new([$(Box::new($matcher)),*], Requirements::Subset) + }} +} + +/// Module for use only by the macros in this module. +/// +/// **For internal use only. API stablility is not guaranteed!** +#[doc(hidden)] +pub mod internal { + 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; + + /// This struct is meant to be used only through the + /// `unordered_elements_are![...]` macro. + /// + /// **For internal use only. API stablility is not guaranteed!** + #[doc(hidden)] + pub struct UnorderedElementsAreMatcher<'a, ContainerT: ?Sized, T: Debug, const N: usize> { + elements: [Box<dyn Matcher<ActualT = T> + 'a>; N], + requirements: Requirements, + phantom: PhantomData<ContainerT>, + } + + impl<'a, ContainerT: ?Sized, T: Debug, const N: usize> + UnorderedElementsAreMatcher<'a, ContainerT, T, N> + { + pub fn new( + elements: [Box<dyn Matcher<ActualT = T> + 'a>; N], + requirements: Requirements, + ) -> Self { + Self { elements, requirements, phantom: Default::default() } + } + } + + // This matcher performs the checks in three different steps in both `matches` + // and `explain_match`. This is useful for performance but also to produce + // an actionable error message. + // 1. `UnorderedElementsAreMatcher` verifies that both collections have the same + // size + // 2. `UnorderedElementsAreMatcher` verifies that each actual element matches at + // least one expected element and vice versa. + // 3. `UnorderedElementsAreMatcher` verifies that a perfect matching exists + // using Ford-Fulkerson. + impl<'a, T: Debug, ContainerT: Debug + ?Sized, const N: usize> Matcher + for UnorderedElementsAreMatcher<'a, ContainerT, T, N> + where + for<'b> &'b ContainerT: IntoIterator<Item = &'b T>, + { + type ActualT = ContainerT; + + fn matches(&self, actual: &ContainerT) -> MatcherResult { + let match_matrix = MatchMatrix::generate(actual, &self.elements); + match_matrix.is_match_for(self.requirements).into() + } + + fn explain_match(&self, actual: &ContainerT) -> String { + if let Some(size_mismatch_explanation) = + self.requirements.explain_size_mismatch(actual, N) + { + return size_mismatch_explanation; + } + + let match_matrix = MatchMatrix::generate(actual, &self.elements); + if let Some(unmatchable_explanation) = + match_matrix.explain_unmatchable(self.requirements) + { + return unmatchable_explanation; + } + + 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()) + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + format!( + "{} elements matching in any order:\n{}", + if matcher_result.into() { "contains" } else { "doesn't contain" }, + self.elements + .iter() + .map(|matcher| matcher.describe(MatcherResult::Match)) + .collect::<Description>() + .enumerate() + .indent() + ) + } + } + + type KeyValueMatcher<'a, KeyT, ValueT> = + (Box<dyn Matcher<ActualT = KeyT> + 'a>, Box<dyn Matcher<ActualT = ValueT> + 'a>); + + /// This is the analogue to [UnorderedElementsAreMatcher] for maps and + /// map-like collections. + /// + /// **For internal use only. API stablility is not guaranteed!** + #[doc(hidden)] + pub struct UnorderedElementsOfMapAreMatcher<'a, ContainerT, KeyT, ValueT, const N: usize> + where + ContainerT: ?Sized, + KeyT: Debug, + ValueT: Debug, + { + elements: [KeyValueMatcher<'a, KeyT, ValueT>; N], + requirements: Requirements, + phantom: PhantomData<ContainerT>, + } + + impl<'a, ContainerT, KeyT: Debug, ValueT: Debug, const N: usize> + UnorderedElementsOfMapAreMatcher<'a, ContainerT, KeyT, ValueT, N> + { + pub fn new( + elements: [KeyValueMatcher<'a, KeyT, ValueT>; N], + requirements: Requirements, + ) -> Self { + Self { elements, requirements, phantom: Default::default() } + } + } + + impl<'a, KeyT: Debug, ValueT: Debug, ContainerT: Debug + ?Sized, const N: usize> Matcher + for UnorderedElementsOfMapAreMatcher<'a, ContainerT, KeyT, ValueT, N> + where + for<'b> &'b ContainerT: IntoIterator<Item = (&'b KeyT, &'b ValueT)>, + { + type ActualT = ContainerT; + + fn matches(&self, actual: &ContainerT) -> MatcherResult { + let match_matrix = MatchMatrix::generate_for_map(actual, &self.elements); + match_matrix.is_match_for(self.requirements).into() + } + + fn explain_match(&self, actual: &ContainerT) -> String { + if let Some(size_mismatch_explanation) = + self.requirements.explain_size_mismatch(actual, N) + { + return size_mismatch_explanation; + } + + let match_matrix = MatchMatrix::generate_for_map(actual, &self.elements); + if let Some(unmatchable_explanation) = + match_matrix.explain_unmatchable(self.requirements) + { + return unmatchable_explanation; + } + + let best_match = match_matrix.find_best_match(); + + best_match + .get_explanation_for_map(actual, &self.elements, self.requirements) + .unwrap_or("whose elements all match".to_string()) + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + format!( + "{} elements matching in any order:\n{}", + if matcher_result.into() { "contains" } else { "doesn't contain" }, + self.elements + .iter() + .map(|(key_matcher, value_matcher)| format!( + "{} => {}", + key_matcher.describe(MatcherResult::Match), + value_matcher.describe(MatcherResult::Match) + )) + .collect::<Description>() + .indent() + ) + } + } + + /// The requirements of the mapping between matchers and actual values by + /// which [`UnorderedElemetnsAre`] is deemed to match its input. + /// + /// **For internal use only. API stablility is not guaranteed!** + #[doc(hidden)] + #[derive(Clone, Copy)] + pub enum Requirements { + /// There must be a 1:1 correspondence between the actual values and the + /// matchers. + PerfectMatch, + + /// The mapping from matched actual values to their corresponding + /// matchers must be surjective. + Superset, + + /// The mapping from matchers to matched actual values must be + /// surjective. + Subset, + } + + impl Requirements { + fn explain_size_mismatch<ContainerT: ?Sized>( + &self, + actual: &ContainerT, + expected_size: usize, + ) -> Option<String> + 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::Superset if actual_size < expected_size => Some(format!( + "which has size {} (expected at least {})", + actual_size, expected_size + )), + + Requirements::Subset if actual_size > expected_size => Some(format!( + "which has size {} (expected at most {})", + actual_size, expected_size + )), + + _ => None, + } + } + } + + impl Display for Requirements { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Requirements::PerfectMatch => { + write!(f, "perfect") + } + Requirements::Superset => { + write!(f, "superset") + } + Requirements::Subset => { + write!(f, "subset") + } + } + } + } + + /// The bipartite matching graph between actual and expected elements. + struct MatchMatrix<const N: usize>(Vec<[MatcherResult; N]>); + + impl<const N: usize> MatchMatrix<N> { + fn generate<'a, T: Debug + 'a, ContainerT: Debug + ?Sized>( + actual: &ContainerT, + expected: &[Box<dyn Matcher<ActualT = T> + 'a>; N], + ) -> Self + where + for<'b> &'b ContainerT: IntoIterator<Item = &'b T>, + { + let mut matrix = MatchMatrix(vec![[MatcherResult::NoMatch; N]; count_elements(actual)]); + for (actual_idx, actual) in actual.into_iter().enumerate() { + for (expected_idx, expected) in expected.iter().enumerate() { + matrix.0[actual_idx][expected_idx] = expected.matches(actual); + } + } + matrix + } + + fn generate_for_map<'a, KeyT: Debug, ValueT: Debug, ContainerT: Debug + ?Sized>( + actual: &ContainerT, + expected: &[KeyValueMatcher<'a, KeyT, ValueT>; N], + ) -> Self + where + for<'b> &'b ContainerT: IntoIterator<Item = (&'b KeyT, &'b ValueT)>, + { + let mut matrix = MatchMatrix(vec![[MatcherResult::NoMatch; N]; count_elements(actual)]); + for (actual_idx, (actual_key, actual_value)) in actual.into_iter().enumerate() { + for (expected_idx, (expected_key, expected_value)) in expected.iter().enumerate() { + matrix.0[actual_idx][expected_idx] = (expected_key.matches(actual_key).into() + && expected_value.matches(actual_value).into()) + .into(); + } + } + matrix + } + + fn is_match_for(&self, requirements: Requirements) -> bool { + match requirements { + Requirements::PerfectMatch => { + !self.find_unmatchable_elements().has_unmatchable_elements() + && self.find_best_match().is_full_match() + } + Requirements::Superset => { + !self.find_unmatched_expected().has_unmatchable_elements() + && self.find_best_match().is_superset_match() + } + Requirements::Subset => { + !self.find_unmatched_actual().has_unmatchable_elements() + && self.find_best_match().is_subset_match() + } + } + } + + fn explain_unmatchable(&self, requirements: Requirements) -> Option<String> { + let unmatchable_elements = match requirements { + Requirements::PerfectMatch => self.find_unmatchable_elements(), + Requirements::Superset => self.find_unmatched_expected(), + Requirements::Subset => self.find_unmatched_actual(), + }; + unmatchable_elements.get_explanation() + } + + // Verifies that each actual matches at least one expected and that + // each expected matches at least one actual. + // This is a necessary condition but not sufficient. But it is faster + // than `find_best_match()`. + fn find_unmatchable_elements(&self) -> UnmatchableElements<N> { + let unmatchable_actual = + self.0.iter().map(|row| row.iter().all(|&e| e.is_no_match())).collect(); + let mut unmatchable_expected = [false; N]; + for (col_idx, expected) in unmatchable_expected.iter_mut().enumerate() { + *expected = self.0.iter().map(|row| row[col_idx]).all(|e| e.is_no_match()); + } + UnmatchableElements { unmatchable_actual, unmatchable_expected } + } + + fn find_unmatched_expected(&self) -> UnmatchableElements<N> { + let mut unmatchable_expected = [false; N]; + for (col_idx, expected) in unmatchable_expected.iter_mut().enumerate() { + *expected = self.0.iter().map(|row| row[col_idx]).all(|e| e.is_no_match()); + } + UnmatchableElements { unmatchable_actual: vec![false; N], unmatchable_expected } + } + + fn find_unmatched_actual(&self) -> UnmatchableElements<N> { + let unmatchable_actual = + self.0.iter().map(|row| row.iter().all(|e| e.is_no_match())).collect(); + UnmatchableElements { unmatchable_actual, unmatchable_expected: [false; N] } + } + + // Verifies that a full match exists. + // + // Uses the well-known Ford-Fulkerson max flow method to find a maximum + // bipartite matching. Flow is considered to be from actual to expected. + // There is an implicit source node that is connected to all of the actual + // nodes, and an implicit sink node that is connected to all of the + // expected nodes. All edges have unit capacity. + // + // Neither the flow graph nor the residual flow graph are represented + // explicitly. Instead, they are implied by the information in `self.0` and + // the local `actual_match : [Option<usize>; N]` whose elements are initialized + // to `None`. This represents the initial state of the algorithm, + // where the flow graph is empty, and the residual flow graph has the + // following edges: + // - An edge from source to each actual element node + // - An edge from each expected element node to sink + // - An edge from each actual element node to each expected element node, if + // the actual element matches the expected element, i.e. + // `matches!(self.0[actual_id][expected_id], Matches)` + // + // When the `try_augment(...)` method adds a flow, it sets `actual_match[l] = + // Some(r)` for some nodes l and r. This induces the following changes: + // - The edges (source, l), (l, r), and (r, sink) are added to the flow graph. + // - The same three edges are removed from the residual flow graph. + // - The reverse edges (l, source), (r, l), and (sink, r) are added to the + // residual flow graph, which is a directional graph representing unused + // flow capacity. + // + // When the method augments a flow (changing `actual_match[l]` from `Some(r1)` + // to `Some(r2)`), this can be thought of as "undoing" the above steps + // with respect to r1 and "redoing" them with respect to r2. + // + // It bears repeating that the flow graph and residual flow graph are + // never represented explicitly, but can be derived by looking at the + // information in 'self.0' and in `actual_match`. + // + // As an optimization, there is a second local `expected_match: [Option<usize>; + // N]` which does not provide any new information. Instead, it enables + // more efficient queries about edges entering or leaving the expected elements + // nodes of the flow or residual flow graphs. The following invariants + // are maintained: + // + // actual_match[a] == None or expected_match[actual_match[a].unwrap()] == + // Some(a) + // expected_match[r] == None or actual_match[expected_match[e].unwrap()] == + // Some(e) + // + // | [ source ] | + // | ||| | + // | ||| | + // | ||\-> actual_match[0]=Some(1) -\ expected_match[0]=None ---\ | + // | || | | | + // | |\--> actual_match[1]=None \-> expected_match[1]=Some(0) --\| | + // | | || | + // | \---> actual_match[2]=Some(2) --> expected_match[2]=Some(2) -\|| | + // | ||| | + // | elements matchers vvv | + // | [ sink ] | + // + // See Also: + // [1] Cormen, et al (2001). "Section 26.2: The Ford-Fulkerson method". + // "Introduction to Algorithms (Second ed.)", pp. 651-664. + // [2] "Ford-Fulkerson algorithm", Wikipedia, + // 'http://en.wikipedia.org/wiki/Ford%E2%80%93Fulkerson_algorithm' + fn find_best_match(&self) -> BestMatch<N> { + let mut actual_match = vec![None; self.0.len()]; + let mut expected_match: [Option<usize>; N] = [None; N]; + // Searches the residual flow graph for a path from each actual node to + // the sink in the residual flow graph, and if one is found, add this path + // to the graph. + // It's okay to search through the actual nodes once. The + // edge from the implicit source node to each previously-visited actual + // node will have flow if that actual node has any path to the sink + // whatsoever. Subsequent augmentations can only add flow to the + // network, and cannot take away that previous flow unit from the source. + // Since the source-to-actual edge can only carry one flow unit (or, + // each actual element can be matched to only one expected element), there is no + // need to visit the actual nodes more than once looking for + // augmented paths. The flow is known to be possible or impossible + // by looking at the node once. + for actual_idx in 0..self.0.len() { + assert!(actual_match[actual_idx].is_none()); + let mut seen = [false; N]; + self.try_augment(actual_idx, &mut seen, &mut actual_match, &mut expected_match); + } + BestMatch(actual_match) + } + + // Perform a depth-first search from actual node `actual_idx` to the sink by + // searching for an unassigned expected node. If a path is found, flow + // is added to the network by linking the actual and expected vector elements + // corresponding each segment of the path. Returns true if a path to + // sink was found, which means that a unit of flow was added to the + // network. The 'seen' array elements correspond to expected nodes and are + // marked to eliminate cycles from the search. + // + // Actual nodes will only be explored at most once because they + // are accessible from at most one expected node in the residual flow + // graph. + // + // Note that `actual_match[actual_idx]` is the only element of `actual_match` + // that `try_augment(...)` will potentially transition from `None` to + // `Some(...)`. Any other `actual_match` element holding `None` before + // `try_augment(...)` will be holding it when `try_augment(...)` + // returns. + // + fn try_augment( + &self, + actual_idx: usize, + seen: &mut [bool; N], + actual_match: &mut [Option<usize>], + expected_match: &mut [Option<usize>; N], + ) -> bool { + for expected_idx in 0..N { + if seen[expected_idx] { + continue; + } + if self.0[actual_idx][expected_idx].is_no_match() { + continue; + } + // There is an edge between `actual_idx` and `expected_idx`. + seen[expected_idx] = true; + // Next a search is performed to determine whether + // this edge is a dead end or leads to the sink. + // + // `expected_match[expected_idx].is_none()` means that there is residual flow + // from expected node at index expected_idx to the sink, so we + // can use that to finish this flow path and return success. + // + // Otherwise, we look for a residual flow starting from + // `expected_match[expected_idx].unwrap()` by calling + // ourselves recursively to see if this ultimately leads to + // sink. + if expected_match[expected_idx].is_none() + || self.try_augment( + expected_match[expected_idx].unwrap(), + seen, + actual_match, + expected_match, + ) + { + // We found a residual flow from source to sink. We thus need to add the new + // edge to the current flow. + // Note: this also remove the potential flow that existed by overwriting the + // value in the `expected_match` and `actual_match`. + expected_match[expected_idx] = Some(actual_idx); + actual_match[actual_idx] = Some(expected_idx); + return true; + } + } + false + } + } + + /// The list of elements that do not match any element in the corresponding + /// set. + /// These lists are represented as fixed sized bit set to avoid + /// allocation. + /// TODO(bjacotg) Use BitArr!(for N) once generic_const_exprs is stable. + struct UnmatchableElements<const N: usize> { + unmatchable_actual: Vec<bool>, + unmatchable_expected: [bool; N], + } + + impl<const N: usize> UnmatchableElements<N> { + fn has_unmatchable_elements(&self) -> bool { + self.unmatchable_actual.iter().any(|b| *b) + || self.unmatchable_expected.iter().any(|b| *b) + } + + fn get_explanation(&self) -> Option<String> { + let unmatchable_actual = self.unmatchable_actual(); + let actual_idx = unmatchable_actual + .iter() + .map(|idx| format!("#{}", idx)) + .collect::<Vec<_>>() + .join(", "); + let unmatchable_expected = self.unmatchable_expected(); + let expected_idx = unmatchable_expected + .iter() + .map(|idx| format!("#{}", idx)) + .collect::<Vec<_>>() + .join(", "); + match (unmatchable_actual.len(), unmatchable_expected.len()) { + (0, 0) => None, + (1, 0) => { + Some(format!("whose element {actual_idx} does not match any expected elements")) + } + (_, 0) => { + Some(format!("whose elements {actual_idx} do not match any expected elements",)) + } + (0, 1) => Some(format!( + "which has no element matching the expected element {expected_idx}" + )), + (0, _) => Some(format!( + "which has no elements matching the expected elements {expected_idx}" + )), + (1, 1) => Some(format!( + "whose element {actual_idx} does not match any expected elements and no elements match the expected element {expected_idx}" + )), + (_, 1) => Some(format!( + "whose elements {actual_idx} do not match any expected elements and no elements match the expected element {expected_idx}" + )), + (1, _) => Some(format!( + "whose element {actual_idx} does not match any expected elements and no elements match the expected elements {expected_idx}" + )), + (_, _) => Some(format!( + "whose elements {actual_idx} do not match any expected elements and no elements match the expected elements {expected_idx}" + )), + } + } + + fn unmatchable_actual(&self) -> Vec<usize> { + self.unmatchable_actual + .iter() + .enumerate() + .filter_map(|(idx, b)| if *b { Some(idx) } else { None }) + .collect() + } + + fn unmatchable_expected(&self) -> Vec<usize> { + self.unmatchable_expected + .iter() + .enumerate() + .filter_map(|(idx, b)| if *b { Some(idx) } else { None }) + .collect() + } + } + + /// The representation of a match between actual and expected. + /// The value at idx represents to which expected the actual at idx is + /// matched with. For example, `BestMatch([Some(0), None, Some(1)])` + /// means: + /// * The 0th element in actual matches the 0th element in expected. + /// * The 1st element in actual does not match. + /// * The 2nd element in actual matches the 1st element in expected. + struct BestMatch<const N: usize>(Vec<Option<usize>>); + + impl<const N: usize> BestMatch<N> { + fn is_full_match(&self) -> bool { + self.0.iter().all(|o| o.is_some()) + } + + fn is_subset_match(&self) -> bool { + self.is_full_match() + } + + fn is_superset_match(&self) -> bool { + self.get_unmatched_expected().is_empty() + } + + fn get_matches(&self) -> impl Iterator<Item = (usize, usize)> + '_ { + self.0.iter().enumerate().filter_map(|(actual_idx, maybe_expected_idx)| { + maybe_expected_idx.map(|expected_idx| (actual_idx, expected_idx)) + }) + } + + fn get_unmatched_actual(&self) -> impl Iterator<Item = usize> + '_ { + self.0 + .iter() + .enumerate() + .filter(|&(_, o)| o.is_none()) + .map(|(actual_idx, _)| actual_idx) + } + + fn get_unmatched_expected(&self) -> Vec<usize> { + let matched_expected: HashSet<_> = self.0.iter().flatten().collect(); + (0..N).filter(|expected_idx| !matched_expected.contains(expected_idx)).collect() + } + + fn get_explanation<'a, T: Debug, ContainerT: Debug + ?Sized>( + &self, + actual: &ContainerT, + expected: &[Box<dyn Matcher<ActualT = T> + 'a>; N], + requirements: Requirements, + ) -> Option<String> + where + for<'b> &'b ContainerT: IntoIterator<Item = &'b T>, + { + let actual: Vec<_> = actual.into_iter().collect(); + if self.is_full_match() { + return None; + } + let mut error_message = + format!("which does not have a {requirements} match with the expected elements."); + + error_message.push_str("\n The best match found was: "); + + let matches = self.get_matches().map(|(actual_idx, expected_idx)|{ + format!( + "Actual element {:?} at index {actual_idx} matched expected element `{}` at index {expected_idx}.", + actual[actual_idx], + expected[expected_idx].describe(MatcherResult::Match), + )}); + + let unmatched_actual = self.get_unmatched_actual().map(|actual_idx| { + format!( + "Actual element {:#?} at index {actual_idx} did not match any remaining expected element.", + actual[actual_idx] + ) + }); + + let unmatched_expected = self.get_unmatched_expected().into_iter().map(|expected_idx|{format!( + "Expected element `{}` at index {expected_idx} did not match any remaining actual element.", + expected[expected_idx].describe(MatcherResult::Match) + )}); + + let best_match = matches + .chain(unmatched_actual) + .chain(unmatched_expected) + .collect::<Description>() + .indent(); + Some(format!( + "which does not have a {requirements} match with the expected elements. The best match found was:\n{best_match}" + )) + } + + fn get_explanation_for_map<'a, KeyT: Debug, ValueT: Debug, ContainerT: Debug + ?Sized>( + &self, + actual: &ContainerT, + expected: &[KeyValueMatcher<'a, KeyT, ValueT>; N], + requirements: Requirements, + ) -> Option<String> + where + for<'b> &'b ContainerT: IntoIterator<Item = (&'b KeyT, &'b ValueT)>, + { + let actual: Vec<_> = actual.into_iter().collect(); + if self.is_full_match() { + return None; + } + let mut error_message = + format!("which does not have a {requirements} match with the expected elements."); + + error_message.push_str("\n The best match found was: "); + + let matches = self.get_matches() + .map(|(actual_idx, expected_idx)| { + format!( + "Actual element {:?} => {:?} at index {actual_idx} matched expected element `{}` => `{}` at index {expected_idx}.", + actual[actual_idx].0, + actual[actual_idx].1, + expected[expected_idx].0.describe(MatcherResult::Match), + expected[expected_idx].1.describe(MatcherResult::Match), + ) + }); + + let unmatched_actual = self.get_unmatched_actual() + .map(|actual_idx| { + format!( + "Actual element {:#?} => {:#?} at index {actual_idx} did not match any remaining expected element.", + actual[actual_idx].0, + actual[actual_idx].1, + ) + }); + + let unmatched_expected = self.get_unmatched_expected() + .into_iter() + .map(|expected_idx| { + format!( + "Expected element `{}` => `{}` at index {expected_idx} did not match any remaining actual element.", + expected[expected_idx].0.describe(MatcherResult::Match), + expected[expected_idx].1.describe(MatcherResult::Match), + ) + }); + + let best_match = matches + .chain(unmatched_actual) + .chain(unmatched_expected) + .collect::<Description>() + .indent(); + Some(format!( + "which does not have a {requirements} match with the expected elements. The best match found was:\n{best_match}" + )) + } + } +} + +#[cfg(test)] +mod tests { + use super::internal::UnorderedElementsOfMapAreMatcher; + use crate::matcher::{Matcher, MatcherResult}; + use crate::prelude::*; + use indoc::indoc; + use std::collections::HashMap; + + #[test] + fn has_correct_description_for_map() -> Result<()> { + // UnorderedElementsAreMatcher maintains references to the matchers, so the + // constituent matchers must live longer. Inside a verify_that! macro, the + // compiler takes care of that, but when the matcher is created separately, + // we must create the constitute matchers separately so that they + // aren't dropped too early. + let matchers = ((eq(2), eq("Two")), (eq(1), eq("One")), (eq(3), eq("Three"))); + let matcher: UnorderedElementsOfMapAreMatcher<HashMap<i32, &str>, _, _, 3> = unordered_elements_are![ + (matchers.0.0, matchers.0.1), + (matchers.1.0, matchers.1.1), + (matchers.2.0, matchers.2.1) + ]; + verify_that!( + Matcher::describe(&matcher, MatcherResult::Match), + 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\"" + )) + ) + } + + #[test] + fn unordered_elements_are_description_no_full_match_with_map() -> Result<()> { + // UnorderedElementsAreMatcher maintains references to the matchers, so the + // constituent matchers must live longer. Inside a verify_that! macro, the + // compiler takes care of that, but when the matcher is created separately, + // we must create the constitute matchers separately so that they + // aren't dropped too early. + let matchers = ((anything(), eq(1)), (anything(), eq(2)), (anything(), eq(2))); + let matcher: UnorderedElementsOfMapAreMatcher<HashMap<u32, u32>, _, _, 3> = unordered_elements_are![ + (matchers.0.0, matchers.0.1), + (matchers.1.0, matchers.1.1), + (matchers.2.0, matchers.2.1), + ]; + let value: HashMap<u32, u32> = HashMap::from_iter([(0, 1), (1, 1), (2, 2)]); + verify_that!( + matcher.explain_match(&value), + displays_as(contains_regex( + "Actual element 2 => 2 at index [0-2] matched expected element `is anything` => `is equal to 2` at index [0-2]." + )).and(displays_as(contains_regex( + "Actual element [0-1] => [0-1] at index [0-2] did not match any remaining expected element." + ))).and(displays_as(contains_substring( + "Expected element `is anything` => `is equal to 2` at index 2 did not match any remaining actual element." + ))) + ) + } +} diff --git a/tests/all_matcher_test.rs b/tests/all_matcher_test.rs new file mode 100644 index 0000000..6d7e3f8 --- /dev/null +++ b/tests/all_matcher_test.rs @@ -0,0 +1,93 @@ +// 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 googletest::matcher::Matcher; +use googletest::prelude::*; +use indoc::indoc; + +#[test] +fn matches_any_value_when_list_is_empty() -> Result<()> { + verify_that!((), all!()) +} + +#[test] +fn matches_value_with_single_matching_component() -> Result<()> { + verify_that!(123, all!(eq(123))) +} + +#[test] +fn does_not_match_value_with_single_non_matching_component() -> Result<()> { + verify_that!(123, not(all!(eq(456)))) +} + +#[test] +fn matches_value_with_two_matching_components() -> Result<()> { + verify_that!("A string", all!(starts_with("A"), ends_with("string"))) +} + +#[test] +fn does_not_match_value_with_one_non_matching_component_among_two_components() -> Result<()> { + verify_that!(123, not(all!(eq(123), eq(456)))) +} + +#[test] +fn supports_trailing_comma() -> Result<()> { + verify_that!( + "An important string", + all!(starts_with("An"), contains_substring("important"), ends_with("string"),) + ) +} + +#[test] +fn admits_matchers_without_static_lifetime() -> Result<()> { + #[derive(Debug, PartialEq)] + struct AStruct(i32); + let expected_value = AStruct(123); + verify_that!(AStruct(123), all![eq_deref_of(&expected_value)]) +} + +#[test] +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\"" + )) + ) +} + +#[test] +fn mismatch_description_empty_matcher() -> Result<()> { + verify_that!(all!().explain_match("Three"), displays_as(eq("which is anything"))) +} + +#[test] +fn all_multiple_failed_assertions() -> Result<()> { + let result = verify_that!(4, all![eq(1), eq(2), eq(3)]); + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + Value of: 4 + Expected: has all the following properties: + * is equal to 1 + * is equal to 2 + * is equal to 3 + Actual: 4, + * which isn't equal to 1 + * which isn't equal to 2 + * which isn't equal to 3" + )))) + ) +} diff --git a/tests/any_matcher_test.rs b/tests/any_matcher_test.rs new file mode 100644 index 0000000..1bdd794 --- /dev/null +++ b/tests/any_matcher_test.rs @@ -0,0 +1,93 @@ +// 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 googletest::matcher::Matcher; +use googletest::prelude::*; +use indoc::indoc; + +#[test] +fn does_not_match_value_when_list_is_empty() -> Result<()> { + verify_that!((), not(any!())) +} + +#[test] +fn matches_value_with_single_matching_component() -> Result<()> { + verify_that!(123, any!(eq(123))) +} + +#[test] +fn does_not_match_value_with_single_non_matching_component() -> Result<()> { + verify_that!(123, not(any!(eq(456)))) +} + +#[test] +fn matches_value_with_first_of_two_matching_components() -> Result<()> { + verify_that!("A string", any!(starts_with("A"), starts_with("string"))) +} + +#[test] +fn matches_value_with_second_of_two_matching_components() -> Result<()> { + verify_that!("A string", any!(starts_with("string"), starts_with("A"))) +} + +#[test] +fn supports_trailing_comma() -> Result<()> { + verify_that!( + "An important string", + any!(starts_with("An"), contains_substring("important"), ends_with("string"),) + ) +} + +#[test] +fn admits_matchers_without_static_lifetime() -> Result<()> { + #[derive(Debug, PartialEq)] + struct AStruct(i32); + let expected_value = AStruct(123); + verify_that!(AStruct(123), any![eq_deref_of(&expected_value)]) +} + +#[test] +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\"" + )) + ) +} + +#[test] +fn mismatch_description_empty_matcher() -> Result<()> { + verify_that!(any!().explain_match("Three"), displays_as(eq("which never matches"))) +} + +#[test] +fn all_multiple_failed_assertions() -> Result<()> { + let result = verify_that!(4, any![eq(1), eq(2), eq(3)]); + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + Value of: 4 + Expected: has at least one of the following properties: + * is equal to 1 + * is equal to 2 + * is equal to 3 + Actual: 4, + * which isn't equal to 1 + * which isn't equal to 2 + * which isn't equal to 3" + )))) + ) +} diff --git a/tests/colorized_diff_test.rs b/tests/colorized_diff_test.rs new file mode 100644 index 0000000..d1b4ecb --- /dev/null +++ b/tests/colorized_diff_test.rs @@ -0,0 +1,52 @@ +// 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 googletest::prelude::*; +use indoc::indoc; +use std::fmt::{Display, Write}; + +// Make a long text with each element of the iterator on one line. +// `collection` must contains at least one element. +fn build_text<T: Display>(mut collection: impl Iterator<Item = T>) -> String { + let mut text = String::new(); + write!(&mut text, "{}", collection.next().expect("Provided collection without elements")) + .unwrap(); + for item in collection { + write!(&mut text, "\n{}", item).unwrap(); + } + text +} + +#[test] +fn colors_appear_when_no_color_is_no_set_and_force_color_is_set() -> Result<()> { + std::env::remove_var("NO_COLOR"); + std::env::set_var("FORCE_COLOR", "1"); + + let result = verify_that!(build_text(1..50), eq(build_text(1..51))); + + verify_that!( + result, + err(displays_as(contains_substring(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" + }))) + ) +} diff --git a/tests/composition_test.rs b/tests/composition_test.rs new file mode 100644 index 0000000..7ae146f --- /dev/null +++ b/tests/composition_test.rs @@ -0,0 +1,71 @@ +// 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 googletest::prelude::*; + +#[test] +fn all_matcher_works_as_inner_matcher() -> Result<()> { + let value = vec![1]; + verify_that!(value, contains_each![all!(gt(0), lt(2))]) +} + +#[test] +fn matches_pattern_works_as_inner_matcher() -> Result<()> { + #[derive(Debug)] + struct AStruct(i32); + verify_that!(vec![AStruct(123)], contains_each![matches_pattern!(AStruct(eq(123)))]) +} + +#[test] +fn matches_pattern_works_with_property_as_inner_matcher() -> Result<()> { + #[derive(Debug)] + struct AStruct(i32); + impl AStruct { + fn get_value(&self) -> i32 { + self.0 + } + } + verify_that!( + vec![AStruct(123)], + contains_each![matches_pattern!(AStruct { + get_value(): eq(123) + })] + ) +} + +#[test] +fn contains_each_works_as_inner_matcher() -> Result<()> { + #[derive(Debug)] + struct AStruct(Vec<i32>); + verify_that!(AStruct(vec![123]), matches_pattern!(AStruct(contains_each![eq(123)]))) +} + +#[test] +fn pointwise_works_as_inner_matcher() -> Result<()> { + #[derive(Debug)] + struct AStruct(Vec<i32>); + verify_that!(AStruct(vec![123]), matches_pattern!(AStruct(pointwise!(eq, [123])))) +} + +#[test] +fn elements_are_works_as_inner_matcher() -> Result<()> { + #[derive(Debug)] + struct AStruct(Vec<i32>); + verify_that!(AStruct(vec![123]), matches_pattern!(AStruct(elements_are![eq(123)]))) +} + +#[test] +fn tuple_works_as_inner_matcher() -> Result<()> { + verify_that!(vec![(123,)], elements_are![(eq(123),)]) +} diff --git a/tests/elements_are_matcher_test.rs b/tests/elements_are_matcher_test.rs new file mode 100644 index 0000000..99e9fe7 --- /dev/null +++ b/tests/elements_are_matcher_test.rs @@ -0,0 +1,125 @@ +// 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 googletest::matcher::Matcher; +use googletest::prelude::*; +use indoc::indoc; + +#[test] +fn elements_are_matches_vector() -> Result<()> { + let value = vec![1, 2, 3]; + verify_that!(value, elements_are![eq(1), eq(2), eq(3)]) +} + +#[test] +fn elements_are_matches_slice() -> Result<()> { + let value = vec![1, 2, 3]; + let slice = value.as_slice(); + verify_that!(*slice, elements_are![eq(1), eq(2), eq(3)]) +} + +#[test] +fn elements_are_matches_array() -> Result<()> { + verify_that!([1, 2, 3], elements_are![eq(1), eq(2), eq(3)]) +} + +#[test] +fn elements_are_supports_trailing_comma() -> Result<()> { + let value = vec![1, 2, 3]; + verify_that!(value, elements_are![eq(1), eq(2), eq(3),]) +} + +#[test] +fn elements_are_returns_no_match_when_expected_and_actual_sizes_differ() -> Result<()> { + let value = vec![1, 2]; + verify_that!(value, not(elements_are![eq(1), eq(2), eq(3)])) +} + +#[test] +fn elements_are_admits_matchers_without_static_lifetime() -> Result<()> { + #[derive(Debug, PartialEq)] + struct AStruct(i32); + let expected_value = AStruct(123); + verify_that!(vec![AStruct(123)], elements_are![eq_deref_of(&expected_value)]) +} + +#[test] +fn elements_are_produces_correct_failure_message() -> Result<()> { + let result = verify_that!(vec![1, 4, 3], elements_are![eq(1), eq(2), eq(3)]); + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + Value of: vec![1, 4, 3] + Expected: has elements: + 0. is equal to 1 + 1. is equal to 2 + 2. is equal to 3 + Actual: [1, 4, 3], + where element #1 is 4, which isn't equal to 2" + )))) + ) +} + +#[test] +fn elements_are_produces_correct_failure_message_nested() -> Result<()> { + let result = verify_that!( + vec![vec![0, 1], vec![1, 2]], + elements_are![elements_are![eq(1), eq(2)], elements_are![eq(2), eq(3)]] + ); + verify_that!( + 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 + 1. is equal to 2 + 1. has elements: + 0. is equal to 2 + 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" + )))) + ) +} + +#[test] +fn elements_are_explain_match_wrong_size() -> Result<()> { + verify_that!( + elements_are![eq(1)].explain_match(&vec![1, 2]), + displays_as(eq("whose size is 2")) + ) +} + +fn create_matcher() -> impl Matcher<ActualT = Vec<i32>> { + elements_are![eq(1)] +} + +#[test] +fn elements_are_works_when_matcher_is_created_in_subroutine() -> Result<()> { + verify_that!(vec![1], create_matcher()) +} + +#[test] +fn elements_are_implicitly_called() -> Result<()> { + verify_that!(vec![1, 2, 3], [eq(1), eq(2), eq(3)]) +} diff --git a/tests/field_matcher_test.rs b/tests/field_matcher_test.rs new file mode 100644 index 0000000..f5d85c5 --- /dev/null +++ b/tests/field_matcher_test.rs @@ -0,0 +1,178 @@ +// 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 googletest::matcher::{Matcher, MatcherResult}; +use googletest::prelude::*; + +#[derive(Debug)] +struct IntField { + int: i32, +} + +#[test] +fn field_matches_integer_field() -> Result<()> { + verify_that!(IntField { int: 32 }, field!(IntField.int, eq(32))) +} + +#[derive(Debug)] +struct StringField { + strink: String, +} + +#[test] +fn field_matches_string_field() -> Result<()> { + verify_that!(StringField { strink: "yes".to_string() }, field!(StringField.strink, eq("yes"))) +} + +#[test] +fn field_error_message_shows_field_name_and_inner_matcher() -> Result<()> { + let matcher = field!(IntField.int, eq(31)); + + verify_that!( + matcher.describe(MatcherResult::Match), + eq("has field `int`, which is equal to 31") + ) +} + +mod sub { + #[derive(Debug)] + pub struct SubStruct { + pub field: i32, + } +} + +#[test] +fn struct_in_other_module_matches() -> Result<()> { + verify_that!(sub::SubStruct { field: 32 }, field!(sub::SubStruct.field, eq(32))) +} + +#[derive(Debug)] +struct Tuple(i32, String); + +#[test] +fn tuple_matches_with_index() -> Result<()> { + verify_that!(Tuple(32, "yes".to_string()), field!(Tuple.0, eq(32))) +} + +#[test] +fn matches_enum_value() -> Result<()> { + #[derive(Debug)] + enum AnEnum { + AValue(u32), + } + let value = AnEnum::AValue(123); + + verify_that!(value, field!(AnEnum::AValue.0, eq(123))) +} + +#[test] +fn shows_correct_failure_message_for_wrong_struct_entry() -> Result<()> { + #[derive(Debug)] + struct AStruct { + a: Vec<u32>, + } + let value = AStruct { a: vec![1] }; + + let result = verify_that!(value, field!(AStruct.a, container_eq([]))); + + verify_that!( + result, + err(displays_as(contains_substring( + "which has field `a`, which contains the unexpected element 1" + ))) + ) +} + +#[test] +fn does_not_match_enum_value_with_wrong_enum_variant() -> Result<()> { + #[derive(Debug)] + enum AnEnum { + #[allow(dead_code)] // This variant is intentionally unused. + AValue(u32), + AnotherValue, + } + let value = AnEnum::AnotherValue; + + verify_that!(value, not(field!(AnEnum::AValue.0, eq(123)))) +} + +#[test] +fn shows_correct_failure_message_for_wrong_enum_value() -> Result<()> { + #[derive(Debug)] + enum AnEnum { + #[allow(dead_code)] // This variant is intentionally unused. + AValue { + a: u32, + }, + AnotherValue, + } + let value = AnEnum::AnotherValue; + + let result = verify_that!(value, field!(AnEnum::AValue.a, eq(123))); + + verify_that!( + result, + err(displays_as(contains_substring("which has the wrong enum variant `AnotherValue`"))) + ) +} + +#[test] +fn shows_correct_failure_message_for_wrong_enum_value_with_tuple_field() -> Result<()> { + #[derive(Debug)] + enum AnEnum { + #[allow(dead_code)] // This variant is intentionally unused. + AValue(u32), + AnotherValue(u32), + } + let value = AnEnum::AnotherValue(123); + + let result = verify_that!(value, field!(AnEnum::AValue.0, eq(123))); + + verify_that!( + result, + err(displays_as(contains_substring("which has the wrong enum variant `AnotherValue`"))) + ) +} + +#[test] +fn shows_correct_failure_message_for_wrong_enum_value_with_named_field() -> Result<()> { + #[derive(Debug)] + enum AnEnum { + #[allow(dead_code)] // This variant is intentionally unused. + AValue(u32), + AnotherValue { + #[allow(unused)] + a: u32, + }, + } + let value = AnEnum::AnotherValue { a: 123 }; + + let result = verify_that!(value, field!(AnEnum::AValue.0, eq(123))); + + verify_that!( + result, + err(displays_as(contains_substring("which has the wrong enum variant `AnotherValue`"))) + ) +} + +#[test] +fn matches_struct_like_enum_value() -> Result<()> { + #[derive(Debug)] + enum AnEnum { + AValue { a_field: u32 }, + } + let value = AnEnum::AValue { a_field: 123 }; + + verify_that!(value, field!(AnEnum::AValue.a_field, eq(123))) +} diff --git a/tests/lib.rs b/tests/lib.rs new file mode 100644 index 0000000..fb243ca --- /dev/null +++ b/tests/lib.rs @@ -0,0 +1,27 @@ +// 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. + +mod all_matcher_test; +mod any_matcher_test; +mod colorized_diff_test; +mod composition_test; +mod elements_are_matcher_test; +mod field_matcher_test; +mod matches_pattern_test; +mod pointwise_matcher_test; +mod property_matcher_test; +#[cfg(feature = "proptest")] +mod proptest_integration_test; +mod tuple_matcher_test; +mod unordered_elements_are_matcher_test; diff --git a/tests/matches_pattern_test.rs b/tests/matches_pattern_test.rs new file mode 100644 index 0000000..4904cf5 --- /dev/null +++ b/tests/matches_pattern_test.rs @@ -0,0 +1,1602 @@ +// 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 googletest::prelude::*; +use indoc::indoc; + +#[test] +fn matches_struct_containing_single_field() -> Result<()> { + #[derive(Debug)] + struct AStruct { + a_field: u32, + } + let actual = AStruct { a_field: 123 }; + + verify_that!(actual, matches_pattern!(AStruct { a_field: eq(123) })) +} + +#[test] +fn matches_struct_containing_two_fields() -> Result<()> { + #[derive(Debug)] + struct AStruct { + a_field: u32, + another_field: u32, + } + let actual = AStruct { a_field: 123, another_field: 234 }; + + verify_that!(actual, matches_pattern!(AStruct { a_field: eq(123), another_field: eq(234) })) +} + +#[test] +#[rustfmt::skip]// Otherwise fmt strips the trailing comma +fn supports_trailing_comma_with_one_field() -> Result<()> { + #[derive(Debug)] + struct AStruct { + a_field: u32, + } + let actual = AStruct { a_field: 123 }; + + verify_that!(actual, matches_pattern!(AStruct { + a_field: eq(123), // Block reformatting + })) +} + +#[test] +fn supports_trailing_comma_with_two_fields() -> Result<()> { + #[derive(Debug)] + struct AStruct { + a_field: u32, + another_field: u32, + } + let actual = AStruct { a_field: 123, another_field: 234 }; + + verify_that!( + actual, + matches_pattern!(AStruct { + a_field: eq(123), + another_field: eq(234), // Block reformatting + }) + ) +} + +#[test] +fn supports_trailing_comma_with_three_fields() -> Result<()> { + #[derive(Debug)] + struct AStruct { + a_field: u32, + another_field: u32, + a_third_field: u32, + } + let actual = AStruct { a_field: 123, another_field: 234, a_third_field: 345 }; + + verify_that!( + actual, + matches_pattern!(AStruct { + a_field: eq(123), + another_field: eq(234), + a_third_field: eq(345), + }) + ) +} + +#[test] +fn matches_struct_containing_nested_struct_with_field() -> Result<()> { + #[derive(Debug)] + struct AStruct { + a_nested_struct: ANestedStruct, + } + #[derive(Debug)] + struct ANestedStruct { + a_field: u32, + } + let actual = AStruct { a_nested_struct: ANestedStruct { a_field: 123 } }; + + verify_that!( + actual, + matches_pattern!(AStruct { a_nested_struct: pat!(ANestedStruct { a_field: eq(123) }) }) + ) +} + +#[test] +fn has_correct_assertion_failure_message_for_single_field() -> Result<()> { + #[derive(Debug)] + struct AStruct { + a_field: u32, + } + let actual = AStruct { a_field: 123 }; + let result = verify_that!(actual, matches_pattern!(AStruct { a_field: eq(234) })); + + verify_that!( + result, + err(displays_as(contains_substring(indoc! {" + Value of: actual + Expected: is AStruct which has field `a_field`, which is equal to 234 + Actual: AStruct { a_field: 123 }, + which has field `a_field`, which isn't equal to 234 + " + }))) + ) +} + +#[test] +fn has_correct_assertion_failure_message_for_two_fields() -> Result<()> { + #[derive(Debug)] + struct AStruct { + a_field: u32, + another_field: u32, + } + let actual = AStruct { a_field: 123, another_field: 234 }; + let result = verify_that!( + actual, + matches_pattern!(AStruct { a_field: eq(234), another_field: eq(123) }) + ); + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + Value of: actual + Expected: is AStruct which has all the following properties: + * has field `a_field`, which is equal to 234 + * has field `another_field`, which is equal to 123 + Actual: AStruct { a_field: 123, another_field: 234 }, + * which has field `a_field`, which isn't equal to 234 + * which has field `another_field`, which isn't equal to 123" + )))) + ) +} + +#[test] +fn has_correct_assertion_failure_message_for_field_and_property() -> Result<()> { + #[derive(Debug)] + struct AStruct { + a_field: u32, + another_field: u32, + } + impl AStruct { + fn get_field(&self) -> u32 { + self.a_field + } + } + let actual = AStruct { a_field: 123, another_field: 234 }; + let result = verify_that!( + actual, + matches_pattern!(AStruct { get_field(): eq(234), another_field: eq(123) }) + ); + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + Value of: actual + Expected: is AStruct which has all the following properties: + * has property `get_field ()`, which is equal to 234 + * has field `another_field`, which is equal to 123 + Actual: AStruct { a_field: 123, another_field: 234 }, + * whose property `get_field ()` is `123`, which isn't equal to 234 + * which has field `another_field`, which isn't equal to 123" + )))) + ) +} + +#[test] +fn has_meaningful_assertion_failure_message_when_wrong_enum_variant_is_used() -> Result<()> { + #[derive(Debug)] + enum AnEnum { + A(u32), + #[allow(unused)] + B(u32), + } + let actual = AnEnum::A(123); + let result = verify_that!(actual, matches_pattern!(AnEnum::B(eq(123)))); + + 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` + " + }))) + ) +} + +#[test] +fn supports_qualified_struct_names() -> Result<()> { + mod a_module { + #[derive(Debug)] + pub(super) struct AStruct { + pub(super) a_field: u32, + } + } + let actual = a_module::AStruct { a_field: 123 }; + + verify_that!(actual, matches_pattern!(a_module::AStruct { a_field: eq(123) })) +} + +#[test] +fn matches_tuple_struct_containing_single_field() -> Result<()> { + #[derive(Debug)] + struct AStruct(u32); + let actual = AStruct(123); + + verify_that!(actual, matches_pattern!(AStruct(eq(123)))) +} + +#[test] +fn matches_tuple_struct_containing_two_fields() -> Result<()> { + #[derive(Debug)] + struct AStruct(u32, u32); + let actual = AStruct(123, 234); + + verify_that!(actual, matches_pattern!(AStruct(eq(123), eq(234)))) +} + +#[test] +fn matches_tuple_struct_containing_three_fields() -> Result<()> { + #[derive(Debug)] + struct AStruct(u32, u32, u32); + let actual = AStruct(123, 234, 345); + + verify_that!(actual, matches_pattern!(AStruct(eq(123), eq(234), eq(345)))) +} + +#[test] +fn matches_tuple_struct_containing_four_fields() -> Result<()> { + #[derive(Debug)] + struct AStruct(u32, u32, u32, u32); + let actual = AStruct(123, 234, 345, 456); + + verify_that!(actual, matches_pattern!(AStruct(eq(123), eq(234), eq(345), eq(456)))) +} + +#[test] +fn matches_tuple_struct_containing_five_fields() -> Result<()> { + #[derive(Debug)] + struct AStruct(u32, u32, u32, u32, u32); + let actual = AStruct(123, 234, 345, 456, 567); + + verify_that!(actual, matches_pattern!(AStruct(eq(123), eq(234), eq(345), eq(456), eq(567)))) +} + +#[test] +fn matches_tuple_struct_containing_six_fields() -> Result<()> { + #[derive(Debug)] + struct AStruct(u32, u32, u32, u32, u32, u32); + let actual = AStruct(123, 234, 345, 456, 567, 678); + + verify_that!( + actual, + matches_pattern!(AStruct(eq(123), eq(234), eq(345), eq(456), eq(567), eq(678))) + ) +} + +#[test] +fn matches_tuple_struct_containing_seven_fields() -> Result<()> { + #[derive(Debug)] + struct AStruct(u32, u32, u32, u32, u32, u32, u32); + let actual = AStruct(123, 234, 345, 456, 567, 678, 789); + + verify_that!( + actual, + matches_pattern!(AStruct(eq(123), eq(234), eq(345), eq(456), eq(567), eq(678), eq(789))) + ) +} + +#[test] +fn matches_tuple_struct_containing_eight_fields() -> Result<()> { + #[derive(Debug)] + struct AStruct(u32, u32, u32, u32, u32, u32, u32, u32); + let actual = AStruct(123, 234, 345, 456, 567, 678, 789, 890); + + verify_that!( + actual, + matches_pattern!(AStruct( + eq(123), + eq(234), + eq(345), + eq(456), + eq(567), + eq(678), + eq(789), + eq(890) + )) + ) +} + +#[test] +fn matches_tuple_struct_containing_nine_fields() -> Result<()> { + #[derive(Debug)] + struct AStruct(u32, u32, u32, u32, u32, u32, u32, u32, u32); + let actual = AStruct(123, 234, 345, 456, 567, 678, 789, 890, 901); + + verify_that!( + actual, + matches_pattern!(AStruct( + eq(123), + eq(234), + eq(345), + eq(456), + eq(567), + eq(678), + eq(789), + eq(890), + eq(901) + )) + ) +} + +#[test] +fn matches_tuple_struct_containing_ten_fields() -> Result<()> { + #[derive(Debug)] + struct AStruct(u32, u32, u32, u32, u32, u32, u32, u32, u32, u32); + let actual = AStruct(123, 234, 345, 456, 567, 678, 789, 890, 901, 12); + + verify_that!( + actual, + matches_pattern!(AStruct( + eq(123), + eq(234), + eq(345), + eq(456), + eq(567), + eq(678), + eq(789), + eq(890), + eq(901), + eq(12) + )) + ) +} + +#[test] +fn matches_tuple_struct_with_trailing_comma() -> Result<()> { + #[derive(Debug)] + struct AStruct(u32); + let actual = AStruct(123); + + verify_that!( + actual, + matches_pattern!(AStruct( + eq(123), // Keep the trailing comma, block reformatting + )) + ) +} + +#[test] +fn matches_tuple_struct_with_two_fields_and_trailing_comma() -> Result<()> { + #[derive(Debug)] + struct AStruct(u32, u32); + let actual = AStruct(123, 234); + + verify_that!( + actual, + matches_pattern!(AStruct( + eq(123), + eq(234), // Keep the trailing comma, block reformatting + )) + ) +} + +#[test] +fn matches_enum_without_field() -> Result<()> { + #[derive(Debug)] + enum AnEnum { + A, + } + let actual = AnEnum::A; + + verify_that!(actual, matches_pattern!(AnEnum::A)) +} + +#[test] +fn generates_correct_failure_output_when_enum_variant_without_field_is_not_matched() -> Result<()> { + #[derive(Debug)] + enum AnEnum { + #[allow(unused)] + A, + B, + } + let actual = AnEnum::B; + + let result = verify_that!(actual, matches_pattern!(AnEnum::A)); + + verify_that!(result, err(displays_as(contains_substring("is not AnEnum :: A")))) +} + +#[test] +fn generates_correct_failure_output_when_enum_variant_without_field_is_matched() -> Result<()> { + #[derive(Debug)] + enum AnEnum { + A, + } + let actual = AnEnum::A; + + let result = verify_that!(actual, not(matches_pattern!(AnEnum::A))); + + verify_that!(result, err(displays_as(contains_substring("is AnEnum :: A")))) +} + +#[test] +fn matches_enum_with_field() -> Result<()> { + #[derive(Debug)] + enum AnEnum { + A(u32), + } + let actual = AnEnum::A(123); + + verify_that!(actual, matches_pattern!(AnEnum::A(eq(123)))) +} + +#[test] +fn does_not_match_wrong_enum_value() -> Result<()> { + #[derive(Debug)] + enum AnEnum { + #[allow(unused)] + A(u32), + B, + } + let actual = AnEnum::B; + + verify_that!(actual, not(matches_pattern!(AnEnum::A(eq(123))))) +} + +#[test] +fn includes_enum_variant_in_description_with_field() -> Result<()> { + #[derive(Debug)] + enum AnEnum { + A(u32), + } + let actual = AnEnum::A(123); + + let result = verify_that!(actual, matches_pattern!(AnEnum::A(eq(234)))); + + verify_that!( + result, + err(displays_as(contains_substring("Expected: is AnEnum :: A which has field `0`"))) + ) +} + +#[test] +fn includes_enum_variant_in_negative_description_with_field() -> Result<()> { + #[derive(Debug)] + enum AnEnum { + A(u32), + } + let actual = AnEnum::A(123); + + let result = verify_that!(actual, not(matches_pattern!(AnEnum::A(eq(123))))); + + verify_that!( + result, + err(displays_as(contains_substring( + "Expected: is not AnEnum :: A which has field `0`, which is equal to" + ))) + ) +} + +#[test] +fn includes_enum_variant_in_description_with_two_fields() -> Result<()> { + #[derive(Debug)] + enum AnEnum { + A(u32, u32), + } + let actual = AnEnum::A(123, 234); + + let result = verify_that!(actual, matches_pattern!(AnEnum::A(eq(234), eq(234)))); + + verify_that!( + result, + err(displays_as(contains_substring( + "Expected: is AnEnum :: A which has all the following properties" + ))) + ) +} + +#[test] +fn includes_enum_variant_in_description_with_three_fields() -> Result<()> { + #[derive(Debug)] + enum AnEnum { + A(u32, u32, u32), + } + let actual = AnEnum::A(123, 234, 345); + + let result = verify_that!(actual, matches_pattern!(AnEnum::A(eq(234), eq(234), eq(345)))); + + verify_that!( + result, + err(displays_as(contains_substring( + "Expected: is AnEnum :: A which has all the following properties" + ))) + ) +} + +#[test] +fn includes_enum_variant_in_description_with_named_field() -> Result<()> { + #[derive(Debug)] + enum AnEnum { + A { field: u32 }, + } + let actual = AnEnum::A { field: 123 }; + + let result = verify_that!(actual, matches_pattern!(AnEnum::A { field: eq(234) })); + + verify_that!( + result, + err(displays_as(contains_substring("Expected: is AnEnum :: A which has field `field`"))) + ) +} + +#[test] +fn includes_enum_variant_in_description_with_two_named_fields() -> Result<()> { + #[derive(Debug)] + enum AnEnum { + A { field: u32, another_field: u32 }, + } + let actual = AnEnum::A { field: 123, another_field: 234 }; + + let result = verify_that!( + actual, + matches_pattern!(AnEnum::A { field: eq(234), another_field: eq(234) }) + ); + + verify_that!( + result, + err(displays_as(contains_substring( + "Expected: is AnEnum :: A which has all the following properties" + ))) + ) +} + +#[test] +fn includes_struct_name_in_description_with_property() -> Result<()> { + #[derive(Debug)] + struct AStruct { + field: u32, + } + impl AStruct { + fn get_field(&self) -> u32 { + self.field + } + } + let actual = AStruct { field: 123 }; + + let result = verify_that!(actual, matches_pattern!(AStruct { get_field(): eq(234) })); + + verify_that!( + result, + err(displays_as(contains_substring( + "Expected: is AStruct which has property `get_field ()`" + ))) + ) +} + +#[test] +fn includes_struct_name_in_description_with_ref_property() -> Result<()> { + #[derive(Debug)] + struct AStruct { + field: u32, + } + impl AStruct { + fn get_field(&self) -> &u32 { + &self.field + } + } + let actual = AStruct { field: 123 }; + + let result = verify_that!(actual, matches_pattern!(AStruct { ref get_field(): eq(234) })); + + verify_that!( + result, + err(displays_as(contains_substring( + "Expected: is AStruct which has property `get_field ()`" + ))) + ) +} + +#[test] +fn includes_struct_name_in_description_with_property_after_field() -> Result<()> { + #[derive(Debug)] + struct AStruct { + field: u32, + } + impl AStruct { + fn get_field(&self) -> u32 { + self.field + } + } + let actual = AStruct { field: 123 }; + + let result = + verify_that!(actual, matches_pattern!(AStruct { field: eq(123), get_field(): eq(234) })); + + verify_that!( + result, + err(displays_as(contains_substring( + "Expected: is AStruct which has all the following properties" + ))) + ) +} + +#[test] +fn includes_struct_name_in_description_with_ref_property_after_field() -> Result<()> { + #[derive(Debug)] + struct AStruct { + field: u32, + } + impl AStruct { + fn get_field(&self) -> &u32 { + &self.field + } + } + let actual = AStruct { field: 123 }; + + let result = verify_that!( + actual, + matches_pattern!(AStruct { field: eq(123), ref get_field(): eq(234) }) + ); + + verify_that!( + result, + err(displays_as(contains_substring( + "Expected: is AStruct which has all the following properties" + ))) + ) +} + +#[test] +fn matches_struct_with_a_method() -> Result<()> { + #[derive(Debug)] + struct AStruct { + a_field: u32, + } + + impl AStruct { + fn get_field(&self) -> u32 { + self.a_field + } + } + + let actual = AStruct { a_field: 123 }; + + verify_that!(actual, matches_pattern!(AStruct { get_field(): eq(123) })) +} + +#[test] +fn matches_struct_with_a_method_and_trailing_comma() -> Result<()> { + #[derive(Debug)] + struct AStruct { + a_field: u32, + } + + impl AStruct { + fn get_field(&self) -> u32 { + self.a_field + } + } + + let actual = AStruct { a_field: 123 }; + + verify_that!(actual, matches_pattern!(AStruct { get_field(): eq(123), })) +} + +#[test] +fn matches_struct_with_a_method_taking_parameter() -> Result<()> { + #[derive(Debug)] + struct AStruct { + a_field: u32, + } + + impl AStruct { + fn add_to_field(&self, a: u32) -> u32 { + self.a_field + a + } + } + + let actual = AStruct { a_field: 1 }; + + verify_that!(actual, matches_pattern!(AStruct { add_to_field(2): eq(3) })) +} + +#[test] +fn matches_struct_with_a_method_taking_two_parameters() -> Result<()> { + #[derive(Debug)] + struct AStruct { + a_field: u32, + } + + impl AStruct { + fn add_product_to_field(&self, a: u32, b: u32) -> u32 { + self.a_field + a * b + } + } + + let actual = AStruct { a_field: 1 }; + + verify_that!(actual, matches_pattern!(AStruct { add_product_to_field(2, 3): eq(7) })) +} + +#[test] +fn matches_struct_with_a_method_taking_enum_value_parameter() -> Result<()> { + enum AnEnum { + AVariant, + } + + #[derive(Debug)] + struct AStruct { + a_field: u32, + } + + impl AStruct { + fn get_a_field(&self, _value: AnEnum) -> u32 { + self.a_field + } + } + + let actual = AStruct { a_field: 1 }; + + verify_that!(actual, matches_pattern!(AStruct { get_a_field(AnEnum::AVariant): eq(1) })) +} + +#[test] +fn matches_struct_with_a_method_taking_two_parameters_with_trailing_comma() -> Result<()> { + #[derive(Debug)] + struct AStruct { + a_field: u32, + } + + impl AStruct { + fn add_product_to_field(&self, a: u32, b: u32) -> u32 { + self.a_field + a * b + } + } + + let actual = AStruct { a_field: 1 }; + + verify_that!(actual, matches_pattern!(AStruct { add_product_to_field(2, 3,): eq(7) })) +} + +#[test] +fn matches_struct_with_a_method_returning_a_reference() -> Result<()> { + #[derive(Debug)] + struct AStruct { + a_field: u32, + } + + impl AStruct { + fn get_field_ref(&self) -> &u32 { + &self.a_field + } + } + + let actual = AStruct { a_field: 123 }; + + verify_that!(actual, matches_pattern!(AStruct { ref get_field_ref(): eq(123) })) +} + +#[test] +fn matches_struct_with_a_method_returning_a_reference_with_trailing_comma() -> Result<()> { + #[derive(Debug)] + struct AStruct { + a_field: u32, + } + + impl AStruct { + fn get_field_ref(&self) -> &u32 { + &self.a_field + } + } + + let actual = AStruct { a_field: 123 }; + + verify_that!(actual, matches_pattern!(AStruct { ref get_field_ref(): eq(123), })) +} + +#[test] +fn matches_struct_with_a_method_taking_two_parameters_ret_ref() -> Result<()> { + #[derive(Debug)] + struct AStruct { + a_field: u32, + } + + impl AStruct { + fn get_field_ref(&self, _a: u32, _b: u32) -> &u32 { + &self.a_field + } + } + + let actual = AStruct { a_field: 1 }; + + verify_that!(actual, matches_pattern!(AStruct { ref get_field_ref(2, 3): eq(1) })) +} + +#[test] +fn matches_struct_with_a_method_returning_reference_taking_enum_value_parameter() -> Result<()> { + enum AnEnum { + AVariant, + } + + #[derive(Debug)] + struct AStruct { + a_field: u32, + } + + impl AStruct { + fn get_field_ref(&self, _value: AnEnum) -> &u32 { + &self.a_field + } + } + + let actual = AStruct { a_field: 1 }; + + verify_that!(actual, matches_pattern!(AStruct { ref get_field_ref(AnEnum::AVariant): eq(1) })) +} + +#[test] +fn matches_struct_with_a_method_taking_two_parameters_with_trailing_comma_ret_ref() -> Result<()> { + #[derive(Debug)] + struct AStruct { + a_field: u32, + } + + impl AStruct { + fn get_field_ref(&self, _a: u32, _b: u32) -> &u32 { + &self.a_field + } + } + + let actual = AStruct { a_field: 1 }; + + verify_that!(actual, matches_pattern!(AStruct { ref get_field_ref(2, 3,): eq(1) })) +} + +#[test] +fn matches_struct_with_a_method_followed_by_a_field() -> Result<()> { + #[derive(Debug)] + struct AStruct { + a_field: u32, + another_field: u32, + } + + impl AStruct { + fn get_field(&self) -> u32 { + self.a_field + } + } + + let actual = AStruct { a_field: 123, another_field: 234 }; + + verify_that!(actual, matches_pattern!(AStruct { get_field(): eq(123), another_field: eq(234) })) +} + +#[test] +fn matches_struct_with_a_method_followed_by_a_field_with_trailing_comma() -> Result<()> { + #[derive(Debug)] + struct AStruct { + a_field: u32, + another_field: u32, + } + + impl AStruct { + fn get_field(&self) -> u32 { + self.a_field + } + } + + let actual = AStruct { a_field: 123, another_field: 234 }; + + verify_that!( + actual, + matches_pattern!(AStruct { get_field(): eq(123), another_field: eq(234), }) + ) +} + +#[test] +fn matches_struct_with_a_method_taking_two_parameters_and_field() -> Result<()> { + #[derive(Debug)] + struct AStruct { + a_field: u32, + another_field: u32, + } + + impl AStruct { + fn add_product_to_field(&self, a: u32, b: u32) -> u32 { + self.a_field + a * b + } + } + + let actual = AStruct { a_field: 1, another_field: 123 }; + + verify_that!( + actual, + matches_pattern!(AStruct { add_product_to_field(2, 3): eq(7), another_field: eq(123) }) + ) +} + +#[test] +fn matches_struct_with_a_method_taking_enum_value_parameter_followed_by_field() -> Result<()> { + enum AnEnum { + AVariant, + } + + #[derive(Debug)] + struct AStruct { + a_field: u32, + another_field: u32, + } + + impl AStruct { + fn get_field(&self, _value: AnEnum) -> u32 { + self.a_field + } + } + + let actual = AStruct { a_field: 1, another_field: 2 }; + + verify_that!( + actual, + matches_pattern!(AStruct { get_field(AnEnum::AVariant): eq(1), another_field: eq(2) }) + ) +} + +#[test] +fn matches_struct_with_a_method_taking_two_parameters_with_trailing_comma_and_field() -> Result<()> +{ + #[derive(Debug)] + struct AStruct { + a_field: u32, + another_field: u32, + } + + impl AStruct { + fn add_product_to_field(&self, a: u32, b: u32) -> u32 { + self.a_field + a * b + } + } + + let actual = AStruct { a_field: 1, another_field: 123 }; + + verify_that!( + actual, + matches_pattern!(AStruct { add_product_to_field(2, 3,): eq(7), another_field: eq(123) }) + ) +} + +#[test] +fn matches_struct_with_a_method_returning_reference_followed_by_a_field() -> Result<()> { + #[derive(Debug)] + struct AStruct { + a_field: u32, + another_field: u32, + } + + impl AStruct { + fn get_field_ref(&self) -> &u32 { + &self.a_field + } + } + + let actual = AStruct { a_field: 123, another_field: 234 }; + + verify_that!( + actual, + matches_pattern!(AStruct { ref get_field_ref(): eq(123), another_field: eq(234) }) + ) +} + +#[test] +fn matches_struct_with_a_method_returning_reference_followed_by_a_field_with_trailing_comma() +-> Result<()> { + #[derive(Debug)] + struct AStruct { + a_field: u32, + another_field: u32, + } + + impl AStruct { + fn get_field_ref(&self) -> &u32 { + &self.a_field + } + } + + let actual = AStruct { a_field: 123, another_field: 234 }; + + verify_that!( + actual, + matches_pattern!(AStruct { ref get_field_ref(): eq(123), another_field: eq(234), }) + ) +} + +#[test] +fn matches_struct_with_a_method_taking_two_parameters_ret_ref_and_field() -> Result<()> { + #[derive(Debug)] + struct AStruct { + a_field: u32, + another_field: u32, + } + + impl AStruct { + fn get_field_ref(&self, _a: u32, _b: u32) -> &u32 { + &self.a_field + } + } + + let actual = AStruct { a_field: 1, another_field: 123 }; + + verify_that!( + actual, + matches_pattern!(AStruct { ref get_field_ref(2, 3): eq(1), another_field: eq(123) }) + ) +} + +#[test] +fn matches_struct_with_a_method_taking_enum_value_param_ret_ref_followed_by_field() -> Result<()> { + enum AnEnum { + AVariant, + } + + #[derive(Debug)] + struct AStruct { + a_field: u32, + another_field: u32, + } + + impl AStruct { + fn get_field_ref(&self, _value: AnEnum) -> &u32 { + &self.a_field + } + } + + let actual = AStruct { a_field: 1, another_field: 2 }; + + verify_that!( + actual, + matches_pattern!(AStruct { ref get_field_ref(AnEnum::AVariant): eq(1), another_field: eq(2) }) + ) +} + +#[test] +fn matches_struct_with_a_method_taking_two_parameters_with_trailing_comma_ret_ref_and_field() +-> Result<()> { + #[derive(Debug)] + struct AStruct { + a_field: u32, + another_field: u32, + } + + impl AStruct { + fn get_field_ref(&self, _a: u32, _b: u32) -> &u32 { + &self.a_field + } + } + + let actual = AStruct { a_field: 1, another_field: 123 }; + + verify_that!( + actual, + matches_pattern!(AStruct { ref get_field_ref(2, 3,): eq(1), another_field: eq(123) }) + ) +} + +#[test] +fn matches_struct_with_a_field_followed_by_a_method() -> Result<()> { + #[derive(Debug)] + struct AStruct { + a_field: u32, + another_field: u32, + } + + impl AStruct { + fn get_field(&self) -> u32 { + self.a_field + } + } + + let actual = AStruct { a_field: 123, another_field: 234 }; + + verify_that!(actual, matches_pattern!(AStruct { another_field: eq(234), get_field(): eq(123) })) +} + +#[test] +fn matches_struct_with_a_field_followed_by_a_method_with_trailing_comma() -> Result<()> { + #[derive(Debug)] + struct AStruct { + a_field: u32, + another_field: u32, + } + + impl AStruct { + fn get_field(&self) -> u32 { + self.a_field + } + } + + let actual = AStruct { a_field: 123, another_field: 234 }; + + verify_that!( + actual, + matches_pattern!(AStruct { another_field: eq(234), get_field(): eq(123), }) + ) +} + +#[test] +fn matches_struct_with_a_field_followed_by_a_method_with_params() -> Result<()> { + #[derive(Debug)] + struct AStruct { + a_field: u32, + another_field: u32, + } + + impl AStruct { + fn add_product_to_field(&self, a: u32, b: u32) -> u32 { + self.a_field + a * b + } + } + + let actual = AStruct { a_field: 1, another_field: 234 }; + + verify_that!( + actual, + matches_pattern!(AStruct { another_field: eq(234), add_product_to_field(2, 3): eq(7) }) + ) +} + +#[test] +fn matches_struct_with_field_followed_by_method_taking_enum_value_param() -> Result<()> { + enum AnEnum { + AVariant, + } + + #[derive(Debug)] + struct AStruct { + a_field: u32, + another_field: u32, + } + + impl AStruct { + fn get_field(&self, _value: AnEnum) -> u32 { + self.a_field + } + } + + let actual = AStruct { a_field: 1, another_field: 2 }; + + verify_that!( + actual, + matches_pattern!(AStruct { another_field: eq(2), get_field(AnEnum::AVariant): eq(1) }) + ) +} + +#[test] +fn matches_struct_with_a_field_followed_by_a_method_with_params_and_trailing_comma() -> Result<()> { + #[derive(Debug)] + struct AStruct { + a_field: u32, + another_field: u32, + } + + impl AStruct { + fn add_product_to_field(&self, a: u32, b: u32) -> u32 { + self.a_field + a * b + } + } + + let actual = AStruct { a_field: 1, another_field: 234 }; + + verify_that!( + actual, + matches_pattern!(AStruct { another_field: eq(234), add_product_to_field(2, 3,): eq(7) }) + ) +} + +#[test] +fn matches_struct_with_a_field_followed_by_a_method_returning_reference() -> Result<()> { + #[derive(Debug)] + struct AStruct { + a_field: u32, + another_field: u32, + } + + impl AStruct { + fn get_field_ref(&self) -> &u32 { + &self.a_field + } + } + + let actual = AStruct { a_field: 123, another_field: 234 }; + + verify_that!( + actual, + matches_pattern!(AStruct { another_field: eq(234), ref get_field_ref(): eq(123) }) + ) +} + +#[test] +fn matches_struct_with_a_field_followed_by_a_method_returning_ref_and_trailing_comma() -> Result<()> +{ + #[derive(Debug)] + struct AStruct { + a_field: u32, + another_field: u32, + } + + impl AStruct { + fn get_field_ref(&self) -> &u32 { + &self.a_field + } + } + + let actual = AStruct { a_field: 123, another_field: 234 }; + + verify_that!( + actual, + matches_pattern!(AStruct { another_field: eq(234), ref get_field_ref(): eq(123), }) + ) +} + +#[test] +fn matches_struct_with_a_field_followed_by_a_method_with_params_ret_ref() -> Result<()> { + #[derive(Debug)] + struct AStruct { + a_field: u32, + another_field: u32, + } + + impl AStruct { + fn get_field_ref(&self, _a: u32, _b: u32) -> &u32 { + &self.a_field + } + } + + let actual = AStruct { a_field: 123, another_field: 234 }; + + verify_that!( + actual, + matches_pattern!(AStruct { another_field: eq(234), ref get_field_ref(2, 3): eq(123) }) + ) +} + +#[test] +fn matches_struct_with_field_followed_by_method_taking_enum_value_param_ret_ref() -> Result<()> { + enum AnEnum { + AVariant, + } + + #[derive(Debug)] + struct AStruct { + a_field: u32, + another_field: u32, + } + + impl AStruct { + fn get_field_ref(&self, _value: AnEnum) -> &u32 { + &self.a_field + } + } + + let actual = AStruct { a_field: 1, another_field: 2 }; + + verify_that!( + actual, + matches_pattern!(AStruct { another_field: eq(2), ref get_field_ref(AnEnum::AVariant): eq(1) }) + ) +} + +#[test] +fn matches_struct_with_a_field_followed_by_a_method_with_params_and_trailing_comma_ret_ref() +-> Result<()> { + #[derive(Debug)] + struct AStruct { + a_field: u32, + another_field: u32, + } + + impl AStruct { + fn get_field_ref(&self, _a: u32, _b: u32) -> &u32 { + &self.a_field + } + } + + let actual = AStruct { a_field: 123, another_field: 234 }; + + verify_that!( + actual, + matches_pattern!(AStruct { another_field: eq(234), ref get_field_ref(2, 3,): eq(123) }) + ) +} + +#[test] +fn matches_struct_with_a_field_followed_by_a_method_followed_by_a_field() -> Result<()> { + #[derive(Debug)] + struct AStruct { + a_field: u32, + another_field: u32, + a_third_field: u32, + } + + impl AStruct { + fn get_field(&self) -> u32 { + self.a_field + } + } + + let actual = AStruct { a_field: 123, another_field: 234, a_third_field: 345 }; + + verify_that!( + actual, + matches_pattern!(AStruct { + another_field: eq(234), + get_field(): eq(123), + a_third_field: eq(345) + }) + ) +} + +#[test] +fn matches_struct_with_a_field_followed_by_a_method_followed_by_a_field_with_trailing_comma() +-> Result<()> { + #[derive(Debug)] + struct AStruct { + a_field: u32, + another_field: u32, + a_third_field: u32, + } + + impl AStruct { + fn get_field(&self) -> u32 { + self.a_field + } + } + + let actual = AStruct { a_field: 123, another_field: 234, a_third_field: 345 }; + + verify_that!( + actual, + matches_pattern!(AStruct { + another_field: eq(234), + get_field(): eq(123), + a_third_field: eq(345), + }) + ) +} + +#[test] +fn matches_struct_with_a_field_followed_by_a_method_with_params_followed_by_a_field() -> Result<()> +{ + #[derive(Debug)] + struct AStruct { + a_field: u32, + another_field: u32, + a_third_field: u32, + } + + impl AStruct { + fn add_product_to_field(&self, a: u32, b: u32) -> u32 { + self.a_field + a * b + } + } + + let actual = AStruct { a_field: 1, another_field: 234, a_third_field: 345 }; + + verify_that!( + actual, + matches_pattern!(AStruct { + another_field: eq(234), + add_product_to_field(2, 3): eq(7), + a_third_field: eq(345), + }) + ) +} + +#[test] +fn matches_struct_with_a_field_followed_by_a_method_with_params_and_trailing_comma_followed_by_a_field() +-> Result<()> { + #[derive(Debug)] + struct AStruct { + a_field: u32, + another_field: u32, + a_third_field: u32, + } + + impl AStruct { + fn add_product_to_field(&self, a: u32, b: u32) -> u32 { + self.a_field + a * b + } + } + + let actual = AStruct { a_field: 1, another_field: 234, a_third_field: 345 }; + + verify_that!( + actual, + matches_pattern!(AStruct { + another_field: eq(234), + add_product_to_field(2, 3,): eq(7), + a_third_field: eq(345), + }) + ) +} + +#[test] +fn matches_struct_with_field_followed_by_method_taking_enum_value_param_followed_by_field() +-> Result<()> { + enum AnEnum { + AVariant, + } + + #[derive(Debug)] + struct AStruct { + a_field: u32, + another_field: u32, + a_third_field: u32, + } + + impl AStruct { + fn get_field(&self, _value: AnEnum) -> u32 { + self.a_field + } + } + + let actual = AStruct { a_field: 1, another_field: 2, a_third_field: 3 }; + + verify_that!( + actual, + matches_pattern!(AStruct { + another_field: eq(2), + get_field(AnEnum::AVariant): eq(1), + a_third_field: eq(3), + }) + ) +} + +#[test] +fn matches_struct_with_a_field_followed_by_a_method_ret_ref_followed_by_a_field() -> Result<()> { + #[derive(Debug)] + struct AStruct { + a_field: u32, + another_field: u32, + a_third_field: u32, + } + + impl AStruct { + fn get_field_ref(&self) -> &u32 { + &self.a_field + } + } + + let actual = AStruct { a_field: 123, another_field: 234, a_third_field: 345 }; + + verify_that!( + actual, + matches_pattern!(AStruct { + another_field: eq(234), + ref get_field_ref(): eq(123), + a_third_field: eq(345) + }) + ) +} + +#[test] +fn matches_struct_with_a_field_followed_by_a_method_ret_ref_followed_by_a_field_with_trailing_comma() +-> Result<()> { + #[derive(Debug)] + struct AStruct { + a_field: u32, + another_field: u32, + a_third_field: u32, + } + + impl AStruct { + fn get_field_ref(&self) -> &u32 { + &self.a_field + } + } + + let actual = AStruct { a_field: 123, another_field: 234, a_third_field: 345 }; + + verify_that!( + actual, + matches_pattern!(AStruct { + another_field: eq(234), + ref get_field_ref(): eq(123), + a_third_field: eq(345), + }) + ) +} + +#[test] +fn matches_struct_with_a_field_followed_by_a_method_with_params_ret_ref_followed_by_a_field() +-> Result<()> { + #[derive(Debug)] + struct AStruct { + a_field: u32, + another_field: u32, + a_third_field: u32, + } + + impl AStruct { + fn get_field_ref(&self, _a: u32, _b: u32) -> &u32 { + &self.a_field + } + } + + let actual = AStruct { a_field: 123, another_field: 234, a_third_field: 345 }; + + verify_that!( + actual, + matches_pattern!(AStruct { + another_field: eq(234), + ref get_field_ref(2, 3): eq(123), + a_third_field: eq(345), + }) + ) +} + +#[test] +fn matches_struct_with_field_followed_by_method_taking_enum_value_param_ret_ref_followed_by_field() +-> Result<()> { + enum AnEnum { + AVariant, + } + + #[derive(Debug)] + struct AStruct { + a_field: u32, + another_field: u32, + a_third_field: u32, + } + + impl AStruct { + fn get_field_ref(&self, _value: AnEnum) -> &u32 { + &self.a_field + } + } + + let actual = AStruct { a_field: 1, another_field: 2, a_third_field: 3 }; + + verify_that!( + actual, + matches_pattern!(AStruct { + another_field: eq(2), + ref get_field_ref(AnEnum::AVariant): eq(1), + a_third_field: eq(3), + }) + ) +} + +#[test] +fn matches_struct_with_a_field_followed_by_a_method_with_params_trailing_comma_ret_ref_followed_by_a_field() +-> Result<()> { + #[derive(Debug)] + struct AStruct { + a_field: u32, + another_field: u32, + a_third_field: u32, + } + + impl AStruct { + fn get_field_ref(&self, _a: u32, _b: u32) -> &u32 { + &self.a_field + } + } + + let actual = AStruct { a_field: 123, another_field: 234, a_third_field: 345 }; + + verify_that!( + actual, + matches_pattern!(AStruct { + another_field: eq(234), + ref get_field_ref(2, 3,): eq(123), + a_third_field: eq(345), + }) + ) +} diff --git a/tests/no_color_test.rs b/tests/no_color_test.rs new file mode 100644 index 0000000..f307890 --- /dev/null +++ b/tests/no_color_test.rs @@ -0,0 +1,54 @@ +// 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. + +#![cfg(feature = "supports-color")] + +use googletest::prelude::*; +use indoc::indoc; +use std::fmt::{Display, Write}; + +// Make a long text with each element of the iterator on one line. +// `collection` must contains at least one element. +fn build_text<T: Display>(mut collection: impl Iterator<Item = T>) -> String { + let mut text = String::new(); + write!(&mut text, "{}", collection.next().expect("Provided collection without elements")) + .unwrap(); + for item in collection { + write!(&mut text, "\n{}", item).unwrap(); + } + text +} + +#[test] +fn colors_suppressed_when_both_no_color_and_force_color_are_set() -> Result<()> { + std::env::set_var("NO_COLOR", "1"); + std::env::set_var("FORCE_COLOR", "1"); + + let result = verify_that!(build_text(1..50), eq(build_text(1..51))); + + verify_that!( + result, + err(displays_as(contains_substring(indoc! { + " + + Difference(-actual / +expected): + 1 + 2 + <---- 45 common lines omitted ----> + 48 + 49 + +50" + }))) + ) +} diff --git a/tests/pointwise_matcher_test.rs b/tests/pointwise_matcher_test.rs new file mode 100644 index 0000000..85d16ff --- /dev/null +++ b/tests/pointwise_matcher_test.rs @@ -0,0 +1,170 @@ +// 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 googletest::prelude::*; +use indoc::indoc; + +#[test] +fn pointwise_matches_single_element() -> Result<()> { + let value = vec![1]; + verify_that!(value, pointwise!(lt, vec![2])) +} + +#[test] +fn pointwise_matches_two_elements() -> Result<()> { + let value = vec![1, 2]; + verify_that!(value, pointwise!(lt, vec![2, 3])) +} + +#[test] +fn pointwise_matches_two_elements_with_array() -> Result<()> { + let value = vec![1, 2]; + verify_that!(value, pointwise!(lt, [2, 3])) +} + +#[test] +fn pointwise_matches_two_element_slice() -> Result<()> { + let value = vec![1, 2]; + let slice = value.as_slice(); + verify_that!(*slice, pointwise!(lt, [2, 3])) +} + +#[test] +fn pointwise_does_not_match_value_of_wrong_length() -> Result<()> { + let value = vec![1]; + verify_that!(value, not(pointwise!(lt, vec![2, 3]))) +} + +#[test] +fn pointwise_does_not_match_value_not_matching_in_first_position() -> Result<()> { + let value = vec![1, 2]; + verify_that!(value, not(pointwise!(lt, vec![1, 3]))) +} + +#[test] +fn pointwise_does_not_match_value_not_matching_in_second_position() -> Result<()> { + let value = vec![1, 2]; + verify_that!(value, not(pointwise!(lt, vec![2, 2]))) +} + +#[test] +fn pointwise_allows_qualified_matcher_name() -> Result<()> { + mod submodule { + pub(super) use super::lt; + } + let value = vec![1]; + verify_that!(value, pointwise!(submodule::lt, vec![2])) +} + +#[test] +fn pointwise_returns_mismatch_when_actual_value_has_wrong_length() -> Result<()> { + let result = verify_that!(vec![1, 2, 3], pointwise!(eq, vec![1, 2])); + + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + Value of: vec![1, 2, 3] + Expected: has elements satisfying respectively: + 0. is equal to 1 + 1. is equal to 2 + Actual: [1, 2, 3], + which has size 3 (expected 2) + " + )))) + ) +} + +#[test] +fn pointwise_returns_mismatch_when_actual_value_does_not_match_on_first_item() -> Result<()> { + let result = verify_that!(vec![1, 2, 3], pointwise!(eq, vec![2, 2, 3])); + + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + Value of: vec![1, 2, 3] + Expected: has elements satisfying respectively: + 0. is equal to 2 + 1. is equal to 2 + 2. is equal to 3 + Actual: [1, 2, 3], + where element #0 is 1, which isn't equal to 2 + " + )))) + ) +} + +#[test] +fn pointwise_returns_mismatch_when_actual_value_does_not_match_on_second_item() -> Result<()> { + let result = verify_that!(vec![1, 2, 3], pointwise!(eq, vec![1, 3, 3])); + + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + Value of: vec![1, 2, 3] + Expected: has elements satisfying respectively: + 0. is equal to 1 + 1. is equal to 3 + 2. is equal to 3 + Actual: [1, 2, 3], + where element #1 is 2, which isn't equal to 3 + " + )))) + ) +} + +#[test] +fn pointwise_returns_mismatch_when_actual_value_does_not_match_on_first_and_second_items() +-> Result<()> { + let result = verify_that!(vec![1, 2, 3], pointwise!(eq, vec![2, 3, 3])); + + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + Value of: vec![1, 2, 3] + Expected: has elements satisfying respectively: + 0. is equal to 2 + 1. is equal to 3 + 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" + )))) + ) +} + +#[test] +fn pointwise_matches_single_element_with_lambda_expression_with_extra_value() -> Result<()> { + let value = vec![1.00001f32]; + verify_that!(value, pointwise!(|v| near(v, 0.0001), vec![1.0])) +} + +#[test] +fn pointwise_matches_single_element_with_two_containers() -> Result<()> { + let value = vec![1.00001f32]; + verify_that!(value, pointwise!(near, vec![1.0], vec![0.0001])) +} + +#[test] +fn pointwise_matches_single_element_with_three_containers() -> Result<()> { + let value = vec![1.00001f32]; + verify_that!( + value, + pointwise!(|v, t, u| near(v, t * u), vec![1.0f32], vec![0.0001f32], vec![0.5f32]) + ) +} diff --git a/tests/property_matcher_test.rs b/tests/property_matcher_test.rs new file mode 100644 index 0000000..f9a88be --- /dev/null +++ b/tests/property_matcher_test.rs @@ -0,0 +1,189 @@ +// 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 googletest::matcher::{Matcher, MatcherResult}; +use googletest::prelude::*; + +#[derive(Debug)] +struct SomeStruct { + a_property: u32, +} + +impl SomeStruct { + fn get_property(&self) -> u32 { + self.a_property + } + + fn get_property_ref(&self) -> &u32 { + &self.a_property + } + + fn add_product_to_field(&self, a: u32, b: u32) -> u32 { + self.a_property + a * b + } + + fn get_property_ref_with_params(&self, _a: u32, _b: u32) -> &u32 { + &self.a_property + } +} + +#[test] +fn matches_struct_with_matching_property() -> Result<()> { + let value = SomeStruct { a_property: 10 }; + verify_that!(value, property!(SomeStruct.get_property(), eq(10))) +} + +#[test] +fn matches_struct_with_matching_property_with_parameters() -> Result<()> { + let value = SomeStruct { a_property: 10 }; + verify_that!(value, property!(SomeStruct.add_product_to_field(2, 3), eq(16))) +} + +#[test] +fn matches_struct_with_matching_property_with_captured_arguments() -> Result<()> { + let value = SomeStruct { a_property: 10 }; + let arg1 = 2; + let arg2 = 3; + verify_that!(value, property!(SomeStruct.add_product_to_field(arg1, arg2), eq(16))) +} + +#[test] +fn matches_struct_with_matching_property_with_parameters_with_trailing_comma() -> Result<()> { + let value = SomeStruct { a_property: 10 }; + verify_that!(value, property!(SomeStruct.add_product_to_field(2, 3,), eq(16))) +} + +#[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))) +} + +#[test] +fn matches_struct_with_matching_string_reference_property() -> Result<()> { + #[derive(Debug)] + struct StructWithString { + property: String, + } + impl StructWithString { + fn get_property_ref(&self) -> &String { + &self.property + } + } + let value = StructWithString { property: "Something".into() }; + verify_that!(value, property!(ref StructWithString.get_property_ref(), eq("Something"))) +} + +#[test] +fn matches_struct_with_matching_slice_property() -> Result<()> { + #[derive(Debug)] + struct StructWithVec { + property: Vec<u32>, + } + impl StructWithVec { + fn get_property_ref(&self) -> &[u32] { + &self.property + } + } + let value = StructWithVec { property: vec![1, 2, 3] }; + verify_that!(value, property!(ref 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))) +} + +#[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))) +} + +#[test] +fn does_not_match_struct_with_non_matching_property() -> Result<()> { + let value = SomeStruct { a_property: 2 }; + verify_that!(value, not(property!(SomeStruct.get_property(), eq(1)))) +} + +#[test] +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") + ) +} + +#[test] +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") + ) +} + +#[test] +fn explains_mismatch_referencing_explanation_of_inner_matcher() -> Result<()> { + impl SomeStruct { + fn get_a_collection(&self) -> Vec<u32> { + vec![] + } + } + let value = SomeStruct { a_property: 2 }; + let result = verify_that!(value, property!(SomeStruct.get_a_collection(), container_eq([1]))); + + verify_that!( + result, + err(displays_as(contains_substring( + "whose property `get_a_collection()` is `[]`, which is missing the element 1" + ))) + ) +} + +#[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") + ) +} + +#[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") + ) +} + +#[test] +fn explains_mismatch_referencing_explanation_of_inner_matcher_for_ref() -> Result<()> { + static EMPTY_COLLECTION: Vec<u32> = vec![]; + impl SomeStruct { + fn get_a_collection_ref(&self) -> &[u32] { + &EMPTY_COLLECTION + } + } + let value = SomeStruct { a_property: 2 }; + let result = + verify_that!(value, property!(ref SomeStruct.get_a_collection_ref(), container_eq([1]))); + + verify_that!( + result, + err(displays_as(contains_substring( + "whose property `get_a_collection_ref()` is `[]`, which is missing the element 1" + ))) + ) +} diff --git a/tests/proptest_integration_test.rs b/tests/proptest_integration_test.rs new file mode 100644 index 0000000..b33c9dd --- /dev/null +++ b/tests/proptest_integration_test.rs @@ -0,0 +1,30 @@ +// 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. + +#![cfg(feature = "proptest")] + +use googletest::prelude::*; +use proptest::test_runner::{Config, TestRunner}; + +#[test] +fn numbers_are_greater_than_zero() -> Result<()> { + let mut runner = TestRunner::new(Config::default()); + runner.run(&(1..100i32), |v| Ok(verify_that!(v, gt(0))?)).into_test_result() +} + +#[test] +fn strings_are_nonempty() -> Result<()> { + let mut runner = TestRunner::new(Config::default()); + runner.run(&"[a-zA-Z0-9]+", |v| Ok(verify_that!(v, not(eq("")))?)).into_test_result() +} diff --git a/tests/tuple_matcher_test.rs b/tests/tuple_matcher_test.rs new file mode 100644 index 0000000..0c1eb01 --- /dev/null +++ b/tests/tuple_matcher_test.rs @@ -0,0 +1,259 @@ +// 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 googletest::matcher::{Matcher, MatcherResult}; +use googletest::prelude::*; +use indoc::indoc; + +#[test] +fn empty_matcher_matches_empty_tuple() -> Result<()> { + verify_that!((), ()) +} + +#[test] +fn singleton_matcher_matches_matching_singleton_tuple() -> Result<()> { + verify_that!((123,), (eq(123),)) +} + +#[test] +fn singleton_matcher_does_not_match_non_matching_singleton_tuple() -> Result<()> { + verify_that!((123,), not((eq(456),))) +} + +#[test] +fn pair_matcher_matches_matching_pair_tuple() -> Result<()> { + verify_that!((123, 456), (eq(123), eq(456))) +} + +#[test] +fn pair_matcher_matches_matching_pair_tuple_with_different_types() -> Result<()> { + verify_that!((123, "A string"), (eq(123), eq("A string"))) +} + +#[test] +fn pair_matcher_with_trailing_comma_matches_matching_pair_tuple() -> Result<()> { + verify_that!((123, 456), (eq(123), eq(456),)) +} + +#[test] +fn tuple_matcher_matches_matching_3_tuple() -> Result<()> { + verify_that!((1, 2, 3), (eq(1), eq(2), eq(3))) +} + +#[test] +fn tuple_matcher_matches_matching_4_tuple() -> Result<()> { + verify_that!((1, 2, 3, 4), (eq(1), eq(2), eq(3), eq(4))) +} + +#[test] +fn tuple_matcher_matches_matching_5_tuple() -> Result<()> { + verify_that!((1, 2, 3, 4, 5), (eq(1), eq(2), eq(3), eq(4), eq(5))) +} + +#[test] +fn tuple_matcher_matches_matching_6_tuple() -> Result<()> { + verify_that!((1, 2, 3, 4, 5, 6), (eq(1), eq(2), eq(3), eq(4), eq(5), eq(6))) +} + +#[test] +fn tuple_matcher_matches_matching_7_tuple() -> Result<()> { + verify_that!((1, 2, 3, 4, 5, 6, 7), (eq(1), eq(2), eq(3), eq(4), eq(5), eq(6), eq(7))) +} + +#[test] +fn tuple_matcher_matches_matching_8_tuple() -> Result<()> { + verify_that!((1, 2, 3, 4, 5, 6, 7, 8), (eq(1), eq(2), eq(3), eq(4), eq(5), eq(6), eq(7), eq(8))) +} + +#[test] +fn tuple_matcher_matches_matching_9_tuple() -> Result<()> { + verify_that!( + (1, 2, 3, 4, 5, 6, 7, 8, 9), + (eq(1), eq(2), eq(3), eq(4), eq(5), eq(6), eq(7), eq(8), eq(9)) + ) +} + +#[test] +fn tuple_matcher_matches_matching_10_tuple() -> Result<()> { + verify_that!( + (1, 2, 3, 4, 5, 6, 7, 8, 9, 10), + (eq(1), eq(2), eq(3), eq(4), eq(5), eq(6), eq(7), eq(8), eq(9), eq(10)) + ) +} + +#[test] +fn tuple_matcher_matches_matching_11_tuple() -> Result<()> { + verify_that!( + (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11), + (eq(1), eq(2), eq(3), eq(4), eq(5), eq(6), eq(7), eq(8), eq(9), eq(10), eq(11)) + ) +} + +#[test] +fn tuple_matcher_matches_matching_12_tuple() -> Result<()> { + verify_that!( + (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12), + (eq(1), eq(2), eq(3), eq(4), eq(5), eq(6), eq(7), eq(8), eq(9), eq(10), eq(11), eq(12)) + ) +} + +#[test] +fn tuple_matcher_with_trailing_comma_matches_matching_3_tuple() -> Result<()> { + verify_that!((1, 2, 3), (eq(1), eq(2), eq(3),)) +} + +#[test] +fn tuple_matcher_with_trailing_comma_matches_matching_4_tuple() -> Result<()> { + verify_that!((1, 2, 3, 4), (eq(1), eq(2), eq(3), eq(4),)) +} + +#[test] +fn tuple_matcher_with_trailing_comma_matches_matching_5_tuple() -> Result<()> { + verify_that!((1, 2, 3, 4, 5), (eq(1), eq(2), eq(3), eq(4), eq(5),)) +} + +#[test] +fn tuple_matcher_with_trailing_comma_matches_matching_6_tuple() -> Result<()> { + verify_that!((1, 2, 3, 4, 5, 6), (eq(1), eq(2), eq(3), eq(4), eq(5), eq(6),)) +} + +#[test] +fn tuple_matcher_with_trailing_comma_matches_matching_7_tuple() -> Result<()> { + verify_that!((1, 2, 3, 4, 5, 6, 7), (eq(1), eq(2), eq(3), eq(4), eq(5), eq(6), eq(7),)) +} + +#[test] +fn tuple_matcher_with_trailing_comma_matches_matching_8_tuple() -> Result<()> { + verify_that!( + (1, 2, 3, 4, 5, 6, 7, 8), + (eq(1), eq(2), eq(3), eq(4), eq(5), eq(6), eq(7), eq(8),) + ) +} + +#[test] +fn tuple_matcher_with_trailing_comma_matches_matching_9_tuple() -> Result<()> { + verify_that!( + (1, 2, 3, 4, 5, 6, 7, 8, 9), + (eq(1), eq(2), eq(3), eq(4), eq(5), eq(6), eq(7), eq(8), eq(9),) + ) +} + +#[test] +fn tuple_matcher_with_trailing_comma_matches_matching_10_tuple() -> Result<()> { + verify_that!( + (1, 2, 3, 4, 5, 6, 7, 8, 9, 10), + (eq(1), eq(2), eq(3), eq(4), eq(5), eq(6), eq(7), eq(8), eq(9), eq(10),) + ) +} + +#[test] +fn tuple_matcher_with_trailing_comma_matches_matching_11_tuple() -> Result<()> { + verify_that!( + (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11), + (eq(1), eq(2), eq(3), eq(4), eq(5), eq(6), eq(7), eq(8), eq(9), eq(10), eq(11),) + ) +} + +#[test] +fn tuple_matcher_with_trailing_comma_matches_matching_12_tuple() -> Result<()> { + verify_that!( + (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12), + (eq(1), eq(2), eq(3), eq(4), eq(5), eq(6), eq(7), eq(8), eq(9), eq(10), eq(11), eq(12),) + ) +} + +#[test] +fn tuple_matcher_1_has_correct_description_for_match() -> Result<()> { + verify_that!( + (eq(1),).describe(MatcherResult::Match), + eq(indoc!( + " + is a tuple whose values respectively match: + is equal to 1, + " + )) + ) +} + +#[test] +fn tuple_matcher_1_has_correct_description_for_mismatch() -> Result<()> { + verify_that!( + (eq(1),).describe(MatcherResult::NoMatch), + eq(indoc!( + " + is a tuple whose values do not respectively match: + 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!( + " + is a tuple whose values respectively match: + 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!( + " + is a tuple whose values do not respectively match: + is equal to 1, + is equal to 2, + " + )) + ) +} + +#[test] +fn describe_match_shows_which_tuple_element_did_not_match() -> Result<()> { + verify_that!( + (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 + " + ))) + ) +} + +#[test] +fn describe_match_shows_which_two_tuple_elements_did_not_match() -> Result<()> { + verify_that!( + (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 + " + ))) + ) +} diff --git a/tests/unordered_elements_are_matcher_test.rs b/tests/unordered_elements_are_matcher_test.rs new file mode 100644 index 0000000..a105b70 --- /dev/null +++ b/tests/unordered_elements_are_matcher_test.rs @@ -0,0 +1,403 @@ +// 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 googletest::matcher::Matcher; +use googletest::prelude::*; +use indoc::indoc; +use std::collections::HashMap; + +#[test] +fn unordered_elements_are_matches_empty_vector() -> Result<()> { + let value: Vec<u32> = vec![]; + verify_that!(value, unordered_elements_are![]) +} + +#[test] +fn unordered_elements_are_matches_empty_vector_with_trailing_comma() -> Result<()> { + let value: Vec<u32> = vec![]; + verify_that!(value, unordered_elements_are![,]) +} + +#[test] +fn unordered_elements_are_matches_vector() -> Result<()> { + let value = vec![1, 2, 3]; + verify_that!(value, unordered_elements_are![eq(1), eq(2), eq(3)]) +} + +#[test] +fn unordered_elements_are_omitted() -> Result<()> { + let value = vec![1, 2, 3]; + verify_that!(value, {eq(3), eq(2), eq(1)}) +} + +#[test] +fn unordered_elements_are_matches_slice() -> Result<()> { + let value = vec![1, 2, 3]; + let slice = value.as_slice(); + verify_that!(*slice, unordered_elements_are![eq(1), eq(2), eq(3)]) +} + +#[test] +fn unordered_elements_are_matches_hash_map() -> Result<()> { + let value: HashMap<u32, &'static str> = HashMap::from([(1, "One"), (2, "Two"), (3, "Three")]); + verify_that!( + value, + unordered_elements_are![(eq(2), eq("Two")), (eq(1), eq("One")), (eq(3), eq("Three"))] + ) +} + +#[test] +fn unordered_elements_are_matches_hash_map_with_trailing_comma() -> Result<()> { + let value: HashMap<u32, &'static str> = HashMap::from([(1, "One"), (2, "Two"), (3, "Three")]); + verify_that!( + value, + unordered_elements_are![(eq(2), eq("Two")), (eq(1), eq("One")), (eq(3), eq("Three")),] + ) +} + +#[test] +fn unordered_elements_are_does_not_match_hash_map_with_wrong_key() -> Result<()> { + let value: HashMap<u32, &'static str> = HashMap::from([(1, "One"), (2, "Two"), (4, "Three")]); + verify_that!( + value, + not(unordered_elements_are![(eq(2), eq("Two")), (eq(1), eq("One")), (eq(3), eq("Three"))]) + ) +} + +#[test] +fn unordered_elements_are_does_not_match_hash_map_with_wrong_value() -> Result<()> { + let value: HashMap<u32, &'static str> = HashMap::from([(1, "One"), (2, "Two"), (3, "Four")]); + verify_that!( + value, + not(unordered_elements_are![(eq(2), eq("Two")), (eq(1), eq("One")), (eq(3), eq("Three"))]) + ) +} + +#[test] +fn unordered_elements_are_does_not_match_hash_map_missing_element() -> Result<()> { + let value: HashMap<u32, &'static str> = HashMap::from([(1, "One"), (2, "Two")]); + verify_that!( + value, + not(unordered_elements_are![(eq(2), eq("Two")), (eq(1), eq("One")), (eq(3), eq("Three"))]) + ) +} + +#[test] +fn unordered_elements_are_does_not_match_hash_map_with_extra_element() -> Result<()> { + let value: HashMap<u32, &'static str> = HashMap::from([(1, "One"), (2, "Two"), (3, "Three")]); + verify_that!(value, not(unordered_elements_are![(eq(2), eq("Two")), (eq(1), eq("One"))])) +} + +#[test] +fn unordered_elements_are_does_not_match_hash_map_with_mismatched_key_and_value() -> Result<()> { + let value: HashMap<u32, &'static str> = HashMap::from([(1, "One"), (2, "Three"), (3, "Two")]); + verify_that!( + value, + not(unordered_elements_are![(eq(2), eq("Two")), (eq(1), eq("One")), (eq(3), eq("Three"))]) + ) +} + +#[test] +fn unordered_elements_are_matches_vector_with_trailing_comma() -> Result<()> { + let value = vec![1, 2, 3]; + verify_that!(value, unordered_elements_are![eq(1), eq(2), eq(3),]) +} + +#[test] +fn unordered_elements_are_matches_size() -> Result<()> { + let value = vec![1, 2]; + verify_that!(value, not(unordered_elements_are![eq(1), eq(2), eq(3)])) +} + +#[test] +fn unordered_elements_are_admits_matchers_without_static_lifetime() -> Result<()> { + #[derive(Debug, PartialEq)] + struct AStruct(i32); + let expected_value = AStruct(123); + verify_that!(vec![AStruct(123)], unordered_elements_are![eq_deref_of(&expected_value)]) +} + +#[test] +fn unordered_elements_are_with_map_admits_matchers_without_static_lifetime() -> Result<()> { + #[derive(Debug, PartialEq)] + struct AStruct(i32); + let expected_value = AStruct(123); + verify_that!( + HashMap::from([(1, AStruct(123))]), + unordered_elements_are![(eq(1), eq_deref_of(&expected_value))] + ) +} + +#[test] +fn unordered_elements_are_description_mismatch() -> Result<()> { + let result = verify_that!(vec![1, 4, 3], unordered_elements_are![eq(1), eq(2), eq(3)]); + verify_that!( + result, + err(displays_as(contains_substring(indoc!( + " + Value of: vec![1, 4, 3] + Expected: contains elements matching in any order: + 0. is equal to 1 + 1. is equal to 2 + 2. is equal to 3 + Actual: [1, 4, 3], + whose element #1 does not match any expected elements and no elements match the expected element #1" + )))) + ) +} + +#[test] +fn unordered_elements_are_matches_unordered() -> Result<()> { + let value = vec![1, 2]; + verify_that!(value, unordered_elements_are![eq(2), eq(1)]) +} + +#[test] +fn unordered_elements_are_matches_unordered_with_repetition() -> Result<()> { + let value = vec![1, 2, 1, 2, 1]; + verify_that!(value, unordered_elements_are![eq(1), eq(1), eq(1), eq(2), eq(2)]) +} + +#[test] +fn unordered_elements_are_explains_mismatch_due_to_wrong_size() -> Result<()> { + verify_that!( + unordered_elements_are![eq(2), eq(3), eq(4)].explain_match(&vec![2, 3]), + displays_as(eq("which has size 2 (expected 3)")) + ) +} + +#[test] +fn unordered_elements_are_description_no_full_match() -> Result<()> { + verify_that!( + unordered_elements_are![eq(1), eq(2), eq(2)].explain_match(&vec![1, 1, 2]), + displays_as(eq(indoc!( + " + which does not have a perfect match with the expected elements. The best match found was: + Actual element 1 at index 0 matched expected element `is equal to 1` at index 0. + Actual element 2 at index 2 matched expected element `is equal to 2` at index 1. + Actual element 1 at index 1 did not match any remaining expected element. + Expected element `is equal to 2` at index 2 did not match any remaining actual element." + ))) + ) +} + +#[test] +fn unordered_elements_are_unmatchable_expected_description_mismatch() -> Result<()> { + verify_that!( + unordered_elements_are![eq(1), eq(2), eq(3)].explain_match(&vec![1, 1, 3]), + displays_as(eq("which has no element matching the expected element #1")) + ) +} + +#[test] +fn unordered_elements_are_unmatchable_actual_description_mismatch() -> Result<()> { + verify_that!( + unordered_elements_are![eq(1), eq(1), eq(3)].explain_match(&vec![1, 2, 3]), + displays_as(eq("whose element #1 does not match any expected elements")) + ) +} + +fn create_matcher() -> impl Matcher<ActualT = Vec<i32>> { + unordered_elements_are![eq(1)] +} + +#[test] +fn unordered_elements_are_works_when_matcher_is_created_in_subroutine() -> Result<()> { + verify_that!(vec![1], create_matcher()) +} + +fn create_matcher_for_map() -> impl Matcher<ActualT = HashMap<i32, i32>> { + unordered_elements_are![(eq(1), eq(1))] +} + +#[test] +fn unordered_elements_are_works_when_matcher_for_maps_is_created_in_subroutine() -> Result<()> { + verify_that!(HashMap::from([(1, 1)]), create_matcher_for_map()) +} + +#[test] +fn contains_each_matches_when_one_to_one_correspondence_present() -> Result<()> { + verify_that!(vec![2, 3, 4], contains_each!(eq(2), eq(3), eq(4))) +} + +#[test] +fn contains_each_supports_trailing_comma() -> Result<()> { + verify_that!(vec![2, 3, 4], contains_each!(eq(2), eq(3), eq(4),)) +} + +#[test] +fn contains_each_matches_hash_map() -> Result<()> { + let value: HashMap<u32, &'static str> = HashMap::from([(1, "One"), (2, "Two"), (3, "Three")]); + verify_that!(value, contains_each![(eq(2), eq("Two")), (eq(1), eq("One"))]) +} + +#[test] +fn contains_each_matches_hash_map_with_trailing_comma() -> Result<()> { + let value: HashMap<u32, &'static str> = HashMap::from([(1, "One"), (2, "Two"), (3, "Three")]); + verify_that!(value, contains_each![(eq(2), eq("Two")), (eq(1), eq("One")),]) +} + +#[test] +fn contains_each_matches_when_no_matchers_present() -> Result<()> { + verify_that!(vec![2, 3, 4], contains_each!()) +} + +#[test] +fn contains_each_matches_when_no_matchers_present_and_trailing_comma() -> Result<()> { + verify_that!(vec![2, 3, 4], contains_each!(,)) +} + +#[test] +fn contains_each_matches_when_list_is_empty_and_no_matchers_present() -> Result<()> { + verify_that!(Vec::<u32>::new(), contains_each!()) +} + +#[test] +fn contains_each_matches_when_excess_elements_present() -> Result<()> { + verify_that!(vec![1, 2, 3, 4], contains_each!(eq(2), eq(3), eq(4))) +} + +#[test] +fn contains_each_does_not_match_when_matchers_are_unmatched() -> Result<()> { + verify_that!(vec![1, 2, 3], not(contains_each!(eq(2), eq(3), eq(4)))) +} + +#[test] +fn contains_each_explains_mismatch_due_to_wrong_size() -> Result<()> { + verify_that!( + contains_each![eq(2), eq(3), eq(4)].explain_match(&vec![2, 3]), + displays_as(eq("which has size 2 (expected at least 3)")) + ) +} + +#[test] +fn contains_each_explains_missing_element_in_mismatch() -> Result<()> { + verify_that!( + contains_each![eq(2), eq(3), eq(4)].explain_match(&vec![1, 2, 3]), + displays_as(eq("which has no element matching the expected element #2")) + ) +} + +#[test] +fn contains_each_explains_missing_elements_in_mismatch() -> Result<()> { + verify_that!( + contains_each![eq(2), eq(3), eq(4), eq(5)].explain_match(&vec![0, 1, 2, 3]), + displays_as(eq("which has no elements matching the expected elements #2, #3")) + ) +} + +#[test] +fn contains_each_explains_mismatch_due_to_no_graph_matching_found() -> Result<()> { + verify_that!( + contains_each![ge(2), ge(2)].explain_match(&vec![1, 2]), + displays_as(eq(indoc!( + " + which does not have a superset match with the expected elements. The best match found was: + Actual element 2 at index 1 matched expected element `is greater than or equal to 2` at index 0. + Actual element 1 at index 0 did not match any remaining expected element. + Expected element `is greater than or equal to 2` at index 1 did not match any remaining actual element.")) + )) +} + +#[test] +fn is_contained_in_matches_with_empty_vector() -> Result<()> { + let value: Vec<u32> = vec![]; + verify_that!(value, is_contained_in!()) +} + +#[test] +fn is_contained_in_matches_with_empty_vector_and_trailing_comma() -> Result<()> { + let value: Vec<u32> = vec![]; + verify_that!(value, is_contained_in!(,)) +} + +#[test] +fn is_contained_in_matches_when_one_to_one_correspondence_present() -> Result<()> { + verify_that!(vec![2, 3, 4], is_contained_in!(eq(2), eq(3), eq(4))) +} + +#[test] +fn is_contained_supports_trailing_comma() -> Result<()> { + verify_that!(vec![2, 3, 4], is_contained_in!(eq(2), eq(3), eq(4),)) +} + +#[test] +fn is_contained_in_matches_hash_map() -> Result<()> { + let value: HashMap<u32, &'static str> = HashMap::from([(1, "One"), (2, "Two")]); + verify_that!( + value, + is_contained_in![(eq(2), eq("Two")), (eq(1), eq("One")), (eq(3), eq("Three"))] + ) +} + +#[test] +fn is_contained_in_matches_hash_map_with_trailing_comma() -> Result<()> { + let value: HashMap<u32, &'static str> = HashMap::from([(1, "One"), (2, "Two")]); + verify_that!( + value, + is_contained_in![(eq(2), eq("Two")), (eq(1), eq("One")), (eq(3), eq("Three")),] + ) +} + +#[test] +fn is_contained_in_matches_when_container_is_empty() -> Result<()> { + verify_that!(vec![], is_contained_in!(eq(2), eq(3), eq(4))) +} + +#[test] +fn is_contained_in_matches_when_excess_matchers_present() -> Result<()> { + verify_that!(vec![3, 4], is_contained_in!(eq(2), eq(3), eq(4))) +} + +#[test] +fn is_contained_in_does_not_match_when_elements_are_unmatched() -> Result<()> { + verify_that!(vec![1, 2, 3], not(is_contained_in!(eq(2), eq(3), eq(4)))) +} + +#[test] +fn is_contained_in_explains_mismatch_due_to_wrong_size() -> Result<()> { + verify_that!( + is_contained_in![eq(2), eq(3)].explain_match(&vec![2, 3, 4]), + displays_as(eq("which has size 3 (expected at most 2)")) + ) +} + +#[test] +fn is_contained_in_explains_missing_element_in_mismatch() -> Result<()> { + verify_that!( + is_contained_in![eq(2), eq(3), eq(4)].explain_match(&vec![1, 2, 3]), + displays_as(eq("whose element #0 does not match any expected elements")) + ) +} + +#[test] +fn is_contained_in_explains_missing_elements_in_mismatch() -> Result<()> { + verify_that!( + is_contained_in![eq(2), eq(3), eq(4), eq(5)].explain_match(&vec![0, 1, 2, 3]), + displays_as(eq("whose elements #0, #1 do not match any expected elements")) + ) +} + +#[test] +fn is_contained_in_explains_mismatch_due_to_no_graph_matching_found() -> Result<()> { + verify_that!( + is_contained_in![ge(1), ge(3)].explain_match(&vec![1, 2]), + displays_as(eq(indoc!( + " + which does not have a subset match with the expected elements. The best match found was: + Actual element 1 at index 0 matched expected element `is greater than or equal to 1` at index 0. + Actual element 2 at index 1 did not match any remaining expected element. + Expected element `is greater than or equal to 3` at index 1 did not match any remaining actual element.")) + )) +} |