use cached::{Cached, LruCache, UnboundCache};
#[test]
fn cache_get_or_set_with_mut_returns_mutable_ref() {
let mut cache: UnboundCache<u32, u32> =
UnboundCache::builder().build().expect("build UnboundCache");
let v: &mut u32 = cache.cache_get_or_set_with_mut(1, || 10);
assert_eq!(*v, 10);
*v += 5;
assert_eq!(cache.cache_get(&1), Some(&15));
let shared: &u32 = cache.cache_get_or_set_with(1, || 999);
assert_eq!(*shared, 15);
}
#[test]
fn cache_try_get_or_set_with_mut_returns_mutable_ref() {
let mut cache: UnboundCache<u32, u32> =
UnboundCache::builder().build().expect("build UnboundCache");
let result: Result<&mut u32, ()> = cache.cache_try_get_or_set_with_mut(1, || Err(()));
assert!(result.is_err());
assert_eq!(cache.cache_get(&1), None);
let v: &mut u32 = cache
.cache_try_get_or_set_with_mut(1, || Ok::<u32, ()>(10))
.unwrap();
assert_eq!(*v, 10);
*v *= 2;
assert_eq!(cache.cache_get(&1), Some(&20));
let shared: &u32 = cache
.cache_try_get_or_set_with(1, || Ok::<u32, ()>(999))
.unwrap();
assert_eq!(*shared, 20);
}
#[test]
fn lru_cache_get_or_set_with_mut_returns_mutable_ref() {
let mut cache: LruCache<u32, u32> = LruCache::builder()
.max_size(10)
.build()
.expect("build LruCache");
let v: &mut u32 = cache.cache_get_or_set_with_mut(1, || 10);
assert_eq!(*v, 10);
*v += 5;
assert_eq!(cache.cache_get(&1), Some(&15));
let hit: &mut u32 = cache.cache_get_or_set_with_mut(1, || 999);
assert_eq!(*hit, 15);
}
#[test]
fn lru_cache_try_get_or_set_with_mut_returns_mutable_ref() {
let mut cache: LruCache<u32, u32> = LruCache::builder()
.max_size(10)
.build()
.expect("build LruCache");
let result: Result<&mut u32, ()> = cache.cache_try_get_or_set_with_mut(1, || Err(()));
assert!(result.is_err());
assert_eq!(cache.cache_get(&1), None);
let v: &mut u32 = cache
.cache_try_get_or_set_with_mut(1, || Ok::<u32, ()>(10))
.unwrap();
assert_eq!(*v, 10);
*v *= 2;
assert_eq!(cache.cache_get(&1), Some(&20));
let hit: &mut u32 = cache
.cache_try_get_or_set_with_mut(1, || Ok::<u32, ()>(999))
.unwrap();
assert_eq!(*hit, 20);
}
mod expiring_cache_mut {
use cached::{Cached, Expires, ExpiringCache};
#[derive(Debug, PartialEq, Clone)]
struct Never(u32);
impl Expires for Never {
fn is_expired(&self) -> bool {
false
}
}
#[test]
fn expiring_cache_get_or_set_with_mut() {
let mut cache: ExpiringCache<u32, Never> = ExpiringCache::builder()
.build()
.expect("build ExpiringCache");
let v: &mut Never = cache.cache_get_or_set_with_mut(1, || Never(10));
assert_eq!(*v, Never(10));
v.0 += 5;
assert_eq!(cache.cache_get(&1), Some(&Never(15)));
let hit: &mut Never = cache.cache_get_or_set_with_mut(1, || Never(999));
assert_eq!(*hit, Never(15));
}
#[test]
fn expiring_cache_try_get_or_set_with_mut() {
let mut cache: ExpiringCache<u32, Never> = ExpiringCache::builder()
.build()
.expect("build ExpiringCache");
let result: Result<&mut Never, ()> = cache.cache_try_get_or_set_with_mut(1, || Err(()));
assert!(result.is_err());
assert_eq!(cache.cache_get(&1), None);
let v: &mut Never = cache
.cache_try_get_or_set_with_mut(1, || Ok::<Never, ()>(Never(20)))
.unwrap();
assert_eq!(*v, Never(20));
v.0 *= 2;
assert_eq!(cache.cache_get(&1), Some(&Never(40)));
let hit: &mut Never = cache
.cache_try_get_or_set_with_mut(1, || Ok::<Never, ()>(Never(999)))
.unwrap();
assert_eq!(*hit, Never(40));
}
}
#[cfg(feature = "time_stores")]
mod ttl_sorted_cache_mut {
use cached::{Cached, TtlSortedCache};
use std::time::Duration;
#[test]
fn ttl_sorted_cache_get_or_set_with_mut() {
let mut cache: TtlSortedCache<u32, u32> = TtlSortedCache::builder()
.ttl(Duration::from_secs(60))
.build()
.expect("build TtlSortedCache");
let v: &mut u32 = cache.cache_get_or_set_with_mut(1, || 10);
assert_eq!(*v, 10);
*v += 5;
assert_eq!(cache.cache_get(&1), Some(&15));
let hit: &mut u32 = cache.cache_get_or_set_with_mut(1, || 999);
assert_eq!(*hit, 15);
}
#[test]
fn ttl_sorted_cache_try_get_or_set_with_mut() {
let mut cache: TtlSortedCache<u32, u32> = TtlSortedCache::builder()
.ttl(Duration::from_secs(60))
.build()
.expect("build TtlSortedCache");
let result: Result<&mut u32, ()> = cache.cache_try_get_or_set_with_mut(1, || Err(()));
assert!(result.is_err());
assert_eq!(cache.cache_get(&1), None);
let v: &mut u32 = cache
.cache_try_get_or_set_with_mut(1, || Ok::<u32, ()>(20))
.unwrap();
assert_eq!(*v, 20);
*v *= 2;
assert_eq!(cache.cache_get(&1), Some(&40));
let hit: &mut u32 = cache
.cache_try_get_or_set_with_mut(1, || Ok::<u32, ()>(999))
.unwrap();
assert_eq!(*hit, 40);
}
}
#[cfg(feature = "time_stores")]
mod try_set_ttl_tests {
use cached::{CacheTtl, Cached, LruTtlCache, SetTtlError, TtlCache, TtlSortedCache};
use std::time::Duration;
#[test]
fn ttl_cache_try_set_ttl_rejects_zero() {
let mut cache = TtlCache::<u32, u32>::builder()
.ttl(Duration::from_secs(10))
.build()
.expect("build TtlCache");
let prev_ttl = cache.ttl();
let result = cache.try_set_ttl(Duration::ZERO);
assert_eq!(result, Err(SetTtlError::ZeroTtl));
assert_eq!(cache.ttl(), prev_ttl);
}
#[test]
fn ttl_cache_try_set_ttl_accepts_nonzero() {
let mut cache = TtlCache::<u32, u32>::builder()
.ttl(Duration::from_secs(10))
.build()
.expect("build TtlCache");
let result = cache.try_set_ttl(Duration::from_secs(30));
assert_eq!(result, Ok(Some(Duration::from_secs(10))));
assert_eq!(cache.ttl(), Some(Duration::from_secs(30)));
}
#[test]
fn lru_ttl_cache_try_set_ttl_rejects_zero() {
let mut cache = LruTtlCache::<u32, u32>::builder()
.max_size(8)
.ttl(Duration::from_secs(5))
.build()
.expect("build LruTtlCache");
assert_eq!(cache.try_set_ttl(Duration::ZERO), Err(SetTtlError::ZeroTtl));
}
#[test]
fn lru_ttl_cache_try_set_ttl_accepts_nonzero() {
let mut cache = LruTtlCache::<u32, u32>::builder()
.max_size(8)
.ttl(Duration::from_secs(5))
.build()
.expect("build LruTtlCache");
let prev = cache.try_set_ttl(Duration::from_secs(20));
assert_eq!(prev, Ok(Some(Duration::from_secs(5))));
assert_eq!(cache.ttl(), Some(Duration::from_secs(20)));
}
#[test]
fn ttl_sorted_cache_try_set_ttl_rejects_zero() {
let mut cache = TtlSortedCache::<u32, u32>::builder()
.ttl(Duration::from_secs(15))
.build()
.expect("build TtlSortedCache");
assert_eq!(cache.try_set_ttl(Duration::ZERO), Err(SetTtlError::ZeroTtl));
}
#[test]
fn ttl_sorted_cache_try_set_ttl_accepts_nonzero() {
let mut cache = TtlSortedCache::<u32, u32>::builder()
.ttl(Duration::from_secs(15))
.build()
.expect("build TtlSortedCache");
let prev = cache.try_set_ttl(Duration::from_secs(45));
assert_eq!(prev, Ok(Some(Duration::from_secs(15))));
assert_eq!(cache.ttl(), Some(Duration::from_secs(45)));
}
#[test]
fn set_ttl_error_display() {
let msg = format!("{}", SetTtlError::ZeroTtl);
assert_eq!(msg, "ttl must be greater than zero");
}
fn assert_zero_ttl_disables_expiry<C: Cached<u32, u32> + CacheTtl>(cache: &mut C) {
let _ = cache.set_ttl(Duration::ZERO);
cache.cache_set(7, 70);
assert_eq!(
cache.cache_get(&7),
Some(&70),
"a zero ttl must disable expiry so a just-inserted entry survives",
);
}
#[test]
fn ttl_cache_try_set_ttl_rejects_zero_set_ttl_disables() {
let mut cache = TtlCache::<u32, u32>::builder()
.ttl(Duration::from_secs(10))
.build()
.expect("build TtlCache");
let prev = cache.ttl();
assert_eq!(cache.try_set_ttl(Duration::ZERO), Err(SetTtlError::ZeroTtl));
assert_eq!(
cache.ttl(),
prev,
"rejected try_set_ttl must not change ttl"
);
cache.cache_set(1, 10);
assert_eq!(cache.cache_get(&1), Some(&10));
let mut disabled = TtlCache::<u32, u32>::builder()
.ttl(Duration::from_secs(10))
.build()
.expect("build TtlCache");
let _ = disabled.set_ttl(Duration::ZERO);
assert_eq!(disabled.ttl(), None, "set_ttl(0) resolves ttl to None");
disabled.cache_set(7, 70);
assert_eq!(
disabled.cache_get(&7),
Some(&70),
"set_ttl(0) must NOT expire a just-inserted entry"
);
}
#[test]
fn lru_ttl_cache_try_set_ttl_rejects_zero_set_ttl_disables() {
let mut cache = LruTtlCache::<u32, u32>::builder()
.max_size(8)
.ttl(Duration::from_secs(10))
.build()
.expect("build LruTtlCache");
let prev = cache.ttl();
assert_eq!(cache.try_set_ttl(Duration::ZERO), Err(SetTtlError::ZeroTtl));
assert_eq!(
cache.ttl(),
prev,
"rejected try_set_ttl must not change ttl"
);
cache.cache_set(1, 10);
assert_eq!(cache.cache_get(&1), Some(&10));
let mut disabled = LruTtlCache::<u32, u32>::builder()
.max_size(8)
.ttl(Duration::from_secs(10))
.build()
.expect("build LruTtlCache");
let _ = disabled.set_ttl(Duration::ZERO);
assert_eq!(disabled.ttl(), None, "set_ttl(0) resolves ttl to None");
disabled.cache_set(7, 70);
assert_eq!(
disabled.cache_get(&7),
Some(&70),
"set_ttl(0) must NOT expire a just-inserted LRU entry"
);
}
#[test]
fn ttl_sorted_cache_try_set_ttl_prevents_zero_ttl_breakage() {
let mut cache = TtlSortedCache::<u32, u32>::builder()
.ttl(Duration::from_secs(10))
.build()
.expect("build TtlSortedCache");
let prev = cache.ttl();
assert_eq!(cache.try_set_ttl(Duration::ZERO), Err(SetTtlError::ZeroTtl));
assert_eq!(
cache.ttl(),
prev,
"rejected try_set_ttl must not change ttl"
);
cache.cache_set(1, 10);
assert_eq!(cache.cache_get(&1), Some(&10));
let mut disabled = TtlSortedCache::<u32, u32>::builder()
.ttl(Duration::from_secs(10))
.build()
.expect("build TtlSortedCache");
assert_zero_ttl_disables_expiry(&mut disabled);
}
#[test]
fn set_ttl_error_is_std_error() {
use std::error::Error;
let err = SetTtlError::ZeroTtl;
assert_eq!(format!("{err:?}"), "ZeroTtl");
let boxed: Box<dyn Error> = Box::new(err.clone());
assert_eq!(boxed.to_string(), "ttl must be greater than zero");
assert!(
err.source().is_none(),
"SetTtlError::ZeroTtl must not report a source"
);
assert!(boxed.source().is_none());
}
}
mod concurrent_len_is_empty {
use cached::{ConcurrentCacheBase, ConcurrentCached, ShardedLruCache, ShardedUnboundCache};
#[test]
fn sharded_unbound_cache_len_is_empty() {
let cache: ShardedUnboundCache<u32, u32> = ShardedUnboundCache::builder()
.build()
.expect("build ShardedUnboundCache");
assert_eq!(ConcurrentCacheBase::cache_is_empty(&cache), Ok(Some(true)));
assert_eq!(ConcurrentCacheBase::cache_size(&cache), Ok(Some(0)));
cache.cache_set(1, 10).unwrap();
assert_eq!(ConcurrentCacheBase::cache_is_empty(&cache), Ok(Some(false)));
assert_eq!(ConcurrentCacheBase::cache_size(&cache), Ok(Some(1)));
cache.cache_set(2, 20).unwrap();
assert_eq!(ConcurrentCacheBase::cache_size(&cache), Ok(Some(2)));
cache.cache_remove(&1).unwrap();
assert_eq!(ConcurrentCacheBase::cache_size(&cache), Ok(Some(1)));
assert_eq!(ConcurrentCacheBase::cache_is_empty(&cache), Ok(Some(false)));
cache.cache_clear().unwrap();
assert_eq!(ConcurrentCacheBase::cache_size(&cache), Ok(Some(0)));
assert_eq!(ConcurrentCacheBase::cache_is_empty(&cache), Ok(Some(true)));
}
#[test]
fn sharded_lru_cache_len_is_empty() {
let cache: ShardedLruCache<u32, u32> = ShardedLruCache::builder()
.max_size(16)
.build()
.expect("build ShardedLruCache");
assert_eq!(ConcurrentCacheBase::cache_is_empty(&cache), Ok(Some(true)));
assert_eq!(ConcurrentCacheBase::cache_size(&cache), Ok(Some(0)));
cache.cache_set(42, 99).unwrap();
assert_eq!(ConcurrentCacheBase::cache_size(&cache), Ok(Some(1)));
assert_eq!(ConcurrentCacheBase::cache_is_empty(&cache), Ok(Some(false)));
cache.cache_set(43, 100).unwrap();
assert_eq!(ConcurrentCacheBase::cache_size(&cache), Ok(Some(2)));
cache.cache_reset().unwrap();
assert_eq!(ConcurrentCacheBase::cache_size(&cache), Ok(Some(0)));
assert_eq!(ConcurrentCacheBase::cache_is_empty(&cache), Ok(Some(true)));
}
}
#[cfg(feature = "async")]
mod concurrent_len_is_empty_async {
#[cfg(feature = "time_stores")]
use cached::ShardedTtlCache;
use cached::{ConcurrentCacheBase, ConcurrentCachedAsync, ShardedUnboundCache};
#[tokio::test]
async fn sharded_unbound_cache_async_len_is_empty() {
let cache: ShardedUnboundCache<u32, u32> = ShardedUnboundCache::builder()
.build()
.expect("build ShardedUnboundCache");
assert_eq!(ConcurrentCacheBase::cache_is_empty(&cache), Ok(Some(true)));
assert_eq!(ConcurrentCacheBase::cache_size(&cache), Ok(Some(0)));
ConcurrentCachedAsync::async_cache_set(&cache, 1, 10)
.await
.expect("infallible");
assert_eq!(ConcurrentCacheBase::cache_is_empty(&cache), Ok(Some(false)));
assert_eq!(ConcurrentCacheBase::cache_size(&cache), Ok(Some(1)));
ConcurrentCachedAsync::async_cache_set(&cache, 2, 20)
.await
.expect("infallible");
assert_eq!(ConcurrentCacheBase::cache_size(&cache), Ok(Some(2)));
ConcurrentCachedAsync::async_cache_clear(&cache)
.await
.expect("infallible");
assert_eq!(ConcurrentCacheBase::cache_size(&cache), Ok(Some(0)));
assert_eq!(ConcurrentCacheBase::cache_is_empty(&cache), Ok(Some(true)));
}
#[cfg(feature = "time_stores")]
#[tokio::test]
async fn sharded_ttl_cache_async_len_is_empty() {
use std::time::Duration;
let cache: ShardedTtlCache<u32, u32> = ShardedTtlCache::builder()
.ttl(Duration::from_secs(60))
.build()
.expect("build ShardedTtlCache");
assert_eq!(ConcurrentCacheBase::cache_is_empty(&cache), Ok(Some(true)));
assert_eq!(ConcurrentCacheBase::cache_size(&cache), Ok(Some(0)));
ConcurrentCachedAsync::async_cache_set(&cache, 1, 10)
.await
.expect("infallible");
ConcurrentCachedAsync::async_cache_set(&cache, 2, 20)
.await
.expect("infallible");
assert_eq!(ConcurrentCacheBase::cache_size(&cache), Ok(Some(2)));
assert_eq!(ConcurrentCacheBase::cache_is_empty(&cache), Ok(Some(false)));
ConcurrentCachedAsync::async_cache_reset(&cache)
.await
.expect("infallible");
assert_eq!(ConcurrentCacheBase::cache_size(&cache), Ok(Some(0)));
assert_eq!(ConcurrentCacheBase::cache_is_empty(&cache), Ok(Some(true)));
}
}
#[cfg(feature = "time_stores")]
mod concurrent_clone_cached_peek {
use cached::{ConcurrentCached, ConcurrentCloneCached, ShardedTtlCache};
use std::time::Duration;
#[test]
fn peek_live_and_absent_no_counter_change() {
let cache: ShardedTtlCache<u32, u32> = ShardedTtlCache::builder()
.ttl(Duration::from_secs(60))
.build()
.expect("build ShardedTtlCache");
ConcurrentCached::cache_set(&cache, 1, 42).expect("infallible");
let before = cache.metrics();
let (val, expired) = ConcurrentCloneCached::cache_peek_with_expiry_status(&cache, &1);
assert_eq!(val, Some(42), "live peek returns the value");
assert!(!expired, "live entry reports expired=false");
let (absent, absent_expired) =
ConcurrentCloneCached::cache_peek_with_expiry_status(&cache, &999);
assert_eq!(absent, None, "absent key returns None");
assert!(!absent_expired, "absent key reports expired=false");
let after = cache.metrics();
assert_eq!(after.hits, before.hits, "peek must not change hits");
assert_eq!(after.misses, before.misses, "peek must not change misses");
assert_eq!(
after.evictions, before.evictions,
"peek must not change evictions"
);
}
#[test]
fn peek_expired_returns_stale_without_removal() {
let cache: ShardedTtlCache<u32, u32> = ShardedTtlCache::builder()
.ttl(Duration::from_millis(10))
.build()
.expect("build ShardedTtlCache");
ConcurrentCached::cache_set(&cache, 1, 77).expect("infallible");
std::thread::sleep(Duration::from_millis(50));
let before = cache.metrics();
let (val, expired) = ConcurrentCloneCached::cache_peek_with_expiry_status(&cache, &1);
assert_eq!(val, Some(77), "expired peek returns the stale value");
assert!(expired, "expired entry reports expired=true");
let after = cache.metrics();
assert_eq!(after.hits, before.hits, "expired peek must not change hits");
assert_eq!(
after.misses, before.misses,
"expired peek must not change misses"
);
assert_eq!(
after.evictions, before.evictions,
"expired peek must not evict"
);
let (val2, expired2) = ConcurrentCloneCached::cache_peek_with_expiry_status(&cache, &1);
assert_eq!(val2, Some(77), "entry must survive the peek");
assert!(expired2, "entry must still be expired after peek");
}
#[test]
fn get_with_expiry_status_alias_matches_and_counts() {
let cache: ShardedTtlCache<u32, u32> = ShardedTtlCache::builder()
.ttl(Duration::from_secs(60))
.build()
.expect("build ShardedTtlCache");
ConcurrentCached::cache_set(&cache, 1, 42).expect("infallible");
let before = cache.metrics();
let via_alias = ConcurrentCloneCached::get_with_expiry_status(&cache, &1);
assert_eq!(
via_alias,
(Some(42), false),
"alias returns (value, expired)"
);
let (absent, absent_expired) = ConcurrentCloneCached::get_with_expiry_status(&cache, &999);
assert_eq!(absent, None);
assert!(!absent_expired);
let after = cache.metrics();
assert_eq!(
after.hits,
before.hits.map(|h| h + 1),
"live get must count a hit"
);
}
}
#[cfg(feature = "redb_store")]
mod redb_serialize_cached {
use cached::stores::RedbCache;
use cached::time::Duration;
use cached::{ConcurrentCached, SerializeCached};
use tempfile::TempDir;
fn build_cache(dir: &TempDir, name: &str) -> RedbCache<u32, String> {
RedbCache::<u32, String>::builder(name)
.disk_dir(dir.path())
.build()
.expect("error building redb cache")
}
#[test]
fn cache_set_ref_round_trip() {
let dir = TempDir::new().unwrap();
let cache = build_cache(&dir, "serialize_cached_round_trip");
let key: u32 = 42;
let value: String = "hello".to_string();
cache
.cache_set_ref(&key, &value)
.expect("cache_set_ref failed");
assert_eq!(key, 42);
assert_eq!(value, "hello");
assert_eq!(cache.cache_get(&key).unwrap(), Some("hello".to_string()));
cache
.cache_set_ref(&key, &"world".to_string())
.expect("cache_set_ref overwrite failed");
assert_eq!(cache.cache_get(&key).unwrap(), Some("world".to_string()));
}
#[test]
fn cache_set_ref_matches_cache_set() {
let dir = TempDir::new().unwrap();
let cache = build_cache(&dir, "serialize_cached_compat");
cache.cache_set(1, "owned".to_string()).unwrap();
cache.cache_set_ref(&2, &"owned".to_string()).unwrap();
assert_eq!(cache.cache_get(&1).unwrap(), cache.cache_get(&2).unwrap());
}
#[test]
fn cache_set_ref_ttl_expiry() {
let dir = TempDir::new().unwrap();
let cache: RedbCache<u32, String> = RedbCache::builder("serialize_cached_ttl_expiry")
.disk_dir(dir.path())
.ttl(Duration::from_secs(2))
.build()
.expect("error building redb cache");
let key: u32 = 1;
let value: String = "expires".to_string();
cache
.cache_set_ref(&key, &value)
.expect("cache_set_ref failed");
assert_eq!(cache.cache_get(&key).unwrap(), Some("expires".to_string()));
std::thread::sleep(std::time::Duration::from_millis(2400));
assert_eq!(cache.cache_get(&key).unwrap(), None);
}
}
#[cfg(all(feature = "redb_store", feature = "async"))]
mod redb_serialize_cached_async {
use cached::stores::RedbCache;
use cached::{ConcurrentCachedAsync, SerializeCachedAsync};
use tempfile::TempDir;
#[tokio::test]
async fn async_cache_set_ref_round_trip() {
let dir = TempDir::new().unwrap();
let cache: RedbCache<u32, String> = RedbCache::builder("serialize_cached_async_round_trip")
.disk_dir(dir.path())
.build()
.expect("error building redb cache");
let key: u32 = 7;
let value: String = "async".to_string();
cache
.async_cache_set_ref(&key, &value)
.await
.expect("async_cache_set_ref failed");
assert_eq!(key, 7);
assert_eq!(value, "async");
assert_eq!(
cache.async_cache_get(&key).await.unwrap(),
Some("async".to_string())
);
}
#[tokio::test]
async fn async_cache_set_ref_overwrite() {
let dir = TempDir::new().unwrap();
let cache: RedbCache<u32, String> = RedbCache::builder("serialize_cached_async_overwrite")
.disk_dir(dir.path())
.build()
.expect("error building redb cache");
let key: u32 = 99;
cache
.async_cache_set_ref(&key, &"first".to_string())
.await
.expect("async_cache_set_ref first failed");
cache
.async_cache_set_ref(&key, &"second".to_string())
.await
.expect("async_cache_set_ref overwrite failed");
assert_eq!(
cache.async_cache_get(&key).await.unwrap(),
Some("second".to_string())
);
}
}
#[test]
fn cache_try_set_default_is_infallible() {
use cached::{Cached, UnboundCache};
let mut cache: UnboundCache<u32, u32> =
UnboundCache::builder().build().expect("build UnboundCache");
let result: Result<Option<u32>, std::convert::Infallible> = cache.cache_try_set(1, 10);
assert_eq!(result.unwrap(), None);
let result: Result<Option<u32>, std::convert::Infallible> = cache.cache_try_set(1, 20);
assert_eq!(result.unwrap(), Some(10));
}
#[cfg(feature = "time_stores")]
#[test]
fn ttl_sorted_cache_try_set_succeeds_normal_ttl() {
use cached::time::Duration;
use cached::{Cached, TtlSortedCache};
let mut cache = TtlSortedCache::<u32, u32>::builder()
.ttl(Duration::from_secs(60))
.build()
.expect("build TtlSortedCache");
let result: Result<Option<u32>, std::convert::Infallible> = cache.cache_try_set(1, 42);
assert_eq!(result.unwrap(), None);
let result: Result<Option<u32>, std::convert::Infallible> = cache.cache_try_set(1, 99);
assert_eq!(result.unwrap(), Some(42));
}
#[cfg(feature = "time_stores")]
#[test]
fn ttl_sorted_cache_try_set_overflow_stores_never_expiring_entry() {
use cached::time::Duration;
use cached::{Cached, CachedExt, TtlSortedCache};
let mut cache = TtlSortedCache::<u32, u32>::builder()
.ttl(Duration::MAX)
.build()
.expect("Duration::MAX is non-zero so build() must succeed");
let result: Result<Option<u32>, std::convert::Infallible> = cache.cache_try_set(1, 42);
assert_eq!(result.unwrap(), None);
assert_eq!(cache.cache_size(), 1);
assert_eq!(cache.cache_get(&1), Some(&42));
let via_alias: Result<Option<u32>, std::convert::Infallible> = cache.try_set(2, 7);
assert_eq!(via_alias.unwrap(), None);
assert_eq!(cache.cache_size(), 2);
}
#[cfg(feature = "time_stores")]
#[test]
fn try_set_alias_is_infallible_for_ttl_sorted_cache() {
use cached::time::Duration;
use cached::{CachedExt, TtlSortedCache};
let mut cache = TtlSortedCache::<u32, u32>::builder()
.ttl(Duration::from_secs(60))
.build()
.expect("build TtlSortedCache");
let result: Result<Option<u32>, std::convert::Infallible> = cache.try_set(1, 7);
assert!(result.is_ok());
assert_eq!(result.unwrap(), None);
}
#[test]
fn cached_error_associated_type_infallible_for_unbound_cache() {
use cached::{Cached, CachedExt, UnboundCache};
let mut cache: UnboundCache<u32, u32> =
UnboundCache::builder().build().expect("build UnboundCache");
let r1: Result<Option<u32>, std::convert::Infallible> = cache.cache_try_set(10, 100);
assert_eq!(r1.unwrap(), None);
let r2: Result<Option<u32>, std::convert::Infallible> = cache.cache_try_set(10, 200);
assert_eq!(r2.unwrap(), Some(100));
let r3: Result<Option<u32>, std::convert::Infallible> = cache.try_set(10, 300);
assert_eq!(r3.unwrap(), Some(200));
}
#[test]
fn cached_error_associated_type_infallible_for_lru_cache() {
use cached::{Cached, LruCache};
let mut cache: LruCache<u32, u32> = LruCache::builder()
.max_size(4)
.build()
.expect("build LruCache");
let r: Result<Option<u32>, std::convert::Infallible> = cache.cache_try_set(1, 42);
assert_eq!(r.unwrap(), None);
}
#[cfg(feature = "time_stores")]
#[test]
fn cached_error_associated_type_infallible_for_ttl_cache() {
use cached::time::Duration;
use cached::{Cached, TtlCache};
let mut cache: TtlCache<u32, u32> = TtlCache::builder()
.ttl(Duration::from_secs(60))
.build()
.expect("build TtlCache");
let r: Result<Option<u32>, std::convert::Infallible> = cache.cache_try_set(1, 99);
assert_eq!(r.unwrap(), None);
}
#[cfg(feature = "time_stores")]
#[test]
fn cached_error_associated_type_infallible_for_lru_ttl_cache() {
use cached::time::Duration;
use cached::{Cached, LruTtlCache};
let mut cache: LruTtlCache<u32, u32> = LruTtlCache::builder()
.max_size(4)
.ttl(Duration::from_secs(60))
.build()
.expect("build LruTtlCache");
let r: Result<Option<u32>, std::convert::Infallible> = cache.cache_try_set(1, 7);
assert_eq!(r.unwrap(), None);
}
#[cfg(feature = "time_stores")]
#[test]
fn cached_error_associated_type_infallible_for_ttl_sorted_cache() {
use cached::time::Duration;
use cached::{Cached, TtlSortedCache};
let mut cache: TtlSortedCache<u32, u32> = TtlSortedCache::builder()
.ttl(Duration::from_secs(60))
.build()
.expect("build TtlSortedCache");
let r: Result<Option<u32>, std::convert::Infallible> = cache.cache_try_set(1, 55);
assert_eq!(r.unwrap(), None);
let mut overflow: TtlSortedCache<u32, u32> = TtlSortedCache::builder()
.ttl(Duration::MAX)
.build()
.expect("Duration::MAX is non-zero");
let r2: Result<Option<u32>, std::convert::Infallible> = overflow.cache_try_set(1, 55);
assert_eq!(r2.unwrap(), None);
assert_eq!(overflow.cache_size(), 1, "overflowing set stores the entry");
assert_eq!(overflow.cache_get(&1), Some(&55));
}
#[cfg(feature = "time_stores")]
#[test]
fn concurrent_refresh_on_hit_default_false() {
use cached::time::Duration;
use cached::{ConcurrentCacheRefreshOnHit, ShardedTtlCache};
let cache: ShardedTtlCache<u32, u32> = ShardedTtlCache::builder()
.ttl(Duration::from_secs(60))
.build()
.expect("build");
assert!(!ConcurrentCacheRefreshOnHit::refresh_on_hit(&cache));
}
#[cfg(feature = "time_stores")]
#[test]
fn concurrent_set_refresh_on_hit_updates_inner_state() {
use cached::time::Duration;
use cached::{ConcurrentCacheRefreshOnHit, ShardedTtlCache};
let cache = ShardedTtlCache::<u32, u32>::builder()
.ttl(Duration::from_secs(60))
.build()
.expect("build ShardedTtlCache");
assert!(!ConcurrentCacheRefreshOnHit::refresh_on_hit(&cache));
let prev = ConcurrentCacheRefreshOnHit::set_refresh_on_hit(&cache, true);
assert!(!prev, "previous value must be false");
assert!(
ConcurrentCacheRefreshOnHit::refresh_on_hit(&cache),
"trait getter must reflect set_refresh_on_hit(true)"
);
assert!(cache.refresh_on_hit());
let prev = ConcurrentCacheRefreshOnHit::set_refresh_on_hit(&cache, false);
assert!(prev, "previous value must be true");
assert!(!ConcurrentCacheRefreshOnHit::refresh_on_hit(&cache));
assert!(!cache.refresh_on_hit());
}
#[cfg(all(feature = "time_stores", feature = "async"))]
#[test]
fn concurrent_async_set_refresh_on_hit_updates_inner_state() {
use cached::time::Duration;
use cached::{ConcurrentCacheRefreshOnHit, ShardedTtlCache};
let cache = ShardedTtlCache::<u32, u32>::builder()
.ttl(Duration::from_secs(60))
.build()
.expect("build ShardedTtlCache");
assert!(!ConcurrentCacheRefreshOnHit::refresh_on_hit(&cache));
let prev = ConcurrentCacheRefreshOnHit::set_refresh_on_hit(&cache, true);
assert!(!prev, "previous flag must be false");
assert!(
cache.refresh_on_hit(),
"inherent getter must read the swapped flag"
);
assert!(
ConcurrentCacheRefreshOnHit::refresh_on_hit(&cache),
"trait getter must reflect set_refresh_on_hit(true)"
);
let prev = ConcurrentCacheRefreshOnHit::set_refresh_on_hit(&cache, false);
assert!(prev, "previous flag must be true");
assert!(!cache.refresh_on_hit());
assert!(!ConcurrentCacheRefreshOnHit::refresh_on_hit(&cache));
}
#[cfg(feature = "time_stores")]
#[test]
fn concurrent_sharded_lru_ttl_refresh_on_hit_getter_reflects_setter() {
use cached::time::Duration;
use cached::{ConcurrentCacheRefreshOnHit, ShardedLruTtlCache};
let cache = ShardedLruTtlCache::<u32, u32>::builder()
.max_size(8)
.ttl(Duration::from_secs(60))
.build()
.expect("build ShardedLruTtlCache");
assert!(!ConcurrentCacheRefreshOnHit::refresh_on_hit(&cache));
let prev = ConcurrentCacheRefreshOnHit::set_refresh_on_hit(&cache, true);
assert!(!prev, "previous flag must be false");
assert!(
ConcurrentCacheRefreshOnHit::refresh_on_hit(&cache),
"trait getter must reflect set_refresh_on_hit(true)"
);
let prev = ConcurrentCacheRefreshOnHit::set_refresh_on_hit(&cache, false);
assert!(prev, "previous flag must be true");
assert!(!ConcurrentCacheRefreshOnHit::refresh_on_hit(&cache));
}
#[cfg(feature = "redb_store")]
#[test]
fn concurrent_redb_refresh_on_hit_getter_reflects_setter() {
use cached::time::Duration;
use cached::{ConcurrentCacheRefreshOnHit, RedbCache};
use tempfile::TempDir;
let dir = TempDir::new().unwrap();
let cache: RedbCache<u32, u32> = RedbCache::builder("concurrent_redb_refresh_getter")
.disk_dir(dir.path())
.ttl(Duration::from_secs(60))
.build()
.expect("build RedbCache");
assert!(!ConcurrentCacheRefreshOnHit::refresh_on_hit(&cache));
let prev = ConcurrentCacheRefreshOnHit::set_refresh_on_hit(&cache, true);
assert!(!prev, "previous flag must be false");
assert!(
ConcurrentCacheRefreshOnHit::refresh_on_hit(&cache),
"trait getter must reflect set_refresh_on_hit(true)"
);
let prev = ConcurrentCacheRefreshOnHit::set_refresh_on_hit(&cache, false);
assert!(prev, "previous flag must be true");
assert!(!ConcurrentCacheRefreshOnHit::refresh_on_hit(&cache));
}
#[test]
fn short_remove_aliases_callable_for_effect() {
use cached::{Cached, CachedExt, UnboundCache};
let mut cache: UnboundCache<u32, u32> =
UnboundCache::builder().build().expect("build UnboundCache");
cache.cache_set(1, 10);
cache.cache_set(2, 20);
cache.remove(&1);
cache.remove_entry(&2);
assert_eq!(
cache.cache_size(),
0,
"both entries removed via short aliases"
);
cache.cache_set(3, 30);
assert_eq!(cache.cache_remove(&3), Some(30));
}
#[allow(dead_code)]
fn _assert_concurrent_cached_dyn_compatible(
store: &dyn cached::ConcurrentCached<String, u32, Error = std::convert::Infallible>,
) {
let _ = store.cache_get(&"k".to_string());
}
#[test]
fn concurrent_cache_get_or_set_with_hit_and_miss() {
use cached::{ConcurrentCached, ShardedUnboundCache};
let cache: ShardedUnboundCache<u32, u32> =
ShardedUnboundCache::builder().build().expect("build");
let v = ConcurrentCached::cache_get_or_set_with(&cache, 1, || 42).expect("infallible");
assert_eq!(v, 42);
assert_eq!(cache.cache_get(&1).unwrap(), Some(42));
let v = ConcurrentCached::cache_get_or_set_with(&cache, 1, || panic!("must not be called"))
.expect("infallible");
assert_eq!(v, 42);
}
#[test]
fn concurrent_cache_get_or_set_with_factory_runs_once() {
use cached::{ConcurrentCached, ShardedUnboundCache};
use std::sync::atomic::{AtomicUsize, Ordering};
let cache: ShardedUnboundCache<u32, u32> =
ShardedUnboundCache::builder().build().expect("build");
let calls = AtomicUsize::new(0);
let v = ConcurrentCached::cache_get_or_set_with(&cache, 1, || {
calls.fetch_add(1, Ordering::SeqCst);
42
})
.expect("infallible");
assert_eq!(v, 42);
assert_eq!(
calls.load(Ordering::SeqCst),
1,
"miss must invoke the factory once"
);
let v = ConcurrentCached::cache_get_or_set_with(&cache, 1, || {
calls.fetch_add(1, Ordering::SeqCst);
999
})
.expect("infallible");
assert_eq!(
v, 42,
"hit must return the stored value, not the recomputed one"
);
assert_eq!(
calls.load(Ordering::SeqCst),
1,
"hit must not invoke the factory again"
);
}
#[test]
fn concurrent_get_or_set_with_alias() {
use cached::{ConcurrentCachedExt, ShardedUnboundCache};
let cache: ShardedUnboundCache<u32, u32> =
ShardedUnboundCache::builder().build().expect("build");
let v = ConcurrentCachedExt::get_or_set_with(&cache, 10, || 99).expect("infallible");
assert_eq!(v, 99);
let v2 = ConcurrentCachedExt::get_or_set_with(&cache, 10, || panic!("must not be called"))
.expect("infallible");
assert_eq!(v2, 99);
}
#[cfg(feature = "async")]
mod async_cache_get_or_set_with_tests {
use cached::{ConcurrentCachedAsync, ShardedUnboundCache};
#[tokio::test]
async fn hit_and_miss() {
let cache: ShardedUnboundCache<u32, u32> =
ShardedUnboundCache::builder().build().expect("build");
let v = ConcurrentCachedAsync::async_cache_get_or_set_with(&cache, 1, || async { 55 })
.await
.expect("infallible");
assert_eq!(v, 55);
let stored = ConcurrentCachedAsync::async_cache_get(&cache, &1)
.await
.unwrap();
assert_eq!(stored, Some(55));
let v = ConcurrentCachedAsync::async_cache_get_or_set_with(&cache, 1, || async {
panic!("must not be called")
})
.await
.expect("infallible");
assert_eq!(v, 55);
}
#[tokio::test]
async fn async_factory_runs_once() {
use std::sync::atomic::{AtomicUsize, Ordering};
let cache: ShardedUnboundCache<u32, u32> =
ShardedUnboundCache::builder().build().expect("build");
let calls = AtomicUsize::new(0);
let v = ConcurrentCachedAsync::async_cache_get_or_set_with(&cache, 1, || async {
calls.fetch_add(1, Ordering::SeqCst);
42
})
.await
.expect("infallible");
assert_eq!(v, 42);
assert_eq!(
calls.load(Ordering::SeqCst),
1,
"miss must run the factory once"
);
let v = ConcurrentCachedAsync::async_cache_get_or_set_with(&cache, 1, || async {
calls.fetch_add(1, Ordering::SeqCst);
999
})
.await
.expect("infallible");
assert_eq!(v, 42, "hit returns the stored value");
assert_eq!(
calls.load(Ordering::SeqCst),
1,
"hit must not run the factory again"
);
}
#[cfg(feature = "time_stores")]
#[tokio::test]
async fn ttl_store_hit_and_miss() {
use cached::ShardedTtlCache;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
let cache: ShardedTtlCache<u32, u32> = ShardedTtlCache::builder()
.ttl(Duration::from_secs(60))
.build()
.expect("build ShardedTtlCache");
let calls = AtomicUsize::new(0);
let v = ConcurrentCachedAsync::async_cache_get_or_set_with(&cache, 1, || async {
calls.fetch_add(1, Ordering::SeqCst);
77
})
.await
.expect("infallible");
assert_eq!(v, 77);
assert_eq!(calls.load(Ordering::SeqCst), 1);
let stored = ConcurrentCachedAsync::async_cache_get(&cache, &1)
.await
.unwrap();
assert_eq!(stored, Some(77));
let v = ConcurrentCachedAsync::async_cache_get_or_set_with(&cache, 1, || async {
calls.fetch_add(1, Ordering::SeqCst);
999
})
.await
.expect("infallible");
assert_eq!(v, 77, "live hit returns the stored value");
assert_eq!(
calls.load(Ordering::SeqCst),
1,
"live hit must not run the factory"
);
}
}
#[test]
fn unbound_cache_size_via_public_api() {
use cached::Cached;
let mut cache = UnboundCache::<u32, u32>::builder().build().unwrap();
cache.cache_set(1, 10);
cache.cache_set(2, 20);
assert_eq!(cache.cache_size(), 2);
assert!(cache.cache_get(&1).is_some());
assert!(cache.cache_get(&2).is_some());
}
#[cfg(feature = "time_stores")]
#[test]
fn ttl_cache_size_and_lookup_via_public_api() {
use cached::time::Duration;
use cached::{Cached, TtlCache};
let mut cache = TtlCache::<u32, u32>::builder()
.ttl(Duration::from_secs(60))
.build()
.unwrap();
cache.cache_set(1, 10);
assert_eq!(cache.cache_size(), 1);
assert_eq!(cache.cache_get(&1), Some(&10));
}
#[cfg(feature = "time_stores")]
#[test]
fn lru_ttl_cache_metrics_via_public_api() {
use cached::time::Duration;
use cached::{Cached, LruTtlCache};
let mut cache = LruTtlCache::<u32, u32>::builder()
.max_size(4)
.ttl(Duration::from_secs(60))
.build()
.unwrap();
cache.cache_set(1, 10);
cache.cache_reset_metrics();
assert!(cache.cache_get(&1).is_some());
assert_eq!(cache.cache_hits(), Some(1));
assert_eq!(cache.cache_misses(), Some(0));
}
#[cfg(feature = "time_stores")]
mod sharded_set_ttl_zero {
use cached::time::Duration;
use cached::{ConcurrentCacheTtl, ConcurrentCached, ShardedLruTtlCache, ShardedTtlCache};
#[test]
fn sharded_ttl_inherent_set_ttl_zero_disables_expiry() {
let cache: ShardedTtlCache<u32, u32> = ShardedTtlCache::builder()
.ttl(Duration::from_secs(60))
.build()
.expect("build ShardedTtlCache");
let prev = cache.set_ttl(Duration::ZERO);
assert_eq!(prev, Some(Duration::from_secs(60)));
assert_eq!(
cache.ttl(),
None,
"a zero ttl disables expiry (resolves to None)"
);
cache.cache_set(1, 10).unwrap();
assert_eq!(cache.cache_get(&1), Ok(Some(10)));
}
#[test]
fn sharded_ttl_trait_set_ttl_zero_disables_expiry() {
let cache: ShardedTtlCache<u32, u32> = ShardedTtlCache::builder()
.ttl(Duration::from_secs(60))
.build()
.expect("build ShardedTtlCache");
let prev = ConcurrentCacheTtl::set_ttl(&cache, Duration::ZERO);
assert_eq!(prev, Some(Duration::from_secs(60)));
cache.cache_set(2, 20).unwrap();
assert_eq!(cache.cache_get(&2), Ok(Some(20)));
}
#[test]
fn sharded_lru_ttl_inherent_set_ttl_zero_disables_expiry() {
let cache: ShardedLruTtlCache<u32, u32> = ShardedLruTtlCache::builder()
.max_size(8)
.ttl(Duration::from_secs(60))
.build()
.expect("build ShardedLruTtlCache");
let prev = cache.set_ttl(Duration::ZERO);
assert_eq!(prev, Some(Duration::from_secs(60)));
assert_eq!(cache.ttl(), None);
cache.cache_set(1, 10).unwrap();
assert_eq!(cache.cache_get(&1), Ok(Some(10)));
}
#[test]
fn sharded_lru_ttl_trait_set_ttl_zero_disables_expiry() {
let cache: ShardedLruTtlCache<u32, u32> = ShardedLruTtlCache::builder()
.max_size(8)
.ttl(Duration::from_secs(60))
.build()
.expect("build ShardedLruTtlCache");
let prev = ConcurrentCacheTtl::set_ttl(&cache, Duration::ZERO);
assert_eq!(prev, Some(Duration::from_secs(60)));
cache.cache_set(2, 20).unwrap();
assert_eq!(cache.cache_get(&2), Ok(Some(20)));
}
#[test]
fn sharded_ttl_set_zero_is_equivalent_to_unset() {
let via_zero: ShardedTtlCache<u32, u32> = ShardedTtlCache::builder()
.ttl(Duration::from_secs(60))
.build()
.expect("build ShardedTtlCache");
let via_unset: ShardedTtlCache<u32, u32> = ShardedTtlCache::builder()
.ttl(Duration::from_secs(60))
.build()
.expect("build ShardedTtlCache");
let _ = via_zero.set_ttl(Duration::ZERO);
let _ = via_unset.unset_ttl();
assert_eq!(via_zero.ttl(), via_unset.ttl());
assert_eq!(via_zero.ttl(), None);
via_zero.cache_set(3, 30).unwrap();
via_unset.cache_set(3, 30).unwrap();
assert_eq!(via_zero.cache_get(&3), Ok(Some(30)));
assert_eq!(via_unset.cache_get(&3), Ok(Some(30)));
via_zero.set_ttl(Duration::from_millis(20));
via_zero.cache_set(4, 40).unwrap();
std::thread::sleep(std::time::Duration::from_millis(60));
assert_eq!(
via_zero.cache_get(&3),
Ok(Some(30)),
"entry inserted while disabled keeps expires_at=None; must survive re-arming"
);
assert_eq!(
via_zero.cache_get(&4),
Ok(None),
"entry inserted after set_ttl(nonzero) must expire at the new deadline"
);
}
}
#[cfg(feature = "redb_store")]
#[test]
fn redb_builder_missing_name_is_server_free_error() {
use cached::{BuildError, RedbCacheBuildError, RedbCacheBuilder};
let result = RedbCacheBuilder::<u32, u32>::new().build();
assert!(
matches!(
result,
Err(RedbCacheBuildError::Build(BuildError::MissingRequired(
"name"
)))
),
"expected Build(MissingRequired(\"name\"))"
);
}
#[cfg(feature = "redis_store")]
#[test]
fn redis_builder_missing_required_is_server_free_error() {
use cached::{BuildError, RedisCacheBuildError, RedisCacheBuilder};
let result = RedisCacheBuilder::<u32, u32>::new().build();
assert!(
matches!(
result,
Err(RedisCacheBuildError::Build(BuildError::MissingRequired(
"prefix"
)))
),
"expected Build(MissingRequired(\"prefix\"))"
);
}
#[cfg(all(feature = "redb_store", feature = "time_stores"))]
mod concurrent_trait_split_no_collision {
use cached::prelude::*;
use cached::time::Duration;
use cached::{
RedbCache, SetTtlError, ShardedLruTtlCache, ShardedTtlCache, ShardedUnboundCache,
};
#[test]
fn redb_shared_helpers_resolve_without_fully_qualified_syntax() {
let dir = tempfile::TempDir::new().expect("temp dir");
let cache: RedbCache<String, u32> = RedbCache::builder("collision-probe")
.disk_dir(dir.path())
.ttl(Duration::from_secs(60))
.build()
.expect("build RedbCache");
assert_eq!(cache.cache_size().expect("cache_size"), None);
assert_eq!(cache.cache_is_empty().expect("cache_is_empty"), None);
assert_eq!(cache.ttl(), Some(Duration::from_secs(60)));
let prev = cache.set_ttl(Duration::from_secs(30));
assert_eq!(prev, Some(Duration::from_secs(60)));
let prev2 = cache.unset_ttl();
assert_eq!(prev2, Some(Duration::from_secs(30)));
assert_eq!(cache.ttl(), None);
assert_eq!(cache.cache_set("k".to_string(), 7).expect("set"), None);
assert_eq!(cache.cache_get(&"k".to_string()).expect("get"), Some(7));
}
#[test]
fn sharded_ttl_shared_helpers_resolve_without_fully_qualified_syntax() {
let cache: ShardedTtlCache<u32, u32> = ShardedTtlCache::builder()
.ttl(Duration::from_secs(60))
.build()
.expect("build ShardedTtlCache");
assert_eq!(ConcurrentCacheBase::cache_size(&cache), Ok(Some(0)));
cache.cache_set(1, 10).expect("infallible");
assert_eq!(ConcurrentCacheBase::cache_size(&cache), Ok(Some(1)));
let prev = cache.set_ttl(Duration::from_secs(30));
assert_eq!(prev, Some(Duration::from_secs(60)));
assert_eq!(cache.unset_ttl(), Some(Duration::from_secs(30)));
}
#[test]
fn concurrent_try_set_ttl_zero_is_rejected() {
let redb_dir = tempfile::TempDir::new().expect("temp dir");
let redb: RedbCache<String, u32> = RedbCache::builder("try-set-ttl-zero")
.disk_dir(redb_dir.path())
.ttl(Duration::from_secs(60))
.build()
.expect("build RedbCache");
assert_eq!(
redb.try_set_ttl(Duration::ZERO),
Err(SetTtlError::ZeroTtl),
"try_set_ttl(ZERO) must reject without disabling expiry"
);
assert_eq!(redb.ttl(), Some(Duration::from_secs(60)));
assert_eq!(
redb.try_set_ttl(Duration::from_secs(10)),
Ok(Some(Duration::from_secs(60)))
);
let sharded: ShardedTtlCache<u32, u32> = ShardedTtlCache::builder()
.ttl(Duration::from_secs(60))
.build()
.expect("build ShardedTtlCache");
assert_eq!(
sharded.try_set_ttl(Duration::ZERO),
Err(SetTtlError::ZeroTtl)
);
let lru_ttl: ShardedLruTtlCache<u32, u32> = ShardedLruTtlCache::builder()
.max_size(8)
.ttl(Duration::from_secs(60))
.build()
.expect("build ShardedLruTtlCache");
assert_eq!(
lru_ttl.try_set_ttl(Duration::ZERO),
Err(SetTtlError::ZeroTtl)
);
}
#[test]
fn non_ttl_sharded_store_base_helpers_resolve() {
let cache: ShardedUnboundCache<u32, u32> =
ShardedUnboundCache::builder().build().expect("build");
assert_eq!(ConcurrentCacheBase::cache_is_empty(&cache), Ok(Some(true)));
cache.cache_set(1, 10).expect("infallible");
assert_eq!(ConcurrentCacheBase::cache_size(&cache), Ok(Some(1)));
}
#[cfg(feature = "async")]
#[tokio::test]
async fn redb_shared_helpers_resolve_unqualified_in_async_context() {
let dir = tempfile::TempDir::new().expect("temp dir");
let cache: RedbCache<String, u32> = RedbCache::builder("collision-probe-async")
.disk_dir(dir.path())
.ttl(Duration::from_secs(60))
.build()
.expect("build RedbCache");
assert_eq!(cache.cache_size().expect("cache_size"), None);
assert_eq!(cache.ttl(), Some(Duration::from_secs(60)));
let prev = cache.set_ttl(Duration::from_secs(30));
assert_eq!(prev, Some(Duration::from_secs(60)));
let set_prev = cache
.async_cache_set("k".to_string(), 7)
.await
.expect("async_cache_set");
assert_eq!(set_prev, None);
assert_eq!(
cache.async_cache_get(&"k".to_string()).await.expect("get"),
Some(7)
);
let prev2 = cache.unset_ttl();
assert_eq!(prev2, Some(Duration::from_secs(30)));
assert_eq!(cache.ttl(), None);
assert_eq!(cache.try_set_ttl(Duration::ZERO), Err(SetTtlError::ZeroTtl));
}
}
#[cfg(feature = "redb_store")]
mod concurrent_base_unknown_size_defaults {
use cached::time::Duration;
use cached::{ConcurrentCacheBase, ConcurrentCached, RedbCache};
#[test]
fn redb_len_and_is_empty_default_to_unknown() {
let dir = tempfile::TempDir::new().expect("temp dir");
let cache: RedbCache<String, u32> = RedbCache::builder("unknown-size-defaults")
.disk_dir(dir.path())
.ttl(Duration::from_secs(60))
.build()
.expect("build RedbCache");
assert_eq!(
ConcurrentCacheBase::cache_size(&cache).expect("cache_size"),
None
);
assert_eq!(ConcurrentCacheBase::cache_size(&cache).expect("len"), None);
assert_eq!(
ConcurrentCacheBase::cache_is_empty(&cache).expect("is_empty"),
None
);
ConcurrentCached::cache_set(&cache, "k".to_string(), 1).expect("infallible set");
assert_eq!(
ConcurrentCacheBase::cache_size(&cache).expect("cache_size"),
None
);
assert_eq!(ConcurrentCacheBase::cache_size(&cache).expect("len"), None);
assert_eq!(
ConcurrentCacheBase::cache_is_empty(&cache).expect("is_empty"),
None
);
}
}
mod concurrent_metrics_via_base_trait {
use cached::{CacheMetrics, ConcurrentCacheBase, ConcurrentCached};
fn assert_metrics_available<S>(store: &S)
where
S: ConcurrentCacheBase,
{
let _ = store.cache_hits();
let _ = store.cache_misses();
let _ = store.cache_capacity();
let _ = store.cache_evictions();
let _m: CacheMetrics = store.metrics();
}
#[test]
fn sharded_unbound_cache_metrics_via_base_bound() {
use cached::ShardedUnboundCache;
let cache: ShardedUnboundCache<u32, u32> = ShardedUnboundCache::builder()
.shards(4)
.build()
.expect("build");
assert_metrics_available(&cache);
assert_eq!(cache.cache_hits(), Some(0));
assert_eq!(cache.cache_misses(), Some(0));
ConcurrentCached::cache_set(&cache, 1, 10).expect("infallible");
let _ = ConcurrentCached::cache_get(&cache, &1).expect("infallible"); let _ = ConcurrentCached::cache_get(&cache, &2).expect("infallible"); assert_eq!(cache.cache_hits(), Some(1));
assert_eq!(cache.cache_misses(), Some(1));
assert_eq!(cache.cache_capacity(), None);
assert_eq!(cache.cache_evictions(), None);
let m = cache.metrics();
assert_eq!(m.hits, Some(1));
assert_eq!(m.misses, Some(1));
assert_eq!(m.evictions, None);
assert_eq!(m.entry_count, Some(1));
assert_eq!(m.capacity, None);
}
#[test]
fn sharded_lru_cache_metrics_via_base_bound() {
use cached::ShardedLruCache;
let cache: ShardedLruCache<u32, u32> = ShardedLruCache::builder()
.shards(2)
.per_shard_max_size(8)
.build()
.expect("build");
assert_metrics_available(&cache);
assert_eq!(cache.cache_capacity(), Some(16));
ConcurrentCached::cache_set(&cache, 1, 10).expect("infallible");
let _ = ConcurrentCached::cache_get(&cache, &1); let _ = ConcurrentCached::cache_get(&cache, &9); assert_eq!(cache.cache_hits(), Some(1));
assert_eq!(cache.cache_misses(), Some(1));
let m = cache.metrics();
assert_eq!(m.hits, Some(1));
assert_eq!(m.misses, Some(1));
assert_eq!(m.capacity, Some(16));
}
#[cfg(feature = "time_stores")]
#[test]
fn sharded_ttl_cache_metrics_via_base_bound() {
use cached::ShardedTtlCache;
use cached::time::Duration;
let cache: ShardedTtlCache<u32, u32> = ShardedTtlCache::builder()
.shards(2)
.ttl(Duration::from_secs(60))
.build()
.expect("build");
assert_metrics_available(&cache);
ConcurrentCached::cache_set(&cache, 1, 10).expect("infallible");
let _ = ConcurrentCached::cache_get(&cache, &1); let _ = ConcurrentCached::cache_get(&cache, &9); assert_eq!(cache.cache_hits(), Some(1));
assert_eq!(cache.cache_misses(), Some(1));
}
}
mod extension_trait_blanket_impls {
#[test]
fn cached_ext_short_aliases_without_cached_in_scope() {
use cached::{CachedExt, UnboundCache};
let mut cache: UnboundCache<u32, u32> = UnboundCache::builder().build().unwrap();
assert_eq!(cache.set(1, 10), None);
assert_eq!(cache.get(&1), Some(&10));
assert_eq!(cache.len(), 1);
assert!(!cache.is_empty());
assert_eq!(cache.remove(&1), Some(10));
assert!(cache.is_empty());
cache.set(2, 20);
assert!(cache.delete(&2));
assert!(!cache.delete(&2));
cache.set(3, 30);
assert!(cache.contains(&3));
cache.clear();
assert!(cache.is_empty());
}
fn fill_and_drain<K, V, C>(cache: &mut C, key: K, val: V) -> Option<V>
where
K: std::hash::Hash + Eq + Clone,
V: Clone + PartialEq + std::fmt::Debug,
C: cached::CachedExt<K, V>,
{
cached::CachedExt::set(cache, key.clone(), val.clone());
assert_eq!(cached::CachedExt::get(cache, &key), Some(&val));
cached::CachedExt::remove(cache, &key)
}
#[test]
fn cached_ext_generic_bound_works() {
use cached::UnboundCache;
let mut c: UnboundCache<u32, u32> = UnboundCache::builder().build().unwrap();
let removed = fill_and_drain(&mut c, 42u32, 100u32);
assert_eq!(removed, Some(100));
}
#[test]
fn cached_ext_metrics_via_ext_trait_only() {
use cached::{CachedExt, UnboundCache};
let mut cache: UnboundCache<u32, u32> = UnboundCache::builder().build().unwrap();
cache.set(1, 10);
let _ = cache.get(&1); let _ = cache.get(&2); assert_eq!(cache.hits(), Some(1));
assert_eq!(cache.misses(), Some(1));
let m = cache.metrics();
assert_eq!(m.hits, Some(1));
assert_eq!(m.misses, Some(1));
assert_eq!(m.entry_count, Some(1));
}
#[test]
fn concurrent_cached_ext_short_aliases_without_concurrent_cached_in_scope() {
use cached::{ConcurrentCachedExt, ShardedUnboundCache};
let cache: ShardedUnboundCache<u32, u32> =
ShardedUnboundCache::builder().build().expect("build");
assert_eq!(
ConcurrentCachedExt::set(&cache, 1u32, 10u32).expect("infallible"),
None
);
assert_eq!(
ConcurrentCachedExt::get(&cache, &1u32).expect("infallible"),
Some(10)
);
assert_eq!(
ConcurrentCachedExt::remove(&cache, &1u32).expect("infallible"),
Some(10)
);
assert_eq!(
ConcurrentCachedExt::get(&cache, &1u32).expect("infallible"),
None
);
}
#[test]
fn concurrent_cached_ext_clear_reset_aliases() {
use cached::{ConcurrentCacheBase, ConcurrentCachedExt, ShardedUnboundCache};
let cache: ShardedUnboundCache<u32, u32> = ShardedUnboundCache::builder()
.shards(1)
.build()
.expect("build");
ConcurrentCachedExt::set(&cache, 1u32, 10u32).expect("infallible");
ConcurrentCachedExt::set(&cache, 2u32, 20u32).expect("infallible");
assert_eq!(cache.cache_size().expect("infallible"), Some(2));
assert_eq!(
ConcurrentCachedExt::get(&cache, &1u32).expect("infallible"),
Some(10),
"hit"
);
assert_eq!(
ConcurrentCachedExt::get(&cache, &999u32).expect("infallible"),
None,
"miss"
);
let before = cache.metrics();
assert_eq!(before.hits, Some(1), "one hit recorded");
assert_eq!(before.misses, Some(1), "one miss recorded");
ConcurrentCachedExt::clear(&cache).expect("infallible");
assert_eq!(cache.cache_size().expect("infallible"), Some(0));
assert_eq!(
ConcurrentCachedExt::get(&cache, &1u32).expect("infallible"),
None
);
let after_clear = cache.metrics();
assert_eq!(
after_clear.hits, before.hits,
"clear must preserve the hit counter"
);
assert_eq!(
after_clear.misses,
Some(2),
"clear preserves misses (the prior 1 plus the post-clear miss), it does not zero them"
);
ConcurrentCachedExt::set(&cache, 3u32, 30u32).expect("infallible");
assert_eq!(cache.cache_size().expect("infallible"), Some(1));
ConcurrentCachedExt::reset(&cache).expect("infallible");
assert_eq!(cache.cache_size().expect("infallible"), Some(0));
assert_eq!(
ConcurrentCachedExt::get(&cache, &3u32).expect("infallible"),
None
);
let after_reset = cache.metrics();
assert_eq!(after_reset.hits, Some(0), "reset must zero the hit counter");
assert_eq!(
after_reset.misses,
Some(1),
"reset zeroed misses; only the one post-reset miss remains"
);
}
fn concurrent_fill<K, V, C>(cache: &C, key: K, val: V) -> Option<V>
where
K: std::hash::Hash + Eq + Clone,
V: Clone,
C: cached::ConcurrentCachedExt<K, V>,
C::Error: std::fmt::Debug,
{
cached::ConcurrentCachedExt::set(cache, key.clone(), val).expect("infallible");
cached::ConcurrentCachedExt::remove(cache, &key).expect("infallible")
}
#[test]
fn concurrent_cached_ext_generic_bound_works() {
use cached::ShardedUnboundCache;
let cache: ShardedUnboundCache<u32, u32> =
ShardedUnboundCache::builder().build().expect("build");
let removed = concurrent_fill(&cache, 7u32, 99u32);
assert_eq!(removed, Some(99));
}
#[test]
fn concurrent_cached_ext_get_or_set_with_works() {
use cached::{ConcurrentCachedExt, ShardedUnboundCache};
let cache: ShardedUnboundCache<u32, u32> =
ShardedUnboundCache::builder().build().expect("build");
let v: u32 = cache.get_or_set_with(10, || 99);
assert_eq!(v, 99);
let v2: u32 = cache.get_or_set_with(10, || panic!("factory must not run on hit"));
assert_eq!(v2, 99);
let v3 = ConcurrentCachedExt::get_or_set_with(&cache, 10, || 0).expect("infallible");
assert_eq!(v3, 99);
}
#[cfg(feature = "time_stores")]
#[test]
fn inherent_get_or_set_with_on_bounded_stores_returns_value() {
use cached::time::Duration;
use cached::{ShardedLruCache, ShardedLruTtlCache};
let lru: ShardedLruCache<u32, u32> = ShardedLruCache::builder()
.max_size(8)
.build()
.expect("build");
let v: u32 = lru.get_or_set_with(1, || 42);
assert_eq!(v, 42);
assert_eq!(
lru.get_or_set_with(1, || panic!("factory must not run on hit")),
42
);
let ttl: ShardedLruTtlCache<u32, u32> = ShardedLruTtlCache::builder()
.max_size(8)
.ttl(Duration::from_secs(60))
.build()
.expect("build");
let w: u32 = ttl.get_or_set_with(2, || 7);
assert_eq!(w, 7);
}
#[test]
fn short_aliases_reachable_via_prelude() {
use cached::prelude::*;
use cached::{ShardedUnboundCache, UnboundCache};
let mut cache: UnboundCache<u32, u32> = UnboundCache::builder().build().unwrap();
assert_eq!(cache.set(1, 10), None);
assert_eq!(cache.get(&1), Some(&10));
assert_eq!(cache.len(), 1);
assert_eq!(cache.hits(), Some(1));
assert_eq!(cache.misses(), Some(0));
let cc: ShardedUnboundCache<u32, u32> =
ShardedUnboundCache::builder().build().expect("build");
ConcurrentCachedExt::set(&cc, 42u32, 99u32).expect("infallible");
assert_eq!(
ConcurrentCachedExt::get(&cc, &42u32).expect("infallible"),
Some(99)
);
}
#[test]
fn into_values_reachable_via_prelude() {
use cached::LruCache;
use cached::prelude::*;
let mut c: LruCache<u32, u32> = LruCache::new(10);
c.set(1, 100);
c.set(2, 200);
assert_eq!(c.value_order().into_values(), vec![200, 100]);
assert_eq!(c.iter_order().into_values(), vec![200, 100]);
}
#[cfg(feature = "async")]
#[tokio::test]
async fn concurrent_peek_async_reachable_via_prelude_without_collision() {
use cached::ShardedUnboundCache;
use cached::prelude::*;
let c: ShardedUnboundCache<u32, u32> = ShardedUnboundCache::new();
c.set(1, 10);
assert_eq!(c.peek(&1), Some(10));
assert_eq!(c.async_cache_peek(&1).await.expect("infallible"), Some(10));
assert_eq!(c.async_peek(&1).await.expect("infallible"), Some(10));
assert_eq!(c.async_peek(&2).await.expect("infallible"), None);
}
}
mod cache_ttl_trait_available_ungated {
use cached::time::Duration;
use cached::{CacheRefreshOnHit, CacheTtl};
#[derive(Default)]
struct ExternalTtlStore {
ttl: Option<Duration>,
refresh: bool,
}
impl CacheTtl for ExternalTtlStore {
fn ttl(&self) -> Option<Duration> {
self.ttl
}
fn set_ttl(&mut self, ttl: Duration) -> Option<Duration> {
self.ttl.replace(ttl)
}
fn unset_ttl(&mut self) -> Option<Duration> {
self.ttl.take()
}
}
impl CacheRefreshOnHit for ExternalTtlStore {
fn refresh_on_hit(&self) -> bool {
self.refresh
}
fn set_refresh_on_hit(&mut self, refresh: bool) -> bool {
std::mem::replace(&mut self.refresh, refresh)
}
}
#[test]
fn external_store_implements_cache_ttl_without_time_stores() {
let mut s = ExternalTtlStore::default();
assert_eq!(s.ttl(), None);
assert_eq!(s.set_ttl(Duration::from_secs(5)), None);
assert_eq!(s.ttl(), Some(Duration::from_secs(5)));
assert_eq!(
s.try_set_ttl(Duration::ZERO),
Err(cached::SetTtlError::ZeroTtl)
);
assert_eq!(s.unset_ttl(), Some(Duration::from_secs(5)));
assert!(!s.set_refresh_on_hit(true));
assert!(s.refresh_on_hit());
}
}
mod hashmap_non_default_hasher {
use cached::Cached;
use std::collections::HashMap;
use std::collections::hash_map::DefaultHasher;
use std::hash::BuildHasher;
struct SeededBuildHasher(u64);
impl BuildHasher for SeededBuildHasher {
type Hasher = DefaultHasher;
fn build_hasher(&self) -> Self::Hasher {
use std::hash::Hasher;
let mut h = DefaultHasher::new();
h.write_u64(self.0);
h
}
}
#[test]
fn cached_hashmap_with_non_default_hasher() {
let mut map: HashMap<u32, u32, SeededBuildHasher> =
HashMap::with_hasher(SeededBuildHasher(0xABCD));
assert_eq!(Cached::cache_set(&mut map, 1, 10), None);
assert_eq!(Cached::cache_set(&mut map, 2, 20), None);
assert_eq!(Cached::cache_get(&mut map, &1), Some(&10));
assert_eq!(Cached::cache_size(&map), 2);
Cached::cache_reset(&mut map);
assert_eq!(Cached::cache_size(&map), 0);
assert_eq!(Cached::cache_set(&mut map, 3, 30), None);
assert_eq!(Cached::cache_get(&mut map, &3), Some(&30));
}
}
#[test]
fn concurrent_cache_get_or_set_with_is_non_atomic() {
use cached::{ConcurrentCached, ShardedUnboundCache};
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::{Arc, Barrier};
let cache: Arc<ShardedUnboundCache<u32, u32>> = Arc::new(
ShardedUnboundCache::builder()
.shards(1)
.build()
.expect("build 1-shard ShardedUnboundCache"),
);
let call_count = Arc::new(AtomicU32::new(0));
let barrier = Arc::new(Barrier::new(2));
let cache_a = cache.clone();
let count_a = call_count.clone();
let barrier_a = barrier.clone();
let handle_a = std::thread::spawn(move || {
ConcurrentCached::cache_get_or_set_with(&*cache_a, 0u32, || {
count_a.fetch_add(1, Ordering::Relaxed);
barrier_a.wait();
99u32
})
.expect("cache_get_or_set_with must not fail on ShardedUnboundCache")
});
let cache_b = cache.clone();
let count_b = call_count.clone();
let barrier_b = barrier.clone();
let handle_b = std::thread::spawn(move || {
ConcurrentCached::cache_get_or_set_with(&*cache_b, 0u32, || {
count_b.fetch_add(1, Ordering::Relaxed);
barrier_b.wait();
99u32
})
.expect("cache_get_or_set_with must not fail on ShardedUnboundCache")
});
let r_a = handle_a.join().expect("thread A panicked");
let r_b = handle_b.join().expect("thread B panicked");
assert_eq!(r_a, 99);
assert_eq!(r_b, 99);
assert_eq!(
call_count.load(Ordering::Relaxed),
2,
"both threads must have run the init closure (non-atomic get-then-set)"
);
}