use std::any::Any;
#[derive(Debug, Eq, PartialEq)]
pub(super) enum PocketIcPanicKind {
DeadInstanceTransport { message: String },
Other { message: String },
}
pub(super) fn panic_payload_to_string(payload: &(dyn Any + Send)) -> String {
if let Some(message) = payload.downcast_ref::<String>() {
return message.clone();
}
if let Some(message) = payload.downcast_ref::<&'static str>() {
return (*message).to_string();
}
"non-string panic payload".to_string()
}
pub(super) fn classify_pocket_ic_panic(payload: Box<dyn Any + Send>) -> PocketIcPanicKind {
let message = panic_payload_to_string(payload.as_ref());
if message_is_dead_instance_transport_error(&message) {
return PocketIcPanicKind::DeadInstanceTransport { message };
}
PocketIcPanicKind::Other { message }
}
pub(super) fn panic_is_dead_instance_transport(payload: &(dyn Any + Send)) -> bool {
matches!(
classify_pocket_ic_panic(Box::new(panic_payload_to_string(payload))),
PocketIcPanicKind::DeadInstanceTransport { .. }
)
}
pub(super) fn is_dead_instance_transport_error(message: &str) -> bool {
message_is_dead_instance_transport_error(message)
}
#[must_use]
pub fn is_dead_pocket_ic_transport_error(error: &(dyn std::error::Error + 'static)) -> bool {
let mut current = Some(error);
while let Some(candidate) = current {
if message_is_dead_instance_transport_error(&candidate.to_string()) {
return true;
}
current = candidate.source();
}
false
}
fn message_is_dead_instance_transport_error(message: &str) -> bool {
message.contains("ConnectionRefused")
|| message.contains("tcp connect error")
|| message.contains("IncompleteMessage")
|| message.contains("connection closed before message completed")
|| message.contains("channel closed")
}
#[cfg(test)]
mod tests {
use super::{
PocketIcPanicKind, classify_pocket_ic_panic, is_dead_instance_transport_error,
is_dead_pocket_ic_transport_error,
};
#[derive(Debug)]
struct WrapperError(std::io::Error);
impl std::fmt::Display for WrapperError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("wrapped PocketIC request failed")
}
}
impl std::error::Error for WrapperError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.0)
}
}
#[test]
fn dead_instance_transport_error_detects_connection_refused() {
assert!(is_dead_instance_transport_error(
"reqwest::Error { source: ConnectError(\"tcp connect error\", 127.0.0.1:1234, Os { code: 111, kind: ConnectionRefused, message: \"Connection refused\" }) }"
));
}
#[test]
fn dead_instance_transport_error_detects_incomplete_message() {
assert!(is_dead_instance_transport_error(
"reqwest::Error { source: hyper::Error(IncompleteMessage) }"
));
}
#[test]
fn classify_pocket_ic_panic_marks_dead_instance_transport() {
let classified = classify_pocket_ic_panic(Box::new(
"reqwest::Error { source: hyper::Error(IncompleteMessage) }".to_string(),
));
assert!(matches!(
classified,
PocketIcPanicKind::DeadInstanceTransport { .. }
));
}
#[test]
fn public_classifier_inspects_the_error_source_chain() {
let dead = WrapperError(std::io::Error::new(
std::io::ErrorKind::ConnectionRefused,
"tcp connect error: ConnectionRefused",
));
assert!(is_dead_pocket_ic_transport_error(&dead));
let unrelated = WrapperError(std::io::Error::other("request rejected"));
assert!(!is_dead_pocket_ic_transport_error(&unrelated));
}
}