assert_within/
lib.rs

1//! Provides a macro `assert_within!` for tests involving floating point numbers.
2//!
3//! assert_within!(+0.001, val, target, "Value was not within additive 0.001: {more} {context}");
4//! assert_within!(~0.05, val, target, "Value was not within 5% of target: {additional} {information:?}");
5//!
6//! Highlights include:
7//!
8//! * Pass arguments by reference or value
9//! * Sigils (+, ~) indicate additive or relative error
10//! * Traps Nan in any of the arguments
11//! * Errors cause both the stringified expressions and their values to be displayed
12//! * Arbitrary additional format args
13//! * Generic over `num_traits::FloatCore`
14//! * no_std compatible
15
16#![no_std]
17
18use core::{
19    borrow::Borrow,
20    fmt::{self, Display},
21};
22use num_traits::{float::FloatCore, NumRef};
23
24/// Helper for asserting that an f64 value is within +/- epsilon of another, additively
25#[doc(hidden)]
26#[allow(clippy::too_many_arguments)]
27pub fn assert_within_add_impl<N: Display + FloatCore>(
28    file: &'static str,
29    line: u32,
30    val: impl Borrow<N>,
31    val_str: &'static str,
32    target: impl Borrow<N>,
33    target_str: &'static str,
34    eps: impl Borrow<N>,
35    context: fmt::Arguments,
36) {
37    let val = val.borrow();
38    let target = target.borrow();
39    let eps = eps.borrow();
40
41    if eps.is_nan() {
42        panic!("assert_within failed at {file}:{line}:\nepsilon was Nan: {eps}\n{context}");
43    }
44
45    if *eps < N::zero() {
46        panic!(
47            "assert_within failed at {file}:{line}:\nEpsilon cannot be negative when used with assert_within! macro: {eps}\n{context}"
48        )
49    }
50
51    if val.is_nan() {
52        panic!("assert_within failed at {file}:{line}:\n`{val_str}` was Nan: {val}\n{context}");
53    }
54
55    if target.is_nan() {
56        panic!(
57            "assert_within failed at {file}:{line}:\n`{target_str}` was Nan: {target}\n{context}"
58        );
59    }
60
61    if *val < *target - *eps {
62        panic!(
63            "assert_within failed at {file}:{line}:\n`{val_str}` was less than `{target_str}` - {eps})\nleft:  {val}\nright: {target}\n{context}"
64        );
65    }
66
67    if *val > *target + *eps {
68        panic!(
69            "assert_within failed at {file}:{line}:\n`{val_str}` was greater than `{target_str}` + {eps})\nleft:  {val}\nright: {target}\n{context}"
70        );
71    }
72}
73
74/// Helper for asserting that an f64 value is within +/- epsilon of another, multiplicatively
75#[doc(hidden)]
76#[allow(clippy::too_many_arguments)]
77pub fn assert_within_mul_impl<N: NumRef + Display + FloatCore>(
78    file: &'static str,
79    line: u32,
80    val: impl Borrow<N>,
81    val_str: &'static str,
82    target: impl Borrow<N>,
83    target_str: &'static str,
84    eps: impl Borrow<N>,
85    context: fmt::Arguments,
86) {
87    let val = val.borrow();
88    let target = target.borrow();
89    let eps = eps.borrow();
90
91    if eps.is_nan() {
92        panic!("assert_within failed at {file}:{line}:\nepsilon was Nan: {eps}\n{context}");
93    }
94
95    if *eps < N::zero() {
96        panic!(
97            "assert_within failed at {file}:{line}:\nEpsilon cannot be negative when used with assert_within! macro: {eps}\n{context}"
98        )
99    }
100
101    if val.is_nan() {
102        panic!("assert_within failed at {file}:{line}:\n`{val_str}` was Nan: {val}\n{context}");
103    }
104
105    if target.is_nan() {
106        panic!(
107            "assert_within failed at {file}:{line}:\n`{target_str}` was Nan: {target}\n{context}"
108        );
109    }
110
111    let one_minus_eps = N::one() - *eps;
112    if *val < one_minus_eps * target {
113        panic!(
114            "assert_within failed at {file}:{line}:\n`{val_str}` was less than (1 ± {eps}) * `{target_str}`\nleft:  {val}\nright: {target}\n{context}"
115        );
116    }
117
118    let one_plus_eps = N::one() + *eps;
119    if *val > one_plus_eps * target {
120        panic!(
121            "assert_within failed at {file}:{line}:\n`{val_str}` was greater than (1 ± {eps}) * `{target_str}`\nleft:  {val}\nright: {target}\n{context}"
122        );
123    }
124}
125
126#[macro_export]
127macro_rules! assert_within {
128    (+ $epsilon:expr, $val:expr, $target:expr) => {
129        $crate::test_utils::assert_within_add_impl(
130            file!(),
131            line!(),
132            $val,
133            stringify!($val),
134            $target,
135            stringify!($target),
136            $epsilon,
137            format_args!(""),
138        )
139    };
140
141    (+ $epsilon:expr, $val:expr, $target:expr, $($fmt_args:tt)*) => {
142        $crate::test_utils::assert_within_add_impl(
143            file!(),
144            line!(),
145            $val,
146            stringify!($val),
147            $target,
148            stringify!($target),
149            $epsilon,
150            format_args!($($fmt_args)*),
151        )
152    };
153
154    (~ $epsilon:expr, $val:expr, $target:expr) => {
155        $crate::test_utils::assert_within_mul_impl(
156            file!(),
157            line!(),
158            $val,
159            stringify!($val),
160            $target,
161            stringify!($target),
162            $epsilon,
163            format_args!(""),
164        )
165    };
166
167    (~ $epsilon:expr, $val:expr, $target:expr, $($fmt_args:tt)*) => {
168        $crate::test_utils::assert_within_mul_impl(
169            file!(),
170            line!(),
171            $val,
172            stringify!($val),
173            $target,
174            stringify!($target),
175            $epsilon,
176            format_args!($($fmt_args)*),
177        )
178    };
179}