Skip to content

Commit 6e3a87d

Browse files
committed
Add try_fail_point!
In my project I have a ton of code which uses `anyhow::Result`. I want a convenient way to force a function to return a stock error from a failpoint. Today this requires something like: ``` fail::fail_point!("main", true, |msg| { let msg = msg.as_deref().unwrap_or("synthetic error"); Err(anyhow::anyhow!("{msg}")) }); ``` which is cumbersome to copy around. Now, I conservatively made this a new macro. I am not sure how often the use case of a fail point for an infallible (i.e. non-`Result`) function occurs. It may make sense to require those to take a distinct `inject_point!` or something? Signed-off-by: Colin Walters <[email protected]>
1 parent 5bc95a1 commit 6e3a87d

File tree

2 files changed

+109
-53
lines changed

2 files changed

+109
-53
lines changed

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ log = { version = "0.4", features = ["std"] }
1818
once_cell = "1.9.0"
1919
rand = "0.8"
2020

21+
[dev-dependencies]
22+
anyhow = "1.0"
23+
2124
[features]
2225
failpoints = []
2326

src/lib.rs

Lines changed: 106 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -126,16 +126,16 @@
126126
//! fail point will immediately return from the function, optionally with a
127127
//! configurable value.
128128
//!
129-
//! The setup for early return requires a slightly diferent invocation of the
130-
//! `fail_point!` macro. To illustrate this, let's modify the `do_fallible_work`
129+
//! The setup for early return is most convenient with the [`try_fail_point`] macro.
130+
//! To illustrate this, let's modify the `do_fallible_work`
131131
//! function we used earlier to return a `Result`:
132132
//!
133133
//! ```rust
134134
//! use fail::{fail_point, FailScenario};
135135
//! use std::io;
136136
//!
137137
//! fn do_fallible_work() -> io::Result<()> {
138-
//! fail_point!("read-dir");
138+
//! try_fail_point!("read-dir");
139139
//! let _dir: Vec<_> = std::fs::read_dir(".")?.collect();
140140
//! // ... do some work on the directory ...
141141
//! Ok(())
@@ -150,53 +150,6 @@
150150
//! }
151151
//! ```
152152
//!
153-
//! This example has more proper Rust error handling, with no unwraps
154-
//! anywhere. Instead it uses `?` to propagate errors via the `Result` type
155-
//! return values. This is more realistic Rust code.
156-
//!
157-
//! The "read-dir" fail point though is not yet configured to support early
158-
//! return, so if we attempt to configure it to "return", we'll see an error
159-
//! like
160-
//!
161-
//! ```sh
162-
//! $ FAILPOINTS=read-dir=return cargo run --features fail/failpoints
163-
//! Finished dev [unoptimized + debuginfo] target(s) in 0.13s
164-
//! Running `target/debug/failpointtest`
165-
//! thread 'main' panicked at 'Return is not supported for the fail point "read-dir"', src/main.rs:7:5
166-
//! note: Run with `RUST_BACKTRACE=1` for a backtrace.
167-
//! ```
168-
//!
169-
//! This error tells us that the "read-dir" fail point is not defined correctly
170-
//! to support early return, and gives us the line number of that fail point.
171-
//! What we're missing in the fail point definition is code describring _how_ to
172-
//! return an error value, and the way we do this is by passing `fail_point!` a
173-
//! closure that returns the same type as the enclosing function.
174-
//!
175-
//! Here's a variation that does so:
176-
//!
177-
//! ```rust
178-
//! # use std::io;
179-
//! fn do_fallible_work() -> io::Result<()> {
180-
//! fail::fail_point!("read-dir", |_| {
181-
//! Err(io::Error::new(io::ErrorKind::PermissionDenied, "error"))
182-
//! });
183-
//! let _dir: Vec<_> = std::fs::read_dir(".")?.collect();
184-
//! // ... do some work on the directory ...
185-
//! Ok(())
186-
//! }
187-
//! ```
188-
//!
189-
//! And now if the "read-dir" fail point is configured to "return" we get a
190-
//! different result:
191-
//!
192-
//! ```sh
193-
//! $ FAILPOINTS=read-dir=return cargo run --features fail/failpoints
194-
//! Compiling failpointtest v0.1.0
195-
//! Finished dev [unoptimized + debuginfo] target(s) in 2.38s
196-
//! Running `target/debug/failpointtest`
197-
//! Error: Custom { kind: PermissionDenied, error: StringError("error") }
198-
//! ```
199-
//!
200153
//! This time, `do_fallible_work` returned the error defined in our closure,
201154
//! which propagated all the way up and out of main.
202155
//!
@@ -207,8 +160,7 @@
207160
//! panic and return early. But that's not all they can do. To learn more see
208161
//! the documentation for [`cfg`](fn.cfg.html),
209162
//! [`cfg_callback`](fn.cfg_callback.html) and
210-
//! [`fail_point!`](macro.fail_point.html).
211-
//!
163+
//! [`fail_point!`](macro.fail_point.html) and [`try_fail_point!`].
212164
//!
213165
//! ## Usage considerations
214166
//!
@@ -227,7 +179,8 @@
227179

228180
use std::collections::HashMap;
229181
use std::env::VarError;
230-
use std::fmt::Debug;
182+
use std::error::Error;
183+
use std::fmt::{Debug, Display};
231184
use std::str::FromStr;
232185
use std::sync::atomic::{AtomicUsize, Ordering};
233186
use std::sync::{Arc, Condvar, Mutex, MutexGuard, RwLock, TryLockError};
@@ -428,6 +381,39 @@ impl FromStr for Action {
428381
}
429382
}
430383

384+
/// A synthetic error created as part of [`try_fail_point!`].
385+
#[doc(hidden)]
386+
#[derive(Debug)]
387+
pub struct ReturnError(pub String);
388+
389+
impl ReturnError {
390+
const SYNTHETIC: &str = "synthetic failpoint error";
391+
}
392+
393+
impl From<Option<String>> for ReturnError {
394+
fn from(msg: Option<String>) -> Self {
395+
Self(msg.unwrap_or_else(|| Self::SYNTHETIC.to_string()))
396+
}
397+
}
398+
399+
impl Display for ReturnError {
400+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
401+
f.write_str(&self.0)
402+
}
403+
}
404+
405+
impl Error for ReturnError {
406+
fn source(&self) -> Option<&(dyn Error + 'static)> {
407+
None
408+
}
409+
}
410+
411+
impl From<ReturnError> for std::io::Error {
412+
fn from(e: ReturnError) -> Self {
413+
std::io::Error::new(std::io::ErrorKind::Other, e.0)
414+
}
415+
}
416+
431417
#[cfg_attr(feature = "cargo-clippy", allow(clippy::mutex_atomic))]
432418
#[derive(Debug)]
433419
struct FailPoint {
@@ -843,6 +829,44 @@ macro_rules! fail_point {
843829
}};
844830
}
845831

832+
/// A variant of [`fail_point`] designed for a function that returns [`std::result::Result`].
833+
///
834+
/// 1. A failpoint that supports e.g. `FAILPOINTS=a-fail-point=return` to return a synthetic error
835+
///
836+
/// ```rust
837+
/// # #[macro_use] extern crate fail;
838+
/// fn fallible_function() -> Result<u32, Box<dyn std::error::Error>> {
839+
/// try_fail_point!("a-fail-point");
840+
/// Ok(42)
841+
/// }
842+
/// ```
843+
///
844+
/// Like [`fail_point`], this also has a form which accepts a runtime condition:
845+
///
846+
/// 2. A fail point with conditional execution:
847+
///
848+
/// ```rust
849+
/// # #[macro_use] extern crate fail;
850+
/// fn function_conditional(enable: bool) -> Result<u32, Box<dyn std::error::Error>> {
851+
/// try_fail_point!("fail-point-3", enable);
852+
/// Ok(42)
853+
/// }
854+
/// ```
855+
#[macro_export]
856+
#[cfg(feature = "failpoints")]
857+
macro_rules! try_fail_point {
858+
($name:expr) => {{
859+
if let Some(e) = $crate::eval($name, |msg| $crate::ReturnError::from(msg)) {
860+
return Err(From::from(e));
861+
}
862+
}};
863+
($name:expr, $cond:expr) => {{
864+
if $cond {
865+
$crate::try_fail_point!($name);
866+
}
867+
}};
868+
}
869+
846870
/// Define a fail point (disabled, see `failpoints` feature).
847871
#[macro_export]
848872
#[cfg(not(feature = "failpoints"))]
@@ -852,6 +876,14 @@ macro_rules! fail_point {
852876
($name:expr, $cond:expr, $e:expr) => {{}};
853877
}
854878

879+
/// Define a fail point for a Result-returning function (disabled, see `failpoints` feature).
880+
#[macro_export]
881+
#[cfg(not(feature = "failpoints"))]
882+
macro_rules! try_fail_point {
883+
($name:expr) => {{}};
884+
($name:expr, $cond:expr) => {{}};
885+
}
886+
855887
#[cfg(test)]
856888
mod tests {
857889
use super::*;
@@ -1032,6 +1064,27 @@ mod tests {
10321064
}
10331065
}
10341066

1067+
#[test]
1068+
#[cfg(feature = "failpoints")]
1069+
fn test_try_failpoint() -> anyhow::Result<()> {
1070+
fn test_anyhow() -> anyhow::Result<()> {
1071+
try_fail_point!("tryfail-with-result");
1072+
Ok(())
1073+
}
1074+
fn test_stderr() -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
1075+
try_fail_point!("tryfail-with-result-2");
1076+
Ok(())
1077+
}
1078+
fn test_stdioerr() -> std::io::Result<()> {
1079+
try_fail_point!("tryfail-with-result-3");
1080+
Ok(())
1081+
}
1082+
test_anyhow()?;
1083+
test_stderr().map_err(anyhow::Error::msg)?;
1084+
test_stdioerr()?;
1085+
Ok(())
1086+
}
1087+
10351088
// This case should be tested as integration case, but when calling `teardown` other cases
10361089
// like `test_pause` maybe also affected, so it's better keep it here.
10371090
#[test]

0 commit comments

Comments
 (0)