use cached::claim::{Claim, ClaimRegistry};
use std::panic::AssertUnwindSafe;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Barrier};
#[test]
fn prelude_glob_brings_both_names_into_scope() {
use cached::prelude::*;
let registry: ClaimRegistry<u32> = ClaimRegistry::new();
let claim: Claim<u32> = registry.claim(1).unwrap();
assert_eq!(claim.key(), &1);
drop(claim);
assert!(registry.is_empty());
}
#[test]
fn root_glob_coexists_with_a_local_same_named_type() {
#[allow(unused_imports)]
use cached::*;
struct ClaimRegistry(u8);
let local = ClaimRegistry(9);
assert_eq!(local.0, 9);
let real = cached::claim::ClaimRegistry::<u8>::new();
assert!(real.is_empty());
}
#[test]
fn second_claim_of_a_live_key_is_none_then_succeeds_once_dropped() {
let registry: ClaimRegistry<&'static str> = ClaimRegistry::new();
let first = registry.claim("k").expect("first caller wins the claim");
assert!(
registry.claim("k").is_none(),
"a second claim on a still-live key must be refused"
);
assert!(registry.claim("k").is_none());
drop(first);
let second = registry
.claim("k")
.expect("the key is claimable again once the first claim is dropped");
drop(second);
assert!(registry.is_empty(), "the registry must drain to empty");
}
#[test]
fn released_on_normal_completion_drains_the_registry() {
let registry: ClaimRegistry<String> = ClaimRegistry::new();
{
let claim = registry.claim("job".to_string()).unwrap();
assert!(registry.is_claimed("job"));
assert_eq!(registry.len(), 1);
drop(claim);
}
assert!(!registry.is_claimed("job"));
assert_eq!(registry.len(), 0);
assert!(registry.is_empty());
}
#[test]
fn released_on_unwind_and_a_reclaim_succeeds() {
let registry: ClaimRegistry<String> = ClaimRegistry::new();
let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
let claim = registry.claim("panicking".to_string()).unwrap();
assert!(registry.is_claimed("panicking"));
let _ = &claim;
panic!("simulated refresh body panic while holding the claim");
}));
assert!(result.is_err(), "the panic must have propagated");
assert!(
!registry.is_claimed("panicking"),
"the claim must be released by unwind, not left live forever"
);
let retry = registry.claim("panicking".to_string());
assert!(
retry.is_some(),
"a retry after the panicking refresh must succeed"
);
drop(retry);
assert!(registry.is_empty(), "the registry must drain to empty");
}
struct HoldClaimThenPending {
_claim: Claim<String>,
polled: bool,
}
impl std::future::Future for HoldClaimThenPending {
type Output = ();
fn poll(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<()> {
let this = self.get_mut();
this.polled = true;
cx.waker().wake_by_ref();
std::task::Poll::Pending
}
}
#[test]
fn released_on_a_future_dropped_mid_poll_without_completing() {
let registry: ClaimRegistry<String> = ClaimRegistry::new();
let claim = registry.claim("cancel-me".to_string()).unwrap();
let mut fut = HoldClaimThenPending {
_claim: claim,
polled: false,
};
let waker = futures::task::noop_waker();
let mut cx = std::task::Context::from_waker(&waker);
let pin = std::pin::Pin::new(&mut fut);
let poll = std::future::Future::poll(pin, &mut cx);
assert_eq!(poll, std::task::Poll::Pending);
assert!(fut.polled, "the future must have actually started");
assert!(registry.is_claimed("cancel-me"));
drop(fut);
assert!(
!registry.is_claimed("cancel-me"),
"a dropped-mid-poll future must release its claim"
);
let retry = registry.claim("cancel-me".to_string());
assert!(retry.is_some(), "the key must be claimable again");
drop(retry);
assert!(registry.is_empty());
}
#[tokio::test]
async fn released_on_an_aborted_task_that_had_actually_started() {
let registry: ClaimRegistry<String> = ClaimRegistry::new();
let started = Arc::new(tokio::sync::Notify::new());
let started_signal = Arc::clone(&started);
let claim = registry.claim("aborted".to_string()).unwrap();
let handle = tokio::spawn(async move {
let _claim = claim;
started_signal.notify_one();
std::future::pending::<()>().await;
});
started.notified().await;
assert!(registry.is_claimed("aborted"));
handle.abort();
let joined = handle.await;
assert!(
joined.unwrap_err().is_cancelled(),
"the task must have been cancelled, not have panicked or completed"
);
assert!(
!registry.is_claimed("aborted"),
"an aborted task must still release its claim via Drop"
);
let retry = registry.claim("aborted".to_string());
assert!(
retry.is_some(),
"the key must be claimable again after the abort"
);
drop(retry);
assert!(registry.is_empty());
}
#[test]
fn n_threads_racing_for_one_key_yield_exactly_one_winner() {
const N: usize = 32;
let registry: ClaimRegistry<&'static str> = ClaimRegistry::new();
let start = Arc::new(Barrier::new(N));
let attempted = Arc::new(Barrier::new(N));
let winners = Arc::new(AtomicUsize::new(0));
let handles: Vec<_> = (0..N)
.map(|_| {
let registry = registry.clone();
let start = Arc::clone(&start);
let attempted = Arc::clone(&attempted);
let winners = Arc::clone(&winners);
std::thread::spawn(move || {
start.wait();
let claim = registry.claim("hot-key");
if claim.is_some() {
winners.fetch_add(1, Ordering::SeqCst);
}
attempted.wait();
drop(claim);
})
})
.collect();
for handle in handles {
handle.join().unwrap();
}
assert_eq!(
winners.load(Ordering::SeqCst),
1,
"exactly one of {N} racing threads must have won the claim"
);
assert!(registry.is_empty(), "the registry must drain to empty");
}
#[test]
fn is_claimed_agrees_with_the_claims_lifetime_including_through_a_borrowed_str() {
let registry: ClaimRegistry<String> = ClaimRegistry::new();
let owned = "user:1".to_string();
assert!(!registry.is_claimed(owned.as_str()));
assert!(!registry.is_claimed(&owned));
let claim = registry.claim(owned.clone()).unwrap();
let borrowed: &str = "user:1";
assert!(registry.is_claimed(borrowed));
assert!(registry.is_claimed(&owned));
drop(claim);
assert!(
!registry.is_claimed(borrowed),
"is_claimed must flip false the instant the claim is dropped"
);
assert!(!registry.is_claimed(&owned));
assert!(registry.is_empty());
}
#[test]
fn a_shared_registry_drains_to_zero_across_mixed_release_paths() {
let registry: ClaimRegistry<String> = ClaimRegistry::new();
let normal = registry.claim("normal".to_string()).unwrap();
drop(normal);
let panicked = std::panic::catch_unwind(AssertUnwindSafe(|| {
let claim = registry.claim("panicked".to_string()).unwrap();
let _ = &claim;
panic!("simulated");
}));
assert!(panicked.is_err());
let concurrent = registry.claim("concurrent".to_string()).unwrap();
assert_eq!(registry.len(), 1);
drop(concurrent);
assert_eq!(registry.len(), 0);
assert!(registry.is_empty());
}