diff --git a/proptest/CHANGELOG.md b/proptest/CHANGELOG.md index adc6b9da..e3e5a1d0 100644 --- a/proptest/CHANGELOG.md +++ b/proptest/CHANGELOG.md @@ -3,6 +3,9 @@ ### New Features - When running persisted regressions, the most recently added regression is now run first. +- Added `handle-panics` feature which enables catching panics raised in tests and turning them into failures +- Added `backtrace` feature which enables capturing backtraces for both test failures and panics, + if `handle-panics` feature is enabled ## 1.5.0 diff --git a/proptest/Cargo.toml b/proptest/Cargo.toml index 5c72e108..aa917bab 100644 --- a/proptest/Cargo.toml +++ b/proptest/Cargo.toml @@ -56,6 +56,15 @@ atomic64bit = [] bit-set = [ "dep:bit-set", "dep:bit-vec" ] +# Enables proper handling of panics +# In particular, hides all intermediate panics flowing into stderr during shrink phase +handle-panics = ["std"] + +# Enables gathering of failure backtraces +# * when test failure is reported via `prop_assert_*` macro +# * when normal assertion fails or panic fires, if `handle-panics` feature is enabled too +backtrace = ["std"] + [dependencies] bitflags = "2" unarray = "0.1.4" diff --git a/proptest/src/collection.rs b/proptest/src/collection.rs index 8ecb0584..91b7e753 100644 --- a/proptest/src/collection.rs +++ b/proptest/src/collection.rs @@ -705,7 +705,7 @@ mod test { match result { Ok(true) => num_successes += 1, - Err(TestError::Fail(_, value)) => { + Err(TestError::Fail(_, _, value)) => { // The minimal case always has between 5 (due to min // length) and 9 (min element value = 1) elements, and // always sums to exactly 9. diff --git a/proptest/src/strategy/flatten.rs b/proptest/src/strategy/flatten.rs index 63ac4160..115b4673 100644 --- a/proptest/src/strategy/flatten.rs +++ b/proptest/src/strategy/flatten.rs @@ -293,7 +293,7 @@ mod test { match result { Ok(_) => {} - Err(TestError::Fail(_, v)) => { + Err(TestError::Fail(_, _, v)) => { failures += 1; assert_eq!((10001, 10002), v); } diff --git a/proptest/src/strategy/unions.rs b/proptest/src/strategy/unions.rs index d174d122..80a88c36 100644 --- a/proptest/src/strategy/unions.rs +++ b/proptest/src/strategy/unions.rs @@ -495,8 +495,8 @@ mod test { match result { Ok(true) => passed += 1, - Err(TestError::Fail(_, 15)) => converged_low += 1, - Err(TestError::Fail(_, 30)) => converged_high += 1, + Err(TestError::Fail(_, _, 15)) => converged_low += 1, + Err(TestError::Fail(_, _, 30)) => converged_high += 1, e => panic!("Unexpected result: {:?}", e), } } @@ -572,8 +572,8 @@ mod test { match result { Ok(true) => passed += 1, - Err(TestError::Fail(_, 15)) => converged_low += 1, - Err(TestError::Fail(_, 30)) => converged_high += 1, + Err(TestError::Fail(_, _, 15)) => converged_low += 1, + Err(TestError::Fail(_, _, 30)) => converged_high += 1, e => panic!("Unexpected result: {:?}", e), } } diff --git a/proptest/src/string.rs b/proptest/src/string.rs index 935cb21c..40c93794 100644 --- a/proptest/src/string.rs +++ b/proptest/src/string.rs @@ -171,10 +171,7 @@ pub fn string_regex_parsed(expr: &Hir) -> ParseResult { /// [`regex` crate's documentation](https://docs.rs/regex/*/regex/#opt-out-of-unicode-support) /// for more information. pub fn bytes_regex(regex: &str) -> ParseResult> { - let hir = ParserBuilder::new() - .utf8(false) - .build() - .parse(regex)?; + let hir = ParserBuilder::new().utf8(false).build().parse(regex)?; bytes_regex_parsed(&hir) } @@ -361,8 +358,8 @@ fn unsupported(error: &'static str) -> Result { mod test { use std::collections::HashSet; - use regex::Regex; use regex::bytes::Regex as BytesRegex; + use regex::Regex; use super::*; @@ -402,7 +399,8 @@ mod test { max_distinct: usize, iterations: usize, ) { - let generated = generate_byte_values_matching_regex(pattern, iterations); + let generated = + generate_byte_values_matching_regex(pattern, iterations); assert!( generated.len() >= min_distinct, "Expected to generate at least {} strings, but only \ @@ -477,7 +475,8 @@ mod test { if !ok { panic!( "Generated string {:?} which does not match {:?}", - printable_ascii(&s), pattern + printable_ascii(&s), + pattern ); } @@ -584,10 +583,15 @@ mod test { fn test_non_utf8_byte_strings() { do_test_bytes(r"(?-u)[\xC0-\xFF]\x20", 64, 64, 512); do_test_bytes(r"(?-u)\x20[\x80-\xBF]", 64, 64, 512); - do_test_bytes(r#"(?x-u) + do_test_bytes( + r#"(?x-u) \xed (( ( \xa0\x80 | \xad\xbf | \xae\x80 | \xaf\xbf ) ( \xed ( \xb0\x80 | \xbf\xbf ) )? ) - | \xb0\x80 | \xbe\x80 | \xbf\xbf )"#, 15, 15, 120); + | \xb0\x80 | \xbe\x80 | \xbf\xbf )"#, + 15, + 15, + 120, + ); } fn assert_send_and_sync(_: T) {} diff --git a/proptest/src/sugar.rs b/proptest/src/sugar.rs index 2dbd8e99..53a484df 100644 --- a/proptest/src/sugar.rs +++ b/proptest/src/sugar.rs @@ -754,7 +754,10 @@ macro_rules! prop_assert { let message = format!($($fmt)*); let message = format!("{} at {}:{}", message, file!(), line!()); return ::core::result::Result::Err( - $crate::test_runner::TestCaseError::fail(message)); + $crate::test_runner::TestCaseError::Fail( + message.into(), + $crate::test_runner::Backtrace::capture(), + )); } }; } diff --git a/proptest/src/test_runner/backtrace.rs b/proptest/src/test_runner/backtrace.rs new file mode 100644 index 00000000..d5a77b2e --- /dev/null +++ b/proptest/src/test_runner/backtrace.rs @@ -0,0 +1,135 @@ +//- +// Copyright 2024 +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use core::fmt; +/// Holds test failure backtrace, if captured +/// +/// If feature `backtrace` is disabled, it's a zero-sized struct with no logic +/// +/// If `backtrace` is enabled, attempts to capture backtrace using `std::backtrace::Backtrace` - +/// if requested +#[derive(Clone, Default)] +pub struct Backtrace(internal::Backtrace); + +impl Backtrace { + /// Creates empty backtrace object + /// + /// Used when client code doesn't care + pub fn empty() -> Self { + Self(internal::Backtrace::empty()) + } + /// Tells whether there's backtrace captured + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + /// Attempts to capture backtrace - but only if `backtrace` feature is enabled + #[inline(always)] + pub fn capture() -> Self { + Self(internal::Backtrace::capture()) + } +} + +impl fmt::Debug for Backtrace { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&self.0, f) + } +} + +impl fmt::Display for Backtrace { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + +#[cfg(feature = "backtrace")] +mod internal { + use core::fmt; + use std::backtrace as bt; + use std::sync::Arc; + + // `std::backtrace::Backtrace` isn't `Clone`, so we have + // to use `Arc` to also maintain `Send + Sync` + #[derive(Clone, Default)] + pub struct Backtrace(Option>); + + impl Backtrace { + pub fn empty() -> Self { + Self(None) + } + + pub fn is_empty(&self) -> bool { + self.0.is_none() + } + + #[inline(always)] + pub fn capture() -> Self { + let bt = bt::Backtrace::capture(); + // Store only if we have backtrace + if bt.status() == bt::BacktraceStatus::Captured { + Self(Some(Arc::new(bt))) + } else { + Self(None) + } + } + } + + impl fmt::Debug for Backtrace { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(ref arc) = self.0 { + fmt::Debug::fmt(arc.as_ref(), f) + } else { + Ok(()) + } + } + } + + impl fmt::Display for Backtrace { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(ref arc) = self.0 { + fmt::Display::fmt(arc.as_ref(), f) + } else { + Ok(()) + } + } + } +} + +#[cfg(not(feature = "backtrace"))] +mod internal { + use core::fmt; + + #[derive(Clone, Default)] + pub struct Backtrace; + + impl Backtrace { + pub fn empty() -> Self { + Self + } + + pub fn is_empty(&self) -> bool { + true + } + + pub fn capture() -> Self { + Self + } + } + + impl fmt::Debug for Backtrace { + fn fmt(&self, _: &mut fmt::Formatter<'_>) -> fmt::Result { + Ok(()) + } + } + + impl fmt::Display for Backtrace { + fn fmt(&self, _: &mut fmt::Formatter<'_>) -> fmt::Result { + Ok(()) + } + } +} diff --git a/proptest/src/test_runner/errors.rs b/proptest/src/test_runner/errors.rs index bf17f201..437b1ad7 100644 --- a/proptest/src/test_runner/errors.rs +++ b/proptest/src/test_runner/errors.rs @@ -1,5 +1,5 @@ //- -// Copyright 2017, 2018 The proptest developers +// Copyright 2017, 2018, 2024 The proptest developers // // Licensed under the Apache License, Version 2.0 or the MIT license @@ -14,6 +14,8 @@ use std::string::ToString; use crate::test_runner::Reason; +use super::Backtrace; + /// Errors which can be returned from test cases to indicate non-successful /// completion. /// @@ -30,7 +32,7 @@ pub enum TestCaseError { /// a new input and try again. Reject(Reason), /// The code under test failed the test. - Fail(Reason), + Fail(Reason, Backtrace), } /// Indicates the type of test that ran successfully. @@ -76,7 +78,7 @@ impl TestCaseError { /// The string should indicate the location of the failure, but may /// generally be any string. pub fn fail(reason: impl Into) -> Self { - TestCaseError::Fail(reason.into()) + TestCaseError::Fail(reason.into(), Backtrace::empty()) } } @@ -86,7 +88,13 @@ impl fmt::Display for TestCaseError { TestCaseError::Reject(ref whence) => { write!(f, "Input rejected at {}", whence) } - TestCaseError::Fail(ref why) => write!(f, "Case failed: {}", why), + TestCaseError::Fail(ref why, ref bt) => { + if bt.is_empty() { + write!(f, "Case failed: {why}") + } else { + write!(f, "Case failed: {why}\n{bt}") + } + } } } } @@ -99,7 +107,7 @@ impl From for TestCaseError { } /// A failure state from running test cases for a single test. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone)] pub enum TestError { /// The test was aborted for the given reason, for example, due to too many /// inputs having been rejected. @@ -107,16 +115,41 @@ pub enum TestError { /// A failing test case was found. The string indicates where and/or why /// the test failed. The `T` is the minimal input found to reproduce the /// failure. - Fail(Reason, T), + Fail(Reason, Backtrace, T), } +impl PartialEq for TestError { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Abort(l0), Self::Abort(r0)) => l0 == r0, + (Self::Fail(l0, _, l2), Self::Fail(r0, _, r2)) => { + l0 == r0 && l2 == r2 + } + (TestError::Abort(_), TestError::Fail(_, _, _)) + | (TestError::Fail(_, _, _), TestError::Abort(_)) => false, + } + } +} + +impl Eq for TestError {} + impl fmt::Display for TestError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { TestError::Abort(ref why) => write!(f, "Test aborted: {}", why), - TestError::Fail(ref why, ref what) => { + TestError::Fail(ref why, ref bt, ref what) => { writeln!(f, "Test failed: {}.", why)?; - write!(f, "minimal failing input: {:#?}", what) + + if !bt.is_empty() { + writeln!(f, "\nstack backtrace:\n{bt}")?; + // No need for extra newline, backtrace seems to print it anyway + } else { + // Extra empty line between failure description and minimal failing input + writeln!(f)?; + } + + writeln!(f, "minimal failing input: {:#?}", what)?; + Ok(()) } } } diff --git a/proptest/src/test_runner/mod.rs b/proptest/src/test_runner/mod.rs index d7516ab9..db7a076d 100644 --- a/proptest/src/test_runner/mod.rs +++ b/proptest/src/test_runner/mod.rs @@ -1,5 +1,5 @@ //- -// Copyright 2017, 2018 The proptest developers +// Copyright 2017, 2018, 2024 The proptest developers // // Licensed under the Apache License, Version 2.0 or the MIT license @@ -12,6 +12,7 @@ //! You do not normally need to access things in this module directly except //! when implementing new low-level strategies. +mod backtrace; mod config; mod errors; mod failure_persistence; @@ -22,6 +23,7 @@ mod result_cache; mod rng; mod runner; +pub use self::backtrace::*; pub use self::config::*; pub use self::errors::*; pub use self::failure_persistence::*; diff --git a/proptest/src/test_runner/replay.rs b/proptest/src/test_runner/replay.rs index 4365d553..e81ce9b4 100644 --- a/proptest/src/test_runner/replay.rs +++ b/proptest/src/test_runner/replay.rs @@ -83,7 +83,7 @@ fn step_to_char(step: &TestCaseResult) -> char { match *step { Ok(_) => '+', Err(TestCaseError::Reject(_)) => '!', - Err(TestCaseError::Fail(_)) => '-', + Err(TestCaseError::Fail(_, _)) => '-', } } diff --git a/proptest/src/test_runner/rng.rs b/proptest/src/test_runner/rng.rs index 31dd3a35..2599ccb3 100644 --- a/proptest/src/test_runner/rng.rs +++ b/proptest/src/test_runner/rng.rs @@ -9,7 +9,7 @@ use crate::std_facade::{Arc, String, ToOwned, Vec}; use core::result::Result; -use core::{fmt, str, u8, convert::TryInto}; +use core::{convert::TryInto, fmt, str, u8}; use rand::{self, Rng, RngCore, SeedableRng}; use rand_chacha::ChaChaRng; diff --git a/proptest/src/test_runner/runner.rs b/proptest/src/test_runner/runner.rs index d209dff6..f419e03f 100644 --- a/proptest/src/test_runner/runner.rs +++ b/proptest/src/test_runner/runner.rs @@ -1,5 +1,5 @@ //- -// Copyright 2017, 2018, 2019 The proptest developers +// Copyright 2017, 2018, 2019, 2024 The proptest developers // // Licensed under the Apache License, Version 2.0 or the MIT license @@ -35,6 +35,8 @@ use crate::test_runner::replay; use crate::test_runner::result_cache::*; use crate::test_runner::rng::TestRng; +use super::Backtrace; + #[cfg(feature = "fork")] const ENV_FORK_FILE: &'static str = "_PROPTEST_FORKFILE"; @@ -210,6 +212,128 @@ where }) } +#[cfg(feature = "handle-panics")] +mod panicky { + //! Implementation of scoped panic hooks + //! + //! 1. `with_hook` serves as entry point, it executes body closure with panic hook closure + //! installed as scoped panic hook + //! 2. Upon first execution, current panic hook is replaced with `scoped_hook_dispatcher` + //! in a thread-safe manner, and original hook is stored for later use + //! 3. When panic occurs, `scoped_hook_dispatcher` either delegates execution to scoped + //! panic hook, if one is installed, or back to original hook stored earlier. + //! This preserves original behavior when scoped hook isn't used + //! 4. When `with_hook` is used, it replaces stored scoped hook pointer with pointer to + //! hook closure passed as parameter. Old hook pointer is set to be restored unconditionally + //! via drop guard. Then, normal body closure is executed. + use std::boxed::Box; + use std::cell::Cell; + use std::panic::{set_hook, take_hook, PanicInfo}; + use std::sync::Once; + use std::{mem, ptr}; + + thread_local! { + /// Pointer to currently installed scoped panic hook, if any + /// + /// NB: pointers to arbitrary fn's are fat, and Rust doesn't allow crafting null pointers + /// to fat objects. So we just store const pointer to tuple with whatever data we need + static SCOPED_HOOK_PTR: Cell<*const (*mut dyn FnMut(&PanicInfo<'_>),)> = Cell::new(ptr::null()); + } + + static INIT_ONCE: Once = Once::new(); + /// Default panic hook, the one which was present before installing scoped one + /// + /// NB: no need for external sync, value is mutated only once, when init is performed + static mut DEFAULT_HOOK: Option) + Send + Sync>> = + None; + /// Replaces currently installed panic hook with `scoped_hook_dispatcher` once, + /// in a thread-safe manner + fn init() { + INIT_ONCE.call_once(|| { + let old_handler = take_hook(); + set_hook(Box::new(scoped_hook_dispatcher)); + unsafe { + DEFAULT_HOOK = Some(old_handler); + } + }); + } + /// Panic hook which delegates execution to scoped hook, + /// if one installed, or to default hook + fn scoped_hook_dispatcher(info: &PanicInfo<'_>) { + let handler = SCOPED_HOOK_PTR.get(); + if !handler.is_null() { + // It's assumed that if container's ptr is not null, ptr to `FnMut` is non-null too. + // Correctness **must** be ensured by hook switch code in `with_hook` + let hook = unsafe { &mut *(*handler).0 }; + (hook)(info); + return; + } + + if let Some(hook) = unsafe { DEFAULT_HOOK.as_ref() } { + (hook)(info); + } + } + /// Executes stored closure when dropped + struct Finally(Option); + + impl Finally { + fn new(body: F) -> Self { + Self(Some(body)) + } + } + + impl Drop for Finally { + fn drop(&mut self) { + if let Some(body) = self.0.take() { + body(); + } + } + } + /// Executes main closure `body` while installing `guard` as scoped panic hook, + /// for execution duration. + /// + /// Any panics which happen during execution of `body` are passed to `guard` hook + /// to collect any info necessary, although unwind process is **NOT** interrupted. + /// See module documentation for details + /// + /// # Parameters + /// * `panic_hook` - scoped panic hook, functions for the duration of `body` execution + /// * `body` - actual logic covered by `panic_hook` + /// + /// # Returns + /// `body`'s return value + pub fn with_hook( + mut panic_hook: impl FnMut(&PanicInfo<'_>), + body: impl FnOnce() -> R, + ) -> R { + init(); + // Construct scoped hook pointer + let guard_tuple = (unsafe { + // `mem::transmute` is needed due to borrow checker restrictions to erase all lifetimes + mem::transmute(&mut panic_hook as *mut dyn FnMut(&PanicInfo<'_>)) + },); + let old_tuple = SCOPED_HOOK_PTR.replace(&guard_tuple); + // Old scoped hook **must** be restored before leaving function scope to keep it sound + let _undo = Finally::new(|| { + SCOPED_HOOK_PTR.set(old_tuple); + }); + body() + } +} + +#[cfg(not(feature = "handle-panics"))] +mod panicky { + use std::panic::PanicInfo; + /// Simply executes `body` and returns its execution result. + /// Hook parameter is ignored + pub fn with_hook( + _: impl FnMut(&PanicInfo<'_>), + body: impl FnOnce() -> R, + ) -> R { + body() + } +} + #[cfg(feature = "std")] fn call_test( runner: &mut TestRunner, @@ -225,6 +349,7 @@ where F: Fn(V) -> TestCaseResult, R: Iterator, { + use core::mem; use std::time; let timeout = runner.config.timeout(); @@ -252,13 +377,21 @@ where let time_start = time::Instant::now(); + let mut bt = Backtrace::empty(); let mut result = unwrap_or!( - panic::catch_unwind(AssertUnwindSafe(|| test(case))), - what => Err(TestCaseError::Fail( - what.downcast::<&'static str>().map(|s| (*s).into()) + panicky::with_hook( + |_| { bt = Backtrace::capture(); }, + || panic::catch_unwind(AssertUnwindSafe(|| test(case))) + ), + what => { + let what = what.downcast::<&'static str>().map(|s| (*s).into()) .or_else(|what| what.downcast::().map(|b| (*b).into())) .or_else(|what| what.downcast::>().map(|b| (*b).into())) - .unwrap_or_else(|_| "".into())))); + .unwrap_or_else(|_| "".into()); + + Err(TestCaseError::Fail(what, mem::take(&mut bt))) + } + ); // If there is a timeout and we exceeded it, fail the test here so we get // consistent behaviour. (The parent process cannot precisely time the test @@ -284,8 +417,14 @@ where Err(TestCaseError::Reject(ref reason)) => { verbose_message!(runner, INFO_LOG, "Test case rejected: {}", reason) } - Err(TestCaseError::Fail(ref reason)) => { - verbose_message!(runner, INFO_LOG, "Test case failed: {}", reason) + Err(TestCaseError::Fail(ref reason, ref bt)) => { + verbose_message!( + runner, + INFO_LOG, + "Test case failed: {}\n{}", + reason, + bt + ) } } @@ -620,7 +759,7 @@ impl TestRunner { &mut fork_output, false, ); - if let Err(TestError::Fail(_, ref value)) = result { + if let Err(TestError::Fail(_, _, ref value)) = result { if let Some(ref mut failure_persistence) = self.config.failure_persistence { @@ -733,8 +872,8 @@ impl TestRunner { match result { Ok(success_type) => Ok(success_type), - Err(TestCaseError::Fail(why)) => { - let why = self + Err(TestCaseError::Fail(why, bt)) => { + let (why, bt) = self .shrink( &mut case, test, @@ -743,8 +882,8 @@ impl TestRunner { fork_output, is_from_persisted_seed, ) - .unwrap_or(why); - Err(TestError::Fail(why, case.current())) + .unwrap_or((why, bt)); + Err(TestError::Fail(why, bt, case.current())) } Err(TestCaseError::Reject(whence)) => { self.reject_global(whence)?; @@ -761,7 +900,7 @@ impl TestRunner { result_cache: &mut dyn ResultCache, fork_output: &mut ForkOutput, is_from_persisted_seed: bool, - ) -> Option { + ) -> Option<(Reason, Backtrace)> { #[cfg(feature = "std")] use std::time; @@ -871,8 +1010,8 @@ impl TestRunner { break; } } - Err(TestCaseError::Fail(why)) => { - last_failure = Some(why); + Err(TestCaseError::Fail(why, bt)) => { + last_failure = Some((why, bt)); if !case.simplify() { verbose_message!( self, @@ -1108,7 +1247,14 @@ mod test { } }); - assert_eq!(Err(TestError::Fail("not less than 5".into(), 5)), result); + assert_eq!( + Err(TestError::Fail( + "not less than 5".into(), + Backtrace::empty(), + 5 + )), + result + ); } #[test] @@ -1121,7 +1267,14 @@ mod test { assert!(v < 5, "not less than 5"); Ok(()) }); - assert_eq!(Err(TestError::Fail("not less than 5".into(), 5)), result); + assert_eq!( + Err(TestError::Fail( + "not less than 5".into(), + Backtrace::empty(), + 5 + )), + result + ); } #[test] @@ -1141,7 +1294,7 @@ mod test { { TestRunner::new(config.clone()) .run(&(0i32..max), |_v| { - Err(TestCaseError::Fail("persist a failure".into())) + Err(TestCaseError::fail("persist a failure")) }) .expect_err("didn't fail?"); } @@ -1190,7 +1343,7 @@ mod test { if v.0 < max / 2 { Ok(()) } else { - Err(TestCaseError::Fail("too big".into())) + Err(TestCaseError::fail("too big")) } }) .expect_err("didn't fail?") @@ -1201,7 +1354,7 @@ mod test { if v.0 >= max / 2 { Ok(()) } else { - Err(TestCaseError::Fail("too small".into())) + Err(TestCaseError::fail("too small")) } }) .expect_err("didn't fail?") @@ -1212,7 +1365,7 @@ mod test { if v.0 < max / 2 { Ok(()) } else { - Err(TestCaseError::Fail("too big".into())) + Err(TestCaseError::fail("too big")) } }) .expect_err("didn't fail?") @@ -1223,7 +1376,7 @@ mod test { if v.0 >= max / 2 { Ok(()) } else { - Err(TestCaseError::Fail("too small".into())) + Err(TestCaseError::fail("too small")) } }) .expect_err("didn't fail?") @@ -1302,7 +1455,7 @@ mod test { .unwrap(); match failure { - TestError::Fail(_, value) => assert_eq!(500, value), + TestError::Fail(_, _, value) => assert_eq!(500, value), failure => panic!("Unexpected failure: {:?}", failure), } } @@ -1330,7 +1483,7 @@ mod test { .unwrap(); match failure { - TestError::Fail(_, value) => assert_eq!(500, value), + TestError::Fail(_, _, value) => assert_eq!(500, value), failure => panic!("Unexpected failure: {:?}", failure), } } @@ -1358,7 +1511,7 @@ mod test { .unwrap(); match failure { - TestError::Fail(_, value) => assert_eq!(500, value), + TestError::Fail(_, _, value) => assert_eq!(500, value), failure => panic!("Unexpected failure: {:?}", failure), } } @@ -1389,7 +1542,7 @@ mod test { .unwrap(); match failure { - TestError::Fail(_, value) => assert_eq!(500, value), + TestError::Fail(_, _, value) => assert_eq!(500, value), failure => panic!("Unexpected failure: {:?}", failure), } } @@ -1430,7 +1583,7 @@ mod test { .unwrap(); match failure { - TestError::Fail(_, value) => assert_eq!(500, value), + TestError::Fail(_, _, value) => assert_eq!(500, value), failure => panic!("Unexpected failure: {:?}", failure), } } @@ -1463,7 +1616,7 @@ mod test { }); assert!(pass.get()); - if let Err(TestError::Fail(_, val)) = result { + if let Err(TestError::Fail(_, _, val)) = result { assert_eq!(6, val); } else { panic!("Incorrect result: {:?}", result); @@ -1540,7 +1693,7 @@ mod timeout_tests { Ok(()) }); - if let Err(TestError::Fail(_, value)) = result { + if let Err(TestError::Fail(_, _, value)) = result { // Ensure the final value was in fact a failing case. assert!(value > u32::MAX as u64); } else {