diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..abbfea6 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,65 @@ +name: CI +on: + pull_request: + push: + branches: + - main + +env: + CARGO_TERM_COLOR: always + +jobs: + format: + name: Format + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup toolchain + uses: dtolnay/rust-toolchain@stable + with: + toolchain: nightly + components: rustfmt + - name: Check formatting + run: cargo fmt --check + + lint: + name: Lint + needs: + - format + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup toolchain + uses: dtolnay/rust-toolchain@clippy + - name: Run clippy + run: cargo clippy --tests -- -D warnings + + test: + name: Test (${{ matrix.name }}) + needs: + - format + - lint + runs-on: ubuntu-latest + strategy: + fail-fast: true + matrix: + name: + - all features + - no features + include: + - name: all features + flags: "" + - name: no features + flags: --no-default-features + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup toolchain + uses: dtolnay/rust-toolchain@stable + with: + toolchain: nightly + components: rustfmt + - name: Run tests + run: cargo test --verbose diff --git a/.vscode/extensions.json b/.vscode/extensions.json index e01467e..ae4ffd3 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,6 +1,5 @@ { "recommendations": [ - "serayuzgur.crates", "rust-lang.rust-analyzer", "tamasfe.even-better-toml" ] diff --git a/Cargo.lock b/Cargo.lock index f799db2..a2a97aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,445 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "backtrace" +version = "0.3.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "cc" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26a5c3fd7bfa1ce3897a3a3501d362b2d87b7f2583ebcb4a949ec25911025cbc" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "expecters" version = "0.1.0" +dependencies = [ + "futures", + "owo-colors", + "pin-project-lite", + "regex", + "test-case", + "tokio", +] + +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "is-terminal" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys", +] + +[[package]] +name = "is_ci" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + +[[package]] +name = "object" +version = "0.36.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f203fa8daa7bb185f760ae12bd8e097f63d17041dcdcaf675ac54cdf863170e" +dependencies = [ + "memchr", +] + +[[package]] +name = "owo-colors" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caff54706df99d2a78a5a4e3455ff45448d81ef1bb63c22cd14052ca0e993a3f" +dependencies = [ + "supports-color", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "supports-color" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6398cde53adc3c4557306a96ce67b302968513830a77a95b2b17305d9719a89" +dependencies = [ + "is-terminal", + "is_ci", +] + +[[package]] +name = "syn" +version = "2.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "test-case" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2550dd13afcd286853192af8601920d959b14c401fcece38071d53bf0768a8" +dependencies = [ + "test-case-macros", +] + +[[package]] +name = "test-case-core" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "test-case-macros" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "test-case-core", +] + +[[package]] +name = "tokio" +version = "1.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" +dependencies = [ + "backtrace", + "pin-project-lite", + "tokio-macros", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml index c94594a..0201ad0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,4 +3,33 @@ name = "expecters" version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" -categories = ["testing", "assertions"] +categories = [ + "asynchronous", + "development-tools", + "development-tools::debugging", + "development-tools::testing", +] +keywords = ["assert", "assertions", "async", "matchers", "testing"] +rust-version = "1.80.1" +publish = false # TODO: remove this when ready to publish + +[features] +default = ["colors", "futures", "regex"] +colors = ["dep:owo-colors"] +futures = ["dep:futures", "dep:pin-project-lite"] +regex = ["dep:regex"] + +[dependencies] +futures = { version = "0.3.30", optional = true, default-features = false, features = [ + "std", + "async-await", +] } +owo-colors = { version = "4.0.0", features = [ + "supports-colors", +], optional = true } +pin-project-lite = { version = "0.2.14", optional = true } +regex = { version = "1.10.6", optional = true } + +[dev-dependencies] +test-case = "3.3.1" +tokio = { version = "1", features = ["macros", "test-util"] } diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..8374a13 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 TehPers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..42bd09f --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# TODO: name + +Build complex, self-describing assertions by chaining together reusable methods. +Supports both synchronous and asynchronous assertions. + +## Example + +```rust +use expecters::prelude::*; + +#[tokio::test] +async fn test() { + expect!(1, as_display, to_equal("1")); + expect!(1..=5, count, to_equal(5)); + + expect!(get_cat_url(0), when_ready, to_contain_substr(".png")).await; +} + +async fn get_cat_url(id: u32) -> String { + format!("cats/{id}.png") +} +``` + +## License + +This repository is dual licensed under [MIT](./LICENSE-MIT) and +[APACHE-2.0](./LICENSE-APACHE). You may choose which license you wish to use. diff --git a/src/assertions.rs b/src/assertions.rs index 759d71d..1e010bf 100644 --- a/src/assertions.rs +++ b/src/assertions.rs @@ -1,683 +1,245 @@ -use std::fmt::Display; - -use crate::combinators::{ - AllCombinator, AnyCombinator, AtPath, CountCombinator, ErrCombinator, MapCombinator, - NotCombinator, NthCombinator, OkCombinator, SomeCombinator, Traversal, WhenCalledCombinator, -}; - -/// A type that defines behavior for assertions. -/// -/// See the methods on this trait for a list of built-in assertions and -/// combinators. -pub trait Assertable: Sized { - /// The type of the target of the assertion. - type Target; - - /// The result of an assertion. Normally, assertions are performed right - /// away, so this type is `()`. However, in some cases, the result of an - /// assertion might not be immediately known (e.g., when the assertion is - /// on the result of a `Future`). In those cases, a value is returned - /// instead which can be used to perform the assertion. - type Result; - - /// Asserts that the target matches the given predicate. If the predicate - /// is not satisfied, this method panics with a message that includes the - /// given expectation. - /// - /// ``` - /// # use expecters::prelude::*; - /// expect!(1).to_satisfy("value is odd", |n| n % 2 == 1); - /// ``` - /// - /// This method panics if the target does not satisfy the predicate: - /// - /// ```should_panic - /// # use expecters::prelude::*; - /// expect!(2).to_satisfy("value is odd", |n| n % 2 == 1); - /// ``` - /// - /// This method is the foundation for all other assertions. It is used to - /// build more complex assertions by composing a complex expectation message - /// and predicate function. If creating a new combinator, this method should - /// be implemented to provide the basic functionality. - fn to_satisfy(self, expectation: impl Display, f: F) -> Self::Result - where - F: FnMut(Self::Target) -> bool; - - // COMBINATORS - - /// Negates an assertion. If the assertion is satisfied, then the result - /// is treated as a failure, and if the assertion is not satisfied, then - /// the result is treated as a success. - /// - /// ``` - /// # use expecters::prelude::*; - /// expect!(1).not().to_equal(2); - /// ``` - /// - /// This method panics if the assertion is satisfied: - /// - /// ```should_panic - /// # use expecters::prelude::*; - /// expect!(1).not().to_equal(1); - /// ``` - fn not(self) -> NotCombinator { - NotCombinator::new(self) - } - - /// Applies a mapping function to the target before applying the assertion. - /// This is useful when the target is a complex type and the assertion - /// should be applied to a specific field or property. - /// - /// Since strings (both [`str`] and [`String`]) can't be directly iterated, - /// this method can be used to map a string to an iterator using the - /// [`str::chars`] method, [`str::bytes`] method, or any other method that - /// returns an iterator. This allows any combinators or assertions that - /// work with iterators to be used with strings as well. - /// - /// ``` - /// # use expecters::prelude::*; - /// expect!("abcd").map(str::chars).any().to_equal('b'); - /// // Ignoring the error message, the above code is equivalent to: - /// expect!("abcd".chars()).any().to_equal('b'); - /// ``` - /// - /// This method panics if the mapped target does not satisfy the assertion: - /// - /// ```should_panic - /// # use expecters::prelude::*; - /// expect!("abcd").map(str::chars).any().to_equal('e'); - /// ``` - fn map(self, map: F) -> MapCombinator - where - F: FnMut(Self::Target) -> T, - { - MapCombinator::new(self, map) - } - - /// Applies an assertion to each element in the target. If any element does - /// not satisfy the assertion, then the result is treated as a failure. - /// - /// ``` - /// # use expecters::prelude::*; - /// expect!([1, 3, 5]).all().to_be_less_than(10); - /// ``` - /// - /// This method panics if any element does not satisfy the assertion: - /// - /// ```should_panic - /// # use expecters::prelude::*; - /// expect!([1, 3, 5]).all().to_equal(5); - /// ``` - fn all(self) -> AllCombinator - where - Self::Target: IntoIterator, - { - AllCombinator::new(self) - } - - /// Applies an assertion to each element in the target. If every element - /// does not satisfy the assertion, then the result is treated as a failure. - /// - /// ``` - /// # use expecters::prelude::*; - /// expect!([1, 3, 5]).any().to_equal(5); - /// ``` - /// - /// This method panics if every element does not satisfy the assertion: - /// - /// ```should_panic - /// # use expecters::prelude::*; - /// expect!([1, 3, 5]).any().to_equal(4); - /// ``` - fn any(self) -> AnyCombinator - where - Self::Target: IntoIterator, - { - AnyCombinator::new(self) - } - - /// Applies an assertion to the number of elements in the target. If the - /// number of elements does not satisfy the assertion, then the result is - /// treated as a failure. - /// - /// This uses the [`Iterator::count`] method to determine the number of - /// elements in the target. If the target is an unbounded iterator, then - /// this method will loop indefinitely. - /// - /// ``` - /// # use expecters::prelude::*; - /// expect!([1, 2, 3]).count().to_equal(3); - /// ``` - /// - /// This method panics if the number of elements does not satisfy the - /// assertion: - /// - /// ```should_panic - /// # use expecters::prelude::*; - /// expect!([1, 2, 3]).count().to_equal(4); - /// ``` - fn count(self) -> CountCombinator - where - Self::Target: IntoIterator, - { - CountCombinator::new(self) - } - - /// Applies an assertion to a specific element in the target. If the element - /// does not exist or does not satisfy the assertion, then the result is - /// treated as a failure. The index is zero-based. - /// - /// ``` - /// # use expecters::prelude::*; - /// expect!([1, 2, 3]).nth(1).to_equal(2); - /// ``` - /// - /// This method panics if the element does not exist: - /// - /// ```should_panic - /// # use expecters::prelude::*; - /// expect!([1, 2, 3]).nth(3).to_equal(4); - /// ``` - /// - /// It also panics if the element does not satisfy the assertion: - /// - /// ```should_panic - /// # use expecters::prelude::*; - /// expect!([1, 2, 3]).nth(1).to_equal(1); - /// ``` - fn nth(self, n: usize) -> NthCombinator - where - Self::Target: IntoIterator, - { - NthCombinator::new(self, n) - } - - /// Applies an assertion to the inner value of an [`Option`]. If the - /// option is [`None`], then the result is treated as a failure. Otherwise, - /// the assertion is applied to the inner value. - /// - /// ``` - /// # use expecters::prelude::*; - /// expect!(Some(1i32)).to_be_some_and().to_equal(1); - /// ``` - /// - /// This method panics if the option is [`None`]: - /// - /// ```should_panic - /// # use expecters::prelude::*; - /// expect!(None::).to_be_some_and().to_equal(2); - /// ``` - /// - /// It also panics if the inner value does not satisfy the assertion: - /// - /// ```should_panic - /// # use expecters::prelude::*; - /// expect!(Some(1i32)).to_be_some_and().to_equal(2); - /// ``` - fn to_be_some_and(self) -> SomeCombinator - where - Self: Assertable>, - { - SomeCombinator::new(self) - } - - /// Applies an assertion to the inner value of a [`Result`]. If the - /// result is [`Err`], then the result is treated as a failure. Otherwise, - /// the assertion is applied to the inner value. - /// - /// ``` - /// # use expecters::prelude::*; - /// let result: Result = Ok(1); - /// expect!(result).to_be_ok_and().to_equal(1); - /// ``` - /// - /// This method panics if the result is [`Err`]: - /// - /// ```should_panic - /// # use expecters::prelude::*; - /// let result: Result = Err("error"); - /// expect!(result).to_be_ok_and().to_equal(1); - /// ``` - /// - /// It also panics if the inner value does not satisfy the assertion: - /// - /// ```should_panic - /// # use expecters::prelude::*; - /// let result: Result = Ok(1); - /// expect!(result).to_be_ok_and().to_equal(2); - /// ``` - fn to_be_ok_and(self) -> OkCombinator - where - Self: Assertable>, - { - OkCombinator::new(self) - } - - /// Applies an assertion to the error value of a [`Result`]. If the - /// result is [`Ok`], then the result is treated as a failure. Otherwise, - /// the assertion is applied to the error value. - /// - /// ``` - /// # use expecters::prelude::*; - /// let result: Result = Err("error"); - /// expect!(result).to_be_err_and().to_equal("error"); - /// ``` - /// - /// This method panics if the result is [`Ok`]: - /// - /// ```should_panic - /// # use expecters::prelude::*; - /// let result: Result = Ok(1); - /// expect!(result).to_be_err_and().to_equal("error"); - /// ``` - /// - /// It also panics if the error value does not satisfy the assertion: - /// - /// ```should_panic - /// # use expecters::prelude::*; - /// let result: Result = Err("error"); - /// expect!(result).to_be_err_and().to_equal("another error"); - /// ``` - fn to_be_err_and(self) -> ErrCombinator - where - Self: Assertable>, - { - ErrCombinator::new(self) - } - - /// Applies an assertion to the return value of a function. This is - /// equivalent to calling - /// [`.when_called_with(())`](Assertable::when_called_with). - /// - /// ``` - /// # use expecters::prelude::*; - /// expect!(|| 1).when_called().to_equal(1); - /// ``` - /// - /// This method panics if the return value does not satisfy the assertion: - /// - /// ```should_panic - /// # use expecters::prelude::*; - /// expect!(|| 1).when_called().to_equal(2); - /// ``` - fn when_called(self) -> WhenCalledCombinator - where - Self::Target: FnOnce() -> R, - { - WhenCalledCombinator::new(self, ()) - } - - /// Applies an assertion to the return value of a function when called with - /// the given arguments. - /// - /// Arguments must be passed as a tuple, including for functions that take - /// no arguments or a single argument. For single-argument functions, the - /// argument must be passed like `(arg,)` to produce a tuple. - /// - /// ``` - /// # use expecters::prelude::*; - /// expect!(|a, b| a + b).when_called_with((1, 2)).to_equal(3); - /// expect!(|n| n * 2).when_called_with((2,)).to_equal(4); - /// ``` - /// - /// This method panics if the return value does not satisfy the assertion: - /// - /// ```should_panic - /// # use expecters::prelude::*; - /// expect!(|a, b| a + b).when_called_with((1, 2)).to_equal(4); - /// ``` - /// - /// Up to 12 arguments are supported. If more arguments are needed, consider - /// calling [`map`](Assertable::map) instead to transform the function into - /// its return value: - /// - /// ``` - /// # use expecters::prelude::*; - /// expect!(|a, b| a + b).map(|f| f(1, 2)).to_equal(3); - /// ``` - fn when_called_with(self, args: Args) -> WhenCalledCombinator - where - WhenCalledCombinator: Assertable, - { - WhenCalledCombinator::new(self, args) - } - - /// Applies an assertion to a sub-path of the target value. This is useful - /// when the target is a complex type and the assertion should be applied to - /// a specific field or property. - /// - /// Unlike [`map`](Assertable::map), this method allows you to access deeply - /// nested values, even through fallible layers (like values with type - /// [`Option`] or [`Result`]), using a simple path syntax. The path is - /// included with the generated error message to help identify the source of - /// the assertion failure. - /// - /// To generate the path, and for more information on the syntax, see the - /// [`path!`](crate::path) macro. - /// - /// ``` - /// # use expecters::prelude::*; - /// struct Foo(i32); - /// - /// expect!(Foo(3)).at_path(path!(.0)).to_equal(3); - /// ``` - /// - /// This method panics if the sub-path cannot be navigated to due to - /// fallible components: - /// - /// ```should_panic - /// # use expecters::prelude::*; - /// struct Foo(Option); - /// - /// expect!(Foo(None)) - /// .at_path(path!(.0?)) - /// .to_satisfy("always succeed", |_| true); - /// ``` - fn at_path(self, path: Traversal) -> AtPath { - AtPath::new(self, path) - } - - // ASSERTIONS - - /// Asserts that the target is equal to the given value. - /// - /// ``` - /// # use expecters::prelude::*; - /// expect!(1).to_equal(1); - /// ``` - /// - /// This method panics if the target is not equal to the given value: - /// - /// ```should_panic - /// # use expecters::prelude::*; - /// expect!(1).to_equal(2); - /// ``` - #[inline] - fn to_equal(self, other: T) -> Self::Result - where - Self::Target: PartialEq, - { - self.to_satisfy("value is equal to a provided value", move |t| t == other) - } - - /// Asserts that the target is less than the given value. - /// - /// ``` - /// # use expecters::prelude::*; - /// expect!(1).to_be_less_than(2); - /// ``` - /// - /// This method panics if the target is not less than the given value: - /// - /// ```should_panic - /// # use expecters::prelude::*; - /// expect!(2).to_be_less_than(1); - /// ``` - #[inline] - fn to_be_less_than(self, other: T) -> Self::Result - where - Self::Target: PartialOrd, - { - self.to_satisfy("value is less than the input", move |t| t < other) - } - - /// Asserts that the target is less than or equal to the given value. - /// - /// ``` - /// # use expecters::prelude::*; - /// expect!(1).to_be_less_than_or_equal_to(1); - /// expect!(1).to_be_less_than_or_equal_to(2); - /// ``` - /// - /// This method panics if the target is greater less the given value: - /// - /// ```should_panic - /// # use expecters::prelude::*; - /// expect!(2).to_be_less_than_or_equal_to(1); - /// ``` - #[inline] - fn to_be_less_than_or_equal_to(self, other: T) -> Self::Result - where - Self::Target: PartialOrd, - { - self.to_satisfy("value is less than or equal to the input", move |t| { - t <= other - }) - } - - /// Asserts that the target is greater than the given value. - /// - /// ``` - /// # use expecters::prelude::*; - /// expect!(2).to_be_greater_than(1); - /// ``` - /// - /// This method panics if the target is not greater than the given value: - /// - /// ```should_panic - /// # use expecters::prelude::*; - /// expect!(1).to_be_greater_than(2); - /// ``` - #[inline] - fn to_be_greater_than(self, other: T) -> Self::Result - where - Self::Target: PartialOrd, - { - self.to_satisfy("value is greater than the input", move |t| t > other) - } - - /// Asserts that the target is greater than or equal to the given value. - /// - /// ``` - /// # use expecters::prelude::*; - /// expect!(1).to_be_greater_than_or_equal_to(1); - /// expect!(1).to_be_greater_than_or_equal_to(0); - /// ``` - /// - /// This method panics if the target is less than than the given value: - /// - /// ```should_panic - /// # use expecters::prelude::*; - /// expect!(1).to_be_greater_than_or_equal_to(2); - /// ``` - #[inline] - fn to_be_greater_than_or_equal_to(self, other: T) -> Self::Result - where - Self::Target: PartialOrd, - { - self.to_satisfy("value is greater than or equal to the input", move |t| { - t >= other - }) - } - - /// Asserts that the target is empty. - /// - /// ``` - /// # use expecters::prelude::*; - /// expect!(Vec::::new()).to_be_empty(); - /// expect!("".chars()).to_be_empty(); - /// ``` - /// - /// This method panics if the target is not empty: - /// - /// ```should_panic - /// # use expecters::prelude::*; - /// expect!([1, 2, 3]).to_be_empty(); - /// ``` - #[inline] - fn to_be_empty(self) -> Self::Result - where - Self::Target: IntoIterator, - { - self.to_satisfy("value is empty", |value| value.into_iter().next().is_none()) - } - - /// Asserts that the target holds a value. - /// - /// ``` - /// # use expecters::prelude::*; - /// expect!(Some(1i32)).to_be_some(); - /// ``` - /// - /// This method panics if the target does not hold a value: - /// - /// ```should_panic - /// # use expecters::prelude::*; - /// expect!(None::).to_be_some(); - /// ``` - #[inline] - fn to_be_some(self) -> Self::Result - where - Self: Assertable>, - { - self.to_satisfy("value is `Some`", |value| value.is_some()) - } - - /// Asserts that the target does not hold a value. - /// - /// ``` - /// # use expecters::prelude::*; - /// expect!(None::).to_be_none(); - /// ``` - /// - /// This method panics if the target holds a value: - /// - /// ```should_panic - /// # use expecters::prelude::*; - /// expect!(Some(1i32)).to_be_none(); - /// ``` - #[inline] - fn to_be_none(self) -> Self::Result - where - Self: Assertable>, - { - self.to_satisfy("value is `None`", |value| value.is_none()) - } - - /// Asserts that the target holds a success. - /// - /// ``` - /// # use expecters::prelude::*; - /// let result: Result = Ok(1); - /// expect!(result).to_be_ok(); - /// ``` - /// - /// This method panics if the target does not hold a success: - /// - /// ```should_panic - /// # use expecters::prelude::*; - /// let result: Result = Err("error"); - /// expect!(result).to_be_ok(); - /// ``` - #[inline] - fn to_be_ok(self) -> Self::Result - where - Self: Assertable>, - { - self.to_satisfy("value is `Ok`", |value| value.is_ok()) - } - - /// Asserts that the target holds an error. - /// - /// ``` - /// # use expecters::prelude::*; - /// let result: Result = Err("error"); - /// expect!(result).to_be_err(); - /// ``` - /// - /// This method panics if the target does not hold an error: - /// - /// ```should_panic - /// # use expecters::prelude::*; - /// let result: Result = Ok(1); - /// expect!(result).to_be_err(); - /// ``` - #[inline] - fn to_be_err(self) -> Self::Result - where - Self: Assertable>, - { - self.to_satisfy("value is `Err`", |value| value.is_err()) - } - - /// Asserts that the target is `true`. - /// - /// ``` - /// # use expecters::prelude::*; - /// expect!(true).to_be_true(); - /// ``` - /// - /// This method panics if the target is `false`: - /// - /// ```should_panic - /// # use expecters::prelude::*; - /// expect!(false).to_be_true(); - /// ``` - fn to_be_true(self) -> Self::Result - where - Self::Target: Into, - { - self.to_satisfy("value is `true`", |value| value.into()) - } - - /// Asserts that the target is `false`. - /// - /// ``` - /// # use expecters::prelude::*; - /// expect!(false).to_be_false(); - /// ``` - /// - /// This method panics if the target is `true`: - /// - /// ```should_panic - /// # use expecters::prelude::*; - /// expect!(true).to_be_false(); - /// ``` - fn to_be_false(self) -> Self::Result - where - Self::Target: Into, - { - self.to_satisfy("value is `false`", |value| !value.into()) - } -} - -#[cfg(test)] -mod tests { - use crate::expect; - - use super::*; - - #[test] - fn all_not() { - expect!([1, 2, 3]).all().not().to_equal(4); - expect!([1, 2, 3]).not().all().to_equal(3); - } - - #[test] - #[should_panic] - fn all_not_fails() { - expect!([1, 2, 3]).all().not().to_equal(3); - } - - #[test] - #[should_panic] - fn not_all_fails() { - expect!([1, 2, 3]).not().to_be_empty(); - expect!([1, 2, 3]).not().all().to_be_less_than(4); - } - - #[test] - fn any_not() { - expect!([1, 2, 3]).any().not().to_equal(4); - expect!([1, 2, 3]).not().any().to_equal(4); - } - - #[test] - fn many_args_called_with() { - fn sum(a: i32, b: i32, c: i32) -> i32 { - a + b + c - } - expect!(sum).when_called_with((1, 2, 3)).to_equal(6); - } -} +//! Assertions and modifiers that are used with [`expect!`], as well as any +//! types used to drive them. +//! +//! When using the [`expect!`] macro, the overall assertion is built up by +//! chaining together modifiers and a final assertion. Modifiers can perform +//! additional checks or transform inputs/outputs to later modifiers/assertions. +//! +//! ``` +//! # use expecters::prelude::*; +//! expect!([1, 2, 3], all, to_be_greater_than(0)); +//! ``` +//! +//! ## Available assertions +//! +//! Many assertions are made available by importing the prelude. To see what +//! assertions are exported by default, look at the [`prelude`](crate::prelude) +//! module's exports. The assertions are defined by traits that are exported by +//! that module. +//! +//! For example, general purpose assertions are found in [`GeneralAssertions`], +//! while assertions on option values are in [`OptionAssertions`]. +//! +//! ## Creating an assertion +//! +//! The signature for assertions is simple. An assertion function, like +//! [`to_be_some`], is a function added by a trait to the [`AssertionBuilder`] +//! that returns a value implementing the [`Assertion`] trait. It acts as a +//! constructor for that type. For example, calling `builder.to_be_some()` +//! returns an instance of the [`ToBeOptionVariantAssertion`] type configured to +//! check if the input it receives is of a particular variant of [`Option`]. +//! +//! Note that the same type is returned by [`to_be_none`], and these types can +//! be reused if needed. +//! +//! To create your own assertion function, first create the type that represents +//! the assertion, then create the function that produces the type. For example, +//! to create an assertion that passes if it receives a `0`: +//! +//! ``` +//! use expecters::{ +//! assertions::{Assertion, AssertionBuilder, AssertionContext}, +//! metadata::Annotated, +//! prelude::*, +//! AssertionOutput, +//! }; +//! +//! // We need to create a struct for our assertion and define its behavior +//! #[derive(Clone, Debug)] +//! pub struct ToBeZeroAssertion(Annotated); +//! +//! impl Assertion for ToBeZeroAssertion { +//! // What does this assertion return when it's executed? Sometimes +//! // assertions want to return other output types, like if they need to +//! // run asynchronously and have to return a future instead. +//! type Output = AssertionOutput; +//! +//! fn execute(self, mut cx: AssertionContext, value: i32) -> Self::Output { +//! // You can annotate the context with additional information +//! cx.annotate("my note", "this appears in failure messages"); +//! cx.annotate("input parameter", &self.0); +//! +//! // Then execute your assertion +//! cx.pass_if(value == 0, "was not zero") +//! } +//! } +//! +//! // Now we need to attach our assertion to the assertion builder. We attach +//! // it through a trait implementation on the builder itself: +//! trait MyAssertions { +//! // Input parameters are automatically annotated, so we need to wrap them +//! // with `Annotated` +//! fn to_be_zero(&self, note: Annotated) -> ToBeZeroAssertion { +//! ToBeZeroAssertion(note) +//! } +//! } +//! +//! // By implementing only for `AssertionBuilder`, we constrain our +//! // assertion to only be allowed on assertions against `i32` values. This is +//! // consistent with our `Assertion` implementation above. +//! impl MyAssertions for AssertionBuilder {} +//! +//! // Now we can use the assertion: +//! expect!(0, to_be_zero("hello, world!".to_string())); +//! // You can also use modifiers with your assertion: +//! expect!(1, not, to_be_zero("this assertion is negated".to_string())); +//! ``` +//! +//! An assertion function that takes no parameters can be called without +//! parentheses when using the [`expect!`] macro. For example, if the assertion +//! function signature is `pub fn to_be_zero() -> ToBeZeroAssertion`, then the +//! assertion can be used like `expect!(0, to_be_zero)`. +//! +//! ## Creating a modifier +//! +//! Modifiers are special types that wrap assertions in their own assertion, +//! then pass their assertion up the chain to the previous modifier. When +//! working with modifiers, it's important to keep in mind the direction that +//! data flows when both building up the intermediate assertion, and executing +//! the assertion. +//! +//! In the code `expect!(1, not, to_equal(2))`, there is the explicit [`not`] +//! modifier, two implicit modifiers added by this crate to track values being +//! passed around and update the assertion context, and one special root +//! modifier that holds the original subject of the assertion. The order that +//! modifiers are being applied is: +//! +//! 1. The root modifier, which holds `1` and drives the assertion. +//! 2. A hidden modifier which annotates intermediate values and notifies the +//! context that the next step in the assertion has begun. +//! 3. The [`not`] modifier, which negates the rest of the assertion. +//! 4. The hidden modifier from step 2. +//! 5. The [`to_equal`] assertion. This is not a modifier, and is the root +//! assertion that all the modifiers are wrapping. +//! +//! The modifiers are constructed in the above order, going from steps 1 through +//! 4, and wrapping the previous modifiers to generate a deeply nested +//! "composite modifier" that represents all those steps. Afterwards, the +//! [`to_equal`] assertion is provided to the composite modifier, and that flows +//! in reverse order back from steps 4 through 1, getting wrapped in another +//! assertion on each step. +//! +//! To create your own modifier, you should create two types: +//! - One that represents the modifier itself (which gets constructed on the +//! first pass, when we're going from the root modifier down to the assertion) +//! - One that represents the assertion the modifier creates when wrapping +//! another assertion, which happens on the second pass when we're passing the +//! assertion at the end of the chain back down to the root. +//! +//! To use the modifier with the [`expect!`] macro, you should also define a +//! function for the modifier in your trait. The function should take the +//! builder by value (`self`), and may define any number of additional inputs +//! that can be used to configure the modifier. It should return the modified +//! builder. +//! +//! ``` +//! use expecters::{ +//! assertions::{ +//! Assertion, +//! AssertionBuilder, +//! AssertionContext, +//! AssertionContextBuilder, +//! AssertionModifier, +//! }, +//! metadata::Annotated, +//! prelude::*, +//! }; +//! +//! // This wraps the modifier chain (first pass, going from root -> assertion) +//! #[derive(Clone, Debug)] +//! pub struct DividedByModifier(M, Annotated); +//! +//! impl AssertionModifier for DividedByModifier +//! where +//! M: AssertionModifier>, +//! { +//! type Output = M::Output; // the output at this step, usually M::Output +//! +//! fn apply(self, cx: AssertionContextBuilder, next: A) -> Self::Output { +//! self.0.apply(cx, DividedByAssertion(next, self.1)) +//! } +//! } +//! +//! // This wraps the assertion chain (second pass, assertion -> root) +//! #[derive(Clone, Debug)] +//! pub struct DividedByAssertion(A, Annotated); +//! +//! impl Assertion for DividedByAssertion +//! where +//! A: Assertion +//! { +//! type Output = A::Output; // output from this assertion +//! +//! fn execute(self, mut cx: AssertionContext, subject: f32) -> Self::Output { +//! cx.annotate("divisor", &self.1); +//! self.0.execute(cx, subject / self.1.into_inner()) +//! } +//! } +//! +//! // Now we need to attach our modifier. We can reuse an existing trait if we +//! // want, if the input types are compatible. Note that we now take `self` +//! // instead of `&self` (unlike the `to_be_zero` assertion): +//! trait MyAssertions { +//! // We return an `AssertionBuilder` here because we're passing +//! // a `f32` value to whatever assertion we receive. If we were to convert +//! // the input `f32` into a `String`, for example, then we'd instead want +//! // to return `AssertionBuilder` here to ensure that only +//! // string assertions can be applied to it. +//! fn divided_by( +//! self, +//! divisor: Annotated, +//! ) -> AssertionBuilder>; +//! } +//! +//! impl MyAssertions for AssertionBuilder { +//! fn divided_by( +//! self, +//! divisor: Annotated, +//! ) -> AssertionBuilder> { +//! // We can't call `self.modify` because `modify` doesn't take `self` +//! // as its first parameter. This is to make sure you don't +//! // accidentally treat `modify` as an assertion when calling +//! // `expect!`. Instead, we do `AssertionBuilder::modify` and pass the +//! // builder as the first parameter to modify the assertion: +//! AssertionBuilder::modify( +//! self, +//! // This constructs our modifier: +//! move |prev| DividedByModifier(prev, divisor), +//! ) +//! } +//! } +//! +//! // Now we can use our modifier +//! expect!(4.0, divided_by(2.0), to_be_less_than(2.1)); +//! ``` +//! +//! Similar to assertions, modifiers that take no arguments can be used in the +//! [`expect!`] macro without an argument list. [`not`] is an example of this, +//! and common usage of it is without parentheses, though parentheses are still +//! allowed. +//! +//! [`GeneralAssertions`]: crate::prelude::GeneralAssertions +//! [`OptionAssertions`]: crate::prelude::OptionAssertions +//! [`ToBeOptionVariantAssertion`]: options::ToBeOptionVariantAssertion +//! [`expect!`]: crate::expect! +//! [`not`]: crate::prelude::GeneralAssertions::not +//! [`to_be_none`]: crate::prelude::OptionAssertions::to_be_none +//! [`to_be_some`]: crate::prelude::OptionAssertions::to_be_some +//! [`to_equal`]: crate::prelude::GeneralAssertions::to_equal + +// pub mod functions; +#[cfg(feature = "futures")] +pub mod futures; +pub mod general; +pub mod iterators; +pub mod options; +pub mod results; +pub mod strings; + +mod assertion; +mod context; +mod error; + +pub use assertion::*; +pub use context::*; +pub use error::*; diff --git a/src/assertions/assertion.rs b/src/assertions/assertion.rs new file mode 100644 index 0000000..43e8a28 --- /dev/null +++ b/src/assertions/assertion.rs @@ -0,0 +1,120 @@ +use std::{ + fmt::{Debug, Formatter}, + marker::PhantomData, +}; + +use crate::metadata::Annotated; + +use super::{general::Root, AssertionContext, AssertionContextBuilder}; + +/// Evaluates a subject and determines whether it satisfies a condition. +/// +/// Assertions take a value, execute some logic to determine whether it +/// satisfies a particular condition, and returns either a success or a failure +/// based on whether the condition was satisfied. Assertions may also attach +/// additional context to indicate why it may have failed. +/// +/// Modifiers create special assertions which may choose to evaluate a +/// condition, but don't always do so. Modifiers create assertions that wrap +/// other assertions and call. They usually either transform the subject, +/// transform the output, or both. +pub trait Assertion { + /// The output type from executing this assertion. + type Output; + + /// Executes this assertion on a given subject. + fn execute(self, cx: AssertionContext, subject: T) -> Self::Output; +} + +/// Modifies an assertion. +/// +/// Modifiers wrap other modifiers, and transform an assertion before passing it +/// to their inner modifier to consume. The assertion that the modifier creates +/// usually either transforms the subject, transforms the output, or both. +pub trait AssertionModifier { + /// The output type from executing this modifier on an assertion. + type Output; + + /// Applies this modifier to a given assertion, then executes the assertion. + /// + /// This is usually a recursive function that calls an inner modifier's + /// `apply` function. Its purpose is to construct the assertion that will be + /// executed, and to invert the flow so that the assertion subject flows + /// from the [`Root`](crate::assertions::general::Root) through each of the + /// modifiers in order before reaching the final assertion. + fn apply(self, cx: AssertionContextBuilder, next: A) -> Self::Output; +} + +/// Builds an assertion. +/// +/// To apply a modifier to this assertion, see [`Self::modify`]. +#[must_use] +pub struct AssertionBuilder { + modifier: M, + marker: PhantomData, +} + +impl AssertionBuilder> { + #[doc(hidden)] + pub fn __new(subject: Annotated) -> Self { + AssertionBuilder { + modifier: Root::new(subject), + marker: PhantomData, + } + } +} + +impl AssertionBuilder { + /// Applies a modifier to the assertion. + /// + /// This associated function does not take `self` to avoid appearing in + /// completions when writing out an expectation. The completions only appear + /// for functions that can be executed on the builder directly, for example + /// `builder.not()`. Because this function doesn't take `self`, it is + /// invalid to write `builder.modify(constructor)`, so it should not appear + /// in the suggested completions for most users. + #[inline] + pub fn modify( + builder: Self, + constructor: impl FnOnce(M) -> M2, + ) -> AssertionBuilder { + AssertionBuilder { + modifier: constructor(builder.modifier), + marker: PhantomData, + } + } + + #[doc(hidden)] + pub fn __apply(builder: Self, cx: AssertionContextBuilder, next: A) -> M::Output + where + M: AssertionModifier, + { + builder.modifier.apply(cx, next) + } +} + +impl Clone for AssertionBuilder +where + M: Clone, +{ + #[inline] + fn clone(&self) -> Self { + Self { + modifier: self.modifier.clone(), + marker: self.marker, + } + } +} + +impl Debug for AssertionBuilder +where + M: Debug, +{ + #[inline] + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + f.debug_struct("AssertionBuilder") + .field("modifier", &self.modifier) + .field("marker", &self.marker) + .finish() + } +} diff --git a/src/assertions/context.rs b/src/assertions/context.rs new file mode 100644 index 0000000..fc56184 --- /dev/null +++ b/src/assertions/context.rs @@ -0,0 +1,200 @@ +use crate::metadata::{Annotated, AnnotatedKind, SourceLoc}; + +use super::general::InitializableOutput; + +/// Context that is passed through an assertion to track the full execution flow +/// that occurred. +/// +/// This stores information needed to provide meaningful error messages on +/// failures. This type is used to generate success and failure values that are +/// returned from assertions, and to annotate steps within the execution flow +/// to provide additional context to failures. +/// +/// Assertion contexts can be cloned to indicate a fork in an execution path. +/// Cloning the context allows the context to be passed down several execution +/// paths, like when using [`all`] or [`any`] to execute an assertion on several +/// values. +/// +/// Forked contexts do not affect each other, so adding an attribute to a forked +/// context or passing it into another assertion will not affect any of the +/// other contexts that were created. +/// +/// [`all`]: crate::prelude::IteratorAssertions::all +/// [`any`]: crate::prelude::IteratorAssertions::any +#[derive(Clone, Debug)] +pub struct AssertionContext { + pub(crate) subject: String, + pub(crate) source_loc: SourceLoc, + pub(crate) visited: Vec, + pub(crate) remaining: &'static [&'static str], + pub(crate) recovered: Vec, +} + +impl AssertionContext { + #[doc(hidden)] + #[must_use] + pub fn __new( + subject: String, + source_loc: SourceLoc, + frames: &'static [&'static str], + ) -> AssertionContextBuilder { + AssertionContextBuilder { + innerner: Self { + subject, + source_loc, + visited: vec![], + remaining: frames, + recovered: vec![], + }, + } + } + + /// Adds an annotation to this frame. The annotation is added to failure + /// messages to help the user understand what happened on the execution path + /// that triggered the failure. + /// + /// ``` + /// use expecters::{ + /// assertions::AssertionContext, + /// metadata::Annotated + /// }; + /// + /// fn execute_to_equal( + /// mut cx: AssertionContext, + /// expected: Annotated + /// ) { + /// // this appears as 'expected: foo' in failures + /// cx.annotate("my other annotation", "foo"); + /// + /// // this appears as 'expected: ' in failures. note that + /// // annotated values always implement ToString and require no + /// // additional type bounds on T + /// cx.annotate("expected", &expected); + /// } + /// ``` + #[allow(clippy::needless_pass_by_value, clippy::missing_panics_doc)] + pub fn annotate(&mut self, key: &'static str, value: impl ToString) { + // self.next() must be called at least once before annotations can be + // added, otherwise there will be no current frame + self.visited + .last_mut() + .expect("no visited frames (this is a bug)") + .annotations + .push((key, value.to_string())); + } + + /// Adds an annotation to this frame if the provided annotated value has a + /// [`Debug`] representation. + /// + /// Note that function parameters to modifiers and assertions *almost + /// always* have a meaningful string representation. Those values should + /// generally be recorded using [`annotate()`](Self::annotate()) instead. + /// + /// This method exists in case a value's stringified representation is not + /// expected to be meaningful, and it is unknown whether that value + /// implements [`Debug`]. For example, a value being passed from one + /// modifier to the next is temporarily stored in a variable, which is then + /// annotated. The name of the variable is not meaningful, so the annotated + /// value only has a meaningful string representation if the value + /// implements [`Debug`]. + /// + /// [`Debug`]: std::fmt::Debug + #[inline] + pub fn try_annotate(&mut self, key: &'static str, value: &Annotated) { + if value.kind() == AnnotatedKind::Debug { + self.annotate(key, value.as_str()); + } + } + + /// Creates a new success value. + #[inline] + #[must_use] + pub fn pass(self) -> O + where + O: InitializableOutput, + { + O::pass(self) + } + + /// Creates a new success value based on a condition. Otherwise, create a + /// new failure value. + /// + /// This is a convenience function for turning a boolean into either a pass + /// or a fail. + #[inline] + pub fn pass_if(self, pass: bool, failure_message: impl ToString) -> O + where + O: InitializableOutput, + { + if pass { + self.pass() + } else { + self.fail(failure_message) + } + } + + /// Creates a new error with the given error message. Context is attached + /// to the error based on the context that was provided through the + /// [`annotate()`](Self::annotate()) function. + /// + /// The full assertion chain and any context associated with the current + /// execution path through that chain (like the index of the item within a + /// parent list) is also recorded onto the error to aid with debugging. + #[inline] + #[allow(clippy::needless_pass_by_value)] + pub fn fail(self, message: impl ToString) -> O + where + O: InitializableOutput, + { + O::fail(self, message.to_string()) + } + + /// Recovers missing frames from another context. + /// + /// The recovered frames are used to provide additional information on what + /// happened during an unsuccessful execution path, especially where part of + /// that execution path was successful but became unsuccessful by an earlier + /// modifier. + pub(crate) fn recover(&mut self, mut other: AssertionContext) { + self.recovered = other + .visited + .drain(self.visited.len()..) + .chain(other.recovered) + .collect(); + } + + /// Creates a child context from this assertion context. This indicates a + /// step through an execution path. + pub(crate) fn next(mut self) -> AssertionContext { + let (next, remaining) = self + .remaining + .split_first() + .expect("no more context (this is a bug)"); + self.visited.push(ContextFrame { + assertion_name: next, + annotations: vec![], + }); + self.remaining = remaining; + + // New execution path, so recovered frames aren't relevant anymore + self.recovered.clear(); + + self + } +} + +/// Prepares an [`AssertionContext`] for use within an assertion. +/// +/// This is passed up through the chain of +/// [`AssertionModifier`](crate::assertions::AssertionModifier)s before the +/// context is built and passed back down through the constructed assertions. +#[derive(Clone, Debug)] +pub struct AssertionContextBuilder { + pub(crate) innerner: AssertionContext, +} + +#[derive(Clone, Debug)] +pub(crate) struct ContextFrame { + pub assertion_name: &'static str, + pub annotations: Vec<(&'static str, String)>, +} diff --git a/src/assertions/error.rs b/src/assertions/error.rs new file mode 100644 index 0000000..ebec4a0 --- /dev/null +++ b/src/assertions/error.rs @@ -0,0 +1,182 @@ +use std::{ + error::Error, + fmt::{Debug, Display, Formatter}, +}; + +use crate::assertions::ContextFrame; + +use super::AssertionContext; + +/// The foundational assertion output. Most assertions either output this type +/// directly, or output a type that wraps this type in some form. +/// +/// Unlike a traditional [`Result`], this type includes additional context about +/// the execution path that led to a success or a failure. It can be converted +/// into a normal [`Result`] with [`into_result`](AssertionOutput::into_result). +/// +/// Note that not all assertions return this as their output (like asynchronous +/// assertions), but it is the preferred foundational output type for +/// assertions. It should be possible to eventually get a value of this type +/// from the output of an assertion by performing some commonly understood (or +/// clearly documented) set of operations on that output (like `.await`ing the +/// output). +#[derive(Clone, Debug)] +#[must_use] +pub struct AssertionOutput { + cx: AssertionContext, + error: Option, +} + +impl AssertionOutput { + #[inline] + pub(crate) fn new(cx: AssertionContext, error: Option) -> Self { + Self { cx, error } + } + + /// Gets whether this output indicates a success. + #[inline] + #[must_use] + pub fn is_pass(&self) -> bool { + self.error.is_none() + } + + /// Sets the state of this output to a pass. This overrides the context of + /// the result. + #[inline] + pub(crate) fn set_pass(&mut self, mut new_cx: AssertionContext) { + self.error = None; + + // Swap the context, but recover missing frames from the new context + std::mem::swap(&mut self.cx, &mut new_cx); + self.cx.recover(new_cx); + } + + /// Sets the state of this output to a failure with the given message. + #[inline] + #[allow(clippy::needless_pass_by_value)] + pub(crate) fn set_fail(&mut self, mut new_cx: AssertionContext, message: impl ToString) { + self.error = Some(message.to_string()); + + // Swap the context, but recover missing frames from the new context + std::mem::swap(&mut self.cx, &mut new_cx); + self.cx.recover(new_cx); + } + + /// Converts this output into a [`Result`]. + #[inline] + pub fn into_result(self) -> Result<(), AssertionError> { + match self.error { + Some(message) => Err(AssertionError::new(self.cx, message)), + None => Ok(()), + } + } +} + +/// An error that can occur during an assertion. +#[must_use] +pub struct AssertionError { + cx: Box, + message: String, +} + +impl AssertionError { + #[inline] + pub(crate) fn new(cx: AssertionContext, message: String) -> Self { + Self { + cx: Box::new(cx), + message, + } + } +} + +#[cfg(feature = "colors")] +mod styles { + use std::fmt::Display; + + use owo_colors::{OwoColorize, Stream}; + + #[inline] + pub fn dimmed(s: &impl Display) -> impl Display + '_ { + s.if_supports_color(Stream::Stderr, |s| s.dimmed()) + } + + #[inline] + pub fn bright_red(s: &impl Display) -> impl Display + '_ { + s.if_supports_color(Stream::Stderr, |s| s.bright_red()) + } +} + +#[cfg(not(feature = "colors"))] +mod styles { + #[inline] + pub fn dimmed(s: &T) -> &T { + s + } + + #[inline] + pub fn bright_red(s: &T) -> &T { + s + } +} + +fn write_frame(f: &mut Formatter, frame: &ContextFrame, comment: &str) -> std::fmt::Result { + writeln!(f, " {}:{comment}", frame.assertion_name)?; + for (key, value) in &frame.annotations { + writeln!(f, " {}", styles::dimmed(&format_args!("{key}: {value}")))?; + } + writeln!(f)?; + Ok(()) +} + +impl Debug for AssertionError { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + writeln!(f, "assertion failed:")?; + writeln!( + f, + " {}", + styles::dimmed(&format_args!("at: {}", self.cx.source_loc)), + )?; + writeln!( + f, + " {}", + styles::dimmed(&format_args!("subject: {}", self.cx.subject)), + )?; + writeln!(f)?; + + // Write visited frames + writeln!(f, "steps:")?; + let mut idx = 0; + for frame in &self.cx.visited { + let comment = if idx == self.cx.visited.len() - 1 { + format!(" {}", styles::bright_red(&self.message)) + } else { + String::new() + }; + write_frame(f, frame, &comment)?; + idx += 1; + } + + // Write recovered frames + for frame in &self.cx.recovered { + write_frame(f, frame, "")?; + idx += 1; + } + + // Write non-visited frames + for frame in &self.cx.remaining[self.cx.recovered.len()..] { + writeln!(f, " {frame}: {}", styles::dimmed(&"(not visited)"))?; + idx += 1; + } + + Ok(()) + } +} + +impl Display for AssertionError { + #[inline] + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + Debug::fmt(self, f) + } +} + +impl Error for AssertionError {} diff --git a/src/assertions/futures.rs b/src/assertions/futures.rs new file mode 100644 index 0000000..7e8ff80 --- /dev/null +++ b/src/assertions/futures.rs @@ -0,0 +1,25 @@ +//! Modifiers used for asynchronous tests. +//! +//! This module contains types used primarily for testing asynchronous code. The +//! assertions created from the modifiers in this module are generally +//! asynchronous and need to be `.await`ed in order for them to execute. +//! +//! This module also contains types that can be useful for writing your own +//! asynchronous assertions and modifiers, if needed. +//! +//! ``` +//! # use expecters::prelude::*; +//! use std::future::ready; +//! # #[tokio::main(flavor = "current_thread")] +//! # async fn main() { +//! expect!(ready(1), when_ready, to_equal(1)).await; +//! # } +//! ``` + +mod extensions; +mod modifiers; +mod outputs; + +pub use extensions::*; +pub use modifiers::*; +pub use outputs::*; diff --git a/src/assertions/futures/extensions.rs b/src/assertions/futures/extensions.rs new file mode 100644 index 0000000..46a8780 --- /dev/null +++ b/src/assertions/futures/extensions.rs @@ -0,0 +1,158 @@ +use std::future::Future; + +use crate::{assertions::AssertionBuilder, metadata::Annotated}; + +use super::{CompletionOrder, CompletionOrderModifier, WhenReadyModifier}; + +/// Assertions and modifiers for [Future]s. +pub trait FutureAssertions +where + T: Future, +{ + /// Executes an assertion on the output of a future. + /// + /// When the subject is ready, the assertion is executed on the output of the + /// subject. This makes the assertion asynchronous, so it must be awaited or + /// passed to an executor in order for it to actually perform the assertion. + /// + /// ``` + /// # use expecters::prelude::*; + /// use std::future::ready; + /// # #[tokio::main(flavor = "current_thread")] + /// # async fn main() { + /// expect!(ready(1), when_ready, to_equal(1)).await; + /// # } + /// ``` + /// + /// Note that this can be chained multiple times if needed, but each level of + /// nesting requires an additional `.await`: + /// + /// ``` + /// # use expecters::prelude::*; + /// use std::future::ready; + /// # #[tokio::main(flavor = "current_thread")] + /// # async fn main() { + /// expect!( + /// ready(ready(1)), + /// when_ready, // outer future + /// when_ready, // inner future + /// to_equal(1) + /// ) + /// .await + /// .await; + /// # } + /// ``` + fn when_ready(self) -> AssertionBuilder>; + + /// Executes an assertion on the output of a future, but only if it does not + /// complete after another future. + /// + /// If the subject completes before or at the same time as the given future, + /// then the rest of the assertion is executed on its output. Otherwise, the + /// assertion fails. + /// + /// ``` + /// # use expecters::prelude::*; + /// use std::{future::ready, time::Duration }; + /// use tokio::time::sleep; + /// + /// # #[tokio::main(flavor = "current_thread")] + /// # async fn main() { + /// let timeout = Duration::from_secs(5); + /// expect!(ready(1), when_ready_before(sleep(timeout)), to_equal(1)).await; + /// // Also passes if the futures tie: + /// expect!(ready(1), when_ready_before(ready(())), to_equal(1)).await; + /// # } + /// ``` + /// + /// The assertion fails if the subject completes last: + /// + /// ```should_panic + /// # use expecters::prelude::*; + /// use std::future::{pending, ready}; + /// # #[tokio::main(flavor = "current_thread")] + /// # async fn main() { + /// expect!(pending::<()>(), when_ready_before(ready(1)), to_equal(())).await; + /// # } + /// ``` + fn when_ready_before( + self, + other: Annotated, + ) -> AssertionBuilder> + where + Fut: Future; + + /// Executes an assertion on the output of a future, but only if it does not + /// complete before another future. + /// + /// If the subject completes after or at the same time as the given future, then + /// the rest of the assertion is executed on its output. Otherwise, the + /// assertion fails. + /// + /// ``` + /// # use expecters::prelude::*; + /// use std::{future::ready, time::Duration}; + /// use tokio::time::sleep; + /// + /// # #[tokio::main(flavor = "current_thread")] + /// # async fn main() { + /// let duration = Duration::from_secs(1); + /// expect!(sleep(duration), when_ready_after(ready(())), to_equal(())).await; + /// // Also passes if the futures tie: + /// expect!(ready(1), when_ready_after(ready(())), to_equal(1)).await; + /// # } + /// ``` + /// + /// The assertion fails if the subject completes first: + /// + /// ```should_panic + /// # use expecters::prelude::*; + /// use std::future::{pending, ready}; + /// # #[tokio::main(flavor = "current_thread")] + /// # async fn main() { + /// expect!(ready(1), when_ready_after(pending::<()>()), to_equal(1)).await; + /// # } + /// ``` + fn when_ready_after( + self, + other: Annotated, + ) -> AssertionBuilder> + where + Fut: Future; +} + +impl FutureAssertions for AssertionBuilder +where + T: Future, +{ + #[inline] + fn when_ready(self) -> AssertionBuilder> { + AssertionBuilder::modify(self, WhenReadyModifier::new) + } + + #[inline] + fn when_ready_before( + self, + other: Annotated, + ) -> AssertionBuilder> + where + Fut: Future, + { + AssertionBuilder::modify(self, move |prev| { + CompletionOrderModifier::new(prev, other, CompletionOrder::Before) + }) + } + + #[inline] + fn when_ready_after( + self, + other: Annotated, + ) -> AssertionBuilder> + where + Fut: Future, + { + AssertionBuilder::modify(self, move |prev| { + CompletionOrderModifier::new(prev, other, CompletionOrder::After) + }) + } +} diff --git a/src/assertions/futures/modifiers.rs b/src/assertions/futures/modifiers.rs new file mode 100644 index 0000000..1161690 --- /dev/null +++ b/src/assertions/futures/modifiers.rs @@ -0,0 +1,5 @@ +mod completion_order; +mod when_ready; + +pub use completion_order::*; +pub use when_ready::*; diff --git a/src/assertions/futures/modifiers/completion_order.rs b/src/assertions/futures/modifiers/completion_order.rs new file mode 100644 index 0000000..673781d --- /dev/null +++ b/src/assertions/futures/modifiers/completion_order.rs @@ -0,0 +1,68 @@ +use std::future::Future; + +use crate::{ + assertions::{ + futures::{CompletionOrder, CompletionOrderFuture}, + Assertion, AssertionContext, AssertionContextBuilder, AssertionModifier, + }, + metadata::Annotated, +}; + +/// Executes an assertion when the subject completes before or after another +/// future. +#[derive(Clone, Debug)] +pub struct CompletionOrderModifier { + prev: M, + fut: Annotated, + order: CompletionOrder, +} + +impl CompletionOrderModifier { + #[inline] + pub(crate) fn new(prev: M, fut: Annotated, order: CompletionOrder) -> Self { + Self { prev, fut, order } + } +} + +impl AssertionModifier for CompletionOrderModifier +where + M: AssertionModifier>, +{ + type Output = M::Output; + + #[inline] + fn apply(self, cx: AssertionContextBuilder, next: A) -> Self::Output { + self.prev.apply( + cx, + CompletionOrderAssertion { + next, + fut: self.fut, + order: self.order, + }, + ) + } +} + +/// Executes the inner assertion when the subject completes before or after +/// another future. +#[derive(Clone, Debug)] +pub struct CompletionOrderAssertion { + next: A, + fut: Annotated, + order: CompletionOrder, +} + +impl Assertion for CompletionOrderAssertion +where + Fut: Future, + A: Assertion, + T: Future, +{ + type Output = CompletionOrderFuture; + + #[inline] + fn execute(self, mut cx: AssertionContext, subject: T) -> Self::Output { + cx.annotate("other", &self.fut); + CompletionOrderFuture::new(cx, subject, self.fut.into_inner(), self.next, self.order) + } +} diff --git a/src/assertions/futures/modifiers/when_ready.rs b/src/assertions/futures/modifiers/when_ready.rs new file mode 100644 index 0000000..8386652 --- /dev/null +++ b/src/assertions/futures/modifiers/when_ready.rs @@ -0,0 +1,50 @@ +use std::future::Future; + +use crate::assertions::{ + futures::WhenReadyFuture, Assertion, AssertionContext, AssertionContextBuilder, + AssertionModifier, +}; + +/// Executes as assertion when the subject is ready. +#[derive(Clone, Debug)] +pub struct WhenReadyModifier { + prev: M, +} + +impl WhenReadyModifier { + #[inline] + pub(crate) fn new(prev: M) -> Self { + Self { prev } + } +} + +impl AssertionModifier for WhenReadyModifier +where + M: AssertionModifier>, +{ + type Output = M::Output; + + #[inline] + fn apply(self, cx: AssertionContextBuilder, next: A) -> Self::Output { + self.prev.apply(cx, WhenReadyAssertion { next }) + } +} + +/// Executes the inner assertion when the subject is ready. +#[derive(Clone, Debug)] +pub struct WhenReadyAssertion { + next: A, +} + +impl Assertion for WhenReadyAssertion +where + T: Future, + A: Assertion, +{ + type Output = WhenReadyFuture; + + #[inline] + fn execute(self, cx: AssertionContext, subject: T) -> Self::Output { + WhenReadyFuture::new(cx, subject, self.next) + } +} diff --git a/src/assertions/futures/outputs.rs b/src/assertions/futures/outputs.rs new file mode 100644 index 0000000..46cc743 --- /dev/null +++ b/src/assertions/futures/outputs.rs @@ -0,0 +1,13 @@ +mod completion_order; +mod initialized; +mod inverted; +mod merged; +mod unwrapped; +mod when_ready; + +pub use completion_order::*; +pub use initialized::*; +pub use inverted::*; +pub use merged::*; +pub use unwrapped::*; +pub use when_ready::*; diff --git a/src/assertions/futures/outputs/completion_order.rs b/src/assertions/futures/outputs/completion_order.rs new file mode 100644 index 0000000..3f4731e --- /dev/null +++ b/src/assertions/futures/outputs/completion_order.rs @@ -0,0 +1,103 @@ +use std::{ + future::Future, + pin::Pin, + task::{Context, Poll}, +}; + +use pin_project_lite::pin_project; + +use crate::{ + assertions::{Assertion, AssertionContext}, + AssertionOutput, +}; + +pin_project! { + /// A [`Future`] that checks the completion order of two inner futures, then + /// executes an inner assertion if the ordering constraint is satisfied. + /// + /// Created by both [`when_ready_before`](crate::prelude::when_ready_before) + /// and [`when_ready_after`](crate::prelude::when_ready_after). + #[derive(Clone, Debug)] + #[must_use] + pub struct CompletionOrderFuture { + #[pin] + subject: T, + #[pin] + fut: Fut, + fut_done: bool, + next: Option<(AssertionContext, A)>, + order: CompletionOrder, + } +} + +impl CompletionOrderFuture { + pub(crate) fn new( + cx: AssertionContext, + subject: T, + fut: Fut, + next: A, + order: CompletionOrder, + ) -> Self { + Self { + subject, + fut, + fut_done: false, + next: Some((cx, next)), + order, + } + } +} + +impl Future for CompletionOrderFuture +where + Fut: Future, + T: Future, + A: Assertion, +{ + type Output = AssertionOutput; + + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { + let projected = self.project(); + + // Update whether we finished the non-subject future + *projected.fut_done = + *projected.fut_done || matches!(projected.fut.poll(cx), Poll::Ready(_)); + + // Get the success/error for the assertion + #[allow(clippy::match_same_arms)] + let result = match ( + projected.order, + *projected.fut_done, + projected.subject.poll(cx), + ) { + // Neither future is done + (_, false, Poll::Pending) => return Poll::Pending, + + // Check if subject completed first (succeed on ties) + (CompletionOrder::Before, _, Poll::Ready(subject)) => Ok(subject), + (CompletionOrder::Before, true, Poll::Pending) => Err("did not complete before"), + + // Check if subject completed last (succeed on ties) + (CompletionOrder::After, true, Poll::Ready(subject)) => Ok(subject), + (CompletionOrder::After, true, Poll::Pending) => return Poll::Pending, // need output + (CompletionOrder::After, false, Poll::Ready(_)) => Err("completed before"), + }; + + // Call next assertion (if success) + let (cx, next) = projected.next.take().expect("poll after ready"); + Poll::Ready(match result { + Ok(subject) => next.execute(cx, subject), + Err(error) => cx.fail(error), + }) + } +} + +/// The order that the futures are expected to complete in. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(crate) enum CompletionOrder { + /// Subject completes before the provided future. + Before, + + /// Subject completes after the provided future. + After, +} diff --git a/src/assertions/futures/outputs/initialized.rs b/src/assertions/futures/outputs/initialized.rs new file mode 100644 index 0000000..4d220fd --- /dev/null +++ b/src/assertions/futures/outputs/initialized.rs @@ -0,0 +1,125 @@ +use std::{ + fmt::Debug, + future::Future, + pin::Pin, + task::{Context, Poll}, +}; + +use pin_project_lite::pin_project; + +use crate::assertions::{ + general::{InitializableOutput, IntoInitializableOutput}, + AssertionContext, +}; + +pin_project! { + /// An asynchronous output that can be initialized on-demand. + #[must_use] + pub struct InitializedOutputFuture + where + F: Future, + { + #[pin] + inner: Inner, + } +} + +impl Clone for InitializedOutputFuture +where + F: Future + Clone, +{ + #[inline] + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + +impl Debug for InitializedOutputFuture +where + F: Future + Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("InitializedOutputFuture") + .field("inner", &self.inner) + .finish() + } +} + +impl Future for InitializedOutputFuture +where + F: Future, +{ + type Output = F::Output; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let projected = self.project().inner.project(); + match projected { + InnerEnum::Pass { data } => { + let cx = data.take().expect("poll after ready"); + Poll::Ready(F::Output::pass(cx)) + } + InnerEnum::Fail { data } => { + let (cx, message) = data.take().expect("poll after ready"); + Poll::Ready(F::Output::fail(cx, message)) + } + InnerEnum::Wrap { inner } => inner.poll(cx), + } + } +} + +pin_project! { + #[project = InnerEnum] + #[derive(Clone, Debug)] + enum Inner + where + F: Future, + { + Pass { + data: Option, + }, + Fail { + data: Option<(AssertionContext, String)>, + }, + Wrap { + #[pin] + inner: F, + }, + } +} + +impl InitializableOutput for InitializedOutputFuture +where + F: Future, +{ + #[inline] + fn pass(cx: AssertionContext) -> Self { + Self { + inner: Inner::Pass { data: Some(cx) }, + } + } + + #[inline] + fn fail(cx: AssertionContext, message: String) -> Self { + Self { + inner: Inner::Fail { + data: Some((cx, message)), + }, + } + } +} + +impl IntoInitializableOutput for F +where + F: Future, +{ + type Initialized = InitializedOutputFuture; + + #[inline] + fn into_initialized(self) -> Self::Initialized { + InitializedOutputFuture { + inner: Inner::Wrap { inner: self }, + } + } +} diff --git a/src/assertions/futures/outputs/inverted.rs b/src/assertions/futures/outputs/inverted.rs new file mode 100644 index 0000000..a1a3b0a --- /dev/null +++ b/src/assertions/futures/outputs/inverted.rs @@ -0,0 +1,60 @@ +use std::{ + future::Future, + pin::Pin, + task::{ready, Context, Poll}, +}; + +use pin_project_lite::pin_project; + +use crate::assertions::{general::InvertibleOutput, AssertionContext}; + +pin_project! { + /// Inverts an asynchronous output. + #[derive(Clone, Debug)] + #[must_use] + pub struct InvertedOutputFuture { + #[pin] + inner: F, + cx: Option, + } +} + +impl InvertedOutputFuture +where + F: Future, +{ + /// Creates a new inverted output future. + #[inline] + pub fn new(cx: AssertionContext, inner: F) -> Self { + Self { + inner, + cx: Some(cx), + } + } +} + +impl Future for InvertedOutputFuture +where + F: Future, +{ + type Output = ::Inverted; + + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { + let projected = self.project(); + let output = ready!(projected.inner.poll(cx)); + let cx = projected.cx.take().expect("poll after ready"); + Poll::Ready(output.invert(cx)) + } +} + +impl InvertibleOutput for F +where + F: Future, +{ + type Inverted = InvertedOutputFuture; + + #[inline] + fn invert(self, cx: AssertionContext) -> Self::Inverted { + InvertedOutputFuture::new(cx, self) + } +} diff --git a/src/assertions/futures/outputs/merged.rs b/src/assertions/futures/outputs/merged.rs new file mode 100644 index 0000000..7aca88d --- /dev/null +++ b/src/assertions/futures/outputs/merged.rs @@ -0,0 +1,78 @@ +use std::{ + future::Future, + pin::Pin, + task::{ready, Context, Poll}, +}; + +use futures::{ + stream::{Collect, FuturesUnordered}, + StreamExt, +}; +use pin_project_lite::pin_project; + +use crate::assertions::{ + iterators::{MergeStrategy, MergeableOutput}, + AssertionContext, +}; + +pin_project! { + /// Merges many asynchronous outputs. + #[derive(Debug)] + #[must_use] + pub struct MergedOutputsFuture + where + F: Future, + { + #[pin] + inner: Collect, Vec>, + cx: Option, + strategy: MergeStrategy, + } +} + +impl MergedOutputsFuture +where + F: Future, +{ + /// Creates a new merged outputs future using the given merge strategy. + #[inline] + pub fn new(cx: AssertionContext, strategy: MergeStrategy, outputs: I) -> Self + where + I: IntoIterator, + { + Self { + inner: FuturesUnordered::from_iter(outputs).collect(), + cx: Some(cx), + strategy, + } + } +} + +impl Future for MergedOutputsFuture +where + F: Future, +{ + type Output = ::Merged; + + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { + let projected = self.project(); + let outputs = ready!(projected.inner.poll(cx)); + let cx = projected.cx.take().expect("poll after ready"); + Poll::Ready(MergeableOutput::merge(cx, *projected.strategy, outputs)) + } +} + +impl MergeableOutput for F +where + F: Future, +{ + type Merged = MergedOutputsFuture; + + #[inline] + fn merge(cx: AssertionContext, strategy: MergeStrategy, outputs: I) -> Self::Merged + where + I: IntoIterator, + { + MergedOutputsFuture::new(cx, strategy, outputs) + } +} diff --git a/src/assertions/futures/outputs/unwrapped.rs b/src/assertions/futures/outputs/unwrapped.rs new file mode 100644 index 0000000..7c0e439 --- /dev/null +++ b/src/assertions/futures/outputs/unwrapped.rs @@ -0,0 +1,98 @@ +use std::{ + future::Future, + pin::Pin, + task::{ready, Context, Poll}, +}; + +use pin_project_lite::pin_project; + +use crate::assertions::general::UnwrappableOutput; + +pin_project! { + /// Unwraps an asynchronous output. + #[derive(Clone, Debug)] + #[must_use] + pub struct UnwrappedOutputFuture { + #[pin] + inner: F, + } +} + +impl UnwrappedOutputFuture +where + F: Future, +{ + /// Creates a new instance of this future. + #[inline] + pub fn new(inner: F) -> Self { + Self { inner } + } +} + +impl Future for UnwrappedOutputFuture +where + F: Future, +{ + type Output = ::Unwrapped; + + #[inline] + #[track_caller] + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { + let projected = self.project(); + let output = ready!(projected.inner.poll(cx)); + Poll::Ready(output.unwrap()) + } +} + +pin_project! { + /// Tries to unwrap an asynchronous output. + #[derive(Clone, Debug)] + #[must_use] + pub struct TryUnwrappedOutputFuture { + #[pin] + inner: F, + } +} + +impl TryUnwrappedOutputFuture +where + F: Future, +{ + /// Creates a new instance of this future. + #[inline] + pub fn new(inner: F) -> Self { + Self { inner } + } +} + +impl Future for TryUnwrappedOutputFuture +where + F: Future, +{ + type Output = ::TryUnwrapped; + + #[inline] + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { + let projected = self.project(); + let output = ready!(projected.inner.poll(cx)); + Poll::Ready(output.try_unwrap()) + } +} + +impl UnwrappableOutput for F +where + F: Future, +{ + type Unwrapped = UnwrappedOutputFuture; + type TryUnwrapped = TryUnwrappedOutputFuture; + + #[inline] + fn unwrap(self) -> Self::Unwrapped { + UnwrappedOutputFuture::new(self) + } + + #[inline] + fn try_unwrap(self) -> Self::TryUnwrapped { + TryUnwrappedOutputFuture::new(self) + } +} diff --git a/src/assertions/futures/outputs/when_ready.rs b/src/assertions/futures/outputs/when_ready.rs new file mode 100644 index 0000000..0168439 --- /dev/null +++ b/src/assertions/futures/outputs/when_ready.rs @@ -0,0 +1,55 @@ +use std::{ + future::Future, + pin::Pin, + task::{ready, Context, Poll}, +}; + +use pin_project_lite::pin_project; + +use crate::assertions::{Assertion, AssertionContext}; + +pin_project! { + /// A [`Future`] which executes an assertion when its subject is ready. + /// + /// Created by [`when_ready`](crate::prelude::when_ready). + #[derive(Clone, Debug)] + #[must_use] + pub struct WhenReadyFuture + where + T: Future, + { + #[pin] + subject: T, + next: Option<(AssertionContext, A)>, + } +} + +impl WhenReadyFuture +where + T: Future, + A: Assertion, +{ + /// Creates a new instance of this future. + #[inline] + pub(crate) fn new(cx: AssertionContext, subject: T, next: A) -> Self { + Self { + subject, + next: Some((cx, next)), + } + } +} + +impl Future for WhenReadyFuture +where + T: Future, + A: Assertion, +{ + type Output = A::Output; + + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { + let projected = self.project(); + let input = ready!(projected.subject.poll(cx)); + let (cx, next) = projected.next.take().expect("poll after ready"); + Poll::Ready(next.execute(cx, input)) + } +} diff --git a/src/assertions/general.rs b/src/assertions/general.rs new file mode 100644 index 0000000..37dfc42 --- /dev/null +++ b/src/assertions/general.rs @@ -0,0 +1,19 @@ +//! Common, general purpose assertions and modifiers. +//! +//! This module contains types, assertions, and modules that are used by many +//! different kinds of assertions. The exports from this module are likely to +//! be commonly used. +//! +//! The assertions and modifiers are re-exported in the crate's prelude, so glob +//! importing the prelude will import all the assertions and modifiers from this +//! module. + +mod assertions; +mod extensions; +mod modifiers; +mod outputs; + +pub use assertions::*; +pub use extensions::*; +pub use modifiers::*; +pub use outputs::*; diff --git a/src/assertions/general/assertions.rs b/src/assertions/general/assertions.rs new file mode 100644 index 0000000..e1707ad --- /dev/null +++ b/src/assertions/general/assertions.rs @@ -0,0 +1,9 @@ +mod to_cmp; +mod to_equal; +mod to_satisfy; +mod to_satisfy_with; + +pub use to_cmp::*; +pub use to_equal::*; +pub use to_satisfy::*; +pub use to_satisfy_with::*; diff --git a/src/assertions/general/assertions/to_cmp.rs b/src/assertions/general/assertions/to_cmp.rs new file mode 100644 index 0000000..3792d8e --- /dev/null +++ b/src/assertions/general/assertions/to_cmp.rs @@ -0,0 +1,65 @@ +use std::cmp::Ordering; + +use crate::{ + assertions::{Assertion, AssertionContext}, + metadata::Annotated, + AssertionOutput, +}; + +/// A general-purpose assertion for comparing the ordering between two values. +#[derive(Clone, Debug)] +pub struct ToCmpAssertion { + boundary: Annotated, + ordering: Ordering, + allow_eq: bool, + cmp_message: &'static str, +} + +impl ToCmpAssertion { + #[inline] + pub(crate) fn new( + boundary: Annotated, + ordering: Ordering, + allow_eq: bool, + cmp_message: &'static str, + ) -> Self { + Self { + boundary, + ordering, + allow_eq, + cmp_message, + } + } +} + +impl Assertion for ToCmpAssertion +where + T: PartialOrd, +{ + type Output = AssertionOutput; + + fn execute(self, mut cx: AssertionContext, subject: T) -> Self::Output { + cx.annotate("boundary", &self.boundary); + cx.annotate( + "actual ordering", + match subject.partial_cmp(self.boundary.inner()) { + Some(Ordering::Less) => "subject < boundary", + Some(Ordering::Equal) => "subject == boundary", + Some(Ordering::Greater) => "subject > boundary", + None => "none", + }, + ); + + // Use a match here to call the specialized comparison functions in case + // those functions were overridden for a type + let boundary = self.boundary.into_inner(); + let success = match (self.ordering, self.allow_eq) { + (Ordering::Less, true) => subject <= boundary, + (Ordering::Less, false) => subject < boundary, + (Ordering::Greater, true) => subject >= boundary, + (Ordering::Greater, false) => subject > boundary, + (Ordering::Equal, _) => return cx.fail("use to_equal instead"), + }; + cx.pass_if(success, format_args!("not {} boundary", self.cmp_message)) + } +} diff --git a/src/assertions/general/assertions/to_equal.rs b/src/assertions/general/assertions/to_equal.rs new file mode 100644 index 0000000..10c158e --- /dev/null +++ b/src/assertions/general/assertions/to_equal.rs @@ -0,0 +1,31 @@ +use crate::{ + assertions::{Assertion, AssertionContext}, + metadata::Annotated, + AssertionOutput, +}; + +/// Asserts that the subject is equal to an expected value. +#[derive(Clone, Debug)] +pub struct ToEqualAssertion { + expected: Annotated, +} + +impl ToEqualAssertion { + #[inline] + pub(crate) fn new(expected: Annotated) -> Self { + Self { expected } + } +} + +impl Assertion for ToEqualAssertion +where + T: PartialEq, +{ + type Output = AssertionOutput; + + #[inline] + fn execute(self, mut cx: AssertionContext, value: T) -> Self::Output { + cx.annotate("expected", &self.expected); + cx.pass_if(value == self.expected.into_inner(), "values not equal") + } +} diff --git a/src/assertions/general/assertions/to_satisfy.rs b/src/assertions/general/assertions/to_satisfy.rs new file mode 100644 index 0000000..46eadee --- /dev/null +++ b/src/assertions/general/assertions/to_satisfy.rs @@ -0,0 +1,33 @@ +use crate::{ + assertions::{Assertion, AssertionContext}, + metadata::Annotated, + AssertionOutput, +}; + +/// Asserts that the subject satisfies a predicate. +#[derive(Clone, Debug)] +pub struct ToSatisfyAssertion { + predicate: Annotated, +} + +impl ToSatisfyAssertion { + #[inline] + pub(crate) fn new(predicate: Annotated) -> Self { + Self { predicate } + } +} + +impl Assertion for ToSatisfyAssertion +where + F: FnOnce(T) -> bool, +{ + type Output = AssertionOutput; + + fn execute(self, mut cx: AssertionContext, subject: T) -> Self::Output { + cx.annotate("predicate", &self.predicate); + cx.pass_if( + (self.predicate.into_inner())(subject), + "did not satisfy predicate", + ) + } +} diff --git a/src/assertions/general/assertions/to_satisfy_with.rs b/src/assertions/general/assertions/to_satisfy_with.rs new file mode 100644 index 0000000..4972305 --- /dev/null +++ b/src/assertions/general/assertions/to_satisfy_with.rs @@ -0,0 +1,43 @@ +use crate::{ + assertions::{Assertion, AssertionContext, AssertionError}, + metadata::Annotated, + AssertionOutput, +}; + +/// Asserts that the subject satisfies a series of assertions. +#[derive(Clone, Debug)] +pub struct ToSatisfyWithAssertion { + predicate: Annotated, +} + +impl ToSatisfyWithAssertion { + #[inline] + pub(crate) fn new(predicate: Annotated) -> Self { + Self { predicate } + } +} + +impl Assertion for ToSatisfyWithAssertion +where + F: FnOnce(T) -> Result<(), AssertionError>, +{ + type Output = AssertionOutput; + + #[inline] + fn execute(self, cx: AssertionContext, subject: T) -> Self::Output { + // TODO: allow error context to be "added" to cx so failure messages + // show the full execution path and not just the child path + let result = (self.predicate.into_inner())(subject); + cx.pass_if(result.is_ok(), "inner assertions failed") + } +} + +#[cfg(test)] +mod tests { + use crate::prelude::*; + + #[test] + fn vacuous() { + expect!(1, to_satisfy_with(|_| Ok(()))); + } +} diff --git a/src/assertions/general/extensions.rs b/src/assertions/general/extensions.rs new file mode 100644 index 0000000..d591d87 --- /dev/null +++ b/src/assertions/general/extensions.rs @@ -0,0 +1,304 @@ +use std::cmp::Ordering; + +use crate::{ + assertions::{AssertionBuilder, AssertionError}, + metadata::Annotated, +}; + +use super::{ + MapModifier, NotModifier, ToCmpAssertion, ToEqualAssertion, ToSatisfyAssertion, + ToSatisfyWithAssertion, +}; + +/// General-purpose assertions and modifiers. +pub trait GeneralAssertions { + /// Inverts the result of an assertion. + /// + /// If (and only if) the assertion is satisfied, then the result is treated as + /// a failure. + /// + /// ``` + /// # use expecters::prelude::*; + /// expect!(1, not, to_equal(2)); + /// ``` + /// + /// This method panics if the assertion is satisfied: + /// + /// ```should_panic + /// # use expecters::prelude::*; + /// expect!(1, not, to_equal(1)); + /// ``` + fn not(self) -> AssertionBuilder>; + + /// Applies a mapping function to the subject before executing an assertion. + /// This is useful when the subject is a complex type and the assertion + /// should be applied to a specific field or property. + /// + /// Since strings (both [`str`] and [`String`]) can't be directly iterated, + /// this method can be used to map a string to an iterator using the + /// [`str::chars`] method, [`str::bytes`] method, or any other method that + /// returns an iterator. This allows any combinators or assertions that + /// work with iterators to be used with strings as well. + /// + /// ``` + /// # use expecters::prelude::*; + /// expect!("abcd", map(str::chars), any, to_equal('b')); + /// // Ignoring the error message, the above code is equivalent to: + /// expect!("abcd".chars(), any, to_equal('b')); + /// ``` + /// + /// This method panics if the mapped target does not satisfy the assertion: + /// + /// ```should_panic + /// # use expecters::prelude::*; + /// expect!("abcd", map(str::chars), any, to_equal('e')); + /// ``` + /// + /// ## Type inference + /// + /// The Rust compiler can sometimes have trouble inferring the type of the value + /// being mapped. This is due to how the [`expect!`] macro is implemented. The + /// macro wraps the mapping function passed to this modifier to annotate it, but + /// in the process needs to know what the exact type of the closure is and can + /// sometimes struggle to infer it. + /// + /// If type inference is an issue, provide the specific type in the closure. For + /// example, this fails to compile: + /// + /// ```compile_fail + /// # use expecters::prelude::*; + /// struct MyStruct(T); + /// expect!(MyStruct(1), map(|n| n.0), to_equal(1)); + /// ``` + /// + /// Providing a specific type (through a pattern or by specifying the exact + /// type) solves this: + /// + /// ``` + /// # use expecters::prelude::*; + /// struct MyStruct(T); + /// expect!(MyStruct(1), map(|n: MyStruct| n.0), to_equal(1)); + /// expect!(MyStruct(1), map(|MyStruct(n)| n), to_equal(1)); + /// ``` + /// + /// [`expect!`]: crate::expect! + fn map(self, f: Annotated) -> AssertionBuilder>; + + /// Asserts that the subject matches the given predicate. + /// + /// ``` + /// # use expecters::prelude::*; + /// expect!(1, to_satisfy(|n| n % 2 == 1)); + /// ``` + /// + /// The assertion fails if the subject does not satisfy the predicate: + /// + /// ```should_panic + /// # use expecters::prelude::*; + /// expect!(2, to_satisfy(|n| n % 2 == 1)); + /// ``` + /// + /// Since the predicate that is passed into this function will be included in + /// the failure message if the assertion fails, it is recommended to keep the + /// predicate short and simple to keep failure message readable. If a more + /// complex predicate is needed, it's possible to define a separate function and + /// pass that function in as an argument instead: + /// + /// ``` + /// # use expecters::prelude::*; + /// fn is_odd(n: i32) -> bool { + /// n % 2 == 1 + /// } + /// + /// expect!(1, to_satisfy(is_odd)); + /// ``` + #[inline] + fn to_satisfy(&self, predicate: Annotated) -> ToSatisfyAssertion + where + F: FnOnce(T) -> bool, + { + ToSatisfyAssertion::new(predicate) + } + + /// Asserts that the subject matches a series of inner assertions. This + /// "forks" the assertion, allowing an intermediate value to have several + /// different assertions applied to it. + /// + /// This assertion expects a function to be provided to it which performs + /// some inner assertions on the value, returning a + /// [`Result<(), AssertionError>`] to indicate whether the assertion should + /// pass or fail. + /// + /// ``` + /// # use expecters::prelude::*; + /// expect!( + /// [1, 2, 3], + /// count, + /// to_satisfy_with(|value| { + /// try_expect!(value, to_be_greater_than(0))?; + /// try_expect!(value, to_be_less_than(4))?; + /// Ok(()) + /// }), + /// ); + /// ``` + /// + /// The assertion fails if any of the results were failures: + /// + /// ```should_panic + /// # use expecters::prelude::*; + /// expect!( + /// [1, 2, 3], + /// count, + /// to_satisfy_with(|value| { + /// try_expect!(value, to_be_greater_than(3))?; + /// Ok(()) + /// }), + /// ); + /// ``` + /// + /// This does **not** work if passed an async function: + /// + /// ```compile_fail + /// # use expecters::prelude::*; + /// expect!( + /// [ready(1), ready(2), ready(3)], + /// all, + /// to_satisfy_with(|value| async move { + /// try_expect!(value, when_ready, to_be_greater_than(0)).await?; + /// Ok(()) + /// }) + /// ) + /// ``` + // TODO: make an async version + #[inline] + fn to_satisfy_with(&self, predicate: Annotated) -> ToSatisfyWithAssertion + where + F: FnOnce(T) -> Result<(), AssertionError>, + { + ToSatisfyWithAssertion::new(predicate) + } + + /// Asserts that the subject is equal to the given value. + /// + /// ``` + /// # use expecters::prelude::*; + /// expect!(1, to_equal(1)); + /// ``` + /// + /// The assertion fails if the subject is not equal to the given value: + /// + /// ```should_panic + /// # use expecters::prelude::*; + /// expect!(1, to_equal(2)); + /// ``` + #[inline] + fn to_equal(&self, expected: Annotated) -> ToEqualAssertion + where + T: PartialEq, + { + ToEqualAssertion::new(expected) + } + + /// Asserts that the target is less than the given value. + /// + /// ``` + /// # use expecters::prelude::*; + /// expect!(1, to_be_less_than(2)); + /// ``` + /// + /// This method panics if the target is not less than the given value: + /// + /// ```should_panic + /// # use expecters::prelude::*; + /// expect!(2, to_be_less_than(1)); + /// ``` + #[inline] + fn to_be_less_than(&self, boundary: Annotated) -> ToCmpAssertion + where + T: PartialOrd, + { + ToCmpAssertion::new(boundary, Ordering::Less, false, "less than") + } + + /// Asserts that the target is less than or equal to the given value. + /// + /// ``` + /// # use expecters::prelude::*; + /// expect!(1, to_be_less_than_or_equal_to(1)); + /// expect!(1, to_be_less_than_or_equal_to(2)); + /// ``` + /// + /// This method panics if the target is greater less the given value: + /// + /// ```should_panic + /// # use expecters::prelude::*; + /// expect!(2, to_be_less_than_or_equal_to(1)); + /// ``` + #[inline] + fn to_be_less_than_or_equal_to(&self, boundary: Annotated) -> ToCmpAssertion + where + T: PartialOrd, + { + ToCmpAssertion::new(boundary, Ordering::Less, true, "less than or equal to") + } + + /// Asserts that the target is greater than the given value. + /// + /// ``` + /// # use expecters::prelude::*; + /// expect!(2, to_be_greater_than(1)); + /// ``` + /// + /// This method panics if the target is not greater than the given value: + /// + /// ```should_panic + /// # use expecters::prelude::*; + /// expect!(1, to_be_greater_than(2)); + /// ``` + #[inline] + fn to_be_greater_than(&self, boundary: Annotated) -> ToCmpAssertion + where + T: PartialOrd, + { + ToCmpAssertion::new(boundary, Ordering::Greater, false, "greater than") + } + + /// Asserts that the target is greater than or equal to the given value. + /// + /// ``` + /// # use expecters::prelude::*; + /// expect!(1, to_be_greater_than_or_equal_to(1)); + /// expect!(1, to_be_greater_than_or_equal_to(0)); + /// ``` + /// + /// This method panics if the target is less than than the given value: + /// + /// ```should_panic + /// # use expecters::prelude::*; + /// expect!(1, to_be_greater_than_or_equal_to(2)); + /// ``` + #[inline] + fn to_be_greater_than_or_equal_to(&self, boundary: Annotated) -> ToCmpAssertion + where + T: PartialOrd, + { + ToCmpAssertion::new( + boundary, + Ordering::Greater, + true, + "greater than or equal to", + ) + } +} + +impl GeneralAssertions for AssertionBuilder { + #[inline] + fn not(self) -> AssertionBuilder> { + AssertionBuilder::modify(self, NotModifier::new) + } + + #[inline] + fn map(self, f: Annotated) -> AssertionBuilder> { + AssertionBuilder::modify(self, move |prev| MapModifier::new(prev, f)) + } +} diff --git a/src/assertions/general/modifiers.rs b/src/assertions/general/modifiers.rs new file mode 100644 index 0000000..0c0b92d --- /dev/null +++ b/src/assertions/general/modifiers.rs @@ -0,0 +1,9 @@ +mod annotate; +mod map; +mod not; +mod root; + +pub use annotate::*; +pub use map::*; +pub use not::*; +pub use root::*; diff --git a/src/assertions/general/modifiers/annotate.rs b/src/assertions/general/modifiers/annotate.rs new file mode 100644 index 0000000..d1db79d --- /dev/null +++ b/src/assertions/general/modifiers/annotate.rs @@ -0,0 +1,119 @@ +use std::fmt::Debug; + +use crate::{ + assertions::{ + Assertion, AssertionBuilder, AssertionContext, AssertionContextBuilder, AssertionModifier, + }, + metadata::{Annotated, AnnotatedKind}, +}; + +#[doc(hidden)] +pub fn __annotate( + builder: AssertionBuilder, + annotate: fn(T) -> Annotated, +) -> AssertionBuilder> { + AssertionBuilder::modify(builder, |prev| AnnotateModifier { prev, annotate }) +} + +/// Annotates and records input values, and updates the [`AssertionContext`] +/// after modifiers are applied. When using the [`expect!`](crate::expect!) +/// macro, this is applied automatically before every modifier and the final +/// assertion in the chain. +pub struct AnnotateModifier { + prev: M, + annotate: fn(T) -> Annotated, +} + +impl Clone for AnnotateModifier +where + M: Clone, +{ + fn clone(&self) -> Self { + Self { + prev: self.prev.clone(), + annotate: self.annotate, + } + } +} + +impl Debug for AnnotateModifier +where + M: Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("AnnotateModifier") + .field("prev", &self.prev) + .field("annotate", &self.annotate) + .finish() + } +} + +impl AssertionModifier for AnnotateModifier +where + M: AssertionModifier>, +{ + type Output = M::Output; + + #[inline] + fn apply(self, cx: AssertionContextBuilder, assertion: A) -> Self::Output { + self.prev.apply( + cx, + AnnotateAssertion { + next: assertion, + annotate: self.annotate, + }, + ) + } +} + +/// Assertion for [`AnnotateModifier`]. See the docs for the modifier for more +/// information. +pub struct AnnotateAssertion { + next: A, + annotate: fn(T) -> Annotated, +} + +impl Clone for AnnotateAssertion +where + A: Clone, +{ + fn clone(&self) -> Self { + Self { + next: self.next.clone(), + annotate: self.annotate, + } + } +} + +impl Debug for AnnotateAssertion +where + A: Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("AnnotateAssertion") + .field("next", &self.next) + .field("annotate", &self.annotate) + .finish() + } +} + +impl Assertion for AnnotateAssertion +where + A: Assertion, +{ + type Output = A::Output; + + fn execute(self, cx: AssertionContext, subject: T) -> Self::Output { + let mut next_cx = cx.next(); + let annotated = (self.annotate)(subject); + next_cx.annotate( + "received", + match annotated.kind() { + AnnotatedKind::Debug => annotated.as_str(), + AnnotatedKind::Stringify => "? (no debug representation)", + }, + ); + + self.next.execute(next_cx, annotated.into_inner()) + } +} diff --git a/src/assertions/general/modifiers/map.rs b/src/assertions/general/modifiers/map.rs new file mode 100644 index 0000000..06ee81c --- /dev/null +++ b/src/assertions/general/modifiers/map.rs @@ -0,0 +1,59 @@ +use crate::{ + assertions::{Assertion, AssertionContext, AssertionContextBuilder, AssertionModifier}, + metadata::Annotated, +}; + +/// Maps the subject to a new value. +#[derive(Clone, Debug)] +pub struct MapModifier { + prev: M, + map: Annotated, +} + +impl MapModifier { + #[inline] + pub(crate) fn new(prev: M, map: Annotated) -> Self { + Self { prev, map } + } +} + +impl AssertionModifier for MapModifier +where + M: AssertionModifier>, +{ + type Output = M::Output; + + #[inline] + fn apply(self, cx: AssertionContextBuilder, next: A) -> Self::Output { + self.prev.apply( + cx, + MapAssertion { + next, + map: self.map, + }, + ) + } +} + +/// Maps the subject to a new value and executes an inner assertion on it. +#[derive(Clone, Debug)] +pub struct MapAssertion { + next: A, + map: Annotated, +} + +impl Assertion for MapAssertion +where + A: Assertion, + F: FnOnce(T) -> U, +{ + type Output = A::Output; + + #[inline] + fn execute(self, mut cx: AssertionContext, subject: T) -> Self::Output { + cx.annotate("function", &self.map); + + let map = self.map.into_inner(); + self.next.execute(cx, map(subject)) + } +} diff --git a/src/assertions/general/modifiers/not.rs b/src/assertions/general/modifiers/not.rs new file mode 100644 index 0000000..45e9ec8 --- /dev/null +++ b/src/assertions/general/modifiers/not.rs @@ -0,0 +1,58 @@ +use crate::assertions::{ + general::InvertibleOutput, Assertion, AssertionContext, AssertionContextBuilder, + AssertionModifier, +}; + +/// Inverts an assertion. +#[derive(Clone, Debug)] +pub struct NotModifier { + prev: M, +} + +impl NotModifier { + #[inline] + pub(crate) fn new(prev: M) -> Self { + NotModifier { prev } + } +} + +impl AssertionModifier for NotModifier +where + M: AssertionModifier>, +{ + type Output = M::Output; + + #[inline] + fn apply(self, cx: AssertionContextBuilder, next: A) -> Self::Output { + self.prev.apply(cx, NotAssertion { next }) + } +} + +/// Inverts an inner assertion. +#[derive(Clone, Debug)] +pub struct NotAssertion { + next: A, +} + +impl Assertion for NotAssertion +where + A: Assertion, +{ + type Output = ::Inverted; + + #[inline] + fn execute(self, cx: AssertionContext, subject: T) -> Self::Output { + self.next.execute(cx.clone(), subject).invert(cx) + } +} + +#[cfg(test)] +mod tests { + use crate::prelude::*; + + #[test] + fn preserves_context() { + let res = try_expect!("blah", not, not, to_contain_substr("world")); + expect!(res, to_be_err_and, as_debug, to_contain_substr("\"world\"")); + } +} diff --git a/src/assertions/general/modifiers/root.rs b/src/assertions/general/modifiers/root.rs new file mode 100644 index 0000000..ef0832c --- /dev/null +++ b/src/assertions/general/modifiers/root.rs @@ -0,0 +1,29 @@ +use crate::{ + assertions::{Assertion, AssertionContextBuilder, AssertionModifier}, + metadata::Annotated, +}; + +/// The root of an assertion. +#[derive(Clone, Debug)] +pub struct Root { + subject: Annotated, +} + +impl Root { + #[inline] + pub(crate) fn new(subject: Annotated) -> Self { + Self { subject } + } +} + +impl AssertionModifier for Root +where + A: Assertion, +{ + type Output = A::Output; + + #[inline] + fn apply(self, cx: AssertionContextBuilder, assertion: A) -> Self::Output { + assertion.execute(cx.innerner, self.subject.into_inner()) + } +} diff --git a/src/assertions/general/outputs.rs b/src/assertions/general/outputs.rs new file mode 100644 index 0000000..75e0793 --- /dev/null +++ b/src/assertions/general/outputs.rs @@ -0,0 +1,7 @@ +mod initializable; +mod invert; +mod unwrap; + +pub use initializable::*; +pub use invert::*; +pub use unwrap::*; diff --git a/src/assertions/general/outputs/initializable.rs b/src/assertions/general/outputs/initializable.rs new file mode 100644 index 0000000..4763ffb --- /dev/null +++ b/src/assertions/general/outputs/initializable.rs @@ -0,0 +1,58 @@ +use crate::{assertions::AssertionContext, AssertionOutput}; + +/// An assertion output that can be directly constructed from an +/// [`AssertionContext`]. +/// +/// Some modifiers need to directly initialize an instance of their output type. +/// For example, fallible modifiers like [`to_be_some_and`] can fail without +/// continuing the rest of the assertion, and those modifiers need a way to +/// construct the failure for their output type. Output types that implement +/// this trait can be constructed directly, so those modifiers are able to fail +/// the assertion early without continuing execution. +/// +/// [`to_be_some_and`]: crate::prelude::OptionAssertions::to_be_some_and +pub trait InitializableOutput { + /// Constructs an output that represents a success. + fn pass(cx: AssertionContext) -> Self; + + /// Constructs an output that represents a failure with a given message. + fn fail(cx: AssertionContext, message: String) -> Self; +} + +impl InitializableOutput for AssertionOutput { + #[inline] + fn pass(cx: AssertionContext) -> Self { + AssertionOutput::new(cx, None) + } + + #[inline] + fn fail(cx: AssertionContext, message: String) -> Self { + AssertionOutput::new(cx, Some(message)) + } +} + +/// An output type that can be converted into an +/// [initializable output type](InitializableOutput). +pub trait IntoInitializableOutput { + /// The initialized output type. + /// + /// This may differ from `Self` if it cannot be constructed directly, but + /// can be wrapped by another type that also supports direct construction + /// (which is often the case for asynchronous outputs). + type Initialized: InitializableOutput; + + /// Converts this output into an instance of the initialized output type. + /// + /// This is important to ensure that an existing instance of this output can + /// be converted to the success/failure types this output can produce. + fn into_initialized(self) -> Self::Initialized; +} + +impl IntoInitializableOutput for AssertionOutput { + type Initialized = Self; + + #[inline] + fn into_initialized(self) -> Self::Initialized { + self + } +} diff --git a/src/assertions/general/outputs/invert.rs b/src/assertions/general/outputs/invert.rs new file mode 100644 index 0000000..1c860cd --- /dev/null +++ b/src/assertions/general/outputs/invert.rs @@ -0,0 +1,44 @@ +use crate::{assertions::AssertionContext, AssertionOutput}; + +/// An assertion output that can be inverted. +/// +/// An inverted output is swapped from a failure to a success, or from a success +/// to a failure. +pub trait InvertibleOutput { + /// The inverted output. + type Inverted; + + /// Inverts the output. + /// + /// A success is converted to a failure, and a failure is converted to a + /// success. + /// + /// If it is not yet known whether the output represents a success or + /// failure, then a value is returned that inverts that output when it is + /// known. + /// + /// The context passed into this method should represent the point at which + /// the output was inverted. For example, an output's internal context may + /// represent an execution flow going through `expect!(1, not, to_equal(2))` + /// and reaching the [`to_equal`] assertion, but the inversion would occur + /// at [`not`]. + /// + /// [`not`]: crate::prelude::GeneralAssertions::not + /// [`to_equal`]: crate::prelude::GeneralAssertions::to_equal + fn invert(self, cx: AssertionContext) -> Self::Inverted; +} + +impl InvertibleOutput for AssertionOutput { + type Inverted = Self; + + #[inline] + fn invert(mut self, cx: AssertionContext) -> Self::Inverted { + if self.is_pass() { + self.set_fail(cx, "expected a failure, received a success"); + } else { + self.set_pass(cx); + } + + self + } +} diff --git a/src/assertions/general/outputs/unwrap.rs b/src/assertions/general/outputs/unwrap.rs new file mode 100644 index 0000000..1a2802a --- /dev/null +++ b/src/assertions/general/outputs/unwrap.rs @@ -0,0 +1,65 @@ +use crate::{assertions::AssertionError, AssertionOutput}; + +/// An assertion output that can be unwrapped. +/// +/// Unwrapping the output causes it to panic as soon as possible. For +/// [`AssertionOutput`]s, the value is converted into a [`Result`] and panics if +/// the result is an [`Err`], for example. Other output types may choose to +/// unwrap in a different manner (like unwrapping an inner output once it's +/// available in the case of asynchronous outputs). +pub trait UnwrappableOutput { + /// The unwrapped output. This is generally either `()` or a wrapper around + /// one (like a future). + type Unwrapped; + + /// The output representing an attempt at unwrapping. This is generally a + /// [`Result<(), AssertionError>`] or a wrapper around one (like a future). + type TryUnwrapped; + + /// Unwraps this output. + /// + /// The purpose of this method is to panic as soon as possible if an + /// assertion fails. Not all outputs will be unwrapped, but if they are, + /// they should provide output to the user as soon as possible if the + /// assertion failed. + /// + /// This is what the assertion returns when calling + /// [`expect!`](crate::expect!). + /// + /// Implementers of this function should also attach `#[track_caller]` to + /// the function that performs the unwrapping. For synchronous outputs, this + /// function is usually the one that unwraps the value, but async outputs + /// may choose to unwrap the value in a `poll` function, for example. + fn unwrap(self) -> Self::Unwrapped; + + /// Tries to unwrap this output. + /// + /// This is similar to [`unwrap`](UnwrappableOutput::unwrap), but instead of + /// panicking on failure, it instead returns an [`Err`] containing the + /// error. On success, returns an [`Ok`] instead. + /// + /// The actual return value from this function may be a [`Result`], or may + /// be another type that eventually becomes a [`Result`] through some series + /// of well-documented operations. For example, for an asynchronous + /// assertion, a future may be returned instead that eventually outputs a + /// [`Result`]. + fn try_unwrap(self) -> Self::TryUnwrapped; +} + +impl UnwrappableOutput for AssertionOutput { + type Unwrapped = (); + type TryUnwrapped = Result<(), AssertionError>; + + #[inline] + #[track_caller] + fn unwrap(self) -> Self::Unwrapped { + if let Err(e) = self.into_result() { + panic!("{e:?}") + } + } + + #[inline] + fn try_unwrap(self) -> Self::TryUnwrapped { + self.into_result() + } +} diff --git a/src/assertions/iterators.rs b/src/assertions/iterators.rs new file mode 100644 index 0000000..aa04def --- /dev/null +++ b/src/assertions/iterators.rs @@ -0,0 +1,9 @@ +//! Assertions and modifiers for tests that involve iterators. + +mod extensions; +mod modifiers; +mod outputs; + +pub use extensions::*; +pub use modifiers::*; +pub use outputs::*; diff --git a/src/assertions/iterators/extensions.rs b/src/assertions/iterators/extensions.rs new file mode 100644 index 0000000..e86c6e0 --- /dev/null +++ b/src/assertions/iterators/extensions.rs @@ -0,0 +1,147 @@ +use crate::{assertions::AssertionBuilder, metadata::Annotated}; + +use super::{CountModifier, MergeModifier, MergeStrategy, NthModifier}; + +/// Assertions and modifiers for [Iterator]s. +pub trait IteratorAssertions +where + T: IntoIterator, +{ + /// Executes an assertion on every value within the subject, and succeeds if and + /// only if none of the assertions fail. + /// + /// ``` + /// # use expecters::prelude::*; + /// expect!([1, 3, 5], all, to_be_less_than(10)); + /// expect!([] as [i32; 0], all, to_equal(1)); + /// ``` + /// + /// The assertion fails if any element does not satisfy the assertion: + /// + /// ```should_panic + /// # use expecters::prelude::*; + /// expect!([1, 3, 5], all, to_equal(5)); + /// ``` + /// + /// Requires that the rest of the assertion is [`Clone`]. The subject of the + /// assertion doesn't need to be cloneable, but the rest of the assertion does. + /// For example, this works fine: + /// + /// ``` + /// # use expecters::prelude::*; + /// #[derive(PartialEq)] + /// struct NotClone(i32); + /// expect!([NotClone(0)], all, to_satisfy(|x| x == NotClone(0))); + /// ``` + /// + /// This does not though since `to_equal` takes ownership of a non-cloneable + /// value: + /// + /// ```compile_fail + /// # use expecters::prelude::*; + /// #[derive(PartialEq)] + /// struct NotClone(i32); + /// expect!([NotClone(0)], all, to_equal(NonClone(0))); + /// ``` + fn all(self) -> AssertionBuilder>; + + /// Executes an assertion on every value within the subject, and succeeds if and + /// only if an assertion succeeds. + /// + /// ``` + /// # use expecters::prelude::*; + /// expect!([1, 3, 5], any, to_equal(5)); + /// expect!([] as [i32; 0], not, any, to_equal(1)); + /// ``` + /// + /// The assertion fails if no element satisfies the assertion: + /// + /// ```should_panic + /// # use expecters::prelude::*; + /// expect!([1, 3, 5], any, to_equal(4)); + /// ``` + /// + /// Requires that the rest of the assertion is [`Clone`]. The subject of the + /// assertion doesn't need to be cloneable, but the rest of the assertion does. + /// For example, this works fine: + /// + /// ``` + /// # use expecters::prelude::*; + /// #[derive(PartialEq)] + /// struct NotClone(i32); + /// expect!([NotClone(0)], any, to_satisfy(|x| x == NotClone(0))); + /// ``` + /// + /// This does not though since `to_equal` takes ownership of a non-cloneable + /// value: + /// + /// ```compile_fail + /// # use expecters::prelude::*; + /// #[derive(PartialEq)] + /// struct NotClone(i32); + /// expect!([NotClone(0)], any, to_equal(NonClone(0))); + /// ``` + fn any(self) -> AssertionBuilder>; + + /// Counts the length of the subject, and executes an assertion on the result. + /// + /// ``` + /// # use expecters::prelude::*; + /// expect!([1, 2, 3], count, to_equal(3)); + /// ``` + /// + /// This uses the [`Iterator::count`] method to determine the number of elements + /// in the subject. If the subject is an unbounded iterator, then the assertion + /// will not complete (unless it panics for another reason). See the iterator + /// method for more information. + fn count(self) -> AssertionBuilder>; + + /// Applies an assertion to a specific element in the target. If the element + /// does not exist or does not satisfy the assertion, then the result is + /// treated as a failure. The index is zero-based. + /// + /// ``` + /// # use expecters::prelude::*; + /// expect!([1, 2, 3], nth(1), to_equal(2)); + /// ``` + /// + /// The assertion fails if the element does not exist: + /// + /// ```should_panic + /// # use expecters::prelude::*; + /// expect!([1, 2, 3], nth(3), to_equal(4)); + /// ``` + /// + /// It also fails if the element does not satisfy the assertion: + /// + /// ```should_panic + /// # use expecters::prelude::*; + /// expect!([1, 2, 3], nth(1), to_equal(1)); + /// ``` + fn nth(self, index: Annotated) -> AssertionBuilder>; +} + +impl IteratorAssertions for AssertionBuilder +where + T: IntoIterator, +{ + #[inline] + fn all(self) -> AssertionBuilder> { + AssertionBuilder::modify(self, |prev| MergeModifier::new(prev, MergeStrategy::All)) + } + + #[inline] + fn any(self) -> AssertionBuilder> { + AssertionBuilder::modify(self, |prev| MergeModifier::new(prev, MergeStrategy::Any)) + } + + #[inline] + fn count(self) -> AssertionBuilder> { + AssertionBuilder::modify(self, CountModifier::new) + } + + #[inline] + fn nth(self, index: Annotated) -> AssertionBuilder> { + AssertionBuilder::modify(self, move |prev| NthModifier::new(prev, index)) + } +} diff --git a/src/assertions/iterators/modifiers.rs b/src/assertions/iterators/modifiers.rs new file mode 100644 index 0000000..89bfb10 --- /dev/null +++ b/src/assertions/iterators/modifiers.rs @@ -0,0 +1,7 @@ +mod count; +mod merge; +mod nth; + +pub use count::*; +pub use merge::*; +pub use nth::*; diff --git a/src/assertions/iterators/modifiers/count.rs b/src/assertions/iterators/modifiers/count.rs new file mode 100644 index 0000000..9d48e74 --- /dev/null +++ b/src/assertions/iterators/modifiers/count.rs @@ -0,0 +1,45 @@ +use crate::assertions::{Assertion, AssertionContext, AssertionContextBuilder, AssertionModifier}; + +/// Counts the number of items in a subject. +#[derive(Clone, Debug)] +pub struct CountModifier { + prev: M, +} + +impl CountModifier { + #[inline] + pub(crate) fn new(prev: M) -> CountModifier { + CountModifier { prev } + } +} + +impl AssertionModifier for CountModifier +where + M: AssertionModifier>, +{ + type Output = M::Output; + + #[inline] + fn apply(self, cx: AssertionContextBuilder, next: A) -> Self::Output { + self.prev.apply(cx, CountAssertion { next }) + } +} + +/// Executes the inner assertion on the number of items in the subject. +#[derive(Clone, Debug)] +pub struct CountAssertion { + next: A, +} + +impl Assertion for CountAssertion +where + A: Assertion, + T: IntoIterator, +{ + type Output = A::Output; + + #[inline] + fn execute(self, cx: AssertionContext, subject: T) -> Self::Output { + self.next.execute(cx, subject.into_iter().count()) + } +} diff --git a/src/assertions/iterators/modifiers/merge.rs b/src/assertions/iterators/modifiers/merge.rs new file mode 100644 index 0000000..2d3f739 --- /dev/null +++ b/src/assertions/iterators/modifiers/merge.rs @@ -0,0 +1,192 @@ +use crate::assertions::{ + iterators::{MergeStrategy, MergeableOutput}, + Assertion, AssertionContext, AssertionContextBuilder, AssertionModifier, +}; + +/// Forks an assertion, executing it for each element of the subject. +#[derive(Clone, Debug)] +pub struct MergeModifier { + prev: M, + strategy: MergeStrategy, +} + +impl MergeModifier { + #[inline] + pub(crate) fn new(prev: M, strategy: MergeStrategy) -> Self { + Self { prev, strategy } + } +} + +impl AssertionModifier for MergeModifier +where + M: AssertionModifier>, +{ + type Output = M::Output; + + #[inline] + fn apply(self, cx: AssertionContextBuilder, next: A) -> Self::Output { + self.prev.apply( + cx, + MergeAssertion { + next, + strategy: self.strategy, + }, + ) + } +} + +/// Forks the inner assertion, executing it for each element of the subject. +#[derive(Clone, Debug)] +pub struct MergeAssertion { + next: A, + strategy: MergeStrategy, +} + +impl Assertion for MergeAssertion +where + A: Assertion + Clone, + T: IntoIterator, +{ + type Output = ::Merged; + + fn execute(self, cx: AssertionContext, subject: T) -> Self::Output { + let outputs = subject.into_iter().enumerate().map({ + // Clone the context so it can be moved into the closure (we need it + // again later to merge the outputs) + let cx = cx.clone(); + + move |(idx, item)| { + // Create a new context for this execution path + let mut cx = cx.clone(); + cx.annotate("index", idx); + + // Call the next assertion + self.next.clone().execute(cx, item) + } + }); + + // Merge the outputs + MergeableOutput::merge(cx, self.strategy, outputs) + } +} + +#[cfg(test)] +mod tests { + use std::{iter::repeat, sync::mpsc::channel, thread::spawn, time::Duration}; + + use test_case::test_case; + + use crate::prelude::*; + + fn with_timeout(t: Duration, f: F) -> bool + where + F: FnOnce() + Send + 'static, + { + let (done_tx, done_rx) = channel(); + let _run = spawn(move || { + f(); + let _ = done_tx.send(()); + }); + + let output = done_rx.recv_timeout(t); + output.is_ok() + } + + #[test_case(false, || expect!(repeat(0), all, to_equal(0)); "all infinite")] + #[test_case(true, || expect!(repeat(0), not, all, to_equal(1)); "all short-circuit")] + #[test_case(false, || expect!(repeat(0), any, to_equal(1)); "any infinite")] + #[test_case(true, || expect!(repeat(0), any, to_equal(0)); "any short-circuit")] + fn short_circuit(should_pass: bool, f: fn()) { + let success = with_timeout(Duration::from_secs(1), f); + expect!(success, to_equal(should_pass)); + } +} + +#[cfg(all(test, feature = "futures"))] +mod async_tests { + use std::{ + future::{ready, Future}, + iter::repeat, + sync::mpsc::channel, + time::Duration, + }; + + use test_case::test_case; + use tokio::spawn; + + use crate::prelude::*; + + fn with_timeout(t: Duration, f: F) -> bool + where + F: Future + Send + 'static, + { + let (done_tx, done_rx) = channel(); + let _run = spawn(async move { + f.await; + let _ = done_tx.send(()); + }); + + let output = done_rx.recv_timeout(t); + output.is_ok() + } + + #[test_case( + false, + // Need to wrap these expectations because even constructing them is + // an infinite loop due to the iterator being collected into a + // FuturesUnordered + async { + expect!(repeat(ready(0)), all, when_ready, to_equal(0)).await; + }; + "all infinite" + )] + #[test_case( + true, + async { + expect!(repeat(ready(0)), not, all, when_ready, to_equal(1)).await; + } => ignore["not implemented yet"]; + "all short-circuit" + )] + #[test_case( + false, + async { + expect!(repeat(ready(0)), any, when_ready, to_equal(1)).await; + }; + "any infinite" + )] + #[test_case( + true, + async { + expect!(repeat(ready(0)), any, when_ready, to_equal(0)).await; + } => ignore["not implemented yet"]; + "any short-circuit" + )] + #[tokio::test] + async fn short_circuit(should_pass: bool, f: F) + where + F: Future + Send + 'static, + { + let success = with_timeout(Duration::from_secs(1), f); + expect!(success, to_equal(should_pass)); + } + + /// Ensures that assertions that use non-Clone opaque features can still be + /// executed with the merging modifiers. This means the assertion executed + /// after the merging modifier must be Clone even if the subject passed into + /// the assertion is not. + #[tokio::test] + async fn opaque_futures() { + #[allow(clippy::unused_async)] + async fn get_cat_url(id: u32) -> String { + format!("cats/{id}.png") + } + + expect!( + [get_cat_url(1), get_cat_url(2)], + all, + when_ready, + to_contain_substr(".png") + ) + .await; + } +} diff --git a/src/assertions/iterators/modifiers/nth.rs b/src/assertions/iterators/modifiers/nth.rs new file mode 100644 index 0000000..e1fc3ff --- /dev/null +++ b/src/assertions/iterators/modifiers/nth.rs @@ -0,0 +1,78 @@ +use crate::{ + assertions::{ + general::IntoInitializableOutput, Assertion, AssertionContext, AssertionContextBuilder, + AssertionModifier, + }, + metadata::Annotated, +}; + +/// Selects an element out of the subject. +#[derive(Clone, Debug)] +pub struct NthModifier { + prev: M, + index: Annotated, +} + +impl NthModifier { + #[inline] + pub(crate) fn new(prev: M, index: Annotated) -> Self { + Self { prev, index } + } +} + +impl AssertionModifier for NthModifier +where + M: AssertionModifier>, +{ + type Output = M::Output; + + #[inline] + fn apply(self, cx: AssertionContextBuilder, next: A) -> Self::Output { + self.prev.apply( + cx, + NthAssertion { + next, + index: self.index, + }, + ) + } +} + +/// Selects an element out of the subject and executes the inner assertion on +/// it. +#[derive(Clone, Debug)] +pub struct NthAssertion { + next: A, + index: Annotated, +} + +impl Assertion for NthAssertion +where + A: Assertion, + T: IntoIterator, +{ + type Output = ::Initialized; + + #[inline] + fn execute(self, mut cx: AssertionContext, subject: T) -> Self::Output { + cx.annotate("index", &self.index); + + let index = self.index.into_inner(); + let Some(subject) = subject.into_iter().nth(index) else { + return cx.fail("index out of bounds"); + }; + self.next.execute(cx, subject).into_initialized() + } +} + +#[cfg(all(test, feature = "futures"))] +mod async_tests { + use std::future::ready; + + use crate::prelude::*; + + #[tokio::test] + async fn nested_async_works() { + expect!([ready(1)], nth(0), when_ready, to_equal(1)).await; + } +} diff --git a/src/assertions/iterators/outputs.rs b/src/assertions/iterators/outputs.rs new file mode 100644 index 0000000..1eb6eb9 --- /dev/null +++ b/src/assertions/iterators/outputs.rs @@ -0,0 +1,3 @@ +mod merge; + +pub use merge::*; diff --git a/src/assertions/iterators/outputs/merge.rs b/src/assertions/iterators/outputs/merge.rs new file mode 100644 index 0000000..f25d2d7 --- /dev/null +++ b/src/assertions/iterators/outputs/merge.rs @@ -0,0 +1,71 @@ +use crate::{assertions::AssertionContext, AssertionOutput}; + +/// A type of assertion output that can be collected from an iterator and merged +/// into a single output. +/// +/// This is the core of how modifiers like [`all`] and [`any`] work. Outputs +/// that implement this trait can be collected from an iterator into a new +/// output following one of two [merge strategies](MergeStrategy): +/// +/// - [`All`](MergeStrategy::All): the merged output succeeds if none of the +/// original outputs were failures. +/// - [`Any`](MergeStrategy::Any): the merged output succeeds if at least one of +/// the original outputs was a success. +/// +/// Note that these are carefully worded to include definitions for empty +/// iterators. An empty iterator represents either a success (for `All`) or a +/// failure (for `Any`) depending on your merge strategy. +/// +/// [`all`]: crate::prelude::IteratorAssertions::all +/// [`any`]: crate::prelude::IteratorAssertions::any +pub trait MergeableOutput { + /// The type of the merged output. + type Merged; + + /// Merges an iterator of assertion outputs into a single output. + /// + /// This method may choose to short-circuit, but it is not guaranteed. For + /// example, while iterators of [`AssertionOutput`]s can be short-circuited + /// since their success/failure status is already known, iterators over + /// futures are unable to do the same since the status is not yet known. + fn merge(cx: AssertionContext, strategy: MergeStrategy, outputs: I) -> Self::Merged + where + I: IntoIterator; +} + +impl MergeableOutput for AssertionOutput { + type Merged = AssertionOutput; + + #[inline] + fn merge(cx: AssertionContext, strategy: MergeStrategy, outputs: I) -> Self::Merged + where + I: IntoIterator, + { + let mut result = cx.pass_if(strategy == MergeStrategy::All, "no outputs"); + for output in outputs { + match (strategy, output.is_pass()) { + (MergeStrategy::Any, true) | (MergeStrategy::All, false) => return output, + _ => result = output, + } + } + + result + } +} + +/// A strategy for merging outputs. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum MergeStrategy { + /// Merged output represents a success if and only if none of the original + /// outputs represented a failure. + /// + /// On failure, the failure represents one or more of the original failures. + All, + + /// Merged output represents a success if and only if at least one of the + /// original outputs represented a success. + /// + /// On success, the success represents one or more of the original + /// successes. + Any, +} diff --git a/src/assertions/options.rs b/src/assertions/options.rs new file mode 100644 index 0000000..b2c6061 --- /dev/null +++ b/src/assertions/options.rs @@ -0,0 +1,11 @@ +//! Assertions and modifiers for tests that involve [`Option`]s. + +mod assertions; +mod extensions; +mod modifiers; +mod optionish; + +pub use assertions::*; +pub use extensions::*; +pub use modifiers::*; +pub use optionish::*; diff --git a/src/assertions/options/assertions.rs b/src/assertions/options/assertions.rs new file mode 100644 index 0000000..5cdc552 --- /dev/null +++ b/src/assertions/options/assertions.rs @@ -0,0 +1,3 @@ +mod to_be_variant; + +pub use to_be_variant::*; diff --git a/src/assertions/options/assertions/to_be_variant.rs b/src/assertions/options/assertions/to_be_variant.rs new file mode 100644 index 0000000..0e316b7 --- /dev/null +++ b/src/assertions/options/assertions/to_be_variant.rs @@ -0,0 +1,69 @@ +use crate::{ + assertions::{options::Optionish, Assertion, AssertionContext}, + AssertionOutput, +}; + +/// Asserts that the subject is a specific [`Option`] variant. +#[derive(Clone, Debug)] +pub struct ToBeOptionVariantAssertion { + expected: OptionVariant, +} + +impl ToBeOptionVariantAssertion { + #[inline] + pub(crate) fn new(expected: OptionVariant) -> Self { + Self { expected } + } +} + +impl Assertion for ToBeOptionVariantAssertion +where + O: Optionish, +{ + type Output = AssertionOutput; + + #[inline] + fn execute(self, cx: AssertionContext, subject: O) -> Self::Output { + match self.expected { + OptionVariant::Some => cx.pass_if(subject.some().is_some(), "received None"), + OptionVariant::None => cx.pass_if(subject.some().is_none(), "received Some"), + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(crate) enum OptionVariant { + Some, + None, +} + +#[cfg(test)] +mod tests { + use crate::prelude::*; + + #[test] + fn some_refs_work() { + let mut option: Option = Some(1); + expect!(&option, to_be_some); + expect!(&mut option, to_be_some); + expect!(option, to_be_some); + + let mut option: Option = None; + expect!(&option, not, to_be_some); + expect!(&mut option, not, to_be_some); + expect!(option, not, to_be_some); + } + + #[test] + fn none_refs_work() { + let mut option: Option = None; + expect!(&option, to_be_none); + expect!(&mut option, to_be_none); + expect!(option, to_be_none); + + let mut option: Option = Some(1); + expect!(&option, not, to_be_none); + expect!(&mut option, not, to_be_none); + expect!(option, not, to_be_none); + } +} diff --git a/src/assertions/options/extensions.rs b/src/assertions/options/extensions.rs new file mode 100644 index 0000000..cac9800 --- /dev/null +++ b/src/assertions/options/extensions.rs @@ -0,0 +1,73 @@ +use crate::assertions::AssertionBuilder; + +use super::{OptionVariant, Optionish, SomeAndModifier, ToBeOptionVariantAssertion}; + +/// Assertions and modifiers for [`Option`]s. +pub trait OptionAssertions +where + T: Optionish, +{ + /// Asserts that the subject holds a value, then continues the assertion with + /// the contained value. + /// + /// ``` + /// # use expecters::prelude::*; + /// expect!(Some(1), to_be_some_and, to_equal(1)); + /// ``` + /// + /// The assertion fails if the option is [`None`]: + /// + /// ```should_panic + /// # use expecters::prelude::*; + /// expect!(None::, to_be_some_and, to_equal(2)); + /// ``` + fn to_be_some_and(self) -> AssertionBuilder>; + + /// Asserts that the subject holds a value. + /// + /// ``` + /// # use expecters::prelude::*; + /// expect!(Some(1), to_be_some); + /// ``` + /// + /// The assertion fails if the subject does not hold a value: + /// + /// ```should_panic + /// # use expecters::prelude::*; + /// expect!(None::, to_be_some); + /// ``` + #[inline] + #[must_use] + fn to_be_some(&self) -> ToBeOptionVariantAssertion { + ToBeOptionVariantAssertion::new(OptionVariant::Some) + } + + /// Asserts that the subject does not hold a value. + /// + /// ``` + /// # use expecters::prelude::*; + /// expect!(None::, to_be_none); + /// ``` + /// + /// The assertion fails if the subject holds a value: + /// + /// ```should_panic + /// # use expecters::prelude::*; + /// expect!(Some(1), to_be_none); + /// ``` + #[inline] + #[must_use] + fn to_be_none(&self) -> ToBeOptionVariantAssertion { + ToBeOptionVariantAssertion::new(OptionVariant::None) + } +} + +impl OptionAssertions for AssertionBuilder +where + T: Optionish, +{ + #[inline] + fn to_be_some_and(self) -> AssertionBuilder> { + AssertionBuilder::modify(self, SomeAndModifier::new) + } +} diff --git a/src/assertions/options/modifiers.rs b/src/assertions/options/modifiers.rs new file mode 100644 index 0000000..3ab5892 --- /dev/null +++ b/src/assertions/options/modifiers.rs @@ -0,0 +1,3 @@ +mod some_and; + +pub use some_and::*; diff --git a/src/assertions/options/modifiers/some_and.rs b/src/assertions/options/modifiers/some_and.rs new file mode 100644 index 0000000..5599fe5 --- /dev/null +++ b/src/assertions/options/modifiers/some_and.rs @@ -0,0 +1,78 @@ +use crate::assertions::{ + general::IntoInitializableOutput, options::Optionish, Assertion, AssertionContext, + AssertionContextBuilder, AssertionModifier, +}; + +/// Maps the subject to its inner value. +#[derive(Clone, Debug)] +pub struct SomeAndModifier { + prev: M, +} + +impl SomeAndModifier { + #[inline] + pub(crate) fn new(prev: M) -> Self { + Self { prev } + } +} + +impl AssertionModifier for SomeAndModifier +where + M: AssertionModifier>, +{ + type Output = M::Output; + + #[inline] + fn apply(self, cx: AssertionContextBuilder, next: A) -> Self::Output { + self.prev.apply(cx, SomeAndAssertion { next }) + } +} + +/// Executes the inner assertion on the subject's inner value. +#[derive(Clone, Debug)] +pub struct SomeAndAssertion { + next: A, +} + +impl Assertion for SomeAndAssertion +where + A: Assertion, + O: Optionish, +{ + type Output = ::Initialized; + + #[inline] + fn execute(self, cx: AssertionContext, subject: O) -> Self::Output { + let Some(subject) = subject.some() else { + return cx.fail("received None"); + }; + self.next.execute(cx, subject).into_initialized() + } +} + +#[cfg(test)] +mod tests { + use crate::prelude::*; + + #[test] + fn refs_work() { + let mut option: Option = Some(1); + expect!(&option, to_be_some_and, to_satisfy(|&n| n == 1)); + expect!(&mut option, to_be_some_and, to_satisfy(|&mut n| n == 1)); + expect!(option, to_be_some_and, to_equal(1)); + + let mut option: Option = None; + expect!(&option, not, to_be_some_and, to_satisfy(|_| true)); + expect!(&mut option, not, to_be_some_and, to_satisfy(|_| true)); + expect!(option, not, to_be_some_and, to_satisfy(|_| true)); + } + + #[cfg(feature = "futures")] + #[tokio::test] + async fn nested_async_works() { + use std::future::ready; + + let result = Some(ready(1)); + expect!(result, to_be_some_and, when_ready, to_equal(1)).await; + } +} diff --git a/src/assertions/options/optionish.rs b/src/assertions/options/optionish.rs new file mode 100644 index 0000000..f1d6e87 --- /dev/null +++ b/src/assertions/options/optionish.rs @@ -0,0 +1,49 @@ +mod sealed { + pub trait Sealed { + type T; + type OutT; + + fn some(self) -> Option; + } + + impl Sealed for Option { + type T = T; + type OutT = T; + + #[inline] + fn some(self) -> Option { + self + } + } + + impl<'a, T> Sealed for &'a Option { + type T = T; + type OutT = &'a T; + + #[inline] + fn some(self) -> Option { + self.as_ref() + } + } + + impl<'a, T> Sealed for &'a mut Option { + type T = T; + type OutT = &'a mut T; + + #[inline] + fn some(self) -> Option { + self.as_mut() + } + } +} + +/// Helper trait for mapping [`Option`] and its references to its inner value +/// and type. +/// +/// This is implemented for: +/// - `Option` +/// - `&Option` +/// - `&mut Option` +pub trait Optionish: sealed::Sealed {} + +impl Optionish for R where R: sealed::Sealed {} diff --git a/src/assertions/results.rs b/src/assertions/results.rs new file mode 100644 index 0000000..6529ed8 --- /dev/null +++ b/src/assertions/results.rs @@ -0,0 +1,11 @@ +//! Assertions and modifiers for tests that involve [`Result`]. + +mod assertions; +mod extensions; +mod modifiers; +mod resultish; + +pub use assertions::*; +pub use extensions::*; +pub use modifiers::*; +pub use resultish::*; diff --git a/src/assertions/results/assertions.rs b/src/assertions/results/assertions.rs new file mode 100644 index 0000000..5cdc552 --- /dev/null +++ b/src/assertions/results/assertions.rs @@ -0,0 +1,3 @@ +mod to_be_variant; + +pub use to_be_variant::*; diff --git a/src/assertions/results/assertions/to_be_variant.rs b/src/assertions/results/assertions/to_be_variant.rs new file mode 100644 index 0000000..42ecc54 --- /dev/null +++ b/src/assertions/results/assertions/to_be_variant.rs @@ -0,0 +1,69 @@ +use crate::{ + assertions::{results::Resultish, Assertion, AssertionContext}, + AssertionOutput, +}; + +/// Asserts that the subject is a specific [`Result`] variant. +#[derive(Clone, Debug)] +pub struct ToBeResultVariantAssertion { + expected: ResultVariant, +} + +impl ToBeResultVariantAssertion { + #[inline] + pub(crate) fn new(expected: ResultVariant) -> Self { + Self { expected } + } +} + +impl Assertion for ToBeResultVariantAssertion +where + R: Resultish, +{ + type Output = AssertionOutput; + + #[inline] + fn execute(self, cx: AssertionContext, subject: R) -> Self::Output { + match self.expected { + ResultVariant::Ok => cx.pass_if(subject.ok().is_some(), "received Err"), + ResultVariant::Err => cx.pass_if(subject.err().is_some(), "received Ok"), + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(crate) enum ResultVariant { + Ok, + Err, +} + +#[cfg(test)] +mod tests { + use crate::prelude::*; + + #[test] + fn ok_refs_work() { + let mut result: Result = Ok(1); + expect!(&result, to_be_ok); + expect!(&mut result, to_be_ok); + expect!(result, to_be_ok); + + let mut result: Result<(), i32> = Err(1); + expect!(&result, not, to_be_ok); + expect!(&mut result, not, to_be_ok); + expect!(result, not, to_be_ok); + } + + #[test] + fn err_refs_work() { + let mut result: Result<(), i32> = Err(1); + expect!(&result, to_be_err); + expect!(&mut result, to_be_err); + expect!(result, to_be_err); + + let mut result: Result = Ok(1); + expect!(&result, not, to_be_err); + expect!(&mut result, not, to_be_err); + expect!(result, not, to_be_err); + } +} diff --git a/src/assertions/results/extensions.rs b/src/assertions/results/extensions.rs new file mode 100644 index 0000000..707fcda --- /dev/null +++ b/src/assertions/results/extensions.rs @@ -0,0 +1,102 @@ +use crate::assertions::AssertionBuilder; + +use super::{ErrAndModifier, OkAndModifier, ResultVariant, Resultish, ToBeResultVariantAssertion}; + +/// Assertions and modifiers for [`Result`]s. +pub trait ResultAssertions +where + T: Resultish, +{ + /// Asserts that the target holds a success, then continues the assertion with + /// the contained value. + /// + /// ``` + /// # use expecters::prelude::*; + /// let mut subject: Result = Ok(1); + /// expect!(subject, to_be_ok_and, to_equal(1)); + /// ``` + /// + /// The assertion fails if the result is [`Err`]: + /// + /// ```should_panic + /// # use expecters::prelude::*; + /// let subject: Result = Err("error"); + /// expect!(subject, to_be_ok_and, to_equal(1)); + /// ``` + fn to_be_ok_and(self) -> AssertionBuilder>; + + /// Asserts that the target holds an error, then continues the assertion with + /// the contained value. + /// + /// ``` + /// # use expecters::prelude::*; + /// let result: Result = Err("error"); + /// expect!(result, to_be_err_and, to_equal("error")); + /// ``` + /// + /// The assertion fails if the result is [`Ok`]: + /// + /// ```should_panic + /// # use expecters::prelude::*; + /// let result: Result = Ok(1); + /// expect!(result, to_be_err_and, to_equal("error")); + /// ``` + fn to_be_err_and(self) -> AssertionBuilder>; + + /// Asserts that the target holds a success. + /// + /// ``` + /// # use expecters::prelude::*; + /// let result: Result = Ok(1); + /// expect!(result, to_be_ok); + /// ``` + /// + /// The assertion fails if the subject does not hold a success: + /// + /// ```should_panic + /// # use expecters::prelude::*; + /// let result: Result = Err("error"); + /// expect!(result, to_be_ok); + /// ``` + #[inline] + #[must_use] + fn to_be_ok(&self) -> ToBeResultVariantAssertion { + ToBeResultVariantAssertion::new(ResultVariant::Ok) + } + + /// Asserts that the subject holds an error. + /// + /// ``` + /// # use expecters::prelude::*; + /// let result: Result = Err("error"); + /// expect!(result, to_be_err); + /// ``` + /// + /// The assertion fails if the subject does not hold an error: + /// + /// ```should_panic + /// # use expecters::prelude::*; + /// let result: Result = Ok(1); + /// expect!(result, to_be_err); + /// ``` + #[inline] + #[must_use] + fn to_be_err(&self) -> ToBeResultVariantAssertion { + ToBeResultVariantAssertion::new(ResultVariant::Err) + } +} + +impl ResultAssertions for AssertionBuilder +where + T: Resultish, +{ + #[inline] + fn to_be_ok_and(self) -> AssertionBuilder> { + AssertionBuilder::modify(self, OkAndModifier::new) + } + + #[inline] + fn to_be_err_and(self) -> AssertionBuilder> { + AssertionBuilder::modify(self, ErrAndModifier::new) + } +} diff --git a/src/assertions/results/modifiers.rs b/src/assertions/results/modifiers.rs new file mode 100644 index 0000000..71c6030 --- /dev/null +++ b/src/assertions/results/modifiers.rs @@ -0,0 +1,5 @@ +mod err_and; +mod ok_and; + +pub use err_and::*; +pub use ok_and::*; diff --git a/src/assertions/results/modifiers/err_and.rs b/src/assertions/results/modifiers/err_and.rs new file mode 100644 index 0000000..655b79d --- /dev/null +++ b/src/assertions/results/modifiers/err_and.rs @@ -0,0 +1,83 @@ +use crate::assertions::{ + general::IntoInitializableOutput, results::Resultish, Assertion, AssertionContext, + AssertionContextBuilder, AssertionModifier, +}; + +/// Maps the subject to its [`Err`] value. +#[derive(Clone, Debug)] +pub struct ErrAndModifier { + prev: M, +} + +impl ErrAndModifier { + #[inline] + pub(crate) fn new(prev: M) -> Self { + Self { prev } + } +} + +impl AssertionModifier for ErrAndModifier +where + M: AssertionModifier>, +{ + type Output = M::Output; + + #[inline] + fn apply(self, cx: AssertionContextBuilder, next: A) -> Self::Output { + self.prev.apply(cx, ErrAndAssertion { next }) + } +} + +/// Executes the inner assertion on the subject's [`Err`] value. +#[derive(Clone, Debug)] +pub struct ErrAndAssertion { + next: A, +} + +impl Assertion for ErrAndAssertion +where + A: Assertion, + R: Resultish, +{ + type Output = ::Initialized; + + #[inline] + fn execute(self, cx: AssertionContext, subject: R) -> Self::Output { + let Some(subject) = subject.err() else { + return cx.fail("received Ok"); + }; + self.next.execute(cx, subject).into_initialized() + } +} + +#[cfg(test)] +mod tests { + use crate::prelude::*; + + #[test] + fn refs_work() { + let mut result: Result<(), i32> = Err(1); + expect!(&result, to_be_err_and, to_satisfy(|&n| n == 1)); + expect!(&mut result, to_be_err_and, to_satisfy(|&mut n| n == 1)); + expect!(result, to_be_err_and, to_equal(1)); + + let mut result: Result<(), i32> = Ok(()); + expect!(&result, not, to_be_err_and, to_satisfy(|_| true)); + expect!(&mut result, not, to_be_err_and, to_satisfy(|_| true)); + expect!(result, not, to_be_err_and, to_satisfy(|_| true)); + } +} + +#[cfg(all(test, feature = "futures"))] +mod async_tests { + use std::future::ready; + + use crate::prelude::*; + + #[cfg(feature = "futures")] + #[tokio::test] + async fn nested_async_works() { + let result: Result<(), _> = Err(ready(1)); + expect!(result, to_be_err_and, when_ready, to_equal(1)).await; + } +} diff --git a/src/assertions/results/modifiers/ok_and.rs b/src/assertions/results/modifiers/ok_and.rs new file mode 100644 index 0000000..b6255f5 --- /dev/null +++ b/src/assertions/results/modifiers/ok_and.rs @@ -0,0 +1,82 @@ +use crate::assertions::{ + general::IntoInitializableOutput, results::Resultish, Assertion, AssertionContext, + AssertionContextBuilder, AssertionModifier, +}; + +/// Maps the subject to its [`Ok`] value. +#[derive(Clone, Debug)] +pub struct OkAndModifier { + prev: M, +} + +impl OkAndModifier { + #[inline] + pub(crate) fn new(prev: M) -> Self { + Self { prev } + } +} + +impl AssertionModifier for OkAndModifier +where + M: AssertionModifier>, +{ + type Output = M::Output; + + #[inline] + fn apply(self, cx: AssertionContextBuilder, next: A) -> Self::Output { + self.prev.apply(cx, OkAndAssertion { next }) + } +} + +/// Executes the inner assertion on the subject's [`Ok`] value. +#[derive(Clone, Debug)] +pub struct OkAndAssertion { + next: A, +} + +impl Assertion for OkAndAssertion +where + A: Assertion, + R: Resultish, +{ + type Output = ::Initialized; + + #[inline] + fn execute(self, cx: AssertionContext, subject: R) -> Self::Output { + let Some(subject) = subject.ok() else { + return cx.fail("received Err"); + }; + self.next.execute(cx, subject).into_initialized() + } +} + +#[cfg(test)] +mod tests { + use crate::prelude::*; + + #[test] + fn refs_work() { + let mut result: Result = Ok(1); + expect!(&result, to_be_ok_and, to_satisfy(|&n| n == 1)); + expect!(&mut result, to_be_ok_and, to_satisfy(|&mut n| n == 1)); + expect!(result, to_be_ok_and, to_equal(1)); + + let mut result: Result = Err(()); + expect!(&result, not, to_be_ok_and, to_satisfy(|_| true)); + expect!(&mut result, not, to_be_ok_and, to_satisfy(|_| true)); + expect!(result, not, to_be_ok_and, to_satisfy(|_| true)); + } +} + +#[cfg(all(test, feature = "futures"))] +mod async_tests { + use std::future::ready; + + use crate::prelude::*; + + #[tokio::test] + async fn nested_async_works() { + let result: Result<_, ()> = Ok(ready(1)); + expect!(result, to_be_ok_and, when_ready, to_equal(1)).await; + } +} diff --git a/src/assertions/results/resultish.rs b/src/assertions/results/resultish.rs new file mode 100644 index 0000000..d87e0ac --- /dev/null +++ b/src/assertions/results/resultish.rs @@ -0,0 +1,77 @@ +mod sealed { + pub trait Sealed { + type T; + type E; + + type OutT; + type OutE; + + fn ok(self) -> Option; + fn err(self) -> Option; + } + + impl Sealed for Result { + type T = T; + type E = E; + + type OutT = T; + type OutE = E; + + #[inline] + fn ok(self) -> Option { + self.ok() + } + + #[inline] + fn err(self) -> Option { + self.err() + } + } + + impl<'a, T, E> Sealed for &'a Result { + type T = T; + type E = E; + + type OutT = &'a T; + type OutE = &'a E; + + #[inline] + fn ok(self) -> Option { + self.as_ref().ok() + } + + #[inline] + fn err(self) -> Option { + self.as_ref().err() + } + } + + impl<'a, T, E> Sealed for &'a mut Result { + type T = T; + type E = E; + + type OutT = &'a mut T; + type OutE = &'a mut E; + + #[inline] + fn ok(self) -> Option { + self.as_mut().ok() + } + + #[inline] + fn err(self) -> Option { + self.as_mut().err() + } + } +} + +/// Helper trait for mapping [`Result`] and its references to its +/// component values and types. +/// +/// This is implemented for: +/// - `Result` +/// - `&Result` +/// - `&mut Result` +pub trait Resultish: sealed::Sealed {} + +impl Resultish for R where R: sealed::Sealed {} diff --git a/src/assertions/strings.rs b/src/assertions/strings.rs new file mode 100644 index 0000000..ddb4bc5 --- /dev/null +++ b/src/assertions/strings.rs @@ -0,0 +1,9 @@ +//! Assertions and modifiers for tests that involve strings. + +mod assertions; +mod extensions; +mod modifiers; + +pub use assertions::*; +pub use extensions::*; +pub use modifiers::*; diff --git a/src/assertions/strings/assertions.rs b/src/assertions/strings/assertions.rs new file mode 100644 index 0000000..37594c3 --- /dev/null +++ b/src/assertions/strings/assertions.rs @@ -0,0 +1,7 @@ +mod to_contain_substr; +#[cfg(feature = "regex")] +mod to_match_regex; + +pub use to_contain_substr::*; +#[cfg(feature = "regex")] +pub use to_match_regex::*; diff --git a/src/assertions/strings/assertions/to_contain_substr.rs b/src/assertions/strings/assertions/to_contain_substr.rs new file mode 100644 index 0000000..63f0e76 --- /dev/null +++ b/src/assertions/strings/assertions/to_contain_substr.rs @@ -0,0 +1,47 @@ +use crate::{ + assertions::{Assertion, AssertionContext}, + metadata::Annotated, + AssertionOutput, +}; + +/// Asserts that the subject contains the given substring. +#[derive(Clone, Debug)] +pub struct ToContainSubstr

{ + pattern: Annotated

, + location: ContainsLocation, +} + +impl

ToContainSubstr

{ + #[inline] + pub(crate) fn new(pattern: Annotated

, location: ContainsLocation) -> Self { + Self { pattern, location } + } +} + +impl Assertion for ToContainSubstr

+where + P: AsRef, + T: AsRef, +{ + type Output = AssertionOutput; + + fn execute(self, mut cx: AssertionContext, subject: T) -> Self::Output { + let pattern = self.pattern.inner().as_ref(); + cx.annotate("expected", format_args!("{pattern:?}")); + + let subject = subject.as_ref(); + let found = match self.location { + ContainsLocation::Anywhere => subject.contains(pattern), + ContainsLocation::Start => subject.starts_with(pattern), + ContainsLocation::End => subject.ends_with(pattern), + }; + cx.pass_if(found, "substring not found") + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(crate) enum ContainsLocation { + Anywhere, + Start, + End, +} diff --git a/src/assertions/strings/assertions/to_match_regex.rs b/src/assertions/strings/assertions/to_match_regex.rs new file mode 100644 index 0000000..2e04b4a --- /dev/null +++ b/src/assertions/strings/assertions/to_match_regex.rs @@ -0,0 +1,39 @@ +use std::sync::Arc; + +use regex::Regex; + +use crate::{ + assertions::{Assertion, AssertionContext}, + AssertionOutput, +}; + +/// Asserts that the subject matches a regular expression. +#[derive(Clone, Debug)] +pub struct ToMatchRegexAssertion { + regex: Arc, +} + +impl ToMatchRegexAssertion { + #[inline] + pub(crate) fn new(pattern: &str) -> Self { + let regex = Regex::new(pattern).expect("invalid regex"); + Self { + regex: Arc::new(regex), + } + } +} + +impl Assertion for ToMatchRegexAssertion +where + T: AsRef, +{ + type Output = AssertionOutput; + + fn execute(self, mut cx: AssertionContext, subject: T) -> Self::Output { + cx.annotate("pattern", self.regex.as_str()); + cx.pass_if( + self.regex.is_match(subject.as_ref()), + "didn't match pattern", + ) + } +} diff --git a/src/assertions/strings/extensions.rs b/src/assertions/strings/extensions.rs new file mode 100644 index 0000000..eba64d3 --- /dev/null +++ b/src/assertions/strings/extensions.rs @@ -0,0 +1,178 @@ +use std::fmt::{Debug, Display}; + +use crate::{assertions::AssertionBuilder, metadata::Annotated}; + +use super::{AsDebugModifier, AsDisplayModifier, CharsModifier, ContainsLocation, ToContainSubstr}; + +/// Assertions and modifiers for [`String`]s. +pub trait StringAssertions +where + T: AsRef, +{ + /// Converts a string to its characters (collected into a [`Vec`]). + /// + /// ``` + /// # use expecters::prelude::*; + /// expect!("Hello, world!", chars, any, to_equal(',')); + /// ``` + fn chars(self) -> AssertionBuilder, CharsModifier>; + + /// Asserts that the subject contains the given substring. + /// + /// ``` + /// # use expecters::prelude::*; + /// expect!("Hello, world!", to_contain_substr("world")); + /// ``` + /// + /// The assertion fails if the subject does not contain the substring: + /// + /// ```should_panic + /// # use expecters::prelude::*; + /// // not case-insensitive + /// expect!("Hello, world!", to_contain_substr("WORLD")); + /// ``` + #[inline] + #[must_use] + fn to_contain_substr

(&self, pattern: Annotated

) -> ToContainSubstr

+ where + P: AsRef, + { + ToContainSubstr::new(pattern, ContainsLocation::Anywhere) + } + + /// Asserts that the subject starts with the given substring. + /// + /// ``` + /// # use expecters::prelude::*; + /// expect!("Hello, world!", to_start_with("Hello")); + /// ``` + /// + /// The assertion fails if the subject does not start with the substring: + /// + /// ```should_panic + /// # use expecters::prelude::*; + /// expect!("Hello, world!", to_start_with("world!")); + /// ``` + #[inline] + #[must_use] + fn to_start_with

(&self, pattern: Annotated

) -> ToContainSubstr

+ where + P: AsRef, + { + ToContainSubstr::new(pattern, ContainsLocation::Start) + } + + /// Asserts that the subject ends with the given substring. + /// + /// ``` + /// # use expecters::prelude::*; + /// expect!("Hello, world!", to_end_with("world!")); + /// ``` + /// + /// The assertion fails if the subject does not end with the substring: + /// + /// ```should_panic + /// # use expecters::prelude::*; + /// expect!("Hello, world!", to_end_with("Hello")); + /// ``` + #[inline] + #[must_use] + fn to_end_with

(&self, pattern: Annotated

) -> ToContainSubstr

+ where + P: AsRef, + { + ToContainSubstr::new(pattern, ContainsLocation::End) + } + + /// Asserts that the subject matches the given regular expression. + /// + /// ``` + /// # use expecters::prelude::*; + /// expect!("12345", to_match_regex(r"\d+")); + /// ``` + /// + /// The assertion fails if the subject does not match the pattern: + /// + /// ```should_panic + /// # use expecters::prelude::*; + /// expect!("abcde", to_match_regex(r"\d+")); + /// ``` + /// + /// ## Panics + /// + /// This panics immediately, without executing the assertion, if the provided + /// pattern is an invalid regular expression. + #[inline] + #[must_use] + #[cfg(feature = "regex")] + fn to_match_regex

(&self, pattern: Annotated

) -> super::ToMatchRegexAssertion + where + P: AsRef, + { + super::ToMatchRegexAssertion::new(pattern.inner().as_ref()) + } +} + +impl StringAssertions for AssertionBuilder +where + T: AsRef, +{ + #[inline] + fn chars(self) -> AssertionBuilder, CharsModifier> { + AssertionBuilder::modify(self, CharsModifier::new) + } +} + +/// Assertions and modifiers for types with a [`Debug`] representation. +pub trait DebugAssertions +where + T: Debug, +{ + /// Converts a value to its [`Debug`] representation. + /// + /// ``` + /// # use expecters::prelude::*; + /// expect!("hello", as_debug, to_equal(r#""hello""#)); + /// ``` + #[allow(clippy::wrong_self_convention)] + fn as_debug(self) -> AssertionBuilder> + where + T: Debug; +} + +impl DebugAssertions for AssertionBuilder +where + T: Debug, +{ + #[inline] + fn as_debug(self) -> AssertionBuilder> { + AssertionBuilder::modify(self, AsDebugModifier::new) + } +} + +/// Assertions and modifiers for types with a [`Display`] representation. +pub trait DisplayAssertions +where + T: Display, +{ + /// Converts a value to its [`Display`] representation. + /// + /// ``` + /// # use expecters::prelude::*; + /// expect!(1, as_display, to_equal("1")); + /// ``` + #[allow(clippy::wrong_self_convention)] + fn as_display(self) -> AssertionBuilder> + where + T: Display; +} + +impl DisplayAssertions for AssertionBuilder +where + T: Display, +{ + #[inline] + fn as_display(self) -> AssertionBuilder> { + AssertionBuilder::modify(self, AsDisplayModifier::new) + } +} diff --git a/src/assertions/strings/modifiers.rs b/src/assertions/strings/modifiers.rs new file mode 100644 index 0000000..1fb31f0 --- /dev/null +++ b/src/assertions/strings/modifiers.rs @@ -0,0 +1,7 @@ +mod chars; +mod debug; +mod display; + +pub use chars::*; +pub use debug::*; +pub use display::*; diff --git a/src/assertions/strings/modifiers/chars.rs b/src/assertions/strings/modifiers/chars.rs new file mode 100644 index 0000000..01ed4c9 --- /dev/null +++ b/src/assertions/strings/modifiers/chars.rs @@ -0,0 +1,45 @@ +use crate::assertions::{Assertion, AssertionContext, AssertionContextBuilder, AssertionModifier}; + +/// Converts the subject to its characters. +#[derive(Clone, Debug)] +pub struct CharsModifier { + prev: M, +} + +impl CharsModifier { + #[inline] + pub(crate) fn new(prev: M) -> Self { + CharsModifier { prev } + } +} + +impl AssertionModifier for CharsModifier +where + M: AssertionModifier>, +{ + type Output = M::Output; + + #[inline] + fn apply(self, cx: AssertionContextBuilder, next: A) -> Self::Output { + self.prev.apply(cx, CharsAssertion { next }) + } +} + +/// Executes the inner assertion with the characters in the subject. +#[derive(Clone, Debug)] +pub struct CharsAssertion { + next: A, +} + +impl Assertion for CharsAssertion +where + A: Assertion>, + T: AsRef, +{ + type Output = A::Output; + + #[inline] + fn execute(self, cx: AssertionContext, subject: T) -> Self::Output { + self.next.execute(cx, subject.as_ref().chars().collect()) + } +} diff --git a/src/assertions/strings/modifiers/debug.rs b/src/assertions/strings/modifiers/debug.rs new file mode 100644 index 0000000..6cfacb1 --- /dev/null +++ b/src/assertions/strings/modifiers/debug.rs @@ -0,0 +1,48 @@ +use std::fmt::Debug; + +use crate::assertions::{Assertion, AssertionContext, AssertionContextBuilder, AssertionModifier}; + +/// Extracts the [`Debug`] representation of the subject. +#[derive(Clone, Debug)] +pub struct AsDebugModifier { + prev: M, +} + +impl AsDebugModifier { + #[inline] + pub(crate) fn new(prev: M) -> Self { + Self { prev } + } +} + +impl AssertionModifier for AsDebugModifier +where + M: AssertionModifier>, +{ + type Output = M::Output; + + #[inline] + fn apply(self, cx: AssertionContextBuilder, next: A) -> Self::Output { + self.prev.apply(cx, AsDebugAssertion { next }) + } +} + +/// Executes the inner assertion with the [`Debug`] representation of the +/// subject. +#[derive(Clone, Debug)] +pub struct AsDebugAssertion { + next: A, +} + +impl Assertion for AsDebugAssertion +where + A: Assertion, + T: Debug, +{ + type Output = A::Output; + + #[inline] + fn execute(self, cx: AssertionContext, subject: T) -> Self::Output { + self.next.execute(cx, format!("{subject:?}")) + } +} diff --git a/src/assertions/strings/modifiers/display.rs b/src/assertions/strings/modifiers/display.rs new file mode 100644 index 0000000..67d6b97 --- /dev/null +++ b/src/assertions/strings/modifiers/display.rs @@ -0,0 +1,48 @@ +use std::fmt::Display; + +use crate::assertions::{Assertion, AssertionContext, AssertionContextBuilder, AssertionModifier}; + +/// Extracts the [`Display`] representation of the subject. +#[derive(Clone, Debug)] +pub struct AsDisplayModifier { + prev: M, +} + +impl AsDisplayModifier { + #[inline] + pub(crate) fn new(prev: M) -> Self { + Self { prev } + } +} + +impl AssertionModifier for AsDisplayModifier +where + M: AssertionModifier>, +{ + type Output = M::Output; + + #[inline] + fn apply(self, cx: AssertionContextBuilder, next: A) -> Self::Output { + self.prev.apply(cx, AsDisplayAssertion { next }) + } +} + +/// Executes the inner assertion with the [`Display`] representation of the +/// subject. +#[derive(Clone, Debug)] +pub struct AsDisplayAssertion { + next: A, +} + +impl Assertion for AsDisplayAssertion +where + A: Assertion, + T: Display, +{ + type Output = A::Output; + + #[inline] + fn execute(self, cx: AssertionContext, subject: T) -> Self::Output { + self.next.execute(cx, subject.to_string()) + } +} diff --git a/src/combinators.rs b/src/combinators.rs deleted file mode 100644 index c99d36c..0000000 --- a/src/combinators.rs +++ /dev/null @@ -1,31 +0,0 @@ -//! This module contains the built-in combinators that can be used to build more -//! complex assertions. -//! -//! For more information on how to use these combinators, see the documentation -//! for the [`Assertable`](crate::Assertable) trait. - -mod all; -mod any; -mod at_path; -mod count; -mod err; -mod map; -mod not; -mod nth; -mod ok; -mod some; -mod when_called; -// mod when_ready; - -pub use all::*; -pub use any::*; -pub use at_path::*; -pub use count::*; -pub use err::*; -pub use map::*; -pub use not::*; -pub use nth::*; -pub use ok::*; -pub use some::*; -pub use when_called::*; -// pub use when_ready::*; diff --git a/src/combinators/all.rs b/src/combinators/all.rs deleted file mode 100644 index cd7a210..0000000 --- a/src/combinators/all.rs +++ /dev/null @@ -1,41 +0,0 @@ -use std::fmt::Display; - -use crate::Assertable; - -/// Wraps an [`Assertable`] and applies the assertion to each element in the -/// target. If there exists an element that fails the chained assertion, then -/// then the whole assertion fails. -/// -/// This is similar to [`AnyCombinator`](crate::combinators::AnyCombinator), -/// but every element needs to satisfy the expectation. -#[derive(Clone, Debug)] -pub struct AllCombinator { - inner: Inner, -} - -impl AllCombinator { - /// Creates a new combinator which wraps an inner [`Assertable`]. - #[inline] - pub fn new(inner: Inner) -> Self { - Self { inner } - } -} - -impl Assertable for AllCombinator -where - Inner: Assertable, - Inner::Target: IntoIterator, -{ - type Target = ::Item; - type Result = Inner::Result; - - fn to_satisfy(self, expectation: impl Display, mut f: F) -> Self::Result - where - F: FnMut(Self::Target) -> bool, - { - self.inner.to_satisfy( - format_args!("for each inner value, {expectation}"), - |values| values.into_iter().all(|value| f(value)), - ) - } -} diff --git a/src/combinators/any.rs b/src/combinators/any.rs deleted file mode 100644 index 6d9eb54..0000000 --- a/src/combinators/any.rs +++ /dev/null @@ -1,41 +0,0 @@ -use std::fmt::Display; - -use crate::Assertable; - -/// Wraps an [`Assertable`] and applies the assertion to each element in the -/// target. If there does not exist an element that satisfies the chained -/// assertion, then the whole assertion fails. -/// -/// This is similar to [`AllCombinator`](crate::combinators::AllCombinator), -/// but only one element needs to satisfy the expectation. -#[derive(Clone, Debug)] -pub struct AnyCombinator { - inner: Inner, -} - -impl AnyCombinator { - /// Creates a new combinator which wraps an inner [`Assertable`]. - #[inline] - pub fn new(inner: Inner) -> Self { - Self { inner } - } -} - -impl Assertable for AnyCombinator -where - Inner: Assertable, - Inner::Target: IntoIterator, -{ - type Target = ::Item; - type Result = Inner::Result; - - fn to_satisfy(self, expectation: impl Display, mut f: F) -> Self::Result - where - F: FnMut(Self::Target) -> bool, - { - self.inner.to_satisfy( - format_args!("for some inner value, {expectation}"), - |values| values.into_iter().any(|value| f(value)), - ) - } -} diff --git a/src/combinators/at_path.rs b/src/combinators/at_path.rs deleted file mode 100644 index d59eeb1..0000000 --- a/src/combinators/at_path.rs +++ /dev/null @@ -1,284 +0,0 @@ -use std::fmt::Display; - -use crate::Assertable; - -/// Wraps an [`Assertable`] and applies an assertion to a sub-path within the -/// target value. -pub struct AtPath -where - Inner: Assertable, -{ - inner: Inner, - traversal: Traversal, -} - -impl AtPath -where - Inner: Assertable, -{ - /// Creates a new combinator which wraps an inner [`Assertable`]. - pub fn new(inner: Inner, traversal: Traversal) -> Self { - Self { inner, traversal } - } -} - -impl Assertable for AtPath -where - Inner: Assertable, -{ - type Target = T; - type Result = Inner::Result; - - fn to_satisfy(self, expectation: impl Display, mut f: F) -> Self::Result - where - F: FnMut(Self::Target) -> bool, - { - self.inner.to_satisfy( - format_args!( - "for the value at path '{}', {}", - self.traversal.path, expectation - ), - |outer| (self.traversal.f)(outer).is_some_and(&mut f), - ) - } -} - -/// Creates a new [`Traversal`] that navigates to a specific path within a target -/// value. -/// -/// The path may contain any number of segments, each prefixed by a period. For -/// example, the path `.foo.bar.baz` would navigate from the value's `foo` field -/// down all the way to the `baz` field. -/// -/// In addition to navigating to deeply nested fields, this macro also handles -/// fallible values along the way. By putting a question mark after a segment, -/// the traversal will automatically handle the fallible path. -/// -/// In addition to the above, more types of traversals are supported. The -/// following is a list of different kinds of traversals that can be performed: -/// -/// - Field access: `.field` -/// - Method call: `.method(args...)` -/// - Indexing: `[index]` (see below) -/// - Unwrapping: `?` -/// - This is primarily used to naviate to the inner value of a `Result` or -/// `Option`, but can be used for other types as well. This converts the -/// value to an iterator and takes the first element, if it exists. -/// - Function call (if not a method call): `(args...)` -/// - Pattern matching: `pattern => path` -/// - This is only supported at the top-level, meaning the full path needs to -/// start with the pattern, followed by a fat arrow, followed by an ident, -/// followed by any of the other traversals. For example, this is a valid -/// path: `Some(n) => n?.0`. -/// -/// By chaining these traversals together, you can navigate to just about any -/// deeply nested path within a target value. For example, a traversal within -/// a list of lists of tuples could look like `.field[1][4].3?[0].2`. -/// -/// ``` -/// # use expecters::prelude::*; -/// struct Foo { -/// bar: Option, -/// } -/// -/// struct Bar(Vec); -/// -/// let value = Some(Foo { -/// bar: Some(Bar(vec![1, 2])), -/// }); -/// expect!(value).at_path(path!(Some(foo) => foo.bar?.0[1])).to_equal(2); -/// ``` -/// -/// ## Indexing -/// -/// Indexing is a unique case during path traversal which uses a form of -/// specialization to handle different kinds of types. As a base case, indexing -/// is supported for all types which can be indexed with a particular key, and -/// where the returned value implements [`Clone`]. This base case panics if the -/// index is out of bounds. -/// -/// However, while the base case should cover many common cases, there are some -/// specializations which can "safely" index into a value and avoid the default -/// panic behavior. This lets `expecters` create its own custom errors for -/// indexing failures, which can be more informative than a panic. -/// -/// In the order they are checked, the following specializations are supported: -/// -/// 1. `HashMap where V: Clone` - You can index this type like normal, -/// and a clone of the value will be returned if it exists. -/// 2. `T: IntoIterator` - If the index is a `usize`, the value will be indexed -/// by converting it to an iterator and taking the element at that index. -/// This relaxes the requirement that the value must be `Clone` since it -/// consumes the container directly. -/// 3. `T: Index where T::Output: Clone` (base case) - This indexes the -/// container like normal, but uses the default panicking behavior if the -/// index is out of bounds. -/// -/// If the [`Clone`] bound is too restrictive, consider using one of the other -/// combinators to navigate into the value (like -/// [`map`](crate::Assertable::map)). -#[macro_export] -macro_rules! path { - ($($path:tt)*) => { - $crate::combinators::Traversal::new( - ::std::stringify!($($path)*), - Box::new(|value| $crate::path_inner!(@traverse value, $($path)*)), - ) - }; -} - -#[macro_export] -#[doc(hidden)] -macro_rules! path_inner { - // Base case - (@traverse $value:expr,) => { - ::core::option::Option::Some($value) - }; - - // Pattern - (@traverse $value:expr, $pattern:pat => $path:ident $($rest:tt)*) => { - match $value { - $pattern => $crate::path_inner!(@traverse $path, $($rest)*), - - #[allow(unreachable_patterns)] - _ => ::core::option::Option::None, - } - }; - - // Method call - (@traverse $value:expr, .$path:ident ($($args:tt)*) $($rest:tt)*) => { - $crate::path_inner!(@traverse $value.$path($($args)*), $($rest)*) - }; - - // Simple path traversal - (@traverse $value:expr, .$path:tt $($rest:tt)*) => { - $crate::path_inner!(@traverse $value.$path, $($rest)*) - }; - - // Fallible traversal - (@traverse $value:expr, ? $($rest:tt)*) => {{ - let mut iterator = ::core::iter::IntoIterator::into_iter($value); - let value = ::core::iter::Iterator::next(&mut iterator)?; - $crate::path_inner!(@traverse value, $($rest)*) - }}; - - // Indexing traversal - (@traverse $value:expr, [$index:expr] $($rest:tt)*) => {{ - #[allow(unused_imports)] - use $crate::specialization::at_path::kinds::*; - - let index = $index; - let value = $value; - let wrapper = $crate::specialization::at_path::Wrapper(&index, &value); - let getter = (&&&wrapper).__expecters_try_index(); - let value = getter(value, index)?; - $crate::path_inner!(@traverse value, $($rest)*) - }}; - - // Function call - (@traverse $value:expr, ($($args:tt)*) $($rest:tt)*) => {{ - let value = $value($($args)*); - $crate::path_inner!(@traverse value, $($rest)*) - }}; -} - -/// A traversal to a specific path within a target value. -/// -/// This type is created using the [`path!`] macro. -pub struct Traversal { - path: &'static str, - f: Box Option>, -} - -impl Traversal { - #[doc(hidden)] - pub fn new(path: &'static str, f: Box Option>) -> Self { - Self { path, f } - } - - /// Applies the traversal to a target value. If the traversal fails at any - /// point, this method will return `None`. - #[inline] - pub fn apply(self, value: T) -> Option { - (self.f)(value) - } -} - -#[cfg(test)] -mod tests { - use std::collections::HashMap; - - use crate::expect; - - use super::*; - - #[derive(Clone, Default)] - struct Foo { - bar: Bar, - opt_bar: Option, - } - - #[derive(Clone, Default)] - struct Bar { - baz: i32, - opt_baz: Option, - } - - struct A(pub T); - - #[test] - fn traversal() { - expect!(Foo::default()).at_path(path!(.bar.baz)).to_equal(0); - expect!((1, 2, 3)).at_path(path!(.2)).to_equal(3); - } - - #[test] - fn fallible() { - expect!(Foo::default()) - .not() - .at_path(path!(.opt_bar?.opt_baz?)) - .to_equal(0); - } - - #[test] - fn indexing() { - expect!([A(1), A(2), A(3)]) - .at_path(path!([1].0)) - .to_equal(2); - expect!([A(A(1))]).at_path(path!([0].0 .0)).to_equal(1); - expect!({ - let mut map = HashMap::new(); - map.insert("a".to_string(), 1); - map.insert("b".to_string(), 2); - map - }) - .at_path(path!(["b"])) - .to_equal(2); - - expect!(vec![1, 2, 3]) - .not() - .at_path(path!([3])) - .to_be_greater_than(0); - expect!([1, 2, 3]) - .not() - .at_path(path!([3])) - .to_be_greater_than(0); - expect!({ - let mut map = HashMap::new(); - map.insert("a".to_string(), 1); - map.insert("b".to_string(), 2); - map - }) - .not() - .at_path(path!(["c"])) - .to_equal(2); - } - - #[test] - fn patterns() { - expect!(Some(1)) - .at_path(path!(Some(n) => n.to_string())) - .to_equal("1"); - expect!(A(1)).at_path(path!(A(n) => n)).to_equal(1); - } -} diff --git a/src/combinators/count.rs b/src/combinators/count.rs deleted file mode 100644 index 8e7c396..0000000 --- a/src/combinators/count.rs +++ /dev/null @@ -1,37 +0,0 @@ -use std::fmt::Display; - -use crate::Assertable; - -/// Wraps an [`Assertable`] and performs an assertion on the number of elements -/// in the target. -#[derive(Clone, Debug)] -pub struct CountCombinator { - inner: Inner, -} - -impl CountCombinator { - /// Creates a new combinator which wraps an inner [`Assertable`]. - #[inline] - pub fn new(inner: Inner) -> Self { - Self { inner } - } -} - -impl Assertable for CountCombinator -where - Inner: Assertable, - ::Target: IntoIterator, -{ - type Target = usize; - type Result = Inner::Result; - - fn to_satisfy(self, expectation: impl Display, mut f: F) -> Self::Result - where - F: FnMut(Self::Target) -> bool, - { - self.inner.to_satisfy( - format_args!("the length satisfies: {expectation}"), - |values| f(values.into_iter().count()), - ) - } -} diff --git a/src/combinators/err.rs b/src/combinators/err.rs deleted file mode 100644 index cb2c5ea..0000000 --- a/src/combinators/err.rs +++ /dev/null @@ -1,37 +0,0 @@ -use std::fmt::Display; - -use crate::Assertable; - -/// Wraps an [`Assertable`] and applies the assertion to the error value -/// contained within the target. If the target is [`Ok`], then the assertion -/// fails instead. -#[derive(Clone, Debug)] -pub struct ErrCombinator { - inner: Inner, -} - -impl ErrCombinator { - /// Creates a new combinator which wraps an inner [`Assertable`]. - #[inline] - pub fn new(inner: Inner) -> Self { - Self { inner } - } -} - -impl Assertable for ErrCombinator -where - Inner: Assertable>, -{ - type Target = E; - type Result = Inner::Result; - - fn to_satisfy(self, expectation: impl Display, mut f: F) -> Self::Result - where - F: FnMut(Self::Target) -> bool, - { - self.inner.to_satisfy( - format_args!("value is `Err`, and inner value satisfies: {expectation}"), - |value| value.is_err_and(&mut f), - ) - } -} diff --git a/src/combinators/map.rs b/src/combinators/map.rs deleted file mode 100644 index 79161cb..0000000 --- a/src/combinators/map.rs +++ /dev/null @@ -1,40 +0,0 @@ -use std::fmt::Display; - -use crate::Assertable; - -/// Wraps an [`Assertable`] and applies the assertion to a target derived from -/// the inner expectation's target. In other words, this maps the target to a -/// new value, then applys the assertion to the new value. -#[derive(Clone, Debug)] -#[must_use = "a combinator does nothing without an assertion"] -pub struct MapCombinator { - inner: Inner, - map: M, -} - -impl MapCombinator { - /// Creates a new combinator which wraps an inner [`Assertable`]. - #[inline] - pub fn new(inner: Inner, map: M) -> Self { - Self { inner, map } - } -} - -impl Assertable for MapCombinator -where - Inner: Assertable, - M: FnMut(Inner::Target) -> T, -{ - type Target = T; - type Result = Inner::Result; - - fn to_satisfy(mut self, expectation: impl Display, mut f: F) -> Self::Result - where - F: FnMut(Self::Target) -> bool, - { - self.inner.to_satisfy( - format_args!("for the mapped value, {expectation}"), - |value| f((self.map)(value)), - ) - } -} diff --git a/src/combinators/not.rs b/src/combinators/not.rs deleted file mode 100644 index dfe22aa..0000000 --- a/src/combinators/not.rs +++ /dev/null @@ -1,36 +0,0 @@ -use std::fmt::Display; - -use crate::Assertable; - -/// Wraps an [`Assertable`] and negates the expectation. The overall assertion -/// succeeds if and only if the chained assertion fails. -#[derive(Clone, Debug)] -pub struct NotCombinator { - inner: Inner, -} - -impl NotCombinator { - /// Creates a new combinator which wraps an inner [`Assertable`]. - #[inline] - pub fn new(inner: Inner) -> Self { - Self { inner } - } -} - -impl Assertable for NotCombinator -where - Inner: Assertable, -{ - type Target = Inner::Target; - type Result = Inner::Result; - - fn to_satisfy(self, expectation: impl Display, mut f: F) -> Self::Result - where - F: FnMut(Self::Target) -> bool, - { - self.inner.to_satisfy( - format_args!("the following is not satisfied: {expectation}"), - |value| !f(value), - ) - } -} diff --git a/src/combinators/nth.rs b/src/combinators/nth.rs deleted file mode 100644 index 22ada4e..0000000 --- a/src/combinators/nth.rs +++ /dev/null @@ -1,38 +0,0 @@ -use std::fmt::Display; - -use crate::Assertable; - -/// Wraps an [`Assertable`] and performs an assertion on a specific element in -/// the target. -#[derive(Clone, Debug)] -pub struct NthCombinator { - inner: Inner, - n: usize, -} - -impl NthCombinator { - /// Creates a new combinator which wraps an inner [`Assertable`]. - #[inline] - pub fn new(inner: Inner, n: usize) -> Self { - Self { inner, n } - } -} - -impl Assertable for NthCombinator -where - Inner: Assertable, - ::Target: IntoIterator, -{ - type Target = ::Item; - type Result = Inner::Result; - - fn to_satisfy(self, expectation: impl Display, mut f: F) -> Self::Result - where - F: FnMut(Self::Target) -> bool, - { - self.inner.to_satisfy( - format_args!("element {} exists and satisfies: {}", self.n, expectation), - |values| values.into_iter().nth(self.n).is_some_and(&mut f), - ) - } -} diff --git a/src/combinators/ok.rs b/src/combinators/ok.rs deleted file mode 100644 index 3c8b875..0000000 --- a/src/combinators/ok.rs +++ /dev/null @@ -1,37 +0,0 @@ -use std::fmt::Display; - -use crate::Assertable; - -/// Wraps an [`Assertable`] and applies the assertion to the inner value -/// contained within the target. If the target is [`Err`], then the assertion -/// fails instead. -#[derive(Clone, Debug)] -pub struct OkCombinator { - inner: Inner, -} - -impl OkCombinator { - /// Creates a new combinator which wraps an inner [`Assertable`]. - #[inline] - pub fn new(inner: Inner) -> Self { - Self { inner } - } -} - -impl Assertable for OkCombinator -where - Inner: Assertable>, -{ - type Target = T; - type Result = Inner::Result; - - fn to_satisfy(self, expectation: impl Display, mut f: F) -> Self::Result - where - F: FnMut(Self::Target) -> bool, - { - self.inner.to_satisfy( - format_args!("value is `Ok`, and inner value satisfies: {expectation}"), - |value| value.is_ok_and(&mut f), - ) - } -} diff --git a/src/combinators/some.rs b/src/combinators/some.rs deleted file mode 100644 index 22341ac..0000000 --- a/src/combinators/some.rs +++ /dev/null @@ -1,37 +0,0 @@ -use std::fmt::Display; - -use crate::Assertable; - -/// Wraps an [`Assertable`] and applies the assertion to the inner value -/// contained within the target. If the target is [`None`], then the -/// assertion fails instead. -#[derive(Clone, Debug)] -pub struct SomeCombinator { - inner: Inner, -} - -impl SomeCombinator { - /// Creates a new combinator which wraps an inner [`Assertable`]. - #[inline] - pub fn new(inner: Inner) -> Self { - Self { inner } - } -} - -impl Assertable for SomeCombinator -where - Inner: Assertable>, -{ - type Target = T; - type Result = Inner::Result; - - fn to_satisfy(self, expectation: impl Display, mut f: F) -> Self::Result - where - F: FnMut(Self::Target) -> bool, - { - self.inner.to_satisfy( - format_args!("value is `Some`, and inner value satisfies: {expectation}"), - |value| value.is_some_and(&mut f), - ) - } -} diff --git a/src/combinators/when_called.rs b/src/combinators/when_called.rs deleted file mode 100644 index 33a5a7f..0000000 --- a/src/combinators/when_called.rs +++ /dev/null @@ -1,59 +0,0 @@ -use std::fmt::Display; - -use crate::Assertable; - -/// Wraps an [`Assertable`] and applies the assertion to the inner function's -/// return value when called with the given arguments. -pub struct WhenCalledCombinator { - inner: Inner, - args: Args, -} - -impl WhenCalledCombinator { - /// Creates a new combinator which wraps an inner [`Assertable`]. - #[inline] - pub fn new(inner: Inner, args: Args) -> Self { - Self { inner, args } - } -} - -macro_rules! impl_when_called_combinator { - ($($arg:ident),*) => { - impl Assertable for WhenCalledCombinator - where - Inner: Assertable, - Inner::Target: FnOnce($($arg),*) -> R, - ($($arg,)*): Clone, - { - type Target = R; - type Result = Inner::Result; - - fn to_satisfy(self, expectation: impl Display, mut f: F) -> Self::Result - where - F: FnMut(Self::Target) -> bool, - { - self.inner - .to_satisfy(format_args!("when called, {expectation}"), |value| { - #[allow(non_snake_case)] - let ($($arg,)*) = self.args.clone(); - let result = value($($arg),*); - f(result) - }) - } - } - }; -} - -impl_when_called_combinator!(); -impl_when_called_combinator!(A1); -impl_when_called_combinator!(A1, A2); -impl_when_called_combinator!(A1, A2, A3); -impl_when_called_combinator!(A1, A2, A3, A4); -impl_when_called_combinator!(A1, A2, A3, A4, A5); -impl_when_called_combinator!(A1, A2, A3, A4, A5, A6); -impl_when_called_combinator!(A1, A2, A3, A4, A5, A6, A7); -impl_when_called_combinator!(A1, A2, A3, A4, A5, A6, A7, A8); -impl_when_called_combinator!(A1, A2, A3, A4, A5, A6, A7, A8, A9); -impl_when_called_combinator!(A1, A2, A3, A4, A5, A6, A7, A8, A9, A10); -impl_when_called_combinator!(A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11); -impl_when_called_combinator!(A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12); diff --git a/src/combinators/when_ready.rs b/src/combinators/when_ready.rs deleted file mode 100644 index 38c3db0..0000000 --- a/src/combinators/when_ready.rs +++ /dev/null @@ -1,77 +0,0 @@ -use std::{ - future::Future, - pin::Pin, - task::{ready, Context, Poll}, -}; - -use pin_project_lite::pin_project; - -use crate::Assertable; - -/// Wraps an [`Assertable`] and performs an expectation on the result of the -/// target future when it is ready. -#[derive(Clone, Debug)] -pub struct WhenReadyExpectation { - inner: Inner, -} - -impl WhenReadyExpectation { - /// Creates a new [`CountExpectation`] which wraps an inner [`Assertable`]. - #[inline] - pub fn new(inner: Inner) -> Self { - Self { inner } - } -} - -impl Assertable for WhenReadyExpectation -where - Inner: Assertable, - ::Target: Future, -{ - type Target = ::Output; - type Result = WhenReadyExpectationFuture; - - fn to_satisfy(self, expectation: &str, f: F) -> Self::Result - where - F: FnMut(Self::Target) -> bool, - { - WhenReadyExpectationFuture { - future: todo!(), - expectation: expectation.to_string(), - inner: self.inner, - predicate: Box::new(f), - } - } -} - -pin_project! { - /// A future that performs an assertion when it is ready. - pub struct WhenReadyExpectationFuture - where - Fut: Future, - { - #[pin] - future: Fut, - expectation: String, - inner: Inner, - predicate: Box bool>, - } -} - -impl Future for WhenReadyExpectationFuture -where - Fut: Future, - Inner: Assertable, -{ - type Output = Inner::Result; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let projected = self.project(); - let output = ready!(projected.future.poll(cx)); - Poll::Ready( - projected - .inner - .to_satisfy(&projected.expectation, projected.predicate), - ) - } -} diff --git a/src/error.rs b/src/error.rs deleted file mode 100644 index f20976c..0000000 --- a/src/error.rs +++ /dev/null @@ -1,41 +0,0 @@ -use std::{ - borrow::Cow, - fmt::{Display, Formatter}, -}; - -/// An error that indicates an assertion failure. -/// -/// This error is formatted to display information about both the failed -/// assertion and the original source of the expectation. -#[derive(Clone, Debug)] -pub struct AssertError { - fields: Vec<(&'static str, Cow<'static, str>)>, -} - -impl AssertError { - /// Creates a new assertion error. Attach fields using the - /// [`Self::with_field`] method. - pub fn new(expectation: impl Into>) -> Self { - Self { - fields: vec![("expected", expectation.into())], - } - } - - /// Attaches a custom field to the error. This will appear in the error when - /// formatting it using its [`Display`] implementation. - pub fn with_field(mut self, name: &'static str, value: impl Into>) -> Self { - self.fields.push((name, value.into())); - self - } -} - -impl Display for AssertError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - writeln!(f, "assertion failed.")?; - for (name, value) in &self.fields { - writeln!(f, " {name}: {value}")?; - } - - Ok(()) - } -} diff --git a/src/expect.rs b/src/expect.rs deleted file mode 100644 index bbb9002..0000000 --- a/src/expect.rs +++ /dev/null @@ -1,156 +0,0 @@ -use std::fmt::{Display, Formatter}; - -use crate::{AssertError, Assertable}; - -/// Begins an assertion. -/// -/// This macro is used to start an assertion. It's intended to be used in a -/// functional manner, chaining combinators together to form a complex assertion -/// that can be applied to the target value. -/// -/// ``` -/// # use expecters::prelude::*; -/// expect!(42).not().to_be_greater_than(100); -/// expect!([1, 2, 3, 4]).all().not().to_equal(0); -/// ``` -/// -/// When using this macro, source information is automatically captured based -/// on where the macro is used, and is included in the error message if the -/// assertion fails. The original target is also included to help with -/// debugging. -/// -/// ```should_panic -/// # use expecters::prelude::*; -/// expect!(10).to_be_less_than(5); -/// -/// // The above line will panic with a message similar to the following: -/// // assertion failed. -/// // expected: value is less than the input -/// // at: src/main.rs:1:1 -/// // original target: 10 -/// ``` -/// -/// For a list of built-in combinators and assertions, see the [`Assertable`] -/// trait. -#[macro_export] -macro_rules! expect { - ($e:expr) => { - // TODO: specialize for types that impl `Display` and `Debug` - $crate::ExpectationRoot::new( - $e, - $crate::SourceInfo::new( - file!(), - line!(), - column!(), - ), - stringify!($e), - ) - }; -} - -// TODO: `check_if!(...)` macro that returns a result instead of panicking - -/// The root of an expectation. Other expectations are built on top of this. -#[derive(Clone, Debug)] -pub struct ExpectationRoot { - target: T, - source_info: SourceInfo, - target_source: &'static str, -} - -impl ExpectationRoot { - /// Creates a new [`ExpectationRoot`] which wraps a target value. - /// - /// This method is not intended to be used directly. Instead, use the - /// [`expect!`] macro to create an expectation. - #[inline] - pub fn new(target: T, source_info: SourceInfo, target_source: &'static str) -> Self { - Self { - target, - source_info, - target_source, - } - } - - /// Converts the expectation into a result. Rather than panicking, this - /// causes the expectation to return an error on failure that can be handled - /// by the caller. - /// - /// ``` - /// # use expecters::prelude::*; - /// let result = expect!(42).as_result().to_equal(41); - /// expect!(result).to_be_err(); - /// ``` - #[inline] - pub fn as_result(self) -> TryExpectationRoot { - TryExpectationRoot { - target: self.target, - source_info: self.source_info, - target_source: self.target_source, - } - } -} - -impl Assertable for ExpectationRoot { - type Target = T; - type Result = (); - - #[inline] - fn to_satisfy(self, expectation: impl Display, f: F) - where - F: FnMut(Self::Target) -> bool, - { - if let Err(error) = self.as_result().to_satisfy(expectation, f) { - panic!("{error}"); - } - } -} - -/// Similar to [`ExpectationRoot`], but returns a result from assertions instead -/// of panicking on failure. -pub struct TryExpectationRoot { - target: T, - source_info: SourceInfo, - target_source: &'static str, -} - -impl Assertable for TryExpectationRoot { - type Target = T; - type Result = Result<(), AssertError>; - - fn to_satisfy(self, expectation: impl Display, mut f: F) -> Self::Result - where - F: FnMut(Self::Target) -> bool, - { - let satisfied = f(self.target); - if satisfied { - Ok(()) - } else { - let error = AssertError::new(expectation.to_string()) - .with_field("at", self.source_info.to_string()) - .with_field("original target", self.target_source); - Err(error) - } - } -} - -/// Information about the source of an expectation. -#[derive(Clone, Debug)] -pub struct SourceInfo { - pub(crate) file: &'static str, - pub(crate) line: u32, - pub(crate) column: u32, -} - -impl SourceInfo { - #[doc(hidden)] - pub const fn new(file: &'static str, line: u32, column: u32) -> Self { - Self { file, line, column } - } -} - -impl Display for SourceInfo { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}:{}:{}", self.file, self.line, self.column) - } -} diff --git a/src/lib.rs b/src/lib.rs index b6b8650..7028122 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,89 +1,100 @@ -//! Build composable assertions with a functional API. +//! Build complex, self-describing assertions by chaining together reusable +//! methods. Supports both synchronous and asynchronous assertions. //! +//! ```sh +//! cargo add --dev expecters //! ``` -//! use expecters::prelude::*; -//! expect!([1, 2, 3]).all().not().to_equal(0); -//! ``` -//! -//! This crate provides a set of combinators and assertions that can be used to -//! build complex assertions in a functional manner. The combinators are -//! designed to be chained together to form a pipeline that is applied to the -//! target value. //! -//! The following built-in combinators are supported: +//! ## Example //! -//! - [`not`](Assertable::not): Invert the result of the chained assertion. -//! - [`map`](Assertable::map): Transform the target value before applying the -//! chained assertion. -//! - [`all`](Assertable::all): Assert that all elements of an iterator satisfy -//! the chained assertion. -//! - [`any`](Assertable::any): Assert that any element of an iterator satisfies -//! the chained assertion. -//! - [`count`](Assertable::count): Assert that the number of elements in an -//! iterator satisfies the chained assertion. -//! - [`nth`](Assertable::nth): Assert that a specific element in an iterator -//! satisfies the chained assertion. -//! - [`to_be_some_and`](Assertable::to_be_some_and): Assert that the target -//! value is `Some` and that the inner value satisfies the chained assertion. -//! - [`to_be_ok_and`](Assertable::to_be_ok_and): Assert that the target value -//! is `Ok` and that the inner value satisfies the chained assertion. -//! - [`to_be_err_and`](Assertable::to_be_err_and): Assert that the target value -//! is `Err` and that the inner value satisfies the chained assertion. -//! - [`when_called`](Assertable::when_called): Assert that the target function -//! returns a value that satisfies the chained assertion. -//! - [`when_called_with`](Assertable::when_called_with): Assert that the target -//! function returns a value that satisfies the chained assertion when called -//! with the given arguments. +//! ``` +//! use expecters::prelude::*; //! -//! These combinators can be chained together as needed. For example: +#![cfg_attr( + feature = "futures", + doc = r#" # #[tokio::main(flavor = "current_thread")]"#, + doc = " # async fn main() {" +)] +#![cfg_attr(not(feature = "futures"), doc = " # fn main() {")] +//! expect!(1, as_display, to_equal("1")); +//! expect!(1..=5, count, to_equal(5)); +//! # #[cfg(feature = "futures")] +//! expect!( +//! [get_cat_url(0), get_cat_url(5)], +//! all, +//! when_ready, +//! to_contain_substr(".png"), +//! ).await; +//! # } //! +//! async fn get_cat_url(id: u32) -> String { +//! format!("cats/{id}.png") +//! } //! ``` +//! +//! If your test fails, knowing why it failed is important. Unlike many other +//! assertions libraries, failures don't generate long expectation strings. +//! Instead, your assertion is broken down into its steps, and information is +//! attached to those steps to help you see what went wrong: +//! +//! ```should_panic //! # use expecters::prelude::*; -//! expect!(i32::checked_add) -//! .when_called_with((1, 2)) -//! .to_be_some_and() -//! .to_equal(3); -//! expect!(i32::checked_add).when_called_with((i32::MAX, 1)).to_be_none(); +//! expect!([1, 2, 3], all, to_satisfy(|n| n % 2 == 1)); //! ``` //! -//! In addition to these combinators, a set of built-in assertions are provided -//! that can be used to form the final assertion. For a full list of assertions, -//! see the [`Assertable`] trait. +//! This produces an error like the following: //! -//! If you need the error from the assertion, you can use the [`as_result`] -//! method at the start of the chain to convert the assertion to a result: +//! ```text +//! assertion failed: +//! at: src\lib.rs:42:8 [your_lib::tests] +//! subject: [1, 2, 3] //! -//! ``` -//! # use expecters::prelude::*; -//! let result = expect!(42).as_result().to_be_less_than(10); -//! expect!(result).to_be_err(); +//! steps: +//! all: +//! received: [1, 2, 3] +//! index: 1 +//! +//! to_satisfy: did not satisfy predicate +//! received: 2 +//! predicate: |n| n % 2 == 1 //! ``` //! -//! Note that this crate does not support any kind of mocking or test harness -//! features. It is only intended to be used for writing assertions in tests. -//! Other crates, such as [`mockall`] and [`test-case`], can be used in -//! conjunction with this crate to enhance testing capabilities. +//! See the [`expect!`] macro's documentation for usage information. For a full +//! list of modifiers and assertions, look at the [`prelude`] module. //! -//! [`as_result`]: ExpectationRoot::as_result -//! [`mockall`]: https://crates.io/crates/mockall -//! [`test-case`]: https://crates.io/crates/test-case - -pub mod combinators; - -mod assertions; -mod error; -mod expect; - -pub use assertions::*; -pub use error::*; -pub use expect::*; - -/// Commonly used types and traits. Import this module to get everything you -/// need to start writing expectations. -pub mod prelude { - // TODO: don't accidentally re-export the expect module - pub use crate::{expect, path, Assertable}; -} +//! ## Crate features +//! +//! Many of the assertions require certain crate features to be enabled. Default +//! features are marked with an asterisk (*) and can be disabled with +//! `default-features = false`: +//! +//! - `futures`*: Enables async assertions. +//! - `regex`*: Enables assertions that use regular expressions. Uses +//! [regex](https://crates.io/crates/regex) to execute them. +//! - `colors`*: Enables styled failure messages. Styled messages can always be +//! disabled by setting `NO_COLOR`. +#![warn( + missing_debug_implementations, + missing_docs, + trivial_casts, + trivial_numeric_casts, + unused_extern_crates, + unused_import_braces, + unused_qualifications, + unused_results, + clippy::all, + clippy::pedantic, + clippy::style +)] +#![allow(clippy::missing_errors_doc, clippy::module_name_repetitions)] +#![forbid(unsafe_code)] +pub mod assertions; +pub mod metadata; +pub mod prelude; #[doc(hidden)] pub mod specialization; + +mod macros; + +pub use assertions::AssertionOutput; diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 0000000..346dc4f --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,307 @@ +/// Performs an assertion. +/// +/// This macro is used to perform an assertion on a subject value. It's intended +/// to be used to build assertions piece-by-piece to perform more complex +/// assertions on a subject value. +/// +/// Note that the "subject" of an assertion is the value the assertion is being +/// executed on. For example, if an assertion is checking whether a value is +/// greater than zero, then the subject of the assertion is the value that is +/// being checked. +/// +/// ``` +/// # use expecters::prelude::*; +/// let subject = 1; +/// expect!(subject, to_be_greater_than(0)); +/// ``` +/// +/// ## Syntax +/// +/// This macro is called like a function. For example: +/// +/// ``` +/// # use expecters::prelude::*; +/// expect!(1, not, to_equal(0)); +/// ``` +/// +/// Breaking this down, the macro accepts arguments in the format +/// `expect!(subject, modifiers..., assertion)`. The subject may be any value +/// that you want to execute an assertion on (and is moved/copied into the +/// assertion - make sure to borrow the value if needed). The final argument +/// must be a fully built assertion. +/// +/// Both the modifiers and the final assertion must be either identifiers or +/// simple function calls in the format `(params...)`. This is because +/// the parameters to function calls will be annotated. This means that **the +/// following syntax is invalid**, as paths are not supported: +/// +/// ```compile_fail +/// # use expecters::prelude::*; +/// expect!(1, not, expecters::prelude::to_equal(0)); +/// ``` +/// +/// To fix this, remove the path: +/// +/// ``` +/// # use expecters::prelude::*; +/// expect!(1, not, to_equal(0)); +/// ``` +/// +/// Modifiers are special assertion builders that are used to modify a later +/// assertion either by transforming the input to that assertion (like [`map`]), +/// transforming the output from the assertion (like [`not`]), or even calling +/// the assertion multiple times (like [`all`]). In practice, a modifier may be +/// used to modify an assertion in any way it wants, and should generate a new +/// assertion from it. +/// +/// Each modifier passed into this macro will be called with the assertion to +/// modify. For example, in the above code snippet, the [`not`] modifier is a +/// function that the macro calls, passing in the later assertion. It is +/// functionally being transformed to `not(to_equal(0))` (although it is not +/// receiving this exact input - more on this below). When chaining multiple +/// modifiers, they are functionally composed together. For example: +/// +/// ``` +/// # use expecters::prelude::*; +/// expect!([1, 2, 3], not, all, to_equal(2)); +/// ``` +/// +/// In this assertion, the [`all`] modifier is functionally being called as +/// `all(to_equal(2))`, and the [`not`] modifier is functionally being called +/// with the assertion returned by *that* function call (since [`all`] returns +/// an assertion). In other words, the final assertion is essentially the result +/// of calling `not(all(to_equal(2)))`. +/// +/// In practice, modifiers are slightly more complicated to this. Modifiers and +/// assertions are called lazily on-demand, and each of the intermediate +/// assertions and the final assertion are wrapped to record additional data +/// about the values being passed between assertions. +/// +/// ## Async assertions +/// +/// > *Note: requires crate feature `futures`.* +/// +/// Async assertions function similar to sync assertions, but need to be +/// `.await`ed. For more information, see the +/// [`futures`](crate::assertions::futures) module. +/// +/// ## Annotations +/// +/// Values passed as parameters to a modifier or the final assertion are +/// annotated. Values passed into assertions (and from modifiers to other +/// assertions) are *transparently* annotated. +/// +/// An annotated value is a value with an additional string representation +/// attached to it. This string representation is generated either from the +/// value's [`Debug`] representation or from the [stringified] source code +/// itself (if no [`Debug`] implementation is available). +/// +/// Above, it was noted that applying, for example, the [`not`] modifier to an +/// assertion `a` was *functionally* equivalent to calling `not(a)`. In +/// implementation, [`not`] does not actually receive the assertion `a`, but +/// instead receives a special annotated assertion which wraps `a`. +/// +/// This annotated assertion is a hidden modifier that annotates the value that +/// it receives. This means that when calling `expect!(1, not, to_equal(2))`, +/// the value being sent from [`not`] to [`to_equal`] is automatically annotated +/// by this macro. Additionally, the `2` parameter to [`to_equal`] is +/// automatically annotated by this macro, so the [`to_equal`] function is +/// actually not receiving an [`i32`], but an annotated version of it. +/// +/// In other words, if the hidden modifier's name is `annotate` and there +/// existed a constructor `Annotated(T)` to construct an annotated value, then +/// the assertion being called could be simplistically represented as +/// `annotate(not(annotate(to_equal(Annotated(2)))))`. Note that the parameter +/// to [`to_equal`] is also annotated, as would any parameters to any modifiers +/// in the chain (if there existed any which accepted parameters). +/// +/// This macro must perform the annotation itself to avoid adding additional +/// bounds to assertions. This is because this macro performs autoref +/// specialization to extract the string representation of the value. Without +/// this, the [`to_equal`] assertion would need to have an additional [`Debug`] +/// constraint on the values that it receives to be able to display those values +/// in case of an assertion failure, meaning that assertion would not be as +/// useful for values that do not have a [`Debug`] representation. +/// +/// One limitation of this approach is that values being passed from modifiers +/// to other assertions down the chain do not have a meaningful source +/// representation. If those values do not have a [`Debug`] implementation, then +/// the string representation of those values will not be meaningful. However, +/// assertions can see whether a meaningful string representation is available +/// before generating error messages, and this approach removes the burden on +/// assertions (and users) to constrain their inputs to values that can be +/// meaningfully represented as a string. +/// +/// Note that there will not always be a meaningful string representation of a +/// value. For values defined directly in source code (like `2` in the example +/// above), a source representation of the value can be used to provide some +/// context on where the value came from. However, for intermediate values (like +/// the value sent from [`not`] to [`to_equal`]), there may not be a meaningful +/// source representation of the value, as the annotated value would simply +/// represent an internal variable of the macro. A best-effort attempt will be +/// made to preserve as much useful information as possible to provide +/// informative error messages. +/// +/// [`Annotated`]: crate::metadata::Annotated +/// [`AnnotatedAssertion`]: crate::assertions::AnnotatedAssertion +/// [`Debug`]: std::fmt::Debug +/// [`all`]: crate::prelude::IteratorAssertions::all +/// [`map`]: crate::prelude::GeneralAssertions::map +/// [`not`]: crate::prelude::GeneralAssertions::not +/// [`to_equal`]: crate::prelude::GeneralAssertions::to_equal +/// [stringified]: std::stringify +#[macro_export] +macro_rules! expect { + ($($tokens:tt)*) => { + $crate::assertions::general::UnwrappableOutput::unwrap( + $crate::__expect_inner!($($tokens)*), + ) + }; +} + +/// Same as [`expect!`], but returns the result itself rather than panicking on +/// failure. +/// +/// More specifically, this does not finalize the output of the assertion. The +/// syntax is exactly the same as [`expect!`] (and async assertions should still +/// be `.await`ed as usual), but the output from it will be a result type that +/// can be inspected rather than panicking on failure. +/// +/// ``` +/// # use expecters::prelude::*; +/// let result = try_expect!(1, to_equal(2)); +/// expect!(result, to_be_err); +/// ``` +/// +/// See [`expect!`] for more information on how to use this macro. +#[macro_export] +macro_rules! try_expect { + ($($tokens:tt)*) => { + $crate::assertions::general::UnwrappableOutput::try_unwrap( + $crate::__expect_inner!($($tokens)*) + ) + }; +} + +// Note: it's important to use the input tokens before stringifying them. This +// is necessary to ensure that the tokens are treated as values instead of +// arbitrary, meaningless tokens, and ensures that LSPs provide real completions +// for those tokens instead of just letting the user type whatever without any +// suggested completions. +#[macro_export] +#[doc(hidden)] +macro_rules! __expect_inner { + // Entrypoint + ( + $subject:expr, + $($assertions:tt)* + ) => {{ + let subject = $crate::annotated!($subject); + let subject_repr = ::std::string::ToString::to_string(&subject); + let builder = $crate::assertions::AssertionBuilder::__new(subject); + $crate::__expect_inner!( + @build_assertion, + [], + subject_repr, + builder, + $($assertions)* + ) + }}; + + // Build assertion (chain modifiers and final assertion) + ( + // Base case (with params) + @build_assertion, + [$($frame_name:expr,)*], + $subject:expr, + $builder:expr, + $assertion:ident($($param:expr),* $(,)?) + $(,)? + ) => {{ + let builder = $crate::__expect_inner!(@annotate, $builder); + let assertion = builder.$assertion($($crate::annotated!($param),)*); + let cx = $crate::assertions::AssertionContext::__new( + $subject, + $crate::source_loc!(), + { + const FRAMES: &'static [&'static str] = &[ + $($frame_name,)* + ::std::stringify!($assertion), + ]; + FRAMES + }, + ); + $crate::assertions::AssertionBuilder::__apply( + builder, + cx, + assertion, + ) + }}; + ( + // Base case (without params) + @build_assertion, + [$($frame_name:expr,)*], + $subject:expr, + $builder:expr, + $assertion:ident + $(,)? + ) => { + $crate::__expect_inner!( + @build_assertion, + [$($frame_name,)*], + $subject, + $builder, + $assertion() + ) + }; + ( + // Recursive case (with params) + @build_assertion, + [$($frame_name:expr,)*], + $subject:expr, + $builder:expr, + $modifier:ident($($param:expr),* $(,)?), + $($rest:tt)* + ) => {{ + let builder = $crate::__expect_inner!(@annotate, $builder); + let builder = builder.$modifier( + $($crate::annotated!($param),)* + ); + $crate::__expect_inner!( + @build_assertion, + [ + $($frame_name,)* + ::std::stringify!($modifier), + ], + $subject, + builder, + $($rest)* + ) + }}; + ( + // Recursive case (without params) + @build_assertion, + [$($frame_name:expr,)*], + $subject:expr, + $builder:expr, + $modifier:ident, + $($rest:tt)* + ) => { + $crate::__expect_inner!( + @build_assertion, + [$($frame_name,)*], + $subject, + $builder, + $modifier(), + $($rest)* + ) + }; + + // Annotate the value being passed down the chain + (@annotate, $builder:expr) => { + $crate::assertions::general::__annotate( + $builder, + |not_debug| $crate::annotated!(not_debug), + ) + }; +} diff --git a/src/metadata.rs b/src/metadata.rs new file mode 100644 index 0000000..ab4837b --- /dev/null +++ b/src/metadata.rs @@ -0,0 +1,7 @@ +//! Types used to track metadata about an assertion's execution flow. + +mod annotated; +mod source_loc; + +pub use annotated::*; +pub use source_loc::*; diff --git a/src/metadata/annotated.rs b/src/metadata/annotated.rs new file mode 100644 index 0000000..1ccd4be --- /dev/null +++ b/src/metadata/annotated.rs @@ -0,0 +1,160 @@ +use std::fmt::{Debug, Display, Formatter}; + +#[macro_export] +#[doc(hidden)] +macro_rules! annotated { + ($value:expr) => {{ + #[allow(unused_imports)] + use $crate::specialization::annotated::*; + + // $value needs to be used as a value before it's stringified to get + // proper completions from tools like rust-analyzer + let wrapper = $crate::specialization::__SpecializeWrapper($value); + (&wrapper) + .__annotated_kind() + .annotate(wrapper.0, ::std::stringify!($value)) + }}; +} + +/// A value annotated with its string representation. +/// +/// This holds a string representation of the stored value. The string +/// representation is obtained in the following order of precedence: +/// +/// 1. the [`Debug`] representation, otherwise... +/// 2. the [stringified](std::stringify) source code (that was annotated). +/// +/// The stringified source code is always available as well, which can be +/// helpful for providing error messages that refer to the actual source code +/// of a value. +/// +/// One drawback is that if the annotated value was a variable, the source +/// representation is the name of that variable, which may provide limited +/// information about the actual value that was annotated. This can happen, for +/// example, if a value is an annotated input into an assertion and was +/// generated by the [`expect!`](crate::expect!) macro (which annotates +/// intermediate values inside of closures). In this case, the only way to +/// generate a meaningful string representation of the value is for that value +/// to implement [`Debug`]. +/// +/// This type makes no guarantees about the string representation of the +/// contained value except for where the representation comes from. Two +/// different compiler versions may result in two different string +/// representations (due to [stringify]'s lack of guarantee). The string +/// representation is *only* intended to be used to augment user-facing +/// messages. +#[derive(Clone, Debug)] +pub struct Annotated { + value: T, + string_repr: Option, + stringified: &'static str, + kind: AnnotatedKind, +} + +impl Annotated { + #[inline] + pub(crate) fn from_stringified(value: T, stringified: &'static str) -> Self { + Self { + string_repr: None, + stringified, + value, + kind: AnnotatedKind::Stringify, + } + } + + /// Gets a reference to the inner value. + #[inline] + pub fn inner(&self) -> &T { + &self.value + } + + /// Gets a mutable reference to the inner value. + #[inline] + pub fn inner_mut(&mut self) -> &mut T { + &mut self.value + } + + /// Extracts the inner value. + #[inline] + pub fn into_inner(self) -> T { + self.value + } + + /// Gets the stringified input's source code. + #[inline] + pub fn stringified(&self) -> &'static str { + self.stringified + } + + /// Gets the source of the string representation of this value. + #[inline] + pub fn kind(&self) -> AnnotatedKind { + self.kind + } + + /// Gets the string representation of this value. + #[inline] + pub fn as_str(&self) -> &str { + self.string_repr.as_deref().unwrap_or(self.stringified) + } +} + +impl Annotated +where + T: Debug, +{ + #[inline] + pub(crate) fn from_debug(value: T, stringified: &'static str) -> Self { + Self { + string_repr: Some(format!("{value:?}")), + stringified, + value, + kind: AnnotatedKind::Debug, + } + } +} + +impl Display for Annotated { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + Display::fmt(self.as_str(), f) + } +} + +/// The source of the string representation for an [`Annotated`] value. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] +#[non_exhaustive] +pub enum AnnotatedKind { + /// The string representation is the [stringified](stringify) source code. + Stringify, + + /// The string representation is the [`Debug`] representation of the value. + Debug, +} + +#[cfg(test)] +mod tests { + use test_case::test_case; + + use crate::metadata::Annotated; + + use super::AnnotatedKind; + + struct UseStringify(T); + + #[test_case(annotated!(1), AnnotatedKind::Debug, "1", "1"; "debug simple")] + #[test_case(annotated!(1 + 3), AnnotatedKind::Debug, "1 + 3", "4"; "debug addition")] + #[test_case(annotated!("test"), AnnotatedKind::Debug, "\"test\"", "\"test\""; "debug string")] + #[test_case(annotated!(UseStringify(1)), AnnotatedKind::Stringify, "UseStringify(1)", "UseStringify(1)"; "stringify simple")] + #[test_case(annotated!(UseStringify(1 + 3)), AnnotatedKind::Stringify, "UseStringify(1 + 3)", "UseStringify(1 + 3)"; "stringify addition")] + #[allow(clippy::needless_pass_by_value)] + fn annotated_macro( + annotated: Annotated, + kind: AnnotatedKind, + stringified: &str, + as_str: &str, + ) { + assert_eq!(annotated.kind(), kind); + assert_eq!(annotated.stringified(), stringified); + assert_eq!(annotated.as_str(), as_str); + } +} diff --git a/src/metadata/source_loc.rs b/src/metadata/source_loc.rs new file mode 100644 index 0000000..849a8e5 --- /dev/null +++ b/src/metadata/source_loc.rs @@ -0,0 +1,82 @@ +use std::fmt::{Display, Formatter}; + +#[macro_export] +#[doc(hidden)] +macro_rules! source_loc { + () => { + $crate::metadata::SourceLoc::new( + ::std::module_path!(), + ::std::file!(), + ::std::line!(), + ::std::column!(), + ) + }; +} + +/// A location in a source code file. +#[derive(Clone, Copy, Debug)] +pub struct SourceLoc { + module_path: &'static str, + file: &'static str, + line: u32, + column: u32, +} + +impl SourceLoc { + #[doc(hidden)] + #[must_use] + pub const fn new( + module_path: &'static str, + file: &'static str, + line: u32, + column: u32, + ) -> Self { + Self { + module_path, + file, + line, + column, + } + } + + /// The [`module_path`] of the source code. + #[inline] + #[must_use] + pub const fn module_path(&self) -> &'static str { + self.module_path + } + + /// The name of the source code file. + #[inline] + #[must_use] + pub const fn file(&self) -> &'static str { + self.file + } + + /// The line within the source code file. + #[inline] + #[must_use] + pub const fn line(&self) -> u32 { + self.line + } + + /// The column within the line of source code. + #[inline] + #[must_use] + pub const fn column(&self) -> u32 { + self.column + } +} + +impl Display for SourceLoc { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{file}:{line}:{column} [{module}]", + file = self.file, + line = self.line, + column = self.column, + module = self.module_path, + ) + } +} diff --git a/src/prelude.rs b/src/prelude.rs new file mode 100644 index 0000000..93f2754 --- /dev/null +++ b/src/prelude.rs @@ -0,0 +1,26 @@ +//! This module contains commonly used exports from this crate. +//! +//! To keep your imports simple, rather than importing these members +//! individually, you can write: +//! +//! ``` +//! # #[allow(unused_imports)] +//! use expecters::prelude::*; +//! ``` +//! +//! While not necessary, it is recommended to glob import this module in any +//! test modules that use this crate. + +pub use crate::{ + assertions::{ + general::GeneralAssertions, + iterators::IteratorAssertions, + options::OptionAssertions, + results::ResultAssertions, + strings::{DebugAssertions, DisplayAssertions, StringAssertions}, + }, + expect, try_expect, +}; + +#[cfg(feature = "futures")] +pub use crate::assertions::futures::FutureAssertions; diff --git a/src/specialization.rs b/src/specialization.rs index 1bbcc5e..8efd76a 100644 --- a/src/specialization.rs +++ b/src/specialization.rs @@ -1 +1,7 @@ -pub mod at_path; +#![allow(missing_debug_implementations)] + +pub mod annotated; + +mod wrapper; + +pub use wrapper::*; diff --git a/src/specialization/annotated.rs b/src/specialization/annotated.rs new file mode 100644 index 0000000..b7a7cf9 --- /dev/null +++ b/src/specialization/annotated.rs @@ -0,0 +1,41 @@ +use std::fmt::Debug; + +use crate::metadata::Annotated; + +use super::__SpecializeWrapper; + +pub struct __AnnotatedStringifyTag; + +impl __AnnotatedStringifyTag { + pub fn annotate(self, value: T, stringified: &'static str) -> Annotated { + Annotated::from_stringified(value, stringified) + } +} +pub trait __AnnotatedStringifyKind { + #[inline] + fn __annotated_kind(&self) -> __AnnotatedStringifyTag { + __AnnotatedStringifyTag + } +} + +impl __AnnotatedStringifyKind for &__SpecializeWrapper {} + +pub struct __AnnotatedDebugTag; + +impl __AnnotatedDebugTag { + pub fn annotate(self, value: T, stringified: &'static str) -> Annotated + where + T: Debug, + { + Annotated::from_debug(value, stringified) + } +} + +pub trait __AnnotatedDebugKind { + #[inline] + fn __annotated_kind(&self) -> __AnnotatedDebugTag { + __AnnotatedDebugTag + } +} + +impl __AnnotatedDebugKind for __SpecializeWrapper where T: Debug {} diff --git a/src/specialization/at_path.rs b/src/specialization/at_path.rs deleted file mode 100644 index 164bc16..0000000 --- a/src/specialization/at_path.rs +++ /dev/null @@ -1,88 +0,0 @@ -#[doc(hidden)] -pub struct Wrapper(pub I, pub T); - -pub mod kinds { - use std::{ - borrow::Borrow, - collections::HashMap, - hash::{BuildHasher, Hash}, - ops::Index, - }; - - use super::Wrapper; - - #[doc(hidden)] - pub trait __ExpectersForceIndexKind { - type __ExpectersInput; - type __ExpectersOutput: ?Sized; - - fn __expecters_try_index( - self, - ) -> fn(&Self::__ExpectersInput, I) -> Option<&Self::__ExpectersOutput>; - } - - impl __ExpectersForceIndexKind for &Wrapper<&I, &T> - where - T: Index, - { - type __ExpectersInput = T; - type __ExpectersOutput = T::Output; - - fn __expecters_try_index( - self, - ) -> fn(&Self::__ExpectersInput, I) -> Option<&Self::__ExpectersOutput> { - |value, index| Some(&value[index]) - } - } - - #[doc(hidden)] - pub trait __ExpectersIteratorKind { - type __ExpectersInput; - type __ExpectersOutput; - - fn __expecters_try_index( - self, - ) -> fn(Self::__ExpectersInput, usize) -> Option; - } - - impl __ExpectersIteratorKind for &&Wrapper<&usize, &T> - where - T: IntoIterator, - { - type __ExpectersInput = T; - type __ExpectersOutput = T::Item; - - fn __expecters_try_index( - self, - ) -> fn(Self::__ExpectersInput, usize) -> Option { - |value, index| value.into_iter().nth(index) - } - } - - #[doc(hidden)] - pub trait __ExpectersMapKind { - type __ExpectersInput; - type __ExpectersOutput; - - fn __expecters_try_index( - self, - ) -> fn(Self::__ExpectersInput, I) -> Option; - } - - impl __ExpectersMapKind<&Q> for &&&Wrapper<&&Q, &HashMap> - where - K: Eq + Hash + Borrow, - V: Clone, - S: BuildHasher, - Q: Hash + Eq + ?Sized, - { - type __ExpectersInput = HashMap; - type __ExpectersOutput = V; - - fn __expecters_try_index( - self, - ) -> fn(Self::__ExpectersInput, &Q) -> Option { - |value, index| value.get(index).cloned() - } - } -} diff --git a/src/specialization/wrapper.rs b/src/specialization/wrapper.rs new file mode 100644 index 0000000..8c0ce85 --- /dev/null +++ b/src/specialization/wrapper.rs @@ -0,0 +1 @@ +pub struct __SpecializeWrapper(pub T); diff --git a/tests/error_messages.rs b/tests/error_messages.rs new file mode 100644 index 0000000..3375971 --- /dev/null +++ b/tests/error_messages.rs @@ -0,0 +1,58 @@ +use expecters::prelude::*; + +#[test] +fn simple() { + expect!( + try_expect!(1, not, to_equal(1)), + to_be_err_and, + as_display, + to_satisfy_with(|message| { + try_expect!(&message, to_contain_substr("subject: 1"))?; + try_expect!(&message, to_contain_substr("not"))?; + try_expect!(&message, to_contain_substr("to_equal"))?; + try_expect!(&message, to_contain_substr("received: 1"))?; + Ok(()) + }), + ); +} + +#[test] +fn non_debug() { + #[derive(PartialEq)] + struct NotDebug(T); + + expect!( + try_expect!(NotDebug(1), to_equal(NotDebug(2))), + to_be_err_and, + as_display, + to_satisfy_with(|message| { + try_expect!(&message, to_contain_substr("subject: NotDebug(1)"))?; + try_expect!(&message, to_contain_substr("expected: NotDebug(2)"))?; + Ok(()) + }), + ); +} + +#[test] +fn propagated_value() { + expect!( + try_expect!([1, 1], all, not, to_equal(1)), + to_be_err_and, + as_display, + to_contain_substr("[1, 1]"), + ); +} + +#[test] +fn annotated_strings() { + expect!( + try_expect!("test", to_equal("")), + to_be_err_and, + as_display, + to_satisfy_with(|message| { + try_expect!(&message, to_contain_substr("\"test\""))?; + try_expect!(&message, to_contain_substr("\"\""))?; + Ok(()) + }), + ); +} diff --git a/tests/simple.rs b/tests/simple.rs new file mode 100644 index 0000000..ae631fa --- /dev/null +++ b/tests/simple.rs @@ -0,0 +1,10 @@ +use expecters::prelude::*; + +#[test] +fn not_debug() { + #[derive(Clone, PartialEq)] + struct NotDebug(T); + + expect!(NotDebug(1), to_equal(NotDebug(1))); + expect!([NotDebug(1)], all, to_equal(NotDebug(1))); +}