rylv-pool 0.1.0

Generic pooling with pluggable providers and thread-local storage
Documentation
use super::*;

use std::{
    cell::Cell,
    error::Error,
    sync::{
        Arc,
        atomic::{AtomicUsize, Ordering},
    },
};

#[derive(Debug)]
struct FactoryFailure;

impl fmt::Display for FactoryFailure {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("factory unavailable")
    }
}

impl Error for FactoryFailure {}

fn panic_error(payload: impl Any + Send) -> PanicError {
    catch_operation(|| std::panic::panic_any(payload))
        .err()
        .unwrap()
}

#[test]
fn catch_returns_the_original_success_value() {
    let value = Box::new(String::from("ready"));
    let original_address = std::ptr::from_ref(&*value).addr();
    let result = PoolError::<FactoryFailure>::catch(|| Ok(value)).unwrap();

    assert_eq!(std::ptr::from_ref(&*result).addr(), original_address);
    assert_eq!(*result, "ready");
}

#[test]
fn catch_returns_the_original_factory_error() {
    let failure = Box::new(42usize);
    let original_address = std::ptr::from_ref(&*failure).addr();
    let error = PoolError::catch(|| Err::<(), _>(failure)).err().unwrap();
    let PoolError::Factory(failure) = error else {
        panic!("expected the factory error");
    };

    assert_eq!(std::ptr::from_ref(&*failure).addr(), original_address);
    assert_eq!(*failure, 42);
}

#[test]
fn catch_invokes_a_consuming_operation_once() {
    let input = String::from("factory input");
    let mut calls = 0;
    let result = PoolError::<FactoryFailure>::catch(|| {
        calls += 1;
        Ok(input)
    })
    .unwrap();

    assert_eq!(result, "factory input");
    assert_eq!(calls, 1);
}

#[test]
fn catch_preserves_work_completed_before_a_panic_without_retrying() {
    let mut events = Vec::new();
    let error = PoolError::<FactoryFailure>::catch(|| -> Result<(), FactoryFailure> {
        events.push("completed");
        panic!("operation panic");
    })
    .err()
    .unwrap();

    assert_eq!(events, ["completed"]);
    let PoolError::Panic(error) = error else {
        panic!("expected an unwind error");
    };
    assert_eq!(error.to_string(), "operation panic");
}

#[test]
fn catch_accepts_exclusively_owned_mutable_operation_state() {
    let state = Cell::new(0);
    let result = PoolError::<FactoryFailure>::catch(|| {
        state.set(42);
        Ok(state.get())
    })
    .unwrap();

    assert_eq!(result, 42);
    assert_eq!(state.get(), 42);
}

#[test]
fn catch_operation_does_not_require_a_result_return_type() {
    let result = catch_operation(|| vec![1, 2, 3]).unwrap();
    assert_eq!(result, [1, 2, 3]);
}

#[test]
fn string_slice_payloads_keep_their_type_and_display_message() {
    for message in ["borrowed panic", ""] {
        let error = panic_error(message);
        assert_eq!(error.to_string(), message);
        assert_eq!(error.into_payload().downcast_ref::<&str>(), Some(&message));
    }
}

#[test]
fn owned_string_payloads_keep_their_type_and_display_message() {
    for message in ["owned panic", ""] {
        let error = panic_error(String::from(message));
        assert_eq!(error.to_string(), message);
        let payload = error.into_payload().downcast::<String>().unwrap();
        assert_eq!(*payload, message);
    }
}

#[test]
fn non_string_payloads_display_a_fallback_and_remain_recoverable() {
    let error = panic_error(Cell::new(42usize));

    assert_eq!(error.to_string(), "non-string panic payload");
    let payload = error.into_payload().downcast::<Cell<usize>>().unwrap();
    assert_eq!(payload.get(), 42);
}

#[test]
fn pool_error_display_accepts_borrowed_values_without_debug_or_error() {
    struct DisplayOnly<'a>(&'a str);

    impl fmt::Display for DisplayOnly<'_> {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            f.write_str(self.0)
        }
    }

    let message = String::from("borrowed failure");
    let error = PoolError::Factory(DisplayOnly(&message));

    assert_eq!(error.to_string(), "pool factory failed: borrowed failure");
}

#[test]
fn factory_error_display_debug_and_source_preserve_the_cause() {
    let error = PoolError::Factory(FactoryFailure);

    assert_eq!(
        error.to_string(),
        "pool factory failed: factory unavailable"
    );
    assert_eq!(format!("{error:?}"), "Factory(FactoryFailure)");
    let source = error.source().unwrap();
    assert!(source.downcast_ref::<FactoryFailure>().is_some());
    assert_eq!(source.to_string(), "factory unavailable");
    assert!(source.source().is_none());
}

#[test]
fn panic_error_display_debug_and_source_preserve_the_cause() {
    let error = PoolError::<FactoryFailure>::Panic(panic_error("root panic"));

    assert_eq!(error.to_string(), "pool operation panicked: root panic");
    let debug = format!("{error:?}");
    assert!(debug.starts_with("Panic(PanicError {"));
    assert!(debug.contains("root panic"));
    assert!(debug.contains(".."));
    let source = error.source().unwrap();
    assert!(source.downcast_ref::<PanicError>().is_some());
    assert_eq!(source.to_string(), "root panic");
    assert!(source.source().is_none());
}

#[test]
fn panic_error_debug_handles_all_payload_formats_without_consuming_them() {
    for error in [
        panic_error("borrowed message"),
        panic_error(String::from("owned message")),
        panic_error(42usize),
    ] {
        let message = error.to_string();
        let debug = format!("{error:?}");
        assert!(debug.starts_with("PanicError {"));
        assert!(debug.contains(&message));
        assert!(debug.ends_with(".. }"));
        assert_eq!(error.to_string(), message);
    }
}

struct PayloadDropProbe(Arc<AtomicUsize>);

impl Drop for PayloadDropProbe {
    fn drop(&mut self) {
        self.0.fetch_add(1, Ordering::Relaxed);
    }
}

#[test]
fn payload_lives_until_the_recovered_payload_is_destroyed() {
    let drops = Arc::new(AtomicUsize::new(0));
    let error = panic_error(PayloadDropProbe(Arc::clone(&drops)));

    assert_eq!(drops.load(Ordering::Relaxed), 0);
    assert_eq!(error.to_string(), "non-string panic payload");
    let payload = error.into_payload();
    assert_eq!(drops.load(Ordering::Relaxed), 0);
    assert!(payload.downcast_ref::<PayloadDropProbe>().is_some());
    drop(payload);
    assert_eq!(drops.load(Ordering::Relaxed), 1);
}

#[test]
fn dropping_the_error_destroys_an_unrecovered_payload_once() {
    let drops = Arc::new(AtomicUsize::new(0));
    let error =
        PoolError::<FactoryFailure>::Panic(panic_error(PayloadDropProbe(Arc::clone(&drops))));

    assert_eq!(drops.load(Ordering::Relaxed), 0);
    drop(error);
    assert_eq!(drops.load(Ordering::Relaxed), 1);
}

#[test]
fn poisoning_the_payload_mutex_preserves_formatting_and_extraction() {
    let error = panic_error(String::from("original payload"));
    let poison = catch_unwind(AssertUnwindSafe(|| {
        let _payload = error.payload.lock().unwrap();
        panic!("poison test");
    }));

    assert!(poison.is_err());
    assert!(error.payload.is_poisoned());
    assert_eq!(error.to_string(), "original payload");
    assert!(format!("{error:?}").contains("original payload"));
    let payload = error.into_payload().downcast::<String>().unwrap();
    assert_eq!(*payload, "original payload");
}

struct FailingWriter;

impl fmt::Write for FailingWriter {
    fn write_str(&mut self, _text: &str) -> fmt::Result {
        Err(fmt::Error)
    }
}

#[test]
fn formatting_errors_are_propagated_without_consuming_the_payload() {
    let error = panic_error("still available");
    let mut writer = FailingWriter;

    assert!(fmt::Write::write_fmt(&mut writer, format_args!("{error}")).is_err());
    assert!(fmt::Write::write_fmt(&mut writer, format_args!("{error:?}")).is_err());
    assert_eq!(error.to_string(), "still available");
    assert_eq!(
        error.into_payload().downcast_ref::<&str>(),
        Some(&"still available")
    );
}

#[test]
fn panic_errors_are_send_and_sync_even_for_non_sync_payloads() {
    fn assert_send_sync<T: Send + Sync>() {}
    assert_send_sync::<PanicError>();
    assert_send_sync::<PoolError<std::io::Error>>();
    assert_send_sync::<PoolError<()>>();

    let error = Arc::new(panic_error(Cell::new(42usize)));
    std::thread::scope(|scope| {
        for _ in 0..4 {
            let shared = Arc::clone(&error);
            scope.spawn(move || {
                for _ in 0..8 {
                    assert_eq!(shared.to_string(), "non-string panic payload");
                    assert!(format!("{shared:?}").contains("non-string panic payload"));
                }
            });
        }
    });

    let error = Arc::try_unwrap(error).unwrap();
    let payload = error.into_payload().downcast::<Cell<usize>>().unwrap();
    assert_eq!(payload.get(), 42);
}

#[test]
fn recovered_non_sync_payload_can_move_to_another_thread() {
    let error = panic_error(Cell::new(42usize));
    let result = std::thread::spawn(move || {
        let payload = error.into_payload().downcast::<Cell<usize>>().unwrap();
        payload.set(payload.get() + 1);
        payload.get()
    })
    .join()
    .unwrap();

    assert_eq!(result, 43);
}