#[cfg(debug_assertions)]
#[derive(Debug, PartialEq, Eq)]
enum Which {
Nth(usize),
Every,
}
#[cfg(debug_assertions)]
fn parse_target(raw: &str) -> Option<(String, Which)> {
let (substring, which) = match raw.rsplit_once(':') {
Some((s, "*")) => (s.to_string(), Which::Every),
Some((s, n)) if n.parse::<usize>().is_ok() => {
(s.to_string(), Which::Nth(n.parse().unwrap()))
}
_ => (raw.to_string(), Which::Nth(1)),
};
if substring.is_empty() || which == Which::Nth(0) {
return None;
}
Some((substring, which))
}
#[cfg(debug_assertions)]
pub fn maybe_fail(path: &std::path::Path) -> std::io::Result<()> {
use std::sync::atomic::{AtomicUsize, Ordering};
static MATCHES: AtomicUsize = AtomicUsize::new(0);
static TARGET: std::sync::OnceLock<Option<(String, Which)>> = std::sync::OnceLock::new();
let target = TARGET.get_or_init(|| parse_target(&std::env::var("FRAME_FAIL_WRITE").ok()?));
let Some((substring, which)) = target else {
return Ok(());
};
if !path.to_string_lossy().contains(substring.as_str()) {
return Ok(());
}
let n = MATCHES.fetch_add(1, Ordering::SeqCst) + 1;
let fail = match which {
Which::Nth(nth) => n == *nth,
Which::Every => true,
};
if fail {
return Err(std::io::Error::other(format!(
"injected write failure: {} (match #{n} for {substring:?})",
path.display()
)));
}
Ok(())
}
#[cfg(all(test, debug_assertions))]
mod tests {
use super::*;
#[test]
fn a_bare_path_fails_the_first_match() {
assert_eq!(
parse_target("tracks/a.md"),
Some(("tracks/a.md".to_string(), Which::Nth(1)))
);
}
#[test]
fn a_trailing_number_selects_that_match() {
assert_eq!(
parse_target("tracks/a.md:3"),
Some(("tracks/a.md".to_string(), Which::Nth(3)))
);
}
#[test]
fn a_trailing_star_fails_every_match() {
assert_eq!(
parse_target("tracks/a.md:*"),
Some(("tracks/a.md".to_string(), Which::Every))
);
}
#[test]
fn a_colon_that_is_not_a_count_stays_part_of_the_path() {
assert_eq!(
parse_target("weird:name.md"),
Some(("weird:name.md".to_string(), Which::Nth(1)))
);
}
#[test]
fn an_empty_or_zeroth_target_disables_injection() {
assert_eq!(parse_target(""), None);
assert_eq!(parse_target("tracks/a.md:0"), None);
assert_eq!(parse_target(":*"), None);
}
}
#[cfg(not(debug_assertions))]
#[inline(always)]
pub fn maybe_fail(_path: &std::path::Path) -> std::io::Result<()> {
Ok(())
}