use parking_lot::Mutex;
use crate::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MergeConstructionFault {
Io,
Corruption,
UnsupportedFormat,
}
pub const INJECTED_CONSTRUCTION_MESSAGE: &str =
"cqlite test fault injection (issue #3154): merge construction failure";
impl MergeConstructionFault {
fn into_error(self) -> Error {
match self {
Self::Io => Error::Io(std::io::Error::other(format!(
"{INJECTED_CONSTRUCTION_MESSAGE} (io)"
))),
Self::Corruption => {
Error::corruption(format!("{INJECTED_CONSTRUCTION_MESSAGE} (corrupt input)"))
}
Self::UnsupportedFormat => Error::unsupported_format(format!(
"{INJECTED_CONSTRUCTION_MESSAGE} (merger-ineligible input)"
)),
}
}
}
struct ConstructionArm {
id: u64,
scope: String,
fault: MergeConstructionFault,
}
static CONSTRUCTION_ARMS: Mutex<Vec<ConstructionArm>> = Mutex::new(Vec::new());
#[must_use = "the fault stays armed only while the guard is alive"]
pub fn arm_merge_construction_error(
scope: &str,
fault: MergeConstructionFault,
) -> ArmedMergeConstructionError {
super::armed::check_scope(scope);
let id = super::armed::next_id();
CONSTRUCTION_ARMS.lock().push(ConstructionArm {
id,
scope: scope.to_string(),
fault,
});
ArmedMergeConstructionError { id }
}
#[derive(Debug)]
pub struct ArmedMergeConstructionError {
id: u64,
}
impl Drop for ArmedMergeConstructionError {
fn drop(&mut self) {
CONSTRUCTION_ARMS.lock().retain(|arm| arm.id != self.id);
}
}
pub(super) fn take(path: &str) -> Option<Error> {
let fault = {
let mut arms = CONSTRUCTION_ARMS.lock();
let index = arms
.iter()
.position(|arm| path.contains(arm.scope.as_str()))?;
arms.remove(index).fault
};
Some(fault.into_error())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn each_fault_injects_its_own_error_variant_and_names_itself() {
for (fault, matched) in [
(
MergeConstructionFault::Io,
matches!(MergeConstructionFault::Io.into_error(), Error::Io(_)),
),
(
MergeConstructionFault::Corruption,
matches!(
MergeConstructionFault::Corruption.into_error(),
Error::Corruption(_)
),
),
(
MergeConstructionFault::UnsupportedFormat,
matches!(
MergeConstructionFault::UnsupportedFormat.into_error(),
Error::UnsupportedFormat(_)
),
),
] {
assert!(matched, "{fault:?} injected the wrong Error variant");
assert!(
fault
.into_error()
.to_string()
.contains(INJECTED_CONSTRUCTION_MESSAGE),
"{fault:?}: the injected error must name itself so a test can prove \
THIS fault ended the read"
);
}
}
#[test]
fn an_arm_is_scoped_and_taken_exactly_once() {
let scope = "issue-3154-scoped-take-probe";
let _armed = arm_merge_construction_error(scope, MergeConstructionFault::Io);
assert!(
take("/some/other/table/nb-1-big-Data.db").is_none(),
"a merge over an unrelated input must not consume this arm"
);
assert!(
take(&format!("/tmp/{scope}/data/nb-1-big-Data.db")).is_some(),
"the matching merge must take the arm"
);
assert!(
take(&format!("/tmp/{scope}/data/nb-2-big-Data.db")).is_none(),
"the arm must be taken exactly once, so a retry is not hit by it again"
);
}
#[test]
fn dropping_the_guard_disarms() {
let scope = "issue-3154-disarm-probe";
let path = format!("/tmp/{scope}/data/nb-1-big-Data.db");
drop(arm_merge_construction_error(
scope,
MergeConstructionFault::Corruption,
));
assert!(
take(&path).is_none(),
"a dropped guard must leave nothing armed"
);
}
}