Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
4b41b04
feat: implement recursive field-by-field comparison of structs
haraldmaida Mar 1, 2026
fca2d4a
doc: document most of the recursive comparison related API
haraldmaida Mar 6, 2026
f164e70
refactor: rename function `to_recursive_values` to `to_recursive_value`
haraldmaida Mar 7, 2026
3bec9ce
refactor: do not import from prelude module inside the crate's code
haraldmaida Mar 7, 2026
3e5e16d
feat: make feature "recursive" support no-std environments
haraldmaida Mar 8, 2026
25082bd
feat: make feature "regex" support no-std environments
haraldmaida Mar 8, 2026
7fa579e
chore: add features "regex" und "recursive" to CI lint- and test-task…
haraldmaida Mar 8, 2026
7c90ae2
feat: create macro value! to construct `Value` with Rust-like/RON-lik…
haraldmaida Mar 7, 2026
1b19686
feat: treat empty tuple as `Value::Unit` as empty tuple and unit type…
haraldmaida Mar 7, 2026
460a6cb
feat: enhance the `value!` macro to support more types of values
haraldmaida Mar 7, 2026
b8d0b1d
feat: enhance the `value!` macro to support seq, struct variant, unit…
haraldmaida Mar 8, 2026
c59d155
test: add tests for support of expressions and captured variables in …
haraldmaida Mar 8, 2026
cbfb4d6
test: add support for nested anonymous and named structs, tuple struc…
haraldmaida Mar 8, 2026
451acf2
fix: compile error in macro tests in no-std environment
haraldmaida Mar 8, 2026
c0ddf55
feat: add support for nested struct, enum variants and seq to tuple a…
haraldmaida Mar 8, 2026
f2cec88
fix: debug string of empty struct contains closing brace only (" }")
haraldmaida Mar 9, 2026
eed93a6
fix: alloc::vec! macro not accessible in value! macro
haraldmaida Mar 9, 2026
854caad
fix: macro value! can not handle multiple nested tuple variants
haraldmaida Mar 9, 2026
29b19c7
feat: add support for maps to the value! macro
haraldmaida Mar 12, 2026
3cde513
doc: add examples for capturing variables and expressions inside the …
haraldmaida Mar 12, 2026
16fa420
doc: hide distracting implementation details of the `value!`-macro fr…
haraldmaida Mar 12, 2026
5ec97f8
doc: rename field name in `value!` example
haraldmaida Mar 13, 2026
d6b389d
doc: remove underscore from unused variable in doctest
haraldmaida Mar 13, 2026
47614ed
test: add test struct is_equivalent_to a value constructed using the…
haraldmaida Mar 13, 2026
cb910d0
doc: document the `AssertEquivalence` trait and document usage of the…
haraldmaida Mar 13, 2026
8ee4546
feat: implement `is_not_equal_to` assertion for recursive comparison
haraldmaida Mar 14, 2026
2766778
feat: implement `is_not_equivalent_to` assertion for recursive compar…
haraldmaida Mar 14, 2026
a0b5fb4
feat: highlight diffs for recursive comparison assertions
haraldmaida Mar 14, 2026
b2829e9
doc: mention field-by-field recursive comparison in the crate level d…
haraldmaida Mar 14, 2026
216c0df
doc: mention field-by-field recursive comparison mode in README
haraldmaida Mar 14, 2026
ff18959
doc: add link for "Highlighted differences" in README
haraldmaida Mar 14, 2026
b5304ad
chore!: change MSRV to 1.82.0 as the `indexmap` crate requires 1.82
haraldmaida Mar 14, 2026
6b33d10
refactor: replace `lazy_static` with `once_cell`
haraldmaida Mar 14, 2026
f2ae768
fix: some issues with packaging of optional features
haraldmaida Mar 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
31 changes: 24 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -19,33 +19,50 @@ 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"

# 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]
Expand Down
36 changes: 29 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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].
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
12 changes: 10 additions & 2 deletions examples/fixture/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 _;
}
4 changes: 2 additions & 2 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down
142 changes: 142 additions & 0 deletions src/assertions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ pub trait AssertSameAs<E> {
///
/// 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
Expand All @@ -114,9 +115,150 @@ pub trait AssertSameAs<E> {
///
/// 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<String>,
/// }
///
/// 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<E> {
/// 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<String>,
/// }
///
/// 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<String>,
/// }
///
/// 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
Expand Down
10 changes: 5 additions & 5 deletions src/bigdecimal/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<BigDecimal> = Lazy::new(bigdecimal_zero);
#[allow(clippy::non_std_lazy_statics)]
static BIGDECIMAL_ONE: Lazy<BigDecimal> = Lazy::new(bigdecimal_one);

#[inline]
fn bigdecimal_zero() -> BigDecimal {
Expand Down
2 changes: 1 addition & 1 deletion src/collection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ impl<T, const N: usize> 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<K, V, S> IsEmptyProperty for HashMap<K, V, S> {
fn is_empty_property(&self) -> bool {
Expand Down
2 changes: 2 additions & 0 deletions src/colored/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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(|| {});
Expand Down
4 changes: 2 additions & 2 deletions src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<EnvStore> = RefCell::new({
Expand Down
Loading
Loading