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}