use std::cell::Cell;
use std::fs::OpenOptions;
use std::io::{ErrorKind, Write};
use std::panic;
use std::path::{Path, PathBuf};
use std::sync::Once;
use crate::runtime::execution::{CurrentSchedule, ExecutionState};
use crate::scheduler::serialization::serialize_schedule;
use crate::{Config, FailurePersistence};
thread_local! {
static SCHEDULE_PERSISTED_AT: Cell<usize> = const { Cell::new(0) };
}
pub fn persist_failure(config: &Config) {
if SCHEDULE_PERSISTED_AT.get() == CurrentSchedule::len() {
return;
}
match &config.failure_persistence {
FailurePersistence::None => {}
FailurePersistence::File(directory) => {
let serialized_schedule = serialize_schedule(&CurrentSchedule::get_schedule());
match persist_failure_to_file(&serialized_schedule, directory.as_ref()) {
Ok(path) => eprintln!("failing schedule persisted to file: {}\npass that path to `shuttle::replay_from_file` to replay the failure", path.display()),
Err(e) => {
eprintln!("failed to persist schedule to file (error: {e}), falling back to printing the schedule");
eprintln!(
"failing schedule:\n\"\n{serialized_schedule}\n\"\npass that string to `shuttle::replay` to replay the failure"
);
}
}
}
FailurePersistence::Print => {
let serialized_schedule = serialize_schedule(&CurrentSchedule::get_schedule());
eprintln!(
"failing schedule:\n\"\n{serialized_schedule}\n\"\npass that string to `shuttle::replay` to replay the failure"
);
}
}
SCHEDULE_PERSISTED_AT.set(CurrentSchedule::len());
}
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{i:03}.txt")));
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()
}
pub fn init_panic_hook(config: Config) {
static INIT: Once = Once::new();
INIT.call_once(|| {
let original_hook = panic::take_hook();
panic::set_hook(Box::new(move |panic_info| {
eprintln!("Task failed, serializing schedule");
let task_name = ExecutionState::failing_task();
eprintln!("test panicked in task '{task_name}'");
persist_failure(&config);
original_hook(panic_info);
}));
});
}