mz_ore/
assert.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License in the LICENSE file at the
6// root of this repository, or online at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! Assertion utilities.
17//!
18//! # Soft assertions
19//!
20//! Soft assertions are like debug assertions, but they can be toggled on and
21//! off at runtime in a release build.
22//!
23//! They are useful in two scenarios:
24//!
25//!   * When a failed assertion should result in a log message rather a process
26//!     crash.
27//!   * When evaluating the condition is too expensive to evaluate in production
28//!     deployments.
29//!
30//! When soft assertions are disabled, the performance cost of each assertion is
31//! one branch on an atomic.
32//!
33//! Ore provides the following macros to make soft assertions:
34//!
35//!   * [`soft_assert_or_log`](crate::soft_assert_or_log)
36//!   * [`soft_assert_eq_or_log`](crate::soft_assert_eq_or_log)
37//!   * [`soft_assert_ne_or_log`](crate::soft_assert_ne_or_log)
38//!   * [`soft_panic_or_log`](crate::soft_panic_or_log)
39//!   * [`soft_assert_no_log`](crate::soft_assert_no_log)
40//!   * [`soft_assert_eq_no_log`](crate::soft_assert_eq_no_log)
41//!   * [`soft_assert_ne_no_log`](crate::soft_assert_ne_no_log)
42//!
43//! The `_or_log` variants should be used by default, as they allow us to find
44//! failed condition checks in production. The `_no_log` variants are silent
45//! in production and should only be used when performance considerations
46//! prohibit the use of the logging variants.
47//!
48//! Due to limitations in Rust, these macros are exported at the crate root.
49
50use std::sync::atomic::AtomicBool;
51
52/// Whether to enable soft assertions.
53///
54/// This value defaults to `true` when `debug_assertions` are enabled, or to the
55/// value of the `MZ_SOFT_ASSERTIONS` environment variable otherwise.
56// The rules about what you can do in a `ctor` function are somewhat fuzzy,
57// because Rust does not explicitly support constructors. But a scan of the
58// stdlib suggests that reading environment variables is safe enough.
59#[cfg(not(any(miri, target_arch = "wasm32")))]
60#[ctor::ctor]
61pub static SOFT_ASSERTIONS: AtomicBool = {
62    let default = cfg!(debug_assertions) || crate::env::is_var_truthy("MZ_SOFT_ASSERTIONS");
63    AtomicBool::new(default)
64};
65
66/// Always enable soft assertions when running [Miri] or wasm.
67///
68/// Note: Miri also doesn't support global constructors, aka [`ctor`], if it ever does we could
69/// get rid of this second definition. See <https://github.com/rust-lang/miri/issues/450> for
70/// more details.
71///
72/// [Miri]: https://github.com/rust-lang/miri
73#[cfg(any(miri, target_arch = "wasm32"))]
74pub static SOFT_ASSERTIONS: AtomicBool = AtomicBool::new(true);
75
76/// Returns if soft assertions are enabled.
77#[inline(always)]
78pub fn soft_assertions_enabled() -> bool {
79    SOFT_ASSERTIONS.load(std::sync::atomic::Ordering::Relaxed)
80}
81
82/// Reports an error message. If the `tracing` feature is enabled, it uses
83/// `tracing::error!` to log the message. Otherwise, it prints the message
84/// to `stderr` using `eprintln!`.
85///
86/// Only intended to be used by macros in this module.
87#[doc(hidden)]
88#[cfg(feature = "tracing")]
89#[macro_export]
90macro_rules! report_error {
91    ($($arg:tt)+) => {{
92        ::tracing::error!($($arg)+);
93    }};
94}
95
96#[doc(hidden)]
97#[cfg(all(not(feature = "tracing"), not(target_arch = "wasm32")))]
98#[macro_export]
99#[deprecated(note = "Enable the `tracing` feature to use this macro.")]
100macro_rules! report_error {
101    ($($arg:tt)+) => {{
102        eprintln!($($arg)+);
103    }};
104}
105
106#[doc(hidden)]
107#[cfg(all(not(feature = "tracing"), target_arch = "wasm32"))]
108#[macro_export]
109macro_rules! report_error {
110    ($($arg:tt)+) => {{
111        eprintln!($($arg)+);
112    }};
113}
114
115/// Asserts that a condition is true if soft assertions are enabled.
116///
117/// Soft assertions have a small runtime cost even when disabled. See
118/// [`ore::assert`](crate::assert#Soft-assertions) for details.
119#[macro_export]
120macro_rules! soft_assert_no_log {
121    ($cond:expr $(, $($arg:tt)+)?) => {{
122        if $crate::assert::soft_assertions_enabled() {
123            assert!($cond$(, $($arg)+)?);
124        }
125    }}
126}
127
128/// Asserts that two values are equal if soft assertions are enabled.
129///
130/// Soft assertions have a small runtime cost even when disabled. See
131/// [`ore::assert`](crate::assert#Soft-assertions) for details.
132#[macro_export]
133macro_rules! soft_assert_eq_no_log {
134    ($cond:expr, $($arg:tt)+) => {{
135        if $crate::assert::soft_assertions_enabled() {
136            assert_eq!($cond, $($arg)+);
137        }
138    }}
139}
140
141/// Asserts that two values are not equal if soft assertions are enabled.
142///
143/// Soft assertions have a small runtime cost even when disabled. See
144/// [`ore::assert`](crate::assert#Soft-assertions) for details.
145#[macro_export]
146macro_rules! soft_assert_ne_no_log {
147    ($cond:expr, $($arg:tt)+) => {{
148        if $crate::assert::soft_assertions_enabled() {
149            assert_ne!($cond, $($arg)+);
150        }
151    }}
152}
153
154/// Asserts that a condition is true if soft assertions are enabled, or logs
155/// an error if soft assertions are disabled and the condition is false.
156#[macro_export]
157macro_rules! soft_assert_or_log {
158    ($cond:expr, $($arg:tt)+) => {{
159        if $crate::assert::soft_assertions_enabled() {
160            assert!($cond, $($arg)+);
161        } else if !$cond {
162            $crate::report_error!($($arg)+)
163        }
164    }}
165}
166
167/// Asserts that two expressions are equal to each other if soft assertions
168/// are enabled, or logs an error if soft assertions are disabled and the
169/// two expressions are not equal.
170#[macro_export]
171macro_rules! soft_assert_eq_or_log {
172    ($left:expr, $right:expr) => {{
173        if $crate::assert::soft_assertions_enabled() {
174            assert_eq!($left, $right);
175        } else {
176            // Borrowed from [`std::assert_eq`].
177            match (&$left, &$right) {
178                (left_val, right_val) => {
179                    if !(*left_val == *right_val) {
180                        $crate::report_error!(
181                            "assertion {:?} == {:?} failed",
182                            left_val, right_val
183                        );
184                    }
185                }
186            }
187        }
188    }};
189    ($left:expr, $right:expr, $($arg:tt)+) => {{
190        if $crate::assert::soft_assertions_enabled() {
191            assert_eq!($left, $right, $($arg)+);
192        } else {
193            // Borrowed from [`std::assert_eq`].
194            match (&$left, &$right) {
195                (left, right) => {
196                    if !(*left == *right) {
197                        $crate::report_error!(
198                            "assertion {:?} == {:?} failed: {}",
199                            left, right, format!($($arg)+)
200                        );
201                    }
202                }
203            }
204        }
205    }};
206}
207
208/// Asserts that two expressions are not equal to each other if soft assertions
209/// are enabled, or logs an error if soft assertions are disabled and the
210/// two expressions are not equal.
211#[macro_export]
212macro_rules! soft_assert_ne_or_log {
213    ($left:expr, $right:expr) => {{
214        if $crate::assert::soft_assertions_enabled() {
215            assert_ne!($left, $right);
216        } else {
217            // Borrowed from [`std::assert_ne`].
218            match (&$left, &$right) {
219                (left_val, right_val) => {
220                    if *left_val == *right_val {
221                        $crate::report_error!(
222                            "assertion {:?} != {:?} failed",
223                            left_val, right_val
224                        );
225                    }
226                }
227            }
228        }
229    }};
230    ($left:expr, $right:expr, $($arg:tt)+) => {{
231        if $crate::assert::soft_assertions_enabled() {
232            assert_ne!($left, $right, $($arg)+);
233        } else {
234            // Borrowed from [`std::assert_ne`].
235            match (&$left, &$right) {
236                (left_val, right_val) => {
237                    if *left_val == *right_val {
238                        $crate::report_error!(
239                            "assertion {:?} != {:?} failed: {}",
240                            $left, $right, format!($($arg)+)
241                        );
242                    }
243                }
244            }
245        }
246    }};
247}
248
249/// Panics if soft assertions are enabled, or logs an error if soft
250/// assertions are disabled.
251#[macro_export]
252macro_rules! soft_panic_or_log {
253    ($($arg:tt)+) => {{
254        if $crate::assert::soft_assertions_enabled() {
255            panic!($($arg)+);
256        } else {
257            $crate::report_error!($($arg)+)
258        }
259    }}
260}
261
262/// Panics if soft assertions are enabled.
263#[macro_export]
264macro_rules! soft_panic_no_log {
265    ($($arg:tt)+) => {{
266        if $crate::assert::soft_assertions_enabled() {
267            panic!($($arg)+);
268        }
269    }}
270}
271
272/// Asserts that the left expression contains the right expression.
273///
274/// Containment is determined by the `contains` method on the left type. If the
275/// left expression does not contain the right expression, the macro will panic
276/// with a descriptive message that includes both the left and right
277/// expressions.
278///
279/// # Motivation
280///
281/// The standard pattern for asserting containment uses the [`assert!`] macro
282///
283/// ```
284/// # let left = &[()];
285/// # let right = ();
286/// assert!(left.contains(&right))
287/// ```
288///
289/// but this pattern panics with a message that only displays `false` as the
290/// cause. This hampers determination of the true cause of the assertion
291/// failure.
292///
293/// # Examples
294///
295/// Check whether a string contains a substring:
296///
297/// ```
298/// use mz_ore::assert_contains;
299/// assert_contains!("hello", "ello");
300/// ```
301///
302/// Check whether a slice contains an element:
303///
304/// ```
305/// use mz_ore::assert_contains;
306/// assert_contains!(&[1, 2, 3], 2);
307/// ```
308///
309/// Failed assertions panic:
310///
311/// ```should_panic
312/// use mz_ore::assert_contains;
313/// assert_contains!("hello", "yellow");
314/// ```
315#[macro_export]
316macro_rules! assert_contains {
317    ($left:expr, $right:expr $(,)?) => {{
318        let left = $left;
319        let right = $right;
320        if !left.contains(&$right) {
321            panic!(
322                r#"assertion failed: `left.contains(right)`:
323  left: `{:?}`
324 right: `{:?}`"#,
325                left, right
326            );
327        }
328    }};
329}
330
331/// Asserts that the provided expression, that returns an `Option`, is `None`.
332///
333/// # Motivation
334///
335/// The standard pattern for asserting a value is `None` using the `assert!` macro is:
336///
337/// ```
338/// # let x: Option<usize> = None;
339/// assert!(x.is_none());
340/// ```
341///
342/// The issue with this pattern is when the assertion fails it only prints `false`
343/// and not the value contained in the `Some(_)` variant which makes debugging difficult.
344///
345/// # Examples
346///
347/// ### Basic Use
348///
349/// ```should_panic
350/// use mz_ore::assert_none;
351/// assert_none!(Some(42));
352/// ```
353///
354/// ### With extra message
355///
356/// ```should_panic
357/// use mz_ore::assert_none;
358/// let other_val = 100;
359/// assert_none!(Some(42), "ohh noo! x {other_val}");
360/// ```
361///
362#[macro_export]
363macro_rules! assert_none {
364    ($val:expr, $($msg:tt)+) => {{
365        if let Some(y) = &$val {
366            panic!("assertion failed: expected None found Some({y:?}), {}", format!($($msg)+));
367        }
368    }};
369    ($val:expr) => {{
370        if let Some(y) = &$val {
371            panic!("assertion failed: expected None found Some({y:?})");
372        }
373    }}
374}
375
376/// Asserts that the provided expression, that returns a `Result`, is `Ok`.
377///
378/// # Motivation
379///
380/// The standard pattern for asserting a value is `Ok` using the `assert!` macro is:
381///
382/// ```
383/// # let x: Result<usize, usize> = Ok(42);
384/// assert!(x.is_ok());
385/// ```
386///
387/// The issue with this pattern is when the assertion fails it only prints `false`
388/// and not the value contained in the `Err(_)` variant which makes debugging difficult.
389///
390/// # Examples
391///
392/// ### Basic Use
393///
394/// ```should_panic
395/// use mz_ore::assert_ok;
396/// let error: Result<usize, usize> = Err(42);
397/// assert_ok!(error);
398/// ```
399///
400/// ### With extra message
401///
402/// ```should_panic
403/// use mz_ore::assert_ok;
404/// let other_val = 100;
405/// let error: Result<usize, usize> = Err(42);
406/// assert_ok!(error, "ohh noo! x {other_val}");
407/// ```
408///
409#[macro_export]
410macro_rules! assert_ok {
411    ($val:expr, $($msg:tt)+) => {{
412        if let Err(y) = &$val {
413            panic!("assertion failed: expected Ok found Err({y:?}), {}", format!($($msg)+));
414        }
415    }};
416    ($val:expr) => {{
417        if let Err(y) = &$val {
418            panic!("assertion failed: expected Ok found Err({y:?})");
419        }
420    }}
421}
422
423/// Asserts that the provided expression, that returns a `Result`, is `Err`.
424///
425/// # Motivation
426///
427/// The standard pattern for asserting a value is `Err` using the `assert!` macro is:
428///
429/// ```
430/// # let x: Result<usize, usize> = Err(42);
431/// assert!(x.is_err());
432/// ```
433///
434/// The issue with this pattern is when the assertion fails it only prints `false`
435/// and not the value contained in the `Ok(_)` variant which makes debugging difficult.
436///
437/// # Examples
438///
439/// ### Basic Use
440///
441/// ```should_panic
442/// use mz_ore::assert_err;
443/// let error: Result<usize, usize> = Ok(42);
444/// assert_err!(error);
445/// ```
446///
447/// ### With extra message
448///
449/// ```should_panic
450/// use mz_ore::assert_err;
451/// let other_val = 100;
452/// let error: Result<usize, usize> = Ok(42);
453/// assert_err!(error, "ohh noo! x {other_val}");
454/// ```
455///
456#[macro_export]
457macro_rules! assert_err {
458    ($val:expr, $($msg:tt)+) => {{
459        if let Ok(y) = &$val {
460            panic!("assertion failed: expected Err found Ok({y:?}), {}", format!($($msg)+));
461        }
462    }};
463    ($val:expr) => {{
464        if let Ok(y) = &$val {
465            panic!("assertion failed: expected Err found Ok({y:?})");
466        }
467    }}
468}
469
470#[cfg(test)]
471mod tests {
472    #[crate::test]
473    fn test_assert_contains_str() {
474        assert_contains!("hello", "ello");
475    }
476
477    #[crate::test]
478    fn test_assert_contains_slice() {
479        assert_contains!(&[1, 2, 3], 2);
480    }
481
482    #[crate::test]
483    #[should_panic(expected = "assertion failed: `left.contains(right)`:
484  left: `\"hello\"`
485 right: `\"yellow\"`")]
486    fn test_assert_contains_fail() {
487        assert_contains!("hello", "yellow");
488    }
489
490    #[crate::test]
491    #[should_panic(expected = "assertion failed: expected None found Some(42)")]
492    fn test_assert_none_fail() {
493        assert_none!(Some(42));
494    }
495
496    #[crate::test]
497    #[should_panic(expected = "assertion failed: expected None found Some(42), ohh no!")]
498    fn test_assert_none_fail_with_msg() {
499        assert_none!(Some(42), "ohh no!");
500    }
501
502    #[crate::test]
503    fn test_assert_ok() {
504        assert_ok!(Ok::<_, usize>(42));
505    }
506
507    #[crate::test]
508    #[should_panic(expected = "assertion failed: expected Ok found Err(42)")]
509    fn test_assert_ok_fail() {
510        assert_ok!(Err::<usize, _>(42));
511    }
512
513    #[crate::test]
514    #[should_panic(expected = "assertion failed: expected Ok found Err(42), ohh no!")]
515    fn test_assert_ok_fail_with_msg() {
516        assert_ok!(Err::<usize, _>(42), "ohh no!");
517    }
518
519    #[crate::test]
520    fn test_assert_err() {
521        assert_err!(Err::<usize, _>(42));
522    }
523
524    #[crate::test]
525    #[should_panic(expected = "assertion failed: expected Err found Ok(42)")]
526    fn test_assert_err_fail() {
527        assert_err!(Ok::<_, usize>(42));
528    }
529
530    #[crate::test]
531    #[should_panic(expected = "assertion failed: expected Err found Ok(42), ohh no!")]
532    fn test_assert_err_fail_with_msg() {
533        assert_err!(Ok::<_, usize>(42), "ohh no!");
534    }
535}