├── .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 | [](https://crates.io/crates/assertor)
6 | [](https://github.com/google/assertor/LICENSE)
7 | [](https://docs.rs/crate/assertor/)
8 | [](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