pub use aristo_macros::{expose_pub, fault_point, yield_point, Inspect};
use std::cell::Cell;
thread_local! {
static HOOK: Cell<Option<fn(&'static str)>> = const { Cell::new(None) };
}
pub fn set_hook(hook: Option<fn(&'static str)>) {
HOOK.with(|h| h.set(hook));
}
#[doc(hidden)]
pub fn __yield_point(label: &'static str) {
if let Some(hook) = HOOK.with(|h| h.get()) {
hook(label);
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Decision {
Continue,
Inject(u64),
}
pub type FaultHook = Box<dyn FnMut(&'static str) -> Decision>;
thread_local! {
static FAULT_HOOK: Cell<Option<FaultHook>> = const { Cell::new(None) };
}
pub fn set_fault_hook(hook: Option<FaultHook>) {
FAULT_HOOK.with(|h| h.set(hook));
}
#[doc(hidden)]
pub fn __fault_point(label: &'static str) -> Decision {
let Some(mut hook) = FAULT_HOOK.with(|h| h.take()) else {
return Decision::Continue;
};
let decision = hook(label);
FAULT_HOOK.with(|h| match h.take() {
Some(reinstalled) => h.set(Some(reinstalled)),
None => h.set(Some(hook)),
});
decision
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::Cell;
thread_local! {
static OBSERVED: Cell<Option<&'static str>> = const { Cell::new(None) };
}
fn capture(label: &'static str) {
OBSERVED.with(|o| o.set(Some(label)));
}
fn ignore(_: &'static str) {}
#[test]
fn default_is_noop() {
set_hook(None);
__yield_point("noop.label");
}
#[test]
fn hook_receives_label() {
OBSERVED.with(|o| o.set(None));
set_hook(Some(capture));
__yield_point("captured.label");
assert_eq!(OBSERVED.with(|o| o.get()), Some("captured.label"));
set_hook(None);
}
#[test]
fn replacing_hook_invokes_only_the_latest() {
OBSERVED.with(|o| o.set(None));
set_hook(Some(capture));
set_hook(Some(ignore));
__yield_point("ignored.label");
assert_eq!(OBSERVED.with(|o| o.get()), None);
set_hook(None);
}
#[test]
fn clearing_hook_restores_noop() {
OBSERVED.with(|o| o.set(None));
set_hook(Some(capture));
set_hook(None);
__yield_point("after.clear");
assert_eq!(OBSERVED.with(|o| o.get()), None);
}
#[test]
fn fault_default_is_continue() {
set_fault_hook(None);
assert_eq!(__fault_point("no.hook"), Decision::Continue);
}
#[test]
fn fault_hook_fails_nth() {
let mut seen = 0u64;
set_fault_hook(Some(Box::new(move |_label| {
seen += 1;
if seen == 2 {
Decision::Inject(7)
} else {
Decision::Continue
}
})));
assert_eq!(__fault_point("op"), Decision::Continue);
assert_eq!(__fault_point("op"), Decision::Inject(7));
assert_eq!(__fault_point("op"), Decision::Continue);
set_fault_hook(None);
}
#[test]
fn replacing_fault_hook_invokes_only_the_latest() {
set_fault_hook(Some(Box::new(|_| Decision::Inject(1))));
set_fault_hook(Some(Box::new(|_| Decision::Continue)));
assert_eq!(__fault_point("op"), Decision::Continue);
set_fault_hook(None);
}
#[test]
fn clearing_fault_hook_restores_continue() {
set_fault_hook(Some(Box::new(|_| Decision::Inject(9))));
set_fault_hook(None);
assert_eq!(__fault_point("op"), Decision::Continue);
}
#[test]
fn reentrant_fault_point_is_continue_and_hook_restored() {
set_fault_hook(Some(Box::new(|_| {
assert_eq!(__fault_point("inner"), Decision::Continue);
Decision::Inject(3)
})));
assert_eq!(__fault_point("outer"), Decision::Inject(3));
assert_eq!(__fault_point("outer"), Decision::Inject(3));
set_fault_hook(None);
}
}