#[cfg(feature = "injection-points")]
extern crate std;
#[cfg(not(feature = "injection-points"))]
#[macro_export]
macro_rules! injection_point {
($name:literal $(, $payload:expr)* $(,)?) => {{
$( let _ = &$payload; )*
let _ = $name;
}};
}
#[cfg(feature = "injection-points")]
#[macro_export]
macro_rules! injection_point {
($name:literal $(, $payload:expr)* $(,)?) => {{
let __spg_inj_payload = ($(&$payload as &dyn ::core::fmt::Debug,)*);
$crate::testkit::injection::__trigger($name, &__spg_inj_payload);
}};
}
pub const REGISTERED_POINTS: &[&str] = &[
"aggregate_spill_trigger",
"planner_first_row_fetch",
"tx_commit_walgroup_leader_switch",
"wal_group_commit_leader_chosen",
"index_build_post_seal",
];
#[cfg(not(feature = "injection-points"))]
mod off {
use core::marker::PhantomData;
#[derive(Debug, Default, Clone)]
pub struct InjectionStore;
#[must_use]
#[derive(Debug)]
pub struct InjectionGuard {
_priv: PhantomData<()>,
}
impl InjectionGuard {
pub(crate) const fn noop() -> Self {
Self { _priv: PhantomData }
}
}
impl Drop for InjectionGuard {
fn drop(&mut self) {
}
}
}
#[cfg(not(feature = "injection-points"))]
pub use off::{InjectionGuard, InjectionStore};
#[cfg(not(feature = "injection-points"))]
pub fn new_guard() -> InjectionGuard {
InjectionGuard::noop()
}
#[cfg(not(feature = "injection-points"))]
pub fn enter_scope(_store: &InjectionStore) -> InjectionGuard {
InjectionGuard::noop()
}
#[cfg(feature = "injection-points")]
mod active {
extern crate std;
use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::cell::RefCell;
use core::fmt;
use std::sync::{Condvar, Mutex};
#[derive(Clone)]
pub enum Action {
Wait(Arc<(Mutex<()>, Condvar)>),
Error(String),
Notice(String),
}
impl fmt::Debug for Action {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Action::Wait(_) => f.write_str("Wait(<cv>)"),
Action::Error(s) => write!(f, "Error({s:?})"),
Action::Notice(s) => write!(f, "Notice({s:?})"),
}
}
}
#[derive(Default)]
pub struct InjectionStore {
inner: Mutex<Inner>,
}
impl fmt::Debug for InjectionStore {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("InjectionStore")
}
}
#[derive(Default)]
struct Inner {
actions: BTreeMap<String, Action>,
notice_count: BTreeMap<String, u64>,
notice_message: BTreeMap<String, String>,
}
impl InjectionStore {
pub fn attach(&self, name: impl Into<String>, action: Action) {
self.inner
.lock()
.expect("InjectionStore poisoned")
.actions
.insert(name.into(), action);
}
pub fn detach(&self, name: &str) {
self.inner
.lock()
.expect("InjectionStore poisoned")
.actions
.remove(name);
}
pub fn get(&self, name: &str) -> Option<Action> {
self.inner
.lock()
.expect("InjectionStore poisoned")
.actions
.get(name)
.cloned()
}
pub fn wakeup(&self, name: &str) {
let cv = {
let guard = self.inner.lock().expect("InjectionStore poisoned");
match guard.actions.get(name) {
Some(Action::Wait(cv)) => cv.clone(),
_ => return,
}
};
cv.1.notify_all();
}
pub fn record_notice(&self, name: &str, msg: &str) {
let mut g = self.inner.lock().expect("InjectionStore poisoned");
*g.notice_count.entry(name.to_string()).or_insert(0) += 1;
g.notice_message.insert(name.to_string(), msg.to_string());
}
pub fn notice_count(&self, name: &str) -> u64 {
self.inner
.lock()
.expect("InjectionStore poisoned")
.notice_count
.get(name)
.copied()
.unwrap_or(0)
}
pub fn notice_message(&self, name: &str) -> Option<String> {
self.inner
.lock()
.expect("InjectionStore poisoned")
.notice_message
.get(name)
.cloned()
}
}
std::thread_local! {
static CURRENT: RefCell<Vec<Arc<InjectionStore>>> = const { RefCell::new(Vec::new()) };
}
#[must_use]
#[derive(Debug)]
pub struct InjectionGuard {
_priv: (),
}
impl Drop for InjectionGuard {
fn drop(&mut self) {
CURRENT.with(|c| {
let mut b = c.borrow_mut();
b.pop();
});
}
}
pub fn enter_scope(store: &Arc<InjectionStore>) -> InjectionGuard {
CURRENT.with(|c| c.borrow_mut().push(store.clone()));
InjectionGuard { _priv: () }
}
pub fn current() -> Option<Arc<InjectionStore>> {
CURRENT.with(|c| c.borrow().last().cloned())
}
#[inline]
pub fn __trigger(name: &'static str, payload: &dyn fmt::Debug) {
let Some(store) = current() else { return };
let Some(action) = store.get(name) else {
return;
};
match action {
Action::Wait(cv) => {
let g = cv.0.lock().expect("inject wait mutex poisoned");
let guard = cv.1.wait(g).expect("inject condvar poisoned");
drop(guard);
}
Action::Error(msg) => {
std::panic::panic_any(InjectedError {
name,
msg,
payload: alloc::format!("{payload:?}"),
});
}
Action::Notice(msg) => {
store.record_notice(name, &msg);
}
}
}
#[derive(Debug)]
pub struct InjectedError {
pub name: &'static str,
pub msg: String,
pub payload: String,
}
impl fmt::Display for InjectedError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"INJECTED ERROR at {}: {} (payload={})",
self.name, self.msg, self.payload
)
}
}
pub fn parse_action(s: &str) -> Result<Action, String> {
let lower = s.trim().to_ascii_lowercase();
if lower == "wait" {
return Ok(Action::Wait(Arc::new((Mutex::new(()), Condvar::new()))));
}
if let Some(rest) = lower.strip_prefix("error") {
let msg = rest.strip_prefix(':').unwrap_or("").trim();
return Ok(Action::Error(if msg.is_empty() {
"injected".into()
} else {
msg.to_string()
}));
}
if let Some(rest) = lower.strip_prefix("notice") {
let msg = rest.strip_prefix(':').unwrap_or("").trim();
return Ok(Action::Notice(if msg.is_empty() {
"notice".into()
} else {
msg.to_string()
}));
}
Err(alloc::format!(
"unknown injection action {s:?}; expected wait | error[:msg] | notice[:msg]"
))
}
}
#[cfg(feature = "injection-points")]
pub use active::{
__trigger, Action, InjectedError, InjectionGuard, InjectionStore, current, enter_scope,
parse_action,
};
#[cfg(all(test, feature = "injection-points"))]
mod tests {
use super::*;
use alloc::sync::Arc;
use std::thread;
use std::time::Duration;
#[test]
fn attach_wait_then_wakeup_releases_waiter() {
let store = Arc::new(InjectionStore::default());
let _g = enter_scope(&store);
store.attach("test_wait", parse_action("wait").unwrap());
let store2 = store.clone();
let h = thread::spawn(move || {
let _g = enter_scope(&store2);
crate::injection_point!("test_wait", &42usize);
"done"
});
thread::sleep(Duration::from_millis(50));
assert!(!h.is_finished(), "worker did not park on injection wait");
store.wakeup("test_wait");
let res = h.join().expect("worker thread panicked");
assert_eq!(res, "done");
}
#[test]
fn notice_increments_counter_without_blocking() {
let store = Arc::new(InjectionStore::default());
let _g = enter_scope(&store);
store.attach("test_notice", parse_action("notice:tag").unwrap());
for _ in 0..3 {
crate::injection_point!("test_notice", &());
}
assert_eq!(store.notice_count("test_notice"), 3);
assert_eq!(store.notice_message("test_notice").as_deref(), Some("tag"));
}
#[test]
fn no_scope_is_silent() {
crate::injection_point!("nobody_attached", &"payload");
}
#[test]
fn detach_removes_action() {
let store = Arc::new(InjectionStore::default());
let _g = enter_scope(&store);
store.attach("test_detach", parse_action("notice").unwrap());
crate::injection_point!("test_detach", &1u8);
assert_eq!(store.notice_count("test_detach"), 1);
store.detach("test_detach");
crate::injection_point!("test_detach", &2u8);
assert_eq!(
store.notice_count("test_detach"),
1,
"detached point should stop counting"
);
}
#[test]
fn registered_points_catalog_nonempty() {
assert!(!REGISTERED_POINTS.is_empty());
for &p in REGISTERED_POINTS {
assert!(!p.is_empty());
assert!(
!p.chars().any(char::is_whitespace),
"point name has whitespace: {p}"
);
}
}
}
#[cfg(all(test, not(feature = "injection-points")))]
mod off_tests {
#[test]
fn macro_off_compiles_and_runs() {
let payload = 42usize;
crate::injection_point!("smoke", &payload);
crate::injection_point!("smoke_no_payload");
}
}