1use std::collections::HashMap;
7use std::sync::{LazyLock, Mutex};
8use std::time::Duration;
9
10#[derive(Clone, Debug)]
12pub enum FailpointAction {
13 Off,
15 Error(String),
17 Panic(String),
19 Abort,
21 Sleep(Duration),
23}
24
25static REGISTRY: LazyLock<Mutex<HashMap<&'static str, FailpointAction>>> =
27 LazyLock::new(|| Mutex::new(HashMap::new()));
28
29pub fn set(name: &'static str, action: FailpointAction) {
35 REGISTRY.lock().unwrap().insert(name, action);
36}
37
38pub fn clear(name: &'static str) {
44 REGISTRY.lock().unwrap().remove(name);
45}
46
47pub fn clear_all() {
53 REGISTRY.lock().unwrap().clear();
54}
55
56pub fn check(name: &str) -> Result<(), String> {
64 let registry = REGISTRY.lock().unwrap();
65 match registry.get(name) {
66 None | Some(FailpointAction::Off) => Ok(()),
67 Some(FailpointAction::Error(msg)) => Err(msg.clone()),
68 Some(FailpointAction::Panic(msg)) => panic!("failpoint {name}: {msg}"),
69 Some(FailpointAction::Abort) => std::process::abort(),
70 Some(FailpointAction::Sleep(d)) => {
71 let d = *d;
72 drop(registry); std::thread::sleep(d);
74 Ok(())
75 }
76 }
77}
78
79#[cfg(feature = "failpoints")]
86#[macro_export]
87macro_rules! fp {
88 ($name:expr) => {
89 $crate::failpoints::check($name)
90 .map_err(|msg| anyhow::anyhow!("failpoint {}: {}", $name, msg))
91 };
92}
93
94#[cfg(not(feature = "failpoints"))]
95#[macro_export]
96macro_rules! fp {
97 ($name:expr) => {
98 Ok::<(), anyhow::Error>(())
99 };
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 #[test]
108 fn fp_noop_when_not_set() {
109 clear_all();
110 let result = fp!("FP_TEST_NOOP");
111 assert!(result.is_ok());
112 }
113
114 #[test]
116 #[cfg(feature = "failpoints")]
117 fn fp_returns_error_when_set() {
118 clear_all();
119 set("FP_TEST_ERROR", FailpointAction::Error("injected".into()));
120 let result = fp!("FP_TEST_ERROR");
121 assert!(result.is_err());
122 let err = result.unwrap_err();
123 assert!(
124 err.to_string().contains("injected"),
125 "expected 'injected' in error: {err}"
126 );
127 clear("FP_TEST_ERROR");
128 }
129
130 #[test]
132 fn clear_all_resets() {
133 set("FP_A", FailpointAction::Error("a".into()));
134 set("FP_B", FailpointAction::Error("b".into()));
135 clear_all();
136 assert!(fp!("FP_A").is_ok());
137 assert!(fp!("FP_B").is_ok());
138 }
139
140 #[test]
144 fn fp_compiles_to_result() {
145 clear_all();
146 let result: Result<(), anyhow::Error> = fp!("FP_COMPILE_CHECK");
147 assert!(result.is_ok());
148 }
149
150 #[test]
152 #[cfg(feature = "failpoints")]
153 fn fp_off_action_is_noop() {
154 clear_all();
155 set("FP_OFF", FailpointAction::Off);
156 assert!(fp!("FP_OFF").is_ok());
157 clear("FP_OFF");
158 }
159
160 #[test]
162 #[cfg(feature = "failpoints")]
163 fn fp_sleep_returns_ok() {
164 clear_all();
165 set(
166 "FP_SLEEP",
167 FailpointAction::Sleep(Duration::from_millis(1)),
168 );
169 assert!(fp!("FP_SLEEP").is_ok());
170 clear("FP_SLEEP");
171 }
172
173 #[test]
175 #[cfg(feature = "failpoints")]
176 fn clear_single_failpoint() {
177 clear_all();
178 set("FP_KEEP", FailpointAction::Error("keep".into()));
179 set("FP_REMOVE", FailpointAction::Error("remove".into()));
180 clear("FP_REMOVE");
181 assert!(fp!("FP_REMOVE").is_ok());
182 assert!(fp!("FP_KEEP").is_err());
183 clear_all();
184 }
185}