From 3368da6d664173bb87214037bf6439cdddefec13 Mon Sep 17 00:00:00 2001 From: TehPers Date: Mon, 5 Aug 2024 22:10:02 -0700 Subject: [PATCH 01/37] Closure-based assertions --- .vscode/extensions.json | 1 - Cargo.lock | 296 ++++++++ Cargo.toml | 28 +- src/assertions.rs | 705 +----------------- src/assertions/annotated.rs | 23 + src/assertions/assertion.rs | 25 + src/assertions/context.rs | 106 +++ src/assertions/error.rs | 75 ++ src/assertions/functions.rs | 0 src/assertions/futures.rs | 20 + .../futures/finalized_output_future.rs | 45 ++ .../futures/inverted_output_future.rs | 104 +++ .../futures/merged_output_future.rs | 144 ++++ src/assertions/futures/modifiers.rs | 41 + src/assertions/futures/when_ready_future.rs | 112 +++ src/assertions/general.rs | 19 + src/assertions/general/comparisons.rs | 159 ++++ src/assertions/general/finalizable_result.rs | 34 + src/assertions/general/invertible_result.rs | 34 + src/assertions/general/modifiers.rs | 69 ++ src/assertions/iterators.rs | 10 + src/assertions/iterators/mergeable_result.rs | 59 ++ src/assertions/iterators/modifiers.rs | 144 ++++ src/assertions/options.rs | 0 src/assertions/results.rs | 0 src/combinators/all.rs | 41 - src/combinators/when_ready.rs | 77 -- src/error.rs | 41 - src/experimenting/assertions.rs | 683 +++++++++++++++++ src/{ => experimenting}/combinators.rs | 2 - src/experimenting/combinators/all.rs | 179 +++++ src/{ => experimenting}/combinators/any.rs | 0 .../combinators/at_path.rs | 0 src/{ => experimenting}/combinators/count.rs | 8 +- src/{ => experimenting}/combinators/err.rs | 0 src/{ => experimenting}/combinators/map.rs | 0 src/{ => experimenting}/combinators/not.rs | 0 src/{ => experimenting}/combinators/nth.rs | 0 src/{ => experimenting}/combinators/ok.rs | 0 src/{ => experimenting}/combinators/some.rs | 0 .../combinators/when_called.rs | 0 src/experimenting/combinators2.rs | 13 + src/experimenting/combinators2/aggregate.rs | 150 ++++ src/experimenting/combinators2/combinator.rs | 90 +++ src/experimenting/combinators2/count.rs | 51 ++ src/experimenting/combinators2/identity.rs | 16 + src/experimenting/combinators2/plan.rs | 34 + src/experimenting/combinators2/when_ready.rs | 84 +++ src/experimenting/error.rs | 64 ++ src/{ => experimenting}/expect.rs | 8 +- src/experimenting/extensions.rs | 5 + src/experimenting/extensions/iterators.rs | 54 ++ src/experimenting/lib.rs | 100 +++ src/experimenting/root.rs | 131 ++++ src/experimenting/specialization.rs | 1 + .../specialization/at_path.rs | 0 src/experimenting/v10/combinators.rs | 11 + src/experimenting/v10/combinators/all.rs | 36 + src/experimenting/v10/combinators/any.rs | 67 ++ .../v10/combinators/combinator.rs | 57 ++ .../v10/combinators/combinator_ext.rs | 109 +++ src/experimenting/v10/combinators/not.rs | 37 + src/experimenting/v11/assertions.rs | 3 + src/experimenting/v11/assertions/assertion.rs | 17 + src/experimenting/v11/combinators.rs | 12 + src/experimenting/v11/combinators/all.rs | 50 ++ .../v11/combinators/combinator.rs | 11 + .../v11/combinators/combinator_ext.rs | 28 + src/experimenting/v11/combinators/macros.rs | 10 + src/experimenting/v11/combinators/not.rs | 48 ++ src/experimenting/v11/expect.rs | 98 +++ src/experimenting/v11/failure.rs | 66 ++ src/experimenting/v11/specialization.rs | 6 + .../v11/specialization/at_path.rs | 83 +++ src/experimenting/v11/specialization/root.rs | 77 ++ .../v11/specialization/wrapper.rs | 2 + src/experimenting/v3.rs | 372 +++++++++ src/experimenting/v4.rs | 341 +++++++++ src/experimenting/v5.rs | 333 +++++++++ src/experimenting/v6.rs | 180 +++++ src/experimenting/v6/all.rs | 67 ++ src/experimenting/v6/assertion.rs | 90 +++ src/experimenting/v6/combinator.rs | 47 ++ src/experimenting/v6/error.rs | 26 + src/experimenting/v6/not.rs | 69 ++ src/experimenting/v7.rs | 180 +++++ src/experimenting/v7/all.rs | 73 ++ src/experimenting/v7/assertion.rs | 90 +++ src/experimenting/v7/combinator.rs | 51 ++ src/experimenting/v7/error.rs | 26 + src/experimenting/v7/not.rs | 73 ++ src/experimenting/v8.rs | 29 + src/experimenting/v9/assertions.rs | 5 + src/experimenting/v9/assertions/assertion.rs | 20 + .../v9/assertions/simple_assertion.rs | 61 ++ src/experimenting/v9/combinators.rs | 13 + src/experimenting/v9/combinators/all.rs | 80 ++ src/experimenting/v9/combinators/any.rs | 80 ++ .../v9/combinators/combinator.rs | 69 ++ .../v9/combinators/combinator_ext.rs | 146 ++++ src/experimenting/v9/combinators/not.rs | 75 ++ .../v9/combinators/when_ready.old.rs | 354 +++++++++ .../v9/combinators/when_ready.rs | 114 +++ src/experimenting/v9/expect.rs | 174 +++++ src/experimenting/v9/failure.rs | 66 ++ src/experimenting/v9/lib.rs | 13 + src/experimenting/v9/output.rs | 208 ++++++ src/experimenting/v9/prelude.rs | 10 + src/experimenting/v9/third_party.rs | 4 + src/experimenting/v9/third_party/either.rs | 84 +++ src/experimenting/v9/thoughts.md | 27 + src/lib.rs | 95 +-- src/macros.rs | 542 ++++++++++++++ src/metadata.rs | 7 + src/metadata/annotated.rs | 160 ++++ src/metadata/source_loc.rs | 78 ++ src/prelude.rs | 26 + src/specialization.rs | 6 +- src/specialization/annotated.rs | 41 + src/specialization/annotated_autoderef.rs | 64 ++ src/specialization/wrapper.rs | 1 + 121 files changed, 8641 insertions(+), 942 deletions(-) create mode 100644 src/assertions/annotated.rs create mode 100644 src/assertions/assertion.rs create mode 100644 src/assertions/context.rs create mode 100644 src/assertions/error.rs create mode 100644 src/assertions/functions.rs create mode 100644 src/assertions/futures.rs create mode 100644 src/assertions/futures/finalized_output_future.rs create mode 100644 src/assertions/futures/inverted_output_future.rs create mode 100644 src/assertions/futures/merged_output_future.rs create mode 100644 src/assertions/futures/modifiers.rs create mode 100644 src/assertions/futures/when_ready_future.rs create mode 100644 src/assertions/general.rs create mode 100644 src/assertions/general/comparisons.rs create mode 100644 src/assertions/general/finalizable_result.rs create mode 100644 src/assertions/general/invertible_result.rs create mode 100644 src/assertions/general/modifiers.rs create mode 100644 src/assertions/iterators.rs create mode 100644 src/assertions/iterators/mergeable_result.rs create mode 100644 src/assertions/iterators/modifiers.rs create mode 100644 src/assertions/options.rs create mode 100644 src/assertions/results.rs delete mode 100644 src/combinators/all.rs delete mode 100644 src/combinators/when_ready.rs delete mode 100644 src/error.rs create mode 100644 src/experimenting/assertions.rs rename src/{ => experimenting}/combinators.rs (92%) create mode 100644 src/experimenting/combinators/all.rs rename src/{ => experimenting}/combinators/any.rs (100%) rename src/{ => experimenting}/combinators/at_path.rs (100%) rename src/{ => experimenting}/combinators/count.rs (87%) rename src/{ => experimenting}/combinators/err.rs (100%) rename src/{ => experimenting}/combinators/map.rs (100%) rename src/{ => experimenting}/combinators/not.rs (100%) rename src/{ => experimenting}/combinators/nth.rs (100%) rename src/{ => experimenting}/combinators/ok.rs (100%) rename src/{ => experimenting}/combinators/some.rs (100%) rename src/{ => experimenting}/combinators/when_called.rs (100%) create mode 100644 src/experimenting/combinators2.rs create mode 100644 src/experimenting/combinators2/aggregate.rs create mode 100644 src/experimenting/combinators2/combinator.rs create mode 100644 src/experimenting/combinators2/count.rs create mode 100644 src/experimenting/combinators2/identity.rs create mode 100644 src/experimenting/combinators2/plan.rs create mode 100644 src/experimenting/combinators2/when_ready.rs create mode 100644 src/experimenting/error.rs rename src/{ => experimenting}/expect.rs (94%) create mode 100644 src/experimenting/extensions.rs create mode 100644 src/experimenting/extensions/iterators.rs create mode 100644 src/experimenting/lib.rs create mode 100644 src/experimenting/root.rs create mode 100644 src/experimenting/specialization.rs rename src/{ => experimenting}/specialization/at_path.rs (100%) create mode 100644 src/experimenting/v10/combinators.rs create mode 100644 src/experimenting/v10/combinators/all.rs create mode 100644 src/experimenting/v10/combinators/any.rs create mode 100644 src/experimenting/v10/combinators/combinator.rs create mode 100644 src/experimenting/v10/combinators/combinator_ext.rs create mode 100644 src/experimenting/v10/combinators/not.rs create mode 100644 src/experimenting/v11/assertions.rs create mode 100644 src/experimenting/v11/assertions/assertion.rs create mode 100644 src/experimenting/v11/combinators.rs create mode 100644 src/experimenting/v11/combinators/all.rs create mode 100644 src/experimenting/v11/combinators/combinator.rs create mode 100644 src/experimenting/v11/combinators/combinator_ext.rs create mode 100644 src/experimenting/v11/combinators/macros.rs create mode 100644 src/experimenting/v11/combinators/not.rs create mode 100644 src/experimenting/v11/expect.rs create mode 100644 src/experimenting/v11/failure.rs create mode 100644 src/experimenting/v11/specialization.rs create mode 100644 src/experimenting/v11/specialization/at_path.rs create mode 100644 src/experimenting/v11/specialization/root.rs create mode 100644 src/experimenting/v11/specialization/wrapper.rs create mode 100644 src/experimenting/v3.rs create mode 100644 src/experimenting/v4.rs create mode 100644 src/experimenting/v5.rs create mode 100644 src/experimenting/v6.rs create mode 100644 src/experimenting/v6/all.rs create mode 100644 src/experimenting/v6/assertion.rs create mode 100644 src/experimenting/v6/combinator.rs create mode 100644 src/experimenting/v6/error.rs create mode 100644 src/experimenting/v6/not.rs create mode 100644 src/experimenting/v7.rs create mode 100644 src/experimenting/v7/all.rs create mode 100644 src/experimenting/v7/assertion.rs create mode 100644 src/experimenting/v7/combinator.rs create mode 100644 src/experimenting/v7/error.rs create mode 100644 src/experimenting/v7/not.rs create mode 100644 src/experimenting/v8.rs create mode 100644 src/experimenting/v9/assertions.rs create mode 100644 src/experimenting/v9/assertions/assertion.rs create mode 100644 src/experimenting/v9/assertions/simple_assertion.rs create mode 100644 src/experimenting/v9/combinators.rs create mode 100644 src/experimenting/v9/combinators/all.rs create mode 100644 src/experimenting/v9/combinators/any.rs create mode 100644 src/experimenting/v9/combinators/combinator.rs create mode 100644 src/experimenting/v9/combinators/combinator_ext.rs create mode 100644 src/experimenting/v9/combinators/not.rs create mode 100644 src/experimenting/v9/combinators/when_ready.old.rs create mode 100644 src/experimenting/v9/combinators/when_ready.rs create mode 100644 src/experimenting/v9/expect.rs create mode 100644 src/experimenting/v9/failure.rs create mode 100644 src/experimenting/v9/lib.rs create mode 100644 src/experimenting/v9/output.rs create mode 100644 src/experimenting/v9/prelude.rs create mode 100644 src/experimenting/v9/third_party.rs create mode 100644 src/experimenting/v9/third_party/either.rs create mode 100644 src/experimenting/v9/thoughts.md create mode 100644 src/macros.rs create mode 100644 src/metadata.rs create mode 100644 src/metadata/annotated.rs create mode 100644 src/metadata/source_loc.rs create mode 100644 src/prelude.rs create mode 100644 src/specialization/annotated.rs create mode 100644 src/specialization/annotated_autoderef.rs create mode 100644 src/specialization/wrapper.rs 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..48ed258 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,302 @@ # 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 = "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", + "pin-project-lite", + "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-executor", + "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-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[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 = "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 = "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 = "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 = "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" diff --git a/Cargo.toml b/Cargo.toml index c94594a..d74d75c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,4 +3,30 @@ 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", "matchers", "testing"] + +[features] +default = ["futures"] +futures = ["dep:futures"] + +[dependencies] +futures = { version = "0.3.30", optional = true, default-features = false, features = [ + "std", + "async-await", +] } +pin-project-lite = "0.2.14" + +[dev-dependencies] +futures = { version = "0.3.30", default-features = false, features = [ + "std", + "async-await", + "executor", +] } +test-case = "3.3.1" +tokio = { version = "1", features = ["macros", "test-util"] } diff --git a/src/assertions.rs b/src/assertions.rs index 759d71d..8253889 100644 --- a/src/assertions.rs +++ b/src/assertions.rs @@ -1,683 +1,22 @@ -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. +//! +//! [`expect!`]: crate::expect! + +// pub mod functions; +#[cfg(feature = "futures")] +pub mod futures; +pub mod iterators; +// pub mod options; +// pub mod results; +pub mod general; + +mod annotated; +mod assertion; +mod context; +mod error; + +pub use annotated::*; +pub use assertion::*; +pub use context::*; +pub use error::*; diff --git a/src/assertions/annotated.rs b/src/assertions/annotated.rs new file mode 100644 index 0000000..ae7158d --- /dev/null +++ b/src/assertions/annotated.rs @@ -0,0 +1,23 @@ +use crate::metadata::{Annotated, AnnotatedKind}; + +use super::AssertionContext; + +#[doc(hidden)] +pub fn __annotate_assertion( + cx: AssertionContext, + subject: Annotated, + next: fn(AssertionContext, T) -> O, +) -> O { + // Build next context + let mut next_cx = cx.next(); + next_cx.annotate( + "received", + match subject.kind() { + AnnotatedKind::Debug => subject.as_str(), + AnnotatedKind::Stringify => "? (no debug representation)", + }, + ); + + // Call inner assertion + next(next_cx, subject.into_inner()) +} diff --git a/src/assertions/assertion.rs b/src/assertions/assertion.rs new file mode 100644 index 0000000..cff0175 --- /dev/null +++ b/src/assertions/assertion.rs @@ -0,0 +1,25 @@ +use super::AssertionContext; + +/// TODO +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. +/// +/// TODO +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 generally a recursive function. + /// + /// TODO + fn apply(self, cx: AssertionContext, assertion: A) -> Self::Output; +} diff --git a/src/assertions/context.rs b/src/assertions/context.rs new file mode 100644 index 0000000..6e7e8b6 --- /dev/null +++ b/src/assertions/context.rs @@ -0,0 +1,106 @@ +use crate::metadata::{Annotated, AnnotatedKind, SourceLoc}; + +use super::AssertionError; + +/// 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. +#[derive(Clone, Debug)] +pub struct AssertionContext { + source_loc: &'static SourceLoc, + visited: Vec, + remaining: &'static [&'static str], +} + +impl AssertionContext { + #[doc(hidden)] + pub fn __new(source_loc: &'static SourceLoc, frames: &'static [&'static str]) -> Self { + Self { + source_loc, + remaining: frames, + visited: vec![], + } + } + + /// Adds an annotation to this frame. + 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`](core::fmt::Debug) representation. + /// + /// Note that function parameters to modifier functions and assertion + /// functions (the functions the user actually calls) *almost always* have a + /// meaningful string representation. Those values should generally be + /// recorded using [`annotate()`](Self::annotate()) instead. + #[inline] + pub fn try_annotate(&mut self, key: &'static str, value: &Annotated) { + match value.kind() { + AnnotatedKind::Debug => self.annotate(key, value.as_str()), + _ => {} + } + } + + /// Creates a new success value. + #[inline] + pub fn pass(&self) { + // TODO: track success path? somehow recover frames when inverting a + // success into a fail? + } + + // TODO: recover missing frames from an error that was recovered from + // pub fn recover(&mut self, error: AssertionError) {} + // probably need another field for the recovered frames to allow context to + // continue to be propagated. on propagation - lose the recovered frames so + // they don't get mixed in with a different execution path and confuse the + // user + + /// 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] + pub fn fail(&self, message: impl ToString) -> AssertionError { + AssertionError::new( + message.to_string(), + self.source_loc, + self.visited.clone(), + self.remaining, + ) + } + + /// Creates a child context from this assertion context, finalizing this + /// frame's annotations. + 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; + self + } +} + +#[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..465ae91 --- /dev/null +++ b/src/assertions/error.rs @@ -0,0 +1,75 @@ +use std::{ + error::Error, + fmt::{Debug, Display, Formatter}, +}; + +use crate::metadata::SourceLoc; + +use super::ContextFrame; + +/// A simple assertion result. +/// +/// Most assertions either output this type directly, or output a type that +/// wraps this type in some form. +/// +/// Note that not all assertions return this as their output (like asynchronous +/// assertions), but it is the preferred foundational output type for +/// assertions, and 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). +pub type AssertionResult = Result<(), AssertionError>; + +/// An error that can occur during an assertion. +pub struct AssertionError { + message: String, + source_loc: &'static SourceLoc, + visited: Vec, + remaining: &'static [&'static str], +} + +impl AssertionError { + pub(crate) fn new( + message: String, + source_loc: &'static SourceLoc, + visited: Vec, + remaining: &'static [&'static str], + ) -> Self { + Self { + message, + source_loc, + visited, + remaining, + } + } +} + +impl Debug for AssertionError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + writeln!(f, "assertion failed: {}", self.message)?; + writeln!(f, " at: {}", self.source_loc)?; + + // Write visited frames + writeln!(f, "steps:")?; + for frame in &self.visited { + writeln!(f, " {}:", frame.assertion_name)?; + for (key, value) in &frame.annotations { + writeln!(f, " {}: {}", key, value)?; + } + } + + for frame in self.remaining { + writeln!(f, " {frame}: # not visited")?; + } + + Ok(()) + } +} + +impl Display for AssertionError { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + Debug::fmt(self, f) + } +} + +impl Error for AssertionError {} diff --git a/src/assertions/functions.rs b/src/assertions/functions.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/assertions/futures.rs b/src/assertions/futures.rs new file mode 100644 index 0000000..8992aba --- /dev/null +++ b/src/assertions/futures.rs @@ -0,0 +1,20 @@ +//! 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. + +mod finalized_output_future; +mod inverted_output_future; +mod merged_output_future; +mod modifiers; +mod when_ready_future; + +pub use finalized_output_future::*; +pub use inverted_output_future::*; +pub use merged_output_future::*; +pub use modifiers::*; +pub use when_ready_future::*; diff --git a/src/assertions/futures/finalized_output_future.rs b/src/assertions/futures/finalized_output_future.rs new file mode 100644 index 0000000..dfd97d8 --- /dev/null +++ b/src/assertions/futures/finalized_output_future.rs @@ -0,0 +1,45 @@ +use std::{ + future::Future, + pin::Pin, + task::{ready, Context, Poll}, +}; + +use pin_project_lite::pin_project; + +use crate::assertions::general::FinalizableResult; + +pin_project! { + /// Finalizes an asynchronous output. + #[derive(Clone, Debug)] + pub struct FinalizedOutputFuture { + #[pin] + inner: F, + } +} + +impl FinalizedOutputFuture +where + F: Future, + F::Output: FinalizableResult, +{ + /// Create a new finalized output future. + #[inline] + pub fn new(inner: F) -> Self { + Self { inner } + } +} + +impl Future for FinalizedOutputFuture +where + F: Future, + F::Output: FinalizableResult, +{ + type Output = ::Finalized; + + #[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.finalize()) + } +} diff --git a/src/assertions/futures/inverted_output_future.rs b/src/assertions/futures/inverted_output_future.rs new file mode 100644 index 0000000..b6a262c --- /dev/null +++ b/src/assertions/futures/inverted_output_future.rs @@ -0,0 +1,104 @@ +use std::{ + future::Future, + pin::Pin, + task::{ready, Context, Poll}, +}; + +use pin_project_lite::pin_project; + +use crate::assertions::{ + general::{FinalizableResult, InvertibleResult}, + iterators::MergeableResult, + AssertionContext, +}; + +use super::{FinalizedOutputFuture, MergedOutputsFuture}; + +pin_project! { + /// Inverts an asynchronous output. + #[derive(Clone, Debug)] + pub struct InvertedOutputFuture { + #[pin] + inner: F, + cx: Option, + } +} + +impl InvertedOutputFuture +where + F: Future, + F::Output: InvertibleResult, +{ + /// 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, + F::Output: InvertibleResult, +{ + 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 InvertibleResult for InvertedOutputFuture { + type Inverted = F; + + #[inline] + fn invert(self, _cx: AssertionContext) -> Self::Inverted { + // Undo the inversion to preserve context + self.inner + } +} + +impl MergeableResult for InvertedOutputFuture +where + F: Future, + F::Output: InvertibleResult, + ::Inverted: MergeableResult, +{ + type Merged = MergedOutputsFuture; + + #[inline] + fn merge_all(cx: AssertionContext, results: I) -> Self::Merged + where + I: IntoIterator, + { + MergedOutputsFuture::all(cx, results) + } + + #[inline] + fn merge_any(cx: AssertionContext, results: I) -> Self::Merged + where + I: IntoIterator, + { + MergedOutputsFuture::any(cx, results) + } +} + +impl FinalizableResult for InvertedOutputFuture +where + F: Future, + F::Output: InvertibleResult, + ::Inverted: FinalizableResult, +{ + type Finalized = FinalizedOutputFuture; + + #[inline] + fn finalize(self) -> Self::Finalized { + FinalizedOutputFuture::new(self) + } +} diff --git a/src/assertions/futures/merged_output_future.rs b/src/assertions/futures/merged_output_future.rs new file mode 100644 index 0000000..c730b53 --- /dev/null +++ b/src/assertions/futures/merged_output_future.rs @@ -0,0 +1,144 @@ +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::{ + general::{FinalizableResult, InvertibleResult}, + iterators::MergeableResult, + AssertionContext, +}; + +use super::{FinalizedOutputFuture, InvertedOutputFuture}; + +pin_project! { + /// Merges many asynchronous outputs. + #[derive(Debug)] + pub struct MergedOutputsFuture + where + F: Future, + { + #[pin] + inner: Collect, Vec>, + cx: Option, + merge_kind: MergeKind, + } +} + +impl MergedOutputsFuture +where + F: Future, + F::Output: MergeableResult, +{ + /// Creates a new merged outputs future using + /// [`merge_all`](MergeableResult::merge_all()). + #[inline] + pub fn all(cx: AssertionContext, futs: I) -> Self + where + I: IntoIterator, + { + Self { + inner: FuturesUnordered::from_iter(futs.into_iter()).collect(), + cx: Some(cx), + merge_kind: MergeKind::All, + } + } + + /// Creates a new merged outputs future using + /// [`merge_any`](MergeableResult::merge_any()). + #[inline] + pub fn any(cx: AssertionContext, futs: I) -> Self + where + I: IntoIterator, + { + Self { + inner: FuturesUnordered::from_iter(futs.into_iter()).collect(), + cx: Some(cx), + merge_kind: MergeKind::Any, + } + } +} + +impl Future for MergedOutputsFuture +where + F: Future, + F::Output: MergeableResult, +{ + 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(match projected.merge_kind { + MergeKind::All => F::Output::merge_all(cx, outputs), + MergeKind::Any => F::Output::merge_any(cx, outputs), + }) + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum MergeKind { + All, + Any, +} + +impl InvertibleResult for MergedOutputsFuture +where + F: Future, + F::Output: MergeableResult, + ::Merged: InvertibleResult, +{ + type Inverted = InvertedOutputFuture; + + #[inline] + fn invert(self, cx: AssertionContext) -> Self::Inverted { + InvertedOutputFuture::new(cx, self) + } +} + +impl MergeableResult for MergedOutputsFuture +where + F: Future, + F::Output: MergeableResult, + ::Merged: MergeableResult, +{ + type Merged = MergedOutputsFuture; + + #[inline] + fn merge_all(cx: AssertionContext, results: I) -> Self::Merged + where + I: IntoIterator, + { + MergedOutputsFuture::all(cx, results) + } + + #[inline] + fn merge_any(cx: AssertionContext, results: I) -> Self::Merged + where + I: IntoIterator, + { + MergedOutputsFuture::any(cx, results) + } +} + +impl FinalizableResult for MergedOutputsFuture +where + F: Future, + F::Output: MergeableResult, + ::Merged: FinalizableResult, +{ + type Finalized = FinalizedOutputFuture; + + #[inline] + fn finalize(self) -> Self::Finalized { + FinalizedOutputFuture::new(self) + } +} diff --git a/src/assertions/futures/modifiers.rs b/src/assertions/futures/modifiers.rs new file mode 100644 index 0000000..440f114 --- /dev/null +++ b/src/assertions/futures/modifiers.rs @@ -0,0 +1,41 @@ +use std::future::Future; + +use crate::assertions::AssertionContext; + +use super::WhenReadyFuture; + +/// 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 core::future::ready; +/// # futures::executor::block_on(async { +/// 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 core::future::ready; +/// # futures::executor::block_on(async { +/// expect!(ready(ready(1)), when_ready, when_ready, to_equal(1)).await.await; +/// # }) +/// ``` +#[inline] +pub fn when_ready( + cx: AssertionContext, + subject: T, + next: fn(AssertionContext, T::Output) -> O, +) -> WhenReadyFuture +where + T: Future, +{ + WhenReadyFuture::new(cx, subject, next) +} diff --git a/src/assertions/futures/when_ready_future.rs b/src/assertions/futures/when_ready_future.rs new file mode 100644 index 0000000..586fd1a --- /dev/null +++ b/src/assertions/futures/when_ready_future.rs @@ -0,0 +1,112 @@ +use std::{ + future::Future, + pin::Pin, + task::{ready, Context, Poll}, +}; + +use pin_project_lite::pin_project; + +use crate::assertions::{ + general::{FinalizableResult, InvertibleResult}, + iterators::MergeableResult, + AssertionContext, +}; + +use super::{FinalizedOutputFuture, InvertedOutputFuture, MergedOutputsFuture}; + +pin_project! { + /// A [`Future`] which executes an assertion when its subject is ready. + #[derive(Clone, Debug)] + pub struct WhenReadyFuture + where + T: Future, + { + #[pin] + subject: T, + cx: Option, + next: fn(AssertionContext, T::Output) -> O, + } +} + +impl WhenReadyFuture +where + T: Future, +{ + /// Creates a new instance of this future. + #[inline] + pub(crate) fn new( + cx: AssertionContext, + subject: T, + next: fn(AssertionContext, T::Output) -> O, + ) -> Self { + Self { + subject, + cx: Some(cx), + next, + } + } +} + +impl Future for WhenReadyFuture +where + T: Future, +{ + type Output = O; + + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { + let projected = self.project(); + let input = ready!(projected.subject.poll(cx)); + let cx = projected.cx.take().expect("poll after ready"); + Poll::Ready((projected.next)(cx, input)) + } +} + +impl InvertibleResult for WhenReadyFuture +where + T: Future, + O: InvertibleResult, +{ + type Inverted = InvertedOutputFuture; + + #[inline] + fn invert(self, cx: AssertionContext) -> Self::Inverted { + InvertedOutputFuture::new(cx, self) + } +} + +impl MergeableResult for WhenReadyFuture +where + T: Future, + O: MergeableResult, +{ + type Merged = MergedOutputsFuture; + + #[inline] + fn merge_all(cx: AssertionContext, results: I) -> Self::Merged + where + I: IntoIterator, + { + MergedOutputsFuture::all(cx, results) + } + + #[inline] + fn merge_any(cx: AssertionContext, results: I) -> Self::Merged + where + I: IntoIterator, + { + MergedOutputsFuture::any(cx, results) + } +} + +impl FinalizableResult for WhenReadyFuture +where + T: Future, + O: FinalizableResult, +{ + type Finalized = FinalizedOutputFuture; + + #[inline] + fn finalize(self) -> Self::Finalized { + FinalizedOutputFuture::new(self) + } +} diff --git a/src/assertions/general.rs b/src/assertions/general.rs new file mode 100644 index 0000000..3034e5e --- /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 comparisons; +mod finalizable_result; +mod invertible_result; +mod modifiers; + +pub use comparisons::*; +pub use finalizable_result::*; +pub use invertible_result::*; +pub use modifiers::*; diff --git a/src/assertions/general/comparisons.rs b/src/assertions/general/comparisons.rs new file mode 100644 index 0000000..1c76d5f --- /dev/null +++ b/src/assertions/general/comparisons.rs @@ -0,0 +1,159 @@ +use crate::{ + assertions::{AssertionContext, AssertionResult}, + metadata::Annotated, +}; + +/// Asserts that the subject 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] +pub fn to_equal(expected: Annotated) -> impl FnOnce(AssertionContext, T) -> AssertionResult +where + T: PartialEq, +{ + move |mut cx, subject| { + cx.annotate("expected", expected.as_str()); + + if subject == expected.into_inner() { + Ok(()) + } else { + Err(cx.fail("subject is not equal to value")) + } + } +} + +/// 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] +pub fn to_be_less_than( + boundary: Annotated, +) -> impl FnOnce(AssertionContext, T) -> AssertionResult +where + T: PartialOrd, +{ + move |mut cx, subject| { + cx.annotate("boundary", &boundary); + + if subject < boundary.into_inner() { + Ok(()) + } else { + Err(cx.fail("subject is not less than value")) + } + } +} + +/// 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] +pub fn to_be_less_than_or_equal_to( + boundary: Annotated, +) -> impl FnOnce(AssertionContext, T) -> AssertionResult +where + T: PartialOrd, +{ + move |mut cx, subject| { + cx.annotate("boundary", &boundary); + + if subject <= boundary.into_inner() { + Ok(()) + } else { + Err(cx.fail("subject is not less than or equal to value")) + } + } +} + +/// 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] +pub fn to_be_greater_than( + boundary: Annotated, +) -> impl FnOnce(AssertionContext, T) -> AssertionResult +where + T: PartialOrd, +{ + move |mut cx, subject| { + cx.annotate("boundary", &boundary); + + if subject > boundary.into_inner() { + Ok(()) + } else { + Err(cx.fail("subject is not greater than value")) + } + } +} + +/// 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] +pub fn to_be_greater_than_or_equal_to( + boundary: Annotated, +) -> impl FnOnce(AssertionContext, T) -> AssertionResult +where + T: PartialOrd, +{ + move |mut cx, subject| { + cx.annotate("boundary", &boundary); + + if subject >= boundary.into_inner() { + Ok(()) + } else { + Err(cx.fail("subject is not greater than or equal to value")) + } + } +} diff --git a/src/assertions/general/finalizable_result.rs b/src/assertions/general/finalizable_result.rs new file mode 100644 index 0000000..045abe7 --- /dev/null +++ b/src/assertions/general/finalizable_result.rs @@ -0,0 +1,34 @@ +use std::fmt::Debug; + +/// An assertion result that can be finalized. +/// +/// Finalizing the result "unwraps" the result. For actual [`Result`] +/// results, this is literally the act of [unwrapping](Result::unwrap()) the +/// result, but other result types may choose to finalize in a different manner +/// (like unwrapping the result once it's available in the case of asynchronous +/// results). +/// +/// The purpose of finalizing the result is to panic as soon as possible if an +/// assertion fails. Not all results will be finalized, but if they are +/// finalized, they should provide output to the user as soon as possible if the +/// assertion failed. +pub trait FinalizableResult { + type Finalized; + + /// Finalizes this result. + fn finalize(self) -> Self::Finalized; +} + +impl FinalizableResult for Result +where + E: Debug, +{ + type Finalized = T; + + fn finalize(self) -> Self::Finalized { + match self { + Ok(t) => t, + Err(e) => panic!("{e:?}"), + } + } +} diff --git a/src/assertions/general/invertible_result.rs b/src/assertions/general/invertible_result.rs new file mode 100644 index 0000000..306b53d --- /dev/null +++ b/src/assertions/general/invertible_result.rs @@ -0,0 +1,34 @@ +use crate::{assertions::AssertionContext, AssertionResult}; + +/// An assertion result that can be inverted. +/// +/// An inverted result is swapped from a failure to a success, or from a success +/// to a failure. +pub trait InvertibleResult { + /// The inverted result. + type Inverted; + + /// Inverts the result. + /// + /// A success is converted to a failure, and a failure is converted to a + /// success. + /// + /// ## Async + /// + /// If it is not yet known whether the result represents a success or + /// failure, then a value is returned that inverts that result when it is + /// known. + fn invert(self, cx: AssertionContext) -> Self::Inverted; +} + +impl InvertibleResult for AssertionResult { + type Inverted = AssertionResult; + + #[inline] + fn invert(self, cx: AssertionContext) -> Self::Inverted { + match self { + Ok(()) => Err(cx.fail("expected a failure, received a success")), + Err(_) => Ok(cx.pass()), + } + } +} diff --git a/src/assertions/general/modifiers.rs b/src/assertions/general/modifiers.rs new file mode 100644 index 0000000..fe7edc2 --- /dev/null +++ b/src/assertions/general/modifiers.rs @@ -0,0 +1,69 @@ +use crate::{assertions::AssertionContext, metadata::Annotated}; + +use super::InvertibleResult; + +/// 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)); +/// ``` +#[inline] +pub fn not( + cx: AssertionContext, + subject: T, + next: fn(AssertionContext, T) -> O, +) -> O::Inverted +where + O: InvertibleResult, +{ + let output = next(cx.clone(), subject); + output.invert(cx) +} + +/// 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')); +/// ``` +#[inline] +pub fn map( + mut f: Annotated, +) -> impl FnOnce(AssertionContext, T, fn(AssertionContext, U) -> O) -> O +where + F: FnMut(T) -> U, +{ + move |mut cx, subject, next| { + cx.annotate("function", &f); + let subject = (f.inner_mut())(subject); + next(cx, subject) + } +} diff --git a/src/assertions/iterators.rs b/src/assertions/iterators.rs new file mode 100644 index 0000000..ade58d1 --- /dev/null +++ b/src/assertions/iterators.rs @@ -0,0 +1,10 @@ +//! Assertions and modifiers for tests that involve iterators. +//! +//! This module contains utilities for manipulating and executing assertions on +//! iterators. + +mod mergeable_result; +mod modifiers; + +pub use mergeable_result::*; +pub use modifiers::*; diff --git a/src/assertions/iterators/mergeable_result.rs b/src/assertions/iterators/mergeable_result.rs new file mode 100644 index 0000000..bc78944 --- /dev/null +++ b/src/assertions/iterators/mergeable_result.rs @@ -0,0 +1,59 @@ +use crate::{assertions::AssertionContext, AssertionResult}; + +/// A type of result that can be collected from an iterator into a merged result +/// value. +/// +/// This is the core of how modifiers like [`all`] and [`any`] work. Results +/// that implement this trait can be collected from an iterator into a new +/// result following one of two strategies: +/// +/// - `all`: the merged result succeeds if none of the original results were +/// failures. +/// - `any`: the merged result succeeds if at least one of the original results +/// was a failure. +/// +/// [`all`]: crate::prelude::all +/// [`any`]: crate::prelude::any +pub trait MergeableResult { + /// The type of the merged result. + type Merged; + + /// Merges an iterator of results. The output represents a success if and + /// only if none of the constituent results failed. + fn merge_all(cx: AssertionContext, results: I) -> Self::Merged + where + I: IntoIterator; + + /// Merges an iterator of results. The output represents a success if and + /// only if at least one of the constituent results succeeded. + fn merge_any(cx: AssertionContext, results: I) -> Self::Merged + where + I: IntoIterator; +} + +impl MergeableResult for AssertionResult { + type Merged = AssertionResult; + + #[inline] + fn merge_all(_cx: AssertionContext, results: I) -> Self::Merged + where + I: IntoIterator, + { + results.into_iter().collect() + } + + fn merge_any(cx: AssertionContext, results: I) -> Self::Merged + where + I: IntoIterator, + { + let mut error = cx.fail("no results"); + for result in results { + match result { + Ok(()) => return Ok(()), + Err(e) => error = e, + } + } + + Err(error) + } +} diff --git a/src/assertions/iterators/modifiers.rs b/src/assertions/iterators/modifiers.rs new file mode 100644 index 0000000..25598d1 --- /dev/null +++ b/src/assertions/iterators/modifiers.rs @@ -0,0 +1,144 @@ +use crate::{assertions::AssertionContext, metadata::Annotated, AssertionResult}; + +use super::MergeableResult; + +/// 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)); +/// ``` +/// +/// This method panics if any element does not satisfy the assertion: +/// +/// ```should_panic +/// # use expecters::prelude::*; +/// expect!([1, 3, 5], all, to_equal(5)); +/// ``` +#[inline] +pub fn all( + cx: AssertionContext, + subject: T, + next: fn(AssertionContext, T::Item) -> O, +) -> O::Merged +where + T: IntoIterator, + O: MergeableResult, +{ + O::merge_all( + cx.clone(), + subject.into_iter().enumerate().map(|(idx, item)| { + let mut cx = cx.clone(); + cx.annotate("index", idx); + next(cx, item) + }), + ) +} + +/// 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)); +/// ``` +/// +/// This method panics if any element does not satisfy the assertion: +/// +/// ```should_panic +/// # use expecters::prelude::*; +/// expect!([1, 3, 5], any, to_equal(4)); +/// ``` +#[inline] +pub fn any( + cx: AssertionContext, + subject: T, + next: fn(AssertionContext, T::Item) -> O, +) -> O::Merged +where + T: IntoIterator, + O: MergeableResult, +{ + O::merge_any( + cx.clone(), + subject.into_iter().enumerate().map(|(idx, item)| { + let mut cx = cx.clone(); + cx.annotate("index", idx); + next(cx, item) + }), + ) +} + +/// Counts the length of the subject, and executes an assertion on the result. +/// +/// 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)); +/// ``` +#[inline] +pub fn count( + mut cx: AssertionContext, + subject: T, + next: fn(AssertionContext, usize) -> O, +) -> O +where + T: IntoIterator, +{ + let count = subject.into_iter().count(); + cx.annotate("count", count); + next(cx, count) +} + +/// 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)); +/// ``` +#[inline] +pub fn nth( + idx: Annotated, +) -> impl FnOnce(AssertionContext, T, fn(AssertionContext, T::Item) -> AssertionResult) -> AssertionResult +where + T: IntoIterator, +{ + move |mut cx, subject, next| { + cx.annotate("index", idx.as_str()); + let item = subject + .into_iter() + .nth(idx.into_inner()) + .ok_or_else(|| cx.fail("index out of bounds"))?; + next(cx, item) + } +} diff --git a/src/assertions/options.rs b/src/assertions/options.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/assertions/results.rs b/src/assertions/results.rs new file mode 100644 index 0000000..e69de29 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/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/experimenting/assertions.rs b/src/experimenting/assertions.rs new file mode 100644 index 0000000..759d71d --- /dev/null +++ b/src/experimenting/assertions.rs @@ -0,0 +1,683 @@ +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); + } +} diff --git a/src/combinators.rs b/src/experimenting/combinators.rs similarity index 92% rename from src/combinators.rs rename to src/experimenting/combinators.rs index c99d36c..7fd36ae 100644 --- a/src/combinators.rs +++ b/src/experimenting/combinators.rs @@ -15,7 +15,6 @@ mod nth; mod ok; mod some; mod when_called; -// mod when_ready; pub use all::*; pub use any::*; @@ -28,4 +27,3 @@ pub use nth::*; pub use ok::*; pub use some::*; pub use when_called::*; -// pub use when_ready::*; diff --git a/src/experimenting/combinators/all.rs b/src/experimenting/combinators/all.rs new file mode 100644 index 0000000..4de2238 --- /dev/null +++ b/src/experimenting/combinators/all.rs @@ -0,0 +1,179 @@ +use std::{ + fmt::Display, + future::{ready, Future}, + pin::Pin, +}; + +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)), + ) + } +} + +//////////// + +pub trait Assertion2 { + type Output; + + /// Executes this assertion. This takes an input, performs some kind of + /// transformation on it, then produces a new output. + fn execute(self, input: Input) -> Self::Output; +} + +impl Assertion2 for F +where + F: FnOnce(I) -> O, +{ + type Output = O; + + /// Executes this assertion. This takes an input, performs some kind of + /// transformation on it, then produces a new output. + fn execute(self, input: I) -> Self::Output { + self(input) + } +} + +pub trait Combinator2 { + type Output; + + /// Applies this combinator, passing an input into the given assertion and + /// returning the transformed output. + fn apply(self, assertion: A) -> Self::Output; +} + +struct Count { + prev: Prev, +} + +impl Combinator2 for Count +where + Prev: Combinator2>, +{ + type Output = Prev::Output; + + fn apply(self, assertion: A) -> Self::Output { + self.prev.apply(CountAssertion { next: assertion }) + } +} + +struct CountAssertion { + next: Next, +} + +impl Assertion2 for CountAssertion +where + Input: IntoIterator, + Next: Assertion2, +{ + type Output = Next::Output; + + fn execute(self, input: Input) -> Self::Output { + self.next.execute(input.into_iter().count()) + } +} + +struct WhenReady { + prev: Prev, +} + +impl Combinator2 for WhenReady +where + Prev: Combinator2>, +{ + type Output = Prev::Output; + + fn apply(self, assertion: A) -> Self::Output { + self.prev.apply(WhenReadyAssertion { next: assertion }) + } +} + +struct WhenReadyAssertion { + next: Next, +} + +impl Assertion2 for WhenReadyAssertion +where + Input: Future + Send + 'static, + Next: Assertion2<::Output> + Send + 'static, +{ + type Output = Pin< + Box::Output>>::Output> + Send>, + >; + + fn execute(self, input: Input) -> Self::Output { + Box::pin(async move { + let input = input.await; + self.next.execute(input) + }) + } +} + +struct Root { + target: T, +} + +impl Combinator2 for Root +where + A: Assertion2, +{ + type Output = A::Output; + + fn apply(self, assertion: A) -> Self::Output { + assertion.execute(self.target) + } +} + +async fn foo() { + let root = Root { target: [1, 2, 3] }; + let combinator = Count { prev: root }; + let _result = combinator.apply(|count| count > 0); + + let assertion = WhenReadyAssertion { + next: CountAssertion { + next: |count| count > 0, + }, + }; + let _result = assertion.execute(ready([1, 2, 3])).await; + + let root = Root { + target: ready([1i32, 2, 3]), + }; + let combinator = Count { + prev: WhenReady { prev: root }, + }; + let _result = combinator.apply(|len| len > 0).await; +} diff --git a/src/combinators/any.rs b/src/experimenting/combinators/any.rs similarity index 100% rename from src/combinators/any.rs rename to src/experimenting/combinators/any.rs diff --git a/src/combinators/at_path.rs b/src/experimenting/combinators/at_path.rs similarity index 100% rename from src/combinators/at_path.rs rename to src/experimenting/combinators/at_path.rs diff --git a/src/combinators/count.rs b/src/experimenting/combinators/count.rs similarity index 87% rename from src/combinators/count.rs rename to src/experimenting/combinators/count.rs index 8e7c396..5a6f084 100644 --- a/src/combinators/count.rs +++ b/src/experimenting/combinators/count.rs @@ -6,14 +6,14 @@ use crate::Assertable; /// in the target. #[derive(Clone, Debug)] pub struct CountCombinator { - inner: Inner, + prev: Inner, } impl CountCombinator { /// Creates a new combinator which wraps an inner [`Assertable`]. #[inline] - pub fn new(inner: Inner) -> Self { - Self { inner } + pub fn new(prev: Inner) -> Self { + Self { prev } } } @@ -29,7 +29,7 @@ where where F: FnMut(Self::Target) -> bool, { - self.inner.to_satisfy( + self.prev.to_satisfy( format_args!("the length satisfies: {expectation}"), |values| f(values.into_iter().count()), ) diff --git a/src/combinators/err.rs b/src/experimenting/combinators/err.rs similarity index 100% rename from src/combinators/err.rs rename to src/experimenting/combinators/err.rs diff --git a/src/combinators/map.rs b/src/experimenting/combinators/map.rs similarity index 100% rename from src/combinators/map.rs rename to src/experimenting/combinators/map.rs diff --git a/src/combinators/not.rs b/src/experimenting/combinators/not.rs similarity index 100% rename from src/combinators/not.rs rename to src/experimenting/combinators/not.rs diff --git a/src/combinators/nth.rs b/src/experimenting/combinators/nth.rs similarity index 100% rename from src/combinators/nth.rs rename to src/experimenting/combinators/nth.rs diff --git a/src/combinators/ok.rs b/src/experimenting/combinators/ok.rs similarity index 100% rename from src/combinators/ok.rs rename to src/experimenting/combinators/ok.rs diff --git a/src/combinators/some.rs b/src/experimenting/combinators/some.rs similarity index 100% rename from src/combinators/some.rs rename to src/experimenting/combinators/some.rs diff --git a/src/combinators/when_called.rs b/src/experimenting/combinators/when_called.rs similarity index 100% rename from src/combinators/when_called.rs rename to src/experimenting/combinators/when_called.rs diff --git a/src/experimenting/combinators2.rs b/src/experimenting/combinators2.rs new file mode 100644 index 0000000..ae9043f --- /dev/null +++ b/src/experimenting/combinators2.rs @@ -0,0 +1,13 @@ +mod aggregate; +mod combinator; +mod count; +mod identity; +mod plan; +mod when_ready; + +pub use aggregate::*; +pub use combinator::*; +pub use count::*; +pub use identity::*; +pub use plan::*; +pub use when_ready::*; diff --git a/src/experimenting/combinators2/aggregate.rs b/src/experimenting/combinators2/aggregate.rs new file mode 100644 index 0000000..33f6720 --- /dev/null +++ b/src/experimenting/combinators2/aggregate.rs @@ -0,0 +1,150 @@ +use std::{ + fmt::{Display, Formatter}, + ops::ControlFlow, +}; + +use crate::AssertionErrorBuilder; + +use super::{Assertion, Combinator}; + +#[derive(Clone, Copy, Debug, Default)] +pub struct AggregateCombinator { + aggregator: A, +} + +impl AggregateCombinator { + /// Creates a new [`AggregateCombinator`]. + pub fn new(aggregator: A) -> Self { + Self { aggregator } + } +} + +impl Combinator for AggregateCombinator { + type Assertion = AggregateAssertion; + + fn build(self, next: Next) -> Self::Assertion { + AggregateAssertion::new(self.aggregator, next) + } +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct AggregateAssertion { + aggregator: A, + next: Next, +} + +impl AggregateAssertion { + /// Creates a new [`AggregateAssertion`]. + pub fn new(aggregator: A, next: Next) -> Self { + Self { aggregator, next } + } +} + +impl Display for AggregateAssertion +where + A: Display, + Next: Display, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "for {}, {}", self.aggregator, self.next) + } +} + +impl Assertion for AggregateAssertion +where + Input: IntoIterator, + Next: Assertion + Clone, + A: Aggregator, +{ + type Output = A::Aggregate; + + fn execute(mut self, input: Input) -> Self::Output { + let output = input + .into_iter() + .map(|input| self.next.clone().execute(input)) + .try_fold(self.aggregator.zero(), |aggregate, output| { + self.aggregator.with_output(aggregate, output) + }); + + match output { + ControlFlow::Continue(aggregate) => aggregate, + ControlFlow::Break(aggregate) => aggregate, + } + } +} + +pub trait Aggregator: Display { + type Aggregate; + + fn zero(&mut self) -> Self::Aggregate; + + fn with_output( + &mut self, + aggregate: Self::Aggregate, + next: Output, + ) -> ControlFlow; +} + +/// Aggregate function that requires all assertions to be successful. +#[derive(Clone, Copy, Debug, Default)] +pub struct AllSucceed; + +impl Display for AllSucceed { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "each inner value") + } +} + +impl Aggregator> for AllSucceed { + type Aggregate = Result<(), AssertionErrorBuilder>; + + /// The "zero value" for the aggregation. This is what the aggregation + /// starts with before any outputs are processed. If there are no outputs + /// to process, this is also the final result. + fn zero(&mut self) -> Self::Aggregate { + Ok(()) + } + + /// Combine the current aggregate with the next output. At any point, the + /// aggregation can short-circuit and return the final result by returning + /// [`ControlFlow::Break`]. + fn with_output( + &mut self, + _: Self::Aggregate, + next: Result<(), AssertionErrorBuilder>, + ) -> ControlFlow { + match next { + Ok(_) => ControlFlow::Continue(Ok(())), + Err(e) => ControlFlow::Break(Err(e)), + } + } +} + +/// Aggregate function that requires any assertions to be successful. +#[derive(Clone, Copy, Debug, Default)] +pub struct AnySucceed; + +impl Display for AnySucceed { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "some inner value") + } +} + +impl Aggregator> for AnySucceed { + type Aggregate = Result<(), AssertionErrorBuilder>; + + fn zero(&mut self) -> Self::Aggregate { + Err(AssertionErrorBuilder::default().with_field("reason", "no assertion succeeded")) + } + + fn with_output( + &mut self, + aggregate: Self::Aggregate, + next: Result<(), AssertionErrorBuilder>, + ) -> ControlFlow { + match next { + Ok(_) => ControlFlow::Break(Ok(())), + Err(_) => ControlFlow::Continue(aggregate), + } + } +} diff --git a/src/experimenting/combinators2/combinator.rs b/src/experimenting/combinators2/combinator.rs new file mode 100644 index 0000000..8ed9f2b --- /dev/null +++ b/src/experimenting/combinators2/combinator.rs @@ -0,0 +1,90 @@ +use std::fmt::{Display, Formatter}; + +/// A transformer for an assertion. +/// +/// Combinators are used to transform the target value while executing an +/// assertion. This can range from something as simple as getting the length of +/// the input value to executing future assertions asynchronously once the +/// target value is ready. It can even execute future assertions multiple times +/// and aggregate the results. +/// +/// These are the core of how the input value is transformed, how more complex +/// assertions can be built, and how the final result is produced. +pub trait Combinator { + /// The assertion that is produced by this combinator when it is used to + /// transform another assertion. + type Assertion; + + /// Builds the assertion that is produced by this combinator. + /// + /// This method is used to transform the next assertion in the chain in some + /// manner dependent on the functionality of the combinator. For example, + /// the assertion returned by this method may transform the input passed to + /// the next assertion or even execute it multiple times. + fn build(self, next: Next) -> Self::Assertion; +} + +/// An assertion that can be executed on an input value. +/// +/// The assertion produces a result often indicative of whether the assertion +/// passed or failed. While in most cases the result is literally a [`Result`], +/// it can sometimes also be other types, such as a future that resolves to a +/// result. +/// +/// ## Display +/// +/// Assertions must implement [`Display`]. This allows the expectation message +/// to be communicated to the user in a human-readable format. The output format +/// should be a short description of what is expected of the input. For example, +/// the output format for an assertion that checks whether a value is true could +/// be: `"the value is true"`. +/// +/// Combinators will want to ensure the expectation message includes the next +/// assertion's message as well, meaning the expectation message will often be +/// created by adding some text to the next assertion's message. For example, a +/// combinator that maps an input iterator to the number of elements in the +/// iterator could have an output format of: `"the length satisfies: {next}"`, +/// where `{next}` indicates the next assertion's expectation. +pub trait Assertion: Display { + /// The output produced by the assertion. This is usually either a + /// [`Result<(), AssertError>`] or a future which resolves to one. + type Output; + + /// Executes the assertion on the input value. + fn execute(self, input: Input) -> Self::Output; +} + +/// A simple assertion constructed using a callback function. +#[derive(Clone, Debug)] +pub struct AssertionFn { + expectation: String, + f: F, +} + +impl AssertionFn { + /// Creates a new [`AssertionFn`]. + #[inline] + pub fn new(expectation: impl Display, f: F) -> Self { + Self { + expectation: expectation.to_string(), + f, + } + } +} + +impl Display for AssertionFn { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.expectation) + } +} + +impl Assertion for AssertionFn +where + F: FnOnce(I) -> O, +{ + type Output = O; + + fn execute(self, input: I) -> Self::Output { + (self.f)(input) + } +} diff --git a/src/experimenting/combinators2/count.rs b/src/experimenting/combinators2/count.rs new file mode 100644 index 0000000..6d19bb0 --- /dev/null +++ b/src/experimenting/combinators2/count.rs @@ -0,0 +1,51 @@ +use std::fmt::{Display, Formatter}; + +use super::{Assertion, Combinator}; + +/// Counts the number of elements in the input. +#[derive(Clone, Copy, Debug, Default)] +pub struct CountCombinator; + +impl Combinator for CountCombinator { + type Assertion = CountAssertion; + + fn build(self, next: Next) -> Self::Assertion { + CountAssertion::new(next) + } +} + +/// Counts the number of elements in the input, then passes the count to the +/// next assertion. +#[derive(Clone, Copy, Debug, Default)] +pub struct CountAssertion { + next: Next, +} + +impl CountAssertion { + /// Creates a new [`CountAssertion`]. + #[inline] + pub fn new(next: Next) -> Self { + Self { next } + } +} + +impl Display for CountAssertion +where + Next: Display, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "the length satisfies: {}", self.next) + } +} + +impl Assertion for CountAssertion +where + Input: IntoIterator, + Next: Assertion, +{ + type Output = Next::Output; + + fn execute(self, input: Input) -> Self::Output { + self.next.execute(input.into_iter().count()) + } +} diff --git a/src/experimenting/combinators2/identity.rs b/src/experimenting/combinators2/identity.rs new file mode 100644 index 0000000..06b0fba --- /dev/null +++ b/src/experimenting/combinators2/identity.rs @@ -0,0 +1,16 @@ +use super::Combinator; + +/// A combinator that does nothing. +/// +/// This combinator is used to terminate a chain of combinators. It outputs the +/// assertion that was passed to it. +#[derive(Clone, Copy, Debug, Default)] +pub struct IdentityCombinator; + +impl Combinator for IdentityCombinator { + type Assertion = Next; + + fn build(self, next: Next) -> Self::Assertion { + next + } +} diff --git a/src/experimenting/combinators2/plan.rs b/src/experimenting/combinators2/plan.rs new file mode 100644 index 0000000..a1c9a04 --- /dev/null +++ b/src/experimenting/combinators2/plan.rs @@ -0,0 +1,34 @@ +use super::Combinator; + +/// Chains two combinators together. +/// +/// This is used to control the order that an input is passed through a chain of +/// combinators. The `Outer` combinator is executed first, then the output is +/// passed to the `Inner` combinator. The output of the `Inner` combinator is +/// returned by the plan. +pub struct Plan { + outer: Outer, + inner: Inner, +} + +impl Plan { + /// Creates a new [`Plan`] combinator. + pub fn new(prev: Outer, next: Inner) -> Self { + Self { + outer: prev, + inner: next, + } + } +} + +impl Combinator for Plan +where + Outer: Combinator, + Next1: Combinator, +{ + type Assertion = Outer::Assertion; + + fn build(self, next: Next2) -> Self::Assertion { + self.outer.build(self.inner.build(next)) + } +} diff --git a/src/experimenting/combinators2/when_ready.rs b/src/experimenting/combinators2/when_ready.rs new file mode 100644 index 0000000..95ff68d --- /dev/null +++ b/src/experimenting/combinators2/when_ready.rs @@ -0,0 +1,84 @@ +use std::{ + fmt::{Display, Formatter}, + future::Future, + pin::Pin, + task::{ready, Context, Poll}, +}; + +use pin_project_lite::pin_project; + +use super::{Assertion, Combinator}; + +/// Performs the assertion when the input future is ready. +#[derive(Clone, Copy, Debug, Default)] +pub struct WhenReadyCombinator; + +impl Combinator for WhenReadyCombinator { + type Assertion = WhenReadyAssertion; + + fn build(self, next: Next) -> Self::Assertion { + WhenReadyAssertion::new(next) + } +} + +/// Waits for the input to be ready, then passes it to the next assertion. +#[derive(Clone, Copy, Debug, Default)] +pub struct WhenReadyAssertion { + next: Next, +} + +impl WhenReadyAssertion { + /// Creates a new [`WhenReadyAssertion`]. + #[inline] + pub fn new(next: Next) -> Self { + Self { next } + } +} + +impl Display for WhenReadyAssertion +where + Next: Display, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "when ready, {}", self.next) + } +} + +impl Assertion for WhenReadyAssertion +where + Input: Future, + Next: Assertion, +{ + type Output = WhenReadyFuture; + + fn execute(self, input: Input) -> Self::Output { + WhenReadyFuture { + input, + next: Some(self.next), + } + } +} + +pin_project! { + /// A future that performs an assertion when it is ready. + pub struct WhenReadyFuture { + #[pin] + input: Input, + next: Option, + } +} + +impl Future for WhenReadyFuture +where + Input: Future, + Next: Assertion, +{ + type Output = Next::Output; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let projected = self.project(); + let res = ready!(projected.input.poll(cx)); + let next = projected.next.take().expect("polled after ready"); + Poll::Ready(next.execute(res)) + } +} diff --git a/src/experimenting/error.rs b/src/experimenting/error.rs new file mode 100644 index 0000000..e9ef9ad --- /dev/null +++ b/src/experimenting/error.rs @@ -0,0 +1,64 @@ +use std::{ + error::Error, + 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 AssertionError { + fields: Vec<(&'static str, String)>, +} + +impl AssertionError { + /// Creates a builder for a new [`AssertionError`]. + pub fn builder() -> AssertionErrorBuilder { + AssertionErrorBuilder::default() + } +} + +impl Display for AssertionError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + writeln!(f, "assertion failed.")?; + for (name, value) in &self.fields { + writeln!(f, " {name}: {value}")?; + } + + Ok(()) + } +} + +impl Error for AssertionError {} + +/// A builder for an [`AssertionError`]. +#[derive(Clone, Debug)] +pub struct AssertionErrorBuilder { + fields: Vec<(&'static str, String)>, +} + +impl Default for AssertionErrorBuilder { + fn default() -> Self { + Self { + fields: vec![("expected", String::new())], + } + } +} + +impl AssertionErrorBuilder { + /// 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 Display) -> Self { + self.fields.push((name, value.to_string())); + self + } + + /// Builds the error with the given expectation. + pub fn build(mut self, expectation: impl Display) -> AssertionError { + self.fields[0].1 = expectation.to_string(); + AssertionError { + fields: self.fields, + } + } +} diff --git a/src/expect.rs b/src/experimenting/expect.rs similarity index 94% rename from src/expect.rs rename to src/experimenting/expect.rs index bbb9002..7c083aa 100644 --- a/src/expect.rs +++ b/src/experimenting/expect.rs @@ -1,6 +1,6 @@ use std::fmt::{Display, Formatter}; -use crate::{AssertError, Assertable}; +use crate::{Assertable, AssertionError}; /// Begins an assertion. /// @@ -48,8 +48,6 @@ macro_rules! expect { }; } -// 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 { @@ -116,7 +114,7 @@ pub struct TryExpectationRoot { impl Assertable for TryExpectationRoot { type Target = T; - type Result = Result<(), AssertError>; + type Result = Result<(), AssertionError>; fn to_satisfy(self, expectation: impl Display, mut f: F) -> Self::Result where @@ -126,7 +124,7 @@ impl Assertable for TryExpectationRoot { if satisfied { Ok(()) } else { - let error = AssertError::new(expectation.to_string()) + let error = AssertionError::new(expectation.to_string()) .with_field("at", self.source_info.to_string()) .with_field("original target", self.target_source); Err(error) diff --git a/src/experimenting/extensions.rs b/src/experimenting/extensions.rs new file mode 100644 index 0000000..97198fd --- /dev/null +++ b/src/experimenting/extensions.rs @@ -0,0 +1,5 @@ +// TODO: rename this module to assertions + +mod iterators; + +pub use iterators::*; diff --git a/src/experimenting/extensions/iterators.rs b/src/experimenting/extensions/iterators.rs new file mode 100644 index 0000000..6280e69 --- /dev/null +++ b/src/experimenting/extensions/iterators.rs @@ -0,0 +1,54 @@ +use std::fmt::{Display, Formatter}; + +use crate::{ + combinators2::{Assertion, AssertionFn, Combinator, CountCombinator, Plan}, + AssertionRoot, +}; + +pub trait IteratorAssertions { + fn count(self) -> AssertionRoot>; +} + +impl IteratorAssertions for AssertionRoot +where + P: Combinator, + // P::Assertion: Assertion, + // ::Output: IntoIterator, +{ + fn count(self) -> AssertionRoot> { + self.chain(CountCombinator) + } +} + +struct IdentityAssertion; + +impl Display for IdentityAssertion { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "always succeeds") + } +} + +impl Assertion for IdentityAssertion { + type Output = Input; + + fn execute(self, input: Input) -> Self::Output { + input + } +} + +fn foo() { + use crate::expect2; + + let success = expect2!([1, 2, 3]) + .count() + .execute(AssertionFn::new("value is not 0", |value| value != 0)); + + let success = expect2!(1) + .count() + .execute(AssertionFn::new("value is not 0", |value| value != 0)); + + let success = expect2!(1) + .count() + .count() + .execute(AssertionFn::new("value is not 0", |value| value != 0)); +} diff --git a/src/experimenting/lib.rs b/src/experimenting/lib.rs new file mode 100644 index 0000000..78f0724 --- /dev/null +++ b/src/experimenting/lib.rs @@ -0,0 +1,100 @@ +//! Build composable assertions with a functional API. +//! +//! ``` +//! 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: +//! +//! - [`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. +//! +//! These combinators can be chained together as needed. For example: +//! +//! ``` +//! # 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(); +//! ``` +//! +//! 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. +//! +//! 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: +//! +//! ``` +//! # use expecters::prelude::*; +//! let result = expect!(42).as_result().to_be_less_than(10); +//! expect!(result).to_be_err(); +//! ``` +//! +//! 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. +//! +//! [`as_result`]: ExpectationRoot::as_result +//! [`mockall`]: https://crates.io/crates/mockall +//! [`test-case`]: https://crates.io/crates/test-case + +pub mod combinators; +pub mod combinators2; + +mod extensions; + +mod assertions; +mod error; +// mod expect; +mod root; +// mod v3; +// mod v4; +mod v5; +// pub mod v6; +pub mod v7; +mod v8; + +pub use assertions::*; +pub use error::*; +// pub use expect::*; +pub use root::*; + +/// 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::{expect2, extensions::*, path, Assertable}; +} + +#[doc(hidden)] +pub mod specialization; diff --git a/src/experimenting/root.rs b/src/experimenting/root.rs new file mode 100644 index 0000000..8d6427a --- /dev/null +++ b/src/experimenting/root.rs @@ -0,0 +1,131 @@ +use crate::combinators2::{Assertion, Combinator, IdentityCombinator, Plan}; + +/// 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! expect2 { + ($e:expr) => { + // TODO: specialize for types that impl `Display` and `Debug` + $crate::AssertionRoot::new( + $e, + $crate::AssertionMetadata::new( + file!(), + line!(), + column!(), + stringify!($e), + ), + ) + }; +} + +/// The root of an assertion. Combinators are chained together to build an +/// assertion plan, and the plan is executed with a final assertion. +#[derive(Clone, Debug)] +#[must_use = "this type does nothing until an assertion is executed"] +pub struct AssertionRoot { + target: T, + plan: P, + metadata: AssertionMetadata, +} + +impl AssertionRoot { + /// Creates a new [`AssertionRoot`] 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, metadata: AssertionMetadata) -> Self { + Self { + target, + plan: IdentityCombinator, + metadata, + } + } +} + +impl AssertionRoot { + /// Chains a combinator onto the end of the assertion plan. + /// + /// This is the core of how combinators are built. Combinators are used to + /// transform the target value before executing a final assertion. This + /// method allows them to be chained together in a functional manner to + /// transform the target value in a complex way. + pub fn chain(self, combinator: Next) -> AssertionRoot> { + AssertionRoot { + target: self.target, + plan: Plan::new(self.plan, combinator), + metadata: self.metadata, + } + } + + /// Executes the assertion plan by providing the final assertion. + /// + /// This is the core of how assertions are executed. The plan is built up + /// using combinators, and then the final assertion is executed on the + /// transformed target value. + /// + /// Normally, you want to call one of the assertion extension methods + /// directly rather than calling this method. + pub fn execute( + self, + assertion: A, + ) -> <

>::Assertion as Assertion>::Output + where + P: Combinator, + P::Assertion: Assertion, + { + self.plan.build(assertion).execute(self.target) + } +} + +/// Metadata about an assertion that's being executed. This includes information +/// about where the assertion was created and the original target of the +/// assertion. +#[derive(Clone, Debug)] +pub struct AssertionMetadata { + pub(crate) file: &'static str, + pub(crate) line: u32, + pub(crate) column: u32, + pub(crate) target_source: &'static str, +} + +impl AssertionMetadata { + // Signature may change at any time! + #[doc(hidden)] + pub fn new(file: &'static str, line: u32, column: u32, target_source: &'static str) -> Self { + Self { + file, + line, + column, + target_source, + } + } +} diff --git a/src/experimenting/specialization.rs b/src/experimenting/specialization.rs new file mode 100644 index 0000000..1bbcc5e --- /dev/null +++ b/src/experimenting/specialization.rs @@ -0,0 +1 @@ +pub mod at_path; diff --git a/src/specialization/at_path.rs b/src/experimenting/specialization/at_path.rs similarity index 100% rename from src/specialization/at_path.rs rename to src/experimenting/specialization/at_path.rs diff --git a/src/experimenting/v10/combinators.rs b/src/experimenting/v10/combinators.rs new file mode 100644 index 0000000..8c6a54a --- /dev/null +++ b/src/experimenting/v10/combinators.rs @@ -0,0 +1,11 @@ +mod all; +mod any; +mod combinator; +mod combinator_ext; +mod not; + +pub use all::*; +pub use any::*; +pub use combinator::*; +pub use combinator_ext::*; +pub use not::*; diff --git a/src/experimenting/v10/combinators/all.rs b/src/experimenting/v10/combinators/all.rs new file mode 100644 index 0000000..027fb50 --- /dev/null +++ b/src/experimenting/v10/combinators/all.rs @@ -0,0 +1,36 @@ +use crate::AssertionResult; + +use super::AssertionCombinator; + +/// Wraps another [`AssertionCombinator`] and ensures the assertion succeeds for +/// every inner value. +#[derive(Clone, Debug)] +pub struct AllCombinator { + inner: Inner, +} + +impl AllCombinator { + /// Creates a new instance of this combinator. + #[inline] + pub fn new(inner: Inner) -> Self { + Self { inner } + } +} + +impl AssertionCombinator for AllCombinator +where + Inner: AssertionCombinator, + Inner::Target: IntoIterator, +{ + type Target = ::Item; + type Result = Inner::Result; + + #[inline] + fn execute(self, mut f: F) -> Self::Result + where + F: FnMut(Self::Target) -> AssertionResult, + { + self.inner + .execute(|values| values.into_iter().map(&mut f).collect()) + } +} diff --git a/src/experimenting/v10/combinators/any.rs b/src/experimenting/v10/combinators/any.rs new file mode 100644 index 0000000..0a204eb --- /dev/null +++ b/src/experimenting/v10/combinators/any.rs @@ -0,0 +1,67 @@ +use std::fmt::{Display, Formatter}; + +use crate::{AssertionFailure, AssertionResult}; + +use super::AssertionCombinator; + +/// Wraps another [`AssertionCombinator`] and ensures the assertion succeeds for +/// some inner value. +#[derive(Clone, Debug)] +pub struct AnyCombinator { + inner: Inner, +} + +impl AnyCombinator { + /// Creates a new instance of this combinator, wrapping the inner + /// combinator. + #[inline] + pub fn new(inner: Inner) -> Self { + Self { inner } + } +} + +impl AssertionCombinator for AnyCombinator +where + Inner: AssertionCombinator, + Inner::Target: IntoIterator, +{ + type Target = ::Item; + type Result = Inner::Result; + + fn execute(self, mut f: F) -> Self::Result + where + F: FnMut(Self::Target) -> AssertionResult, + { + self.inner.execute(|values| { + let mut error = AssertionFailure::builder().build("TODO"); + for value in values { + match f(value) { + Ok(()) => return Ok(()), + Err(e) => error = e, + } + } + + Err(error) + }) + } +} + +// impl Assertion for AnyAssertion +// where +// Target: IntoIterator, +// Next: Assertion + Clone, +// { +// type Output = AssertionResult; + +// fn assert(self, target: Target) -> Self::Output { +// let mut error = AssertionFailure::builder().build("TODO"); +// for value in target { +// match self.next.clone().assert(value) { +// Ok(()) => return Ok(()), +// Err(e) => error = e, +// } +// } + +// Err(error) +// } +// } diff --git a/src/experimenting/v10/combinators/combinator.rs b/src/experimenting/v10/combinators/combinator.rs new file mode 100644 index 0000000..97e3032 --- /dev/null +++ b/src/experimenting/v10/combinators/combinator.rs @@ -0,0 +1,57 @@ +// TODO: docs + +use crate::AssertionResult; + +/// Transforms an assertion into a more complex assertion. +/// +/// Combinators are used to build more complex assertions out of less complex +/// ones. They function as a sort of "middleware" and are able to transform both +/// the input to an assertion and the output from one. +/// +/// The type parameter represents the type of assertion this combinator can +/// wrap. +#[must_use = "combinators do nothing until they are applied"] +pub trait AssertionCombinator { + /// The type of value passed to this combinator's assertion. + /// + /// For many combinators which wrap another combinator, this is simply + /// `Inner::Target` (where `Inner` is the inner combinator). + type Target; + + /// The result type from executing an assertion with this combinator. + type Result; + + /// Wraps an assertion. + /// + /// This function is the foundation for how combinators work. This is used + /// to create more complex assertions. The combinator works by wrapping the + /// given assertion with its own assertion. For example, the assertion + /// returned by this combinator can negate the output of the provided + /// assertion, call the provided assertion multiple times (if it's `Clone`), + /// or transform the output of the provided assertion in some other manner. + /// + /// The returned assertion still needs to be executed on a value. The type + /// of input the returned assertion accepts is determined by the + /// [`AssertionCombinator::Target`] property. + /// + /// This method also usually has the side effect of "inverting" the type. + /// For example, calling `expect!(value).not().all()` will create an + /// `AllCombinator>>` (where `T` is the type + /// of the value), and applying the combinator will generate an instance of + /// `RootAssertion>>`. + fn execute(self, f: F) -> Self::Result + where + F: FnMut(Self::Target) -> AssertionResult; +} + +impl AssertionCombinator for ExpectationRoot { + type Target = Target; + type Result = AssertionResult; + + fn execute(self, mut f: F) -> Self::Result + where + F: FnMut(Self::Target) -> AssertionResult, + { + f(self.target) + } +} diff --git a/src/experimenting/v10/combinators/combinator_ext.rs b/src/experimenting/v10/combinators/combinator_ext.rs new file mode 100644 index 0000000..ba87610 --- /dev/null +++ b/src/experimenting/v10/combinators/combinator_ext.rs @@ -0,0 +1,109 @@ +use crate::AssertionResult; + +use super::{AllCombinator, AssertionCombinator, NotCombinator}; + +pub trait AssertionCombinatorExt: AssertionCombinator + Sized { + /// 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::NextTarget: IntoIterator, + // { + // AnyCombinator::new(self) + // } + + // fn to_equal(self, other: U) -> impl AssertionOutput + // where + // Self::Target: PartialEq, + // { + // self.apply("the values to be equal", move |value| { + // (value == other) + // .then_some(()) + // .ok_or_else(|| AssertionFailure::builder().build("the values to be equal")) + // }) + // } +} + +impl AssertionCombinatorExt for T where T: AssertionCombinator {} diff --git a/src/experimenting/v10/combinators/not.rs b/src/experimenting/v10/combinators/not.rs new file mode 100644 index 0000000..8a78d83 --- /dev/null +++ b/src/experimenting/v10/combinators/not.rs @@ -0,0 +1,37 @@ +use crate::{AssertionFailure, AssertionResult}; + +use super::AssertionCombinator; + +/// Negates the output of an assertion. 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 instance of this combinator wrapping the inner combinator. + #[inline] + pub fn new(inner: Inner) -> Self { + Self { inner } + } +} + +impl AssertionCombinator for NotCombinator +where + Inner: AssertionCombinator, +{ + type Target = Inner::Target; + type Result = Inner::Result; + + #[inline] + fn execute(self, mut f: F) -> Self::Result + where + F: FnMut(Self::Target) -> AssertionResult, + { + self.inner.execute(|value| match f(value) { + Ok(_) => Err(AssertionFailure::builder().build("TODO")), + Err(_) => Ok(()), + }) + } +} diff --git a/src/experimenting/v11/assertions.rs b/src/experimenting/v11/assertions.rs new file mode 100644 index 0000000..7ce3c0f --- /dev/null +++ b/src/experimenting/v11/assertions.rs @@ -0,0 +1,3 @@ +mod assertion; + +pub use assertion::*; diff --git a/src/experimenting/v11/assertions/assertion.rs b/src/experimenting/v11/assertions/assertion.rs new file mode 100644 index 0000000..c3c7414 --- /dev/null +++ b/src/experimenting/v11/assertions/assertion.rs @@ -0,0 +1,17 @@ +use crate::AssertionResult; + +pub trait Assertion { + // type Output; + + fn execute(&mut self, target: T) -> AssertionResult; +} + +impl Assertion for F +where + F: FnMut(T) -> AssertionResult, +{ + #[inline] + fn execute(&mut self, target: T) -> AssertionResult { + self(target) + } +} diff --git a/src/experimenting/v11/combinators.rs b/src/experimenting/v11/combinators.rs new file mode 100644 index 0000000..4597a64 --- /dev/null +++ b/src/experimenting/v11/combinators.rs @@ -0,0 +1,12 @@ +#[macro_use] +mod macros; + +mod all; +mod combinator; +mod combinator_ext; +mod not; + +pub use all::*; +pub use combinator::*; +pub use combinator_ext::*; +pub use not::*; diff --git a/src/experimenting/v11/combinators/all.rs b/src/experimenting/v11/combinators/all.rs new file mode 100644 index 0000000..9b7921e --- /dev/null +++ b/src/experimenting/v11/combinators/all.rs @@ -0,0 +1,50 @@ +use crate::{assertions::Assertion, AssertionResult}; + +use super::AssertionCombinator; + +#[derive(Clone, Debug)] +pub struct AllCombinator { + inner: Inner, +} + +impl AllCombinator { + simple_ctor!(new(inner: Inner)); +} + +impl AssertionCombinator for AllCombinator +where + Inner: AssertionCombinator>, + Inner::NextTarget: IntoIterator, + A: Assertion<::Item>, +{ + type NextTarget = ::Item; + type Output = Inner::Output; + + #[inline] + fn apply(self, assertion: A) -> Self::Output { + self.inner.apply(AllAssertion::new(assertion)) + } +} + +#[derive(Clone, Debug)] +pub struct AllAssertion { + inner: Inner, +} + +impl AllAssertion { + simple_ctor!(new(inner: Inner)); +} + +impl Assertion for AllAssertion +where + T: IntoIterator, + Inner: Assertion, +{ + #[inline] + fn execute(&mut self, target: T) -> AssertionResult { + target + .into_iter() + .map(|value| self.inner.execute(value)) + .collect() + } +} diff --git a/src/experimenting/v11/combinators/combinator.rs b/src/experimenting/v11/combinators/combinator.rs new file mode 100644 index 0000000..305b04e --- /dev/null +++ b/src/experimenting/v11/combinators/combinator.rs @@ -0,0 +1,11 @@ +use crate::assertions::Assertion; + +pub trait AssertionCombinator +where + A: Assertion, +{ + type NextTarget; + type Output; + + fn apply(self, assertion: A) -> Self::Output; +} diff --git a/src/experimenting/v11/combinators/combinator_ext.rs b/src/experimenting/v11/combinators/combinator_ext.rs new file mode 100644 index 0000000..7505988 --- /dev/null +++ b/src/experimenting/v11/combinators/combinator_ext.rs @@ -0,0 +1,28 @@ +use crate::assertions::Assertion; + +use super::{AllCombinator, AssertionCombinator, NotCombinator}; + +pub trait AssertionCombinatorExt: AssertionCombinator + Sized +where + A: Assertion, +{ + #[inline] + fn not(self) -> NotCombinator { + NotCombinator::new(self) + } + + #[inline] + fn all(self) -> AllCombinator + where + Self::NextTarget: IntoIterator, + { + AllCombinator::new(self) + } +} + +impl AssertionCombinatorExt for C +where + C: AssertionCombinator, + A: Assertion, +{ +} diff --git a/src/experimenting/v11/combinators/macros.rs b/src/experimenting/v11/combinators/macros.rs new file mode 100644 index 0000000..77997db --- /dev/null +++ b/src/experimenting/v11/combinators/macros.rs @@ -0,0 +1,10 @@ +macro_rules! simple_ctor { + ($fn_name:ident($($field_name:ident: $field_ty:ty),*)) => { + #[inline] + pub fn $fn_name($($field_name: $field_ty),*) -> Self { + Self { + $($field_name,)* + } + } + }; +} diff --git a/src/experimenting/v11/combinators/not.rs b/src/experimenting/v11/combinators/not.rs new file mode 100644 index 0000000..ef6c0f4 --- /dev/null +++ b/src/experimenting/v11/combinators/not.rs @@ -0,0 +1,48 @@ +use crate::{assertions::Assertion, AssertionFailure, AssertionResult}; + +use super::AssertionCombinator; + +#[derive(Clone, Debug)] +pub struct NotCombinator { + inner: Inner, +} + +impl NotCombinator { + simple_ctor!(new(inner: Inner)); +} + +impl AssertionCombinator for NotCombinator +where + Inner: AssertionCombinator>, + A: Assertion, +{ + type NextTarget = Inner::NextTarget; + type Output = Inner::Output; + + #[inline] + fn apply(self, assertion: A) -> Self::Output { + self.inner.apply(NotAssertion::new(assertion)) + } +} + +#[derive(Clone, Debug)] +pub struct NotAssertion { + inner: Inner, +} + +impl NotAssertion { + simple_ctor!(new(inner: Inner)); +} + +impl Assertion for NotAssertion +where + Inner: Assertion, +{ + #[inline] + fn execute(&mut self, target: T) -> AssertionResult { + match self.inner.execute(target) { + Ok(_) => Err(AssertionFailure::builder().build("TODO")), + Err(_) => Ok(()), + } + } +} diff --git a/src/experimenting/v11/expect.rs b/src/experimenting/v11/expect.rs new file mode 100644 index 0000000..df27bd3 --- /dev/null +++ b/src/experimenting/v11/expect.rs @@ -0,0 +1,98 @@ +use std::fmt::{Display, Formatter}; + +use crate::{assertions::Assertion, combinators::AssertionCombinator, AssertionResult, SourceLoc}; + +/// 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::source_loc!(), + ::core::stringify!($e), + ) + }; +} + +/// The root of an expectation. Other expectations are built on top of this. +#[derive(Clone, Debug)] +pub struct ExpectationRoot { + target: Target, + source_info: SourceLoc, + 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: Target, source_info: SourceLoc, target_source: &'static str) -> Self { + Self { + target, + source_info, + target_source, + } + } +} + +impl AssertionCombinator for ExpectationRoot +where + A: Assertion, +{ + type NextTarget = Target; + type Output = AssertionResult; + + fn apply(self, mut assertion: A) -> Self::Output { + assertion.execute(self.target) + } +} + +#[cfg(test)] +mod tests { + use crate::{combinators::*, prelude::*}; + + #[test] + fn foo() { + // let combinator = ExpectationRoot::new([1, 2, 3], todo!(), "a"); + // let _a = expect!([1, 2, 3]).not().all(); + + let _a = expect!([1, 2, 3]).not().apply(|_n| Ok(())); + + let _a = NotCombinator::new(AllCombinator::new(expect!([1, 2, 3]))); + let result = _a.apply(|_n| Ok(())); + + // let output = expect!(1).to_equal(1); + } +} diff --git a/src/experimenting/v11/failure.rs b/src/experimenting/v11/failure.rs new file mode 100644 index 0000000..ce39740 --- /dev/null +++ b/src/experimenting/v11/failure.rs @@ -0,0 +1,66 @@ +use std::{ + error::Error, + fmt::{Display, Formatter}, +}; + +pub type AssertionResult = Result; + +/// 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 AssertionFailure { + fields: Vec<(&'static str, String)>, +} + +impl AssertionFailure { + /// Creates a builder for a new failure. + pub fn builder() -> AssertionFailureBuilder { + AssertionFailureBuilder::default() + } +} + +impl Display for AssertionFailure { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + writeln!(f, "assertion failed.")?; + for (name, value) in &self.fields { + writeln!(f, " {name}: {value}")?; + } + + Ok(()) + } +} + +impl Error for AssertionFailure {} + +/// A builder for a failure. +#[derive(Clone, Debug)] +pub struct AssertionFailureBuilder { + fields: Vec<(&'static str, String)>, +} + +impl Default for AssertionFailureBuilder { + fn default() -> Self { + Self { + fields: vec![("expected", String::new())], + } + } +} + +impl AssertionFailureBuilder { + /// 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 Display) -> Self { + self.fields.push((name, value.to_string())); + self + } + + /// Builds the error with the given expectation. + pub fn build(mut self, expectation: impl Display) -> AssertionFailure { + self.fields[0].1 = expectation.to_string(); + AssertionFailure { + fields: self.fields, + } + } +} diff --git a/src/experimenting/v11/specialization.rs b/src/experimenting/v11/specialization.rs new file mode 100644 index 0000000..452baaf --- /dev/null +++ b/src/experimenting/v11/specialization.rs @@ -0,0 +1,6 @@ +// pub mod at_path; +pub mod root; + +mod wrapper; + +pub use wrapper::*; diff --git a/src/experimenting/v11/specialization/at_path.rs b/src/experimenting/v11/specialization/at_path.rs new file mode 100644 index 0000000..083c3a4 --- /dev/null +++ b/src/experimenting/v11/specialization/at_path.rs @@ -0,0 +1,83 @@ +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/experimenting/v11/specialization/root.rs b/src/experimenting/v11/specialization/root.rs new file mode 100644 index 0000000..9a46472 --- /dev/null +++ b/src/experimenting/v11/specialization/root.rs @@ -0,0 +1,77 @@ +use std::fmt::{Debug, Display}; + +use crate::{specialization::Wrapper, ExpectationRoot, SourceLoc}; + +pub struct __ExpectersOtherTag; + +impl __ExpectersOtherTag { + #[inline] + pub fn new_root( + target: T, + source_info: SourceLoc, + target_source: &'static str, + ) -> ExpectationRoot { + ExpectationRoot::new(target, source_info, target_source) + } +} + +pub trait __ExpectersOtherKind { + #[inline] + fn __expecters_kind(&self) -> __ExpectersOtherTag { + __ExpectersOtherTag + } +} + +impl __ExpectersOtherKind for &Wrapper<&T> {} + +pub struct __ExpectersDebugTag; + +impl __ExpectersDebugTag { + #[inline] + pub fn new_root( + target: T, + source_info: SourceLoc, + target_source: &'static str, + ) -> ExpectationRoot + where + T: Debug, + { + // TODO: use `format!("{target:?}")` + ExpectationRoot::new(target, source_info, target_source) + } +} + +pub trait __ExpectersDebugKind { + #[inline] + fn __expecters_kind(&self) -> __ExpectersDebugTag { + __ExpectersDebugTag + } +} + +impl __ExpectersDebugKind for &&Wrapper<&T> where T: Debug {} + +pub struct __ExpectersDisplayTag; + +impl __ExpectersDisplayTag { + #[inline] + pub fn new_root( + target: T, + source_info: SourceLoc, + target_source: &'static str, + ) -> ExpectationRoot + where + T: Display, + { + // TODO: use `format!("{target}")` + ExpectationRoot::new(target, source_info, target_source) + } +} + +pub trait __ExpectersDisplayKind { + #[inline] + fn __expecters_kind(&self) -> __ExpectersDisplayTag { + __ExpectersDisplayTag + } +} + +impl __ExpectersDisplayKind for &&&Wrapper<&T> where T: Display {} diff --git a/src/experimenting/v11/specialization/wrapper.rs b/src/experimenting/v11/specialization/wrapper.rs new file mode 100644 index 0000000..44defbc --- /dev/null +++ b/src/experimenting/v11/specialization/wrapper.rs @@ -0,0 +1,2 @@ +/// Wrapper struct used to specialize macros. +pub struct Wrapper(pub T); diff --git a/src/experimenting/v3.rs b/src/experimenting/v3.rs new file mode 100644 index 0000000..c60bbe3 --- /dev/null +++ b/src/experimenting/v3.rs @@ -0,0 +1,372 @@ +use std::{ + fmt::{self, Display, Formatter}, + future::{ready, Future}, + marker::PhantomData, + pin::Pin, + sync::Arc, +}; + +use crate::combinators2::Combinator; + +#[derive(Debug, Default)] +pub struct AssertionFailure { + fields: Vec<(String, String)>, +} + +/// A type which builds an assertion to execute on a value. +pub trait AssertionCombinator: Sized +where + A: Assertion, +{ + // /// The type of value this combinator expects assertions to be executed on. + // /// This constrains the type of assertions that can be executed on this + // /// combinator to just those which can be executed on types this combinator + // /// can provide to them. + // // Keep this as an associated type - it helps with error messages and + // // intellisense! + // // This flows "forwards" - it's the type that gets passed to the next item + // // in the chain, whether that's a combinator or assertion + // type Target; + + type NextInput; + + /// The transformed assertion, given an assertion as input. This is the type + /// returned when transforming an assertion with this combinator, and is + /// itself a type of assertion. + // This flows "backwards" - it constrains the kinds of combinators this + // combinator can be applied to, since `TransformedAssertion` is itself + // only an assertion in certain circumstances. The target of the transformed + // assertion may be different than `Self::Target` because, for example, this + // assertion may take an iterator as input, but pass an item in that + // iterator to the next item in the chain. + type Transformed: Assertion; + + /// Builds an assertion using this combinator. + fn build(self, assertion: A) -> Self::Transformed; + + fn not(self) -> NotCombinator + where + NotCombinator: Combinator, + { + NotCombinator { prev: self } + } + + fn all(self) -> AllCombinator { + AllCombinator { prev: self } + } + + fn to_satisfy(self, f: F) -> Self::Transformed + where + Self: Combinator::Input>>, + Self::Transformed: Assertion>, + + F: FnOnce( + ::Input, + ) -> ::Output, + { + self.build(SimpleAssertion::new("a", f)) + } +} + +/// Performs a validation on a value. The [`Display`] implementation should +/// output the predicate this assertion expects to be true of the value. +pub trait Assertion: Display + Sized { + type Input; + type Output; + + /// Execute the assertion on a target value. + fn assert(self, target: Self::Input) -> Self::Output; + + fn not(self) -> NotAssertion + where + NotAssertion: Assertion, + { + NotAssertion(self) + } +} + +#[derive(Clone, Debug)] +pub struct SimpleAssertion { + expectation: Arc, + predicate: fn(T) -> Result<(), AssertionFailure>, +} + +impl SimpleAssertion { + pub fn new( + expectation: impl ToString, + predicate: fn(T) -> Result<(), AssertionFailure>, + ) -> Self { + Self { + expectation: expectation.to_string().into(), + predicate, + } + } +} + +impl Display for SimpleAssertion { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.expectation) + } +} + +impl Assertion for SimpleAssertion { + type Input = T; + type Output = Result<(), AssertionFailure>; + + fn assert(self, target: Self::Input) -> Result<(), AssertionFailure> { + (self.predicate)(target) + } +} + +///////// + +#[derive(Clone, Debug)] +pub struct AssertionRoot { + target: T, +} + +impl AssertionCombinator for AssertionRoot +where + A: Assertion, +{ + type NextInput = T; + type Transformed = RootAssertion; + + fn build(self, assertion: A) -> Self::Transformed { + RootAssertion { + target: self.target, + next: assertion, + } + } +} + +#[derive(Clone, Debug)] +pub struct RootAssertion { + target: T, + next: Next, +} + +impl Display for RootAssertion +where + Next: Display, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + writeln!(f, "{}", self.next) + } +} + +impl Assertion for RootAssertion +where + Next: Assertion, +{ + type Input = (); + + type Output = Next::Output; + + fn assert(self, _target: Self::Input) -> Self::Output { + self.next.assert(self.target) + } +} + +#[derive(Clone, Debug)] +pub struct NotCombinator { + prev: Prev, +} + +impl AssertionCombinator for NotCombinator +where + A: Assertion>, + Prev: AssertionCombinator>, +{ + type NextInput = Prev::NextInput; + type Transformed = Prev::Transformed; + + fn build(self, assertion: A) -> Self::Transformed { + self.prev.build(NotAssertion(assertion)) + } +} + +#[derive(Clone, Debug)] +pub struct NotAssertion(Next); + +impl Display for NotAssertion +where + Next: Display, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "the following to not be satisfied: {}", self.0) + } +} + +impl Assertion for NotAssertion +where + Next: Assertion>, +{ + type Input = Next::Input; + type Output = Next::Output; + + fn assert(self, target: Self::Input) -> Result<(), AssertionFailure> { + match self.0.assert(target) { + Ok(_) => Err(AssertionFailure::default()), + Err(_) => todo!(), + } + } +} + +pub struct AllCombinator { + prev: Prev, +} + +impl AssertionCombinator for AllCombinator +where + A: Assertion, +{ + type NextInput = ::Item; + type Transformed = AllAssertion; + + fn build(self, assertion: A) -> Self::Transformed { + AllAssertion { + next: assertion, + marker: PhantomData, + } + } +} + +#[derive(Clone, Debug)] +pub struct AllAssertion { + marker: PhantomData, + next: Next, +} + +impl Display for AllAssertion +where + Next: Display, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + todo!() + } +} + +impl Assertion for AllAssertion +where + I: IntoIterator, + Next: Assertion + Clone, + Result<(), AssertionFailure>: FromIterator, +{ + type Input = I; + type Output = Result<(), AssertionFailure>; + + fn assert(self, target: Self::Input) -> Self::Output { + target + .into_iter() + .map(|target| self.next.clone().assert(target)) + .collect() + } +} + +#[derive(Clone, Debug)] +pub struct MapAssertion { + next: Next, + f: fn(I) -> O, +} + +impl Display for MapAssertion +where + Next: Display, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + todo!() + } +} + +impl Assertion for MapAssertion +where + Next: Assertion, +{ + type Input = I; + type Output = Next::Output; + + fn assert(self, target: Self::Input) -> Self::Output { + self.next.assert((self.f)(target)) + } +} + +#[derive(Clone, Debug)] +pub struct WhenReadyAssertion { + next: Next, + marker: PhantomData, +} + +impl Display for WhenReadyAssertion +where + Next: Display, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + todo!() + } +} + +impl Assertion for WhenReadyAssertion +where + I: Future + Send + 'static, + Next: Assertion + Send + 'static, +{ + type Input = I; + type Output = Pin + Send>>; + + fn assert(self, target: Self::Input) -> Self::Output { + Box::pin(async move { self.next.assert(target.await) }) + } +} + +async fn foo() { + let assertion = RootAssertion { + target: ready([1, 2, 3]), + next: WhenReadyAssertion { + marker: PhantomData, + next: AllAssertion { + marker: PhantomData, + next: SimpleAssertion::new("non-zero", |value| { + if value == 0 { + Err(AssertionFailure::default()) + } else { + Ok(()) + } + }), + }, + }, + }; + let result = assertion.assert(()).await; + + let assertion = RootAssertion { + target: [1, 2, 3], + next: AllAssertion { + marker: PhantomData, + next: SimpleAssertion::new("non-zero", |value| { + if value == 0 { + Err(AssertionFailure::default()) + } else { + Ok(()) + } + }), + }, + }; + let result = assertion.assert(()); + + let combinator = AssertionRoot { target: [1, 2, 3] } + .not() + .to_satisfy(|values: [i32; 3]| values.is_empty()); + + AllCombinator { + marker: PhantomData, + prev: AssertionRoot { target: [1, 2, 3] }, + }; + let assertion = combinator.build(SimpleAssertion::new("non-zero", |value| { + if value == 0 { + Err(AssertionFailure::default()) + } else { + Ok(()) + } + })); + let result = assertion.assert(()); +} diff --git a/src/experimenting/v4.rs b/src/experimenting/v4.rs new file mode 100644 index 0000000..6452d8c --- /dev/null +++ b/src/experimenting/v4.rs @@ -0,0 +1,341 @@ +use std::{ + fmt::{self, Display, Formatter}, + future::{ready, Future}, + marker::PhantomData, + pin::Pin, + sync::Arc, +}; + +#[derive(Debug, Default)] +pub struct AssertionFailure { + fields: Vec<(String, String)>, +} + +/// A type which builds an assertion to execute on a value. +pub trait AssertionCombinator: Sized { + type NextInput; + + type Assertion + where + A: Assertion; + + /// Builds an assertion using this combinator. + fn build(self, assertion: A) -> Self::Assertion + where + A: Assertion; + + fn not(self) -> NotCombinator { + NotCombinator { prev: self } + } + + fn all(self) -> AllCombinator { + AllCombinator { prev: self } + } +} + +/// Performs a validation on a value. The [`Display`] implementation should +/// output the predicate this assertion expects to be true of the value. +pub trait Assertion: Display + Sized { + type Output; + + /// Execute the assertion on a target value. + fn assert(self, target: Input) -> Self::Output; +} + +#[derive(Clone, Debug)] +pub struct SimpleAssertion { + expectation: Arc, + predicate: F, +} + +impl SimpleAssertion { + pub fn new(expectation: impl ToString, predicate: F) -> Self { + Self { + expectation: expectation.to_string().into(), + predicate, + } + } +} + +impl Display for SimpleAssertion { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.expectation) + } +} + +impl Assertion for SimpleAssertion +where + F: FnOnce(I) -> O, +{ + type Output = O; + + fn assert(self, target: I) -> Self::Output { + (self.predicate)(target) + } +} + +///////// + +#[derive(Clone, Debug)] +pub struct AssertionRoot { + target: T, +} + +impl AssertionCombinator for AssertionRoot { + type NextInput = T; + + type Assertion = RootAssertion + where + A: Assertion; + + fn build(self, assertion: A) -> Self::Assertion + where + A: Assertion, + { + RootAssertion { + target: self.target, + next: assertion, + } + } +} + +#[derive(Clone, Debug)] +pub struct RootAssertion { + target: T, + next: Next, +} + +impl Display for RootAssertion +where + Next: Display, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + writeln!(f, "{}", self.next) + } +} + +impl Assertion<()> for RootAssertion +where + Next: Assertion, +{ + type Output = Next::Output; + + fn assert(self, _target: ()) -> Self::Output { + self.next.assert(self.target) + } +} + +#[derive(Clone, Debug)] +pub struct NotCombinator { + prev: Prev, +} + +impl AssertionCombinator for NotCombinator +where + Prev: AssertionCombinator, +{ + type NextInput = Prev::NextInput; + + type Assertion = Prev::Assertion> + where + A: Assertion; + + fn build(self, assertion: A) -> Self::Assertion + where + A: Assertion, + { + NotAssertion(self.prev.build(assertion)) + } +} + +fn a() { + let assertion = NotCombinator { + prev: AssertionRoot { target: 1 }, + } + .build(SimpleAssertion::new("not zero", |value| value != 0)); + + let _result = assertion.assert(()); +} + +#[derive(Clone, Debug)] +pub struct NotAssertion(Next); + +impl Display for NotAssertion +where + Next: Display, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "the following to not be satisfied: {}", self.0) + } +} + +impl Assertion for NotAssertion +where + Next: Assertion>, +{ + type Output = Next::Output; + + fn assert(self, target: Input) -> Self::Output { + match self.0.assert(target) { + Ok(_) => Err(AssertionFailure::default()), + Err(_) => todo!(), + } + } +} + +pub struct AllCombinator { + prev: Prev, +} + +impl AssertionCombinator for AllCombinator +where + Prev: AssertionCombinator, + Prev::NextInput: IntoIterator, +{ + type NextInput = ::Item; + + type Assertion = AllAssertion + where + A: Assertion; + + fn build(self, assertion: A) -> Self::Assertion + where + A: Assertion, + { + self.prev.build(AllAssertion { next: assertion }) + } +} + +#[derive(Clone, Debug)] +pub struct AllAssertion { + next: Next, +} + +impl Display for AllAssertion +where + Next: Display, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + todo!() + } +} + +impl Assertion for AllAssertion +where + I: IntoIterator, + Next: Assertion + Clone, + Result<(), AssertionFailure>: FromIterator, +{ + type Input = I; + type Output = Result<(), AssertionFailure>; + + fn assert(self, target: Self::Input) -> Self::Output { + target + .into_iter() + .map(|target| self.next.clone().assert(target)) + .collect() + } +} + +#[derive(Clone, Debug)] +pub struct MapAssertion { + next: Next, + f: fn(I) -> O, +} + +impl Display for MapAssertion +where + Next: Display, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + todo!() + } +} + +impl Assertion for MapAssertion +where + Next: Assertion, +{ + type Input = I; + type Output = Next::Output; + + fn assert(self, target: Self::Input) -> Self::Output { + self.next.assert((self.f)(target)) + } +} + +#[derive(Clone, Debug)] +pub struct WhenReadyAssertion { + next: Next, + marker: PhantomData, +} + +impl Display for WhenReadyAssertion +where + Next: Display, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + todo!() + } +} + +impl Assertion for WhenReadyAssertion +where + I: Future + Send + 'static, + Next: Assertion + Send + 'static, +{ + type Input = I; + type Output = Pin + Send>>; + + fn assert(self, target: Self::Input) -> Self::Output { + Box::pin(async move { self.next.assert(target.await) }) + } +} + +async fn foo() { + let assertion = RootAssertion { + target: ready([1, 2, 3]), + next: WhenReadyAssertion { + marker: PhantomData, + next: AllAssertion { + marker: PhantomData, + next: SimpleAssertion::new("non-zero", |value| { + if value == 0 { + Err(AssertionFailure::default()) + } else { + Ok(()) + } + }), + }, + }, + }; + let result = assertion.assert(()).await; + + let assertion = RootAssertion { + target: [1, 2, 3], + next: AllAssertion { + marker: PhantomData, + next: SimpleAssertion::new("non-zero", |value| { + if value == 0 { + Err(AssertionFailure::default()) + } else { + Ok(()) + } + }), + }, + }; + let result = assertion.assert(()); + + let combinator = AllCombinator { + marker: PhantomData, + prev: AssertionRoot { target: [1, 2, 3] }, + }; + let assertion = combinator.build(SimpleAssertion::new("non-zero", |value| { + if value == 0 { + Err(AssertionFailure::default()) + } else { + Ok(()) + } + })); + let result = assertion.assert(()); +} diff --git a/src/experimenting/v5.rs b/src/experimenting/v5.rs new file mode 100644 index 0000000..4fe22dc --- /dev/null +++ b/src/experimenting/v5.rs @@ -0,0 +1,333 @@ +use std::{ + fmt::{self, Display, Formatter}, + future::{ready, Future}, + marker::PhantomData, + pin::Pin, + sync::Arc, +}; + +// - combinators are built in the direction data flows +// - assertions are built in the direction the assertion is wrapped +// - this is opposite the direction combinators are built in +// - need to know the type of the next input to constrain the chain +// - ex should be possible to know when `.all()` is applicable +// - already know the input type since the combinator wraps the root value +// - no need to be generic over it as a result + +#[derive(Debug, Default)] +pub struct AssertionFailure { + fields: Vec<(String, String)>, +} + +/// A type which builds an assertion to execute on a value. +pub trait AssertionCombinator: Sized +where + Next: Assertion, +{ + type NextInput; + type Assertion; + + /// Builds an assertion using this combinator. + fn build(self, assertion: Next) -> Self::Assertion; + + fn not(self) -> NotCombinator { + NotCombinator { prev: self } + } + + fn all(self) -> AllCombinator { + AllCombinator { prev: self } + } +} + +/// Performs a validation on a value. The [`Display`] implementation should +/// output the predicate this assertion expects to be true of the value. +pub trait Assertion: Display + Sized { + type Output; + + /// Execute the assertion on a target value. + fn assert(self, target: Input) -> Self::Output; +} + +#[derive(Clone, Debug)] +pub struct SimpleAssertion { + expectation: Arc, + predicate: F, +} + +impl SimpleAssertion { + pub fn new(expectation: impl ToString, predicate: F) -> Self { + Self { + expectation: expectation.to_string().into(), + predicate, + } + } +} + +impl Display for SimpleAssertion { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.expectation) + } +} + +impl Assertion for SimpleAssertion +where + F: FnOnce(I) -> O, +{ + type Output = O; + + fn assert(self, target: I) -> Self::Output { + (self.predicate)(target) + } +} + +///////// + +#[derive(Clone, Debug)] +pub struct AssertionRoot { + target: T, +} + +impl AssertionCombinator for AssertionRoot +where + A: Assertion, +{ + type NextInput = T; + type Assertion = RootAssertion; + + fn build(self, assertion: A) -> Self::Assertion { + RootAssertion { + target: self.target, + next: assertion, + } + } +} + +#[derive(Clone, Debug)] +pub struct RootAssertion { + target: T, + next: Next, +} + +impl Display for RootAssertion +where + Next: Display, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + writeln!(f, "{}", self.next) + } +} + +impl Assertion<()> for RootAssertion +where + Next: Assertion, +{ + type Output = Next::Output; + + fn assert(self, _target: ()) -> Self::Output { + self.next.assert(self.target) + } +} + +#[derive(Clone, Debug)] +pub struct NotCombinator { + prev: Prev, +} + +impl AssertionCombinator for NotCombinator +where + Prev: AssertionCombinator>, + A: Assertion>, +{ + type NextInput = Prev::NextInput; + type Assertion = Prev::Assertion; + + fn build(self, assertion: A) -> Self::Assertion { + self.prev.build(NotAssertion(assertion)) + } +} + +#[derive(Clone, Debug)] +pub struct NotAssertion(Next); + +impl Display for NotAssertion +where + Next: Display, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "the following to not be satisfied: {}", self.0) + } +} + +impl Assertion for NotAssertion +where + Next: Assertion>, +{ + type Output = Next::Output; + + fn assert(self, target: Input) -> Self::Output { + match self.0.assert(target) { + Ok(_) => Err(AssertionFailure::default()), + Err(_) => todo!(), + } + } +} + +pub struct AllCombinator { + prev: Prev, +} + +impl AssertionCombinator for AllCombinator +where + Prev: AssertionCombinator>, + Prev::NextInput: IntoIterator, + A: Assertion<::Item> + Clone, +{ + type NextInput = ::Item; + type Assertion = Prev::Assertion; + + fn build(self, assertion: A) -> Self::Assertion { + self.prev.build(AllAssertion { next: assertion }) + } +} + +#[derive(Clone, Debug)] +pub struct AllAssertion { + next: Next, +} + +impl Display for AllAssertion +where + Next: Display, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + todo!() + } +} + +impl Assertion for AllAssertion +where + I: IntoIterator, + Next: Assertion + Clone, + Result<(), AssertionFailure>: FromIterator, +{ + type Output = Result<(), AssertionFailure>; + + fn assert(self, target: I) -> Self::Output { + target + .into_iter() + .map(|target| self.next.clone().assert(target)) + .collect() + } +} + +fn a() { + let assertion = NotCombinator { + prev: AssertionRoot { target: 1 }, + } + .build(SimpleAssertion::new("not zero", |value| value != 0)); + + let _result = assertion.assert(()); +} + +// #[derive(Clone, Debug)] +// pub struct MapAssertion { +// next: Next, +// f: fn(I) -> O, +// } + +// impl Display for MapAssertion +// where +// Next: Display, +// { +// fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { +// todo!() +// } +// } + +// impl Assertion for MapAssertion +// where +// Next: Assertion, +// { +// type Input = I; +// type Output = Next::Output; + +// fn assert(self, target: Self::Input) -> Self::Output { +// self.next.assert((self.f)(target)) +// } +// } + +// #[derive(Clone, Debug)] +// pub struct WhenReadyAssertion { +// next: Next, +// marker: PhantomData, +// } + +// impl Display for WhenReadyAssertion +// where +// Next: Display, +// { +// fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { +// todo!() +// } +// } + +// impl Assertion for WhenReadyAssertion +// where +// I: Future + Send + 'static, +// Next: Assertion + Send + 'static, +// { +// type Input = I; +// type Output = Pin + Send>>; + +// fn assert(self, target: Self::Input) -> Self::Output { +// Box::pin(async move { self.next.assert(target.await) }) +// } +// } + +// async fn foo() { +// let assertion = RootAssertion { +// target: ready([1, 2, 3]), +// next: WhenReadyAssertion { +// marker: PhantomData, +// next: AllAssertion { +// marker: PhantomData, +// next: SimpleAssertion::new("non-zero", |value| { +// if value == 0 { +// Err(AssertionFailure::default()) +// } else { +// Ok(()) +// } +// }), +// }, +// }, +// }; +// let result = assertion.assert(()).await; + +// let assertion = RootAssertion { +// target: [1, 2, 3], +// next: AllAssertion { +// marker: PhantomData, +// next: SimpleAssertion::new("non-zero", |value| { +// if value == 0 { +// Err(AssertionFailure::default()) +// } else { +// Ok(()) +// } +// }), +// }, +// }; +// let result = assertion.assert(()); + +// let combinator = AllCombinator { +// marker: PhantomData, +// prev: AssertionRoot { target: [1, 2, 3] }, +// }; +// let assertion = combinator.build(SimpleAssertion::new("non-zero", |value| { +// if value == 0 { +// Err(AssertionFailure::default()) +// } else { +// Ok(()) +// } +// })); +// let result = assertion.assert(()); +// } diff --git a/src/experimenting/v6.rs b/src/experimenting/v6.rs new file mode 100644 index 0000000..3abf033 --- /dev/null +++ b/src/experimenting/v6.rs @@ -0,0 +1,180 @@ +mod all; +mod assertion; +mod combinator; +mod error; +mod not; + +pub use all::*; +pub use assertion::*; +pub use combinator::*; +pub use error::*; +pub use not::*; + +use std::fmt::{self, Display, Formatter}; + +///////// + +#[derive(Clone, Debug)] +pub struct AssertionRoot { + target: T, +} + +impl AssertionCombinator for AssertionRoot +where + A: Assertion, +{ + type NextInput = T; + type Assertion = RootAssertion; + + fn build(self, assertion: A) -> Self::Assertion { + RootAssertion { + target: self.target, + next: assertion, + } + } +} + +#[derive(Clone, Debug)] +pub struct RootAssertion { + target: T, + next: Next, +} + +impl Display for RootAssertion +where + Next: Display, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + writeln!(f, "{}", self.next) + } +} + +impl Assertion<()> for RootAssertion +where + Next: Assertion, +{ + type Output = Next::Output; + + fn assert(self, _target: ()) -> Self::Output { + self.next.assert(self.target) + } +} + +fn a() { + let root = AssertionRoot { target: [1, 2, 3] }; + let assertion = root + .all() + .not() + .build(SimpleAssertion::new("not zero", |value| { + if value != 0 { + Ok(()) + } else { + Err(AssertionError::default()) + } + })); + + let _result = assertion.assert(()); +} + +// #[derive(Clone, Debug)] +// pub struct MapAssertion { +// next: Next, +// f: fn(I) -> O, +// } + +// impl Display for MapAssertion +// where +// Next: Display, +// { +// fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { +// todo!() +// } +// } + +// impl Assertion for MapAssertion +// where +// Next: Assertion, +// { +// type Input = I; +// type Output = Next::Output; + +// fn assert(self, target: Self::Input) -> Self::Output { +// self.next.assert((self.f)(target)) +// } +// } + +// #[derive(Clone, Debug)] +// pub struct WhenReadyAssertion { +// next: Next, +// marker: PhantomData, +// } + +// impl Display for WhenReadyAssertion +// where +// Next: Display, +// { +// fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { +// todo!() +// } +// } + +// impl Assertion for WhenReadyAssertion +// where +// I: Future + Send + 'static, +// Next: Assertion + Send + 'static, +// { +// type Input = I; +// type Output = Pin + Send>>; + +// fn assert(self, target: Self::Input) -> Self::Output { +// Box::pin(async move { self.next.assert(target.await) }) +// } +// } + +// async fn foo() { +// let assertion = RootAssertion { +// target: ready([1, 2, 3]), +// next: WhenReadyAssertion { +// marker: PhantomData, +// next: AllAssertion { +// marker: PhantomData, +// next: SimpleAssertion::new("non-zero", |value| { +// if value == 0 { +// Err(AssertionFailure::default()) +// } else { +// Ok(()) +// } +// }), +// }, +// }, +// }; +// let result = assertion.assert(()).await; + +// let assertion = RootAssertion { +// target: [1, 2, 3], +// next: AllAssertion { +// marker: PhantomData, +// next: SimpleAssertion::new("non-zero", |value| { +// if value == 0 { +// Err(AssertionFailure::default()) +// } else { +// Ok(()) +// } +// }), +// }, +// }; +// let result = assertion.assert(()); + +// let combinator = AllCombinator { +// marker: PhantomData, +// prev: AssertionRoot { target: [1, 2, 3] }, +// }; +// let assertion = combinator.build(SimpleAssertion::new("non-zero", |value| { +// if value == 0 { +// Err(AssertionFailure::default()) +// } else { +// Ok(()) +// } +// })); +// let result = assertion.assert(()); +// } diff --git a/src/experimenting/v6/all.rs b/src/experimenting/v6/all.rs new file mode 100644 index 0000000..56e7d40 --- /dev/null +++ b/src/experimenting/v6/all.rs @@ -0,0 +1,67 @@ +use std::fmt::{self, Display, Formatter}; + +use super::{Assertion, AssertionCombinator, AssertionError}; + +pub struct AllCombinator { + prev: Prev, +} + +impl AllCombinator { + /// Creates a new [`AllCombinator`]. + #[inline] + pub fn new(prev: Prev) -> Self { + Self { prev } + } +} + +impl AssertionCombinator for AllCombinator +where + Prev: AssertionCombinator>, + Prev::NextInput: IntoIterator, + AllAssertion: Assertion, +{ + type NextInput = ::Item; + type Assertion = Prev::Assertion; + + fn build(self, assertion: Next) -> Self::Assertion { + self.prev.build(AllAssertion::new(assertion)) + } +} + +#[derive(Clone, Debug)] +pub struct AllAssertion { + next: Next, +} + +impl AllAssertion { + /// Creates a new [`AllAssertion`]. + #[inline] + pub fn new(next: Next) -> Self { + Self { next } + } +} + +impl Display for AllAssertion +where + Next: Display, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "for each inner value, {}", self.next) + } +} + +impl Assertion for AllAssertion +where + Input: IntoIterator, + Next: Assertion + Clone, + Result<(), AssertionError>: FromIterator, +{ + type Output = Result<(), AssertionError>; + + fn assert(self, target: Input) -> Self::Output { + target + .into_iter() + .map(|item| self.next.clone().assert(item)) + .collect() + } +} diff --git a/src/experimenting/v6/assertion.rs b/src/experimenting/v6/assertion.rs new file mode 100644 index 0000000..7b7cb73 --- /dev/null +++ b/src/experimenting/v6/assertion.rs @@ -0,0 +1,90 @@ +use std::{ + fmt::{self, Display, Formatter}, + sync::Arc, +}; + +use super::AssertionError; + +/// Performs a validation on a value. The [`Display`] implementation should +/// output the predicate this assertion expects to be true of the value. +pub trait Assertion: Display + Sized { + /// The output of this assertion. + type Output: AssertionOutput; + + /// Execute the assertion on a target value. + fn assert(self, target: Input) -> Self::Output; +} + +/// An output from executing an assertion. +/// +/// This generally represents some type that internally contains some +/// representation that can be converted to/from a +/// [`Result<(), AssertionFailure>`]. Since not all assertions can return a +/// [`Result`] directly, this allows those assertions to return something more +/// suitable for that assertion. +/// +/// For example, the [`WhenReadyCombinator`] returns a [`Future`] which can be +/// `.await`ed into a [`Result`]. +pub trait AssertionOutput { + /// The inverted result type. This is returned by + /// [`map`](AssertionOutput::map). + type Mapped: AssertionOutput + where + F: FnOnce(Result<(), AssertionError>) -> Result<(), AssertionError>; + + /// Maps the result represented by this output to a new result. + /// + /// This enables combinators to transform the result of the assertion + /// without necessarily needing to know the type of the assertion. For + /// example, the [`NotCombinator`] uses this method to transform a success + /// into a failure and a failure into a success. + fn map(self, f: F) -> Self::Mapped + where + F: FnOnce(Result<(), AssertionError>) -> Result<(), AssertionError>; +} + +impl AssertionOutput for Result<(), AssertionError> { + type Mapped = Self + where + F: FnOnce(Result<(), AssertionError>) -> Result<(), AssertionError>; + + fn map(self, f: F) -> Self::Mapped + where + F: FnOnce(Result<(), AssertionError>) -> Result<(), AssertionError>, + { + f(self) + } +} + +#[derive(Clone, Debug)] +pub struct SimpleAssertion { + expectation: Arc, + predicate: F, +} + +impl SimpleAssertion { + pub fn new(expectation: impl ToString, predicate: F) -> Self { + Self { + expectation: expectation.to_string().into(), + predicate, + } + } +} + +impl Display for SimpleAssertion { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.expectation) + } +} + +impl Assertion for SimpleAssertion +where + F: FnOnce(I) -> O, + O: AssertionOutput, +{ + type Output = O; + + fn assert(self, target: I) -> Self::Output { + (self.predicate)(target) + } +} diff --git a/src/experimenting/v6/combinator.rs b/src/experimenting/v6/combinator.rs new file mode 100644 index 0000000..30dfa0b --- /dev/null +++ b/src/experimenting/v6/combinator.rs @@ -0,0 +1,47 @@ +use super::{AllCombinator, NotCombinator}; + +/// A type which builds an assertion to execute on a value. +pub trait AssertionCombinator: Sized { + /// The input passed to the *next assertion* in the chain. + /// + /// This combinator's associated assertion may receive a different type. For + /// example, this combinator may accept some (known) iterable value, like an + /// `[i32; 4]`, and pass an [`i32`] to the next assertion in the chain. This + /// type would then be [`i32`]. + type NextInput; + + /// The associated assertion. This is what gets built when this combinator + /// is applied to another assertion. + // This type cannot be a GAT since each combinator may have different type + // bounds on the assertion. For example, `NotAssertion` may require + // that `Next` returns a `Result<(), AssertionFailure>` to implement the + // `Assertion` trait. + // + // In those cases, the bound would either need to be specified in the `impl` + // block for the combinator, or on the `impl` block for the assertion. If + // this associated type is constrained such that it must be an assertion, + // then the combinator's `impl` block must enforce the constraint as well. + type Assertion; + + /// Builds an assertion using this combinator. + fn build(self, assertion: Next) -> Self::Assertion; + + // Since we don't know yet what assertion will be passed into the + // `NotCombinator` type, we can't specify the `Next` parameter to constrain + // this method such that it can only be called when that particular impl's + // bounds are satisfied. We also don't yet know what the return value will + // be for the final assertion because it hasn't been fully built yet. + // + // There needs to be a way to constrain the assertions chained onto this so + // that they satisfy the bounds on the `NotCombinator`'s assertion. + fn not(self) -> NotCombinator { + NotCombinator::new(self) + } + + fn all(self) -> AllCombinator + where + Self::NextInput: IntoIterator, + { + AllCombinator::new(self) + } +} diff --git a/src/experimenting/v6/error.rs b/src/experimenting/v6/error.rs new file mode 100644 index 0000000..7043626 --- /dev/null +++ b/src/experimenting/v6/error.rs @@ -0,0 +1,26 @@ +// - combinators are built in the direction data flows +// - eg. All>>> +// - assertions are built in the direction the assertion is wrapped +// - eg. Root>>>> +// - this is opposite the direction combinators are built in +// - combinators: +// - need to know the type of the next input to constrain the chain +// - ex should be possible to know when `.all()` is applicable +// - already know the input type since the combinator wraps the root value +// - no need to be generic over it as a result +// - making them generic over the "next assertion" at the trait level makes it +// difficult to create bounds over the trait since a known assertion type +// must be provided to get the associated types +// - assertion type must be generic over the "next assertion" since it wraps +// the assertion +// - assertions: +// - do not need to be generic over the input type because they already know +// the type they are being executed on +// - have variable return types - `()`, `impl Future`, etc +// - `NotAssertion` needs to know how to invert the return type between +// success and failure +// - can use a trait for this rather than requiring a fixed output type +#[derive(Debug, Default)] +pub struct AssertionError { + fields: Vec<(String, String)>, +} diff --git a/src/experimenting/v6/not.rs b/src/experimenting/v6/not.rs new file mode 100644 index 0000000..f3f7bb6 --- /dev/null +++ b/src/experimenting/v6/not.rs @@ -0,0 +1,69 @@ +use std::fmt::{self, Display, Formatter}; + +use super::{Assertion, AssertionCombinator, AssertionError, AssertionOutput}; + +/// Inverts the result of an assertion. +#[derive(Clone, Debug)] +pub struct NotCombinator { + prev: Prev, +} + +impl NotCombinator { + /// Creates a new [`NotCombinator`]. + #[inline] + pub fn new(prev: Prev) -> Self { + Self { prev } + } +} + +impl AssertionCombinator for NotCombinator +where + Prev: AssertionCombinator>, + NotAssertion: Assertion, +{ + type NextInput = Prev::NextInput; + type Assertion = Prev::Assertion; + + fn build(self, assertion: Next) -> Self::Assertion { + self.prev.build(NotAssertion::new(assertion)) + } +} + +/// Inverts the result of the next assertion. +#[derive(Clone, Debug)] +pub struct NotAssertion { + next: Next, +} + +impl NotAssertion { + /// Creates a new [`NotAssertion`]. + #[inline] + pub fn new(next: Next) -> Self { + NotAssertion { next } + } +} + +impl Display for NotAssertion +where + Next: Display, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "the following to not be satisfied: {}", self.next) + } +} + +impl Assertion for NotAssertion +where + Next: Assertion, +{ + type Output = ::Mapped< + fn(Result<(), AssertionError>) -> Result<(), AssertionError>, + >; + + fn assert(self, target: Input) -> Self::Output { + self.next.assert(target).map(|result| match result { + Ok(_) => Err(AssertionError::default()), + Err(_) => Ok(()), + }) + } +} diff --git a/src/experimenting/v7.rs b/src/experimenting/v7.rs new file mode 100644 index 0000000..3abf033 --- /dev/null +++ b/src/experimenting/v7.rs @@ -0,0 +1,180 @@ +mod all; +mod assertion; +mod combinator; +mod error; +mod not; + +pub use all::*; +pub use assertion::*; +pub use combinator::*; +pub use error::*; +pub use not::*; + +use std::fmt::{self, Display, Formatter}; + +///////// + +#[derive(Clone, Debug)] +pub struct AssertionRoot { + target: T, +} + +impl AssertionCombinator for AssertionRoot +where + A: Assertion, +{ + type NextInput = T; + type Assertion = RootAssertion; + + fn build(self, assertion: A) -> Self::Assertion { + RootAssertion { + target: self.target, + next: assertion, + } + } +} + +#[derive(Clone, Debug)] +pub struct RootAssertion { + target: T, + next: Next, +} + +impl Display for RootAssertion +where + Next: Display, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + writeln!(f, "{}", self.next) + } +} + +impl Assertion<()> for RootAssertion +where + Next: Assertion, +{ + type Output = Next::Output; + + fn assert(self, _target: ()) -> Self::Output { + self.next.assert(self.target) + } +} + +fn a() { + let root = AssertionRoot { target: [1, 2, 3] }; + let assertion = root + .all() + .not() + .build(SimpleAssertion::new("not zero", |value| { + if value != 0 { + Ok(()) + } else { + Err(AssertionError::default()) + } + })); + + let _result = assertion.assert(()); +} + +// #[derive(Clone, Debug)] +// pub struct MapAssertion { +// next: Next, +// f: fn(I) -> O, +// } + +// impl Display for MapAssertion +// where +// Next: Display, +// { +// fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { +// todo!() +// } +// } + +// impl Assertion for MapAssertion +// where +// Next: Assertion, +// { +// type Input = I; +// type Output = Next::Output; + +// fn assert(self, target: Self::Input) -> Self::Output { +// self.next.assert((self.f)(target)) +// } +// } + +// #[derive(Clone, Debug)] +// pub struct WhenReadyAssertion { +// next: Next, +// marker: PhantomData, +// } + +// impl Display for WhenReadyAssertion +// where +// Next: Display, +// { +// fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { +// todo!() +// } +// } + +// impl Assertion for WhenReadyAssertion +// where +// I: Future + Send + 'static, +// Next: Assertion + Send + 'static, +// { +// type Input = I; +// type Output = Pin + Send>>; + +// fn assert(self, target: Self::Input) -> Self::Output { +// Box::pin(async move { self.next.assert(target.await) }) +// } +// } + +// async fn foo() { +// let assertion = RootAssertion { +// target: ready([1, 2, 3]), +// next: WhenReadyAssertion { +// marker: PhantomData, +// next: AllAssertion { +// marker: PhantomData, +// next: SimpleAssertion::new("non-zero", |value| { +// if value == 0 { +// Err(AssertionFailure::default()) +// } else { +// Ok(()) +// } +// }), +// }, +// }, +// }; +// let result = assertion.assert(()).await; + +// let assertion = RootAssertion { +// target: [1, 2, 3], +// next: AllAssertion { +// marker: PhantomData, +// next: SimpleAssertion::new("non-zero", |value| { +// if value == 0 { +// Err(AssertionFailure::default()) +// } else { +// Ok(()) +// } +// }), +// }, +// }; +// let result = assertion.assert(()); + +// let combinator = AllCombinator { +// marker: PhantomData, +// prev: AssertionRoot { target: [1, 2, 3] }, +// }; +// let assertion = combinator.build(SimpleAssertion::new("non-zero", |value| { +// if value == 0 { +// Err(AssertionFailure::default()) +// } else { +// Ok(()) +// } +// })); +// let result = assertion.assert(()); +// } diff --git a/src/experimenting/v7/all.rs b/src/experimenting/v7/all.rs new file mode 100644 index 0000000..cb104b0 --- /dev/null +++ b/src/experimenting/v7/all.rs @@ -0,0 +1,73 @@ +use std::fmt::{self, Display, Formatter}; + +use super::{Assertion, AssertionCombinator, AssertionError}; + +pub struct AllCombinator { + prev: Prev, +} + +impl AllCombinator { + /// Creates a new [`AllCombinator`]. + #[inline] + pub fn new(prev: Prev) -> Self { + Self { prev } + } +} + +impl AssertionCombinator for AllCombinator +where + Prev: AssertionCombinator, + Prev::NextInput: IntoIterator, + // Result<(), AssertionError>: FromIterator, + Result<(), AssertionError>: FromIterator<::NextInput as IntoIterator>::Item>>::Output> +{ + type NextInput = ::Item; + type Assertion = Prev::Assertion> + where + Next: Assertion; + + fn build(self, assertion: Next) -> Self::Assertion + where + Next: Assertion, + { + self.prev.build(AllAssertion::new(assertion)) + } +} + +#[derive(Clone, Debug)] +pub struct AllAssertion { + next: Next, +} + +impl AllAssertion { + /// Creates a new [`AllAssertion`]. + #[inline] + pub fn new(next: Next) -> Self { + Self { next } + } +} + +impl Display for AllAssertion +where + Next: Display, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "for each inner value, {}", self.next) + } +} + +impl Assertion for AllAssertion +where + Input: IntoIterator, + Next: Assertion, + Result<(), AssertionError>: FromIterator, +{ + type Output = Result<(), AssertionError>; + + fn assert(&mut self, target: Input) -> Self::Output { + target + .into_iter() + .map(|item| self.next.assert(item)) + .collect() + } +} diff --git a/src/experimenting/v7/assertion.rs b/src/experimenting/v7/assertion.rs new file mode 100644 index 0000000..41e5a72 --- /dev/null +++ b/src/experimenting/v7/assertion.rs @@ -0,0 +1,90 @@ +use std::{ + fmt::{self, Display, Formatter}, + sync::Arc, +}; + +use super::AssertionError; + +/// Performs a validation on a value. The [`Display`] implementation should +/// output the predicate this assertion expects to be true of the value. +pub trait Assertion: Display + Sized { + /// The output of this assertion. + type Output: AssertionOutput; + + /// Execute the assertion on a target value. + fn assert(&mut self, target: Input) -> Self::Output; +} + +/// An output from executing an assertion. +/// +/// This generally represents some type that internally contains some +/// representation that can be converted to/from a +/// [`Result<(), AssertionFailure>`]. Since not all assertions can return a +/// [`Result`] directly, this allows those assertions to return something more +/// suitable for that assertion. +/// +/// For example, the [`WhenReadyCombinator`] returns a [`Future`] which can be +/// `.await`ed into a [`Result`]. +pub trait AssertionOutput { + /// The inverted result type. This is returned by + /// [`map`](AssertionOutput::map). + type Mapped: AssertionOutput + where + F: FnOnce(Result<(), AssertionError>) -> Result<(), AssertionError>; + + /// Maps the result represented by this output to a new result. + /// + /// This enables combinators to transform the result of the assertion + /// without necessarily needing to know the type of the assertion. For + /// example, the [`NotCombinator`] uses this method to transform a success + /// into a failure and a failure into a success. + fn map(self, f: F) -> Self::Mapped + where + F: FnOnce(Result<(), AssertionError>) -> Result<(), AssertionError>; +} + +impl AssertionOutput for Result<(), AssertionError> { + type Mapped = Self + where + F: FnOnce(Result<(), AssertionError>) -> Result<(), AssertionError>; + + fn map(self, f: F) -> Self::Mapped + where + F: FnOnce(Result<(), AssertionError>) -> Result<(), AssertionError>, + { + f(self) + } +} + +#[derive(Clone, Debug)] +pub struct SimpleAssertion { + expectation: Arc, + predicate: F, +} + +impl SimpleAssertion { + pub fn new(expectation: impl ToString, predicate: F) -> Self { + Self { + expectation: expectation.to_string().into(), + predicate, + } + } +} + +impl Display for SimpleAssertion { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.expectation) + } +} + +impl Assertion for SimpleAssertion +where + F: FnMut(I) -> O, + O: AssertionOutput, +{ + type Output = O; + + fn assert(&mut self, target: I) -> Self::Output { + (self.predicate)(target) + } +} diff --git a/src/experimenting/v7/combinator.rs b/src/experimenting/v7/combinator.rs new file mode 100644 index 0000000..a0c4855 --- /dev/null +++ b/src/experimenting/v7/combinator.rs @@ -0,0 +1,51 @@ +use super::{AllCombinator, Assertion, NotCombinator}; + +/// A type which builds an assertion to execute on a value. +pub trait AssertionCombinator: Sized { + /// The input passed to the *next assertion* in the chain. + /// + /// This combinator's associated assertion may receive a different type. For + /// example, this combinator may accept some (known) iterable value, like an + /// `[i32; 4]`, and pass an [`i32`] to the next assertion in the chain. This + /// type would then be [`i32`]. + type NextInput; + + /// The associated assertion. This is what gets built when this combinator + /// is applied to another assertion. + // This type cannot be a GAT since each combinator may have different type + // bounds on the assertion. For example, `NotAssertion` may require + // that `Next` returns a `Result<(), AssertionFailure>` to implement the + // `Assertion` trait. + // + // In those cases, the bound would either need to be specified in the `impl` + // block for the combinator, or on the `impl` block for the assertion. If + // this associated type is constrained such that it must be an assertion, + // then the combinator's `impl` block must enforce the constraint as well. + type Assertion + where + Next: Assertion; + + /// Builds an assertion using this combinator. + fn build(self, assertion: Next) -> Self::Assertion + where + Next: Assertion; + + // Since we don't know yet what assertion will be passed into the + // `NotCombinator` type, we can't specify the `Next` parameter to constrain + // this method such that it can only be called when that particular impl's + // bounds are satisfied. We also don't yet know what the return value will + // be for the final assertion because it hasn't been fully built yet. + // + // There needs to be a way to constrain the assertions chained onto this so + // that they satisfy the bounds on the `NotCombinator`'s assertion. + fn not(self) -> NotCombinator { + NotCombinator::new(self) + } + + fn all(self) -> AllCombinator + where + Self::NextInput: IntoIterator, + { + AllCombinator::new(self) + } +} diff --git a/src/experimenting/v7/error.rs b/src/experimenting/v7/error.rs new file mode 100644 index 0000000..7043626 --- /dev/null +++ b/src/experimenting/v7/error.rs @@ -0,0 +1,26 @@ +// - combinators are built in the direction data flows +// - eg. All>>> +// - assertions are built in the direction the assertion is wrapped +// - eg. Root>>>> +// - this is opposite the direction combinators are built in +// - combinators: +// - need to know the type of the next input to constrain the chain +// - ex should be possible to know when `.all()` is applicable +// - already know the input type since the combinator wraps the root value +// - no need to be generic over it as a result +// - making them generic over the "next assertion" at the trait level makes it +// difficult to create bounds over the trait since a known assertion type +// must be provided to get the associated types +// - assertion type must be generic over the "next assertion" since it wraps +// the assertion +// - assertions: +// - do not need to be generic over the input type because they already know +// the type they are being executed on +// - have variable return types - `()`, `impl Future`, etc +// - `NotAssertion` needs to know how to invert the return type between +// success and failure +// - can use a trait for this rather than requiring a fixed output type +#[derive(Debug, Default)] +pub struct AssertionError { + fields: Vec<(String, String)>, +} diff --git a/src/experimenting/v7/not.rs b/src/experimenting/v7/not.rs new file mode 100644 index 0000000..f01766a --- /dev/null +++ b/src/experimenting/v7/not.rs @@ -0,0 +1,73 @@ +use std::fmt::{self, Display, Formatter}; + +use super::{Assertion, AssertionCombinator, AssertionError, AssertionOutput}; + +/// Inverts the result of an assertion. +#[derive(Clone, Debug)] +pub struct NotCombinator { + prev: Prev, +} + +impl NotCombinator { + /// Creates a new [`NotCombinator`]. + #[inline] + pub fn new(prev: Prev) -> Self { + Self { prev } + } +} + +impl AssertionCombinator for NotCombinator +where + Prev: AssertionCombinator, +{ + type NextInput = Prev::NextInput; + type Assertion = Prev::Assertion> + where + Next: Assertion; + + fn build(self, assertion: Next) -> Self::Assertion + where + Next: Assertion, + { + self.prev.build(NotAssertion::new(assertion)) + } +} + +/// Inverts the result of the next assertion. +#[derive(Clone, Debug)] +pub struct NotAssertion { + next: Next, +} + +impl NotAssertion { + /// Creates a new [`NotAssertion`]. + #[inline] + pub fn new(next: Next) -> Self { + NotAssertion { next } + } +} + +impl Display for NotAssertion +where + Next: Display, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "the following to not be satisfied: {}", self.next) + } +} + +impl Assertion for NotAssertion +where + Next: Assertion, +{ + type Output = ::Mapped< + fn(Result<(), AssertionError>) -> Result<(), AssertionError>, + >; + + fn assert(&mut self, target: Input) -> Self::Output { + self.next.assert(target).map(|result| match result { + Ok(_) => Err(AssertionError::default()), + Err(_) => Ok(()), + }) + } +} diff --git a/src/experimenting/v8.rs b/src/experimenting/v8.rs new file mode 100644 index 0000000..dc33f30 --- /dev/null +++ b/src/experimenting/v8.rs @@ -0,0 +1,29 @@ +use std::{fmt::Display, future::Future}; + +pub trait SyncCombinator { + type Target; + type Result; + + fn to_satisfy(self, assertion: A) -> Self::Result + where + A: Assertion>; +} + +pub trait AsyncCombinator { + type Target; + type Result: Future + where + A: Assertion, + A::Output: Future>; + + fn to_satisfy(self, assertion: A) -> Self::Result + where + A: Assertion, + A::Output: Future>; +} + +pub trait Assertion: Display { + type Output; + + fn assert(self, input: Input) -> Self::Output; +} diff --git a/src/experimenting/v9/assertions.rs b/src/experimenting/v9/assertions.rs new file mode 100644 index 0000000..e4711b0 --- /dev/null +++ b/src/experimenting/v9/assertions.rs @@ -0,0 +1,5 @@ +mod assertion; +mod simple_assertion; + +pub use assertion::*; +pub use simple_assertion::*; diff --git a/src/experimenting/v9/assertions/assertion.rs b/src/experimenting/v9/assertions/assertion.rs new file mode 100644 index 0000000..8dddd66 --- /dev/null +++ b/src/experimenting/v9/assertions/assertion.rs @@ -0,0 +1,20 @@ +use std::fmt::Display; + +/// An assertion which can pass or fail when provided an input. The input value +/// is represented by the `Target` type parameter. +/// +/// The [`Display`] implementation of the assertion is used to describe the +/// assertion's expectation. The format of the expectation should be all +/// lowercase without any sentence terminating puctuation (like periods), and +/// should succinctly describe what the assertion is testing. For example, an +/// expectation could be "the given value is even". For an assertion that wraps +/// another assertion, the inner assertion's [`Display`] implementation can be +/// used, for example "when the future is ready, {inner}". +#[must_use = "assertions do nothing until 'assert' is called"] +pub trait Assertion: Display { + /// The output from executing this assertion. + type Output; + + /// Performs the assertion on a target value. + fn assert(self, target: Target) -> Self::Output; +} diff --git a/src/experimenting/v9/assertions/simple_assertion.rs b/src/experimenting/v9/assertions/simple_assertion.rs new file mode 100644 index 0000000..8285eb1 --- /dev/null +++ b/src/experimenting/v9/assertions/simple_assertion.rs @@ -0,0 +1,61 @@ +use std::fmt::{Display, Formatter}; + +use dyn_clone::{clone_trait_object, DynClone}; + +use crate::AssertionResult; + +use super::Assertion; + +/// A simple assertion consisting only of a closure and an expectation string. +#[derive(Clone, Debug)] +pub struct SimpleAssertion { + expectation: String, + assert: F, +} + +impl SimpleAssertion { + /// Create a new simple assertion. + #[inline] + pub fn new(expectation: impl Display, assert: F) -> Self { + Self { + expectation: expectation.to_string(), + assert, + } + } +} + +impl Display for SimpleAssertion { + #[inline] + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + self.expectation.fmt(f) + } +} + +impl Assertion for SimpleAssertion +where + F: FnOnce(Target) -> Output, +{ + type Output = Output; + + #[inline] + fn assert(self, target: Target) -> Self::Output { + (self.assert)(target) + } +} + +/// A simple, cloneable, boxable assertion function. This is used to create a +/// boxed assertion that can be trivially shared. +pub trait SimpleAssertionFn: DynClone { + fn call(self, target: Target) -> AssertionResult; +} + +clone_trait_object!( SimpleAssertionFn); + +impl SimpleAssertionFn for F +where + F: (FnOnce(Target) -> AssertionResult) + Clone, +{ + fn call(self, target: Target) -> AssertionResult { + self(target) + } +} diff --git a/src/experimenting/v9/combinators.rs b/src/experimenting/v9/combinators.rs new file mode 100644 index 0000000..f554af9 --- /dev/null +++ b/src/experimenting/v9/combinators.rs @@ -0,0 +1,13 @@ +mod all; +mod any; +mod combinator; +mod combinator_ext; +mod not; +mod when_ready; + +pub use all::*; +pub use any::*; +pub use combinator::*; +pub use combinator_ext::*; +pub use not::*; +pub use when_ready::*; diff --git a/src/experimenting/v9/combinators/all.rs b/src/experimenting/v9/combinators/all.rs new file mode 100644 index 0000000..404d76b --- /dev/null +++ b/src/experimenting/v9/combinators/all.rs @@ -0,0 +1,80 @@ +use std::fmt::{Display, Formatter}; + +use crate::AssertionResult; + +use super::{Assertion, AssertionCombinator}; + +/// Wraps another [`AssertionCombinator`] and ensures the assertion succeeds for +/// every inner value. +#[derive(Clone, Debug)] +pub struct AllCombinator { + inner: Inner, +} + +impl AllCombinator { + /// Creates a new instance of this combinator, wrapping the inner + /// combinator. + #[inline] + pub fn new(inner: Inner) -> Self { + Self { inner } + } +} + +impl AssertionCombinator for AllCombinator +where + Inner: AssertionCombinator>, + Inner::NextTarget: IntoIterator, +{ + type Target = Inner::Target; + type NextTarget = ::Item; + type Assertion = Inner::Assertion; + + #[inline] + fn apply(self, next: Next) -> Self::Assertion { + self.inner.apply(AllAssertion::new(next)) + } +} + +/// Wraps another assertion and ensures that the inner assertion does not fail +/// for any value in the provided target. +#[derive(Clone, Debug, Default)] +pub struct AllAssertion { + next: Next, +} + +impl AllAssertion { + /// Creates a new instance of this assertion. + #[inline] + pub fn new(next: Next) -> Self { + Self { next } + } +} + +impl Display for AllAssertion +where + Next: Display, +{ + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "for each item, {}", self.next) + } +} + +impl Assertion for AllAssertion +where + Target: IntoIterator, + Next: Assertion + Clone, +{ + type Output = AssertionResult; + + fn assert(self, target: Target) -> Self::Output { + target + .into_iter() + .map(move |target| self.next.clone().assert(target)) + .collect() + } +} + +// TODO: make an "AllOutput"-style trait so this doesn't need to have the output +// bound, which would work similar to the previous `AssertionOutput` trait but +// only for the `.all()` combinator diff --git a/src/experimenting/v9/combinators/any.rs b/src/experimenting/v9/combinators/any.rs new file mode 100644 index 0000000..f6481aa --- /dev/null +++ b/src/experimenting/v9/combinators/any.rs @@ -0,0 +1,80 @@ +use std::fmt::{Display, Formatter}; + +use crate::{AssertionFailure, AssertionResult}; + +use super::{Assertion, AssertionCombinator}; + +/// Wraps another [`AssertionCombinator`] and ensures the assertion succeeds for +/// some inner value. +#[derive(Clone, Debug)] +pub struct AnyCombinator { + inner: Inner, +} + +impl AnyCombinator { + /// Creates a new instance of this combinator, wrapping the inner + /// combinator. + #[inline] + pub fn new(inner: Inner) -> Self { + Self { inner } + } +} + +impl AssertionCombinator for AnyCombinator +where + Inner: AssertionCombinator>, + Inner::NextTarget: IntoIterator, +{ + type Target = Inner::Target; + type NextTarget = ::Item; + type Assertion = Inner::Assertion; + + fn apply(self, next: Next) -> Self::Assertion { + self.inner.apply(AnyAssertion::new(next)) + } +} + +/// Wraps another assertion and ensures that the inner assertion passes for at +/// least one value in the provided target. +#[derive(Clone, Debug, Default)] +pub struct AnyAssertion { + next: Next, +} + +impl AnyAssertion { + /// Creates a new instance of this assertion. + #[inline] + pub fn new(next: Next) -> Self { + AnyAssertion { next } + } +} + +impl Display for AnyAssertion +where + Next: Display, +{ + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "for each item, {}", self.next) + } +} + +impl Assertion for AnyAssertion +where + Target: IntoIterator, + Next: Assertion + Clone, +{ + type Output = AssertionResult; + + fn assert(self, target: Target) -> Self::Output { + let mut error = AssertionFailure::builder().build("TODO"); + for value in target { + match self.next.clone().assert(value) { + Ok(()) => return Ok(()), + Err(e) => error = e, + } + } + + Err(error) + } +} diff --git a/src/experimenting/v9/combinators/combinator.rs b/src/experimenting/v9/combinators/combinator.rs new file mode 100644 index 0000000..385a4ac --- /dev/null +++ b/src/experimenting/v9/combinators/combinator.rs @@ -0,0 +1,69 @@ +use crate::assertions::Assertion; + +/// Transforms an assertion into a more complex assertion. +/// +/// Combinators are used to build more complex assertions out of less complex +/// ones. They function as a sort of "middleware" and are able to transform both +/// the input to an assertion and the output from one. +/// +/// The type parameter represents the type of assertion this combinator can +/// wrap. +#[must_use = "combinators do nothing until they are applied"] +pub trait AssertionCombinator { + /// The type of value passed to this combinator's assertion. + /// + /// For many combinators which wrap another combinator, this is simply + /// `Inner::Target` (where `Inner` is the inner combinator). + type Target; + + /// The type of value passed to the assertion that is passed to this + /// combinator. This is used to determine which combinators and assertions + /// are valid to be chained to a combinator. For example, this is used to + /// prevent code like `expect!(1).all()` from compiling and to provide + /// better completions to users. + /// + /// This should be set to the type of value that the assertion built by this + /// combinator will pass to its inner/next assertion. `expect!("hi")` will + /// pass a `&str` to the chained assertion for example, despite its + /// [`Target`](AssertionCombinator::Target) being `()`. + type NextTarget; + + /// The type of assertion that this combinator wraps the given assertion + /// with. + /// + /// For many combinators which wrap another combinator, this is simply + /// `Inner::Assertion` (where `Inner` is the inner combinator). Note that + /// this usually implies a bound on the implementation of + /// `Inner: AssertionCombinator>` if passing a custom + /// assertion into the inner combinator (which is generally recommended). + type Assertion: Assertion; + + /// Wraps an assertion. + /// + /// This function is the foundation for how combinators work. This is used + /// to create more complex assertions. The combinator works by wrapping the + /// given assertion with its own assertion. For example, the assertion + /// returned by this combinator can negate the output of the provided + /// assertion, call the provided assertion multiple times (if it's `Clone`), + /// or transform the output of the provided assertion in some other manner. + /// + /// The returned assertion still needs to be executed on a value. The type + /// of input the returned assertion accepts is determined by the + /// [`AssertionCombinator::Target`] property. + /// + /// This method also usually has the side effect of "inverting" the type. + /// For example, calling `expect!(value).not().all()` will create an + /// `AllCombinator>>` (where `T` is the type + /// of the value), and applying the combinator will generate an instance of + /// `RootAssertion>>`. + fn apply(self, next: Next) -> Self::Assertion; +} + +// expect!(a).not().all() +// -> AllCombinator>> +// .apply(next) +// -> Root>> +// -> Next needs to be Clone, must be type param to ensure that and so +// -> the AllAssertion can clone it +// .assert(value) +// -> first apply .all(), then apply .not() to invert, then identity at root diff --git a/src/experimenting/v9/combinators/combinator_ext.rs b/src/experimenting/v9/combinators/combinator_ext.rs new file mode 100644 index 0000000..94f2cdc --- /dev/null +++ b/src/experimenting/v9/combinators/combinator_ext.rs @@ -0,0 +1,146 @@ +use crate::{assertions::SimpleAssertion, AssertionFailure}; + +use super::{AllCombinator, AnyCombinator, AssertionCombinator, NotCombinator}; + +pub trait AssertionCombinatorExt: AssertionCombinator + Sized { + /// 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::NextTarget: 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::NextTarget: IntoIterator, + { + AnyCombinator::new(self) + } + + // fn all(self) -> AllCombinator + // where + // AllCombinator: AssertionCombinator AssertionResult>>, + // { + // AllCombinator::new(self) + // } + // fn any(self) -> AnyCombinator + // where + // AnyCombinator: + // AssertionCombinator AssertionResult>>, + // { + // AnyCombinator::new(self) + // } + + // fn to_equal(self, other: U) -> impl AssertionOutput + // where + // Self::Target: PartialEq, + // { + // self.apply("the values to be equal", move |value| { + // (value == other) + // .then_some(()) + // .ok_or_else(|| AssertionFailure::builder().build("the values to be equal")) + // }) + // } +} + +impl AssertionCombinatorExt for T where T: AssertionCombinator {} + +pub trait AssertionCombinatorAssertionsExt: + AssertionCombinator, NextTarget = Target> + Sized +{ + fn to_equal(self, other: U) + where + Target: PartialEq, + { + let assertion = SimpleAssertion::new("the values are equal", move |target| { + if target == other { + Ok(()) + } else { + Err(AssertionFailure::builder().build("TODO")) + } + }); + let assertion = self.apply(assertion); + } +} + +impl AssertionCombinatorAssertionsExt for C where + C: AssertionCombinator, NextTarget = Target> +{ +} diff --git a/src/experimenting/v9/combinators/not.rs b/src/experimenting/v9/combinators/not.rs new file mode 100644 index 0000000..b5f7ecd --- /dev/null +++ b/src/experimenting/v9/combinators/not.rs @@ -0,0 +1,75 @@ +use std::fmt::{Display, Formatter}; + +use crate::{AssertionFailure, AssertionResult}; + +use super::{Assertion, AssertionCombinator}; + +/// Wraps another [`AssertionCombinator`] and negates the output. 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 instance of this combinator, wrapping the inner + /// combinator. + #[inline] + pub fn new(inner: Inner) -> Self { + Self { inner } + } +} + +impl AssertionCombinator for NotCombinator +where + Inner: AssertionCombinator>, + Next: Assertion, +{ + type Target = Inner::Target; + type NextTarget = Inner::Target; + type Assertion = Inner::Assertion; + + fn apply(self, next: Next) -> Self::Assertion { + self.inner.apply(NotAssertion::new(next)) + } +} + +/// Wraps another assertion and inverts the result, passing if and only if the +/// inner assertion failed and failing if it passes. +#[derive(Clone, Debug, Default)] +pub struct NotAssertion { + next: Next, +} + +impl NotAssertion { + /// Creates a new instance of this assertion. + #[inline] + pub fn new(next: Next) -> Self { + NotAssertion { next } + } +} + +impl Display for NotAssertion +where + Next: Display, +{ + #[inline] + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + write!(f, "the following is not satisfied: {}", self.next) + } +} + +impl Assertion for NotAssertion +where + Next: Assertion, +{ + type Output = Next::Output; + + fn assert(self, target: Target) -> Self::Output { + let result = self.next.assert(target); + match result { + Ok(_) => Err(AssertionFailure::builder().build("TODO")), + Err(_) => Ok(()), + } + } +} diff --git a/src/experimenting/v9/combinators/when_ready.old.rs b/src/experimenting/v9/combinators/when_ready.old.rs new file mode 100644 index 0000000..524fdc3 --- /dev/null +++ b/src/experimenting/v9/combinators/when_ready.old.rs @@ -0,0 +1,354 @@ +use std::{ + fmt::{Display, Formatter}, + future::Future, + pin::Pin, + task::{ready, Context, Poll}, +}; + +use pin_project_lite::pin_project; + +use crate::{AssertionOutput, AssertionResult}; + +use super::{Assertion, AssertionCombinator}; + +/// Wraps another [`AssertionCombinator`] and executes assertions on the output +/// of the inner value's future. +/// +/// This causes assertions to be asynchronous. Assertion outputs from this +/// combinator will be wrapped in futures, meaning the assertions must be +/// `.await`ed. +#[derive(Clone, Debug)] +pub struct WhenReadyCombinator { + inner: Inner, +} + +impl WhenReadyCombinator { + /// Creates a new instance of this combinator, wrapping the inner + /// combinator. + #[inline] + pub fn new(inner: Inner) -> Self { + Self { inner } + } +} + +// impl AssertionCombinator for WhenReadyCombinator +// where +// Inner: AssertionCombinator, +// Inner::Target: Future, +// { +// type Target = ::Output; + +// type Assertion = WhenReadyOutput::Output) -> R>> +// where +// R: AssertionOutput, +// F: FnMut(Self::Target) -> R; + +// fn to_satisfy(self, expectation: impl Display, assert: F) -> Self::Assertion +// where +// R: AssertionOutput, +// F: FnMut(Self::Target) -> R, +// { +// // Since the assertion is passed to potentially multiple futures, we +// // need to wrap it in an Arc> to ensure that only one of those +// // futures actually executes the assertion at a time. +// let assert = Arc::new(Mutex::new(assert)); + +// self.inner.to_satisfy( +// format!("when the future is ready, {expectation}"), +// move |fut| { +// let assert = assert.clone(); +// WhenReadyOutput::evaluated( +// fut, +// Box::new(move |value| { +// let mut assert = assert.lock().unwrap(); +// assert(value) +// }) +// as Box::Output) -> R>, +// ) +// }, +// ) +// } +// } + +impl AssertionCombinator for WhenReadyCombinator +where + Inner: AssertionCombinator>, + Inner::Target: Future, +{ + type Target = Inner::Target; + + type Assertion = Inner::Assertion; + + fn apply(self, next: Next) -> Self::Assertion { + self.inner.apply(WhenReadyAssertion::new(next)) + } +} + +pub struct WhenReadyAssertion { + next: Next, +} + +impl WhenReadyAssertion { + pub fn new(next: Next) -> Self { + WhenReadyAssertion { next } + } +} + +impl Display for WhenReadyAssertion +where + Next: Display, +{ + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + write!(f, "when the future is ready, {}", self.next) + } +} + +impl Assertion for WhenReadyAssertion +where + Target: Future, + Next: Assertion, +{ + type Output = WhenReadyOutput; + + fn assert(self, target: Target) -> Self::Output { + WhenReadyOutput::evaluated(target, self.next) + } +} + +pin_project! { + /// An assertion output that will resolve to a success or an error + /// eventually. This is usually created when performing an assertion on the + /// output of a future. + #[derive(Clone, Debug)] + #[must_use] + pub struct WhenReadyOutput { + // TODO: is the double-pin needed? + #[pin] + inner: WhenReadyOutputInner + } +} + +impl WhenReadyOutput { + #[inline] + fn evaluated(fut: Fut, map: A) -> Self { + Self { + inner: WhenReadyOutputInner::Evaluated { + fut, + map: Some(map), + }, + } + } + + #[inline] + fn forced(result: AssertionResult) -> Self { + Self { + inner: WhenReadyOutputInner::Forced { + result: Some(result), + }, + } + } +} + +pin_project! { + #[project = WhenReadyOutputInnerProj] + #[derive(Clone, Debug)] + enum WhenReadyOutputInner { + Forced { + result: Option, + }, + Evaluated { + #[pin] + fut: Fut, + map: Option, + }, + } +} + +// impl Future for WhenReadyOutput +// where +// Fut: Future, +// A: FnOnce(Fut::Output) -> R, +// R: AssertionOutput, +// { +// type Output = R; + +// fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { +// let projected = self.project(); +// let projected = projected.inner.project(); + +// match projected { +// WhenReadyOutputInnerProj::Forced { result } => { +// let result = result.take().expect("poll after ready"); +// Poll::Ready(match result { +// Ok(()) => R::new_success(), +// Err(failure) => R::new_failure(failure), +// }) +// } +// WhenReadyOutputInnerProj::Evaluated { fut, assert } => { +// let output = ready!(fut.poll(cx)); +// let assert = assert.take().expect("polled after ready"); +// Poll::Ready(assert(output)) +// } +// } +// } +// } + +impl Future for WhenReadyOutput +where + Fut: Future, + M: FnOnce(Fut::Output) -> R, + R: AssertionOutput, +{ + type Output = R; + + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { + let projected = self.project(); + let projected = projected.inner.project(); + + match projected { + WhenReadyOutputInnerProj::Forced { result } => { + let result = result.take().expect("poll after ready"); + Poll::Ready(match result { + Ok(()) => Self::Output::new_success(), + Err(failure) => Self::Output::new_failure(failure), + }) + } + WhenReadyOutputInnerProj::Evaluated { fut, map } => { + let output = ready!(fut.poll(cx)); + let map = map.take().expect("polled after ready"); + Poll::Ready(map(output)) + } + } + } +} + +// impl AssertionOutput for WhenReadyOutput +// where +// Fut: Future, +// A: Assertion, +// { +// type Mapped = WhenReadyOutput< +// Self, +// SimpleAssertion< +// A::Output, +// ::Mapped +// > +// > +// where +// F: FnMut(AssertionResult) -> AssertionResult; + +// type AndThen +// where +// O: AssertionOutput, +// F: FnOnce() -> O; + +// type OrElse +// where +// O: AssertionOutput, +// F: FnOnce() -> O; + +// type All +// where +// I: IntoIterator; + +// type Any +// where +// I: IntoIterator; + +// #[inline] +// fn new_success() -> Self { +// Self::forced(Ok(())) +// } + +// #[inline] +// fn new_failure(failure: AssertionFailure) -> Self { +// Self::forced(Err(failure)) +// } + +// #[inline] +// fn map(self, f: F) -> Self::Mapped +// where +// F: FnMut(AssertionResult) -> AssertionResult, +// { +// WhenReadyOutput::evaluated( +// self, +// SimpleAssertion::new("", move |output: A::Output| output.map(f)), +// ) +// } + +// fn and_then(self, other: F) -> Self::AndThen +// where +// O: AssertionOutput, +// F: FnOnce() -> O, +// { +// todo!() +// } + +// fn or_else(self, other: F) -> Self::OrElse +// where +// O: AssertionOutput, +// F: FnOnce() -> O, +// { +// todo!() +// } + +// fn all(outputs: I) -> Self::All +// where +// I: IntoIterator, +// { +// todo!() +// } + +// fn any(outputs: I) -> Self::Any +// where +// I: IntoIterator, +// { +// todo!() +// } + +// // #[inline] +// // fn map(self, f: F) -> impl AssertionOutput +// // where +// // F: FnMut(AssertionResult) -> AssertionResult, +// // { +// // WhenReadyOutput::evaluated(self, move |output: R| output.map(f)) +// // } + +// // #[inline] +// // fn and_then(self, other: F) -> impl AssertionOutput +// // where +// // O: AssertionOutput, +// // F: FnOnce() -> O, +// // { +// // WhenReadyOutput::evaluated(self, move |output: ::Output| { +// // output.and_then(other) +// // }) +// // } + +// // #[inline] +// // fn or_else(self, other: F) -> impl AssertionOutput +// // where +// // O: AssertionOutput, +// // F: FnOnce() -> O, +// // { +// // WhenReadyOutput::evaluated(self, move |output: ::Output| { +// // output.or_else(other) +// // }) +// // } + +// // #[inline] +// // fn all(outputs: impl IntoIterator) -> impl AssertionOutput { +// // // It would be nice to support short-circuiting here, but we need all +// // // the outputs ready at once to pass them to `R::all` +// // let stream: FuturesUnordered<_> = outputs.into_iter().collect(); +// // WhenReadyOutput::evaluated(stream.collect::>(), R::all) +// // } + +// // #[inline] +// // fn any(outputs: impl IntoIterator) -> impl AssertionOutput { +// // // It would be nice to support short-circuiting here, but we need all +// // // the outputs ready at once to pass them to `R::any` +// // let stream: FuturesUnordered<_> = outputs.into_iter().collect(); +// // WhenReadyOutput::evaluated(stream.collect::>(), R::any) +// // } +// } diff --git a/src/experimenting/v9/combinators/when_ready.rs b/src/experimenting/v9/combinators/when_ready.rs new file mode 100644 index 0000000..0e6e274 --- /dev/null +++ b/src/experimenting/v9/combinators/when_ready.rs @@ -0,0 +1,114 @@ +use std::{ + fmt::{Display, Formatter}, + future::Future, + pin::Pin, + task::{ready, Context, Poll}, +}; + +use pin_project_lite::pin_project; + +use super::{Assertion, AssertionCombinator}; + +/// Wraps another [`AssertionCombinator`] and executes assertions on the output +/// of the inner value's future. +/// +/// This causes assertions to be asynchronous. Assertion outputs from this +/// combinator will be wrapped in futures, meaning the assertions must be +/// `.await`ed. +#[derive(Clone, Debug)] +pub struct WhenReadyCombinator { + inner: Inner, +} + +impl WhenReadyCombinator { + /// Creates a new instance of this combinator, wrapping the inner + /// combinator. + #[inline] + pub fn new(inner: Inner) -> Self { + Self { inner } + } +} + +impl AssertionCombinator for WhenReadyCombinator +where + Inner: AssertionCombinator>, + Inner::NextTarget: Future, +{ + type Target = Inner::Target; + type NextTarget = ::Output; + type Assertion = Inner::Assertion; + + #[inline] + fn apply(self, next: Next) -> Self::Assertion { + self.inner.apply(WhenReadyAssertion::new(next)) + } +} + +/// Wraps another assertion and executes it on the output of the provided +/// future. +#[derive(Clone, Debug, Default)] +pub struct WhenReadyAssertion { + next: Next, +} + +impl WhenReadyAssertion { + /// Creates a new instance of this assertion. + #[inline] + pub fn new(next: Next) -> Self { + Self { next } + } +} + +impl Display for WhenReadyAssertion +where + Next: Display, +{ + #[inline] + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + write!(f, "when the future is ready, {}", self.next) + } +} + +impl Assertion for WhenReadyAssertion +where + Target: Future, + Next: Assertion, +{ + type Output = WhenReadyOutput; + + #[inline] + fn assert(self, target: Target) -> Self::Output { + WhenReadyOutput { + target, + next: Some(self.next), + } + } +} + +pin_project! { + /// An assertion output that will resolve to a success or an error + /// eventually. This is usually created when performing an assertion on the + /// output of a future. + #[derive(Clone, Debug, Default)] + #[must_use] + pub struct WhenReadyOutput { + #[pin] + target: Target, + next: Option, + } +} + +impl Future for WhenReadyOutput +where + Target: Future, + Next: Assertion, +{ + type Output = Next::Output; + + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { + let projected = self.project(); + let target = ready!(projected.target.poll(cx)); + let next = projected.next.take().expect("poll after ready"); + Poll::Ready(next.assert(target)) + } +} diff --git a/src/experimenting/v9/expect.rs b/src/experimenting/v9/expect.rs new file mode 100644 index 0000000..400d991 --- /dev/null +++ b/src/experimenting/v9/expect.rs @@ -0,0 +1,174 @@ +use std::fmt::{Display, Formatter}; + +use crate::combinators::{Assertion, AssertionCombinator}; + +/// 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), + ) + }; +} + +/// The root of an expectation. Other expectations are built on top of this. +#[derive(Clone, Debug)] +pub struct ExpectationRoot { + target: Target, + 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: Target, source_info: SourceInfo, target_source: &'static str) -> Self { + Self { + target, + source_info, + target_source, + } + } +} + +impl AssertionCombinator for ExpectationRoot +where + Next: Assertion, +{ + type Target = (); + type NextTarget = Target; + type Assertion = RootAssertion; + + #[inline] + fn apply(self, next: Next) -> Self::Assertion { + RootAssertion::new(self.target, next) + } +} + +/// The root-level assertion. This assertion wraps another assertion, passes +/// that assertion the target, and attaches additional context to the error if +/// the assertion fails. All assertions are normally eventually wrapped by this +/// type. +#[derive(Clone, Debug, Default)] +pub struct RootAssertion { + target: Target, + next: Next, +} + +impl RootAssertion { + /// Creates a new instance of this assertion. + #[inline] + pub fn new(target: Target, next: Next) -> Self { + Self { target, next } + } +} + +impl Display for RootAssertion +where + Next: Display, +{ + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.next.fmt(f) + } +} + +impl Assertion<()> for RootAssertion +where + Next: Assertion, +{ + type Output = Next::Output; + + #[inline] + fn assert(self, (): ()) -> Self::Output { + // TODO: attach source info to the error + self.next.assert(self.target) + } +} + +/// 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) + } +} + +#[cfg(test)] +mod tests { + use std::future::ready; + + use crate::{combinators::*, prelude::*, AssertionResult, ExpectationRoot}; + + #[test] + fn foo() { + // let combinator = ExpectationRoot::new([1, 2, 3], todo!(), "a"); + let _ = expect!([1, 2, 3]).all().all(); + // let combinator = ExpectationRoot::new(ready([1, 2, 3]), todo!(), "a"); + // let combinator = WhenReadyCombinator::new(combinator); + // let combinator = NotCombinator::new(combinator); + let combinator = AllCombinator::new(combinator); + let assertion = SimpleAssertion::new("always pass", |_value| AssertionResult::Ok(())); + let assertion = combinator.apply(assertion); + // let assertion = AssertionCombinator::apply(combinator, assertion); + // AllAssertion::new(assertion); + // let assertion = NotAssertion::new(AllAssertion::new(assertion)); + let _blah = assertion.assert(()); + // let assertion = combinator + + // let output = expect!(1).to_equal(1); + } +} diff --git a/src/experimenting/v9/failure.rs b/src/experimenting/v9/failure.rs new file mode 100644 index 0000000..ce39740 --- /dev/null +++ b/src/experimenting/v9/failure.rs @@ -0,0 +1,66 @@ +use std::{ + error::Error, + fmt::{Display, Formatter}, +}; + +pub type AssertionResult = Result; + +/// 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 AssertionFailure { + fields: Vec<(&'static str, String)>, +} + +impl AssertionFailure { + /// Creates a builder for a new failure. + pub fn builder() -> AssertionFailureBuilder { + AssertionFailureBuilder::default() + } +} + +impl Display for AssertionFailure { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + writeln!(f, "assertion failed.")?; + for (name, value) in &self.fields { + writeln!(f, " {name}: {value}")?; + } + + Ok(()) + } +} + +impl Error for AssertionFailure {} + +/// A builder for a failure. +#[derive(Clone, Debug)] +pub struct AssertionFailureBuilder { + fields: Vec<(&'static str, String)>, +} + +impl Default for AssertionFailureBuilder { + fn default() -> Self { + Self { + fields: vec![("expected", String::new())], + } + } +} + +impl AssertionFailureBuilder { + /// 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 Display) -> Self { + self.fields.push((name, value.to_string())); + self + } + + /// Builds the error with the given expectation. + pub fn build(mut self, expectation: impl Display) -> AssertionFailure { + self.fields[0].1 = expectation.to_string(); + AssertionFailure { + fields: self.fields, + } + } +} diff --git a/src/experimenting/v9/lib.rs b/src/experimenting/v9/lib.rs new file mode 100644 index 0000000..4ec39cd --- /dev/null +++ b/src/experimenting/v9/lib.rs @@ -0,0 +1,13 @@ +pub mod assertions; +pub mod combinators; +pub mod prelude; + +mod expect; +mod failure; +mod output; +mod third_party; + +pub use expect::*; +pub use failure::*; +pub use output::*; +pub use third_party::*; diff --git a/src/experimenting/v9/output.rs b/src/experimenting/v9/output.rs new file mode 100644 index 0000000..83b7d3a --- /dev/null +++ b/src/experimenting/v9/output.rs @@ -0,0 +1,208 @@ +use either::Either; + +use crate::{AssertionFailure, AssertionResult}; + +/// An output from performing an assertion. This represents either a success or +/// a failure, and should include additional information about the reason for +/// the failure. +/// +/// Assertion outputs fundamentally parallel `Result<(), AssertionFailure>`, +/// though it's not always trivial to convert one to a result. For example, +/// while `Result<(), AssertionFailure>` *is* an [`AssertionOutput`] and is +/// trivially convertible to itself, a [`Future`] +/// cannot be converted to a result trivially and must be polled to completion +/// to get a result. +pub trait AssertionOutput { + // TODO: docs + type Mapped: AssertionOutput + where + F: FnMut(AssertionResult) -> AssertionResult; + + type AndThen: AssertionOutput + where + O: AssertionOutput, + F: FnOnce() -> O; + + type OrElse: AssertionOutput + where + O: AssertionOutput, + F: FnOnce() -> O; + + type All: AssertionOutput + where + I: IntoIterator; + + type Any: AssertionOutput + where + I: IntoIterator; + + /// Creates a new output representing a success. + /// + /// This acts as a "default" success value for combinators that need it. For + /// example, the `.all` combinator uses this to create a success when there + /// are no values to execute its assertion on. + fn new_success() -> Self; + + /// Creates a new output representing the given failure. + /// + /// This acts as a "default" failure value for combinators that need it. For + /// example, the `.any` combinator uses this to create a failure when there + /// are no values to execute its assertion on. + fn new_failure(failure: AssertionFailure) -> Self; + + /// Maps this output to a new output. + /// + /// The function passed in is provided this output's value represented as a + /// result. It is not guaranteed that this function will be called right + /// away, and may be called in the future when needed instead. + fn map(self, f: F) -> Self::Mapped + where + F: FnMut(AssertionResult) -> AssertionResult; + + /// Creates a new output that succeeds if and only if this and another + /// output both succeed. + /// + /// This may choose to lazily evaluate `other`, but it is not guaranteed. + /// This means that `other` may not actually be evaluated if this output + /// already represents a failure (but this is not a guarantee). + /// + /// While it cannot be enforced at the type level, it is strongly encouraged + /// for implementers to support the commutitive law. This means that whether + /// this output `and` another output represents a success should be the same + /// as whether the other output `and` this output represents a success. + fn and_then(self, other: F) -> Self::AndThen + where + O: AssertionOutput, + F: FnOnce() -> O; + + /// Creates a new output that succeeds if and only if either this or another + /// output (or both) succeed. + /// + /// The remarks for [`and_then`](AssertionOutput::and_then) regarding lazy + /// evaluation and commutitivity apply here as well. See that documentation + /// for more information. + fn or_else(self, other: F) -> Self::OrElse + where + O: AssertionOutput, + F: FnOnce() -> O; + + /// Creates a new output that succeeds if and only if there are no outputs + /// in the given iterator that represent a failure. + /// + /// Note that this means empty iterators will always result in a success. + /// + /// The order the outputs are checked for success is unspecified, and it's + /// up to the implementer to decide what order they want to compare the + /// outputs in. For example, suppose the iterator represented a sequence of + /// 3 outputs. The implementer may decide to check if the second output + /// represents a success before looking at the other outputs. Because the + /// implementation may choose to short-circuit, if the second output + /// represents a failure, this means there is a chance that the other + /// outputs are not checked. + /// + /// In most cases, this does not matter. However, it may be relevant for + /// asynchronous outputs. In this case, if one output is ready before the + /// others and represents a failure, the implementation may choose to + /// short-circuit the rest of the outputs and not complete those futures. + /// However, this behavior is optional, and an implementer may also choose + /// to evaluate all the futures before checking if any are a success or a + /// failure. + // TODO: might be useful to explore a more robust "aggregate output" trait + // for things like mixing required/optional outputs, min/max successes + // or failures, support for short-circuiting, etc. + fn all(outputs: I) -> Self::All + where + I: IntoIterator; + + /// Creates a new output that succeeds if and only if there exists an output + /// that represents a success in the given iterator. + /// + /// Note that this means empty iterators will always result in a failure. + /// + /// The remarks for [`all`](AssertionOutput::all) regarding order + /// specificity also apply here. See that documentation for more + /// information. + fn any(outputs: I) -> Self::Any + where + I: IntoIterator; +} + +impl AssertionOutput for Result<(), AssertionFailure> { + type Mapped = Self + where + F: FnMut(AssertionResult) -> AssertionResult; + + type AndThen = Either + where + O: AssertionOutput, + F: FnOnce() -> O; + + type OrElse = Either + where + O: AssertionOutput, + F: FnOnce() -> O; + + type All = Self + where + I: IntoIterator; + + type Any = Self + where + I: IntoIterator; + + fn new_success() -> Self { + Ok(()) + } + + fn new_failure(failure: AssertionFailure) -> Self { + Err(failure) + } + + fn map(self, mut f: F) -> Self::Mapped + where + F: FnMut(Result<(), AssertionFailure>) -> Result<(), AssertionFailure>, + { + f(self) + } + + fn and_then(self, other: F) -> Self::AndThen + where + O: AssertionOutput, + F: FnOnce() -> O, + { + match self { + Ok(()) => Either::Right(other()), + failure => Either::Left(failure), + } + } + + fn or_else(self, other: F) -> Self::OrElse + where + O: AssertionOutput, + F: FnOnce() -> O, + { + match self { + Ok(()) => Either::Left(Ok(())), + Err(_) => Either::Right(other()), + } + } + + fn all(outputs: I) -> Self::All + where + I: IntoIterator, + { + outputs.into_iter().collect::() + } + + fn any(outputs: I) -> Self::Any + where + I: IntoIterator, + { + outputs + .into_iter() + .filter_map(Result::ok) + .next() + // TODO: build the failure correctly + .ok_or_else(|| AssertionFailure::builder().build("no values in assertion")) + } +} diff --git a/src/experimenting/v9/prelude.rs b/src/experimenting/v9/prelude.rs new file mode 100644 index 0000000..c3b183b --- /dev/null +++ b/src/experimenting/v9/prelude.rs @@ -0,0 +1,10 @@ +//! This module contains commonly used types, traits, and macros. To keep your +//! imports simple, rather than importing these members individually, you can +//! just write: +//! +//! ``` +//! # #[allow(unused_imports)] +//! use expecters::prelude::*; +//! ``` + +pub use crate::{combinators::AssertionCombinatorExt, expect}; diff --git a/src/experimenting/v9/third_party.rs b/src/experimenting/v9/third_party.rs new file mode 100644 index 0000000..695d72e --- /dev/null +++ b/src/experimenting/v9/third_party.rs @@ -0,0 +1,4 @@ +mod either; + +// Re-export to integrate better with other libraries +pub use ::either::Either; diff --git a/src/experimenting/v9/third_party/either.rs b/src/experimenting/v9/third_party/either.rs new file mode 100644 index 0000000..a5e5be1 --- /dev/null +++ b/src/experimenting/v9/third_party/either.rs @@ -0,0 +1,84 @@ +use either::Either; + +use crate::{AssertionFailure, AssertionOutput}; + +macro_rules! map_either { + ($value:expr, $pattern:pat => $result:expr) => { + match $value { + Either::Left($pattern) => Either::Left($result), + Either::Right($pattern) => Either::Right($result), + } + }; +} + +impl AssertionOutput for Either +where + L: AssertionOutput, + R: AssertionOutput, +{ + type Mapped = Either, R::Mapped> + where + F: FnMut(crate::AssertionResult) -> crate::AssertionResult; + + #[inline] + fn new_success() -> Self { + Either::Left(L::new_success()) + } + + #[inline] + fn new_failure(failure: AssertionFailure) -> Self { + Either::Left(L::new_failure(failure)) + } + + #[inline] + fn map(self, f: F) -> Self::Mapped + where + F: FnMut(Result<(), AssertionFailure>) -> Result<(), AssertionFailure>, + { + map_either!(self, inner => inner.map(f)) + } + + #[inline] + fn and_then(self, other: F) -> impl AssertionOutput + where + O: AssertionOutput, + F: FnOnce() -> O, + { + map_either!(self, inner => inner.and_then(other)) + } + + #[inline] + fn or_else(self, other: F) -> impl AssertionOutput + where + O: AssertionOutput, + F: FnOnce() -> O, + { + map_either!(self, inner => inner.or_else(other)) + } + + fn all(outputs: impl IntoIterator) -> impl AssertionOutput { + let mut lefts = Vec::new(); + let mut rights = Vec::new(); + for output in outputs { + match output { + Either::Left(inner) => lefts.push(inner), + Either::Right(inner) => rights.push(inner), + } + } + + L::all(lefts).and_then(move || R::all(rights)) + } + + fn any(outputs: impl IntoIterator) -> impl AssertionOutput { + let mut lefts = Vec::new(); + let mut rights = Vec::new(); + for output in outputs { + match output { + Either::Left(inner) => lefts.push(inner), + Either::Right(inner) => rights.push(inner), + } + } + + L::any(lefts).or_else(move || R::any(rights)) + } +} diff --git a/src/experimenting/v9/thoughts.md b/src/experimenting/v9/thoughts.md new file mode 100644 index 0000000..4adb72a --- /dev/null +++ b/src/experimenting/v9/thoughts.md @@ -0,0 +1,27 @@ +# Thoughts + +## combinator/middleware + +- knows what it's receiving (since it wraps the previous combinator) + - and doesn't need to say what it is (since it already "has" the value) +- knows what it's propagating + - and needs to say what it is (so other assertions/combinators know if they can be built from it) +- may have additional bounds on next assertion + - all/any need to be able to clone the next assertion + - alternatively, all assertions can be executed multiple times, but this is constraining + - always a bound on next assertion that it receives what the combinator propagates + +type params: + +- next assertion + +assoc types: + +- propagated value + +## assertion + +TODO + +- knows what it's receiving +- knows what it's propagating \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index b6b8650..fe4b9cf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,89 +1,12 @@ -//! Build composable assertions with a functional API. -//! -//! ``` -//! 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: -//! -//! - [`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. -//! -//! These combinators can be chained together as needed. For example: -//! -//! ``` -//! # 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(); -//! ``` -//! -//! 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. -//! -//! 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: -//! -//! ``` -//! # use expecters::prelude::*; -//! let result = expect!(42).as_result().to_be_less_than(10); -//! expect!(result).to_be_err(); -//! ``` -//! -//! 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. -//! -//! [`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}; -} +#![forbid(unsafe_code)] +pub mod assertions; +pub mod metadata; +pub mod prelude; #[doc(hidden)] pub mod specialization; + +mod macros; + +// pub use expect::*; +pub use assertions::AssertionResult; diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 0000000..73551c2 --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,542 @@ +/// 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 similar to how a function is called. 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, either import [`to_equal`] directly, or alias it: +/// +/// ``` +/// # #[allow(unused_import)] +/// # use expecters::prelude::*; +/// use expecters::prelude::to_equal as expect_to_equal; +/// expect!(1, not, expect_to_equal(0)); +/// ``` +/// +/// Note that aliasing a modifier or assertion will change its name in the error +/// message it generates as well. Error messages produced by the above assertion +/// will refer to the final parameter as `expect_to_equal` instead of `to_equal` +/// because of the alias. +/// +/// 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. +/// +/// TODO - maybe doc this in the `futures` module so it's cfg-locked and can use +/// doc tests +/// +/// ## 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`]: core::fmt::Debug +/// [`all`]: crate::prelude::all +/// [`map`]: crate::prelude::map +/// [`not`]: crate::prelude::not +/// [`to_equal`]: crate::prelude::to_equal +/// [stringified]: core::stringify +#[macro_export] +macro_rules! expect { + ($($tokens:tt)*) => { + $crate::__expect_inner!($($tokens)*) + }; +} + +#[macro_export] +#[doc(hidden)] +macro_rules! __expect_inner { + // Entrypoint + ( + $subject:expr, + $($assertions:tt)* + ) => {{ + let cx = $crate::assertions::AssertionContext::__new( + $crate::source_loc!(), + $crate::__expect_inner!( + @build_ctx_frames, + [], + $($assertions)* + ), + ); + let result = $crate::__expect_inner!( + @build_assertion, + cx, + $subject, + $($assertions)* + ); + $crate::assertions::general::FinalizableResult::finalize(result) + }}; + + // Build context frame names (from modifier/assertion names) + ( + // Recursive case + @build_ctx_frames, + [$($frames:expr),*], + $frame_name:ident $(($($_:tt)*))?, + $($assertions:tt)* + ) => { + $crate::__expect_inner!( + @build_ctx_frames, + [$($frames,)* ::core::stringify!($frame_name)], + $($assertions)* + ) + }; + ( + // Base case + @build_ctx_frames, + [$($frames:expr),*], + $frame_name:ident $(($($_:tt)*))? + ) => {{ + const FRAMES: &'static [&'static str] = &[ + $($frames,)* + ::core::stringify!($frame_name), + ]; + FRAMES + }}; + + // Build assertion (chain modifiers and final assertion) + ( + // Recursive case (with params) + @build_assertion, + $cx:expr, + $subject:expr, + $assertion:ident($($param:expr),* $(,)?), + $($rest:tt)* + ) => { + $crate::__expect_inner!( + @annotate_assertion, + $cx, + $subject, + |cx, subject| { + let assertion = $assertion($($crate::annotated!($param),)*); + assertion(cx, subject, |cx, no_debug_impl| { + $crate::__expect_inner!( + @build_assertion, + cx, + no_debug_impl, + $($rest)* + ) + }) + } + ) + }; + ( + // Recursive case (without params) + @build_assertion, + $cx:expr, + $subject:expr, + $assertion:ident, + $($rest:tt)* + ) => { + $crate::__expect_inner!( + @annotate_assertion, + $cx, + $subject, + |cx, subject| { + $assertion(cx, subject, |cx, no_debug_impl| { + $crate::__expect_inner!( + @build_assertion, + cx, + no_debug_impl, + $($rest)* + ) + }) + } + ) + }; + ( + // Base case (with params) + @build_assertion, + $cx:expr, + $subject:expr, + $assertion:ident($($param:expr),* $(,)?) $(,)? + ) => { + $crate::__expect_inner!( + @annotate_assertion, + $cx, + $subject, + |cx, subject| { + let assertion = $assertion($($crate::annotated!($param),)*); + assertion(cx, subject) + } + ) + }; + ( + // Base case (without params) + @build_assertion, + $cx:expr, + $subject:expr, + $assertion:ident $(,)? + ) => { + $crate::__expect_inner!( + @annotate_assertion, + $cx, + $subject, + |cx, subject| { + let assertion = $assertion; + assertion(cx, subject) + } + ) + }; + + // Wrap assertion and annotate intermediate value + ( + @annotate_assertion, + $cx:expr, + $subject:expr, + |$cx_param:pat_param, $subject_param:pat_param| $assertion:expr + ) => { + $crate::assertions::__annotate_assertion( + $cx, + $crate::annotated!($subject), + |$cx_param, $subject_param| $assertion, + ) + }; +} + +#[cfg(test)] +mod tests { + use core::future::ready; + use std::marker::PhantomData; + + use crate::{ + assertions::{ + general::{FinalizableResult, InvertibleResult}, + Assertion, AssertionContext, AssertionModifier, + }, + metadata::Annotated, + prelude::*, + AssertionResult, + }; + + #[derive(PartialEq)] + struct NotDebug(T); + + #[tokio::test] + async fn test_debugging() { + debugging().await; + } + + async fn debugging() { + expect!( + [NotDebug(1), NotDebug(2), NotDebug(3)], + all, + map(|x: NotDebug<_>| x.0), + not, + to_be_less_than(3) + ); + + expect!([ready(1), ready(2)], all, when_ready, to_be_less_than(2)).await; + } + + // async fn debugging2() { + // // expect!(1, not, to_equal(0)); + + // let x = 1; + // expect!(1, not, to_equal(x)); + + // { + // const SOURCE_LOC: crate::metadata::SourceLoc = crate::source_loc!(); + // let cx = crate::assertions::AssertionContext::__new(&SOURCE_LOC, { + // const FRAMES: &'static [&'static str] = &[("not"), "to_equal"]; + // FRAMES + // }); + // let result = + // crate::assertions::__annotate_assertion(cx, crate::annotated!(1), |cx, subject| { + // not(cx, subject, |cx, no_debug_impl| { + // crate::assertions::__annotate_assertion( + // cx, + // crate::annotated!(no_debug_impl), + // |cx, subject| { + // let assertion = to_equal(crate::annotated!(0)); + // assertion(cx, subject) + // }, + // ) + // }) + // }); + // crate::assertions::general::FinalizableResult::finalize(result) + // } + + // /* + // { + // let subject = crate::annotated!(1); + // let assertion = __annotate_assertion_begin( + // &subject, + // |x| crate::annotated!(x), + // ); + // let assertion = assertion.apply( + // not().apply( + // __annotate_assertion2( + // to_equal(0) + // ) + // ); + // } + // */ + // } + + #[test] + fn debugging3() { + let _res = { + let cx = AssertionContext::__new( + crate::source_loc!(), + &["not2", "not2", "not2", "to_equal2"], + ); + + // TODO + let chain = Root(1); + let chain = annotate_input(|x| crate::annotated!(x))(chain); + let chain = not2(chain); + let chain = annotate_input(|x| crate::annotated!(x))(chain); + let chain = not2(chain); + let chain = annotate_input(|x| crate::annotated!(x))(chain); + let chain = not2(chain); + let chain = annotate_input(|x| crate::annotated!(x))(chain); + let assert = to_equal2(1); + let res = chain.apply(cx, assert); + + res.finalize() + }; + } + + struct Root(T); + + impl AssertionModifier for Root + where + A: Assertion, + { + type Output = A::Output; + + fn apply(self, cx: AssertionContext, assertion: A) -> Self::Output { + assertion.execute(cx, self.0) + } + } + + fn annotate_input( + annotate: fn(T) -> Annotated, + ) -> impl FnOnce(M) -> AnnotateModifier { + move |prev| AnnotateModifier { prev, annotate } + } + + #[derive(Clone, Debug)] + struct AnnotateModifier { + prev: M, + annotate: fn(T) -> Annotated, + } + + impl AssertionModifier for AnnotateModifier + where + M: AssertionModifier>, + { + type Output = M::Output; + + fn apply(self, cx: AssertionContext, assertion: A) -> Self::Output { + self.prev.apply( + cx, + AnnotateAssertion { + next: assertion, + annotate: self.annotate, + }, + ) + } + } + + #[derive(Clone, Debug)] + struct AnnotateAssertion { + next: A, + annotate: fn(T) -> Annotated, + } + + impl Assertion for AnnotateAssertion + where + A: Assertion, + { + type Output = A::Output; + + fn execute(self, cx: AssertionContext, value: T) -> Self::Output { + self.next.execute(cx.next(), value) + } + } + + #[inline] + fn not2(prev: M) -> Not2Modifier { + Not2Modifier(prev, PhantomData) + } + + #[derive(Clone, Debug)] + struct Not2Modifier(M, PhantomData); + + impl AssertionModifier for Not2Modifier + where + A: Assertion, + A::Output: InvertibleResult, + M: AssertionModifier>, + { + type Output = M::Output; + + #[inline] + fn apply(self, cx: AssertionContext, assertion: A) -> Self::Output { + self.0.apply(cx, Not2Assertion(assertion)) + } + } + + #[derive(Clone, Debug)] + struct Not2Assertion(A); + + impl Assertion for Not2Assertion + where + A: Assertion, + A::Output: InvertibleResult, + { + type Output = ::Inverted; + + #[inline] + fn execute(self, cx: AssertionContext, value: T) -> Self::Output { + self.0.execute(cx.clone(), value).invert(cx) + } + } + + fn to_equal2(value: U) -> ToEqual2Assertion { + ToEqual2Assertion(value) + } + + #[derive(Clone, Debug)] + struct ToEqual2Assertion(U); + + impl Assertion for ToEqual2Assertion + where + T: PartialEq, + { + type Output = AssertionResult; + + fn execute(self, cx: AssertionContext, value: T) -> Self::Output { + if value == self.0 { + Ok(()) + } else { + Err(cx.fail("not equal")) + } + } + } +} 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..0297caf --- /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::*; + + let wrapper = $crate::specialization::__SpecializeWrapper($value); + (&wrapper) + .__annotated_kind() + .annotate(wrapper.0, ::core::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](core::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_ref() + .map(String::as_str) + .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")] + 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..1851561 --- /dev/null +++ b/src/metadata/source_loc.rs @@ -0,0 +1,78 @@ +use std::fmt::{Display, Formatter}; + +#[macro_export] +#[doc(hidden)] +macro_rules! source_loc { + () => {{ + const SOURCE_LOC: crate::metadata::SourceLoc = $crate::metadata::SourceLoc::new( + ::core::module_path!(), + ::core::file!(), + ::core::line!(), + ::core::column!(), + ); + &SOURCE_LOC + }}; +} + +/// 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)] + 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] + pub const fn module_path(&self) -> &'static str { + self.module_path + } + + /// The name of the source code file. + #[inline] + pub const fn file(&self) -> &'static str { + self.file + } + + /// The line within the source code file. + #[inline] + pub const fn line(&self) -> u32 { + self.line + } + + /// The column within the line of source code. + #[inline] + pub const fn column(&self) -> u32 { + self.column + } +} + +impl Display for SourceLoc { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{module} [{file}:{line}:{column}]", + 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..4a3725e --- /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::{ + map, not, to_be_greater_than, to_be_greater_than_or_equal_to, to_be_less_than, + to_be_less_than_or_equal_to, to_equal, + }, + iterators::{all, any, count, nth}, + }, + expect, +}; + +#[cfg(feature = "futures")] +pub use crate::assertions::futures::when_ready; diff --git a/src/specialization.rs b/src/specialization.rs index 1bbcc5e..d26857a 100644 --- a/src/specialization.rs +++ b/src/specialization.rs @@ -1 +1,5 @@ -pub mod at_path; +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/annotated_autoderef.rs b/src/specialization/annotated_autoderef.rs new file mode 100644 index 0000000..bc3c0eb --- /dev/null +++ b/src/specialization/annotated_autoderef.rs @@ -0,0 +1,64 @@ +// BREAKS IN SOME CALLBACKS WHERE TYPE INFERENCE IS NEEDED: +// expect!(ready(1), when_ready, to_equal(1)); + +use std::fmt::{Debug, Display}; + +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 {} + +pub struct __AnnotatedDisplayTag; + +impl __AnnotatedDisplayTag { + pub fn annotate(self, value: T, stringified: &'static str) -> Annotated + where + T: Display, + { + Annotated::from_display(value, stringified) + } +} + +pub trait __AnnotatedDisplayKind { + #[inline] + fn __annotated_kind(&self) -> __AnnotatedDisplayTag { + __AnnotatedDisplayTag + } +} + +impl __AnnotatedDisplayKind for &&&__SpecializeWrapper where T: Display {} 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); From 575c215b4a04972e014f7ad1608388abcbcfce8b Mon Sep 17 00:00:00 2001 From: TehPers Date: Thu, 8 Aug 2024 07:40:47 -0700 Subject: [PATCH 02/37] Move to trait-based assertions --- Cargo.lock | 155 +++++++ Cargo.toml | 14 +- .../assertions.rs | 0 .../combinators.rs | 0 .../combinators/all.rs | 0 .../combinators/any.rs | 0 .../combinators/at_path.rs | 0 .../combinators/count.rs | 0 .../combinators/err.rs | 0 .../combinators/map.rs | 0 .../combinators/not.rs | 0 .../combinators/nth.rs | 0 .../combinators/ok.rs | 0 .../combinators/some.rs | 0 .../combinators/when_called.rs | 0 .../combinators2.rs | 0 .../combinators2/aggregate.rs | 0 .../combinators2/combinator.rs | 0 .../combinators2/count.rs | 0 .../combinators2/identity.rs | 0 .../combinators2/plan.rs | 0 .../combinators2/when_ready.rs | 0 .../error.rs | 0 .../expect.rs | 0 .../extensions.rs | 0 .../extensions/iterators.rs | 0 src/{experimenting => .experimenting}/lib.rs | 0 src/{experimenting => .experimenting}/root.rs | 0 .../specialization.rs | 0 .../specialization/at_path.rs | 0 .../v10/combinators.rs | 0 .../v10/combinators/all.rs | 0 .../v10/combinators/any.rs | 0 .../v10/combinators/combinator.rs | 0 .../v10/combinators/combinator_ext.rs | 0 .../v10/combinators/not.rs | 0 .../v11/assertions.rs | 0 .../v11/assertions/assertion.rs | 0 .../v11/combinators.rs | 0 .../v11/combinators/all.rs | 0 .../v11/combinators/combinator.rs | 0 .../v11/combinators/combinator_ext.rs | 0 .../v11/combinators/macros.rs | 0 .../v11/combinators/not.rs | 0 .../v11/expect.rs | 0 .../v11/failure.rs | 0 .../v11/specialization.rs | 0 .../v11/specialization/at_path.rs | 0 .../v11/specialization/root.rs | 0 .../v11/specialization/wrapper.rs | 0 src/{experimenting => .experimenting}/v3.rs | 0 src/{experimenting => .experimenting}/v4.rs | 0 src/{experimenting => .experimenting}/v5.rs | 0 src/{experimenting => .experimenting}/v6.rs | 0 .../v6/all.rs | 0 .../v6/assertion.rs | 0 .../v6/combinator.rs | 0 .../v6/error.rs | 0 .../v6/not.rs | 0 src/{experimenting => .experimenting}/v7.rs | 0 .../v7/all.rs | 0 .../v7/assertion.rs | 0 .../v7/combinator.rs | 0 .../v7/error.rs | 0 .../v7/not.rs | 0 src/{experimenting => .experimenting}/v8.rs | 0 .../v9/assertions.rs | 0 .../v9/assertions/assertion.rs | 0 .../v9/assertions/simple_assertion.rs | 0 .../v9/combinators.rs | 0 .../v9/combinators/all.rs | 0 .../v9/combinators/any.rs | 0 .../v9/combinators/combinator.rs | 0 .../v9/combinators/combinator_ext.rs | 0 .../v9/combinators/not.rs | 0 .../v9/combinators/when_ready.old.rs | 0 .../v9/combinators/when_ready.rs | 0 .../v9/expect.rs | 0 .../v9/failure.rs | 0 .../v9/lib.rs | 0 .../v9/output.rs | 0 .../v9/prelude.rs | 0 .../v9/third_party.rs | 0 .../v9/third_party/either.rs | 0 .../v9/thoughts.md | 0 src/assertions.rs | 9 +- src/assertions/annotated.rs | 23 - src/assertions/assertion.rs | 58 ++- src/assertions/context.rs | 134 ++++-- src/assertions/error.rs | 161 +++++-- src/assertions/futures.rs | 10 +- .../futures/inverted_output_future.rs | 104 ----- .../futures/merged_output_future.rs | 144 ------ src/assertions/futures/modifiers.rs | 42 +- .../futures/modifiers/when_ready.rs | 89 ++++ src/assertions/futures/outputs.rs | 9 + src/assertions/futures/outputs/inverted.rs | 49 ++ src/assertions/futures/outputs/merged.rs | 62 +++ .../unwrapped.rs} | 20 +- src/assertions/futures/outputs/when_ready.rs | 52 +++ src/assertions/futures/when_ready_future.rs | 112 ----- src/assertions/general.rs | 10 +- src/assertions/general/assertions.rs | 9 + src/assertions/general/assertions/to_cmp.rs | 141 ++++++ src/assertions/general/assertions/to_equal.rs | 42 ++ .../general/assertions/to_satisfy.rs | 59 +++ .../general/assertions/to_satisfy_merge.rs | 171 +++++++ src/assertions/general/comparisons.rs | 159 ------- src/assertions/general/finalizable_result.rs | 34 -- src/assertions/general/invertible_result.rs | 34 -- src/assertions/general/modifiers.rs | 76 +--- src/assertions/general/modifiers/annotate.rs | 67 +++ src/assertions/general/modifiers/map.rs | 96 ++++ src/assertions/general/modifiers/not.rs | 85 ++++ src/assertions/general/modifiers/root.rs | 29 ++ src/assertions/general/outputs.rs | 5 + src/assertions/general/outputs/invert.rs | 54 +++ src/assertions/general/outputs/unwrap.rs | 54 +++ src/assertions/iterators.rs | 7 +- src/assertions/iterators/mergeable_result.rs | 59 --- src/assertions/iterators/modifiers.rs | 149 +------ src/assertions/iterators/modifiers/count.rs | 63 +++ src/assertions/iterators/modifiers/merge.rs | 124 ++++++ src/assertions/iterators/modifiers/nth.rs | 97 ++++ src/assertions/iterators/outputs.rs | 3 + src/assertions/iterators/outputs/merge.rs | 87 ++++ src/assertions/options.rs | 9 + src/assertions/options/assertions.rs | 3 + .../options/assertions/to_be_variant.rs | 105 +++++ src/assertions/options/modifiers.rs | 3 + src/assertions/options/modifiers/some_and.rs | 98 ++++ src/assertions/options/optionish.rs | 44 ++ src/assertions/results.rs | 9 + src/assertions/results/assertions.rs | 3 + .../results/assertions/to_be_variant.rs | 109 +++++ src/assertions/results/modifiers.rs | 5 + src/assertions/results/modifiers/err_and.rs | 97 ++++ src/assertions/results/modifiers/ok_and.rs | 97 ++++ src/assertions/results/resultish.rs | 72 +++ src/assertions/strings.rs | 5 + src/assertions/strings/assertions.rs | 7 + .../strings/assertions/to_contain_substr.rs | 48 ++ .../strings/assertions/to_match_regex.rs | 55 +++ src/assertions/strings/modifiers.rs | 5 + src/assertions/strings/modifiers/debug.rs | 61 +++ src/assertions/strings/modifiers/display.rs | 64 +++ src/lib.rs | 17 +- src/macros.rs | 420 +++++------------- src/metadata/annotated.rs | 5 +- src/metadata/source_loc.rs | 9 +- src/prelude.rs | 10 +- src/specialization.rs | 2 + 152 files changed, 2983 insertions(+), 1340 deletions(-) rename src/{experimenting => .experimenting}/assertions.rs (100%) rename src/{experimenting => .experimenting}/combinators.rs (100%) rename src/{experimenting => .experimenting}/combinators/all.rs (100%) rename src/{experimenting => .experimenting}/combinators/any.rs (100%) rename src/{experimenting => .experimenting}/combinators/at_path.rs (100%) rename src/{experimenting => .experimenting}/combinators/count.rs (100%) rename src/{experimenting => .experimenting}/combinators/err.rs (100%) rename src/{experimenting => .experimenting}/combinators/map.rs (100%) rename src/{experimenting => .experimenting}/combinators/not.rs (100%) rename src/{experimenting => .experimenting}/combinators/nth.rs (100%) rename src/{experimenting => .experimenting}/combinators/ok.rs (100%) rename src/{experimenting => .experimenting}/combinators/some.rs (100%) rename src/{experimenting => .experimenting}/combinators/when_called.rs (100%) rename src/{experimenting => .experimenting}/combinators2.rs (100%) rename src/{experimenting => .experimenting}/combinators2/aggregate.rs (100%) rename src/{experimenting => .experimenting}/combinators2/combinator.rs (100%) rename src/{experimenting => .experimenting}/combinators2/count.rs (100%) rename src/{experimenting => .experimenting}/combinators2/identity.rs (100%) rename src/{experimenting => .experimenting}/combinators2/plan.rs (100%) rename src/{experimenting => .experimenting}/combinators2/when_ready.rs (100%) rename src/{experimenting => .experimenting}/error.rs (100%) rename src/{experimenting => .experimenting}/expect.rs (100%) rename src/{experimenting => .experimenting}/extensions.rs (100%) rename src/{experimenting => .experimenting}/extensions/iterators.rs (100%) rename src/{experimenting => .experimenting}/lib.rs (100%) rename src/{experimenting => .experimenting}/root.rs (100%) rename src/{experimenting => .experimenting}/specialization.rs (100%) rename src/{experimenting => .experimenting}/specialization/at_path.rs (100%) rename src/{experimenting => .experimenting}/v10/combinators.rs (100%) rename src/{experimenting => .experimenting}/v10/combinators/all.rs (100%) rename src/{experimenting => .experimenting}/v10/combinators/any.rs (100%) rename src/{experimenting => .experimenting}/v10/combinators/combinator.rs (100%) rename src/{experimenting => .experimenting}/v10/combinators/combinator_ext.rs (100%) rename src/{experimenting => .experimenting}/v10/combinators/not.rs (100%) rename src/{experimenting => .experimenting}/v11/assertions.rs (100%) rename src/{experimenting => .experimenting}/v11/assertions/assertion.rs (100%) rename src/{experimenting => .experimenting}/v11/combinators.rs (100%) rename src/{experimenting => .experimenting}/v11/combinators/all.rs (100%) rename src/{experimenting => .experimenting}/v11/combinators/combinator.rs (100%) rename src/{experimenting => .experimenting}/v11/combinators/combinator_ext.rs (100%) rename src/{experimenting => .experimenting}/v11/combinators/macros.rs (100%) rename src/{experimenting => .experimenting}/v11/combinators/not.rs (100%) rename src/{experimenting => .experimenting}/v11/expect.rs (100%) rename src/{experimenting => .experimenting}/v11/failure.rs (100%) rename src/{experimenting => .experimenting}/v11/specialization.rs (100%) rename src/{experimenting => .experimenting}/v11/specialization/at_path.rs (100%) rename src/{experimenting => .experimenting}/v11/specialization/root.rs (100%) rename src/{experimenting => .experimenting}/v11/specialization/wrapper.rs (100%) rename src/{experimenting => .experimenting}/v3.rs (100%) rename src/{experimenting => .experimenting}/v4.rs (100%) rename src/{experimenting => .experimenting}/v5.rs (100%) rename src/{experimenting => .experimenting}/v6.rs (100%) rename src/{experimenting => .experimenting}/v6/all.rs (100%) rename src/{experimenting => .experimenting}/v6/assertion.rs (100%) rename src/{experimenting => .experimenting}/v6/combinator.rs (100%) rename src/{experimenting => .experimenting}/v6/error.rs (100%) rename src/{experimenting => .experimenting}/v6/not.rs (100%) rename src/{experimenting => .experimenting}/v7.rs (100%) rename src/{experimenting => .experimenting}/v7/all.rs (100%) rename src/{experimenting => .experimenting}/v7/assertion.rs (100%) rename src/{experimenting => .experimenting}/v7/combinator.rs (100%) rename src/{experimenting => .experimenting}/v7/error.rs (100%) rename src/{experimenting => .experimenting}/v7/not.rs (100%) rename src/{experimenting => .experimenting}/v8.rs (100%) rename src/{experimenting => .experimenting}/v9/assertions.rs (100%) rename src/{experimenting => .experimenting}/v9/assertions/assertion.rs (100%) rename src/{experimenting => .experimenting}/v9/assertions/simple_assertion.rs (100%) rename src/{experimenting => .experimenting}/v9/combinators.rs (100%) rename src/{experimenting => .experimenting}/v9/combinators/all.rs (100%) rename src/{experimenting => .experimenting}/v9/combinators/any.rs (100%) rename src/{experimenting => .experimenting}/v9/combinators/combinator.rs (100%) rename src/{experimenting => .experimenting}/v9/combinators/combinator_ext.rs (100%) rename src/{experimenting => .experimenting}/v9/combinators/not.rs (100%) rename src/{experimenting => .experimenting}/v9/combinators/when_ready.old.rs (100%) rename src/{experimenting => .experimenting}/v9/combinators/when_ready.rs (100%) rename src/{experimenting => .experimenting}/v9/expect.rs (100%) rename src/{experimenting => .experimenting}/v9/failure.rs (100%) rename src/{experimenting => .experimenting}/v9/lib.rs (100%) rename src/{experimenting => .experimenting}/v9/output.rs (100%) rename src/{experimenting => .experimenting}/v9/prelude.rs (100%) rename src/{experimenting => .experimenting}/v9/third_party.rs (100%) rename src/{experimenting => .experimenting}/v9/third_party/either.rs (100%) rename src/{experimenting => .experimenting}/v9/thoughts.md (100%) delete mode 100644 src/assertions/annotated.rs delete mode 100644 src/assertions/futures/inverted_output_future.rs delete mode 100644 src/assertions/futures/merged_output_future.rs create mode 100644 src/assertions/futures/modifiers/when_ready.rs create mode 100644 src/assertions/futures/outputs.rs create mode 100644 src/assertions/futures/outputs/inverted.rs create mode 100644 src/assertions/futures/outputs/merged.rs rename src/assertions/futures/{finalized_output_future.rs => outputs/unwrapped.rs} (56%) create mode 100644 src/assertions/futures/outputs/when_ready.rs delete mode 100644 src/assertions/futures/when_ready_future.rs create mode 100644 src/assertions/general/assertions.rs create mode 100644 src/assertions/general/assertions/to_cmp.rs create mode 100644 src/assertions/general/assertions/to_equal.rs create mode 100644 src/assertions/general/assertions/to_satisfy.rs create mode 100644 src/assertions/general/assertions/to_satisfy_merge.rs delete mode 100644 src/assertions/general/comparisons.rs delete mode 100644 src/assertions/general/finalizable_result.rs delete mode 100644 src/assertions/general/invertible_result.rs create mode 100644 src/assertions/general/modifiers/annotate.rs create mode 100644 src/assertions/general/modifiers/map.rs create mode 100644 src/assertions/general/modifiers/not.rs create mode 100644 src/assertions/general/modifiers/root.rs create mode 100644 src/assertions/general/outputs.rs create mode 100644 src/assertions/general/outputs/invert.rs create mode 100644 src/assertions/general/outputs/unwrap.rs delete mode 100644 src/assertions/iterators/mergeable_result.rs create mode 100644 src/assertions/iterators/modifiers/count.rs create mode 100644 src/assertions/iterators/modifiers/merge.rs create mode 100644 src/assertions/iterators/modifiers/nth.rs create mode 100644 src/assertions/iterators/outputs.rs create mode 100644 src/assertions/iterators/outputs/merge.rs create mode 100644 src/assertions/options/assertions.rs create mode 100644 src/assertions/options/assertions/to_be_variant.rs create mode 100644 src/assertions/options/modifiers.rs create mode 100644 src/assertions/options/modifiers/some_and.rs create mode 100644 src/assertions/options/optionish.rs create mode 100644 src/assertions/results/assertions.rs create mode 100644 src/assertions/results/assertions/to_be_variant.rs create mode 100644 src/assertions/results/modifiers.rs create mode 100644 src/assertions/results/modifiers/err_and.rs create mode 100644 src/assertions/results/modifiers/ok_and.rs create mode 100644 src/assertions/results/resultish.rs create mode 100644 src/assertions/strings.rs create mode 100644 src/assertions/strings/assertions.rs create mode 100644 src/assertions/strings/assertions/to_contain_substr.rs create mode 100644 src/assertions/strings/assertions/to_match_regex.rs create mode 100644 src/assertions/strings/modifiers.rs create mode 100644 src/assertions/strings/modifiers/debug.rs create mode 100644 src/assertions/strings/modifiers/display.rs diff --git a/Cargo.lock b/Cargo.lock index 48ed258..5e3dc4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,15 @@ 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" @@ -55,7 +64,9 @@ name = "expecters" version = "0.1.0" dependencies = [ "futures", + "owo-colors", "pin-project-lite", + "regex", "test-case", "tokio", ] @@ -155,6 +166,29 @@ 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" @@ -185,6 +219,15 @@ 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" @@ -215,6 +258,35 @@ 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" @@ -230,6 +302,16 @@ 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" @@ -301,3 +383,76 @@ 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 d74d75c..261c64a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,18 +9,24 @@ categories = [ "development-tools::debugging", "development-tools::testing", ] -keywords = ["assert", "assertions", "matchers", "testing"] +keywords = ["assert", "assertions", "async", "matchers", "testing"] [features] -default = ["futures"] -futures = ["dep:futures"] +default = ["colors", "futures", "regex"] +futures = ["dep:futures", "dep:pin-project-lite"] +colors = ["dep:owo-colors"] +regex = ["dep:regex"] [dependencies] futures = { version = "0.3.30", optional = true, default-features = false, features = [ "std", "async-await", ] } -pin-project-lite = "0.2.14" +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] futures = { version = "0.3.30", default-features = false, features = [ diff --git a/src/experimenting/assertions.rs b/src/.experimenting/assertions.rs similarity index 100% rename from src/experimenting/assertions.rs rename to src/.experimenting/assertions.rs diff --git a/src/experimenting/combinators.rs b/src/.experimenting/combinators.rs similarity index 100% rename from src/experimenting/combinators.rs rename to src/.experimenting/combinators.rs diff --git a/src/experimenting/combinators/all.rs b/src/.experimenting/combinators/all.rs similarity index 100% rename from src/experimenting/combinators/all.rs rename to src/.experimenting/combinators/all.rs diff --git a/src/experimenting/combinators/any.rs b/src/.experimenting/combinators/any.rs similarity index 100% rename from src/experimenting/combinators/any.rs rename to src/.experimenting/combinators/any.rs diff --git a/src/experimenting/combinators/at_path.rs b/src/.experimenting/combinators/at_path.rs similarity index 100% rename from src/experimenting/combinators/at_path.rs rename to src/.experimenting/combinators/at_path.rs diff --git a/src/experimenting/combinators/count.rs b/src/.experimenting/combinators/count.rs similarity index 100% rename from src/experimenting/combinators/count.rs rename to src/.experimenting/combinators/count.rs diff --git a/src/experimenting/combinators/err.rs b/src/.experimenting/combinators/err.rs similarity index 100% rename from src/experimenting/combinators/err.rs rename to src/.experimenting/combinators/err.rs diff --git a/src/experimenting/combinators/map.rs b/src/.experimenting/combinators/map.rs similarity index 100% rename from src/experimenting/combinators/map.rs rename to src/.experimenting/combinators/map.rs diff --git a/src/experimenting/combinators/not.rs b/src/.experimenting/combinators/not.rs similarity index 100% rename from src/experimenting/combinators/not.rs rename to src/.experimenting/combinators/not.rs diff --git a/src/experimenting/combinators/nth.rs b/src/.experimenting/combinators/nth.rs similarity index 100% rename from src/experimenting/combinators/nth.rs rename to src/.experimenting/combinators/nth.rs diff --git a/src/experimenting/combinators/ok.rs b/src/.experimenting/combinators/ok.rs similarity index 100% rename from src/experimenting/combinators/ok.rs rename to src/.experimenting/combinators/ok.rs diff --git a/src/experimenting/combinators/some.rs b/src/.experimenting/combinators/some.rs similarity index 100% rename from src/experimenting/combinators/some.rs rename to src/.experimenting/combinators/some.rs diff --git a/src/experimenting/combinators/when_called.rs b/src/.experimenting/combinators/when_called.rs similarity index 100% rename from src/experimenting/combinators/when_called.rs rename to src/.experimenting/combinators/when_called.rs diff --git a/src/experimenting/combinators2.rs b/src/.experimenting/combinators2.rs similarity index 100% rename from src/experimenting/combinators2.rs rename to src/.experimenting/combinators2.rs diff --git a/src/experimenting/combinators2/aggregate.rs b/src/.experimenting/combinators2/aggregate.rs similarity index 100% rename from src/experimenting/combinators2/aggregate.rs rename to src/.experimenting/combinators2/aggregate.rs diff --git a/src/experimenting/combinators2/combinator.rs b/src/.experimenting/combinators2/combinator.rs similarity index 100% rename from src/experimenting/combinators2/combinator.rs rename to src/.experimenting/combinators2/combinator.rs diff --git a/src/experimenting/combinators2/count.rs b/src/.experimenting/combinators2/count.rs similarity index 100% rename from src/experimenting/combinators2/count.rs rename to src/.experimenting/combinators2/count.rs diff --git a/src/experimenting/combinators2/identity.rs b/src/.experimenting/combinators2/identity.rs similarity index 100% rename from src/experimenting/combinators2/identity.rs rename to src/.experimenting/combinators2/identity.rs diff --git a/src/experimenting/combinators2/plan.rs b/src/.experimenting/combinators2/plan.rs similarity index 100% rename from src/experimenting/combinators2/plan.rs rename to src/.experimenting/combinators2/plan.rs diff --git a/src/experimenting/combinators2/when_ready.rs b/src/.experimenting/combinators2/when_ready.rs similarity index 100% rename from src/experimenting/combinators2/when_ready.rs rename to src/.experimenting/combinators2/when_ready.rs diff --git a/src/experimenting/error.rs b/src/.experimenting/error.rs similarity index 100% rename from src/experimenting/error.rs rename to src/.experimenting/error.rs diff --git a/src/experimenting/expect.rs b/src/.experimenting/expect.rs similarity index 100% rename from src/experimenting/expect.rs rename to src/.experimenting/expect.rs diff --git a/src/experimenting/extensions.rs b/src/.experimenting/extensions.rs similarity index 100% rename from src/experimenting/extensions.rs rename to src/.experimenting/extensions.rs diff --git a/src/experimenting/extensions/iterators.rs b/src/.experimenting/extensions/iterators.rs similarity index 100% rename from src/experimenting/extensions/iterators.rs rename to src/.experimenting/extensions/iterators.rs diff --git a/src/experimenting/lib.rs b/src/.experimenting/lib.rs similarity index 100% rename from src/experimenting/lib.rs rename to src/.experimenting/lib.rs diff --git a/src/experimenting/root.rs b/src/.experimenting/root.rs similarity index 100% rename from src/experimenting/root.rs rename to src/.experimenting/root.rs diff --git a/src/experimenting/specialization.rs b/src/.experimenting/specialization.rs similarity index 100% rename from src/experimenting/specialization.rs rename to src/.experimenting/specialization.rs diff --git a/src/experimenting/specialization/at_path.rs b/src/.experimenting/specialization/at_path.rs similarity index 100% rename from src/experimenting/specialization/at_path.rs rename to src/.experimenting/specialization/at_path.rs diff --git a/src/experimenting/v10/combinators.rs b/src/.experimenting/v10/combinators.rs similarity index 100% rename from src/experimenting/v10/combinators.rs rename to src/.experimenting/v10/combinators.rs diff --git a/src/experimenting/v10/combinators/all.rs b/src/.experimenting/v10/combinators/all.rs similarity index 100% rename from src/experimenting/v10/combinators/all.rs rename to src/.experimenting/v10/combinators/all.rs diff --git a/src/experimenting/v10/combinators/any.rs b/src/.experimenting/v10/combinators/any.rs similarity index 100% rename from src/experimenting/v10/combinators/any.rs rename to src/.experimenting/v10/combinators/any.rs diff --git a/src/experimenting/v10/combinators/combinator.rs b/src/.experimenting/v10/combinators/combinator.rs similarity index 100% rename from src/experimenting/v10/combinators/combinator.rs rename to src/.experimenting/v10/combinators/combinator.rs diff --git a/src/experimenting/v10/combinators/combinator_ext.rs b/src/.experimenting/v10/combinators/combinator_ext.rs similarity index 100% rename from src/experimenting/v10/combinators/combinator_ext.rs rename to src/.experimenting/v10/combinators/combinator_ext.rs diff --git a/src/experimenting/v10/combinators/not.rs b/src/.experimenting/v10/combinators/not.rs similarity index 100% rename from src/experimenting/v10/combinators/not.rs rename to src/.experimenting/v10/combinators/not.rs diff --git a/src/experimenting/v11/assertions.rs b/src/.experimenting/v11/assertions.rs similarity index 100% rename from src/experimenting/v11/assertions.rs rename to src/.experimenting/v11/assertions.rs diff --git a/src/experimenting/v11/assertions/assertion.rs b/src/.experimenting/v11/assertions/assertion.rs similarity index 100% rename from src/experimenting/v11/assertions/assertion.rs rename to src/.experimenting/v11/assertions/assertion.rs diff --git a/src/experimenting/v11/combinators.rs b/src/.experimenting/v11/combinators.rs similarity index 100% rename from src/experimenting/v11/combinators.rs rename to src/.experimenting/v11/combinators.rs diff --git a/src/experimenting/v11/combinators/all.rs b/src/.experimenting/v11/combinators/all.rs similarity index 100% rename from src/experimenting/v11/combinators/all.rs rename to src/.experimenting/v11/combinators/all.rs diff --git a/src/experimenting/v11/combinators/combinator.rs b/src/.experimenting/v11/combinators/combinator.rs similarity index 100% rename from src/experimenting/v11/combinators/combinator.rs rename to src/.experimenting/v11/combinators/combinator.rs diff --git a/src/experimenting/v11/combinators/combinator_ext.rs b/src/.experimenting/v11/combinators/combinator_ext.rs similarity index 100% rename from src/experimenting/v11/combinators/combinator_ext.rs rename to src/.experimenting/v11/combinators/combinator_ext.rs diff --git a/src/experimenting/v11/combinators/macros.rs b/src/.experimenting/v11/combinators/macros.rs similarity index 100% rename from src/experimenting/v11/combinators/macros.rs rename to src/.experimenting/v11/combinators/macros.rs diff --git a/src/experimenting/v11/combinators/not.rs b/src/.experimenting/v11/combinators/not.rs similarity index 100% rename from src/experimenting/v11/combinators/not.rs rename to src/.experimenting/v11/combinators/not.rs diff --git a/src/experimenting/v11/expect.rs b/src/.experimenting/v11/expect.rs similarity index 100% rename from src/experimenting/v11/expect.rs rename to src/.experimenting/v11/expect.rs diff --git a/src/experimenting/v11/failure.rs b/src/.experimenting/v11/failure.rs similarity index 100% rename from src/experimenting/v11/failure.rs rename to src/.experimenting/v11/failure.rs diff --git a/src/experimenting/v11/specialization.rs b/src/.experimenting/v11/specialization.rs similarity index 100% rename from src/experimenting/v11/specialization.rs rename to src/.experimenting/v11/specialization.rs diff --git a/src/experimenting/v11/specialization/at_path.rs b/src/.experimenting/v11/specialization/at_path.rs similarity index 100% rename from src/experimenting/v11/specialization/at_path.rs rename to src/.experimenting/v11/specialization/at_path.rs diff --git a/src/experimenting/v11/specialization/root.rs b/src/.experimenting/v11/specialization/root.rs similarity index 100% rename from src/experimenting/v11/specialization/root.rs rename to src/.experimenting/v11/specialization/root.rs diff --git a/src/experimenting/v11/specialization/wrapper.rs b/src/.experimenting/v11/specialization/wrapper.rs similarity index 100% rename from src/experimenting/v11/specialization/wrapper.rs rename to src/.experimenting/v11/specialization/wrapper.rs diff --git a/src/experimenting/v3.rs b/src/.experimenting/v3.rs similarity index 100% rename from src/experimenting/v3.rs rename to src/.experimenting/v3.rs diff --git a/src/experimenting/v4.rs b/src/.experimenting/v4.rs similarity index 100% rename from src/experimenting/v4.rs rename to src/.experimenting/v4.rs diff --git a/src/experimenting/v5.rs b/src/.experimenting/v5.rs similarity index 100% rename from src/experimenting/v5.rs rename to src/.experimenting/v5.rs diff --git a/src/experimenting/v6.rs b/src/.experimenting/v6.rs similarity index 100% rename from src/experimenting/v6.rs rename to src/.experimenting/v6.rs diff --git a/src/experimenting/v6/all.rs b/src/.experimenting/v6/all.rs similarity index 100% rename from src/experimenting/v6/all.rs rename to src/.experimenting/v6/all.rs diff --git a/src/experimenting/v6/assertion.rs b/src/.experimenting/v6/assertion.rs similarity index 100% rename from src/experimenting/v6/assertion.rs rename to src/.experimenting/v6/assertion.rs diff --git a/src/experimenting/v6/combinator.rs b/src/.experimenting/v6/combinator.rs similarity index 100% rename from src/experimenting/v6/combinator.rs rename to src/.experimenting/v6/combinator.rs diff --git a/src/experimenting/v6/error.rs b/src/.experimenting/v6/error.rs similarity index 100% rename from src/experimenting/v6/error.rs rename to src/.experimenting/v6/error.rs diff --git a/src/experimenting/v6/not.rs b/src/.experimenting/v6/not.rs similarity index 100% rename from src/experimenting/v6/not.rs rename to src/.experimenting/v6/not.rs diff --git a/src/experimenting/v7.rs b/src/.experimenting/v7.rs similarity index 100% rename from src/experimenting/v7.rs rename to src/.experimenting/v7.rs diff --git a/src/experimenting/v7/all.rs b/src/.experimenting/v7/all.rs similarity index 100% rename from src/experimenting/v7/all.rs rename to src/.experimenting/v7/all.rs diff --git a/src/experimenting/v7/assertion.rs b/src/.experimenting/v7/assertion.rs similarity index 100% rename from src/experimenting/v7/assertion.rs rename to src/.experimenting/v7/assertion.rs diff --git a/src/experimenting/v7/combinator.rs b/src/.experimenting/v7/combinator.rs similarity index 100% rename from src/experimenting/v7/combinator.rs rename to src/.experimenting/v7/combinator.rs diff --git a/src/experimenting/v7/error.rs b/src/.experimenting/v7/error.rs similarity index 100% rename from src/experimenting/v7/error.rs rename to src/.experimenting/v7/error.rs diff --git a/src/experimenting/v7/not.rs b/src/.experimenting/v7/not.rs similarity index 100% rename from src/experimenting/v7/not.rs rename to src/.experimenting/v7/not.rs diff --git a/src/experimenting/v8.rs b/src/.experimenting/v8.rs similarity index 100% rename from src/experimenting/v8.rs rename to src/.experimenting/v8.rs diff --git a/src/experimenting/v9/assertions.rs b/src/.experimenting/v9/assertions.rs similarity index 100% rename from src/experimenting/v9/assertions.rs rename to src/.experimenting/v9/assertions.rs diff --git a/src/experimenting/v9/assertions/assertion.rs b/src/.experimenting/v9/assertions/assertion.rs similarity index 100% rename from src/experimenting/v9/assertions/assertion.rs rename to src/.experimenting/v9/assertions/assertion.rs diff --git a/src/experimenting/v9/assertions/simple_assertion.rs b/src/.experimenting/v9/assertions/simple_assertion.rs similarity index 100% rename from src/experimenting/v9/assertions/simple_assertion.rs rename to src/.experimenting/v9/assertions/simple_assertion.rs diff --git a/src/experimenting/v9/combinators.rs b/src/.experimenting/v9/combinators.rs similarity index 100% rename from src/experimenting/v9/combinators.rs rename to src/.experimenting/v9/combinators.rs diff --git a/src/experimenting/v9/combinators/all.rs b/src/.experimenting/v9/combinators/all.rs similarity index 100% rename from src/experimenting/v9/combinators/all.rs rename to src/.experimenting/v9/combinators/all.rs diff --git a/src/experimenting/v9/combinators/any.rs b/src/.experimenting/v9/combinators/any.rs similarity index 100% rename from src/experimenting/v9/combinators/any.rs rename to src/.experimenting/v9/combinators/any.rs diff --git a/src/experimenting/v9/combinators/combinator.rs b/src/.experimenting/v9/combinators/combinator.rs similarity index 100% rename from src/experimenting/v9/combinators/combinator.rs rename to src/.experimenting/v9/combinators/combinator.rs diff --git a/src/experimenting/v9/combinators/combinator_ext.rs b/src/.experimenting/v9/combinators/combinator_ext.rs similarity index 100% rename from src/experimenting/v9/combinators/combinator_ext.rs rename to src/.experimenting/v9/combinators/combinator_ext.rs diff --git a/src/experimenting/v9/combinators/not.rs b/src/.experimenting/v9/combinators/not.rs similarity index 100% rename from src/experimenting/v9/combinators/not.rs rename to src/.experimenting/v9/combinators/not.rs diff --git a/src/experimenting/v9/combinators/when_ready.old.rs b/src/.experimenting/v9/combinators/when_ready.old.rs similarity index 100% rename from src/experimenting/v9/combinators/when_ready.old.rs rename to src/.experimenting/v9/combinators/when_ready.old.rs diff --git a/src/experimenting/v9/combinators/when_ready.rs b/src/.experimenting/v9/combinators/when_ready.rs similarity index 100% rename from src/experimenting/v9/combinators/when_ready.rs rename to src/.experimenting/v9/combinators/when_ready.rs diff --git a/src/experimenting/v9/expect.rs b/src/.experimenting/v9/expect.rs similarity index 100% rename from src/experimenting/v9/expect.rs rename to src/.experimenting/v9/expect.rs diff --git a/src/experimenting/v9/failure.rs b/src/.experimenting/v9/failure.rs similarity index 100% rename from src/experimenting/v9/failure.rs rename to src/.experimenting/v9/failure.rs diff --git a/src/experimenting/v9/lib.rs b/src/.experimenting/v9/lib.rs similarity index 100% rename from src/experimenting/v9/lib.rs rename to src/.experimenting/v9/lib.rs diff --git a/src/experimenting/v9/output.rs b/src/.experimenting/v9/output.rs similarity index 100% rename from src/experimenting/v9/output.rs rename to src/.experimenting/v9/output.rs diff --git a/src/experimenting/v9/prelude.rs b/src/.experimenting/v9/prelude.rs similarity index 100% rename from src/experimenting/v9/prelude.rs rename to src/.experimenting/v9/prelude.rs diff --git a/src/experimenting/v9/third_party.rs b/src/.experimenting/v9/third_party.rs similarity index 100% rename from src/experimenting/v9/third_party.rs rename to src/.experimenting/v9/third_party.rs diff --git a/src/experimenting/v9/third_party/either.rs b/src/.experimenting/v9/third_party/either.rs similarity index 100% rename from src/experimenting/v9/third_party/either.rs rename to src/.experimenting/v9/third_party/either.rs diff --git a/src/experimenting/v9/thoughts.md b/src/.experimenting/v9/thoughts.md similarity index 100% rename from src/experimenting/v9/thoughts.md rename to src/.experimenting/v9/thoughts.md diff --git a/src/assertions.rs b/src/assertions.rs index 8253889..6b29c79 100644 --- a/src/assertions.rs +++ b/src/assertions.rs @@ -6,17 +6,16 @@ // pub mod functions; #[cfg(feature = "futures")] pub mod futures; -pub mod iterators; -// pub mod options; -// pub mod results; pub mod general; +pub mod iterators; +pub mod options; +pub mod results; +pub mod strings; -mod annotated; mod assertion; mod context; mod error; -pub use annotated::*; pub use assertion::*; pub use context::*; pub use error::*; diff --git a/src/assertions/annotated.rs b/src/assertions/annotated.rs deleted file mode 100644 index ae7158d..0000000 --- a/src/assertions/annotated.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::metadata::{Annotated, AnnotatedKind}; - -use super::AssertionContext; - -#[doc(hidden)] -pub fn __annotate_assertion( - cx: AssertionContext, - subject: Annotated, - next: fn(AssertionContext, T) -> O, -) -> O { - // Build next context - let mut next_cx = cx.next(); - next_cx.annotate( - "received", - match subject.kind() { - AnnotatedKind::Debug => subject.as_str(), - AnnotatedKind::Stringify => "? (no debug representation)", - }, - ); - - // Call inner assertion - next(next_cx, subject.into_inner()) -} diff --git a/src/assertions/assertion.rs b/src/assertions/assertion.rs index cff0175..7173285 100644 --- a/src/assertions/assertion.rs +++ b/src/assertions/assertion.rs @@ -1,6 +1,18 @@ +use std::{fmt::Debug, marker::PhantomData}; + use super::AssertionContext; -/// TODO +/// 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; @@ -11,7 +23,9 @@ pub trait Assertion { /// Modifies an assertion. /// -/// TODO +/// 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; @@ -21,5 +35,43 @@ pub trait AssertionModifier { /// This is generally a recursive function. /// /// TODO - fn apply(self, cx: AssertionContext, assertion: A) -> Self::Output; + fn apply(self, next: A) -> Self::Output; +} + +/// Indicates the type of subject being passed to the next modifier/assertion in +/// the chain. +/// +/// This is necessary to help the compiler infer the type of the value being +/// passed from a modifier to the next modifier/assertion. +/// +/// To create an instance of it, use: +/// +/// ``` +/// use expecters::assertions::key; +/// let key = key::(); +/// # let _ = key; +/// ``` +pub struct SubjectKey(PhantomData); + +impl Clone for SubjectKey { + #[inline] + fn clone(&self) -> Self { + *self + } +} + +impl Copy for SubjectKey {} + +impl Debug for SubjectKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("SubjectKey").field(&self.0).finish() + } +} + +/// Creates a new key for a subject. Modifiers must return a key on creation to +/// indicate the type of subject the next modifier/assertion is expected to +/// receive. +#[must_use] +pub const fn key() -> SubjectKey { + SubjectKey(PhantomData) } diff --git a/src/assertions/context.rs b/src/assertions/context.rs index 6e7e8b6..24bebd4 100644 --- a/src/assertions/context.rs +++ b/src/assertions/context.rs @@ -1,6 +1,6 @@ use crate::metadata::{Annotated, AnnotatedKind, SourceLoc}; -use super::AssertionError; +use super::AssertionResult; /// Context that is passed through an assertion to track the full execution flow /// that occurred. @@ -9,24 +9,64 @@ use super::AssertionError; /// 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`](crate::prelude::all) or +/// [`any`](crate::prelude::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. #[derive(Clone, Debug)] pub struct AssertionContext { - source_loc: &'static SourceLoc, - visited: Vec, - remaining: &'static [&'static str], + pub(crate) subject: String, + pub(crate) source_loc: &'static SourceLoc, + pub(crate) visited: Vec, + pub(crate) remaining: &'static [&'static str], + pub(crate) recovered: Vec, } impl AssertionContext { #[doc(hidden)] - pub fn __new(source_loc: &'static SourceLoc, frames: &'static [&'static str]) -> Self { + #[must_use] + pub fn __new( + subject: String, + source_loc: &'static SourceLoc, + frames: &'static [&'static str], + ) -> Self { Self { + subject, source_loc, - remaining: frames, visited: vec![], + remaining: frames, + recovered: vec![], } } - /// Adds an annotation to this frame. + /// 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 @@ -38,33 +78,47 @@ impl AssertionContext { } /// Adds an annotation to this frame if the provided annotated value has a - /// [`Debug`](core::fmt::Debug) representation. + /// [`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. /// - /// Note that function parameters to modifier functions and assertion - /// functions (the functions the user actually calls) *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`]: core::fmt::Debug #[inline] pub fn try_annotate(&mut self, key: &'static str, value: &Annotated) { - match value.kind() { - AnnotatedKind::Debug => self.annotate(key, value.as_str()), - _ => {} + if value.kind() == AnnotatedKind::Debug { + self.annotate(key, value.as_str()); } } /// Creates a new success value. #[inline] - pub fn pass(&self) { - // TODO: track success path? somehow recover frames when inverting a - // success into a fail? + pub fn pass(self) -> AssertionResult { + AssertionResult::new(self, None) } - // TODO: recover missing frames from an error that was recovered from - // pub fn recover(&mut self, error: AssertionError) {} - // probably need another field for the recovered frames to allow context to - // continue to be propagated. on propagation - lose the recovered frames so - // they don't get mixed in with a different execution path and confuse the - // user + /// 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) -> AssertionResult { + 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 @@ -74,17 +128,27 @@ impl AssertionContext { /// 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] - pub fn fail(&self, message: impl ToString) -> AssertionError { - AssertionError::new( - message.to_string(), - self.source_loc, - self.visited.clone(), - self.remaining, - ) + #[allow(clippy::needless_pass_by_value)] + pub fn fail(self, message: impl ToString) -> AssertionResult { + AssertionResult::new(self, Some(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, finalizing this - /// frame's annotations. + /// 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 @@ -95,6 +159,10 @@ impl AssertionContext { annotations: vec![], }); self.remaining = remaining; + + // New execution path, so recovered frames aren't relevant anymore + self.recovered.clear(); + self } } diff --git a/src/assertions/error.rs b/src/assertions/error.rs index 465ae91..8675163 100644 --- a/src/assertions/error.rs +++ b/src/assertions/error.rs @@ -3,63 +3,160 @@ use std::{ fmt::{Debug, Display, Formatter}, }; -use crate::metadata::SourceLoc; +use owo_colors::{OwoColorize, Stream, SupportsColorsDisplay}; -use super::ContextFrame; +use crate::assertions::ContextFrame; -/// A simple assertion result. +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. /// -/// 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`](AssertionResult::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, and it should be possible to eventually get a value of this type +/// 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). -pub type AssertionResult = Result<(), AssertionError>; +#[derive(Clone, Debug)] +#[must_use] +pub struct AssertionResult { + cx: AssertionContext, + error: Option, +} + +impl AssertionResult { + #[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 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 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`]. + // TODO: this should be called when finalizing? + #[inline] + pub(crate) 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. pub struct AssertionError { + cx: AssertionContext, message: String, - source_loc: &'static SourceLoc, - visited: Vec, - remaining: &'static [&'static str], } impl AssertionError { - pub(crate) fn new( - message: String, - source_loc: &'static SourceLoc, - visited: Vec, - remaining: &'static [&'static str], - ) -> Self { - Self { - message, - source_loc, - visited, - remaining, - } + pub(crate) fn new(cx: AssertionContext, message: String) -> Self { + Self { cx, message } } } +#[cfg(feature = "colors")] +fn colored<'a, T, U, F>(val: &'a T, apply: F) -> SupportsColorsDisplay<'a, T, U, F> +where + T: OwoColorize, + F: Fn(&'a T) -> U, +{ + val.if_supports_color(Stream::Stderr, apply) +} + impl Debug for AssertionError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - writeln!(f, "assertion failed: {}", self.message)?; - writeln!(f, " at: {}", self.source_loc)?; + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + // TODO: keep colors? + writeln!(f, "assertion failed:")?; + writeln!( + f, + " {}", + colored(&format_args!("at: {}", self.cx.source_loc), |text| text + .dimmed()) + )?; + writeln!( + f, + " {}", + colored(&format_args!("subject: {}", self.cx.subject), |text| text + .dimmed()) + )?; + writeln!(f)?; - // Write visited frames - writeln!(f, "steps:")?; - for frame in &self.visited { - writeln!(f, " {}:", frame.assertion_name)?; + 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, " {}: {}", key, value)?; + #[cfg(feature = "colors")] + writeln!( + f, + " {}", + colored(&format_args!("{key}: {value}"), |text| text.dimmed()) + )?; + #[cfg(not(feature = "colors"))] + writeln!(f, " {key}: {value}")?; } + writeln!(f)?; + Ok(()) + } + + // Write visited frames + writeln!(f, "steps:")?; + let mut idx = 0; + for frame in self.cx.visited.iter() { + let comment = if idx == self.cx.visited.len() - 1 { + format!( + " {}", + self.message + .if_supports_color(Stream::Stderr, |text| text.bright_red().to_string()) + ) + } 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; } - for frame in self.remaining { - writeln!(f, " {frame}: # not visited")?; + // Write non-visited frames + for frame in &self.cx.remaining[self.cx.recovered.len()..] { + writeln!(f, " {frame}: (not visited)")?; + idx += 1; } Ok(()) diff --git a/src/assertions/futures.rs b/src/assertions/futures.rs index 8992aba..9ee6a29 100644 --- a/src/assertions/futures.rs +++ b/src/assertions/futures.rs @@ -7,14 +7,8 @@ //! This module also contains types that can be useful for writing your own //! asynchronous assertions and modifiers, if needed. -mod finalized_output_future; -mod inverted_output_future; -mod merged_output_future; mod modifiers; -mod when_ready_future; +mod outputs; -pub use finalized_output_future::*; -pub use inverted_output_future::*; -pub use merged_output_future::*; pub use modifiers::*; -pub use when_ready_future::*; +pub use outputs::*; diff --git a/src/assertions/futures/inverted_output_future.rs b/src/assertions/futures/inverted_output_future.rs deleted file mode 100644 index b6a262c..0000000 --- a/src/assertions/futures/inverted_output_future.rs +++ /dev/null @@ -1,104 +0,0 @@ -use std::{ - future::Future, - pin::Pin, - task::{ready, Context, Poll}, -}; - -use pin_project_lite::pin_project; - -use crate::assertions::{ - general::{FinalizableResult, InvertibleResult}, - iterators::MergeableResult, - AssertionContext, -}; - -use super::{FinalizedOutputFuture, MergedOutputsFuture}; - -pin_project! { - /// Inverts an asynchronous output. - #[derive(Clone, Debug)] - pub struct InvertedOutputFuture { - #[pin] - inner: F, - cx: Option, - } -} - -impl InvertedOutputFuture -where - F: Future, - F::Output: InvertibleResult, -{ - /// 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, - F::Output: InvertibleResult, -{ - 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 InvertibleResult for InvertedOutputFuture { - type Inverted = F; - - #[inline] - fn invert(self, _cx: AssertionContext) -> Self::Inverted { - // Undo the inversion to preserve context - self.inner - } -} - -impl MergeableResult for InvertedOutputFuture -where - F: Future, - F::Output: InvertibleResult, - ::Inverted: MergeableResult, -{ - type Merged = MergedOutputsFuture; - - #[inline] - fn merge_all(cx: AssertionContext, results: I) -> Self::Merged - where - I: IntoIterator, - { - MergedOutputsFuture::all(cx, results) - } - - #[inline] - fn merge_any(cx: AssertionContext, results: I) -> Self::Merged - where - I: IntoIterator, - { - MergedOutputsFuture::any(cx, results) - } -} - -impl FinalizableResult for InvertedOutputFuture -where - F: Future, - F::Output: InvertibleResult, - ::Inverted: FinalizableResult, -{ - type Finalized = FinalizedOutputFuture; - - #[inline] - fn finalize(self) -> Self::Finalized { - FinalizedOutputFuture::new(self) - } -} diff --git a/src/assertions/futures/merged_output_future.rs b/src/assertions/futures/merged_output_future.rs deleted file mode 100644 index c730b53..0000000 --- a/src/assertions/futures/merged_output_future.rs +++ /dev/null @@ -1,144 +0,0 @@ -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::{ - general::{FinalizableResult, InvertibleResult}, - iterators::MergeableResult, - AssertionContext, -}; - -use super::{FinalizedOutputFuture, InvertedOutputFuture}; - -pin_project! { - /// Merges many asynchronous outputs. - #[derive(Debug)] - pub struct MergedOutputsFuture - where - F: Future, - { - #[pin] - inner: Collect, Vec>, - cx: Option, - merge_kind: MergeKind, - } -} - -impl MergedOutputsFuture -where - F: Future, - F::Output: MergeableResult, -{ - /// Creates a new merged outputs future using - /// [`merge_all`](MergeableResult::merge_all()). - #[inline] - pub fn all(cx: AssertionContext, futs: I) -> Self - where - I: IntoIterator, - { - Self { - inner: FuturesUnordered::from_iter(futs.into_iter()).collect(), - cx: Some(cx), - merge_kind: MergeKind::All, - } - } - - /// Creates a new merged outputs future using - /// [`merge_any`](MergeableResult::merge_any()). - #[inline] - pub fn any(cx: AssertionContext, futs: I) -> Self - where - I: IntoIterator, - { - Self { - inner: FuturesUnordered::from_iter(futs.into_iter()).collect(), - cx: Some(cx), - merge_kind: MergeKind::Any, - } - } -} - -impl Future for MergedOutputsFuture -where - F: Future, - F::Output: MergeableResult, -{ - 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(match projected.merge_kind { - MergeKind::All => F::Output::merge_all(cx, outputs), - MergeKind::Any => F::Output::merge_any(cx, outputs), - }) - } -} - -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -enum MergeKind { - All, - Any, -} - -impl InvertibleResult for MergedOutputsFuture -where - F: Future, - F::Output: MergeableResult, - ::Merged: InvertibleResult, -{ - type Inverted = InvertedOutputFuture; - - #[inline] - fn invert(self, cx: AssertionContext) -> Self::Inverted { - InvertedOutputFuture::new(cx, self) - } -} - -impl MergeableResult for MergedOutputsFuture -where - F: Future, - F::Output: MergeableResult, - ::Merged: MergeableResult, -{ - type Merged = MergedOutputsFuture; - - #[inline] - fn merge_all(cx: AssertionContext, results: I) -> Self::Merged - where - I: IntoIterator, - { - MergedOutputsFuture::all(cx, results) - } - - #[inline] - fn merge_any(cx: AssertionContext, results: I) -> Self::Merged - where - I: IntoIterator, - { - MergedOutputsFuture::any(cx, results) - } -} - -impl FinalizableResult for MergedOutputsFuture -where - F: Future, - F::Output: MergeableResult, - ::Merged: FinalizableResult, -{ - type Finalized = FinalizedOutputFuture; - - #[inline] - fn finalize(self) -> Self::Finalized { - FinalizedOutputFuture::new(self) - } -} diff --git a/src/assertions/futures/modifiers.rs b/src/assertions/futures/modifiers.rs index 440f114..6474b31 100644 --- a/src/assertions/futures/modifiers.rs +++ b/src/assertions/futures/modifiers.rs @@ -1,41 +1,3 @@ -use std::future::Future; +mod when_ready; -use crate::assertions::AssertionContext; - -use super::WhenReadyFuture; - -/// 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 core::future::ready; -/// # futures::executor::block_on(async { -/// 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 core::future::ready; -/// # futures::executor::block_on(async { -/// expect!(ready(ready(1)), when_ready, when_ready, to_equal(1)).await.await; -/// # }) -/// ``` -#[inline] -pub fn when_ready( - cx: AssertionContext, - subject: T, - next: fn(AssertionContext, T::Output) -> O, -) -> WhenReadyFuture -where - T: Future, -{ - WhenReadyFuture::new(cx, subject, next) -} +pub use when_ready::*; diff --git a/src/assertions/futures/modifiers/when_ready.rs b/src/assertions/futures/modifiers/when_ready.rs new file mode 100644 index 0000000..47d8060 --- /dev/null +++ b/src/assertions/futures/modifiers/when_ready.rs @@ -0,0 +1,89 @@ +use std::{future::Future, marker::PhantomData}; + +use crate::assertions::{ + futures::WhenReadyFuture, key, Assertion, AssertionContext, AssertionModifier, SubjectKey, +}; + +/// 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 core::future::ready; +/// # futures::executor::block_on(async { +/// 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 core::future::ready; +/// # futures::executor::block_on(async { +/// expect!( +/// ready(ready(1)), +/// when_ready, // outer future +/// when_ready, // inner future +/// to_equal(1) +/// ) +/// .await +/// .await; +/// # }) +/// ``` +#[inline] +pub fn when_ready( + prev: M, + _: SubjectKey, +) -> (WhenReadyModifier, SubjectKey) +where + T: Future, +{ + ( + WhenReadyModifier { + prev, + marker: PhantomData, + }, + key(), + ) +} + +/// Modifier for [`when_ready()`]. +#[derive(Clone, Debug)] +pub struct WhenReadyModifier { + prev: M, + marker: PhantomData, +} + +impl AssertionModifier for WhenReadyModifier +where + M: AssertionModifier>, +{ + type Output = M::Output; + + fn apply(self, next: A) -> Self::Output { + self.prev.apply(WhenReadyAssertion { next }) + } +} + +/// Assertion for [`when_ready()`]. +#[derive(Clone, Debug)] +pub struct WhenReadyAssertion { + next: A, +} + +impl Assertion for WhenReadyAssertion +where + T: Future, + A: Assertion, +{ + type Output = WhenReadyFuture; + + 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..d46d4eb --- /dev/null +++ b/src/assertions/futures/outputs.rs @@ -0,0 +1,9 @@ +mod inverted; +mod merged; +mod unwrapped; +mod when_ready; + +pub use inverted::*; +pub use merged::*; +pub use unwrapped::*; +pub use when_ready::*; diff --git a/src/assertions/futures/outputs/inverted.rs b/src/assertions/futures/outputs/inverted.rs new file mode 100644 index 0000000..4a78f35 --- /dev/null +++ b/src/assertions/futures/outputs/inverted.rs @@ -0,0 +1,49 @@ +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)] + pub struct InvertedOutputFuture { + #[pin] + inner: F, + cx: Option, + } +} + +impl InvertedOutputFuture +where + F: Future, + F::Output: InvertibleOutput, +{ + /// 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, + F::Output: InvertibleOutput, +{ + 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)) + } +} diff --git a/src/assertions/futures/outputs/merged.rs b/src/assertions/futures/outputs/merged.rs new file mode 100644 index 0000000..4dba320 --- /dev/null +++ b/src/assertions/futures/outputs/merged.rs @@ -0,0 +1,62 @@ +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)] + 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)) + } +} diff --git a/src/assertions/futures/finalized_output_future.rs b/src/assertions/futures/outputs/unwrapped.rs similarity index 56% rename from src/assertions/futures/finalized_output_future.rs rename to src/assertions/futures/outputs/unwrapped.rs index dfd97d8..6a233f7 100644 --- a/src/assertions/futures/finalized_output_future.rs +++ b/src/assertions/futures/outputs/unwrapped.rs @@ -6,21 +6,20 @@ use std::{ use pin_project_lite::pin_project; -use crate::assertions::general::FinalizableResult; +use crate::assertions::general::UnwrappableOutput; pin_project! { - /// Finalizes an asynchronous output. + /// Unwraps an asynchronous output. #[derive(Clone, Debug)] - pub struct FinalizedOutputFuture { + pub struct UnwrappedOutputFuture { #[pin] inner: F, } } -impl FinalizedOutputFuture +impl UnwrappedOutputFuture where - F: Future, - F::Output: FinalizableResult, + F: Future, { /// Create a new finalized output future. #[inline] @@ -29,17 +28,16 @@ where } } -impl Future for FinalizedOutputFuture +impl Future for UnwrappedOutputFuture where - F: Future, - F::Output: FinalizableResult, + F: Future, { - type Output = ::Finalized; + type Output = ::Unwrapped; #[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.finalize()) + Poll::Ready(output.unwrap()) } } diff --git a/src/assertions/futures/outputs/when_ready.rs b/src/assertions/futures/outputs/when_ready.rs new file mode 100644 index 0000000..1bc15ef --- /dev/null +++ b/src/assertions/futures/outputs/when_ready.rs @@ -0,0 +1,52 @@ +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. + #[derive(Clone, Debug)] + 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/futures/when_ready_future.rs b/src/assertions/futures/when_ready_future.rs deleted file mode 100644 index 586fd1a..0000000 --- a/src/assertions/futures/when_ready_future.rs +++ /dev/null @@ -1,112 +0,0 @@ -use std::{ - future::Future, - pin::Pin, - task::{ready, Context, Poll}, -}; - -use pin_project_lite::pin_project; - -use crate::assertions::{ - general::{FinalizableResult, InvertibleResult}, - iterators::MergeableResult, - AssertionContext, -}; - -use super::{FinalizedOutputFuture, InvertedOutputFuture, MergedOutputsFuture}; - -pin_project! { - /// A [`Future`] which executes an assertion when its subject is ready. - #[derive(Clone, Debug)] - pub struct WhenReadyFuture - where - T: Future, - { - #[pin] - subject: T, - cx: Option, - next: fn(AssertionContext, T::Output) -> O, - } -} - -impl WhenReadyFuture -where - T: Future, -{ - /// Creates a new instance of this future. - #[inline] - pub(crate) fn new( - cx: AssertionContext, - subject: T, - next: fn(AssertionContext, T::Output) -> O, - ) -> Self { - Self { - subject, - cx: Some(cx), - next, - } - } -} - -impl Future for WhenReadyFuture -where - T: Future, -{ - type Output = O; - - fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { - let projected = self.project(); - let input = ready!(projected.subject.poll(cx)); - let cx = projected.cx.take().expect("poll after ready"); - Poll::Ready((projected.next)(cx, input)) - } -} - -impl InvertibleResult for WhenReadyFuture -where - T: Future, - O: InvertibleResult, -{ - type Inverted = InvertedOutputFuture; - - #[inline] - fn invert(self, cx: AssertionContext) -> Self::Inverted { - InvertedOutputFuture::new(cx, self) - } -} - -impl MergeableResult for WhenReadyFuture -where - T: Future, - O: MergeableResult, -{ - type Merged = MergedOutputsFuture; - - #[inline] - fn merge_all(cx: AssertionContext, results: I) -> Self::Merged - where - I: IntoIterator, - { - MergedOutputsFuture::all(cx, results) - } - - #[inline] - fn merge_any(cx: AssertionContext, results: I) -> Self::Merged - where - I: IntoIterator, - { - MergedOutputsFuture::any(cx, results) - } -} - -impl FinalizableResult for WhenReadyFuture -where - T: Future, - O: FinalizableResult, -{ - type Finalized = FinalizedOutputFuture; - - #[inline] - fn finalize(self) -> Self::Finalized { - FinalizedOutputFuture::new(self) - } -} diff --git a/src/assertions/general.rs b/src/assertions/general.rs index 3034e5e..d607713 100644 --- a/src/assertions/general.rs +++ b/src/assertions/general.rs @@ -8,12 +8,10 @@ //! importing the prelude will import all the assertions and modifiers from this //! module. -mod comparisons; -mod finalizable_result; -mod invertible_result; +mod assertions; mod modifiers; +mod outputs; -pub use comparisons::*; -pub use finalizable_result::*; -pub use invertible_result::*; +pub use assertions::*; 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..18da712 --- /dev/null +++ b/src/assertions/general/assertions.rs @@ -0,0 +1,9 @@ +mod to_cmp; +mod to_equal; +mod to_satisfy; +mod to_satisfy_merge; + +pub use to_cmp::*; +pub use to_equal::*; +pub use to_satisfy::*; +pub use to_satisfy_merge::*; diff --git a/src/assertions/general/assertions/to_cmp.rs b/src/assertions/general/assertions/to_cmp.rs new file mode 100644 index 0000000..976a72d --- /dev/null +++ b/src/assertions/general/assertions/to_cmp.rs @@ -0,0 +1,141 @@ +use std::cmp::Ordering; + +use crate::{ + assertions::{Assertion, AssertionContext}, + metadata::Annotated, + AssertionResult, +}; + +/// 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] +pub fn to_be_less_than(boundary: Annotated) -> ToCmpAssertion { + ToCmpAssertion { + boundary, + ordering: Ordering::Less, + allow_eq: false, + cmp_message: "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] +pub fn to_be_less_than_or_equal_to(boundary: Annotated) -> ToCmpAssertion { + ToCmpAssertion { + boundary, + ordering: Ordering::Less, + allow_eq: true, + cmp_message: "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] +pub fn to_be_greater_than(boundary: Annotated) -> ToCmpAssertion { + ToCmpAssertion { + boundary, + ordering: Ordering::Greater, + allow_eq: false, + cmp_message: "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] +pub fn to_be_greater_than_or_equal_to(boundary: Annotated) -> ToCmpAssertion { + ToCmpAssertion { + boundary, + ordering: Ordering::Greater, + allow_eq: true, + cmp_message: "greater than or equal to", + } +} + +/// 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 Assertion for ToCmpAssertion +where + T: PartialOrd, +{ + type Output = AssertionResult; + + fn execute(self, mut cx: AssertionContext, subject: T) -> Self::Output { + cx.annotate("boundary", &self.boundary); + cx.annotate( + "ordering", + format_args!("{:?}", subject.partial_cmp(self.boundary.inner())), + ); + cx.annotate("expected ordering", self.cmp_message); + + // 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!("subject 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..fb54fa3 --- /dev/null +++ b/src/assertions/general/assertions/to_equal.rs @@ -0,0 +1,42 @@ +use crate::{ + assertions::{Assertion, AssertionContext}, + metadata::Annotated, + AssertionResult, +}; + +/// 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] +pub fn to_equal(expected: Annotated) -> ToEqualAssertion { + ToEqualAssertion { expected } +} + +/// Assertion for [`to_equal`]. +#[derive(Clone, Debug)] +pub struct ToEqualAssertion { + expected: Annotated, +} + +impl Assertion for ToEqualAssertion +where + T: PartialEq, +{ + type Output = AssertionResult; + + #[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..2d82b57 --- /dev/null +++ b/src/assertions/general/assertions/to_satisfy.rs @@ -0,0 +1,59 @@ +use crate::{ + assertions::{Assertion, AssertionContext}, + metadata::Annotated, + AssertionResult, +}; + +/// 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] +pub fn to_satisfy(predicate: Annotated) -> ToSatisfyAssertion { + ToSatisfyAssertion { predicate } +} + +/// Assertion for [`to_satisfy()`]. +#[derive(Clone, Debug)] +pub struct ToSatisfyAssertion { + predicate: Annotated, +} + +impl Assertion for ToSatisfyAssertion +where + F: FnOnce(T) -> bool, +{ + type Output = AssertionResult; + + fn execute(self, mut cx: AssertionContext, subject: T) -> Self::Output { + cx.annotate("predicate", &self.predicate); + cx.pass_if( + (self.predicate.into_inner())(subject), + "subject did not satisfy predicate", + ) + } +} diff --git a/src/assertions/general/assertions/to_satisfy_merge.rs b/src/assertions/general/assertions/to_satisfy_merge.rs new file mode 100644 index 0000000..40a242e --- /dev/null +++ b/src/assertions/general/assertions/to_satisfy_merge.rs @@ -0,0 +1,171 @@ +use crate::{ + assertions::{ + iterators::{MergeStrategy, MergeableOutput}, + Assertion, AssertionContext, + }, + metadata::Annotated, +}; + +/// Asserts that the subject matches all of the given predicates. This "forks" +/// the assertion, allowing an intermediate value to have several different +/// assertions applied to it. +/// +/// ``` +/// # use expecters::prelude::*; +/// expect!( +/// [1, 2, 3], +/// count, +/// to_satisfy_all(|value| [ +/// try_expect!(value, to_be_greater_than(0)), +/// try_expect!(value, to_be_less_than(4)), +/// ]), +/// ); +/// ``` +/// +/// The assertion fails if any of the results were failures: +/// +/// ```should_panic +/// # use expecters::prelude::*; +/// expect!( +/// [1, 2, 3], +/// count, +/// to_satisfy_all(|value| [ +/// try_expect!(value, to_be_greater_than(3)), +/// ]), +/// ); +/// ``` +#[inline] +#[must_use] +pub fn to_satisfy_all(predicates: Annotated) -> ToSatisfyMergeAssertion { + ToSatisfyMergeAssertion { + predicates, + strategy: MergeStrategy::All, + } +} + +/// Asserts that the subject matches any of the given predicates. This "forks" +/// the assertion, allowing an intermediate value to have several different +/// assertions applied to it. +/// +/// ``` +/// # use expecters::prelude::*; +/// expect!( +/// [1, 2, 3], +/// count, +/// to_satisfy_any(|value| [ +/// try_expect!(value, to_be_greater_than(0)), +/// try_expect!(value, to_be_less_than(0)), +/// ]), +/// ); +/// ``` +/// +/// The assertion fails if none of the results were successes: +/// +/// ```should_panic +/// # use expecters::prelude::*; +/// expect!( +/// [1, 2, 3], +/// count, +/// to_satisfy_any(|value| [ +/// try_expect!(value, to_be_greater_than(3)), +/// ]), +/// ); +/// ``` +#[inline] +#[must_use] +pub fn to_satisfy_any(predicates: Annotated) -> ToSatisfyMergeAssertion { + ToSatisfyMergeAssertion { + predicates, + strategy: MergeStrategy::Any, + } +} + +/// Assertion for [`to_satisfy_all()`] and [`to_satisfy_any()`]. +#[derive(Clone, Debug)] +pub struct ToSatisfyMergeAssertion { + predicates: Annotated, + strategy: MergeStrategy, +} + +impl Assertion for ToSatisfyMergeAssertion +where + F: FnOnce(T) -> R, + R: IntoIterator, +{ + type Output = ::Merged; + + fn execute(self, cx: AssertionContext, subject: T) -> Self::Output { + // TODO: allow result contexts to be "added" to cx so failure messages + // show the full execution path and not just the child path + let outputs = (self.predicates.into_inner())(subject); + MergeableOutput::merge(cx, self.strategy, outputs) + } +} + +#[cfg(test)] +mod tests { + use crate::{prelude::*, AssertionResult}; + + #[test] + fn vacuous() { + expect!(1, to_satisfy_all(|_| -> [AssertionResult; 0] { [] })); + expect!(1, not, to_satisfy_any(|_| -> [AssertionResult; 0] { [] })); + } +} + +#[cfg(all(test, feature = "futures"))] +mod async_tests { + use std::future::ready; + + use crate::prelude::*; + + #[tokio::test] + async fn test_async_all() { + // Outer async + expect!( + ready([1, 2, 3]), + when_ready, + to_satisfy_all(|values| [try_expect!(values, count, to_equal(3))]), + ) + .await; + expect!( + ready([1, 2, 3]), + when_ready, + not, + to_satisfy_all(|values| [try_expect!(values, count, to_equal(4))]), + ) + .await; + + // Nested async + expect!( + ready([1, 2, 3]), + to_satisfy_all(|values| [try_expect!(values, when_ready, count, to_equal(3))]), + ) + .await; + } + + #[tokio::test] + async fn test_async_any() { + // Outer async + expect!( + ready([1, 2, 3]), + when_ready, + to_satisfy_any(|values| [try_expect!(values, count, to_equal(3))]), + ) + .await; + expect!( + ready([1, 2, 3]), + when_ready, + not, + to_satisfy_any(|values| [try_expect!(values, count, to_equal(4))]), + ) + .await; + + // Nested async + expect!( + ready([1, 2, 3]), + to_satisfy_any(|values| [try_expect!(values, when_ready, count, to_equal(3))]), + ) + .await; + } +} diff --git a/src/assertions/general/comparisons.rs b/src/assertions/general/comparisons.rs deleted file mode 100644 index 1c76d5f..0000000 --- a/src/assertions/general/comparisons.rs +++ /dev/null @@ -1,159 +0,0 @@ -use crate::{ - assertions::{AssertionContext, AssertionResult}, - metadata::Annotated, -}; - -/// Asserts that the subject 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] -pub fn to_equal(expected: Annotated) -> impl FnOnce(AssertionContext, T) -> AssertionResult -where - T: PartialEq, -{ - move |mut cx, subject| { - cx.annotate("expected", expected.as_str()); - - if subject == expected.into_inner() { - Ok(()) - } else { - Err(cx.fail("subject is not equal to value")) - } - } -} - -/// 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] -pub fn to_be_less_than( - boundary: Annotated, -) -> impl FnOnce(AssertionContext, T) -> AssertionResult -where - T: PartialOrd, -{ - move |mut cx, subject| { - cx.annotate("boundary", &boundary); - - if subject < boundary.into_inner() { - Ok(()) - } else { - Err(cx.fail("subject is not less than value")) - } - } -} - -/// 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] -pub fn to_be_less_than_or_equal_to( - boundary: Annotated, -) -> impl FnOnce(AssertionContext, T) -> AssertionResult -where - T: PartialOrd, -{ - move |mut cx, subject| { - cx.annotate("boundary", &boundary); - - if subject <= boundary.into_inner() { - Ok(()) - } else { - Err(cx.fail("subject is not less than or equal to value")) - } - } -} - -/// 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] -pub fn to_be_greater_than( - boundary: Annotated, -) -> impl FnOnce(AssertionContext, T) -> AssertionResult -where - T: PartialOrd, -{ - move |mut cx, subject| { - cx.annotate("boundary", &boundary); - - if subject > boundary.into_inner() { - Ok(()) - } else { - Err(cx.fail("subject is not greater than value")) - } - } -} - -/// 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] -pub fn to_be_greater_than_or_equal_to( - boundary: Annotated, -) -> impl FnOnce(AssertionContext, T) -> AssertionResult -where - T: PartialOrd, -{ - move |mut cx, subject| { - cx.annotate("boundary", &boundary); - - if subject >= boundary.into_inner() { - Ok(()) - } else { - Err(cx.fail("subject is not greater than or equal to value")) - } - } -} diff --git a/src/assertions/general/finalizable_result.rs b/src/assertions/general/finalizable_result.rs deleted file mode 100644 index 045abe7..0000000 --- a/src/assertions/general/finalizable_result.rs +++ /dev/null @@ -1,34 +0,0 @@ -use std::fmt::Debug; - -/// An assertion result that can be finalized. -/// -/// Finalizing the result "unwraps" the result. For actual [`Result`] -/// results, this is literally the act of [unwrapping](Result::unwrap()) the -/// result, but other result types may choose to finalize in a different manner -/// (like unwrapping the result once it's available in the case of asynchronous -/// results). -/// -/// The purpose of finalizing the result is to panic as soon as possible if an -/// assertion fails. Not all results will be finalized, but if they are -/// finalized, they should provide output to the user as soon as possible if the -/// assertion failed. -pub trait FinalizableResult { - type Finalized; - - /// Finalizes this result. - fn finalize(self) -> Self::Finalized; -} - -impl FinalizableResult for Result -where - E: Debug, -{ - type Finalized = T; - - fn finalize(self) -> Self::Finalized { - match self { - Ok(t) => t, - Err(e) => panic!("{e:?}"), - } - } -} diff --git a/src/assertions/general/invertible_result.rs b/src/assertions/general/invertible_result.rs deleted file mode 100644 index 306b53d..0000000 --- a/src/assertions/general/invertible_result.rs +++ /dev/null @@ -1,34 +0,0 @@ -use crate::{assertions::AssertionContext, AssertionResult}; - -/// An assertion result that can be inverted. -/// -/// An inverted result is swapped from a failure to a success, or from a success -/// to a failure. -pub trait InvertibleResult { - /// The inverted result. - type Inverted; - - /// Inverts the result. - /// - /// A success is converted to a failure, and a failure is converted to a - /// success. - /// - /// ## Async - /// - /// If it is not yet known whether the result represents a success or - /// failure, then a value is returned that inverts that result when it is - /// known. - fn invert(self, cx: AssertionContext) -> Self::Inverted; -} - -impl InvertibleResult for AssertionResult { - type Inverted = AssertionResult; - - #[inline] - fn invert(self, cx: AssertionContext) -> Self::Inverted { - match self { - Ok(()) => Err(cx.fail("expected a failure, received a success")), - Err(_) => Ok(cx.pass()), - } - } -} diff --git a/src/assertions/general/modifiers.rs b/src/assertions/general/modifiers.rs index fe7edc2..0c0b92d 100644 --- a/src/assertions/general/modifiers.rs +++ b/src/assertions/general/modifiers.rs @@ -1,69 +1,9 @@ -use crate::{assertions::AssertionContext, metadata::Annotated}; +mod annotate; +mod map; +mod not; +mod root; -use super::InvertibleResult; - -/// 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)); -/// ``` -#[inline] -pub fn not( - cx: AssertionContext, - subject: T, - next: fn(AssertionContext, T) -> O, -) -> O::Inverted -where - O: InvertibleResult, -{ - let output = next(cx.clone(), subject); - output.invert(cx) -} - -/// 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')); -/// ``` -#[inline] -pub fn map( - mut f: Annotated, -) -> impl FnOnce(AssertionContext, T, fn(AssertionContext, U) -> O) -> O -where - F: FnMut(T) -> U, -{ - move |mut cx, subject, next| { - cx.annotate("function", &f); - let subject = (f.inner_mut())(subject); - next(cx, subject) - } -} +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..3ff1430 --- /dev/null +++ b/src/assertions/general/modifiers/annotate.rs @@ -0,0 +1,67 @@ +use crate::{ + assertions::{key, Assertion, AssertionContext, AssertionModifier, SubjectKey}, + metadata::{Annotated, AnnotatedKind}, +}; + +#[doc(hidden)] +pub fn __annotate( + _: SubjectKey, + prev: M, + annotate: fn(T) -> Annotated, +) -> (AnnotateModifier, SubjectKey) { + (AnnotateModifier { prev, annotate }, key()) +} + +/// 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. +#[derive(Clone, Debug)] +pub struct AnnotateModifier { + prev: M, + annotate: fn(T) -> Annotated, +} + +impl AssertionModifier for AnnotateModifier +where + M: AssertionModifier>, +{ + type Output = M::Output; + + #[inline] + fn apply(self, assertion: A) -> Self::Output { + self.prev.apply(AnnotateAssertion { + next: assertion, + annotate: self.annotate, + }) + } +} + +/// Assertion for [`AnnotateModifier`]. See the docs for the modifier for more +/// information. +#[derive(Clone, Debug)] +pub struct AnnotateAssertion { + next: A, + annotate: fn(T) -> Annotated, +} + +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..4a36097 --- /dev/null +++ b/src/assertions/general/modifiers/map.rs @@ -0,0 +1,96 @@ +use std::marker::PhantomData; + +use crate::{ + assertions::{key, Assertion, AssertionContext, AssertionModifier, SubjectKey}, + metadata::Annotated, +}; + +/// 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')); +/// ``` +#[inline] +pub fn map( + f: Annotated, +) -> impl FnOnce(M, SubjectKey) -> (MapModifier, SubjectKey) +where + F: FnOnce(T) -> U, +{ + move |prev, _| { + ( + MapModifier { + prev, + map: f, + marker: PhantomData, + }, + key(), + ) + } +} + +/// Modifier for [`map()`]. +#[derive(Clone, Debug)] +pub struct MapModifier { + prev: M, + map: Annotated, + marker: PhantomData U>, +} + +impl AssertionModifier for MapModifier +where + M: AssertionModifier>, +{ + type Output = M::Output; + + #[inline] + fn apply(self, next: A) -> Self::Output { + self.prev.apply(MapAssertion { + next, + map: self.map, + marker: PhantomData, + }) + } +} + +/// Assertion for [`map()`]. +#[derive(Clone, Debug)] +pub struct MapAssertion { + next: A, + map: Annotated, + marker: PhantomData U>, +} + +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..473a52d --- /dev/null +++ b/src/assertions/general/modifiers/not.rs @@ -0,0 +1,85 @@ +use std::marker::PhantomData; + +use crate::assertions::{ + general::InvertibleOutput, key, Assertion, AssertionContext, AssertionModifier, SubjectKey, +}; + +/// 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)); +/// ``` +#[inline] +pub fn not(prev: M, _: SubjectKey) -> (NotModifier, SubjectKey) { + ( + NotModifier { + prev, + marker: PhantomData, + }, + key(), + ) +} + +/// Modifier for [`not()`]. +#[derive(Clone, Debug)] +pub struct NotModifier { + prev: M, + marker: PhantomData, +} + +impl AssertionModifier for NotModifier +where + M: AssertionModifier>, +{ + type Output = M::Output; + + #[inline] + fn apply(self, next: A) -> Self::Output { + self.prev.apply(NotAssertion { next }) + } +} + +/// Assertion for [`not()`]. +#[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")).into_result(); + expect!( + res, + to_be_err_and, + as_debug, + to_contain_substr(r#""world""#) + ); + } +} diff --git a/src/assertions/general/modifiers/root.rs b/src/assertions/general/modifiers/root.rs new file mode 100644 index 0000000..108fb30 --- /dev/null +++ b/src/assertions/general/modifiers/root.rs @@ -0,0 +1,29 @@ +use crate::{ + assertions::{key, Assertion, AssertionContext, AssertionModifier, SubjectKey}, + metadata::Annotated, +}; + +#[doc(hidden)] +#[inline] +pub fn __root(cx: AssertionContext, subject: Annotated) -> (Root, SubjectKey) { + (Root { cx, subject }, key()) +} + +/// The root of an assertion. +#[derive(Clone, Debug)] +pub struct Root { + cx: AssertionContext, + subject: Annotated, +} + +impl AssertionModifier for Root +where + A: Assertion, +{ + type Output = A::Output; + + #[inline] + fn apply(self, assertion: A) -> Self::Output { + assertion.execute(self.cx, self.subject.into_inner()) + } +} diff --git a/src/assertions/general/outputs.rs b/src/assertions/general/outputs.rs new file mode 100644 index 0000000..6f81baf --- /dev/null +++ b/src/assertions/general/outputs.rs @@ -0,0 +1,5 @@ +mod invert; +mod unwrap; + +pub use invert::*; +pub use unwrap::*; diff --git a/src/assertions/general/outputs/invert.rs b/src/assertions/general/outputs/invert.rs new file mode 100644 index 0000000..0e6784f --- /dev/null +++ b/src/assertions/general/outputs/invert.rs @@ -0,0 +1,54 @@ +use std::future::Future; + +use crate::{assertions::AssertionContext, AssertionResult}; + +/// An assertion result that can be inverted. +/// +/// An inverted result is swapped from a failure to a success, or from a success +/// to a failure. +pub trait InvertibleOutput { + /// The inverted result. + type Inverted; + + /// Inverts the result. + /// + /// A success is converted to a failure, and a failure is converted to a + /// success. + /// + /// If it is not yet known whether the result represents a success or + /// failure, then a value is returned that inverts that result when it is + /// known. + fn invert(self, cx: AssertionContext) -> Self::Inverted; +} + +impl InvertibleOutput for AssertionResult { + 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 + } +} + +#[cfg(feature = "futures")] +const _: () = { + use crate::assertions::futures::InvertedOutputFuture; + + 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/general/outputs/unwrap.rs b/src/assertions/general/outputs/unwrap.rs new file mode 100644 index 0000000..6ab2e86 --- /dev/null +++ b/src/assertions/general/outputs/unwrap.rs @@ -0,0 +1,54 @@ +use crate::AssertionResult; + +/// An assertion output that can be unwrapped. +/// +/// Unwrapping the output causes it to panic as soon as possible. For +/// [`AssertionResult`]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; + + /// 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!). + fn unwrap(self) -> Self::Unwrapped; +} + +impl UnwrappableOutput for AssertionResult { + type Unwrapped = (); + + #[inline] + fn unwrap(self) -> Self::Unwrapped { + if let Err(e) = self.into_result() { + panic!("{e:?}") + } + } +} + +#[cfg(feature = "futures")] +const _: () = { + use std::future::Future; + + use crate::assertions::futures::UnwrappedOutputFuture; + + impl UnwrappableOutput for F + where + F: Future, + { + type Unwrapped = UnwrappedOutputFuture; + + fn unwrap(self) -> Self::Unwrapped { + UnwrappedOutputFuture::new(self) + } + } +}; diff --git a/src/assertions/iterators.rs b/src/assertions/iterators.rs index ade58d1..cf186ff 100644 --- a/src/assertions/iterators.rs +++ b/src/assertions/iterators.rs @@ -1,10 +1,7 @@ //! Assertions and modifiers for tests that involve iterators. -//! -//! This module contains utilities for manipulating and executing assertions on -//! iterators. -mod mergeable_result; mod modifiers; +mod outputs; -pub use mergeable_result::*; pub use modifiers::*; +pub use outputs::*; diff --git a/src/assertions/iterators/mergeable_result.rs b/src/assertions/iterators/mergeable_result.rs deleted file mode 100644 index bc78944..0000000 --- a/src/assertions/iterators/mergeable_result.rs +++ /dev/null @@ -1,59 +0,0 @@ -use crate::{assertions::AssertionContext, AssertionResult}; - -/// A type of result that can be collected from an iterator into a merged result -/// value. -/// -/// This is the core of how modifiers like [`all`] and [`any`] work. Results -/// that implement this trait can be collected from an iterator into a new -/// result following one of two strategies: -/// -/// - `all`: the merged result succeeds if none of the original results were -/// failures. -/// - `any`: the merged result succeeds if at least one of the original results -/// was a failure. -/// -/// [`all`]: crate::prelude::all -/// [`any`]: crate::prelude::any -pub trait MergeableResult { - /// The type of the merged result. - type Merged; - - /// Merges an iterator of results. The output represents a success if and - /// only if none of the constituent results failed. - fn merge_all(cx: AssertionContext, results: I) -> Self::Merged - where - I: IntoIterator; - - /// Merges an iterator of results. The output represents a success if and - /// only if at least one of the constituent results succeeded. - fn merge_any(cx: AssertionContext, results: I) -> Self::Merged - where - I: IntoIterator; -} - -impl MergeableResult for AssertionResult { - type Merged = AssertionResult; - - #[inline] - fn merge_all(_cx: AssertionContext, results: I) -> Self::Merged - where - I: IntoIterator, - { - results.into_iter().collect() - } - - fn merge_any(cx: AssertionContext, results: I) -> Self::Merged - where - I: IntoIterator, - { - let mut error = cx.fail("no results"); - for result in results { - match result { - Ok(()) => return Ok(()), - Err(e) => error = e, - } - } - - Err(error) - } -} diff --git a/src/assertions/iterators/modifiers.rs b/src/assertions/iterators/modifiers.rs index 25598d1..89bfb10 100644 --- a/src/assertions/iterators/modifiers.rs +++ b/src/assertions/iterators/modifiers.rs @@ -1,144 +1,7 @@ -use crate::{assertions::AssertionContext, metadata::Annotated, AssertionResult}; +mod count; +mod merge; +mod nth; -use super::MergeableResult; - -/// 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)); -/// ``` -/// -/// This method panics if any element does not satisfy the assertion: -/// -/// ```should_panic -/// # use expecters::prelude::*; -/// expect!([1, 3, 5], all, to_equal(5)); -/// ``` -#[inline] -pub fn all( - cx: AssertionContext, - subject: T, - next: fn(AssertionContext, T::Item) -> O, -) -> O::Merged -where - T: IntoIterator, - O: MergeableResult, -{ - O::merge_all( - cx.clone(), - subject.into_iter().enumerate().map(|(idx, item)| { - let mut cx = cx.clone(); - cx.annotate("index", idx); - next(cx, item) - }), - ) -} - -/// 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)); -/// ``` -/// -/// This method panics if any element does not satisfy the assertion: -/// -/// ```should_panic -/// # use expecters::prelude::*; -/// expect!([1, 3, 5], any, to_equal(4)); -/// ``` -#[inline] -pub fn any( - cx: AssertionContext, - subject: T, - next: fn(AssertionContext, T::Item) -> O, -) -> O::Merged -where - T: IntoIterator, - O: MergeableResult, -{ - O::merge_any( - cx.clone(), - subject.into_iter().enumerate().map(|(idx, item)| { - let mut cx = cx.clone(); - cx.annotate("index", idx); - next(cx, item) - }), - ) -} - -/// Counts the length of the subject, and executes an assertion on the result. -/// -/// 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)); -/// ``` -#[inline] -pub fn count( - mut cx: AssertionContext, - subject: T, - next: fn(AssertionContext, usize) -> O, -) -> O -where - T: IntoIterator, -{ - let count = subject.into_iter().count(); - cx.annotate("count", count); - next(cx, count) -} - -/// 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)); -/// ``` -#[inline] -pub fn nth( - idx: Annotated, -) -> impl FnOnce(AssertionContext, T, fn(AssertionContext, T::Item) -> AssertionResult) -> AssertionResult -where - T: IntoIterator, -{ - move |mut cx, subject, next| { - cx.annotate("index", idx.as_str()); - let item = subject - .into_iter() - .nth(idx.into_inner()) - .ok_or_else(|| cx.fail("index out of bounds"))?; - next(cx, item) - } -} +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..56cc82f --- /dev/null +++ b/src/assertions/iterators/modifiers/count.rs @@ -0,0 +1,63 @@ +use std::marker::PhantomData; + +use crate::assertions::{key, Assertion, AssertionContext, AssertionModifier, SubjectKey}; + +/// 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. +#[inline] +pub fn count(prev: M, _: SubjectKey) -> (CountModifier, SubjectKey) { + ( + CountModifier { + prev, + marker: PhantomData, + }, + key(), + ) +} + +/// Modifier for [`count()`]. +#[derive(Clone, Debug)] +pub struct CountModifier { + prev: M, + marker: PhantomData, +} + +impl AssertionModifier for CountModifier +where + M: AssertionModifier>, +{ + type Output = M::Output; + + #[inline] + fn apply(self, next: A) -> Self::Output { + self.prev.apply(CountAssertion { next }) + } +} + +/// Assertion for [`count()`]. +#[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..f7a92c4 --- /dev/null +++ b/src/assertions/iterators/modifiers/merge.rs @@ -0,0 +1,124 @@ +use std::marker::PhantomData; + +use crate::assertions::{ + iterators::{MergeStrategy, MergeableOutput}, + key, Assertion, AssertionContext, AssertionModifier, SubjectKey, +}; + +/// 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)); +/// ``` +#[inline] +pub fn all(prev: M, _: SubjectKey) -> (MergeModifier, SubjectKey) +where + T: IntoIterator, +{ + ( + MergeModifier { + prev, + strategy: MergeStrategy::All, + marker: PhantomData, + }, + key(), + ) +} + +/// 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)); +/// ``` +#[inline] +pub fn any(prev: M, _: SubjectKey) -> (MergeModifier, SubjectKey) +where + T: IntoIterator, +{ + ( + MergeModifier { + prev, + strategy: MergeStrategy::Any, + marker: PhantomData, + }, + key(), + ) +} + +/// Modifier for [`all()`] and [`any()`]. +#[derive(Clone, Debug)] +pub struct MergeModifier { + prev: M, + strategy: MergeStrategy, + marker: PhantomData, +} + +impl AssertionModifier for MergeModifier +where + M: AssertionModifier>, +{ + type Output = M::Output; + + #[inline] + fn apply(self, next: A) -> Self::Output { + self.prev.apply(MergeAssertion { + next, + strategy: self.strategy, + }) + } +} + +/// Assertion for [`all()`] and [`any()`]. +#[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) + } +} diff --git a/src/assertions/iterators/modifiers/nth.rs b/src/assertions/iterators/modifiers/nth.rs new file mode 100644 index 0000000..024f133 --- /dev/null +++ b/src/assertions/iterators/modifiers/nth.rs @@ -0,0 +1,97 @@ +use std::marker::PhantomData; + +use crate::{ + assertions::{key, Assertion, AssertionContext, AssertionModifier, SubjectKey}, + metadata::Annotated, + AssertionResult, +}; + +/// 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)); +/// ``` +#[inline] +pub fn nth( + index: Annotated, +) -> impl FnOnce(M, SubjectKey) -> (NthModifier, SubjectKey) +where + T: IntoIterator, +{ + move |prev, _| { + ( + NthModifier { + prev, + index, + marker: PhantomData, + }, + key(), + ) + } +} + +/// Modifier for [`nth()`]. +#[derive(Clone, Debug)] +pub struct NthModifier { + prev: M, + index: Annotated, + marker: PhantomData, +} + +impl AssertionModifier for NthModifier +where + M: AssertionModifier>, +{ + type Output = M::Output; + + #[inline] + fn apply(self, next: A) -> Self::Output { + self.prev.apply(NthAssertion { + next, + index: self.index, + }) + } +} + +/// Assertion for [`nth()`]. +#[derive(Clone, Debug)] +pub struct NthAssertion { + next: A, + index: Annotated, +} + +impl Assertion for NthAssertion +where + A: Assertion, + T: IntoIterator, +{ + type Output = AssertionResult; + + #[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) + } +} 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..8720f61 --- /dev/null +++ b/src/assertions/iterators/outputs/merge.rs @@ -0,0 +1,87 @@ +use crate::{assertions::AssertionContext, AssertionResult}; + +/// 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::all +/// [`any`]: crate::prelude::any +pub trait MergeableOutput { + /// The type of the merged output. + type Merged; + + /// Merges an iterator of assertion outputs into a single output. + fn merge(cx: AssertionContext, strategy: MergeStrategy, outputs: I) -> Self::Merged + where + I: IntoIterator; +} + +impl MergeableOutput for AssertionResult { + type Merged = AssertionResult; + + #[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 + } +} + +#[cfg(feature = "futures")] +const _: () = { + use std::future::Future; + + use crate::assertions::futures::MergedOutputsFuture; + + impl MergeableOutput for F + where + F: Future, + { + type Merged = MergedOutputsFuture; + + fn merge(cx: AssertionContext, strategy: MergeStrategy, outputs: I) -> Self::Merged + where + I: IntoIterator, + { + MergedOutputsFuture::new(cx, strategy, outputs) + } + } +}; + +/// 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 index e69de29..c258c40 100644 --- a/src/assertions/options.rs +++ b/src/assertions/options.rs @@ -0,0 +1,9 @@ +//! Assertions and modifiers for tests that involve [`Option`]s. + +mod assertions; +mod modifiers; +mod optionish; + +pub use assertions::*; +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..098fb53 --- /dev/null +++ b/src/assertions/options/assertions/to_be_variant.rs @@ -0,0 +1,105 @@ +use crate::{ + assertions::{options::Optionish, Assertion, AssertionContext}, + AssertionResult, +}; + +/// 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] +pub fn to_be_some() -> ToBeOptionVariantAssertion { + ToBeOptionVariantAssertion { + expected: Variant::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] +pub fn to_be_none() -> ToBeOptionVariantAssertion { + ToBeOptionVariantAssertion { + expected: Variant::None, + } +} + +/// Assertion for [`to_be_some()`] and [`to_be_none()`]. +#[derive(Clone, Debug)] +pub struct ToBeOptionVariantAssertion { + expected: Variant, +} + +impl Assertion for ToBeOptionVariantAssertion +where + O: Optionish, +{ + type Output = AssertionResult; + + fn execute(self, mut cx: AssertionContext, subject: O) -> Self::Output { + cx.annotate("expected", format_args!("{:?}", self.expected)); + + match self.expected { + Variant::Some => cx.pass_if(subject.some().is_some(), "subject is None"), + Variant::None => cx.pass_if(subject.some().is_none(), "subject is Some"), + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum Variant { + 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/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..0581b05 --- /dev/null +++ b/src/assertions/options/modifiers/some_and.rs @@ -0,0 +1,98 @@ +use std::marker::PhantomData; + +use crate::{ + assertions::{ + key, options::Optionish, Assertion, AssertionContext, AssertionModifier, SubjectKey, + }, + AssertionResult, +}; + +/// 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)); +/// ``` +#[inline] +pub fn to_be_some_and( + prev: M, + _: SubjectKey, +) -> (SomeAndModifier, SubjectKey) +where + O: Optionish, +{ + ( + SomeAndModifier { + prev, + marker: PhantomData, + }, + key(), + ) +} + +/// Modifier for [`to_be_some_and()`]. +#[derive(Clone, Debug)] +pub struct SomeAndModifier { + prev: M, + marker: PhantomData, +} + +impl AssertionModifier for SomeAndModifier +where + M: AssertionModifier>, +{ + type Output = M::Output; + + #[inline] + fn apply(self, next: A) -> Self::Output { + self.prev.apply(SomeAndAssertion { next }) + } +} + +/// Assertion for [`to_be_some_and()`]. +#[derive(Clone, Debug)] +pub struct SomeAndAssertion { + next: A, +} + +impl Assertion for SomeAndAssertion +where + A: Assertion, + O: Optionish, +{ + type Output = AssertionResult; + + #[inline] + fn execute(self, cx: AssertionContext, subject: O) -> Self::Output { + let Some(subject) = subject.some() else { + return cx.fail("subject is None"); + }; + self.next.execute(cx, subject) + } +} + +#[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)); + } +} diff --git a/src/assertions/options/optionish.rs b/src/assertions/options/optionish.rs new file mode 100644 index 0000000..1cd49c7 --- /dev/null +++ b/src/assertions/options/optionish.rs @@ -0,0 +1,44 @@ +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. +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 index e69de29..d30e491 100644 --- a/src/assertions/results.rs +++ b/src/assertions/results.rs @@ -0,0 +1,9 @@ +//! Assertions and modifiers for tests that involve [`Result`]. + +mod assertions; +mod modifiers; +mod resultish; + +pub use assertions::*; +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..a6e43d6 --- /dev/null +++ b/src/assertions/results/assertions/to_be_variant.rs @@ -0,0 +1,109 @@ +use crate::{ + assertions::{results::Resultish, Assertion, AssertionContext}, + AssertionResult, +}; + +/// 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] +pub fn to_be_ok() -> ToBeResultVariantAssertion { + ToBeResultVariantAssertion { + expected: Variant::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] +pub fn to_be_err() -> ToBeResultVariantAssertion { + ToBeResultVariantAssertion { + expected: Variant::Err, + } +} + +/// Assertion for [`to_be_ok()`] and [`to_be_err()`]. +#[derive(Clone, Debug)] +pub struct ToBeResultVariantAssertion { + expected: Variant, +} + +impl Assertion for ToBeResultVariantAssertion +where + R: Resultish, +{ + type Output = AssertionResult; + + fn execute(self, mut cx: AssertionContext, subject: R) -> Self::Output { + cx.annotate("expected", format_args!("{:?}", self.expected)); + + match self.expected { + Variant::Ok => cx.pass_if(subject.ok().is_some(), "subject is Err"), + Variant::Err => cx.pass_if(subject.err().is_some(), "subject is Ok"), + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum Variant { + 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/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..ed63ff9 --- /dev/null +++ b/src/assertions/results/modifiers/err_and.rs @@ -0,0 +1,97 @@ +use std::marker::PhantomData; + +use crate::{ + assertions::{ + key, results::Resultish, Assertion, AssertionContext, AssertionModifier, SubjectKey, + }, + AssertionResult, +}; + +/// 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")); +/// ``` +#[inline] +pub fn to_be_err_and(prev: M, _: SubjectKey) -> (ErrAndModifier, SubjectKey) +where + R: Resultish, +{ + ( + ErrAndModifier { + prev, + marker: PhantomData, + }, + key(), + ) +} + +/// Modifier for [`to_be_err_and()`]. +#[derive(Clone, Debug)] +pub struct ErrAndModifier { + prev: M, + marker: PhantomData, +} + +impl AssertionModifier for ErrAndModifier +where + M: AssertionModifier>, +{ + type Output = M::Output; + + #[inline] + fn apply(self, next: A) -> Self::Output { + self.prev.apply(ErrAndAssertion { next }) + } +} + +/// Assertion for [`to_be_err_and()`]. +#[derive(Clone, Debug)] +pub struct ErrAndAssertion { + next: A, +} + +impl Assertion for ErrAndAssertion +where + A: Assertion, + R: Resultish, +{ + type Output = AssertionResult; + + #[inline] + fn execute(self, cx: AssertionContext, subject: R) -> Self::Output { + let Some(subject) = subject.err() else { + return cx.fail("subject is Ok"); + }; + self.next.execute(cx, subject) + } +} + +#[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)); + } +} diff --git a/src/assertions/results/modifiers/ok_and.rs b/src/assertions/results/modifiers/ok_and.rs new file mode 100644 index 0000000..4de5c8b --- /dev/null +++ b/src/assertions/results/modifiers/ok_and.rs @@ -0,0 +1,97 @@ +use std::marker::PhantomData; + +use crate::{ + assertions::{ + key, results::Resultish, Assertion, AssertionContext, AssertionModifier, SubjectKey, + }, + AssertionResult, +}; + +/// 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)); +/// ``` +#[inline] +pub fn to_be_ok_and(prev: M, _: SubjectKey) -> (OkAndModifier, SubjectKey) +where + R: Resultish, +{ + ( + OkAndModifier { + prev, + marker: PhantomData, + }, + key(), + ) +} + +/// Modifier for [`to_be_ok_and()`]. +#[derive(Clone, Debug)] +pub struct OkAndModifier { + prev: M, + marker: PhantomData, +} + +impl AssertionModifier for OkAndModifier +where + M: AssertionModifier>, +{ + type Output = M::Output; + + #[inline] + fn apply(self, next: A) -> Self::Output { + self.prev.apply(OkAndAssertion { next }) + } +} + +/// Assertion for [`to_be_ok_and()`]. +#[derive(Clone, Debug)] +pub struct OkAndAssertion { + next: A, +} + +impl Assertion for OkAndAssertion +where + A: Assertion, + R: Resultish, +{ + type Output = AssertionResult; + + #[inline] + fn execute(self, cx: AssertionContext, subject: R) -> Self::Output { + let Some(subject) = subject.ok() else { + return cx.fail("subject is Err"); + }; + self.next.execute(cx, subject) + } +} + +#[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)); + } +} diff --git a/src/assertions/results/resultish.rs b/src/assertions/results/resultish.rs new file mode 100644 index 0000000..19f8730 --- /dev/null +++ b/src/assertions/results/resultish.rs @@ -0,0 +1,72 @@ +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. +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..db38415 --- /dev/null +++ b/src/assertions/strings.rs @@ -0,0 +1,5 @@ +mod assertions; +mod modifiers; + +pub use assertions::*; +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..b6a1cc5 --- /dev/null +++ b/src/assertions/strings/assertions/to_contain_substr.rs @@ -0,0 +1,48 @@ +use crate::{ + assertions::{Assertion, AssertionContext}, + metadata::Annotated, + AssertionResult, +}; + +/// 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] +pub fn to_contain_substr

(pattern: Annotated

) -> ToContainSubstr

+where + P: AsRef, +{ + ToContainSubstr { pattern } +} + +/// Assertion for [`to_contain_substr()`]. +#[derive(Clone, Debug)] +pub struct ToContainSubstr

{ + pattern: Annotated

, +} + +impl Assertion for ToContainSubstr

+where + P: AsRef, + T: AsRef, +{ + type Output = AssertionResult; + + fn execute(self, mut cx: AssertionContext, subject: T) -> Self::Output { + let pattern = self.pattern.inner().as_ref(); + cx.annotate("expected", format_args!("{pattern:?}")); + + cx.pass_if(subject.as_ref().contains(pattern), "substring not found") + } +} 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..2560752 --- /dev/null +++ b/src/assertions/strings/assertions/to_match_regex.rs @@ -0,0 +1,55 @@ +use std::sync::Arc; + +use regex::Regex; + +use crate::{ + assertions::{Assertion, AssertionContext}, + metadata::Annotated, + AssertionResult, +}; + +/// 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+")); +/// ``` +pub fn to_match_regex

(pattern: Annotated

) -> ToMatchRegexAssertion +where + P: AsRef, +{ + let pattern = pattern.inner().as_ref(); + let regex = Regex::new(pattern.as_ref()).expect("invalid regex"); + ToMatchRegexAssertion { + regex: Arc::new(regex), + } +} + +/// Assertion for [`to_match_regex()`]. +#[derive(Clone, Debug)] +pub struct ToMatchRegexAssertion { + regex: Arc, +} + +impl Assertion for ToMatchRegexAssertion +where + T: AsRef, +{ + type Output = AssertionResult; + + 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()), + "subject didn't match pattern", + ) + } +} diff --git a/src/assertions/strings/modifiers.rs b/src/assertions/strings/modifiers.rs new file mode 100644 index 0000000..c8ce9ef --- /dev/null +++ b/src/assertions/strings/modifiers.rs @@ -0,0 +1,5 @@ +mod debug; +mod display; + +pub use debug::*; +pub use display::*; diff --git a/src/assertions/strings/modifiers/debug.rs b/src/assertions/strings/modifiers/debug.rs new file mode 100644 index 0000000..40c29f0 --- /dev/null +++ b/src/assertions/strings/modifiers/debug.rs @@ -0,0 +1,61 @@ +use std::{fmt::Debug, marker::PhantomData}; + +use crate::assertions::{key, Assertion, AssertionContext, AssertionModifier, SubjectKey}; + +/// Converts a value to its [`Debug`] representation. +/// +/// ``` +/// # use expecters::prelude::*; +/// expect!("hello", as_debug, to_equal(r#""hello""#)); +/// ``` +#[inline] +pub fn as_debug(prev: M, _: SubjectKey) -> (AsDebugModifier, SubjectKey) +where + T: Debug, +{ + ( + AsDebugModifier { + prev, + marker: PhantomData, + }, + key(), + ) +} + +/// Modifier for [`as_debug()`]. +#[derive(Clone, Debug)] +pub struct AsDebugModifier { + prev: M, + marker: PhantomData, +} + +impl AssertionModifier for AsDebugModifier +where + M: AssertionModifier>, +{ + type Output = M::Output; + + #[inline] + fn apply(self, next: A) -> Self::Output { + self.prev.apply(AsDebugAssertion { next }) + } +} + +/// Assertion for [`as_debug()`]. +#[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..fd427c2 --- /dev/null +++ b/src/assertions/strings/modifiers/display.rs @@ -0,0 +1,64 @@ +use std::{ + fmt::{Debug, Display}, + marker::PhantomData, +}; + +use crate::assertions::{key, Assertion, AssertionContext, AssertionModifier, SubjectKey}; + +/// Converts a value to its [`Display`] representation. +/// +/// ``` +/// # use expecters::prelude::*; +/// expect!(1, as_display, to_equal("1")); +/// ``` +#[inline] +pub fn as_display(prev: M, _: SubjectKey) -> (AsDisplayModifier, SubjectKey) +where + T: Display, +{ + ( + AsDisplayModifier { + prev, + marker: PhantomData, + }, + key(), + ) +} + +/// Modifier for [`as_display()`]. +#[derive(Clone, Debug)] +pub struct AsDisplayModifier { + prev: M, + marker: PhantomData, +} + +impl AssertionModifier for AsDisplayModifier +where + M: AssertionModifier>, +{ + type Output = M::Output; + + #[inline] + fn apply(self, next: A) -> Self::Output { + self.prev.apply(AsDisplayAssertion { next }) + } +} + +/// Assertion for [`as_display()`]. +#[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/lib.rs b/src/lib.rs index fe4b9cf..ae122bf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,19 @@ +//! TODO + +#![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::module_name_repetitions)] #![forbid(unsafe_code)] pub mod assertions; @@ -8,5 +24,4 @@ pub mod specialization; mod macros; -// pub use expect::*; pub use assertions::AssertionResult; diff --git a/src/macros.rs b/src/macros.rs index 73551c2..49365a7 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -160,6 +160,24 @@ /// [stringified]: core::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 an +/// [`AssertionResult`](crate::AssertionResult) instead. +/// +/// See [`expect!`] for more information on how to use this macro. +#[macro_export] +macro_rules! try_expect { ($($tokens:tt)*) => { $crate::__expect_inner!($($tokens)*) }; @@ -173,7 +191,9 @@ macro_rules! __expect_inner { $subject:expr, $($assertions:tt)* ) => {{ + let subject = $crate::annotated!($subject); let cx = $crate::assertions::AssertionContext::__new( + ::std::string::ToString::to_string(&subject), $crate::source_loc!(), $crate::__expect_inner!( @build_ctx_frames, @@ -181,34 +201,22 @@ macro_rules! __expect_inner { $($assertions)* ), ); - let result = $crate::__expect_inner!( + let (root, _key) = $crate::assertions::general::__root(cx, subject); + $crate::__expect_inner!( @build_assertion, - cx, - $subject, + root, + _key, $($assertions)* - ); - $crate::assertions::general::FinalizableResult::finalize(result) + ) }}; // Build context frame names (from modifier/assertion names) - ( - // Recursive case - @build_ctx_frames, - [$($frames:expr),*], - $frame_name:ident $(($($_:tt)*))?, - $($assertions:tt)* - ) => { - $crate::__expect_inner!( - @build_ctx_frames, - [$($frames,)* ::core::stringify!($frame_name)], - $($assertions)* - ) - }; ( // Base case @build_ctx_frames, [$($frames:expr),*], $frame_name:ident $(($($_:tt)*))? + $(,)? ) => {{ const FRAMES: &'static [&'static str] = &[ $($frames,)* @@ -216,327 +224,131 @@ macro_rules! __expect_inner { ]; FRAMES }}; - - // Build assertion (chain modifiers and final assertion) - ( - // Recursive case (with params) - @build_assertion, - $cx:expr, - $subject:expr, - $assertion:ident($($param:expr),* $(,)?), - $($rest:tt)* - ) => { - $crate::__expect_inner!( - @annotate_assertion, - $cx, - $subject, - |cx, subject| { - let assertion = $assertion($($crate::annotated!($param),)*); - assertion(cx, subject, |cx, no_debug_impl| { - $crate::__expect_inner!( - @build_assertion, - cx, - no_debug_impl, - $($rest)* - ) - }) - } - ) - }; ( - // Recursive case (without params) - @build_assertion, - $cx:expr, - $subject:expr, - $assertion:ident, - $($rest:tt)* + // Recursive case + @build_ctx_frames, + [$($frames:expr),*], + $frame_name:ident $(($($_:tt)*))?, + $($assertions:tt)* ) => { $crate::__expect_inner!( - @annotate_assertion, - $cx, - $subject, - |cx, subject| { - $assertion(cx, subject, |cx, no_debug_impl| { - $crate::__expect_inner!( - @build_assertion, - cx, - no_debug_impl, - $($rest)* - ) - }) - } + @build_ctx_frames, + [$($frames,)* ::core::stringify!($frame_name)], + $($assertions)* ) }; + + // Build assertion (chain modifiers and final assertion) ( // Base case (with params) @build_assertion, - $cx:expr, - $subject:expr, - $assertion:ident($($param:expr),* $(,)?) $(,)? - ) => { - $crate::__expect_inner!( - @annotate_assertion, - $cx, - $subject, - |cx, subject| { - let assertion = $assertion($($crate::annotated!($param),)*); - assertion(cx, subject) - } + $chain:expr, + $key:expr, + $assertion:ident($($param:expr),* $(,)?) + $(,)? + ) => {{ + let (chain, _key) = $crate::__expect_inner!(@annotate, $chain, $key); + let assertion = $assertion($($crate::annotated!($param),)*); + $crate::assertions::AssertionModifier::apply( + chain, + assertion, ) - }; + }}; ( // Base case (without params) @build_assertion, - $cx:expr, - $subject:expr, - $assertion:ident $(,)? - ) => { - $crate::__expect_inner!( - @annotate_assertion, - $cx, - $subject, - |cx, subject| { - let assertion = $assertion; - assertion(cx, subject) - } + $chain:expr, + $key:expr, + $assertion:ident + $(,)? + ) => {{ + let (chain, _key) = $crate::__expect_inner!(@annotate, $chain, $key); + let assertion = $assertion(); + $crate::assertions::AssertionModifier::apply( + chain, + assertion, ) - }; - - // Wrap assertion and annotate intermediate value + }}; ( - @annotate_assertion, - $cx:expr, - $subject:expr, - |$cx_param:pat_param, $subject_param:pat_param| $assertion:expr - ) => { - $crate::assertions::__annotate_assertion( - $cx, - $crate::annotated!($subject), - |$cx_param, $subject_param| $assertion, + // Recursive case (with params) + @build_assertion, + $chain:expr, + $key:expr, + $modifier:ident($($param:expr),* $(,)?), + $($rest:tt)* + ) => {{ + let (chain, _key) = $crate::__expect_inner!(@annotate, $chain, $key); + let modifier = $modifier($($crate::annotated!($param),)*); + let (chain, _key) = modifier(chain, _key); + $crate::__expect_inner!(@build_assertion, chain, _key, $($rest)*) + }}; + ( + // Recursive case (without params) + @build_assertion, + $chain:expr, + $key:expr, + $modifier:ident, + $($rest:tt)* + ) => {{ + let (chain, _key) = $crate::__expect_inner!(@annotate, $chain, $key); + let (chain, _key) = $modifier(chain, _key); + $crate::__expect_inner!(@build_assertion, chain, _key, $($rest)*) + }}; + + // Annotate the value being passed down the chain + (@annotate, $chain:expr, $key:expr) => { + $crate::assertions::general::__annotate( + $key, + $chain, + |not_debug| $crate::annotated!(not_debug), ) }; } #[cfg(test)] mod tests { - use core::future::ready; - use std::marker::PhantomData; + use std::future::ready; - use crate::{ - assertions::{ - general::{FinalizableResult, InvertibleResult}, - Assertion, AssertionContext, AssertionModifier, - }, - metadata::Annotated, - prelude::*, - AssertionResult, - }; + use crate::prelude::*; - #[derive(PartialEq)] + #[derive(Clone, PartialEq)] struct NotDebug(T); #[tokio::test] + // #[ignore] async fn test_debugging() { debugging().await; } async fn debugging() { - expect!( - [NotDebug(1), NotDebug(2), NotDebug(3)], - all, - map(|x: NotDebug<_>| x.0), - not, - to_be_less_than(3) - ); - - expect!([ready(1), ready(2)], all, when_ready, to_be_less_than(2)).await; - } - - // async fn debugging2() { - // // expect!(1, not, to_equal(0)); - - // let x = 1; - // expect!(1, not, to_equal(x)); - - // { - // const SOURCE_LOC: crate::metadata::SourceLoc = crate::source_loc!(); - // let cx = crate::assertions::AssertionContext::__new(&SOURCE_LOC, { - // const FRAMES: &'static [&'static str] = &[("not"), "to_equal"]; - // FRAMES - // }); - // let result = - // crate::assertions::__annotate_assertion(cx, crate::annotated!(1), |cx, subject| { - // not(cx, subject, |cx, no_debug_impl| { - // crate::assertions::__annotate_assertion( - // cx, - // crate::annotated!(no_debug_impl), - // |cx, subject| { - // let assertion = to_equal(crate::annotated!(0)); - // assertion(cx, subject) - // }, - // ) - // }) - // }); - // crate::assertions::general::FinalizableResult::finalize(result) - // } - - // /* - // { - // let subject = crate::annotated!(1); - // let assertion = __annotate_assertion_begin( - // &subject, - // |x| crate::annotated!(x), - // ); - // let assertion = assertion.apply( - // not().apply( - // __annotate_assertion2( - // to_equal(0) - // ) - // ); - // } - // */ - // } - - #[test] - fn debugging3() { - let _res = { - let cx = AssertionContext::__new( - crate::source_loc!(), - &["not2", "not2", "not2", "to_equal2"], - ); - - // TODO - let chain = Root(1); - let chain = annotate_input(|x| crate::annotated!(x))(chain); - let chain = not2(chain); - let chain = annotate_input(|x| crate::annotated!(x))(chain); - let chain = not2(chain); - let chain = annotate_input(|x| crate::annotated!(x))(chain); - let chain = not2(chain); - let chain = annotate_input(|x| crate::annotated!(x))(chain); - let assert = to_equal2(1); - let res = chain.apply(cx, assert); - - res.finalize() - }; - } + // expect!(ready(1), when_ready, to_equal(2)).await; + // expect!([1, 2, 3], count, not, to_equal(3)); + // expect!([1, 2, 3], any, to_equal(4)); - struct Root(T); + expect!("blah", to_match_regex(r"\d+")); - impl AssertionModifier for Root - where - A: Assertion, - { - type Output = A::Output; - - fn apply(self, cx: AssertionContext, assertion: A) -> Self::Output { - assertion.execute(cx, self.0) - } - } - - fn annotate_input( - annotate: fn(T) -> Annotated, - ) -> impl FnOnce(M) -> AnnotateModifier { - move |prev| AnnotateModifier { prev, annotate } - } - - #[derive(Clone, Debug)] - struct AnnotateModifier { - prev: M, - annotate: fn(T) -> Annotated, - } - - impl AssertionModifier for AnnotateModifier - where - M: AssertionModifier>, - { - type Output = M::Output; - - fn apply(self, cx: AssertionContext, assertion: A) -> Self::Output { - self.prev.apply( - cx, - AnnotateAssertion { - next: assertion, - annotate: self.annotate, - }, - ) - } - } - - #[derive(Clone, Debug)] - struct AnnotateAssertion { - next: A, - annotate: fn(T) -> Annotated, - } - - impl Assertion for AnnotateAssertion - where - A: Assertion, - { - type Output = A::Output; - - fn execute(self, cx: AssertionContext, value: T) -> Self::Output { - self.next.execute(cx.next(), value) - } - } - - #[inline] - fn not2(prev: M) -> Not2Modifier { - Not2Modifier(prev, PhantomData) - } - - #[derive(Clone, Debug)] - struct Not2Modifier(M, PhantomData); - - impl AssertionModifier for Not2Modifier - where - A: Assertion, - A::Output: InvertibleResult, - M: AssertionModifier>, - { - type Output = M::Output; - - #[inline] - fn apply(self, cx: AssertionContext, assertion: A) -> Self::Output { - self.0.apply(cx, Not2Assertion(assertion)) - } - } - - #[derive(Clone, Debug)] - struct Not2Assertion(A); - - impl Assertion for Not2Assertion - where - A: Assertion, - A::Output: InvertibleResult, - { - type Output = ::Inverted; - - #[inline] - fn execute(self, cx: AssertionContext, value: T) -> Self::Output { - self.0.execute(cx.clone(), value).invert(cx) - } - } + expect!( + "Hello, world!", + map(|s: &str| format!("{} {} {}", s.to_uppercase(), s.to_lowercase(), s)), + to_contain_substr("not present in this text because this is a really long substring"), + ); - fn to_equal2(value: U) -> ToEqual2Assertion { - ToEqual2Assertion(value) - } + expect!( + ready([NotDebug(1), NotDebug(2), NotDebug(3)]), + when_ready, + any, + not, + map(|NotDebug(x)| x), + to_be_less_than(4) + ) + .await; - #[derive(Clone, Debug)] - struct ToEqual2Assertion(U); + // expect!(ready([1, 2, 3]), when_ready, nth(3), to_equal(3)).await; + // expect!(ready(1), when_ready, to_satisfy(|n| n < 0)).await; - impl Assertion for ToEqual2Assertion - where - T: PartialEq, - { - type Output = AssertionResult; + // let res = try_expect!(ready(1), when_ready, to_satisfy(|n| n < 0)).await; + // res.into_result().unwrap(); - fn execute(self, cx: AssertionContext, value: T) -> Self::Output { - if value == self.0 { - Ok(()) - } else { - Err(cx.fail("not equal")) - } - } + // expect!([ready(1), ready(2)], all, when_ready, to_be_less_than(2)).await; } } diff --git a/src/metadata/annotated.rs b/src/metadata/annotated.rs index 0297caf..6dc51ba 100644 --- a/src/metadata/annotated.rs +++ b/src/metadata/annotated.rs @@ -93,10 +93,7 @@ impl Annotated { /// Gets the string representation of this value. #[inline] pub fn as_str(&self) -> &str { - self.string_repr - .as_ref() - .map(String::as_str) - .unwrap_or(self.stringified) + self.string_repr.as_deref().unwrap_or(self.stringified) } } diff --git a/src/metadata/source_loc.rs b/src/metadata/source_loc.rs index 1851561..830b33c 100644 --- a/src/metadata/source_loc.rs +++ b/src/metadata/source_loc.rs @@ -4,7 +4,7 @@ use std::fmt::{Display, Formatter}; #[doc(hidden)] macro_rules! source_loc { () => {{ - const SOURCE_LOC: crate::metadata::SourceLoc = $crate::metadata::SourceLoc::new( + const SOURCE_LOC: $crate::metadata::SourceLoc = $crate::metadata::SourceLoc::new( ::core::module_path!(), ::core::file!(), ::core::line!(), @@ -25,6 +25,7 @@ pub struct SourceLoc { impl SourceLoc { #[doc(hidden)] + #[must_use] pub const fn new( module_path: &'static str, file: &'static str, @@ -41,24 +42,28 @@ impl SourceLoc { /// 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 } @@ -68,7 +73,7 @@ impl Display for SourceLoc { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, - "{module} [{file}:{line}:{column}]", + "{file}:{line}:{column} [{module}]", file = self.file, line = self.line, column = self.column, diff --git a/src/prelude.rs b/src/prelude.rs index 4a3725e..8d0b53d 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -15,12 +15,18 @@ pub use crate::{ assertions::{ general::{ map, not, to_be_greater_than, to_be_greater_than_or_equal_to, to_be_less_than, - to_be_less_than_or_equal_to, to_equal, + to_be_less_than_or_equal_to, to_equal, to_satisfy, to_satisfy_all, to_satisfy_any, }, iterators::{all, any, count, nth}, + options::{to_be_none, to_be_some, to_be_some_and}, + results::{to_be_err, to_be_err_and, to_be_ok, to_be_ok_and}, + strings::{as_debug, as_display, to_contain_substr}, }, - expect, + expect, try_expect, }; #[cfg(feature = "futures")] pub use crate::assertions::futures::when_ready; + +#[cfg(feature = "regex")] +pub use crate::assertions::strings::to_match_regex; diff --git a/src/specialization.rs b/src/specialization.rs index d26857a..8efd76a 100644 --- a/src/specialization.rs +++ b/src/specialization.rs @@ -1,3 +1,5 @@ +#![allow(missing_debug_implementations)] + pub mod annotated; mod wrapper; From 31968334eafead3a6e9a938b056c119bec8261ff Mon Sep 17 00:00:00 2001 From: TehPers Date: Thu, 8 Aug 2024 08:02:16 -0700 Subject: [PATCH 03/37] Simplify failure messages --- src/assertions/general/assertions/to_cmp.rs | 5 +---- src/assertions/general/assertions/to_satisfy.rs | 2 +- src/assertions/options/assertions/to_be_variant.rs | 4 ++-- src/assertions/options/modifiers/some_and.rs | 2 +- src/assertions/results/assertions/to_be_variant.rs | 4 ++-- src/assertions/results/modifiers/err_and.rs | 2 +- src/assertions/results/modifiers/ok_and.rs | 2 +- src/assertions/strings/assertions/to_match_regex.rs | 2 +- 8 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/assertions/general/assertions/to_cmp.rs b/src/assertions/general/assertions/to_cmp.rs index 976a72d..b87b052 100644 --- a/src/assertions/general/assertions/to_cmp.rs +++ b/src/assertions/general/assertions/to_cmp.rs @@ -133,9 +133,6 @@ where (Ordering::Greater, false) => subject > boundary, (Ordering::Equal, _) => return cx.fail("use to_equal instead"), }; - cx.pass_if( - success, - format_args!("subject not {} boundary", self.cmp_message), - ) + cx.pass_if(success, format_args!("not {} boundary", self.cmp_message)) } } diff --git a/src/assertions/general/assertions/to_satisfy.rs b/src/assertions/general/assertions/to_satisfy.rs index 2d82b57..232c5f9 100644 --- a/src/assertions/general/assertions/to_satisfy.rs +++ b/src/assertions/general/assertions/to_satisfy.rs @@ -53,7 +53,7 @@ where cx.annotate("predicate", &self.predicate); cx.pass_if( (self.predicate.into_inner())(subject), - "subject did not satisfy predicate", + "did not satisfy predicate", ) } } diff --git a/src/assertions/options/assertions/to_be_variant.rs b/src/assertions/options/assertions/to_be_variant.rs index 098fb53..87a7e6d 100644 --- a/src/assertions/options/assertions/to_be_variant.rs +++ b/src/assertions/options/assertions/to_be_variant.rs @@ -61,8 +61,8 @@ where cx.annotate("expected", format_args!("{:?}", self.expected)); match self.expected { - Variant::Some => cx.pass_if(subject.some().is_some(), "subject is None"), - Variant::None => cx.pass_if(subject.some().is_none(), "subject is Some"), + Variant::Some => cx.pass_if(subject.some().is_some(), "received None"), + Variant::None => cx.pass_if(subject.some().is_none(), "received Some"), } } } diff --git a/src/assertions/options/modifiers/some_and.rs b/src/assertions/options/modifiers/some_and.rs index 0581b05..13b1de8 100644 --- a/src/assertions/options/modifiers/some_and.rs +++ b/src/assertions/options/modifiers/some_and.rs @@ -73,7 +73,7 @@ where #[inline] fn execute(self, cx: AssertionContext, subject: O) -> Self::Output { let Some(subject) = subject.some() else { - return cx.fail("subject is None"); + return cx.fail("received None"); }; self.next.execute(cx, subject) } diff --git a/src/assertions/results/assertions/to_be_variant.rs b/src/assertions/results/assertions/to_be_variant.rs index a6e43d6..03d6a25 100644 --- a/src/assertions/results/assertions/to_be_variant.rs +++ b/src/assertions/results/assertions/to_be_variant.rs @@ -65,8 +65,8 @@ where cx.annotate("expected", format_args!("{:?}", self.expected)); match self.expected { - Variant::Ok => cx.pass_if(subject.ok().is_some(), "subject is Err"), - Variant::Err => cx.pass_if(subject.err().is_some(), "subject is Ok"), + Variant::Ok => cx.pass_if(subject.ok().is_some(), "received Err"), + Variant::Err => cx.pass_if(subject.err().is_some(), "received Ok"), } } } diff --git a/src/assertions/results/modifiers/err_and.rs b/src/assertions/results/modifiers/err_and.rs index ed63ff9..0e91316 100644 --- a/src/assertions/results/modifiers/err_and.rs +++ b/src/assertions/results/modifiers/err_and.rs @@ -72,7 +72,7 @@ where #[inline] fn execute(self, cx: AssertionContext, subject: R) -> Self::Output { let Some(subject) = subject.err() else { - return cx.fail("subject is Ok"); + return cx.fail("received Ok"); }; self.next.execute(cx, subject) } diff --git a/src/assertions/results/modifiers/ok_and.rs b/src/assertions/results/modifiers/ok_and.rs index 4de5c8b..da2fa49 100644 --- a/src/assertions/results/modifiers/ok_and.rs +++ b/src/assertions/results/modifiers/ok_and.rs @@ -72,7 +72,7 @@ where #[inline] fn execute(self, cx: AssertionContext, subject: R) -> Self::Output { let Some(subject) = subject.ok() else { - return cx.fail("subject is Err"); + return cx.fail("received Err"); }; self.next.execute(cx, subject) } diff --git a/src/assertions/strings/assertions/to_match_regex.rs b/src/assertions/strings/assertions/to_match_regex.rs index 2560752..b0ada0b 100644 --- a/src/assertions/strings/assertions/to_match_regex.rs +++ b/src/assertions/strings/assertions/to_match_regex.rs @@ -49,7 +49,7 @@ where cx.pass_if( self.regex.is_match(subject.as_ref()), - "subject didn't match pattern", + "didn't match pattern", ) } } From 3b0a795ca7789c1e1c9b7f49eb5810869438fed6 Mon Sep 17 00:00:00 2001 From: TehPers Date: Fri, 9 Aug 2024 00:36:41 -0700 Subject: [PATCH 04/37] Add some assertions/modifiers, clean type params --- Cargo.lock | 12 -- Cargo.toml | 7 +- README.md | 22 +++ src/assertions.rs | 6 +- src/assertions/context.rs | 21 ++- src/assertions/error.rs | 19 ++- src/assertions/functions.rs | 0 src/assertions/futures.rs | 9 ++ src/assertions/futures/modifiers.rs | 2 + .../futures/modifiers/completion_order.rs | 153 ++++++++++++++++++ .../futures/modifiers/when_ready.rs | 34 ++-- src/assertions/futures/outputs.rs | 4 + .../futures/outputs/completion_order.rs | 101 ++++++++++++ src/assertions/futures/outputs/initialized.rs | 124 ++++++++++++++ src/assertions/futures/outputs/inverted.rs | 18 ++- src/assertions/futures/outputs/merged.rs | 14 ++ src/assertions/futures/outputs/unwrapped.rs | 11 ++ src/assertions/futures/outputs/when_ready.rs | 2 + src/assertions/general/modifiers/map.rs | 28 +--- src/assertions/general/modifiers/not.rs | 17 +- src/assertions/general/outputs.rs | 2 + .../general/outputs/initializable.rs | 70 ++++++++ src/assertions/general/outputs/invert.rs | 19 --- src/assertions/general/outputs/unwrap.rs | 18 --- src/assertions/iterators/modifiers/count.rs | 17 +- src/assertions/iterators/modifiers/merge.rs | 22 +-- src/assertions/iterators/modifiers/nth.rs | 44 ++--- src/assertions/iterators/outputs/merge.rs | 21 --- src/assertions/options/modifiers/some_and.rs | 43 +++-- src/assertions/results/modifiers/err_and.rs | 45 +++--- src/assertions/results/modifiers/ok_and.rs | 44 ++--- src/assertions/strings.rs | 2 + src/assertions/strings/modifiers/debug.rs | 17 +- src/assertions/strings/modifiers/display.rs | 20 +-- src/lib.rs | 63 +++++++- src/macros.rs | 24 +-- src/prelude.rs | 2 +- 37 files changed, 775 insertions(+), 302 deletions(-) create mode 100644 README.md delete mode 100644 src/assertions/functions.rs create mode 100644 src/assertions/futures/modifiers/completion_order.rs create mode 100644 src/assertions/futures/outputs/completion_order.rs create mode 100644 src/assertions/futures/outputs/initialized.rs create mode 100644 src/assertions/general/outputs/initializable.rs diff --git a/Cargo.lock b/Cargo.lock index 5e3dc4c..a2a97aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -79,7 +79,6 @@ checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", - "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -102,17 +101,6 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" -[[package]] -name = "futures-executor" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - [[package]] name = "futures-io" version = "0.3.30" diff --git a/Cargo.toml b/Cargo.toml index 261c64a..374effe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,8 +13,8 @@ keywords = ["assert", "assertions", "async", "matchers", "testing"] [features] default = ["colors", "futures", "regex"] -futures = ["dep:futures", "dep:pin-project-lite"] colors = ["dep:owo-colors"] +futures = ["dep:futures", "dep:pin-project-lite"] regex = ["dep:regex"] [dependencies] @@ -29,10 +29,5 @@ pin-project-lite = { version = "0.2.14", optional = true } regex = { version = "1.10.6", optional = true } [dev-dependencies] -futures = { version = "0.3.30", default-features = false, features = [ - "std", - "async-await", - "executor", -] } test-case = "3.3.1" tokio = { version = "1", features = ["macros", "test-util"] } diff --git a/README.md b/README.md new file mode 100644 index 0000000..ea64492 --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# 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") +} +``` diff --git a/src/assertions.rs b/src/assertions.rs index 6b29c79..9bb3295 100644 --- a/src/assertions.rs +++ b/src/assertions.rs @@ -1,7 +1,5 @@ -//! Assertions and modifiers that are used with [`expect!`], as well as any -//! types used to drive them. -//! -//! [`expect!`]: crate::expect! +//! Assertions and modifiers that are used with [`expect!`](crate::expect!), as +//! well as any types used to drive them. // pub mod functions; #[cfg(feature = "futures")] diff --git a/src/assertions/context.rs b/src/assertions/context.rs index 24bebd4..4601de6 100644 --- a/src/assertions/context.rs +++ b/src/assertions/context.rs @@ -1,6 +1,6 @@ use crate::metadata::{Annotated, AnnotatedKind, SourceLoc}; -use super::AssertionResult; +use super::general::InitializableOutput; /// Context that is passed through an assertion to track the full execution flow /// that occurred. @@ -102,8 +102,11 @@ impl AssertionContext { /// Creates a new success value. #[inline] - pub fn pass(self) -> AssertionResult { - AssertionResult::new(self, None) + pub fn pass(self) -> O + where + O: InitializableOutput, + { + O::pass(self) } /// Creates a new success value based on a condition. Otherwise, create a @@ -112,7 +115,10 @@ impl AssertionContext { /// 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) -> AssertionResult { + pub fn pass_if(self, pass: bool, failure_message: impl ToString) -> O + where + O: InitializableOutput, + { if pass { self.pass() } else { @@ -129,8 +135,11 @@ impl AssertionContext { /// 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) -> AssertionResult { - AssertionResult::new(self, Some(message.to_string())) + pub fn fail(self, message: impl ToString) -> O + where + O: InitializableOutput, + { + O::fail(self, message.to_string()) } /// Recovers missing frames from another context. diff --git a/src/assertions/error.rs b/src/assertions/error.rs index 8675163..376a9be 100644 --- a/src/assertions/error.rs +++ b/src/assertions/error.rs @@ -45,7 +45,7 @@ impl AssertionResult { /// Sets the state of this output to a pass. This overrides the context of /// the result. #[inline] - pub fn set_pass(&mut self, mut new_cx: AssertionContext) { + 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 @@ -56,7 +56,7 @@ impl AssertionResult { /// Sets the state of this output to a failure with the given message. #[inline] #[allow(clippy::needless_pass_by_value)] - pub fn set_fail(&mut self, mut new_cx: AssertionContext, message: impl ToString) { + 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 @@ -65,9 +65,8 @@ impl AssertionResult { } /// Converts this output into a [`Result`]. - // TODO: this should be called when finalizing? #[inline] - pub(crate) fn into_result(self) -> Result<(), AssertionError> { + pub fn into_result(self) -> Result<(), AssertionError> { match self.error { Some(message) => Err(AssertionError::new(self.cx, message)), None => Ok(()), @@ -135,11 +134,7 @@ impl Debug for AssertionError { let mut idx = 0; for frame in self.cx.visited.iter() { let comment = if idx == self.cx.visited.len() - 1 { - format!( - " {}", - self.message - .if_supports_color(Stream::Stderr, |text| text.bright_red().to_string()) - ) + format!(" {}", colored(&self.message, |text| text.bright_red())) } else { String::new() }; @@ -155,7 +150,11 @@ impl Debug for AssertionError { // Write non-visited frames for frame in &self.cx.remaining[self.cx.recovered.len()..] { - writeln!(f, " {frame}: (not visited)")?; + writeln!( + f, + " {frame}: {}", + colored(&"(not visited)", |text| text.dimmed()) + )?; idx += 1; } diff --git a/src/assertions/functions.rs b/src/assertions/functions.rs deleted file mode 100644 index e69de29..0000000 diff --git a/src/assertions/futures.rs b/src/assertions/futures.rs index 9ee6a29..45ad018 100644 --- a/src/assertions/futures.rs +++ b/src/assertions/futures.rs @@ -6,6 +6,15 @@ //! //! 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 modifiers; mod outputs; diff --git a/src/assertions/futures/modifiers.rs b/src/assertions/futures/modifiers.rs index 6474b31..1161690 100644 --- a/src/assertions/futures/modifiers.rs +++ b/src/assertions/futures/modifiers.rs @@ -1,3 +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..8430bcf --- /dev/null +++ b/src/assertions/futures/modifiers/completion_order.rs @@ -0,0 +1,153 @@ +use std::future::Future; + +use crate::{ + assertions::{ + futures::{CompletionOrder, CompletionOrderFuture}, + key, Assertion, AssertionContext, AssertionModifier, SubjectKey, + }, + metadata::Annotated, +}; + +/// 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 core::future::{pending, ready}; +/// # #[tokio::main(flavor = "current_thread")] +/// # async fn main() { +/// expect!(ready(1), when_ready_before(pending::<()>()), to_equal(1)).await; +/// expect!(ready(1), when_ready_before(ready(())), to_equal(1)).await; +/// # } +/// ``` +/// +/// The assertion fails if the provided future completes first: +/// +/// ```should_panic +/// # use expecters::prelude::*; +/// use core::future::{pending, ready}; +/// # #[tokio::main(flavor = "current_thread")] +/// # async fn main() { +/// expect!(pending::<()>(), when_ready_before(ready(1)), to_equal(())).await; +/// # } +/// ``` +pub fn when_ready_before( + fut: Annotated, +) -> impl FnOnce(M, SubjectKey) -> (CompletionOrderModifier, SubjectKey) +where + Fut: Future, + T: Future, +{ + move |prev, _| { + ( + CompletionOrderModifier { + prev, + fut, + order: CompletionOrder::Before, + }, + key(), + ) + } +} + +/// 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 core::{future::ready, time::Duration}; +/// use tokio::time::sleep; +/// # #[tokio::main(flavor = "current_thread")] +/// # async fn main() { +/// let fut = async { +/// sleep(Duration::from_secs(1)).await; +/// 1 +/// }; +/// expect!(fut, when_ready_after(ready(())), to_equal(1)).await; +/// expect!(ready(1), when_ready_after(ready(())), to_equal(1)).await; +/// # } +/// ``` +/// +/// The assertion fails if the provided future completes first: +/// +/// ```should_panic +/// # use expecters::prelude::*; +/// use core::future::{pending, ready}; +/// # #[tokio::main(flavor = "current_thread")] +/// # async fn main() { +/// expect!(ready(1), when_ready_after(pending::<()>()), to_equal(1)).await; +/// # } +/// ``` +pub fn when_ready_after( + fut: Annotated, +) -> impl FnOnce(M, SubjectKey) -> (CompletionOrderModifier, SubjectKey) +where + Fut: Future, + T: Future, +{ + move |prev, _| { + ( + CompletionOrderModifier { + prev, + fut, + order: CompletionOrder::After, + }, + key(), + ) + } +} + +/// Modifier for [`when_ready_before()`] and [`when_ready_after()`]. +#[derive(Clone, Debug)] +pub struct CompletionOrderModifier { + prev: M, + fut: Annotated, + order: CompletionOrder, +} + +impl AssertionModifier for CompletionOrderModifier +where + M: AssertionModifier>, +{ + type Output = M::Output; + + #[inline] + fn apply(self, next: A) -> Self::Output { + self.prev.apply(CompletionOrderAssertion { + next, + fut: self.fut, + order: self.order, + }) + } +} + +/// Assertion for [`when_ready_before()`] and [`when_ready_after()`]. +#[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 index 47d8060..c37ec81 100644 --- a/src/assertions/futures/modifiers/when_ready.rs +++ b/src/assertions/futures/modifiers/when_ready.rs @@ -1,4 +1,4 @@ -use std::{future::Future, marker::PhantomData}; +use std::future::Future; use crate::assertions::{ futures::WhenReadyFuture, key, Assertion, AssertionContext, AssertionModifier, SubjectKey, @@ -13,9 +13,10 @@ use crate::assertions::{ /// ``` /// # use expecters::prelude::*; /// use core::future::ready; -/// # futures::executor::block_on(async { +/// # #[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 @@ -24,7 +25,8 @@ use crate::assertions::{ /// ``` /// # use expecters::prelude::*; /// use core::future::ready; -/// # futures::executor::block_on(async { +/// # #[tokio::main(flavor = "current_thread")] +/// # async fn main() { /// expect!( /// ready(ready(1)), /// when_ready, // outer future @@ -33,33 +35,23 @@ use crate::assertions::{ /// ) /// .await /// .await; -/// # }) +/// # } /// ``` #[inline] -pub fn when_ready( - prev: M, - _: SubjectKey, -) -> (WhenReadyModifier, SubjectKey) +pub fn when_ready(prev: M, _: SubjectKey) -> (WhenReadyModifier, SubjectKey) where T: Future, { - ( - WhenReadyModifier { - prev, - marker: PhantomData, - }, - key(), - ) + (WhenReadyModifier { prev }, key()) } -/// Modifier for [`when_ready()`]. +/// Modifier for [`when_ready`]. #[derive(Clone, Debug)] -pub struct WhenReadyModifier { +pub struct WhenReadyModifier { prev: M, - marker: PhantomData, } -impl AssertionModifier for WhenReadyModifier +impl AssertionModifier for WhenReadyModifier where M: AssertionModifier>, { @@ -70,7 +62,7 @@ where } } -/// Assertion for [`when_ready()`]. +/// Assertion for [`when_ready`]. #[derive(Clone, Debug)] pub struct WhenReadyAssertion { next: A, diff --git a/src/assertions/futures/outputs.rs b/src/assertions/futures/outputs.rs index d46d4eb..46cc743 100644 --- a/src/assertions/futures/outputs.rs +++ b/src/assertions/futures/outputs.rs @@ -1,8 +1,12 @@ +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::*; diff --git a/src/assertions/futures/outputs/completion_order.rs b/src/assertions/futures/outputs/completion_order.rs new file mode 100644 index 0000000..7cadc73 --- /dev/null +++ b/src/assertions/futures/outputs/completion_order.rs @@ -0,0 +1,101 @@ +use std::{ + future::Future, + pin::Pin, + task::{Context, Poll}, +}; + +use pin_project_lite::pin_project; + +use crate::{ + assertions::{Assertion, AssertionContext}, + AssertionResult, +}; + +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)] + 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 = AssertionResult; + + 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 + 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..a599025 --- /dev/null +++ b/src/assertions/futures/outputs/initialized.rs @@ -0,0 +1,124 @@ +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. + 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 index 4a78f35..1c6eb70 100644 --- a/src/assertions/futures/outputs/inverted.rs +++ b/src/assertions/futures/outputs/inverted.rs @@ -20,8 +20,7 @@ pin_project! { impl InvertedOutputFuture where - F: Future, - F::Output: InvertibleOutput, + F: Future, { /// Creates a new inverted output future. #[inline] @@ -35,8 +34,7 @@ where impl Future for InvertedOutputFuture where - F: Future, - F::Output: InvertibleOutput, + F: Future, { type Output = ::Inverted; @@ -47,3 +45,15 @@ where 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 index 4dba320..d5bf693 100644 --- a/src/assertions/futures/outputs/merged.rs +++ b/src/assertions/futures/outputs/merged.rs @@ -60,3 +60,17 @@ where Poll::Ready(MergeableOutput::merge(cx, *projected.strategy, outputs)) } } + +impl MergeableOutput for F +where + F: Future, +{ + type Merged = MergedOutputsFuture; + + 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 index 6a233f7..2b40c7a 100644 --- a/src/assertions/futures/outputs/unwrapped.rs +++ b/src/assertions/futures/outputs/unwrapped.rs @@ -41,3 +41,14 @@ where Poll::Ready(output.unwrap()) } } + +impl UnwrappableOutput for F +where + F: Future, +{ + type Unwrapped = UnwrappedOutputFuture; + + fn unwrap(self) -> Self::Unwrapped { + UnwrappedOutputFuture::new(self) + } +} diff --git a/src/assertions/futures/outputs/when_ready.rs b/src/assertions/futures/outputs/when_ready.rs index 1bc15ef..7fdb272 100644 --- a/src/assertions/futures/outputs/when_ready.rs +++ b/src/assertions/futures/outputs/when_ready.rs @@ -10,6 +10,8 @@ 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)] pub struct WhenReadyFuture where diff --git a/src/assertions/general/modifiers/map.rs b/src/assertions/general/modifiers/map.rs index 4a36097..1444ac3 100644 --- a/src/assertions/general/modifiers/map.rs +++ b/src/assertions/general/modifiers/map.rs @@ -1,5 +1,3 @@ -use std::marker::PhantomData; - use crate::{ assertions::{key, Assertion, AssertionContext, AssertionModifier, SubjectKey}, metadata::Annotated, @@ -31,33 +29,23 @@ use crate::{ #[inline] pub fn map( f: Annotated, -) -> impl FnOnce(M, SubjectKey) -> (MapModifier, SubjectKey) +) -> impl FnOnce(M, SubjectKey) -> (MapModifier, SubjectKey) where F: FnOnce(T) -> U, { - move |prev, _| { - ( - MapModifier { - prev, - map: f, - marker: PhantomData, - }, - key(), - ) - } + move |prev, _| (MapModifier { prev, map: f }, key()) } /// Modifier for [`map()`]. #[derive(Clone, Debug)] -pub struct MapModifier { +pub struct MapModifier { prev: M, map: Annotated, - marker: PhantomData U>, } -impl AssertionModifier for MapModifier +impl AssertionModifier for MapModifier where - M: AssertionModifier>, + M: AssertionModifier>, { type Output = M::Output; @@ -66,20 +54,18 @@ where self.prev.apply(MapAssertion { next, map: self.map, - marker: PhantomData, }) } } /// Assertion for [`map()`]. #[derive(Clone, Debug)] -pub struct MapAssertion { +pub struct MapAssertion { next: A, map: Annotated, - marker: PhantomData U>, } -impl Assertion for MapAssertion +impl Assertion for MapAssertion where A: Assertion, F: FnOnce(T) -> U, diff --git a/src/assertions/general/modifiers/not.rs b/src/assertions/general/modifiers/not.rs index 473a52d..1da10a1 100644 --- a/src/assertions/general/modifiers/not.rs +++ b/src/assertions/general/modifiers/not.rs @@ -1,5 +1,3 @@ -use std::marker::PhantomData; - use crate::assertions::{ general::InvertibleOutput, key, Assertion, AssertionContext, AssertionModifier, SubjectKey, }; @@ -21,24 +19,17 @@ use crate::assertions::{ /// expect!(1, not, to_equal(1)); /// ``` #[inline] -pub fn not(prev: M, _: SubjectKey) -> (NotModifier, SubjectKey) { - ( - NotModifier { - prev, - marker: PhantomData, - }, - key(), - ) +pub fn not(prev: M, _: SubjectKey) -> (NotModifier, SubjectKey) { + (NotModifier { prev }, key()) } /// Modifier for [`not()`]. #[derive(Clone, Debug)] -pub struct NotModifier { +pub struct NotModifier { prev: M, - marker: PhantomData, } -impl AssertionModifier for NotModifier +impl AssertionModifier for NotModifier where M: AssertionModifier>, { diff --git a/src/assertions/general/outputs.rs b/src/assertions/general/outputs.rs index 6f81baf..75e0793 100644 --- a/src/assertions/general/outputs.rs +++ b/src/assertions/general/outputs.rs @@ -1,5 +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..d1794b2 --- /dev/null +++ b/src/assertions/general/outputs/initializable.rs @@ -0,0 +1,70 @@ +use crate::{assertions::AssertionContext, AssertionResult}; + +/// 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`](crate::prelude::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. +pub trait InitializableOutput { + // /// 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; + + /// 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; + + // /// 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_initializable(self) -> Self::Initialized; +} + +impl InitializableOutput for AssertionResult { + #[inline] + fn pass(cx: AssertionContext) -> Self { + AssertionResult::new(cx, None) + } + + #[inline] + fn fail(cx: AssertionContext, message: String) -> Self { + AssertionResult::new(cx, Some(message)) + } +} + +/// An output type that can be converted into an +/// [initializable output type][initializable]. +/// +/// [initializable]: 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 AssertionResult { + 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 index 0e6784f..285c57a 100644 --- a/src/assertions/general/outputs/invert.rs +++ b/src/assertions/general/outputs/invert.rs @@ -1,5 +1,3 @@ -use std::future::Future; - use crate::{assertions::AssertionContext, AssertionResult}; /// An assertion result that can be inverted. @@ -35,20 +33,3 @@ impl InvertibleOutput for AssertionResult { self } } - -#[cfg(feature = "futures")] -const _: () = { - use crate::assertions::futures::InvertedOutputFuture; - - 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/general/outputs/unwrap.rs b/src/assertions/general/outputs/unwrap.rs index 6ab2e86..1244155 100644 --- a/src/assertions/general/outputs/unwrap.rs +++ b/src/assertions/general/outputs/unwrap.rs @@ -34,21 +34,3 @@ impl UnwrappableOutput for AssertionResult { } } } - -#[cfg(feature = "futures")] -const _: () = { - use std::future::Future; - - use crate::assertions::futures::UnwrappedOutputFuture; - - impl UnwrappableOutput for F - where - F: Future, - { - type Unwrapped = UnwrappedOutputFuture; - - fn unwrap(self) -> Self::Unwrapped { - UnwrappedOutputFuture::new(self) - } - } -}; diff --git a/src/assertions/iterators/modifiers/count.rs b/src/assertions/iterators/modifiers/count.rs index 56cc82f..6b6ec64 100644 --- a/src/assertions/iterators/modifiers/count.rs +++ b/src/assertions/iterators/modifiers/count.rs @@ -1,5 +1,3 @@ -use std::marker::PhantomData; - use crate::assertions::{key, Assertion, AssertionContext, AssertionModifier, SubjectKey}; /// Counts the length of the subject, and executes an assertion on the result. @@ -14,24 +12,17 @@ use crate::assertions::{key, Assertion, AssertionContext, AssertionModifier, Sub /// will not complete (unless it panics for another reason). See the iterator /// method for more information. #[inline] -pub fn count(prev: M, _: SubjectKey) -> (CountModifier, SubjectKey) { - ( - CountModifier { - prev, - marker: PhantomData, - }, - key(), - ) +pub fn count(prev: M, _: SubjectKey) -> (CountModifier, SubjectKey) { + (CountModifier { prev }, key()) } /// Modifier for [`count()`]. #[derive(Clone, Debug)] -pub struct CountModifier { +pub struct CountModifier { prev: M, - marker: PhantomData, } -impl AssertionModifier for CountModifier +impl AssertionModifier for CountModifier where M: AssertionModifier>, { diff --git a/src/assertions/iterators/modifiers/merge.rs b/src/assertions/iterators/modifiers/merge.rs index f7a92c4..e15d971 100644 --- a/src/assertions/iterators/modifiers/merge.rs +++ b/src/assertions/iterators/modifiers/merge.rs @@ -1,5 +1,3 @@ -use std::marker::PhantomData; - use crate::assertions::{ iterators::{MergeStrategy, MergeableOutput}, key, Assertion, AssertionContext, AssertionModifier, SubjectKey, @@ -20,8 +18,17 @@ use crate::assertions::{ /// # use expecters::prelude::*; /// expect!([1, 3, 5], all, to_equal(5)); /// ``` +/// +/// Requires that the rest of the assertion is [`Clone`]. For example, comparing +/// each item to a non-cloneable value will not compile: +/// +/// ```compile_fail +/// # use expecters::prelude::*; +/// struct NotClone(i32); +/// expect!([NotClone(0)], all, map(|NotClone(x)| x), to_equal(0)); +/// ``` #[inline] -pub fn all(prev: M, _: SubjectKey) -> (MergeModifier, SubjectKey) +pub fn all(prev: M, _: SubjectKey) -> (MergeModifier, SubjectKey) where T: IntoIterator, { @@ -29,7 +36,6 @@ where MergeModifier { prev, strategy: MergeStrategy::All, - marker: PhantomData, }, key(), ) @@ -51,7 +57,7 @@ where /// expect!([1, 3, 5], any, to_equal(4)); /// ``` #[inline] -pub fn any(prev: M, _: SubjectKey) -> (MergeModifier, SubjectKey) +pub fn any(prev: M, _: SubjectKey) -> (MergeModifier, SubjectKey) where T: IntoIterator, { @@ -59,7 +65,6 @@ where MergeModifier { prev, strategy: MergeStrategy::Any, - marker: PhantomData, }, key(), ) @@ -67,13 +72,12 @@ where /// Modifier for [`all()`] and [`any()`]. #[derive(Clone, Debug)] -pub struct MergeModifier { +pub struct MergeModifier { prev: M, strategy: MergeStrategy, - marker: PhantomData, } -impl AssertionModifier for MergeModifier +impl AssertionModifier for MergeModifier where M: AssertionModifier>, { diff --git a/src/assertions/iterators/modifiers/nth.rs b/src/assertions/iterators/modifiers/nth.rs index 024f133..562575d 100644 --- a/src/assertions/iterators/modifiers/nth.rs +++ b/src/assertions/iterators/modifiers/nth.rs @@ -1,9 +1,9 @@ -use std::marker::PhantomData; - use crate::{ - assertions::{key, Assertion, AssertionContext, AssertionModifier, SubjectKey}, + assertions::{ + general::IntoInitializableOutput, key, Assertion, AssertionContext, AssertionModifier, + SubjectKey, + }, metadata::Annotated, - AssertionResult, }; /// Applies an assertion to a specific element in the target. If the element @@ -31,31 +31,21 @@ use crate::{ #[inline] pub fn nth( index: Annotated, -) -> impl FnOnce(M, SubjectKey) -> (NthModifier, SubjectKey) +) -> impl FnOnce(M, SubjectKey) -> (NthModifier, SubjectKey) where T: IntoIterator, { - move |prev, _| { - ( - NthModifier { - prev, - index, - marker: PhantomData, - }, - key(), - ) - } + move |prev, _| (NthModifier { prev, index }, key()) } /// Modifier for [`nth()`]. #[derive(Clone, Debug)] -pub struct NthModifier { +pub struct NthModifier { prev: M, index: Annotated, - marker: PhantomData, } -impl AssertionModifier for NthModifier +impl AssertionModifier for NthModifier where M: AssertionModifier>, { @@ -79,10 +69,10 @@ pub struct NthAssertion { impl Assertion for NthAssertion where - A: Assertion, + A: Assertion, T: IntoIterator, { - type Output = AssertionResult; + type Output = ::Initialized; #[inline] fn execute(self, mut cx: AssertionContext, subject: T) -> Self::Output { @@ -92,6 +82,18 @@ where let Some(subject) = subject.into_iter().nth(index) else { return cx.fail("index out of bounds"); }; - self.next.execute(cx, subject) + 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/merge.rs b/src/assertions/iterators/outputs/merge.rs index 8720f61..946f39b 100644 --- a/src/assertions/iterators/outputs/merge.rs +++ b/src/assertions/iterators/outputs/merge.rs @@ -48,27 +48,6 @@ impl MergeableOutput for AssertionResult { } } -#[cfg(feature = "futures")] -const _: () = { - use std::future::Future; - - use crate::assertions::futures::MergedOutputsFuture; - - impl MergeableOutput for F - where - F: Future, - { - type Merged = MergedOutputsFuture; - - fn merge(cx: AssertionContext, strategy: MergeStrategy, outputs: I) -> Self::Merged - where - I: IntoIterator, - { - MergedOutputsFuture::new(cx, strategy, outputs) - } - } -}; - /// A strategy for merging outputs. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum MergeStrategy { diff --git a/src/assertions/options/modifiers/some_and.rs b/src/assertions/options/modifiers/some_and.rs index 13b1de8..6312d3b 100644 --- a/src/assertions/options/modifiers/some_and.rs +++ b/src/assertions/options/modifiers/some_and.rs @@ -1,10 +1,6 @@ -use std::marker::PhantomData; - -use crate::{ - assertions::{ - key, options::Optionish, Assertion, AssertionContext, AssertionModifier, SubjectKey, - }, - AssertionResult, +use crate::assertions::{ + general::IntoInitializableOutput, key, options::Optionish, Assertion, AssertionContext, + AssertionModifier, SubjectKey, }; /// Asserts that the subject holds a value, then continues the assertion with @@ -22,30 +18,20 @@ use crate::{ /// expect!(None::, to_be_some_and, to_equal(2)); /// ``` #[inline] -pub fn to_be_some_and( - prev: M, - _: SubjectKey, -) -> (SomeAndModifier, SubjectKey) +pub fn to_be_some_and(prev: M, _: SubjectKey) -> (SomeAndModifier, SubjectKey) where O: Optionish, { - ( - SomeAndModifier { - prev, - marker: PhantomData, - }, - key(), - ) + (SomeAndModifier { prev }, key()) } /// Modifier for [`to_be_some_and()`]. #[derive(Clone, Debug)] -pub struct SomeAndModifier { +pub struct SomeAndModifier { prev: M, - marker: PhantomData, } -impl AssertionModifier for SomeAndModifier +impl AssertionModifier for SomeAndModifier where M: AssertionModifier>, { @@ -65,17 +51,17 @@ pub struct SomeAndAssertion { impl Assertion for SomeAndAssertion where - A: Assertion, + A: Assertion, O: Optionish, { - type Output = AssertionResult; + 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) + self.next.execute(cx, subject).into_initialized() } } @@ -95,4 +81,13 @@ mod tests { 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/results/modifiers/err_and.rs b/src/assertions/results/modifiers/err_and.rs index 0e91316..8089a33 100644 --- a/src/assertions/results/modifiers/err_and.rs +++ b/src/assertions/results/modifiers/err_and.rs @@ -1,10 +1,6 @@ -use std::marker::PhantomData; - -use crate::{ - assertions::{ - key, results::Resultish, Assertion, AssertionContext, AssertionModifier, SubjectKey, - }, - AssertionResult, +use crate::assertions::{ + general::IntoInitializableOutput, key, results::Resultish, Assertion, AssertionContext, + AssertionModifier, SubjectKey, }; /// Asserts that the target holds an error, then continues the assertion with @@ -24,27 +20,20 @@ use crate::{ /// expect!(result, to_be_err_and, to_equal("error")); /// ``` #[inline] -pub fn to_be_err_and(prev: M, _: SubjectKey) -> (ErrAndModifier, SubjectKey) +pub fn to_be_err_and(prev: M, _: SubjectKey) -> (ErrAndModifier, SubjectKey) where R: Resultish, { - ( - ErrAndModifier { - prev, - marker: PhantomData, - }, - key(), - ) + (ErrAndModifier { prev }, key()) } /// Modifier for [`to_be_err_and()`]. #[derive(Clone, Debug)] -pub struct ErrAndModifier { +pub struct ErrAndModifier { prev: M, - marker: PhantomData, } -impl AssertionModifier for ErrAndModifier +impl AssertionModifier for ErrAndModifier where M: AssertionModifier>, { @@ -64,17 +53,17 @@ pub struct ErrAndAssertion { impl Assertion for ErrAndAssertion where - A: Assertion, + A: Assertion, R: Resultish, { - type Output = AssertionResult; + 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) + self.next.execute(cx, subject).into_initialized() } } @@ -95,3 +84,17 @@ mod tests { 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 index da2fa49..92c9284 100644 --- a/src/assertions/results/modifiers/ok_and.rs +++ b/src/assertions/results/modifiers/ok_and.rs @@ -1,10 +1,6 @@ -use std::marker::PhantomData; - -use crate::{ - assertions::{ - key, results::Resultish, Assertion, AssertionContext, AssertionModifier, SubjectKey, - }, - AssertionResult, +use crate::assertions::{ + general::IntoInitializableOutput, key, results::Resultish, Assertion, AssertionContext, + AssertionModifier, SubjectKey, }; /// Asserts that the target holds a success, then continues the assertion with @@ -24,27 +20,20 @@ use crate::{ /// expect!(subject, to_be_ok_and, to_equal(1)); /// ``` #[inline] -pub fn to_be_ok_and(prev: M, _: SubjectKey) -> (OkAndModifier, SubjectKey) +pub fn to_be_ok_and(prev: M, _: SubjectKey) -> (OkAndModifier, SubjectKey) where R: Resultish, { - ( - OkAndModifier { - prev, - marker: PhantomData, - }, - key(), - ) + (OkAndModifier { prev }, key()) } /// Modifier for [`to_be_ok_and()`]. #[derive(Clone, Debug)] -pub struct OkAndModifier { +pub struct OkAndModifier { prev: M, - marker: PhantomData, } -impl AssertionModifier for OkAndModifier +impl AssertionModifier for OkAndModifier where M: AssertionModifier>, { @@ -64,17 +53,17 @@ pub struct OkAndAssertion { impl Assertion for OkAndAssertion where - A: Assertion, + A: Assertion, R: Resultish, { - type Output = AssertionResult; + 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) + self.next.execute(cx, subject).into_initialized() } } @@ -95,3 +84,16 @@ mod tests { 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/strings.rs b/src/assertions/strings.rs index db38415..f76b7ab 100644 --- a/src/assertions/strings.rs +++ b/src/assertions/strings.rs @@ -1,3 +1,5 @@ +//! Assertions and modifiers for tests that involve strings. + mod assertions; mod modifiers; diff --git a/src/assertions/strings/modifiers/debug.rs b/src/assertions/strings/modifiers/debug.rs index 40c29f0..fbe2256 100644 --- a/src/assertions/strings/modifiers/debug.rs +++ b/src/assertions/strings/modifiers/debug.rs @@ -1,4 +1,4 @@ -use std::{fmt::Debug, marker::PhantomData}; +use std::fmt::Debug; use crate::assertions::{key, Assertion, AssertionContext, AssertionModifier, SubjectKey}; @@ -9,27 +9,20 @@ use crate::assertions::{key, Assertion, AssertionContext, AssertionModifier, Sub /// expect!("hello", as_debug, to_equal(r#""hello""#)); /// ``` #[inline] -pub fn as_debug(prev: M, _: SubjectKey) -> (AsDebugModifier, SubjectKey) +pub fn as_debug(prev: M, _: SubjectKey) -> (AsDebugModifier, SubjectKey) where T: Debug, { - ( - AsDebugModifier { - prev, - marker: PhantomData, - }, - key(), - ) + (AsDebugModifier { prev }, key()) } /// Modifier for [`as_debug()`]. #[derive(Clone, Debug)] -pub struct AsDebugModifier { +pub struct AsDebugModifier { prev: M, - marker: PhantomData, } -impl AssertionModifier for AsDebugModifier +impl AssertionModifier for AsDebugModifier where M: AssertionModifier>, { diff --git a/src/assertions/strings/modifiers/display.rs b/src/assertions/strings/modifiers/display.rs index fd427c2..68e4c66 100644 --- a/src/assertions/strings/modifiers/display.rs +++ b/src/assertions/strings/modifiers/display.rs @@ -1,7 +1,4 @@ -use std::{ - fmt::{Debug, Display}, - marker::PhantomData, -}; +use std::fmt::Display; use crate::assertions::{key, Assertion, AssertionContext, AssertionModifier, SubjectKey}; @@ -12,27 +9,20 @@ use crate::assertions::{key, Assertion, AssertionContext, AssertionModifier, Sub /// expect!(1, as_display, to_equal("1")); /// ``` #[inline] -pub fn as_display(prev: M, _: SubjectKey) -> (AsDisplayModifier, SubjectKey) +pub fn as_display(prev: M, _: SubjectKey) -> (AsDisplayModifier, SubjectKey) where T: Display, { - ( - AsDisplayModifier { - prev, - marker: PhantomData, - }, - key(), - ) + (AsDisplayModifier { prev }, key()) } /// Modifier for [`as_display()`]. #[derive(Clone, Debug)] -pub struct AsDisplayModifier { +pub struct AsDisplayModifier { prev: M, - marker: PhantomData, } -impl AssertionModifier for AsDisplayModifier +impl AssertionModifier for AsDisplayModifier where M: AssertionModifier>, { diff --git a/src/lib.rs b/src/lib.rs index ae122bf..a6c7b9e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,65 @@ -//! TODO +//! Build complex, self-describing assertions by chaining together reusable methods. +//! Supports both synchronous and asynchronous assertions. +//! +//! ## Example +//! +//! ``` +//! use expecters::prelude::*; +//! +//! # #[tokio::main(flavor = "current_thread")] +//! # async fn main() { +//! 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") +//! } +//! ``` +//! +//! 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!([1, 2, 3], all, to_satisfy(|n| n % 2 == 1)); +//! ``` +//! +//! This produces an error like the following: +//! +//! ```text +//! assertion failed: +//! at: src\lib.rs:42:8 [your_lib::tests] +//! subject: [1, 2, 3] +//! +//! steps: +//! all: +//! received: [1, 2, 3] +//! index: 1 +//! +//! to_satisfy: did not satisfy predicate +//! received: 2 +//! predicate: |n| n % 2 == 1 +//! ``` +//! +//! ## 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`. +//! +//! See the [`expect!`] macro's documentation for usage information. For a full +//! list of modifiers and assertions, look at the [`prelude`] module. #![warn( missing_debug_implementations, diff --git a/src/macros.rs b/src/macros.rs index 49365a7..5c2be17 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -17,7 +17,7 @@ /// /// ## Syntax /// -/// This macro is called similar to how a function is called. For example: +/// This macro is called like a function. For example: /// /// ``` /// # use expecters::prelude::*; @@ -88,10 +88,9 @@ /// /// > *Note: requires crate feature `futures`.* /// -/// Async assertions function similar to sync assertions. -/// -/// TODO - maybe doc this in the `futures` module so it's cfg-locked and can use -/// doc tests +/// Async assertions function similar to sync assertions, but need to be +/// `.await`ed. For more information, see the +/// [`futures`](crate::assertions::futures) module. /// /// ## Annotations /// @@ -172,8 +171,14 @@ macro_rules! expect { /// /// 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 an -/// [`AssertionResult`](crate::AssertionResult) instead. +/// 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.into_result(), to_be_err); +/// ``` /// /// See [`expect!`] for more information on how to use this macro. #[macro_export] @@ -315,7 +320,7 @@ mod tests { struct NotDebug(T); #[tokio::test] - // #[ignore] + #[ignore] async fn test_debugging() { debugging().await; } @@ -324,6 +329,7 @@ mod tests { // expect!(ready(1), when_ready, to_equal(2)).await; // expect!([1, 2, 3], count, not, to_equal(3)); // expect!([1, 2, 3], any, to_equal(4)); + expect!([1, 2, 3], all, to_satisfy(|n| n % 2 == 1)); expect!("blah", to_match_regex(r"\d+")); @@ -339,7 +345,7 @@ mod tests { any, not, map(|NotDebug(x)| x), - to_be_less_than(4) + to_be_less_than(4), ) .await; diff --git a/src/prelude.rs b/src/prelude.rs index 8d0b53d..e3b6385 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -26,7 +26,7 @@ pub use crate::{ }; #[cfg(feature = "futures")] -pub use crate::assertions::futures::when_ready; +pub use crate::assertions::futures::{when_ready, when_ready_after, when_ready_before}; #[cfg(feature = "regex")] pub use crate::assertions::strings::to_match_regex; From 4b4f474697ed64e817a72811aa157c43788bb061 Mon Sep 17 00:00:00 2001 From: TehPers Date: Fri, 9 Aug 2024 14:48:27 -0700 Subject: [PATCH 05/37] Improve panic messages by changing source of panic --- src/assertions/futures/outputs/unwrapped.rs | 2 ++ src/assertions/general/outputs/unwrap.rs | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/src/assertions/futures/outputs/unwrapped.rs b/src/assertions/futures/outputs/unwrapped.rs index 2b40c7a..827bfae 100644 --- a/src/assertions/futures/outputs/unwrapped.rs +++ b/src/assertions/futures/outputs/unwrapped.rs @@ -35,6 +35,7 @@ where 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)); @@ -48,6 +49,7 @@ where { type Unwrapped = UnwrappedOutputFuture; + #[inline] fn unwrap(self) -> Self::Unwrapped { UnwrappedOutputFuture::new(self) } diff --git a/src/assertions/general/outputs/unwrap.rs b/src/assertions/general/outputs/unwrap.rs index 1244155..a0af29d 100644 --- a/src/assertions/general/outputs/unwrap.rs +++ b/src/assertions/general/outputs/unwrap.rs @@ -21,6 +21,11 @@ pub trait UnwrappableOutput { /// /// 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; } @@ -28,6 +33,7 @@ impl UnwrappableOutput for AssertionResult { type Unwrapped = (); #[inline] + #[track_caller] fn unwrap(self) -> Self::Unwrapped { if let Err(e) = self.into_result() { panic!("{e:?}") From f44eb9d037f853bd027538b4b794c3b113101860 Mon Sep 17 00:00:00 2001 From: TehPers Date: Fri, 9 Aug 2024 14:48:47 -0700 Subject: [PATCH 06/37] Simplify __expect_inner --- src/macros.rs | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/macros.rs b/src/macros.rs index 5c2be17..7ea3adb 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -266,14 +266,9 @@ macro_rules! __expect_inner { $key:expr, $assertion:ident $(,)? - ) => {{ - let (chain, _key) = $crate::__expect_inner!(@annotate, $chain, $key); - let assertion = $assertion(); - $crate::assertions::AssertionModifier::apply( - chain, - assertion, - ) - }}; + ) => { + $crate::__expect_inner!(@build_assertion, $chain, $key, $assertion()) + }; ( // Recursive case (with params) @build_assertion, @@ -282,10 +277,16 @@ macro_rules! __expect_inner { $modifier:ident($($param:expr),* $(,)?), $($rest:tt)* ) => {{ - let (chain, _key) = $crate::__expect_inner!(@annotate, $chain, $key); - let modifier = $modifier($($crate::annotated!($param),)*); - let (chain, _key) = modifier(chain, _key); - $crate::__expect_inner!(@build_assertion, chain, _key, $($rest)*) + let modifier = $modifier( + $($crate::annotated!($param),)* + ); + $crate::__expect_inner!( + @build_assertion, + $chain, + $key, + modifier, + $($rest)* + ) }}; ( // Recursive case (without params) From 8eeadd61c4f555cabace2832fe09620cadacb02a Mon Sep 17 00:00:00 2001 From: TehPers Date: Fri, 9 Aug 2024 14:49:29 -0700 Subject: [PATCH 07/37] Add docs for creating custom assertions/modifiers --- src/assertions.rs | 152 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 150 insertions(+), 2 deletions(-) diff --git a/src/assertions.rs b/src/assertions.rs index 9bb3295..cb385a9 100644 --- a/src/assertions.rs +++ b/src/assertions.rs @@ -1,5 +1,153 @@ -//! Assertions and modifiers that are used with [`expect!`](crate::expect!), as -//! well as any types used to drive them. +//! 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. +//! +//! ## Creating an assertion +//! +//! The signature for assertions is simple. An assertion function, like +//! [`to_be_some`](crate::prelude::to_be_some), is a regular function that +//! returns a value implementing the [`Assertion`] trait. It acts as a +//! constructor for that type. For example, calling `to_be_some()` returns an +//! instance of the [`ToBeOptionVariantAssertion`] type configured to check if +//! the input it receives is of the [`Some`] variant. +//! +//! 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, AssertionContext}, +//! metadata::Annotated, +//! prelude::*, +//! AssertionResult, +//! }; +//! +//! // Input parameters are automatically annotated, so we need to wrap them +//! // with `Annotated` +//! pub fn to_be_zero(annotation: Annotated) -> ToBeZeroAssertion { +//! ToBeZeroAssertion(annotation) +//! } +//! +//! 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 = AssertionResult; +//! +//! fn execute(self, mut cx: AssertionContext, value: i32) -> Self::Output { +//! cx.annotate("my annotation", "this appears in failure messages"); +//! cx.annotate("input parameter", &self.0); +//! cx.pass_if(value == 0, "was not zero") +//! } +//! } +//! +//! 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. +//! +//! ``` +//! use expecters::{ +//! assertions::{ +//! Assertion, +//! AssertionContext, +//! AssertionModifier, +//! SubjectKey, +//! key, +//! } +//! prelude::*, +//! }; +//! +//! pub fn divided_by( +//! prev: M, // the modifier we're wrapping +//! _: SubjectKey, // the type we're expecting to receive in this step +//! divisor: Annotated, // our own custom parameter +//! ) -> ( +//! DividedByModifier, // our constructed modifier type, wrapping M +//! SubjectKey, // the type we're passing to the next step +//! ) { +//! (DividedByModifier(prev, divisor), key()) +//! } +//! +//! 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, next: A) -> Self::Output { +//! self.0.apply(DividedByAssertion(next, self.1)) +//! } +//! } +//! +//! 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()) +//! } +//! } +//! +//! expect!(4.0, divided_by(2.0), to_be_less_than(2.1)); +//! ``` +//! +//! [`ToBeOptionVariantAssertion`]: options::ToBeOptionVariantAssertion +//! [`expect!`]: crate::expect! +//! [`not`]: crate::prelude::not +//! [`to_equal`]: crate::prelude::to_equal // pub mod functions; #[cfg(feature = "futures")] From d6de998ebd02f8021663664415933c638d73805b Mon Sep 17 00:00:00 2001 From: TehPers Date: Sat, 10 Aug 2024 01:37:30 -0700 Subject: [PATCH 08/37] core -> std --- src/assertions/context.rs | 2 +- src/assertions/futures/modifiers/completion_order.rs | 8 ++++---- src/assertions/futures/modifiers/when_ready.rs | 4 ++-- src/macros.rs | 8 ++++---- src/metadata/annotated.rs | 4 ++-- src/metadata/source_loc.rs | 8 ++++---- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/assertions/context.rs b/src/assertions/context.rs index 4601de6..48bac6e 100644 --- a/src/assertions/context.rs +++ b/src/assertions/context.rs @@ -92,7 +92,7 @@ impl AssertionContext { /// value only has a meaningful string representation if the value /// implements [`Debug`]. /// - /// [`Debug`]: core::fmt::Debug + /// [`Debug`]: std::fmt::Debug #[inline] pub fn try_annotate(&mut self, key: &'static str, value: &Annotated) { if value.kind() == AnnotatedKind::Debug { diff --git a/src/assertions/futures/modifiers/completion_order.rs b/src/assertions/futures/modifiers/completion_order.rs index 8430bcf..593efd3 100644 --- a/src/assertions/futures/modifiers/completion_order.rs +++ b/src/assertions/futures/modifiers/completion_order.rs @@ -17,7 +17,7 @@ use crate::{ /// /// ``` /// # use expecters::prelude::*; -/// use core::future::{pending, ready}; +/// use std::future::{pending, ready}; /// # #[tokio::main(flavor = "current_thread")] /// # async fn main() { /// expect!(ready(1), when_ready_before(pending::<()>()), to_equal(1)).await; @@ -29,7 +29,7 @@ use crate::{ /// /// ```should_panic /// # use expecters::prelude::*; -/// use core::future::{pending, ready}; +/// use std::future::{pending, ready}; /// # #[tokio::main(flavor = "current_thread")] /// # async fn main() { /// expect!(pending::<()>(), when_ready_before(ready(1)), to_equal(())).await; @@ -63,7 +63,7 @@ where /// /// ``` /// # use expecters::prelude::*; -/// use core::{future::ready, time::Duration}; +/// use std::{future::ready, time::Duration}; /// use tokio::time::sleep; /// # #[tokio::main(flavor = "current_thread")] /// # async fn main() { @@ -80,7 +80,7 @@ where /// /// ```should_panic /// # use expecters::prelude::*; -/// use core::future::{pending, ready}; +/// use std::future::{pending, ready}; /// # #[tokio::main(flavor = "current_thread")] /// # async fn main() { /// expect!(ready(1), when_ready_after(pending::<()>()), to_equal(1)).await; diff --git a/src/assertions/futures/modifiers/when_ready.rs b/src/assertions/futures/modifiers/when_ready.rs index c37ec81..b6d53a4 100644 --- a/src/assertions/futures/modifiers/when_ready.rs +++ b/src/assertions/futures/modifiers/when_ready.rs @@ -12,7 +12,7 @@ use crate::assertions::{ /// /// ``` /// # use expecters::prelude::*; -/// use core::future::ready; +/// use std::future::ready; /// # #[tokio::main(flavor = "current_thread")] /// # async fn main() { /// expect!(ready(1), when_ready, to_equal(1)).await; @@ -24,7 +24,7 @@ use crate::assertions::{ /// /// ``` /// # use expecters::prelude::*; -/// use core::future::ready; +/// use std::future::ready; /// # #[tokio::main(flavor = "current_thread")] /// # async fn main() { /// expect!( diff --git a/src/macros.rs b/src/macros.rs index 7ea3adb..105e6d9 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -151,12 +151,12 @@ /// /// [`Annotated`]: crate::metadata::Annotated /// [`AnnotatedAssertion`]: crate::assertions::AnnotatedAssertion -/// [`Debug`]: core::fmt::Debug +/// [`Debug`]: std::fmt::Debug /// [`all`]: crate::prelude::all /// [`map`]: crate::prelude::map /// [`not`]: crate::prelude::not /// [`to_equal`]: crate::prelude::to_equal -/// [stringified]: core::stringify +/// [stringified]: std::stringify #[macro_export] macro_rules! expect { ($($tokens:tt)*) => { @@ -225,7 +225,7 @@ macro_rules! __expect_inner { ) => {{ const FRAMES: &'static [&'static str] = &[ $($frames,)* - ::core::stringify!($frame_name), + ::std::stringify!($frame_name), ]; FRAMES }}; @@ -238,7 +238,7 @@ macro_rules! __expect_inner { ) => { $crate::__expect_inner!( @build_ctx_frames, - [$($frames,)* ::core::stringify!($frame_name)], + [$($frames,)* ::std::stringify!($frame_name)], $($assertions)* ) }; diff --git a/src/metadata/annotated.rs b/src/metadata/annotated.rs index 6dc51ba..458ce4c 100644 --- a/src/metadata/annotated.rs +++ b/src/metadata/annotated.rs @@ -10,7 +10,7 @@ macro_rules! annotated { let wrapper = $crate::specialization::__SpecializeWrapper($value); (&wrapper) .__annotated_kind() - .annotate(wrapper.0, ::core::stringify!($value)) + .annotate(wrapper.0, ::std::stringify!($value)) }}; } @@ -20,7 +20,7 @@ macro_rules! annotated { /// representation is obtained in the following order of precedence: /// /// 1. the [`Debug`] representation, otherwise... -/// 2. the [stringified](core::stringify) source code (that was annotated). +/// 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 diff --git a/src/metadata/source_loc.rs b/src/metadata/source_loc.rs index 830b33c..c0cfcd4 100644 --- a/src/metadata/source_loc.rs +++ b/src/metadata/source_loc.rs @@ -5,10 +5,10 @@ use std::fmt::{Display, Formatter}; macro_rules! source_loc { () => {{ const SOURCE_LOC: $crate::metadata::SourceLoc = $crate::metadata::SourceLoc::new( - ::core::module_path!(), - ::core::file!(), - ::core::line!(), - ::core::column!(), + ::std::module_path!(), + ::std::file!(), + ::std::line!(), + ::std::column!(), ); &SOURCE_LOC }}; From eee9d897305f634b02371fca0a573cfcc6f62277 Mon Sep 17 00:00:00 2001 From: TehPers Date: Sat, 10 Aug 2024 01:38:15 -0700 Subject: [PATCH 09/37] Simplify modifier functions --- src/assertions.rs | 29 +++++++++++- .../futures/modifiers/completion_order.rs | 44 +++++++++---------- src/assertions/general/modifiers/annotate.rs | 2 +- src/assertions/general/modifiers/map.rs | 39 ++++++++++++++-- src/assertions/iterators/modifiers/nth.rs | 6 ++- src/macros.rs | 25 ++++++----- 6 files changed, 102 insertions(+), 43 deletions(-) diff --git a/src/assertions.rs b/src/assertions.rs index cb385a9..d32a939 100644 --- a/src/assertions.rs +++ b/src/assertions.rs @@ -5,6 +5,11 @@ //! 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)); +//! ``` +//! //! ## Creating an assertion //! //! The signature for assertions is simple. An assertion function, like @@ -32,6 +37,7 @@ //! ToBeZeroAssertion(annotation) //! } //! +//! #[derive(Clone, Debug)] //! pub struct ToBeZeroAssertion(Annotated); //! //! impl Assertion for ToBeZeroAssertion { @@ -93,6 +99,11 @@ //! 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. The function should take at minimum two required +//! inputs (but it may specify additional inputs), and should return a pair +//! containing the constructed modifier and a subject key. +//! //! ``` //! use expecters::{ //! assertions::{ @@ -101,10 +112,13 @@ //! AssertionModifier, //! SubjectKey, //! key, -//! } +//! }, +//! metadata::Annotated, //! prelude::*, //! }; //! +//! // The first two parameters are required, but you may specify any number of +//! // additional parameters to your modifier: //! pub fn divided_by( //! prev: M, // the modifier we're wrapping //! _: SubjectKey, // the type we're expecting to receive in this step @@ -116,8 +130,11 @@ //! (DividedByModifier(prev, divisor), key()) //! } //! +//! // This wraps the modifier chain (first pass, going from root -> assertion) +//! #[derive(Clone, Debug)] //! pub struct DividedByModifier(M, Annotated); -//! impl AssertionModifier for DividedByModifier +//! +//! impl AssertionModifier for DividedByModifier //! where //! M: AssertionModifier>, //! { @@ -128,7 +145,10 @@ //! } //! } //! +//! // This wraps the assertion chain (second pass, assertion -> root) +//! #[derive(Clone, Debug)] //! pub struct DividedByAssertion(A, Annotated); +//! //! impl Assertion for DividedByAssertion //! where //! A: Assertion @@ -144,6 +164,11 @@ //! 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. +//! //! [`ToBeOptionVariantAssertion`]: options::ToBeOptionVariantAssertion //! [`expect!`]: crate::expect! //! [`not`]: crate::prelude::not diff --git a/src/assertions/futures/modifiers/completion_order.rs b/src/assertions/futures/modifiers/completion_order.rs index 593efd3..b4d3e5c 100644 --- a/src/assertions/futures/modifiers/completion_order.rs +++ b/src/assertions/futures/modifiers/completion_order.rs @@ -36,22 +36,22 @@ use crate::{ /// # } /// ``` pub fn when_ready_before( + prev: M, + _: SubjectKey, fut: Annotated, -) -> impl FnOnce(M, SubjectKey) -> (CompletionOrderModifier, SubjectKey) +) -> (CompletionOrderModifier, SubjectKey) where Fut: Future, T: Future, { - move |prev, _| { - ( - CompletionOrderModifier { - prev, - fut, - order: CompletionOrder::Before, - }, - key(), - ) - } + ( + CompletionOrderModifier { + prev, + fut, + order: CompletionOrder::Before, + }, + key(), + ) } /// Executes an assertion on the output of a future, but only if it does not @@ -87,22 +87,22 @@ where /// # } /// ``` pub fn when_ready_after( + prev: M, + _: SubjectKey, fut: Annotated, -) -> impl FnOnce(M, SubjectKey) -> (CompletionOrderModifier, SubjectKey) +) -> (CompletionOrderModifier, SubjectKey) where Fut: Future, T: Future, { - move |prev, _| { - ( - CompletionOrderModifier { - prev, - fut, - order: CompletionOrder::After, - }, - key(), - ) - } + ( + CompletionOrderModifier { + prev, + fut, + order: CompletionOrder::After, + }, + key(), + ) } /// Modifier for [`when_ready_before()`] and [`when_ready_after()`]. diff --git a/src/assertions/general/modifiers/annotate.rs b/src/assertions/general/modifiers/annotate.rs index 3ff1430..b719dfa 100644 --- a/src/assertions/general/modifiers/annotate.rs +++ b/src/assertions/general/modifiers/annotate.rs @@ -5,8 +5,8 @@ use crate::{ #[doc(hidden)] pub fn __annotate( - _: SubjectKey, prev: M, + _: SubjectKey, annotate: fn(T) -> Annotated, ) -> (AnnotateModifier, SubjectKey) { (AnnotateModifier { prev, annotate }, key()) diff --git a/src/assertions/general/modifiers/map.rs b/src/assertions/general/modifiers/map.rs index 1444ac3..a274f1e 100644 --- a/src/assertions/general/modifiers/map.rs +++ b/src/assertions/general/modifiers/map.rs @@ -26,17 +26,48 @@ use crate::{ /// # 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! #[inline] pub fn map( + prev: M, + _: SubjectKey, f: Annotated, -) -> impl FnOnce(M, SubjectKey) -> (MapModifier, SubjectKey) +) -> (MapModifier, SubjectKey) where F: FnOnce(T) -> U, { - move |prev, _| (MapModifier { prev, map: f }, key()) + (MapModifier { prev, map: f }, key()) } -/// Modifier for [`map()`]. +/// Modifier for [`map`]. #[derive(Clone, Debug)] pub struct MapModifier { prev: M, @@ -58,7 +89,7 @@ where } } -/// Assertion for [`map()`]. +/// Assertion for [`map`]. #[derive(Clone, Debug)] pub struct MapAssertion { next: A, diff --git a/src/assertions/iterators/modifiers/nth.rs b/src/assertions/iterators/modifiers/nth.rs index 562575d..233ebfc 100644 --- a/src/assertions/iterators/modifiers/nth.rs +++ b/src/assertions/iterators/modifiers/nth.rs @@ -30,12 +30,14 @@ use crate::{ /// ``` #[inline] pub fn nth( + prev: M, + _: SubjectKey, index: Annotated, -) -> impl FnOnce(M, SubjectKey) -> (NthModifier, SubjectKey) +) -> (NthModifier, SubjectKey) where T: IntoIterator, { - move |prev, _| (NthModifier { prev, index }, key()) + (NthModifier { prev, index }, key()) } /// Modifier for [`nth()`]. diff --git a/src/macros.rs b/src/macros.rs index 105e6d9..c99ba70 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -277,16 +277,13 @@ macro_rules! __expect_inner { $modifier:ident($($param:expr),* $(,)?), $($rest:tt)* ) => {{ - let modifier = $modifier( + let (chain, key) = $crate::__expect_inner!(@annotate, $chain, $key); + let (chain, _key) = $modifier( + chain, + key, $($crate::annotated!($param),)* ); - $crate::__expect_inner!( - @build_assertion, - $chain, - $key, - modifier, - $($rest)* - ) + $crate::__expect_inner!(@build_assertion, chain, _key, $($rest)*) }}; ( // Recursive case (without params) @@ -296,16 +293,20 @@ macro_rules! __expect_inner { $modifier:ident, $($rest:tt)* ) => {{ - let (chain, _key) = $crate::__expect_inner!(@annotate, $chain, $key); - let (chain, _key) = $modifier(chain, _key); - $crate::__expect_inner!(@build_assertion, chain, _key, $($rest)*) + $crate::__expect_inner!( + @build_assertion, + $chain, + $key, + $modifier(), + $($rest)* + ) }}; // Annotate the value being passed down the chain (@annotate, $chain:expr, $key:expr) => { $crate::assertions::general::__annotate( - $key, $chain, + $key, |not_debug| $crate::annotated!(not_debug), ) }; From df517cb6e2ffbb389c86ff9d7c50df9cbd843982 Mon Sep 17 00:00:00 2001 From: TehPers Date: Sat, 10 Aug 2024 12:53:02 -0700 Subject: [PATCH 10/37] Add #[inline] --- src/assertions/futures/outputs/merged.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/assertions/futures/outputs/merged.rs b/src/assertions/futures/outputs/merged.rs index d5bf693..d67c3f6 100644 --- a/src/assertions/futures/outputs/merged.rs +++ b/src/assertions/futures/outputs/merged.rs @@ -67,6 +67,7 @@ where { type Merged = MergedOutputsFuture; + #[inline] fn merge(cx: AssertionContext, strategy: MergeStrategy, outputs: I) -> Self::Merged where I: IntoIterator, From 8ae6fd3ebad72ba81b2f39c2c67649b2ae304b1d Mon Sep 17 00:00:00 2001 From: TehPers Date: Sat, 10 Aug 2024 12:53:13 -0700 Subject: [PATCH 11/37] Test for short-circuiting --- src/assertions/iterators/modifiers/merge.rs | 115 ++++++++++++++++++++ src/assertions/iterators/outputs/merge.rs | 5 + 2 files changed, 120 insertions(+) diff --git a/src/assertions/iterators/modifiers/merge.rs b/src/assertions/iterators/modifiers/merge.rs index e15d971..5826d91 100644 --- a/src/assertions/iterators/modifiers/merge.rs +++ b/src/assertions/iterators/modifiers/merge.rs @@ -126,3 +126,118 @@ where MergeableOutput::merge(cx, self.strategy, outputs) } } + +#[cfg(test)] +mod tests { + use std::{iter::repeat, sync::mpsc::channel, thread::spawn, time::Duration}; + + 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] + fn test_any_short_circuit() { + let success = with_timeout(Duration::from_secs(1), || { + expect!(repeat(0), any, to_equal(0)); + }); + expect!(success, to_equal(true)); + } + + #[test] + fn test_any_infinite() { + let success = with_timeout(Duration::from_secs(1), || { + expect!(repeat(0), any, to_equal(1)); + }); + expect!(success, to_equal(false)); + } + + #[test] + fn test_all_short_circuit() { + let success = with_timeout(Duration::from_secs(1), || { + expect!(repeat(0), not, all, to_equal(1)); + }); + expect!(success, to_equal(true)); + } + + #[test] + fn test_all_infinite() { + let success = with_timeout(Duration::from_secs(1), || { + expect!(repeat(0), all, to_equal(0)); + }); + expect!(success, to_equal(false)); + } +} + +#[cfg(test)] +mod async_tests { + use std::{ + future::{ready, Future}, + iter::repeat, + sync::mpsc::channel, + time::Duration, + }; + + 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() + } + + #[tokio::test] + #[ignore = "currently async assertions do not short-circuit"] + async fn test_any_short_circuit() { + let success = with_timeout(Duration::from_secs(1), async { + expect!(repeat(ready(0)), any, when_ready, to_equal(0)).await; + }); + expect!(success, to_equal(true)); + } + + #[tokio::test] + async fn test_any_infinite() { + let success = with_timeout(Duration::from_secs(1), async { + expect!(repeat(ready(0)), any, when_ready, to_equal(1)).await; + }); + expect!(success, to_equal(false)); + } + + #[tokio::test] + #[ignore = "currently async assertions do not short-circuit"] + async fn test_all_short_circuit() { + let success = with_timeout(Duration::from_secs(1), async { + expect!(repeat(ready(0)), not, all, when_ready, to_equal(1)).await; + }); + expect!(success, to_equal(true)); + } + + #[tokio::test] + async fn test_all_infinite() { + let success = with_timeout(Duration::from_secs(1), async { + expect!(repeat(ready(0)), all, when_ready, to_equal(0)).await; + }); + expect!(success, to_equal(false)); + } +} diff --git a/src/assertions/iterators/outputs/merge.rs b/src/assertions/iterators/outputs/merge.rs index 946f39b..7acca96 100644 --- a/src/assertions/iterators/outputs/merge.rs +++ b/src/assertions/iterators/outputs/merge.rs @@ -23,6 +23,11 @@ pub trait MergeableOutput { 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 [`AssertionResult`]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; From b936c44f5615a30dfc84a16b438b38123cbe7c0e Mon Sep 17 00:00:00 2001 From: TehPers Date: Sat, 10 Aug 2024 13:20:10 -0700 Subject: [PATCH 12/37] Delete old experiments folder --- src/.experimenting/assertions.rs | 683 ------------------ src/.experimenting/combinators.rs | 29 - src/.experimenting/combinators/all.rs | 179 ----- src/.experimenting/combinators/any.rs | 41 -- src/.experimenting/combinators/at_path.rs | 284 -------- src/.experimenting/combinators/count.rs | 37 - src/.experimenting/combinators/err.rs | 37 - src/.experimenting/combinators/map.rs | 40 - src/.experimenting/combinators/not.rs | 36 - src/.experimenting/combinators/nth.rs | 38 - src/.experimenting/combinators/ok.rs | 37 - src/.experimenting/combinators/some.rs | 37 - src/.experimenting/combinators/when_called.rs | 59 -- src/.experimenting/combinators2.rs | 13 - src/.experimenting/combinators2/aggregate.rs | 150 ---- src/.experimenting/combinators2/combinator.rs | 90 --- src/.experimenting/combinators2/count.rs | 51 -- src/.experimenting/combinators2/identity.rs | 16 - src/.experimenting/combinators2/plan.rs | 34 - src/.experimenting/combinators2/when_ready.rs | 84 --- src/.experimenting/error.rs | 64 -- src/.experimenting/expect.rs | 154 ---- src/.experimenting/extensions.rs | 5 - src/.experimenting/extensions/iterators.rs | 54 -- src/.experimenting/lib.rs | 100 --- src/.experimenting/root.rs | 131 ---- src/.experimenting/specialization.rs | 1 - src/.experimenting/specialization/at_path.rs | 88 --- src/.experimenting/v10/combinators.rs | 11 - src/.experimenting/v10/combinators/all.rs | 36 - src/.experimenting/v10/combinators/any.rs | 67 -- .../v10/combinators/combinator.rs | 57 -- .../v10/combinators/combinator_ext.rs | 109 --- src/.experimenting/v10/combinators/not.rs | 37 - src/.experimenting/v11/assertions.rs | 3 - .../v11/assertions/assertion.rs | 17 - src/.experimenting/v11/combinators.rs | 12 - src/.experimenting/v11/combinators/all.rs | 50 -- .../v11/combinators/combinator.rs | 11 - .../v11/combinators/combinator_ext.rs | 28 - src/.experimenting/v11/combinators/macros.rs | 10 - src/.experimenting/v11/combinators/not.rs | 48 -- src/.experimenting/v11/expect.rs | 98 --- src/.experimenting/v11/failure.rs | 66 -- src/.experimenting/v11/specialization.rs | 6 - .../v11/specialization/at_path.rs | 83 --- src/.experimenting/v11/specialization/root.rs | 77 -- .../v11/specialization/wrapper.rs | 2 - src/.experimenting/v3.rs | 372 ---------- src/.experimenting/v4.rs | 341 --------- src/.experimenting/v5.rs | 333 --------- src/.experimenting/v6.rs | 180 ----- src/.experimenting/v6/all.rs | 67 -- src/.experimenting/v6/assertion.rs | 90 --- src/.experimenting/v6/combinator.rs | 47 -- src/.experimenting/v6/error.rs | 26 - src/.experimenting/v6/not.rs | 69 -- src/.experimenting/v7.rs | 180 ----- src/.experimenting/v7/all.rs | 73 -- src/.experimenting/v7/assertion.rs | 90 --- src/.experimenting/v7/combinator.rs | 51 -- src/.experimenting/v7/error.rs | 26 - src/.experimenting/v7/not.rs | 73 -- src/.experimenting/v8.rs | 29 - src/.experimenting/v9/assertions.rs | 5 - src/.experimenting/v9/assertions/assertion.rs | 20 - .../v9/assertions/simple_assertion.rs | 61 -- src/.experimenting/v9/combinators.rs | 13 - src/.experimenting/v9/combinators/all.rs | 80 -- src/.experimenting/v9/combinators/any.rs | 80 -- .../v9/combinators/combinator.rs | 69 -- .../v9/combinators/combinator_ext.rs | 146 ---- src/.experimenting/v9/combinators/not.rs | 75 -- .../v9/combinators/when_ready.old.rs | 354 --------- .../v9/combinators/when_ready.rs | 114 --- src/.experimenting/v9/expect.rs | 174 ----- src/.experimenting/v9/failure.rs | 66 -- src/.experimenting/v9/lib.rs | 13 - src/.experimenting/v9/output.rs | 208 ------ src/.experimenting/v9/prelude.rs | 10 - src/.experimenting/v9/third_party.rs | 4 - src/.experimenting/v9/third_party/either.rs | 84 --- src/.experimenting/v9/thoughts.md | 27 - 83 files changed, 7050 deletions(-) delete mode 100644 src/.experimenting/assertions.rs delete mode 100644 src/.experimenting/combinators.rs delete mode 100644 src/.experimenting/combinators/all.rs delete mode 100644 src/.experimenting/combinators/any.rs delete mode 100644 src/.experimenting/combinators/at_path.rs delete mode 100644 src/.experimenting/combinators/count.rs delete mode 100644 src/.experimenting/combinators/err.rs delete mode 100644 src/.experimenting/combinators/map.rs delete mode 100644 src/.experimenting/combinators/not.rs delete mode 100644 src/.experimenting/combinators/nth.rs delete mode 100644 src/.experimenting/combinators/ok.rs delete mode 100644 src/.experimenting/combinators/some.rs delete mode 100644 src/.experimenting/combinators/when_called.rs delete mode 100644 src/.experimenting/combinators2.rs delete mode 100644 src/.experimenting/combinators2/aggregate.rs delete mode 100644 src/.experimenting/combinators2/combinator.rs delete mode 100644 src/.experimenting/combinators2/count.rs delete mode 100644 src/.experimenting/combinators2/identity.rs delete mode 100644 src/.experimenting/combinators2/plan.rs delete mode 100644 src/.experimenting/combinators2/when_ready.rs delete mode 100644 src/.experimenting/error.rs delete mode 100644 src/.experimenting/expect.rs delete mode 100644 src/.experimenting/extensions.rs delete mode 100644 src/.experimenting/extensions/iterators.rs delete mode 100644 src/.experimenting/lib.rs delete mode 100644 src/.experimenting/root.rs delete mode 100644 src/.experimenting/specialization.rs delete mode 100644 src/.experimenting/specialization/at_path.rs delete mode 100644 src/.experimenting/v10/combinators.rs delete mode 100644 src/.experimenting/v10/combinators/all.rs delete mode 100644 src/.experimenting/v10/combinators/any.rs delete mode 100644 src/.experimenting/v10/combinators/combinator.rs delete mode 100644 src/.experimenting/v10/combinators/combinator_ext.rs delete mode 100644 src/.experimenting/v10/combinators/not.rs delete mode 100644 src/.experimenting/v11/assertions.rs delete mode 100644 src/.experimenting/v11/assertions/assertion.rs delete mode 100644 src/.experimenting/v11/combinators.rs delete mode 100644 src/.experimenting/v11/combinators/all.rs delete mode 100644 src/.experimenting/v11/combinators/combinator.rs delete mode 100644 src/.experimenting/v11/combinators/combinator_ext.rs delete mode 100644 src/.experimenting/v11/combinators/macros.rs delete mode 100644 src/.experimenting/v11/combinators/not.rs delete mode 100644 src/.experimenting/v11/expect.rs delete mode 100644 src/.experimenting/v11/failure.rs delete mode 100644 src/.experimenting/v11/specialization.rs delete mode 100644 src/.experimenting/v11/specialization/at_path.rs delete mode 100644 src/.experimenting/v11/specialization/root.rs delete mode 100644 src/.experimenting/v11/specialization/wrapper.rs delete mode 100644 src/.experimenting/v3.rs delete mode 100644 src/.experimenting/v4.rs delete mode 100644 src/.experimenting/v5.rs delete mode 100644 src/.experimenting/v6.rs delete mode 100644 src/.experimenting/v6/all.rs delete mode 100644 src/.experimenting/v6/assertion.rs delete mode 100644 src/.experimenting/v6/combinator.rs delete mode 100644 src/.experimenting/v6/error.rs delete mode 100644 src/.experimenting/v6/not.rs delete mode 100644 src/.experimenting/v7.rs delete mode 100644 src/.experimenting/v7/all.rs delete mode 100644 src/.experimenting/v7/assertion.rs delete mode 100644 src/.experimenting/v7/combinator.rs delete mode 100644 src/.experimenting/v7/error.rs delete mode 100644 src/.experimenting/v7/not.rs delete mode 100644 src/.experimenting/v8.rs delete mode 100644 src/.experimenting/v9/assertions.rs delete mode 100644 src/.experimenting/v9/assertions/assertion.rs delete mode 100644 src/.experimenting/v9/assertions/simple_assertion.rs delete mode 100644 src/.experimenting/v9/combinators.rs delete mode 100644 src/.experimenting/v9/combinators/all.rs delete mode 100644 src/.experimenting/v9/combinators/any.rs delete mode 100644 src/.experimenting/v9/combinators/combinator.rs delete mode 100644 src/.experimenting/v9/combinators/combinator_ext.rs delete mode 100644 src/.experimenting/v9/combinators/not.rs delete mode 100644 src/.experimenting/v9/combinators/when_ready.old.rs delete mode 100644 src/.experimenting/v9/combinators/when_ready.rs delete mode 100644 src/.experimenting/v9/expect.rs delete mode 100644 src/.experimenting/v9/failure.rs delete mode 100644 src/.experimenting/v9/lib.rs delete mode 100644 src/.experimenting/v9/output.rs delete mode 100644 src/.experimenting/v9/prelude.rs delete mode 100644 src/.experimenting/v9/third_party.rs delete mode 100644 src/.experimenting/v9/third_party/either.rs delete mode 100644 src/.experimenting/v9/thoughts.md diff --git a/src/.experimenting/assertions.rs b/src/.experimenting/assertions.rs deleted file mode 100644 index 759d71d..0000000 --- a/src/.experimenting/assertions.rs +++ /dev/null @@ -1,683 +0,0 @@ -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); - } -} diff --git a/src/.experimenting/combinators.rs b/src/.experimenting/combinators.rs deleted file mode 100644 index 7fd36ae..0000000 --- a/src/.experimenting/combinators.rs +++ /dev/null @@ -1,29 +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; - -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::*; diff --git a/src/.experimenting/combinators/all.rs b/src/.experimenting/combinators/all.rs deleted file mode 100644 index 4de2238..0000000 --- a/src/.experimenting/combinators/all.rs +++ /dev/null @@ -1,179 +0,0 @@ -use std::{ - fmt::Display, - future::{ready, Future}, - pin::Pin, -}; - -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)), - ) - } -} - -//////////// - -pub trait Assertion2 { - type Output; - - /// Executes this assertion. This takes an input, performs some kind of - /// transformation on it, then produces a new output. - fn execute(self, input: Input) -> Self::Output; -} - -impl Assertion2 for F -where - F: FnOnce(I) -> O, -{ - type Output = O; - - /// Executes this assertion. This takes an input, performs some kind of - /// transformation on it, then produces a new output. - fn execute(self, input: I) -> Self::Output { - self(input) - } -} - -pub trait Combinator2 { - type Output; - - /// Applies this combinator, passing an input into the given assertion and - /// returning the transformed output. - fn apply(self, assertion: A) -> Self::Output; -} - -struct Count { - prev: Prev, -} - -impl Combinator2 for Count -where - Prev: Combinator2>, -{ - type Output = Prev::Output; - - fn apply(self, assertion: A) -> Self::Output { - self.prev.apply(CountAssertion { next: assertion }) - } -} - -struct CountAssertion { - next: Next, -} - -impl Assertion2 for CountAssertion -where - Input: IntoIterator, - Next: Assertion2, -{ - type Output = Next::Output; - - fn execute(self, input: Input) -> Self::Output { - self.next.execute(input.into_iter().count()) - } -} - -struct WhenReady { - prev: Prev, -} - -impl Combinator2 for WhenReady -where - Prev: Combinator2>, -{ - type Output = Prev::Output; - - fn apply(self, assertion: A) -> Self::Output { - self.prev.apply(WhenReadyAssertion { next: assertion }) - } -} - -struct WhenReadyAssertion { - next: Next, -} - -impl Assertion2 for WhenReadyAssertion -where - Input: Future + Send + 'static, - Next: Assertion2<::Output> + Send + 'static, -{ - type Output = Pin< - Box::Output>>::Output> + Send>, - >; - - fn execute(self, input: Input) -> Self::Output { - Box::pin(async move { - let input = input.await; - self.next.execute(input) - }) - } -} - -struct Root { - target: T, -} - -impl Combinator2 for Root -where - A: Assertion2, -{ - type Output = A::Output; - - fn apply(self, assertion: A) -> Self::Output { - assertion.execute(self.target) - } -} - -async fn foo() { - let root = Root { target: [1, 2, 3] }; - let combinator = Count { prev: root }; - let _result = combinator.apply(|count| count > 0); - - let assertion = WhenReadyAssertion { - next: CountAssertion { - next: |count| count > 0, - }, - }; - let _result = assertion.execute(ready([1, 2, 3])).await; - - let root = Root { - target: ready([1i32, 2, 3]), - }; - let combinator = Count { - prev: WhenReady { prev: root }, - }; - let _result = combinator.apply(|len| len > 0).await; -} diff --git a/src/.experimenting/combinators/any.rs b/src/.experimenting/combinators/any.rs deleted file mode 100644 index 6d9eb54..0000000 --- a/src/.experimenting/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/.experimenting/combinators/at_path.rs b/src/.experimenting/combinators/at_path.rs deleted file mode 100644 index d59eeb1..0000000 --- a/src/.experimenting/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/.experimenting/combinators/count.rs b/src/.experimenting/combinators/count.rs deleted file mode 100644 index 5a6f084..0000000 --- a/src/.experimenting/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 { - prev: Inner, -} - -impl CountCombinator { - /// Creates a new combinator which wraps an inner [`Assertable`]. - #[inline] - pub fn new(prev: Inner) -> Self { - Self { prev } - } -} - -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.prev.to_satisfy( - format_args!("the length satisfies: {expectation}"), - |values| f(values.into_iter().count()), - ) - } -} diff --git a/src/.experimenting/combinators/err.rs b/src/.experimenting/combinators/err.rs deleted file mode 100644 index cb2c5ea..0000000 --- a/src/.experimenting/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/.experimenting/combinators/map.rs b/src/.experimenting/combinators/map.rs deleted file mode 100644 index 79161cb..0000000 --- a/src/.experimenting/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/.experimenting/combinators/not.rs b/src/.experimenting/combinators/not.rs deleted file mode 100644 index dfe22aa..0000000 --- a/src/.experimenting/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/.experimenting/combinators/nth.rs b/src/.experimenting/combinators/nth.rs deleted file mode 100644 index 22ada4e..0000000 --- a/src/.experimenting/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/.experimenting/combinators/ok.rs b/src/.experimenting/combinators/ok.rs deleted file mode 100644 index 3c8b875..0000000 --- a/src/.experimenting/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/.experimenting/combinators/some.rs b/src/.experimenting/combinators/some.rs deleted file mode 100644 index 22341ac..0000000 --- a/src/.experimenting/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/.experimenting/combinators/when_called.rs b/src/.experimenting/combinators/when_called.rs deleted file mode 100644 index 33a5a7f..0000000 --- a/src/.experimenting/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/.experimenting/combinators2.rs b/src/.experimenting/combinators2.rs deleted file mode 100644 index ae9043f..0000000 --- a/src/.experimenting/combinators2.rs +++ /dev/null @@ -1,13 +0,0 @@ -mod aggregate; -mod combinator; -mod count; -mod identity; -mod plan; -mod when_ready; - -pub use aggregate::*; -pub use combinator::*; -pub use count::*; -pub use identity::*; -pub use plan::*; -pub use when_ready::*; diff --git a/src/.experimenting/combinators2/aggregate.rs b/src/.experimenting/combinators2/aggregate.rs deleted file mode 100644 index 33f6720..0000000 --- a/src/.experimenting/combinators2/aggregate.rs +++ /dev/null @@ -1,150 +0,0 @@ -use std::{ - fmt::{Display, Formatter}, - ops::ControlFlow, -}; - -use crate::AssertionErrorBuilder; - -use super::{Assertion, Combinator}; - -#[derive(Clone, Copy, Debug, Default)] -pub struct AggregateCombinator { - aggregator: A, -} - -impl AggregateCombinator { - /// Creates a new [`AggregateCombinator`]. - pub fn new(aggregator: A) -> Self { - Self { aggregator } - } -} - -impl Combinator for AggregateCombinator { - type Assertion = AggregateAssertion; - - fn build(self, next: Next) -> Self::Assertion { - AggregateAssertion::new(self.aggregator, next) - } -} - -#[derive(Clone, Copy, Debug, Default)] -pub struct AggregateAssertion { - aggregator: A, - next: Next, -} - -impl AggregateAssertion { - /// Creates a new [`AggregateAssertion`]. - pub fn new(aggregator: A, next: Next) -> Self { - Self { aggregator, next } - } -} - -impl Display for AggregateAssertion -where - A: Display, - Next: Display, -{ - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "for {}, {}", self.aggregator, self.next) - } -} - -impl Assertion for AggregateAssertion -where - Input: IntoIterator, - Next: Assertion + Clone, - A: Aggregator, -{ - type Output = A::Aggregate; - - fn execute(mut self, input: Input) -> Self::Output { - let output = input - .into_iter() - .map(|input| self.next.clone().execute(input)) - .try_fold(self.aggregator.zero(), |aggregate, output| { - self.aggregator.with_output(aggregate, output) - }); - - match output { - ControlFlow::Continue(aggregate) => aggregate, - ControlFlow::Break(aggregate) => aggregate, - } - } -} - -pub trait Aggregator: Display { - type Aggregate; - - fn zero(&mut self) -> Self::Aggregate; - - fn with_output( - &mut self, - aggregate: Self::Aggregate, - next: Output, - ) -> ControlFlow; -} - -/// Aggregate function that requires all assertions to be successful. -#[derive(Clone, Copy, Debug, Default)] -pub struct AllSucceed; - -impl Display for AllSucceed { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "each inner value") - } -} - -impl Aggregator> for AllSucceed { - type Aggregate = Result<(), AssertionErrorBuilder>; - - /// The "zero value" for the aggregation. This is what the aggregation - /// starts with before any outputs are processed. If there are no outputs - /// to process, this is also the final result. - fn zero(&mut self) -> Self::Aggregate { - Ok(()) - } - - /// Combine the current aggregate with the next output. At any point, the - /// aggregation can short-circuit and return the final result by returning - /// [`ControlFlow::Break`]. - fn with_output( - &mut self, - _: Self::Aggregate, - next: Result<(), AssertionErrorBuilder>, - ) -> ControlFlow { - match next { - Ok(_) => ControlFlow::Continue(Ok(())), - Err(e) => ControlFlow::Break(Err(e)), - } - } -} - -/// Aggregate function that requires any assertions to be successful. -#[derive(Clone, Copy, Debug, Default)] -pub struct AnySucceed; - -impl Display for AnySucceed { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "some inner value") - } -} - -impl Aggregator> for AnySucceed { - type Aggregate = Result<(), AssertionErrorBuilder>; - - fn zero(&mut self) -> Self::Aggregate { - Err(AssertionErrorBuilder::default().with_field("reason", "no assertion succeeded")) - } - - fn with_output( - &mut self, - aggregate: Self::Aggregate, - next: Result<(), AssertionErrorBuilder>, - ) -> ControlFlow { - match next { - Ok(_) => ControlFlow::Break(Ok(())), - Err(_) => ControlFlow::Continue(aggregate), - } - } -} diff --git a/src/.experimenting/combinators2/combinator.rs b/src/.experimenting/combinators2/combinator.rs deleted file mode 100644 index 8ed9f2b..0000000 --- a/src/.experimenting/combinators2/combinator.rs +++ /dev/null @@ -1,90 +0,0 @@ -use std::fmt::{Display, Formatter}; - -/// A transformer for an assertion. -/// -/// Combinators are used to transform the target value while executing an -/// assertion. This can range from something as simple as getting the length of -/// the input value to executing future assertions asynchronously once the -/// target value is ready. It can even execute future assertions multiple times -/// and aggregate the results. -/// -/// These are the core of how the input value is transformed, how more complex -/// assertions can be built, and how the final result is produced. -pub trait Combinator { - /// The assertion that is produced by this combinator when it is used to - /// transform another assertion. - type Assertion; - - /// Builds the assertion that is produced by this combinator. - /// - /// This method is used to transform the next assertion in the chain in some - /// manner dependent on the functionality of the combinator. For example, - /// the assertion returned by this method may transform the input passed to - /// the next assertion or even execute it multiple times. - fn build(self, next: Next) -> Self::Assertion; -} - -/// An assertion that can be executed on an input value. -/// -/// The assertion produces a result often indicative of whether the assertion -/// passed or failed. While in most cases the result is literally a [`Result`], -/// it can sometimes also be other types, such as a future that resolves to a -/// result. -/// -/// ## Display -/// -/// Assertions must implement [`Display`]. This allows the expectation message -/// to be communicated to the user in a human-readable format. The output format -/// should be a short description of what is expected of the input. For example, -/// the output format for an assertion that checks whether a value is true could -/// be: `"the value is true"`. -/// -/// Combinators will want to ensure the expectation message includes the next -/// assertion's message as well, meaning the expectation message will often be -/// created by adding some text to the next assertion's message. For example, a -/// combinator that maps an input iterator to the number of elements in the -/// iterator could have an output format of: `"the length satisfies: {next}"`, -/// where `{next}` indicates the next assertion's expectation. -pub trait Assertion: Display { - /// The output produced by the assertion. This is usually either a - /// [`Result<(), AssertError>`] or a future which resolves to one. - type Output; - - /// Executes the assertion on the input value. - fn execute(self, input: Input) -> Self::Output; -} - -/// A simple assertion constructed using a callback function. -#[derive(Clone, Debug)] -pub struct AssertionFn { - expectation: String, - f: F, -} - -impl AssertionFn { - /// Creates a new [`AssertionFn`]. - #[inline] - pub fn new(expectation: impl Display, f: F) -> Self { - Self { - expectation: expectation.to_string(), - f, - } - } -} - -impl Display for AssertionFn { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.expectation) - } -} - -impl Assertion for AssertionFn -where - F: FnOnce(I) -> O, -{ - type Output = O; - - fn execute(self, input: I) -> Self::Output { - (self.f)(input) - } -} diff --git a/src/.experimenting/combinators2/count.rs b/src/.experimenting/combinators2/count.rs deleted file mode 100644 index 6d19bb0..0000000 --- a/src/.experimenting/combinators2/count.rs +++ /dev/null @@ -1,51 +0,0 @@ -use std::fmt::{Display, Formatter}; - -use super::{Assertion, Combinator}; - -/// Counts the number of elements in the input. -#[derive(Clone, Copy, Debug, Default)] -pub struct CountCombinator; - -impl Combinator for CountCombinator { - type Assertion = CountAssertion; - - fn build(self, next: Next) -> Self::Assertion { - CountAssertion::new(next) - } -} - -/// Counts the number of elements in the input, then passes the count to the -/// next assertion. -#[derive(Clone, Copy, Debug, Default)] -pub struct CountAssertion { - next: Next, -} - -impl CountAssertion { - /// Creates a new [`CountAssertion`]. - #[inline] - pub fn new(next: Next) -> Self { - Self { next } - } -} - -impl Display for CountAssertion -where - Next: Display, -{ - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "the length satisfies: {}", self.next) - } -} - -impl Assertion for CountAssertion -where - Input: IntoIterator, - Next: Assertion, -{ - type Output = Next::Output; - - fn execute(self, input: Input) -> Self::Output { - self.next.execute(input.into_iter().count()) - } -} diff --git a/src/.experimenting/combinators2/identity.rs b/src/.experimenting/combinators2/identity.rs deleted file mode 100644 index 06b0fba..0000000 --- a/src/.experimenting/combinators2/identity.rs +++ /dev/null @@ -1,16 +0,0 @@ -use super::Combinator; - -/// A combinator that does nothing. -/// -/// This combinator is used to terminate a chain of combinators. It outputs the -/// assertion that was passed to it. -#[derive(Clone, Copy, Debug, Default)] -pub struct IdentityCombinator; - -impl Combinator for IdentityCombinator { - type Assertion = Next; - - fn build(self, next: Next) -> Self::Assertion { - next - } -} diff --git a/src/.experimenting/combinators2/plan.rs b/src/.experimenting/combinators2/plan.rs deleted file mode 100644 index a1c9a04..0000000 --- a/src/.experimenting/combinators2/plan.rs +++ /dev/null @@ -1,34 +0,0 @@ -use super::Combinator; - -/// Chains two combinators together. -/// -/// This is used to control the order that an input is passed through a chain of -/// combinators. The `Outer` combinator is executed first, then the output is -/// passed to the `Inner` combinator. The output of the `Inner` combinator is -/// returned by the plan. -pub struct Plan { - outer: Outer, - inner: Inner, -} - -impl Plan { - /// Creates a new [`Plan`] combinator. - pub fn new(prev: Outer, next: Inner) -> Self { - Self { - outer: prev, - inner: next, - } - } -} - -impl Combinator for Plan -where - Outer: Combinator, - Next1: Combinator, -{ - type Assertion = Outer::Assertion; - - fn build(self, next: Next2) -> Self::Assertion { - self.outer.build(self.inner.build(next)) - } -} diff --git a/src/.experimenting/combinators2/when_ready.rs b/src/.experimenting/combinators2/when_ready.rs deleted file mode 100644 index 95ff68d..0000000 --- a/src/.experimenting/combinators2/when_ready.rs +++ /dev/null @@ -1,84 +0,0 @@ -use std::{ - fmt::{Display, Formatter}, - future::Future, - pin::Pin, - task::{ready, Context, Poll}, -}; - -use pin_project_lite::pin_project; - -use super::{Assertion, Combinator}; - -/// Performs the assertion when the input future is ready. -#[derive(Clone, Copy, Debug, Default)] -pub struct WhenReadyCombinator; - -impl Combinator for WhenReadyCombinator { - type Assertion = WhenReadyAssertion; - - fn build(self, next: Next) -> Self::Assertion { - WhenReadyAssertion::new(next) - } -} - -/// Waits for the input to be ready, then passes it to the next assertion. -#[derive(Clone, Copy, Debug, Default)] -pub struct WhenReadyAssertion { - next: Next, -} - -impl WhenReadyAssertion { - /// Creates a new [`WhenReadyAssertion`]. - #[inline] - pub fn new(next: Next) -> Self { - Self { next } - } -} - -impl Display for WhenReadyAssertion -where - Next: Display, -{ - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "when ready, {}", self.next) - } -} - -impl Assertion for WhenReadyAssertion -where - Input: Future, - Next: Assertion, -{ - type Output = WhenReadyFuture; - - fn execute(self, input: Input) -> Self::Output { - WhenReadyFuture { - input, - next: Some(self.next), - } - } -} - -pin_project! { - /// A future that performs an assertion when it is ready. - pub struct WhenReadyFuture { - #[pin] - input: Input, - next: Option, - } -} - -impl Future for WhenReadyFuture -where - Input: Future, - Next: Assertion, -{ - type Output = Next::Output; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let projected = self.project(); - let res = ready!(projected.input.poll(cx)); - let next = projected.next.take().expect("polled after ready"); - Poll::Ready(next.execute(res)) - } -} diff --git a/src/.experimenting/error.rs b/src/.experimenting/error.rs deleted file mode 100644 index e9ef9ad..0000000 --- a/src/.experimenting/error.rs +++ /dev/null @@ -1,64 +0,0 @@ -use std::{ - error::Error, - 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 AssertionError { - fields: Vec<(&'static str, String)>, -} - -impl AssertionError { - /// Creates a builder for a new [`AssertionError`]. - pub fn builder() -> AssertionErrorBuilder { - AssertionErrorBuilder::default() - } -} - -impl Display for AssertionError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - writeln!(f, "assertion failed.")?; - for (name, value) in &self.fields { - writeln!(f, " {name}: {value}")?; - } - - Ok(()) - } -} - -impl Error for AssertionError {} - -/// A builder for an [`AssertionError`]. -#[derive(Clone, Debug)] -pub struct AssertionErrorBuilder { - fields: Vec<(&'static str, String)>, -} - -impl Default for AssertionErrorBuilder { - fn default() -> Self { - Self { - fields: vec![("expected", String::new())], - } - } -} - -impl AssertionErrorBuilder { - /// 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 Display) -> Self { - self.fields.push((name, value.to_string())); - self - } - - /// Builds the error with the given expectation. - pub fn build(mut self, expectation: impl Display) -> AssertionError { - self.fields[0].1 = expectation.to_string(); - AssertionError { - fields: self.fields, - } - } -} diff --git a/src/.experimenting/expect.rs b/src/.experimenting/expect.rs deleted file mode 100644 index 7c083aa..0000000 --- a/src/.experimenting/expect.rs +++ /dev/null @@ -1,154 +0,0 @@ -use std::fmt::{Display, Formatter}; - -use crate::{Assertable, AssertionError}; - -/// 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), - ) - }; -} - -/// 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<(), AssertionError>; - - 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 = AssertionError::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/.experimenting/extensions.rs b/src/.experimenting/extensions.rs deleted file mode 100644 index 97198fd..0000000 --- a/src/.experimenting/extensions.rs +++ /dev/null @@ -1,5 +0,0 @@ -// TODO: rename this module to assertions - -mod iterators; - -pub use iterators::*; diff --git a/src/.experimenting/extensions/iterators.rs b/src/.experimenting/extensions/iterators.rs deleted file mode 100644 index 6280e69..0000000 --- a/src/.experimenting/extensions/iterators.rs +++ /dev/null @@ -1,54 +0,0 @@ -use std::fmt::{Display, Formatter}; - -use crate::{ - combinators2::{Assertion, AssertionFn, Combinator, CountCombinator, Plan}, - AssertionRoot, -}; - -pub trait IteratorAssertions { - fn count(self) -> AssertionRoot>; -} - -impl IteratorAssertions for AssertionRoot -where - P: Combinator, - // P::Assertion: Assertion, - // ::Output: IntoIterator, -{ - fn count(self) -> AssertionRoot> { - self.chain(CountCombinator) - } -} - -struct IdentityAssertion; - -impl Display for IdentityAssertion { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "always succeeds") - } -} - -impl Assertion for IdentityAssertion { - type Output = Input; - - fn execute(self, input: Input) -> Self::Output { - input - } -} - -fn foo() { - use crate::expect2; - - let success = expect2!([1, 2, 3]) - .count() - .execute(AssertionFn::new("value is not 0", |value| value != 0)); - - let success = expect2!(1) - .count() - .execute(AssertionFn::new("value is not 0", |value| value != 0)); - - let success = expect2!(1) - .count() - .count() - .execute(AssertionFn::new("value is not 0", |value| value != 0)); -} diff --git a/src/.experimenting/lib.rs b/src/.experimenting/lib.rs deleted file mode 100644 index 78f0724..0000000 --- a/src/.experimenting/lib.rs +++ /dev/null @@ -1,100 +0,0 @@ -//! Build composable assertions with a functional API. -//! -//! ``` -//! 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: -//! -//! - [`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. -//! -//! These combinators can be chained together as needed. For example: -//! -//! ``` -//! # 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(); -//! ``` -//! -//! 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. -//! -//! 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: -//! -//! ``` -//! # use expecters::prelude::*; -//! let result = expect!(42).as_result().to_be_less_than(10); -//! expect!(result).to_be_err(); -//! ``` -//! -//! 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. -//! -//! [`as_result`]: ExpectationRoot::as_result -//! [`mockall`]: https://crates.io/crates/mockall -//! [`test-case`]: https://crates.io/crates/test-case - -pub mod combinators; -pub mod combinators2; - -mod extensions; - -mod assertions; -mod error; -// mod expect; -mod root; -// mod v3; -// mod v4; -mod v5; -// pub mod v6; -pub mod v7; -mod v8; - -pub use assertions::*; -pub use error::*; -// pub use expect::*; -pub use root::*; - -/// 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::{expect2, extensions::*, path, Assertable}; -} - -#[doc(hidden)] -pub mod specialization; diff --git a/src/.experimenting/root.rs b/src/.experimenting/root.rs deleted file mode 100644 index 8d6427a..0000000 --- a/src/.experimenting/root.rs +++ /dev/null @@ -1,131 +0,0 @@ -use crate::combinators2::{Assertion, Combinator, IdentityCombinator, Plan}; - -/// 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! expect2 { - ($e:expr) => { - // TODO: specialize for types that impl `Display` and `Debug` - $crate::AssertionRoot::new( - $e, - $crate::AssertionMetadata::new( - file!(), - line!(), - column!(), - stringify!($e), - ), - ) - }; -} - -/// The root of an assertion. Combinators are chained together to build an -/// assertion plan, and the plan is executed with a final assertion. -#[derive(Clone, Debug)] -#[must_use = "this type does nothing until an assertion is executed"] -pub struct AssertionRoot { - target: T, - plan: P, - metadata: AssertionMetadata, -} - -impl AssertionRoot { - /// Creates a new [`AssertionRoot`] 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, metadata: AssertionMetadata) -> Self { - Self { - target, - plan: IdentityCombinator, - metadata, - } - } -} - -impl AssertionRoot { - /// Chains a combinator onto the end of the assertion plan. - /// - /// This is the core of how combinators are built. Combinators are used to - /// transform the target value before executing a final assertion. This - /// method allows them to be chained together in a functional manner to - /// transform the target value in a complex way. - pub fn chain(self, combinator: Next) -> AssertionRoot> { - AssertionRoot { - target: self.target, - plan: Plan::new(self.plan, combinator), - metadata: self.metadata, - } - } - - /// Executes the assertion plan by providing the final assertion. - /// - /// This is the core of how assertions are executed. The plan is built up - /// using combinators, and then the final assertion is executed on the - /// transformed target value. - /// - /// Normally, you want to call one of the assertion extension methods - /// directly rather than calling this method. - pub fn execute( - self, - assertion: A, - ) -> <

>::Assertion as Assertion>::Output - where - P: Combinator, - P::Assertion: Assertion, - { - self.plan.build(assertion).execute(self.target) - } -} - -/// Metadata about an assertion that's being executed. This includes information -/// about where the assertion was created and the original target of the -/// assertion. -#[derive(Clone, Debug)] -pub struct AssertionMetadata { - pub(crate) file: &'static str, - pub(crate) line: u32, - pub(crate) column: u32, - pub(crate) target_source: &'static str, -} - -impl AssertionMetadata { - // Signature may change at any time! - #[doc(hidden)] - pub fn new(file: &'static str, line: u32, column: u32, target_source: &'static str) -> Self { - Self { - file, - line, - column, - target_source, - } - } -} diff --git a/src/.experimenting/specialization.rs b/src/.experimenting/specialization.rs deleted file mode 100644 index 1bbcc5e..0000000 --- a/src/.experimenting/specialization.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod at_path; diff --git a/src/.experimenting/specialization/at_path.rs b/src/.experimenting/specialization/at_path.rs deleted file mode 100644 index 164bc16..0000000 --- a/src/.experimenting/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/.experimenting/v10/combinators.rs b/src/.experimenting/v10/combinators.rs deleted file mode 100644 index 8c6a54a..0000000 --- a/src/.experimenting/v10/combinators.rs +++ /dev/null @@ -1,11 +0,0 @@ -mod all; -mod any; -mod combinator; -mod combinator_ext; -mod not; - -pub use all::*; -pub use any::*; -pub use combinator::*; -pub use combinator_ext::*; -pub use not::*; diff --git a/src/.experimenting/v10/combinators/all.rs b/src/.experimenting/v10/combinators/all.rs deleted file mode 100644 index 027fb50..0000000 --- a/src/.experimenting/v10/combinators/all.rs +++ /dev/null @@ -1,36 +0,0 @@ -use crate::AssertionResult; - -use super::AssertionCombinator; - -/// Wraps another [`AssertionCombinator`] and ensures the assertion succeeds for -/// every inner value. -#[derive(Clone, Debug)] -pub struct AllCombinator { - inner: Inner, -} - -impl AllCombinator { - /// Creates a new instance of this combinator. - #[inline] - pub fn new(inner: Inner) -> Self { - Self { inner } - } -} - -impl AssertionCombinator for AllCombinator -where - Inner: AssertionCombinator, - Inner::Target: IntoIterator, -{ - type Target = ::Item; - type Result = Inner::Result; - - #[inline] - fn execute(self, mut f: F) -> Self::Result - where - F: FnMut(Self::Target) -> AssertionResult, - { - self.inner - .execute(|values| values.into_iter().map(&mut f).collect()) - } -} diff --git a/src/.experimenting/v10/combinators/any.rs b/src/.experimenting/v10/combinators/any.rs deleted file mode 100644 index 0a204eb..0000000 --- a/src/.experimenting/v10/combinators/any.rs +++ /dev/null @@ -1,67 +0,0 @@ -use std::fmt::{Display, Formatter}; - -use crate::{AssertionFailure, AssertionResult}; - -use super::AssertionCombinator; - -/// Wraps another [`AssertionCombinator`] and ensures the assertion succeeds for -/// some inner value. -#[derive(Clone, Debug)] -pub struct AnyCombinator { - inner: Inner, -} - -impl AnyCombinator { - /// Creates a new instance of this combinator, wrapping the inner - /// combinator. - #[inline] - pub fn new(inner: Inner) -> Self { - Self { inner } - } -} - -impl AssertionCombinator for AnyCombinator -where - Inner: AssertionCombinator, - Inner::Target: IntoIterator, -{ - type Target = ::Item; - type Result = Inner::Result; - - fn execute(self, mut f: F) -> Self::Result - where - F: FnMut(Self::Target) -> AssertionResult, - { - self.inner.execute(|values| { - let mut error = AssertionFailure::builder().build("TODO"); - for value in values { - match f(value) { - Ok(()) => return Ok(()), - Err(e) => error = e, - } - } - - Err(error) - }) - } -} - -// impl Assertion for AnyAssertion -// where -// Target: IntoIterator, -// Next: Assertion + Clone, -// { -// type Output = AssertionResult; - -// fn assert(self, target: Target) -> Self::Output { -// let mut error = AssertionFailure::builder().build("TODO"); -// for value in target { -// match self.next.clone().assert(value) { -// Ok(()) => return Ok(()), -// Err(e) => error = e, -// } -// } - -// Err(error) -// } -// } diff --git a/src/.experimenting/v10/combinators/combinator.rs b/src/.experimenting/v10/combinators/combinator.rs deleted file mode 100644 index 97e3032..0000000 --- a/src/.experimenting/v10/combinators/combinator.rs +++ /dev/null @@ -1,57 +0,0 @@ -// TODO: docs - -use crate::AssertionResult; - -/// Transforms an assertion into a more complex assertion. -/// -/// Combinators are used to build more complex assertions out of less complex -/// ones. They function as a sort of "middleware" and are able to transform both -/// the input to an assertion and the output from one. -/// -/// The type parameter represents the type of assertion this combinator can -/// wrap. -#[must_use = "combinators do nothing until they are applied"] -pub trait AssertionCombinator { - /// The type of value passed to this combinator's assertion. - /// - /// For many combinators which wrap another combinator, this is simply - /// `Inner::Target` (where `Inner` is the inner combinator). - type Target; - - /// The result type from executing an assertion with this combinator. - type Result; - - /// Wraps an assertion. - /// - /// This function is the foundation for how combinators work. This is used - /// to create more complex assertions. The combinator works by wrapping the - /// given assertion with its own assertion. For example, the assertion - /// returned by this combinator can negate the output of the provided - /// assertion, call the provided assertion multiple times (if it's `Clone`), - /// or transform the output of the provided assertion in some other manner. - /// - /// The returned assertion still needs to be executed on a value. The type - /// of input the returned assertion accepts is determined by the - /// [`AssertionCombinator::Target`] property. - /// - /// This method also usually has the side effect of "inverting" the type. - /// For example, calling `expect!(value).not().all()` will create an - /// `AllCombinator>>` (where `T` is the type - /// of the value), and applying the combinator will generate an instance of - /// `RootAssertion>>`. - fn execute(self, f: F) -> Self::Result - where - F: FnMut(Self::Target) -> AssertionResult; -} - -impl AssertionCombinator for ExpectationRoot { - type Target = Target; - type Result = AssertionResult; - - fn execute(self, mut f: F) -> Self::Result - where - F: FnMut(Self::Target) -> AssertionResult, - { - f(self.target) - } -} diff --git a/src/.experimenting/v10/combinators/combinator_ext.rs b/src/.experimenting/v10/combinators/combinator_ext.rs deleted file mode 100644 index ba87610..0000000 --- a/src/.experimenting/v10/combinators/combinator_ext.rs +++ /dev/null @@ -1,109 +0,0 @@ -use crate::AssertionResult; - -use super::{AllCombinator, AssertionCombinator, NotCombinator}; - -pub trait AssertionCombinatorExt: AssertionCombinator + Sized { - /// 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::NextTarget: IntoIterator, - // { - // AnyCombinator::new(self) - // } - - // fn to_equal(self, other: U) -> impl AssertionOutput - // where - // Self::Target: PartialEq, - // { - // self.apply("the values to be equal", move |value| { - // (value == other) - // .then_some(()) - // .ok_or_else(|| AssertionFailure::builder().build("the values to be equal")) - // }) - // } -} - -impl AssertionCombinatorExt for T where T: AssertionCombinator {} diff --git a/src/.experimenting/v10/combinators/not.rs b/src/.experimenting/v10/combinators/not.rs deleted file mode 100644 index 8a78d83..0000000 --- a/src/.experimenting/v10/combinators/not.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::{AssertionFailure, AssertionResult}; - -use super::AssertionCombinator; - -/// Negates the output of an assertion. 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 instance of this combinator wrapping the inner combinator. - #[inline] - pub fn new(inner: Inner) -> Self { - Self { inner } - } -} - -impl AssertionCombinator for NotCombinator -where - Inner: AssertionCombinator, -{ - type Target = Inner::Target; - type Result = Inner::Result; - - #[inline] - fn execute(self, mut f: F) -> Self::Result - where - F: FnMut(Self::Target) -> AssertionResult, - { - self.inner.execute(|value| match f(value) { - Ok(_) => Err(AssertionFailure::builder().build("TODO")), - Err(_) => Ok(()), - }) - } -} diff --git a/src/.experimenting/v11/assertions.rs b/src/.experimenting/v11/assertions.rs deleted file mode 100644 index 7ce3c0f..0000000 --- a/src/.experimenting/v11/assertions.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod assertion; - -pub use assertion::*; diff --git a/src/.experimenting/v11/assertions/assertion.rs b/src/.experimenting/v11/assertions/assertion.rs deleted file mode 100644 index c3c7414..0000000 --- a/src/.experimenting/v11/assertions/assertion.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::AssertionResult; - -pub trait Assertion { - // type Output; - - fn execute(&mut self, target: T) -> AssertionResult; -} - -impl Assertion for F -where - F: FnMut(T) -> AssertionResult, -{ - #[inline] - fn execute(&mut self, target: T) -> AssertionResult { - self(target) - } -} diff --git a/src/.experimenting/v11/combinators.rs b/src/.experimenting/v11/combinators.rs deleted file mode 100644 index 4597a64..0000000 --- a/src/.experimenting/v11/combinators.rs +++ /dev/null @@ -1,12 +0,0 @@ -#[macro_use] -mod macros; - -mod all; -mod combinator; -mod combinator_ext; -mod not; - -pub use all::*; -pub use combinator::*; -pub use combinator_ext::*; -pub use not::*; diff --git a/src/.experimenting/v11/combinators/all.rs b/src/.experimenting/v11/combinators/all.rs deleted file mode 100644 index 9b7921e..0000000 --- a/src/.experimenting/v11/combinators/all.rs +++ /dev/null @@ -1,50 +0,0 @@ -use crate::{assertions::Assertion, AssertionResult}; - -use super::AssertionCombinator; - -#[derive(Clone, Debug)] -pub struct AllCombinator { - inner: Inner, -} - -impl AllCombinator { - simple_ctor!(new(inner: Inner)); -} - -impl AssertionCombinator for AllCombinator -where - Inner: AssertionCombinator>, - Inner::NextTarget: IntoIterator, - A: Assertion<::Item>, -{ - type NextTarget = ::Item; - type Output = Inner::Output; - - #[inline] - fn apply(self, assertion: A) -> Self::Output { - self.inner.apply(AllAssertion::new(assertion)) - } -} - -#[derive(Clone, Debug)] -pub struct AllAssertion { - inner: Inner, -} - -impl AllAssertion { - simple_ctor!(new(inner: Inner)); -} - -impl Assertion for AllAssertion -where - T: IntoIterator, - Inner: Assertion, -{ - #[inline] - fn execute(&mut self, target: T) -> AssertionResult { - target - .into_iter() - .map(|value| self.inner.execute(value)) - .collect() - } -} diff --git a/src/.experimenting/v11/combinators/combinator.rs b/src/.experimenting/v11/combinators/combinator.rs deleted file mode 100644 index 305b04e..0000000 --- a/src/.experimenting/v11/combinators/combinator.rs +++ /dev/null @@ -1,11 +0,0 @@ -use crate::assertions::Assertion; - -pub trait AssertionCombinator -where - A: Assertion, -{ - type NextTarget; - type Output; - - fn apply(self, assertion: A) -> Self::Output; -} diff --git a/src/.experimenting/v11/combinators/combinator_ext.rs b/src/.experimenting/v11/combinators/combinator_ext.rs deleted file mode 100644 index 7505988..0000000 --- a/src/.experimenting/v11/combinators/combinator_ext.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::assertions::Assertion; - -use super::{AllCombinator, AssertionCombinator, NotCombinator}; - -pub trait AssertionCombinatorExt: AssertionCombinator + Sized -where - A: Assertion, -{ - #[inline] - fn not(self) -> NotCombinator { - NotCombinator::new(self) - } - - #[inline] - fn all(self) -> AllCombinator - where - Self::NextTarget: IntoIterator, - { - AllCombinator::new(self) - } -} - -impl AssertionCombinatorExt for C -where - C: AssertionCombinator, - A: Assertion, -{ -} diff --git a/src/.experimenting/v11/combinators/macros.rs b/src/.experimenting/v11/combinators/macros.rs deleted file mode 100644 index 77997db..0000000 --- a/src/.experimenting/v11/combinators/macros.rs +++ /dev/null @@ -1,10 +0,0 @@ -macro_rules! simple_ctor { - ($fn_name:ident($($field_name:ident: $field_ty:ty),*)) => { - #[inline] - pub fn $fn_name($($field_name: $field_ty),*) -> Self { - Self { - $($field_name,)* - } - } - }; -} diff --git a/src/.experimenting/v11/combinators/not.rs b/src/.experimenting/v11/combinators/not.rs deleted file mode 100644 index ef6c0f4..0000000 --- a/src/.experimenting/v11/combinators/not.rs +++ /dev/null @@ -1,48 +0,0 @@ -use crate::{assertions::Assertion, AssertionFailure, AssertionResult}; - -use super::AssertionCombinator; - -#[derive(Clone, Debug)] -pub struct NotCombinator { - inner: Inner, -} - -impl NotCombinator { - simple_ctor!(new(inner: Inner)); -} - -impl AssertionCombinator for NotCombinator -where - Inner: AssertionCombinator>, - A: Assertion, -{ - type NextTarget = Inner::NextTarget; - type Output = Inner::Output; - - #[inline] - fn apply(self, assertion: A) -> Self::Output { - self.inner.apply(NotAssertion::new(assertion)) - } -} - -#[derive(Clone, Debug)] -pub struct NotAssertion { - inner: Inner, -} - -impl NotAssertion { - simple_ctor!(new(inner: Inner)); -} - -impl Assertion for NotAssertion -where - Inner: Assertion, -{ - #[inline] - fn execute(&mut self, target: T) -> AssertionResult { - match self.inner.execute(target) { - Ok(_) => Err(AssertionFailure::builder().build("TODO")), - Err(_) => Ok(()), - } - } -} diff --git a/src/.experimenting/v11/expect.rs b/src/.experimenting/v11/expect.rs deleted file mode 100644 index df27bd3..0000000 --- a/src/.experimenting/v11/expect.rs +++ /dev/null @@ -1,98 +0,0 @@ -use std::fmt::{Display, Formatter}; - -use crate::{assertions::Assertion, combinators::AssertionCombinator, AssertionResult, SourceLoc}; - -/// 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::source_loc!(), - ::core::stringify!($e), - ) - }; -} - -/// The root of an expectation. Other expectations are built on top of this. -#[derive(Clone, Debug)] -pub struct ExpectationRoot { - target: Target, - source_info: SourceLoc, - 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: Target, source_info: SourceLoc, target_source: &'static str) -> Self { - Self { - target, - source_info, - target_source, - } - } -} - -impl AssertionCombinator for ExpectationRoot -where - A: Assertion, -{ - type NextTarget = Target; - type Output = AssertionResult; - - fn apply(self, mut assertion: A) -> Self::Output { - assertion.execute(self.target) - } -} - -#[cfg(test)] -mod tests { - use crate::{combinators::*, prelude::*}; - - #[test] - fn foo() { - // let combinator = ExpectationRoot::new([1, 2, 3], todo!(), "a"); - // let _a = expect!([1, 2, 3]).not().all(); - - let _a = expect!([1, 2, 3]).not().apply(|_n| Ok(())); - - let _a = NotCombinator::new(AllCombinator::new(expect!([1, 2, 3]))); - let result = _a.apply(|_n| Ok(())); - - // let output = expect!(1).to_equal(1); - } -} diff --git a/src/.experimenting/v11/failure.rs b/src/.experimenting/v11/failure.rs deleted file mode 100644 index ce39740..0000000 --- a/src/.experimenting/v11/failure.rs +++ /dev/null @@ -1,66 +0,0 @@ -use std::{ - error::Error, - fmt::{Display, Formatter}, -}; - -pub type AssertionResult = Result; - -/// 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 AssertionFailure { - fields: Vec<(&'static str, String)>, -} - -impl AssertionFailure { - /// Creates a builder for a new failure. - pub fn builder() -> AssertionFailureBuilder { - AssertionFailureBuilder::default() - } -} - -impl Display for AssertionFailure { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - writeln!(f, "assertion failed.")?; - for (name, value) in &self.fields { - writeln!(f, " {name}: {value}")?; - } - - Ok(()) - } -} - -impl Error for AssertionFailure {} - -/// A builder for a failure. -#[derive(Clone, Debug)] -pub struct AssertionFailureBuilder { - fields: Vec<(&'static str, String)>, -} - -impl Default for AssertionFailureBuilder { - fn default() -> Self { - Self { - fields: vec![("expected", String::new())], - } - } -} - -impl AssertionFailureBuilder { - /// 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 Display) -> Self { - self.fields.push((name, value.to_string())); - self - } - - /// Builds the error with the given expectation. - pub fn build(mut self, expectation: impl Display) -> AssertionFailure { - self.fields[0].1 = expectation.to_string(); - AssertionFailure { - fields: self.fields, - } - } -} diff --git a/src/.experimenting/v11/specialization.rs b/src/.experimenting/v11/specialization.rs deleted file mode 100644 index 452baaf..0000000 --- a/src/.experimenting/v11/specialization.rs +++ /dev/null @@ -1,6 +0,0 @@ -// pub mod at_path; -pub mod root; - -mod wrapper; - -pub use wrapper::*; diff --git a/src/.experimenting/v11/specialization/at_path.rs b/src/.experimenting/v11/specialization/at_path.rs deleted file mode 100644 index 083c3a4..0000000 --- a/src/.experimenting/v11/specialization/at_path.rs +++ /dev/null @@ -1,83 +0,0 @@ -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/.experimenting/v11/specialization/root.rs b/src/.experimenting/v11/specialization/root.rs deleted file mode 100644 index 9a46472..0000000 --- a/src/.experimenting/v11/specialization/root.rs +++ /dev/null @@ -1,77 +0,0 @@ -use std::fmt::{Debug, Display}; - -use crate::{specialization::Wrapper, ExpectationRoot, SourceLoc}; - -pub struct __ExpectersOtherTag; - -impl __ExpectersOtherTag { - #[inline] - pub fn new_root( - target: T, - source_info: SourceLoc, - target_source: &'static str, - ) -> ExpectationRoot { - ExpectationRoot::new(target, source_info, target_source) - } -} - -pub trait __ExpectersOtherKind { - #[inline] - fn __expecters_kind(&self) -> __ExpectersOtherTag { - __ExpectersOtherTag - } -} - -impl __ExpectersOtherKind for &Wrapper<&T> {} - -pub struct __ExpectersDebugTag; - -impl __ExpectersDebugTag { - #[inline] - pub fn new_root( - target: T, - source_info: SourceLoc, - target_source: &'static str, - ) -> ExpectationRoot - where - T: Debug, - { - // TODO: use `format!("{target:?}")` - ExpectationRoot::new(target, source_info, target_source) - } -} - -pub trait __ExpectersDebugKind { - #[inline] - fn __expecters_kind(&self) -> __ExpectersDebugTag { - __ExpectersDebugTag - } -} - -impl __ExpectersDebugKind for &&Wrapper<&T> where T: Debug {} - -pub struct __ExpectersDisplayTag; - -impl __ExpectersDisplayTag { - #[inline] - pub fn new_root( - target: T, - source_info: SourceLoc, - target_source: &'static str, - ) -> ExpectationRoot - where - T: Display, - { - // TODO: use `format!("{target}")` - ExpectationRoot::new(target, source_info, target_source) - } -} - -pub trait __ExpectersDisplayKind { - #[inline] - fn __expecters_kind(&self) -> __ExpectersDisplayTag { - __ExpectersDisplayTag - } -} - -impl __ExpectersDisplayKind for &&&Wrapper<&T> where T: Display {} diff --git a/src/.experimenting/v11/specialization/wrapper.rs b/src/.experimenting/v11/specialization/wrapper.rs deleted file mode 100644 index 44defbc..0000000 --- a/src/.experimenting/v11/specialization/wrapper.rs +++ /dev/null @@ -1,2 +0,0 @@ -/// Wrapper struct used to specialize macros. -pub struct Wrapper(pub T); diff --git a/src/.experimenting/v3.rs b/src/.experimenting/v3.rs deleted file mode 100644 index c60bbe3..0000000 --- a/src/.experimenting/v3.rs +++ /dev/null @@ -1,372 +0,0 @@ -use std::{ - fmt::{self, Display, Formatter}, - future::{ready, Future}, - marker::PhantomData, - pin::Pin, - sync::Arc, -}; - -use crate::combinators2::Combinator; - -#[derive(Debug, Default)] -pub struct AssertionFailure { - fields: Vec<(String, String)>, -} - -/// A type which builds an assertion to execute on a value. -pub trait AssertionCombinator: Sized -where - A: Assertion, -{ - // /// The type of value this combinator expects assertions to be executed on. - // /// This constrains the type of assertions that can be executed on this - // /// combinator to just those which can be executed on types this combinator - // /// can provide to them. - // // Keep this as an associated type - it helps with error messages and - // // intellisense! - // // This flows "forwards" - it's the type that gets passed to the next item - // // in the chain, whether that's a combinator or assertion - // type Target; - - type NextInput; - - /// The transformed assertion, given an assertion as input. This is the type - /// returned when transforming an assertion with this combinator, and is - /// itself a type of assertion. - // This flows "backwards" - it constrains the kinds of combinators this - // combinator can be applied to, since `TransformedAssertion` is itself - // only an assertion in certain circumstances. The target of the transformed - // assertion may be different than `Self::Target` because, for example, this - // assertion may take an iterator as input, but pass an item in that - // iterator to the next item in the chain. - type Transformed: Assertion; - - /// Builds an assertion using this combinator. - fn build(self, assertion: A) -> Self::Transformed; - - fn not(self) -> NotCombinator - where - NotCombinator: Combinator, - { - NotCombinator { prev: self } - } - - fn all(self) -> AllCombinator { - AllCombinator { prev: self } - } - - fn to_satisfy(self, f: F) -> Self::Transformed - where - Self: Combinator::Input>>, - Self::Transformed: Assertion>, - - F: FnOnce( - ::Input, - ) -> ::Output, - { - self.build(SimpleAssertion::new("a", f)) - } -} - -/// Performs a validation on a value. The [`Display`] implementation should -/// output the predicate this assertion expects to be true of the value. -pub trait Assertion: Display + Sized { - type Input; - type Output; - - /// Execute the assertion on a target value. - fn assert(self, target: Self::Input) -> Self::Output; - - fn not(self) -> NotAssertion - where - NotAssertion: Assertion, - { - NotAssertion(self) - } -} - -#[derive(Clone, Debug)] -pub struct SimpleAssertion { - expectation: Arc, - predicate: fn(T) -> Result<(), AssertionFailure>, -} - -impl SimpleAssertion { - pub fn new( - expectation: impl ToString, - predicate: fn(T) -> Result<(), AssertionFailure>, - ) -> Self { - Self { - expectation: expectation.to_string().into(), - predicate, - } - } -} - -impl Display for SimpleAssertion { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.expectation) - } -} - -impl Assertion for SimpleAssertion { - type Input = T; - type Output = Result<(), AssertionFailure>; - - fn assert(self, target: Self::Input) -> Result<(), AssertionFailure> { - (self.predicate)(target) - } -} - -///////// - -#[derive(Clone, Debug)] -pub struct AssertionRoot { - target: T, -} - -impl AssertionCombinator for AssertionRoot -where - A: Assertion, -{ - type NextInput = T; - type Transformed = RootAssertion; - - fn build(self, assertion: A) -> Self::Transformed { - RootAssertion { - target: self.target, - next: assertion, - } - } -} - -#[derive(Clone, Debug)] -pub struct RootAssertion { - target: T, - next: Next, -} - -impl Display for RootAssertion -where - Next: Display, -{ - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - writeln!(f, "{}", self.next) - } -} - -impl Assertion for RootAssertion -where - Next: Assertion, -{ - type Input = (); - - type Output = Next::Output; - - fn assert(self, _target: Self::Input) -> Self::Output { - self.next.assert(self.target) - } -} - -#[derive(Clone, Debug)] -pub struct NotCombinator { - prev: Prev, -} - -impl AssertionCombinator for NotCombinator -where - A: Assertion>, - Prev: AssertionCombinator>, -{ - type NextInput = Prev::NextInput; - type Transformed = Prev::Transformed; - - fn build(self, assertion: A) -> Self::Transformed { - self.prev.build(NotAssertion(assertion)) - } -} - -#[derive(Clone, Debug)] -pub struct NotAssertion(Next); - -impl Display for NotAssertion -where - Next: Display, -{ - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "the following to not be satisfied: {}", self.0) - } -} - -impl Assertion for NotAssertion -where - Next: Assertion>, -{ - type Input = Next::Input; - type Output = Next::Output; - - fn assert(self, target: Self::Input) -> Result<(), AssertionFailure> { - match self.0.assert(target) { - Ok(_) => Err(AssertionFailure::default()), - Err(_) => todo!(), - } - } -} - -pub struct AllCombinator { - prev: Prev, -} - -impl AssertionCombinator for AllCombinator -where - A: Assertion, -{ - type NextInput = ::Item; - type Transformed = AllAssertion; - - fn build(self, assertion: A) -> Self::Transformed { - AllAssertion { - next: assertion, - marker: PhantomData, - } - } -} - -#[derive(Clone, Debug)] -pub struct AllAssertion { - marker: PhantomData, - next: Next, -} - -impl Display for AllAssertion -where - Next: Display, -{ - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - todo!() - } -} - -impl Assertion for AllAssertion -where - I: IntoIterator, - Next: Assertion + Clone, - Result<(), AssertionFailure>: FromIterator, -{ - type Input = I; - type Output = Result<(), AssertionFailure>; - - fn assert(self, target: Self::Input) -> Self::Output { - target - .into_iter() - .map(|target| self.next.clone().assert(target)) - .collect() - } -} - -#[derive(Clone, Debug)] -pub struct MapAssertion { - next: Next, - f: fn(I) -> O, -} - -impl Display for MapAssertion -where - Next: Display, -{ - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - todo!() - } -} - -impl Assertion for MapAssertion -where - Next: Assertion, -{ - type Input = I; - type Output = Next::Output; - - fn assert(self, target: Self::Input) -> Self::Output { - self.next.assert((self.f)(target)) - } -} - -#[derive(Clone, Debug)] -pub struct WhenReadyAssertion { - next: Next, - marker: PhantomData, -} - -impl Display for WhenReadyAssertion -where - Next: Display, -{ - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - todo!() - } -} - -impl Assertion for WhenReadyAssertion -where - I: Future + Send + 'static, - Next: Assertion + Send + 'static, -{ - type Input = I; - type Output = Pin + Send>>; - - fn assert(self, target: Self::Input) -> Self::Output { - Box::pin(async move { self.next.assert(target.await) }) - } -} - -async fn foo() { - let assertion = RootAssertion { - target: ready([1, 2, 3]), - next: WhenReadyAssertion { - marker: PhantomData, - next: AllAssertion { - marker: PhantomData, - next: SimpleAssertion::new("non-zero", |value| { - if value == 0 { - Err(AssertionFailure::default()) - } else { - Ok(()) - } - }), - }, - }, - }; - let result = assertion.assert(()).await; - - let assertion = RootAssertion { - target: [1, 2, 3], - next: AllAssertion { - marker: PhantomData, - next: SimpleAssertion::new("non-zero", |value| { - if value == 0 { - Err(AssertionFailure::default()) - } else { - Ok(()) - } - }), - }, - }; - let result = assertion.assert(()); - - let combinator = AssertionRoot { target: [1, 2, 3] } - .not() - .to_satisfy(|values: [i32; 3]| values.is_empty()); - - AllCombinator { - marker: PhantomData, - prev: AssertionRoot { target: [1, 2, 3] }, - }; - let assertion = combinator.build(SimpleAssertion::new("non-zero", |value| { - if value == 0 { - Err(AssertionFailure::default()) - } else { - Ok(()) - } - })); - let result = assertion.assert(()); -} diff --git a/src/.experimenting/v4.rs b/src/.experimenting/v4.rs deleted file mode 100644 index 6452d8c..0000000 --- a/src/.experimenting/v4.rs +++ /dev/null @@ -1,341 +0,0 @@ -use std::{ - fmt::{self, Display, Formatter}, - future::{ready, Future}, - marker::PhantomData, - pin::Pin, - sync::Arc, -}; - -#[derive(Debug, Default)] -pub struct AssertionFailure { - fields: Vec<(String, String)>, -} - -/// A type which builds an assertion to execute on a value. -pub trait AssertionCombinator: Sized { - type NextInput; - - type Assertion - where - A: Assertion; - - /// Builds an assertion using this combinator. - fn build(self, assertion: A) -> Self::Assertion - where - A: Assertion; - - fn not(self) -> NotCombinator { - NotCombinator { prev: self } - } - - fn all(self) -> AllCombinator { - AllCombinator { prev: self } - } -} - -/// Performs a validation on a value. The [`Display`] implementation should -/// output the predicate this assertion expects to be true of the value. -pub trait Assertion: Display + Sized { - type Output; - - /// Execute the assertion on a target value. - fn assert(self, target: Input) -> Self::Output; -} - -#[derive(Clone, Debug)] -pub struct SimpleAssertion { - expectation: Arc, - predicate: F, -} - -impl SimpleAssertion { - pub fn new(expectation: impl ToString, predicate: F) -> Self { - Self { - expectation: expectation.to_string().into(), - predicate, - } - } -} - -impl Display for SimpleAssertion { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.expectation) - } -} - -impl Assertion for SimpleAssertion -where - F: FnOnce(I) -> O, -{ - type Output = O; - - fn assert(self, target: I) -> Self::Output { - (self.predicate)(target) - } -} - -///////// - -#[derive(Clone, Debug)] -pub struct AssertionRoot { - target: T, -} - -impl AssertionCombinator for AssertionRoot { - type NextInput = T; - - type Assertion = RootAssertion - where - A: Assertion; - - fn build(self, assertion: A) -> Self::Assertion - where - A: Assertion, - { - RootAssertion { - target: self.target, - next: assertion, - } - } -} - -#[derive(Clone, Debug)] -pub struct RootAssertion { - target: T, - next: Next, -} - -impl Display for RootAssertion -where - Next: Display, -{ - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - writeln!(f, "{}", self.next) - } -} - -impl Assertion<()> for RootAssertion -where - Next: Assertion, -{ - type Output = Next::Output; - - fn assert(self, _target: ()) -> Self::Output { - self.next.assert(self.target) - } -} - -#[derive(Clone, Debug)] -pub struct NotCombinator { - prev: Prev, -} - -impl AssertionCombinator for NotCombinator -where - Prev: AssertionCombinator, -{ - type NextInput = Prev::NextInput; - - type Assertion = Prev::Assertion> - where - A: Assertion; - - fn build(self, assertion: A) -> Self::Assertion - where - A: Assertion, - { - NotAssertion(self.prev.build(assertion)) - } -} - -fn a() { - let assertion = NotCombinator { - prev: AssertionRoot { target: 1 }, - } - .build(SimpleAssertion::new("not zero", |value| value != 0)); - - let _result = assertion.assert(()); -} - -#[derive(Clone, Debug)] -pub struct NotAssertion(Next); - -impl Display for NotAssertion -where - Next: Display, -{ - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "the following to not be satisfied: {}", self.0) - } -} - -impl Assertion for NotAssertion -where - Next: Assertion>, -{ - type Output = Next::Output; - - fn assert(self, target: Input) -> Self::Output { - match self.0.assert(target) { - Ok(_) => Err(AssertionFailure::default()), - Err(_) => todo!(), - } - } -} - -pub struct AllCombinator { - prev: Prev, -} - -impl AssertionCombinator for AllCombinator -where - Prev: AssertionCombinator, - Prev::NextInput: IntoIterator, -{ - type NextInput = ::Item; - - type Assertion = AllAssertion - where - A: Assertion; - - fn build(self, assertion: A) -> Self::Assertion - where - A: Assertion, - { - self.prev.build(AllAssertion { next: assertion }) - } -} - -#[derive(Clone, Debug)] -pub struct AllAssertion { - next: Next, -} - -impl Display for AllAssertion -where - Next: Display, -{ - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - todo!() - } -} - -impl Assertion for AllAssertion -where - I: IntoIterator, - Next: Assertion + Clone, - Result<(), AssertionFailure>: FromIterator, -{ - type Input = I; - type Output = Result<(), AssertionFailure>; - - fn assert(self, target: Self::Input) -> Self::Output { - target - .into_iter() - .map(|target| self.next.clone().assert(target)) - .collect() - } -} - -#[derive(Clone, Debug)] -pub struct MapAssertion { - next: Next, - f: fn(I) -> O, -} - -impl Display for MapAssertion -where - Next: Display, -{ - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - todo!() - } -} - -impl Assertion for MapAssertion -where - Next: Assertion, -{ - type Input = I; - type Output = Next::Output; - - fn assert(self, target: Self::Input) -> Self::Output { - self.next.assert((self.f)(target)) - } -} - -#[derive(Clone, Debug)] -pub struct WhenReadyAssertion { - next: Next, - marker: PhantomData, -} - -impl Display for WhenReadyAssertion -where - Next: Display, -{ - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - todo!() - } -} - -impl Assertion for WhenReadyAssertion -where - I: Future + Send + 'static, - Next: Assertion + Send + 'static, -{ - type Input = I; - type Output = Pin + Send>>; - - fn assert(self, target: Self::Input) -> Self::Output { - Box::pin(async move { self.next.assert(target.await) }) - } -} - -async fn foo() { - let assertion = RootAssertion { - target: ready([1, 2, 3]), - next: WhenReadyAssertion { - marker: PhantomData, - next: AllAssertion { - marker: PhantomData, - next: SimpleAssertion::new("non-zero", |value| { - if value == 0 { - Err(AssertionFailure::default()) - } else { - Ok(()) - } - }), - }, - }, - }; - let result = assertion.assert(()).await; - - let assertion = RootAssertion { - target: [1, 2, 3], - next: AllAssertion { - marker: PhantomData, - next: SimpleAssertion::new("non-zero", |value| { - if value == 0 { - Err(AssertionFailure::default()) - } else { - Ok(()) - } - }), - }, - }; - let result = assertion.assert(()); - - let combinator = AllCombinator { - marker: PhantomData, - prev: AssertionRoot { target: [1, 2, 3] }, - }; - let assertion = combinator.build(SimpleAssertion::new("non-zero", |value| { - if value == 0 { - Err(AssertionFailure::default()) - } else { - Ok(()) - } - })); - let result = assertion.assert(()); -} diff --git a/src/.experimenting/v5.rs b/src/.experimenting/v5.rs deleted file mode 100644 index 4fe22dc..0000000 --- a/src/.experimenting/v5.rs +++ /dev/null @@ -1,333 +0,0 @@ -use std::{ - fmt::{self, Display, Formatter}, - future::{ready, Future}, - marker::PhantomData, - pin::Pin, - sync::Arc, -}; - -// - combinators are built in the direction data flows -// - assertions are built in the direction the assertion is wrapped -// - this is opposite the direction combinators are built in -// - need to know the type of the next input to constrain the chain -// - ex should be possible to know when `.all()` is applicable -// - already know the input type since the combinator wraps the root value -// - no need to be generic over it as a result - -#[derive(Debug, Default)] -pub struct AssertionFailure { - fields: Vec<(String, String)>, -} - -/// A type which builds an assertion to execute on a value. -pub trait AssertionCombinator: Sized -where - Next: Assertion, -{ - type NextInput; - type Assertion; - - /// Builds an assertion using this combinator. - fn build(self, assertion: Next) -> Self::Assertion; - - fn not(self) -> NotCombinator { - NotCombinator { prev: self } - } - - fn all(self) -> AllCombinator { - AllCombinator { prev: self } - } -} - -/// Performs a validation on a value. The [`Display`] implementation should -/// output the predicate this assertion expects to be true of the value. -pub trait Assertion: Display + Sized { - type Output; - - /// Execute the assertion on a target value. - fn assert(self, target: Input) -> Self::Output; -} - -#[derive(Clone, Debug)] -pub struct SimpleAssertion { - expectation: Arc, - predicate: F, -} - -impl SimpleAssertion { - pub fn new(expectation: impl ToString, predicate: F) -> Self { - Self { - expectation: expectation.to_string().into(), - predicate, - } - } -} - -impl Display for SimpleAssertion { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.expectation) - } -} - -impl Assertion for SimpleAssertion -where - F: FnOnce(I) -> O, -{ - type Output = O; - - fn assert(self, target: I) -> Self::Output { - (self.predicate)(target) - } -} - -///////// - -#[derive(Clone, Debug)] -pub struct AssertionRoot { - target: T, -} - -impl AssertionCombinator for AssertionRoot -where - A: Assertion, -{ - type NextInput = T; - type Assertion = RootAssertion; - - fn build(self, assertion: A) -> Self::Assertion { - RootAssertion { - target: self.target, - next: assertion, - } - } -} - -#[derive(Clone, Debug)] -pub struct RootAssertion { - target: T, - next: Next, -} - -impl Display for RootAssertion -where - Next: Display, -{ - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - writeln!(f, "{}", self.next) - } -} - -impl Assertion<()> for RootAssertion -where - Next: Assertion, -{ - type Output = Next::Output; - - fn assert(self, _target: ()) -> Self::Output { - self.next.assert(self.target) - } -} - -#[derive(Clone, Debug)] -pub struct NotCombinator { - prev: Prev, -} - -impl AssertionCombinator for NotCombinator -where - Prev: AssertionCombinator>, - A: Assertion>, -{ - type NextInput = Prev::NextInput; - type Assertion = Prev::Assertion; - - fn build(self, assertion: A) -> Self::Assertion { - self.prev.build(NotAssertion(assertion)) - } -} - -#[derive(Clone, Debug)] -pub struct NotAssertion(Next); - -impl Display for NotAssertion -where - Next: Display, -{ - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "the following to not be satisfied: {}", self.0) - } -} - -impl Assertion for NotAssertion -where - Next: Assertion>, -{ - type Output = Next::Output; - - fn assert(self, target: Input) -> Self::Output { - match self.0.assert(target) { - Ok(_) => Err(AssertionFailure::default()), - Err(_) => todo!(), - } - } -} - -pub struct AllCombinator { - prev: Prev, -} - -impl AssertionCombinator for AllCombinator -where - Prev: AssertionCombinator>, - Prev::NextInput: IntoIterator, - A: Assertion<::Item> + Clone, -{ - type NextInput = ::Item; - type Assertion = Prev::Assertion; - - fn build(self, assertion: A) -> Self::Assertion { - self.prev.build(AllAssertion { next: assertion }) - } -} - -#[derive(Clone, Debug)] -pub struct AllAssertion { - next: Next, -} - -impl Display for AllAssertion -where - Next: Display, -{ - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - todo!() - } -} - -impl Assertion for AllAssertion -where - I: IntoIterator, - Next: Assertion + Clone, - Result<(), AssertionFailure>: FromIterator, -{ - type Output = Result<(), AssertionFailure>; - - fn assert(self, target: I) -> Self::Output { - target - .into_iter() - .map(|target| self.next.clone().assert(target)) - .collect() - } -} - -fn a() { - let assertion = NotCombinator { - prev: AssertionRoot { target: 1 }, - } - .build(SimpleAssertion::new("not zero", |value| value != 0)); - - let _result = assertion.assert(()); -} - -// #[derive(Clone, Debug)] -// pub struct MapAssertion { -// next: Next, -// f: fn(I) -> O, -// } - -// impl Display for MapAssertion -// where -// Next: Display, -// { -// fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { -// todo!() -// } -// } - -// impl Assertion for MapAssertion -// where -// Next: Assertion, -// { -// type Input = I; -// type Output = Next::Output; - -// fn assert(self, target: Self::Input) -> Self::Output { -// self.next.assert((self.f)(target)) -// } -// } - -// #[derive(Clone, Debug)] -// pub struct WhenReadyAssertion { -// next: Next, -// marker: PhantomData, -// } - -// impl Display for WhenReadyAssertion -// where -// Next: Display, -// { -// fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { -// todo!() -// } -// } - -// impl Assertion for WhenReadyAssertion -// where -// I: Future + Send + 'static, -// Next: Assertion + Send + 'static, -// { -// type Input = I; -// type Output = Pin + Send>>; - -// fn assert(self, target: Self::Input) -> Self::Output { -// Box::pin(async move { self.next.assert(target.await) }) -// } -// } - -// async fn foo() { -// let assertion = RootAssertion { -// target: ready([1, 2, 3]), -// next: WhenReadyAssertion { -// marker: PhantomData, -// next: AllAssertion { -// marker: PhantomData, -// next: SimpleAssertion::new("non-zero", |value| { -// if value == 0 { -// Err(AssertionFailure::default()) -// } else { -// Ok(()) -// } -// }), -// }, -// }, -// }; -// let result = assertion.assert(()).await; - -// let assertion = RootAssertion { -// target: [1, 2, 3], -// next: AllAssertion { -// marker: PhantomData, -// next: SimpleAssertion::new("non-zero", |value| { -// if value == 0 { -// Err(AssertionFailure::default()) -// } else { -// Ok(()) -// } -// }), -// }, -// }; -// let result = assertion.assert(()); - -// let combinator = AllCombinator { -// marker: PhantomData, -// prev: AssertionRoot { target: [1, 2, 3] }, -// }; -// let assertion = combinator.build(SimpleAssertion::new("non-zero", |value| { -// if value == 0 { -// Err(AssertionFailure::default()) -// } else { -// Ok(()) -// } -// })); -// let result = assertion.assert(()); -// } diff --git a/src/.experimenting/v6.rs b/src/.experimenting/v6.rs deleted file mode 100644 index 3abf033..0000000 --- a/src/.experimenting/v6.rs +++ /dev/null @@ -1,180 +0,0 @@ -mod all; -mod assertion; -mod combinator; -mod error; -mod not; - -pub use all::*; -pub use assertion::*; -pub use combinator::*; -pub use error::*; -pub use not::*; - -use std::fmt::{self, Display, Formatter}; - -///////// - -#[derive(Clone, Debug)] -pub struct AssertionRoot { - target: T, -} - -impl AssertionCombinator for AssertionRoot -where - A: Assertion, -{ - type NextInput = T; - type Assertion = RootAssertion; - - fn build(self, assertion: A) -> Self::Assertion { - RootAssertion { - target: self.target, - next: assertion, - } - } -} - -#[derive(Clone, Debug)] -pub struct RootAssertion { - target: T, - next: Next, -} - -impl Display for RootAssertion -where - Next: Display, -{ - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - writeln!(f, "{}", self.next) - } -} - -impl Assertion<()> for RootAssertion -where - Next: Assertion, -{ - type Output = Next::Output; - - fn assert(self, _target: ()) -> Self::Output { - self.next.assert(self.target) - } -} - -fn a() { - let root = AssertionRoot { target: [1, 2, 3] }; - let assertion = root - .all() - .not() - .build(SimpleAssertion::new("not zero", |value| { - if value != 0 { - Ok(()) - } else { - Err(AssertionError::default()) - } - })); - - let _result = assertion.assert(()); -} - -// #[derive(Clone, Debug)] -// pub struct MapAssertion { -// next: Next, -// f: fn(I) -> O, -// } - -// impl Display for MapAssertion -// where -// Next: Display, -// { -// fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { -// todo!() -// } -// } - -// impl Assertion for MapAssertion -// where -// Next: Assertion, -// { -// type Input = I; -// type Output = Next::Output; - -// fn assert(self, target: Self::Input) -> Self::Output { -// self.next.assert((self.f)(target)) -// } -// } - -// #[derive(Clone, Debug)] -// pub struct WhenReadyAssertion { -// next: Next, -// marker: PhantomData, -// } - -// impl Display for WhenReadyAssertion -// where -// Next: Display, -// { -// fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { -// todo!() -// } -// } - -// impl Assertion for WhenReadyAssertion -// where -// I: Future + Send + 'static, -// Next: Assertion + Send + 'static, -// { -// type Input = I; -// type Output = Pin + Send>>; - -// fn assert(self, target: Self::Input) -> Self::Output { -// Box::pin(async move { self.next.assert(target.await) }) -// } -// } - -// async fn foo() { -// let assertion = RootAssertion { -// target: ready([1, 2, 3]), -// next: WhenReadyAssertion { -// marker: PhantomData, -// next: AllAssertion { -// marker: PhantomData, -// next: SimpleAssertion::new("non-zero", |value| { -// if value == 0 { -// Err(AssertionFailure::default()) -// } else { -// Ok(()) -// } -// }), -// }, -// }, -// }; -// let result = assertion.assert(()).await; - -// let assertion = RootAssertion { -// target: [1, 2, 3], -// next: AllAssertion { -// marker: PhantomData, -// next: SimpleAssertion::new("non-zero", |value| { -// if value == 0 { -// Err(AssertionFailure::default()) -// } else { -// Ok(()) -// } -// }), -// }, -// }; -// let result = assertion.assert(()); - -// let combinator = AllCombinator { -// marker: PhantomData, -// prev: AssertionRoot { target: [1, 2, 3] }, -// }; -// let assertion = combinator.build(SimpleAssertion::new("non-zero", |value| { -// if value == 0 { -// Err(AssertionFailure::default()) -// } else { -// Ok(()) -// } -// })); -// let result = assertion.assert(()); -// } diff --git a/src/.experimenting/v6/all.rs b/src/.experimenting/v6/all.rs deleted file mode 100644 index 56e7d40..0000000 --- a/src/.experimenting/v6/all.rs +++ /dev/null @@ -1,67 +0,0 @@ -use std::fmt::{self, Display, Formatter}; - -use super::{Assertion, AssertionCombinator, AssertionError}; - -pub struct AllCombinator { - prev: Prev, -} - -impl AllCombinator { - /// Creates a new [`AllCombinator`]. - #[inline] - pub fn new(prev: Prev) -> Self { - Self { prev } - } -} - -impl AssertionCombinator for AllCombinator -where - Prev: AssertionCombinator>, - Prev::NextInput: IntoIterator, - AllAssertion: Assertion, -{ - type NextInput = ::Item; - type Assertion = Prev::Assertion; - - fn build(self, assertion: Next) -> Self::Assertion { - self.prev.build(AllAssertion::new(assertion)) - } -} - -#[derive(Clone, Debug)] -pub struct AllAssertion { - next: Next, -} - -impl AllAssertion { - /// Creates a new [`AllAssertion`]. - #[inline] - pub fn new(next: Next) -> Self { - Self { next } - } -} - -impl Display for AllAssertion -where - Next: Display, -{ - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "for each inner value, {}", self.next) - } -} - -impl Assertion for AllAssertion -where - Input: IntoIterator, - Next: Assertion + Clone, - Result<(), AssertionError>: FromIterator, -{ - type Output = Result<(), AssertionError>; - - fn assert(self, target: Input) -> Self::Output { - target - .into_iter() - .map(|item| self.next.clone().assert(item)) - .collect() - } -} diff --git a/src/.experimenting/v6/assertion.rs b/src/.experimenting/v6/assertion.rs deleted file mode 100644 index 7b7cb73..0000000 --- a/src/.experimenting/v6/assertion.rs +++ /dev/null @@ -1,90 +0,0 @@ -use std::{ - fmt::{self, Display, Formatter}, - sync::Arc, -}; - -use super::AssertionError; - -/// Performs a validation on a value. The [`Display`] implementation should -/// output the predicate this assertion expects to be true of the value. -pub trait Assertion: Display + Sized { - /// The output of this assertion. - type Output: AssertionOutput; - - /// Execute the assertion on a target value. - fn assert(self, target: Input) -> Self::Output; -} - -/// An output from executing an assertion. -/// -/// This generally represents some type that internally contains some -/// representation that can be converted to/from a -/// [`Result<(), AssertionFailure>`]. Since not all assertions can return a -/// [`Result`] directly, this allows those assertions to return something more -/// suitable for that assertion. -/// -/// For example, the [`WhenReadyCombinator`] returns a [`Future`] which can be -/// `.await`ed into a [`Result`]. -pub trait AssertionOutput { - /// The inverted result type. This is returned by - /// [`map`](AssertionOutput::map). - type Mapped: AssertionOutput - where - F: FnOnce(Result<(), AssertionError>) -> Result<(), AssertionError>; - - /// Maps the result represented by this output to a new result. - /// - /// This enables combinators to transform the result of the assertion - /// without necessarily needing to know the type of the assertion. For - /// example, the [`NotCombinator`] uses this method to transform a success - /// into a failure and a failure into a success. - fn map(self, f: F) -> Self::Mapped - where - F: FnOnce(Result<(), AssertionError>) -> Result<(), AssertionError>; -} - -impl AssertionOutput for Result<(), AssertionError> { - type Mapped = Self - where - F: FnOnce(Result<(), AssertionError>) -> Result<(), AssertionError>; - - fn map(self, f: F) -> Self::Mapped - where - F: FnOnce(Result<(), AssertionError>) -> Result<(), AssertionError>, - { - f(self) - } -} - -#[derive(Clone, Debug)] -pub struct SimpleAssertion { - expectation: Arc, - predicate: F, -} - -impl SimpleAssertion { - pub fn new(expectation: impl ToString, predicate: F) -> Self { - Self { - expectation: expectation.to_string().into(), - predicate, - } - } -} - -impl Display for SimpleAssertion { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.expectation) - } -} - -impl Assertion for SimpleAssertion -where - F: FnOnce(I) -> O, - O: AssertionOutput, -{ - type Output = O; - - fn assert(self, target: I) -> Self::Output { - (self.predicate)(target) - } -} diff --git a/src/.experimenting/v6/combinator.rs b/src/.experimenting/v6/combinator.rs deleted file mode 100644 index 30dfa0b..0000000 --- a/src/.experimenting/v6/combinator.rs +++ /dev/null @@ -1,47 +0,0 @@ -use super::{AllCombinator, NotCombinator}; - -/// A type which builds an assertion to execute on a value. -pub trait AssertionCombinator: Sized { - /// The input passed to the *next assertion* in the chain. - /// - /// This combinator's associated assertion may receive a different type. For - /// example, this combinator may accept some (known) iterable value, like an - /// `[i32; 4]`, and pass an [`i32`] to the next assertion in the chain. This - /// type would then be [`i32`]. - type NextInput; - - /// The associated assertion. This is what gets built when this combinator - /// is applied to another assertion. - // This type cannot be a GAT since each combinator may have different type - // bounds on the assertion. For example, `NotAssertion` may require - // that `Next` returns a `Result<(), AssertionFailure>` to implement the - // `Assertion` trait. - // - // In those cases, the bound would either need to be specified in the `impl` - // block for the combinator, or on the `impl` block for the assertion. If - // this associated type is constrained such that it must be an assertion, - // then the combinator's `impl` block must enforce the constraint as well. - type Assertion; - - /// Builds an assertion using this combinator. - fn build(self, assertion: Next) -> Self::Assertion; - - // Since we don't know yet what assertion will be passed into the - // `NotCombinator` type, we can't specify the `Next` parameter to constrain - // this method such that it can only be called when that particular impl's - // bounds are satisfied. We also don't yet know what the return value will - // be for the final assertion because it hasn't been fully built yet. - // - // There needs to be a way to constrain the assertions chained onto this so - // that they satisfy the bounds on the `NotCombinator`'s assertion. - fn not(self) -> NotCombinator { - NotCombinator::new(self) - } - - fn all(self) -> AllCombinator - where - Self::NextInput: IntoIterator, - { - AllCombinator::new(self) - } -} diff --git a/src/.experimenting/v6/error.rs b/src/.experimenting/v6/error.rs deleted file mode 100644 index 7043626..0000000 --- a/src/.experimenting/v6/error.rs +++ /dev/null @@ -1,26 +0,0 @@ -// - combinators are built in the direction data flows -// - eg. All>>> -// - assertions are built in the direction the assertion is wrapped -// - eg. Root>>>> -// - this is opposite the direction combinators are built in -// - combinators: -// - need to know the type of the next input to constrain the chain -// - ex should be possible to know when `.all()` is applicable -// - already know the input type since the combinator wraps the root value -// - no need to be generic over it as a result -// - making them generic over the "next assertion" at the trait level makes it -// difficult to create bounds over the trait since a known assertion type -// must be provided to get the associated types -// - assertion type must be generic over the "next assertion" since it wraps -// the assertion -// - assertions: -// - do not need to be generic over the input type because they already know -// the type they are being executed on -// - have variable return types - `()`, `impl Future`, etc -// - `NotAssertion` needs to know how to invert the return type between -// success and failure -// - can use a trait for this rather than requiring a fixed output type -#[derive(Debug, Default)] -pub struct AssertionError { - fields: Vec<(String, String)>, -} diff --git a/src/.experimenting/v6/not.rs b/src/.experimenting/v6/not.rs deleted file mode 100644 index f3f7bb6..0000000 --- a/src/.experimenting/v6/not.rs +++ /dev/null @@ -1,69 +0,0 @@ -use std::fmt::{self, Display, Formatter}; - -use super::{Assertion, AssertionCombinator, AssertionError, AssertionOutput}; - -/// Inverts the result of an assertion. -#[derive(Clone, Debug)] -pub struct NotCombinator { - prev: Prev, -} - -impl NotCombinator { - /// Creates a new [`NotCombinator`]. - #[inline] - pub fn new(prev: Prev) -> Self { - Self { prev } - } -} - -impl AssertionCombinator for NotCombinator -where - Prev: AssertionCombinator>, - NotAssertion: Assertion, -{ - type NextInput = Prev::NextInput; - type Assertion = Prev::Assertion; - - fn build(self, assertion: Next) -> Self::Assertion { - self.prev.build(NotAssertion::new(assertion)) - } -} - -/// Inverts the result of the next assertion. -#[derive(Clone, Debug)] -pub struct NotAssertion { - next: Next, -} - -impl NotAssertion { - /// Creates a new [`NotAssertion`]. - #[inline] - pub fn new(next: Next) -> Self { - NotAssertion { next } - } -} - -impl Display for NotAssertion -where - Next: Display, -{ - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "the following to not be satisfied: {}", self.next) - } -} - -impl Assertion for NotAssertion -where - Next: Assertion, -{ - type Output = ::Mapped< - fn(Result<(), AssertionError>) -> Result<(), AssertionError>, - >; - - fn assert(self, target: Input) -> Self::Output { - self.next.assert(target).map(|result| match result { - Ok(_) => Err(AssertionError::default()), - Err(_) => Ok(()), - }) - } -} diff --git a/src/.experimenting/v7.rs b/src/.experimenting/v7.rs deleted file mode 100644 index 3abf033..0000000 --- a/src/.experimenting/v7.rs +++ /dev/null @@ -1,180 +0,0 @@ -mod all; -mod assertion; -mod combinator; -mod error; -mod not; - -pub use all::*; -pub use assertion::*; -pub use combinator::*; -pub use error::*; -pub use not::*; - -use std::fmt::{self, Display, Formatter}; - -///////// - -#[derive(Clone, Debug)] -pub struct AssertionRoot { - target: T, -} - -impl AssertionCombinator for AssertionRoot -where - A: Assertion, -{ - type NextInput = T; - type Assertion = RootAssertion; - - fn build(self, assertion: A) -> Self::Assertion { - RootAssertion { - target: self.target, - next: assertion, - } - } -} - -#[derive(Clone, Debug)] -pub struct RootAssertion { - target: T, - next: Next, -} - -impl Display for RootAssertion -where - Next: Display, -{ - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - writeln!(f, "{}", self.next) - } -} - -impl Assertion<()> for RootAssertion -where - Next: Assertion, -{ - type Output = Next::Output; - - fn assert(self, _target: ()) -> Self::Output { - self.next.assert(self.target) - } -} - -fn a() { - let root = AssertionRoot { target: [1, 2, 3] }; - let assertion = root - .all() - .not() - .build(SimpleAssertion::new("not zero", |value| { - if value != 0 { - Ok(()) - } else { - Err(AssertionError::default()) - } - })); - - let _result = assertion.assert(()); -} - -// #[derive(Clone, Debug)] -// pub struct MapAssertion { -// next: Next, -// f: fn(I) -> O, -// } - -// impl Display for MapAssertion -// where -// Next: Display, -// { -// fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { -// todo!() -// } -// } - -// impl Assertion for MapAssertion -// where -// Next: Assertion, -// { -// type Input = I; -// type Output = Next::Output; - -// fn assert(self, target: Self::Input) -> Self::Output { -// self.next.assert((self.f)(target)) -// } -// } - -// #[derive(Clone, Debug)] -// pub struct WhenReadyAssertion { -// next: Next, -// marker: PhantomData, -// } - -// impl Display for WhenReadyAssertion -// where -// Next: Display, -// { -// fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { -// todo!() -// } -// } - -// impl Assertion for WhenReadyAssertion -// where -// I: Future + Send + 'static, -// Next: Assertion + Send + 'static, -// { -// type Input = I; -// type Output = Pin + Send>>; - -// fn assert(self, target: Self::Input) -> Self::Output { -// Box::pin(async move { self.next.assert(target.await) }) -// } -// } - -// async fn foo() { -// let assertion = RootAssertion { -// target: ready([1, 2, 3]), -// next: WhenReadyAssertion { -// marker: PhantomData, -// next: AllAssertion { -// marker: PhantomData, -// next: SimpleAssertion::new("non-zero", |value| { -// if value == 0 { -// Err(AssertionFailure::default()) -// } else { -// Ok(()) -// } -// }), -// }, -// }, -// }; -// let result = assertion.assert(()).await; - -// let assertion = RootAssertion { -// target: [1, 2, 3], -// next: AllAssertion { -// marker: PhantomData, -// next: SimpleAssertion::new("non-zero", |value| { -// if value == 0 { -// Err(AssertionFailure::default()) -// } else { -// Ok(()) -// } -// }), -// }, -// }; -// let result = assertion.assert(()); - -// let combinator = AllCombinator { -// marker: PhantomData, -// prev: AssertionRoot { target: [1, 2, 3] }, -// }; -// let assertion = combinator.build(SimpleAssertion::new("non-zero", |value| { -// if value == 0 { -// Err(AssertionFailure::default()) -// } else { -// Ok(()) -// } -// })); -// let result = assertion.assert(()); -// } diff --git a/src/.experimenting/v7/all.rs b/src/.experimenting/v7/all.rs deleted file mode 100644 index cb104b0..0000000 --- a/src/.experimenting/v7/all.rs +++ /dev/null @@ -1,73 +0,0 @@ -use std::fmt::{self, Display, Formatter}; - -use super::{Assertion, AssertionCombinator, AssertionError}; - -pub struct AllCombinator { - prev: Prev, -} - -impl AllCombinator { - /// Creates a new [`AllCombinator`]. - #[inline] - pub fn new(prev: Prev) -> Self { - Self { prev } - } -} - -impl AssertionCombinator for AllCombinator -where - Prev: AssertionCombinator, - Prev::NextInput: IntoIterator, - // Result<(), AssertionError>: FromIterator, - Result<(), AssertionError>: FromIterator<::NextInput as IntoIterator>::Item>>::Output> -{ - type NextInput = ::Item; - type Assertion = Prev::Assertion> - where - Next: Assertion; - - fn build(self, assertion: Next) -> Self::Assertion - where - Next: Assertion, - { - self.prev.build(AllAssertion::new(assertion)) - } -} - -#[derive(Clone, Debug)] -pub struct AllAssertion { - next: Next, -} - -impl AllAssertion { - /// Creates a new [`AllAssertion`]. - #[inline] - pub fn new(next: Next) -> Self { - Self { next } - } -} - -impl Display for AllAssertion -where - Next: Display, -{ - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "for each inner value, {}", self.next) - } -} - -impl Assertion for AllAssertion -where - Input: IntoIterator, - Next: Assertion, - Result<(), AssertionError>: FromIterator, -{ - type Output = Result<(), AssertionError>; - - fn assert(&mut self, target: Input) -> Self::Output { - target - .into_iter() - .map(|item| self.next.assert(item)) - .collect() - } -} diff --git a/src/.experimenting/v7/assertion.rs b/src/.experimenting/v7/assertion.rs deleted file mode 100644 index 41e5a72..0000000 --- a/src/.experimenting/v7/assertion.rs +++ /dev/null @@ -1,90 +0,0 @@ -use std::{ - fmt::{self, Display, Formatter}, - sync::Arc, -}; - -use super::AssertionError; - -/// Performs a validation on a value. The [`Display`] implementation should -/// output the predicate this assertion expects to be true of the value. -pub trait Assertion: Display + Sized { - /// The output of this assertion. - type Output: AssertionOutput; - - /// Execute the assertion on a target value. - fn assert(&mut self, target: Input) -> Self::Output; -} - -/// An output from executing an assertion. -/// -/// This generally represents some type that internally contains some -/// representation that can be converted to/from a -/// [`Result<(), AssertionFailure>`]. Since not all assertions can return a -/// [`Result`] directly, this allows those assertions to return something more -/// suitable for that assertion. -/// -/// For example, the [`WhenReadyCombinator`] returns a [`Future`] which can be -/// `.await`ed into a [`Result`]. -pub trait AssertionOutput { - /// The inverted result type. This is returned by - /// [`map`](AssertionOutput::map). - type Mapped: AssertionOutput - where - F: FnOnce(Result<(), AssertionError>) -> Result<(), AssertionError>; - - /// Maps the result represented by this output to a new result. - /// - /// This enables combinators to transform the result of the assertion - /// without necessarily needing to know the type of the assertion. For - /// example, the [`NotCombinator`] uses this method to transform a success - /// into a failure and a failure into a success. - fn map(self, f: F) -> Self::Mapped - where - F: FnOnce(Result<(), AssertionError>) -> Result<(), AssertionError>; -} - -impl AssertionOutput for Result<(), AssertionError> { - type Mapped = Self - where - F: FnOnce(Result<(), AssertionError>) -> Result<(), AssertionError>; - - fn map(self, f: F) -> Self::Mapped - where - F: FnOnce(Result<(), AssertionError>) -> Result<(), AssertionError>, - { - f(self) - } -} - -#[derive(Clone, Debug)] -pub struct SimpleAssertion { - expectation: Arc, - predicate: F, -} - -impl SimpleAssertion { - pub fn new(expectation: impl ToString, predicate: F) -> Self { - Self { - expectation: expectation.to_string().into(), - predicate, - } - } -} - -impl Display for SimpleAssertion { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.expectation) - } -} - -impl Assertion for SimpleAssertion -where - F: FnMut(I) -> O, - O: AssertionOutput, -{ - type Output = O; - - fn assert(&mut self, target: I) -> Self::Output { - (self.predicate)(target) - } -} diff --git a/src/.experimenting/v7/combinator.rs b/src/.experimenting/v7/combinator.rs deleted file mode 100644 index a0c4855..0000000 --- a/src/.experimenting/v7/combinator.rs +++ /dev/null @@ -1,51 +0,0 @@ -use super::{AllCombinator, Assertion, NotCombinator}; - -/// A type which builds an assertion to execute on a value. -pub trait AssertionCombinator: Sized { - /// The input passed to the *next assertion* in the chain. - /// - /// This combinator's associated assertion may receive a different type. For - /// example, this combinator may accept some (known) iterable value, like an - /// `[i32; 4]`, and pass an [`i32`] to the next assertion in the chain. This - /// type would then be [`i32`]. - type NextInput; - - /// The associated assertion. This is what gets built when this combinator - /// is applied to another assertion. - // This type cannot be a GAT since each combinator may have different type - // bounds on the assertion. For example, `NotAssertion` may require - // that `Next` returns a `Result<(), AssertionFailure>` to implement the - // `Assertion` trait. - // - // In those cases, the bound would either need to be specified in the `impl` - // block for the combinator, or on the `impl` block for the assertion. If - // this associated type is constrained such that it must be an assertion, - // then the combinator's `impl` block must enforce the constraint as well. - type Assertion - where - Next: Assertion; - - /// Builds an assertion using this combinator. - fn build(self, assertion: Next) -> Self::Assertion - where - Next: Assertion; - - // Since we don't know yet what assertion will be passed into the - // `NotCombinator` type, we can't specify the `Next` parameter to constrain - // this method such that it can only be called when that particular impl's - // bounds are satisfied. We also don't yet know what the return value will - // be for the final assertion because it hasn't been fully built yet. - // - // There needs to be a way to constrain the assertions chained onto this so - // that they satisfy the bounds on the `NotCombinator`'s assertion. - fn not(self) -> NotCombinator { - NotCombinator::new(self) - } - - fn all(self) -> AllCombinator - where - Self::NextInput: IntoIterator, - { - AllCombinator::new(self) - } -} diff --git a/src/.experimenting/v7/error.rs b/src/.experimenting/v7/error.rs deleted file mode 100644 index 7043626..0000000 --- a/src/.experimenting/v7/error.rs +++ /dev/null @@ -1,26 +0,0 @@ -// - combinators are built in the direction data flows -// - eg. All>>> -// - assertions are built in the direction the assertion is wrapped -// - eg. Root>>>> -// - this is opposite the direction combinators are built in -// - combinators: -// - need to know the type of the next input to constrain the chain -// - ex should be possible to know when `.all()` is applicable -// - already know the input type since the combinator wraps the root value -// - no need to be generic over it as a result -// - making them generic over the "next assertion" at the trait level makes it -// difficult to create bounds over the trait since a known assertion type -// must be provided to get the associated types -// - assertion type must be generic over the "next assertion" since it wraps -// the assertion -// - assertions: -// - do not need to be generic over the input type because they already know -// the type they are being executed on -// - have variable return types - `()`, `impl Future`, etc -// - `NotAssertion` needs to know how to invert the return type between -// success and failure -// - can use a trait for this rather than requiring a fixed output type -#[derive(Debug, Default)] -pub struct AssertionError { - fields: Vec<(String, String)>, -} diff --git a/src/.experimenting/v7/not.rs b/src/.experimenting/v7/not.rs deleted file mode 100644 index f01766a..0000000 --- a/src/.experimenting/v7/not.rs +++ /dev/null @@ -1,73 +0,0 @@ -use std::fmt::{self, Display, Formatter}; - -use super::{Assertion, AssertionCombinator, AssertionError, AssertionOutput}; - -/// Inverts the result of an assertion. -#[derive(Clone, Debug)] -pub struct NotCombinator { - prev: Prev, -} - -impl NotCombinator { - /// Creates a new [`NotCombinator`]. - #[inline] - pub fn new(prev: Prev) -> Self { - Self { prev } - } -} - -impl AssertionCombinator for NotCombinator -where - Prev: AssertionCombinator, -{ - type NextInput = Prev::NextInput; - type Assertion = Prev::Assertion> - where - Next: Assertion; - - fn build(self, assertion: Next) -> Self::Assertion - where - Next: Assertion, - { - self.prev.build(NotAssertion::new(assertion)) - } -} - -/// Inverts the result of the next assertion. -#[derive(Clone, Debug)] -pub struct NotAssertion { - next: Next, -} - -impl NotAssertion { - /// Creates a new [`NotAssertion`]. - #[inline] - pub fn new(next: Next) -> Self { - NotAssertion { next } - } -} - -impl Display for NotAssertion -where - Next: Display, -{ - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "the following to not be satisfied: {}", self.next) - } -} - -impl Assertion for NotAssertion -where - Next: Assertion, -{ - type Output = ::Mapped< - fn(Result<(), AssertionError>) -> Result<(), AssertionError>, - >; - - fn assert(&mut self, target: Input) -> Self::Output { - self.next.assert(target).map(|result| match result { - Ok(_) => Err(AssertionError::default()), - Err(_) => Ok(()), - }) - } -} diff --git a/src/.experimenting/v8.rs b/src/.experimenting/v8.rs deleted file mode 100644 index dc33f30..0000000 --- a/src/.experimenting/v8.rs +++ /dev/null @@ -1,29 +0,0 @@ -use std::{fmt::Display, future::Future}; - -pub trait SyncCombinator { - type Target; - type Result; - - fn to_satisfy(self, assertion: A) -> Self::Result - where - A: Assertion>; -} - -pub trait AsyncCombinator { - type Target; - type Result: Future - where - A: Assertion, - A::Output: Future>; - - fn to_satisfy(self, assertion: A) -> Self::Result - where - A: Assertion, - A::Output: Future>; -} - -pub trait Assertion: Display { - type Output; - - fn assert(self, input: Input) -> Self::Output; -} diff --git a/src/.experimenting/v9/assertions.rs b/src/.experimenting/v9/assertions.rs deleted file mode 100644 index e4711b0..0000000 --- a/src/.experimenting/v9/assertions.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod assertion; -mod simple_assertion; - -pub use assertion::*; -pub use simple_assertion::*; diff --git a/src/.experimenting/v9/assertions/assertion.rs b/src/.experimenting/v9/assertions/assertion.rs deleted file mode 100644 index 8dddd66..0000000 --- a/src/.experimenting/v9/assertions/assertion.rs +++ /dev/null @@ -1,20 +0,0 @@ -use std::fmt::Display; - -/// An assertion which can pass or fail when provided an input. The input value -/// is represented by the `Target` type parameter. -/// -/// The [`Display`] implementation of the assertion is used to describe the -/// assertion's expectation. The format of the expectation should be all -/// lowercase without any sentence terminating puctuation (like periods), and -/// should succinctly describe what the assertion is testing. For example, an -/// expectation could be "the given value is even". For an assertion that wraps -/// another assertion, the inner assertion's [`Display`] implementation can be -/// used, for example "when the future is ready, {inner}". -#[must_use = "assertions do nothing until 'assert' is called"] -pub trait Assertion: Display { - /// The output from executing this assertion. - type Output; - - /// Performs the assertion on a target value. - fn assert(self, target: Target) -> Self::Output; -} diff --git a/src/.experimenting/v9/assertions/simple_assertion.rs b/src/.experimenting/v9/assertions/simple_assertion.rs deleted file mode 100644 index 8285eb1..0000000 --- a/src/.experimenting/v9/assertions/simple_assertion.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::fmt::{Display, Formatter}; - -use dyn_clone::{clone_trait_object, DynClone}; - -use crate::AssertionResult; - -use super::Assertion; - -/// A simple assertion consisting only of a closure and an expectation string. -#[derive(Clone, Debug)] -pub struct SimpleAssertion { - expectation: String, - assert: F, -} - -impl SimpleAssertion { - /// Create a new simple assertion. - #[inline] - pub fn new(expectation: impl Display, assert: F) -> Self { - Self { - expectation: expectation.to_string(), - assert, - } - } -} - -impl Display for SimpleAssertion { - #[inline] - fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { - self.expectation.fmt(f) - } -} - -impl Assertion for SimpleAssertion -where - F: FnOnce(Target) -> Output, -{ - type Output = Output; - - #[inline] - fn assert(self, target: Target) -> Self::Output { - (self.assert)(target) - } -} - -/// A simple, cloneable, boxable assertion function. This is used to create a -/// boxed assertion that can be trivially shared. -pub trait SimpleAssertionFn: DynClone { - fn call(self, target: Target) -> AssertionResult; -} - -clone_trait_object!( SimpleAssertionFn); - -impl SimpleAssertionFn for F -where - F: (FnOnce(Target) -> AssertionResult) + Clone, -{ - fn call(self, target: Target) -> AssertionResult { - self(target) - } -} diff --git a/src/.experimenting/v9/combinators.rs b/src/.experimenting/v9/combinators.rs deleted file mode 100644 index f554af9..0000000 --- a/src/.experimenting/v9/combinators.rs +++ /dev/null @@ -1,13 +0,0 @@ -mod all; -mod any; -mod combinator; -mod combinator_ext; -mod not; -mod when_ready; - -pub use all::*; -pub use any::*; -pub use combinator::*; -pub use combinator_ext::*; -pub use not::*; -pub use when_ready::*; diff --git a/src/.experimenting/v9/combinators/all.rs b/src/.experimenting/v9/combinators/all.rs deleted file mode 100644 index 404d76b..0000000 --- a/src/.experimenting/v9/combinators/all.rs +++ /dev/null @@ -1,80 +0,0 @@ -use std::fmt::{Display, Formatter}; - -use crate::AssertionResult; - -use super::{Assertion, AssertionCombinator}; - -/// Wraps another [`AssertionCombinator`] and ensures the assertion succeeds for -/// every inner value. -#[derive(Clone, Debug)] -pub struct AllCombinator { - inner: Inner, -} - -impl AllCombinator { - /// Creates a new instance of this combinator, wrapping the inner - /// combinator. - #[inline] - pub fn new(inner: Inner) -> Self { - Self { inner } - } -} - -impl AssertionCombinator for AllCombinator -where - Inner: AssertionCombinator>, - Inner::NextTarget: IntoIterator, -{ - type Target = Inner::Target; - type NextTarget = ::Item; - type Assertion = Inner::Assertion; - - #[inline] - fn apply(self, next: Next) -> Self::Assertion { - self.inner.apply(AllAssertion::new(next)) - } -} - -/// Wraps another assertion and ensures that the inner assertion does not fail -/// for any value in the provided target. -#[derive(Clone, Debug, Default)] -pub struct AllAssertion { - next: Next, -} - -impl AllAssertion { - /// Creates a new instance of this assertion. - #[inline] - pub fn new(next: Next) -> Self { - Self { next } - } -} - -impl Display for AllAssertion -where - Next: Display, -{ - #[inline] - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "for each item, {}", self.next) - } -} - -impl Assertion for AllAssertion -where - Target: IntoIterator, - Next: Assertion + Clone, -{ - type Output = AssertionResult; - - fn assert(self, target: Target) -> Self::Output { - target - .into_iter() - .map(move |target| self.next.clone().assert(target)) - .collect() - } -} - -// TODO: make an "AllOutput"-style trait so this doesn't need to have the output -// bound, which would work similar to the previous `AssertionOutput` trait but -// only for the `.all()` combinator diff --git a/src/.experimenting/v9/combinators/any.rs b/src/.experimenting/v9/combinators/any.rs deleted file mode 100644 index f6481aa..0000000 --- a/src/.experimenting/v9/combinators/any.rs +++ /dev/null @@ -1,80 +0,0 @@ -use std::fmt::{Display, Formatter}; - -use crate::{AssertionFailure, AssertionResult}; - -use super::{Assertion, AssertionCombinator}; - -/// Wraps another [`AssertionCombinator`] and ensures the assertion succeeds for -/// some inner value. -#[derive(Clone, Debug)] -pub struct AnyCombinator { - inner: Inner, -} - -impl AnyCombinator { - /// Creates a new instance of this combinator, wrapping the inner - /// combinator. - #[inline] - pub fn new(inner: Inner) -> Self { - Self { inner } - } -} - -impl AssertionCombinator for AnyCombinator -where - Inner: AssertionCombinator>, - Inner::NextTarget: IntoIterator, -{ - type Target = Inner::Target; - type NextTarget = ::Item; - type Assertion = Inner::Assertion; - - fn apply(self, next: Next) -> Self::Assertion { - self.inner.apply(AnyAssertion::new(next)) - } -} - -/// Wraps another assertion and ensures that the inner assertion passes for at -/// least one value in the provided target. -#[derive(Clone, Debug, Default)] -pub struct AnyAssertion { - next: Next, -} - -impl AnyAssertion { - /// Creates a new instance of this assertion. - #[inline] - pub fn new(next: Next) -> Self { - AnyAssertion { next } - } -} - -impl Display for AnyAssertion -where - Next: Display, -{ - #[inline] - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "for each item, {}", self.next) - } -} - -impl Assertion for AnyAssertion -where - Target: IntoIterator, - Next: Assertion + Clone, -{ - type Output = AssertionResult; - - fn assert(self, target: Target) -> Self::Output { - let mut error = AssertionFailure::builder().build("TODO"); - for value in target { - match self.next.clone().assert(value) { - Ok(()) => return Ok(()), - Err(e) => error = e, - } - } - - Err(error) - } -} diff --git a/src/.experimenting/v9/combinators/combinator.rs b/src/.experimenting/v9/combinators/combinator.rs deleted file mode 100644 index 385a4ac..0000000 --- a/src/.experimenting/v9/combinators/combinator.rs +++ /dev/null @@ -1,69 +0,0 @@ -use crate::assertions::Assertion; - -/// Transforms an assertion into a more complex assertion. -/// -/// Combinators are used to build more complex assertions out of less complex -/// ones. They function as a sort of "middleware" and are able to transform both -/// the input to an assertion and the output from one. -/// -/// The type parameter represents the type of assertion this combinator can -/// wrap. -#[must_use = "combinators do nothing until they are applied"] -pub trait AssertionCombinator { - /// The type of value passed to this combinator's assertion. - /// - /// For many combinators which wrap another combinator, this is simply - /// `Inner::Target` (where `Inner` is the inner combinator). - type Target; - - /// The type of value passed to the assertion that is passed to this - /// combinator. This is used to determine which combinators and assertions - /// are valid to be chained to a combinator. For example, this is used to - /// prevent code like `expect!(1).all()` from compiling and to provide - /// better completions to users. - /// - /// This should be set to the type of value that the assertion built by this - /// combinator will pass to its inner/next assertion. `expect!("hi")` will - /// pass a `&str` to the chained assertion for example, despite its - /// [`Target`](AssertionCombinator::Target) being `()`. - type NextTarget; - - /// The type of assertion that this combinator wraps the given assertion - /// with. - /// - /// For many combinators which wrap another combinator, this is simply - /// `Inner::Assertion` (where `Inner` is the inner combinator). Note that - /// this usually implies a bound on the implementation of - /// `Inner: AssertionCombinator>` if passing a custom - /// assertion into the inner combinator (which is generally recommended). - type Assertion: Assertion; - - /// Wraps an assertion. - /// - /// This function is the foundation for how combinators work. This is used - /// to create more complex assertions. The combinator works by wrapping the - /// given assertion with its own assertion. For example, the assertion - /// returned by this combinator can negate the output of the provided - /// assertion, call the provided assertion multiple times (if it's `Clone`), - /// or transform the output of the provided assertion in some other manner. - /// - /// The returned assertion still needs to be executed on a value. The type - /// of input the returned assertion accepts is determined by the - /// [`AssertionCombinator::Target`] property. - /// - /// This method also usually has the side effect of "inverting" the type. - /// For example, calling `expect!(value).not().all()` will create an - /// `AllCombinator>>` (where `T` is the type - /// of the value), and applying the combinator will generate an instance of - /// `RootAssertion>>`. - fn apply(self, next: Next) -> Self::Assertion; -} - -// expect!(a).not().all() -// -> AllCombinator>> -// .apply(next) -// -> Root>> -// -> Next needs to be Clone, must be type param to ensure that and so -// -> the AllAssertion can clone it -// .assert(value) -// -> first apply .all(), then apply .not() to invert, then identity at root diff --git a/src/.experimenting/v9/combinators/combinator_ext.rs b/src/.experimenting/v9/combinators/combinator_ext.rs deleted file mode 100644 index 94f2cdc..0000000 --- a/src/.experimenting/v9/combinators/combinator_ext.rs +++ /dev/null @@ -1,146 +0,0 @@ -use crate::{assertions::SimpleAssertion, AssertionFailure}; - -use super::{AllCombinator, AnyCombinator, AssertionCombinator, NotCombinator}; - -pub trait AssertionCombinatorExt: AssertionCombinator + Sized { - /// 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::NextTarget: 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::NextTarget: IntoIterator, - { - AnyCombinator::new(self) - } - - // fn all(self) -> AllCombinator - // where - // AllCombinator: AssertionCombinator AssertionResult>>, - // { - // AllCombinator::new(self) - // } - // fn any(self) -> AnyCombinator - // where - // AnyCombinator: - // AssertionCombinator AssertionResult>>, - // { - // AnyCombinator::new(self) - // } - - // fn to_equal(self, other: U) -> impl AssertionOutput - // where - // Self::Target: PartialEq, - // { - // self.apply("the values to be equal", move |value| { - // (value == other) - // .then_some(()) - // .ok_or_else(|| AssertionFailure::builder().build("the values to be equal")) - // }) - // } -} - -impl AssertionCombinatorExt for T where T: AssertionCombinator {} - -pub trait AssertionCombinatorAssertionsExt: - AssertionCombinator, NextTarget = Target> + Sized -{ - fn to_equal(self, other: U) - where - Target: PartialEq, - { - let assertion = SimpleAssertion::new("the values are equal", move |target| { - if target == other { - Ok(()) - } else { - Err(AssertionFailure::builder().build("TODO")) - } - }); - let assertion = self.apply(assertion); - } -} - -impl AssertionCombinatorAssertionsExt for C where - C: AssertionCombinator, NextTarget = Target> -{ -} diff --git a/src/.experimenting/v9/combinators/not.rs b/src/.experimenting/v9/combinators/not.rs deleted file mode 100644 index b5f7ecd..0000000 --- a/src/.experimenting/v9/combinators/not.rs +++ /dev/null @@ -1,75 +0,0 @@ -use std::fmt::{Display, Formatter}; - -use crate::{AssertionFailure, AssertionResult}; - -use super::{Assertion, AssertionCombinator}; - -/// Wraps another [`AssertionCombinator`] and negates the output. 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 instance of this combinator, wrapping the inner - /// combinator. - #[inline] - pub fn new(inner: Inner) -> Self { - Self { inner } - } -} - -impl AssertionCombinator for NotCombinator -where - Inner: AssertionCombinator>, - Next: Assertion, -{ - type Target = Inner::Target; - type NextTarget = Inner::Target; - type Assertion = Inner::Assertion; - - fn apply(self, next: Next) -> Self::Assertion { - self.inner.apply(NotAssertion::new(next)) - } -} - -/// Wraps another assertion and inverts the result, passing if and only if the -/// inner assertion failed and failing if it passes. -#[derive(Clone, Debug, Default)] -pub struct NotAssertion { - next: Next, -} - -impl NotAssertion { - /// Creates a new instance of this assertion. - #[inline] - pub fn new(next: Next) -> Self { - NotAssertion { next } - } -} - -impl Display for NotAssertion -where - Next: Display, -{ - #[inline] - fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { - write!(f, "the following is not satisfied: {}", self.next) - } -} - -impl Assertion for NotAssertion -where - Next: Assertion, -{ - type Output = Next::Output; - - fn assert(self, target: Target) -> Self::Output { - let result = self.next.assert(target); - match result { - Ok(_) => Err(AssertionFailure::builder().build("TODO")), - Err(_) => Ok(()), - } - } -} diff --git a/src/.experimenting/v9/combinators/when_ready.old.rs b/src/.experimenting/v9/combinators/when_ready.old.rs deleted file mode 100644 index 524fdc3..0000000 --- a/src/.experimenting/v9/combinators/when_ready.old.rs +++ /dev/null @@ -1,354 +0,0 @@ -use std::{ - fmt::{Display, Formatter}, - future::Future, - pin::Pin, - task::{ready, Context, Poll}, -}; - -use pin_project_lite::pin_project; - -use crate::{AssertionOutput, AssertionResult}; - -use super::{Assertion, AssertionCombinator}; - -/// Wraps another [`AssertionCombinator`] and executes assertions on the output -/// of the inner value's future. -/// -/// This causes assertions to be asynchronous. Assertion outputs from this -/// combinator will be wrapped in futures, meaning the assertions must be -/// `.await`ed. -#[derive(Clone, Debug)] -pub struct WhenReadyCombinator { - inner: Inner, -} - -impl WhenReadyCombinator { - /// Creates a new instance of this combinator, wrapping the inner - /// combinator. - #[inline] - pub fn new(inner: Inner) -> Self { - Self { inner } - } -} - -// impl AssertionCombinator for WhenReadyCombinator -// where -// Inner: AssertionCombinator, -// Inner::Target: Future, -// { -// type Target = ::Output; - -// type Assertion = WhenReadyOutput::Output) -> R>> -// where -// R: AssertionOutput, -// F: FnMut(Self::Target) -> R; - -// fn to_satisfy(self, expectation: impl Display, assert: F) -> Self::Assertion -// where -// R: AssertionOutput, -// F: FnMut(Self::Target) -> R, -// { -// // Since the assertion is passed to potentially multiple futures, we -// // need to wrap it in an Arc> to ensure that only one of those -// // futures actually executes the assertion at a time. -// let assert = Arc::new(Mutex::new(assert)); - -// self.inner.to_satisfy( -// format!("when the future is ready, {expectation}"), -// move |fut| { -// let assert = assert.clone(); -// WhenReadyOutput::evaluated( -// fut, -// Box::new(move |value| { -// let mut assert = assert.lock().unwrap(); -// assert(value) -// }) -// as Box::Output) -> R>, -// ) -// }, -// ) -// } -// } - -impl AssertionCombinator for WhenReadyCombinator -where - Inner: AssertionCombinator>, - Inner::Target: Future, -{ - type Target = Inner::Target; - - type Assertion = Inner::Assertion; - - fn apply(self, next: Next) -> Self::Assertion { - self.inner.apply(WhenReadyAssertion::new(next)) - } -} - -pub struct WhenReadyAssertion { - next: Next, -} - -impl WhenReadyAssertion { - pub fn new(next: Next) -> Self { - WhenReadyAssertion { next } - } -} - -impl Display for WhenReadyAssertion -where - Next: Display, -{ - fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { - write!(f, "when the future is ready, {}", self.next) - } -} - -impl Assertion for WhenReadyAssertion -where - Target: Future, - Next: Assertion, -{ - type Output = WhenReadyOutput; - - fn assert(self, target: Target) -> Self::Output { - WhenReadyOutput::evaluated(target, self.next) - } -} - -pin_project! { - /// An assertion output that will resolve to a success or an error - /// eventually. This is usually created when performing an assertion on the - /// output of a future. - #[derive(Clone, Debug)] - #[must_use] - pub struct WhenReadyOutput { - // TODO: is the double-pin needed? - #[pin] - inner: WhenReadyOutputInner - } -} - -impl WhenReadyOutput { - #[inline] - fn evaluated(fut: Fut, map: A) -> Self { - Self { - inner: WhenReadyOutputInner::Evaluated { - fut, - map: Some(map), - }, - } - } - - #[inline] - fn forced(result: AssertionResult) -> Self { - Self { - inner: WhenReadyOutputInner::Forced { - result: Some(result), - }, - } - } -} - -pin_project! { - #[project = WhenReadyOutputInnerProj] - #[derive(Clone, Debug)] - enum WhenReadyOutputInner { - Forced { - result: Option, - }, - Evaluated { - #[pin] - fut: Fut, - map: Option, - }, - } -} - -// impl Future for WhenReadyOutput -// where -// Fut: Future, -// A: FnOnce(Fut::Output) -> R, -// R: AssertionOutput, -// { -// type Output = R; - -// fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { -// let projected = self.project(); -// let projected = projected.inner.project(); - -// match projected { -// WhenReadyOutputInnerProj::Forced { result } => { -// let result = result.take().expect("poll after ready"); -// Poll::Ready(match result { -// Ok(()) => R::new_success(), -// Err(failure) => R::new_failure(failure), -// }) -// } -// WhenReadyOutputInnerProj::Evaluated { fut, assert } => { -// let output = ready!(fut.poll(cx)); -// let assert = assert.take().expect("polled after ready"); -// Poll::Ready(assert(output)) -// } -// } -// } -// } - -impl Future for WhenReadyOutput -where - Fut: Future, - M: FnOnce(Fut::Output) -> R, - R: AssertionOutput, -{ - type Output = R; - - fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { - let projected = self.project(); - let projected = projected.inner.project(); - - match projected { - WhenReadyOutputInnerProj::Forced { result } => { - let result = result.take().expect("poll after ready"); - Poll::Ready(match result { - Ok(()) => Self::Output::new_success(), - Err(failure) => Self::Output::new_failure(failure), - }) - } - WhenReadyOutputInnerProj::Evaluated { fut, map } => { - let output = ready!(fut.poll(cx)); - let map = map.take().expect("polled after ready"); - Poll::Ready(map(output)) - } - } - } -} - -// impl AssertionOutput for WhenReadyOutput -// where -// Fut: Future, -// A: Assertion, -// { -// type Mapped = WhenReadyOutput< -// Self, -// SimpleAssertion< -// A::Output, -// ::Mapped -// > -// > -// where -// F: FnMut(AssertionResult) -> AssertionResult; - -// type AndThen -// where -// O: AssertionOutput, -// F: FnOnce() -> O; - -// type OrElse -// where -// O: AssertionOutput, -// F: FnOnce() -> O; - -// type All -// where -// I: IntoIterator; - -// type Any -// where -// I: IntoIterator; - -// #[inline] -// fn new_success() -> Self { -// Self::forced(Ok(())) -// } - -// #[inline] -// fn new_failure(failure: AssertionFailure) -> Self { -// Self::forced(Err(failure)) -// } - -// #[inline] -// fn map(self, f: F) -> Self::Mapped -// where -// F: FnMut(AssertionResult) -> AssertionResult, -// { -// WhenReadyOutput::evaluated( -// self, -// SimpleAssertion::new("", move |output: A::Output| output.map(f)), -// ) -// } - -// fn and_then(self, other: F) -> Self::AndThen -// where -// O: AssertionOutput, -// F: FnOnce() -> O, -// { -// todo!() -// } - -// fn or_else(self, other: F) -> Self::OrElse -// where -// O: AssertionOutput, -// F: FnOnce() -> O, -// { -// todo!() -// } - -// fn all(outputs: I) -> Self::All -// where -// I: IntoIterator, -// { -// todo!() -// } - -// fn any(outputs: I) -> Self::Any -// where -// I: IntoIterator, -// { -// todo!() -// } - -// // #[inline] -// // fn map(self, f: F) -> impl AssertionOutput -// // where -// // F: FnMut(AssertionResult) -> AssertionResult, -// // { -// // WhenReadyOutput::evaluated(self, move |output: R| output.map(f)) -// // } - -// // #[inline] -// // fn and_then(self, other: F) -> impl AssertionOutput -// // where -// // O: AssertionOutput, -// // F: FnOnce() -> O, -// // { -// // WhenReadyOutput::evaluated(self, move |output: ::Output| { -// // output.and_then(other) -// // }) -// // } - -// // #[inline] -// // fn or_else(self, other: F) -> impl AssertionOutput -// // where -// // O: AssertionOutput, -// // F: FnOnce() -> O, -// // { -// // WhenReadyOutput::evaluated(self, move |output: ::Output| { -// // output.or_else(other) -// // }) -// // } - -// // #[inline] -// // fn all(outputs: impl IntoIterator) -> impl AssertionOutput { -// // // It would be nice to support short-circuiting here, but we need all -// // // the outputs ready at once to pass them to `R::all` -// // let stream: FuturesUnordered<_> = outputs.into_iter().collect(); -// // WhenReadyOutput::evaluated(stream.collect::>(), R::all) -// // } - -// // #[inline] -// // fn any(outputs: impl IntoIterator) -> impl AssertionOutput { -// // // It would be nice to support short-circuiting here, but we need all -// // // the outputs ready at once to pass them to `R::any` -// // let stream: FuturesUnordered<_> = outputs.into_iter().collect(); -// // WhenReadyOutput::evaluated(stream.collect::>(), R::any) -// // } -// } diff --git a/src/.experimenting/v9/combinators/when_ready.rs b/src/.experimenting/v9/combinators/when_ready.rs deleted file mode 100644 index 0e6e274..0000000 --- a/src/.experimenting/v9/combinators/when_ready.rs +++ /dev/null @@ -1,114 +0,0 @@ -use std::{ - fmt::{Display, Formatter}, - future::Future, - pin::Pin, - task::{ready, Context, Poll}, -}; - -use pin_project_lite::pin_project; - -use super::{Assertion, AssertionCombinator}; - -/// Wraps another [`AssertionCombinator`] and executes assertions on the output -/// of the inner value's future. -/// -/// This causes assertions to be asynchronous. Assertion outputs from this -/// combinator will be wrapped in futures, meaning the assertions must be -/// `.await`ed. -#[derive(Clone, Debug)] -pub struct WhenReadyCombinator { - inner: Inner, -} - -impl WhenReadyCombinator { - /// Creates a new instance of this combinator, wrapping the inner - /// combinator. - #[inline] - pub fn new(inner: Inner) -> Self { - Self { inner } - } -} - -impl AssertionCombinator for WhenReadyCombinator -where - Inner: AssertionCombinator>, - Inner::NextTarget: Future, -{ - type Target = Inner::Target; - type NextTarget = ::Output; - type Assertion = Inner::Assertion; - - #[inline] - fn apply(self, next: Next) -> Self::Assertion { - self.inner.apply(WhenReadyAssertion::new(next)) - } -} - -/// Wraps another assertion and executes it on the output of the provided -/// future. -#[derive(Clone, Debug, Default)] -pub struct WhenReadyAssertion { - next: Next, -} - -impl WhenReadyAssertion { - /// Creates a new instance of this assertion. - #[inline] - pub fn new(next: Next) -> Self { - Self { next } - } -} - -impl Display for WhenReadyAssertion -where - Next: Display, -{ - #[inline] - fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { - write!(f, "when the future is ready, {}", self.next) - } -} - -impl Assertion for WhenReadyAssertion -where - Target: Future, - Next: Assertion, -{ - type Output = WhenReadyOutput; - - #[inline] - fn assert(self, target: Target) -> Self::Output { - WhenReadyOutput { - target, - next: Some(self.next), - } - } -} - -pin_project! { - /// An assertion output that will resolve to a success or an error - /// eventually. This is usually created when performing an assertion on the - /// output of a future. - #[derive(Clone, Debug, Default)] - #[must_use] - pub struct WhenReadyOutput { - #[pin] - target: Target, - next: Option, - } -} - -impl Future for WhenReadyOutput -where - Target: Future, - Next: Assertion, -{ - type Output = Next::Output; - - fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { - let projected = self.project(); - let target = ready!(projected.target.poll(cx)); - let next = projected.next.take().expect("poll after ready"); - Poll::Ready(next.assert(target)) - } -} diff --git a/src/.experimenting/v9/expect.rs b/src/.experimenting/v9/expect.rs deleted file mode 100644 index 400d991..0000000 --- a/src/.experimenting/v9/expect.rs +++ /dev/null @@ -1,174 +0,0 @@ -use std::fmt::{Display, Formatter}; - -use crate::combinators::{Assertion, AssertionCombinator}; - -/// 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), - ) - }; -} - -/// The root of an expectation. Other expectations are built on top of this. -#[derive(Clone, Debug)] -pub struct ExpectationRoot { - target: Target, - 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: Target, source_info: SourceInfo, target_source: &'static str) -> Self { - Self { - target, - source_info, - target_source, - } - } -} - -impl AssertionCombinator for ExpectationRoot -where - Next: Assertion, -{ - type Target = (); - type NextTarget = Target; - type Assertion = RootAssertion; - - #[inline] - fn apply(self, next: Next) -> Self::Assertion { - RootAssertion::new(self.target, next) - } -} - -/// The root-level assertion. This assertion wraps another assertion, passes -/// that assertion the target, and attaches additional context to the error if -/// the assertion fails. All assertions are normally eventually wrapped by this -/// type. -#[derive(Clone, Debug, Default)] -pub struct RootAssertion { - target: Target, - next: Next, -} - -impl RootAssertion { - /// Creates a new instance of this assertion. - #[inline] - pub fn new(target: Target, next: Next) -> Self { - Self { target, next } - } -} - -impl Display for RootAssertion -where - Next: Display, -{ - #[inline] - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - self.next.fmt(f) - } -} - -impl Assertion<()> for RootAssertion -where - Next: Assertion, -{ - type Output = Next::Output; - - #[inline] - fn assert(self, (): ()) -> Self::Output { - // TODO: attach source info to the error - self.next.assert(self.target) - } -} - -/// 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) - } -} - -#[cfg(test)] -mod tests { - use std::future::ready; - - use crate::{combinators::*, prelude::*, AssertionResult, ExpectationRoot}; - - #[test] - fn foo() { - // let combinator = ExpectationRoot::new([1, 2, 3], todo!(), "a"); - let _ = expect!([1, 2, 3]).all().all(); - // let combinator = ExpectationRoot::new(ready([1, 2, 3]), todo!(), "a"); - // let combinator = WhenReadyCombinator::new(combinator); - // let combinator = NotCombinator::new(combinator); - let combinator = AllCombinator::new(combinator); - let assertion = SimpleAssertion::new("always pass", |_value| AssertionResult::Ok(())); - let assertion = combinator.apply(assertion); - // let assertion = AssertionCombinator::apply(combinator, assertion); - // AllAssertion::new(assertion); - // let assertion = NotAssertion::new(AllAssertion::new(assertion)); - let _blah = assertion.assert(()); - // let assertion = combinator - - // let output = expect!(1).to_equal(1); - } -} diff --git a/src/.experimenting/v9/failure.rs b/src/.experimenting/v9/failure.rs deleted file mode 100644 index ce39740..0000000 --- a/src/.experimenting/v9/failure.rs +++ /dev/null @@ -1,66 +0,0 @@ -use std::{ - error::Error, - fmt::{Display, Formatter}, -}; - -pub type AssertionResult = Result; - -/// 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 AssertionFailure { - fields: Vec<(&'static str, String)>, -} - -impl AssertionFailure { - /// Creates a builder for a new failure. - pub fn builder() -> AssertionFailureBuilder { - AssertionFailureBuilder::default() - } -} - -impl Display for AssertionFailure { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - writeln!(f, "assertion failed.")?; - for (name, value) in &self.fields { - writeln!(f, " {name}: {value}")?; - } - - Ok(()) - } -} - -impl Error for AssertionFailure {} - -/// A builder for a failure. -#[derive(Clone, Debug)] -pub struct AssertionFailureBuilder { - fields: Vec<(&'static str, String)>, -} - -impl Default for AssertionFailureBuilder { - fn default() -> Self { - Self { - fields: vec![("expected", String::new())], - } - } -} - -impl AssertionFailureBuilder { - /// 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 Display) -> Self { - self.fields.push((name, value.to_string())); - self - } - - /// Builds the error with the given expectation. - pub fn build(mut self, expectation: impl Display) -> AssertionFailure { - self.fields[0].1 = expectation.to_string(); - AssertionFailure { - fields: self.fields, - } - } -} diff --git a/src/.experimenting/v9/lib.rs b/src/.experimenting/v9/lib.rs deleted file mode 100644 index 4ec39cd..0000000 --- a/src/.experimenting/v9/lib.rs +++ /dev/null @@ -1,13 +0,0 @@ -pub mod assertions; -pub mod combinators; -pub mod prelude; - -mod expect; -mod failure; -mod output; -mod third_party; - -pub use expect::*; -pub use failure::*; -pub use output::*; -pub use third_party::*; diff --git a/src/.experimenting/v9/output.rs b/src/.experimenting/v9/output.rs deleted file mode 100644 index 83b7d3a..0000000 --- a/src/.experimenting/v9/output.rs +++ /dev/null @@ -1,208 +0,0 @@ -use either::Either; - -use crate::{AssertionFailure, AssertionResult}; - -/// An output from performing an assertion. This represents either a success or -/// a failure, and should include additional information about the reason for -/// the failure. -/// -/// Assertion outputs fundamentally parallel `Result<(), AssertionFailure>`, -/// though it's not always trivial to convert one to a result. For example, -/// while `Result<(), AssertionFailure>` *is* an [`AssertionOutput`] and is -/// trivially convertible to itself, a [`Future`] -/// cannot be converted to a result trivially and must be polled to completion -/// to get a result. -pub trait AssertionOutput { - // TODO: docs - type Mapped: AssertionOutput - where - F: FnMut(AssertionResult) -> AssertionResult; - - type AndThen: AssertionOutput - where - O: AssertionOutput, - F: FnOnce() -> O; - - type OrElse: AssertionOutput - where - O: AssertionOutput, - F: FnOnce() -> O; - - type All: AssertionOutput - where - I: IntoIterator; - - type Any: AssertionOutput - where - I: IntoIterator; - - /// Creates a new output representing a success. - /// - /// This acts as a "default" success value for combinators that need it. For - /// example, the `.all` combinator uses this to create a success when there - /// are no values to execute its assertion on. - fn new_success() -> Self; - - /// Creates a new output representing the given failure. - /// - /// This acts as a "default" failure value for combinators that need it. For - /// example, the `.any` combinator uses this to create a failure when there - /// are no values to execute its assertion on. - fn new_failure(failure: AssertionFailure) -> Self; - - /// Maps this output to a new output. - /// - /// The function passed in is provided this output's value represented as a - /// result. It is not guaranteed that this function will be called right - /// away, and may be called in the future when needed instead. - fn map(self, f: F) -> Self::Mapped - where - F: FnMut(AssertionResult) -> AssertionResult; - - /// Creates a new output that succeeds if and only if this and another - /// output both succeed. - /// - /// This may choose to lazily evaluate `other`, but it is not guaranteed. - /// This means that `other` may not actually be evaluated if this output - /// already represents a failure (but this is not a guarantee). - /// - /// While it cannot be enforced at the type level, it is strongly encouraged - /// for implementers to support the commutitive law. This means that whether - /// this output `and` another output represents a success should be the same - /// as whether the other output `and` this output represents a success. - fn and_then(self, other: F) -> Self::AndThen - where - O: AssertionOutput, - F: FnOnce() -> O; - - /// Creates a new output that succeeds if and only if either this or another - /// output (or both) succeed. - /// - /// The remarks for [`and_then`](AssertionOutput::and_then) regarding lazy - /// evaluation and commutitivity apply here as well. See that documentation - /// for more information. - fn or_else(self, other: F) -> Self::OrElse - where - O: AssertionOutput, - F: FnOnce() -> O; - - /// Creates a new output that succeeds if and only if there are no outputs - /// in the given iterator that represent a failure. - /// - /// Note that this means empty iterators will always result in a success. - /// - /// The order the outputs are checked for success is unspecified, and it's - /// up to the implementer to decide what order they want to compare the - /// outputs in. For example, suppose the iterator represented a sequence of - /// 3 outputs. The implementer may decide to check if the second output - /// represents a success before looking at the other outputs. Because the - /// implementation may choose to short-circuit, if the second output - /// represents a failure, this means there is a chance that the other - /// outputs are not checked. - /// - /// In most cases, this does not matter. However, it may be relevant for - /// asynchronous outputs. In this case, if one output is ready before the - /// others and represents a failure, the implementation may choose to - /// short-circuit the rest of the outputs and not complete those futures. - /// However, this behavior is optional, and an implementer may also choose - /// to evaluate all the futures before checking if any are a success or a - /// failure. - // TODO: might be useful to explore a more robust "aggregate output" trait - // for things like mixing required/optional outputs, min/max successes - // or failures, support for short-circuiting, etc. - fn all(outputs: I) -> Self::All - where - I: IntoIterator; - - /// Creates a new output that succeeds if and only if there exists an output - /// that represents a success in the given iterator. - /// - /// Note that this means empty iterators will always result in a failure. - /// - /// The remarks for [`all`](AssertionOutput::all) regarding order - /// specificity also apply here. See that documentation for more - /// information. - fn any(outputs: I) -> Self::Any - where - I: IntoIterator; -} - -impl AssertionOutput for Result<(), AssertionFailure> { - type Mapped = Self - where - F: FnMut(AssertionResult) -> AssertionResult; - - type AndThen = Either - where - O: AssertionOutput, - F: FnOnce() -> O; - - type OrElse = Either - where - O: AssertionOutput, - F: FnOnce() -> O; - - type All = Self - where - I: IntoIterator; - - type Any = Self - where - I: IntoIterator; - - fn new_success() -> Self { - Ok(()) - } - - fn new_failure(failure: AssertionFailure) -> Self { - Err(failure) - } - - fn map(self, mut f: F) -> Self::Mapped - where - F: FnMut(Result<(), AssertionFailure>) -> Result<(), AssertionFailure>, - { - f(self) - } - - fn and_then(self, other: F) -> Self::AndThen - where - O: AssertionOutput, - F: FnOnce() -> O, - { - match self { - Ok(()) => Either::Right(other()), - failure => Either::Left(failure), - } - } - - fn or_else(self, other: F) -> Self::OrElse - where - O: AssertionOutput, - F: FnOnce() -> O, - { - match self { - Ok(()) => Either::Left(Ok(())), - Err(_) => Either::Right(other()), - } - } - - fn all(outputs: I) -> Self::All - where - I: IntoIterator, - { - outputs.into_iter().collect::() - } - - fn any(outputs: I) -> Self::Any - where - I: IntoIterator, - { - outputs - .into_iter() - .filter_map(Result::ok) - .next() - // TODO: build the failure correctly - .ok_or_else(|| AssertionFailure::builder().build("no values in assertion")) - } -} diff --git a/src/.experimenting/v9/prelude.rs b/src/.experimenting/v9/prelude.rs deleted file mode 100644 index c3b183b..0000000 --- a/src/.experimenting/v9/prelude.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! This module contains commonly used types, traits, and macros. To keep your -//! imports simple, rather than importing these members individually, you can -//! just write: -//! -//! ``` -//! # #[allow(unused_imports)] -//! use expecters::prelude::*; -//! ``` - -pub use crate::{combinators::AssertionCombinatorExt, expect}; diff --git a/src/.experimenting/v9/third_party.rs b/src/.experimenting/v9/third_party.rs deleted file mode 100644 index 695d72e..0000000 --- a/src/.experimenting/v9/third_party.rs +++ /dev/null @@ -1,4 +0,0 @@ -mod either; - -// Re-export to integrate better with other libraries -pub use ::either::Either; diff --git a/src/.experimenting/v9/third_party/either.rs b/src/.experimenting/v9/third_party/either.rs deleted file mode 100644 index a5e5be1..0000000 --- a/src/.experimenting/v9/third_party/either.rs +++ /dev/null @@ -1,84 +0,0 @@ -use either::Either; - -use crate::{AssertionFailure, AssertionOutput}; - -macro_rules! map_either { - ($value:expr, $pattern:pat => $result:expr) => { - match $value { - Either::Left($pattern) => Either::Left($result), - Either::Right($pattern) => Either::Right($result), - } - }; -} - -impl AssertionOutput for Either -where - L: AssertionOutput, - R: AssertionOutput, -{ - type Mapped = Either, R::Mapped> - where - F: FnMut(crate::AssertionResult) -> crate::AssertionResult; - - #[inline] - fn new_success() -> Self { - Either::Left(L::new_success()) - } - - #[inline] - fn new_failure(failure: AssertionFailure) -> Self { - Either::Left(L::new_failure(failure)) - } - - #[inline] - fn map(self, f: F) -> Self::Mapped - where - F: FnMut(Result<(), AssertionFailure>) -> Result<(), AssertionFailure>, - { - map_either!(self, inner => inner.map(f)) - } - - #[inline] - fn and_then(self, other: F) -> impl AssertionOutput - where - O: AssertionOutput, - F: FnOnce() -> O, - { - map_either!(self, inner => inner.and_then(other)) - } - - #[inline] - fn or_else(self, other: F) -> impl AssertionOutput - where - O: AssertionOutput, - F: FnOnce() -> O, - { - map_either!(self, inner => inner.or_else(other)) - } - - fn all(outputs: impl IntoIterator) -> impl AssertionOutput { - let mut lefts = Vec::new(); - let mut rights = Vec::new(); - for output in outputs { - match output { - Either::Left(inner) => lefts.push(inner), - Either::Right(inner) => rights.push(inner), - } - } - - L::all(lefts).and_then(move || R::all(rights)) - } - - fn any(outputs: impl IntoIterator) -> impl AssertionOutput { - let mut lefts = Vec::new(); - let mut rights = Vec::new(); - for output in outputs { - match output { - Either::Left(inner) => lefts.push(inner), - Either::Right(inner) => rights.push(inner), - } - } - - L::any(lefts).or_else(move || R::any(rights)) - } -} diff --git a/src/.experimenting/v9/thoughts.md b/src/.experimenting/v9/thoughts.md deleted file mode 100644 index 4adb72a..0000000 --- a/src/.experimenting/v9/thoughts.md +++ /dev/null @@ -1,27 +0,0 @@ -# Thoughts - -## combinator/middleware - -- knows what it's receiving (since it wraps the previous combinator) - - and doesn't need to say what it is (since it already "has" the value) -- knows what it's propagating - - and needs to say what it is (so other assertions/combinators know if they can be built from it) -- may have additional bounds on next assertion - - all/any need to be able to clone the next assertion - - alternatively, all assertions can be executed multiple times, but this is constraining - - always a bound on next assertion that it receives what the combinator propagates - -type params: - -- next assertion - -assoc types: - -- propagated value - -## assertion - -TODO - -- knows what it's receiving -- knows what it's propagating \ No newline at end of file From 35b51b5f5b87d2ba8033c79eca658825b02d8ff9 Mon Sep 17 00:00:00 2001 From: TehPers Date: Sat, 10 Aug 2024 16:58:13 -0700 Subject: [PATCH 13/37] Rename AssertionResult -> AssertionOutput --- src/assertions.rs | 4 ++-- src/assertions/error.rs | 6 +++--- .../futures/outputs/completion_order.rs | 6 +++--- src/assertions/general/assertions/to_cmp.rs | 4 ++-- src/assertions/general/assertions/to_equal.rs | 4 ++-- src/assertions/general/assertions/to_satisfy.rs | 4 ++-- .../general/assertions/to_satisfy_merge.rs | 6 +++--- src/assertions/general/outputs/initializable.rs | 10 +++++----- src/assertions/general/outputs/invert.rs | 16 ++++++++-------- src/assertions/general/outputs/unwrap.rs | 6 +++--- src/assertions/iterators/outputs/merge.rs | 8 ++++---- .../options/assertions/to_be_variant.rs | 4 ++-- .../results/assertions/to_be_variant.rs | 4 ++-- .../strings/assertions/to_contain_substr.rs | 4 ++-- .../strings/assertions/to_match_regex.rs | 4 ++-- src/lib.rs | 2 +- 16 files changed, 46 insertions(+), 46 deletions(-) diff --git a/src/assertions.rs b/src/assertions.rs index d32a939..d20e379 100644 --- a/src/assertions.rs +++ b/src/assertions.rs @@ -28,7 +28,7 @@ //! assertions::{Assertion, AssertionContext}, //! metadata::Annotated, //! prelude::*, -//! AssertionResult, +//! AssertionOutput, //! }; //! //! // Input parameters are automatically annotated, so we need to wrap them @@ -44,7 +44,7 @@ //! // 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 = AssertionResult; +//! type Output = AssertionOutput; //! //! fn execute(self, mut cx: AssertionContext, value: i32) -> Self::Output { //! cx.annotate("my annotation", "this appears in failure messages"); diff --git a/src/assertions/error.rs b/src/assertions/error.rs index 376a9be..da56d18 100644 --- a/src/assertions/error.rs +++ b/src/assertions/error.rs @@ -14,7 +14,7 @@ use super::AssertionContext; /// /// 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`](AssertionResult::into_result). +/// 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 @@ -24,12 +24,12 @@ use super::AssertionContext; /// output). #[derive(Clone, Debug)] #[must_use] -pub struct AssertionResult { +pub struct AssertionOutput { cx: AssertionContext, error: Option, } -impl AssertionResult { +impl AssertionOutput { #[inline] pub(crate) fn new(cx: AssertionContext, error: Option) -> Self { Self { cx, error } diff --git a/src/assertions/futures/outputs/completion_order.rs b/src/assertions/futures/outputs/completion_order.rs index 7cadc73..b4311dd 100644 --- a/src/assertions/futures/outputs/completion_order.rs +++ b/src/assertions/futures/outputs/completion_order.rs @@ -8,7 +8,7 @@ use pin_project_lite::pin_project; use crate::{ assertions::{Assertion, AssertionContext}, - AssertionResult, + AssertionOutput, }; pin_project! { @@ -51,9 +51,9 @@ impl Future for CompletionOrderFuture where Fut: Future, T: Future, - A: Assertion, + A: Assertion, { - type Output = AssertionResult; + type Output = AssertionOutput; fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { let projected = self.project(); diff --git a/src/assertions/general/assertions/to_cmp.rs b/src/assertions/general/assertions/to_cmp.rs index b87b052..6bbbc1a 100644 --- a/src/assertions/general/assertions/to_cmp.rs +++ b/src/assertions/general/assertions/to_cmp.rs @@ -3,7 +3,7 @@ use std::cmp::Ordering; use crate::{ assertions::{Assertion, AssertionContext}, metadata::Annotated, - AssertionResult, + AssertionOutput, }; /// Asserts that the target is less than the given value. @@ -113,7 +113,7 @@ impl Assertion for ToCmpAssertion where T: PartialOrd, { - type Output = AssertionResult; + type Output = AssertionOutput; fn execute(self, mut cx: AssertionContext, subject: T) -> Self::Output { cx.annotate("boundary", &self.boundary); diff --git a/src/assertions/general/assertions/to_equal.rs b/src/assertions/general/assertions/to_equal.rs index fb54fa3..3651e6d 100644 --- a/src/assertions/general/assertions/to_equal.rs +++ b/src/assertions/general/assertions/to_equal.rs @@ -1,7 +1,7 @@ use crate::{ assertions::{Assertion, AssertionContext}, metadata::Annotated, - AssertionResult, + AssertionOutput, }; /// Asserts that the subject is equal to the given value. @@ -32,7 +32,7 @@ impl Assertion for ToEqualAssertion where T: PartialEq, { - type Output = AssertionResult; + type Output = AssertionOutput; #[inline] fn execute(self, mut cx: AssertionContext, value: T) -> Self::Output { diff --git a/src/assertions/general/assertions/to_satisfy.rs b/src/assertions/general/assertions/to_satisfy.rs index 232c5f9..6120d2f 100644 --- a/src/assertions/general/assertions/to_satisfy.rs +++ b/src/assertions/general/assertions/to_satisfy.rs @@ -1,7 +1,7 @@ use crate::{ assertions::{Assertion, AssertionContext}, metadata::Annotated, - AssertionResult, + AssertionOutput, }; /// Asserts that the subject matches the given predicate. @@ -47,7 +47,7 @@ impl Assertion for ToSatisfyAssertion where F: FnOnce(T) -> bool, { - type Output = AssertionResult; + type Output = AssertionOutput; fn execute(self, mut cx: AssertionContext, subject: T) -> Self::Output { cx.annotate("predicate", &self.predicate); diff --git a/src/assertions/general/assertions/to_satisfy_merge.rs b/src/assertions/general/assertions/to_satisfy_merge.rs index 40a242e..d0673c8 100644 --- a/src/assertions/general/assertions/to_satisfy_merge.rs +++ b/src/assertions/general/assertions/to_satisfy_merge.rs @@ -104,12 +104,12 @@ where #[cfg(test)] mod tests { - use crate::{prelude::*, AssertionResult}; + use crate::{prelude::*, AssertionOutput}; #[test] fn vacuous() { - expect!(1, to_satisfy_all(|_| -> [AssertionResult; 0] { [] })); - expect!(1, not, to_satisfy_any(|_| -> [AssertionResult; 0] { [] })); + expect!(1, to_satisfy_all(|_| -> [AssertionOutput; 0] { [] })); + expect!(1, not, to_satisfy_any(|_| -> [AssertionOutput; 0] { [] })); } } diff --git a/src/assertions/general/outputs/initializable.rs b/src/assertions/general/outputs/initializable.rs index d1794b2..30d5997 100644 --- a/src/assertions/general/outputs/initializable.rs +++ b/src/assertions/general/outputs/initializable.rs @@ -1,4 +1,4 @@ -use crate::{assertions::AssertionContext, AssertionResult}; +use crate::{assertions::AssertionContext, AssertionOutput}; /// An assertion output that can be directly constructed from an /// [`AssertionContext`]. @@ -29,15 +29,15 @@ pub trait InitializableOutput { // fn into_initializable(self) -> Self::Initialized; } -impl InitializableOutput for AssertionResult { +impl InitializableOutput for AssertionOutput { #[inline] fn pass(cx: AssertionContext) -> Self { - AssertionResult::new(cx, None) + AssertionOutput::new(cx, None) } #[inline] fn fail(cx: AssertionContext, message: String) -> Self { - AssertionResult::new(cx, Some(message)) + AssertionOutput::new(cx, Some(message)) } } @@ -60,7 +60,7 @@ pub trait IntoInitializableOutput { fn into_initialized(self) -> Self::Initialized; } -impl IntoInitializableOutput for AssertionResult { +impl IntoInitializableOutput for AssertionOutput { type Initialized = Self; #[inline] diff --git a/src/assertions/general/outputs/invert.rs b/src/assertions/general/outputs/invert.rs index 285c57a..c746168 100644 --- a/src/assertions/general/outputs/invert.rs +++ b/src/assertions/general/outputs/invert.rs @@ -1,25 +1,25 @@ -use crate::{assertions::AssertionContext, AssertionResult}; +use crate::{assertions::AssertionContext, AssertionOutput}; -/// An assertion result that can be inverted. +/// An assertion output that can be inverted. /// -/// An inverted result is swapped from a failure to a success, or from a success +/// An inverted output is swapped from a failure to a success, or from a success /// to a failure. pub trait InvertibleOutput { - /// The inverted result. + /// The inverted output. type Inverted; - /// Inverts the result. + /// 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 result represents a success or - /// failure, then a value is returned that inverts that result when it is + /// 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. fn invert(self, cx: AssertionContext) -> Self::Inverted; } -impl InvertibleOutput for AssertionResult { +impl InvertibleOutput for AssertionOutput { type Inverted = Self; #[inline] diff --git a/src/assertions/general/outputs/unwrap.rs b/src/assertions/general/outputs/unwrap.rs index a0af29d..727e3f8 100644 --- a/src/assertions/general/outputs/unwrap.rs +++ b/src/assertions/general/outputs/unwrap.rs @@ -1,9 +1,9 @@ -use crate::AssertionResult; +use crate::AssertionOutput; /// An assertion output that can be unwrapped. /// /// Unwrapping the output causes it to panic as soon as possible. For -/// [`AssertionResult`]s, the value is converted into a [`Result`] and panics if +/// [`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). @@ -29,7 +29,7 @@ pub trait UnwrappableOutput { fn unwrap(self) -> Self::Unwrapped; } -impl UnwrappableOutput for AssertionResult { +impl UnwrappableOutput for AssertionOutput { type Unwrapped = (); #[inline] diff --git a/src/assertions/iterators/outputs/merge.rs b/src/assertions/iterators/outputs/merge.rs index 7acca96..24f4bb5 100644 --- a/src/assertions/iterators/outputs/merge.rs +++ b/src/assertions/iterators/outputs/merge.rs @@ -1,4 +1,4 @@ -use crate::{assertions::AssertionContext, AssertionResult}; +use crate::{assertions::AssertionContext, AssertionOutput}; /// A type of assertion output that can be collected from an iterator and merged /// into a single output. @@ -25,7 +25,7 @@ pub trait MergeableOutput { /// 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 [`AssertionResult`]s can be short-circuited + /// 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 @@ -33,8 +33,8 @@ pub trait MergeableOutput { I: IntoIterator; } -impl MergeableOutput for AssertionResult { - type Merged = AssertionResult; +impl MergeableOutput for AssertionOutput { + type Merged = AssertionOutput; #[inline] fn merge(cx: AssertionContext, strategy: MergeStrategy, outputs: I) -> Self::Merged diff --git a/src/assertions/options/assertions/to_be_variant.rs b/src/assertions/options/assertions/to_be_variant.rs index 87a7e6d..3e91b1c 100644 --- a/src/assertions/options/assertions/to_be_variant.rs +++ b/src/assertions/options/assertions/to_be_variant.rs @@ -1,6 +1,6 @@ use crate::{ assertions::{options::Optionish, Assertion, AssertionContext}, - AssertionResult, + AssertionOutput, }; /// Asserts that the subject holds a value. @@ -55,7 +55,7 @@ impl Assertion for ToBeOptionVariantAssertion where O: Optionish, { - type Output = AssertionResult; + type Output = AssertionOutput; fn execute(self, mut cx: AssertionContext, subject: O) -> Self::Output { cx.annotate("expected", format_args!("{:?}", self.expected)); diff --git a/src/assertions/results/assertions/to_be_variant.rs b/src/assertions/results/assertions/to_be_variant.rs index 03d6a25..81529fe 100644 --- a/src/assertions/results/assertions/to_be_variant.rs +++ b/src/assertions/results/assertions/to_be_variant.rs @@ -1,6 +1,6 @@ use crate::{ assertions::{results::Resultish, Assertion, AssertionContext}, - AssertionResult, + AssertionOutput, }; /// Asserts that the target holds a success. @@ -59,7 +59,7 @@ impl Assertion for ToBeResultVariantAssertion where R: Resultish, { - type Output = AssertionResult; + type Output = AssertionOutput; fn execute(self, mut cx: AssertionContext, subject: R) -> Self::Output { cx.annotate("expected", format_args!("{:?}", self.expected)); diff --git a/src/assertions/strings/assertions/to_contain_substr.rs b/src/assertions/strings/assertions/to_contain_substr.rs index b6a1cc5..92ff7a9 100644 --- a/src/assertions/strings/assertions/to_contain_substr.rs +++ b/src/assertions/strings/assertions/to_contain_substr.rs @@ -1,7 +1,7 @@ use crate::{ assertions::{Assertion, AssertionContext}, metadata::Annotated, - AssertionResult, + AssertionOutput, }; /// Asserts that the subject contains the given substring. @@ -37,7 +37,7 @@ where P: AsRef, T: AsRef, { - type Output = AssertionResult; + type Output = AssertionOutput; fn execute(self, mut cx: AssertionContext, subject: T) -> Self::Output { let pattern = self.pattern.inner().as_ref(); diff --git a/src/assertions/strings/assertions/to_match_regex.rs b/src/assertions/strings/assertions/to_match_regex.rs index b0ada0b..a0ac3f4 100644 --- a/src/assertions/strings/assertions/to_match_regex.rs +++ b/src/assertions/strings/assertions/to_match_regex.rs @@ -5,7 +5,7 @@ use regex::Regex; use crate::{ assertions::{Assertion, AssertionContext}, metadata::Annotated, - AssertionResult, + AssertionOutput, }; /// Asserts that the subject matches the given regular expression. @@ -42,7 +42,7 @@ impl Assertion for ToMatchRegexAssertion where T: AsRef, { - type Output = AssertionResult; + type Output = AssertionOutput; fn execute(self, mut cx: AssertionContext, subject: T) -> Self::Output { cx.annotate("pattern", &self.regex.as_str()); diff --git a/src/lib.rs b/src/lib.rs index a6c7b9e..b5c6b38 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -85,4 +85,4 @@ pub mod specialization; mod macros; -pub use assertions::AssertionResult; +pub use assertions::AssertionOutput; From c523b829b1649302da93fa8b2b01f4febf2129ba Mon Sep 17 00:00:00 2001 From: TehPers Date: Sat, 10 Aug 2024 16:58:53 -0700 Subject: [PATCH 14/37] Some doc updates --- src/assertions/general/outputs/invert.rs | 9 +++++++++ src/lib.rs | 7 +++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/assertions/general/outputs/invert.rs b/src/assertions/general/outputs/invert.rs index c746168..e02ff9c 100644 --- a/src/assertions/general/outputs/invert.rs +++ b/src/assertions/general/outputs/invert.rs @@ -16,6 +16,15 @@ pub trait InvertibleOutput { /// 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::not + /// [`to_equal`]: crate::prelude::to_equal fn invert(self, cx: AssertionContext) -> Self::Inverted; } diff --git a/src/lib.rs b/src/lib.rs index b5c6b38..b9b97cf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -46,6 +46,9 @@ //! predicate: |n| n % 2 == 1 //! ``` //! +//! See the [`expect!`] macro's documentation for usage information. For a full +//! list of modifiers and assertions, look at the [`prelude`] module. +//! //! ## Crate features //! //! Many of the assertions require certain crate features to be enabled. Default @@ -57,10 +60,6 @@ //! [regex](https://crates.io/crates/regex) to execute them. //! - `colors`*: Enables styled failure messages. Styled messages can always be //! disabled by setting `NO_COLOR`. -//! -//! See the [`expect!`] macro's documentation for usage information. For a full -//! list of modifiers and assertions, look at the [`prelude`] module. - #![warn( missing_debug_implementations, missing_docs, From 97a89db076161b8ebd3eae247d6d5a70bffe94ef Mon Sep 17 00:00:00 2001 From: TehPers Date: Sat, 10 Aug 2024 20:41:58 -0700 Subject: [PATCH 15/37] Simply some repetitive tests using test-case --- src/assertions/iterators/modifiers/merge.rs | 108 +++++++++----------- 1 file changed, 47 insertions(+), 61 deletions(-) diff --git a/src/assertions/iterators/modifiers/merge.rs b/src/assertions/iterators/modifiers/merge.rs index 5826d91..7e6b1c5 100644 --- a/src/assertions/iterators/modifiers/merge.rs +++ b/src/assertions/iterators/modifiers/merge.rs @@ -131,6 +131,8 @@ where 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 @@ -147,36 +149,13 @@ mod tests { output.is_ok() } - #[test] - fn test_any_short_circuit() { - let success = with_timeout(Duration::from_secs(1), || { - expect!(repeat(0), any, to_equal(0)); - }); - expect!(success, to_equal(true)); - } - - #[test] - fn test_any_infinite() { - let success = with_timeout(Duration::from_secs(1), || { - expect!(repeat(0), any, to_equal(1)); - }); - expect!(success, to_equal(false)); - } - - #[test] - fn test_all_short_circuit() { - let success = with_timeout(Duration::from_secs(1), || { - expect!(repeat(0), not, all, to_equal(1)); - }); - expect!(success, to_equal(true)); - } - - #[test] - fn test_all_infinite() { - let success = with_timeout(Duration::from_secs(1), || { - expect!(repeat(0), all, to_equal(0)); - }); - expect!(success, to_equal(false)); + #[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)); } } @@ -189,6 +168,7 @@ mod async_tests { time::Duration, }; + use test_case::test_case; use tokio::spawn; use crate::prelude::*; @@ -207,37 +187,43 @@ mod async_tests { 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] - #[ignore = "currently async assertions do not short-circuit"] - async fn test_any_short_circuit() { - let success = with_timeout(Duration::from_secs(1), async { - expect!(repeat(ready(0)), any, when_ready, to_equal(0)).await; - }); - expect!(success, to_equal(true)); - } - - #[tokio::test] - async fn test_any_infinite() { - let success = with_timeout(Duration::from_secs(1), async { - expect!(repeat(ready(0)), any, when_ready, to_equal(1)).await; - }); - expect!(success, to_equal(false)); - } - - #[tokio::test] - #[ignore = "currently async assertions do not short-circuit"] - async fn test_all_short_circuit() { - let success = with_timeout(Duration::from_secs(1), async { - expect!(repeat(ready(0)), not, all, when_ready, to_equal(1)).await; - }); - expect!(success, to_equal(true)); - } - - #[tokio::test] - async fn test_all_infinite() { - let success = with_timeout(Duration::from_secs(1), async { - expect!(repeat(ready(0)), all, when_ready, to_equal(0)).await; - }); - expect!(success, to_equal(false)); + 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)); } } From 386407e5187e35dca5531d221a0aee6139922c48 Mon Sep 17 00:00:00 2001 From: TehPers Date: Sat, 10 Aug 2024 22:06:48 -0700 Subject: [PATCH 16/37] Add try_unwrap --- src/assertions/futures/outputs/unwrapped.rs | 42 ++++- src/assertions/general/assertions.rs | 4 +- .../general/assertions/to_satisfy_merge.rs | 171 ------------------ .../general/assertions/to_satisfy_with.rs | 75 ++++++++ src/assertions/general/modifiers/not.rs | 2 +- src/assertions/general/outputs/unwrap.rs | 25 ++- src/macros.rs | 6 +- src/prelude.rs | 2 +- 8 files changed, 148 insertions(+), 179 deletions(-) delete mode 100644 src/assertions/general/assertions/to_satisfy_merge.rs create mode 100644 src/assertions/general/assertions/to_satisfy_with.rs diff --git a/src/assertions/futures/outputs/unwrapped.rs b/src/assertions/futures/outputs/unwrapped.rs index 827bfae..ee47d27 100644 --- a/src/assertions/futures/outputs/unwrapped.rs +++ b/src/assertions/futures/outputs/unwrapped.rs @@ -21,7 +21,7 @@ impl UnwrappedOutputFuture where F: Future, { - /// Create a new finalized output future. + /// Creates a new instance of this future. #[inline] pub fn new(inner: F) -> Self { Self { inner } @@ -43,14 +43,54 @@ where } } +pin_project! { + /// Tries to unwrap an asynchronous output. + #[derive(Clone, Debug)] + 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/general/assertions.rs b/src/assertions/general/assertions.rs index 18da712..e1707ad 100644 --- a/src/assertions/general/assertions.rs +++ b/src/assertions/general/assertions.rs @@ -1,9 +1,9 @@ mod to_cmp; mod to_equal; mod to_satisfy; -mod to_satisfy_merge; +mod to_satisfy_with; pub use to_cmp::*; pub use to_equal::*; pub use to_satisfy::*; -pub use to_satisfy_merge::*; +pub use to_satisfy_with::*; diff --git a/src/assertions/general/assertions/to_satisfy_merge.rs b/src/assertions/general/assertions/to_satisfy_merge.rs deleted file mode 100644 index d0673c8..0000000 --- a/src/assertions/general/assertions/to_satisfy_merge.rs +++ /dev/null @@ -1,171 +0,0 @@ -use crate::{ - assertions::{ - iterators::{MergeStrategy, MergeableOutput}, - Assertion, AssertionContext, - }, - metadata::Annotated, -}; - -/// Asserts that the subject matches all of the given predicates. This "forks" -/// the assertion, allowing an intermediate value to have several different -/// assertions applied to it. -/// -/// ``` -/// # use expecters::prelude::*; -/// expect!( -/// [1, 2, 3], -/// count, -/// to_satisfy_all(|value| [ -/// try_expect!(value, to_be_greater_than(0)), -/// try_expect!(value, to_be_less_than(4)), -/// ]), -/// ); -/// ``` -/// -/// The assertion fails if any of the results were failures: -/// -/// ```should_panic -/// # use expecters::prelude::*; -/// expect!( -/// [1, 2, 3], -/// count, -/// to_satisfy_all(|value| [ -/// try_expect!(value, to_be_greater_than(3)), -/// ]), -/// ); -/// ``` -#[inline] -#[must_use] -pub fn to_satisfy_all(predicates: Annotated) -> ToSatisfyMergeAssertion { - ToSatisfyMergeAssertion { - predicates, - strategy: MergeStrategy::All, - } -} - -/// Asserts that the subject matches any of the given predicates. This "forks" -/// the assertion, allowing an intermediate value to have several different -/// assertions applied to it. -/// -/// ``` -/// # use expecters::prelude::*; -/// expect!( -/// [1, 2, 3], -/// count, -/// to_satisfy_any(|value| [ -/// try_expect!(value, to_be_greater_than(0)), -/// try_expect!(value, to_be_less_than(0)), -/// ]), -/// ); -/// ``` -/// -/// The assertion fails if none of the results were successes: -/// -/// ```should_panic -/// # use expecters::prelude::*; -/// expect!( -/// [1, 2, 3], -/// count, -/// to_satisfy_any(|value| [ -/// try_expect!(value, to_be_greater_than(3)), -/// ]), -/// ); -/// ``` -#[inline] -#[must_use] -pub fn to_satisfy_any(predicates: Annotated) -> ToSatisfyMergeAssertion { - ToSatisfyMergeAssertion { - predicates, - strategy: MergeStrategy::Any, - } -} - -/// Assertion for [`to_satisfy_all()`] and [`to_satisfy_any()`]. -#[derive(Clone, Debug)] -pub struct ToSatisfyMergeAssertion { - predicates: Annotated, - strategy: MergeStrategy, -} - -impl Assertion for ToSatisfyMergeAssertion -where - F: FnOnce(T) -> R, - R: IntoIterator, -{ - type Output = ::Merged; - - fn execute(self, cx: AssertionContext, subject: T) -> Self::Output { - // TODO: allow result contexts to be "added" to cx so failure messages - // show the full execution path and not just the child path - let outputs = (self.predicates.into_inner())(subject); - MergeableOutput::merge(cx, self.strategy, outputs) - } -} - -#[cfg(test)] -mod tests { - use crate::{prelude::*, AssertionOutput}; - - #[test] - fn vacuous() { - expect!(1, to_satisfy_all(|_| -> [AssertionOutput; 0] { [] })); - expect!(1, not, to_satisfy_any(|_| -> [AssertionOutput; 0] { [] })); - } -} - -#[cfg(all(test, feature = "futures"))] -mod async_tests { - use std::future::ready; - - use crate::prelude::*; - - #[tokio::test] - async fn test_async_all() { - // Outer async - expect!( - ready([1, 2, 3]), - when_ready, - to_satisfy_all(|values| [try_expect!(values, count, to_equal(3))]), - ) - .await; - expect!( - ready([1, 2, 3]), - when_ready, - not, - to_satisfy_all(|values| [try_expect!(values, count, to_equal(4))]), - ) - .await; - - // Nested async - expect!( - ready([1, 2, 3]), - to_satisfy_all(|values| [try_expect!(values, when_ready, count, to_equal(3))]), - ) - .await; - } - - #[tokio::test] - async fn test_async_any() { - // Outer async - expect!( - ready([1, 2, 3]), - when_ready, - to_satisfy_any(|values| [try_expect!(values, count, to_equal(3))]), - ) - .await; - expect!( - ready([1, 2, 3]), - when_ready, - not, - to_satisfy_any(|values| [try_expect!(values, count, to_equal(4))]), - ) - .await; - - // Nested async - expect!( - ready([1, 2, 3]), - to_satisfy_any(|values| [try_expect!(values, when_ready, count, to_equal(3))]), - ) - .await; - } -} 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..faa8b42 --- /dev/null +++ b/src/assertions/general/assertions/to_satisfy_with.rs @@ -0,0 +1,75 @@ +use crate::{ + assertions::{Assertion, AssertionContext, AssertionError}, + metadata::Annotated, + AssertionOutput, +}; + +/// Asserts that the subject matches all of the given predicates. This "forks" +/// the assertion, allowing an intermediate value to have several different +/// assertions applied to it. +/// +/// ``` +/// # 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 with nested async assertions. +// TODO: make an async version +#[inline] +#[must_use] +pub fn to_satisfy_with(predicate: Annotated) -> ToSatisfyMergeAssertion { + ToSatisfyMergeAssertion { predicate } +} + +/// Assertion for [`to_satisfy_with()`]. +#[derive(Clone, Debug)] +pub struct ToSatisfyMergeAssertion { + predicate: Annotated, +} + +impl Assertion for ToSatisfyMergeAssertion +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/modifiers/not.rs b/src/assertions/general/modifiers/not.rs index 1da10a1..400ad92 100644 --- a/src/assertions/general/modifiers/not.rs +++ b/src/assertions/general/modifiers/not.rs @@ -65,7 +65,7 @@ mod tests { #[test] fn preserves_context() { - let res = try_expect!("blah", not, not, to_contain_substr("world")).into_result(); + let res = try_expect!("blah", not, not, to_contain_substr("world")); expect!( res, to_be_err_and, diff --git a/src/assertions/general/outputs/unwrap.rs b/src/assertions/general/outputs/unwrap.rs index 727e3f8..1a2802a 100644 --- a/src/assertions/general/outputs/unwrap.rs +++ b/src/assertions/general/outputs/unwrap.rs @@ -1,4 +1,4 @@ -use crate::AssertionOutput; +use crate::{assertions::AssertionError, AssertionOutput}; /// An assertion output that can be unwrapped. /// @@ -12,6 +12,10 @@ pub trait UnwrappableOutput { /// 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 @@ -27,10 +31,24 @@ pub trait UnwrappableOutput { /// 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] @@ -39,4 +57,9 @@ impl UnwrappableOutput for AssertionOutput { panic!("{e:?}") } } + + #[inline] + fn try_unwrap(self) -> Self::TryUnwrapped { + self.into_result() + } } diff --git a/src/macros.rs b/src/macros.rs index c99ba70..c31352c 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -177,14 +177,16 @@ macro_rules! expect { /// ``` /// # use expecters::prelude::*; /// let result = try_expect!(1, to_equal(2)); -/// expect!(result.into_result(), to_be_err); +/// 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::__expect_inner!($($tokens)*) + $crate::assertions::general::UnwrappableOutput::try_unwrap( + $crate::__expect_inner!($($tokens)*) + ) }; } diff --git a/src/prelude.rs b/src/prelude.rs index e3b6385..817b23e 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -15,7 +15,7 @@ pub use crate::{ assertions::{ general::{ map, not, to_be_greater_than, to_be_greater_than_or_equal_to, to_be_less_than, - to_be_less_than_or_equal_to, to_equal, to_satisfy, to_satisfy_all, to_satisfy_any, + to_be_less_than_or_equal_to, to_equal, to_satisfy, to_satisfy_with, }, iterators::{all, any, count, nth}, options::{to_be_none, to_be_some, to_be_some_and}, From 47e03271c14486c2ae2dd7dfea4047baa96d069c Mon Sep 17 00:00:00 2001 From: TehPers Date: Sat, 10 Aug 2024 22:07:30 -0700 Subject: [PATCH 17/37] Add support for non-clone subjects in all/any --- src/assertions/general/modifiers/annotate.rs | 52 +++++++++++++++++- src/assertions/iterators/modifiers/merge.rs | 58 +++++++++++++++++++- src/lib.rs | 8 ++- 3 files changed, 111 insertions(+), 7 deletions(-) diff --git a/src/assertions/general/modifiers/annotate.rs b/src/assertions/general/modifiers/annotate.rs index b719dfa..62b6df9 100644 --- a/src/assertions/general/modifiers/annotate.rs +++ b/src/assertions/general/modifiers/annotate.rs @@ -1,3 +1,5 @@ +use std::fmt::Debug; + use crate::{ assertions::{key, Assertion, AssertionContext, AssertionModifier, SubjectKey}, metadata::{Annotated, AnnotatedKind}, @@ -16,12 +18,35 @@ pub fn __annotate( /// 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. -#[derive(Clone, Debug)] 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.clone(), + } + } +} + +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>, @@ -39,12 +64,35 @@ where /// Assertion for [`AnnotateModifier`]. See the docs for the modifier for more /// information. -#[derive(Clone, Debug)] 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.clone(), + } + } +} + +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, diff --git a/src/assertions/iterators/modifiers/merge.rs b/src/assertions/iterators/modifiers/merge.rs index 7e6b1c5..b36d390 100644 --- a/src/assertions/iterators/modifiers/merge.rs +++ b/src/assertions/iterators/modifiers/merge.rs @@ -19,13 +19,25 @@ use crate::assertions::{ /// expect!([1, 3, 5], all, to_equal(5)); /// ``` /// -/// Requires that the rest of the assertion is [`Clone`]. For example, comparing -/// each item to a non-cloneable value will not compile: +/// 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, map(|NotClone(x)| x), to_equal(0)); +/// expect!([NotClone(0)], all, to_equal(NonClone(0))); /// ``` #[inline] pub fn all(prev: M, _: SubjectKey) -> (MergeModifier, SubjectKey) @@ -56,6 +68,27 @@ where /// # 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))); +/// ``` #[inline] pub fn any(prev: M, _: SubjectKey) -> (MergeModifier, SubjectKey) where @@ -226,4 +259,23 @@ mod async_tests { 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() { + 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/lib.rs b/src/lib.rs index b9b97cf..51de624 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,8 +10,12 @@ //! # async fn main() { //! 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; +//! 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 { From ce29c4152842213bf7acda19e0950b6d01bf4504 Mon Sep 17 00:00:00 2001 From: TehPers Date: Sat, 10 Aug 2024 22:07:39 -0700 Subject: [PATCH 18/37] Add install instructions to docs --- src/lib.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 51de624..06ed093 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,9 @@ -//! Build complex, self-describing assertions by chaining together reusable methods. -//! Supports both synchronous and asynchronous assertions. +//! Build complex, self-describing assertions by chaining together reusable +//! methods. Supports both synchronous and asynchronous assertions. +//! +//! ```sh +//! cargo add --dev expecters +//! ``` //! //! ## Example //! From 7d8e7a1c7d7a758c14221d944afda91ea7cc1eda Mon Sep 17 00:00:00 2001 From: TehPers Date: Sun, 11 Aug 2024 00:02:34 -0700 Subject: [PATCH 19/37] Update error to compile without colors feature --- src/assertions/error.rs | 56 +++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/src/assertions/error.rs b/src/assertions/error.rs index da56d18..260f9cb 100644 --- a/src/assertions/error.rs +++ b/src/assertions/error.rs @@ -3,8 +3,6 @@ use std::{ fmt::{Debug, Display, Formatter}, }; -use owo_colors::{OwoColorize, Stream, SupportsColorsDisplay}; - use crate::assertions::ContextFrame; use super::AssertionContext; @@ -87,43 +85,50 @@ impl AssertionError { } #[cfg(feature = "colors")] -fn colored<'a, T, U, F>(val: &'a T, apply: F) -> SupportsColorsDisplay<'a, T, U, F> -where - T: OwoColorize, - F: Fn(&'a T) -> U, -{ - val.if_supports_color(Stream::Stderr, apply) +mod styles { + use std::fmt::Display; + + use owo_colors::{OwoColorize, Stream}; + + pub fn dimmed<'a>(s: &'a impl Display) -> impl Display + 'a { + OwoColorize::if_supports_color(s, Stream::Stderr, |s| s.dimmed()) + } + + pub fn bright_red<'a>(s: &'a impl Display) -> impl Display + 'a { + OwoColorize::if_supports_color(s, Stream::Stderr, |s| s.bright_red()) + } +} + +#[cfg(not(feature = "colors"))] +mod styles { + pub fn dimmed(s: &T) -> &T { + s + } + + pub fn bright_red(s: &T) -> &T { + s + } } impl Debug for AssertionError { fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { - // TODO: keep colors? writeln!(f, "assertion failed:")?; writeln!( f, " {}", - colored(&format_args!("at: {}", self.cx.source_loc), |text| text - .dimmed()) + styles::dimmed(&format_args!("at: {}", self.cx.source_loc)), )?; writeln!( f, " {}", - colored(&format_args!("subject: {}", self.cx.subject), |text| text - .dimmed()) + styles::dimmed(&format_args!("subject: {}", self.cx.subject)), )?; writeln!(f)?; 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 { - #[cfg(feature = "colors")] - writeln!( - f, - " {}", - colored(&format_args!("{key}: {value}"), |text| text.dimmed()) - )?; - #[cfg(not(feature = "colors"))] - writeln!(f, " {key}: {value}")?; + writeln!(f, " {}", styles::dimmed(&format_args!("{key}: {value}")))?; } writeln!(f)?; Ok(()) @@ -134,7 +139,7 @@ impl Debug for AssertionError { let mut idx = 0; for frame in self.cx.visited.iter() { let comment = if idx == self.cx.visited.len() - 1 { - format!(" {}", colored(&self.message, |text| text.bright_red())) + format!(" {}", styles::bright_red(&self.message)) } else { String::new() }; @@ -150,11 +155,7 @@ impl Debug for AssertionError { // Write non-visited frames for frame in &self.cx.remaining[self.cx.recovered.len()..] { - writeln!( - f, - " {frame}: {}", - colored(&"(not visited)", |text| text.dimmed()) - )?; + writeln!(f, " {frame}: {}", styles::dimmed(&"(not visited)"))?; idx += 1; } @@ -163,6 +164,7 @@ impl Debug for AssertionError { } impl Display for AssertionError { + #[inline] fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { Debug::fmt(self, f) } From 50326e6adcd85e2dde6be8d1dcdd766ac6688cb3 Mon Sep 17 00:00:00 2001 From: TehPers Date: Sun, 11 Aug 2024 00:02:53 -0700 Subject: [PATCH 20/37] Cleanup some cfg attrs and remove unused code --- src/assertions/iterators/modifiers/merge.rs | 2 +- src/lib.rs | 9 ++- src/macros.rs | 49 ---------------- src/specialization/annotated_autoderef.rs | 64 --------------------- 4 files changed, 8 insertions(+), 116 deletions(-) delete mode 100644 src/specialization/annotated_autoderef.rs diff --git a/src/assertions/iterators/modifiers/merge.rs b/src/assertions/iterators/modifiers/merge.rs index b36d390..0fedc9f 100644 --- a/src/assertions/iterators/modifiers/merge.rs +++ b/src/assertions/iterators/modifiers/merge.rs @@ -192,7 +192,7 @@ mod tests { } } -#[cfg(test)] +#[cfg(all(test, feature = "futures"))] mod async_tests { use std::{ future::{ready, Future}, diff --git a/src/lib.rs b/src/lib.rs index 06ed093..57aa2cb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,10 +10,15 @@ //! ``` //! use expecters::prelude::*; //! -//! # #[tokio::main(flavor = "current_thread")] -//! # async fn main() { +#![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, diff --git a/src/macros.rs b/src/macros.rs index c31352c..3a3980a 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -313,52 +313,3 @@ macro_rules! __expect_inner { ) }; } - -#[cfg(test)] -mod tests { - use std::future::ready; - - use crate::prelude::*; - - #[derive(Clone, PartialEq)] - struct NotDebug(T); - - #[tokio::test] - #[ignore] - async fn test_debugging() { - debugging().await; - } - - async fn debugging() { - // expect!(ready(1), when_ready, to_equal(2)).await; - // expect!([1, 2, 3], count, not, to_equal(3)); - // expect!([1, 2, 3], any, to_equal(4)); - expect!([1, 2, 3], all, to_satisfy(|n| n % 2 == 1)); - - expect!("blah", to_match_regex(r"\d+")); - - expect!( - "Hello, world!", - map(|s: &str| format!("{} {} {}", s.to_uppercase(), s.to_lowercase(), s)), - to_contain_substr("not present in this text because this is a really long substring"), - ); - - expect!( - ready([NotDebug(1), NotDebug(2), NotDebug(3)]), - when_ready, - any, - not, - map(|NotDebug(x)| x), - to_be_less_than(4), - ) - .await; - - // expect!(ready([1, 2, 3]), when_ready, nth(3), to_equal(3)).await; - // expect!(ready(1), when_ready, to_satisfy(|n| n < 0)).await; - - // let res = try_expect!(ready(1), when_ready, to_satisfy(|n| n < 0)).await; - // res.into_result().unwrap(); - - // expect!([ready(1), ready(2)], all, when_ready, to_be_less_than(2)).await; - } -} diff --git a/src/specialization/annotated_autoderef.rs b/src/specialization/annotated_autoderef.rs deleted file mode 100644 index bc3c0eb..0000000 --- a/src/specialization/annotated_autoderef.rs +++ /dev/null @@ -1,64 +0,0 @@ -// BREAKS IN SOME CALLBACKS WHERE TYPE INFERENCE IS NEEDED: -// expect!(ready(1), when_ready, to_equal(1)); - -use std::fmt::{Debug, Display}; - -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 {} - -pub struct __AnnotatedDisplayTag; - -impl __AnnotatedDisplayTag { - pub fn annotate(self, value: T, stringified: &'static str) -> Annotated - where - T: Display, - { - Annotated::from_display(value, stringified) - } -} - -pub trait __AnnotatedDisplayKind { - #[inline] - fn __annotated_kind(&self) -> __AnnotatedDisplayTag { - __AnnotatedDisplayTag - } -} - -impl __AnnotatedDisplayKind for &&&__SpecializeWrapper where T: Display {} From 2f2797878d1ead8b5c4acf216e268108245c70d8 Mon Sep 17 00:00:00 2001 From: TehPers Date: Sun, 11 Aug 2024 00:03:07 -0700 Subject: [PATCH 21/37] Add integration tests (simple, error messages) --- tests/error_messages.rs | 70 +++++++++++++++++++++++++++++++++++++++++ tests/simple.rs | 10 ++++++ 2 files changed, 80 insertions(+) create mode 100644 tests/error_messages.rs create mode 100644 tests/simple.rs diff --git a/tests/error_messages.rs b/tests/error_messages.rs new file mode 100644 index 0000000..079a9d0 --- /dev/null +++ b/tests/error_messages.rs @@ -0,0 +1,70 @@ +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 alias() { + use expecters::prelude::not as my_not; + + expect!( + try_expect!(1, my_not, to_equal(1)), + to_be_err_and, + as_display, + to_contain_substr("my_not"), + ); +} + +#[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))); +} From 42a843ca3263d005a79090c9f25ef8dca45f66e9 Mon Sep 17 00:00:00 2001 From: TehPers Date: Sun, 11 Aug 2024 00:22:09 -0700 Subject: [PATCH 22/37] Fix clippy warnings --- src/assertions/context.rs | 1 + src/assertions/error.rs | 28 +++++++++---------- .../futures/outputs/completion_order.rs | 1 + src/assertions/general/modifiers/annotate.rs | 4 +-- src/assertions/iterators/modifiers/merge.rs | 9 +++--- .../strings/assertions/to_match_regex.rs | 10 +++++-- src/lib.rs | 2 +- src/metadata/annotated.rs | 1 + 8 files changed, 33 insertions(+), 23 deletions(-) diff --git a/src/assertions/context.rs b/src/assertions/context.rs index 48bac6e..a5898a8 100644 --- a/src/assertions/context.rs +++ b/src/assertions/context.rs @@ -102,6 +102,7 @@ impl AssertionContext { /// Creates a new success value. #[inline] + #[must_use] pub fn pass(self) -> O where O: InitializableOutput, diff --git a/src/assertions/error.rs b/src/assertions/error.rs index 260f9cb..16bc99a 100644 --- a/src/assertions/error.rs +++ b/src/assertions/error.rs @@ -90,12 +90,12 @@ mod styles { use owo_colors::{OwoColorize, Stream}; - pub fn dimmed<'a>(s: &'a impl Display) -> impl Display + 'a { - OwoColorize::if_supports_color(s, Stream::Stderr, |s| s.dimmed()) + pub fn dimmed(s: &impl Display) -> impl Display + '_ { + s.if_supports_color(Stream::Stderr, |s| s.dimmed()) } - pub fn bright_red<'a>(s: &'a impl Display) -> impl Display + 'a { - OwoColorize::if_supports_color(s, Stream::Stderr, |s| s.bright_red()) + pub fn bright_red(s: &impl Display) -> impl Display + '_ { + s.if_supports_color(Stream::Stderr, |s| s.bright_red()) } } @@ -110,6 +110,15 @@ mod styles { } } +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:")?; @@ -125,19 +134,10 @@ impl Debug for AssertionError { )?; writeln!(f)?; - 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(()) - } - // Write visited frames writeln!(f, "steps:")?; let mut idx = 0; - for frame in self.cx.visited.iter() { + for frame in &self.cx.visited { let comment = if idx == self.cx.visited.len() - 1 { format!(" {}", styles::bright_red(&self.message)) } else { diff --git a/src/assertions/futures/outputs/completion_order.rs b/src/assertions/futures/outputs/completion_order.rs index b4311dd..13bf219 100644 --- a/src/assertions/futures/outputs/completion_order.rs +++ b/src/assertions/futures/outputs/completion_order.rs @@ -63,6 +63,7 @@ where *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, diff --git a/src/assertions/general/modifiers/annotate.rs b/src/assertions/general/modifiers/annotate.rs index 62b6df9..c1a302c 100644 --- a/src/assertions/general/modifiers/annotate.rs +++ b/src/assertions/general/modifiers/annotate.rs @@ -30,7 +30,7 @@ where fn clone(&self) -> Self { Self { prev: self.prev.clone(), - annotate: self.annotate.clone(), + annotate: self.annotate, } } } @@ -76,7 +76,7 @@ where fn clone(&self) -> Self { Self { next: self.next.clone(), - annotate: self.annotate.clone(), + annotate: self.annotate, } } } diff --git a/src/assertions/iterators/modifiers/merge.rs b/src/assertions/iterators/modifiers/merge.rs index 0fedc9f..901cc1a 100644 --- a/src/assertions/iterators/modifiers/merge.rs +++ b/src/assertions/iterators/modifiers/merge.rs @@ -226,28 +226,28 @@ mod async_tests { // an infinite loop due to the iterator being collected into a // FuturesUnordered async { - expect!(repeat(ready(0)), all, when_ready, to_equal(0)).await + 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 + 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 + 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 + expect!(repeat(ready(0)), any, when_ready, to_equal(0)).await; } => ignore["not implemented yet"]; "any short-circuit" )] @@ -266,6 +266,7 @@ mod async_tests { /// 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") } diff --git a/src/assertions/strings/assertions/to_match_regex.rs b/src/assertions/strings/assertions/to_match_regex.rs index a0ac3f4..eb8f660 100644 --- a/src/assertions/strings/assertions/to_match_regex.rs +++ b/src/assertions/strings/assertions/to_match_regex.rs @@ -21,12 +21,18 @@ use crate::{ /// # 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. +#[allow(clippy::needless_pass_by_value)] pub fn to_match_regex

(pattern: Annotated

) -> ToMatchRegexAssertion where P: AsRef, { let pattern = pattern.inner().as_ref(); - let regex = Regex::new(pattern.as_ref()).expect("invalid regex"); + let regex = Regex::new(pattern).expect("invalid regex"); ToMatchRegexAssertion { regex: Arc::new(regex), } @@ -45,7 +51,7 @@ where type Output = AssertionOutput; fn execute(self, mut cx: AssertionContext, subject: T) -> Self::Output { - cx.annotate("pattern", &self.regex.as_str()); + cx.annotate("pattern", self.regex.as_str()); cx.pass_if( self.regex.is_match(subject.as_ref()), diff --git a/src/lib.rs b/src/lib.rs index 57aa2cb..7028122 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -86,7 +86,7 @@ clippy::pedantic, clippy::style )] -#![allow(clippy::module_name_repetitions)] +#![allow(clippy::missing_errors_doc, clippy::module_name_repetitions)] #![forbid(unsafe_code)] pub mod assertions; diff --git a/src/metadata/annotated.rs b/src/metadata/annotated.rs index 458ce4c..5a19440 100644 --- a/src/metadata/annotated.rs +++ b/src/metadata/annotated.rs @@ -144,6 +144,7 @@ mod tests { #[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, From b72a7f2c4c59083c1732a125a470bb5038e11a43 Mon Sep 17 00:00:00 2001 From: TehPers Date: Sun, 11 Aug 2024 00:24:06 -0700 Subject: [PATCH 23/37] Add CI workflow --- .github/workflows/ci.yaml | 65 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 .github/workflows/ci.yaml 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 From 345f12ba71ccc753648f69a3f8678a5667125d50 Mon Sep 17 00:00:00 2001 From: TehPers Date: Sun, 11 Aug 2024 00:42:11 -0700 Subject: [PATCH 24/37] Add license files and note to README --- LICENSE-APACHE | 201 +++++++++++++++++++++++++++++++++++++++++++++++++ LICENSE-MIT | 21 ++++++ README.md | 5 ++ 3 files changed, 227 insertions(+) create mode 100644 LICENSE-APACHE create mode 100644 LICENSE-MIT 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 index ea64492..42bd09f 100644 --- a/README.md +++ b/README.md @@ -20,3 +20,8 @@ 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. From f8be080b812cc63abb5ff76729e672a4412f354e Mon Sep 17 00:00:00 2001 From: TehPers Date: Sun, 11 Aug 2024 12:27:16 -0700 Subject: [PATCH 25/37] Remove extra borrow on SourceLoc --- src/assertions/context.rs | 4 ++-- src/metadata/source_loc.rs | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/assertions/context.rs b/src/assertions/context.rs index a5898a8..6f1da89 100644 --- a/src/assertions/context.rs +++ b/src/assertions/context.rs @@ -20,7 +20,7 @@ use super::general::InitializableOutput; #[derive(Clone, Debug)] pub struct AssertionContext { pub(crate) subject: String, - pub(crate) source_loc: &'static SourceLoc, + pub(crate) source_loc: SourceLoc, pub(crate) visited: Vec, pub(crate) remaining: &'static [&'static str], pub(crate) recovered: Vec, @@ -31,7 +31,7 @@ impl AssertionContext { #[must_use] pub fn __new( subject: String, - source_loc: &'static SourceLoc, + source_loc: SourceLoc, frames: &'static [&'static str], ) -> Self { Self { diff --git a/src/metadata/source_loc.rs b/src/metadata/source_loc.rs index c0cfcd4..849a8e5 100644 --- a/src/metadata/source_loc.rs +++ b/src/metadata/source_loc.rs @@ -3,15 +3,14 @@ use std::fmt::{Display, Formatter}; #[macro_export] #[doc(hidden)] macro_rules! source_loc { - () => {{ - const SOURCE_LOC: $crate::metadata::SourceLoc = $crate::metadata::SourceLoc::new( + () => { + $crate::metadata::SourceLoc::new( ::std::module_path!(), ::std::file!(), ::std::line!(), ::std::column!(), - ); - &SOURCE_LOC - }}; + ) + }; } /// A location in a source code file. From 2874be77e54f65cfb4a4dcc7ce05b1cf15359f7e Mon Sep 17 00:00:00 2001 From: TehPers Date: Sun, 11 Aug 2024 12:27:33 -0700 Subject: [PATCH 26/37] Disable publishing to avoid accidential publish --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 374effe..ce1a1be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ categories = [ "development-tools::testing", ] keywords = ["assert", "assertions", "async", "matchers", "testing"] +publish = false # TODO: remove this when ready to publish [features] default = ["colors", "futures", "regex"] From 8711c04437d3b0c49f15096de401ba48d1d124de Mon Sep 17 00:00:00 2001 From: TehPers Date: Sun, 11 Aug 2024 13:28:13 -0700 Subject: [PATCH 27/37] Simplfy context frame name collection --- src/macros.rs | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/src/macros.rs b/src/macros.rs index 3a3980a..1154147 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -204,7 +204,6 @@ macro_rules! __expect_inner { $crate::source_loc!(), $crate::__expect_inner!( @build_ctx_frames, - [], $($assertions)* ), ); @@ -219,31 +218,15 @@ macro_rules! __expect_inner { // Build context frame names (from modifier/assertion names) ( - // Base case @build_ctx_frames, - [$($frames:expr),*], - $frame_name:ident $(($($_:tt)*))? + $($frame_name:ident $(($($_:tt)*))?),* $(,)? ) => {{ const FRAMES: &'static [&'static str] = &[ - $($frames,)* - ::std::stringify!($frame_name), + $(::std::stringify!($frame_name),)* ]; FRAMES }}; - ( - // Recursive case - @build_ctx_frames, - [$($frames:expr),*], - $frame_name:ident $(($($_:tt)*))?, - $($assertions:tt)* - ) => { - $crate::__expect_inner!( - @build_ctx_frames, - [$($frames,)* ::std::stringify!($frame_name)], - $($assertions)* - ) - }; // Build assertion (chain modifiers and final assertion) ( From edc17dc4613657b3be40869d440e981f85e8158d Mon Sep 17 00:00:00 2001 From: TehPers Date: Sun, 11 Aug 2024 15:26:25 -0700 Subject: [PATCH 28/37] Restructure macro to provide better completions --- src/assertions.rs | 5 +- src/assertions/assertion.rs | 12 ++-- src/assertions/context.rs | 26 +++++--- .../futures/modifiers/completion_order.rs | 17 +++--- .../futures/modifiers/when_ready.rs | 7 ++- src/assertions/general/modifiers/annotate.rs | 17 ++++-- src/assertions/general/modifiers/map.rs | 17 ++++-- src/assertions/general/modifiers/not.rs | 7 ++- src/assertions/general/modifiers/root.rs | 11 ++-- src/assertions/iterators/modifiers/count.rs | 8 ++- src/assertions/iterators/modifiers/merge.rs | 15 +++-- src/assertions/iterators/modifiers/nth.rs | 17 +++--- src/assertions/options/modifiers/some_and.rs | 6 +- src/assertions/results/modifiers/err_and.rs | 6 +- src/assertions/results/modifiers/ok_and.rs | 6 +- src/assertions/strings/modifiers/debug.rs | 8 ++- src/assertions/strings/modifiers/display.rs | 8 ++- src/macros.rs | 61 +++++++++++++++---- src/metadata/annotated.rs | 2 + 19 files changed, 169 insertions(+), 87 deletions(-) diff --git a/src/assertions.rs b/src/assertions.rs index d20e379..1e14a9b 100644 --- a/src/assertions.rs +++ b/src/assertions.rs @@ -109,6 +109,7 @@ //! assertions::{ //! Assertion, //! AssertionContext, +//! AssertionContextBuilder, //! AssertionModifier, //! SubjectKey, //! key, @@ -140,8 +141,8 @@ //! { //! type Output = M::Output; // the output at this step, usually M::Output //! -//! fn apply(self, next: A) -> Self::Output { -//! self.0.apply(DividedByAssertion(next, self.1)) +//! fn apply(self, cx: AssertionContextBuilder, next: A) -> Self::Output { +//! self.0.apply(cx, DividedByAssertion(next, self.1)) //! } //! } //! diff --git a/src/assertions/assertion.rs b/src/assertions/assertion.rs index 7173285..018d3bd 100644 --- a/src/assertions/assertion.rs +++ b/src/assertions/assertion.rs @@ -1,6 +1,6 @@ use std::{fmt::Debug, marker::PhantomData}; -use super::AssertionContext; +use super::{AssertionContext, AssertionContextBuilder}; /// Evaluates a subject and determines whether it satisfies a condition. /// @@ -32,10 +32,12 @@ pub trait AssertionModifier { /// Applies this modifier to a given assertion, then executes the assertion. /// - /// This is generally a recursive function. - /// - /// TODO - fn apply(self, next: A) -> Self::Output; + /// 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; } /// Indicates the type of subject being passed to the next modifier/assertion in diff --git a/src/assertions/context.rs b/src/assertions/context.rs index 6f1da89..45ade5a 100644 --- a/src/assertions/context.rs +++ b/src/assertions/context.rs @@ -33,13 +33,15 @@ impl AssertionContext { subject: String, source_loc: SourceLoc, frames: &'static [&'static str], - ) -> Self { - Self { - subject, - source_loc, - visited: vec![], - remaining: frames, - recovered: vec![], + ) -> AssertionContextBuilder { + AssertionContextBuilder { + innerner: Self { + subject, + source_loc, + visited: vec![], + remaining: frames, + recovered: vec![], + }, } } @@ -177,6 +179,16 @@ impl AssertionContext { } } +/// 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, diff --git a/src/assertions/futures/modifiers/completion_order.rs b/src/assertions/futures/modifiers/completion_order.rs index b4d3e5c..0389869 100644 --- a/src/assertions/futures/modifiers/completion_order.rs +++ b/src/assertions/futures/modifiers/completion_order.rs @@ -3,7 +3,7 @@ use std::future::Future; use crate::{ assertions::{ futures::{CompletionOrder, CompletionOrderFuture}, - key, Assertion, AssertionContext, AssertionModifier, SubjectKey, + key, Assertion, AssertionContext, AssertionContextBuilder, AssertionModifier, SubjectKey, }, metadata::Annotated, }; @@ -120,12 +120,15 @@ where type Output = M::Output; #[inline] - fn apply(self, next: A) -> Self::Output { - self.prev.apply(CompletionOrderAssertion { - next, - fut: self.fut, - order: self.order, - }) + fn apply(self, cx: AssertionContextBuilder, next: A) -> Self::Output { + self.prev.apply( + cx, + CompletionOrderAssertion { + next, + fut: self.fut, + order: self.order, + }, + ) } } diff --git a/src/assertions/futures/modifiers/when_ready.rs b/src/assertions/futures/modifiers/when_ready.rs index b6d53a4..ed7d9b3 100644 --- a/src/assertions/futures/modifiers/when_ready.rs +++ b/src/assertions/futures/modifiers/when_ready.rs @@ -1,7 +1,8 @@ use std::future::Future; use crate::assertions::{ - futures::WhenReadyFuture, key, Assertion, AssertionContext, AssertionModifier, SubjectKey, + futures::WhenReadyFuture, key, Assertion, AssertionContext, AssertionContextBuilder, + AssertionModifier, SubjectKey, }; /// Executes an assertion on the output of a future. @@ -57,8 +58,8 @@ where { type Output = M::Output; - fn apply(self, next: A) -> Self::Output { - self.prev.apply(WhenReadyAssertion { next }) + fn apply(self, cx: AssertionContextBuilder, next: A) -> Self::Output { + self.prev.apply(cx, WhenReadyAssertion { next }) } } diff --git a/src/assertions/general/modifiers/annotate.rs b/src/assertions/general/modifiers/annotate.rs index c1a302c..1c4436c 100644 --- a/src/assertions/general/modifiers/annotate.rs +++ b/src/assertions/general/modifiers/annotate.rs @@ -1,7 +1,9 @@ use std::fmt::Debug; use crate::{ - assertions::{key, Assertion, AssertionContext, AssertionModifier, SubjectKey}, + assertions::{ + key, Assertion, AssertionContext, AssertionContextBuilder, AssertionModifier, SubjectKey, + }, metadata::{Annotated, AnnotatedKind}, }; @@ -54,11 +56,14 @@ where type Output = M::Output; #[inline] - fn apply(self, assertion: A) -> Self::Output { - self.prev.apply(AnnotateAssertion { - next: assertion, - annotate: self.annotate, - }) + fn apply(self, cx: AssertionContextBuilder, assertion: A) -> Self::Output { + self.prev.apply( + cx, + AnnotateAssertion { + next: assertion, + annotate: self.annotate, + }, + ) } } diff --git a/src/assertions/general/modifiers/map.rs b/src/assertions/general/modifiers/map.rs index a274f1e..79c58a3 100644 --- a/src/assertions/general/modifiers/map.rs +++ b/src/assertions/general/modifiers/map.rs @@ -1,5 +1,7 @@ use crate::{ - assertions::{key, Assertion, AssertionContext, AssertionModifier, SubjectKey}, + assertions::{ + key, Assertion, AssertionContext, AssertionContextBuilder, AssertionModifier, SubjectKey, + }, metadata::Annotated, }; @@ -81,11 +83,14 @@ where type Output = M::Output; #[inline] - fn apply(self, next: A) -> Self::Output { - self.prev.apply(MapAssertion { - next, - map: self.map, - }) + fn apply(self, cx: AssertionContextBuilder, next: A) -> Self::Output { + self.prev.apply( + cx, + MapAssertion { + next, + map: self.map, + }, + ) } } diff --git a/src/assertions/general/modifiers/not.rs b/src/assertions/general/modifiers/not.rs index 400ad92..3171563 100644 --- a/src/assertions/general/modifiers/not.rs +++ b/src/assertions/general/modifiers/not.rs @@ -1,5 +1,6 @@ use crate::assertions::{ - general::InvertibleOutput, key, Assertion, AssertionContext, AssertionModifier, SubjectKey, + general::InvertibleOutput, key, Assertion, AssertionContext, AssertionContextBuilder, + AssertionModifier, SubjectKey, }; /// Inverts the result of an assertion. @@ -36,8 +37,8 @@ where type Output = M::Output; #[inline] - fn apply(self, next: A) -> Self::Output { - self.prev.apply(NotAssertion { next }) + fn apply(self, cx: AssertionContextBuilder, next: A) -> Self::Output { + self.prev.apply(cx, NotAssertion { next }) } } diff --git a/src/assertions/general/modifiers/root.rs b/src/assertions/general/modifiers/root.rs index 108fb30..524e36f 100644 --- a/src/assertions/general/modifiers/root.rs +++ b/src/assertions/general/modifiers/root.rs @@ -1,18 +1,17 @@ use crate::{ - assertions::{key, Assertion, AssertionContext, AssertionModifier, SubjectKey}, + assertions::{key, Assertion, AssertionContextBuilder, AssertionModifier, SubjectKey}, metadata::Annotated, }; #[doc(hidden)] #[inline] -pub fn __root(cx: AssertionContext, subject: Annotated) -> (Root, SubjectKey) { - (Root { cx, subject }, key()) +pub fn __root(subject: Annotated) -> (Root, SubjectKey) { + (Root { subject }, key()) } /// The root of an assertion. #[derive(Clone, Debug)] pub struct Root { - cx: AssertionContext, subject: Annotated, } @@ -23,7 +22,7 @@ where type Output = A::Output; #[inline] - fn apply(self, assertion: A) -> Self::Output { - assertion.execute(self.cx, self.subject.into_inner()) + fn apply(self, cx: AssertionContextBuilder, assertion: A) -> Self::Output { + assertion.execute(cx.innerner, self.subject.into_inner()) } } diff --git a/src/assertions/iterators/modifiers/count.rs b/src/assertions/iterators/modifiers/count.rs index 6b6ec64..4ebd58a 100644 --- a/src/assertions/iterators/modifiers/count.rs +++ b/src/assertions/iterators/modifiers/count.rs @@ -1,4 +1,6 @@ -use crate::assertions::{key, Assertion, AssertionContext, AssertionModifier, SubjectKey}; +use crate::assertions::{ + key, Assertion, AssertionContext, AssertionContextBuilder, AssertionModifier, SubjectKey, +}; /// Counts the length of the subject, and executes an assertion on the result. /// @@ -29,8 +31,8 @@ where type Output = M::Output; #[inline] - fn apply(self, next: A) -> Self::Output { - self.prev.apply(CountAssertion { next }) + fn apply(self, cx: AssertionContextBuilder, next: A) -> Self::Output { + self.prev.apply(cx, CountAssertion { next }) } } diff --git a/src/assertions/iterators/modifiers/merge.rs b/src/assertions/iterators/modifiers/merge.rs index 901cc1a..24d2886 100644 --- a/src/assertions/iterators/modifiers/merge.rs +++ b/src/assertions/iterators/modifiers/merge.rs @@ -1,6 +1,6 @@ use crate::assertions::{ iterators::{MergeStrategy, MergeableOutput}, - key, Assertion, AssertionContext, AssertionModifier, SubjectKey, + key, Assertion, AssertionContext, AssertionContextBuilder, AssertionModifier, SubjectKey, }; /// Executes an assertion on every value within the subject, and succeeds if and @@ -117,11 +117,14 @@ where type Output = M::Output; #[inline] - fn apply(self, next: A) -> Self::Output { - self.prev.apply(MergeAssertion { - next, - strategy: self.strategy, - }) + fn apply(self, cx: AssertionContextBuilder, next: A) -> Self::Output { + self.prev.apply( + cx, + MergeAssertion { + next, + strategy: self.strategy, + }, + ) } } diff --git a/src/assertions/iterators/modifiers/nth.rs b/src/assertions/iterators/modifiers/nth.rs index 233ebfc..748a1a5 100644 --- a/src/assertions/iterators/modifiers/nth.rs +++ b/src/assertions/iterators/modifiers/nth.rs @@ -1,7 +1,7 @@ use crate::{ assertions::{ - general::IntoInitializableOutput, key, Assertion, AssertionContext, AssertionModifier, - SubjectKey, + general::IntoInitializableOutput, key, Assertion, AssertionContext, + AssertionContextBuilder, AssertionModifier, SubjectKey, }, metadata::Annotated, }; @@ -54,11 +54,14 @@ where type Output = M::Output; #[inline] - fn apply(self, next: A) -> Self::Output { - self.prev.apply(NthAssertion { - next, - index: self.index, - }) + fn apply(self, cx: AssertionContextBuilder, next: A) -> Self::Output { + self.prev.apply( + cx, + NthAssertion { + next, + index: self.index, + }, + ) } } diff --git a/src/assertions/options/modifiers/some_and.rs b/src/assertions/options/modifiers/some_and.rs index 6312d3b..5f95df5 100644 --- a/src/assertions/options/modifiers/some_and.rs +++ b/src/assertions/options/modifiers/some_and.rs @@ -1,6 +1,6 @@ use crate::assertions::{ general::IntoInitializableOutput, key, options::Optionish, Assertion, AssertionContext, - AssertionModifier, SubjectKey, + AssertionContextBuilder, AssertionModifier, SubjectKey, }; /// Asserts that the subject holds a value, then continues the assertion with @@ -38,8 +38,8 @@ where type Output = M::Output; #[inline] - fn apply(self, next: A) -> Self::Output { - self.prev.apply(SomeAndAssertion { next }) + fn apply(self, cx: AssertionContextBuilder, next: A) -> Self::Output { + self.prev.apply(cx, SomeAndAssertion { next }) } } diff --git a/src/assertions/results/modifiers/err_and.rs b/src/assertions/results/modifiers/err_and.rs index 8089a33..15cb56c 100644 --- a/src/assertions/results/modifiers/err_and.rs +++ b/src/assertions/results/modifiers/err_and.rs @@ -1,6 +1,6 @@ use crate::assertions::{ general::IntoInitializableOutput, key, results::Resultish, Assertion, AssertionContext, - AssertionModifier, SubjectKey, + AssertionContextBuilder, AssertionModifier, SubjectKey, }; /// Asserts that the target holds an error, then continues the assertion with @@ -40,8 +40,8 @@ where type Output = M::Output; #[inline] - fn apply(self, next: A) -> Self::Output { - self.prev.apply(ErrAndAssertion { next }) + fn apply(self, cx: AssertionContextBuilder, next: A) -> Self::Output { + self.prev.apply(cx, ErrAndAssertion { next }) } } diff --git a/src/assertions/results/modifiers/ok_and.rs b/src/assertions/results/modifiers/ok_and.rs index 92c9284..296300e 100644 --- a/src/assertions/results/modifiers/ok_and.rs +++ b/src/assertions/results/modifiers/ok_and.rs @@ -1,6 +1,6 @@ use crate::assertions::{ general::IntoInitializableOutput, key, results::Resultish, Assertion, AssertionContext, - AssertionModifier, SubjectKey, + AssertionContextBuilder, AssertionModifier, SubjectKey, }; /// Asserts that the target holds a success, then continues the assertion with @@ -40,8 +40,8 @@ where type Output = M::Output; #[inline] - fn apply(self, next: A) -> Self::Output { - self.prev.apply(OkAndAssertion { next }) + fn apply(self, cx: AssertionContextBuilder, next: A) -> Self::Output { + self.prev.apply(cx, OkAndAssertion { next }) } } diff --git a/src/assertions/strings/modifiers/debug.rs b/src/assertions/strings/modifiers/debug.rs index fbe2256..91b55be 100644 --- a/src/assertions/strings/modifiers/debug.rs +++ b/src/assertions/strings/modifiers/debug.rs @@ -1,6 +1,8 @@ use std::fmt::Debug; -use crate::assertions::{key, Assertion, AssertionContext, AssertionModifier, SubjectKey}; +use crate::assertions::{ + key, Assertion, AssertionContext, AssertionContextBuilder, AssertionModifier, SubjectKey, +}; /// Converts a value to its [`Debug`] representation. /// @@ -29,8 +31,8 @@ where type Output = M::Output; #[inline] - fn apply(self, next: A) -> Self::Output { - self.prev.apply(AsDebugAssertion { next }) + fn apply(self, cx: AssertionContextBuilder, next: A) -> Self::Output { + self.prev.apply(cx, AsDebugAssertion { next }) } } diff --git a/src/assertions/strings/modifiers/display.rs b/src/assertions/strings/modifiers/display.rs index 68e4c66..ec9086b 100644 --- a/src/assertions/strings/modifiers/display.rs +++ b/src/assertions/strings/modifiers/display.rs @@ -1,6 +1,8 @@ use std::fmt::Display; -use crate::assertions::{key, Assertion, AssertionContext, AssertionModifier, SubjectKey}; +use crate::assertions::{ + key, Assertion, AssertionContext, AssertionContextBuilder, AssertionModifier, SubjectKey, +}; /// Converts a value to its [`Display`] representation. /// @@ -29,8 +31,8 @@ where type Output = M::Output; #[inline] - fn apply(self, next: A) -> Self::Output { - self.prev.apply(AsDisplayAssertion { next }) + fn apply(self, cx: AssertionContextBuilder, next: A) -> Self::Output { + self.prev.apply(cx, AsDisplayAssertion { next }) } } diff --git a/src/macros.rs b/src/macros.rs index 1154147..87fbfb4 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -190,6 +190,11 @@ macro_rules! try_expect { }; } +// 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 { @@ -199,17 +204,12 @@ macro_rules! __expect_inner { $($assertions:tt)* ) => {{ let subject = $crate::annotated!($subject); - let cx = $crate::assertions::AssertionContext::__new( - ::std::string::ToString::to_string(&subject), - $crate::source_loc!(), - $crate::__expect_inner!( - @build_ctx_frames, - $($assertions)* - ), - ); - let (root, _key) = $crate::assertions::general::__root(cx, subject); + let subject_repr = ::std::string::ToString::to_string(&subject); + let (root, _key) = $crate::assertions::general::__root(subject); $crate::__expect_inner!( @build_assertion, + [], + subject_repr, root, _key, $($assertions)* @@ -232,6 +232,8 @@ macro_rules! __expect_inner { ( // Base case (with params) @build_assertion, + [$($frame_name:expr,)*], + $subject:expr, $chain:expr, $key:expr, $assertion:ident($($param:expr),* $(,)?) @@ -239,24 +241,47 @@ macro_rules! __expect_inner { ) => {{ let (chain, _key) = $crate::__expect_inner!(@annotate, $chain, $key); let assertion = $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::AssertionModifier::apply( chain, + cx, assertion, ) }}; ( // Base case (without params) @build_assertion, + [$($frame_name:expr,)*], + $subject:expr, $chain:expr, $key:expr, $assertion:ident $(,)? ) => { - $crate::__expect_inner!(@build_assertion, $chain, $key, $assertion()) + $crate::__expect_inner!( + @build_assertion, + [$($frame_name,)*], + $subject, + $chain, + $key, + $assertion() + ) }; ( // Recursive case (with params) @build_assertion, + [$($frame_name:expr,)*], + $subject:expr, $chain:expr, $key:expr, $modifier:ident($($param:expr),* $(,)?), @@ -268,11 +293,23 @@ macro_rules! __expect_inner { key, $($crate::annotated!($param),)* ); - $crate::__expect_inner!(@build_assertion, chain, _key, $($rest)*) + $crate::__expect_inner!( + @build_assertion, + [ + $($frame_name,)* + ::std::stringify!($modifier), + ], + $subject, + chain, + _key, + $($rest)* + ) }}; ( // Recursive case (without params) @build_assertion, + [$($frame_name:expr,)*], + $subject:expr, $chain:expr, $key:expr, $modifier:ident, @@ -280,6 +317,8 @@ macro_rules! __expect_inner { ) => {{ $crate::__expect_inner!( @build_assertion, + [$($frame_name,)*], + $subject, $chain, $key, $modifier(), diff --git a/src/metadata/annotated.rs b/src/metadata/annotated.rs index 5a19440..1ccd4be 100644 --- a/src/metadata/annotated.rs +++ b/src/metadata/annotated.rs @@ -7,6 +7,8 @@ macro_rules! annotated { #[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() From bdc0112278cc53b4dbc94fd213918033014632aa Mon Sep 17 00:00:00 2001 From: TehPers Date: Sun, 11 Aug 2024 21:43:23 -0700 Subject: [PATCH 29/37] Remove extra branch from __expect_inner --- src/macros.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/macros.rs b/src/macros.rs index 87fbfb4..fc28675 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -216,18 +216,6 @@ macro_rules! __expect_inner { ) }}; - // Build context frame names (from modifier/assertion names) - ( - @build_ctx_frames, - $($frame_name:ident $(($($_:tt)*))?),* - $(,)? - ) => {{ - const FRAMES: &'static [&'static str] = &[ - $(::std::stringify!($frame_name),)* - ]; - FRAMES - }}; - // Build assertion (chain modifiers and final assertion) ( // Base case (with params) From c29b369e93c75dce70dd9ff99fca74cb06e84b68 Mon Sep 17 00:00:00 2001 From: TehPers Date: Sun, 11 Aug 2024 21:43:58 -0700 Subject: [PATCH 30/37] Remove extra braces --- src/macros.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/macros.rs b/src/macros.rs index fc28675..67e8414 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -302,7 +302,7 @@ macro_rules! __expect_inner { $key:expr, $modifier:ident, $($rest:tt)* - ) => {{ + ) => { $crate::__expect_inner!( @build_assertion, [$($frame_name,)*], @@ -312,7 +312,7 @@ macro_rules! __expect_inner { $modifier(), $($rest)* ) - }}; + }; // Annotate the value being passed down the chain (@annotate, $chain:expr, $key:expr) => { From e8240e605f753d91b9a8b3d17b5dca25068c014d Mon Sep 17 00:00:00 2001 From: TehPers Date: Mon, 12 Aug 2024 10:23:54 -0700 Subject: [PATCH 31/37] Change macro to work with extension methods --- src/assertions/assertion.rs | 99 ++++-- src/assertions/futures.rs | 2 + src/assertions/futures/extensions.rs | 155 +++++++++ .../futures/modifiers/completion_order.rs | 112 +------ .../futures/modifiers/when_ready.rs | 58 +--- src/assertions/general.rs | 2 + src/assertions/general/assertions/to_cmp.rs | 121 ++----- src/assertions/general/assertions/to_equal.rs | 27 +- .../general/assertions/to_satisfy.rs | 42 +-- .../general/assertions/to_satisfy_with.rs | 52 +-- src/assertions/general/extensions.rs | 304 ++++++++++++++++++ src/assertions/general/modifiers/annotate.rs | 9 +- src/assertions/general/modifiers/map.rs | 79 +---- src/assertions/general/modifiers/not.rs | 43 +-- src/assertions/general/modifiers/root.rs | 15 +- src/assertions/iterators.rs | 2 + src/assertions/iterators/extensions.rs | 147 +++++++++ src/assertions/iterators/modifiers/count.rs | 31 +- src/assertions/iterators/modifiers/merge.rs | 113 +------ src/assertions/iterators/modifiers/nth.rs | 50 +-- src/assertions/options.rs | 2 + .../options/assertions/to_be_variant.rs | 62 +--- src/assertions/options/extensions.rs | 73 +++++ src/assertions/options/modifiers/some_and.rs | 37 +-- src/assertions/results.rs | 2 + .../results/assertions/to_be_variant.rs | 66 +--- src/assertions/results/extensions.rs | 102 ++++++ src/assertions/results/modifiers/err_and.rs | 39 +-- src/assertions/results/modifiers/ok_and.rs | 39 +-- src/assertions/strings.rs | 2 + .../strings/assertions/to_contain_substr.rs | 30 +- .../strings/assertions/to_match_regex.rs | 44 +-- src/assertions/strings/extensions.rs | 118 +++++++ src/assertions/strings/modifiers/debug.rs | 30 +- src/assertions/strings/modifiers/display.rs | 30 +- src/macros.rs | 56 ++-- src/prelude.rs | 18 +- 37 files changed, 1246 insertions(+), 967 deletions(-) create mode 100644 src/assertions/futures/extensions.rs create mode 100644 src/assertions/general/extensions.rs create mode 100644 src/assertions/iterators/extensions.rs create mode 100644 src/assertions/options/extensions.rs create mode 100644 src/assertions/results/extensions.rs create mode 100644 src/assertions/strings/extensions.rs diff --git a/src/assertions/assertion.rs b/src/assertions/assertion.rs index 018d3bd..43e8a28 100644 --- a/src/assertions/assertion.rs +++ b/src/assertions/assertion.rs @@ -1,6 +1,11 @@ -use std::{fmt::Debug, marker::PhantomData}; +use std::{ + fmt::{Debug, Formatter}, + marker::PhantomData, +}; -use super::{AssertionContext, AssertionContextBuilder}; +use crate::metadata::Annotated; + +use super::{general::Root, AssertionContext, AssertionContextBuilder}; /// Evaluates a subject and determines whether it satisfies a condition. /// @@ -40,40 +45,76 @@ pub trait AssertionModifier { fn apply(self, cx: AssertionContextBuilder, next: A) -> Self::Output; } -/// Indicates the type of subject being passed to the next modifier/assertion in -/// the chain. -/// -/// This is necessary to help the compiler infer the type of the value being -/// passed from a modifier to the next modifier/assertion. -/// -/// To create an instance of it, use: +/// Builds an assertion. /// -/// ``` -/// use expecters::assertions::key; -/// let key = key::(); -/// # let _ = key; -/// ``` -pub struct SubjectKey(PhantomData); +/// To apply a modifier to this assertion, see [`Self::modify`]. +#[must_use] +pub struct AssertionBuilder { + modifier: M, + marker: PhantomData, +} -impl Clone for SubjectKey { - #[inline] - fn clone(&self) -> Self { - *self +impl AssertionBuilder> { + #[doc(hidden)] + pub fn __new(subject: Annotated) -> Self { + AssertionBuilder { + modifier: Root::new(subject), + marker: PhantomData, + } } } -impl Copy for SubjectKey {} +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 Debug for SubjectKey { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_tuple("SubjectKey").field(&self.0).finish() +impl Clone for AssertionBuilder +where + M: Clone, +{ + #[inline] + fn clone(&self) -> Self { + Self { + modifier: self.modifier.clone(), + marker: self.marker, + } } } -/// Creates a new key for a subject. Modifiers must return a key on creation to -/// indicate the type of subject the next modifier/assertion is expected to -/// receive. -#[must_use] -pub const fn key() -> SubjectKey { - SubjectKey(PhantomData) +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/futures.rs b/src/assertions/futures.rs index 45ad018..7e8ff80 100644 --- a/src/assertions/futures.rs +++ b/src/assertions/futures.rs @@ -16,8 +16,10 @@ //! # } //! ``` +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..c14f48e --- /dev/null +++ b/src/assertions/futures/extensions.rs @@ -0,0 +1,155 @@ +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::{pending, ready}; + /// # #[tokio::main(flavor = "current_thread")] + /// # async fn main() { + /// expect!(ready(1), when_ready_before(pending::<()>()), to_equal(1)).await; + /// expect!(ready(1), when_ready_before(ready(())), to_equal(1)).await; + /// # } + /// ``` + /// + /// The assertion fails if the provided future completes first: + /// + /// ```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 fut = async { + /// sleep(Duration::from_secs(1)).await; + /// 1 + /// }; + /// expect!(fut, when_ready_after(ready(())), to_equal(1)).await; + /// expect!(ready(1), when_ready_after(ready(())), to_equal(1)).await; + /// # } + /// ``` + /// + /// The assertion fails if the provided future 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/completion_order.rs b/src/assertions/futures/modifiers/completion_order.rs index 0389869..673781d 100644 --- a/src/assertions/futures/modifiers/completion_order.rs +++ b/src/assertions/futures/modifiers/completion_order.rs @@ -3,109 +3,13 @@ use std::future::Future; use crate::{ assertions::{ futures::{CompletionOrder, CompletionOrderFuture}, - key, Assertion, AssertionContext, AssertionContextBuilder, AssertionModifier, SubjectKey, + Assertion, AssertionContext, AssertionContextBuilder, AssertionModifier, }, metadata::Annotated, }; -/// 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::{pending, ready}; -/// # #[tokio::main(flavor = "current_thread")] -/// # async fn main() { -/// expect!(ready(1), when_ready_before(pending::<()>()), to_equal(1)).await; -/// expect!(ready(1), when_ready_before(ready(())), to_equal(1)).await; -/// # } -/// ``` -/// -/// The assertion fails if the provided future completes first: -/// -/// ```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; -/// # } -/// ``` -pub fn when_ready_before( - prev: M, - _: SubjectKey, - fut: Annotated, -) -> (CompletionOrderModifier, SubjectKey) -where - Fut: Future, - T: Future, -{ - ( - CompletionOrderModifier { - prev, - fut, - order: CompletionOrder::Before, - }, - key(), - ) -} - -/// 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 fut = async { -/// sleep(Duration::from_secs(1)).await; -/// 1 -/// }; -/// expect!(fut, when_ready_after(ready(())), to_equal(1)).await; -/// expect!(ready(1), when_ready_after(ready(())), to_equal(1)).await; -/// # } -/// ``` -/// -/// The assertion fails if the provided future 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; -/// # } -/// ``` -pub fn when_ready_after( - prev: M, - _: SubjectKey, - fut: Annotated, -) -> (CompletionOrderModifier, SubjectKey) -where - Fut: Future, - T: Future, -{ - ( - CompletionOrderModifier { - prev, - fut, - order: CompletionOrder::After, - }, - key(), - ) -} - -/// Modifier for [`when_ready_before()`] and [`when_ready_after()`]. +/// Executes an assertion when the subject completes before or after another +/// future. #[derive(Clone, Debug)] pub struct CompletionOrderModifier { prev: M, @@ -113,6 +17,13 @@ pub struct CompletionOrderModifier { 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>, @@ -132,7 +43,8 @@ where } } -/// Assertion for [`when_ready_before()`] and [`when_ready_after()`]. +/// Executes the inner assertion when the subject completes before or after +/// another future. #[derive(Clone, Debug)] pub struct CompletionOrderAssertion { next: A, diff --git a/src/assertions/futures/modifiers/when_ready.rs b/src/assertions/futures/modifiers/when_ready.rs index ed7d9b3..8386652 100644 --- a/src/assertions/futures/modifiers/when_ready.rs +++ b/src/assertions/futures/modifiers/when_ready.rs @@ -1,69 +1,36 @@ use std::future::Future; use crate::assertions::{ - futures::WhenReadyFuture, key, Assertion, AssertionContext, AssertionContextBuilder, - AssertionModifier, SubjectKey, + futures::WhenReadyFuture, Assertion, AssertionContext, AssertionContextBuilder, + AssertionModifier, }; -/// 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; -/// # } -/// ``` -#[inline] -pub fn when_ready(prev: M, _: SubjectKey) -> (WhenReadyModifier, SubjectKey) -where - T: Future, -{ - (WhenReadyModifier { prev }, key()) -} - -/// Modifier for [`when_ready`]. +/// 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 }) } } -/// Assertion for [`when_ready`]. +/// Executes the inner assertion when the subject is ready. #[derive(Clone, Debug)] pub struct WhenReadyAssertion { next: A, @@ -76,6 +43,7 @@ where { type Output = WhenReadyFuture; + #[inline] fn execute(self, cx: AssertionContext, subject: T) -> Self::Output { WhenReadyFuture::new(cx, subject, self.next) } diff --git a/src/assertions/general.rs b/src/assertions/general.rs index d607713..37dfc42 100644 --- a/src/assertions/general.rs +++ b/src/assertions/general.rs @@ -9,9 +9,11 @@ //! 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/to_cmp.rs b/src/assertions/general/assertions/to_cmp.rs index 6bbbc1a..3792d8e 100644 --- a/src/assertions/general/assertions/to_cmp.rs +++ b/src/assertions/general/assertions/to_cmp.rs @@ -6,100 +6,6 @@ use crate::{ AssertionOutput, }; -/// 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] -pub fn to_be_less_than(boundary: Annotated) -> ToCmpAssertion { - ToCmpAssertion { - boundary, - ordering: Ordering::Less, - allow_eq: false, - cmp_message: "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] -pub fn to_be_less_than_or_equal_to(boundary: Annotated) -> ToCmpAssertion { - ToCmpAssertion { - boundary, - ordering: Ordering::Less, - allow_eq: true, - cmp_message: "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] -pub fn to_be_greater_than(boundary: Annotated) -> ToCmpAssertion { - ToCmpAssertion { - boundary, - ordering: Ordering::Greater, - allow_eq: false, - cmp_message: "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] -pub fn to_be_greater_than_or_equal_to(boundary: Annotated) -> ToCmpAssertion { - ToCmpAssertion { - boundary, - ordering: Ordering::Greater, - allow_eq: true, - cmp_message: "greater than or equal to", - } -} - /// A general-purpose assertion for comparing the ordering between two values. #[derive(Clone, Debug)] pub struct ToCmpAssertion { @@ -109,6 +15,23 @@ pub struct ToCmpAssertion { 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, @@ -118,10 +41,14 @@ where fn execute(self, mut cx: AssertionContext, subject: T) -> Self::Output { cx.annotate("boundary", &self.boundary); cx.annotate( - "ordering", - format_args!("{:?}", subject.partial_cmp(self.boundary.inner())), + "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", + }, ); - cx.annotate("expected ordering", self.cmp_message); // Use a match here to call the specialized comparison functions in case // those functions were overridden for a type diff --git a/src/assertions/general/assertions/to_equal.rs b/src/assertions/general/assertions/to_equal.rs index 3651e6d..10c158e 100644 --- a/src/assertions/general/assertions/to_equal.rs +++ b/src/assertions/general/assertions/to_equal.rs @@ -4,30 +4,19 @@ use crate::{ AssertionOutput, }; -/// 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] -pub fn to_equal(expected: Annotated) -> ToEqualAssertion { - ToEqualAssertion { expected } -} - -/// Assertion for [`to_equal`]. +/// 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, diff --git a/src/assertions/general/assertions/to_satisfy.rs b/src/assertions/general/assertions/to_satisfy.rs index 6120d2f..46eadee 100644 --- a/src/assertions/general/assertions/to_satisfy.rs +++ b/src/assertions/general/assertions/to_satisfy.rs @@ -4,45 +4,19 @@ use crate::{ AssertionOutput, }; -/// 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] -pub fn to_satisfy(predicate: Annotated) -> ToSatisfyAssertion { - ToSatisfyAssertion { predicate } -} - -/// Assertion for [`to_satisfy()`]. +/// 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, diff --git a/src/assertions/general/assertions/to_satisfy_with.rs b/src/assertions/general/assertions/to_satisfy_with.rs index faa8b42..4972305 100644 --- a/src/assertions/general/assertions/to_satisfy_with.rs +++ b/src/assertions/general/assertions/to_satisfy_with.rs @@ -4,52 +4,20 @@ use crate::{ AssertionOutput, }; -/// Asserts that the subject matches all of the given predicates. This "forks" -/// the assertion, allowing an intermediate value to have several different -/// assertions applied to it. -/// -/// ``` -/// # 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 with nested async assertions. -// TODO: make an async version -#[inline] -#[must_use] -pub fn to_satisfy_with(predicate: Annotated) -> ToSatisfyMergeAssertion { - ToSatisfyMergeAssertion { predicate } -} - -/// Assertion for [`to_satisfy_with()`]. +/// Asserts that the subject satisfies a series of assertions. #[derive(Clone, Debug)] -pub struct ToSatisfyMergeAssertion { +pub struct ToSatisfyWithAssertion { predicate: Annotated, } -impl Assertion for ToSatisfyMergeAssertion +impl ToSatisfyWithAssertion { + #[inline] + pub(crate) fn new(predicate: Annotated) -> Self { + Self { predicate } + } +} + +impl Assertion for ToSatisfyWithAssertion where F: FnOnce(T) -> Result<(), AssertionError>, { 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/annotate.rs b/src/assertions/general/modifiers/annotate.rs index 1c4436c..a126dfc 100644 --- a/src/assertions/general/modifiers/annotate.rs +++ b/src/assertions/general/modifiers/annotate.rs @@ -2,18 +2,17 @@ use std::fmt::Debug; use crate::{ assertions::{ - key, Assertion, AssertionContext, AssertionContextBuilder, AssertionModifier, SubjectKey, + Assertion, AssertionBuilder, AssertionContext, AssertionContextBuilder, AssertionModifier, }, metadata::{Annotated, AnnotatedKind}, }; #[doc(hidden)] pub fn __annotate( - prev: M, - _: SubjectKey, + builder: AssertionBuilder, annotate: fn(T) -> Annotated, -) -> (AnnotateModifier, SubjectKey) { - (AnnotateModifier { prev, annotate }, key()) +) -> AssertionBuilder> { + AssertionBuilder::modify(builder, |prev| AnnotateModifier { prev, annotate }) } /// Annotates and records input values, and updates the [`AssertionContext`] diff --git a/src/assertions/general/modifiers/map.rs b/src/assertions/general/modifiers/map.rs index 79c58a3..06ee81c 100644 --- a/src/assertions/general/modifiers/map.rs +++ b/src/assertions/general/modifiers/map.rs @@ -1,81 +1,22 @@ use crate::{ - assertions::{ - key, Assertion, AssertionContext, AssertionContextBuilder, AssertionModifier, SubjectKey, - }, + assertions::{Assertion, AssertionContext, AssertionContextBuilder, AssertionModifier}, metadata::Annotated, }; -/// 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! -#[inline] -pub fn map( - prev: M, - _: SubjectKey, - f: Annotated, -) -> (MapModifier, SubjectKey) -where - F: FnOnce(T) -> U, -{ - (MapModifier { prev, map: f }, key()) -} - -/// Modifier for [`map`]. +/// 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>, @@ -94,7 +35,7 @@ where } } -/// Assertion for [`map`]. +/// Maps the subject to a new value and executes an inner assertion on it. #[derive(Clone, Debug)] pub struct MapAssertion { next: A, diff --git a/src/assertions/general/modifiers/not.rs b/src/assertions/general/modifiers/not.rs index 3171563..45e9ec8 100644 --- a/src/assertions/general/modifiers/not.rs +++ b/src/assertions/general/modifiers/not.rs @@ -1,35 +1,21 @@ use crate::assertions::{ - general::InvertibleOutput, key, Assertion, AssertionContext, AssertionContextBuilder, - AssertionModifier, SubjectKey, + general::InvertibleOutput, Assertion, AssertionContext, AssertionContextBuilder, + AssertionModifier, }; -/// 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)); -/// ``` -#[inline] -pub fn not(prev: M, _: SubjectKey) -> (NotModifier, SubjectKey) { - (NotModifier { prev }, key()) -} - -/// Modifier for [`not()`]. +/// 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>, @@ -42,7 +28,7 @@ where } } -/// Assertion for [`not()`]. +/// Inverts an inner assertion. #[derive(Clone, Debug)] pub struct NotAssertion { next: A, @@ -67,11 +53,6 @@ mod tests { #[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(r#""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 index 524e36f..ef0832c 100644 --- a/src/assertions/general/modifiers/root.rs +++ b/src/assertions/general/modifiers/root.rs @@ -1,20 +1,21 @@ use crate::{ - assertions::{key, Assertion, AssertionContextBuilder, AssertionModifier, SubjectKey}, + assertions::{Assertion, AssertionContextBuilder, AssertionModifier}, metadata::Annotated, }; -#[doc(hidden)] -#[inline] -pub fn __root(subject: Annotated) -> (Root, SubjectKey) { - (Root { subject }, key()) -} - /// 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, diff --git a/src/assertions/iterators.rs b/src/assertions/iterators.rs index cf186ff..aa04def 100644 --- a/src/assertions/iterators.rs +++ b/src/assertions/iterators.rs @@ -1,7 +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/count.rs b/src/assertions/iterators/modifiers/count.rs index 4ebd58a..9d48e74 100644 --- a/src/assertions/iterators/modifiers/count.rs +++ b/src/assertions/iterators/modifiers/count.rs @@ -1,29 +1,18 @@ -use crate::assertions::{ - key, Assertion, AssertionContext, AssertionContextBuilder, AssertionModifier, SubjectKey, -}; +use crate::assertions::{Assertion, AssertionContext, AssertionContextBuilder, AssertionModifier}; -/// 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. -#[inline] -pub fn count(prev: M, _: SubjectKey) -> (CountModifier, SubjectKey) { - (CountModifier { prev }, key()) -} - -/// Modifier for [`count()`]. +/// 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>, @@ -36,7 +25,7 @@ where } } -/// Assertion for [`count()`]. +/// Executes the inner assertion on the number of items in the subject. #[derive(Clone, Debug)] pub struct CountAssertion { next: A, diff --git a/src/assertions/iterators/modifiers/merge.rs b/src/assertions/iterators/modifiers/merge.rs index 24d2886..2d3f739 100644 --- a/src/assertions/iterators/modifiers/merge.rs +++ b/src/assertions/iterators/modifiers/merge.rs @@ -1,115 +1,22 @@ use crate::assertions::{ iterators::{MergeStrategy, MergeableOutput}, - key, Assertion, AssertionContext, AssertionContextBuilder, AssertionModifier, SubjectKey, + Assertion, AssertionContext, AssertionContextBuilder, AssertionModifier, }; -/// 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))); -/// ``` -#[inline] -pub fn all(prev: M, _: SubjectKey) -> (MergeModifier, SubjectKey) -where - T: IntoIterator, -{ - ( - MergeModifier { - prev, - strategy: MergeStrategy::All, - }, - key(), - ) -} - -/// 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))); -/// ``` -#[inline] -pub fn any(prev: M, _: SubjectKey) -> (MergeModifier, SubjectKey) -where - T: IntoIterator, -{ - ( - MergeModifier { - prev, - strategy: MergeStrategy::Any, - }, - key(), - ) -} - -/// Modifier for [`all()`] and [`any()`]. +/// 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>, @@ -128,7 +35,7 @@ where } } -/// Assertion for [`all()`] and [`any()`]. +/// Forks the inner assertion, executing it for each element of the subject. #[derive(Clone, Debug)] pub struct MergeAssertion { next: A, diff --git a/src/assertions/iterators/modifiers/nth.rs b/src/assertions/iterators/modifiers/nth.rs index 748a1a5..e1fc3ff 100644 --- a/src/assertions/iterators/modifiers/nth.rs +++ b/src/assertions/iterators/modifiers/nth.rs @@ -1,52 +1,25 @@ use crate::{ assertions::{ - general::IntoInitializableOutput, key, Assertion, AssertionContext, - AssertionContextBuilder, AssertionModifier, SubjectKey, + general::IntoInitializableOutput, Assertion, AssertionContext, AssertionContextBuilder, + AssertionModifier, }, metadata::Annotated, }; -/// 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)); -/// ``` -#[inline] -pub fn nth( - prev: M, - _: SubjectKey, - index: Annotated, -) -> (NthModifier, SubjectKey) -where - T: IntoIterator, -{ - (NthModifier { prev, index }, key()) -} - -/// Modifier for [`nth()`]. +/// 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>, @@ -65,7 +38,8 @@ where } } -/// Assertion for [`nth()`]. +/// Selects an element out of the subject and executes the inner assertion on +/// it. #[derive(Clone, Debug)] pub struct NthAssertion { next: A, diff --git a/src/assertions/options.rs b/src/assertions/options.rs index c258c40..b2c6061 100644 --- a/src/assertions/options.rs +++ b/src/assertions/options.rs @@ -1,9 +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/to_be_variant.rs b/src/assertions/options/assertions/to_be_variant.rs index 3e91b1c..0e316b7 100644 --- a/src/assertions/options/assertions/to_be_variant.rs +++ b/src/assertions/options/assertions/to_be_variant.rs @@ -3,72 +3,36 @@ use crate::{ AssertionOutput, }; -/// 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] -pub fn to_be_some() -> ToBeOptionVariantAssertion { - ToBeOptionVariantAssertion { - expected: Variant::Some, - } +/// Asserts that the subject is a specific [`Option`] variant. +#[derive(Clone, Debug)] +pub struct ToBeOptionVariantAssertion { + expected: OptionVariant, } -/// 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] -pub fn to_be_none() -> ToBeOptionVariantAssertion { - ToBeOptionVariantAssertion { - expected: Variant::None, +impl ToBeOptionVariantAssertion { + #[inline] + pub(crate) fn new(expected: OptionVariant) -> Self { + Self { expected } } } -/// Assertion for [`to_be_some()`] and [`to_be_none()`]. -#[derive(Clone, Debug)] -pub struct ToBeOptionVariantAssertion { - expected: Variant, -} - impl Assertion for ToBeOptionVariantAssertion where O: Optionish, { type Output = AssertionOutput; - fn execute(self, mut cx: AssertionContext, subject: O) -> Self::Output { - cx.annotate("expected", format_args!("{:?}", self.expected)); - + #[inline] + fn execute(self, cx: AssertionContext, subject: O) -> Self::Output { match self.expected { - Variant::Some => cx.pass_if(subject.some().is_some(), "received None"), - Variant::None => cx.pass_if(subject.some().is_none(), "received Some"), + 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)] -enum Variant { +pub(crate) enum OptionVariant { Some, 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/some_and.rs b/src/assertions/options/modifiers/some_and.rs index 5f95df5..5599fe5 100644 --- a/src/assertions/options/modifiers/some_and.rs +++ b/src/assertions/options/modifiers/some_and.rs @@ -1,36 +1,21 @@ use crate::assertions::{ - general::IntoInitializableOutput, key, options::Optionish, Assertion, AssertionContext, - AssertionContextBuilder, AssertionModifier, SubjectKey, + general::IntoInitializableOutput, options::Optionish, Assertion, AssertionContext, + AssertionContextBuilder, AssertionModifier, }; -/// 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)); -/// ``` -#[inline] -pub fn to_be_some_and(prev: M, _: SubjectKey) -> (SomeAndModifier, SubjectKey) -where - O: Optionish, -{ - (SomeAndModifier { prev }, key()) -} - -/// Modifier for [`to_be_some_and()`]. +/// 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>, @@ -43,7 +28,7 @@ where } } -/// Assertion for [`to_be_some_and()`]. +/// Executes the inner assertion on the subject's inner value. #[derive(Clone, Debug)] pub struct SomeAndAssertion { next: A, diff --git a/src/assertions/results.rs b/src/assertions/results.rs index d30e491..6529ed8 100644 --- a/src/assertions/results.rs +++ b/src/assertions/results.rs @@ -1,9 +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/to_be_variant.rs b/src/assertions/results/assertions/to_be_variant.rs index 81529fe..42ecc54 100644 --- a/src/assertions/results/assertions/to_be_variant.rs +++ b/src/assertions/results/assertions/to_be_variant.rs @@ -3,76 +3,36 @@ use crate::{ AssertionOutput, }; -/// 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] -pub fn to_be_ok() -> ToBeResultVariantAssertion { - ToBeResultVariantAssertion { - expected: Variant::Ok, - } +/// Asserts that the subject is a specific [`Result`] variant. +#[derive(Clone, Debug)] +pub struct ToBeResultVariantAssertion { + expected: ResultVariant, } -/// 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] -pub fn to_be_err() -> ToBeResultVariantAssertion { - ToBeResultVariantAssertion { - expected: Variant::Err, +impl ToBeResultVariantAssertion { + #[inline] + pub(crate) fn new(expected: ResultVariant) -> Self { + Self { expected } } } -/// Assertion for [`to_be_ok()`] and [`to_be_err()`]. -#[derive(Clone, Debug)] -pub struct ToBeResultVariantAssertion { - expected: Variant, -} - impl Assertion for ToBeResultVariantAssertion where R: Resultish, { type Output = AssertionOutput; - fn execute(self, mut cx: AssertionContext, subject: R) -> Self::Output { - cx.annotate("expected", format_args!("{:?}", self.expected)); - + #[inline] + fn execute(self, cx: AssertionContext, subject: R) -> Self::Output { match self.expected { - Variant::Ok => cx.pass_if(subject.ok().is_some(), "received Err"), - Variant::Err => cx.pass_if(subject.err().is_some(), "received Ok"), + 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)] -enum Variant { +pub(crate) enum ResultVariant { Ok, 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/err_and.rs b/src/assertions/results/modifiers/err_and.rs index 15cb56c..655b79d 100644 --- a/src/assertions/results/modifiers/err_and.rs +++ b/src/assertions/results/modifiers/err_and.rs @@ -1,38 +1,21 @@ use crate::assertions::{ - general::IntoInitializableOutput, key, results::Resultish, Assertion, AssertionContext, - AssertionContextBuilder, AssertionModifier, SubjectKey, + general::IntoInitializableOutput, results::Resultish, Assertion, AssertionContext, + AssertionContextBuilder, AssertionModifier, }; -/// 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")); -/// ``` -#[inline] -pub fn to_be_err_and(prev: M, _: SubjectKey) -> (ErrAndModifier, SubjectKey) -where - R: Resultish, -{ - (ErrAndModifier { prev }, key()) -} - -/// Modifier for [`to_be_err_and()`]. +/// 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>, @@ -45,7 +28,7 @@ where } } -/// Assertion for [`to_be_err_and()`]. +/// Executes the inner assertion on the subject's [`Err`] value. #[derive(Clone, Debug)] pub struct ErrAndAssertion { next: A, diff --git a/src/assertions/results/modifiers/ok_and.rs b/src/assertions/results/modifiers/ok_and.rs index 296300e..b6255f5 100644 --- a/src/assertions/results/modifiers/ok_and.rs +++ b/src/assertions/results/modifiers/ok_and.rs @@ -1,38 +1,21 @@ use crate::assertions::{ - general::IntoInitializableOutput, key, results::Resultish, Assertion, AssertionContext, - AssertionContextBuilder, AssertionModifier, SubjectKey, + general::IntoInitializableOutput, results::Resultish, Assertion, AssertionContext, + AssertionContextBuilder, AssertionModifier, }; -/// 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)); -/// ``` -#[inline] -pub fn to_be_ok_and(prev: M, _: SubjectKey) -> (OkAndModifier, SubjectKey) -where - R: Resultish, -{ - (OkAndModifier { prev }, key()) -} - -/// Modifier for [`to_be_ok_and()`]. +/// 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>, @@ -45,7 +28,7 @@ where } } -/// Assertion for [`to_be_ok_and()`]. +/// Executes the inner assertion on the subject's [`Ok`] value. #[derive(Clone, Debug)] pub struct OkAndAssertion { next: A, diff --git a/src/assertions/strings.rs b/src/assertions/strings.rs index f76b7ab..ddb4bc5 100644 --- a/src/assertions/strings.rs +++ b/src/assertions/strings.rs @@ -1,7 +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/to_contain_substr.rs b/src/assertions/strings/assertions/to_contain_substr.rs index 92ff7a9..b1cd21e 100644 --- a/src/assertions/strings/assertions/to_contain_substr.rs +++ b/src/assertions/strings/assertions/to_contain_substr.rs @@ -5,33 +5,18 @@ use crate::{ }; /// 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] -pub fn to_contain_substr

(pattern: Annotated

) -> ToContainSubstr

-where - P: AsRef, -{ - ToContainSubstr { pattern } -} - -/// Assertion for [`to_contain_substr()`]. #[derive(Clone, Debug)] pub struct ToContainSubstr

{ pattern: Annotated

, } +impl

ToContainSubstr

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

) -> Self { + Self { pattern } + } +} + impl Assertion for ToContainSubstr

where P: AsRef, @@ -42,7 +27,6 @@ where fn execute(self, mut cx: AssertionContext, subject: T) -> Self::Output { let pattern = self.pattern.inner().as_ref(); cx.annotate("expected", format_args!("{pattern:?}")); - cx.pass_if(subject.as_ref().contains(pattern), "substring not found") } } diff --git a/src/assertions/strings/assertions/to_match_regex.rs b/src/assertions/strings/assertions/to_match_regex.rs index eb8f660..2e04b4a 100644 --- a/src/assertions/strings/assertions/to_match_regex.rs +++ b/src/assertions/strings/assertions/to_match_regex.rs @@ -4,46 +4,25 @@ use regex::Regex; use crate::{ assertions::{Assertion, AssertionContext}, - metadata::Annotated, AssertionOutput, }; -/// 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. -#[allow(clippy::needless_pass_by_value)] -pub fn to_match_regex

(pattern: Annotated

) -> ToMatchRegexAssertion -where - P: AsRef, -{ - let pattern = pattern.inner().as_ref(); - let regex = Regex::new(pattern).expect("invalid regex"); - ToMatchRegexAssertion { - regex: Arc::new(regex), - } -} - -/// Assertion for [`to_match_regex()`]. +/// 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, @@ -52,7 +31,6 @@ where 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..d8d9d6a --- /dev/null +++ b/src/assertions/strings/extensions.rs @@ -0,0 +1,118 @@ +use std::fmt::{Debug, Display}; + +use crate::{assertions::AssertionBuilder, metadata::Annotated}; + +use super::{AsDebugModifier, AsDisplayModifier, ToContainSubstr}; + +/// Assertions and modifiers for [`String`]s. +pub trait StringAssertions +where + T: AsRef, +{ + /// 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) + } + + /// 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 {} + +/// 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/debug.rs b/src/assertions/strings/modifiers/debug.rs index 91b55be..6cfacb1 100644 --- a/src/assertions/strings/modifiers/debug.rs +++ b/src/assertions/strings/modifiers/debug.rs @@ -1,29 +1,20 @@ use std::fmt::Debug; -use crate::assertions::{ - key, Assertion, AssertionContext, AssertionContextBuilder, AssertionModifier, SubjectKey, -}; - -/// Converts a value to its [`Debug`] representation. -/// -/// ``` -/// # use expecters::prelude::*; -/// expect!("hello", as_debug, to_equal(r#""hello""#)); -/// ``` -#[inline] -pub fn as_debug(prev: M, _: SubjectKey) -> (AsDebugModifier, SubjectKey) -where - T: Debug, -{ - (AsDebugModifier { prev }, key()) -} +use crate::assertions::{Assertion, AssertionContext, AssertionContextBuilder, AssertionModifier}; -/// Modifier for [`as_debug()`]. +/// 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>, @@ -36,7 +27,8 @@ where } } -/// Assertion for [`as_debug()`]. +/// Executes the inner assertion with the [`Debug`] representation of the +/// subject. #[derive(Clone, Debug)] pub struct AsDebugAssertion { next: A, diff --git a/src/assertions/strings/modifiers/display.rs b/src/assertions/strings/modifiers/display.rs index ec9086b..67d6b97 100644 --- a/src/assertions/strings/modifiers/display.rs +++ b/src/assertions/strings/modifiers/display.rs @@ -1,29 +1,20 @@ use std::fmt::Display; -use crate::assertions::{ - key, Assertion, AssertionContext, AssertionContextBuilder, AssertionModifier, SubjectKey, -}; - -/// Converts a value to its [`Display`] representation. -/// -/// ``` -/// # use expecters::prelude::*; -/// expect!(1, as_display, to_equal("1")); -/// ``` -#[inline] -pub fn as_display(prev: M, _: SubjectKey) -> (AsDisplayModifier, SubjectKey) -where - T: Display, -{ - (AsDisplayModifier { prev }, key()) -} +use crate::assertions::{Assertion, AssertionContext, AssertionContextBuilder, AssertionModifier}; -/// Modifier for [`as_display()`]. +/// 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>, @@ -36,7 +27,8 @@ where } } -/// Assertion for [`as_display()`]. +/// Executes the inner assertion with the [`Display`] representation of the +/// subject. #[derive(Clone, Debug)] pub struct AsDisplayAssertion { next: A, diff --git a/src/macros.rs b/src/macros.rs index 67e8414..117ebbc 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -40,20 +40,13 @@ /// expect!(1, not, expecters::prelude::to_equal(0)); /// ``` /// -/// To fix this, either import [`to_equal`] directly, or alias it: +/// To fix this, remove the path: /// /// ``` -/// # #[allow(unused_import)] /// # use expecters::prelude::*; -/// use expecters::prelude::to_equal as expect_to_equal; -/// expect!(1, not, expect_to_equal(0)); +/// expect!(1, not, to_equal(0)); /// ``` /// -/// Note that aliasing a modifier or assertion will change its name in the error -/// message it generates as well. Error messages produced by the above assertion -/// will refer to the final parameter as `expect_to_equal` instead of `to_equal` -/// because of the alias. -/// /// 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 @@ -205,13 +198,12 @@ macro_rules! __expect_inner { ) => {{ let subject = $crate::annotated!($subject); let subject_repr = ::std::string::ToString::to_string(&subject); - let (root, _key) = $crate::assertions::general::__root(subject); + let builder = $crate::assertions::AssertionBuilder::__new(subject); $crate::__expect_inner!( @build_assertion, [], subject_repr, - root, - _key, + builder, $($assertions)* ) }}; @@ -222,13 +214,12 @@ macro_rules! __expect_inner { @build_assertion, [$($frame_name:expr,)*], $subject:expr, - $chain:expr, - $key:expr, + $builder:expr, $assertion:ident($($param:expr),* $(,)?) $(,)? ) => {{ - let (chain, _key) = $crate::__expect_inner!(@annotate, $chain, $key); - let assertion = $assertion($($crate::annotated!($param),)*); + let builder = $crate::__expect_inner!(@annotate, $builder); + let assertion = builder.$assertion($($crate::annotated!($param),)*); let cx = $crate::assertions::AssertionContext::__new( $subject, $crate::source_loc!(), @@ -240,8 +231,8 @@ macro_rules! __expect_inner { FRAMES }, ); - $crate::assertions::AssertionModifier::apply( - chain, + $crate::assertions::AssertionBuilder::__apply( + builder, cx, assertion, ) @@ -251,8 +242,7 @@ macro_rules! __expect_inner { @build_assertion, [$($frame_name:expr,)*], $subject:expr, - $chain:expr, - $key:expr, + $builder:expr, $assertion:ident $(,)? ) => { @@ -260,8 +250,7 @@ macro_rules! __expect_inner { @build_assertion, [$($frame_name,)*], $subject, - $chain, - $key, + $builder, $assertion() ) }; @@ -270,15 +259,12 @@ macro_rules! __expect_inner { @build_assertion, [$($frame_name:expr,)*], $subject:expr, - $chain:expr, - $key:expr, + $builder:expr, $modifier:ident($($param:expr),* $(,)?), $($rest:tt)* ) => {{ - let (chain, key) = $crate::__expect_inner!(@annotate, $chain, $key); - let (chain, _key) = $modifier( - chain, - key, + let builder = $crate::__expect_inner!(@annotate, $builder); + let builder = builder.$modifier( $($crate::annotated!($param),)* ); $crate::__expect_inner!( @@ -288,8 +274,7 @@ macro_rules! __expect_inner { ::std::stringify!($modifier), ], $subject, - chain, - _key, + builder, $($rest)* ) }}; @@ -298,8 +283,7 @@ macro_rules! __expect_inner { @build_assertion, [$($frame_name:expr,)*], $subject:expr, - $chain:expr, - $key:expr, + $builder:expr, $modifier:ident, $($rest:tt)* ) => { @@ -307,18 +291,16 @@ macro_rules! __expect_inner { @build_assertion, [$($frame_name,)*], $subject, - $chain, - $key, + $builder, $modifier(), $($rest)* ) }; // Annotate the value being passed down the chain - (@annotate, $chain:expr, $key:expr) => { + (@annotate, $builder:expr) => { $crate::assertions::general::__annotate( - $chain, - $key, + $builder, |not_debug| $crate::annotated!(not_debug), ) }; diff --git a/src/prelude.rs b/src/prelude.rs index 817b23e..93f2754 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -13,20 +13,14 @@ pub use crate::{ assertions::{ - general::{ - map, not, to_be_greater_than, to_be_greater_than_or_equal_to, to_be_less_than, - to_be_less_than_or_equal_to, to_equal, to_satisfy, to_satisfy_with, - }, - iterators::{all, any, count, nth}, - options::{to_be_none, to_be_some, to_be_some_and}, - results::{to_be_err, to_be_err_and, to_be_ok, to_be_ok_and}, - strings::{as_debug, as_display, to_contain_substr}, + general::GeneralAssertions, + iterators::IteratorAssertions, + options::OptionAssertions, + results::ResultAssertions, + strings::{DebugAssertions, DisplayAssertions, StringAssertions}, }, expect, try_expect, }; #[cfg(feature = "futures")] -pub use crate::assertions::futures::{when_ready, when_ready_after, when_ready_before}; - -#[cfg(feature = "regex")] -pub use crate::assertions::strings::to_match_regex; +pub use crate::assertions::futures::FutureAssertions; From 608f198c39966ecd73ec49e168a3f3d937c6c48b Mon Sep 17 00:00:00 2001 From: TehPers Date: Mon, 12 Aug 2024 10:24:24 -0700 Subject: [PATCH 32/37] Add some attributes and box a value for clippy --- src/assertions/error.rs | 13 +++++++++++-- src/assertions/futures/outputs/completion_order.rs | 1 + src/assertions/futures/outputs/initialized.rs | 1 + src/assertions/futures/outputs/inverted.rs | 1 + src/assertions/futures/outputs/merged.rs | 1 + src/assertions/futures/outputs/unwrapped.rs | 2 ++ src/assertions/futures/outputs/when_ready.rs | 1 + 7 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/assertions/error.rs b/src/assertions/error.rs index 16bc99a..ebec4a0 100644 --- a/src/assertions/error.rs +++ b/src/assertions/error.rs @@ -73,14 +73,19 @@ impl AssertionOutput { } /// An error that can occur during an assertion. +#[must_use] pub struct AssertionError { - cx: AssertionContext, + cx: Box, message: String, } impl AssertionError { + #[inline] pub(crate) fn new(cx: AssertionContext, message: String) -> Self { - Self { cx, message } + Self { + cx: Box::new(cx), + message, + } } } @@ -90,10 +95,12 @@ mod styles { 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()) } @@ -101,10 +108,12 @@ mod styles { #[cfg(not(feature = "colors"))] mod styles { + #[inline] pub fn dimmed(s: &T) -> &T { s } + #[inline] pub fn bright_red(s: &T) -> &T { s } diff --git a/src/assertions/futures/outputs/completion_order.rs b/src/assertions/futures/outputs/completion_order.rs index 13bf219..3f4731e 100644 --- a/src/assertions/futures/outputs/completion_order.rs +++ b/src/assertions/futures/outputs/completion_order.rs @@ -18,6 +18,7 @@ pin_project! { /// 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, diff --git a/src/assertions/futures/outputs/initialized.rs b/src/assertions/futures/outputs/initialized.rs index a599025..4d220fd 100644 --- a/src/assertions/futures/outputs/initialized.rs +++ b/src/assertions/futures/outputs/initialized.rs @@ -14,6 +14,7 @@ use crate::assertions::{ pin_project! { /// An asynchronous output that can be initialized on-demand. + #[must_use] pub struct InitializedOutputFuture where F: Future, diff --git a/src/assertions/futures/outputs/inverted.rs b/src/assertions/futures/outputs/inverted.rs index 1c6eb70..a1a3b0a 100644 --- a/src/assertions/futures/outputs/inverted.rs +++ b/src/assertions/futures/outputs/inverted.rs @@ -11,6 +11,7 @@ use crate::assertions::{general::InvertibleOutput, AssertionContext}; pin_project! { /// Inverts an asynchronous output. #[derive(Clone, Debug)] + #[must_use] pub struct InvertedOutputFuture { #[pin] inner: F, diff --git a/src/assertions/futures/outputs/merged.rs b/src/assertions/futures/outputs/merged.rs index d67c3f6..7aca88d 100644 --- a/src/assertions/futures/outputs/merged.rs +++ b/src/assertions/futures/outputs/merged.rs @@ -18,6 +18,7 @@ use crate::assertions::{ pin_project! { /// Merges many asynchronous outputs. #[derive(Debug)] + #[must_use] pub struct MergedOutputsFuture where F: Future, diff --git a/src/assertions/futures/outputs/unwrapped.rs b/src/assertions/futures/outputs/unwrapped.rs index ee47d27..7c0e439 100644 --- a/src/assertions/futures/outputs/unwrapped.rs +++ b/src/assertions/futures/outputs/unwrapped.rs @@ -11,6 +11,7 @@ use crate::assertions::general::UnwrappableOutput; pin_project! { /// Unwraps an asynchronous output. #[derive(Clone, Debug)] + #[must_use] pub struct UnwrappedOutputFuture { #[pin] inner: F, @@ -46,6 +47,7 @@ where pin_project! { /// Tries to unwrap an asynchronous output. #[derive(Clone, Debug)] + #[must_use] pub struct TryUnwrappedOutputFuture { #[pin] inner: F, diff --git a/src/assertions/futures/outputs/when_ready.rs b/src/assertions/futures/outputs/when_ready.rs index 7fdb272..0168439 100644 --- a/src/assertions/futures/outputs/when_ready.rs +++ b/src/assertions/futures/outputs/when_ready.rs @@ -13,6 +13,7 @@ pin_project! { /// /// Created by [`when_ready`](crate::prelude::when_ready). #[derive(Clone, Debug)] + #[must_use] pub struct WhenReadyFuture where T: Future, From d349b97fc9ef879e5e32588f64f9244786cfd7d4 Mon Sep 17 00:00:00 2001 From: TehPers Date: Mon, 12 Aug 2024 22:08:05 -0700 Subject: [PATCH 33/37] Fix and improve doc comments --- src/assertions.rs | 118 +++++++++++++----- src/assertions/context.rs | 8 +- src/assertions/futures/extensions.rs | 21 ++-- .../general/outputs/initializable.rs | 20 +-- src/assertions/general/outputs/invert.rs | 4 +- src/assertions/iterators/outputs/merge.rs | 4 +- src/assertions/options/optionish.rs | 5 + src/assertions/results/resultish.rs | 5 + src/macros.rs | 8 +- 9 files changed, 125 insertions(+), 68 deletions(-) diff --git a/src/assertions.rs b/src/assertions.rs index 1e14a9b..1e010bf 100644 --- a/src/assertions.rs +++ b/src/assertions.rs @@ -10,14 +10,27 @@ //! 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`](crate::prelude::to_be_some), is a regular function that -//! returns a value implementing the [`Assertion`] trait. It acts as a -//! constructor for that type. For example, calling `to_be_some()` returns an -//! instance of the [`ToBeOptionVariantAssertion`] type configured to check if -//! the input it receives is of the [`Some`] variant. +//! [`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, @@ -25,18 +38,13 @@ //! //! ``` //! use expecters::{ -//! assertions::{Assertion, AssertionContext}, +//! assertions::{Assertion, AssertionBuilder, AssertionContext}, //! metadata::Annotated, //! prelude::*, //! AssertionOutput, //! }; //! -//! // Input parameters are automatically annotated, so we need to wrap them -//! // with `Annotated` -//! pub fn to_be_zero(annotation: Annotated) -> ToBeZeroAssertion { -//! ToBeZeroAssertion(annotation) -//! } -//! +//! // We need to create a struct for our assertion and define its behavior //! #[derive(Clone, Debug)] //! pub struct ToBeZeroAssertion(Annotated); //! @@ -47,12 +55,31 @@ //! type Output = AssertionOutput; //! //! fn execute(self, mut cx: AssertionContext, value: i32) -> Self::Output { -//! cx.annotate("my annotation", "this appears in failure messages"); +//! // 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())); @@ -100,37 +127,24 @@ //! 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. The function should take at minimum two required -//! inputs (but it may specify additional inputs), and should return a pair -//! containing the constructed modifier and a subject key. +//! 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, -//! SubjectKey, -//! key, //! }, //! metadata::Annotated, //! prelude::*, //! }; //! -//! // The first two parameters are required, but you may specify any number of -//! // additional parameters to your modifier: -//! pub fn divided_by( -//! prev: M, // the modifier we're wrapping -//! _: SubjectKey, // the type we're expecting to receive in this step -//! divisor: Annotated, // our own custom parameter -//! ) -> ( -//! DividedByModifier, // our constructed modifier type, wrapping M -//! SubjectKey, // the type we're passing to the next step -//! ) { -//! (DividedByModifier(prev, divisor), key()) -//! } -//! //! // This wraps the modifier chain (first pass, going from root -> assertion) //! #[derive(Clone, Debug)] //! pub struct DividedByModifier(M, Annotated); @@ -162,6 +176,40 @@ //! } //! } //! +//! // 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)); //! ``` //! @@ -170,10 +218,14 @@ //! 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::not -//! [`to_equal`]: crate::prelude::to_equal +//! [`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")] diff --git a/src/assertions/context.rs b/src/assertions/context.rs index 45ade5a..fc56184 100644 --- a/src/assertions/context.rs +++ b/src/assertions/context.rs @@ -12,11 +12,15 @@ use super::general::InitializableOutput; /// /// 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`](crate::prelude::all) or -/// [`any`](crate::prelude::any) to execute an assertion on several values. +/// 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, diff --git a/src/assertions/futures/extensions.rs b/src/assertions/futures/extensions.rs index c14f48e..46a8780 100644 --- a/src/assertions/futures/extensions.rs +++ b/src/assertions/futures/extensions.rs @@ -53,15 +53,19 @@ where /// /// ``` /// # use expecters::prelude::*; - /// use std::future::{pending, ready}; + /// use std::{future::ready, time::Duration }; + /// use tokio::time::sleep; + /// /// # #[tokio::main(flavor = "current_thread")] /// # async fn main() { - /// expect!(ready(1), when_ready_before(pending::<()>()), to_equal(1)).await; + /// 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 provided future completes first: + /// The assertion fails if the subject completes last: /// /// ```should_panic /// # use expecters::prelude::*; @@ -89,18 +93,17 @@ where /// # use expecters::prelude::*; /// use std::{future::ready, time::Duration}; /// use tokio::time::sleep; + /// /// # #[tokio::main(flavor = "current_thread")] /// # async fn main() { - /// let fut = async { - /// sleep(Duration::from_secs(1)).await; - /// 1 - /// }; - /// expect!(fut, when_ready_after(ready(())), to_equal(1)).await; + /// 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 provided future completes first: + /// The assertion fails if the subject completes first: /// /// ```should_panic /// # use expecters::prelude::*; diff --git a/src/assertions/general/outputs/initializable.rs b/src/assertions/general/outputs/initializable.rs index 30d5997..4763ffb 100644 --- a/src/assertions/general/outputs/initializable.rs +++ b/src/assertions/general/outputs/initializable.rs @@ -4,29 +4,19 @@ use crate::{assertions::AssertionContext, AssertionOutput}; /// [`AssertionContext`]. /// /// Some modifiers need to directly initialize an instance of their output type. -/// For example, fallible modifiers like -/// [`to_be_some_and`](crate::prelude::to_be_some_and) can fail without +/// 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 { - // /// 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; - /// 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; - - // /// 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_initializable(self) -> Self::Initialized; } impl InitializableOutput for AssertionOutput { @@ -42,9 +32,7 @@ impl InitializableOutput for AssertionOutput { } /// An output type that can be converted into an -/// [initializable output type][initializable]. -/// -/// [initializable]: InitializableOutput +/// [initializable output type](InitializableOutput). pub trait IntoInitializableOutput { /// The initialized output type. /// diff --git a/src/assertions/general/outputs/invert.rs b/src/assertions/general/outputs/invert.rs index e02ff9c..1c860cd 100644 --- a/src/assertions/general/outputs/invert.rs +++ b/src/assertions/general/outputs/invert.rs @@ -23,8 +23,8 @@ pub trait InvertibleOutput { /// and reaching the [`to_equal`] assertion, but the inversion would occur /// at [`not`]. /// - /// [`not`]: crate::prelude::not - /// [`to_equal`]: crate::prelude::to_equal + /// [`not`]: crate::prelude::GeneralAssertions::not + /// [`to_equal`]: crate::prelude::GeneralAssertions::to_equal fn invert(self, cx: AssertionContext) -> Self::Inverted; } diff --git a/src/assertions/iterators/outputs/merge.rs b/src/assertions/iterators/outputs/merge.rs index 24f4bb5..f25d2d7 100644 --- a/src/assertions/iterators/outputs/merge.rs +++ b/src/assertions/iterators/outputs/merge.rs @@ -16,8 +16,8 @@ use crate::{assertions::AssertionContext, AssertionOutput}; /// iterators. An empty iterator represents either a success (for `All`) or a /// failure (for `Any`) depending on your merge strategy. /// -/// [`all`]: crate::prelude::all -/// [`any`]: crate::prelude::any +/// [`all`]: crate::prelude::IteratorAssertions::all +/// [`any`]: crate::prelude::IteratorAssertions::any pub trait MergeableOutput { /// The type of the merged output. type Merged; diff --git a/src/assertions/options/optionish.rs b/src/assertions/options/optionish.rs index 1cd49c7..f1d6e87 100644 --- a/src/assertions/options/optionish.rs +++ b/src/assertions/options/optionish.rs @@ -39,6 +39,11 @@ mod sealed { /// 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/resultish.rs b/src/assertions/results/resultish.rs index 19f8730..d87e0ac 100644 --- a/src/assertions/results/resultish.rs +++ b/src/assertions/results/resultish.rs @@ -67,6 +67,11 @@ mod sealed { /// 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/macros.rs b/src/macros.rs index 117ebbc..346dc4f 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -145,10 +145,10 @@ /// [`Annotated`]: crate::metadata::Annotated /// [`AnnotatedAssertion`]: crate::assertions::AnnotatedAssertion /// [`Debug`]: std::fmt::Debug -/// [`all`]: crate::prelude::all -/// [`map`]: crate::prelude::map -/// [`not`]: crate::prelude::not -/// [`to_equal`]: crate::prelude::to_equal +/// [`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 { From ced63aea30342e1f1cc8b0269979cacbfe8b6498 Mon Sep 17 00:00:00 2001 From: TehPers Date: Mon, 12 Aug 2024 22:08:27 -0700 Subject: [PATCH 34/37] Unify type param order for annotate types --- src/assertions/general/modifiers/annotate.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/assertions/general/modifiers/annotate.rs b/src/assertions/general/modifiers/annotate.rs index a126dfc..d1db79d 100644 --- a/src/assertions/general/modifiers/annotate.rs +++ b/src/assertions/general/modifiers/annotate.rs @@ -50,7 +50,7 @@ where impl AssertionModifier for AnnotateModifier where - M: AssertionModifier>, + M: AssertionModifier>, { type Output = M::Output; @@ -68,12 +68,12 @@ where /// Assertion for [`AnnotateModifier`]. See the docs for the modifier for more /// information. -pub struct AnnotateAssertion { +pub struct AnnotateAssertion { next: A, annotate: fn(T) -> Annotated, } -impl Clone for AnnotateAssertion +impl Clone for AnnotateAssertion where A: Clone, { @@ -85,7 +85,7 @@ where } } -impl Debug for AnnotateAssertion +impl Debug for AnnotateAssertion where A: Debug, { @@ -97,7 +97,7 @@ where } } -impl Assertion for AnnotateAssertion +impl Assertion for AnnotateAssertion where A: Assertion, { From 15968af08df083fd48abe40ed30bacaf4e173e4e Mon Sep 17 00:00:00 2001 From: TehPers Date: Mon, 12 Aug 2024 22:08:47 -0700 Subject: [PATCH 35/37] Add to_{start,end}_with, chars --- .../strings/assertions/to_contain_substr.rs | 21 +++++- src/assertions/strings/extensions.rs | 66 ++++++++++++++++++- src/assertions/strings/modifiers.rs | 2 + src/assertions/strings/modifiers/chars.rs | 45 +++++++++++++ 4 files changed, 128 insertions(+), 6 deletions(-) create mode 100644 src/assertions/strings/modifiers/chars.rs diff --git a/src/assertions/strings/assertions/to_contain_substr.rs b/src/assertions/strings/assertions/to_contain_substr.rs index b1cd21e..63f0e76 100644 --- a/src/assertions/strings/assertions/to_contain_substr.rs +++ b/src/assertions/strings/assertions/to_contain_substr.rs @@ -8,12 +8,13 @@ use crate::{ #[derive(Clone, Debug)] pub struct ToContainSubstr

{ pattern: Annotated

, + location: ContainsLocation, } impl

ToContainSubstr

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

) -> Self { - Self { pattern } + pub(crate) fn new(pattern: Annotated

, location: ContainsLocation) -> Self { + Self { pattern, location } } } @@ -27,6 +28,20 @@ where fn execute(self, mut cx: AssertionContext, subject: T) -> Self::Output { let pattern = self.pattern.inner().as_ref(); cx.annotate("expected", format_args!("{pattern:?}")); - cx.pass_if(subject.as_ref().contains(pattern), "substring not found") + + 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/extensions.rs b/src/assertions/strings/extensions.rs index d8d9d6a..eba64d3 100644 --- a/src/assertions/strings/extensions.rs +++ b/src/assertions/strings/extensions.rs @@ -2,13 +2,21 @@ use std::fmt::{Debug, Display}; use crate::{assertions::AssertionBuilder, metadata::Annotated}; -use super::{AsDebugModifier, AsDisplayModifier, ToContainSubstr}; +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. /// /// ``` @@ -29,7 +37,51 @@ where where P: AsRef, { - ToContainSubstr::new(pattern) + 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. @@ -61,7 +113,15 @@ where } } -impl StringAssertions for AssertionBuilder where T: AsRef {} +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 diff --git a/src/assertions/strings/modifiers.rs b/src/assertions/strings/modifiers.rs index c8ce9ef..1fb31f0 100644 --- a/src/assertions/strings/modifiers.rs +++ b/src/assertions/strings/modifiers.rs @@ -1,5 +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()) + } +} From 7d0fc4864589a750e57cb2653e8927b5f6b82438 Mon Sep 17 00:00:00 2001 From: TehPers Date: Mon, 12 Aug 2024 22:13:05 -0700 Subject: [PATCH 36/37] Remove outdated test --- tests/error_messages.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/tests/error_messages.rs b/tests/error_messages.rs index 079a9d0..3375971 100644 --- a/tests/error_messages.rs +++ b/tests/error_messages.rs @@ -16,18 +16,6 @@ fn simple() { ); } -#[test] -fn alias() { - use expecters::prelude::not as my_not; - - expect!( - try_expect!(1, my_not, to_equal(1)), - to_be_err_and, - as_display, - to_contain_substr("my_not"), - ); -} - #[test] fn non_debug() { #[derive(PartialEq)] From bb56b140787ed6c1a364fbee7413b651b85afecb Mon Sep 17 00:00:00 2001 From: TehPers Date: Mon, 12 Aug 2024 22:16:48 -0700 Subject: [PATCH 37/37] Add a super conservative msrv --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index ce1a1be..0201ad0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ categories = [ "development-tools::testing", ] keywords = ["assert", "assertions", "async", "matchers", "testing"] +rust-version = "1.80.1" publish = false # TODO: remove this when ready to publish [features]