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}