Skip to main content

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