use std::any::Any;
use std::future::Future;
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::task::Poll;
pub(crate) type PanicPayload = Box<dyn Any + Send + 'static>;
pub(crate) async fn catch_callback<F: Future>(fut: F) -> Result<F::Output, PanicPayload> {
tokio::pin!(fut);
std::future::poll_fn(
|cx| match catch_unwind(AssertUnwindSafe(|| fut.as_mut().poll(cx))) {
Ok(poll) => poll.map(Ok),
Err(payload) => Poll::Ready(Err(payload)),
},
)
.await
}
pub(crate) fn catch_sync<T>(f: impl FnOnce() -> T) -> Result<T, PanicPayload> {
catch_unwind(AssertUnwindSafe(f))
}
pub(crate) fn payload_into_string(payload: PanicPayload) -> String {
let payload = match payload.downcast::<&'static str>() {
Ok(s) => return (*s).to_string(),
Err(p) => p,
};
let payload = match payload.downcast::<String>() {
Ok(s) => return *s,
Err(p) => p,
};
let _ = catch_unwind(AssertUnwindSafe(move || drop(payload)));
"non-string panic payload".to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use std::panic;
#[tokio::test]
async fn passes_through_normal_completion() {
let out = catch_callback(async { 41 + 1 }).await;
assert_eq!(out.unwrap(), 42);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn catches_panic_before_first_await() {
let out = catch_callback(async {
panic!("early boom");
#[allow(unreachable_code)]
()
})
.await;
let payload = out.unwrap_err();
assert_eq!(payload_into_string(payload), "early boom");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn catches_panic_after_await_point() {
let out = catch_callback(async {
tokio::task::yield_now().await;
panic!("late boom");
#[allow(unreachable_code)]
()
})
.await;
let payload = out.unwrap_err();
assert_eq!(payload_into_string(payload), "late boom");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn formatted_runtime_payload_is_string() {
let value = std::hint::black_box(7);
let out = catch_callback(async move {
panic!("value was {value}");
})
.await;
assert_eq!(payload_into_string(out.unwrap_err()), "value was 7");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn non_string_payload_becomes_placeholder() {
let out = catch_callback(async {
panic::panic_any(1234_u32);
})
.await;
assert_eq!(
payload_into_string(out.unwrap_err()),
"non-string panic payload"
);
}
#[test]
fn catch_sync_catches() {
let err = catch_sync(|| -> u32 { panic!("sync boom") }).unwrap_err();
assert_eq!(payload_into_string(err), "sync boom");
assert_eq!(catch_sync(|| 5).unwrap(), 5);
}
}