diff --git a/Cargo.toml b/Cargo.toml index 52244e8..33831c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,9 @@ log = { version = "0.4", features = ["std"] } once_cell = "1.9.0" rand = "0.8" +[dev-dependencies] +anyhow = "1.0" + [features] failpoints = [] diff --git a/src/lib.rs b/src/lib.rs index f23cc44..916987a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -227,7 +227,8 @@ use std::collections::HashMap; use std::env::VarError; -use std::fmt::Debug; +use std::error::Error; +use std::fmt::{Debug, Display}; use std::str::FromStr; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Condvar, Mutex, MutexGuard, RwLock, TryLockError}; @@ -428,6 +429,33 @@ impl FromStr for Action { } } +/// A synthetic error created as part of [`try_fail_point!`]. +#[doc(hidden)] +#[derive(Debug)] +pub struct ReturnError(pub String); + +impl ReturnError { + const SYNTHETIC: &str = "synthetic failpoint error"; +} + +impl From> for ReturnError { + fn from(msg: Option) -> Self { + Self(msg.unwrap_or_else(|| Self::SYNTHETIC.to_string())) + } +} + +impl Display for ReturnError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } +} + +impl Error for ReturnError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + None + } +} + #[cfg_attr(feature = "cargo-clippy", allow(clippy::mutex_atomic))] #[derive(Debug)] struct FailPoint { @@ -843,6 +871,32 @@ macro_rules! fail_point { }}; } +/// A variant of [`fail_point`] designed for a function that returns [`std::result::Result`]. +/// +/// ```rust +/// # #[macro_use] extern crate fail; +/// fn fallible_function() -> Result> { +/// try_fail_point!("a-fail-point"); +/// Ok(42) +/// } +/// ``` +/// +/// This +#[macro_export] +#[cfg(feature = "failpoints")] +macro_rules! try_fail_point { + ($name:expr) => {{ + if let Some(e) = $crate::eval($name, |msg| $crate::ReturnError::from(msg)) { + return Err(From::from(e)); + } + }}; + ($name:expr, $cond:expr) => {{ + if $cond { + $crate::try_fail_point!($name); + } + }}; +} + /// Define a fail point (disabled, see `failpoints` feature). #[macro_export] #[cfg(not(feature = "failpoints"))] @@ -852,6 +906,14 @@ macro_rules! fail_point { ($name:expr, $cond:expr, $e:expr) => {{}}; } +/// Define a fail point for a Result-returning function (disabled, see `failpoints` feature). +#[macro_export] +#[cfg(not(feature = "failpoints"))] +macro_rules! try_fail_point { + ($name:expr) => {{}}; + ($name:expr, $cond:expr) => {{}}; +} + #[cfg(test)] mod tests { use super::*; @@ -1032,6 +1094,25 @@ mod tests { } } + #[test] + #[cfg(feature = "failpoints")] + fn test_try_failpoint() -> anyhow::Result<()> { + // This is effective a compile-only test. + #[allow(dead_code)] + fn test_anyhow() -> anyhow::Result<()> { + try_fail_point!("tryfail-with-result"); + Ok(()) + } + #[allow(dead_code)] + fn test_stderr() -> Result<(), Box> { + try_fail_point!("tryfail-with-result-2"); + Ok(()) + } + test_anyhow()?; + test_stderr().map_err(anyhow::Error::msg)?; + Ok(()) + } + // This case should be tested as integration case, but when calling `teardown` other cases // like `test_pause` maybe also affected, so it's better keep it here. #[test]