faine/
lib.rs

1// SPDX-FileCopyrightText: Copyright 2025 Dmitry Marakasov <amdmi3@amdmi3.ru>
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! `faine` stands for _FAultpoint INjection, Exhaustible/Exploring_ and is an
5//! implementation of testing technique known as
6//! [_fail points_](https://man.freebsd.org/cgi/man.cgi?query=fail),
7//! [_fault injection_](https://en.wikipedia.org/wiki/Fault_injection),
8//! or [_chaos engineering_](https://en.wikipedia.org/wiki/Chaos_engineering),
9//! which allows testing otherwise hard or impossible to reproduce conditions
10//! such as I/O errors.
11//!
12//! # How this works
13//!
14//! - You instrument the source code, adding (fail)points where normal code flow
15//!   can be overridden externally, to, for instance, return a specific error instead
16//!   of calling an I/O function (note that for surrounding code, triggering such
17//!   failpoint would be the same as named I/O function returning an error).
18//! - You trigger these failpoints in the tests, effectively simulating otherwise
19//!   hard to reproduce failures, and check that your code behaves correctly under
20//!   these conditions.
21//!
22//! On top of supporting that, `faine` implements automated execution path exploration,
23//! running a tested code multiple times with different combinations of failpoints enabled
24//! and disabled (NB: in much more effective way than trying all N² possible combinations).
25//! This allows simpler tests (which do not know inner workings of the code, that is to
26//! know which failpoints to trigger and which effects to expect), with much higher coverage
27//! (as all possible code paths are tested).
28//!
29//! # Example
30//!
31//! Let's test a code which is supposed to atomically replace a file with given content.
32//!
33//! Instrument the code by adding failpoint macros before (or around) each operation
34//! you want to simulate failures of:
35//!
36//! ```
37//! # use std::path::Path;
38//! # use std::fs::File;
39//! # use std::io::{self, Write};
40//! use faine::inject_return_io_error;
41//!
42//! fn atomic_replace_file(path: &Path, content: &str) -> io::Result<()> {
43//!     inject_return_io_error!("create file"); // <- added failpoint
44//!     let mut file = File::create(path)?;
45//!     inject_return_io_error!("write file");  // <- added failpoint
46//!     file.write_all(content.as_bytes())?;
47//!     Ok(())
48//! }
49//! ```
50//!
51//! Now write a test, utilizing [`faine::Runner`](crate::Runner):
52//!
53//! ```should_panic
54//! # use std::path::Path;
55//! # use std::fs::{File, read_to_string};
56//! # use std::io::{self, Write};
57//! # use faine::inject_return_io_error;
58//! # fn atomic_replace_file(path: &Path, content: &str) -> io::Result<()> {
59//! #     inject_return_io_error!("create file"); // <- added failpoint
60//! #     let mut file = File::create(path)?;
61//! #     inject_return_io_error!("write file");  // <- added failpoint
62//! #     file.write_all(content.as_bytes())?;
63//! #     Ok(())
64//! # }
65//! use faine::Runner;
66//!
67//! #[test]
68//! # fn dummy() {}
69//! fn test_replace_file_is_atomic() {
70//!     Runner::default().run(|_| {
71//!         // prepare filesystem state for testing
72//!         let tempdir = tempfile::tempdir().unwrap();
73//!         let path = tempdir.path().join("myfile");
74//!         File::create(&path).unwrap().write_all(b"old").unwrap();
75//!         // run the tested code
76//!         let res = atomic_replace_file(&path, "new");
77//!         // check resulting filesystem state
78//!         let contents = read_to_string(path).unwrap();
79//!         assert!(
80//!            res.is_ok() && contents == "new" ||
81//!            res.is_err() && contents == "old"
82//!         ); // fires!
83//!     }).unwrap();
84//! }
85//! # test_replace_file_is_atomic();
86//! ```
87//!
88//! See `examples/atomic_replace_file.rs` for complete code for this example.
89//!
90//! # Quick reference
91//!
92//! ## Specifying failpoints which inject early return
93//!
94//! The complete macro signature allows to specify failpoint name and returned value:
95//!
96//! ```
97//! # use std::io;
98//! # use faine::inject_return;
99//! inject_return!("failpoint name", Err(io::Error::other("injected error")));
100//! # Ok::<(), io::Error>(())
101//! ```
102//!
103//! You can omit failpoint name (in which case it's generated from source file
104//! path, line and position), and, as testing I/O related code is quite common
105//! case, there are shortcuts which return `Err(io::Error::other()))` right away:
106//!
107//! ```
108//! # use std::io;
109//! # use faine::{inject_return, inject_return_io_error};
110//! inject_return!(Err(io::Error::other("injected error")));  // name autogenerated
111//! inject_return_io_error!("failpoint name");                // return io::Error
112//! inject_return_io_error!();
113//! # Ok::<(), io::Error>(())
114//! ```
115//!
116//! ## Specifying failpoints which wrap expressions
117//!
118//! There is a set of macros with the same variations which, instead of returning
119//! early, wrap an expression and replace it with something else when failpoint
120//! is activated:
121//!
122//! ```no_run
123//! # use std::io;
124//! # use std::fs::File;
125//! # use faine::{inject_override, inject_override_io_error};
126//! let f = inject_override!(File::open("foo"), "failpoint name", Err(io::Error::other("injected error")));
127//! let f = inject_override!(File::open("foo"), Err(io::Error::other("injected error")));
128//! let f = inject_override_io_error!(File::open("foo"), "failpoint name");
129//! let f = inject_override_io_error!(File::open("foo"));
130//! # Ok::<(), io::Error>(())
131//! ```
132//!
133//! These are particularly useful if you branch based on I/O operation result:
134//!
135//! ```
136//! # use std::path::Path;
137//! # use std::fs::File;
138//! # use std::io;
139//! # use faine::{inject_override, inject_override_io_error};
140//! fn open_with_fallback() -> io::Result<File> {
141//!     if let Ok(file) = inject_override_io_error!(File::open("main.dat")) {
142//!         Ok(file)
143//!     } else {
144//!         inject_override_io_error!(File::open("backup.dat"))
145//!     }
146//! }
147//! ```
148//!
149//! There's also a similar set of macros `inject_override_with_side_effect*`
150//! which do the same, but still call an expression if a failpoint is activated,
151//! allowing its possible side effect to happen.
152//!
153//! ## Executing the instrumented code
154//!
155//! In the test, construct a default [`Runner`](crate::Runner) and call its
156//! [`run()`](crate::Runner::run) method with a code to test (with optional
157//! preparation code an asserts, just like a normal test):
158//!
159//! ```
160//! # use faine::Runner;
161//! # fn tested_code() -> bool { true }
162//! #[test]
163//! # fn dummy() {}
164//! fn test_foobar() {
165//!     Runner::default().run(|_| {
166//!         // ...preparation...
167//!         let res = tested_code();
168//!         assert!(res);
169//!     }).unwrap();
170//! }
171//! ```
172//!
173//! ## Controlling execution
174//!
175//! [`Runner`](crate::Runner) has methods to tune its behavior:
176//!
177//! - [`with_branch_preference()`](crate::Runner::with_branch_preference)
178//!
179//! ## Enabling failpoints
180//!
181//! You can toggle failpoints processing with [`enable_failpoints`](crate::enable_failpoints)
182//! macro. This is particularly useful to test how subsequent runs of the code
183//! recover from any previous errors:
184//!
185//! ```
186//! # use std::io;
187//! # use faine::inject_return_io_error;
188//! use faine::enable_failpoints;
189//! # fn tested_code() {}
190//! tested_code(); // this fails in all possible ways
191//! tested_code(); // this also fails, and sees the previous errors
192//! enable_failpoints!(false);
193//! tested_code(); // this recovers
194//! # Ok::<(), io::Error>(())
195//! ```
196//!
197//! ## Introspection
198//!
199//! You may inspect which failpoints the code execution has passed through,
200//! and which of these were activated. It is possible from both the executed
201//! code (to examine current execution trace) and after [`Runner::run`] completion
202//! (to examine traces for all performed executions with different failpoint paths).
203//!
204//! ```
205//! # use faine::{Runner, Branch};
206//! # fn tested_code() -> bool { true }
207//! #[test]
208//! # fn dummy() {}
209//! fn test_foobar() {
210//!     let report = Runner::default().run(|handle| {
211//!         let res = tested_code();
212//!         eprintln!("current trace: {:?}", handle.trace());
213//!         // assert logic may take failpoint status in the current run into account
214//!         if handle.trace().failpoint_status_first("commit transaction") == Some(Branch::Skip) {
215//!             assert!(res);
216//!         }
217//!     }).unwrap();
218//!     // prints the same set of traces
219//!     eprintln!("all traces: {:#?}", report.traces);
220//! }
221//! ```
222//!
223//! By default, traces are printed in a fancy way to make them most readable. This may
224//! be disabled through `fancy-traces` feature. An example [`Report::traces`] dump for
225//! a linear code with three failpoints:
226//!
227//! ```text
228//! [
229//!     (💥create temp file),
230//!     (create temp file)→(💥write temp file),
231//!     (create temp file)→(write temp file)→(💥replace file),
232//!     (create temp file)→(write temp file)→(replace file),
233//! ]
234//! ```
235//!
236//! Run `cargo test --example atomic_replace_file -- --no-capture` to reproduce.
237//!
238//! # Other implementations of the same concept
239//!
240//! Neither supports path exploration as far as I know.
241//!
242//! - [chaos-rs](https://crates.io/crates/chaos-rs)
243//! - [fail](https://crates.io/crates/fail)
244//! - [fail-parallel](https://crates.io/crates/fail-parallel)
245//! - [failpoints](https://crates.io/crates/failpoints)
246//! - [fault-injection](https://crates.io/crates/fault-injection)
247
248mod collections;
249mod common;
250mod error;
251mod introspection;
252mod macros;
253mod options;
254mod runner;
255mod tree;
256
257#[doc(hidden)]
258pub mod __private;
259
260pub use common::Branch;
261pub use error::Error;
262pub use introspection::{Report, Step, Trace};
263pub use runner::Runner;