diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 239dd1c..1089d59 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -141,7 +141,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: test - args: --no-default-features --features "colored, float-cmp, num-bigint, rust-decimal, bigdecimal" + args: --no-default-features --features "colored, float-cmp, num-bigint, recursive, regex, rust-decimal, bigdecimal" - uses: Swatinem/rust-cache@v2 msrv: diff --git a/Cargo.toml b/Cargo.toml index ea577b6..030be14 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.13.1" authors = ["haraldmaida"] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.78.0" +rust-version = "1.82.0" repository = "https://github.com/innoave/asserting" readme = "README.md" @@ -19,15 +19,27 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [features] -default = ["std", "colored", "float-cmp", "panic", "regex"] -bigdecimal = ["dep:bigdecimal", "dep:lazy_static"] +default = ["std", "colored", "float-cmp", "panic", "recursive", "regex"] +bigdecimal = ["dep:bigdecimal", "dep:once_cell"] colored = ["dep:sdiff"] float-cmp = ["dep:float-cmp"] -num-bigint = ["dep:num-bigint", "dep:lazy_static"] +num-bigint = ["dep:num-bigint", "dep:once_cell"] +recursive = ["dep:serde_core", "dep:indexmap", "indexmap/serde", "dep:rapidhash"] rust-decimal = ["dep:rust_decimal"] panic = ["std"] regex = ["dep:regex"] -std = [] +std = [ + "bigdecimal?/std", + "float-cmp?/std", + "indexmap?/std", + "num-bigint?/std", + "once_cell?/std", + "rapidhash?/std", + "regex?/std", + "rust_decimal?/std", + "sdiff?/std", + "serde_core?/std" +] [dependencies] hashbrown = "0.16" @@ -35,17 +47,22 @@ hashbrown = "0.16" # optional bigdecimal = { version = "0.4", optional = true, default-features = false } float-cmp = { version = "0.10", optional = true } -lazy_static = { version = "1", optional = true } +indexmap = { version = "2", optional = true, default-features = false } num-bigint = { version = "0.4", optional = true, default-features = false } +once_cell = { version = "1", optional = true, default-features = false, features = ["alloc", "critical-section"] } +rapidhash = { version = "4", optional = true, default-features = false } regex = { version = "1", optional = true } rust_decimal = { version = "1", optional = true, default-features = false } -sdiff = { version = "0.1", optional = true } +sdiff = { version = "0.1", optional = true, default-features = false } +serde_core = { version = "1", optional = true, default-features = false, features = ["alloc"] } [dev-dependencies] anyhow = "1" fakeenv = { version = "0.1", default-features = false, features = ["fake"] } proptest = "1" time = { version = "0.3", default-features = false, features = ["macros"] } +serde = { version = "1", default-features = false, features = ["alloc", "derive"] } +serde_bytes = { version = "0.11", default-features = false, features = ["alloc"] } version-sync = "0.9" [profile.dev.package] diff --git a/README.md b/README.md index 0bc640c..8551212 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,15 @@ Features of `asserting`: 1. assertions are convenient to write and easy to read 2. helpful error messages in case of failing assertions 3. colored diffs between expected and actual values -4. provide a reasonable number of assertions out of the box -5. chaining of multiple assertions on the same subject (see ["Chaining assertions"]) + (see ["Highlighted differences"](#highlighted-differences)) +4. chaining of multiple assertions on the same subject (see ["Chaining assertions"]) +5. field-by-field recursive comparison (see ["Field-by-field recursive comparison"]) :new: 6. soft assertions (execute multiple assertions before panicking) (see ["Soft assertions"]) -7. do not require that asserted types have to implement traits if it is not absolutely necessary -8. support for asserting custom types with provided assertions -9. writing custom assertions requires minimal effort -10. support no-std environments +7. provide a reasonable number of assertions out of the box +8. do not require that asserted types have to implement traits if it is not absolutely necessary +9. support for asserting custom types with provided assertions +10. writing custom assertions requires minimal effort +11. support no-std environments For an overview of the provided features and many examples on how to use `asserting` see the [crate-level documentation][docs-url]. @@ -83,11 +85,29 @@ require std can still be added. ```toml [dev-dependencies] -asserting = { version = "0.13", default-features = false, features = ["colored", "float-cmp", "regex"] } +asserting = { version = "0.13", default-features = false, features = ["colored", "float-cmp", "recursive", "regex"] } ``` An allocator is still needed for no-std. +## Crate Features + +Overview of the crate features of `asserting`. The column "no-std" specifies if this feature is +available in no-std environments. The column "default" specifies if this feature is enabled by +default. + +| Feature | Description | no-std | default | +|----------------|-----------------------------------------------------------------------|:------:|:-------:| +| `std` | Use the `std` library | no | yes | +| `colored` | Colored highlighting of differences between actual and expected value | yes | yes | +| `recursive` | Field-by-field recursive comparison mode | yes | yes | +| `float-cmp` | Floating point comparison (`ìs_close_to`) | yes | yes | +| `regex` | String matches Regex assertions (`matching`) | yes | yes | +| `panic` | Assert that code panics (with the expected message) | no | yes | +| `num-bigint` | Enhanced support for `num-bigint::BigInt` | yes | no | +| `bigdecimal` | Enhanced support for `bigdecimal::BigDecimal` | yes | no | +| `rust-decimal` | Enhanded support for `rust_decimal::Decimal` | yes | no | + ## Highlighted differences `asserting` can highlight the differences between the expected value(s) and the actual value(s) when @@ -493,6 +513,8 @@ To start assertions on code, use the `assert_that_code!()` macro. ["Chaining assertions"]: https://docs.rs/asserting/latest/asserting/#chaining-assertions-on-the-same-subject +["Field-by-field recursive comparison"]: https://docs.rs/asserting/latest/asserting/#field-by-field-recursive-comparison + ["soft assertions"]: https://docs.rs/asserting/#soft-assertions [custom assertions]: https://docs.rs/asserting/#custom-assertions diff --git a/examples/fixture/mod.rs b/examples/fixture/mod.rs index d8b0a93..713f282 100644 --- a/examples/fixture/mod.rs +++ b/examples/fixture/mod.rs @@ -8,17 +8,25 @@ mod dummy_extern_uses { #[cfg(feature = "float-cmp")] use float_cmp as _; use hashbrown as _; - #[cfg(feature = "num-bigint")] - use lazy_static as _; + #[cfg(feature = "recursive")] + use indexmap as _; #[cfg(feature = "num-bigint")] use num_bigint as _; + #[cfg(any(feature = "bigdecimal", feature = "num-bigint"))] + use once_cell as _; use proptest as _; + #[cfg(feature = "recursive")] + use rapidhash as _; #[cfg(feature = "regex")] use regex as _; #[cfg(feature = "rust-decimal")] use rust_decimal as _; #[cfg(feature = "colored")] use sdiff as _; + use serde as _; + use serde_bytes as _; + #[cfg(feature = "recursive")] + use serde_core as _; use time as _; use version_sync as _; } diff --git a/justfile b/justfile index c586115..31e18dc 100644 --- a/justfile +++ b/justfile @@ -44,7 +44,7 @@ lint-default: # linting code using Clippy for no-std environment lint-no-std: - cargo clippy --all-targets --no-default-features --features "colored, float-cmp, num-bigint, rust-decimal, bigdecimal" + cargo clippy --all-targets --no-default-features --features "colored, float-cmp, num-bigint, recursive, regex, rust-decimal, bigdecimal" # linting code using Clippy with no features enabled lint-no-features: @@ -67,7 +67,7 @@ test-default: # run tests for no-std environment test-no-std: - cargo test --no-default-features --features "colored, float-cmp, num-bigint, rust-decimal, bigdecimal" + cargo test --no-default-features --features "colored, float-cmp, num-bigint, recursive, regex, rust-decimal, bigdecimal" # run tests with no features enabled test-no-features: diff --git a/src/assertions.rs b/src/assertions.rs index fa0de9b..a493d5b 100644 --- a/src/assertions.rs +++ b/src/assertions.rs @@ -99,6 +99,7 @@ pub trait AssertSameAs { /// /// assert_that!(42).is_same_as(42); /// ``` + #[track_caller] fn is_same_as(self, expected: E) -> Self; /// Verifies that the subject is of the same type but has a different value @@ -114,9 +115,150 @@ pub trait AssertSameAs { /// /// assert_that!(41).is_not_same_as(42); /// ``` + #[track_caller] fn is_not_same_as(self, expected: E) -> Self; } +/// Assert whether a value is equivalent to a value of type [`Value`] using +/// field-by-field recursive comparison. +/// +/// These assertions are intended for the field-by-field recursive comparison +/// mode and are implemented for all types that implement [`serde::Serialize`]. +/// +/// # Examples +/// +/// ``` +/// use asserting::prelude::*; +/// use serde::Serialize; +/// +/// #[derive(Serialize)] +/// struct Person { +/// id: u64, +/// name: String, +/// age: u8, +/// email: Vec, +/// } +/// +/// let person = Person { +/// id: 456, +/// name: "Silvia".into(), +/// age: 25, +/// email: vec!["silvia@domain.com".to_string()], +/// }; +/// +/// assert_that!(person) +/// .using_recursive_comparison() +/// .ignoring_not_expected_fields() +/// .is_equivalent_to(value!({ +/// name: "Silvia", +/// age: 25_u8, +/// })); +/// ``` +/// +/// [`Value`]: crate::recursive_comparison::value::Value +/// [`serde::Serialize`]: serde_core::Serialize +#[cfg(feature = "recursive")] +#[cfg_attr(docsrs, doc(cfg(feature = "recursive")))] +pub trait AssertEquivalence { + /// Verifies that the subject is equivalent to the expected value of type + /// [`Value`]. + /// + /// The actual value (subject) and the expected value (parameter) are + /// compared field-by-field recursively. + /// + /// The intended and most convenient way to construct a [`Value`] instance + /// is using the [`value!`] macro. + /// + /// # Examples + /// + /// ``` + /// use asserting::prelude::*; + /// use serde::Serialize; + /// + /// #[derive(Serialize)] + /// struct Person { + /// id: u64, + /// name: String, + /// age: u8, + /// email: Vec, + /// } + /// + /// let person = Person { + /// id: 456, + /// name: "Silvia".into(), + /// age: 25, + /// email: vec!["silvia@domain.com".to_string()], + /// }; + /// + /// assert_that!(person) + /// .using_recursive_comparison() + /// .ignoring_not_expected_fields() + /// .is_equivalent_to(value!({ + /// name: "Silvia", + /// age: 25_u8, + /// })); + /// ``` + /// + /// See the documentation of the [`recursive_comparison`] module for more + /// details. + /// + /// [`value!`]: crate::prelude::value + /// [`Value`]: crate::recursive_comparison::value::Value + /// [`recursive_comparison`]: crate::recursive_comparison + /// [`serde::Serialize`]: serde_core::Serialize + #[track_caller] + fn is_equivalent_to(self, expected: E) -> Self; + + /// Verifies that the subject is not equivalent to the expected value of + /// type [`Value`]. + /// + /// The actual value (subject) and the expected value (parameter) are + /// compared field-by-field recursively. Fields of the actual value that + /// are not present in the actual value are always ignored. + /// + /// The intended and most convenient way to construct a [`Value`] instance + /// is using the [`value!`] macro. + /// + /// # Examples + /// + /// ``` + /// use asserting::prelude::*; + /// use serde::Serialize; + /// + /// #[derive(Serialize)] + /// struct Person { + /// id: u64, + /// name: String, + /// age: u8, + /// email: Vec, + /// } + /// + /// let person = Person { + /// id: 456, + /// name: "Silvia".into(), + /// age: 25, + /// email: vec!["silvia@domain.com".to_string()], + /// }; + /// + /// assert_that!(person) + /// .using_recursive_comparison() + /// .is_not_equivalent_to(value!({ + /// name: "Silvia", + /// age: 21_u8, + /// })); + /// ``` + /// + /// See the documentation of the [`recursive_comparison`] module for more + /// details. + /// + /// [`value!`]: crate::prelude::value + /// [`Value`]: crate::recursive_comparison::value::Value + /// [`recursive_comparison`]: crate::recursive_comparison + /// [`serde::Serialize`]: serde_core::Serialize + #[track_caller] + fn is_not_equivalent_to(self, expected: E) -> Self; +} + /// Assert approximate equality for floating point numbers. /// /// # Examples diff --git a/src/bigdecimal/mod.rs b/src/bigdecimal/mod.rs index aa48426..2816b10 100644 --- a/src/bigdecimal/mod.rs +++ b/src/bigdecimal/mod.rs @@ -3,12 +3,12 @@ use crate::properties::{ }; use bigdecimal::num_bigint::Sign; use bigdecimal::{BigDecimal, BigDecimalRef, One, Zero}; -use lazy_static::lazy_static; +use once_cell::sync::Lazy; -lazy_static! { - static ref BIGDECIMAL_ZERO: BigDecimal = bigdecimal_zero(); - static ref BIGDECIMAL_ONE: BigDecimal = bigdecimal_one(); -} +#[allow(clippy::non_std_lazy_statics)] +static BIGDECIMAL_ZERO: Lazy = Lazy::new(bigdecimal_zero); +#[allow(clippy::non_std_lazy_statics)] +static BIGDECIMAL_ONE: Lazy = Lazy::new(bigdecimal_one); #[inline] fn bigdecimal_zero() -> BigDecimal { diff --git a/src/collection.rs b/src/collection.rs index 28dca7d..323a1b1 100644 --- a/src/collection.rs +++ b/src/collection.rs @@ -98,7 +98,7 @@ impl IsEmptyProperty for [T; N] { #[cfg(feature = "std")] mod std { use crate::properties::{IsEmptyProperty, LengthProperty}; - use std::collections::{HashMap, HashSet}; + use crate::std::collections::{HashMap, HashSet}; impl IsEmptyProperty for HashMap { fn is_empty_property(&self) -> bool { diff --git a/src/colored/tests.rs b/src/colored/tests.rs index 83226f1..75e235f 100644 --- a/src/colored/tests.rs +++ b/src/colored/tests.rs @@ -522,6 +522,7 @@ mod with_colored_and_std_features { assert_that(assertion.diff_format()).is_equal_to(&DIFF_FORMAT_NO_HIGHLIGHT); } + #[cfg(feature = "panic")] #[test] fn assert_that_code_sets_the_diff_format_to_red_green() { env::set_var(ENV_VAR_HIGHLIGHT_DIFFS, "red-green"); @@ -531,6 +532,7 @@ mod with_colored_and_std_features { assert_that(assertion.diff_format()).is_equal_to(&DIFF_FORMAT_RED_GREEN); } + #[cfg(feature = "panic")] #[test] fn verify_that_code_sets_the_diff_format_to_no_highlighting() { let assertion = verify_that_code(|| {}); diff --git a/src/env.rs b/src/env.rs index 1ba8911..649545c 100644 --- a/src/env.rs +++ b/src/env.rs @@ -14,9 +14,9 @@ pub use fake_env::*; #[cfg(test)] mod fake_env { + use crate::std::cell::RefCell; + use crate::std::env::VarError; use fakeenv::EnvStore; - use std::cell::RefCell; - use std::env::VarError; thread_local! { static ENV_STORE: RefCell = RefCell::new({ diff --git a/src/lib.rs b/src/lib.rs index 4df3171..d63e18a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,7 +5,7 @@ //! particularly as provided by this crate: //! //! * express the intent of an assertion -//! * an assertion reads more like natural english +//! * an assertion reads more like natural English //! * concise and expressive assertions for more complex types like collections //! * distinct and more helpful error messages for specific assertions //! * easy spotting the difference between the expected and the actual value @@ -320,6 +320,85 @@ //! # } //! ``` //! +//! ## Field-by-field recursive comparison +//! +//! Requires crate feature `recursive`. +//! +//! The recursive comparison mode compares the subject and the expected value +//! field-by-field recursively. The recursive comparison mode is available for +//! all types that implement [`serde::Serialize`]. +//! +//! Basic usage: +//! +//! ``` +//! # #[cfg(not(feature = "recursive"))] +//! # fn main() {} +//! # #[cfg(feature = "recursive")] +//! # fn main() { +//! use asserting::prelude::*; +//! use serde::Serialize; +//! +//! #[derive(Serialize)] +//! struct Address { +//! id: u64, +//! street: String, +//! city: String, +//! zip: u16, +//! } +//! +//! #[derive(Serialize)] +//! struct Person { +//! id: u64, +//! name: String, +//! age: u8, +//! address: Address, +//! } +//! +//! let person = Person { +//! id: 123, +//! name: "Silvia".into(), +//! age: 25, +//! address: Address { +//! id: 92, +//! street: "Second Street".into(), +//! city: "New York".into(), +//! zip: 12345, +//! } +//! }; +//! +//! // ignore some fields +//! assert_that!(&person) +//! .using_recursive_comparison() +//! .ignoring_fields(["id", "address.id", "address.street"]) +//! .is_equal_to(Person { +//! id: 0, +//! name: "Silvia".into(), +//! age: 25, +//! address: Address { +//! id: 0, +//! street: "Main Street".into(), +//! city: "New York".into(), +//! zip: 12345, +//! } +//! }); +//! +//! // assert only fields relevant for a testcase +//! assert_that!(person) +//! .using_recursive_comparison() +//! .ignoring_not_expected_fields() +//! .is_equivalent_to(value!({ +//! name: "Silvia", +//! age: 25_u8, +//! address: { +//! zip: 12345_u16, +//! } +//! })); +//! # } +//! ``` +//! +//! A more in-depth description of the recursive comparison mode is given in the +//! [`recursive_comparison`] module. +//! //! # The `assert_that` and `verify_that` functions and macros //! //! Assertions can be written in two ways. The standard way that panics when @@ -341,7 +420,7 @@ //! failures from assertions, which can be read later. //! //! The [`Spec`] can hold additional information about the subject, such as the -//! expression we are asserting, the code location of the assert statement and +//! expression we are asserting, the code location of the assert statement, and //! an optional description of what we are going to assert. These attributes are //! all optional and must be set explicitly by the user. //! @@ -756,10 +835,11 @@ //! [`assert_that_code`]: spec::assert_that_code //! [`verify_that`]: spec::verify_that //! [`verify_that_code`]: spec::verify_that_code -//! [`display_failures()`]: spec::Spec::display_failures +//! [`display_failures()`]: spec::GetFailures::display_failures //! [`failures()`]: spec::GetFailures::failures //! [`named()`]: spec::Spec::named //! [`located_at()`]: spec::Spec::located_at +//! [`serde::Serialize`]: serde_core::Serialize #![doc(html_root_url = "https://docs.rs/asserting/0.13.1")] #![cfg_attr(not(feature = "std"), no_std)] @@ -815,11 +895,22 @@ mod std { pub use std::*; } +// Not public API. Used from macro-generated code. +#[doc(hidden)] +pub mod __private { + extern crate alloc; + #[doc(hidden)] + pub use alloc::vec; +} + pub mod assertions; pub mod colored; pub mod expectations; pub mod prelude; pub mod properties; +#[cfg(feature = "recursive")] +#[cfg_attr(docsrs, doc(cfg(feature = "recursive")))] +pub mod recursive_comparison; pub mod spec; #[cfg(feature = "bigdecimal")] @@ -829,7 +920,7 @@ mod c_string; mod char; mod char_count; mod collection; -#[cfg(feature = "std")] +#[cfg(all(feature = "std", feature = "colored"))] mod env; mod equality; mod error; @@ -870,6 +961,8 @@ type TestCodeSnippetsInReadme = (); mod dummy_extern_uses { use fakeenv as _; use proptest as _; + use serde as _; + use serde_bytes as _; use time as _; use version_sync as _; } diff --git a/src/map/mod.rs b/src/map/mod.rs index 515113c..f0c9beb 100644 --- a/src/map/mod.rs +++ b/src/map/mod.rs @@ -503,8 +503,8 @@ mod hashbrown_impls { #[cfg(feature = "std")] mod std_hashmap_impls { use crate::properties::MapProperties; + use crate::std::collections::HashMap; use crate::std::iter::Iterator; - use std::collections::HashMap; impl MapProperties for HashMap { type Key = K; diff --git a/src/num_bigint/mod.rs b/src/num_bigint/mod.rs index 540fa98..92cc67e 100644 --- a/src/num_bigint/mod.rs +++ b/src/num_bigint/mod.rs @@ -1,15 +1,15 @@ use crate::properties::{AdditiveIdentityProperty, MultiplicativeIdentityProperty, SignumProperty}; use crate::std::vec; -use lazy_static::lazy_static; use num_bigint::{BigInt, BigUint, Sign}; +use once_cell::sync::Lazy; static BIGINT_ZERO: BigInt = BigInt::ZERO; static BIGUINT_ZERO: BigUint = BigUint::ZERO; -lazy_static! { - static ref BIGINT_ONE: BigInt = bigint_one(); - static ref BIGUINT_ONE: BigUint = biguint_one(); -} +#[allow(clippy::non_std_lazy_statics)] +static BIGINT_ONE: Lazy = Lazy::new(bigint_one); +#[allow(clippy::non_std_lazy_statics)] +static BIGUINT_ONE: Lazy = Lazy::new(biguint_one); #[inline] fn bigint_one() -> BigInt { diff --git a/src/prelude.rs b/src/prelude.rs index e20eeba..e900534 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -1,7 +1,7 @@ -//! Re-export of all types, traits, functions and macros that are needed to +//! Re-export of all types, traits, functions, and macros that are needed to //! write assertions in tests. //! -//! When writing assertions in tests importing this prelude is all that should +//! When writing assertions in tests, importing this prelude is all that should //! be needed. //! //! # Example @@ -39,3 +39,7 @@ pub use super::{ spec::{assert_that_code, verify_that_code}, verify_that_code, }; + +#[cfg(feature = "recursive")] +#[cfg_attr(docsrs, doc(cfg(feature = "recursive")))] +pub use super::value; diff --git a/src/recursive_comparison/macros/mod.rs b/src/recursive_comparison/macros/mod.rs new file mode 100644 index 0000000..6e21cb9 --- /dev/null +++ b/src/recursive_comparison/macros/mod.rs @@ -0,0 +1,614 @@ +/// Construct a [`Value`] from a Rust-like constructor expression. +/// +/// With this macro it is possible to construct values that have the same +/// structure as actual types like structs, enums, tuples, or even primitive +/// types. It is not necessary to declare the types in advance. +/// +/// # Syntax +/// +/// The syntax is more or less the same as Rust's constructor expressions for +/// structs, enums, and tuples. The name of structs can be omitted. Enum +/// variants must be written in the form `Foo::Bar` (specifying only the +/// variant is not supported, even if the variant is imported). +/// +/// Literals for bool, char, number, and strings can be written similar to Rust +/// literals with some minor differences: a `&str` does not have to be converted +/// to a `String` (this is done automatically), and brackets are used for +/// sequences, not arrays. Each number literal should contain the type, e.g., +/// `42_u64`, `1.2_f32`, etc. This is necessary because the macro cannot infer +/// the type of number literals. +/// +/// The following example gives an overview of the syntax, with elements of +/// various types. +/// +/// ``` +/// use asserting::prelude::*; +/// +/// let value = value!({ // a struct +/// foo: 2.3_f64, // a float literal +/// bar: { // an embedded struct +/// baz: "alpha", // a string literal +/// qux: 123_i16, // an integer literal +/// corge: true, // a boolean literal +/// }, +/// grault: Sample::Two("beta", -456_i64), // a tuple variant +/// waldo: (123_u8, 234_u8, 56_u8), // a tuple +/// fred: ["alpha", "beta", "gamma"], // a sequence +/// quux: #{ 'a' => 1, 'b' => 2, 'c' => 3}, // a map +/// thud: Named(0.8_f32), // a tuple struct +/// }); +/// ``` +/// +/// Variables in scope can be referenced inside the `value!`-macro. +/// +/// ``` +/// use asserting::prelude::*; +/// +/// let one = 1; +/// let two = 2; +/// let three = 3; +/// +/// let value = value!([one, two, three]); +/// +/// assert_eq!(format!("{value:?}"), "[1, 2, 3]"); +/// ``` +/// +/// Expressions can be used inside the `value!`-macro as well: +/// +/// ``` +/// use asserting::prelude::*; +/// +/// let value = value!(Sum(13_i16 + 17_i16)); +/// +/// assert_eq!(format!("{value:?}"), "Sum(30)"); +/// ``` +/// +/// ## Structs +/// +/// Structs can be constructed on the fly, without prior declaration of a type. +/// In `asserting` they are called "anonymous structs". The name of a struct +/// can be omitted. +/// +/// ``` +/// # use asserting::prelude::*; +/// # +/// let value = value!({ +/// name: "Silvia", +/// age: 25_u8, +/// }); +/// ``` +/// +/// The name of a struct to be constructed can be specified as by the usual +/// syntax in Rust. +/// +/// ``` +/// # use asserting::prelude::*; +/// # +/// let value = value!(Person { +/// name: "Silvia", +/// age: 25_u8, +/// }); +/// ``` +/// +/// Note: The name of a struct is not compared in the field-by-field recursive +/// comparison mode. +/// +/// ## Tuples +/// +/// A tuple is constructed using parenthesis as in plain Rust. +/// +/// ``` +/// # use asserting::prelude::*; +/// # +/// let value = value!((42_u64, "alpha", true)); +/// ``` +/// +/// ## Enums-Variants +/// +/// Example for constructing a value of unit variant: +/// +/// ``` +/// # use asserting::prelude::*; +/// # +/// let value = value!(Foo::Bar); +/// ``` +/// +/// Example for constructing a value of tuple variant: +/// +/// ``` +/// # use asserting::prelude::*; +/// # +/// let value = value!(Foo::Bar(-1.3_f32)); +/// ``` +/// +/// Example for constructing a value of struct variant: +/// +/// ``` +/// # use asserting::prelude::*; +/// # +/// let value = value!(Foo::Bar { left: "alpha", right: -123_i16 }); +/// ``` +/// +/// ## Sequences +/// +/// A sequence is constructed by enclosing a list of values inside brackets. +/// In the following example we construct a sequence of chars. +/// +/// ``` +/// # use asserting::prelude::*; +/// # +/// let value = value!(['a', 'b', 'c']); +/// ``` +/// +/// ## Maps +/// +/// A map starts with `#{` and ends with `}`. An association between a key and +/// a value is separated by `=>`. Multiple key/value-pairs are separated by `,`. +/// +/// ``` +/// # use asserting::prelude::*; +/// # +/// let value = value!(#{ +/// 'a' => 1, +/// 'b' => 2, +/// 'c' => 3, +/// }); +/// ``` +/// +/// ## Primitive types +/// +/// | Type | Example | +/// |----------|-------------------| +/// | `bool` | `true` or `false` | +/// | `char` | `'a'` | +/// | `f32` | `1.2_f32` | +/// | `f64` | `1.2_f64` | +/// | `str` | `"alpha"` | +/// | `String` | `"alpha"` | +/// | `i8` | `-12_i8` | +/// | `i16` | `-12_i16` | +/// | `i32` | `-12_i32` | +/// | `i64` | `-12_i64` | +/// | `i128` | `-12_i128` | +/// | `u8` | `12_i8` | +/// | `u16` | `12_i16` | +/// | `u32` | `12_i32` | +/// | `u64` | `12_i64` | +/// | `u128` | `12_i128` | +/// +/// Note: `isize` and `usize` are not supported by `serde`. Therefore, `isize` +/// values are converted to i64 or i128 and `usize` values are converted to +/// `u64` or `u128`. +/// +/// # Limitations +/// +/// ## No Field Init Shorthand +/// +/// When initializing a struct field with the value of a variable, the field +/// init shorthand is not supported. Even if the field has the same name as the +/// variable, the variable must be repeated after the colon. +/// +/// Instead of using the field init shorthand, which does not compile: +/// +/// ```compile_fail +/// use asserting::prelude::*; +/// +/// let bar = "alpha"; +/// +/// let value = value!(Foo { bar }); +/// ``` +/// +/// using the normal (verbose) syntax works: +/// +/// ``` +/// use asserting::prelude::*; +/// +/// let bar = "alpha"; +/// +/// let value = value!(Foo { bar: bar }); +/// +/// assert_eq!(value, value!(Foo { bar: "alpha" })); +/// ``` +/// +/// ## No Unit Structs +/// +/// This macro does not support unit structs. Unit structs interfere with +/// identifiers captured from the environment. We decided that capturing +/// variables from the environment of the macro is more valuable than unit +/// structs. +/// +/// [`Value`]: crate::recursive_comparison::value::Value +#[macro_export] +macro_rules! value { + // hide distracting implementation details from the generated rustdoc. + ($($value:tt)+) => { + $crate::value_impl!($($value)+) + }; +} + +/// DO NOT RELY ON THIS MACRO AS IT MAY CHANGE WITHOUT NOTICE! +#[macro_export] +#[doc(hidden)] +macro_rules! value_impl { + //////////////////////////////////////////////////////////////////////// + // TT muncher for parsing the values of a seq [...]. + // + // Must be invoked as: value!(@seq [] ($($tt)*)) + // It returns a 'Vec`. + //////////////////////////////////////////////////////////////////////// + + // Done with a trailing comma. + (@seq [$($elems:expr,)*] ()) => { + $crate::__private::vec![$($elems,)*] + }; + + // Done without a trailing comma. + (@seq [$($elems:expr),*] ()) => { + $crate::__private::vec![$($elems),*] + }; + + // The next element is a seq. + (@seq [$($elems:expr,)*] ([ $($val:tt)* ] $($rest:tt)*)) => { + $crate::value_impl!(@seq [$($elems,)* $crate::value_impl!([$($val)*]),] ($($rest)*)) + }; + + // The next element is a map. + (@seq [$($elems:expr,)*] (#{ $($val:tt)* } $($rest:tt)*)) => { + $crate::value_impl!(@seq [$($elems,)* $crate::value_impl!(#{$($val)*}),] ($($rest)*)) + }; + + // The next element is an anonymous struct. + (@seq [$($elems:expr,)*] ({ $($val:tt)* } $($rest:tt)*)) => { + $crate::value_impl!(@seq [$($elems,)* $crate::value_impl!({$($val)*}),] ($($rest)*)) + }; + + // The next element is a named struct. + (@seq [$($elems:expr,)*] ($name:ident { $($val:tt)* } $($rest:tt)*)) => { + $crate::value_impl!(@seq [$($elems,)* $crate::value_impl!($name {$($val)*}),] ($($rest)*)) + }; + + // The next element is a tuple struct. + (@seq [$($elems:expr,)*] ($name:ident ( $($val:tt)* ) $($rest:tt)*)) => { + $crate::value_impl!(@seq [$($elems,)* $crate::value_impl!($name ($($val)*)),] ($($rest)*)) + }; + + // The next element is a struct variant. + (@seq [$($elems:expr,)*] ($name:ident :: $variant:ident { $($val:tt)* } $($rest:tt)*)) => { + $crate::value_impl!(@seq [$($elems,)* $crate::value_impl!($name :: $variant {$($val)*}),] ($($rest)*)) + }; + + // The next element is a tuple variant. + (@seq [$($elems:expr,)*] ($name:ident :: $variant:ident ( $($val:tt)* ) $($rest:tt)*)) => { + $crate::value_impl!(@seq [$($elems,)* $crate::value_impl!($name :: $variant ($($val)*)),] ($($rest)*)) + }; + + // The next element is a unit variant. + (@seq [$($elems:expr,)*] ($name:ident :: $variant:ident $($rest:tt)*)) => { + $crate::value_impl!(@seq [$($elems,)* $crate::value_impl!($name :: $variant),] ($($rest)*)) + }; + + // The next element is an expression followed by a comma. + (@seq [$($elems:expr,)*] ($next:expr , $($rest:tt)*)) => { + $crate::value_impl!(@seq [$($elems,)* $crate::value_impl!($next),] ($($rest)*)) + }; + + // The last element is an expression with no trailing comma. + (@seq [$($elems:expr,)*] ($last:expr)) => { + $crate::value_impl!(@seq [$($elems,)* $crate::value_impl!($last),] ()) + }; + + // Comma after the most recent element. + (@seq [$($elems:expr,)*] (, $($rest:tt)*)) => { + $crate::value_impl!(@seq [$($elems,)*] ($($rest)*)) + }; + + //////////////////////////////////////////////////////////////////////// + // TT muncher for parsing the fields of a struct {...}. + // + // Must be invoked as: value!(@fields [] ($($tt)*)) + // It returns a 'Vec'. + //////////////////////////////////////////////////////////////////////// + + // Done with a trailing comma. + (@fields [$($fields:expr,)*] ()) => { + $crate::__private::vec![$($fields,)*] + }; + + // Done without a trailing comma. + (@fields [$($fields:expr),*] ()) => { + $crate::__private::vec![$($fields),*] + }; + + // The next value is a seq. + (@fields [$($fields:expr,)*] ($key:ident : [ $($val:tt)* ] $($rest:tt)*)) => { + $crate::value_impl!(@fields [$($fields,)* + $crate::recursive_comparison::value::Field { + name: stringify!($key).into(), + value: $crate::value_impl!([$($val)*]), + }, + ] ($($rest)*)) + }; + + // The next value is a map. + (@fields [$($fields:expr,)*] ($key:ident : #{ $($val:tt)* } $($rest:tt)*)) => { + $crate::value_impl!(@fields [$($fields,)* + $crate::recursive_comparison::value::Field { + name: stringify!($key).into(), + value: $crate::value_impl!(#{$($val)*}), + }, + ] ($($rest)*)) + }; + + // The next value is a named struct. + (@fields [$($fields:expr,)*] ($key:ident : $name:ident { $($val:tt)* } $($rest:tt)*)) => { + $crate::value_impl!(@fields [$($fields,)* + $crate::recursive_comparison::value::Field { + name: stringify!($key).into(), + value: $crate::value_impl!($name { $($val)* }), + }, + ] ($($rest)*)) + }; + + // The next value is an anonymous struct. + (@fields [$($fields:expr,)*] ($key:ident : { $($val:tt)* } $($rest:tt)*)) => { + $crate::value_impl!(@fields [$($fields,)* + $crate::recursive_comparison::value::Field { + name: stringify!($key).into(), + value: $crate::value_impl!({$($val)*}), + }, + ] ($($rest)*)) + }; + + // The next value is a tuple struct. + (@fields [$($fields:expr,)*] ($key:ident : $name:ident ( $($val:tt)* ) $($rest:tt)*)) => { + $crate::value_impl!(@fields [$($fields,)* + $crate::recursive_comparison::value::Field { + name: stringify!($key).into(), + value: $crate::value_impl!($name ($($val)*)), + }, + ] ($($rest)*)) + }; + + // The next value is a struct variant. + (@fields [$($fields:expr,)*] ($key:ident : $name:ident :: $variant:ident { $($val:tt)* } $($rest:tt)*)) => { + $crate::value_impl!(@fields [$($fields,)* + $crate::recursive_comparison::value::Field { + name: stringify!($key).into(), + value: $crate::value_impl!($name :: $variant {$($val)*}), + }, + ] ($($rest)*)) + }; + + // The next value is a tuple variant. + (@fields [$($fields:expr,)*] ($key:ident : $name:ident :: $variant:ident ( $($val:tt)* ) $($rest:tt)*)) => { + $crate::value_impl!(@fields [$($fields,)* + $crate::recursive_comparison::value::Field { + name: stringify!($key).into(), + value: $crate::value_impl!($name :: $variant ($($val)*)), + }, + ] ($($rest)*)) + }; + + // The next value is a unit variant. + (@fields [$($fields:expr,)*] ($key:ident : $name:ident :: $variant:ident $($rest:tt)*)) => { + $crate::value_impl!(@fields [$($fields,)* + $crate::recursive_comparison::value::Field { + name: stringify!($key).into(), + value: $crate::value_impl!($name :: $variant), + }, + ] ($($rest)*)) + }; + + // The next field followed by a comma. + (@fields [$($fields:expr,)*] ($key:ident : $val:expr , $($rest:tt)*)) => { + $crate::value_impl!(@fields [$($fields,)* + $crate::recursive_comparison::value::Field { + name: stringify!($key).into(), + value: $crate::value_impl!($val) + }, + ] ($($rest)*)) + }; + + // The last field without a trailing comma. + (@fields [$($fields:expr,)*] ($key:ident : $val:expr)) => { + $crate::value_impl!(@fields [$($fields,)* + $crate::recursive_comparison::value::Field { + name: stringify!($key).into(), + value: $crate::value_impl!($val), + }, + ] ()) + }; + + // Comma after the most recent field. + (@fields [$($fields:expr,)*] (, $($rest:tt)*)) => { + $crate::value_impl!(@fields [$($fields,)*] ($($rest)*)) + }; + + //////////////////////////////////////////////////////////////////////// + // TT muncher for parsing the values of a map #{...}. + // + // Must be invoked as: value!(@map [] () ($($tt)*)) + // It returns a 'Vec<(Value, Value)>`. + //////////////////////////////////////////////////////////////////////// + + // Done + (@map [$($pairs:expr,)*] () ()) => { + $crate::__private::vec![$($pairs,)*] + }; + + // The key is finished, start parsing the value. + (@map [$($pairs:expr,)*] ($($key:tt)+) (=> $($rest:tt)*)) => { + $crate::value_impl!(@map_val [$($pairs,)*] ($($key)+) () ($($rest)*)) + }; + + // Munch the next token for the key. + (@map [$($pairs:expr,)*] ($($key:tt)*) ($next:tt $($rest:tt)*)) => { + $crate::value_impl!(@map [$($pairs,)*] ($($key)* $next) ($($rest)*)) + }; + + // The value is finished by a comma, start parsing the next entry. + (@map_val [$($pairs:expr,)*] ($($key:tt)+) ($($val:tt)+) (, $($rest:tt)*)) => { + $crate::value_impl!(@map [$($pairs,)* ($crate::value_impl!($($key)+), $crate::value_impl!($($val)+)),] () ($($rest)*)) + }; + + // The value is finished (end of tokens). + (@map_val [$($pairs:expr,)*] ($($key:tt)+) ($($val:tt)+) ()) => { + $crate::value_impl!(@map [$($pairs,)* ($crate::value_impl!($($key)+), $crate::value_impl!($($val)+)),] () ()) + }; + + // Munch the next token for the value. + (@map_val [$($pairs:expr,)*] ($($key:tt)+) ($($val:tt)*) ($next:tt $($rest:tt)*)) => { + $crate::value_impl!(@map_val [$($pairs,)*] ($($key)+) ($($val)* $next) ($($rest)*)) + }; + + //////////////////////////////////////////////////////////////////////// + // The main implementation. + // + // Must be invoked as: value!($($tt)+) + //////////////////////////////////////////////////////////////////////// + + // Booleans + (false) => { + $crate::recursive_comparison::value::Value::Bool(false) + }; + (true) => { + $crate::recursive_comparison::value::Value::Bool(true) + }; + + // Empty Seq: [ ] + ([ ]) => { + $crate::recursive_comparison::value::Value::Seq($crate::__private::vec![]) + }; + + // Seq: [ 1, 2, 3 ] + ([ $($tt:tt)+ ]) => { + $crate::recursive_comparison::value::Value::Seq($crate::value_impl!(@seq [] ($($tt)+))) + }; + + // Empty named struct: Foo {} + ($name:ident { }) => { + $crate::recursive_comparison::value::Value::Struct { + type_name: stringify!($name).into(), + fields: $crate::__private::vec![], + } + }; + + // Named struct: Foo { a: 1, b: 2 } + ($name:ident { $($tt:tt)+ }) => { + $crate::recursive_comparison::value::Value::Struct { + type_name: stringify!($name).into(), + fields: $crate::value_impl!(@fields [] ($($tt)+)), + } + }; + + // Empty anonymous struct: { } + ({ }) => { + $crate::recursive_comparison::value::Value::Struct { + type_name: "".into(), + fields: $crate::__private::vec![], + } + }; + + // Anonymous struct: { a: 1, b: 2 } + ({ $($tt:tt)+ }) => { + $crate::recursive_comparison::value::Value::Struct { + type_name: "".into(), + fields: $crate::value_impl!(@fields [] ($($tt)+)), + } + }; + + // Empty struct variant: Foo::Bar { } + ($name:ident :: $variant:ident { }) => { + $crate::recursive_comparison::value::Value::StructVariant { + type_name: stringify!($name).into(), + variant: stringify!($variant).into(), + fields: $crate::__private::vec![], + } + }; + + // Struct variant: Foo::Bar { a: 1, b: 2 } + ($name:ident :: $variant:ident { $($tt:tt)+ }) => { + $crate::recursive_comparison::value::Value::StructVariant { + type_name: stringify!($name).into(), + variant: stringify!($variant).into(), + fields: $crate::value_impl!(@fields [] ($($tt)+)), + } + }; + + // Tuple Variant: Foo::Bar(1, 2) + ($name:ident :: $variant:ident ( $($tt:tt)+ )) => { + $crate::recursive_comparison::value::tuple_variant( + stringify!($name), + stringify!($variant), + $crate::value_impl!(@seq [] ($($tt)+)) + ) + }; + + // Unit Variant: Foo::Bar + ($name:ident :: $variant:ident) => { + $crate::recursive_comparison::value::Value::UnitVariant { + type_name: stringify!($name).into(), + variant: stringify!($variant).into(), + } + }; + + // Empty tuple struct: Foo() + ($name:ident ( )) => { + $crate::recursive_comparison::value::Value::Struct { + type_name: stringify!($name).into(), + fields: $crate::__private::vec![], + } + }; + + // Tuple struct: Foo(1, 2) + ($name:ident ( $($tt:tt)+ )) => { + $crate::recursive_comparison::value::tuple_struct( + stringify!($name), + $crate::value_impl!(@seq [] ($($tt)+)) + ) + }; + + // Empty Map: #{ } + (#{ }) => { + $crate::recursive_comparison::value::Value::Map( + $crate::recursive_comparison::value::Map::new() + ) + }; + + // Map: #{ a => 1, b => 2 } + (#{ $($tt:tt)+ }) => { + $crate::recursive_comparison::value::Value::Map( + $crate::recursive_comparison::value::Map::from_iter( + $crate::value_impl!(@map [] () ($($tt)+)) + ) + ) + }; + + // Unit Struct: Foo + ($name:ident) => { + $crate::recursive_comparison::value::Value::Struct { + type_name: stringify!($name).into(), + fields: $crate::__private::vec![], + } + }; + + // Empty Tuple or Unit: () + (()) => { + $crate::recursive_comparison::value::Value::Unit + }; + + // Tuple: (1, 2) + (( $($tt:tt)+ )) => { + $crate::recursive_comparison::value::tuple($crate::value_impl!(@seq [] ($($tt)+))) + }; + + // Any Serialize type: numbers, strings, chars, variables, etc. + // Must be below every other rule! + ($val:expr) => { + $crate::recursive_comparison::serialize::to_recursive_value(&$val) + .unwrap_or_else(|err| panic!("failed to serialize expression: {err}")) + }; +} + +#[cfg(test)] +mod tests; diff --git a/src/recursive_comparison/macros/tests.rs b/src/recursive_comparison/macros/tests.rs new file mode 100644 index 0000000..1d917f9 --- /dev/null +++ b/src/recursive_comparison/macros/tests.rs @@ -0,0 +1,1137 @@ +use crate::recursive_comparison::value::*; +use crate::std::string::ToString; +use crate::std::vec::Vec; + +#[test] +fn false_value() { + let value = value!(false); + + assert_eq!(value, bool(false)); +} + +#[test] +fn true_value() { + let value = value!(true); + + assert_eq!(value, bool(true)); +} + +#[test] +fn char_value() { + let value = value!('@'); + + assert_eq!(value, char('@')); +} + +#[test] +fn u8_value() { + let value = value!(42_u8); + + assert_eq!(value, uint8(42)); +} + +#[test] +fn implicit_i32_value() { + let value = value!(-234); + + assert_eq!(value, int32(-234)); +} + +#[test] +fn str_value() { + let value = value!("Hello, world!"); + + assert_eq!(value, string("Hello, world!")); +} + +#[test] +fn string_value() { + let value = value!("Hello, world!".to_string()); + + assert_eq!(value, string("Hello, world!")); +} + +#[test] +fn anonymous_struct_value_with_one_field_no_trailing_comma() { + let value = value!({ name: "Alice" }); + + assert_eq!(value, struct_with_fields([field("name", string("Alice"))])); +} + +#[test] +fn anonymous_struct_value_with_one_field_of_negative_number() { + let value = value!({ sum: -22_i16 }); + + assert_eq!(value, struct_with_fields([field("sum", int16(-22))])); +} + +#[test] +fn anonymous_struct_value_with_one_field_and_optional_comma() { + let value = value!({ + name: "Alice", + }); + + assert_eq!(value, struct_with_fields([field("name", string("Alice"))])); +} + +#[test] +fn anonymous_struct_value_with_two_fields_without_trailing_comma() { + let value = value!({ name: "Alice", age: 25_u8 }); + + assert_eq!( + value, + struct_with_fields([field("name", string("Alice")), field("age", uint8(25))]) + ); +} + +#[test] +fn anonymous_struct_value_with_two_fields() { + let value = value!({ + name: "Alice", + age: 25_u8, + }); + + assert_eq!( + value, + struct_with_fields([field("name", string("Alice")), field("age", uint8(25))]) + ); +} + +#[test] +fn named_struct_value_with_one_field() { + let value = value!(Foo { name: "Alice" }); + + assert_eq!(value, struct_("Foo", [field("name", string("Alice"))])); +} + +#[test] +fn named_struct_value_with_one_field_of_negative_number() { + let value = value!(Foo { count: -234_i64 }); + + assert_eq!(value, struct_("Foo", [field("count", int64(-234))])); +} + +#[test] +fn named_struct_value_with_two_fields_no_trailing_comma() { + let value = value!(Foo { + name: "Alice", + age: 25_u8 + }); + + assert_eq!( + value, + struct_( + "Foo", + [field("name", string("Alice")), field("age", uint8(25))] + ) + ); +} + +#[test] +fn named_struct_value_with_two_fields_and_trailing_comma() { + let value = value!(Foo { + name: "Alice", + age: 25_u8, + }); + + assert_eq!( + value, + struct_( + "Foo", + [field("name", string("Alice")), field("age", uint8(25))] + ) + ); +} + +#[test] +fn empty_named_struct() { + let value = value!(Foo {}); + + assert_eq!(value, struct_("Foo", Vec::::new())); +} + +#[test] +fn struct_variant_value_with_one_field_no_trailing_comma() { + let value = value!(Foo::Bar { baz: -5.5_f32 }); + + assert_eq!( + value, + struct_variant("Foo", "Bar", [("baz", float32(-5.5))]) + ); +} + +#[test] +fn struct_variant_value_with_two_field_no_trailing_comma() { + let value = value!(Foo::Bar { + baz: -5.5_f32, + qux: "Silvia".to_string() + }); + + assert_eq!( + value, + struct_variant( + "Foo", + "Bar", + [("baz", float32(-5.5)), ("qux", string("Silvia"))] + ) + ); +} + +#[test] +fn struct_variant_value_with_two_fields_and_trailing_comma() { + let value = value!(Foo::Bar { + baz: '@', + qux: "hello, world!".to_string(), + }); + + assert_eq!( + value, + struct_variant( + "Foo", + "Bar", + [("baz", char('@')), ("qux", string("hello, world!"))] + ) + ); +} + +#[test] +fn empty_struct_variant() { + let value = value!(Foo::Bar {}); + + assert_eq!(value, struct_variant("Foo", "Bar", Vec::::new())); +} + +#[test] +fn tuple_struct_value_with_one_field() { + let value = value!(Foo("Silvia")); + + assert_eq!(value, tuple_struct("Foo", [string("Silvia")])); +} + +#[test] +fn tuple_struct_value_with_three_fields_no_trailing_comma() { + let value = value!(Foo("Silvia", true, -2.4_f32)); + + assert_eq!( + value, + tuple_struct("Foo", [string("Silvia"), bool(true), float32(-2.4)]) + ); +} + +#[test] +fn tuple_struct_value_with_three_fields_and_trailing_comma() { + let value = value!(Foo("Silvia", true, -2.4_f32,)); + + assert_eq!( + value, + tuple_struct("Foo", [string("Silvia"), bool(true), float32(-2.4)]) + ); +} + +#[test] +fn empty_tuple_struct() { + let value = value!(Foo()); + + assert_eq!(value, unit_struct("Foo")); +} + +#[test] +fn tuple_value_with_one_field_no_trailing_comma() { + let value = value!(("Alice")); + + assert_eq!(value, tuple([string("Alice")])); +} + +#[test] +fn tuple_value_with_one_field_and_trailing_comma() { + let value = value!(("Alice",)); + + assert_eq!(value, tuple([string("Alice")])); +} + +#[test] +fn tuple_value_with_two_fields() { + let value = value!(("Alice", 25_u8)); + + assert_eq!(value, tuple([string("Alice"), uint8(25)])); +} + +#[test] +fn tuple_value_with_two_fields_and_trailing_comma() { + let value = value!(("Alice", 25_u8,)); + + assert_eq!(value, tuple([string("Alice"), uint8(25)])); +} + +#[test] +fn tuple_value_with_three_fields_and_negative_number() { + let value = value!(("Alice", 1.2_f64, -87_i16)); + + assert_eq!(value, tuple([string("Alice"), float64(1.2), int16(-87)])); +} + +#[test] +fn empty_struct() { + let value = value!({}); + + assert_eq!(value, struct_with_fields::([])); +} + +#[test] +fn empty_tuple() { + let value = value!(()); + + assert_eq!(value, tuple([])); +} + +#[test] +fn unit_value() { + let value = value!(()); + + assert_eq!(value, unit()); +} + +#[test] +fn tuple_variant_value_with_one_field() { + let value = value!(Foo::Bar(2.5_f32)); + + assert_eq!(value, tuple_variant("Foo", "Bar", [float32(2.5)])); +} + +#[test] +fn tuple_variant_value_with_one_field_negative_number() { + let value = value!(Foo::Bar(-2.5_f32)); + + assert_eq!(value, tuple_variant("Foo", "Bar", [float32(-2.5)])); +} + +#[test] +fn tuple_variant_value_with_two_field() { + let value = value!(Foo::Bar("Silvia", 1228_i64)); + + assert_eq!( + value, + tuple_variant("Foo", "Bar", [string("Silvia"), int64(1228)]) + ); +} + +#[test] +fn tuple_variant_value_with_two_field_one_with_negative_number() { + let value = value!(Foo::Bar("Silvia", -1228_i64)); + + assert_eq!( + value, + tuple_variant("Foo", "Bar", [string("Silvia"), int64(-1228)]) + ); +} + +#[test] +fn unit_variant_value() { + let value = value!(Foo::Bar); + + assert_eq!(value, unit_variant("Foo", "Bar")); +} + +#[test] +fn unit_struct_value() { + let value = value!(Foo); + + assert_eq!(value, unit_struct("Foo")); +} + +#[test] +fn empty_seq_value() { + let value = value!([]); + + assert_eq!(value, seq([])); +} + +#[test] +fn seq_value_with_one_element() { + let value = value!([25_u32]); + + assert_eq!(value, seq([uint32(25)])); +} + +#[test] +fn seq_value_with_one_element_and_trailing_comma() { + let value = value!(['@',]); + + assert_eq!(value, seq([char('@')])); +} + +#[test] +fn seq_value_with_two_elements_no_trailing_comma() { + let value = value!([25, -32]); + + assert_eq!(value, seq([int32(25), int32(-32)])); +} + +#[test] +fn seq_value_with_two_elements_and_trailing_comma() { + let value = value!([25, -32,]); + + assert_eq!(value, seq([int32(25), int32(-32)])); +} + +#[test] +fn seq_value_with_three_elements() { + let value = value!(["alpha", "beta", "gamma"]); + + assert_eq!( + value, + seq([string("alpha"), string("beta"), string("gamma")]) + ); +} + +#[test] +fn seq_value_with_expression() { + let value = value!([25, 3 + 7, 55]); + + assert_eq!(value, seq([int32(25), int32(10), int32(55)])); +} + +#[test] +fn struct_value_with_captured_variable() { + let name = "Alice".to_string(); + + let value = value!({ + name: name, + age: 25_u8, + }); + + assert_eq!( + value, + struct_with_fields([("name", string("Alice")), ("age", uint8(25))]) + ); +} + +#[test] +fn anonymous_struct_with_nested_anonymous_struct_value_and_unit_variant() { + let value = value!({ + name: "Silvia", + gender: Gender::Female, + age: 25_u8, + address: { + street: "123 Main St", + city: "New York", + state: "NY", + zip: 10001_u32, + home: true, + } + }); + + assert_eq!( + value, + struct_with_fields([ + ("name", string("Silvia")), + ("gender", unit_variant("Gender", "Female")), + ("age", uint8(25)), + ( + "address", + struct_with_fields([ + ("street", string("123 Main St")), + ("city", string("New York")), + ("state", string("NY")), + ("zip", uint32(10001)), + ("home", bool(true)), + ]) + ) + ]) + ); +} + +#[test] +fn anonymous_struct_with_nested_unit_variant() { + let value = value!({ + foo: Foo::Bar + }); + + assert_eq!( + value, + struct_with_fields([("foo", unit_variant("Foo", "Bar"))]) + ); +} + +#[test] +fn anonymous_struct_with_nested_struct_variant() { + let value = value!({ + foo: Foo::Bar { + gender: Gender::Male, + count: 3472_u64, + }, + }); + + assert_eq!( + value, + struct_with_fields([( + "foo", + struct_variant( + "Foo", + "Bar", + [ + ("gender", unit_variant("Gender", "Male")), + ("count", uint64(3472)) + ] + ) + )]) + ); +} + +#[test] +fn anonymous_struct_with_nested_seq() { + let value = value!({ + names: [ + "Alice", + "Bob", + "Charlie", + ], + }); + + assert_eq!( + value, + struct_with_fields([( + "names", + seq([string("Alice"), string("Bob"), string("Charlie")]) + )]) + ); +} + +#[test] +fn anonymous_struct_with_nested_tuple() { + let value = value!({ + foo: ("Silvia", false, -2.3_f32), + }); + + assert_eq!( + value, + struct_with_fields([( + "foo", + tuple([string("Silvia"), bool(false), float32(-2.3_f32)]) + )]) + ); +} + +#[test] +fn anonymous_struct_with_nested_named_struct() { + let value = value!({ + foo: Foo { + bar: "xyz", + baz: 42_i16, + }, + qux: "abc" + }); + + assert_eq!( + value, + struct_with_fields([ + ( + "foo", + struct_("Foo", [("bar", string("xyz")), ("baz", int16(42))]) + ), + ("qux", string("abc")) + ]) + ); +} + +#[test] +fn anonymous_struct_with_nested_tuple_struct() { + let value = value!({ + foo: Bar(4.6_f32, 12) + } + ); + + assert_eq!( + value, + struct_with_fields([("foo", tuple_struct("Bar", [float32(4.6), int32(12)]))]) + ); +} + +#[test] +fn anonymous_struct_with_nested_tuple_variant() { + let value = value!({ + foo: Foo::Bar("alpha", 4.6_f32) + }); + + assert_eq!( + value, + struct_with_fields([( + "foo", + tuple_variant("Foo", "Bar", [string("alpha"), float32(4.6)]) + )]) + ); +} + +#[test] +fn anonymous_struct_with_nested_tuple_variant_with_trailing_comma() { + let value = value!({ + bar: Foo::Bar("alpha", 4.6_f32), + baz: Foo::Baz('b', "beta", false), + }); + + assert_eq!( + value, + struct_with_fields([ + ( + "bar", + tuple_variant("Foo", "Bar", [string("alpha"), float32(4.6)]) + ), + ( + "baz", + tuple_variant("Foo", "Baz", [char('b'), string("beta"), bool(false)]) + ) + ]) + ); +} + +#[test] +fn tuple_with_nested_anonymous_struct() { + let value = value!((1.2_f32, { foo: Foo::Bar("alpha") }, 33_u64)); + + assert_eq!( + value, + tuple([ + float32(1.2), + struct_with_fields([("foo", tuple_variant("Foo", "Bar", [string("alpha")]))]), + uint64(33) + ]) + ); +} + +#[test] +fn tuple_with_nested_named_struct() { + let value = value!(( + 'X', + Foo { + bar: "alpha", + baz: 42_i64 + } + )); + + assert_eq!( + value, + tuple([ + char('X'), + struct_("Foo", [("bar", string("alpha")), ("baz", int64(42))]) + ]) + ); +} + +#[test] +fn tuple_with_nested_tuple_struct() { + let value = value!((Foo("alpha", 1.83_f64), -33_i8,)); + + assert_eq!( + value, + tuple([ + tuple_struct("Foo", [string("alpha"), float64(1.83)]), + int8(-33) + ]) + ); +} + +#[test] +fn tuple_with_nested_struct_variant() { + let value = value!(("alpha", Foo::Bar { qux: 2.7_f32 })); + + assert_eq!( + value, + tuple([ + string("alpha"), + struct_variant("Foo", "Bar", [("qux", float32(2.7))]) + ]) + ); +} + +#[test] +fn tuple_with_nested_tuple_variant() { + let value = value!((Bar::Baz('a', "alpha"), "epsilon")); + + assert_eq!( + value, + tuple([ + tuple_variant("Bar", "Baz", [char('a'), string("alpha")]), + string("epsilon") + ]) + ); +} + +#[test] +fn tuple_with_nested_unit_variant() { + let value = value!((Foo::Bar, Baz::Qux)); + + assert_eq!( + value, + tuple([unit_variant("Foo", "Bar"), unit_variant("Baz", "Qux")]) + ); +} + +#[test] +fn tuple_with_nested_seq() { + let value = value!(("alpha", [1.2_f32, 3.4_f32, 5.6_f32])); + + assert_eq!( + value, + tuple([ + string("alpha"), + seq([float32(1.2), float32(3.4), float32(5.6)]), + ]) + ); +} + +#[test] +fn seq_with_nested_anonymous_struct() { + let value = value!([1.2_f32, { foo: Foo::Bar("alpha") }, 33_u64]); + + assert_eq!( + value, + seq([ + float32(1.2), + struct_with_fields([("foo", tuple_variant("Foo", "Bar", [string("alpha")]))]), + uint64(33) + ]) + ); +} + +#[test] +fn seq_with_nested_named_struct() { + let value = value!([ + 'X', + Foo { + bar: "alpha", + baz: 42_i64 + } + ]); + + assert_eq!( + value, + seq([ + char('X'), + struct_("Foo", [("bar", string("alpha")), ("baz", int64(42))]) + ]) + ); +} + +#[test] +fn seq_with_nested_tuple_struct() { + let value = value!([Foo("alpha", 1.83_f64), -33_i8,]); + + assert_eq!( + value, + seq([ + tuple_struct("Foo", [string("alpha"), float64(1.83)]), + int8(-33) + ]) + ); +} + +#[test] +fn seq_with_nested_struct_variant() { + let value = value!(["alpha", Foo::Bar { qux: 2.7_f32 }]); + + assert_eq!( + value, + seq([ + string("alpha"), + struct_variant("Foo", "Bar", [("qux", float32(2.7))]) + ]) + ); +} + +#[test] +fn seq_with_nested_tuple_variant() { + let value = value!([Bar::Baz('a', "alpha"), "epsilon"]); + + assert_eq!( + value, + seq([ + tuple_variant("Bar", "Baz", [char('a'), string("alpha")]), + string("epsilon") + ]) + ); +} + +#[test] +fn seq_with_nested_unit_variant() { + let value = value!([Foo::Bar, Baz::Qux]); + + assert_eq!( + value, + seq([unit_variant("Foo", "Bar"), unit_variant("Baz", "Qux")]) + ); +} + +#[test] +fn seq_with_nested_seq() { + let value = value!([["alpha", "beta", "gamma"], [1.2_f32, 3.4_f32]]); + + assert_eq!( + value, + seq([ + seq([string("alpha"), string("beta"), string("gamma")]), + seq([float32(1.2), float32(3.4)]) + ]) + ); +} + +#[test] +fn empty_map_value() { + let value = value!(#{}); + + assert_eq!(value, map([])); +} + +#[test] +fn map_value_with_one_association_no_trailing_comma() { + let value = value!(#{'a' => "alpha"}); + + assert_eq!(value, map([(char('a'), string("alpha"))])); +} + +#[test] +fn map_value_with_one_association_and_trailing_comma() { + let value = value!(#{ + 'a' => "alpha", + }); + + assert_eq!(value, map([(char('a'), string("alpha"))])); +} + +#[test] +fn map_value_with_two_associations_no_trailing_comma() { + let value = value!(#{'a' => "alpha", "beta" => -555_i16}); + + assert_eq!( + value, + map([(char('a'), string("alpha")), (string("beta"), int16(-555))]) + ); +} + +#[test] +fn map_value_with_three_associations_and_trailing_comma() { + let value = value!(#{ + "alpha" => 33_u64, + 65_u32 => 'A', + -808 => "beta", + }); + + assert_eq!( + value, + map([ + (string("alpha"), uint64(33)), + (uint32(65), char('A')), + (int32(-808), string("beta")), + ]) + ); +} + +#[test] +fn map_with_anonymous_struct_as_key() { + let value = value!(#{ + { + foo: Foo::Bar, + bar: "alpha", + } => 33_u64, + }); + + assert_eq!( + value, + map([( + struct_with_fields([ + ("foo", unit_variant("Foo", "Bar")), + ("bar", string("alpha")) + ]), + uint64(33) + )]) + ); +} + +#[test] +fn map_with_named_struct_as_key() { + let value = value!(#{ + Qux { + foo: Foo::Bar, + bar: "alpha", + } => 33_u64, + }); + + assert_eq!( + value, + map([( + struct_( + "Qux", + [ + ("foo", unit_variant("Foo", "Bar")), + ("bar", string("alpha")) + ] + ), + uint64(33) + )]) + ); +} + +#[test] +fn map_with_tuple_as_key() { + let value = value!(#{ + ('a', "alpha") => true, + }); + + assert_eq!( + value, + map([(tuple([char('a'), string("alpha")]), bool(true))]) + ); +} + +#[test] +fn map_with_tuple_variant_as_key() { + let value = value!(#{ + Foo::Bar('a', -2.5_f64) => false, + }); + + assert_eq!( + value, + map([( + tuple_variant("Foo", "Bar", [char('a'), float64(-2.5)]), + bool(false) + )]) + ); +} + +#[test] +fn map_with_struct_variant_as_key() { + let value = value!(#{ + Foo::Bar { + baz: ('a', "alpha"), + qux: Sample::One, + } => 'X', + }); + + assert_eq!( + value, + map([( + struct_variant( + "Foo", + "Bar", + [ + ("baz", tuple([char('a'), string("alpha")])), + ("qux", unit_variant("Sample", "One")) + ] + ), + char('X') + )]) + ); +} + +#[test] +fn map_with_unit_variant_as_key() { + let value = value!(#{ + Foo::Bar => -32.98_f64, + }); + + assert_eq!(value, map([(unit_variant("Foo", "Bar"), float64(-32.98))])); +} + +#[test] +fn map_with_anonymous_struct_as_value() { + let value = value!(#{ + 33_u64 => + { + foo: Foo::Bar, + bar: "alpha", + } + }); + + assert_eq!( + value, + map([( + uint64(33), + struct_with_fields([ + ("foo", unit_variant("Foo", "Bar")), + ("bar", string("alpha")) + ]) + )]) + ); +} + +#[test] +fn map_with_named_struct_as_value() { + let value = value!(#{ + -32_i16 => + Qux { + foo: Foo::Bar, + bar: "alpha", + }, + }); + + assert_eq!( + value, + map([( + int16(-32), + struct_( + "Qux", + [ + ("foo", unit_variant("Foo", "Bar")), + ("bar", string("alpha")) + ] + ) + )]) + ); +} + +#[test] +fn map_with_tuple_as_value() { + let value = value!(#{ + true => ('a', "alpha"), + }); + + assert_eq!( + value, + map([(bool(true), tuple([char('a'), string("alpha")]))]) + ); +} + +#[test] +fn map_with_tuple_variant_as_value() { + let value = value!(#{ + false => Foo::Bar('a', -2.5_f64) + }); + + assert_eq!( + value, + map([( + bool(false), + tuple_variant("Foo", "Bar", [char('a'), float64(-2.5)]) + )]) + ); +} + +#[test] +fn map_with_struct_variant_as_value() { + let value = value!(#{ + 'X' => + Foo::Bar { + baz: ('a', "alpha"), + qux: Sample::One, + }, + }); + + assert_eq!( + value, + map([( + char('X'), + struct_variant( + "Foo", + "Bar", + [ + ("baz", tuple([char('a'), string("alpha")])), + ("qux", unit_variant("Sample", "One")) + ] + ) + )]) + ); +} + +#[test] +fn map_with_unit_variant_as_value() { + let value = value!(#{ + -32.98_f64 => Foo::Bar, + }); + + assert_eq!(value, map([(float64(-32.98), unit_variant("Foo", "Bar"))])); +} + +#[test] +fn map_with_another_map_as_value() { + let value = value!(#{ + "alpha" => #{ + 'a' => 1_u32, + 'b' => 2_u32, + 'c' => 3_u32, + }, + "beta" => #{ + 'd' => 4_u32, + 'e' => 5_u32, + 'f' => 6_u32, + } + }); + + assert_eq!( + value, + map([ + ( + string("alpha"), + map([ + (char('a'), uint32(1)), + (char('b'), uint32(2)), + (char('c'), uint32(3)) + ]) + ), + ( + string("beta"), + map([ + (char('d'), uint32(4)), + (char('e'), uint32(5)), + (char('f'), uint32(6)) + ]) + ), + ]) + ); +} + +#[test] +fn seq_value_with_nested_map() { + let value = value!([ + #{ + "alpha" => [1_u32, 2_u32, 3_u32], + "beta" => [4_u32, 5_u32, 6_u32], + }, + #{ + "gamma" => [7_u32, 8_u32, 9_u32], + "delta" => [10_u32, 11_u32, 12_u32], + }]); + + assert_eq!( + value, + seq([ + map([ + (string("alpha"), seq([uint32(1), uint32(2), uint32(3)])), + (string("beta"), seq([uint32(4), uint32(5), uint32(6)])), + ]), + map([ + (string("gamma"), seq([uint32(7), uint32(8), uint32(9)])), + (string("delta"), seq([uint32(10), uint32(11), uint32(12)])), + ]) + ]) + ); +} + +#[test] +fn tuple_value_with_nested_map() { + let value = value!(( + -123_i32, + #{ + "alpha" => [1_u32, 2_u32, 3_u32], + "beta" => [4_u32, 5_u32, 6_u32], + }, + true, + #{ + "gamma" => [7_u32, 8_u32, 9_u32], + "delta" => [10_u32, 11_u32, 12_u32], + })); + + assert_eq!( + value, + tuple([ + int32(-123), + map([ + (string("alpha"), seq([uint32(1), uint32(2), uint32(3)])), + (string("beta"), seq([uint32(4), uint32(5), uint32(6)])), + ]), + bool(true), + map([ + (string("gamma"), seq([uint32(7), uint32(8), uint32(9)])), + (string("delta"), seq([uint32(10), uint32(11), uint32(12)])), + ]) + ]) + ); +} diff --git a/src/recursive_comparison/mod.rs b/src/recursive_comparison/mod.rs new file mode 100644 index 0000000..0c21aee --- /dev/null +++ b/src/recursive_comparison/mod.rs @@ -0,0 +1,725 @@ +//! Field-by-field recursive comparison of graphs of structs, enums, and tuples. +//! +//! Any type that implements [`serde::Serialize`] can be compared recursively. +//! This is useful for comparing graphs of structs, enums, and tuples. +//! The type to be compared does not need to implement `PartialEq` and `Debug` +//! or any other trait (besides [`serde::Serialize`]). +//! +//! There are several scenarios where recursive comparison is useful: +//! +//! * comparing types that have similar fields, like an entity and a DTO +//! representation of the same thing in the application's domain. +//! * comparing only fields that are relevant for a specific test case and +//! ignoring others. +//! * comparing types field-by-field but ignoring fields, where the actual value +//! may vary like for IDs or timestamps. +//! * comparing types that implement `Serialize` but not `PartialEq` or `Debug` +//! +//! Recursive comparison provides detailed failure reports in case of a failing +//! assertion. The failure details contain a list of fields, for which the +//! actual value is not equal to the expected one. This is another reason why +//! recursive comparison might be the preferred way, especially when comparing +//! structs that have many fields and/or contain nested structs. +//! +//! The recursive comparison mode starts after calling the +//! `using_recursive_comparison` method. +//! +//! Recursive comparison is not symmetrical since it is limited to the fields +//! of the subject (actual value). It gathers the actual fields of the subject +//! and compares them to the corresponding fields having the same name in the +//! expected value. +//! +//! Structs, enums, and tuples in the subject and expected value do not +//! have to be of the exact same type. They are compared field-by-field. As +//! long as the actual and expected fields have the same name and value, they +//! are considered equal. Though primitive types like char, integer, float, and +//! bool have to be of the same type. For example, an actual field of an `u8` +//! value is only equal to the expected field if the names and values are equal +//! and the expected value is of type `u8` too. +//! +//! Recursive comparison is limited down to a max depth of 128 levels, +//! which is the default max depth of [`serde::Serialize`]. +//! +//! The recursive comparison mode provides the following capabilities: +//! +//! * [Ignoring fields in the comparison](#ignoring-some-fields) +//! * [Comparing only specified fields](RecursiveComparison::comparing_only_fields) +//! * [Ignoring all fields that are not present in the expected value](#ignoring-not-expected-fields) +//! * [Comparing only relevant fields](#comparing-only-relevant-fields) +//! +//! # Examples +//! +//! ## Comparing structs with several fields and nested structs +//! +//! The following example shows how to compare two structs. The structs are +//! compared field-by-field recursively. The actual and the expected value can +//! be of the same struct type or different struct types. By default, the +//! expected struct must have at least all the fields of the actual struct but +//! can have more fields not present in the actual struct. +//! +//! ``` +//! use asserting::prelude::*; +//! use serde::Serialize; +//! +//! #[derive(Serialize)] +//! struct Email { +//! purpose: String, +//! address: String, +//! } +//! +//! #[derive(Serialize)] +//! struct Person { +//! name: String, +//! email: Vec, +//! age: u8, +//! } +//! +//! let person = Person { +//! name: "Silvia".into(), +//! email: vec![ +//! Email { +//! purpose: "main".into(), +//! address: "silvia@domain.com".into(), +//! }, +//! Email { +//! purpose: "private".into(), +//! address: "silvia@mail.com".into(), +//! }, +//! ], +//! age: 25, +//! }; +//! +//! assert_that!(person) +//! .using_recursive_comparison() +//! .is_equal_to(Person { +//! name: "Silvia".into(), +//! email: vec![ +//! Email { +//! purpose: "main".into(), +//! address: "silvia@domain.com".into(), +//! }, +//! Email { +//! purpose: "private".into(), +//! address: "silvia@mail.com".into(), +//! }, +//! ], +//! age: 25, +//! }); +//! ``` +//! +//! The field-by-field recursive comparison is started by calling the +//! `using_recursive_comparison` method. +//! +//! ## Ignoring some fields +//! +//! We can ignore some fields of the subject, which will be excluded from the +//! field-by-field recursive comparison. To do so, we add the names of the fields +//! that shall be ignored to the configuration of the recursive comparison using +//! either the `ignoring_field` method, which adds one field at a time, or the +//! `ignoring_fields` methods which adds multiple fields at once. +//! +//! ``` +//! use asserting::prelude::*; +//! use serde::Serialize; +//! +//! #[derive(Serialize)] +//! struct Address { +//! street: String, +//! city: String, +//! zip: u16, +//! } +//! +//! #[derive(Serialize)] +//! struct Person { +//! name: String, +//! age: u8, +//! address: Address, +//! } +//! +//! let person = Person { +//! name: "Silvia".into(), +//! age: 25, +//! address: Address { +//! street: "Second Street".into(), +//! city: "New York".into(), +//! zip: 12345, +//! } +//! }; +//! +//! assert_that!(&person) +//! .using_recursive_comparison() +//! .ignoring_fields(["age", "address.street"]) +//! .is_equal_to(Person { +//! name: "Silvia".into(), +//! age: 27, +//! address: Address { +//! street: "Main Street".into(), +//! city: "New York".into(), +//! zip: 12345, +//! } +//! }); +//! +//! assert_that!(person) +//! .using_recursive_comparison() +//! .ignoring_field("age") +//! .ignoring_field("address.street") +//! .is_equal_to(Person { +//! name: "Silvia".into(), +//! age: 27, +//! address: Address { +//! street: "Main Street".into(), +//! city: "New York".into(), +//! zip: 12345, +//! } +//! }); +//! ``` +//! +//! Once a field is ignored, its subfields are ignored as well. In the following +//! example the assertion succeeds because the `address` field is ignored and +//! therefore also the fields `street`, `city`, and `zip` are ignored. +//! +//! ``` +//! use asserting::prelude::*; +//! use serde::Serialize; +//! # +//! # #[derive(Serialize)] +//! # struct Address { +//! # street: String, +//! # city: String, +//! # zip: u16, +//! # } +//! # +//! # #[derive(Serialize)] +//! # struct Person { +//! # name: String, +//! # age: u8, +//! # address: Address, +//! # } +//! +//! let person = Person { +//! name: "Silvia".into(), +//! age: 25, +//! address: Address { +//! street: "Second Street".into(), +//! city: "Chicago".into(), +//! zip: 33333, +//! } +//! }; +//! +//! assert_that!(person) +//! .using_recursive_comparison() +//! .ignoring_field("address") +//! .is_equal_to(Person { +//! name: "Silvia".into(), +//! age: 25, +//! address: Address { +//! street: "Main Street".into(), +//! city: "New York".into(), +//! zip: 12345, +//! } +//! }); +//! ``` +//! +//! ## Ignoring not expected fields +//! +//! With field-by-field recursive comparison, it is possible to compare similar +//! structs that share most of their fields but not all, like a domain object, +//! an entity, and a DTO of the same thing. +//! +//! ``` +//! use asserting::prelude::*; +//! use serde::Serialize; +//! +//! #[derive(Serialize)] +//! struct PersonEntity { +//! id: u64, +//! name: String, +//! age: u8, +//! } +//! +//! #[derive(Serialize)] +//! struct PersonDto { +//! name: String, +//! age: u8, +//! } +//! +//! let person = PersonEntity { +//! id: 123, +//! name: "Silvia".into(), +//! age: 25, +//! }; +//! +//! assert_that!(person) +//! .using_recursive_comparison() +//! .ignoring_not_expected_fields() +//! .is_equal_to(PersonDto { +//! name: "Silvia".into(), +//! age: 25, +//! }); +//! ``` +//! +//! Here we are comparing the subject of type `PersonEntity` with a `PersonDto`. +//! In contrast to the `PersonEntity` the `PersonDto` does not have an `id` +//! field. So we ignore it by using the `ignoring_not_expected_fields` +//! option. +//! +//! ## Comparing only relevant fields +//! +//! In real-world applications, we often have complex types. We might want to +//! write several tests where only some fields or parts of the complex type are +//! of interest. In this case, it can be annoying having to specify all fields +//! of a struct for the expected value. Declaring an additional struct having +//! only the relevant fields is additional boilerplate code. +//! +//! The solution is the [`value!`] macro. It allows us to construct expected +//! values with only the relevant fields. The [`value!`] macro is a convenient +//! way to construct a [`Value`] that serves as the expected value without +//! having to declare a custom type beforehand. +//! +//! If the expected value shall be a [`Value`], we use the `is_equivalent_to` +//! assertion method instead of `is_equal_to`. +//! +//! ``` +//! use asserting::prelude::*; +//! use serde::Serialize; +//! +//! #[derive(Serialize)] +//! struct Person { +//! id: u64, +//! name: String, +//! age: u8, +//! email: Vec, +//! } +//! +//! let person = Person { +//! id: 456, +//! name: "Silvia".into(), +//! age: 25, +//! email: vec!["silvia@domain.com".to_string()], +//! }; +//! +//! assert_that!(person) +//! .using_recursive_comparison() +//! .ignoring_not_expected_fields() +//! .is_equivalent_to(value!({ +//! name: "Silvia", +//! age: 25_u8, +//! })); +//! ``` +//! +//! [`value!`]: crate::prelude::value +//! [`serde::Serialize`]: Serialize + +pub mod path; +pub mod serialize; +pub mod value; + +mod macros; + +use crate::assertions::{AssertEquality, AssertEquivalence}; +use crate::colored::mark_diff_str; +use crate::recursive_comparison::path::Path; +use crate::recursive_comparison::serialize::to_recursive_value; +use crate::recursive_comparison::value::Value; +use crate::spec::{ + AssertFailure, CollectFailures, DiffFormat, DoFail, FailingStrategy, GetFailures, SoftPanic, + Spec, +}; +use crate::std::fmt::{self, Display}; +use crate::std::string::{String, ToString}; +use crate::std::vec::Vec; +use crate::std::{format, vec}; +use serde_core::Serialize; + +/// Data of an actual assertion in field-by-field recursive comparison mode. +/// +/// It wraps a [`Spec`] and holds additional options for the field-by-field +/// recursive comparison, such as which fields to compare and which to ignore. +/// +/// See the [module documentation](crate::recursive_comparison) for details +/// about field-by-field recursive comparison. +pub struct RecursiveComparison<'a, S, R> { + spec: Spec<'a, S, R>, + compared_fields: Vec>, + ignored_fields: Vec>, + ignore_not_expected_fields: bool, +} + +impl GetFailures for RecursiveComparison<'_, S, R> { + fn has_failures(&self) -> bool { + self.spec.has_failures() + } + + fn failures(&self) -> Vec { + self.spec.failures() + } + + fn display_failures(&self) -> Vec { + self.spec.display_failures() + } +} + +impl DoFail for RecursiveComparison<'_, S, R> +where + R: FailingStrategy, +{ + fn do_fail_with(&mut self, failures: impl IntoIterator) { + self.spec.do_fail_with(failures); + } + + fn do_fail_with_message(&mut self, message: impl Into) { + self.spec.do_fail_with_message(message); + } +} + +impl SoftPanic for RecursiveComparison<'_, S, CollectFailures> { + fn soft_panic(&self) { + self.spec.soft_panic(); + } +} + +impl<'a, S, R> RecursiveComparison<'a, S, R> { + pub(crate) fn new(spec: Spec<'a, S, R>) -> Self { + Self { + spec, + compared_fields: vec![], + ignored_fields: vec![], + ignore_not_expected_fields: false, + } + } + + /// Adds one field that shall be compared in a field-by-field recursive + /// comparison. + /// + /// This method can be called multiple times to add several paths to the + /// list of paths to be compared. Each call of this method adds the given + /// field-path to the list of compared paths. + /// + /// Fields are addressed by their path. To learn how to specify a path and + /// its syntax, see the documentation of the [`Path`] struct. + /// + /// If the same path is added to the list of ignored paths, this path is + /// effectively ignored. Ignored paths take precedence over compared ones. + #[must_use = "the returned `RecursiveComparison` does nothing unless an assertion method like `is_equal_to` is called"] + pub fn comparing_only_field(mut self, field_path: impl Into>) -> Self { + self.compared_fields.push(field_path.into()); + self + } + + /// Adds multiple fields to the list of fields to be compared in a + /// field-by-field recursive comparison. + /// + /// This method can be called multiple times. Each call of this method + /// extends the list of compared fields with the given paths. + /// + /// Fields are addressed by their path. To learn how to specify a path and + /// its syntax, see the documentation of the [`Path`] struct. + /// + /// If the same path is added to the list of ignored paths, this path is + /// effectively ignored. Ignored paths take precedence over compared ones. + #[must_use = "the returned `RecursiveComparison` does nothing unless an assertion method like `is_equal_to` is called"] + pub fn comparing_only_fields

( + mut self, + list_of_field_path: impl IntoIterator, + ) -> Self + where + P: Into>, + { + self.compared_fields + .extend(list_of_field_path.into_iter().map(Into::into)); + self + } + + /// Adds one field that shall be ignored in a field-by-field recursive + /// comparison. + /// + /// This method can be called multiple times to add several paths to the + /// list of paths to be ignored. Each call of this method adds the given + /// field-path to the list of ignored paths. + /// + /// Fields are addressed by their path. To learn how to specify a path and + /// its syntax, see the documentation of the [`Path`] struct. + /// + /// If the same path is added to the list of compared paths, this path is + /// effectively ignored. Ignored paths take precedence over compared paths. + #[must_use = "the returned `RecursiveComparison` does nothing unless an assertion method like `is_equal_to` is called"] + pub fn ignoring_field(mut self, field_path: impl Into>) -> Self { + self.ignored_fields.push(field_path.into()); + self + } + + /// Adds multiple fields to the list of fields to be ignored in a + /// field-by-field recursive comparison. + /// + /// This method can be called multiple times. Each call of this method + /// extends the list of ignored fields with the given paths. + /// + /// Fields are addressed by their path. To learn how to specify a path and + /// its syntax, see the documentation of the [`Path`] struct. + /// + /// If the same path is added to the list of compared paths, this path is + /// effectively ignored. Ignored paths take precedence over compared paths. + #[must_use = "the returned `RecursiveComparison` does nothing unless an assertion method like `is_equal_to` is called"] + pub fn ignoring_fields

(mut self, list_of_field_path: impl IntoIterator) -> Self + where + P: Into>, + { + self.ignored_fields + .extend(list_of_field_path.into_iter().map(Into::into)); + self + } + + /// Specifies that the recursive comparison shall ignore fields that are + /// not present in the expected value. + /// + /// By default, the recursive comparison tries to compare all fields of the + /// actual value (subject). If a field of the actual value is not present in + /// the expected value, the assertion fails. + /// + /// With this option, we can tell the recursive comparison to ignore fields + /// that are not present in the expected value. This is useful when not all + /// fields are relevant to be compared for a specific test case. + #[must_use = "the returned `RecursiveComparison` does nothing unless an assertion method like `is_equal_to` is called"] + pub fn ignoring_not_expected_fields(mut self) -> Self { + self.ignore_not_expected_fields = true; + self + } + + fn compare<'b>(&self, actual: &'b Value, expected: &'b Value) -> ComparisonResult<'b> { + let mut ignored = Vec::new(); + let mut not_expected = Vec::new(); + let mut non_equal = Vec::new(); + + for (actual_path, actual_value) in actual.depth_first_iter() { + if self + .ignored_fields + .iter() + .any(|ignored| actual_path.starts_with(ignored)) + || (!self.compared_fields.is_empty() + && !self + .compared_fields + .iter() + .any(|compared| actual_path.starts_with(compared))) + { + ignored.push(actual_path); + continue; + } + if let Some(expected_value) = expected.get_path(&actual_path) { + if actual_value != expected_value { + non_equal.push(NonEqual { + path: actual_path, + actual_value, + expected_value, + }); + } + } else if self.ignore_not_expected_fields { + ignored.push(actual_path); + } else { + not_expected.push(NotExpected { + path: actual_path, + value: actual_value, + }); + } + } + + ComparisonResult { + ignored, + non_equal, + not_expected, + } + } +} + +struct NotExpected<'a> { + path: Path<'a>, + value: &'a Value, +} + +impl Display for NotExpected<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let NotExpected { path, value } = self; + write!(f, "{path}: {value:?}") + } +} + +struct NonEqual<'a> { + path: Path<'a>, + actual_value: &'a Value, + expected_value: &'a Value, +} + +struct ComparisonResult<'a> { + ignored: Vec>, + non_equal: Vec>, + not_expected: Vec>, +} + +impl ComparisonResult<'_> { + fn has_failure(&self) -> bool { + !self.non_equal.is_empty() || !self.not_expected.is_empty() + } +} + +fn display_non_equal(non_equal: &NonEqual<'_>, diff_format: &DiffFormat) -> String { + let NonEqual { + path, + actual_value, + expected_value, + } = non_equal; + let mut display_details = String::new(); + let debug_actual = format!("{actual_value:?}"); + let debug_expected = format!("{expected_value:?}"); + display_details.push_str(&path.to_string()); + if debug_actual == debug_expected { + let (marked_actual_type, marked_expected_type) = mark_diff_str( + &actual_value.type_name(), + &expected_value.type_name(), + diff_format, + ); + display_details.push_str(": value <"); + display_details.push_str(&debug_actual); + display_details.push_str("> was equal, but type was <"); + display_details.push_str(&marked_actual_type); + display_details.push_str("> and expected type is <"); + display_details.push_str(&marked_expected_type); + } else { + let (marked_actual, marked_expected) = + mark_diff_str(&debug_actual, &debug_expected, diff_format); + display_details.push_str(": expected <"); + display_details.push_str(&marked_expected); + display_details.push_str("> but was <"); + display_details.push_str(&marked_actual); + } + display_details.push('>'); + display_details +} + +fn display_compare_details(compared: &ComparisonResult<'_>, diff_format: &DiffFormat) -> String { + let mut display_details = String::new(); + if !compared.non_equal.is_empty() { + display_details.push_str("\n non equal fields:\n"); + for a_non_equal in &compared.non_equal { + display_details.push_str(" "); + display_details.push_str(&display_non_equal(a_non_equal, diff_format)); + display_details.push('\n'); + } + } + if !compared.not_expected.is_empty() { + display_details.push_str("\n the following fields were not expected:\n"); + for a_not_expected in &compared.not_expected { + display_details.push_str(" "); + display_details.push_str(&a_not_expected.to_string()); + display_details.push('\n'); + } + } + if !compared.ignored.is_empty() { + display_details.push_str("\n the following fields were ignored:\n"); + for an_ignored in &compared.ignored { + display_details.push_str(" "); + display_details.push_str(&an_ignored.to_string()); + display_details.push('\n'); + } + } + display_details +} + +impl AssertEquality for RecursiveComparison<'_, S, R> +where + S: Serialize, + E: Serialize, + R: FailingStrategy, +{ + fn is_equal_to(mut self, expected: E) -> Self { + let expression = self.spec.expression(); + let actual = to_recursive_value(self.spec.subject()) + .unwrap_or_else(|err| panic!("failed to serialize the subject, reason: {err}")); + let expected = to_recursive_value(&expected) + .unwrap_or_else(|err| panic!("failed to serialize the expected value, reason: {err}")); + + let compared = self.compare(&actual, &expected); + + if compared.has_failure() { + let compare_details = display_compare_details(&compared, self.spec.diff_format()); + + self.do_fail_with_message(format!( + r"expected {expression} to be equal to {expected:?} (using recursive comparison) + but was: {actual:?} + expected: {expected:?} +{compare_details}" + )); + } + self + } + + fn is_not_equal_to(mut self, expected: E) -> Self { + let expression = self.spec.expression(); + let actual = to_recursive_value(self.spec.subject()) + .unwrap_or_else(|err| panic!("failed to serialize the subject, reason: {err}")); + let expected = to_recursive_value(&expected) + .unwrap_or_else(|err| panic!("failed to serialize the expected value, reason: {err}")); + + let compared = self.compare(&actual, &expected); + + if !compared.has_failure() { + let compare_details = display_compare_details(&compared, self.spec.diff_format()); + + self.do_fail_with_message(format!( + r"expected {expression} to be not equal to {expected:?} (using recursive comparison) + but was: {actual:?} + expected: {expected:?} +{compare_details}" + )); + } + self + } +} + +impl AssertEquivalence for RecursiveComparison<'_, S, R> +where + S: Serialize, + R: FailingStrategy, +{ + fn is_equivalent_to(mut self, expected: Value) -> Self { + let expression = self.spec.expression(); + let actual = to_recursive_value(self.spec.subject()) + .unwrap_or_else(|err| panic!("failed to serialize the subject, reason: {err}")); + + let compared = self.compare(&actual, &expected); + + if compared.has_failure() { + let compare_details = display_compare_details(&compared, self.spec.diff_format()); + + self.do_fail_with_message(format!( + r"expected {expression} to be equivalent to {expected:?} (using recursive comparison) + but was: {actual:?} + expected: {expected:?} +{compare_details}" + )); + } + self + } + + fn is_not_equivalent_to(mut self, expected: Value) -> Self { + let expression = self.spec.expression(); + let actual = to_recursive_value(self.spec.subject()) + .unwrap_or_else(|err| panic!("failed to serialize the subject, reason: {err}")); + + self.ignore_not_expected_fields = true; + let compared = self.compare(&actual, &expected); + + if !compared.has_failure() { + let compare_details = display_compare_details(&compared, self.spec.diff_format()); + + self.do_fail_with_message(format!( + r"expected {expression} to be not equivalent to {expected:?} (using recursive comparison) + but was: {actual:?} + expected: {expected:?} +{compare_details}" + )); + } + self + } +} + +#[cfg(test)] +mod tests; diff --git a/src/recursive_comparison/path/mod.rs b/src/recursive_comparison/path/mod.rs new file mode 100644 index 0000000..db4c9a4 --- /dev/null +++ b/src/recursive_comparison/path/mod.rs @@ -0,0 +1,183 @@ +//! Defines the [`Path`] type that addresses a field in a [`Value`]. +//! +//! [`Value`]: crate::recursive_comparison::value::Value + +use crate::std::borrow::Cow; +use crate::std::borrow::ToOwned; +use crate::std::fmt; +use crate::std::fmt::{Debug, Display}; +use crate::std::string::String; +use crate::std::vec; +use crate::std::vec::Vec; + +/// Defines a path to a field in a struct, tuple, or enum variant. +/// +/// In the text representation the path to a field contains the navigation from +/// field to field. The fields are separated by a dot (`'.'`). The last field in +/// the path is the target field. A leading or trailing dot has no meaning and +/// is ignored. +/// +/// Examples for paths addressing a field: +/// +/// * `"name"` - addresses the field `name` on the first level of a struct +/// * `"address.zip"` - addresses the field `zip` of the embedded struct behind field `address` +/// +/// The values of a tuple can be addressed by specifying the index of a value +/// inside the tuple. The index is zero-based, with an index 0 addressing the +/// first value in the tuple. +/// +/// Examples for paths addressing single values in a tuple: +/// +/// * `"path.to.tuple.0"` - for the first value in the tuple +/// * `"path.to.tuple.1"` - for the second value in the tuple +/// +/// A path can also address an item in a sequence by specifying the index into +/// the sequence. The index is zero-based, with an index 0 addressing the first +/// item in a sequence. +/// +/// Examples for paths address items in a sequence: +/// +/// * `"order.items.0"` - for one item in the sequence +/// * `"order.items.1.product_id"` - for a field of the second item in the sequence +/// +/// # Usage +/// +/// A [`Path`] can be contructed from a string by using the [`Path::new`] +/// method, or by converting a string into a [`Path`]. +/// +/// Examples: +/// +/// ``` +/// # use asserting::recursive_comparison::path::Path; +/// let path1 = Path::new("path.to.field"); +/// let path2 = Path::from("path.to.field"); +/// +/// assert_eq!(path1, path2); +/// ``` +/// +/// Another way to construct a [`Path`] is to start with an empty path and then +/// append segments as needed using the [`Path::append`] method. The segments +/// given to the `append` method should not contain any dot (`'.'`) as every given +/// string is treated as one segment. The given segments are not parsed for +/// dots. +/// +/// Example: +/// +/// ``` +/// # use asserting::recursive_comparison::path::Path; +/// let path = Path::empty() +/// .append("order") +/// .append("items") +/// .append("0") +/// .append("product_id"); +/// +/// assert_eq!(path, Path::from("order.items.0.product_id")) +/// ``` +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct Path<'a>(Vec>); + +impl<'a> Path<'a> { + /// The separator used to separate segments in a path. + pub const SEPARATOR: char = '.'; + + /// Creates a new [`Path`] from a string. + /// + /// The string is parsed for dots (`'.'`) to get the separate segments of + /// the path. A leading or trailing dot has no meaning and is ignored. + pub fn new(field_path: &'a str) -> Self { + Self::from(field_path) + } + + /// Creates an empty [`Path`]. + pub fn empty() -> Self { + Self(vec![]) + } + + /// Returns a slice of the segments of this path. + pub fn segments(&self) -> &[Cow<'a, str>] { + &self.0 + } + + /// Appends a new segment to this path and returns the resulting path as a + /// new [`Path`]. + /// + /// The [`Path`] is an immutable data type. + #[must_use = "Path is immutable, so append returns a new Path with the given segment appended"] + pub fn append(&self, segment: impl Into>) -> Self { + let mut path = self.0.clone(); + path.push(segment.into()); + Self(path) + } + + /// Returns `true` if this path starts with the given path. + /// + /// Returns `false` if this path does not start with the given path and if + /// the given path is longer than this path. + /// + /// If the given path is empty, it returns `true` if this path is also + /// empty, and `false` otherwise. + pub fn starts_with(&self, other: &Self) -> bool { + let other_len = other.0.len(); + let self_len = self.0.len(); + if other_len == 0 { + return self_len == 0; + } + self_len >= other_len && self.0[..other_len] == other.0 + } +} + +impl Debug for Path<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_list().entries(self.0.iter()).finish() + } +} + +impl Display for Path<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut first = true; + for field_name in &self.0 { + if first { + first = false; + write!(f, "{field_name}")?; + } else { + write!(f, "{}{field_name}", Self::SEPARATOR)?; + } + } + Ok(()) + } +} + +impl<'a> From<&'a str> for Path<'a> { + fn from(value: &'a str) -> Self { + let field_names = value + .split(Path::SEPARATOR) + .filter_map(|field_name| { + if field_name.is_empty() { + None + } else { + Some(Cow::Borrowed(field_name)) + } + }) + .collect::>(); + Self(field_names) + } +} + +impl From for Path<'_> { + fn from(field_path: String) -> Self { + let field_names = field_path + .split(Path::SEPARATOR) + .filter_map(|field_name| { + if field_name.is_empty() { + None + } else { + Some(Cow::Owned(field_name.to_owned())) + } + }) + .collect::>(); + Path(field_names) + } +} + +#[cfg(test)] +mod tests; diff --git a/src/recursive_comparison/path/tests.rs b/src/recursive_comparison/path/tests.rs new file mode 100644 index 0000000..e6147bc --- /dev/null +++ b/src/recursive_comparison/path/tests.rs @@ -0,0 +1,270 @@ +use super::*; +use crate::std::format; +use crate::std::string::ToString; + +#[test] +fn path_from_empty_str() { + let path = Path::from(""); + + assert!(path.segments().is_empty()); +} + +#[test] +fn path_from_empty_str_is_equal_to_empty_path() { + let path = Path::from(""); + + assert_eq!(path, Path::empty()); +} + +#[test] +fn path_from_str_one_fields_deep() { + let path = Path::from("foo"); + + assert_eq!(path.segments(), &[Cow::Borrowed("foo")]); +} + +#[test] +fn path_from_str_two_fields_deep() { + let path = Path::from("foo.bar"); + + assert_eq!( + path.segments(), + &[Cow::Borrowed("foo"), Cow::Borrowed("bar")] + ); +} + +#[test] +fn path_from_str_three_fields_deep_with_wildcards() { + let path = Path::from("foo.bar.*"); + + assert_eq!( + path.segments(), + &[ + Cow::Borrowed("foo"), + Cow::Borrowed("bar"), + Cow::Borrowed("*") + ] + ); +} + +#[test] +fn path_from_empty_string() { + let path = Path::from(String::new()); + + assert!(path.segments().is_empty()); +} + +#[test] +fn path_from_empty_string_is_equal_to_empty_path() { + let path = Path::from(String::new()); + + assert_eq!(path, Path::empty()); +} + +#[test] +fn path_from_string_one_fields_deep() { + let path = Path::from(String::from("foo")); + + assert_eq!(path.segments(), &[Cow::Borrowed("foo")]); +} + +#[test] +fn path_from_string_two_fields_deep() { + let path = Path::from(String::from("foo.bar")); + + assert_eq!( + path.segments(), + &[Cow::Borrowed("foo"), Cow::Borrowed("bar")] + ); +} + +#[test] +fn path_from_string_three_fields_deep_with_wildcards() { + let path = Path::from(String::from("foo.bar.*")); + + assert_eq!( + path.segments(), + &[ + Cow::Borrowed("foo"), + Cow::Borrowed("bar"), + Cow::Borrowed("*") + ] + ); +} + +#[test] +fn debug_string_of_empty_path() { + let path = Path::new(""); + + let debug_string = format!("{path:?}"); + + assert_eq!(debug_string, "[]"); +} + +#[test] +fn debug_string_of_path_with_one_segment() { + let path = Path::new("foo"); + + let debug_string = format!("{path:?}"); + + assert_eq!(debug_string, r#"["foo"]"#); +} + +#[test] +fn debug_string_of_path_with_three_segments() { + let path = Path::new("foo.bar.qux"); + + let debug_string = format!("{path:?}"); + + assert_eq!(debug_string, r#"["foo", "bar", "qux"]"#); +} + +#[test] +fn display_string_of_empty_path() { + let path = Path::new(""); + + let display_string = path.to_string(); + + assert_eq!(display_string, ""); +} + +#[test] +fn display_string_of_path_with_one_segment() { + let path = Path::new("foo"); + + let display_string = path.to_string(); + + assert_eq!(display_string, "foo"); +} + +#[test] +fn display_string_of_path_with_three_segments() { + let path = Path::new("foo.bar.qux"); + + let display_string = path.to_string(); + + assert_eq!(display_string, "foo.bar.qux"); +} + +#[test] +fn append_field_name_to_empty_path() { + let path = Path::empty(); + + let new_path = path.append("foo"); + + assert_eq!(new_path.segments(), &[Cow::Borrowed("foo")]); + assert!(path.segments().is_empty()); +} + +#[test] +fn append_field_name_to_path_one_field_deep() { + let path = Path::new("foo"); + + let new_path = path.append("bar"); + + assert_eq!( + new_path.segments(), + &[Cow::Borrowed("foo"), Cow::Borrowed("bar")] + ); + assert_eq!(path.segments(), &[Cow::Borrowed("foo")]); +} + +#[test] +fn append_field_name_to_path_two_fields_deep() { + let path = Path::new("foo.bar"); + + let new_path = path.append("qux"); + + assert_eq!( + new_path.segments(), + &[ + Cow::Borrowed("foo"), + Cow::Borrowed("bar"), + Cow::Borrowed("qux") + ] + ); + assert_eq!( + path.segments(), + &[Cow::Borrowed("foo"), Cow::Borrowed("bar")] + ); +} + +#[test] +fn empty_path_start_with_empty_path() { + assert!(Path::from("").starts_with(&Path::empty())); +} + +#[test] +fn non_empty_path_does_not_start_with_empty_path() { + assert!(!Path::from("foo.bar.baz").starts_with(&Path::empty())); +} + +#[test] +fn one_field_path_starts_with_one_field_path() { + assert!(Path::from("foo").starts_with(&Path::from("foo"))); +} + +#[test] +fn one_field_path_does_not_start_with_one_field_path_with_shorter_field_name() { + assert!(!Path::from("foobar").starts_with(&Path::from("foo"))); +} + +#[test] +fn one_field_path_does_not_start_with_one_field_path_with_longer_field_name() { + assert!(!Path::from("foo").starts_with(&Path::from("foobar"))); +} + +#[test] +fn one_field_path_does_not_start_with_two_field_path() { + assert!(!Path::from("foo").starts_with(&Path::from("foo.bar"))); +} + +#[test] +fn two_fields_path_starts_with_two_fields_path() { + assert!(Path::from("foo.bar").starts_with(&Path::from("foo.bar"))); +} + +#[test] +fn two_fields_path_does_not_start_with_two_fields_path_with_shorter_field_name() { + assert!(!Path::from("foo.barx").starts_with(&Path::from("foo.bar"))); +} + +#[test] +fn two_fields_path_does_not_start_with_two_fields_path_with_longer_field_name() { + assert!(!Path::from("foo.bar").starts_with(&Path::from("foo.barx"))); +} + +#[test] +fn two_fields_path_does_not_start_with_three_fields_path() { + assert!(!Path::from("foo.bar").starts_with(&Path::from("foo.bar.baz"))); +} + +#[test] +fn two_fields_path_starts_with_one_field_path() { + assert!(Path::from("foo.bar").starts_with(&Path::from("foo"))); +} + +#[test] +fn three_fields_path_starts_with_three_fields_path() { + assert!(Path::from("foo.bar.baz").starts_with(&Path::from("foo.bar.baz"))); +} + +#[test] +fn three_fields_path_does_not_start_with_three_fields_path_with_shorter_field_name() { + assert!(!Path::from("foo.bar.baz").starts_with(&Path::from("foo.bar.b"))); +} + +#[test] +fn three_fields_path_starts_with_two_fields_path() { + assert!(Path::from("foo.bar.baz").starts_with(&Path::from("foo.bar"))); +} + +#[test] +fn three_fields_path_does_not_start_with_two_fields_path_with_shorter_field_name() { + assert!(!Path::from("foo.bar.baz").starts_with(&Path::from("foo.b"))); +} + +#[test] +fn three_fields_path_starts_with_one_field_path() { + assert!(Path::from("foo.bar.baz").starts_with(&Path::from("foo"))); +} diff --git a/src/recursive_comparison/serialize/mod.rs b/src/recursive_comparison/serialize/mod.rs new file mode 100644 index 0000000..f7048ec --- /dev/null +++ b/src/recursive_comparison/serialize/mod.rs @@ -0,0 +1,508 @@ +//! Serialization of any type to a [`Value`] using `serde`. + +use crate::recursive_comparison::value::{Field, Number}; +use crate::recursive_comparison::value::{Map, Value}; +use crate::std::borrow::Cow; +use crate::std::error::Error as StdError; +use crate::std::fmt::{self, Display}; +use crate::std::string::{String, ToString}; +use crate::std::vec; +use crate::std::vec::Vec; +use serde_core::ser::Error as SerdeError; +use serde_core::{ser, Serialize, Serializer}; + +/// Serializes the given object of some type into a [`Value`]. The given type +/// must implement [`serde::Serialize`]. +/// +/// # Errors +/// +/// This method returns a `Result` as of the API of `serde`. As the given object +/// is serialized into an in-memory representation, there are practically no +/// errors that can occur. +/// +/// [`serde::Serialize`]: Serialize +pub fn to_recursive_value(object: &T) -> Result +where + T: Serialize + ?Sized, +{ + object.serialize(SerializeValue) +} + +/// The error type used when serializing objects to a [`Value`]. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Error { + /// A custom error (as required by [`serde::Serialize`]) + /// + /// [`serde::Serialize`]: Serialize + Message(String), +} + +impl Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Message(message) => write!(f, "{message}"), + } + } +} + +impl StdError for Error {} + +impl SerdeError for Error { + fn custom(msg: T) -> Self + where + T: Display, + { + Self::Message(msg.to_string()) + } +} + +struct SerializeValue; + +impl Serializer for SerializeValue { + type Ok = Value; + type Error = Error; + type SerializeSeq = SerializeSeq; + type SerializeTuple = SerializeTuple; + type SerializeTupleStruct = SerializeTupleStruct; + type SerializeTupleVariant = SerializeTupleVariant; + type SerializeMap = SerializeMap; + type SerializeStruct = SerializeStruct; + type SerializeStructVariant = SerializeStructVariant; + + fn serialize_bool(self, value: bool) -> Result { + Ok(Value::Bool(value)) + } + + fn serialize_i8(self, value: i8) -> Result { + Ok(Value::Number(value.into())) + } + + fn serialize_i16(self, value: i16) -> Result { + Ok(Value::Number(value.into())) + } + + fn serialize_i32(self, value: i32) -> Result { + Ok(Value::Number(value.into())) + } + + fn serialize_i64(self, value: i64) -> Result { + Ok(Value::Number(value.into())) + } + + fn serialize_i128(self, value: i128) -> Result { + Ok(Value::Number(value.into())) + } + + fn serialize_u8(self, value: u8) -> Result { + Ok(Value::Number(value.into())) + } + + fn serialize_u16(self, value: u16) -> Result { + Ok(Value::Number(value.into())) + } + + fn serialize_u32(self, value: u32) -> Result { + Ok(Value::Number(value.into())) + } + + fn serialize_u64(self, value: u64) -> Result { + Ok(Value::Number(value.into())) + } + + fn serialize_u128(self, value: u128) -> Result { + Ok(Value::Number(value.into())) + } + + fn serialize_f32(self, value: f32) -> Result { + Ok(Value::Number(value.into())) + } + + fn serialize_f64(self, value: f64) -> Result { + Ok(Value::Number(value.into())) + } + + fn serialize_char(self, value: char) -> Result { + Ok(Value::Char(value)) + } + + fn serialize_str(self, value: &str) -> Result { + Ok(Value::String(value.into())) + } + + fn serialize_bytes(self, value: &[u8]) -> Result { + Ok(Value::Seq( + value + .iter() + .map(|v| Value::Number(Number::U8(*v))) + .collect(), + )) + } + + fn serialize_none(self) -> Result { + Ok(Value::UnitVariant { + type_name: "Option".into(), + variant: "None".into(), + }) + } + + fn serialize_some(self, value: &T) -> Result + where + T: ?Sized + Serialize, + { + let value = value.serialize(Self)?; + Ok(Value::TupleVariant { + type_name: "Option".into(), + variant: "Some".into(), + values: vec![Field { + name: "0".into(), + value, + }], + }) + } + + fn serialize_unit(self) -> Result { + Ok(Value::Unit) + } + + fn serialize_unit_struct(self, name: &'static str) -> Result { + Ok(Value::Struct { + type_name: name.into(), + fields: Vec::new(), + }) + } + + fn serialize_unit_variant( + self, + name: &'static str, + _variant_index: u32, + variant: &'static str, + ) -> Result { + Ok(Value::UnitVariant { + type_name: name.into(), + variant: variant.into(), + }) + } + + fn serialize_newtype_struct( + self, + name: &'static str, + value: &T, + ) -> Result + where + T: ?Sized + Serialize, + { + Ok(Value::TupleStruct { + type_name: name.into(), + values: vec![Field { + name: "0".into(), + value: value.serialize(Self)?, + }], + }) + } + + fn serialize_newtype_variant( + self, + name: &'static str, + _variant_index: u32, + variant: &'static str, + value: &T, + ) -> Result + where + T: ?Sized + Serialize, + { + Ok(Value::TupleVariant { + type_name: name.into(), + variant: variant.into(), + values: vec![Field { + name: "0".into(), + value: value.serialize(Self)?, + }], + }) + } + + fn serialize_seq(self, len: Option) -> Result { + Ok(SerializeSeq { + elements: len.map(Vec::with_capacity).unwrap_or_default(), + }) + } + + fn serialize_tuple(self, len: usize) -> Result { + Ok(SerializeTuple { + values: Vec::with_capacity(len), + next_index: 0, + }) + } + + fn serialize_tuple_struct( + self, + name: &'static str, + len: usize, + ) -> Result { + Ok(SerializeTupleStruct { + type_name: name.into(), + values: Vec::with_capacity(len), + next_index: 0, + }) + } + + fn serialize_tuple_variant( + self, + name: &'static str, + _variant_index: u32, + variant: &'static str, + len: usize, + ) -> Result { + Ok(SerializeTupleVariant { + type_name: name.into(), + variant: variant.into(), + values: Vec::with_capacity(len), + next_index: 0, + }) + } + + fn serialize_map(self, len: Option) -> Result { + Ok(SerializeMap { + map: len.map(Map::with_capacity).unwrap_or_default(), + next_key: None, + }) + } + + fn serialize_struct( + self, + name: &'static str, + len: usize, + ) -> Result { + Ok(SerializeStruct { + struct_name: name.into(), + fields: Vec::with_capacity(len), + }) + } + + fn serialize_struct_variant( + self, + name: &'static str, + _variant_index: u32, + variant: &'static str, + len: usize, + ) -> Result { + Ok(SerializeStructVariant { + type_name: name.into(), + variant: variant.into(), + values: Vec::with_capacity(len), + }) + } +} + +struct SerializeSeq { + elements: Vec, +} + +impl ser::SerializeSeq for SerializeSeq { + type Ok = Value; + type Error = Error; + + fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> + where + T: ?Sized + Serialize, + { + let value = value.serialize(SerializeValue)?; + self.elements.push(value); + Ok(()) + } + + fn end(self) -> Result { + Ok(Value::Seq(self.elements)) + } +} + +struct SerializeMap { + map: Map, + next_key: Option, +} + +impl ser::SerializeMap for SerializeMap { + type Ok = Value; + type Error = Error; + + fn serialize_key(&mut self, key: &T) -> Result<(), Self::Error> + where + T: ?Sized + Serialize, + { + let key = key.serialize(SerializeValue)?; + self.next_key = Some(key); + Ok(()) + } + + fn serialize_value(&mut self, value: &T) -> Result<(), Self::Error> + where + T: ?Sized + Serialize, + { + let key = self.next_key.take(); + // Panic because this indicates a bug in the program rather than an expected failure. + let key = key.unwrap_or_else(|| panic!("serialize_value called before serialize_key")); + let value = value.serialize(SerializeValue)?; + self.map.insert(key, value); + Ok(()) + } + + fn serialize_entry(&mut self, key: &K, value: &V) -> Result<(), Self::Error> + where + K: ?Sized + Serialize, + V: ?Sized + Serialize, + { + let key = key.serialize(SerializeValue)?; + let value = value.serialize(SerializeValue)?; + self.map.insert(key, value); + Ok(()) + } + + fn end(self) -> Result { + Ok(Value::Map(self.map)) + } +} + +struct SerializeTuple { + values: Vec, + next_index: usize, +} + +impl ser::SerializeTuple for SerializeTuple { + type Ok = Value; + type Error = Error; + + fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> + where + T: ?Sized + Serialize, + { + let name = self.next_index.to_string().into(); + self.next_index += 1; + let value = value.serialize(SerializeValue)?; + self.values.push(Field { name, value }); + Ok(()) + } + + fn end(self) -> Result { + Ok(Value::Tuple(self.values)) + } +} + +struct SerializeStruct { + struct_name: Cow<'static, str>, + fields: Vec, +} + +impl ser::SerializeStruct for SerializeStruct { + type Ok = Value; + type Error = Error; + + fn serialize_field(&mut self, key: &'static str, value: &T) -> Result<(), Self::Error> + where + T: ?Sized + Serialize, + { + let value = value.serialize(SerializeValue)?; + self.fields.push(Field { + name: key.into(), + value, + }); + Ok(()) + } + + fn end(self) -> Result { + Ok(Value::Struct { + type_name: self.struct_name, + fields: self.fields, + }) + } +} + +struct SerializeTupleStruct { + type_name: Cow<'static, str>, + values: Vec, + next_index: usize, +} + +impl ser::SerializeTupleStruct for SerializeTupleStruct { + type Ok = Value; + type Error = Error; + + fn serialize_field(&mut self, value: &T) -> Result<(), Self::Error> + where + T: ?Sized + Serialize, + { + let name = self.next_index.to_string().into(); + self.next_index += 1; + let value = value.serialize(SerializeValue)?; + self.values.push(Field { name, value }); + Ok(()) + } + + fn end(self) -> Result { + Ok(Value::TupleStruct { + type_name: self.type_name, + values: self.values, + }) + } +} + +struct SerializeStructVariant { + type_name: Cow<'static, str>, + variant: Cow<'static, str>, + values: Vec, +} + +impl ser::SerializeStructVariant for SerializeStructVariant { + type Ok = Value; + type Error = Error; + + fn serialize_field(&mut self, key: &'static str, value: &T) -> Result<(), Self::Error> + where + T: ?Sized + Serialize, + { + let name = key.into(); + let value = value.serialize(SerializeValue)?; + self.values.push(Field { name, value }); + Ok(()) + } + + fn end(self) -> Result { + Ok(Value::StructVariant { + type_name: self.type_name, + variant: self.variant, + fields: self.values, + }) + } +} + +struct SerializeTupleVariant { + type_name: Cow<'static, str>, + variant: Cow<'static, str>, + values: Vec, + next_index: usize, +} + +impl ser::SerializeTupleVariant for SerializeTupleVariant { + type Ok = Value; + type Error = Error; + + fn serialize_field(&mut self, value: &T) -> Result<(), Self::Error> + where + T: ?Sized + Serialize, + { + let name = self.next_index.to_string().into(); + self.next_index += 1; + let value = value.serialize(SerializeValue)?; + self.values.push(Field { name, value }); + Ok(()) + } + + fn end(self) -> Result { + Ok(Value::TupleVariant { + type_name: self.type_name, + variant: self.variant, + values: self.values, + }) + } +} + +#[cfg(test)] +mod tests; diff --git a/src/recursive_comparison/serialize/tests.rs b/src/recursive_comparison/serialize/tests.rs new file mode 100644 index 0000000..8d0221e --- /dev/null +++ b/src/recursive_comparison/serialize/tests.rs @@ -0,0 +1,340 @@ +use super::*; +use crate::recursive_comparison::value::{ + self, bool, char, field, float32, float64, int128, int16, int32, int64, int8, isize, map, none, + seq, some, string, struct_, struct_variant, tuple, tuple_struct, tuple_variant, uint128, + uint16, uint32, uint64, uint8, unit, unit_struct, unit_variant, usize, +}; +use crate::std::string::ToString; +use indexmap::IndexMap; +use serde::Serialize; +use serde_bytes::Bytes; + +mod error { + use super::*; + + #[test] + fn can_create_custom_error() { + let error = Error::custom("gubergren eu nonummy"); + + assert_eq!(error, Error::Message("gubergren eu nonummy".into())); + } + + #[test] + fn display_string_of_custom_error() { + let error = Error::custom("gubergren eu nonummy"); + + assert_eq!(error.to_string(), "gubergren eu nonummy"); + } +} + +#[test] +fn serialize_tuple_of_bool() { + let value = (true, false); + + let serialized = to_recursive_value(&value).unwrap_or_else(|err| panic!("{err:?}")); + + assert_eq!(serialized, tuple([bool(true), bool(false)])); +} + +#[test] +fn serialize_int8() { + let serialized = to_recursive_value(&-42_i8).unwrap_or_else(|err| panic!("{err:?}")); + assert_eq!(serialized, int8(-42)); +} + +#[test] +fn serialize_int16() { + let serialized = to_recursive_value(&-16_i16).unwrap_or_else(|err| panic!("{err:?}")); + assert_eq!(serialized, int16(-16)); +} + +#[test] +fn serialize_int32() { + let serialized = to_recursive_value(&-32_i32).unwrap_or_else(|err| panic!("{err:?}")); + assert_eq!(serialized, int32(-32)); +} + +#[test] +fn serialize_int64() { + let serialized = to_recursive_value(&-64_i64).unwrap_or_else(|err| panic!("{err:?}")); + assert_eq!(serialized, int64(-64)); +} + +#[test] +fn serialize_int128() { + let serialized = to_recursive_value(&-128_i128).unwrap_or_else(|err| panic!("{err:?}")); + assert_eq!(serialized, int128(-128)); +} + +#[test] +fn serialize_isize() { + let serialized = to_recursive_value(&-333_isize).unwrap_or_else(|err| panic!("{err:?}")); + assert_eq!(serialized, isize(-333)); +} + +#[test] +fn serialize_uint8() { + let serialized = to_recursive_value(&42_u8).unwrap_or_else(|err| panic!("{err:?}")); + assert_eq!(serialized, uint8(42)); +} + +#[test] +fn serialize_uint16() { + let serialized = to_recursive_value(&16_u16).unwrap_or_else(|err| panic!("{err:?}")); + assert_eq!(serialized, uint16(16)); +} + +#[test] +fn serialize_uint32() { + let serialized = to_recursive_value(&32_u32).unwrap_or_else(|err| panic!("{err:?}")); + assert_eq!(serialized, uint32(32)); +} + +#[test] +fn serialize_uint64() { + let serialized = to_recursive_value(&64_u64).unwrap_or_else(|err| panic!("{err:?}")); + assert_eq!(serialized, uint64(64)); +} + +#[test] +fn serialize_uint128() { + let serialized = to_recursive_value(&128_u128).unwrap_or_else(|err| panic!("{err:?}")); + assert_eq!(serialized, uint128(128)); +} + +#[test] +fn serialize_usize() { + let serialized = to_recursive_value(&555_usize).unwrap_or_else(|err| panic!("{err:?}")); + assert_eq!(serialized, usize(555)); +} + +#[test] +fn serialize_float32() { + let serialized = to_recursive_value(&-0.5_f32).unwrap_or_else(|err| panic!("{err:?}")); + assert_eq!(serialized, float32(-0.5)); +} + +#[test] +fn serialize_float64() { + let serialized = to_recursive_value(&1.2_f64).unwrap_or_else(|err| panic!("{err:?}")); + assert_eq!(serialized, float64(1.2)); +} + +#[test] +fn serialize_char() { + let serialized = to_recursive_value(&'@').unwrap_or_else(|err| panic!("{err:?}")); + assert_eq!(serialized, char('@')); +} + +#[test] +fn serialize_string() { + let serialized = + to_recursive_value(&"hello".to_string()).unwrap_or_else(|err| panic!("{err:?}")); + assert_eq!(serialized, string("hello")); +} + +#[test] +fn serialize_bytes() { + let buffer = Bytes::new(&[65, 66, 67]); + + let serialized = to_recursive_value(&buffer).unwrap_or_else(|err| panic!("{err:?}")); + + assert_eq!(serialized, seq([uint8(65), uint8(66), uint8(67)])); +} + +#[test] +fn serialize_none() { + let maybe: Option = None; + + let serialized = to_recursive_value(&maybe).unwrap_or_else(|err| panic!("{err:?}")); + + assert_eq!(serialized, none()); +} + +#[test] +fn serialize_some_i16() { + let maybe: Option = Some(-60); + + let serialized = to_recursive_value(&maybe).unwrap_or_else(|err| panic!("{err:?}")); + + assert_eq!(serialized, some(int16(-60))); +} + +#[test] +fn serialize_unit() { + let serialized = to_recursive_value(&()).unwrap_or_else(|err| panic!("{err:?}")); + assert_eq!(serialized, unit()); +} + +#[test] +fn serialize_unit_struct() { + #[derive(Serialize)] + struct Noop; + + let serialized = to_recursive_value(&Noop).unwrap_or_else(|err| panic!("{err:?}")); + assert_eq!(serialized, unit_struct("Noop")); +} + +#[test] +fn serialize_unit_variant() { + #[allow(dead_code)] + #[derive(Serialize)] + enum Opacity { + Transparent, + Opaque, + } + + let serialized = + to_recursive_value(&Opacity::Transparent).unwrap_or_else(|err| panic!("{err:?}")); + assert_eq!(serialized, unit_variant("Opacity", "Transparent"),); +} + +#[test] +fn serialize_vec_of_i32() { + let sequence = vec![-1, -2, -3]; + + let serialized = to_recursive_value(&sequence).unwrap_or_else(|err| panic!("{err:?}")); + + assert_eq!(serialized, seq([int32(-1), int32(-2), int32(-3)])); +} + +#[test] +fn serialize_slice_of_i32() { + let slice = &[-1, -2, -3][..]; + + let serialized = to_recursive_value(slice).unwrap_or_else(|err| panic!("{err:?}")); + + assert_eq!(serialized, seq([int32(-1), int32(-2), int32(-3)])); +} + +#[test] +fn serialize_array_of_i32() { + let array = [-1, -2, -3]; + + let serialized = to_recursive_value(&array).unwrap_or_else(|err| panic!("{err:?}")); + + assert_eq!(serialized, value::array([int32(-1), int32(-2), int32(-3)])); +} + +#[test] +fn serialize_tuple_of_string_and_bool_and_u64() { + let tuple = ("foo".to_string(), true, 42_u64); + + let serialized = to_recursive_value(&tuple).unwrap_or_else(|err| panic!("{err:?}")); + + assert_eq!( + serialized, + value::tuple([string("foo"), bool(true), uint64(42)]) + ); +} + +#[test] +fn serialize_tuple_struct() { + #[derive(Serialize)] + struct Point3D(i16, i16, i16); + + let point = Point3D(16, -7, 0); + + let serialized = to_recursive_value(&point).unwrap_or_else(|err| panic!("{err:?}")); + + assert_eq!( + serialized, + tuple_struct("Point3D", [int16(16), int16(-7), int16(0)]) + ); +} + +#[test] +fn serialize_tuple_variant() { + #[allow(dead_code)] + #[derive(Serialize)] + enum Color { + Rgb(u8, u8, u8), + Hsl(u8, u8, u8), + } + + let color = Color::Rgb(128, 64, 32); + + let serialized = to_recursive_value(&color).unwrap_or_else(|err| panic!("{err:?}")); + + assert_eq!( + serialized, + tuple_variant("Color", "Rgb", [uint8(128), uint8(64), uint8(32)]) + ); +} + +#[test] +fn serialize_map_of_string_u64() { + let mapping: IndexMap = IndexMap::from_iter([ + ("one".to_string(), 1), + ("two".to_string(), 2), + ("three".to_string(), 3), + ]); + + let serialized = to_recursive_value(&mapping).unwrap_or_else(|err| panic!("{err:?}")); + + assert_eq!( + serialized, + map([ + (string("one"), uint64(1)), + (string("two"), uint64(2)), + (string("three"), uint64(3)) + ]) + ); +} + +#[test] +fn serialize_struct() { + #[derive(Serialize)] + struct Point { + x: i32, + y: i32, + } + + let point = Point { x: 10, y: -8 }; + + let serialized = to_recursive_value(&point).unwrap_or_else(|err| panic!("{err:?}")); + + assert_eq!( + serialized, + struct_("Point", [field("x", int32(10)), field("y", int32(-8))]) + ); +} + +#[test] +fn serialize_stuct_variant() { + #[allow(dead_code)] + #[derive(Serialize)] + enum Color { + Rgb { + red: u8, + green: u8, + blue: u8, + }, + Hsl { + hue: u8, + saturation: u8, + lightness: u8, + }, + } + + let color = Color::Rgb { + red: 128, + green: 64, + blue: 32, + }; + + let serialized = to_recursive_value(&color).unwrap_or_else(|err| panic!("{err:?}")); + + assert_eq!( + serialized, + struct_variant( + "Color", + "Rgb", + [ + ("red", uint8(128)), + ("green", uint8(64)), + ("blue", uint8(32)) + ] + ) + ); +} diff --git a/src/recursive_comparison/tests.rs b/src/recursive_comparison/tests.rs new file mode 100644 index 0000000..2d77416 --- /dev/null +++ b/src/recursive_comparison/tests.rs @@ -0,0 +1,1568 @@ +use crate::prelude::*; +use crate::recursive_comparison::value::{ + string, struct_with_fields, uint16, uint32, unit_variant, +}; +use crate::std::string::{String, ToString}; +use serde::Serialize; + +#[derive(Serialize)] +enum Gender { + Male, + Female, + NonBinary, + PreferNotToSay, +} + +#[derive(Serialize)] +struct Person { + id: usize, + name: String, + age: u8, + gender: Gender, + address: Address, +} + +#[derive(Serialize)] +struct Address { + id: usize, + street: String, + zip: u16, + city: String, +} + +#[derive(Serialize)] +struct PersonDto { + name: String, + age: u8, + gender: Gender, + address: AddressDto, +} + +#[derive(Serialize)] +struct AddressDto { + street: String, + zip: u16, + city: String, +} + +#[test] +fn struct_is_equal_to_using_recursive_comparison_all_fields() { + let person = Person { + id: 123, + name: "Silvia".into(), + age: 25, + gender: Gender::Female, + address: Address { + id: 91, + street: "Main Street".into(), + zip: 12345, + city: "New York".into(), + }, + }; + + assert_that(&person) + .using_recursive_comparison() + .is_equal_to(Person { + id: 123, + name: "Silvia".into(), + age: 25, + gender: Gender::Female, + address: Address { + id: 91, + street: "Main Street".into(), + zip: 12345, + city: "New York".into(), + }, + }); +} + +#[test] +fn verify_struct_is_equal_to_using_recursive_comparison_all_fields_fails() { + let person = Person { + id: 123, + name: "Silvia".into(), + age: 25, + gender: Gender::NonBinary, + address: Address { + id: 91, + street: "Second Street".into(), + zip: 12345, + city: "New York".into(), + }, + }; + + let failures = verify_that(&person) + .named("person") + .using_recursive_comparison() + .is_equal_to(Person { + id: 123, + name: "Silvia".to_string(), + age: 21, + gender: Gender::Female, + address: Address { + id: 91, + street: "Main Street".into(), + zip: 12345, + city: "New York".into(), + }, + }) + .display_failures(); + + assert_eq!( + failures, + &[ + r#"expected person to be equal to Person { id: 123, name: "Silvia", age: 21, gender: Female, address: Address { id: 91, street: "Main Street", zip: 12345, city: "New York" } } (using recursive comparison) + but was: Person { id: 123, name: "Silvia", age: 25, gender: NonBinary, address: Address { id: 91, street: "Second Street", zip: 12345, city: "New York" } } + expected: Person { id: 123, name: "Silvia", age: 21, gender: Female, address: Address { id: 91, street: "Main Street", zip: 12345, city: "New York" } } + + non equal fields: + age: expected <21> but was <25> + gender: expected but was + address.street: expected <"Main Street"> but was <"Second Street"> + +"# + ] + ); +} + +#[test] +fn struct_is_equal_to_using_recursive_comparison_ignoring_one_field() { + let person = Person { + id: 123, + name: "Silvia".into(), + age: 25, + gender: Gender::Female, + address: Address { + id: 91, + street: "Main Street".into(), + zip: 12345, + city: "New York".into(), + }, + }; + + assert_that(&person) + .using_recursive_comparison() + .ignoring_field("gender") + .is_equal_to(Person { + id: 123, + name: "Silvia".into(), + age: 25, + gender: Gender::PreferNotToSay, + address: Address { + id: 91, + street: "Main Street".into(), + zip: 12345, + city: "New York".into(), + }, + }); +} + +#[test] +fn verify_struct_is_equal_to_using_recursive_comparison_ignoring_one_field_fails() { + let person = Person { + id: 123, + name: "silvia".into(), + age: 25, + gender: Gender::Female, + address: Address { + id: 91, + street: "Second Street".into(), + zip: 12345, + city: "New York".into(), + }, + }; + + let failures = verify_that(&person) + .named("person") + .using_recursive_comparison() + .ignoring_field("gender") + .is_equal_to(Person { + id: 123, + name: "Silvia".to_string(), + age: 21, + gender: Gender::PreferNotToSay, + address: Address { + id: 91, + street: "Main Street".into(), + zip: 12345, + city: "New York".into(), + }, + }) + .display_failures(); + + assert_eq!( + failures, + &[ + r#"expected person to be equal to Person { id: 123, name: "Silvia", age: 21, gender: PreferNotToSay, address: Address { id: 91, street: "Main Street", zip: 12345, city: "New York" } } (using recursive comparison) + but was: Person { id: 123, name: "silvia", age: 25, gender: Female, address: Address { id: 91, street: "Second Street", zip: 12345, city: "New York" } } + expected: Person { id: 123, name: "Silvia", age: 21, gender: PreferNotToSay, address: Address { id: 91, street: "Main Street", zip: 12345, city: "New York" } } + + non equal fields: + name: expected <"Silvia"> but was <"silvia"> + age: expected <21> but was <25> + address.street: expected <"Main Street"> but was <"Second Street"> + + the following fields were ignored: + gender + +"# + ] + ); +} + +#[test] +fn struct_is_equal_to_using_recursive_comparison_ignoring_one_field_two_levels_deep() { + let person = Person { + id: 123, + name: "Silvia".into(), + age: 25, + gender: Gender::NonBinary, + address: Address { + id: 91, + street: "Second Street".into(), + zip: 12345, + city: "New York".into(), + }, + }; + + assert_that(&person) + .using_recursive_comparison() + .ignoring_field("address.street") + .is_equal_to(Person { + id: 123, + name: "Silvia".into(), + age: 25, + gender: Gender::NonBinary, + address: Address { + id: 91, + street: "Main Street".into(), + zip: 12345, + city: "New York".into(), + }, + }); +} + +#[test] +fn verify_struct_is_equal_to_using_recursive_comparison_ignoring_one_field_two_levels_deep_fails() { + let person = Person { + id: 123, + name: "Silvia".into(), + age: 25, + gender: Gender::Female, + address: Address { + id: 90, + street: "Second Street".into(), + zip: 12345, + city: "New York".into(), + }, + }; + + let failures = verify_that(&person) + .named("person") + .using_recursive_comparison() + .ignoring_field("address.street") + .is_equal_to(Person { + id: 123, + name: "Silvia".to_string(), + age: 25, + gender: Gender::PreferNotToSay, + address: Address { + id: 91, + street: "Main Street".into(), + zip: 12345, + city: "New York".into(), + }, + }) + .display_failures(); + + assert_eq!( + failures, + &[ + r#"expected person to be equal to Person { id: 123, name: "Silvia", age: 25, gender: PreferNotToSay, address: Address { id: 91, street: "Main Street", zip: 12345, city: "New York" } } (using recursive comparison) + but was: Person { id: 123, name: "Silvia", age: 25, gender: Female, address: Address { id: 90, street: "Second Street", zip: 12345, city: "New York" } } + expected: Person { id: 123, name: "Silvia", age: 25, gender: PreferNotToSay, address: Address { id: 91, street: "Main Street", zip: 12345, city: "New York" } } + + non equal fields: + gender: expected but was + address.id: expected <91> but was <90> + + the following fields were ignored: + address.street + +"# + ] + ); +} + +#[test] +fn struct_is_equal_to_using_recursive_comparison_ignoring_one_field_and_all_its_subfields() { + let person = Person { + id: 123, + name: "Silvia".into(), + age: 25, + gender: Gender::NonBinary, + address: Address { + id: 91, + street: "Second Street".into(), + zip: 12345, + city: "New York".into(), + }, + }; + + assert_that(&person) + .using_recursive_comparison() + .ignoring_field("address") + .is_equal_to(Person { + id: 123, + name: "Silvia".into(), + age: 25, + gender: Gender::NonBinary, + address: Address { + id: 0, + street: "Main Street".into(), + zip: 33333, + city: "Chicago".into(), + }, + }); +} + +#[test] +fn verify_struct_is_equal_to_using_recursive_comparison_ignoring_one_field_and_all_its_subfields_fails( +) { + let person = Person { + id: 123, + name: "Silvia".into(), + age: 25, + gender: Gender::NonBinary, + address: Address { + id: 91, + street: "Second Street".into(), + zip: 12345, + city: "New York".into(), + }, + }; + + let failures = verify_that(&person) + .named("person") + .using_recursive_comparison() + .ignoring_field("address") + .is_equal_to(Person { + id: 0, + name: "Silvia".to_string(), + age: 25, + gender: Gender::NonBinary, + address: Address { + id: 0, + street: "Main Street".into(), + zip: 33333, + city: "Chicago".into(), + }, + }) + .display_failures(); + + assert_eq!( + failures, + &[ + r#"expected person to be equal to Person { id: 0, name: "Silvia", age: 25, gender: NonBinary, address: Address { id: 0, street: "Main Street", zip: 33333, city: "Chicago" } } (using recursive comparison) + but was: Person { id: 123, name: "Silvia", age: 25, gender: NonBinary, address: Address { id: 91, street: "Second Street", zip: 12345, city: "New York" } } + expected: Person { id: 0, name: "Silvia", age: 25, gender: NonBinary, address: Address { id: 0, street: "Main Street", zip: 33333, city: "Chicago" } } + + non equal fields: + id: expected <0> but was <123> + + the following fields were ignored: + address.id + address.street + address.zip + address.city + +"# + ] + ); +} + +#[test] +fn struct_is_equal_to_using_recursive_comparison_ignoring_three_fields_repeated_method_calls() { + let person = Person { + id: 123, + name: "Silvia".into(), + age: 25, + gender: Gender::Male, + address: Address { + id: 91, + street: "Second Street".into(), + zip: 12345, + city: "New York".into(), + }, + }; + + assert_that(&person) + .using_recursive_comparison() + .ignoring_field("id") + .ignoring_field("address.street") + .ignoring_field("gender") + .is_equal_to(Person { + id: 0, + name: "Silvia".into(), + age: 25, + gender: Gender::Female, + address: Address { + id: 91, + street: "Main Street".into(), + zip: 12345, + city: "New York".into(), + }, + }); +} + +#[test] +fn verify_struct_is_equal_to_using_recursive_comparison_ignoring_three_fields_repeated_method_calls_fails( +) { + let person = Person { + id: 123, + name: "Silvia".into(), + age: 25, + gender: Gender::Male, + address: Address { + id: 91, + street: "Second Street".into(), + zip: 12345, + city: "New York".into(), + }, + }; + + let failures = verify_that(&person) + .named("person") + .using_recursive_comparison() + .ignoring_field("id") + .ignoring_field("address.street") + .ignoring_field("gender") + .is_equal_to(Person { + id: 0, + name: "Silvia".to_string(), + age: 21, + gender: Gender::Female, + address: Address { + id: 0, + street: "Main Street".to_string(), + zip: 33333, + city: "Chicago".to_string(), + }, + }) + .display_failures(); + + assert_eq!( + failures, + &[ + r#"expected person to be equal to Person { id: 0, name: "Silvia", age: 21, gender: Female, address: Address { id: 0, street: "Main Street", zip: 33333, city: "Chicago" } } (using recursive comparison) + but was: Person { id: 123, name: "Silvia", age: 25, gender: Male, address: Address { id: 91, street: "Second Street", zip: 12345, city: "New York" } } + expected: Person { id: 0, name: "Silvia", age: 21, gender: Female, address: Address { id: 0, street: "Main Street", zip: 33333, city: "Chicago" } } + + non equal fields: + age: expected <21> but was <25> + address.id: expected <0> but was <91> + address.zip: expected <33333> but was <12345> + address.city: expected <"Chicago"> but was <"New York"> + + the following fields were ignored: + id + gender + address.street + +"# + ] + ); +} + +#[test] +fn struct_is_equal_to_using_recursive_comparison_ignoring_three_fields() { + let person = Person { + id: 123, + name: "Silvia".into(), + age: 25, + gender: Gender::Male, + address: Address { + id: 91, + street: "Second Street".into(), + zip: 12345, + city: "New York".into(), + }, + }; + + assert_that(&person) + .using_recursive_comparison() + .ignoring_fields(["id", "gender", "address.street"]) + .is_equal_to(Person { + id: 0, + name: "Silvia".into(), + age: 25, + gender: Gender::Female, + address: Address { + id: 91, + street: "Main Street".into(), + zip: 12345, + city: "New York".into(), + }, + }); +} + +#[test] +fn verify_struct_is_equal_to_using_recursive_comparison_ignoring_three_fields_fails() { + let person = Person { + id: 123, + name: "Silvia".into(), + age: 25, + gender: Gender::Male, + address: Address { + id: 91, + street: "Second Street".into(), + zip: 12345, + city: "New York".into(), + }, + }; + + let failures = verify_that(&person) + .named("person") + .using_recursive_comparison() + .ignoring_fields(["address.id", "gender", "address.city"]) + .is_equal_to(Person { + id: 0, + name: "Silvia".to_string(), + age: 25, + gender: Gender::Female, + address: Address { + id: 0, + street: "Main Street".to_string(), + zip: 33333, + city: "Chicago".to_string(), + }, + }) + .display_failures(); + + assert_eq!( + failures, + &[ + r#"expected person to be equal to Person { id: 0, name: "Silvia", age: 25, gender: Female, address: Address { id: 0, street: "Main Street", zip: 33333, city: "Chicago" } } (using recursive comparison) + but was: Person { id: 123, name: "Silvia", age: 25, gender: Male, address: Address { id: 91, street: "Second Street", zip: 12345, city: "New York" } } + expected: Person { id: 0, name: "Silvia", age: 25, gender: Female, address: Address { id: 0, street: "Main Street", zip: 33333, city: "Chicago" } } + + non equal fields: + id: expected <0> but was <123> + address.street: expected <"Main Street"> but was <"Second Street"> + address.zip: expected <33333> but was <12345> + + the following fields were ignored: + gender + address.id + address.city + +"# + ] + ); +} + +#[test] +fn struct_is_equal_to_using_recursive_comparison_ignoring_id_fields_on_different_levels() { + let person = Person { + id: 123, + name: "Silvia".into(), + age: 25, + gender: Gender::Female, + address: Address { + id: 91, + street: "Main Street".into(), + zip: 12345, + city: "New York".into(), + }, + }; + + assert_that(&person) + .using_recursive_comparison() + .ignoring_fields(["id", "address.id"]) + .is_equal_to(Person { + id: 0, + name: "Silvia".into(), + age: 25, + gender: Gender::Female, + address: Address { + id: 0, + street: "Main Street".into(), + zip: 12345, + city: "New York".into(), + }, + }); +} + +#[test] +fn verify_struct_is_equal_to_using_recursive_comparison_ignoring_id_fields_on_different_levels_fails( +) { + let person = Person { + id: 123, + name: "Silvia".into(), + age: 25, + gender: Gender::Female, + address: Address { + id: 91, + street: "Main Street".into(), + zip: 12345, + city: "New York".into(), + }, + }; + + let failures = verify_that(&person) + .named("person") + .using_recursive_comparison() + .ignoring_fields(["id", "address.id"]) + .is_equal_to(Person { + id: 0, + name: "Silvia".to_string(), + age: 21, + gender: Gender::Female, + address: Address { + id: 0, + street: "Main Street".to_string(), + zip: 33333, + city: "New York".to_string(), + }, + }) + .display_failures(); + + assert_eq!( + failures, + &[ + r#"expected person to be equal to Person { id: 0, name: "Silvia", age: 21, gender: Female, address: Address { id: 0, street: "Main Street", zip: 33333, city: "New York" } } (using recursive comparison) + but was: Person { id: 123, name: "Silvia", age: 25, gender: Female, address: Address { id: 91, street: "Main Street", zip: 12345, city: "New York" } } + expected: Person { id: 0, name: "Silvia", age: 21, gender: Female, address: Address { id: 0, street: "Main Street", zip: 33333, city: "New York" } } + + non equal fields: + age: expected <21> but was <25> + address.zip: expected <33333> but was <12345> + + the following fields were ignored: + id + address.id + +"# + ] + ); +} + +#[test] +fn struct_is_equal_to_using_recursive_comparison_comparing_only_specified_fields() { + let person = Person { + id: 456, + name: "Silvia".into(), + age: 27, + gender: Gender::Female, + address: Address { + id: 291, + street: "Second Street".into(), + zip: 12345, + city: "New York".into(), + }, + }; + + assert_that!(person) + .using_recursive_comparison() + .comparing_only_field("name") + .comparing_only_fields(["gender", "address.zip", "address.city"]) + .is_equal_to(Person { + id: 0, + name: "Silvia".into(), + age: 25, + gender: Gender::Female, + address: Address { + id: 0, + street: "Main Street".into(), + zip: 12345, + city: "New York".into(), + }, + }); +} + +#[test] +fn verify_struct_is_equal_to_using_recursive_comparison_comparing_only_specified_fields_fails() { + let person = Person { + id: 123, + name: "Silvia".into(), + age: 25, + gender: Gender::Female, + address: Address { + id: 91, + street: "Second Street".into(), + zip: 12345, + city: "New York".into(), + }, + }; + + let failures = verify_that(&person) + .named("person") + .using_recursive_comparison() + .comparing_only_field("name") + .comparing_only_fields(["gender", "address.zip", "address.city"]) + .is_equal_to(Person { + id: 0, + name: "Silvia".to_string(), + age: 21, + gender: Gender::Female, + address: Address { + id: 0, + street: "Main Street".to_string(), + zip: 33333, + city: "Chicago".to_string(), + }, + }) + .display_failures(); + + assert_eq!( + failures, + &[ + r#"expected person to be equal to Person { id: 0, name: "Silvia", age: 21, gender: Female, address: Address { id: 0, street: "Main Street", zip: 33333, city: "Chicago" } } (using recursive comparison) + but was: Person { id: 123, name: "Silvia", age: 25, gender: Female, address: Address { id: 91, street: "Second Street", zip: 12345, city: "New York" } } + expected: Person { id: 0, name: "Silvia", age: 21, gender: Female, address: Address { id: 0, street: "Main Street", zip: 33333, city: "Chicago" } } + + non equal fields: + address.zip: expected <33333> but was <12345> + address.city: expected <"Chicago"> but was <"New York"> + + the following fields were ignored: + id + age + address.id + address.street + +"# + ] + ); +} + +#[test] +fn struct_is_equal_to_equivalent_type() { + let person = Person { + id: 456, + name: "Silvia".into(), + age: 25, + gender: Gender::Female, + address: Address { + id: 291, + street: "Main Street".into(), + zip: 12345, + city: "New York".into(), + }, + }; + + assert_that(&person) + .using_recursive_comparison() + .ignoring_not_expected_fields() + .is_equal_to(PersonDto { + name: "Silvia".into(), + age: 25, + gender: Gender::Female, + address: AddressDto { + street: "Main Street".into(), + zip: 12345, + city: "New York".into(), + }, + }); +} + +#[test] +fn verify_struct_is_equal_to_equivalent_type_do_not_ignore_not_expected_fields_fails() { + let person = Person { + id: 456, + name: "Silvia".into(), + age: 25, + gender: Gender::Female, + address: Address { + id: 291, + street: "Main Street".into(), + zip: 12345, + city: "New York".into(), + }, + }; + + let failures = verify_that(&person) + .named("person") + .using_recursive_comparison() + .is_equal_to(PersonDto { + name: "Silvia".into(), + age: 25, + gender: Gender::Female, + address: AddressDto { + street: "Main Street".into(), + zip: 12345, + city: "New York".into(), + }, + }) + .display_failures(); + + assert_eq!( + failures, + &[ + r#"expected person to be equal to PersonDto { name: "Silvia", age: 25, gender: Female, address: AddressDto { street: "Main Street", zip: 12345, city: "New York" } } (using recursive comparison) + but was: Person { id: 456, name: "Silvia", age: 25, gender: Female, address: Address { id: 291, street: "Main Street", zip: 12345, city: "New York" } } + expected: PersonDto { name: "Silvia", age: 25, gender: Female, address: AddressDto { street: "Main Street", zip: 12345, city: "New York" } } + + the following fields were not expected: + id: 456 + address.id: 291 + +"# + ] + ); +} + +#[test] +fn verify_struct_is_equal_to_equivalent_type_fails_all_fields_different() { + let person = Person { + id: 456, + name: "silvia".into(), + age: 27, + gender: Gender::Male, + address: Address { + id: 291, + street: "Second Street".into(), + zip: 33333, + city: "Chicago".into(), + }, + }; + + let failures = verify_that(&person) + .named("person") + .using_recursive_comparison() + .ignoring_not_expected_fields() + .is_equal_to(PersonDto { + name: "Silvia".into(), + age: 25, + gender: Gender::Female, + address: AddressDto { + street: "Main Street".into(), + zip: 12345, + city: "New York".into(), + }, + }) + .display_failures(); + + assert_eq!( + failures, + &[ + r#"expected person to be equal to PersonDto { name: "Silvia", age: 25, gender: Female, address: AddressDto { street: "Main Street", zip: 12345, city: "New York" } } (using recursive comparison) + but was: Person { id: 456, name: "silvia", age: 27, gender: Male, address: Address { id: 291, street: "Second Street", zip: 33333, city: "Chicago" } } + expected: PersonDto { name: "Silvia", age: 25, gender: Female, address: AddressDto { street: "Main Street", zip: 12345, city: "New York" } } + + non equal fields: + name: expected <"Silvia"> but was <"silvia"> + age: expected <25> but was <27> + gender: expected but was + address.street: expected <"Main Street"> but was <"Second Street"> + address.zip: expected <12345> but was <33333> + address.city: expected <"New York"> but was <"Chicago"> + + the following fields were ignored: + id + address.id + +"# + ] + ); +} + +#[test] +fn verify_struct_is_equal_to_equivalent_type_fails_for_different_type() { + #[derive(Serialize)] + struct PersonDto { + name: String, + age: u16, + gender: Gender, + address: AddressDto, + } + + let person = Person { + id: 456, + name: "Silvia".into(), + age: 25, + gender: Gender::Female, + address: Address { + id: 291, + street: "Second Street".into(), + zip: 33333, + city: "New York".into(), + }, + }; + + let failures = verify_that(&person) + .named("person") + .using_recursive_comparison() + .ignoring_not_expected_fields() + .is_equal_to(PersonDto { + name: "Silvia".into(), + age: 25, + gender: Gender::Female, + address: AddressDto { + street: "Main Street".into(), + zip: 12345, + city: "New York".into(), + }, + }) + .display_failures(); + + assert_eq!( + failures, + &[ + r#"expected person to be equal to PersonDto { name: "Silvia", age: 25, gender: Female, address: AddressDto { street: "Main Street", zip: 12345, city: "New York" } } (using recursive comparison) + but was: Person { id: 456, name: "Silvia", age: 25, gender: Female, address: Address { id: 291, street: "Second Street", zip: 33333, city: "New York" } } + expected: PersonDto { name: "Silvia", age: 25, gender: Female, address: AddressDto { street: "Main Street", zip: 12345, city: "New York" } } + + non equal fields: + age: value <25> was equal, but type was and expected type is + address.street: expected <"Main Street"> but was <"Second Street"> + address.zip: expected <12345> but was <33333> + + the following fields were ignored: + id + address.id + +"# + ] + ); +} + +#[test] +fn struct_is_equivalent_to_struct_with_relevant_fields() { + let person = Person { + id: 456, + name: "Silvia".into(), + age: 25, + gender: Gender::Female, + address: Address { + id: 291, + street: "Second Street".into(), + zip: 12345, + city: "New York".into(), + }, + }; + + assert_that(&person) + .using_recursive_comparison() + .ignoring_not_expected_fields() + .is_equivalent_to(struct_with_fields([ + ("name", string("Silvia")), + ("gender", unit_variant("Gender", "Female")), + ( + "address", + struct_with_fields([("zip", uint16(12345)), ("city", string("New York"))]), + ), + ])); +} + +#[test] +fn verify_struct_is_equivalent_to_struct_with_relevant_fields_do_not_ignore_not_expected_fields_fails( +) { + let person = Person { + id: 456, + name: "Silvia".into(), + age: 25, + gender: Gender::Female, + address: Address { + id: 291, + street: "Main Street".into(), + zip: 12345, + city: "New York".into(), + }, + }; + + let failures = verify_that(&person) + .named("person") + .using_recursive_comparison() + .is_equivalent_to(struct_with_fields([ + ("name", string("Silvia")), + ("gender", unit_variant("Gender", "Female")), + ( + "address", + struct_with_fields([("zip", uint16(12345)), ("city", string("New York"))]), + ), + ])) + .display_failures(); + + assert_eq!( + failures, + &[ + r#"expected person to be equivalent to { name: "Silvia", gender: Female, address: { zip: 12345, city: "New York" } } (using recursive comparison) + but was: Person { id: 456, name: "Silvia", age: 25, gender: Female, address: Address { id: 291, street: "Main Street", zip: 12345, city: "New York" } } + expected: { name: "Silvia", gender: Female, address: { zip: 12345, city: "New York" } } + + the following fields were not expected: + id: 456 + age: 25 + address.id: 291 + address.street: "Main Street" + +"# + ] + ); +} + +#[test] +fn verify_struct_is_equivalent_to_struct_with_relevant_fields_fails_for_different_type() { + let person = Person { + id: 456, + name: "Silvia".into(), + age: 25, + gender: Gender::Female, + address: Address { + id: 291, + street: "Main Street".into(), + zip: 12345, + city: "New York".into(), + }, + }; + + let failures = verify_that(&person) + .named("person") + .using_recursive_comparison() + .ignoring_not_expected_fields() + .is_equivalent_to(struct_with_fields([ + ("name", string("Silvia")), + ("gender", unit_variant("Gender", "Female")), + ( + "address", + struct_with_fields([("zip", uint32(12345)), ("city", string("New York"))]), + ), + ])) + .display_failures(); + + assert_eq!( + failures, + &[ + r#"expected person to be equivalent to { name: "Silvia", gender: Female, address: { zip: 12345, city: "New York" } } (using recursive comparison) + but was: Person { id: 456, name: "Silvia", age: 25, gender: Female, address: Address { id: 291, street: "Main Street", zip: 12345, city: "New York" } } + expected: { name: "Silvia", gender: Female, address: { zip: 12345, city: "New York" } } + + non equal fields: + address.zip: value <12345> was equal, but type was and expected type is + + the following fields were ignored: + id + age + address.id + address.street + +"# + ] + ); +} + +#[test] +fn struct_is_equivalent_to_value_from_macro() { + let person = Person { + id: 456, + name: "Silvia".into(), + age: 25, + gender: Gender::Female, + address: Address { + id: 291, + street: "Main Street".into(), + zip: 12345, + city: "New York".into(), + }, + }; + + assert_that(&person) + .using_recursive_comparison() + .ignoring_not_expected_fields() + .is_equivalent_to(value!({ + name: "Silvia", + gender: Gender::Female, + address: { + zip: 12345_u16, + city: "New York", + }, + })); +} + +#[test] +fn struct_is_equivalent_to_value_with_additional_field_from_macro() { + let person = Person { + id: 456, + name: "Silvia".into(), + age: 25, + gender: Gender::Female, + address: Address { + id: 291, + street: "Main Street".into(), + zip: 12345, + city: "New York".into(), + }, + }; + + assert_that(&person) + .using_recursive_comparison() + .ignoring_not_expected_fields() + .is_equivalent_to(value!({ + name: "Silvia", + gender: Gender::Female, + other_field: ("present in actual value", false), + address: { + zip: 12345_u16, + city: "New York", + state: "not present in actual value", + }, + })); +} + +#[test] +fn struct_is_not_equal_to_using_recursive_comparison_all_fields() { + let person = Person { + id: 456, + name: "Silvia".into(), + age: 25, + gender: Gender::Female, + address: Address { + id: 291, + street: "Main Street".into(), + zip: 12345, + city: "New York".into(), + }, + }; + + assert_that(&person) + .using_recursive_comparison() + .is_not_equal_to(Person { + id: 456, + name: "Silvia".into(), + age: 25, + gender: Gender::Female, + address: Address { + id: 123, + street: "Main Street".into(), + zip: 12345, + city: "New York".into(), + }, + }); +} + +#[test] +fn verify_struct_is_not_equal_using_recursive_comparison_all_fields_fails() { + let person = Person { + id: 123, + name: "Silvia".into(), + age: 25, + gender: Gender::Female, + address: Address { + id: 91, + street: "Main Street".into(), + zip: 12345, + city: "New York".into(), + }, + }; + + let failures = verify_that(&person) + .named("person") + .using_recursive_comparison() + .is_not_equal_to(Person { + id: 123, + name: "Silvia".into(), + age: 25, + gender: Gender::Female, + address: Address { + id: 91, + street: "Main Street".into(), + zip: 12345, + city: "New York".into(), + }, + }) + .display_failures(); + + assert_eq!( + failures, + &[ + r#"expected person to be not equal to Person { id: 123, name: "Silvia", age: 25, gender: Female, address: Address { id: 91, street: "Main Street", zip: 12345, city: "New York" } } (using recursive comparison) + but was: Person { id: 123, name: "Silvia", age: 25, gender: Female, address: Address { id: 91, street: "Main Street", zip: 12345, city: "New York" } } + expected: Person { id: 123, name: "Silvia", age: 25, gender: Female, address: Address { id: 91, street: "Main Street", zip: 12345, city: "New York" } } + +"# + ] + ); +} + +#[test] +fn struct_is_not_equal_to_using_recursive_comparison_ignoring_id_fields() { + let person = Person { + id: 456, + name: "Silvia".into(), + age: 25, + gender: Gender::Female, + address: Address { + id: 291, + street: "Second Street".into(), + zip: 12345, + city: "New York".into(), + }, + }; + + assert_that(&person) + .using_recursive_comparison() + .ignoring_fields(["id", "address.id"]) + .is_not_equal_to(Person { + id: 0, + name: "Silvia".into(), + age: 25, + gender: Gender::Female, + address: Address { + id: 0, + street: "Main Street".into(), + zip: 12345, + city: "New York".into(), + }, + }); +} + +#[test] +fn verify_struct_is_not_equal_to_using_recursive_comparison_ignoring_id_fields_fails() { + let person = Person { + id: 456, + name: "Silvia".into(), + age: 25, + gender: Gender::Female, + address: Address { + id: 291, + street: "Main Street".into(), + zip: 12345, + city: "New York".into(), + }, + }; + + let failures = verify_that(&person) + .named("person") + .using_recursive_comparison() + .ignoring_fields(["id", "address.id"]) + .is_not_equal_to(Person { + id: 0, + name: "Silvia".into(), + age: 25, + gender: Gender::Female, + address: Address { + id: 0, + street: "Main Street".into(), + zip: 12345, + city: "New York".into(), + }, + }) + .display_failures(); + + assert_eq!( + failures, + &[ + r#"expected person to be not equal to Person { id: 0, name: "Silvia", age: 25, gender: Female, address: Address { id: 0, street: "Main Street", zip: 12345, city: "New York" } } (using recursive comparison) + but was: Person { id: 456, name: "Silvia", age: 25, gender: Female, address: Address { id: 291, street: "Main Street", zip: 12345, city: "New York" } } + expected: Person { id: 0, name: "Silvia", age: 25, gender: Female, address: Address { id: 0, street: "Main Street", zip: 12345, city: "New York" } } + + the following fields were ignored: + id + address.id + +"# + ] + ); +} + +#[test] +fn struct_is_not_equivalent_to_value_from_macro_all_fields() { + let person = Person { + id: 456, + name: "Silvia".into(), + age: 25, + gender: Gender::Female, + address: Address { + id: 291, + street: "Main Street".into(), + zip: 12345, + city: "New York".into(), + }, + }; + + assert_that(&person) + .using_recursive_comparison() + .is_not_equivalent_to(value!({ + id: 456_u64, + name: "Silvia", + age: 21_u8, + gender: Gender::Female, + address: { + id: 291_u64, + street: "Main Street", + zip: 12345_u16, + city: "New York", + }, + })); +} + +#[test] +fn verify_struct_is_not_equivalent_to_value_from_macro_all_fields_fails() { + let person = Person { + id: 456, + name: "Silvia".into(), + age: 25, + gender: Gender::Female, + address: Address { + id: 291, + street: "Main Street".into(), + zip: 12345, + city: "New York".into(), + }, + }; + + let failures = verify_that(&person) + .named("person") + .using_recursive_comparison() + .is_not_equivalent_to(value!({ + id: 456_u64, + name: "Silvia", + age: 25_u8, + gender: Gender::Female, + address: { + id: 291_u64, + street: "Main Street", + zip: 12345_u16, + city: "New York", + }, + })) + .display_failures(); + + assert_eq!( + failures, + &[ + r#"expected person to be not equivalent to { id: 456, name: "Silvia", age: 25, gender: Female, address: { id: 291, street: "Main Street", zip: 12345, city: "New York" } } (using recursive comparison) + but was: Person { id: 456, name: "Silvia", age: 25, gender: Female, address: Address { id: 291, street: "Main Street", zip: 12345, city: "New York" } } + expected: { id: 456, name: "Silvia", age: 25, gender: Female, address: { id: 291, street: "Main Street", zip: 12345, city: "New York" } } + +"# + ] + ); +} + +#[test] +fn struct_is_not_equivalent_to_value_from_macro() { + let person = Person { + id: 456, + name: "Silvia".into(), + age: 25, + gender: Gender::Female, + address: Address { + id: 291, + street: "Main Street".into(), + zip: 12345, + city: "Chicago".into(), + }, + }; + + assert_that(&person) + .using_recursive_comparison() + .ignoring_not_expected_fields() + .is_not_equivalent_to(value!({ + name: "Silvia", + gender: Gender::Female, + address: { + zip: 12345_u16, + city: "New York", + }, + })); +} + +#[test] +fn verify_struct_id_not_equivalent_to_value_from_macro_fails() { + let person = Person { + id: 456, + name: "Silvia".into(), + age: 25, + gender: Gender::Female, + address: Address { + id: 291, + street: "Main Street".into(), + zip: 12345, + city: "New York".into(), + }, + }; + + let failures = verify_that(&person) + .named("person") + .using_recursive_comparison() + .ignoring_not_expected_fields() + .is_not_equivalent_to(value!({ + name: "Silvia", + gender: Gender::Female, + address: { + zip: 12345_u16, + city: "New York", + }, + })) + .display_failures(); + + assert_eq!( + failures, + &[ + r#"expected person to be not equivalent to { name: "Silvia", gender: Female, address: { zip: 12345, city: "New York" } } (using recursive comparison) + but was: Person { id: 456, name: "Silvia", age: 25, gender: Female, address: Address { id: 291, street: "Main Street", zip: 12345, city: "New York" } } + expected: { name: "Silvia", gender: Female, address: { zip: 12345, city: "New York" } } + + the following fields were ignored: + id + age + address.id + address.street + +"# + ] + ); +} + +#[test] +fn verify_struct_id_not_equivalent_to_value_from_macro_fails_always_ignoring_not_expected_fields() { + let person = Person { + id: 456, + name: "Silvia".into(), + age: 25, + gender: Gender::Female, + address: Address { + id: 291, + street: "Main Street".into(), + zip: 12345, + city: "New York".into(), + }, + }; + + let failures = verify_that(&person) + .named("person") + .using_recursive_comparison() + .is_not_equivalent_to(value!({ + name: "Silvia", + gender: Gender::Female, + address: { + zip: 12345_u16, + city: "New York", + }, + })) + .display_failures(); + + assert_eq!( + failures, + &[ + r#"expected person to be not equivalent to { name: "Silvia", gender: Female, address: { zip: 12345, city: "New York" } } (using recursive comparison) + but was: Person { id: 456, name: "Silvia", age: 25, gender: Female, address: Address { id: 291, street: "Main Street", zip: 12345, city: "New York" } } + expected: { name: "Silvia", gender: Female, address: { zip: 12345, city: "New York" } } + + the following fields were ignored: + id + age + address.id + address.street + +"# + ] + ); +} + +#[cfg(feature = "colored")] +mod colored { + use super::*; + + #[test] + fn highlight_diffs_struct_is_equal_to_using_recursive_comparison_all_fields() { + let person = Person { + id: 123, + name: "Silvia".into(), + age: 25, + gender: Gender::NonBinary, + address: Address { + id: 91, + street: "Second Street".into(), + zip: 12345, + city: "New York".into(), + }, + }; + + let failures = verify_that(&person) + .named("person") + .with_configured_diff_format() + .using_recursive_comparison() + .is_equal_to(Person { + id: 123, + name: "Silvia".to_string(), + age: 21, + gender: Gender::Female, + address: Address { + id: 91, + street: "Main Street".into(), + zip: 12345, + city: "New York".into(), + }, + }) + .display_failures(); + + assert_eq!( + failures, + &[ + "expected person to be equal to Person { id: 123, name: \"Silvia\", age: 21, gender: Female, address: Address { id: 91, street: \"Main Street\", zip: 12345, city: \"New York\" } } (using recursive comparison)\n \ + but was: Person { id: 123, name: \"Silvia\", age: 25, gender: NonBinary, address: Address { id: 91, street: \"Second Street\", zip: 12345, city: \"New York\" } }\n \ + expected: Person { id: 123, name: \"Silvia\", age: 21, gender: Female, address: Address { id: 91, street: \"Main Street\", zip: 12345, city: \"New York\" } }\n\ + \n \ + non equal fields:\n \ + age: expected <2\u{1b}[32m1\u{1b}[0m> but was <2\u{1b}[31m5\u{1b}[0m>\n \ + gender: expected <\u{1b}[32mFem\u{1b}[0ma\u{1b}[32mle\u{1b}[0m> but was <\u{1b}[31mNonBin\u{1b}[0ma\u{1b}[31mry\u{1b}[0m>\n \ + address.street: expected <\"\u{1b}[32mMai\u{1b}[0mn Street\"> but was <\"\u{1b}[31mSeco\u{1b}[0mn\u{1b}[31md\u{1b}[0m Street\">\n\ + \n" + ] + ); + } + + #[test] + fn highlight_diffs_struct_is_equivalent_to_struct_with_relevant_fields() { + let person = Person { + id: 123, + name: "Silvia".into(), + age: 25, + gender: Gender::Female, + address: Address { + id: 91, + street: "Second Street".into(), + zip: 12345, + city: "New York".into(), + }, + }; + + let failures = verify_that(&person) + .named("person") + .with_configured_diff_format() + .using_recursive_comparison() + .ignoring_not_expected_fields() + .is_equivalent_to(value!({ + name: "Silvia", + age: 21_u8, + gender: Gender::Female, + address: { + zip: 12345_u32, + city: "New York", + } + })) + .display_failures(); + + assert_eq!( + failures, + &[ + "expected person to be equivalent to { name: \"Silvia\", age: 21, gender: Female, address: { zip: 12345, city: \"New York\" } } (using recursive comparison)\n \ + but was: Person { id: 123, name: \"Silvia\", age: 25, gender: Female, address: Address { id: 91, street: \"Second Street\", zip: 12345, city: \"New York\" } }\n \ + expected: { name: \"Silvia\", age: 21, gender: Female, address: { zip: 12345, city: \"New York\" } }\n\ + \n \ + non equal fields:\n \ + age: expected <2\u{1b}[32m1\u{1b}[0m> but was <2\u{1b}[31m5\u{1b}[0m>\n \ + address.zip: value <12345> was equal, but type was and expected type is \n\ + \n \ + the following fields were ignored:\n \ + id\n \ + address.id\n \ + address.street\n\ + \n" + ] + ); + } +} diff --git a/src/recursive_comparison/value/map/mod.rs b/src/recursive_comparison/value/map/mod.rs new file mode 100644 index 0000000..5d1cd32 --- /dev/null +++ b/src/recursive_comparison/value/map/mod.rs @@ -0,0 +1,199 @@ +//! Defines the [`Map`] type which represents a map in the [`Value`] data +//! structure used for recursive comparison. + +use super::Value; +use crate::std::borrow::{Borrow, Cow}; +use crate::std::cmp::Ordering; +use crate::std::fmt::{self, Debug}; +use crate::std::format; +use crate::std::hash::{Hash, Hasher}; +use indexmap::IndexMap; +use rapidhash::quality::RandomState; + +/// The map type used inside the [`Value`] type. +#[derive(Default, Clone)] +pub struct Map(IndexMap); + +impl Map { + /// Creates a new empty `Map`. + pub fn new() -> Self { + Self(IndexMap::with_hasher(RandomState::new())) + } + + /// Creates a new `Map` with the given capacity. + pub fn with_capacity(capacity: usize) -> Self { + Self(IndexMap::with_capacity_and_hasher( + capacity, + RandomState::new(), + )) + } + + /// Returns the number of elements in the `Map`. + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns `true` if the `Map` contains no elements. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Returns the type name of this map at runtime. + /// + /// # Returns + /// + /// If the map is empty, it returns "Map". If the map contains + /// values, it returns the type Map with the type names of the first key and + /// value, e.g., "Map". + pub fn type_name(&self) -> Cow<'static, str> { + if let Some((key, value)) = self.0.iter().next() { + let key_type = key.type_name(); + let value_type = value.type_name(); + Cow::Owned(format!("Map<{key_type}, {value_type}>")) + } else { + Cow::Borrowed("Map") + } + } + + /// Inserts a new key-value pair into this map. + /// + /// If the map already contains an association for the given key, the key + /// is associated with the new value and the previous value is returned. + /// + /// # Returns + /// + /// If the key is already associated with a value, the previous value is + /// returned. If this map does not already contain the given key, `None` is + /// returned. + pub fn insert(&mut self, key: Value, value: Value) -> Option { + self.0.insert(key, value) + } + + /// Get a read-only reference to the value associated with the given key. + pub fn get(&self, key: &Q) -> Option<&Value> + where + Value: Borrow, + Q: ?Sized + Ord + Eq + Hash, + { + self.0.get(key) + } + + /// Returns an iterator over the borrowed entries (key-value pairs) of this + /// map. + #[allow(dead_code)] + pub fn iter(&self) -> Iter<'_> { + Iter { + inner: self.0.iter(), + } + } +} + +impl Debug for Map { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_map().entries(self.0.iter()).finish() + } +} + +impl Hash for Map { + fn hash(&self, state: &mut H) { + self.0.iter().for_each(|x| x.hash(state)); + } +} + +impl PartialEq for Map { + fn eq(&self, other: &Self) -> bool { + self.cmp(other).is_eq() + } +} + +impl Eq for Map {} + +impl PartialOrd for Map { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Map { + fn cmp(&self, other: &Self) -> Ordering { + self.0.iter().cmp(other.0.iter()) + } +} + +impl FromIterator<(Value, Value)> for Map { + fn from_iter(iter: I) -> Self + where + I: IntoIterator, + { + Self(IndexMap::from_iter(iter)) + } +} + +impl IntoIterator for Map { + type Item = (Value, Value); + type IntoIter = IntoIter; + + fn into_iter(self) -> Self::IntoIter { + IntoIter { + inner: self.0.into_iter(), + } + } +} + +/// Iterator over the owned entries (key-value pairs) of a [`Map`]. +pub struct IntoIter { + inner: indexmap::map::IntoIter, +} + +impl Iterator for IntoIter { + type Item = (Value, Value); + + fn next(&mut self) -> Option { + self.inner.next() + } + + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } +} + +impl DoubleEndedIterator for IntoIter { + fn next_back(&mut self) -> Option { + self.inner.next_back() + } +} + +impl<'a> IntoIterator for &'a Map { + type Item = (&'a Value, &'a Value); + type IntoIter = Iter<'a>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +/// Iterator over the borrowed entries (key-value pairs) of a [`Map`]. +pub struct Iter<'a> { + inner: indexmap::map::Iter<'a, Value, Value>, +} + +impl<'a> Iterator for Iter<'a> { + type Item = (&'a Value, &'a Value); + + fn next(&mut self) -> Option { + self.inner.next() + } + + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } +} + +impl DoubleEndedIterator for Iter<'_> { + fn next_back(&mut self) -> Option { + self.inner.next_back() + } +} + +#[cfg(test)] +mod tests; diff --git a/src/recursive_comparison/value/map/tests.rs b/src/recursive_comparison/value/map/tests.rs new file mode 100644 index 0000000..9360f82 --- /dev/null +++ b/src/recursive_comparison/value/map/tests.rs @@ -0,0 +1,24 @@ +use super::*; +use crate::recursive_comparison::value::proptest_support::*; +use proptest::prelude::*; + +#[test] +fn type_name_of_empty_map() { + let map = Map::new(); + + assert_eq!(map.type_name(), "Map"); +} + +proptest! { + #[test] + fn type_name_of_map_with_one_entry( + key in any_value(), + value in any_value(), + ) { + let key_type = key.type_name(); + let value_type = value.type_name(); + let map = Map::from_iter([(key, value)]); + + assert_eq!(map.type_name(), format!("Map<{key_type}, {value_type}>")); + } +} diff --git a/src/recursive_comparison/value/mod.rs b/src/recursive_comparison/value/mod.rs new file mode 100644 index 0000000..cf2943d --- /dev/null +++ b/src/recursive_comparison/value/mod.rs @@ -0,0 +1,796 @@ +//! Provides the [`Value`] type - the data structure used for recursive +//! comparison. + +mod map; +mod number; +#[cfg(test)] +pub mod proptest_support; + +use crate::recursive_comparison::path::Path; +use crate::std::borrow::Cow; +use crate::std::fmt::{self, Debug}; +use crate::std::string::{String, ToString}; +use crate::std::vec::Vec; +use crate::std::{format, vec}; +pub use map::Map; +pub use number::{Number, F32, F64}; + +/// Represents a field in a struct, tuple, or enum. +#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Field { + /// The name of the field. + pub name: Cow<'static, str>, + /// The value of the field. + pub value: Value, +} + +impl Debug for Field { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let name = &self.name; + let value = &self.value; + write!(f, "{name}: {value:?}",) + } +} + +impl From<(N, Value)> for Field +where + N: Into>, +{ + fn from((name, value): (N, Value)) -> Self { + Self { + name: name.into(), + value, + } + } +} + +/// The [`Value`] is a data structure that can represent a value of any type in +/// Rust, including structs, tuples, and enums. +/// +/// A [`Value`] can mimic any type. The type does not have to be declared +/// beforehand and does not have to be in scope when constructing a [`Value`]. +/// It is possible to specify an expected value with only those fields that are +/// of interest in a specific test case without having to declare a custom type +/// for it. This type would be used only in test cases. Not having to declare +/// several types just to use them as expected values in test cases avoids +/// some boilerplate code. +/// +/// In `asserting` we call a [`Value`] that represents an undeclared type as +/// "ad-hoc type" or "anonymous struct" in case it represents a struct without +/// a type-name. +/// +/// The most convenient way to construct a [`Value`] is by using the [`value!`] +/// macro. +/// +/// ## Note on `usize` and `isize` +/// +/// The `serde` crate does not explicitly support `usize` and `isize`, because +/// the size of those types is platform-dependent and cannot be safely +/// serialized on one platform and deserialized on another. +/// +/// For recursive comparison purposes we try to convert `usize` and `isize` +/// values to `u64` and `i64` values respectively. If the conversion fails, we +/// try to convert them to `u128` and `i128` values. If both conversions fail, +/// we panic. +/// +/// ## Note on `f32` and `f64` +/// +/// The [`Value`] type implements the `Eq`, `Ord` and `Hash` traits. Although +/// these traits cannot be implemented for `f32` and `f64` in a mathematically +/// correct way, for the purpose of asserting whether two values are equal, +/// the naive implementation we are using is perfectly fine. +/// +/// [`value!`]: crate::prelude::value +#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum Value { + /// A boolean (`bool`) + Bool(bool), + /// A character (`char`) + Char(char), + /// A map of `Value` keys to `Value` values + Map(Map), + /// A number + Number(Number), + /// A sequence of `Value`s + Seq(Vec), + /// A string (`String` or `str`) + String(String), + /// A struct + Struct { + /// The type name of the struct + type_name: Cow<'static, str>, + /// The fields of the struct + fields: Vec, + }, + /// A struct variant of an enum + StructVariant { + /// The type name of the enum + type_name: Cow<'static, str>, + /// The name of the variant of the enum + variant: Cow<'static, str>, + /// The fields of the struct variant + fields: Vec, + }, + /// A tuple of any size + /// + /// The field name (`Field.name`) of a value in the tuple is the index into + /// the tuple represented as string. + Tuple(Vec), + /// A tuple struct of any size + TupleStruct { + /// The type name of the tuple struct + type_name: Cow<'static, str>, + /// The values in the tuple struct + /// + /// The field name (`Field.name`) of a value in the tuple is the index + /// into the tuple represented as string. + values: Vec, + }, + /// A tuple variant of an enum + TupleVariant { + /// The type name of the enum + type_name: Cow<'static, str>, + /// The name of the variant of the enum + variant: Cow<'static, str>, + /// The values in the tuple variant + /// + /// The field name (`Field.name`) of a value in the tuple is the index + /// into the tuple represented as string. + values: Vec, + }, + /// A unit variant of an enum + UnitVariant { + /// The type name of the enum + type_name: Cow<'static, str>, + /// The name of the variant of the enum + variant: Cow<'static, str>, + }, + /// The unit value `()` + Unit, +} + +impl Debug for Value { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Bool(value) => write!(f, "{value}"), + Self::Char(value) => write!(f, "{value:?}"), + Self::Map(value) => write!(f, "{value:?}"), + Self::Number(value) => write!(f, "{value:?}"), + Self::Seq(value) => write!(f, "{value:?}"), + Self::String(value) => write!(f, "{value:?}"), + Self::Struct { type_name, fields } => { + if type_name.is_empty() { + if fields.is_empty() { + f.write_str("{}") + } else { + let mut prefix = "{ "; + for Field { name, value } in fields { + f.write_str(prefix)?; + f.write_str(name)?; + f.write_str(": ")?; + value.fmt(f)?; + prefix = ", "; + } + f.write_str(" }") + } + } else { + let mut debug_struct = f.debug_struct(type_name); + for field in fields { + debug_struct.field(&field.name, &field.value); + } + debug_struct.finish() + } + }, + Self::StructVariant { + type_name: _, + variant, + fields, + } => { + let mut debug_struct = f.debug_struct(variant); + for field in fields { + debug_struct.field(&field.name, &field.value); + } + debug_struct.finish() + }, + Self::Tuple(values) => { + let mut debug_tuple = f.debug_tuple(""); + for value in values { + debug_tuple.field(&value.value); + } + debug_tuple.finish() + }, + Self::TupleStruct { type_name, values } => { + let mut debug_tuple = f.debug_tuple(type_name); + for value in values { + debug_tuple.field(&value.value); + } + debug_tuple.finish() + }, + Self::TupleVariant { + type_name: _, + variant, + values, + } => { + let mut debug_tuple = f.debug_tuple(variant); + for value in values { + debug_tuple.field(&value.value); + } + debug_tuple.finish() + }, + Self::UnitVariant { + type_name: _, + variant, + } => { + write!(f, "{variant}") + }, + Self::Unit => f.write_str("()"), + } + } +} + +fn parse_index(token: &str) -> Option { + if token.starts_with('+') || (token.starts_with('0') && token.len() != 1) { + None + } else { + token.parse().ok() + } +} + +impl Value { + /// Returns the type name of the actual value at runtime. + pub fn type_name(&self) -> Cow<'static, str> { + match self { + Self::Bool(_) => Cow::Borrowed("bool"), + Self::Char(_) => Cow::Borrowed("char"), + Self::Map(map) => map.type_name(), + Self::Number(number) => number.type_name(), + Self::Seq(seq) => { + if let Some(element) = seq.first() { + let element_type = element.type_name(); + Cow::Owned(format!("Vec<{element_type}>")) + } else { + Cow::Borrowed("Vec") + } + }, + Self::String(_) => Cow::Borrowed("String"), + Self::Tuple(values) => { + if values.is_empty() { + return Cow::Borrowed("()"); + } + let mut type_name = String::from("("); + for field in values { + type_name.push_str(&field.value.type_name()); + type_name.push_str(", "); + } + if values.len() > 1 { + type_name.pop(); + } + type_name.pop(); + type_name.push(')'); + Cow::Owned(type_name) + }, + Self::Struct { type_name, .. } + | Self::StructVariant { type_name, .. } + | Self::TupleStruct { type_name, .. } + | Self::TupleVariant { type_name, .. } + | Self::UnitVariant { type_name, .. } => type_name.clone(), + Self::Unit => Cow::Borrowed("()"), + } + } + + /// Returns a reference to the value at the given path in this `Value`. + /// + /// The [`Value`] type is a tree-like data structure. The value of a field + /// at any level can be addressed by a [`Path`]. A path describes the + /// field names that have to be traversed to reach the desired value. + /// + /// A path may look like `"order.id"` or `"customer.address.city"`. + /// + /// The values of a tuple can be addressed by their index. E.g., the path + /// `"foo.0"` returns the first value of the tuple in the foo field, the + /// path `"foo.1"` returns the second value, and so on. + /// + /// A path may also be used to index into a sequence. E.g., the path + /// `"order.items.1.amount"` returns the amount of the second item in an + /// order. + pub fn get_path(&self, path: &Path<'_>) -> Option<&Self> { + path.segments() + .iter() + .try_fold(self, |target, token| match target { + Self::Map(map) => map.get(&Self::String(token.to_string())), + Self::Seq(seq) => parse_index(token).and_then(|index| seq.get(index)), + Self::Struct { fields, .. } => fields.iter().find_map(|field| { + if field.name == *token { + Some(&field.value) + } else { + None + } + }), + Self::StructVariant { fields, .. } => fields.iter().find_map(|field| { + if field.name == *token { + Some(&field.value) + } else { + None + } + }), + Self::Tuple(values) => values.iter().find_map(|field| { + if field.name == *token { + Some(&field.value) + } else { + None + } + }), + Self::TupleStruct { values, .. } => values.iter().find_map(|field| { + if field.name == *token { + Some(&field.value) + } else { + None + } + }), + Self::TupleVariant { values, .. } => values.iter().find_map(|field| { + if field.name == *token { + Some(&field.value) + } else { + None + } + }), + _ => None, + }) + } + + /// Returns an iterator over all values of this [`Value`]. The iterator + /// traverses the values in depth-first order. + /// + /// The order of the returned field values is guaranteed to be the order + /// in which the fields are defined in the source code of a struct. + pub fn depth_first_iter(&self) -> DepthFirstIter<'_> { + DepthFirstIter::new(self) + } +} + +/// An iterator over all values of a [`Value`] that yields the values in +/// depth-first order. +pub struct DepthFirstIter<'a> { + stack: Vec<(Path<'a>, &'a Value)>, +} + +impl<'a> DepthFirstIter<'a> { + fn new(value: &'a Value) -> Self { + Self { + stack: vec![(Path::empty(), value)], + } + } +} + +impl<'a> Iterator for DepthFirstIter<'a> { + type Item = (Path<'a>, &'a Value); + + fn next(&mut self) -> Option { + loop { + if let Some((path, value)) = self.stack.pop() { + match value { + Value::Struct { fields, .. } => { + self.stack.extend(fields.iter().rev().map(|field| { + let sub_path = path.append(field.name.clone()); + (sub_path, &field.value) + })); + }, + Value::StructVariant { fields, .. } => { + self.stack.extend(fields.iter().rev().map(|field| { + let sub_path = path.append(field.name.clone()); + (sub_path, &field.value) + })); + }, + Value::Tuple(values) => { + self.stack.extend(values.iter().rev().map(|field| { + let sub_path = path.append(field.name.clone()); + (sub_path, &field.value) + })); + }, + Value::TupleStruct { values, .. } => { + self.stack.extend(values.iter().rev().map(|field| { + let sub_path = path.append(field.name.clone()); + (sub_path, &field.value) + })); + }, + Value::TupleVariant { values, .. } => { + self.stack.extend(values.iter().rev().map(|field| { + let sub_path = path.append(field.name.clone()); + (sub_path, &field.value) + })); + }, + value @ (Value::Bool(_) + | Value::Char(_) + | Value::Map(_) + | Value::Number(_) + | Value::Seq(_) + | Value::String(_) + | Value::UnitVariant { .. } + | Value::Unit) => return Some((path, value)), + } + } else { + return None; + } + } + } +} + +/// Constructs a [`Field`] with the given name and value. +pub fn field(name: impl Into>, value: Value) -> Field { + Field { + name: name.into(), + value, + } +} + +/// Constructs a [`Value`] representing an anonymous struct. +/// +/// Although Rust does not have anonymous structs, [`Value`]s created by this +/// function can be used as the expected value in the [`is_equivalent_to`] +/// assertion. +/// +/// [`is_equivalent_to`]: crate::assertions::AssertEquivalence::is_equivalent_to +pub fn struct_with_fields(fields: impl IntoIterator) -> Value +where + T: Into, +{ + struct_("", fields) +} + +/// Constructs a [`Value`] representing a struct with the given type name and +/// fields. +/// +/// The fields can be created either by calling the [`field`] function or by +/// specifying a tuple of field-name and value. +/// +/// # Examples +/// +/// Creating a [`Value`] for a struct with fields. The fields are created using +/// the [`field`] function: +/// +/// ``` +/// # use asserting::recursive_comparison::value::*; +/// let value = struct_("MyDto", [ +/// field("name", string("Silvia")), +/// field("age", uint8(25)), +/// field("confirmed", bool(true)), +/// ]); +/// ``` +/// +/// Creating a [`Value`] for a struct with fields. The fields are specified as +/// tuples of field-name and value: +/// +/// ``` +/// # use asserting::recursive_comparison::value::*; +/// let value = struct_("MyDto", [ +/// ("name", string("Silvia")), +/// ("age", uint8(25)), +/// ("confirmed", bool(true)), +/// ]); +/// ``` +pub fn struct_( + type_name: impl Into>, + fields: impl IntoIterator, +) -> Value +where + T: Into, +{ + Value::Struct { + type_name: type_name.into(), + fields: fields.into_iter().map(Into::into).collect(), + } +} + +/// Constructs a [`Value`] representing a struct-variant of an enum. +/// +/// # Examples +/// +/// ``` +/// # use asserting::recursive_comparison::value::*; +/// let value = struct_variant("Foo", "Bar", [ +/// ("name", string("Silvia")), +/// ("visit_count", uint32(12)), +/// ]); +/// ``` +pub fn struct_variant( + type_name: impl Into>, + variant: impl Into>, + fields: impl IntoIterator, +) -> Value +where + T: Into, +{ + Value::StructVariant { + type_name: type_name.into(), + variant: variant.into(), + fields: fields.into_iter().map(Into::into).collect(), + } +} + +/// Constructs a [`Value`] representing a tuple-struct. +/// +/// # Examples +/// +/// ``` +/// # use asserting::recursive_comparison::value::*; +/// let value = tuple_struct("Velocity", [ +/// float32(1.2) +/// ]); +/// ``` +pub fn tuple_struct( + type_name: impl Into>, + values: impl IntoIterator, +) -> Value { + Value::TupleStruct { + type_name: type_name.into(), + values: values + .into_iter() + .enumerate() + .map(|(index, value)| Field { + name: index.to_string().into(), + value, + }) + .collect(), + } +} + +/// Constructs a [`Value`] representing a tuple. +/// +/// # Examples +/// +/// ``` +/// # use asserting::recursive_comparison::value::*; +/// let value = tuple([ +/// string("Dog"), +/// bool(false), +/// int16(144), +/// ]); +/// ``` +pub fn tuple(values: impl IntoIterator) -> Value { + let fields = values + .into_iter() + .enumerate() + .map(|(index, value)| Field { + name: index.to_string().into(), + value, + }) + .collect::>(); + if fields.is_empty() { + return Value::Unit; + } + Value::Tuple(fields) +} + +/// Constructs a [`Value`] representing a tuple-variant of an enum. +/// +/// # Examples +/// +/// ``` +/// # use asserting::recursive_comparison::value::*; +/// use asserting::recursive_comparison::value::Value::TupleStruct; +/// let value = tuple_variant("Animal", "Cat", [ +/// string("Mimi"), +/// uint8(7), +/// tuple_variant("Color", "Rgb", [uint8(200), uint8(180), uint8(26)]), +/// ]); +/// ``` +pub fn tuple_variant( + type_name: impl Into>, + variant: impl Into>, + values: impl IntoIterator, +) -> Value { + Value::TupleVariant { + type_name: type_name.into(), + variant: variant.into(), + values: values + .into_iter() + .enumerate() + .map(|(index, value)| Field { + name: index.to_string().into(), + value, + }) + .collect(), + } +} + +/// Constructs a [`Value`] representing a unit struct value. +/// +/// # Examples +/// +/// ``` +/// # use serde::Serialize; +/// # use asserting::recursive_comparison::serialize::to_recursive_value; +/// # use asserting::recursive_comparison::value::unit_struct; +/// #[derive(Serialize)] +/// struct Foo; +/// +/// assert_eq!(to_recursive_value(&Foo), Ok(unit_struct("Foo"))); +/// ``` +pub fn unit_struct(type_name: impl Into>) -> Value { + Value::Struct { + type_name: type_name.into(), + fields: vec![], + } +} + +/// Constructs a [`Value`] representing a unit variant of an enum. +/// +/// # Examples +/// +/// ``` +/// # use asserting::recursive_comparison::value::unit_variant; +/// # use asserting::recursive_comparison::serialize::to_recursive_value; +/// let maybe: Option = None; +/// +/// assert_eq!(to_recursive_value(&maybe), Ok(unit_variant("Option", "None"))); +/// ``` +pub fn unit_variant( + type_name: impl Into>, + variant: impl Into>, +) -> Value { + Value::UnitVariant { + type_name: type_name.into(), + variant: variant.into(), + } +} + +/// Constructs a [`Value`] representing the unit type `()`. +pub fn unit() -> Value { + Value::Unit +} + +/// Constructs a [`Value`] representing an array of values. +/// +/// Arrays are represented as tuples in a [`Value`]. This is because an array +/// has a fixed size known at compile time and `serde` serializes fixed-sized +/// types as tuples. +pub fn array(values: impl IntoIterator) -> Value { + tuple(values) +} + +/// Constructs a [`Value`] representing the given `bool` value. +pub fn bool(value: bool) -> Value { + Value::Bool(value) +} + +/// Constructs a [`Value`] representing the given `i8` value. +pub fn int8(value: i8) -> Value { + Value::Number(value.into()) +} + +/// Constructs a [`Value`] representing the given `i16` value. +pub fn int16(value: i16) -> Value { + Value::Number(value.into()) +} + +/// Constructs a [`Value`] representing the given `i32` value. +pub fn int32(value: i32) -> Value { + Value::Number(value.into()) +} + +/// Constructs a [`Value`] representing the given `i64` value. +pub fn int64(value: i64) -> Value { + Value::Number(value.into()) +} + +/// Constructs a [`Value`] representing the given `i128` value. +pub fn int128(value: i128) -> Value { + Value::Number(value.into()) +} + +/// Constructs a [`Value`] representing the given `isize` value. +/// +/// As `serde` does not support `isize`, this method tries to convert the given +/// value to `i64`. If the conversion fails, it tries to convert it to `i128`. +/// If both conversions fail, it panics. +/// +/// # Panics +/// +/// This method panics if the given `isize` value cannot be converted to `i64` +/// or `i128`. +pub fn isize(value: isize) -> Value { + Result::unwrap_or_else(i64::try_from(value).map(int64), |_| { + i128::try_from(value).map_or_else( + |err| panic!("can not convert isize to `Value`: {err}"), + int128, + ) + }) +} + +/// Constructs a [`Value`] representing the given `u8` value. +pub fn uint8(value: u8) -> Value { + Value::Number(value.into()) +} + +/// Constructs a [`Value`] representing the given `u16` value. +pub fn uint16(value: u16) -> Value { + Value::Number(value.into()) +} + +/// Constructs a [`Value`] representing the given `u32` value. +pub fn uint32(value: u32) -> Value { + Value::Number(value.into()) +} + +/// Constructs a [`Value`] representing the given `u64` value. +pub fn uint64(value: u64) -> Value { + Value::Number(value.into()) +} + +/// Constructs a [`Value`] representing the given `u128` value. +pub fn uint128(value: u128) -> Value { + Value::Number(value.into()) +} + +/// Constructs a [`Value`] representing the given `usize` value. +/// +/// As `serde` does not support `usize`, this method tries to convert the given +/// value to `u64`. If the conversion fails, it tries to convert it to `u128`. +/// If both conversions fail, it panics. +/// +/// # Panics +/// +/// This method panics if the given `usize` value cannot be converted to `u64` +/// or `u128`. +pub fn usize(value: usize) -> Value { + Result::unwrap_or_else(u64::try_from(value).map(uint64), |_| { + u128::try_from(value).map_or_else( + |err| panic!("can not convert usize to `Value`: {err}"), + uint128, + ) + }) +} + +/// Constructs a [`Value`] representing the given `f32` value. +pub fn float32(value: f32) -> Value { + Value::Number(value.into()) +} + +/// Constructs a [`Value`] representing the given `f64` value. +pub fn float64(value: f64) -> Value { + Value::Number(value.into()) +} + +/// Constructs a [`Value`] representing the given `char` value. +pub fn char(value: char) -> Value { + Value::Char(value) +} + +/// Constructs a [`Value`] representing the given `String` or `&str` value. +pub fn string(value: impl Into) -> Value { + Value::String(value.into()) +} + +/// Constructs a [`Value`] representing the given sequence of `Value` values. +pub fn seq(values: impl IntoIterator) -> Value { + Value::Seq(Vec::from_iter(values)) +} + +/// Constructs a [`Value`] representing the given map of key-value pairs. +pub fn map(values: impl IntoIterator) -> Value { + Value::Map(Map::from_iter(values)) +} + +/// Constructs a [`Value`] representing the given value as the `Some` variant of +/// an `Option`. +pub fn some(value: Value) -> Value { + Value::TupleVariant { + type_name: "Option".into(), + variant: "Some".into(), + values: vec![Field { + name: "0".into(), + value, + }], + } +} + +/// Constructs a [`Value`] representing the `None` value of an `Option`. +pub fn none() -> Value { + Value::UnitVariant { + type_name: "Option".into(), + variant: "None".into(), + } +} + +#[cfg(test)] +mod tests; diff --git a/src/recursive_comparison/value/number/mod.rs b/src/recursive_comparison/value/number/mod.rs new file mode 100644 index 0000000..945a1ae --- /dev/null +++ b/src/recursive_comparison/value/number/mod.rs @@ -0,0 +1,249 @@ +//! Defines the [`Number`] type which represents numbers in the [`Value`] +//! data structure used for recursive comparison. +//! +//! [`Value`]: crate::recursive_comparison::value::Value + +use crate::std::borrow::Cow; +use crate::std::fmt::{self, Debug, Display}; + +/// Represents a number in the [`Value`] data structure used for recursive +/// comparison. +/// +/// It can hold integer and float values of different sizes. +/// +/// # Note on `usize` and `isize` +/// +/// The `serde` crate does not explicitly support `usize` and `isize`, because +/// the size of those types is platform-dependent and cannot be safely +/// serialized on one platform and deserialized on another. +/// +/// For recursive comparison purposes we try to convert `usize` and `isize` +/// values to `u64` and `i64` values respectively. If the conversion fails, we +/// try to convert them to `u128` and `i128` values. If both conversions fail, +/// we panic. +/// +/// # Note on `f32` and `f64` +/// +/// The [`Value`] type implements the `Eq`, `Ord` and `Hash` traits. Although +/// these traits cannot be implemented for `f32` and `f64` in a mathematically +/// correct way, for the purpose of asserting whether two values are equal, +/// the naive implementation we are using is perfectly fine. +/// +/// [`Value`]: crate::recursive_comparison::value::Value +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum Number { + /// An `i8` signed integer + I8(i8), + /// An `i16` signed integer + I16(i16), + /// An `i32` signed integer + I32(i32), + /// An `i64` signed integer + I64(i64), + /// An `i128` signed integer + I128(i128), + /// An `u8` unsigned integer + U8(u8), + /// An `u16` unsigned integer + U16(u16), + /// An `u32` unsigned integer + U32(u32), + /// An `u64` unsigned integer + U64(u64), + /// An `u128` unsigned integer + U128(u128), + /// A `f32` floating point number + F32(F32), + /// A `f64` floating point number + F64(F64), +} + +impl Number { + /// Returns the type name of the number variant as a string. + pub fn type_name(&self) -> Cow<'static, str> { + match self { + Self::I8(_) => Cow::Borrowed("i8"), + Self::I16(_) => Cow::Borrowed("i16"), + Self::I32(_) => Cow::Borrowed("i32"), + Self::I64(_) => Cow::Borrowed("i64"), + Self::I128(_) => Cow::Borrowed("i128"), + Self::U8(_) => Cow::Borrowed("u8"), + Self::U16(_) => Cow::Borrowed("u16"), + Self::U32(_) => Cow::Borrowed("u32"), + Self::U64(_) => Cow::Borrowed("u64"), + Self::U128(_) => Cow::Borrowed("u128"), + Self::F32(_) => Cow::Borrowed("f32"), + Self::F64(_) => Cow::Borrowed("f64"), + } + } +} + +impl Debug for Number { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::I8(value) => write!(f, "{value:?}"), + Self::I16(value) => write!(f, "{value:?}"), + Self::I32(value) => write!(f, "{value:?}"), + Self::I64(value) => write!(f, "{value:?}"), + Self::I128(value) => write!(f, "{value:?}"), + Self::U8(value) => write!(f, "{value:?}"), + Self::U16(value) => write!(f, "{value:?}"), + Self::U32(value) => write!(f, "{value:?}"), + Self::U64(value) => write!(f, "{value:?}"), + Self::U128(value) => write!(f, "{value:?}"), + Self::F32(value) => write!(f, "{value:?}"), + Self::F64(value) => write!(f, "{value:?}"), + } + } +} + +impl Display for Number { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::I8(value) => write!(f, "{value}"), + Self::I16(value) => write!(f, "{value}"), + Self::I32(value) => write!(f, "{value}"), + Self::I64(value) => write!(f, "{value}"), + Self::I128(value) => write!(f, "{value}"), + Self::U8(value) => write!(f, "{value}"), + Self::U16(value) => write!(f, "{value}"), + Self::U32(value) => write!(f, "{value}"), + Self::U64(value) => write!(f, "{value}"), + Self::U128(value) => write!(f, "{value}"), + Self::F32(value) => write!(f, "{value}"), + Self::F64(value) => write!(f, "{value}"), + } + } +} + +macro_rules! define_float_type { + ($ty:ident($float:ty)) => { + /// A wrapper around a float value. + /// + /// This wrapper enables us to provide implementations for the + /// `Eq`, `Ord`, and `Hash` traits. + #[derive(Clone, Copy)] + pub struct $ty(pub $float); + + impl $ty { + /// Returns the underlying float value. + pub fn val(self) -> $float { + self.0 + } + } + + impl crate::std::convert::From<$float> for $ty { + fn from(val: $float) -> Self { + Self(val) + } + } + + impl crate::std::borrow::Borrow<$float> for $ty { + fn borrow(&self) -> &$float { + &self.0 + } + } + + impl crate::std::ops::Deref for $ty { + type Target = $float; + + fn deref(&self) -> &Self::Target { + &self.0 + } + } + + impl crate::std::ops::DerefMut for $ty { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } + } + + impl crate::std::fmt::Debug for $ty { + fn fmt(&self, f: &mut crate::std::fmt::Formatter<'_>) -> crate::std::fmt::Result { + crate::std::write!(f, "{:?}", self.0) + } + } + + impl crate::std::hash::Hash for $ty { + fn hash(&self, state: &mut H) + where + H: crate::std::hash::Hasher, + { + if self.0 == -0. { + let zero: $float = 0.; + return zero.to_bits().hash(state); + } + self.0.to_bits().hash(state) + } + } + + impl crate::std::cmp::Ord for $ty { + fn cmp(&self, other: &Self) -> crate::std::cmp::Ordering { + self.0.total_cmp(&other.0) + } + } + + impl crate::std::cmp::PartialOrd for $ty { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } + } + + impl crate::std::cmp::PartialEq for $ty { + fn eq(&self, other: &Self) -> bool { + if self.0 == 0. && other.0 == -0. { + return true; + } + self.cmp(other).is_eq() + } + } + + impl Eq for $ty {} + + impl crate::std::fmt::Display for $ty { + fn fmt(&self, f: &mut crate::std::fmt::Formatter<'_>) -> crate::std::fmt::Result { + crate::std::write!(f, "{}", self.0) + } + } + }; +} + +define_float_type! { F32(f32) } +define_float_type! { F64(f64) } + +macro_rules! impl_number_from_float { + ($ty:ty => $variant:ident) => { + impl crate::std::convert::From<$ty> for Number { + fn from(value: $ty) -> Self { + Self::$variant(value.into()) + } + } + }; +} + +impl_number_from_float! { f32 => F32 } +impl_number_from_float! { f64 => F64 } + +macro_rules! impl_number_from_integer { + ($ty:ty => $variant:ident) => { + impl crate::std::convert::From<$ty> for Number { + fn from(value: $ty) -> Self { + Self::$variant(value) + } + } + }; +} + +impl_number_from_integer! { i8 => I8 } +impl_number_from_integer! { i16 => I16 } +impl_number_from_integer! { i32 => I32 } +impl_number_from_integer! { i64 => I64 } +impl_number_from_integer! { i128 => I128 } +impl_number_from_integer! { u8 => U8 } +impl_number_from_integer! { u16 => U16 } +impl_number_from_integer! { u32 => U32 } +impl_number_from_integer! { u64 => U64 } +impl_number_from_integer! { u128 => U128 } + +#[cfg(test)] +mod tests; diff --git a/src/recursive_comparison/value/number/tests.rs b/src/recursive_comparison/value/number/tests.rs new file mode 100644 index 0000000..c171eda --- /dev/null +++ b/src/recursive_comparison/value/number/tests.rs @@ -0,0 +1,479 @@ +use super::*; +use crate::recursive_comparison::value::proptest_support::*; +use crate::std::cmp::Ordering; +use crate::std::format; +use crate::std::hash::BuildHasher; +use hashbrown::DefaultHashBuilder; +use proptest::prelude::*; + +proptest! { + #[test] + fn number_from_i8_integer( + value in any::() + ) { + let number = Number::from(value); + + prop_assert_eq!(number, Number::I8(value)); + } + + #[test] + fn number_from_i16_integer( + value in any::() + ) { + let number = Number::from(value); + + prop_assert_eq!(number, Number::I16(value)); + } + + #[test] + fn number_from_i32_integer( + value in any::() + ) { + let number = Number::from(value); + + prop_assert_eq!(number, Number::I32(value)); + } + + #[test] + fn number_from_i64_integer( + value in any::() + ) { + let number = Number::from(value); + + prop_assert_eq!(number, Number::I64(value)); + } + + #[test] + fn number_from_i128_integer( + value in any::() + ) { + let number = Number::from(value); + + prop_assert_eq!(number, Number::I128(value)); + } + + #[test] + fn number_from_u8_integer( + value in any::() + ) { + let number = Number::from(value); + + prop_assert_eq!(number, Number::U8(value)); + } + + #[test] + fn number_from_u16_integer( + value in any::() + ) { + let number = Number::from(value); + + prop_assert_eq!(number, Number::U16(value)); + } + + #[test] + fn number_from_u32_integer( + value in any::() + ) { + let number = Number::from(value); + + prop_assert_eq!(number, Number::U32(value)); + } + + #[test] + fn number_from_u64_integer( + value in any::() + ) { + let number = Number::from(value); + + prop_assert_eq!(number, Number::U64(value)); + } + + #[test] + fn number_from_u128_integer( + value in any::() + ) { + let number = Number::from(value); + + prop_assert_eq!(number, Number::U128(value)); + } + + #[test] + fn number_from_f32_integer( + value in any::() + ) { + let number = Number::from(value); + + prop_assert_eq!(number, Number::F32(F32(value))); + } + + #[test] + fn number_from_f64_integer( + value in any::() + ) { + let number = Number::from(value); + + prop_assert_eq!(number, Number::F64(F64(value))); + } +} + +proptest! { + #[test] + fn newtype_f32_from_f32( + value in any::() + ) { + let float = F32::from(value); + + prop_assert_eq!(float, F32(value)); + } + + #[test] + fn newtype_f64_from_f64( + value in any::() + ) { + let float = F64::from(value); + + prop_assert_eq!(float, F64(value)); + } + + #[test] + fn newtype_f32_debug_string( + value in any_f32_newtype() + ) { + prop_assert_eq!(format!("{:?}", value), format!("{:?}", value.0)); + } + + #[test] + fn newtype_f64_debug_string( + value in any_f64_newtype() + ) { + prop_assert_eq!(format!("{:?}", value), format!("{:?}", value.0)); + } + + #[test] + fn newtype_f32_display_string( + value in any_f32_newtype() + ) { + prop_assert_eq!(format!("{}", value), format!("{}", value.0)); + } + + #[test] + fn newtype_f64_display_string( + value in any_f64_newtype() + ) { + prop_assert_eq!(format!("{}", value), format!("{}", value.0)); + } + + #[allow(clippy::float_cmp)] + #[test] + fn newtype_f32_val( + value in any_f32_newtype() + ) { + prop_assert_eq!(value.val(), value.0); + } + + #[allow(clippy::float_cmp)] + #[test] + fn newtype_f64_val( + value in any_f64_newtype() + ) { + prop_assert_eq!(value.val(), value.0); + } +} + +#[test] +fn newtype_f32_eq_nan() { + assert_eq!(F32(f32::NAN), F32(f32::NAN)); + assert_ne!(F32(f32::NAN), F32(0.)); + assert_ne!(F32(f32::NAN), F32(f32::MAX)); + assert_ne!(F32(f32::NAN), F32(f32::MIN)); +} + +#[test] +fn newtype_f32_eq_infinity() { + assert_eq!(F32(f32::INFINITY), F32(f32::INFINITY)); + assert_ne!(F32(f32::INFINITY), F32(f32::NEG_INFINITY)); +} + +#[test] +fn newtype_f32_eq_negative_infinity() { + assert_eq!(F32(f32::NEG_INFINITY), F32(f32::NEG_INFINITY)); + assert_ne!(F32(f32::NEG_INFINITY), F32(f32::INFINITY)); +} + +#[test] +fn newtype_f32_cmp_nan() { + assert_eq!( + F32(f32::NAN).partial_cmp(&F32(f32::NAN)), + Some(Ordering::Equal) + ); + assert_eq!(F32(f32::NAN).partial_cmp(&F32(0.)), Some(Ordering::Greater)); + assert_eq!( + F32(f32::NAN).partial_cmp(&F32(f32::MAX)), + Some(Ordering::Greater) + ); + assert_eq!( + F32(f32::NAN).partial_cmp(&F32(f32::MIN)), + Some(Ordering::Greater) + ); + + assert_eq!(F32(f32::NAN).cmp(&F32(f32::NAN)), Ordering::Equal); + assert_eq!(F32(f32::NAN).cmp(&F32(0.)), Ordering::Greater); + assert_eq!(F32(f32::NAN).cmp(&F32(f32::MAX)), Ordering::Greater); + assert_eq!(F32(f32::NAN).cmp(&F32(f32::MIN)), Ordering::Greater); +} + +#[test] +fn newtype_f32_cmp_infinity() { + assert_eq!( + F32(f32::INFINITY).partial_cmp(&F32(f32::INFINITY)), + Some(Ordering::Equal) + ); + assert_eq!( + F32(f32::INFINITY).partial_cmp(&F32(f32::NEG_INFINITY)), + Some(Ordering::Greater) + ); + + assert_eq!(F32(f32::INFINITY).cmp(&F32(f32::INFINITY)), Ordering::Equal); + assert_eq!( + F32(f32::INFINITY).cmp(&F32(f32::NEG_INFINITY)), + Ordering::Greater + ); +} + +#[test] +fn newtype_f32_cmp_negative_infinity() { + assert_eq!( + F32(f32::NEG_INFINITY).partial_cmp(&F32(f32::NEG_INFINITY)), + Some(Ordering::Equal) + ); + assert_eq!( + F32(f32::NEG_INFINITY).partial_cmp(&F32(f32::INFINITY)), + Some(Ordering::Less) + ); + + assert_eq!( + F32(f32::NEG_INFINITY).cmp(&F32(f32::NEG_INFINITY)), + Ordering::Equal + ); + assert_eq!( + F32(f32::NEG_INFINITY).cmp(&F32(f32::INFINITY)), + Ordering::Less + ); +} + +#[allow(clippy::float_cmp)] +#[test] +fn newtype_f32_cmp_zero_and_negative_zero() { + assert_eq!(F32(0.).partial_cmp(&F32(-0.)), Some(Ordering::Greater)); + assert_eq!(F32(0.).cmp(&F32(-0.)), Ordering::Greater); + assert_eq!(F32(0.), F32(-0.)); + + assert_eq!(0_f32.total_cmp(&-0_f32), Ordering::Greater); + assert_eq!(0_f32, -0_f32); +} + +#[test] +fn newtype_f32_hash() { + let hash_builder = DefaultHashBuilder::default(); + + let nan_hash = hash_builder.hash_one(F32(f32::NAN)); + let inf_hash = hash_builder.hash_one(F32(f32::INFINITY)); + let neg_inf_hash = hash_builder.hash_one(F32(f32::NEG_INFINITY)); + + assert_ne!(nan_hash, inf_hash); + assert_ne!(nan_hash, neg_inf_hash); + assert_ne!(inf_hash, neg_inf_hash); + + let zero_hash = hash_builder.hash_one(F64(0.)); + let neg_zero_hash = hash_builder.hash_one(F64(-0.)); + + assert_eq!(zero_hash, neg_zero_hash); + + assert_ne!(zero_hash, nan_hash); + assert_ne!(zero_hash, inf_hash); + assert_ne!(neg_zero_hash, nan_hash); + assert_ne!(neg_zero_hash, neg_inf_hash); +} + +#[test] +fn newtype_f64_eq_nan() { + assert_eq!(F64(f64::NAN), F64(f64::NAN)); + assert_ne!(F64(f64::NAN), F64(0.)); + assert_ne!(F64(f64::NAN), F64(f64::MAX)); + assert_ne!(F64(f64::NAN), F64(f64::MIN)); +} + +#[test] +fn newtype_f64_eq_infinity() { + assert_eq!(F64(f64::INFINITY), F64(f64::INFINITY)); + assert_ne!(F64(f64::INFINITY), F64(f64::NEG_INFINITY)); +} + +#[test] +fn newtype_f64_eq_negative_infinity() { + assert_eq!(F64(f64::NEG_INFINITY), F64(f64::NEG_INFINITY)); + assert_ne!(F64(f64::NEG_INFINITY), F64(f64::INFINITY)); +} + +#[test] +fn newtype_f64_cmp_nan() { + assert_eq!( + F64(f64::NAN).partial_cmp(&F64(f64::NAN)), + Some(Ordering::Equal) + ); + assert_eq!(F64(f64::NAN).partial_cmp(&F64(0.)), Some(Ordering::Greater)); + assert_eq!( + F64(f64::NAN).partial_cmp(&F64(f64::MAX)), + Some(Ordering::Greater) + ); + assert_eq!( + F64(f64::NAN).partial_cmp(&F64(f64::MIN)), + Some(Ordering::Greater) + ); + + assert_eq!(F64(f64::NAN).cmp(&F64(f64::NAN)), Ordering::Equal); + assert_eq!(F64(f64::NAN).cmp(&F64(0.)), Ordering::Greater); + assert_eq!(F64(f64::NAN).cmp(&F64(f64::MAX)), Ordering::Greater); + assert_eq!(F64(f64::NAN).cmp(&F64(f64::MIN)), Ordering::Greater); +} + +#[test] +fn newtype_f64_cmp_infinity() { + assert_eq!( + F64(f64::INFINITY).partial_cmp(&F64(f64::INFINITY)), + Some(Ordering::Equal) + ); + assert_eq!( + F64(f64::INFINITY).partial_cmp(&F64(f64::NEG_INFINITY)), + Some(Ordering::Greater) + ); + + assert_eq!(F64(f64::INFINITY).cmp(&F64(f64::INFINITY)), Ordering::Equal); + assert_eq!( + F64(f64::INFINITY).cmp(&F64(f64::NEG_INFINITY)), + Ordering::Greater + ); +} + +#[test] +fn newtype_f64_cmp_negative_infinity() { + assert_eq!( + F64(f64::NEG_INFINITY).partial_cmp(&F64(f64::NEG_INFINITY)), + Some(Ordering::Equal) + ); + assert_eq!( + F64(f64::NEG_INFINITY).partial_cmp(&F64(f64::INFINITY)), + Some(Ordering::Less) + ); + + assert_eq!( + F64(f64::NEG_INFINITY).cmp(&F64(f64::NEG_INFINITY)), + Ordering::Equal + ); + assert_eq!( + F64(f64::NEG_INFINITY).cmp(&F64(f64::INFINITY)), + Ordering::Less + ); +} + +#[allow(clippy::float_cmp)] +#[test] +fn newtype_f64_cmp_zero_and_negative_zero() { + assert_eq!(F64(0.).partial_cmp(&F64(-0.)), Some(Ordering::Greater)); + assert_eq!(F64(0.).cmp(&F64(-0.)), Ordering::Greater); + assert_eq!(F64(0.), F64(-0.)); + + assert_eq!(0_f64.total_cmp(&-0_f64), Ordering::Greater); + assert_eq!(0_f64, -0_f64); +} + +#[test] +fn newtype_f64_hash() { + let hash_builder = DefaultHashBuilder::default(); + + let nan_hash = hash_builder.hash_one(F64(f64::NAN)); + let inf_hash = hash_builder.hash_one(F64(f64::INFINITY)); + let neg_inf_hash = hash_builder.hash_one(F64(f64::NEG_INFINITY)); + + assert_ne!(nan_hash, inf_hash); + assert_ne!(nan_hash, neg_inf_hash); + assert_ne!(inf_hash, neg_inf_hash); + + let zero_hash = hash_builder.hash_one(F64(0.)); + let neg_zero_hash = hash_builder.hash_one(F64(-0.)); + + assert_eq!(zero_hash, neg_zero_hash); + + assert_ne!(zero_hash, nan_hash); + assert_ne!(zero_hash, inf_hash); + assert_ne!(neg_zero_hash, nan_hash); + assert_ne!(neg_zero_hash, neg_inf_hash); +} + +proptest! { + #[test] + fn number_from_primitive( + number in any_number() + ) { + let value = match number { + Number::I8(val) => Number::from(val), + Number::I16(val) => Number::from(val), + Number::I32(val) => Number::from(val), + Number::I64(val) => Number::from(val), + Number::I128(val) => Number::from(val), + Number::U8(val) => Number::from(val), + Number::U16(val) => Number::from(val), + Number::U32(val) => Number::from(val), + Number::U64(val) => Number::from(val), + Number::U128(val) => Number::from(val), + Number::F32(F32(val)) => Number::from(val), + Number::F64(F64(val)) => Number::from(val), + }; + + prop_assert_eq!(value, number); + } + + #[test] + fn number_debug_string( + number in any_number() + ) { + let primitive_debug_string = match number { + Number::I8(val) => format!("{val:?}"), + Number::I16(val) => format!("{val:?}"), + Number::I32(val) => format!("{val:?}"), + Number::I64(val) => format!("{val:?}"), + Number::I128(val) => format!("{val:?}"), + Number::U8(val) => format!("{val:?}"), + Number::U16(val) => format!("{val:?}"), + Number::U32(val) => format!("{val:?}"), + Number::U64(val) => format!("{val:?}"), + Number::U128(val) => format!("{val:?}"), + Number::F32(F32(val)) => format!("{val:?}"), + Number::F64(F64(val)) => format!("{val:?}"), + }; + + prop_assert_eq!(format!("{number:?}"), primitive_debug_string); + } + + #[test] + fn number_display_string( + number in any_number() + ) { + let primitive_display_string = match number { + Number::I8(val) => format!("{val}"), + Number::I16(val) => format!("{val}"), + Number::I32(val) => format!("{val}"), + Number::I64(val) => format!("{val}"), + Number::I128(val) => format!("{val}"), + Number::U8(val) => format!("{val}"), + Number::U16(val) => format!("{val}"), + Number::U32(val) => format!("{val}"), + Number::U64(val) => format!("{val}"), + Number::U128(val) => format!("{val}"), + Number::F32(F32(val)) => format!("{val}"), + Number::F64(F64(val)) => format!("{val}"), + }; + + prop_assert_eq!(format!("{number}"), primitive_display_string); + } +} diff --git a/src/recursive_comparison/value/proptest_support.rs b/src/recursive_comparison/value/proptest_support.rs new file mode 100644 index 0000000..1458674 --- /dev/null +++ b/src/recursive_comparison/value/proptest_support.rs @@ -0,0 +1,47 @@ +//! Support for property-based testing with the [`proptest`] crate. +//! +//! This module mainly provides methods that provide [`Strategy`]s for +//! generating arbitrary values of type [`Value`] and [`Number`]. + +use crate::recursive_comparison::value::{Number, Value, F32, F64}; +use crate::std::string::String; +use crate::std::vec; +use proptest::prelude::*; + +/// Returns a [`Strategy`] for generating arbitrary values of type [`Value`]. +pub fn any_value() -> impl Strategy { + prop_oneof![ + any::().prop_map(Value::Bool), + any::().prop_map(Value::Char), + any_number().prop_map(Value::Number), + any::().prop_map(Value::String), + ] +} + +/// Returns a [`Strategy`] for generating arbitrary values of type [`Number`]. +pub fn any_number() -> impl Strategy { + prop_oneof![ + any::().prop_map(Number::from), + any::().prop_map(Number::from), + any::().prop_map(Number::from), + any::().prop_map(Number::from), + any::().prop_map(Number::from), + any::().prop_map(Number::from), + any::().prop_map(Number::from), + any::().prop_map(Number::from), + any::().prop_map(Number::from), + any::().prop_map(Number::from), + any::().prop_map(Number::from), + any::().prop_map(Number::from), + ] +} + +/// Returns a [`Strategy`] for generating arbitrary values of type [`F32`]. +pub fn any_f32_newtype() -> impl Strategy { + any::().prop_map(F32) +} + +/// Returns a [`Strategy`] for generating arbitrary values of type [`F64`]. +pub fn any_f64_newtype() -> impl Strategy { + any::().prop_map(F64) +} diff --git a/src/recursive_comparison/value/tests.rs b/src/recursive_comparison/value/tests.rs new file mode 100644 index 0000000..1e18b72 --- /dev/null +++ b/src/recursive_comparison/value/tests.rs @@ -0,0 +1,899 @@ +use super::*; +use crate::recursive_comparison::serialize::to_recursive_value; +use crate::std::string::ToString; +use indexmap::IndexMap; +use serde::Serialize; + +#[derive(Serialize, Debug)] +struct Foo { + text: String, + age: u8, + qux: Qux, + bytes: Vec, + precision: Option, + array: Vec, + grouped: IndexMap<&'static str, Vec>, + pair: (usize, &'static str), + bar: Bar, + baz: Baz, + samples: Vec, +} + +#[derive(Serialize, Debug)] +struct Qux { + name: String, + corge: Corge, + baz: Baz, +} + +#[derive(Serialize, Debug)] +struct Corge { + grault: bool, + tinu: (), +} + +#[derive(Serialize, Debug)] +enum Sample { + One, + Two(i64), + Three(u64, String), + Four { left: String, right: char }, +} + +#[derive(Serialize, Debug)] +struct Bar(i32); + +#[derive(Serialize, Debug)] +struct Baz(usize, Vec); + +impl Default for Bar { + fn default() -> Self { + Self(-99) + } +} + +impl Default for Baz { + fn default() -> Self { + Self(122, vec![0, 0, -1, 1, -2, 2]) + } +} + +impl Default for Qux { + fn default() -> Self { + Self { + name: "Silvia".to_string(), + corge: Corge { + grault: true, + tinu: (), + }, + baz: Baz(99, vec![100, 666, -100]), + } + } +} + +impl Default for Foo { + fn default() -> Self { + Self { + text: "magna laborum".to_string(), + age: 21, + qux: Qux::default(), + bytes: vec![24, 17, 64, 19], + precision: Some(2.5), + array: vec![12, -8, -34, 55, 76], + grouped: IndexMap::from_iter([("old", vec![1, -1]), ("new", vec![-1, 0, 1])]), + pair: (123_456, "sit wisi"), + bar: Bar::default(), + baz: Baz::default(), + samples: vec![ + Sample::One, + Sample::Two(22), + Sample::Three(33, "amet".into()), + Sample::Four { + left: "dolores".into(), + right: 'v', + }, + ], + } + } +} + +#[test] +fn debug_string_of_field() { + let field = Field { + name: "foo".into(), + value: Value::Bool(true), + }; + + assert_eq!(format!("{field:?}"), "foo: true"); +} + +#[test] +fn debug_string_of_value() { + let foo = Foo::default(); + + let value = to_recursive_value(&foo).unwrap_or_else(|err| panic!("{err:?}")); + + assert_eq!(format!("{value:?}"), format!("{foo:?}")); +} + +#[test] +fn debug_string_of_empty_struct() { + #[derive(Serialize, Debug)] + struct AnEmptyStruct {} + + let data = AnEmptyStruct {}; + + let value = to_recursive_value(&data).unwrap_or_else(|err| panic!("{err:?}")); + + assert_eq!(format!("{value:?}"), format!("{data:?}")); +} + +#[test] +fn debug_string_of_anonymous_struct() { + let value = struct_( + "", + vec![ + Field { + name: "foo".into(), + value: Value::Bool(true), + }, + Field { + name: "bar".into(), + value: Value::Number(Number::I64(42)), + }, + ], + ); + + assert_eq!(format!("{value:?}"), "{ foo: true, bar: 42 }"); +} + +#[test] +fn debug_string_of_empty_anonymous_struct() { + let value = struct_("", Vec::::new()); + + assert_eq!(format!("{value:?}"), "{}"); +} + +#[test] +fn debug_string_of_tuple() { + let value = tuple(vec![Value::Bool(true), Value::Number(Number::I64(42))]); + + assert_eq!(format!("{value:?}"), "(true, 42)"); +} + +#[test] +fn debug_string_of_tuple_struct() { + let value = tuple_struct( + "TruesCount", + vec![Value::Bool(true), Value::Number(Number::I64(42))], + ); + + assert_eq!(format!("{value:?}"), "TruesCount(true, 42)"); +} + +#[test] +fn type_name_of_bool_value() { + let value = Value::Bool(true); + + assert_eq!(value.type_name(), Cow::Borrowed("bool")); +} + +#[test] +fn type_name_of_char_value() { + let value = Value::Char('@'); + + assert_eq!(value.type_name(), Cow::Borrowed("char")); +} + +#[test] +fn type_name_of_map_value() { + let value = Value::Map(Map::from_iter([ + (Value::String("foo".into()), Value::Number(Number::U64(0))), + (Value::String("bar".into()), Value::Number(Number::U64(42))), + ])); + + assert_eq!(value.type_name(), Cow::Borrowed("Map")); +} + +#[test] +fn type_name_of_int32_value() { + let value = Value::Number(Number::I32(42)); + + assert_eq!(value.type_name(), Cow::Borrowed("i32")); +} + +#[test] +fn type_name_of_empty_sequence_value() { + let value = Value::Seq(vec![]); + + assert_eq!(value.type_name(), Cow::Borrowed("Vec")); +} + +#[test] +fn type_name_of_sequence_of_u16_values() { + let value = Value::Seq(vec![Value::Number(Number::U16(42))]); + + assert_eq!(value.type_name(), Cow::Borrowed("Vec")); +} + +#[test] +fn type_name_of_string_value() { + let value = Value::String("foo".into()); + + assert_eq!(value.type_name(), Cow::Borrowed("String")); +} + +#[test] +fn type_name_of_person_struct() { + let value = Value::Struct { + type_name: "Person".into(), + fields: vec![], + }; + + assert_eq!(value.type_name(), Cow::Borrowed("Person")); +} + +#[test] +fn type_name_of_tuple_one_element() { + let value = tuple([Value::String("foo".into())]); + + assert_eq!(value.type_name(), Cow::Borrowed("(String,)")); +} + +#[test] +fn type_name_of_tuple_two_elements() { + let value = tuple([Value::String("foo".into()), Value::Number(Number::I16(-42))]); + + assert_eq!(value.type_name(), Cow::Borrowed("(String, i16)")); +} + +#[test] +fn type_name_of_empty_tuple() { + let value = tuple([]); + + assert_eq!(value.type_name(), Cow::Borrowed("()")); +} + +#[test] +fn type_name_of_unit_value() { + let value = Value::Unit; + + assert_eq!(value.type_name(), Cow::Borrowed("()")); +} + +#[test] +fn depth_first_iterator_visits_fields_in_correct_order() { + let foo = Foo { + text: "magna laborum".to_string(), + age: 21, + qux: Qux { + name: "Silvia".to_string(), + corge: Corge { + grault: false, + tinu: (), + }, + baz: Baz(99, vec![100, 666, -100]), + }, + bytes: vec![24, 17, 64, 19], + precision: Some(2.5), + array: vec![12, -8, -34, 55, 76], + grouped: IndexMap::from_iter([("old", vec![1, -1]), ("new", vec![-1, 0, 1])]), + pair: (123_456, "sit wisi"), + bar: Bar(-99), + baz: Baz(122, vec![0, 0, -1, 1, -2, 2]), + samples: vec![ + Sample::One, + Sample::Two(22), + Sample::Three(33, "amet".into()), + Sample::Four { + left: "dolores".into(), + right: 'v', + }, + ], + }; + + let value = to_recursive_value(&foo).unwrap_or_else(|err| panic!("{err:?}")); + + let visited = value + .depth_first_iter() + .map(|(path, field)| (path.to_string(), format!("{field:?}"))) + .collect::>(); + + assert_eq!( + visited, + vec![ + ("text".to_string(), "\"magna laborum\"".to_string()), + ("age".to_string(), "21".to_string()), + ("qux.name".to_string(), "\"Silvia\"".to_string()), + ("qux.corge.grault".to_string(), "false".to_string()), + ("qux.corge.tinu".to_string(), "()".to_string()), + ("qux.baz.0".to_string(), "99".to_string()), + ("qux.baz.1".to_string(), "[100, 666, -100]".to_string()), + ("bytes".to_string(), "[24, 17, 64, 19]".to_string()), + ("precision.0".to_string(), "2.5".to_string()), + ("array".to_string(), "[12, -8, -34, 55, 76]".to_string()), + ( + "grouped".to_string(), + "{\"old\": [1, -1], \"new\": [-1, 0, 1]}".to_string() + ), + ("pair.0".to_string(), "123456".to_string()), + ("pair.1".to_string(), "\"sit wisi\"".to_string()), + ("bar.0".to_string(), "-99".to_string()), + ("baz.0".to_string(), "122".to_string()), + ("baz.1".to_string(), "[0, 0, -1, 1, -2, 2]".to_string()), + ( + "samples".to_string(), + "[One, Two(22), Three(33, \"amet\"), Four { left: \"dolores\", right: 'v' }]" + .to_string() + ), + ] + ); +} + +#[test] +fn get_path_foo_empty_path() { + let foo = Foo::default(); + let foo_value = to_recursive_value(&foo).unwrap_or_else(|err| panic!("{err:?}")); + + let value = foo_value.get_path(&Path::from("")); + + assert_eq!(value, Some(&foo_value)); +} + +#[test] +fn get_path_foo_one_level_deep_not_existing() { + let foo = Foo::default(); + let foo_value = to_recursive_value(&foo).unwrap_or_else(|err| panic!("{err:?}")); + + let value = foo_value.get_path(&Path::from("not_existing")); + + assert_eq!(value, None); +} + +#[test] +fn get_path_foo_text() { + let foo = Foo::default(); + let foo_value = to_recursive_value(&foo).unwrap_or_else(|err| panic!("{err:?}")); + + let value = foo_value.get_path(&Path::from("text")); + + assert_eq!(value, Some(&Value::String("magna laborum".into()))); +} + +#[test] +fn get_path_foo_age() { + let foo = Foo::default(); + let foo_value = to_recursive_value(&foo).unwrap_or_else(|err| panic!("{err:?}")); + + let value = foo_value.get_path(&Path::from("age")); + + assert_eq!(value, Some(&Value::Number(Number::U8(21)))); +} + +#[test] +fn get_path_foo_qux() { + let foo = Foo::default(); + let foo_value = to_recursive_value(&foo).unwrap_or_else(|err| panic!("{err:?}")); + + let value = foo_value.get_path(&Path::from("qux")); + + assert_eq!( + value, + Some(&Value::Struct { + type_name: "Qux".into(), + fields: vec![ + Field { + name: "name".into(), + value: Value::String("Silvia".into()), + }, + Field { + name: "corge".into(), + value: Value::Struct { + type_name: "Corge".into(), + fields: vec![ + Field { + name: "grault".into(), + value: Value::Bool(true), + }, + Field { + name: "tinu".into(), + value: Value::Unit, + }, + ] + }, + }, + Field { + name: "baz".into(), + value: Value::TupleStruct { + type_name: "Baz".into(), + values: vec![ + Field { + name: "0".into(), + value: Value::Number(Number::U64(99)) + }, + Field { + name: "1".into(), + value: Value::Seq(vec![ + Value::Number(Number::I32(100)), + Value::Number(Number::I32(666)), + Value::Number(Number::I32(-100)), + ]) + }, + ], + } + } + ], + }) + ); +} + +#[test] +fn get_path_foo_bytes() { + let foo = Foo::default(); + let foo_value = to_recursive_value(&foo).unwrap_or_else(|err| panic!("{err:?}")); + + let value = foo_value.get_path(&Path::from("bytes")); + + assert_eq!( + value, + Some(&Value::Seq(vec![ + Value::Number(Number::U8(24)), + Value::Number(Number::U8(17)), + Value::Number(Number::U8(64)), + Value::Number(Number::U8(19)) + ])) + ); +} + +#[test] +fn get_path_foo_precision() { + let foo = Foo::default(); + let foo_value = to_recursive_value(&foo).unwrap_or_else(|err| panic!("{err:?}")); + + let value = foo_value.get_path(&Path::from("precision")); + + assert_eq!( + value, + Some(&Value::TupleVariant { + type_name: "Option".into(), + variant: "Some".into(), + values: vec![Field { + name: "0".into(), + value: Value::Number(Number::F32(F32(2.5))), + }], + }) + ); +} + +#[test] +fn get_path_foo_grouped() { + let foo = Foo::default(); + let foo_value = to_recursive_value(&foo).unwrap_or_else(|err| panic!("{err:?}")); + + let value = foo_value.get_path(&Path::from("grouped")); + + assert_eq!( + value, + Some(&Value::Map(Map::from_iter([ + ( + Value::String("old".into()), + Value::Seq(vec![ + Value::Number(Number::I64(1)), + Value::Number(Number::I64(-1)) + ]) + ), + ( + Value::String("new".into()), + Value::Seq(vec![ + Value::Number(Number::I64(-1)), + Value::Number(Number::I64(0)), + Value::Number(Number::I64(1)) + ]) + ) + ]))) + ); +} + +#[test] +fn get_path_foo_pair() { + let foo = Foo::default(); + let foo_value = to_recursive_value(&foo).unwrap_or_else(|err| panic!("{err:?}")); + + let value = foo_value.get_path(&Path::from("pair")); + + assert_eq!( + value, + Some(&Value::Tuple(vec![ + Field { + name: "0".into(), + value: Value::Number(Number::U64(123_456)), + }, + Field { + name: "1".into(), + value: Value::String("sit wisi".into()), + } + ])) + ); +} + +#[test] +fn get_path_foo_baz() { + let foo = Foo::default(); + let foo_value = to_recursive_value(&foo).unwrap_or_else(|err| panic!("{err:?}")); + + let value = foo_value.get_path(&Path::from("baz")); + + assert_eq!( + value, + Some(&Value::TupleStruct { + type_name: "Baz".into(), + values: vec![ + Field { + name: "0".into(), + value: Value::Number(Number::U64(122)), + }, + Field { + name: "1".into(), + value: Value::Seq(vec![ + Value::Number(Number::I32(0)), + Value::Number(Number::I32(0)), + Value::Number(Number::I32(-1)), + Value::Number(Number::I32(1)), + Value::Number(Number::I32(-2)), + Value::Number(Number::I32(2)), + ]), + } + ], + }) + ); +} + +#[test] +fn get_path_foo_samples() { + let foo = Foo::default(); + let foo_value = to_recursive_value(&foo).unwrap_or_else(|err| panic!("{err:?}")); + + let value = foo_value.get_path(&Path::from("samples")); + + assert_eq!( + value, + Some(&Value::Seq(vec![ + Value::UnitVariant { + type_name: "Sample".into(), + variant: "One".into() + }, + Value::TupleVariant { + type_name: "Sample".into(), + variant: "Two".into(), + values: vec![Field { + name: "0".into(), + value: Value::Number(Number::I64(22)) + }] + }, + Value::TupleVariant { + type_name: "Sample".into(), + variant: "Three".into(), + values: vec![ + Field { + name: "0".into(), + value: Value::Number(Number::U64(33)) + }, + Field { + name: "1".into(), + value: Value::String("amet".into()) + } + ] + }, + Value::StructVariant { + type_name: "Sample".into(), + variant: "Four".into(), + fields: vec![ + Field { + name: "left".into(), + value: Value::String("dolores".into()) + }, + Field { + name: "right".into(), + value: Value::Char('v') + } + ] + }, + ])) + ); +} + +#[test] +fn get_path_foo_two_levels_deep_not_existing() { + let foo = Foo::default(); + let foo_value = to_recursive_value(&foo).unwrap_or_else(|err| panic!("{err:?}")); + + let value = foo_value.get_path(&Path::from("qux.not_existing")); + + assert_eq!(value, None); +} + +#[test] +fn get_path_foo_qux_name() { + let foo = Foo::default(); + let foo_value = to_recursive_value(&foo).unwrap_or_else(|err| panic!("{err:?}")); + + let value = foo_value.get_path(&Path::from("qux.name")); + + assert_eq!(value, Some(&Value::String("Silvia".into()))); +} + +#[test] +fn get_path_foo_qux_corge() { + let foo = Foo::default(); + let foo_value = to_recursive_value(&foo).unwrap_or_else(|err| panic!("{err:?}")); + + let value = foo_value.get_path(&Path::from("qux.corge")); + + assert_eq!( + value, + Some(&Value::Struct { + type_name: "Corge".into(), + fields: vec![ + Field { + name: "grault".into(), + value: Value::Bool(true), + }, + Field { + name: "tinu".into(), + value: Value::Unit + } + ], + }) + ); +} + +#[test] +fn get_path_foo_qux_baz() { + let foo = Foo::default(); + let foo_value = to_recursive_value(&foo).unwrap_or_else(|err| panic!("{err:?}")); + + let value = foo_value.get_path(&Path::from("qux.baz")); + + assert_eq!( + value, + Some(&Value::TupleStruct { + type_name: "Baz".into(), + values: vec![ + Field { + name: "0".into(), + value: Value::Number(Number::U64(99)), + }, + Field { + name: "1".into(), + value: Value::Seq(vec![ + Value::Number(Number::I32(100)), + Value::Number(Number::I32(666)), + Value::Number(Number::I32(-100)), + ]), + } + ] + }) + ); +} + +#[test] +fn get_path_foo_three_levels_deep_not_existing() { + let foo = Foo::default(); + let foo_value = to_recursive_value(&foo).unwrap_or_else(|err| panic!("{err:?}")); + + let value = foo_value.get_path(&Path::from("qux.corge.not_existing")); + + assert_eq!(value, None); +} + +#[test] +fn get_path_foo_qux_corge_grault() { + let foo = Foo::default(); + let foo_value = to_recursive_value(&foo).unwrap_or_else(|err| panic!("{err:?}")); + + let value = foo_value.get_path(&Path::from("qux.corge.grault")); + + assert_eq!(value, Some(&Value::Bool(true))); +} + +#[test] +fn get_path_foo_qux_corge_tinu() { + let foo = Foo::default(); + let foo_value = to_recursive_value(&foo).unwrap_or_else(|err| panic!("{err:?}")); + + let value = foo_value.get_path(&Path::from("qux.corge.tinu")); + + assert_eq!(value, Some(&Value::Unit)); +} + +#[test] +fn get_path_foo_indexing_into_tuple_0() { + let foo = Foo::default(); + let foo_value = to_recursive_value(&foo).unwrap_or_else(|err| panic!("{err:?}")); + + let value = foo_value.get_path(&Path::from("pair.0")); + + assert_eq!(value, Some(&Value::Number(Number::U64(123_456)))); +} + +#[test] +fn get_path_foo_indexing_into_tuple_1() { + let foo = Foo::default(); + let foo_value = to_recursive_value(&foo).unwrap_or_else(|err| panic!("{err:?}")); + + let value = foo_value.get_path(&Path::from("pair.1")); + + assert_eq!(value, Some(&Value::String("sit wisi".into()))); +} + +#[test] +fn get_path_foo_indexing_into_tuple_struct_0() { + let foo = Foo::default(); + let foo_value = to_recursive_value(&foo).unwrap_or_else(|err| panic!("{err:?}")); + + let value = foo_value.get_path(&Path::from("baz.0")); + + assert_eq!(value, Some(&Value::Number(Number::U64(122)))); +} + +#[test] +fn get_path_foo_indexing_into_tuple_struct_1() { + let foo = Foo::default(); + let foo_value = to_recursive_value(&foo).unwrap_or_else(|err| panic!("{err:?}")); + + let value = foo_value.get_path(&Path::from("baz.1")); + + assert_eq!( + value, + Some(&Value::Seq(vec![ + Value::Number(Number::I32(0)), + Value::Number(Number::I32(0)), + Value::Number(Number::I32(-1)), + Value::Number(Number::I32(1)), + Value::Number(Number::I32(-2)), + Value::Number(Number::I32(2)), + ])) + ); +} + +#[test] +fn get_path_foo_indexing_into_tuple_struct_out_of_bounds() { + let foo = Foo::default(); + let foo_value = to_recursive_value(&foo).unwrap_or_else(|err| panic!("{err:?}")); + + let value = foo_value.get_path(&Path::from("baz.2")); + + assert_eq!(value, None); +} + +#[test] +fn get_path_foo_indexing_into_sequence_0() { + let foo = Foo::default(); + let foo_value = to_recursive_value(&foo).unwrap_or_else(|err| panic!("{err:?}")); + + let value = foo_value.get_path(&Path::from("array.0")); + + assert_eq!(value, Some(&Value::Number(Number::I32(12)))); +} + +#[test] +fn get_path_foo_indexing_into_sequence_1() { + let foo = Foo::default(); + let foo_value = to_recursive_value(&foo).unwrap_or_else(|err| panic!("{err:?}")); + + let value = foo_value.get_path(&Path::from("array.1")); + + assert_eq!(value, Some(&Value::Number(Number::I32(-8)))); +} + +#[test] +fn get_path_foo_indexing_into_sequence_4() { + let foo = Foo::default(); + let foo_value = to_recursive_value(&foo).unwrap_or_else(|err| panic!("{err:?}")); + + let value = foo_value.get_path(&Path::from("array.4")); + + assert_eq!(value, Some(&Value::Number(Number::I32(76)))); +} + +#[test] +fn get_path_foo_indexing_into_sequence_out_of_bounds() { + let foo = Foo::default(); + let foo_value = to_recursive_value(&foo).unwrap_or_else(|err| panic!("{err:?}")); + + let value = foo_value.get_path(&Path::from("array.5")); + + assert_eq!(value, None); +} + +#[test] +fn get_path_foo_get_key_from_map_old() { + let foo = Foo::default(); + let foo_value = to_recursive_value(&foo).unwrap_or_else(|err| panic!("{err:?}")); + + let value = foo_value.get_path(&Path::from("grouped.old")); + + assert_eq!( + value, + Some(&Value::Seq(vec![ + Value::Number(Number::I64(1)), + Value::Number(Number::I64(-1)) + ])) + ); +} + +#[test] +fn get_path_foo_get_key_from_map_new() { + let foo = Foo::default(); + let foo_value = to_recursive_value(&foo).unwrap_or_else(|err| panic!("{err:?}")); + + let value = foo_value.get_path(&Path::from("grouped.new")); + + assert_eq!( + value, + Some(&Value::Seq(vec![ + Value::Number(Number::I64(-1)), + Value::Number(Number::I64(0)), + Value::Number(Number::I64(1)) + ])) + ); +} + +#[test] +fn get_path_foo_get_key_from_map_no_mapping() { + let foo = Foo::default(); + let foo_value = to_recursive_value(&foo).unwrap_or_else(|err| panic!("{err:?}")); + + let value = foo_value.get_path(&Path::from("grouped.not_existing")); + + assert_eq!(value, None); +} + +#[test] +fn get_path_foo_path_to_tuple_variant_field_0() { + let foo = Foo::default(); + let foo_value = to_recursive_value(&foo).unwrap_or_else(|err| panic!("{err:?}")); + + let value = foo_value.get_path(&Path::from("samples.2.0")); + + assert_eq!(value, Some(&Value::Number(Number::U64(33)))); +} + +#[test] +fn get_path_foo_path_to_tuple_variant_field_1() { + let foo = Foo::default(); + let foo_value = to_recursive_value(&foo).unwrap_or_else(|err| panic!("{err:?}")); + + let value = foo_value.get_path(&Path::from("samples.2.1")); + + assert_eq!(value, Some(&Value::String("amet".to_string()))); +} + +#[test] +fn get_path_foo_path_to_struct_variant_field_left() { + let foo = Foo::default(); + let foo_value = to_recursive_value(&foo).unwrap_or_else(|err| panic!("{err:?}")); + + let value = foo_value.get_path(&Path::from("samples.3.left")); + + assert_eq!(value, Some(&Value::String("dolores".into()))); +} + +#[test] +fn get_path_foo_path_to_struct_variant_field_right() { + let foo = Foo::default(); + let foo_value = to_recursive_value(&foo).unwrap_or_else(|err| panic!("{err:?}")); + + let value = foo_value.get_path(&Path::from("samples.3.right")); + + assert_eq!(value, Some(&Value::Char('v'))); +} + +#[test] +fn get_path_string_not_existing() { + let string_value = Value::String("Lorem ipsum dolor sit amet".into()); + + let value = string_value.get_path(&Path::from("Lorem")); + + assert_eq!(value, None); +} diff --git a/src/rust_decimal/mod.rs b/src/rust_decimal/mod.rs index 28893ef..866d5e9 100644 --- a/src/rust_decimal/mod.rs +++ b/src/rust_decimal/mod.rs @@ -1,5 +1,6 @@ -use crate::prelude::DecimalProperties; -use crate::properties::{AdditiveIdentityProperty, MultiplicativeIdentityProperty, SignumProperty}; +use crate::properties::{ + AdditiveIdentityProperty, DecimalProperties, MultiplicativeIdentityProperty, SignumProperty, +}; use rust_decimal::Decimal; impl SignumProperty for Decimal { diff --git a/src/spec/mod.rs b/src/spec/mod.rs index c484193..2c95f93 100644 --- a/src/spec/mod.rs +++ b/src/spec/mod.rs @@ -2,7 +2,10 @@ use crate::colored; use crate::expectations::satisfies; +#[cfg(feature = "recursive")] +use crate::recursive_comparison::RecursiveComparison; use crate::std::any; +use crate::std::borrow::Borrow; use crate::std::borrow::Cow; use crate::std::error::Error as StdError; use crate::std::fmt::{self, Debug, Display}; @@ -469,6 +472,12 @@ impl Display for Expression<'_> { } } +impl Borrow for Expression<'_> { + fn borrow(&self) -> &str { + &self.0 + } +} + impl Deref for Expression<'_> { type Target = str; @@ -746,6 +755,26 @@ impl<'a, S, R> Spec<'a, S, R> { } } + /// Switches this [`Spec`] to the "field-by-field recursive comparison + /// mode". + /// + /// It returns a [`RecursiveComparison`] which is a specialized `Spec` that + /// provides extra configuration options for the recursive comparison and + /// the special `is_equivalent_to`/`is_not_equivalent_to` assertions of the + /// [`AssertEquivalence`] trait. + /// + /// See the documentation of the [`recursive_comparison`] module for details + /// about field-by-field recursive comparison. + /// + /// [`AssertEquivalence`]: crate::assertions::AssertEquivalence + /// [`recursive_comparison`]: crate::recursive_comparison + #[cfg(feature = "recursive")] + #[cfg_attr(docsrs, doc(cfg(feature = "recursive")))] + #[must_use = "the returned `RecursiveComparison` does nothing unless an assertion method like `is_equal_to` is called"] + pub fn using_recursive_comparison(self) -> RecursiveComparison<'a, S, R> { + RecursiveComparison::new(self) + } + /// Maps the current subject to some other value. /// /// It takes a closure that maps the current subject to a new subject and @@ -1432,8 +1461,8 @@ pub struct Code(Rc>>); #[cfg(feature = "panic")] mod code { use super::Code; - use std::cell::RefCell; - use std::rc::Rc; + use crate::std::cell::RefCell; + use crate::std::rc::Rc; impl From for Code where diff --git a/src/string/mod.rs b/src/string/mod.rs index b37dc64..1c10265 100644 --- a/src/string/mod.rs +++ b/src/string/mod.rs @@ -705,6 +705,8 @@ mod regex { use crate::expectations::{not, string_matches, StringMatches}; use crate::spec::{DiffFormat, Expectation, Expression, FailingStrategy, Invertible, Spec}; use crate::std::fmt::Debug; + use crate::std::format; + use crate::std::string::String; impl AssertStringMatches for Spec<'_, S, R> where diff --git a/src/string/tests.rs b/src/string/tests.rs index 4c57fc9..ac3f31e 100644 --- a/src/string/tests.rs +++ b/src/string/tests.rs @@ -1376,6 +1376,7 @@ fn verify_string_does_not_end_with_char_fails() { #[cfg(feature = "regex")] mod regex { use crate::prelude::*; + use crate::std::string::{String, ToString}; #[test] fn string_matches_regex() { @@ -2045,7 +2046,7 @@ mod colored { #[cfg(all(feature = "colored", feature = "regex"))] mod colored_regex { use crate::prelude::*; - use crate::std::string::ToString; + use crate::std::string::{String, ToString}; #[test] fn highlight_diffs_string_matches_regex() { diff --git a/tests/version_numbers.rs b/tests/version_numbers.rs index 2482567..247942e 100644 --- a/tests/version_numbers.rs +++ b/tests/version_numbers.rs @@ -12,17 +12,25 @@ mod dummy_extern_uses { #[cfg(feature = "float-cmp")] use float_cmp as _; use hashbrown as _; - #[cfg(feature = "num-bigint")] - use lazy_static as _; + #[cfg(feature = "recursive")] + use indexmap as _; #[cfg(feature = "num-bigint")] use num_bigint as _; + #[cfg(any(feature = "bigdecimal", feature = "num-bigint"))] + use once_cell as _; use proptest as _; + #[cfg(feature = "recursive")] + use rapidhash as _; #[cfg(feature = "regex")] use regex as _; #[cfg(feature = "rust-decimal")] use rust_decimal as _; #[cfg(feature = "colored")] use sdiff as _; + use serde as _; + use serde_bytes as _; + #[cfg(feature = "recursive")] + use serde_core as _; use time as _; }