cosmwasm_std/testing/
assertions.rs

1use crate::{Decimal, Uint128};
2
3use alloc::string::String;
4#[cfg(test)]
5use core::hash::{Hash, Hasher};
6use core::str::FromStr as _;
7
8/// Asserts that two expressions are approximately equal to each other.
9///
10/// The `max_rel_diff` argument defines the maximum relative difference
11/// of the `left` and `right` values.
12///
13/// On panic, this macro will print the values of the arguments and
14/// the actual relative difference.
15///
16/// Like [`assert_eq!`], this macro has a second form, where a custom
17/// panic message can be provided.
18#[macro_export]
19macro_rules! assert_approx_eq {
20    ($left:expr, $right:expr, $max_rel_diff:expr $(,)?) => {{
21        $crate::testing::assert_approx_eq_impl($left, $right, $max_rel_diff, None);
22    }};
23    ($left:expr, $right:expr, $max_rel_diff:expr, $($args:tt)+) => {{
24        $crate::testing::assert_approx_eq_impl($left, $right, $max_rel_diff, Some(format!($($args)*)));
25    }};
26}
27
28/// Tests that type `T` implements `Eq` and `Hash` traits correctly.
29///
30/// `left` and `right` must be unequal objects.
31///
32/// Some object pairs may produce the same hash causing test failure.
33/// In those cases try different objects. The test uses stable hasher
34/// so once working pair is identified, the test’s going to continue
35/// passing.
36#[macro_export]
37#[cfg(test)]
38macro_rules! assert_hash_works {
39    ($left:expr, $right:expr $(,)?) => {{
40        $crate::testing::assert_hash_works_impl($left, $right, None);
41    }};
42    ($left:expr, $right:expr, $($args:tt)+) => {{
43        $crate::testing::assert_hash_works_impl($left, $right, Some(format!($($args)*)));
44    }};
45}
46
47/// Implementation for the [`cosmwasm_std::assert_approx_eq`] macro. This does not provide any
48/// stability guarantees and may change any time.
49#[track_caller]
50#[doc(hidden)]
51pub fn assert_approx_eq_impl<U: Into<Uint128>>(
52    left: U,
53    right: U,
54    max_rel_diff: &str,
55    panic_msg: Option<String>,
56) {
57    let left = left.into();
58    let right = right.into();
59
60    if left == right {
61        // If both values are equal, we don't need to check the relative difference.
62        // We check this first to avoid division by zero below.
63        return;
64    }
65
66    let max_rel_diff = Decimal::from_str(max_rel_diff).unwrap();
67
68    let largest = core::cmp::max(left, right);
69    let rel_diff = Decimal::from_ratio(left.abs_diff(right), largest);
70
71    if rel_diff > max_rel_diff {
72        do_panic(format_args!("assertion failed: `(left ≈ right)`\nleft: {left}\nright: {right}\nrelative difference: {rel_diff}\nmax allowed relative difference: {max_rel_diff}"), panic_msg);
73    }
74}
75
76/// Tests that type `T` implements `Eq` and `Hash` traits correctly.
77///
78/// `left` and `right` must be unequal objects.
79///
80/// Some object pairs may produce the same hash causing test failure.  In those
81/// cases try different objects. The test uses stable hasher so once working
82/// pair is identified, the test’s going to continue passing.
83#[track_caller]
84#[doc(hidden)]
85#[cfg(test)]
86pub fn assert_hash_works_impl<T: Clone + Eq + Hash>(left: T, right: T, panic_msg: Option<String>) {
87    fn hash(value: &impl Hash) -> u64 {
88        let mut hasher = crc32fast::Hasher::default();
89        value.hash(&mut hasher);
90        hasher.finish()
91    }
92
93    // Check clone
94    #[allow(clippy::redundant_clone)]
95    let clone = left.clone();
96    if left != clone {
97        do_panic("assertion failed: `left == left.clone()`", panic_msg);
98    }
99    if hash(&left) != hash(&clone) {
100        do_panic(
101            "assertion failed: `hash(left) == hash(left.clone())`",
102            panic_msg,
103        );
104    }
105
106    // Check different object
107    if left == right {
108        do_panic("assertion failed: `left != right`", panic_msg);
109    }
110    if hash(&left) == hash(&right) {
111        do_panic("assertion failed: `hash(left) != hash(right)`", panic_msg);
112    }
113}
114
115/// Panics concatenating both arguments.
116///
117/// If second argument is `None` panics with just the first argument as message.
118/// Otherwise, formats the panic message as `{reason}:\n{panic_msg}`.
119#[track_caller]
120fn do_panic(reason: impl core::fmt::Display, panic_msg: Option<String>) -> ! {
121    match panic_msg {
122        Some(panic_msg) => panic!("{reason}:\n{panic_msg}"),
123        None => panic!("{reason}"),
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    #[test]
130    fn assert_approx() {
131        assert_approx_eq!(9_u32, 10_u32, "0.12");
132        assert_approx_eq!(9_u64, 10_u64, "0.12");
133        assert_approx_eq!(
134            9_000_000_000_000_000_000_000_000_000_000_000_000_u128,
135            10_000_000_000_000_000_000_000_000_000_000_000_000_u128,
136            "0.10"
137        );
138        assert_approx_eq!(0_u32, 0_u32, "0.12");
139        assert_approx_eq!(1_u64, 0_u64, "1");
140        assert_approx_eq!(0_u64, 1_u64, "1");
141        assert_approx_eq!(5_u64, 0_u64, "1");
142        assert_approx_eq!(0_u64, 5_u64, "1");
143    }
144
145    #[test]
146    fn assert_approx_with_vars() {
147        let a = 66_u32;
148        let b = 67_u32;
149        assert_approx_eq!(a, b, "0.02");
150
151        let a = 66_u64;
152        let b = 67_u64;
153        assert_approx_eq!(a, b, "0.02");
154
155        let a = 66_u128;
156        let b = 67_u128;
157        assert_approx_eq!(a, b, "0.02");
158    }
159
160    #[test]
161    #[should_panic(
162        expected = "assertion failed: `(left ≈ right)`\nleft: 8\nright: 10\nrelative difference: 0.2\nmax allowed relative difference: 0.12"
163    )]
164    fn assert_approx_fail() {
165        assert_approx_eq!(8_u32, 10_u32, "0.12");
166    }
167
168    #[test]
169    #[should_panic(
170        expected = "assertion failed: `(left ≈ right)`\nleft: 17\nright: 20\nrelative difference: 0.15\nmax allowed relative difference: 0.12:\nsome extra info about the error: Foo(8)"
171    )]
172    fn assert_approx_with_custom_panic_msg() {
173        let adjective = "extra";
174        #[allow(dead_code)]
175        #[derive(Debug)]
176        struct Foo(u32);
177        assert_approx_eq!(
178            17_u32,
179            20_u32,
180            "0.12",
181            "some {adjective} {} about the error: {:?}",
182            "info",
183            Foo(8),
184        );
185    }
186}