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}