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/// Asserts that a condition is true if soft assertions are enabled.
83///
84/// Soft assertions have a small runtime cost even when disabled. See
85/// [`ore::assert`](crate::assert#Soft-assertions) for details.
86#[macro_export]
87macro_rules! soft_assert_no_log {
88    ($cond:expr $(, $($arg:tt)+)?) => {{
89        if $crate::assert::soft_assertions_enabled() {
90            assert!($cond$(, $($arg)+)?);
91        }
92    }}
93}
94
95/// Asserts that two values are equal if soft assertions are enabled.
96///
97/// Soft assertions have a small runtime cost even when disabled. See
98/// [`ore::assert`](crate::assert#Soft-assertions) for details.
99#[macro_export]
100macro_rules! soft_assert_eq_no_log {
101    ($cond:expr, $($arg:tt)+) => {{
102        if $crate::assert::soft_assertions_enabled() {
103            assert_eq!($cond, $($arg)+);
104        }
105    }}
106}
107
108/// Asserts that two values are not equal if soft assertions are enabled.
109///
110/// Soft assertions have a small runtime cost even when disabled. See
111/// [`ore::assert`](crate::assert#Soft-assertions) for details.
112#[macro_export]
113macro_rules! soft_assert_ne_no_log {
114    ($cond:expr, $($arg:tt)+) => {{
115        if $crate::assert::soft_assertions_enabled() {
116            assert_ne!($cond, $($arg)+);
117        }
118    }}
119}
120
121/// Asserts that a condition is true if soft assertions are enabled, or logs
122/// an error if soft assertions are disabled and the condition is false.
123#[macro_export]
124macro_rules! soft_assert_or_log {
125    ($cond:expr, $($arg:tt)+) => {{
126        if $crate::assert::soft_assertions_enabled() {
127            assert!($cond, $($arg)+);
128        } else if !$cond {
129            ::tracing::error!($($arg)+)
130        }
131    }}
132}
133
134/// Asserts that two expressions are equal to each other if soft assertions
135/// are enabled, or logs an error if soft assertions are disabled and the
136/// two expressions are not equal.
137#[macro_export]
138macro_rules! soft_assert_eq_or_log {
139    ($left:expr, $right:expr) => {{
140        if $crate::assert::soft_assertions_enabled() {
141            assert_eq!($left, $right);
142        } else {
143            // Borrowed from [`std::assert_eq`].
144            match (&$left, &$right) {
145                (left_val, right_val) => {
146                    if !(*left_val == *right_val) {
147                        ::tracing::error!(
148                            "assertion {:?} == {:?} failed",
149                            left_val, right_val
150                        );
151                    }
152                }
153            }
154        }
155    }};
156    ($left:expr, $right:expr, $($arg:tt)+) => {{
157        if $crate::assert::soft_assertions_enabled() {
158            assert_eq!($left, $right, $($arg)+);
159        } else {
160            // Borrowed from [`std::assert_eq`].
161            match (&$left, &$right) {
162                (left, right) => {
163                    if !(*left == *right) {
164                        ::tracing::error!(
165                            "assertion {:?} == {:?} failed: {}",
166                            left, right, format!($($arg)+)
167                        );
168                    }
169                }
170            }
171        }
172    }};
173}
174
175/// Asserts that two expressions are not equal to each other if soft assertions
176/// are enabled, or logs an error if soft assertions are disabled and the
177/// two expressions are not equal.
178#[macro_export]
179macro_rules! soft_assert_ne_or_log {
180    ($left:expr, $right:expr) => {{
181        if $crate::assert::soft_assertions_enabled() {
182            assert_ne!($left, $right);
183        } else {
184            // Borrowed from [`std::assert_ne`].
185            match (&$left, &$right) {
186                (left_val, right_val) => {
187                    if *left_val == *right_val {
188                        ::tracing::error!(
189                            "assertion {:?} != {:?} failed",
190                            left_val, right_val
191                        );
192                    }
193                }
194            }
195        }
196    }};
197    ($left:expr, $right:expr, $($arg:tt)+) => {{
198        if $crate::assert::soft_assertions_enabled() {
199            assert_ne!($left, $right, $($arg)+);
200        } else {
201            // Borrowed from [`std::assert_ne`].
202            match (&$left, &$right) {
203                (left_val, right_val) => {
204                    if *left_val == *right_val {
205                        ::tracing::error!(
206                            "assertion {:?} != {:?} failed: {}",
207                            $left, $right, format!($($arg)+)
208                        );
209                    }
210                }
211            }
212        }
213    }};
214}
215
216/// Panics if soft assertions are enabled, or logs an error if soft
217/// assertions are disabled.
218#[macro_export]
219macro_rules! soft_panic_or_log {
220    ($($arg:tt)+) => {{
221        if $crate::assert::soft_assertions_enabled() {
222            panic!($($arg)+);
223        } else {
224            ::tracing::error!($($arg)+)
225        }
226    }}
227}
228
229/// Panics if soft assertions are enabled.
230#[macro_export]
231macro_rules! soft_panic_no_log {
232    ($($arg:tt)+) => {{
233        if $crate::assert::soft_assertions_enabled() {
234            panic!($($arg)+);
235        }
236    }}
237}
238
239/// Asserts that the left expression contains the right expression.
240///
241/// Containment is determined by the `contains` method on the left type. If the
242/// left expression does not contain the right expression, the macro will panic
243/// with a descriptive message that includes both the left and right
244/// expressions.
245///
246/// # Motivation
247///
248/// The standard pattern for asserting containment uses the [`assert!`] macro
249///
250/// ```
251/// # let left = &[()];
252/// # let right = ();
253/// assert!(left.contains(&right))
254/// ```
255///
256/// but this pattern panics with a message that only displays `false` as the
257/// cause. This hampers determination of the true cause of the assertion
258/// failure.
259///
260/// # Examples
261///
262/// Check whether a string contains a substring:
263///
264/// ```
265/// use mz_ore::assert_contains;
266/// assert_contains!("hello", "ello");
267/// ```
268///
269/// Check whether a slice contains an element:
270///
271/// ```
272/// use mz_ore::assert_contains;
273/// assert_contains!(&[1, 2, 3], 2);
274/// ```
275///
276/// Failed assertions panic:
277///
278/// ```should_panic
279/// use mz_ore::assert_contains;
280/// assert_contains!("hello", "yellow");
281/// ```
282#[macro_export]
283macro_rules! assert_contains {
284    ($left:expr, $right:expr $(,)?) => {{
285        let left = $left;
286        let right = $right;
287        if !left.contains(&$right) {
288            panic!(
289                r#"assertion failed: `left.contains(right)`:
290  left: `{:?}`
291 right: `{:?}`"#,
292                left, right
293            );
294        }
295    }};
296}
297
298/// Asserts that the provided expression, that returns an `Option`, is `None`.
299///
300/// # Motivation
301///
302/// The standard pattern for asserting a value is `None` using the `assert!` macro is:
303///
304/// ```
305/// # let x: Option<usize> = None;
306/// assert!(x.is_none());
307/// ```
308///
309/// The issue with this pattern is when the assertion fails it only prints `false`
310/// and not the value contained in the `Some(_)` variant which makes debugging difficult.
311///
312/// # Examples
313///
314/// ### Basic Use
315///
316/// ```should_panic
317/// use mz_ore::assert_none;
318/// assert_none!(Some(42));
319/// ```
320///
321/// ### With extra message
322///
323/// ```should_panic
324/// use mz_ore::assert_none;
325/// let other_val = 100;
326/// assert_none!(Some(42), "ohh noo! x {other_val}");
327/// ```
328///
329#[macro_export]
330macro_rules! assert_none {
331    ($val:expr, $($msg:tt)+) => {{
332        if let Some(y) = &$val {
333            panic!("assertion failed: expected None found Some({y:?}), {}", format!($($msg)+));
334        }
335    }};
336    ($val:expr) => {{
337        if let Some(y) = &$val {
338            panic!("assertion failed: expected None found Some({y:?})");
339        }
340    }}
341}
342
343/// Asserts that the provided expression, that returns a `Result`, is `Ok`.
344///
345/// # Motivation
346///
347/// The standard pattern for asserting a value is `Ok` using the `assert!` macro is:
348///
349/// ```
350/// # let x: Result<usize, usize> = Ok(42);
351/// assert!(x.is_ok());
352/// ```
353///
354/// The issue with this pattern is when the assertion fails it only prints `false`
355/// and not the value contained in the `Err(_)` variant which makes debugging difficult.
356///
357/// # Examples
358///
359/// ### Basic Use
360///
361/// ```should_panic
362/// use mz_ore::assert_ok;
363/// let error: Result<usize, usize> = Err(42);
364/// assert_ok!(error);
365/// ```
366///
367/// ### With extra message
368///
369/// ```should_panic
370/// use mz_ore::assert_ok;
371/// let other_val = 100;
372/// let error: Result<usize, usize> = Err(42);
373/// assert_ok!(error, "ohh noo! x {other_val}");
374/// ```
375///
376#[macro_export]
377macro_rules! assert_ok {
378    ($val:expr, $($msg:tt)+) => {{
379        if let Err(y) = &$val {
380            panic!("assertion failed: expected Ok found Err({y:?}), {}", format!($($msg)+));
381        }
382    }};
383    ($val:expr) => {{
384        if let Err(y) = &$val {
385            panic!("assertion failed: expected Ok found Err({y:?})");
386        }
387    }}
388}
389
390/// Asserts that the provided expression, that returns a `Result`, is `Err`.
391///
392/// # Motivation
393///
394/// The standard pattern for asserting a value is `Err` using the `assert!` macro is:
395///
396/// ```
397/// # let x: Result<usize, usize> = Err(42);
398/// assert!(x.is_err());
399/// ```
400///
401/// The issue with this pattern is when the assertion fails it only prints `false`
402/// and not the value contained in the `Ok(_)` variant which makes debugging difficult.
403///
404/// # Examples
405///
406/// ### Basic Use
407///
408/// ```should_panic
409/// use mz_ore::assert_err;
410/// let error: Result<usize, usize> = Ok(42);
411/// assert_err!(error);
412/// ```
413///
414/// ### With extra message
415///
416/// ```should_panic
417/// use mz_ore::assert_err;
418/// let other_val = 100;
419/// let error: Result<usize, usize> = Ok(42);
420/// assert_err!(error, "ohh noo! x {other_val}");
421/// ```
422///
423#[macro_export]
424macro_rules! assert_err {
425    ($val:expr, $($msg:tt)+) => {{
426        if let Ok(y) = &$val {
427            panic!("assertion failed: expected Err found Ok({y:?}), {}", format!($($msg)+));
428        }
429    }};
430    ($val:expr) => {{
431        if let Ok(y) = &$val {
432            panic!("assertion failed: expected Err found Ok({y:?})");
433        }
434    }}
435}
436
437#[cfg(test)]
438mod tests {
439    #[crate::test]
440    fn test_assert_contains_str() {
441        assert_contains!("hello", "ello");
442    }
443
444    #[crate::test]
445    fn test_assert_contains_slice() {
446        assert_contains!(&[1, 2, 3], 2);
447    }
448
449    #[crate::test]
450    #[should_panic(expected = "assertion failed: `left.contains(right)`:
451  left: `\"hello\"`
452 right: `\"yellow\"`")]
453    fn test_assert_contains_fail() {
454        assert_contains!("hello", "yellow");
455    }
456
457    #[crate::test]
458    #[should_panic(expected = "assertion failed: expected None found Some(42)")]
459    fn test_assert_none_fail() {
460        assert_none!(Some(42));
461    }
462
463    #[crate::test]
464    #[should_panic(expected = "assertion failed: expected None found Some(42), ohh no!")]
465    fn test_assert_none_fail_with_msg() {
466        assert_none!(Some(42), "ohh no!");
467    }
468
469    #[crate::test]
470    fn test_assert_ok() {
471        assert_ok!(Ok::<_, usize>(42));
472    }
473
474    #[crate::test]
475    #[should_panic(expected = "assertion failed: expected Ok found Err(42)")]
476    fn test_assert_ok_fail() {
477        assert_ok!(Err::<usize, _>(42));
478    }
479
480    #[crate::test]
481    #[should_panic(expected = "assertion failed: expected Ok found Err(42), ohh no!")]
482    fn test_assert_ok_fail_with_msg() {
483        assert_ok!(Err::<usize, _>(42), "ohh no!");
484    }
485
486    #[crate::test]
487    fn test_assert_err() {
488        assert_err!(Err::<usize, _>(42));
489    }
490
491    #[crate::test]
492    #[should_panic(expected = "assertion failed: expected Err found Ok(42)")]
493    fn test_assert_err_fail() {
494        assert_err!(Ok::<_, usize>(42));
495    }
496
497    #[crate::test]
498    #[should_panic(expected = "assertion failed: expected Err found Ok(42), ohh no!")]
499    fn test_assert_err_fail_with_msg() {
500        assert_err!(Ok::<_, usize>(42), "ohh no!");
501    }
502}