use crate::logger::Logger;
use futures::FutureExt;
use std::future::Future;
pub(crate) async fn catch_contained<F>(
future: F,
) -> Result<F::Output, Box<dyn std::any::Any + Send>>
where
F: Future,
{
std::panic::AssertUnwindSafe(future).catch_unwind().await
}
pub(crate) fn payload_text(payload: &Box<dyn std::any::Any + Send>) -> String {
payload
.downcast_ref::<&str>()
.map(|s| s.to_string())
.or_else(|| payload.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "non-string panic payload".to_owned())
}
pub(crate) fn contain<F>(what: &'static str, logger: Option<&Logger>, callback: F)
where
F: FnOnce(),
{
if let Err(payload) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(callback)) {
report(what, logger, &payload);
}
}
pub(crate) async fn contain_join(
what: &'static str,
logger: Option<&Logger>,
join: tokio::task::JoinHandle<()>,
) {
match std::panic::AssertUnwindSafe(join).catch_unwind().await {
Err(payload) => report(what, logger, &payload),
Ok(Ok(())) => {}
Ok(Err(join_error)) if join_error.is_panic() => {
report(what, logger, &join_error.into_panic());
}
Ok(Err(_cancelled)) => {}
}
}
pub(crate) fn report_text(logger: Option<&Logger>, text: String) {
match logger {
Some(logger) => logger.warn(text),
None => eprintln!("{text}"),
}
}
fn report(what: &'static str, logger: Option<&Logger>, payload: &Box<dyn std::any::Any + Send>) {
report_text(
logger,
format!("cordis: {what} panicked: {}", payload_text(payload)),
);
}
#[cfg(test)]
pub(crate) fn quiet_hook<T>(f: impl FnOnce() -> T) -> T {
let default = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let out = f();
std::panic::set_hook(default);
out
}
#[cfg(test)]
mod tests {
use super::{contain, contain_join, quiet_hook};
use crate::logger::{BufferExporter, Level, Logger, LoggerService};
use std::sync::Arc;
fn logger_with_buffer() -> (Logger, Arc<BufferExporter>) {
let service = LoggerService::new();
let buffer = Arc::new(BufferExporter::new(16, Level::Debug).unwrap());
let id = service.reserve_exporter();
service.insert_reserved(id, buffer.clone());
(Logger::new_for_test(Arc::new(service)), buffer)
}
fn texts(exporter: &BufferExporter) -> Vec<String> {
exporter
.snapshot()
.into_iter()
.map(|record| record.text().to_owned())
.collect()
}
#[test]
fn sync_panicking_callback_is_contained_and_reaches_exporters() {
let (logger, buffer) = logger_with_buffer();
quiet_hook(|| {
contain("test callback", Some(&logger), || {
panic!("boom &str");
})
});
let records = buffer.snapshot();
assert_eq!(records.len(), 1, "exactly one report per contained panic");
assert_eq!(records[0].level(), Level::Warn);
assert_eq!(
records[0].text(),
"cordis: test callback panicked: boom &str"
);
}
#[test]
fn payload_kinds_render_once_each() {
let (logger, buffer) = logger_with_buffer();
quiet_hook(|| {
contain("site", Some(&logger), || panic!("as str"));
contain("site", Some(&logger), || {
std::panic::panic_any("as String".to_owned())
});
contain("site", Some(&logger), || std::panic::panic_any(7u32));
});
assert_eq!(
texts(&buffer),
[
"cordis: site panicked: as str",
"cordis: site panicked: as String",
"cordis: site panicked: non-string panic payload",
]
);
}
#[test]
fn sync_without_logger_falls_back_to_stderr() {
quiet_hook(|| contain("no logger", None, || panic!("quiet boom")));
}
#[tokio::test]
async fn panicked_task_is_reported_through_the_join_handle() {
let (logger, buffer) = logger_with_buffer();
let default = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let join = tokio::spawn(async {
panic!("join boom");
});
contain_join("join site", Some(&logger), join).await;
std::panic::set_hook(default);
assert_eq!(texts(&buffer), ["cordis: join site panicked: join boom"]);
}
}