faine 0.2.1

Failpoints implementation with automatic path exploration
Documentation
// SPDX-FileCopyrightText: Copyright 2025 Dmitry Marakasov <amdmi3@amdmi3.ru>
// SPDX-License-Identifier: Apache-2.0 OR MIT

//! `faine` stands for _FAultpoint INjection, Exhaustible/Exploring_ and is an
//! implementation of testing technique known as
//! [_fail points_](https://man.freebsd.org/cgi/man.cgi?query=fail),
//! [_fault injection_](https://en.wikipedia.org/wiki/Fault_injection),
//! or [_chaos engineering_](https://en.wikipedia.org/wiki/Chaos_engineering),
//! which allows testing otherwise hard or impossible to reproduce conditions
//! such as I/O errors.
//!
//! # How this works
//!
//! - You instrument the source code, adding (fail)points where normal code flow
//!   can be overridden externally, to, for instance, return a specific error instead
//!   of calling an I/O function (note that for surrounding code, triggering such
//!   failpoint would be the same as named I/O function returning an error).
//! - You trigger these failpoints in the tests, effectively simulating otherwise
//!   hard to reproduce failures, and check that your code behaves correctly under
//!   these conditions.
//!
//! On top of supporting that, `faine` implements automated execution path exploration,
//! running a tested code multiple times with different combinations of failpoints enabled
//! and disabled (NB: in much more effective way than trying all N² possible combinations).
//! This allows simpler tests (which do not know inner workings of the code, that is to
//! know which failpoints to trigger and which effects to expect), with much higher coverage
//! (as all possible code paths are tested).
//!
//! # Example
//!
//! Let's test a code which is supposed to atomically replace a file with given content.
//!
//! Instrument the code by adding failpoint macros before (or around) each operation
//! you want to simulate failures of:
//!
//! ```
//! # use std::path::Path;
//! # use std::fs::File;
//! # use std::io::{self, Write};
//! use faine::inject_return_io_error;
//!
//! fn atomic_replace_file(path: &Path, content: &str) -> io::Result<()> {
//!     inject_return_io_error!("create file"); // <- added failpoint
//!     let mut file = File::create(path)?;
//!     inject_return_io_error!("write file");  // <- added failpoint
//!     file.write_all(content.as_bytes())?;
//!     Ok(())
//! }
//! ```
//!
//! Now write a test, utilizing [`faine::Runner`](crate::Runner):
//!
//! ```should_panic
//! # use std::path::Path;
//! # use std::fs::{File, read_to_string};
//! # use std::io::{self, Write};
//! # use faine::inject_return_io_error;
//! # fn atomic_replace_file(path: &Path, content: &str) -> io::Result<()> {
//! #     inject_return_io_error!("create file"); // <- added failpoint
//! #     let mut file = File::create(path)?;
//! #     inject_return_io_error!("write file");  // <- added failpoint
//! #     file.write_all(content.as_bytes())?;
//! #     Ok(())
//! # }
//! use faine::Runner;
//!
//! #[test]
//! # fn dummy() {}
//! fn test_replace_file_is_atomic() {
//!     Runner::default().run(|_| {
//!         // prepare filesystem state for testing
//!         let tempdir = tempfile::tempdir().unwrap();
//!         let path = tempdir.path().join("myfile");
//!         File::create(&path).unwrap().write_all(b"old").unwrap();
//!         // run the tested code
//!         let res = atomic_replace_file(&path, "new");
//!         // check resulting filesystem state
//!         let contents = read_to_string(path).unwrap();
//!         assert!(
//!            res.is_ok() && contents == "new" ||
//!            res.is_err() && contents == "old"
//!         ); // fires!
//!     }).unwrap();
//! }
//! # test_replace_file_is_atomic();
//! ```
//!
//! See `examples/atomic_replace_file.rs` for complete code for this example.
//!
//! # Quick reference
//!
//! ## Specifying failpoints which inject early return
//!
//! The complete macro signature allows to specify failpoint name and returned value:
//!
//! ```
//! # use std::io;
//! # use faine::inject_return;
//! inject_return!("failpoint name", Err(io::Error::other("injected error")));
//! # Ok::<(), io::Error>(())
//! ```
//!
//! You can omit failpoint name (in which case it's generated from source file
//! path, line and position), and, as testing I/O related code is quite common
//! case, there are shortcuts which return `Err(io::Error::other()))` right away:
//!
//! ```
//! # use std::io;
//! # use faine::{inject_return, inject_return_io_error};
//! inject_return!(Err(io::Error::other("injected error")));  // name autogenerated
//! inject_return_io_error!("failpoint name");                // return io::Error
//! inject_return_io_error!();
//! # Ok::<(), io::Error>(())
//! ```
//!
//! ## Specifying failpoints which wrap expressions
//!
//! There is a set of macros with the same variations which, instead of returning
//! early, wrap an expression and replace it with something else when failpoint
//! is activated:
//!
//! ```no_run
//! # use std::io;
//! # use std::fs::File;
//! # use faine::{inject_override, inject_override_io_error};
//! let f = inject_override!(File::open("foo"), "failpoint name", Err(io::Error::other("injected error")));
//! let f = inject_override!(File::open("foo"), Err(io::Error::other("injected error")));
//! let f = inject_override_io_error!(File::open("foo"), "failpoint name");
//! let f = inject_override_io_error!(File::open("foo"));
//! # Ok::<(), io::Error>(())
//! ```
//!
//! These are particularly useful if you branch based on I/O operation result:
//!
//! ```
//! # use std::path::Path;
//! # use std::fs::File;
//! # use std::io;
//! # use faine::{inject_override, inject_override_io_error};
//! fn open_with_fallback() -> io::Result<File> {
//!     if let Ok(file) = inject_override_io_error!(File::open("main.dat")) {
//!         Ok(file)
//!     } else {
//!         inject_override_io_error!(File::open("backup.dat"))
//!     }
//! }
//! ```
//!
//! There's also a similar set of macros `inject_override_with_side_effect*`
//! which do the same, but still call an expression if a failpoint is activated,
//! allowing its possible side effect to happen.
//!
//! ## Executing the instrumented code
//!
//! In the test, construct a default [`Runner`](crate::Runner) and call its
//! [`run()`](crate::Runner::run) method with a code to test (with optional
//! preparation code an asserts, just like a normal test):
//!
//! ```
//! # use faine::Runner;
//! # fn tested_code() -> bool { true }
//! #[test]
//! # fn dummy() {}
//! fn test_foobar() {
//!     Runner::default().run(|_| {
//!         // ...preparation...
//!         let res = tested_code();
//!         assert!(res);
//!     }).unwrap();
//! }
//! ```
//!
//! ## Controlling execution
//!
//! [`Runner`](crate::Runner) has methods to tune its behavior:
//!
//! - [`with_branch_preference()`](crate::Runner::with_branch_preference)
//!
//! ## Enabling failpoints
//!
//! You can toggle failpoints processing with [`enable_failpoints`](crate::enable_failpoints)
//! macro. This is particularly useful to test how subsequent runs of the code
//! recover from any previous errors:
//!
//! ```
//! # use std::io;
//! # use faine::inject_return_io_error;
//! use faine::enable_failpoints;
//! # fn tested_code() {}
//! tested_code(); // this fails in all possible ways
//! tested_code(); // this also fails, and sees the previous errors
//! enable_failpoints!(false);
//! tested_code(); // this recovers
//! # Ok::<(), io::Error>(())
//! ```
//!
//! ## Introspection
//!
//! You may inspect which failpoints the code execution has passed through,
//! and which of these were activated. It is possible from both the executed
//! code (to examine current execution trace) and after [`Runner::run`] completion
//! (to examine traces for all performed executions with different failpoint paths).
//!
//! ```
//! # use faine::{Runner, Branch};
//! # fn tested_code() -> bool { true }
//! #[test]
//! # fn dummy() {}
//! fn test_foobar() {
//!     let report = Runner::default().run(|handle| {
//!         let res = tested_code();
//!         eprintln!("current trace: {:?}", handle.trace());
//!         // assert logic may take failpoint status in the current run into account
//!         if handle.trace().failpoint_status_first("commit transaction") == Some(Branch::Skip) {
//!             assert!(res);
//!         }
//!     }).unwrap();
//!     // prints the same set of traces
//!     eprintln!("all traces: {:#?}", report.traces);
//! }
//! ```
//!
//! By default, traces are printed in a fancy way to make them most readable. This may
//! be disabled through `fancy-traces` feature. An example [`Report::traces`] dump for
//! a linear code with three failpoints:
//!
//! ```text
//! [
//!     (💥create temp file),
//!     (create temp file)→(💥write temp file),
//!     (create temp file)→(write temp file)→(💥replace file),
//!     (create temp file)→(write temp file)→(replace file),
//! ]
//! ```
//!
//! Run `cargo test --example atomic_replace_file -- --no-capture` to reproduce.
//!
//! # Other implementations of the same concept
//!
//! Neither supports path exploration as far as I know.
//!
//! - [chaos-rs](https://crates.io/crates/chaos-rs)
//! - [fail](https://crates.io/crates/fail)
//! - [fail-parallel](https://crates.io/crates/fail-parallel)
//! - [failpoints](https://crates.io/crates/failpoints)
//! - [fault-injection](https://crates.io/crates/fault-injection)

mod collections;
mod common;
mod error;
mod introspection;
mod macros;
mod options;
mod runner;
mod tree;

#[doc(hidden)]
pub mod __private;

pub use common::Branch;
pub use error::Error;
pub use introspection::{Report, Step, Trace};
pub use runner::Runner;