#![cfg(not(target_arch = "wasm32"))]
use std::{
env,
os::unix::process::ExitStatusExt,
process::{Command, Output},
thread,
};
use reifydb_runtime::fatal::{FatalConfig, install, report::ISSUE_URL};
const TRIGGER: &str = "REIFYDB_FATAL_TEST_TRIGGER";
const SIGABRT: i32 = 6;
fn triggered() -> Option<String> {
env::var(TRIGGER).ok()
}
fn run_child(test_name: &str, trigger: &str) -> Output {
Command::new(env::current_exe().expect("the test binary must be locatable to re-enter it"))
.args(["--exact", test_name, "--nocapture"])
.env(TRIGGER, trigger)
.env("RUST_BACKTRACE", "1")
.output()
.expect("the child test process must start")
}
fn stderr_of(output: &Output) -> String {
String::from_utf8_lossy(&output.stderr).to_string()
}
#[test]
fn an_armed_panic_aborts_the_process_and_prints_a_ticket_grade_report() {
if triggered().as_deref() == Some("panic") {
install(FatalConfig::armed(true));
panic!("deliberate fatal from the child");
}
let output = run_child("an_armed_panic_aborts_the_process_and_prints_a_ticket_grade_report", "panic");
let stderr = stderr_of(&output);
assert_eq!(
output.status.signal(),
Some(SIGABRT),
"an armed fatal must abort, not exit; got status {:?} with stderr:\n{}",
output.status,
stderr
);
assert!(stderr.contains("REIFYDB FATAL"), "the report must be greppable; stderr was:\n{}", stderr);
assert!(stderr.contains("deliberate fatal from the child"), "the panic message must survive into the report");
assert!(stderr.contains("kind: panic"));
assert!(stderr.contains(ISSUE_URL), "a report nobody can file is not a report");
assert!(stderr.contains("thread:"), "the report must name the thread the panic came from");
assert!(stderr.contains("version:"), "a ticket without a version cannot be triaged");
}
#[test]
fn the_report_carries_the_stack_from_the_panic_site_not_the_handler() {
if triggered().as_deref() == Some("panic") {
install(FatalConfig::armed(true));
fn the_frame_that_must_appear() -> ! {
panic!("deliberate fatal from a named frame");
}
the_frame_that_must_appear();
}
let output = run_child("the_report_carries_the_stack_from_the_panic_site_not_the_handler", "panic");
let stderr = stderr_of(&output);
assert_eq!(output.status.signal(), Some(SIGABRT), "stderr was:\n{}", stderr);
assert!(stderr.contains("backtrace:"), "stderr was:\n{}", stderr);
assert!(
stderr.contains("the_frame_that_must_appear"),
"the panicking frame must be in the stack, otherwise the backtrace is the handler's and useless; stderr was:\n{}",
stderr
);
}
#[test]
fn a_panic_on_a_background_thread_takes_the_whole_process_down() {
if triggered().as_deref() == Some("panic") {
install(FatalConfig::armed(true));
let worker = thread::Builder::new()
.name("fatal-test-worker".to_string())
.spawn(|| panic!("deliberate fatal from a background thread"))
.expect("the worker thread must spawn");
let _ = worker.join();
println!("PARENT SURVIVED THE WORKER PANIC");
}
let output = run_child("a_panic_on_a_background_thread_takes_the_whole_process_down", "panic");
let stderr = stderr_of(&output);
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
assert_eq!(
output.status.signal(),
Some(SIGABRT),
"a background panic must kill the process; stdout:\n{}\nstderr:\n{}",
stdout,
stderr
);
assert!(
!stdout.contains("PARENT SURVIVED THE WORKER PANIC"),
"the main thread must never resume past a background fatal"
);
assert!(
stderr.contains("fatal-test-worker"),
"the report must name the thread that died, or an operator cannot tell which actor it was; stderr was:\n{}",
stderr
);
}
#[test]
fn an_armed_fatal_beats_a_catch_unwind_that_would_have_swallowed_it() {
if triggered().as_deref() == Some("panic") {
install(FatalConfig::armed(true));
let swallowed = std::panic::catch_unwind(|| panic!("deliberate fatal inside a catch_unwind"));
println!("SWALLOWED: {}", swallowed.is_err());
}
let output = run_child("an_armed_fatal_beats_a_catch_unwind_that_would_have_swallowed_it", "panic");
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = stderr_of(&output);
assert_eq!(
output.status.signal(),
Some(SIGABRT),
"a catch_unwind must not be able to rescue a panic once armed; stdout:\n{}\nstderr:\n{}",
stdout,
stderr
);
assert!(
!stdout.contains("SWALLOWED"),
"reaching the catch arm means the process outlived something impossible"
);
}
#[test]
fn an_unexpected_error_can_be_routed_into_the_same_fatal_path() {
if triggered().as_deref() == Some("error") {
install(FatalConfig::armed(true));
let outcome: Result<(), String> = Err("storage flush could not reach sqlite".to_string());
reifydb_runtime::fatal_on_err!(outcome, "operator flush");
unreachable!("fatal_on_err must not return on the Err arm");
}
let output = run_child("an_unexpected_error_can_be_routed_into_the_same_fatal_path", "error");
let stderr = stderr_of(&output);
assert_eq!(output.status.signal(), Some(SIGABRT), "stderr was:\n{}", stderr);
assert!(stderr.contains("kind: unexpected error"), "stderr was:\n{}", stderr);
assert!(stderr.contains("component: operator flush"), "the component must localise the failure");
assert!(stderr.contains("storage flush could not reach sqlite"));
}
#[test]
fn a_violated_invariant_can_be_declared_fatal_without_a_panic() {
if triggered().as_deref() == Some("invariant") {
install(FatalConfig::armed(true));
reifydb_runtime::fatal!("watermark moved backwards: {} -> {}", 9, 4);
}
let output = run_child("a_violated_invariant_can_be_declared_fatal_without_a_panic", "invariant");
let stderr = stderr_of(&output);
assert_eq!(output.status.signal(), Some(SIGABRT), "stderr was:\n{}", stderr);
assert!(stderr.contains("kind: invariant violated"), "stderr was:\n{}", stderr);
assert!(stderr.contains("watermark moved backwards: 9 -> 4"));
assert!(stderr.contains("location: crates/runtime/tests/fatal.rs"), "the macro must record its own call site");
}
#[test]
fn a_disarmed_hook_reports_but_lets_the_panic_unwind() {
if triggered().as_deref() == Some("panic") {
install(FatalConfig::disarmed());
panic!("deliberate fatal while disarmed");
}
let output = run_child("a_disarmed_hook_reports_but_lets_the_panic_unwind", "panic");
let stderr = stderr_of(&output);
assert_eq!(output.status.signal(), None, "a disarmed hook must not abort; stderr was:\n{}", stderr);
assert!(
stderr.contains("REIFYDB FATAL"),
"disarming silences the abort, never the report; stderr was:\n{}",
stderr
);
assert!(stderr.contains("deliberate fatal while disarmed"));
}
#[test]
fn the_env_var_disarms_a_config_that_asked_to_be_armed() {
if triggered().as_deref() == Some("panic") {
install(FatalConfig::default());
panic!("deliberate fatal with the env override set");
}
let output = Command::new(env::current_exe().expect("the test binary must be locatable"))
.args(["--exact", "the_env_var_disarms_a_config_that_asked_to_be_armed", "--nocapture"])
.env(TRIGGER, "panic")
.env("REIFYDB_FATAL", "0")
.output()
.expect("the child test process must start");
let stderr = stderr_of(&output);
assert_eq!(output.status.signal(), None, "REIFYDB_FATAL=0 must disarm the default config; stderr:\n{}", stderr);
assert!(stderr.contains("REIFYDB FATAL"), "the report still fires when disarmed");
}