├── .gitignore ├── rustfmt.toml ├── .github ├── workflows │ ├── publish.yml │ ├── ci.yml │ └── scorecard.yml └── dependabot.yml ├── Cargo.toml ├── src ├── assertions │ ├── mod.rs │ ├── boolean.rs │ ├── anyhow.rs │ ├── cow.rs │ ├── basic.rs │ ├── option.rs │ ├── testing.rs │ ├── float.rs │ ├── string.rs │ ├── result.rs │ ├── vec.rs │ ├── set.rs │ └── iterator.rs ├── testing.rs ├── lib.rs ├── diff.rs └── base.rs ├── CONTRIBUTING.md ├── README.md ├── how_to_extend.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | wrap_comments = true 2 | comment_width = 100 -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | permissions: read-all 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | publish: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v5 19 | - name: Publish release of assertor 20 | env: 21 | CARGO_REGISTRY_TOKEN: ${{secrets.CARGO_REGISTRY_TOKEN}} 22 | run: cargo publish -p assertor 23 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "assertor" 3 | version = "0.0.4" 4 | description = "Fluent assertion library with readable failure messages." 5 | license = "Apache-2.0" 6 | repository = "https://github.com/google/assertor" 7 | readme = "README.md" 8 | edition = "2021" 9 | rust-version = "1.67.0" 10 | 11 | keywords = ["assert", "assertions", "testing", "unit"] 12 | categories = ["development-tools::testing", "development-tools::debugging"] 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [dependencies] 17 | num-traits = { version = "0.2.15", optional = true } 18 | anyhow = { version = "1.0.86", optional = true } 19 | 20 | [dev-dependencies] 21 | test-case = "3.1.0" 22 | 23 | [features] 24 | default = ["float"] 25 | float = ["dep:num-traits"] 26 | testing = [] 27 | anyhow = ["dep:anyhow"] 28 | -------------------------------------------------------------------------------- /src/assertions/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | pub mod basic; 16 | pub mod boolean; 17 | pub mod cow; 18 | pub mod iterator; 19 | pub mod map; 20 | pub mod option; 21 | pub mod result; 22 | pub mod set; 23 | pub mod string; 24 | pub mod vec; 25 | 26 | #[cfg(feature = "float")] 27 | pub mod float; 28 | 29 | #[cfg(feature = "anyhow")] 30 | pub mod anyhow; 31 | 32 | #[cfg(any(test, doc, feature = "testing"))] 33 | pub(crate) mod testing; 34 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code Reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows [Google's Open Source Community 28 | Guidelines](https://opensource.google/conduct/). 29 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} 11 | cancel-in-progress: true 12 | 13 | permissions: read-all 14 | 15 | env: 16 | CARGO_TERM_COLOR: always 17 | 18 | jobs: 19 | clippy-rustfmt: 20 | runs-on: ubuntu-latest 21 | name: clippy / ${{ matrix.toolchain }} 22 | permissions: 23 | contents: read 24 | checks: write 25 | strategy: 26 | fail-fast: false 27 | matrix: 28 | toolchain: [ stable, nightly ] 29 | steps: 30 | - uses: actions/checkout@v5 31 | - name: Install ${{ matrix.toolchain }} 32 | uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b 33 | with: 34 | toolchain: ${{ matrix.toolchain }} 35 | components: clippy, rustfmt 36 | - name: cargo clippy 37 | uses: actions-rs/clippy-check@v1.0.7 38 | with: 39 | token: ${{ secrets.GITHUB_TOKEN }} 40 | 41 | test: 42 | runs-on: ubuntu-latest 43 | name: test / ubuntu / ${{ matrix.toolchain }} 44 | strategy: 45 | matrix: 46 | toolchain: [ stable, nightly ] 47 | steps: 48 | - uses: actions/checkout@v5 49 | - name: Install ${{ matrix.toolchain }} 50 | uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b 51 | with: 52 | toolchain: ${{ matrix.toolchain }} 53 | - name: cargo generate-lockfile 54 | if: hashFiles('Cargo.lock') == '' 55 | run: cargo generate-lockfile 56 | - name: cargo test --locked 57 | run: cargo test --locked --all-features 58 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | rebase-strategy: disabled 8 | commit-message: 9 | prefix: ":seedling:" 10 | open-pull-requests-limit: 3 11 | - package-ecosystem: gomod 12 | directory: "/tools" 13 | schedule: 14 | interval: daily 15 | rebase-strategy: disabled 16 | commit-message: 17 | prefix: ":seedling:" 18 | open-pull-requests-limit: 3 19 | - package-ecosystem: "github-actions" 20 | directory: "/" 21 | schedule: 22 | interval: "daily" 23 | rebase-strategy: disabled 24 | commit-message: 25 | prefix: ":seedling:" 26 | - package-ecosystem: docker 27 | directory: "/" 28 | schedule: 29 | interval: weekly 30 | rebase-strategy: disabled 31 | commit-message: 32 | prefix: ":seedling:" 33 | open-pull-requests-limit: 3 34 | - package-ecosystem: docker 35 | directory: "/cron/internal/bq" 36 | schedule: 37 | interval: weekly 38 | rebase-strategy: disabled 39 | commit-message: 40 | prefix: ":seedling:" 41 | open-pull-requests-limit: 3 42 | - package-ecosystem: docker 43 | directory: "/cron/internal/worker" 44 | schedule: 45 | interval: weekly 46 | rebase-strategy: disabled 47 | commit-message: 48 | prefix: ":seedling:" 49 | - package-ecosystem: docker 50 | directory: "/cron/internal/webhook" 51 | schedule: 52 | interval: weekly 53 | rebase-strategy: disabled 54 | commit-message: 55 | prefix: ":seedling:" 56 | - package-ecosystem: docker 57 | directory: "/cron/internal/controller" 58 | schedule: 59 | interval: weekly 60 | rebase-strategy: disabled 61 | commit-message: 62 | prefix: ":seedling:" 63 | - package-ecosystem: docker 64 | directory: "/cron/internal/cii" 65 | schedule: 66 | interval: weekly 67 | rebase-strategy: disabled 68 | commit-message: 69 | prefix: ":seedling:" 70 | - package-ecosystem: docker 71 | directory: "/clients/githubrepo/roundtripper/tokens/server" 72 | schedule: 73 | interval: weekly 74 | rebase-strategy: disabled 75 | commit-message: 76 | prefix: ":seedling:" 77 | - package-ecosystem: "cargo" 78 | directory: "/" 79 | schedule: 80 | interval: "weekly" 81 | rebase-strategy: "disabled" 82 | -------------------------------------------------------------------------------- /src/testing.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | pub use crate::assertions::testing::CheckThatResultAssertion; 16 | pub use crate::{assert_that, check_that, Fact}; 17 | use crate::{AssertionResult, AssertionStrategy}; 18 | 19 | /// *Only for library developers.* An assertion macro to get the result of assertion without 20 | /// throwing panic. Expected to be used for testing assertion library. 21 | /// 22 | /// # Example 23 | /// 24 | /// ```ignore 25 | /// use assertor::*; 26 | /// use assertor::testing::*; 27 | /// 28 | /// assert_that!(check_that!("actual_string").is_same_string_to("expected_string")).facts_are(vec![ 29 | /// Fact::new("expected", "expected_string"), 30 | /// Fact::new("actual", "actual_string"), 31 | /// ]); 32 | /// ``` 33 | #[macro_export] 34 | macro_rules! check_that { 35 | ($actual:expr) => { 36 | $crate::Subject::new( 37 | &$actual, 38 | stringify!($actual).to_string(), 39 | /* description= */ None, 40 | /* option= */ (), 41 | Some($crate::Location::new( 42 | file!().to_string(), 43 | line!(), 44 | column!(), 45 | )), 46 | std::marker::PhantomData::<$crate::testing::CheckThatResult>, 47 | ) 48 | }; 49 | } 50 | 51 | /// Data structure that contains assertion result and messages. 52 | pub struct CheckThatResult(Result<(), AssertionResult>); 53 | 54 | impl AssertionStrategy for AssertionResult { 55 | fn do_fail(self) -> CheckThatResult { 56 | CheckThatResult(Err(self)) 57 | } 58 | 59 | fn do_ok(self) -> CheckThatResult { 60 | // XXX: Unnecessary AssertionResult instantiation for ok cases. 61 | CheckThatResult(Ok(())) 62 | } 63 | } 64 | 65 | impl AsRef> for CheckThatResult { 66 | fn as_ref(&self) -> &Result<(), AssertionResult> { 67 | &self.0 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/assertions/boolean.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use crate::base::{AssertionApi, AssertionResult, AssertionStrategy, Subject}; 16 | 17 | /// Trait for boolean assertion. 18 | /// 19 | /// # Example 20 | /// ``` 21 | /// use assertor::*; 22 | /// 23 | /// assert_that!(true).is_true(); 24 | /// assert_that!(false).is_false(); 25 | /// ``` 26 | pub trait BooleanAssertion { 27 | /// Checks that the subject is equal to `true`. 28 | #[track_caller] 29 | fn is_true(&self) -> R; 30 | 31 | /// Checks that the subject is equal to `false`. 32 | #[track_caller] 33 | fn is_false(&self) -> R; 34 | } 35 | 36 | impl BooleanAssertion for Subject<'_, bool, (), R> 37 | where 38 | AssertionResult: AssertionStrategy, 39 | { 40 | fn is_true(&self) -> R { 41 | if *self.actual() { 42 | self.new_result().do_ok() 43 | } else { 44 | self.new_result() 45 | .add_simple_fact("expected true") 46 | .add_simple_fact("but actual was false") 47 | .do_fail() 48 | } 49 | } 50 | 51 | fn is_false(&self) -> R { 52 | if !self.actual() { 53 | self.new_result().do_ok() 54 | } else { 55 | self.new_result() 56 | .add_simple_fact("expected false") 57 | .add_simple_fact("but actual was true") 58 | .do_fail() 59 | } 60 | } 61 | } 62 | 63 | #[cfg(test)] 64 | mod tests { 65 | use crate::testing::*; 66 | 67 | use super::*; 68 | 69 | #[test] 70 | fn is_true() { 71 | assert_that!(true).is_true(); 72 | 73 | assert_that!(check_that!(false).is_true()).facts_are(vec![ 74 | Fact::new_simple_fact("expected true"), 75 | Fact::new_simple_fact("but actual was false"), 76 | ]) 77 | } 78 | 79 | #[test] 80 | fn is_false() { 81 | assert_that!(false).is_false(); 82 | 83 | assert_that!(check_that!(true).is_false()).facts_are(vec![ 84 | Fact::new_simple_fact("expected false"), 85 | Fact::new_simple_fact("but actual was true"), 86 | ]) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Assertor 2 | ======== 3 | Assertor makes test assertions and failure messages more human-readable. 4 | 5 | [![crates.io](https://img.shields.io/crates/v/assertor.svg)](https://crates.io/crates/assertor) 6 | [![license](https://img.shields.io/github/license/google/assertor.svg)](https://github.com/google/assertor/LICENSE) 7 | [![docs.rs](https://docs.rs/assertor/badge.svg)](https://docs.rs/crate/assertor/) 8 | [![OpenSSF 9 | Scorecard](https://api.securityscorecards.dev/projects/github.com/google/assertor/badge)](https://api.securityscorecards.dev/projects/github.com/google/assertor) 10 | 11 | Assertor is heavily affected by [Java Truth](https://github.com/google/truth) in terms of API design and error messages, 12 | but this is a totally different project from Java Truth. 13 | 14 | ### Disclaimer 15 | 16 | This is not an official Google product, it is just code that happens to be owned by Google. 17 | 18 | ⚠ The API is not fully stable and may be changed until version 1.0. 19 | 20 | Example 21 | ------- 22 | 23 | ```rust 24 | use assertor::*; 25 | 26 | #[test] 27 | fn test_it() { 28 | assert_that!("foobarbaz").contains("bar"); 29 | assert_that!("foobarbaz").ends_with("baz"); 30 | 31 | assert_that!(0.5).with_abs_tol(0.2).is_approx_equal_to(0.6); 32 | 33 | assert_that!(vec!["a", "b"]).contains("a"); 34 | assert_that!(vec!["a", "b"]).has_length(2); 35 | assert_that!(vec!["a", "b"]).contains_exactly(vec!["a", "b"]); 36 | 37 | assert_that!(Option::Some("Foo")).has_value("Foo"); 38 | } 39 | ``` 40 | 41 | ### Failure cases 42 | 43 | ```rust 44 | use assertor::*; 45 | 46 | fn test_it() { 47 | assert_that!(vec!["a", "b", "c"]).contains_exactly(vec!["b", "c", "d"]); 48 | // missing (1) : ["d"] 49 | // unexpected (1): ["a"] 50 | // --- 51 | // expected : ["b", "c", "d"] 52 | // actual : ["a", "b", "c"] 53 | } 54 | ``` 55 | 56 | ## anyhow 57 | 58 | Supports asserting error value of `anyhow` under `anyhow` feature flag. 59 | 60 | ```toml 61 | [dependencies] 62 | assertor = { version = "*", features = ["anyhow"] } 63 | ``` 64 | 65 | ```rust 66 | use assertor::*; 67 | use anyhow::anyhow; 68 | 69 | fn anyhow_func() -> anyhow::Result<()> { 70 | Err(anyhow!("failed to parse something in foobar")) 71 | } 72 | 73 | fn test_it() { 74 | assert_that!(anyhow_func()).err().has_message("failed to parse something in foobar"); 75 | assert_that!(anyhow_func()).err().as_string().contains("parse something"); 76 | assert_that!(anyhow_func()).err().as_string().starts_with("failed"); 77 | assert_that!(anyhow_func()).err().as_string().ends_with("foobar"); 78 | } 79 | ``` 80 | 81 | ## Feature ideas 82 | 83 | - [ ] Color / Bold 84 | - [ ] Better diff: vec 85 | - [ ] Better diff: set 86 | - [ ] Better diff: HashMap 87 | 88 | 89 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Assertor makes test assertions and failure messages more human-readable. 16 | //! 17 | //! Assertor is heavy affected by [Java Truth](https://github.com/google/truth) in terms of API 18 | //! design and error messages. 19 | //! 20 | //! # Example 21 | //! ``` 22 | //! use assertor::*; 23 | //! 24 | //! assert_that!("foobarbaz").contains("bar"); 25 | //! assert_that!("foobarbaz").ends_with("baz"); 26 | //! 27 | //! assert_that!(0.5).with_abs_tol(0.2).is_approx_equal_to(0.6); 28 | //! 29 | //! assert_that!(vec!["a", "b"]).contains("a"); 30 | //! assert_that!(vec!["a", "b"]).has_length(2); 31 | //! assert_that!(vec!["a", "b"]).contains_exactly(vec!["a", "b"]); 32 | //! 33 | //! assert_that!(Option::Some("Foo")).has_value("Foo"); 34 | //! ``` 35 | //! 36 | //! ## Failure cases 37 | //! ```should_panic 38 | //! use assertor::*; 39 | //! assert_that!(vec!["a", "b", "c"]).contains_exactly(vec!["b", "c", "d"]); 40 | //! // missing (1) : ["d"] 41 | //! // unexpected (1): ["a"] 42 | //! // --- 43 | //! // expected : ["b", "c", "d"] 44 | //! // actual : ["a", "b", "c"] 45 | //! ``` 46 | #![warn(missing_docs)] 47 | 48 | #[cfg(feature = "float")] 49 | extern crate num_traits; 50 | 51 | #[cfg(feature = "anyhow")] 52 | pub use assertions::anyhow::AnyhowErrorAssertion; 53 | pub use assertions::basic::{ComparableAssertion, EqualityAssertion}; 54 | pub use assertions::boolean::BooleanAssertion; 55 | pub use assertions::cow::CowAssertion; 56 | #[cfg(feature = "float")] 57 | pub use assertions::float::FloatAssertion; 58 | pub use assertions::iterator::IteratorAssertion; 59 | pub use assertions::map::MapAssertion; 60 | pub use assertions::map::OrderedMapAssertion; 61 | pub use assertions::option::OptionAssertion; 62 | pub use assertions::result::ResultAssertion; 63 | pub use assertions::set::OrderedSetAssertion; 64 | pub use assertions::set::SetAssertion; 65 | pub use assertions::string::StringAssertion; 66 | pub use assertions::vec::VecAssertion; 67 | pub use base::{AssertionResult, AssertionStrategy, Fact, Location, Subject}; 68 | 69 | mod assertions; 70 | mod base; 71 | mod diff; 72 | 73 | /// Module for testing the assertor library itself. Expected to be used by library developers. 74 | #[cfg(any(test, doc, feature = "testing"))] 75 | pub mod testing; 76 | -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are provided 2 | # by a third-party and are governed by separate terms of service, privacy 3 | # policy, and support documentation. 4 | 5 | name: Scorecard supply-chain security 6 | on: 7 | # For Branch-Protection check. Only the default branch is supported. See 8 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 9 | branch_protection_rule: 10 | # To guarantee Maintained check is occasionally updated. See 11 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 12 | schedule: 13 | - cron: '42 22 * * 2' 14 | push: 15 | branches: [ "master" ] 16 | 17 | # Declare default permissions as read only. 18 | permissions: read-all 19 | 20 | jobs: 21 | analysis: 22 | name: Scorecard analysis 23 | runs-on: ubuntu-latest 24 | permissions: 25 | # Needed to upload the results to code-scanning dashboard. 26 | security-events: write 27 | # Needed to publish results and get a badge (see publish_results below). 28 | id-token: write 29 | # Uncomment the permissions below if installing in a private repository. 30 | # contents: read 31 | # actions: read 32 | 33 | steps: 34 | - name: "Checkout code" 35 | uses: actions/checkout@v5 # v3.5.2 36 | with: 37 | persist-credentials: false 38 | 39 | - name: "Run analysis" 40 | uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2 41 | with: 42 | results_file: results.sarif 43 | results_format: sarif 44 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 45 | # - you want to enable the Branch-Protection check on a *public* repository, or 46 | # - you are installing Scorecard on a *private* repository 47 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. 48 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 49 | 50 | # Public repositories: 51 | # - Publish results to OpenSSF REST API for easy access by consumers 52 | # - Allows the repository to include the Scorecard badge. 53 | # - See https://github.com/ossf/scorecard-action#publishing-results. 54 | # For private repositories: 55 | # - `publish_results` will always be set to `false`, regardless 56 | # of the value entered here. 57 | publish_results: true 58 | 59 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 60 | # format to the repository Actions tab. 61 | - name: "Upload artifact" 62 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 63 | with: 64 | name: SARIF file 65 | path: results.sarif 66 | retention-days: 5 67 | 68 | # Upload the results to GitHub's code scanning dashboard. 69 | - name: "Upload to code-scanning" 70 | uses: github/codeql-action/upload-sarif@3c3833e0f8c1c83d449a7478aa59c036a9165498 # v3.29.11 71 | with: 72 | sarif_file: results.sarif 73 | -------------------------------------------------------------------------------- /how_to_extend.md: -------------------------------------------------------------------------------- 1 | How to extend 2 | ============= 3 | 4 | ## 1. Write your own Assertion trait. 5 | - S: Subject type (e.g. HashSet) 6 | - R: Return type of assertion 7 | - For `assert_that!` macro, `R` is `()` (or panic when the assertion fails). 8 | - For `check_that!` macro, `R` is `CheckThatResult`. 9 | 10 | ```rust 11 | trait SetAssertion { 12 | fn contains>(&self, expected: B) -> R; 13 | fn is_equal_to>(&self, expected: S) -> R; 14 | } 15 | ``` 16 | 17 | ## 2. Write implementation for `Subject`. 18 | 19 | ```rust 20 | impl SetAssertion, T, R> for Subject, (), R> { 21 | fn contains>(&self, expected: B) -> R { 22 | if (self.actual().contains(expected)) { 23 | self.new_result().do_ok() 24 | } else { 25 | self.new_result() 26 | .add_fact("expected to contain", "XXX") 27 | .add_fact(...) 28 | .do_fail() 29 | } 30 | } 31 | } 32 | ``` 33 | 34 | ## 3. Write tests. 35 | 36 | ```rust 37 | #[cfg(test)] 38 | mod tests { 39 | use std::iter::FromIterator; 40 | 41 | use crate::*; 42 | 43 | use super::*; 44 | 45 | #[test] 46 | fn contains() { 47 | assert_that!(HashSet::from_iter(vec![1, 2, 3].iter())).contains(&3); 48 | 49 | // Failures 50 | let result = check_that!(HashSet::from_iter(vec![1, 2, 3].iter())).contains(&10); 51 | assert_that!(result).facts_are_at_least(vec![ 52 | Fact::new("expected to contain", "10"), 53 | Fact::new_simple_fact("but did not"), 54 | ]); 55 | assert_that!(result) 56 | .fact_keys() 57 | .contains(&"though it did contain".to_string()); 58 | // Skip test for value because key order is not stable. 59 | } 60 | } 61 | ``` 62 | 63 | Do and Dont 64 | =========== 65 | 66 | DONT: Method chain of multiple independent assertions 67 | ----------------------------------------------------- 68 | 69 | ```rust 70 | assert_that!(3).is_less_than(10).is_larger_than(1); 71 | assert_that!(vec![1,2,3]).contains_exactly(3, 2, 1).in_order(); // Java Truth like 72 | ``` 73 | 74 | ### DO: 75 | 76 | ##### Recommended: Independent assertions. 77 | 78 | ```rust 79 | assert_that!(3).is_less_than(10); 80 | assert_that!(3).is_larger_than(1); 81 | ``` 82 | 83 | ##### Ok: Define one assertion method to check both. 84 | 85 | ```rust 86 | assert_that!(3).is_in(1, 10); 87 | assert_that!(vec![1,2,3]).contains_exactly_in_order(3, 2, 1); 88 | ``` 89 | 90 | ### Why? 91 | 92 | - Readability 93 | - One `assert_that!()` should check one assertion for readability. 94 | - Design complication 95 | - Return type of `assert_that!(3).is_less_than(10)` will be complicated like `Result, R>`. 96 | 97 | DO: Derived subjects 98 | -------------------- 99 | 100 | ```rust 101 | assert_that!(hash_map).key_set().contains( & "foo"); 102 | assert_that!(hash_map).key_set().contains_exactly( & "foo", & "bar"); 103 | ``` 104 | 105 | It is one option not to use derived subject when the assertion method is expected to be used frequently and when the api 106 | of derived subject is hard to use (`&` in argument in previous example). 107 | 108 | ##### Ok 109 | 110 | ```rust 111 | assert_that!(hash_map).contains_key("foo"); 112 | assert_that!(hash_map).contains_key("bar"); 113 | ``` 114 | 115 | ### Why? 116 | 117 | - API simplicity 118 | - To avoid having many method having `key_set` prefix ( 119 | ex. `assert_that!(..).key_set_contains(), assert_that!(..).key_set_contains_exactly()`) 120 | - Code duplication 121 | - Derived subject enables to reuse the existing implementation easily. 122 | 123 | ### What is `derived subject`? 124 | 125 | A feature to create a new subject whose type is different from the original subject type. 126 | 127 | - `Subject.new_subject(..)` 128 | - `Subject.new_owned_subject(..)` 129 | 130 | ```rust 131 | 132 | impl<'a, K, V, R> MapAssertion<'a, K, V, R> for Subject<'a, HashMap, (), R> 133 | where 134 | AssertionResult: ReturnStrategy, 135 | { 136 | fn key_set(&self) -> Subject<'a, Keys, (), R> { // <- Returns new subject whose type is Keys<> from HashMap<> 137 | self.new_owned_subject( 138 | self.actual().keys(), 139 | Some(format!("{}.keys()", self.description_or_expr())), 140 | (), 141 | ) 142 | } 143 | } 144 | ``` -------------------------------------------------------------------------------- /src/assertions/anyhow.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 16 | use crate::base::{AssertionApi, AssertionResult, AssertionStrategy, Subject}; 17 | use crate::StringAssertion; 18 | 19 | /// Trait for anyhow error assertion. 20 | /// 21 | /// # Example 22 | /// 23 | /// ```rust 24 | /// use assertor::*; 25 | /// use anyhow::anyhow; 26 | /// 27 | /// fn anyhow_func() -> anyhow::Result<()> { 28 | /// Err(anyhow!("failed to parse something in foobar")) 29 | /// } 30 | /// 31 | /// fn test_it() { 32 | /// assert_that!(anyhow_func()).err().has_message("failed to parse something in foobar"); 33 | /// assert_that!(anyhow_func()).err().as_string().contains("parse something"); 34 | /// assert_that!(anyhow_func()).err().as_string().starts_with("failed"); 35 | /// assert_that!(anyhow_func()).err().as_string().ends_with("foobar"); 36 | /// } 37 | /// ``` 38 | pub trait AnyhowErrorAssertion { 39 | /// Returns a new `String` subject which is the message of the error. 40 | /// 41 | /// Related: [`StringAssertion`](crate::StringAssertion) 42 | /// 43 | /// ``` 44 | /// use assertor::*; 45 | /// use anyhow::anyhow; 46 | /// 47 | /// assert_that!(anyhow!("error message")).as_string().is_same_string_to("error message"); 48 | /// assert_that!(anyhow!("error message")).as_string().contains("error"); 49 | /// 50 | /// 51 | /// fn some_func() -> anyhow::Result<()> { 52 | /// Err(anyhow!("error message")) 53 | /// } 54 | /// assert_that!(some_func()).err().as_string().starts_with("error"); 55 | /// assert_that!(some_func()).err().as_string().ends_with("message"); 56 | /// ``` 57 | fn as_string(&self) -> Subject; 58 | 59 | /// Checks that the error message contains `expected`. 60 | /// ``` 61 | /// use assertor::*; 62 | /// use anyhow::anyhow; 63 | /// 64 | /// assert_that!(anyhow!("error message")).has_message("error message"); 65 | /// 66 | /// fn some_func() -> anyhow::Result<()> { 67 | /// Err(anyhow!("error message")) 68 | /// } 69 | /// assert_that!(some_func()).err().has_message("error message") 70 | /// ``` 71 | #[track_caller] 72 | fn has_message>(&self, expected: E) -> R; 73 | } 74 | 75 | impl AnyhowErrorAssertion for Subject<'_, anyhow::Error, (), R> 76 | where 77 | AssertionResult: AssertionStrategy, 78 | { 79 | fn as_string(&self) -> Subject { 80 | let message = self.actual().to_string(); 81 | self.new_owned_subject(message, Some(format!("{}.to_string()", self.description_or_expr())), ()) 82 | } 83 | 84 | fn has_message>(&self, expected: E) -> R { 85 | self.as_string().is_same_string_to(expected) 86 | } 87 | } 88 | 89 | #[cfg(test)] 90 | mod tests { 91 | use crate::testing::*; 92 | 93 | use super::*; 94 | 95 | #[test] 96 | fn as_string() { 97 | assert_that!(anyhow::Error::msg("error message")).as_string().is_same_string_to("error message"); 98 | assert_that!(anyhow::Error::msg("error message")).as_string().starts_with("error"); 99 | assert_that!(anyhow::Error::msg("error message")).as_string().contains("or"); 100 | 101 | assert_that!(check_that!(anyhow::Error::msg("error message")).as_string().is_same_string_to("wrong")).facts_are( 102 | vec![ 103 | Fact::new("expected", "\"wrong\""), 104 | Fact::new("actual", "\"error message\""), 105 | ] 106 | ); 107 | } 108 | 109 | #[test] 110 | fn has_message() { 111 | assert_that!(anyhow::Error::msg("error message")).has_message("error message"); 112 | assert_that!(check_that!(anyhow::Error::msg("error message")).has_message("wrong")).facts_are( 113 | vec![ 114 | Fact::new("expected", "\"wrong\""), 115 | Fact::new("actual", "\"error message\""), 116 | ] 117 | ); 118 | } 119 | } -------------------------------------------------------------------------------- /src/assertions/cow.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::borrow::Cow; 16 | 17 | use crate::base::{AssertionApi, AssertionResult, AssertionStrategy, Subject}; 18 | 19 | /// Trait for Cow assertion. 20 | /// 21 | /// # Example 22 | /// ``` 23 | /// use std::borrow::Cow; 24 | /// use assertor::*; 25 | /// 26 | /// assert_that!(Cow::Borrowed("foobar")).is_borrowed(); 27 | /// assert_that!(Cow::::Owned("foobar".to_string())).is_owned(); 28 | /// assert_that!(Cow::::Owned("foobar".to_string())).deref().is_same_string_to("foobar"); 29 | /// ``` 30 | pub trait CowAssertion 31 | { 32 | /// Checks that the subject is [`Cow::Borrowed(_)`](`std::borrow::Cow::Borrowed`). 33 | #[track_caller] 34 | fn is_borrowed(&self) -> R; 35 | 36 | /// Checks that the subject is [`Cow::Owned(_)`](`std::borrow::Cow::Owned`). 37 | #[track_caller] 38 | fn is_owned(&self) -> R; 39 | 40 | /// Returns a new subject which is the dereferenced value of the subject. 41 | /// 42 | /// # Example 43 | /// ``` 44 | /// use std::borrow::Cow; 45 | /// use assertor::*; 46 | /// 47 | /// let owned: Cow = Cow::Owned("owned".to_string()); 48 | /// let borrowed: Cow = Cow::Borrowed("borrowed"); 49 | /// assert_that!(owned).deref().is_same_string_to("owned"); 50 | /// assert_that!(borrowed).deref().is_same_string_to("borrowed"); 51 | /// 52 | /// let cow_float_value: Cow = Cow::Owned(1.23); 53 | /// assert_that!(cow_float_value).deref().is_approx_equal_to(1.23); 54 | fn deref(&self) -> Subject; 55 | } 56 | 57 | impl<'a, T: ?Sized, Y, R> CowAssertion for Subject<'a, Cow<'a, T>, (), R> 58 | where 59 | T: ToOwned, 60 | AssertionResult: AssertionStrategy, 61 | { 62 | fn is_borrowed(&self) -> R { 63 | if matches!(self.actual(), Cow::Borrowed(_)) { 64 | self.new_result().do_ok() 65 | } else { 66 | self.new_result().add_simple_fact("expected borrowed, but actual was owned").do_fail() 67 | } 68 | } 69 | 70 | fn is_owned(&self) -> R { 71 | if matches!(self.actual(), Cow::Owned(_)) { 72 | self.new_result().do_ok() 73 | } else { 74 | self.new_result().add_simple_fact("expected owned, but actual was borrowed").do_fail() 75 | } 76 | } 77 | 78 | fn deref(&self) -> Subject { 79 | let value = self.actual().as_ref().to_owned(); 80 | self.new_owned_subject(value, Some(format!("{}.deref()", self.description_or_expr())), ()) 81 | } 82 | } 83 | 84 | #[cfg(test)] 85 | mod tests { 86 | use crate::*; 87 | use crate::testing::CheckThatResultAssertion; 88 | 89 | use super::*; 90 | 91 | #[test] 92 | fn is_borrowed() { 93 | assert_that!(Cow::Borrowed("foobar")).is_borrowed(); 94 | assert_that!(check_that!(Cow::::Owned("foobar".to_string())).is_borrowed()).facts_are( 95 | vec![ 96 | Fact::new_simple_fact("expected borrowed, but actual was owned") 97 | ] 98 | ); 99 | } 100 | 101 | #[test] 102 | fn is_owned() { 103 | assert_that!(Cow::::Owned("foobar".to_string())).is_owned(); 104 | assert_that!(check_that!(Cow::Borrowed("foobar")).is_owned()).facts_are( 105 | vec![ 106 | Fact::new_simple_fact("expected owned, but actual was borrowed") 107 | ] 108 | ); 109 | } 110 | 111 | #[test] 112 | fn value() { 113 | assert_that!(Cow::::Owned("foobar".to_string())).deref().is_same_string_to("foobar"); 114 | assert_that!(Cow::Borrowed("foobar")).deref().is_same_string_to("foobar"); 115 | 116 | let owned: Cow> = Cow::Owned(Some(42)); 117 | assert_that!(check_that!(owned).deref().is_none()).facts_are(vec![ 118 | Fact::new("value of", "owned.deref()"), 119 | Fact::new("expected", "None"), 120 | Fact::new("actual", "Some(42)"), 121 | ]); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/assertions/basic.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::borrow::Borrow; 16 | use std::fmt::Debug; 17 | 18 | use crate::base::{AssertionApi, AssertionResult, AssertionStrategy, Subject}; 19 | 20 | /// Trait for equality assertion. 21 | /// # Example 22 | /// ``` 23 | /// use assertor::*; 24 | /// assert_that!(1).is_equal_to(1); 25 | /// assert_that!(1).is_not_equal_to(2); 26 | /// ``` 27 | pub trait EqualityAssertion { 28 | /// Checks if the subject is equal to `expected`. 29 | #[track_caller] 30 | fn is_equal_to>(&self, expected: B) -> R; 31 | 32 | /// Checks if the subject value is NOT equal to `expected`. 33 | #[track_caller] 34 | fn is_not_equal_to>(&self, expected: B) -> R; 35 | } 36 | 37 | impl EqualityAssertion for Subject<'_, S, (), R> 38 | where 39 | AssertionResult: AssertionStrategy, 40 | { 41 | fn is_equal_to>(&self, expected: B) -> R { 42 | if self.actual().eq(expected.borrow()) { 43 | self.new_result().do_ok() 44 | } else { 45 | self.new_result() 46 | .add_fact("expected", format!("{:?}", expected.borrow())) 47 | .add_fact("actual", format!("{:?}", self.actual())) 48 | .do_fail() 49 | } 50 | } 51 | fn is_not_equal_to>(&self, expected: B) -> R { 52 | if !self.actual().ne(expected.borrow()) { 53 | self.new_result().do_fail() 54 | } else { 55 | self.new_result().do_ok() 56 | } 57 | } 58 | } 59 | 60 | /// Trait for comparison assertions. 61 | pub trait ComparableAssertion { 62 | /// Checks that the subject is greater than or equal to `expected`. 63 | #[track_caller] 64 | fn is_at_least>(&self, expected: B) -> R; 65 | 66 | /// Checks that the subject is less than or equal to `expected`. 67 | #[track_caller] 68 | fn is_at_most>(&self, expected: B) -> R; 69 | 70 | /// Checks that the subject is greater than `expected`. 71 | #[track_caller] 72 | fn is_greater_than>(&self, expected: B) -> R; 73 | 74 | /// Checks that the subject is less than `expected`. 75 | #[track_caller] 76 | fn is_less_than>(&self, expected: B) -> R; 77 | } 78 | 79 | impl ComparableAssertion for Subject<'_, S, (), R> 80 | where 81 | AssertionResult: AssertionStrategy, 82 | { 83 | fn is_at_least>(&self, expected: B) -> R { 84 | if self.actual().ge(expected.borrow()) { 85 | self.new_result().do_ok() 86 | } else { 87 | self.new_result() 88 | .add_fact("expected", format!("{:?}", self.actual())) 89 | .add_fact("to be at least", format!("{:?}", expected.borrow())) 90 | .do_fail() 91 | } 92 | } 93 | 94 | fn is_at_most>(&self, expected: B) -> R { 95 | if self.actual().le(expected.borrow()) { 96 | self.new_result().do_ok() 97 | } else { 98 | self.new_result() 99 | .add_fact("expected", format!("{:?}", self.actual())) 100 | .add_fact("to be at most", format!("{:?}", expected.borrow())) 101 | .do_fail() 102 | } 103 | } 104 | 105 | fn is_greater_than>(&self, expected: B) -> R { 106 | if self.actual().gt(expected.borrow()) { 107 | self.new_result().do_ok() 108 | } else { 109 | self.new_result() 110 | .add_fact("expected", format!("{:?}", self.actual())) 111 | .add_fact("to be greater than", format!("{:?}", expected.borrow())) 112 | .do_fail() 113 | } 114 | } 115 | 116 | fn is_less_than>(&self, expected: B) -> R { 117 | if self.actual().lt(expected.borrow()) { 118 | self.new_result().do_ok() 119 | } else { 120 | self.new_result() 121 | .add_fact("expected", format!("{:?}", self.actual())) 122 | .add_fact("to be less than", format!("{:?}", expected.borrow())) 123 | .do_fail() 124 | } 125 | } 126 | } 127 | 128 | #[cfg(test)] 129 | mod tests { 130 | use crate::testing::*; 131 | 132 | use super::*; 133 | 134 | #[test] 135 | fn is_equal_to() { 136 | assert_that!(1).is_equal_to(1); 137 | assert_that!(2).is_equal_to(2); 138 | assert_that!(vec![1]).is_equal_to(vec![1]); 139 | 140 | // failures 141 | } 142 | 143 | #[test] 144 | fn is_equal_to_error_message() { 145 | let result = check_that!(1).is_equal_to(3); 146 | 147 | assert_that!(result).facts_are(vec![Fact::new("expected", "3"), Fact::new("actual", "1")]) 148 | } 149 | 150 | #[test] 151 | fn is_not_equal_to() { 152 | assert_that!(1).is_not_equal_to(2); 153 | assert_that!(2).is_not_equal_to(1); 154 | assert_that!(vec![1]).is_not_equal_to(vec![]); 155 | assert_that!(vec![1]).is_not_equal_to(vec![2]); 156 | } 157 | 158 | #[test] 159 | fn is_at_least() { 160 | assert_that!(2).is_at_least(1); 161 | assert_that!(2).is_at_least(2); 162 | assert_that!(2_f32).is_at_least(1.); 163 | 164 | assert_that!(check_that!(2).is_at_least(3)).facts_are(vec![ 165 | Fact::new("expected", "2"), 166 | Fact::new("to be at least", "3"), 167 | ]); 168 | assert_that!(check_that!(2).is_at_most(1)).facts_are(vec![ 169 | Fact::new("expected", "2"), 170 | Fact::new("to be at most", "1"), 171 | ]); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/assertions/option.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::borrow::Borrow; 16 | use std::fmt::Debug; 17 | 18 | use crate::{AssertionResult, AssertionStrategy, Subject}; 19 | use crate::base::AssertionApi; 20 | 21 | /// Trait for option assertion. 22 | /// 23 | /// # Example 24 | /// ``` 25 | /// use assertor::*; 26 | /// 27 | /// assert_that!(Option::Some(1)).has_value(1); 28 | /// assert_that!(Option::Some(1)).is_some(); 29 | /// assert_that!(Option::::None).is_none(); 30 | /// assert_that!(Option::Some("foobar")).some().starts_with("foo"); 31 | /// ``` 32 | pub trait OptionAssertion 33 | where 34 | AssertionResult: AssertionStrategy, 35 | { 36 | /// Checks the subject is [`Option::None`]. 37 | #[track_caller] 38 | fn is_none(&self) -> R 39 | where 40 | T: PartialEq + Debug; 41 | 42 | /// Checks the subject is [`Option::Some(_)`](`Option::Some`). 43 | #[track_caller] 44 | fn is_some(&self) -> R 45 | where 46 | T: PartialEq + Debug; 47 | 48 | /// Checks the subject is [`Option::Some(expected)`](`Option::Some`). 49 | #[track_caller] 50 | fn has_value(&self, expected: B) -> R 51 | where 52 | B: Borrow, 53 | T: PartialEq + Debug; 54 | 55 | /// Returns a new subject which is the value of the subject if the subject is [`Option::Some(_)`](`Option::Some`). Otherwise, it fails. 56 | /// 57 | /// # Example 58 | /// ``` 59 | /// use assertor::*; 60 | /// 61 | /// let value = Option::Some("foobar"); 62 | /// assert_that!(value).some().starts_with("foo"); 63 | /// assert_that!(value).some().ends_with("bar"); 64 | /// ``` 65 | #[track_caller] 66 | fn some(&self) -> Subject 67 | where 68 | T: PartialEq + Debug; 69 | } 70 | 71 | impl OptionAssertion for Subject<'_, Option, (), R> 72 | where 73 | AssertionResult: AssertionStrategy, 74 | { 75 | fn is_none(&self) -> R 76 | where 77 | T: PartialEq + Debug, 78 | { 79 | match self.actual() { 80 | None => self.new_result().do_ok(), 81 | Some(actual) => self 82 | .new_result() 83 | .add_fact("expected", "None") 84 | .add_fact("actual", format!("Some({:?})", actual)) 85 | .do_fail(), 86 | } 87 | } 88 | 89 | fn is_some(&self) -> R 90 | where 91 | T: PartialEq + Debug, 92 | { 93 | match self.actual() { 94 | None => self 95 | .new_result() 96 | .add_fact("expected", "Some(_)") 97 | .add_fact("actual", "None") 98 | .do_fail(), 99 | Some(_) => self.new_result().do_ok(), 100 | } 101 | } 102 | 103 | fn has_value(&self, expected: B) -> R 104 | where 105 | B: Borrow, 106 | T: PartialEq + Debug, 107 | { 108 | match self.actual() { 109 | Some(actual) if expected.borrow().eq(actual) => self.new_result().do_ok(), 110 | Some(actual) => self 111 | .new_result() 112 | .add_fact("expected", format!("Some({:?})", expected.borrow())) 113 | .add_fact("actual", format!("Some({:?})", actual)) 114 | .do_fail(), 115 | None => self 116 | .new_result() 117 | .add_fact("expected", format!("Some({:?})", expected.borrow())) 118 | .add_fact("actual", "None") 119 | .do_fail(), 120 | } 121 | } 122 | 123 | fn some(&self) -> Subject 124 | where 125 | T: PartialEq + Debug, 126 | { 127 | let value = self.actual().as_ref().expect("Expected Some(_) but was None"); 128 | self.new_subject(value, None, ()) 129 | } 130 | } 131 | 132 | #[cfg(test)] 133 | mod tests { 134 | use crate::testing::*; 135 | 136 | use super::*; 137 | 138 | #[test] 139 | fn is_none() { 140 | let none: Option = Option::None; 141 | let one: Option = Option::from(1); 142 | assert_that!(none).is_none(); 143 | assert_that!(check_that!(one).is_none()).facts_are(vec![ 144 | Fact::new("expected", "None"), 145 | Fact::new("actual", "Some(1)"), 146 | ]); 147 | assert_that!(check_that!(Option::Some("some")).is_none()).facts_are(vec![ 148 | Fact::new("expected", "None"), 149 | Fact::new("actual", r#"Some("some")"#), 150 | ]); 151 | } 152 | 153 | #[test] 154 | fn is_some() { 155 | let none: Option = Option::None; 156 | let one: Option = Option::from(1); 157 | assert_that!(one).is_some(); 158 | assert_that!(check_that!(none).is_some()).facts_are(vec![ 159 | Fact::new("expected", "Some(_)"), 160 | Fact::new("actual", "None"), 161 | ]); 162 | } 163 | 164 | #[test] 165 | fn has_value() { 166 | let none: Option = Option::None; 167 | let one: Option = Option::from(1); 168 | assert_that!(one).has_value(1); 169 | assert_that!(Option::from("")).has_value(""); 170 | 171 | assert_that!(check_that!(none).has_value(1)).facts_are(vec![ 172 | Fact::new("expected", "Some(1)"), 173 | Fact::new("actual", "None"), 174 | ]); 175 | assert_that!(check_that!(one).has_value(2)).facts_are(vec![ 176 | Fact::new("expected", "Some(2)"), 177 | Fact::new("actual", "Some(1)"), 178 | ]); 179 | assert_that!(check_that!(Option::from("1")).has_value("2")).facts_are(vec![ 180 | Fact::new("expected", r#"Some("2")"#), 181 | Fact::new("actual", r#"Some("1")"#), 182 | ]); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/assertions/testing.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::borrow::Borrow; 16 | use std::collections::HashSet; 17 | 18 | use crate::assertions::iterator::IteratorAssertion; 19 | use crate::base::{AssertionApi, AssertionResult, AssertionStrategy, Fact, Subject}; 20 | use crate::testing::CheckThatResult; 21 | 22 | /// Trait for assertions for assertion messages. 23 | /// 24 | /// # Example 25 | /// 26 | /// ```ignore 27 | /// use assertor::*; 28 | /// use assertor::testing::*; 29 | /// 30 | /// assert_that!(check_that!("actual_string").is_same_string_to("expected_string")).facts_are(vec![ 31 | /// Fact::new("expected", "expected_string"), 32 | /// Fact::new("actual", "actual_string"), 33 | /// ]); 34 | /// ``` 35 | pub trait CheckThatResultAssertion<'a, R> { 36 | /// Checks that the assertion result contains elements of `facts` in order. 37 | #[track_caller] 38 | fn facts_are>>(&self, facts: B) -> R; 39 | 40 | /// Checks that the assertion result contains elements of `facts` in order. 41 | #[track_caller] 42 | fn facts_are_at_least>>(&self, facts: B) -> R; 43 | 44 | /// Returns the first fact value whose key is equal to `key`. 45 | #[track_caller] 46 | fn fact_value_for_key>(&self, key: I) -> Subject; 47 | 48 | /// Returns keys of the assertion messages. 49 | #[track_caller] 50 | fn fact_keys(&self) -> Subject<'a, HashSet<&String>, (), R>; 51 | } 52 | 53 | fn get_assertion_result<'a, 'o, R>( 54 | subject: &'o Subject<'a, CheckThatResult, (), R>, 55 | ) -> &'o AssertionResult { 56 | subject 57 | .actual() 58 | .as_ref() 59 | .as_ref() 60 | // TODO: Improve error message; should have line-no. 61 | .expect_err("Expected Err but got Ok because this is assertion for error message.") 62 | } 63 | 64 | impl<'a, R> CheckThatResultAssertion<'a, R> for Subject<'a, CheckThatResult, (), R> 65 | where 66 | AssertionResult: AssertionStrategy, 67 | { 68 | fn facts_are>>(&self, expected: B) -> R { 69 | self.new_owned_subject( 70 | get_assertion_result(self).facts().iter(), 71 | Some(format!("{}.facts()", self.description_or_expr())), 72 | (), 73 | ) 74 | .contains_exactly_in_order(expected.borrow().iter()) 75 | } 76 | 77 | fn facts_are_at_least>>(&self, facts: B) -> R { 78 | self.new_owned_subject( 79 | get_assertion_result(self).facts().iter(), 80 | Some(format!("{}.facts()", self.description_or_expr())), 81 | (), 82 | ) 83 | .contains_all_of_in_order(facts.borrow().iter()) 84 | } 85 | 86 | fn fact_value_for_key>(&self, key: I) -> Subject { 87 | let key_str = key.into(); 88 | let assertion_result = get_assertion_result(self); 89 | let value = assertion_result 90 | .facts() 91 | .iter() 92 | .flat_map(|fact| match fact { 93 | Fact::KeyValue { key: k, value } if k.eq(&key_str) => Some(value), 94 | _ => None, 95 | }) 96 | .next() 97 | .unwrap_or_else(|| { 98 | panic!( 99 | "key `{}` not found in assertion result.\n{:?}", 100 | key_str, 101 | assertion_result.generate_message() 102 | ) 103 | }) 104 | .clone(); 105 | self.new_owned_subject( 106 | value, 107 | Some(format!("{}.[key={}]", self.description_or_expr(), key_str)), 108 | (), 109 | ) 110 | } 111 | 112 | fn fact_keys(&self) -> Subject, (), R> { 113 | let assertion_result = get_assertion_result(self); 114 | let keys: HashSet<&String> = assertion_result 115 | .facts() 116 | .iter() 117 | .flat_map(|fact| match fact { 118 | Fact::KeyValue { key, .. } => Some(key), 119 | Fact::KeyValues { key, .. } => Some(key), 120 | _ => None, 121 | }) 122 | .collect(); 123 | self.new_owned_subject( 124 | keys, 125 | Some(format!("{}.keys()", self.description_or_expr())), 126 | (), 127 | ) 128 | } 129 | } 130 | 131 | #[cfg(test)] 132 | mod tests { 133 | use std::fmt::Debug; 134 | 135 | use crate::testing::*; 136 | 137 | use super::*; 138 | 139 | trait TestAssertion<'a, S, R> { 140 | fn is_same_to(&self, expected: B) -> R 141 | where 142 | B: Borrow, 143 | S: PartialEq + Debug; 144 | } 145 | 146 | impl<'a, S, R> TestAssertion<'a, S, R> for Subject<'a, S, (), R> 147 | where 148 | AssertionResult: AssertionStrategy, 149 | { 150 | fn is_same_to(&self, expected: B) -> R 151 | where 152 | B: Borrow, 153 | S: PartialEq + Debug, 154 | { 155 | match expected.borrow().eq(self.actual().borrow()) { 156 | true => self.new_result().add_simple_fact("same").do_ok(), 157 | false => self.new_result().add_simple_fact("not same").do_fail(), 158 | } 159 | } 160 | } 161 | 162 | #[test] 163 | fn test_assertion() { 164 | assert_that!("same").is_same_to("same"); 165 | assert_that!(check_that!("actual").is_same_to("expected")) 166 | .facts_are(vec![Fact::new_simple_fact("not same")]); 167 | } 168 | 169 | #[test] 170 | fn facts_are() { 171 | let failed: CheckThatResult = check_that!("actual").is_same_to("expected"); 172 | assert_that!(check_that!(failed).facts_are(vec![])).facts_are(vec![ 173 | Fact::new("value of", "failed.facts()"), 174 | Fact::new("unexpected (1)", r#"[Value { value: "not same" }]"#), 175 | Fact::new_splitter(), 176 | Fact::new_multi_value_fact::<&str, &str>("expected", vec![]), 177 | Fact::new_multi_value_fact("actual", vec!["Value { value: \"not same\" }"]), 178 | ]); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/assertions/float.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::borrow::Borrow; 16 | use std::fmt::Debug; 17 | 18 | use num_traits::{Float, Zero}; 19 | 20 | use crate::base::{AssertionApi, AssertionResult, AssertionStrategy, Subject}; 21 | 22 | /// Trait for float assertion. 23 | /// 24 | /// # Example 25 | /// ``` 26 | /// use assertor::*; 27 | /// assert_that!(0.1_f32).is_approx_equal_to(0.1); 28 | /// assert_that!(0.1_f32) 29 | /// .with_abs_tol(0.5) 30 | /// .is_approx_equal_to(0.5); 31 | /// assert_that!(0.1_f64) 32 | /// .with_rel_tol(0.2) 33 | /// .is_approx_equal_to(0.12); // 0.1 ± 0.12 * 0.2 34 | /// ``` 35 | pub trait FloatAssertion<'a, S, R> { 36 | /// Set the relative tolerance. 37 | fn with_rel_tol(self, rel_tol: S) -> Subject<'a, S, FloatTolerance, R>; 38 | /// Set the absolute tolerance. 39 | fn with_abs_tol(self, abs_tol: S) -> Subject<'a, S, FloatTolerance, R>; 40 | 41 | /// Checks the subject is equal to `expected` with tolerance. 42 | /// 43 | /// The equality with tolerance is defined as following: 44 | /// ```math 45 | /// abs(actual - expected) <= (asb_tol + rel_tol * abs(expected)) 46 | /// ``` 47 | /// See also: [numpy.isclose](https://numpy.org/doc/stable/reference/generated/numpy.isclose.html) 48 | #[track_caller] 49 | fn is_approx_equal_to>(&self, expected: B) -> R 50 | where 51 | FloatTolerance: Default; 52 | } 53 | 54 | pub struct FloatTolerance { 55 | /// relative tolerance 56 | rel_tol: S, 57 | /// absolute tolerance 58 | abs_tol: S, 59 | } 60 | 61 | impl FloatTolerance { 62 | fn new(rel_tol: S, abs_tol: S) -> Self { 63 | FloatTolerance { rel_tol, abs_tol } 64 | } 65 | fn with_rel_tol(mut self, rel_tol: S) -> Self { 66 | self.rel_tol = rel_tol; 67 | self 68 | } 69 | fn with_abs_tol(mut self, abs_tol: S) -> Self { 70 | self.abs_tol = abs_tol; 71 | self 72 | } 73 | } 74 | 75 | impl FloatTolerance { 76 | fn zeros() -> Self { 77 | FloatTolerance::new(S::zero(), S::zero()) 78 | } 79 | } 80 | 81 | impl Default for FloatTolerance { 82 | fn default() -> Self { 83 | // from numpy.isclose() 84 | FloatTolerance::new(1e-05, 1e-08) 85 | } 86 | } 87 | 88 | impl Default for FloatTolerance { 89 | fn default() -> Self { 90 | // from numpy.isclose() 91 | FloatTolerance::new(1e-05, 1e-08) 92 | } 93 | } 94 | 95 | impl<'a, S, R> FloatAssertion<'a, S, R> for Subject<'a, S, FloatTolerance, R> 96 | where 97 | S: Float + Debug, 98 | AssertionResult: AssertionStrategy, 99 | { 100 | fn with_rel_tol(mut self, rel_tol: S) -> Subject<'a, S, FloatTolerance, R> { 101 | self.option_mut().rel_tol = rel_tol; 102 | self 103 | } 104 | 105 | fn with_abs_tol(mut self, abs_tol: S) -> Subject<'a, S, FloatTolerance, R> { 106 | self.option_mut().abs_tol = abs_tol; 107 | self 108 | } 109 | 110 | fn is_approx_equal_to>(&self, expected: B) -> R { 111 | let diff = (*self.actual() - *expected.borrow()).abs(); 112 | let tolerance: S = self.option().abs_tol + self.option().rel_tol * *expected.borrow(); 113 | if diff < tolerance { 114 | self.new_result().do_ok() 115 | } else { 116 | self.new_result() 117 | .add_fact("expected", format!("{:?}", expected.borrow())) 118 | .add_fact("but was", format!("{:?}", self.actual())) 119 | .add_fact("outside tolerance", format!("{:?}", tolerance)) 120 | .do_fail() 121 | } 122 | } 123 | } 124 | 125 | impl<'a, S, R: 'a> FloatAssertion<'a, S, R> for Subject<'a, S, (), R> 126 | where 127 | S: Float + Debug, 128 | AssertionResult: AssertionStrategy, 129 | { 130 | fn with_rel_tol(self, rel_tol: S) -> Subject<'a, S, FloatTolerance, R> { 131 | // XXX: consider to remove clone. 132 | self.new_owned_subject( 133 | *self.actual(), 134 | self.description().clone(), 135 | FloatTolerance::zeros().with_rel_tol(rel_tol), 136 | ) 137 | } 138 | 139 | fn with_abs_tol(self, abs_tol: S) -> Subject<'a, S, FloatTolerance, R> { 140 | // XXX: consider to remove clone. 141 | self.new_owned_subject( 142 | *self.actual(), 143 | self.description().clone(), 144 | FloatTolerance::zeros().with_abs_tol(abs_tol), 145 | ) 146 | } 147 | 148 | fn is_approx_equal_to>(&self, expected: B) -> R 149 | where 150 | FloatTolerance: Default, 151 | { 152 | self.new_subject(self.actual(), None, FloatTolerance::default()) 153 | .is_approx_equal_to(expected) 154 | } 155 | } 156 | 157 | #[cfg(test)] 158 | mod tests { 159 | use crate::testing::*; 160 | 161 | use super::*; 162 | 163 | #[test] 164 | fn is_approx_equal_to() { 165 | assert_that!(0.1_f32).is_approx_equal_to(0.1); 166 | assert_that!(0.1_f32).is_approx_equal_to(0.1); 167 | assert_that!(0.1_f32) 168 | .with_abs_tol(0.5) 169 | .is_approx_equal_to(0.5); 170 | assert_that!(0.1_f32) 171 | .with_rel_tol(0.2) 172 | .is_approx_equal_to(0.12); // 0.1 ± 0.12 * 0.2 173 | 174 | assert_that!(0.1_f64).is_approx_equal_to(0.1); 175 | assert_that!(0.1_f64).is_approx_equal_to(0.100000001); 176 | assert_that!(0.1_f64) 177 | .with_abs_tol(0.5) 178 | .is_approx_equal_to(0.5); 179 | assert_that!(0.1_f64) 180 | .with_rel_tol(0.2) 181 | .is_approx_equal_to(0.12); // 0.1 ± 0.12 * 0.2 182 | 183 | // Failures 184 | assert_that!(check_that!(0.1).with_abs_tol(0.1).is_approx_equal_to(0.25)).facts_are(vec![ 185 | Fact::new("expected", "0.25"), 186 | Fact::new("but was", "0.1"), 187 | Fact::new("outside tolerance", "0.1"), 188 | ]); 189 | assert_that!(check_that!(0.1).is_approx_equal_to(0.3)).facts_are(vec![ 190 | Fact::new("expected", "0.3"), 191 | Fact::new("but was", "0.1"), 192 | Fact::new("outside tolerance", "3.01e-6"), 193 | ]) 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/assertions/string.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use crate::assertions::basic::EqualityAssertion; 16 | use crate::base::{AssertionApi, AssertionResult, AssertionStrategy, Subject}; 17 | 18 | /// Trait for string assertion. 19 | /// 20 | /// # Example 21 | /// ``` 22 | /// use assertor::*; 23 | /// 24 | /// assert_that!("foobarbaz").is_same_string_to("foobarbaz"); 25 | /// assert_that!("foobarbaz").contains("bar"); 26 | /// assert_that!("foobarbaz").starts_with("foo"); 27 | /// assert_that!("foobarbaz").ends_with("baz"); 28 | /// ``` 29 | pub trait StringAssertion { 30 | /// Checks that the subject is same string to `expected`. 31 | #[track_caller] 32 | fn is_same_string_to>(&self, expected: E) -> R; 33 | 34 | /// Checks that the subject contains `expected`. 35 | #[track_caller] 36 | fn contains>(&self, expected: E) -> R; 37 | 38 | /// Checks that the subject does not contains `value`. 39 | #[track_caller] 40 | fn does_not_contain>(&self, value: E) -> R; 41 | 42 | /// Checks that the subject starts with `expected`. 43 | #[track_caller] 44 | fn starts_with>(&self, expected: E) -> R; 45 | 46 | /// Checks that the subject ends with `expected`. 47 | #[track_caller] 48 | fn ends_with>(&self, expected: E) -> R; 49 | } 50 | 51 | impl StringAssertion for Subject<'_, String, (), R> 52 | where 53 | AssertionResult: AssertionStrategy, 54 | { 55 | fn is_same_string_to>(&self, expected: E) -> R { 56 | let subject: Subject = self.new_subject(self.actual(), None, ()); 57 | EqualityAssertion::is_equal_to(&subject, expected.into()) 58 | } 59 | 60 | fn contains>(&self, expected: E) -> R { 61 | let expected_str = expected.into(); 62 | if self.actual().contains(&expected_str) { 63 | self.new_result().do_ok() 64 | } else { 65 | self.new_result() 66 | .add_fact("expected a string that contains", expected_str) 67 | .add_fact("but was", self.actual()) 68 | .do_fail() 69 | } 70 | } 71 | 72 | fn does_not_contain>(&self, value: E) -> R { 73 | let expected_str = value.into(); 74 | if self.actual().contains(&expected_str) { 75 | self.new_result() 76 | .add_fact("expected a string to not contain", expected_str) 77 | .add_fact("but was", self.actual()) 78 | .do_fail() 79 | } else { 80 | self.new_result().do_ok() 81 | } 82 | } 83 | 84 | fn starts_with>(&self, expected: E) -> R { 85 | let expected_str = expected.into(); 86 | if self.actual().starts_with(&expected_str) { 87 | self.new_result().do_ok() 88 | } else { 89 | self.new_result() 90 | .add_fact("expected a string that starts with", expected_str) 91 | .add_fact("but was", self.actual()) 92 | .do_fail() 93 | } 94 | } 95 | 96 | fn ends_with>(&self, expected: E) -> R { 97 | let expected_str = expected.into(); 98 | if self.actual().ends_with(&expected_str) { 99 | self.new_result().do_ok() 100 | } else { 101 | self.new_result() 102 | .add_fact("expected a string that ends with", expected_str) 103 | .add_fact("but was", self.actual()) 104 | .do_fail() 105 | } 106 | } 107 | } 108 | 109 | impl StringAssertion for Subject<'_, &str, (), R> 110 | where 111 | AssertionResult: AssertionStrategy, 112 | { 113 | #[track_caller] 114 | fn is_same_string_to>(&self, expected: E) -> R { 115 | self.new_owned_subject(self.actual().to_string(), None, ()) 116 | .is_same_string_to(expected) 117 | } 118 | 119 | #[track_caller] 120 | fn contains>(&self, expected: E) -> R { 121 | self.new_owned_subject(self.actual().to_string(), None, ()) 122 | .contains(expected) 123 | } 124 | 125 | #[track_caller] 126 | fn does_not_contain>(&self, value: E) -> R { 127 | self.new_owned_subject(self.actual().to_string(), None, ()) 128 | .does_not_contain(value) 129 | } 130 | 131 | #[track_caller] 132 | fn starts_with>(&self, expected: E) -> R { 133 | self.new_owned_subject(self.actual().to_string(), None, ()) 134 | .starts_with(expected) 135 | } 136 | 137 | #[track_caller] 138 | fn ends_with>(&self, expected: E) -> R { 139 | self.new_owned_subject(self.actual().to_string(), None, ()) 140 | .ends_with(expected) 141 | } 142 | } 143 | 144 | #[cfg(test)] 145 | mod tests { 146 | use crate::testing::*; 147 | 148 | use super::*; 149 | 150 | #[test] 151 | fn is_same_string_to() { 152 | assert_that!("foo").is_same_string_to("foo"); 153 | assert_that!("").is_same_string_to(""); 154 | assert_that!("ninja".to_string()).is_same_string_to("ninja"); 155 | assert_that!("ninja".to_string()).is_same_string_to("ninja".to_string()); 156 | assert_that!(check_that!("ninja").is_same_string_to("bar")).facts_are(vec![ 157 | Fact::new("expected", r#""bar""#), 158 | Fact::new("actual", r#""ninja""#), 159 | ]); 160 | } 161 | 162 | #[test] 163 | fn starts_with() { 164 | assert_that!("foobarbaz").starts_with("foo"); 165 | assert_that!(check_that!("foobarbaz").starts_with("baz")).facts_are(vec![ 166 | Fact::new("expected a string that starts with", "baz"), 167 | Fact::new("but was", "foobarbaz"), 168 | ]) 169 | } 170 | 171 | #[test] 172 | fn ends_with() { 173 | assert_that!("foobarbaz").ends_with("baz"); 174 | assert_that!(check_that!("foobarbaz").ends_with("foo")).facts_are(vec![ 175 | Fact::new("expected a string that ends with", "foo"), 176 | Fact::new("but was", "foobarbaz"), 177 | ]) 178 | } 179 | 180 | #[test] 181 | fn contains() { 182 | assert_that!("foobarbaz").contains("foo"); 183 | assert_that!("foobarbaz").contains("bar"); 184 | assert_that!("foobarbaz").contains("baz"); 185 | assert_that!("foobarbaz").contains("b"); 186 | 187 | assert_that!(check_that!("foo").contains("baz")).facts_are(vec![ 188 | Fact::new("expected a string that contains", "baz"), 189 | Fact::new("but was", "foo"), 190 | ]) 191 | } 192 | 193 | #[test] 194 | fn does_not_contain() { 195 | assert_that!("foobarbaz").does_not_contain("was"); 196 | assert_that!("foobarbaz").does_not_contain("bla"); 197 | assert_that!("foobarbaz").does_not_contain("up"); 198 | assert_that!("foobarbaz").does_not_contain("x"); 199 | 200 | assert_that!(check_that!("foo").does_not_contain("fo")).facts_are(vec![ 201 | Fact::new("expected a string to not contain", "fo"), 202 | Fact::new("but was", "foo"), 203 | ]) 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/assertions/result.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::borrow::Borrow; 16 | use std::fmt::Debug; 17 | 18 | use crate::assert_that; 19 | use crate::base::{AssertionApi, AssertionResult, AssertionStrategy, Subject}; 20 | 21 | /// Trait for result assertion. 22 | /// 23 | /// # Example 24 | /// ``` 25 | /// use assertor::*; 26 | /// 27 | /// let ok : Result= Ok(0); 28 | /// let err : Result= Err(1); 29 | /// 30 | /// assert_that!(ok).is_ok(); 31 | /// assert_that!(err).is_err(); 32 | /// assert_that!(ok).has_ok(0); 33 | /// assert_that!(err).has_err(1); 34 | /// ``` 35 | pub trait ResultAssertion { 36 | /// Checks that the subject is [`Result::Ok(_)`](`std::result::Result::Ok`). 37 | #[track_caller] 38 | fn is_ok(&self) -> R; 39 | 40 | /// Checks that the subject is [`Result::Err(_)`](`std::result::Result::Err`). 41 | #[track_caller] 42 | fn is_err(&self) -> R; 43 | 44 | /// Checks that the subject is [`Result::Ok(expected)`](`std::result::Result::Err`). 45 | #[track_caller] 46 | fn has_ok>(&self, expected: B) -> R 47 | where 48 | OK: PartialEq; 49 | 50 | /// Checks that the subject is [`Result::Err(expected)`](`std::result::Result::Err`). 51 | #[track_caller] 52 | fn has_err>(&self, expected: B) -> R 53 | where 54 | ERR: PartialEq; 55 | 56 | /// Returns a new subject which is the ok value of the subject if the subject has ok value. Otherwise, it fails. 57 | #[track_caller] 58 | fn ok(&self) -> Subject; 59 | 60 | /// Returns a new subject which is the error value of the subject if the subject has error value. Otherwise, it fails. 61 | #[track_caller] 62 | fn err(&self) -> Subject; 63 | } 64 | 65 | impl ResultAssertion for Subject<'_, Result, (), R> 66 | where 67 | AssertionResult: AssertionStrategy, 68 | { 69 | fn is_ok(&self) -> R { 70 | if self.actual().is_ok() { 71 | self.new_result().do_ok() 72 | } else { 73 | self.new_result() 74 | .add_fact("expected", "Result::Err") 75 | .add_fact("actual", "Result::Ok") 76 | .add_splitter() 77 | .add_fact("actual", format!("{:?}", self.actual())) 78 | .do_fail() 79 | } 80 | } 81 | 82 | fn is_err(&self) -> R { 83 | if self.actual().is_err() { 84 | self.new_result().do_ok() 85 | } else { 86 | self.new_result() 87 | .add_fact("expected", "Result::Err") 88 | .add_fact("actual", "Result::Ok") 89 | .add_splitter() 90 | .add_fact("actual", format!("{:?}", self.actual())) 91 | .do_fail() 92 | } 93 | } 94 | 95 | fn has_ok>(&self, expected: B) -> R 96 | where 97 | OK: PartialEq, 98 | { 99 | match self.actual() { 100 | Ok(actual) if actual.eq(expected.borrow()) => self.new_result().do_ok(), 101 | Ok(actual) => self 102 | .new_result() 103 | .add_fact("expected", format!("Ok({:?})", expected.borrow())) 104 | .add_fact("actual", format!("Ok({:?})", actual)) 105 | .do_fail(), 106 | Err(actual) => self 107 | .new_result() 108 | .add_fact("expected", format!("Ok({:?})", expected.borrow())) 109 | .add_fact("actual", format!("Err({:?})", actual)) 110 | .do_fail(), 111 | } 112 | } 113 | 114 | fn has_err>(&self, expected: B) -> R 115 | where 116 | ERR: PartialEq, 117 | { 118 | match self.actual() { 119 | Err(actual) if actual.eq(expected.borrow()) => self.new_result().do_ok(), 120 | Err(actual) => self 121 | .new_result() 122 | .add_fact("expected", format!("Err({:?})", expected.borrow())) 123 | .add_fact("actual", format!("Err({:?})", actual)) 124 | .do_fail(), 125 | Ok(actual) => self 126 | .new_result() 127 | .add_fact("expected", format!("Err({:?})", expected.borrow())) 128 | .add_fact("actual", format!("Ok({:?})", actual)) 129 | .do_fail(), 130 | } 131 | } 132 | 133 | fn ok(&self) -> Subject { 134 | assert_that!(*self.actual()).is_ok(); 135 | self.new_subject(self.actual().as_ref().ok().unwrap(), Some(format!("{}.ok", self.description_or_expr())), ()) 136 | } 137 | 138 | fn err(&self) -> Subject { 139 | assert_that!(*self.actual()).is_err(); 140 | self.new_subject(self.actual().as_ref().err().unwrap(), Some(format!("{}.err", self.description_or_expr())), ()) 141 | } 142 | } 143 | 144 | #[cfg(test)] 145 | mod tests { 146 | use crate::ComparableAssertion; 147 | use crate::testing::*; 148 | 149 | use super::*; 150 | 151 | #[test] 152 | fn is_ok() { 153 | assert_that!(Result::<_, ()>::Ok(0)).is_ok(); 154 | assert_that!(Result::<_, ()>::Ok("23")).is_ok(); 155 | assert_that!(Result::<_, ()>::Ok(())).is_ok(); 156 | } 157 | 158 | #[test] 159 | fn is_err() { 160 | assert_that!(Result::<(), _>::Err(0)).is_err(); 161 | assert_that!(Result::<(), _>::Err("23")).is_err(); 162 | assert_that!(Result::<(), _>::Err(())).is_err(); 163 | } 164 | 165 | #[test] 166 | fn has_ok() { 167 | assert_that!(Result::<_, ()>::Ok(0)).has_ok(0); 168 | assert_that!(Result::<_, ()>::Ok("23")).has_ok("23"); 169 | assert_that!(Result::<_, ()>::Ok(())).has_ok(()); 170 | 171 | // Failures 172 | assert_that!(check_that!(Result::<_, ()>::Ok(0)).has_ok(1)).facts_are(vec![ 173 | Fact::new("expected", "Ok(1)"), 174 | Fact::new("actual", "Ok(0)"), 175 | ]); 176 | assert_that!(check_that!(Result::<(), ()>::Err(())).has_ok(())).facts_are(vec![ 177 | Fact::new("expected", "Ok(())"), 178 | Fact::new("actual", "Err(())"), 179 | ]); 180 | assert_that!(check_that!(Result::<&str, &str>::Err("")).has_ok("")).facts_are(vec![ 181 | Fact::new("expected", r#"Ok("")"#), 182 | Fact::new("actual", r#"Err("")"#), 183 | ]); 184 | assert_that!(check_that!(Result::<&str, &str>::Ok("")).has_ok("expected")).facts_are(vec![ 185 | Fact::new("expected", r#"Ok("expected")"#), 186 | Fact::new("actual", r#"Ok("")"#), 187 | ]); 188 | } 189 | 190 | #[test] 191 | fn has_err() { 192 | assert_that!(Result::<(), _>::Err(0)).has_err(0); 193 | assert_that!(Result::<(), _>::Err("23")).has_err("23"); 194 | assert_that!(Result::<(), _>::Err(())).has_err(()); 195 | 196 | // Failures 197 | assert_that!(check_that!(Result::<(), _>::Err(0)).has_err(1)).facts_are(vec![ 198 | Fact::new("expected", "Err(1)"), 199 | Fact::new("actual", "Err(0)"), 200 | ]); 201 | assert_that!(check_that!(Result::<(), ()>::Ok(())).has_err(())).facts_are(vec![ 202 | Fact::new("expected", "Err(())"), 203 | Fact::new("actual", "Ok(())"), 204 | ]); 205 | assert_that!(check_that!(Result::<&str, &str>::Ok("")).has_err("")).facts_are(vec![ 206 | Fact::new("expected", r#"Err("")"#), 207 | Fact::new("actual", r#"Ok("")"#), 208 | ]); 209 | assert_that!(check_that!(Result::<&str, &str>::Err("")).has_err("expected")).facts_are( 210 | vec![ 211 | Fact::new("expected", r#"Err("expected")"#), 212 | Fact::new("actual", r#"Err("")"#), 213 | ], 214 | ); 215 | } 216 | 217 | #[test] 218 | fn ok() { 219 | assert_that!(Result::::Ok(0.)).ok().is_at_most(1.); 220 | } 221 | 222 | #[test] 223 | #[should_panic] 224 | fn ok_panic() { 225 | assert_that!(Result::::Err(())).ok().is_at_most(1.); 226 | } 227 | 228 | #[test] 229 | fn err() { 230 | assert_that!(Result::<(), f64>::Err(0.)).err().is_at_most(1.); 231 | } 232 | 233 | #[test] 234 | #[should_panic] 235 | fn err_panic() { 236 | assert_that!(Result::<(), f64>::Ok(())).err().is_at_most(1.); 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/assertions/vec.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::borrow::Borrow; 16 | use std::fmt::Debug; 17 | 18 | use crate::assertions::iterator::{ 19 | check_has_length, check_is_empty, check_is_not_empty, IteratorAssertion, 20 | }; 21 | use crate::base::{AssertionApi, AssertionResult, AssertionStrategy, Subject}; 22 | 23 | /// Trait for vector assertion. 24 | /// 25 | /// Compared to [`crate::IteratorAssertion`], [`VecAssertion`] simplifies code because it is not 26 | /// needed to take reference of the expected value and to call `vec.iter()` in the actual value. 27 | /// 28 | /// ``` 29 | /// use assertor::*; 30 | /// assert_that!(vec![1,2,3].iter()).contains(&2); // IteratorAssertion 31 | /// assert_that!(vec![1,2,3]).contains(2); // VecAssertion 32 | /// ``` 33 | /// 34 | /// # Example 35 | /// ``` 36 | /// use assertor::*; 37 | /// use assertor::VecAssertion; 38 | /// 39 | /// assert_that!(Vec::::new()).is_empty(); 40 | /// assert_that!(vec![1,2,3]).has_length(3); 41 | /// assert_that!(vec![1,2,3]).contains(2); 42 | /// assert_that!(vec![1,2,3]).contains_exactly(vec![3,2,1]); 43 | /// assert_that!(vec![1,2,3]).contains_exactly_in_order(vec![1,2,3]); 44 | /// ``` 45 | pub trait VecAssertion<'a, S, T, R> 46 | where 47 | AssertionResult: AssertionStrategy, 48 | Self: Sized, 49 | { 50 | /// Checks that the subject contains the element `expected`. 51 | /// 52 | /// # Example 53 | /// ``` 54 | /// use assertor::*; 55 | /// assert_that!(vec![1, 2, 3]).contains(2); 56 | /// ``` 57 | #[track_caller] 58 | fn contains(&self, element: B) -> R 59 | where 60 | B: Borrow, 61 | T: PartialEq + Debug; 62 | 63 | /// Checks that the subject does not contains the `element`. 64 | /// 65 | /// # Example 66 | /// ``` 67 | /// use assertor::*; 68 | /// assert_that!(vec![1, 2, 3]).does_not_contain(5); 69 | /// ``` 70 | #[track_caller] 71 | fn does_not_contain(&self, element: B) -> R 72 | where 73 | B: Borrow, 74 | T: PartialEq + Debug; 75 | 76 | /// Checks that the subject exactly contains elements of `expected_vec`. 77 | /// 78 | /// This method doesn't take care of the order. Use 79 | /// [contains_exactly_in_order](`VecAssertion::contains_exactly_in_order`) to check 80 | /// elements are in the same order. 81 | /// 82 | /// # Example 83 | /// ``` 84 | /// use assertor::*; 85 | /// assert_that!(vec![1, 2, 3]).contains_exactly(vec![3, 2, 1]); 86 | /// ``` 87 | /// ```should_panic 88 | /// use assertor::*; 89 | /// assert_that!(vec![1]).contains_exactly(vec![1,2]); 90 | /// assert_that!(vec![1,2]).contains_exactly(vec![1]); 91 | /// ``` 92 | #[track_caller] 93 | fn contains_exactly>>(self, expected_vec: B) -> R 94 | where 95 | T: PartialEq + Debug; 96 | 97 | /// Checks that the subject exactly contains `expected_vec` in the same order. 98 | /// 99 | /// # Example 100 | /// ``` 101 | /// use assertor::*; 102 | /// assert_that!(vec![1, 2, 3]).contains_exactly_in_order(vec![1, 2, 3]); 103 | /// ``` 104 | /// ```should_panic 105 | /// use assertor::*; 106 | /// assert_that!(vec![1]).contains_exactly_in_order(vec![1,2]); 107 | /// assert_that!(vec![1,2]).contains_exactly_in_order(vec![1]); 108 | /// ``` 109 | #[track_caller] 110 | fn contains_exactly_in_order>>(self, expected_vec: B) -> R 111 | where 112 | T: PartialEq + Debug; 113 | 114 | /// Checks that the subject does not contain any element of `elements`. 115 | /// 116 | /// # Example 117 | /// ``` 118 | /// use assertor::*; 119 | /// assert_that!(vec![1, 2, 3]).does_not_contain_any(vec![0, -5]); 120 | /// ``` 121 | /// ```should_panic 122 | /// use assertor::*; 123 | /// assert_that!(vec![1,2]).does_not_contain_any(vec![1]); 124 | /// ``` 125 | #[track_caller] 126 | fn does_not_contain_any>>(&self, elements: B) -> R 127 | where 128 | T: PartialEq + Debug; 129 | 130 | /// Checks that the subject is empty. 131 | /// 132 | /// # Example 133 | /// ``` 134 | /// use assertor::*; 135 | /// assert_that!(Vec::::new()).is_empty(); 136 | /// ``` 137 | #[track_caller] 138 | fn is_empty(&self) -> R 139 | where 140 | T: Debug; 141 | 142 | /// Checks that the subject is not empty. 143 | /// 144 | /// # Example 145 | /// ``` 146 | /// use assertor::*; 147 | /// assert_that!(vec![1]).is_not_empty(); 148 | /// ``` 149 | #[track_caller] 150 | fn is_not_empty(&self) -> R 151 | where 152 | T: Debug; 153 | 154 | /// Checks that the subject is the given length. 155 | /// 156 | /// # Example 157 | /// ``` 158 | /// use assertor::*; 159 | /// assert_that!(vec![1, 2, 3]).has_length(3); 160 | /// ``` 161 | #[track_caller] 162 | fn has_length(&self, length: usize) -> R; 163 | } 164 | 165 | impl<'a, T, R> VecAssertion<'a, Vec, T, R> for Subject<'a, Vec, (), R> 166 | where 167 | AssertionResult: AssertionStrategy, 168 | { 169 | fn contains(&self, element: B) -> R 170 | where 171 | B: Borrow, 172 | T: PartialEq + Debug, 173 | { 174 | self.new_subject(&self.actual().iter(), None, ()) 175 | .contains(element.borrow()) 176 | } 177 | 178 | fn does_not_contain(&self, element: B) -> R 179 | where 180 | B: Borrow, 181 | T: PartialEq + Debug, 182 | { 183 | self.new_owned_subject(self.actual().iter(), None, ()) 184 | .does_not_contain(element.borrow()) 185 | } 186 | 187 | fn contains_exactly>>(self, expected_iter: B) -> R 188 | where 189 | T: PartialEq + Debug, 190 | { 191 | self.new_owned_subject(self.actual().iter(), None, ()) 192 | .contains_exactly(expected_iter.borrow().iter()) 193 | } 194 | 195 | fn contains_exactly_in_order>>(self, expected_iter: B) -> R 196 | where 197 | T: PartialEq + Debug, 198 | { 199 | self.new_owned_subject(self.actual().iter(), None, ()) 200 | .contains_exactly_in_order(expected_iter.borrow().iter()) 201 | } 202 | 203 | fn does_not_contain_any>>(&self, elements: B) -> R 204 | where 205 | T: PartialEq + Debug, 206 | { 207 | self.new_owned_subject(self.actual().iter(), None, ()) 208 | .does_not_contain_any(elements.borrow().iter()) 209 | } 210 | 211 | fn is_empty(&self) -> R 212 | where 213 | T: Debug, 214 | { 215 | check_is_empty(self.new_result(), self.actual().iter()) 216 | } 217 | 218 | fn is_not_empty(&self) -> R 219 | where 220 | T: Debug, 221 | { 222 | check_is_not_empty(self.new_result(), self.actual().iter()) 223 | } 224 | 225 | fn has_length(&self, length: usize) -> R { 226 | check_has_length(self.new_result(), self.actual().iter(), self.expr(), length) 227 | } 228 | } 229 | 230 | #[cfg(test)] 231 | mod tests { 232 | use crate::testing::*; 233 | 234 | use super::*; 235 | 236 | #[test] 237 | fn contains() { 238 | assert_that!(vec![1, 2, 3]).contains(&3); 239 | 240 | // Failures 241 | assert_that!(check_that!(vec![1, 2, 3]).contains(&10)).facts_are(vec![ 242 | Fact::new("expected to contain", "10"), 243 | Fact::new_simple_fact("but did not"), 244 | Fact::new_multi_value_fact("though it did contain", vec!["1", "2", "3"]), 245 | ]); 246 | } 247 | 248 | #[test] 249 | fn contains_exactly() { 250 | assert_that!(vec![1, 2, 3]).contains_exactly(vec![1, 2, 3]); 251 | assert_that!(vec![2, 1, 3]).contains_exactly(vec![1, 2, 3]); 252 | } 253 | 254 | #[test] 255 | fn contains_exactly_in_order() { 256 | assert_that!(vec![1, 2, 3]).contains_exactly_in_order(vec![1, 2, 3]); 257 | assert_that!(check_that!(vec![2, 1, 3]).contains_exactly_in_order(vec![1, 2, 3])).facts_are( 258 | vec![ 259 | Fact::new_simple_fact("contents match, but order was wrong"), 260 | Fact::new_splitter(), 261 | Fact::new_multi_value_fact("expected", vec!["1", "2", "3"]), 262 | Fact::new_multi_value_fact("actual", vec!["2", "1", "3"]), 263 | ], 264 | ) 265 | } 266 | 267 | #[test] 268 | fn is_empty() { 269 | assert_that!(Vec::::new()).is_empty(); 270 | 271 | // Failures 272 | assert_that!(check_that!(vec![1]).is_empty()).facts_are(vec![ 273 | Fact::new_simple_fact("expected to be empty"), 274 | Fact::new_splitter(), 275 | Fact::new_multi_value_fact("actual", vec!["1"]), 276 | ]) 277 | } 278 | 279 | #[test] 280 | fn is_not_empty() { 281 | assert_that!(Vec::::from([1])).is_not_empty(); 282 | 283 | // Failures 284 | assert_that!(check_that!(Vec::::new()).is_not_empty()).facts_are(vec![ 285 | Fact::new_simple_fact("expected to be non-empty"), 286 | Fact::new_splitter(), 287 | Fact::new("actual", "[]"), 288 | ]) 289 | } 290 | 291 | #[test] 292 | fn has_size() { 293 | assert_that!(vec![1, 2, 3]).has_length(3); 294 | assert_that!(Vec::::new()).has_length(0); 295 | 296 | // Failures 297 | assert_that!(check_that!(Vec::::new()).has_length(3)).facts_are(vec![ 298 | Fact::new("value of", "Vec::::new().size()"), 299 | Fact::new("expected", "3"), 300 | Fact::new("actual", "0"), 301 | ]); 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /src/assertions/set.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::borrow::Borrow; 16 | use std::collections::{BTreeSet, HashSet}; 17 | use std::fmt::Debug; 18 | use std::hash::Hash; 19 | 20 | use crate::assertions::iterator::{check_is_empty, IteratorAssertion}; 21 | use crate::base::{AssertionApi, AssertionResult, AssertionStrategy, Subject}; 22 | use crate::EqualityAssertion; 23 | 24 | /// Trait for set assertion. 25 | /// 26 | /// # Example 27 | /// ``` 28 | /// use assertor::*; 29 | /// use std::collections::HashSet; 30 | /// 31 | /// let mut set = HashSet::new(); 32 | /// assert_that!(set).is_empty(); 33 | /// 34 | /// set.insert("a"); 35 | /// set.insert("b"); 36 | /// set.insert("c"); 37 | /// 38 | /// assert_that!(set).contains("a"); 39 | /// assert_that!(set).has_length(3); 40 | /// ``` 41 | /// ```should_panic 42 | /// use assertor::*; 43 | /// use std::collections::HashSet; 44 | /// 45 | /// let mut set = HashSet::new(); 46 | /// set.insert("a"); 47 | /// assert_that!(set).contains("z"); 48 | /// // expected to contain : "z" 49 | /// // but did not 50 | /// // though it did contain: ["a"] 51 | /// ``` 52 | pub trait SetAssertion<'a, S, T, R> { 53 | /// Checks that the subject has the given length. 54 | #[track_caller] 55 | fn has_length(&self, length: usize) -> R; 56 | 57 | /// Checks that the subject is empty. 58 | #[track_caller] 59 | fn is_empty(&self) -> R 60 | where 61 | T: Debug; 62 | 63 | /// Checks that the subject has `expected`. 64 | #[track_caller] 65 | fn contains>(&self, expected: B) -> R 66 | where 67 | T: PartialEq + Eq + Debug + Hash; 68 | 69 | /// Checks that the subject does not contain `element`. 70 | #[track_caller] 71 | fn does_not_contain(&self, element: B) -> R 72 | where 73 | B: Borrow, 74 | T: PartialEq + Debug; 75 | 76 | /// Checks that the subject does not contain any element of `elements`. 77 | #[track_caller] 78 | fn does_not_contain_any>>(&self, elements: B) -> R 79 | where 80 | T: PartialEq + Debug; 81 | } 82 | 83 | impl<'a, T, R, ST> SetAssertion<'a, ST, T, R> for Subject<'a, ST, (), R> 84 | where 85 | AssertionResult: AssertionStrategy, 86 | T: Eq + Debug, 87 | ST: SetLike, 88 | { 89 | fn has_length(&self, length: usize) -> R { 90 | self.new_subject( 91 | &self.actual().len(), 92 | Some(format!("{}.len()", self.description_or_expr())), 93 | (), 94 | ) 95 | .is_equal_to(length) 96 | } 97 | 98 | fn is_empty(&self) -> R 99 | where 100 | T: Debug, 101 | { 102 | check_is_empty(self.new_result(), self.actual().iter()) 103 | } 104 | 105 | fn contains>(&self, expected: B) -> R 106 | where 107 | T: PartialEq + Eq + Debug + Hash, 108 | { 109 | self.new_owned_subject(self.actual().iter(), None, ()) 110 | .contains(expected.borrow()) 111 | } 112 | 113 | fn does_not_contain(&self, element: B) -> R 114 | where 115 | B: Borrow, 116 | T: PartialEq + Debug, 117 | { 118 | self.new_owned_subject(self.actual().iter(), None, ()) 119 | .does_not_contain(element.borrow()) 120 | } 121 | 122 | fn does_not_contain_any>>(&self, elements: B) -> R 123 | where 124 | T: PartialEq + Debug, 125 | { 126 | self.new_owned_subject(self.actual().iter(), None, ()) 127 | .does_not_contain_any(elements.borrow().iter()) 128 | } 129 | } 130 | 131 | /// Trait for sorted set assertions. 132 | /// 133 | /// # Example 134 | /// ``` 135 | /// use assertor::*; 136 | /// use std::collections::BTreeSet; 137 | /// 138 | /// let mut set = BTreeSet::new(); 139 | /// assert_that!(set).is_empty(); 140 | /// 141 | /// set.insert("a"); 142 | /// set.insert("b"); 143 | /// set.insert("c"); 144 | /// 145 | /// assert_that!(set).contains("a"); 146 | /// assert_that!(set).has_length(3); 147 | /// assert_that!(set).contains_all_of_in_order(BTreeSet::from(["b", "c"])); 148 | /// ``` 149 | /// ```should_panic 150 | /// use assertor::*; 151 | /// use std::collections::BTreeSet; 152 | /// 153 | /// let mut set = BTreeSet::new(); 154 | /// set.insert("a"); 155 | /// assert_that!(set).contains("z"); 156 | /// // expected to contain : "z" 157 | /// // but did not 158 | /// // though it did contain: ["a"] 159 | /// ``` 160 | pub trait OrderedSetAssertion<'a, ST, T, R>: SetAssertion<'a, ST, T, R> 161 | where 162 | AssertionResult: AssertionStrategy, 163 | T: PartialOrd + Eq + Debug, 164 | ST: OrderedSetLike, 165 | { 166 | /// Checks that the subject contains at least all elements of `expected` in the same order. 167 | /// 168 | /// # Example 169 | /// ``` 170 | /// use assertor::*; 171 | /// use std::collections::BTreeSet; 172 | /// assert_that!(BTreeSet::from([1, 2, 3])).contains_all_of_in_order(BTreeSet::from([1, 2, 3])); 173 | /// ``` 174 | fn contains_all_of_in_order(&self, expected: OSA) -> R 175 | where 176 | T: PartialOrd + Eq + Debug, 177 | OS: OrderedSetLike, 178 | OSA: Borrow; 179 | 180 | /// Checks that the subject exactly contains elements of `expected` in the same order. 181 | /// 182 | /// # Example 183 | /// ``` 184 | /// use assertor::*; 185 | /// use std::collections::BTreeSet; 186 | /// assert_that!(BTreeSet::from([1, 2, 3])).contains_exactly_in_order(BTreeSet::from([1, 2, 3])); 187 | /// ``` 188 | fn contains_exactly_in_order(&self, expected: OSA) -> R 189 | where 190 | T: PartialOrd + Eq + Debug, 191 | OS: OrderedSetLike, 192 | OSA: Borrow; 193 | } 194 | 195 | impl<'a, T, R, ST> OrderedSetAssertion<'a, ST, T, R> for Subject<'a, ST, (), R> 196 | where 197 | AssertionResult: AssertionStrategy, 198 | T: Eq + PartialOrd + Debug, 199 | ST: OrderedSetLike, 200 | { 201 | fn contains_all_of_in_order(&self, expected: OSA) -> R 202 | where 203 | T: PartialOrd + Eq + Debug, 204 | OS: OrderedSetLike, 205 | OSA: Borrow, 206 | { 207 | self.new_owned_subject(self.actual().iter(), None, ()) 208 | .contains_all_of_in_order(expected.borrow().iter()) 209 | } 210 | 211 | fn contains_exactly_in_order(&self, expected: OSA) -> R 212 | where 213 | T: PartialOrd + Eq + Debug, 214 | OS: OrderedSetLike, 215 | OSA: Borrow, 216 | { 217 | self.new_owned_subject(self.actual().iter(), None, ()) 218 | .contains_exactly_in_order(expected.borrow().iter()) 219 | } 220 | } 221 | 222 | pub trait SetLike { 223 | type It<'a>: Iterator + Clone 224 | where 225 | T: 'a, 226 | Self: 'a; 227 | fn iter<'a>(&'a self) -> Self::It<'a>; 228 | 229 | fn len(&self) -> usize { 230 | self.iter().count() 231 | } 232 | } 233 | 234 | pub trait OrderedSetLike: SetLike {} 235 | 236 | impl SetLike for HashSet { 237 | type It<'a> = std::collections::hash_set::Iter<'a, T> where T: 'a, Self: 'a; 238 | 239 | fn iter<'a>(&'a self) -> Self::It<'a> { 240 | self.into_iter() 241 | } 242 | } 243 | 244 | impl SetLike for BTreeSet { 245 | type It<'a> = std::collections::btree_set::Iter<'a, T> where T: 'a, Self: 'a; 246 | 247 | fn iter<'a>(&'a self) -> Self::It<'a> { 248 | self.into_iter() 249 | } 250 | } 251 | 252 | impl OrderedSetLike for BTreeSet {} 253 | 254 | #[cfg(test)] 255 | mod tests { 256 | use std::iter::FromIterator; 257 | 258 | use crate::testing::*; 259 | 260 | use super::*; 261 | 262 | #[test] 263 | fn has_length() { 264 | assert_that!(HashSet::from_iter(vec![1].iter())).has_length(1); 265 | assert_that!(HashSet::from_iter(vec![1, 2, 3].iter())).has_length(3); 266 | assert_that!(check_that!(HashSet::from_iter(vec![1].iter())).has_length(3)).facts_are( 267 | vec![ 268 | Fact::new("value of", "HashSet::from_iter(vec![1].iter()).len()"), 269 | Fact::new("expected", "3"), 270 | Fact::new("actual", "1"), 271 | ], 272 | ); 273 | } 274 | 275 | #[test] 276 | fn is_empty() { 277 | assert_that!(HashSet::<&usize>::from_iter(vec![].iter())).is_empty(); 278 | assert_that!(check_that!(HashSet::from_iter(vec![1].iter())).is_empty()).facts_are(vec![ 279 | Fact::new_simple_fact("expected to be empty"), 280 | Fact::new_splitter(), 281 | Fact::new_multi_value_fact("actual", vec!["1"]), 282 | ]); 283 | } 284 | 285 | #[test] 286 | fn contains() { 287 | assert_that!(HashSet::from_iter(vec![1, 2, 3].iter())).contains(&3); 288 | 289 | // Failures 290 | let result = check_that!(HashSet::from_iter(vec![1, 2, 3].iter())).contains(&10); 291 | assert_that!(result).facts_are_at_least(vec![ 292 | Fact::new("expected to contain", "10"), 293 | Fact::new_simple_fact("but did not"), 294 | ]); 295 | assert_that!(result) 296 | .fact_keys() 297 | .contains(&"though it did contain".to_string()); 298 | // Skip test for value because key order is not stable. 299 | } 300 | 301 | #[test] 302 | fn works_for_btree_set() { 303 | let btree_set = BTreeSet::from(["hello", "world"]); 304 | let empty: BTreeSet<&str> = BTreeSet::new(); 305 | assert_that!(btree_set).has_length(2); 306 | assert_that!(empty).is_empty(); 307 | assert_that!(btree_set).contains("hello"); 308 | assert_that!(btree_set).does_not_contain("nope"); 309 | assert_that!(btree_set).does_not_contain_any(vec!["one", "two"]); 310 | } 311 | 312 | #[test] 313 | fn contains_all_of_in_order() { 314 | assert_that!(BTreeSet::from([1, 2, 3])).contains_all_of_in_order(BTreeSet::from([])); 315 | assert_that!(BTreeSet::from([1, 2, 3])).contains_all_of_in_order(BTreeSet::from([1, 2])); 316 | assert_that!(BTreeSet::from([1, 2, 3])).contains_all_of_in_order(BTreeSet::from([2, 3])); 317 | assert_that!(BTreeSet::from([1, 2, 3])).contains_all_of_in_order(BTreeSet::from([1, 3])); 318 | assert_that!(BTreeSet::from([1, 2, 3])).contains_all_of_in_order(BTreeSet::from([1, 2, 3])); 319 | } 320 | 321 | #[test] 322 | fn contains_exactly_in_order() { 323 | assert_that!(BTreeSet::from([1, 2, 3])) 324 | .contains_exactly_in_order(BTreeSet::from([1, 2, 3])); 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /src/diff.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | pub(crate) mod map { 16 | use crate::diff::iter::{SequenceComparison, SequenceOrderComparison}; 17 | use std::collections::{BTreeMap, HashMap}; 18 | use std::fmt::Debug; 19 | use std::hash::Hash; 20 | 21 | /// Difference for a single key in a Map-like data structure. 22 | pub(crate) struct MapValueDiff { 23 | pub(crate) key: K, 24 | pub(crate) actual_value: V, 25 | pub(crate) expected_value: V, 26 | } 27 | 28 | /// Disjoint and commonalities representation between two Map-like data structures. 29 | pub(crate) struct MapComparison { 30 | pub(crate) extra: Vec<(K, V)>, 31 | pub(crate) missing: Vec<(K, V)>, 32 | pub(crate) different_values: Vec>, 33 | pub(crate) common: Vec<(K, V)>, 34 | pub(crate) key_order_comparison: Option>, 35 | } 36 | 37 | pub trait MapLike { 38 | type It<'a>: Iterator 39 | where 40 | K: 'a, 41 | V: 'a, 42 | Self: 'a; 43 | 44 | fn get(&self, k: &K) -> Option<&V>; 45 | fn contains(&self, k: &K) -> bool { 46 | self.get(k).is_some() 47 | } 48 | 49 | fn keys_iter<'a>(&'a self) -> Self::It<'a> 50 | where 51 | K: 'a, 52 | V: 'a; 53 | 54 | fn keys_ordered(&self) -> bool; 55 | 56 | fn len(&self) -> usize { 57 | self.keys_iter().count() 58 | } 59 | 60 | fn keys<'a>(&'a self) -> Vec<&'a K> 61 | where 62 | K: 'a, 63 | V: 'a, 64 | { 65 | self.keys_iter().collect() 66 | } 67 | fn entries(&self) -> Vec<(&K, &V)>; 68 | } 69 | 70 | pub trait OrderedMapLike: MapLike {} 71 | 72 | impl MapLike for BTreeMap { 73 | type It<'a> = std::collections::btree_map::Keys<'a, K, V> where K: 'a, V: 'a; 74 | 75 | fn get(&self, k: &K) -> Option<&V> { 76 | self.get(k) 77 | } 78 | 79 | fn keys_iter<'a>(&'a self) -> Self::It<'a> 80 | where 81 | K: 'a, 82 | V: 'a, 83 | { 84 | self.keys() 85 | } 86 | 87 | fn keys_ordered(&self) -> bool { 88 | true 89 | } 90 | 91 | fn entries(&self) -> Vec<(&K, &V)> { 92 | self.into_iter().collect() 93 | } 94 | } 95 | 96 | impl OrderedMapLike for BTreeMap {} 97 | 98 | impl MapLike for HashMap { 99 | type It<'a> = std::collections::hash_map::Keys<'a, K, V> where K: 'a, V: 'a; 100 | 101 | fn get(&self, k: &K) -> Option<&V> { 102 | self.get(k) 103 | } 104 | 105 | fn keys_iter<'a>(&'a self) -> Self::It<'a> 106 | where 107 | K: 'a, 108 | V: 'a, 109 | { 110 | self.keys() 111 | } 112 | 113 | fn keys_ordered(&self) -> bool { 114 | false 115 | } 116 | 117 | fn entries(&self) -> Vec<(&K, &V)> { 118 | self.into_iter().collect() 119 | } 120 | } 121 | 122 | impl MapComparison { 123 | pub(crate) fn from_map_like<'a, M1, M2>( 124 | actual: &'a M1, 125 | expected: &'a M2, 126 | order_comparison: Option, 127 | ) -> MapComparison<&'a K, &'a V> 128 | where 129 | M1: MapLike, 130 | M2: MapLike, 131 | { 132 | let mut extra = vec![]; 133 | let mut missing = vec![]; 134 | let mut different_values = vec![]; 135 | let mut common = vec![]; 136 | 137 | for (key, value) in actual.entries() { 138 | match expected.get(key) { 139 | Some(rv) if value == rv => { 140 | common.push((key, value)); 141 | } 142 | Some(rv) => different_values.push(MapValueDiff { 143 | key, 144 | actual_value: value, 145 | expected_value: rv, 146 | }), 147 | None => { 148 | extra.push((key, value)); 149 | } 150 | } 151 | } 152 | 153 | for (key, value) in expected.entries() { 154 | if !actual.contains(key) { 155 | missing.push((key, value)); 156 | } 157 | } 158 | 159 | let key_order_comparison = order_comparison 160 | .filter(|_| actual.keys_ordered() && expected.keys_ordered()) 161 | .map(|comparison| { 162 | SequenceComparison::from_iter( 163 | actual.keys().into_iter(), 164 | expected.keys().into_iter(), 165 | comparison, 166 | ) 167 | }); 168 | 169 | MapComparison { 170 | extra, 171 | missing, 172 | different_values, 173 | common, 174 | key_order_comparison, 175 | } 176 | } 177 | } 178 | 179 | #[cfg(test)] 180 | mod tests { 181 | use std::collections::{BTreeMap, HashMap}; 182 | 183 | use crate::diff::iter::SequenceOrderComparison; 184 | use crate::diff::map::MapComparison; 185 | use test_case::test_case; 186 | /* 187 | expected actual extra missing common name 188 | */ 189 | #[test_case(vec![], vec![], vec![], vec![], vec![] ; "empty maps")] 190 | #[test_case(vec![("123", 2)], vec![], vec![(&"123", &2)], vec![], vec![] ; "extra entry")] 191 | #[test_case(vec![], vec![("123", 2)], vec![], vec![(&"123", &2)], vec![] ; "missing entry")] 192 | #[test_case(vec![("123", 2)], vec![("123", 2)], vec![], vec![], vec![(&"123", &2)] ; "common entry")] 193 | fn unordered_map_diff( 194 | left: Vec<(&str, i32)>, 195 | right: Vec<(&str, i32)>, 196 | extra: Vec<(&&str, &i32)>, 197 | missing: Vec<(&&str, &i32)>, 198 | common: Vec<(&&str, &i32)>, 199 | ) { 200 | let l: HashMap<&str, i32> = left.into_iter().collect(); 201 | let r: HashMap<&str, i32> = right.into_iter().collect(); 202 | let result = MapComparison::from_map_like(&l, &r, None); 203 | assert_eq!(common, result.common); 204 | assert_eq!(extra, result.extra); 205 | assert_eq!(missing, result.missing); 206 | } 207 | 208 | /* 209 | expected actual extra missing common order_preserved order_extra order_missing name 210 | */ 211 | #[test_case(vec![(1, 1), (2, 2), (3, 3), (4, 4)], vec![(1, 1), (2, 2), (3, 3)], vec![(&4, &4)], vec![], vec![(&1, &1), (&2, &2), (&3, &3)], true, vec![&4], vec![] ; "prefix sub-sequence")] 212 | #[test_case(vec![(1, 1), (2, 2), (3, 3), (4, 4)], vec![(2, 2), (3, 3), (4, 4)], vec![(&1, &1)], vec![], vec![(&2, &2), (&3, &3), (&4, &4)], true, vec![&1], vec![] ; "suffix sub-sequence")] 213 | fn relative_key_order_map_diff( 214 | left: Vec<(i32, i32)>, 215 | right: Vec<(i32, i32)>, 216 | extra: Vec<(&i32, &i32)>, 217 | missing: Vec<(&i32, &i32)>, 218 | common: Vec<(&i32, &i32)>, 219 | order_preserved: bool, 220 | order_extra: Vec<&i32>, 221 | order_missing: Vec<&i32>, 222 | ) { 223 | let l: BTreeMap = left.into_iter().collect(); 224 | let r: BTreeMap = right.into_iter().collect(); 225 | let result = 226 | MapComparison::from_map_like(&l, &r, Some(SequenceOrderComparison::Relative)); 227 | assert_eq!(common, result.common); 228 | assert_eq!(extra, result.extra); 229 | assert_eq!(missing, result.missing); 230 | let order_comparison = result.key_order_comparison.unwrap(); 231 | assert_eq!(order_preserved, order_comparison.order_preserved); 232 | assert_eq!(order_extra, order_comparison.extra); 233 | assert_eq!(order_missing, order_comparison.missing); 234 | } 235 | 236 | /* 237 | expected actual extra missing common order_preserved order_extra order_missing name 238 | */ 239 | #[test_case(vec![(1, 1), (2, 2), (3, 3), (4, 4)], vec![(1, 1), (2, 2), (3, 3)], vec![(&4, &4)], vec![], vec![(&1, &1), (&2, &2), (&3, &3)], true, vec![&4], vec![] ; "prefix sub-sequence")] 240 | #[test_case(vec![(1, 1), (2, 2), (3, 3), (4, 4)], vec![(2, 2), (3, 3), (4, 4)], vec![(&1, &1)], vec![], vec![(&2, &2), (&3, &3), (&4, &4)], false, vec![&1], vec![] ; "suffix sub-sequence")] 241 | fn strict_key_order_map_diff( 242 | left: Vec<(i32, i32)>, 243 | right: Vec<(i32, i32)>, 244 | extra: Vec<(&i32, &i32)>, 245 | missing: Vec<(&i32, &i32)>, 246 | common: Vec<(&i32, &i32)>, 247 | order_preserved: bool, 248 | order_extra: Vec<&i32>, 249 | order_missing: Vec<&i32>, 250 | ) { 251 | let l: BTreeMap = left.into_iter().collect(); 252 | let r: BTreeMap = right.into_iter().collect(); 253 | let result = 254 | MapComparison::from_map_like(&l, &r, Some(SequenceOrderComparison::Strict)); 255 | assert_eq!(common, result.common); 256 | assert_eq!(extra, result.extra); 257 | assert_eq!(missing, result.missing); 258 | let order_comparison = result.key_order_comparison.unwrap(); 259 | assert_eq!(order_preserved, order_comparison.order_preserved); 260 | assert_eq!(order_extra, order_comparison.extra); 261 | assert_eq!(order_missing, order_comparison.missing); 262 | } 263 | 264 | #[test] 265 | fn ordered_unordered_key_order_comparison() { 266 | let actual = BTreeMap::from([(1, 1)]); 267 | let expected = HashMap::from([(2, 2)]); 268 | let comparison = MapComparison::from_map_like( 269 | &actual, 270 | &expected, 271 | Some(SequenceOrderComparison::Strict), 272 | ); 273 | assert!(comparison.key_order_comparison.is_none()); 274 | } 275 | 276 | #[test] 277 | fn unordered_ordered_key_order_comparison() { 278 | let actual = HashMap::from([(2, 2)]); 279 | let expected = BTreeMap::from([(1, 1)]); 280 | let comparison = MapComparison::from_map_like( 281 | &actual, 282 | &expected, 283 | Some(SequenceOrderComparison::Strict), 284 | ); 285 | assert!(comparison.key_order_comparison.is_none()); 286 | } 287 | } 288 | } 289 | 290 | pub(crate) mod iter { 291 | use std::fmt::Debug; 292 | 293 | /// Differences between two Sequence-like structures. 294 | pub(crate) struct SequenceComparison { 295 | pub(crate) order_preserved: bool, 296 | pub(crate) extra: Vec, 297 | pub(crate) missing: Vec, 298 | } 299 | 300 | pub(crate) enum SequenceOrderComparison { 301 | Relative, 302 | Strict, 303 | } 304 | 305 | impl SequenceComparison { 306 | pub(crate) fn contains_exactly(&self) -> bool { 307 | self.extra.is_empty() && self.missing.is_empty() 308 | } 309 | 310 | pub(crate) fn contains_all(&self) -> bool { 311 | self.missing.is_empty() 312 | } 313 | 314 | pub(crate) fn from_iter< 315 | ICL: Iterator + Clone, 316 | ICR: Iterator + Clone, 317 | >( 318 | left: ICL, 319 | right: ICR, 320 | sequence_order: SequenceOrderComparison, 321 | ) -> SequenceComparison { 322 | match sequence_order { 323 | SequenceOrderComparison::Strict => { 324 | Self::strict_order_comparison(left.clone(), right.clone()) 325 | } 326 | SequenceOrderComparison::Relative => { 327 | Self::relative_order_comparison(left.clone(), right.clone()) 328 | } 329 | } 330 | } 331 | 332 | pub(self) fn strict_order_comparison, ICR: Iterator>( 333 | mut actual_iter: ICL, 334 | mut expected_iter: ICR, 335 | ) -> SequenceComparison { 336 | let mut extra = vec![]; 337 | let mut missing = vec![]; 338 | let mut order_preserved = true; 339 | let move_element = |el: T, source: &mut Vec, target: &mut Vec| { 340 | if let Some(idx) = source.iter().position(|e: &T| e.eq(&el)) { 341 | source.remove(idx); 342 | } else { 343 | target.push(el); 344 | } 345 | }; 346 | loop { 347 | match (actual_iter.next(), expected_iter.next()) { 348 | (Some(actual_elem), Some(expect_elem)) => { 349 | if actual_elem.eq(&expect_elem) { 350 | continue; 351 | } 352 | order_preserved = false; 353 | move_element(expect_elem, &mut extra, &mut missing); 354 | move_element(actual_elem, &mut missing, &mut extra); 355 | } 356 | (None, Some(expect_elem)) => { 357 | move_element(expect_elem, &mut extra, &mut missing); 358 | } 359 | (Some(actual_elem), None) => { 360 | move_element(actual_elem, &mut missing, &mut extra); 361 | } 362 | (None, None) => break, 363 | } 364 | } 365 | SequenceComparison { 366 | order_preserved, 367 | extra, 368 | missing, 369 | } 370 | } 371 | 372 | pub(self) fn relative_order_comparison, ICR: Iterator>( 373 | mut actual_iter: ICL, 374 | mut expected_iter: ICR, 375 | ) -> SequenceComparison { 376 | let mut missing: Vec = vec![]; 377 | let mut extra: Vec = vec![]; 378 | let mut actual_value = actual_iter.next(); 379 | let mut expected_value = expected_iter.next(); 380 | loop { 381 | if expected_value.is_none() { 382 | if let Some(actual) = actual_value { 383 | extra.push(actual); 384 | } 385 | extra.extend(actual_iter); 386 | break; 387 | } 388 | if actual_value.is_none() { 389 | missing.push(expected_value.unwrap()); 390 | missing.extend(expected_iter); 391 | break; 392 | } 393 | if actual_value.eq(&expected_value) { 394 | actual_value = actual_iter.next(); 395 | expected_value = expected_iter.next(); 396 | } else { 397 | extra.push(actual_value.unwrap()); 398 | actual_value = actual_iter.next(); 399 | } 400 | } 401 | let order_preserved = missing.is_empty(); 402 | 403 | // check out of order elements. 404 | if !missing.is_empty() { 405 | for extra_elem in extra.iter() { 406 | if let Some(idx) = missing.iter().position(|m: &T| m.eq(extra_elem)) { 407 | missing.remove(idx); 408 | } 409 | } 410 | } 411 | 412 | SequenceComparison { 413 | order_preserved, 414 | extra, 415 | missing, 416 | } 417 | } 418 | } 419 | 420 | #[cfg(test)] 421 | mod tests { 422 | use super::SequenceComparison; 423 | use crate::diff::iter::SequenceOrderComparison; 424 | use test_case::test_case; 425 | 426 | /* 427 | expected actual extra missing order name 428 | */ 429 | #[test_case(vec![1, 2], vec![], vec![&1, &2], vec![], true ; "empty right operand")] 430 | #[test_case(vec![], vec![1, 2], vec![], vec![&1, &2], false ; "empty left operand")] 431 | #[test_case(vec![1, 2, 3], vec![1, 3], vec![&2], vec![], true ; "extra and relative order")] 432 | #[test_case(vec![1, 2, 3], vec![1, 3, 4], vec![&2], vec![&4], false ; "not found, both extra and missing")] 433 | #[test_case(vec![1, 2], vec![1, 2, 4], vec![], vec![&4], false ; "not found, extra prefix")] 434 | #[test_case(vec![1, 2], vec![0, 1, 2], vec![&1, &2], vec![&0], false ; "not found, extra suffix")] 435 | #[test_case(vec![1, 2, 3], vec![3, 1], vec![&1, &2], vec![], false ; "all found, out of order")] 436 | #[test_case(vec![1, 2, 3], vec![1, 2, 3], vec![], vec![], true ; "equal")] 437 | #[test_case(vec![1, 2, 3, 4, 5, 6], vec![1, 3, 6], vec![&2, &4, &5], vec![], true ; "order preserved relatively")] 438 | #[test_case(vec![1, 2, 3, 4], vec![1, 2, 3], vec![&4], vec![], true ; "prefix sub-sequence")] 439 | #[test_case(vec![1, 2, 3, 4], vec![2, 3, 4], vec![&1], vec![], true ; "suffix sub-sequence")] 440 | fn relative_order_comparison( 441 | left: Vec, 442 | right: Vec, 443 | expected_extra: Vec<&i32>, 444 | expected_missing: Vec<&i32>, 445 | expected_order: bool, 446 | ) { 447 | let result = SequenceComparison::from_iter( 448 | left.iter(), 449 | right.iter(), 450 | SequenceOrderComparison::Relative, 451 | ); 452 | assert_eq!(expected_extra, result.extra); 453 | assert_eq!(expected_missing, result.missing); 454 | assert_eq!(expected_order, result.order_preserved); 455 | } 456 | 457 | // expected actual extra missing order 458 | // name 459 | #[test_case(vec![1, 2], vec![], vec![&1, &2], vec![], true ; "empty right operand")] 460 | #[test_case(vec![], vec![1, 2], vec![], vec![&1, &2], true ; "empty left operand")] 461 | #[test_case(vec![1, 2, 3], vec![1, 3], vec![&2], vec![], false ; "extra and relative order")] 462 | #[test_case(vec![1, 2, 3], vec![2, 3, 4], vec![&1], vec![&4], false ; "not found, both extra and missing")] 463 | #[test_case(vec![1, 2], vec![1, 2, 4], vec![], vec![&4], true ; "not found, extra prefix")] 464 | #[test_case(vec![1, 2], vec![0, 1, 2], vec![], vec![&0], false ; "not found, extra suffix")] 465 | #[test_case(vec![1, 2, 3], vec![3, 1], vec![&2], vec![], false ; "all found, out of order")] 466 | #[test_case(vec![1, 2, 3], vec![1, 2, 3], vec![], vec![], true ; "equal")] 467 | #[test_case(vec![1, 2, 3, 4, 5, 6], vec![1, 3, 6], vec![&2, &4, &5], vec![], false ; "order preserved relatively")] 468 | #[test_case(vec![1, 2, 3, 4, 5, 6], vec![3, 4, 5], vec![&1, &2, &6], vec![], false ; "order preserved strictly")] 469 | #[test_case(vec![1, 2, 3, 4], vec![1, 2, 3], vec![&4], vec![], true ; "prefix sub-sequence")] 470 | #[test_case(vec![1, 2, 3, 4], vec![2, 3, 4], vec![&1], vec![], false ; "suffix sub-sequence")] 471 | fn strict_order_comparison( 472 | left: Vec, 473 | right: Vec, 474 | expected_extra: Vec<&i32>, 475 | expected_missing: Vec<&i32>, 476 | expected_order: bool, 477 | ) { 478 | let result = SequenceComparison::from_iter( 479 | left.iter(), 480 | right.iter(), 481 | SequenceOrderComparison::Strict, 482 | ); 483 | assert_eq!(expected_extra, result.extra); 484 | assert_eq!(expected_missing, result.missing); 485 | assert_eq!(expected_order, result.order_preserved); 486 | } 487 | } 488 | } 489 | -------------------------------------------------------------------------------- /src/base.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::fmt; 16 | use std::fmt::Debug; 17 | use std::marker::PhantomData; 18 | use std::ops::Deref; 19 | 20 | /// An assertion macro that panics when the assertion fails. 21 | #[macro_export] 22 | macro_rules! assert_that { 23 | ($actual:expr) => { 24 | $crate::Subject::new( 25 | &$actual, 26 | stringify!($actual) 27 | .to_string() 28 | .replace(" ", "") 29 | .replace("\n", ""), 30 | /* description= */ None, 31 | /* option= */ (), 32 | Some($crate::Location::new( 33 | file!().to_string(), 34 | line!(), 35 | column!(), 36 | )), 37 | std::marker::PhantomData::<()>, 38 | ) 39 | }; 40 | } 41 | 42 | /// Data structure that contains a value to be tested (actual value) with auxiliary data (ex. line 43 | /// pos, description). 44 | pub struct Subject<'a, Sub, Opt, Ret> { 45 | actual: ActualValue<'a, Sub>, 46 | 47 | /// Stringified expression of actual value. 48 | /// Ex. `assert_that!(vec![1,2,3]).has_length(10)` -> `vec![1,2,3]` 49 | expr: String, 50 | 51 | /// Description for actual value. Will be used with "value of" fact message. 52 | /// Ex. assert_that!(actual_vec).has_length(10) -> "value of: actual_vec.len()" 53 | description: Option, 54 | 55 | /// Options that changes assertion behavior. 56 | /// Ex. tolerance for float almost equality assertion. 57 | /// 58 | /// For subjects having no option, unit `()` should be specified as option type `Opt`. 59 | /// 60 | /// Design Note: this option should be in generics to achieve changing available methods 61 | /// depending on the option type. Ex. when float tolerance is specified, not related methods 62 | /// should be unavailable. 63 | option: Opt, 64 | 65 | location: Option, 66 | return_type: PhantomData, 67 | } 68 | 69 | impl<'a, Sub, Opt, Ret> Subject<'a, Sub, Opt, Ret> { 70 | #[allow(dead_code)] // Used by macros. 71 | /// Creates a new subject with a referenced actual value. 72 | pub fn new( 73 | actual: &'a Sub, 74 | expr: String, 75 | description: Option, 76 | option: Opt, 77 | location: Option, 78 | return_type: PhantomData, 79 | ) -> Self { 80 | Subject { 81 | actual: ActualValue::Borrowed(actual), 82 | expr, 83 | description, 84 | option, 85 | location, 86 | return_type, 87 | } 88 | } 89 | 90 | /// Creates a new subject with an owned actual value. 91 | pub(super) fn new_from_owned_actual( 92 | actual: Sub, 93 | expr: String, 94 | description: Option, 95 | option: Opt, 96 | location: Option, 97 | return_type: PhantomData, 98 | ) -> Self { 99 | Subject { 100 | actual: ActualValue::Owned(actual), 101 | expr, 102 | description, 103 | option, 104 | location, 105 | return_type, 106 | } 107 | } 108 | } 109 | 110 | pub enum ActualValue<'a, S> { 111 | Owned(S), 112 | Borrowed(&'a S), 113 | } 114 | 115 | impl<'a, S> Deref for ActualValue<'a, S> { 116 | type Target = S; 117 | 118 | fn deref(&self) -> &Self::Target { 119 | match &self { 120 | ActualValue::Owned(value) => value, 121 | ActualValue::Borrowed(value) => value, 122 | } 123 | } 124 | } 125 | 126 | /// API for assertion library developer. 127 | /// 128 | /// Note: This trait hides methods for library developer from library users. 129 | /// TODO: think better name... 130 | pub trait AssertionApi<'a, Sub, Opt, Ret> { 131 | /// Builds a new instance of [AssertionResult]. 132 | fn new_result(&self) -> AssertionResult; 133 | 134 | /// Actual value. 135 | fn actual(&self) -> ⋐ 136 | 137 | /// Returns [stringified](https://doc.rust-lang.org/std/macro.stringify.html) expression of 138 | /// applied actual value. 139 | /// Ex. For `assert_that!(vec![1,2,3])`, `expr` will be `"vec![1,2,3]"`. 140 | fn expr(&self) -> &String; 141 | 142 | /// Returns description for actual value. For derived subjects (see [AssertionApi.new_subject]), 143 | /// `description` describes how derived from the original subject. For non derived subjects, 144 | /// `None` is returned instead. 145 | fn description(&self) -> &Option; 146 | 147 | /// Returns description for actual value. For derived subjects (see [AssertionApi.new_subject]), 148 | /// `description` describes how derived from the original subject. For non derived subjects, 149 | /// `expr` is returned instead. 150 | fn description_or_expr(&self) -> &String; 151 | 152 | fn option(&self) -> &Opt; 153 | fn option_mut(&mut self) -> &mut Opt; 154 | 155 | /// Code location. 156 | fn location(&self) -> &Option; 157 | 158 | /// Creates a new derived subject. 159 | /// 160 | /// `new_description` should describe how it derives from the previous subject in 161 | /// code-like style. For example, in case of asserting the length of a vector `vec![1,2,3]`, a 162 | /// derived subject for the vector length can be created by this method. The new_actual will be 163 | /// `vec![1,2,3].len()` and `new_description` can be `vec![1,2,3].len()` or 164 | /// `vec![1,2,3].size()`. You may need `format!()` and `AssertionApi::description_or_expr()` to 165 | /// generate `new_description`. 166 | fn new_subject( 167 | &self, 168 | new_actual: &'a NewSub, 169 | new_description: Option, 170 | new_opt: NewOpt, 171 | ) -> Subject; 172 | 173 | /// Creates a new derived subject. 174 | /// 175 | /// `new_description` should describe how it derives from the previous subject in 176 | /// code-like style. For example, in case of asserting the length of a vector `vec![1,2,3]`, a 177 | /// derived subject for the vector length can be created by this method. The new_actual will be 178 | /// `vec![1,2,3].len()` and `new_description` can be `vec![1,2,3].len()` or 179 | /// `vec![1,2,3].size()`. You may need `format!()` and `AssertionApi::description_or_expr()` to 180 | /// generate `new_description`. 181 | /// 182 | /// Differently from `new_subject`, this method takes owned actual value instead reference. 183 | fn new_owned_subject<'b, NewSub, NewOpt>( 184 | &self, 185 | new_actual: NewSub, 186 | new_description: Option, 187 | new_option: NewOpt, 188 | ) -> Subject<'b, NewSub, NewOpt, Ret>; 189 | } 190 | 191 | impl<'a, Sub, Opt, Ret> AssertionApi<'a, Sub, Opt, Ret> for Subject<'a, Sub, Opt, Ret> { 192 | fn new_result(&self) -> AssertionResult { 193 | let mut result = AssertionResult::new(self.location()); 194 | match &self.description { 195 | None => {} 196 | Some(description) => { 197 | result = result.add_fact("value of", description); 198 | } 199 | }; 200 | result 201 | } 202 | 203 | fn actual(&self) -> &Sub { 204 | &self.actual 205 | } 206 | 207 | fn expr(&self) -> &String { 208 | &self.expr 209 | } 210 | 211 | fn description(&self) -> &Option { 212 | &self.description 213 | } 214 | 215 | fn description_or_expr(&self) -> &String { 216 | match &self.description { 217 | None => self.expr(), 218 | Some(value) => value, 219 | } 220 | } 221 | 222 | fn option(&self) -> &Opt { 223 | &self.option 224 | } 225 | fn option_mut(&mut self) -> &mut Opt { 226 | &mut self.option 227 | } 228 | 229 | fn location(&self) -> &Option { 230 | &self.location 231 | } 232 | 233 | fn new_subject( 234 | &self, 235 | new_actual: &'a NewSub, 236 | new_description: Option, 237 | new_option: NewOpt, 238 | ) -> Subject { 239 | Subject::new( 240 | new_actual, 241 | self.expr.clone(), 242 | new_description, 243 | new_option, 244 | self.location.clone(), 245 | self.return_type, 246 | ) 247 | } 248 | fn new_owned_subject<'b, NewSub, NewOpt>( 249 | &self, 250 | new_actual: NewSub, 251 | new_description: Option, 252 | new_option: NewOpt, 253 | ) -> Subject<'b, NewSub, NewOpt, Ret> { 254 | Subject::new_from_owned_actual( 255 | new_actual, 256 | self.expr.clone(), 257 | new_description, 258 | new_option, 259 | self.location.clone(), 260 | self.return_type, 261 | ) 262 | } 263 | } 264 | 265 | /// A behavior for assertion pass and failure. [`AssertionResult`] implements this traits. 266 | /// 267 | /// Behavior for assertion pass and failure is different between [`assert_that`] and [`check_that`]. 268 | /// [`assert_that`] panics when assertion fails, but [`check_that`] results a struct in both cases. 269 | /// Those assertion behavior is switched by [`Subject.return_type`] and [`AssertionStrategy`]. 270 | pub trait AssertionStrategy { 271 | /// Behavior when assertion fails. 272 | #[track_caller] 273 | fn do_fail(self) -> R; 274 | 275 | /// Behavior when assertion passes. 276 | #[track_caller] 277 | fn do_ok(self) -> R; 278 | } 279 | 280 | impl AssertionStrategy<()> for AssertionResult { 281 | fn do_fail(self) { 282 | std::panic::panic_any(self.generate_message()); 283 | } 284 | 285 | fn do_ok(self) {} 286 | } 287 | 288 | /// Contains assertion results which will be shown in the assertion messages. 289 | #[allow(missing_docs)] 290 | #[derive(Clone)] 291 | pub struct AssertionResult { 292 | location: Option, 293 | facts: Vec, 294 | } 295 | 296 | #[allow(missing_docs)] 297 | impl AssertionResult { 298 | const DEBUG_LENGTH_WRAP_LIMIT: usize = 80; 299 | 300 | pub(self) fn new(location: &Option) -> Self { 301 | AssertionResult { 302 | location: location.as_ref().map(|loc| format!("{}", loc)), 303 | facts: vec![], 304 | } 305 | } 306 | 307 | #[inline] 308 | pub fn add_fact, V: Into>(mut self, key: K, value: V) -> Self { 309 | self.facts.push(Fact::new(key, value)); 310 | self 311 | } 312 | 313 | pub fn add_formatted_fact, V: Debug>(mut self, key: K, value: V) -> Self { 314 | self.facts.push(Fact::new(key, format!("{:?}", value))); 315 | self 316 | } 317 | 318 | #[inline] 319 | pub fn add_formatted_values_fact, V: Debug>( 320 | mut self, 321 | key: K, 322 | values: Vec, 323 | ) -> Self { 324 | let str_values = values.iter().map(|v| format!("{:?}", v)).collect(); 325 | self.facts.push(Fact::new_multi_value_fact(key, str_values)); 326 | self 327 | } 328 | 329 | #[inline] 330 | pub fn add_simple_formatted_fact(mut self, value: V) -> Self { 331 | self.facts 332 | .push(Fact::new_simple_fact(format!("{:?}", value))); 333 | self 334 | } 335 | 336 | #[inline] 337 | pub fn add_simple_fact>(mut self, value: V) -> Self { 338 | self.facts.push(Fact::new_simple_fact(value)); 339 | self 340 | } 341 | 342 | #[inline] 343 | pub fn add_splitter(mut self) -> Self { 344 | self.facts.push(Fact::new_splitter()); 345 | self 346 | } 347 | 348 | /// Generates an assertion message from the assertion result. 349 | pub fn generate_message(&self) -> String { 350 | let mut messages = vec![]; 351 | 352 | messages.push(format!( 353 | "assertion failed{maybe_loc}", 354 | maybe_loc = match &self.location { 355 | None => String::new(), 356 | Some(loc) => format!(": {}", loc), 357 | } 358 | )); 359 | 360 | let longest_key_length = self 361 | .facts 362 | .iter() 363 | .flat_map(|fact| match fact { 364 | Fact::KeyValue { key, .. } => Some(key), 365 | Fact::KeyValues { key, .. } => Some(key), 366 | _ => None, 367 | }) 368 | .map(|key| key.len()) 369 | .max() 370 | .unwrap_or(0); 371 | 372 | for x in self.facts.iter() { 373 | match x { 374 | Fact::KeyValue { key, value } => messages.push(format!( 375 | "{key:width$}: {value}", 376 | key = key, 377 | value = value, 378 | width = longest_key_length 379 | )), 380 | Fact::KeyValues { key, values } => { 381 | let values_size = values.len(); 382 | let use_multiline_output = values 383 | .clone() 384 | .iter() 385 | .map(|x| format!("{:?}", x).len()) 386 | .max_by(|x, y| x.cmp(y)) 387 | .unwrap_or(0) 388 | > Self::DEBUG_LENGTH_WRAP_LIMIT; 389 | let formatted_values = format!( 390 | "{}", 391 | if use_multiline_output { 392 | let elements = values 393 | .iter() 394 | .map(|el| format!(" - {}", el)) 395 | .collect::>() 396 | .join("\n"); 397 | if values_size > 0 { 398 | format!("[\n{}\n]", elements) 399 | } else { 400 | "[]".to_string() 401 | } 402 | } else { 403 | format!( 404 | "[ {} ]", 405 | values 406 | .iter() 407 | .map(|el| format!("{}", el)) 408 | .collect::>() 409 | .join(", ") 410 | ) 411 | } 412 | ); 413 | println!("{}", formatted_values); 414 | messages.push(format!( 415 | "{key:width$}: {value}", 416 | key = key, 417 | value = formatted_values, 418 | width = longest_key_length 419 | )); 420 | } 421 | Fact::Value { value } => messages.push(value.to_string()), 422 | Fact::Splitter => messages.push(String::from("---")), 423 | } 424 | } 425 | messages.join("\n") 426 | } 427 | 428 | pub fn facts(&self) -> &Vec { 429 | &self.facts 430 | } 431 | } 432 | 433 | impl Debug for AssertionResult { 434 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 435 | f.write_str(&self.generate_message()) 436 | } 437 | } 438 | 439 | /// Code location. 440 | /// 441 | /// # Related 442 | /// - [`core::panic::Location`] 443 | #[derive(Debug, Clone)] 444 | pub struct Location { 445 | file: String, 446 | line: u32, 447 | column: u32, 448 | } 449 | 450 | impl Location { 451 | /// Creates a new location instance. 452 | #[allow(dead_code)] // Used by macros. 453 | pub fn new>(file: I, line: u32, column: u32) -> Self { 454 | Location { 455 | file: file.into(), 456 | line, 457 | column, 458 | } 459 | } 460 | } 461 | 462 | impl fmt::Display for Location { 463 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 464 | f.write_fmt(format_args!("{}:{}:{}", self.file, self.line, self.column)) 465 | } 466 | } 467 | 468 | /// A piece of assertion message. 469 | /// 470 | /// # Design discussion 471 | /// - New entry for having elements? 472 | /// - `KeyValues {key: String, value: Vec}` 473 | /// - New entry for comparing elements? 474 | /// - `Comparison {key: String, actual: Vec, expected: Vec}` 475 | #[allow(missing_docs)] 476 | #[derive(Debug, Clone, Eq, PartialEq)] 477 | pub enum Fact { 478 | /// Keyed assertion message 479 | /// 480 | /// # Example 481 | /// ```text 482 | /// Fact {key: "expected", value: "foo"} 483 | /// Fact {key: "actual", value: "var"} 484 | /// ``` 485 | KeyValue { key: String, value: String }, 486 | /// Keyed assertion message for multiple values 487 | /// 488 | /// # Example 489 | /// ```text 490 | /// Fact {key: "expected", values: vec!["foo", "bar"]} 491 | /// ``` 492 | KeyValues { key: String, values: Vec }, 493 | /// Single assertion message 494 | /// 495 | /// # Example 496 | /// ```text 497 | /// Fact {value: "expected that the vec is empty"} 498 | /// ``` 499 | Value { value: String }, 500 | /// Splitter 501 | Splitter, 502 | } 503 | 504 | #[allow(missing_docs)] 505 | impl Fact { 506 | pub fn new, V: Into>(key: K, value: V) -> Fact { 507 | Fact::KeyValue { 508 | key: key.into(), 509 | value: value.into(), 510 | } 511 | } 512 | pub fn new_simple_fact>(value: V) -> Fact { 513 | Fact::Value { 514 | value: value.into(), 515 | } 516 | } 517 | pub fn new_multi_value_fact, V: Into>(key: K, values: Vec) -> Fact { 518 | Fact::KeyValues { 519 | key: key.into(), 520 | values: values.into_iter().map(|v| v.into()).collect(), 521 | } 522 | } 523 | pub fn new_splitter() -> Fact { 524 | Fact::Splitter 525 | } 526 | } 527 | 528 | #[cfg(test)] 529 | mod tests { 530 | use crate::*; 531 | use crate::testing::CheckThatResult; 532 | 533 | use super::*; 534 | 535 | #[test] 536 | fn assert_that() { 537 | // macro doesn't fail 538 | assert_that!(1); 539 | assert_that!(vec![""]); 540 | } 541 | 542 | #[test] 543 | fn assert_that_unit_return_type() { 544 | assert_eq!(assert_that!(1).return_type, PhantomData::<()>::default()); 545 | assert_eq!( 546 | assert_that!(vec![""]).return_type, 547 | PhantomData::<()>::default() 548 | ); 549 | } 550 | 551 | #[test] 552 | fn check_that() { 553 | // macro doesn't fail 554 | check_that!(1); 555 | check_that!(vec![""]); 556 | } 557 | 558 | #[test] 559 | fn check_that_result_return_type() { 560 | assert_eq!( 561 | check_that!(1).return_type, 562 | PhantomData::::default() 563 | ); 564 | assert_eq!( 565 | check_that!(vec![""]).return_type, 566 | PhantomData::::default() 567 | ); 568 | } 569 | 570 | #[test] 571 | fn assert_result_message_generation() { 572 | assert_eq!( 573 | AssertionResult::new(&None).generate_message(), 574 | "assertion failed" 575 | ); 576 | assert_eq!( 577 | AssertionResult::new(&Some(Location::new("foo.rs", 123, 456))).generate_message(), 578 | "assertion failed: foo.rs:123:456" 579 | ); 580 | assert_eq!( 581 | AssertionResult::new(&Some(Location::new("foo.rs", 123, 456))) 582 | .add_fact("foo", "bar") 583 | .generate_message(), 584 | r#"assertion failed: foo.rs:123:456 585 | foo: bar"# 586 | ); 587 | assert_eq!( 588 | AssertionResult::new(&Some(Location::new("foo.rs", 123, 456))) 589 | .add_fact("foo", "bar") 590 | .add_fact("looooong key", "align indent") 591 | .generate_message(), 592 | r#"assertion failed: foo.rs:123:456 593 | foo : bar 594 | looooong key: align indent"# 595 | ); 596 | assert_eq!( 597 | AssertionResult::new(&Some(Location::new("foo.rs", 123, 456))) 598 | .add_fact("foo", "bar") 599 | .add_fact("looooong key", "align indent") 600 | .add_fact("s", "hort") 601 | .generate_message(), 602 | r#"assertion failed: foo.rs:123:456 603 | foo : bar 604 | looooong key: align indent 605 | s : hort"# 606 | ); 607 | assert_eq!( 608 | AssertionResult::new(&Some(Location::new("foo.rs", 123, 456))) 609 | .add_fact("foo", "bar") 610 | .add_splitter() 611 | .add_fact("s", "hort") 612 | .generate_message(), 613 | r#"assertion failed: foo.rs:123:456 614 | foo: bar 615 | --- 616 | s : hort"# 617 | ); 618 | assert_eq!( 619 | AssertionResult::new(&Some(Location::new("foo.rs", 123, 456))) 620 | .add_fact("foo", "bar") 621 | .add_simple_fact("I am ninja") 622 | .add_fact("s", "hort") 623 | .generate_message(), 624 | r#"assertion failed: foo.rs:123:456 625 | foo: bar 626 | I am ninja 627 | s : hort"# 628 | ); 629 | assert_eq!( 630 | AssertionResult::new(&Some(Location::new("foo.rs", 123, 456))) 631 | .add_fact("looooong key", "align indent") 632 | .add_formatted_values_fact("kv_key", vec!["short_value"]) 633 | .generate_message(), 634 | r#"assertion failed: foo.rs:123:456 635 | looooong key: align indent 636 | kv_key : [ "short_value" ]"# 637 | ); 638 | assert_eq!( 639 | AssertionResult::new(&Some(Location::new("foo.rs", 123, 456))) 640 | .add_fact("looooong key", "align indent") 641 | .add_formatted_values_fact("kv_key", vec!["short_value", "Very long value is formatted using new lines, this is done to improve output readability."]) 642 | .generate_message(), 643 | r#"assertion failed: foo.rs:123:456 644 | looooong key: align indent 645 | kv_key : [ 646 | - "short_value" 647 | - "Very long value is formatted using new lines, this is done to improve output readability." 648 | ]"# 649 | ); 650 | assert_eq!( 651 | AssertionResult::new(&Some(Location::new("foo.rs", 123, 456))) 652 | .add_formatted_values_fact("kv_key", vec![1, 2, 3]) 653 | .generate_message(), 654 | r#"assertion failed: foo.rs:123:456 655 | kv_key: [ 1, 2, 3 ]"# 656 | ); 657 | assert_eq!( 658 | AssertionResult::new(&Some(Location::new("foo.rs", 123, 456))) 659 | .add_formatted_values_fact("kv_key", vec!["1", "2", "3"]) 660 | .generate_message(), 661 | r#"assertion failed: foo.rs:123:456 662 | kv_key: [ "1", "2", "3" ]"# 663 | ); 664 | #[derive(Debug)] 665 | struct LongOutputData<'a> { 666 | #[allow(dead_code)] 667 | val: Option, 668 | #[allow(dead_code)] 669 | nested: Vec<&'a str>, 670 | } 671 | assert_eq!( 672 | AssertionResult::new(&Some(Location::new("foo.rs", 123, 456))) 673 | .add_formatted_values_fact( 674 | "kv_key_sht", 675 | vec![LongOutputData { 676 | val: None, 677 | nested: vec!["123", "321"] 678 | }] 679 | ) 680 | .add_formatted_values_fact( 681 | "kv_key_lng", 682 | vec![ 683 | LongOutputData { 684 | val: Some(123456789), 685 | nested: vec!["hello", "long", "debug", "output"] 686 | }, 687 | LongOutputData { 688 | val: None, 689 | nested: vec![] 690 | } 691 | ] 692 | ) 693 | .generate_message(), 694 | r#"assertion failed: foo.rs:123:456 695 | kv_key_sht: [ LongOutputData { val: None, nested: ["123", "321"] } ] 696 | kv_key_lng: [ 697 | - LongOutputData { val: Some(123456789), nested: ["hello", "long", "debug", "output"] } 698 | - LongOutputData { val: None, nested: [] } 699 | ]"# 700 | ); 701 | assert_eq!( 702 | AssertionResult::new(&Some(Location::new("foo.rs", 123, 456))) 703 | .add_formatted_fact( 704 | "k", 705 | LongOutputData { 706 | val: Some(1), 707 | nested: vec!["123", "321"] 708 | } 709 | ) 710 | .add_simple_formatted_fact(LongOutputData { 711 | val: Some(2), 712 | nested: vec!["1234"] 713 | }) 714 | .generate_message(), 715 | r#"assertion failed: foo.rs:123:456 716 | k: LongOutputData { val: Some(1), nested: ["123", "321"] } 717 | LongOutputData { val: Some(2), nested: ["1234"] }"# 718 | ); 719 | } 720 | } 721 | -------------------------------------------------------------------------------- /src/assertions/iterator.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::borrow::Borrow; 16 | use std::fmt::Debug; 17 | 18 | use crate::base::{AssertionApi, AssertionResult, AssertionStrategy, Subject}; 19 | use crate::diff::iter::{SequenceComparison, SequenceOrderComparison}; 20 | 21 | /// Trait for iterator assertion. 22 | /// 23 | /// # Example 24 | /// ``` 25 | /// use assertor::*; 26 | /// 27 | /// assert_that!(Vec::::new()).is_empty(); 28 | /// assert_that!(vec![1,2,3].iter()).contains(&2); 29 | /// assert_that!(vec![1,2,3].iter()).contains_exactly(vec![3,2,1].iter()); 30 | /// assert_that!(vec![1,2,3].iter()).contains_exactly_in_order(vec![1,2,3].iter()); 31 | /// ``` 32 | /// ```should_panic 33 | /// use assertor::*; 34 | /// assert_that!(vec![1,2,3].iter()).contains(&4); // <- Panic here 35 | /// // expected to contain : 4 36 | /// // but did not 37 | /// // though it did contain: [1, 2, 3] 38 | /// ``` 39 | /// ```should_panic 40 | /// use assertor::*; 41 | /// assert_that!(vec![1,2,3].iter()).contains_exactly_in_order(vec![3,2,1].iter()); // <- Panic here 42 | /// // contents match, but order was wrong 43 | /// // --- 44 | /// // expected: [3, 2, 1] 45 | /// // actual : [1, 2, 3] 46 | /// ``` 47 | pub trait IteratorAssertion<'a, S, T, R> 48 | where 49 | AssertionResult: AssertionStrategy, 50 | { 51 | /// Checks that the subject iterator contains the element `expected`. 52 | /// 53 | /// # Example 54 | /// ``` 55 | /// use assertor::*; 56 | /// assert_that!(vec![1, 2, 3].iter()).contains(&2); 57 | /// assert_that!("foobar".chars()).contains(&'a'); 58 | /// ``` 59 | /// ```should_panic 60 | /// use assertor::*; 61 | /// assert_that!("foobar".chars()).contains(&'z'); 62 | /// // expected to contain : 'z' 63 | /// // but did not 64 | /// // though it did contain: ['f', 'o', 'o', 'b', 'a', 'r'] 65 | /// ``` 66 | /// 67 | /// ## Related: 68 | /// - [`crate::StringAssertion::contains`] 69 | /// - [`crate::VecAssertion::contains`] 70 | #[track_caller] 71 | fn contains(&self, element: B) -> R 72 | where 73 | B: Borrow, 74 | T: PartialEq + Debug; 75 | 76 | /// Checks that the subject iterator does not contains the element `expected`. 77 | /// 78 | /// # Example 79 | /// ``` 80 | /// use assertor::*; 81 | /// assert_that!(vec![1, 2, 3].iter()).does_not_contain(&5); 82 | /// assert_that!("foobar".chars()).does_not_contain(&'x'); 83 | /// ``` 84 | /// ```should_panic 85 | /// use assertor::*; 86 | /// assert_that!("foobar".chars()).does_not_contain(&'a'); 87 | /// // expected to not contain : 'a' 88 | /// // but element was found 89 | /// // though it did contain: ['f', 'o', 'o', 'b', 'a', 'r'] 90 | /// ``` 91 | /// 92 | /// ## Related: 93 | /// - [`crate::StringAssertion::does_not_contain`] 94 | /// - [`crate::VecAssertion::does_not_contain`] 95 | #[track_caller] 96 | fn does_not_contain(&self, element: B) -> R 97 | where 98 | B: Borrow, 99 | T: PartialEq + Debug; 100 | 101 | /// Checks that the subject exactly contains elements of `expected_iter`. 102 | /// 103 | /// This method doesn't take care of the order. Use 104 | /// [contains_exactly_in_order](`IteratorAssertion::contains_exactly_in_order`) to check 105 | /// elements are in the same order. 106 | /// 107 | /// # Example 108 | /// ``` 109 | /// use assertor::*; 110 | /// assert_that!(vec![1, 2, 3].iter()).contains_exactly(vec![3, 2, 1].iter()); 111 | /// assert_that!("foobarbaz".chars()).contains_exactly("bazbarfoo".chars()); 112 | /// ``` 113 | /// ```should_panic 114 | /// use assertor::*; 115 | /// assert_that!("foobarbaz".chars()).contains_exactly("bazbar".chars()); 116 | /// // unexpected (3): ['f', 'o', 'o'] 117 | /// //--- 118 | /// // expected : ['b', 'a', 'z', 'b', 'a', 'r'] 119 | /// // actual : ['f', 'o', 'o', 'b', 'a', 'r', 'b', 'a', 'z'] 120 | /// ``` 121 | #[track_caller] 122 | fn contains_exactly + Clone>(self, expected_iter: EI) -> R 123 | where 124 | T: PartialEq + Debug; 125 | 126 | /// Checks that the subject exactly contains elements of `expected_iter` in the same order. 127 | /// 128 | /// # Example 129 | /// ``` 130 | /// use assertor::*; 131 | /// assert_that!(vec![1, 2, 3].iter()).contains_exactly_in_order(vec![1, 2, 3].iter()); 132 | /// assert_that!("foobarbaz".chars()).contains_exactly_in_order("foobarbaz".chars()); 133 | /// ``` 134 | /// ```should_panic 135 | /// use assertor::*; 136 | /// assert_that!("foobarbaz".chars()).contains_exactly_in_order("bazbar".chars()); 137 | /// // unexpected (3): ['f', 'o', 'o'] 138 | /// //--- 139 | /// // expected : ['b', 'a', 'z', 'b', 'a', 'r'] 140 | /// // actual : ['f', 'o', 'o', 'b', 'a', 'r', 'b', 'a', 'z'] 141 | /// ``` 142 | /// ```should_panic 143 | /// use assertor::*; 144 | /// assert_that!("foobarbaz".chars()).contains_exactly_in_order("bazbarfoo".chars()); 145 | /// // contents match, but order was wrong 146 | /// // --- 147 | /// // expected: ['b', 'a', 'z', 'b', 'a', 'r', 'f', 'o', 'o'] 148 | /// // actual : ['f', 'o', 'o', 'b', 'a', 'r', 'b', 'a', 'z'] 149 | /// ``` 150 | #[track_caller] 151 | fn contains_exactly_in_order + Clone>(self, expected_iter: EI) -> R 152 | where 153 | T: PartialEq + Debug; 154 | 155 | /// Checks that the subject contains at least all elements of `expected_iter`. 156 | /// 157 | /// This method doesn't take care of the order. Use 158 | /// [contains_all_of_in_order](`IteratorAssertion::contains_all_of_in_order`) to check 159 | /// elements are in the same order. 160 | /// 161 | /// # Example 162 | /// ``` 163 | /// use assertor::*; 164 | /// assert_that!(vec![1, 2, 3].iter()).contains_all_of(vec![2, 3].iter()); 165 | /// assert_that!("foobarbaz".chars()).contains_all_of("bazbar".chars()); 166 | /// ``` 167 | #[track_caller] 168 | fn contains_all_of + Clone>(self, expected_iter: EI) -> R 169 | where 170 | T: PartialEq + Debug; 171 | 172 | /// Checks that the subject does not contains any elements of `elements`. 173 | /// 174 | /// # Example 175 | /// ``` 176 | /// use assertor::*; 177 | /// assert_that!(vec![1, 2, 3].iter()).does_not_contain_any(vec![0, -5].iter()); 178 | /// assert_that!("foobarbaz".chars()).does_not_contain_any("xyw".chars()); 179 | /// ``` 180 | /// ```should_panic 181 | /// use assertor::*; 182 | /// assert_that!("foobarbaz".chars()).does_not_contain_any("ab".chars()); 183 | /// // unexpected (2): ['a', 'b'] 184 | /// //--- 185 | /// // expected to contain none of : ['a', 'b'] 186 | /// // but was : ['f', 'o', 'o', 'b', 'a', 'r', 'b', 'a', 'z'] 187 | /// ``` 188 | #[track_caller] 189 | fn does_not_contain_any + Clone>(&self, elements: EI) -> R 190 | where 191 | T: PartialEq + Debug; 192 | 193 | /// Checks that the subject contains at least all elements of `expected_iter` in the same order. 194 | /// 195 | /// # Example 196 | /// ``` 197 | /// use assertor::*; 198 | /// assert_that!(vec![1, 2, 3].iter()).contains_all_of_in_order(vec![2, 3].iter()); 199 | /// assert_that!("foobarbaz".chars()).contains_all_of_in_order("obarb".chars()); 200 | /// ``` 201 | #[track_caller] 202 | fn contains_all_of_in_order + Clone>(self, expected_iter: EI) -> R 203 | where 204 | T: PartialEq + Debug; 205 | 206 | /// Checks that the subject is empty. 207 | /// 208 | /// # Example 209 | /// ``` 210 | /// use assertor::*; 211 | /// assert_that!(Vec::::new()).is_empty(); 212 | /// assert_that!("".chars()).is_empty(); 213 | /// ``` 214 | /// ```should_panic 215 | /// use assertor::*; 216 | /// assert_that!(vec![1]).is_empty(); 217 | /// // expected to be empty 218 | /// // --- 219 | /// // actual: [1] 220 | /// ``` 221 | #[track_caller] 222 | fn is_empty(&self) -> R 223 | where 224 | T: Debug; 225 | 226 | /// Checks that the subject is not empty. 227 | /// 228 | /// # Example 229 | /// ``` 230 | /// use assertor::*; 231 | /// assert_that!(vec![1]).is_not_empty(); 232 | /// assert_that!("abc".chars()).is_not_empty(); 233 | /// ``` 234 | /// ```should_panic 235 | /// use assertor::*; 236 | /// assert_that!(Vec::::new()).is_not_empty(); 237 | /// // expected to be non-empty 238 | /// // --- 239 | /// // actual: [] 240 | /// ``` 241 | #[track_caller] 242 | fn is_not_empty(&self) -> R 243 | where 244 | T: Debug; 245 | 246 | /// Checks that the subject has the given length. 247 | /// 248 | /// # Example 249 | /// ``` 250 | /// use assertor::*; 251 | /// assert_that!(vec![1,2,3]).has_length(3); 252 | /// assert_that!("foobarbaz".chars()).has_length(9); 253 | /// ``` 254 | /// ```should_panic 255 | /// use assertor::*; 256 | /// assert_that!(vec![1,2,3]).has_length(2); 257 | /// // value of: vec![1,2,3].size() 258 | /// // expected: 2 259 | /// // actual : 3 260 | /// ``` 261 | #[track_caller] 262 | fn has_length(&self, length: usize) -> R 263 | where 264 | T: Debug; 265 | } 266 | 267 | impl<'a, S, T, R> IteratorAssertion<'a, S, T, R> for Subject<'a, S, (), R> 268 | where 269 | S: Iterator + Clone, 270 | AssertionResult: AssertionStrategy, 271 | { 272 | fn contains(&self, element: B) -> R 273 | where 274 | B: Borrow, 275 | T: PartialEq + Debug, 276 | { 277 | check_contains(self.new_result(), self.actual().clone(), element.borrow()) 278 | } 279 | 280 | fn does_not_contain(&self, element: B) -> R 281 | where 282 | B: Borrow, 283 | T: PartialEq + Debug, 284 | { 285 | check_does_not_contain(self.new_result(), self.actual().clone(), element.borrow()) 286 | } 287 | 288 | fn contains_exactly + Clone>(self, expected_iter: EI) -> R 289 | where 290 | T: PartialEq + Debug, 291 | { 292 | let comparison = SequenceComparison::from_iter( 293 | self.actual().clone(), 294 | expected_iter.clone(), 295 | SequenceOrderComparison::Strict, 296 | ); 297 | if comparison.contains_exactly() { 298 | self.new_result().do_ok() 299 | } else { 300 | feed_facts_about_item_diff( 301 | self.new_result(), 302 | &comparison, 303 | self.actual().clone(), 304 | expected_iter, 305 | ) 306 | .do_fail() 307 | } 308 | } 309 | 310 | fn contains_exactly_in_order + Clone>(self, expected_iter: EI) -> R 311 | where 312 | T: PartialEq + Debug, 313 | { 314 | let comparison = SequenceComparison::from_iter( 315 | self.actual().clone(), 316 | expected_iter.clone(), 317 | SequenceOrderComparison::Strict, 318 | ); 319 | let (result, ok) = check_contains_exactly_in_order( 320 | comparison, 321 | self.actual().clone(), 322 | expected_iter, 323 | self.new_result(), 324 | ); 325 | if ok { 326 | result.do_ok() 327 | } else { 328 | result.do_fail() 329 | } 330 | } 331 | 332 | fn contains_all_of + Clone>(self, expected_iter: EI) -> R 333 | where 334 | T: PartialEq + Debug, 335 | { 336 | let comparison = SequenceComparison::from_iter( 337 | self.actual().clone(), 338 | expected_iter.clone(), 339 | SequenceOrderComparison::Relative, 340 | ); 341 | if comparison.contains_all() { 342 | self.new_result().do_ok() 343 | } else { 344 | let missing = comparison.missing; 345 | self.new_result() 346 | .add_fact( 347 | format!("missing ({})", missing.len()), 348 | format!("{:?}", missing), 349 | ) 350 | .add_splitter() 351 | .add_formatted_values_fact("expected to contain at least", expected_iter.collect()) 352 | .add_formatted_values_fact("but was", self.actual().clone().collect()) 353 | // Idea: implement near_miss_obj 354 | // .add_fact("tough it did contain", format!("{:?}", near_miss_obj)) 355 | .do_fail() 356 | } 357 | } 358 | 359 | fn does_not_contain_any + Clone>(&self, elements: EI) -> R 360 | where 361 | T: PartialEq + Debug, 362 | { 363 | let els = elements.clone().collect::>(); 364 | // set-like intersection satisfies containment requirement for this case 365 | // TODO: move to sequence comparison API instead of in-place computation 366 | let intersection: Vec = self 367 | .actual() 368 | .clone() 369 | .filter(|el| els.contains(el)) 370 | .collect(); 371 | // handle empty iterables 372 | if intersection.is_empty() 373 | || self.actual().clone().next().is_none() 374 | || elements.clone().next().is_none() 375 | { 376 | self.new_result().do_ok() 377 | } else { 378 | self.new_result() 379 | .add_fact( 380 | format!("found ({})", intersection.len()), 381 | format!("{:?}", intersection), 382 | ) 383 | .add_splitter() 384 | .add_formatted_values_fact("expected to contain none of", elements.collect()) 385 | .add_formatted_values_fact("but was", self.actual().clone().collect()) 386 | .do_fail() 387 | } 388 | } 389 | 390 | fn contains_all_of_in_order + Clone>(self, expected_iter: EI) -> R 391 | where 392 | T: PartialEq + Debug, 393 | { 394 | let comparison = SequenceComparison::from_iter( 395 | self.actual().clone(), 396 | expected_iter.clone(), 397 | SequenceOrderComparison::Relative, 398 | ); 399 | let (result, ok) = check_contains_all_of_in_order( 400 | comparison, 401 | self.actual().clone(), 402 | expected_iter, 403 | self.new_result(), 404 | ); 405 | if ok { 406 | result.do_ok() 407 | } else { 408 | result.do_fail() 409 | } 410 | } 411 | 412 | fn is_empty(&self) -> R 413 | where 414 | T: Debug, 415 | { 416 | check_is_empty(self.new_result(), self.actual().clone()) 417 | } 418 | 419 | fn is_not_empty(&self) -> R 420 | where 421 | T: Debug, 422 | { 423 | check_is_not_empty(self.new_result(), self.actual().clone()) 424 | } 425 | 426 | fn has_length(&self, length: usize) -> R 427 | where 428 | T: Debug, 429 | { 430 | check_has_length( 431 | self.new_result(), 432 | self.actual().clone(), 433 | self.expr(), 434 | length, 435 | ) 436 | } 437 | } 438 | 439 | pub(crate) fn check_is_empty(assertion_result: AssertionResult, actual_iter: I) -> R 440 | where 441 | AssertionResult: AssertionStrategy, 442 | I: Iterator + Clone, 443 | T: Debug, 444 | { 445 | if actual_iter.clone().next().is_none() { 446 | assertion_result.do_ok() 447 | } else { 448 | assertion_result 449 | .add_simple_fact("expected to be empty") 450 | .add_splitter() 451 | .add_formatted_values_fact("actual", actual_iter.collect()) 452 | .do_fail() 453 | } 454 | } 455 | 456 | pub(crate) fn check_is_not_empty(assertion_result: AssertionResult, actual_iter: I) -> R 457 | where 458 | AssertionResult: AssertionStrategy, 459 | I: Iterator + Clone, 460 | T: Debug, 461 | { 462 | if actual_iter.clone().next().is_none() { 463 | assertion_result 464 | .add_simple_fact("expected to be non-empty") 465 | .add_splitter() 466 | .add_fact("actual", format!("{:?}", actual_iter.collect::>())) 467 | .do_fail() 468 | } else { 469 | assertion_result.do_ok() 470 | } 471 | } 472 | 473 | pub(crate) fn check_contains( 474 | assertion_result: AssertionResult, 475 | actual_iter: I, 476 | element: &T, 477 | ) -> R 478 | where 479 | AssertionResult: AssertionStrategy, 480 | I: Iterator + Clone, 481 | T: PartialEq + Debug, 482 | { 483 | if actual_iter.clone().any(|x| x.eq(element.borrow())) { 484 | assertion_result.do_ok() 485 | } else { 486 | assertion_result 487 | .add_fact("expected to contain", format!("{:?}", element)) 488 | .add_simple_fact("but did not") 489 | .add_formatted_values_fact("though it did contain", actual_iter.clone().collect()) 490 | .do_fail() 491 | } 492 | } 493 | 494 | pub(crate) fn check_does_not_contain( 495 | assertion_result: AssertionResult, 496 | actual_iter: I, 497 | element: &T, 498 | ) -> R 499 | where 500 | AssertionResult: AssertionStrategy, 501 | I: Iterator + Clone, 502 | T: PartialEq + Debug, 503 | { 504 | if actual_iter.clone().any(|x| x.eq(element.borrow())) { 505 | assertion_result 506 | .add_fact("expected to not contain", format!("{:?}", element)) 507 | .add_simple_fact("but element was found") 508 | .add_formatted_values_fact("though it did contain", actual_iter.clone().collect()) 509 | .do_fail() 510 | } else { 511 | assertion_result.do_ok() 512 | } 513 | } 514 | 515 | pub(crate) fn check_contains_exactly_in_order( 516 | comparison: SequenceComparison, 517 | actual: I, 518 | expected_iter: EI, 519 | assertion_result: AssertionResult, 520 | ) -> (AssertionResult, bool) 521 | where 522 | AssertionResult: AssertionStrategy, 523 | T: PartialEq + Debug, 524 | EI: Iterator + Clone, 525 | I: Iterator + Clone, 526 | { 527 | if comparison.contains_exactly() && comparison.order_preserved { 528 | (assertion_result, true) 529 | } else if comparison.contains_exactly() && !comparison.order_preserved { 530 | ( 531 | assertion_result 532 | .add_simple_fact("contents match, but order was wrong") 533 | .add_splitter() 534 | .add_formatted_values_fact("expected", expected_iter.collect()) 535 | .add_formatted_values_fact("actual", actual.collect()), 536 | false, 537 | ) 538 | } else { 539 | ( 540 | feed_facts_about_item_diff(assertion_result, &comparison, actual, expected_iter), 541 | false, 542 | ) 543 | } 544 | } 545 | 546 | pub(crate) fn check_contains_all_of_in_order( 547 | comparison: SequenceComparison, 548 | actual: I, 549 | expected_iter: EI, 550 | assertion_result: AssertionResult, 551 | ) -> (AssertionResult, bool) 552 | where 553 | AssertionResult: AssertionStrategy, 554 | T: PartialEq + Debug, 555 | EI: Iterator + Clone, 556 | I: Iterator + Clone, 557 | { 558 | if comparison.contains_all() && comparison.order_preserved { 559 | (assertion_result, true) 560 | } else if comparison.contains_all() { 561 | ( 562 | assertion_result 563 | .add_simple_fact("required elements were all found, but order was wrong") 564 | .add_formatted_values_fact( 565 | "expected order for required elements", 566 | expected_iter.clone().collect(), 567 | ) 568 | .add_formatted_values_fact("but was", actual.collect()), 569 | false, 570 | ) 571 | } else { 572 | let missing = comparison.missing; 573 | ( 574 | assertion_result 575 | .add_fact( 576 | format!("missing ({})", missing.len()), 577 | format!("{:?}", missing), 578 | ) 579 | // Idea: implement near_miss_obj 580 | // .add_fact("tough it did contain", format!("{:?}", near_miss_obj)) 581 | .add_splitter() 582 | .add_formatted_values_fact("expected to contain at least", expected_iter.collect()) 583 | .add_formatted_values_fact("but was", actual.collect()), 584 | false, 585 | ) 586 | } 587 | } 588 | 589 | pub(crate) fn feed_facts_about_item_diff< 590 | T: Debug + PartialEq, 591 | A: Debug, 592 | E: Debug, 593 | IA: Iterator + Clone, 594 | IE: Iterator + Clone, 595 | >( 596 | mut result: AssertionResult, 597 | comparison: &SequenceComparison, 598 | actual_iter: IA, 599 | expected_iter: IE, 600 | ) -> AssertionResult { 601 | let mut splitter = false; 602 | if !comparison.missing.is_empty() { 603 | result = result.add_fact( 604 | format!("missing ({})", comparison.missing.len()), 605 | format!("{:?}", comparison.missing), 606 | ); 607 | splitter = true; 608 | } 609 | if !comparison.extra.is_empty() { 610 | result = result.add_fact( 611 | format!("unexpected ({})", comparison.extra.len()), 612 | format!("{:?}", comparison.extra), 613 | ); 614 | splitter = true; 615 | } 616 | if splitter { 617 | result = result.add_splitter(); 618 | } 619 | result 620 | .add_formatted_values_fact("expected", expected_iter.clone().collect()) 621 | .add_formatted_values_fact("actual", actual_iter.clone().collect()) 622 | } 623 | 624 | pub(crate) fn check_has_length( 625 | assertion_result: AssertionResult, 626 | actual_iter: I, 627 | actual_expr: &str, 628 | length: usize, 629 | ) -> R 630 | where 631 | AssertionResult: AssertionStrategy, 632 | I: Iterator + Clone, 633 | { 634 | let actual = actual_iter.count(); 635 | if actual.eq(&length) { 636 | assertion_result.do_ok() 637 | } else { 638 | assertion_result 639 | .add_fact("value of", format!("{}.size()", actual_expr)) 640 | .add_fact("expected", format!("{}", length)) 641 | .add_fact("actual", format!("{}", actual)) 642 | .do_fail() 643 | } 644 | } 645 | 646 | #[cfg(test)] 647 | mod tests { 648 | use crate::testing::*; 649 | 650 | use super::*; 651 | 652 | #[test] 653 | fn contains() { 654 | assert_that!(vec![1, 2, 3].iter()).contains(&3); 655 | 656 | // Failures 657 | assert_that!(check_that!(vec![1, 2, 3].iter()).contains(&10)).facts_are(vec![ 658 | Fact::new("expected to contain", "10"), 659 | Fact::new_simple_fact("but did not"), 660 | Fact::new_multi_value_fact("though it did contain", vec!["1", "2", "3"]), 661 | ]); 662 | } 663 | 664 | #[test] 665 | fn contains_exactly() { 666 | assert_that!(vec![1, 2, 3].iter()).contains_exactly(vec![1, 2, 3].iter()); 667 | assert_that!(vec![2, 1, 3].iter()).contains_exactly(vec![1, 2, 3].iter()); 668 | 669 | assert_that!(check_that!("foobarbaz".chars()).contains_exactly("bazbar".chars())) 670 | .facts_are(vec![ 671 | Fact::new("unexpected (3)", r#"['f', 'o', 'o']"#), 672 | Fact::Splitter, 673 | Fact::new_multi_value_fact( 674 | "expected", 675 | vec!["'b'", "'a'", "'z'", "'b'", "'a'", "'r'"], 676 | ), 677 | Fact::new_multi_value_fact( 678 | "actual", 679 | vec![ 680 | "'f'", "'o'", "'o'", "'b'", "'a'", "'r'", "'b'", "'a'", "'z'", 681 | ], 682 | ), 683 | ]); 684 | } 685 | 686 | #[test] 687 | fn contains_exactly_in_order() { 688 | assert_that!(vec![1, 2, 3].iter()).contains_exactly_in_order(vec![1, 2, 3].iter()); 689 | // failures 690 | assert_that!(check_that!(vec![1, 2].iter()).contains_exactly_in_order(vec![1, 2, 3].iter())) 691 | .facts_are(vec![ 692 | Fact::new("missing (1)", "[3]"), 693 | Fact::new_splitter(), 694 | Fact::new_multi_value_fact("expected", vec!["1", "2", "3"]), 695 | Fact::new_multi_value_fact("actual", vec!["1", "2"]), 696 | ]); 697 | assert_that!(check_that!(vec![1, 2, 3].iter()).contains_exactly_in_order(vec![1, 2].iter())) 698 | .facts_are(vec![ 699 | Fact::new("unexpected (1)", "[3]"), 700 | Fact::new_splitter(), 701 | Fact::new_multi_value_fact("expected", vec!["1", "2"]), 702 | Fact::new_multi_value_fact("actual", vec!["1", "2", "3"]), 703 | ]); 704 | assert_that!(check_that!(vec![1, 2].iter()).contains_exactly_in_order(vec![2, 3].iter())) 705 | .facts_are(vec![ 706 | Fact::new("missing (1)", "[3]"), 707 | Fact::new("unexpected (1)", "[1]"), 708 | Fact::new_splitter(), 709 | Fact::new_multi_value_fact("expected", vec!["2", "3"]), 710 | Fact::new_multi_value_fact("actual", vec!["1", "2"]), 711 | ]); 712 | assert_that!( 713 | check_that!(vec![2, 1, 3].iter()).contains_exactly_in_order(vec![1, 2, 3].iter()) 714 | ) 715 | .facts_are(vec![ 716 | Fact::new_simple_fact("contents match, but order was wrong"), 717 | Fact::new_splitter(), 718 | Fact::new_multi_value_fact("expected", vec!["1", "2", "3"]), 719 | Fact::new_multi_value_fact("actual", vec!["2", "1", "3"]), 720 | ]) 721 | } 722 | 723 | #[test] 724 | fn contains_at_least() { 725 | assert_that!(vec![1, 2, 3].iter()).contains_all_of(vec![].iter()); 726 | assert_that!(vec![1, 2, 3].iter()).contains_all_of(vec![1, 2].iter()); 727 | assert_that!(vec![1, 2, 3].iter()).contains_all_of(vec![2, 3].iter()); 728 | assert_that!(vec![1, 2, 3].iter()).contains_all_of(vec![1, 2, 3].iter()); 729 | 730 | // Failures 731 | assert_that!(check_that!(vec![1, 2, 3].iter()).contains_all_of(vec![3, 4].iter())) 732 | .facts_are(vec![ 733 | Fact::new("missing (1)", "[4]"), 734 | Fact::new_splitter(), 735 | Fact::new_multi_value_fact("expected to contain at least", vec!["3", "4"]), 736 | Fact::new_multi_value_fact("but was", vec!["1", "2", "3"]), 737 | ]) 738 | } 739 | 740 | #[test] 741 | fn contains_all_of_in_order() { 742 | assert_that!(vec![1, 2, 3].iter()).contains_all_of_in_order(vec![].iter()); 743 | assert_that!(vec![1, 2, 3].iter()).contains_all_of_in_order(vec![1, 2].iter()); 744 | assert_that!(vec![1, 2, 3].iter()).contains_all_of_in_order(vec![2, 3].iter()); 745 | assert_that!(vec![1, 2, 3].iter()).contains_all_of_in_order(vec![1, 3].iter()); 746 | assert_that!(vec![1, 2, 3].iter()).contains_all_of_in_order(vec![1, 2, 3].iter()); 747 | 748 | // Failures 749 | assert_that!(check_that!(vec![1, 2, 3].iter()).contains_all_of_in_order(vec![3, 4].iter())) 750 | .facts_are(vec![ 751 | Fact::new("missing (1)", "[4]"), 752 | Fact::new_splitter(), 753 | Fact::new_multi_value_fact("expected to contain at least", vec!["3", "4"]), 754 | Fact::new_multi_value_fact("but was", vec!["1", "2", "3"]), 755 | ]); 756 | assert_that!( 757 | check_that!(vec![1, 2, 3].iter()).contains_all_of_in_order(vec![3, 2, 1].iter()) 758 | ) 759 | .facts_are(vec![ 760 | Fact::new_simple_fact("required elements were all found, but order was wrong"), 761 | Fact::new_multi_value_fact("expected order for required elements", vec!["3", "2", "1"]), 762 | Fact::new_multi_value_fact("but was", vec!["1", "2", "3"]), 763 | ]); 764 | assert_that!(check_that!(vec![1, 2, 3].iter()).contains_all_of_in_order(vec![2, 1].iter())) 765 | .facts_are(vec![ 766 | Fact::new_simple_fact("required elements were all found, but order was wrong"), 767 | Fact::new_multi_value_fact("expected order for required elements", vec!["2", "1"]), 768 | Fact::new_multi_value_fact("but was", vec!["1", "2", "3"]), 769 | ]); 770 | } 771 | 772 | #[test] 773 | fn is_empty() { 774 | assert_that!(Vec::::new().iter()).is_empty(); 775 | 776 | // Failures 777 | assert_that!(check_that!(vec![1].iter()).is_empty()).facts_are(vec![ 778 | Fact::new_simple_fact("expected to be empty"), 779 | Fact::new_splitter(), 780 | Fact::new_multi_value_fact("actual", vec!["1"]), 781 | ]); 782 | } 783 | 784 | #[test] 785 | fn has_size() { 786 | assert_that!(vec![1, 2, 3].iter()).has_length(3); 787 | assert_that!(Vec::::new().iter()).has_length(0); 788 | 789 | // Failures 790 | assert_that!(check_that!(Vec::::new().iter()).has_length(3)).facts_are(vec![ 791 | Fact::new("value of", "Vec::::new().iter().size()"), 792 | Fact::new("expected", "3"), 793 | Fact::new("actual", "0"), 794 | ]); 795 | } 796 | 797 | #[test] 798 | fn does_not_contain() { 799 | assert_that!(vec![1, 2, 3].iter()).does_not_contain(&5); 800 | assert_that!(Vec::::new().iter()).does_not_contain(&0); 801 | 802 | // Failures 803 | assert_that!(check_that!(vec![1].iter()).does_not_contain(&1)).facts_are(vec![ 804 | Fact::new("expected to not contain", "1"), 805 | Fact::new_simple_fact("but element was found"), 806 | Fact::new_multi_value_fact("though it did contain", vec!["1"]), 807 | ]); 808 | } 809 | 810 | #[test] 811 | fn does_not_contain_any() { 812 | assert_that!(vec![1, 2, 3].iter()).does_not_contain_any(vec![4, 5].iter()); 813 | assert_that!(vec![1, 2, 3].iter()).does_not_contain_any(vec![].iter()); 814 | assert_that!(Vec::::new().iter()).does_not_contain_any(vec![1, 2, 3].iter()); 815 | assert_that!(Vec::::new().iter()).does_not_contain_any(Vec::::new().iter()); 816 | 817 | // Failures 818 | assert_that!(check_that!(vec![1, 2, 3].iter()).does_not_contain_any(vec![2, 3].iter())) 819 | .facts_are(vec![ 820 | Fact::new("found (2)", "[2, 3]"), 821 | Fact::new_splitter(), 822 | Fact::new_multi_value_fact("expected to contain none of", vec!["2", "3"]), 823 | Fact::new_multi_value_fact("but was", vec!["1", "2", "3"]), 824 | ]); 825 | } 826 | } 827 | --------------------------------------------------------------------------------