use std::fs::OpenOptions;
use std::io::{ErrorKind, Write};
use std::panic;
use std::path::{Path, PathBuf};
use std::sync::{Mutex, Once};
use crate::runtime::execution::ExecutionState;
use crate::scheduler::serialization::serialize_schedule;
use crate::scheduler::Schedule;
use crate::{Config, FailurePersistence};
pub fn persist_failure(schedule: &Schedule, message: String, config: &Config, print_if_fresh: bool) -> String {
if let PanicHookState::Persisted(persisted_message) = PANIC_HOOK.with(|lock| lock.lock().unwrap().clone()) {
return persisted_message;
}
let persisted_message = persist_failure_inner(schedule, message, config);
PANIC_HOOK.with(|lock| *lock.lock().unwrap() = PanicHookState::Persisted(persisted_message.clone()));
if print_if_fresh {
eprintln!("{}", persisted_message);
}
persisted_message
}
pub fn persist_task_failure(schedule: &Schedule, task_name: String, config: &Config, print_if_fresh: bool) -> String {
persist_failure(
schedule,
format!("test panicked in task '{}'", task_name),
config,
print_if_fresh,
)
}
fn persist_failure_inner(schedule: &Schedule, message: String, config: &Config) -> String {
if config.failure_persistence == FailurePersistence::None {
return message;
}
let serialized_schedule = serialize_schedule(schedule);
if let FailurePersistence::File(directory) = &config.failure_persistence {
match persist_failure_to_file(&serialized_schedule, directory.as_ref()) {
Ok(path) => return format!("{}\nfailing schedule persisted to file: {}\npass that path to `shuttle::replay_from_file` to replay the failure", message, path.display()),
Err(e) => eprintln!("failed to persist schedule to file (error: {}), falling back to printing the schedule", e),
}
}
format!(
"{}\nfailing schedule:\n\"\n{}\n\"\npass that string to `shuttle::replay` to replay the failure",
message, serialized_schedule
)
}
fn persist_failure_to_file(serialized_schedule: &str, destination: Option<&PathBuf>) -> std::io::Result<PathBuf> {
let mut i = 0;
let dir = if let Some(dir) = destination {
dir.clone()
} else {
std::env::current_dir()?
};
let (path, mut file) = loop {
let path = dir.clone().join(Path::new(&format!("schedule{:03}.txt", i)));
match OpenOptions::new().write(true).create_new(true).open(&path) {
Ok(file) => break (path, file),
Err(e) => {
if e.kind() != ErrorKind::AlreadyExists {
return Err(e);
}
}
}
i += 1;
};
file.write_all(serialized_schedule.as_bytes())?;
path.canonicalize()
}
#[derive(Debug, Clone)]
enum PanicHookState {
Disarmed,
Armed(Config),
Persisted(String),
}
thread_local! {
static PANIC_HOOK: Mutex<PanicHookState> = const { Mutex::new(PanicHookState::Disarmed) };
}
#[derive(Debug)]
#[non_exhaustive]
pub struct PanicHookGuard;
impl Drop for PanicHookGuard {
fn drop(&mut self) {
PANIC_HOOK.with(|lock| *lock.lock().unwrap() = PanicHookState::Disarmed);
}
}
#[must_use = "the panic hook will be disarmed when the returned guard is dropped"]
pub fn init_panic_hook(config: Config) -> PanicHookGuard {
static INIT: Once = Once::new();
INIT.call_once(|| {
let original_hook = panic::take_hook();
panic::set_hook(Box::new(move |panic_info| {
let state = PANIC_HOOK.with(|lock| std::mem::replace(&mut *lock.lock().unwrap(), PanicHookState::Disarmed));
if let PanicHookState::Armed(config) = state {
if let Some((name, schedule)) = ExecutionState::failure_info() {
persist_task_failure(&schedule, name, &config, true);
}
}
original_hook(panic_info);
}));
});
PANIC_HOOK.with(|lock| *lock.lock().unwrap() = PanicHookState::Armed(config));
PanicHookGuard
}