#[cfg(feature = "time_stores")]
use cached::time::Duration;
use cached::{Cached, LruCache, UnboundCache};
use cached::{Expires, ExpiringLruCache};
#[cfg(feature = "proc_macro")]
use cached::{macros::cached, macros::once};
#[cfg(all(not(feature = "time_stores"), feature = "proc_macro"))]
use std::time::Duration;
#[test]
#[cfg(feature = "proc_macro")]
fn compile_fail_unsync_reads_basic() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/ui/unsync_reads_sized_cache.rs");
t.compile_fail("tests/ui/unsync_reads_mutex_lock.rs");
t.compile_fail("tests/ui/result_fallback_without_result.rs");
t.compile_fail("tests/ui/with_cached_flag_return_like.rs");
t.compile_fail("tests/ui/with_cached_flag_foreign_return.rs");
t.compile_fail("tests/ui/cached_with_cached_flag_unqualified_return.rs");
t.compile_fail("tests/ui/once_by_key_rejected.rs");
t.compile_fail("tests/ui/time_attr_renamed.rs");
t.compile_fail("tests/ui/time_refresh_attr_renamed.rs");
t.compile_fail("tests/ui/result_fallback_unbound_cache.rs");
t.compile_fail("tests/ui/concurrent_cached_time_refresh_attr_renamed.rs");
}
#[test]
#[cfg(all(feature = "proc_macro", feature = "time_stores"))]
fn compile_fail_unsync_reads_timed() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/ui/unsync_reads_timed_cache.rs");
}
#[test]
#[cfg(feature = "proc_macro")]
fn compile_fail_macro_arg_validation() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/ui/cached_self_method.rs");
t.compile_fail("tests/ui/cached_with_cached_flag_no_return.rs");
t.compile_fail("tests/ui/cached_key_without_convert.rs");
t.compile_fail("tests/ui/cached_convert_without_key.rs");
t.compile_fail("tests/ui/cached_ty_without_create.rs");
t.compile_fail("tests/ui/cached_create_without_ty.rs");
t.compile_fail("tests/ui/cached_store_types_exclusive.rs");
t.compile_fail("tests/ui/cached_sync_writes_buckets_zero.rs");
t.compile_fail("tests/ui/cached_result_fallback_sync_writes.rs");
t.compile_fail("tests/ui/cached_sync_lock_unknown.rs");
t.compile_fail("tests/ui/cached_expires_ttl_exclusive.rs");
t.compile_fail("tests/ui/cached_expires_type_exclusive.rs");
t.compile_fail("tests/ui/cached_expires_create_exclusive.rs");
t.compile_fail("tests/ui/cached_expires_with_cached_flag_exclusive.rs");
t.compile_fail("tests/ui/cached_expires_unsync_reads_exclusive.rs");
t.compile_fail("tests/ui/cached_expires_non_expires_type.rs");
t.compile_fail("tests/ui/cached_expires_refresh_exclusive.rs");
t.compile_fail("tests/ui/cached_expires_unbound_exclusive.rs");
t.compile_fail("tests/ui/cached_expires_cache_none_exclusive.rs");
t.compile_fail("tests/ui/cached_expires_cache_err_exclusive.rs");
t.compile_fail("tests/ui/cached_cache_err_requires_result_return.rs");
t.compile_fail("tests/ui/cached_cache_none_requires_option_return.rs");
t.compile_fail("tests/ui/cached_cache_err_result_fallback_exclusive.rs");
t.compile_fail("tests/ui/cached_cache_none_with_cached_flag_exclusive.rs");
t.compile_fail("tests/ui/cached_size_max_size_exclusive.rs");
t.compile_fail("tests/ui/cached_size_attr_deprecated.rs");
t.compile_fail("tests/ui/cached_ttl_zero.rs");
t.compile_fail("tests/ui/cached_max_size_zero.rs");
t.compile_fail("tests/ui/once_self_method.rs");
t.compile_fail("tests/ui/once_time_attr_renamed.rs");
t.compile_fail("tests/ui/once_with_cached_flag_foreign.rs");
t.compile_fail("tests/ui/once_expires_ttl_exclusive.rs");
t.compile_fail("tests/ui/once_expires_non_expires_type.rs");
t.compile_fail("tests/ui/once_expires_cache_none_exclusive.rs");
t.compile_fail("tests/ui/once_expires_cache_err_exclusive.rs");
t.compile_fail("tests/ui/once_expires_with_cached_flag_exclusive.rs");
t.compile_fail("tests/ui/once_sync_writes_buckets_zero.rs");
t.compile_fail("tests/ui/once_cache_err_requires_result_return.rs");
t.compile_fail("tests/ui/once_cache_none_requires_option_return.rs");
t.compile_fail("tests/ui/once_cache_none_with_cached_flag_exclusive.rs");
t.compile_fail("tests/ui/once_ttl_zero.rs");
t.compile_fail("tests/ui/concurrent_cached_self_method.rs");
t.compile_fail("tests/ui/concurrent_cached_time_attr_renamed.rs");
t.compile_fail("tests/ui/concurrent_cached_with_cached_flag_foreign.rs");
t.compile_fail("tests/ui/concurrent_cached_no_return.rs");
t.compile_fail("tests/ui/concurrent_cached_complex_return.rs");
t.compile_fail("tests/ui/concurrent_cached_non_result_return.rs");
t.compile_fail("tests/ui/concurrent_cached_redis_create_conflict.rs");
t.compile_fail("tests/ui/concurrent_cached_redis_disk_exclusive.rs");
t.compile_fail("tests/ui/concurrent_cached_async_redis_no_ttl.rs");
t.compile_fail("tests/ui/concurrent_cached_redis_no_ttl.rs");
t.compile_fail("tests/ui/concurrent_cached_disk_create_conflict.rs");
t.compile_fail("tests/ui/concurrent_cached_max_size_create_conflict.rs");
t.compile_fail("tests/ui/concurrent_cached_disk_create_ignored_attrs.rs");
t.compile_fail("tests/ui/concurrent_cached_option_return.rs");
t.compile_fail("tests/ui/concurrent_cached_result_attr_unsupported.rs");
t.compile_fail("tests/ui/concurrent_cached_option_attr_unsupported.rs");
t.compile_fail("tests/ui/concurrent_cached_sync_writes_attr_unsupported.rs");
t.compile_fail("tests/ui/concurrent_cached_custom_create_required.rs");
t.compile_fail("tests/ui/concurrent_cached_shards_zero.rs");
t.compile_fail("tests/ui/concurrent_cached_size_zero.rs");
t.compile_fail("tests/ui/concurrent_cached_max_size_zero.rs");
t.compile_fail("tests/ui/concurrent_cached_size_max_size_exclusive.rs");
t.compile_fail("tests/ui/concurrent_cached_size_attr_deprecated.rs");
t.compile_fail("tests/ui/concurrent_cached_ttl_zero.rs");
t.compile_fail("tests/ui/concurrent_cached_shards_with_redis.rs");
t.compile_fail("tests/ui/concurrent_cached_shards_with_disk.rs");
t.compile_fail("tests/ui/concurrent_cached_size_with_redis.rs");
t.compile_fail("tests/ui/concurrent_cached_size_with_disk.rs");
t.compile_fail("tests/ui/concurrent_cached_size_with_redis_ty.rs");
t.compile_fail("tests/ui/concurrent_cached_size_with_disk_ty.rs");
t.compile_fail("tests/ui/concurrent_cached_key_without_convert.rs");
t.compile_fail("tests/ui/concurrent_cached_refresh_without_ttl.rs");
t.compile_fail("tests/ui/concurrent_cached_expires_ttl_exclusive.rs");
t.compile_fail("tests/ui/concurrent_cached_expires_redis_exclusive.rs");
t.compile_fail("tests/ui/concurrent_cached_expires_disk_exclusive.rs");
t.compile_fail("tests/ui/concurrent_cached_expires_ty_exclusive.rs");
t.compile_fail("tests/ui/concurrent_cached_expires_create_exclusive.rs");
t.compile_fail("tests/ui/concurrent_cached_expires_refresh_exclusive.rs");
t.compile_fail("tests/ui/concurrent_cached_expires_cache_none_exclusive.rs");
t.compile_fail("tests/ui/concurrent_cached_expires_cache_err_exclusive.rs");
t.compile_fail("tests/ui/concurrent_cached_result_fallback_expires_exclusive.rs");
t.compile_fail("tests/ui/concurrent_cached_result_fallback_redis_exclusive.rs");
t.compile_fail("tests/ui/concurrent_cached_result_fallback_disk_exclusive.rs");
t.compile_fail("tests/ui/concurrent_cached_result_fallback_requires_ttl.rs");
t.compile_fail("tests/ui/concurrent_cached_with_cached_flag_option.rs");
t.compile_fail("tests/ui/concurrent_cached_option_with_redis.rs");
t.compile_fail("tests/ui/concurrent_cached_cache_none_with_redis.rs");
t.compile_fail("tests/ui/concurrent_cached_cache_err_result_fallback_exclusive.rs");
t.compile_fail("tests/ui/concurrent_cached_result_fallback_with_cached_flag_exclusive.rs");
t.compile_fail("tests/ui/cached_result_attr_removed.rs");
t.compile_fail("tests/ui/cached_option_attr_removed.rs");
t.compile_fail("tests/ui/once_result_attr_removed.rs");
t.compile_fail("tests/ui/once_option_attr_removed.rs");
t.compile_fail("tests/ui/concurrent_cached_option_attr_removed.rs");
t.compile_fail("tests/ui/concurrent_cached_map_error_on_infallible.rs");
}
#[cfg(feature = "proc_macro")]
use serial_test::serial;
#[cfg(any(feature = "proc_macro", feature = "time_stores"))]
use std::thread::sleep;
#[cfg(feature = "proc_macro")]
struct NoClone {}
#[cfg(feature = "proc_macro")]
#[cached(unsync_reads = true)]
fn unsync_double(n: u32) -> u32 {
n * 2
}
#[cfg(feature = "proc_macro")]
#[cached(unsync_reads = true, sync_writes = "default")]
fn unsync_double_sync_writes(n: u32) -> u32 {
n * 2
}
#[cfg(feature = "proc_macro")]
#[cached]
fn proc_cached_result(n: u32) -> Result<Vec<u32>, NoClone> {
if n < 5 { Ok(vec![n]) } else { Err(NoClone {}) }
}
#[cfg(feature = "proc_macro")]
#[test]
fn test_proc_cached_result() {
assert!(proc_cached_result(2).is_ok());
assert!(proc_cached_result(4).is_ok());
assert!(proc_cached_result(6).is_err());
assert!(proc_cached_result(6).is_err());
assert!(proc_cached_result(2).is_ok());
assert!(proc_cached_result(4).is_ok());
{
let cache = PROC_CACHED_RESULT.read();
assert_eq!(2, cache.cache_size());
assert_eq!(2, cache.cache_hits().unwrap());
assert_eq!(4, cache.cache_misses().unwrap());
}
}
#[cfg(feature = "proc_macro")]
#[cached]
fn proc_cached_option(n: u32) -> Option<Vec<u32>> {
if n < 5 { Some(vec![n]) } else { None }
}
#[cfg(feature = "proc_macro")]
#[test]
fn test_proc_cached_option() {
assert!(proc_cached_option(2).is_some());
assert!(proc_cached_option(4).is_some());
assert!(proc_cached_option(1).is_some());
assert!(proc_cached_option(6).is_none());
assert!(proc_cached_option(6).is_none());
assert!(proc_cached_option(2).is_some());
assert!(proc_cached_option(1).is_some());
assert!(proc_cached_option(4).is_some());
{
let cache = PROC_CACHED_OPTION.read();
assert_eq!(3, cache.cache_size());
assert_eq!(3, cache.cache_hits().unwrap());
assert_eq!(5, cache.cache_misses().unwrap());
}
}
#[cfg(feature = "proc_macro")]
static CACHED_CACHE_ERR_TRUE_CALLS: std::sync::atomic::AtomicUsize =
std::sync::atomic::AtomicUsize::new(0);
#[cfg(feature = "proc_macro")]
#[cached(cache_err = true)]
fn cached_cache_err_true(n: u32) -> Result<u32, u32> {
CACHED_CACHE_ERR_TRUE_CALLS.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
Err(n)
}
#[cfg(feature = "proc_macro")]
#[test]
fn test_cached_cache_err_true_caches_err() {
let before = CACHED_CACHE_ERR_TRUE_CALLS.load(std::sync::atomic::Ordering::SeqCst);
assert_eq!(cached_cache_err_true(7), Err(7));
assert_eq!(cached_cache_err_true(7), Err(7));
assert_eq!(
CACHED_CACHE_ERR_TRUE_CALLS.load(std::sync::atomic::Ordering::SeqCst),
before + 1
);
}
#[cfg(feature = "proc_macro")]
static CACHED_CACHE_NONE_TRUE_CALLS: std::sync::atomic::AtomicUsize =
std::sync::atomic::AtomicUsize::new(0);
#[cfg(feature = "proc_macro")]
#[cached(cache_none = true)]
fn cached_cache_none_true(n: u32) -> Option<u32> {
CACHED_CACHE_NONE_TRUE_CALLS.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
if n == 0 { None } else { Some(n) }
}
#[cfg(feature = "proc_macro")]
#[test]
fn test_cached_cache_none_true_caches_none() {
let before = CACHED_CACHE_NONE_TRUE_CALLS.load(std::sync::atomic::Ordering::SeqCst);
assert_eq!(cached_cache_none_true(0), None);
assert_eq!(cached_cache_none_true(0), None);
assert_eq!(
CACHED_CACHE_NONE_TRUE_CALLS.load(std::sync::atomic::Ordering::SeqCst),
before + 1
);
}
#[cfg(feature = "proc_macro")]
static CONCURRENT_CACHED_CACHE_ERR_TRUE_CALLS: std::sync::atomic::AtomicUsize =
std::sync::atomic::AtomicUsize::new(0);
#[cfg(feature = "proc_macro")]
#[cached::macros::concurrent_cached(cache_err = true)]
fn concurrent_cached_cache_err_true(n: u32) -> Result<u32, u32> {
CONCURRENT_CACHED_CACHE_ERR_TRUE_CALLS.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
Err(n)
}
#[cfg(feature = "proc_macro")]
#[test]
fn test_concurrent_cached_cache_err_true_caches_err() {
let before = CONCURRENT_CACHED_CACHE_ERR_TRUE_CALLS.load(std::sync::atomic::Ordering::SeqCst);
assert_eq!(concurrent_cached_cache_err_true(7), Err(7));
assert_eq!(concurrent_cached_cache_err_true(7), Err(7));
assert_eq!(
CONCURRENT_CACHED_CACHE_ERR_TRUE_CALLS.load(std::sync::atomic::Ordering::SeqCst),
before + 1
);
}
#[cfg(feature = "proc_macro")]
static CONCURRENT_CACHED_CACHE_NONE_TRUE_CALLS: std::sync::atomic::AtomicUsize =
std::sync::atomic::AtomicUsize::new(0);
#[cfg(feature = "proc_macro")]
#[cached::macros::concurrent_cached(cache_none = true)]
fn concurrent_cached_cache_none_true(n: u32) -> Option<u32> {
CONCURRENT_CACHED_CACHE_NONE_TRUE_CALLS.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
if n == 0 { None } else { Some(n) }
}
#[cfg(feature = "proc_macro")]
#[test]
fn test_concurrent_cached_cache_none_true_caches_none() {
let before = CONCURRENT_CACHED_CACHE_NONE_TRUE_CALLS.load(std::sync::atomic::Ordering::SeqCst);
assert_eq!(concurrent_cached_cache_none_true(0), None);
assert_eq!(concurrent_cached_cache_none_true(0), None);
assert_eq!(
CONCURRENT_CACHED_CACHE_NONE_TRUE_CALLS.load(std::sync::atomic::Ordering::SeqCst),
before + 1
);
}
#[cfg(feature = "proc_macro")]
#[cached(with_cached_flag = true)]
fn cached_return_flag(n: i32) -> cached::Return<i32> {
cached::Return::new(n)
}
#[cfg(feature = "proc_macro")]
#[test]
fn test_cached_return_flag() {
let r = cached_return_flag(1);
assert!(!r.was_cached);
assert_eq!(*r, 1);
let r = cached_return_flag(1);
assert!(r.was_cached);
assert_eq!(*r, 1);
assert!(r.is_positive());
{
let cache = CACHED_RETURN_FLAG.read();
assert_eq!(cache.cache_hits(), Some(1));
assert_eq!(cache.cache_misses(), Some(1));
}
}
#[cfg(feature = "proc_macro")]
#[cached(with_cached_flag = true)]
fn cached_return_flag_result(n: i32) -> Result<cached::Return<i32>, ()> {
if n == 10 {
return Err(());
}
Ok(cached::Return::new(n))
}
#[cfg(feature = "proc_macro")]
#[test]
fn test_cached_return_flag_result() {
let r = cached_return_flag_result(1).unwrap();
assert!(!r.was_cached);
assert_eq!(*r, 1);
let r = cached_return_flag_result(1).unwrap();
assert!(r.was_cached);
assert_eq!(*r, 1);
assert!(r.is_positive());
let r = cached_return_flag_result(10);
assert!(r.is_err());
{
let cache = CACHED_RETURN_FLAG_RESULT.read();
assert_eq!(cache.cache_hits(), Some(1));
assert_eq!(cache.cache_misses(), Some(2));
}
}
#[cfg(feature = "proc_macro")]
#[cached(with_cached_flag = true)]
fn cached_return_flag_option(n: i32) -> Option<cached::Return<i32>> {
if n == 10 {
return None;
}
Some(cached::Return::new(n))
}
#[cfg(feature = "proc_macro")]
#[test]
fn test_cached_return_flag_option() {
let r = cached_return_flag_option(1).unwrap();
assert!(!r.was_cached);
assert_eq!(*r, 1);
let r = cached_return_flag_option(1).unwrap();
assert!(r.was_cached);
assert_eq!(*r, 1);
assert!(r.is_positive());
let r = cached_return_flag_option(10);
assert!(r.is_none());
{
let cache = CACHED_RETURN_FLAG_OPTION.read();
assert_eq!(cache.cache_hits(), Some(1));
assert_eq!(cache.cache_misses(), Some(2));
}
}
#[cfg(feature = "proc_macro")]
#[once]
fn only_cached_result_once(s: String, error: bool) -> std::result::Result<Vec<String>, u32> {
if error { Err(1) } else { Ok(vec![s]) }
}
#[cfg(feature = "proc_macro")]
#[test]
fn test_only_cached_result_once() {
assert!(only_cached_result_once("z".to_string(), true).is_err());
let a = only_cached_result_once("a".to_string(), false).unwrap();
let b = only_cached_result_once("b".to_string(), false).unwrap();
assert_eq!(a, b);
sleep(Duration::new(1, 0));
let b = only_cached_result_once("b".to_string(), false).unwrap();
assert_eq!(a, b);
}
#[cfg(feature = "proc_macro")]
static ONCE_CACHE_ERR_TRUE_CALLS: std::sync::atomic::AtomicUsize =
std::sync::atomic::AtomicUsize::new(0);
#[cfg(feature = "proc_macro")]
#[once(cache_err = true)]
fn once_cache_err_true(code: u32) -> Result<u32, u32> {
ONCE_CACHE_ERR_TRUE_CALLS.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
Err(code)
}
#[cfg(feature = "proc_macro")]
#[test]
fn test_once_cache_err_true_caches_err() {
let before = ONCE_CACHE_ERR_TRUE_CALLS.load(std::sync::atomic::Ordering::SeqCst);
assert_eq!(once_cache_err_true(7), Err(7));
assert_eq!(once_cache_err_true(9), Err(7));
assert_eq!(
ONCE_CACHE_ERR_TRUE_CALLS.load(std::sync::atomic::Ordering::SeqCst),
before + 1
);
}
#[cfg(feature = "proc_macro")]
#[once]
fn only_cached_option_once(s: String, none: bool) -> Option<Vec<String>> {
if none { None } else { Some(vec![s]) }
}
#[cfg(feature = "proc_macro")]
#[test]
fn test_only_cached_option_once() {
assert!(only_cached_option_once("z".to_string(), true).is_none());
let a = only_cached_option_once("a".to_string(), false).unwrap();
let b = only_cached_option_once("b".to_string(), false).unwrap();
assert_eq!(a, b);
sleep(Duration::new(1, 0));
let b = only_cached_option_once("b".to_string(), false).unwrap();
assert_eq!(a, b);
}
#[cfg(feature = "proc_macro")]
static ONCE_CACHE_NONE_TRUE_CALLS: std::sync::atomic::AtomicUsize =
std::sync::atomic::AtomicUsize::new(0);
#[cfg(feature = "proc_macro")]
#[once(cache_none = true)]
fn once_cache_none_true(n: u32) -> Option<u32> {
ONCE_CACHE_NONE_TRUE_CALLS.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
if n == 0 { None } else { Some(n) }
}
#[cfg(feature = "proc_macro")]
#[test]
fn test_once_cache_none_true_caches_none() {
let before = ONCE_CACHE_NONE_TRUE_CALLS.load(std::sync::atomic::Ordering::SeqCst);
assert_eq!(once_cache_none_true(0), None);
assert_eq!(once_cache_none_true(1), None);
assert_eq!(
ONCE_CACHE_NONE_TRUE_CALLS.load(std::sync::atomic::Ordering::SeqCst),
before + 1
);
}
#[cfg(feature = "proc_macro")]
#[cached(max_size = 2)]
fn cached_smartstring(s: smartstring::alias::String) -> smartstring::alias::String {
if s == "very stringy" {
smartstring::alias::String::from("equal")
} else {
smartstring::alias::String::from("not equal")
}
}
#[cfg(feature = "proc_macro")]
#[test]
fn test_cached_smartstring() {
let mut string = smartstring::alias::String::new();
string.push_str("very stringy");
assert_eq!("equal", cached_smartstring(string.clone()));
{
let cache = CACHED_SMARTSTRING.read();
assert_eq!(cache.cache_hits(), Some(0));
assert_eq!(cache.cache_misses(), Some(1));
}
assert_eq!("equal", cached_smartstring(string.clone()));
{
let cache = CACHED_SMARTSTRING.read();
assert_eq!(cache.cache_hits(), Some(1));
assert_eq!(cache.cache_misses(), Some(1));
}
let string = smartstring::alias::String::from("also stringy");
assert_eq!("not equal", cached_smartstring(string));
{
let cache = CACHED_SMARTSTRING.read();
assert_eq!(cache.cache_hits(), Some(1));
assert_eq!(cache.cache_misses(), Some(2));
}
}
#[cfg(feature = "proc_macro")]
#[cached(
max_size = 2,
key = "smartstring::alias::String",
convert = r#"{ smartstring::alias::String::from(s) }"#
)]
fn cached_smartstring_from_str(s: &str) -> bool {
s == "true"
}
#[cfg(feature = "proc_macro")]
#[cached(max_size = 2)]
fn cached_max_size_alias(n: u32) -> u32 {
n * 2
}
#[cfg(feature = "proc_macro")]
#[test]
fn test_cached_max_size_alias_sets_bound() {
assert_eq!(cached_max_size_alias(1), 2);
assert_eq!(cached_max_size_alias(2), 4);
assert_eq!(cached_max_size_alias(3), 6); let cache = CACHED_MAX_SIZE_ALIAS.read();
assert_eq!(cache.capacity(), 2);
assert_eq!(cache.cache_size(), 2);
}
#[cfg(feature = "proc_macro")]
#[allow(deprecated)]
mod deprecated_size_alias {
use super::*;
use cached::macros::concurrent_cached;
use std::sync::atomic::{AtomicU64, Ordering};
#[cached(size = 2)]
fn cached_size_alias(n: u32) -> u32 {
n * 2
}
#[test]
fn deprecated_size_attr_still_sets_bound() {
assert_eq!(cached_size_alias(1), 2);
assert_eq!(cached_size_alias(2), 4);
assert_eq!(cached_size_alias(3), 6); let cache = CACHED_SIZE_ALIAS.read();
assert_eq!(cache.capacity(), 2);
assert_eq!(cache.cache_size(), 2);
}
static SIZE_ALIAS_CALLS: AtomicU64 = AtomicU64::new(0);
#[concurrent_cached(size = 100)]
fn concurrent_size_alias(x: u64) -> u64 {
SIZE_ALIAS_CALLS.fetch_add(1, Ordering::Relaxed);
x * 2
}
#[test]
fn deprecated_size_attr_routes_to_sharded_lru() {
SIZE_ALIAS_CALLS.store(0, Ordering::Relaxed);
assert_eq!(concurrent_size_alias(10), 20);
assert_eq!(concurrent_size_alias(10), 20); assert_eq!(concurrent_size_alias(11), 22); assert_eq!(SIZE_ALIAS_CALLS.load(Ordering::Relaxed), 2);
}
}
#[test]
fn sync_cached_remove_entry_and_delete_aliases() {
let mut cache: UnboundCache<String, u32> = UnboundCache::builder().build().unwrap();
cache.cache_set("a".to_string(), 1);
cache.cache_set("b".to_string(), 2);
assert_eq!(cache.remove_entry("a"), Some(("a".to_string(), 1)));
assert_eq!(cache.remove_entry("a"), None);
assert!(cache.delete("b"));
assert!(!cache.delete("b"));
assert_eq!(cache.cache_size(), 0);
}
#[cfg(feature = "proc_macro")]
#[test]
fn test_cached_smartstring_from_str() {
assert!(cached_smartstring_from_str("true"));
{
let cache = CACHED_SMARTSTRING_FROM_STR.read();
assert_eq!(cache.cache_hits(), Some(0));
assert_eq!(cache.cache_misses(), Some(1));
}
assert!(cached_smartstring_from_str("true"));
{
let cache = CACHED_SMARTSTRING_FROM_STR.read();
assert_eq!(cache.cache_hits(), Some(1));
assert_eq!(cache.cache_misses(), Some(1));
}
assert!(!cached_smartstring_from_str("false"));
{
let cache = CACHED_SMARTSTRING_FROM_STR.read();
assert_eq!(cache.cache_hits(), Some(1));
assert_eq!(cache.cache_misses(), Some(2));
}
}
#[cfg(feature = "proc_macro")]
#[once]
fn once_for_priming() -> bool {
true
}
#[cfg(feature = "proc_macro")]
#[test]
fn test_once_for_priming() {
assert!(once_for_priming_prime_cache());
{
let cache = ONCE_FOR_PRIMING.read();
assert!(cache.is_some());
}
}
#[cfg(feature = "proc_macro")]
#[cached]
fn mutable_args(mut a: i32, mut b: i32) -> (i32, i32) {
a += 1;
b += 1;
(a, b)
}
#[cfg(feature = "proc_macro")]
#[test]
fn test_mutable_args() {
assert_eq!((2, 2), mutable_args(1, 1));
assert_eq!((2, 2), mutable_args(1, 1));
}
#[cfg(feature = "proc_macro")]
#[cached]
fn mutable_args_str(mut a: String) -> String {
a.push_str("-ok");
a
}
#[cfg(feature = "proc_macro")]
#[test]
fn test_mutable_args_str() {
assert_eq!("a-ok", mutable_args_str(String::from("a")));
assert_eq!("a-ok", mutable_args_str(String::from("a")));
}
#[cfg(feature = "proc_macro")]
#[once]
fn mutable_args_once(mut a: i32, mut b: i32) -> (i32, i32) {
a += 1;
b += 1;
(a, b)
}
#[cfg(feature = "proc_macro")]
#[test]
fn test_mutable_args_once() {
assert_eq!((2, 2), mutable_args_once(1, 1));
assert_eq!((2, 2), mutable_args_once(1, 1));
assert_eq!((2, 2), mutable_args_once(5, 6));
}
#[cfg(feature = "proc_macro")]
mod expires_macro_tests {
use cached::macros::{cached, once};
use cached::{Cached, Expires};
#[derive(Clone, Debug)]
struct Val {
v: u32,
expired: bool,
}
impl Expires for Val {
fn is_expired(&self) -> bool {
self.expired
}
}
#[cached(expires = true, key = "u32", convert = "{ k }")]
fn sm_cached_expires(k: u32, expired: bool) -> Val {
Val { v: k, expired }
}
#[cached(expires = true, max_size = 4, key = "u32", convert = "{ k }")]
fn sm_cached_expires_lru(k: u32, expired: bool) -> Val {
Val { v: k, expired }
}
#[once(expires = true)]
fn sm_once_expires(expired: bool) -> Val {
Val { v: 42, expired }
}
#[test]
fn test_expires_macro_hit_and_miss() {
{
let mut c = SM_CACHED_EXPIRES.write();
c.cache_clear();
c.cache_reset_metrics();
}
let v1 = sm_cached_expires(1, false);
assert_eq!(v1.v, 1);
assert!(!v1.expired);
let v2 = sm_cached_expires(1, false);
assert!(!v2.expired);
{
let c = SM_CACHED_EXPIRES.read();
assert_eq!(c.cache_hits(), Some(1));
assert_eq!(c.cache_misses(), Some(1));
}
let v3 = sm_cached_expires(2, true);
assert!(v3.expired);
let v4 = sm_cached_expires(2, false);
assert!(!v4.expired);
{
let c = SM_CACHED_EXPIRES.read();
assert_eq!(c.cache_evictions(), Some(1));
}
}
#[test]
fn test_expires_lru_macro_hit_and_miss() {
{
let mut c = SM_CACHED_EXPIRES_LRU.write();
c.cache_clear();
c.cache_reset_metrics();
}
let v1 = sm_cached_expires_lru(10, false);
assert_eq!(v1.v, 10);
let v2 = sm_cached_expires_lru(10, false);
assert!(!v2.expired);
{
let c = SM_CACHED_EXPIRES_LRU.read();
assert_eq!(c.cache_hits(), Some(1));
assert_eq!(c.cache_misses(), Some(1));
}
let v3 = sm_cached_expires_lru(11, true);
assert!(v3.expired);
let v4 = sm_cached_expires_lru(11, false);
assert!(!v4.expired);
{
let c = SM_CACHED_EXPIRES_LRU.read();
assert_eq!(c.cache_evictions(), Some(1));
}
}
#[test]
fn test_once_expires_macro() {
let v1 = sm_once_expires(false);
assert!(!v1.expired);
let v2 = sm_once_expires(true);
assert!(!v2.expired);
{
let mut cache = SM_ONCE_EXPIRES.write();
*cache = None;
}
let v3 = sm_once_expires(true);
assert!(v3.expired);
let v4 = sm_once_expires(false);
assert!(!v4.expired);
}
}
#[cfg(all(feature = "time_stores", feature = "proc_macro"))]
mod time_store_tests {
use super::*;
use cached::stores::TtlSortedCache;
use cached::time::Instant;
use cached::{CachedPeek, CachedRead};
#[cached(
ty = "TtlSortedCache<String, usize>",
create = "{ TtlSortedCache::builder().ttl(Duration::from_secs(60)).build().unwrap() }",
key = "String",
convert = r#"{ input.to_string() }"#,
unsync_reads = true
)]
fn expiring_sized_unsync_read(input: &str) -> usize {
input.len()
}
#[once(ttl = 1)]
fn slow_once_timestamp_after_body(input: u32) -> u32 {
sleep(Duration::from_millis(1100));
input
}
#[test]
fn test_expiring_sized_unsync_read_macro() {
assert_eq!(3, expiring_sized_unsync_read("abc"));
assert_eq!(3, expiring_sized_unsync_read("abc"));
let cache = EXPIRING_SIZED_UNSYNC_READ.read();
assert_eq!(Some(&3), cache.cache_peek("abc"));
assert_eq!(Some(&3), cache.cache_get_read("abc"));
}
#[test]
#[serial]
fn test_once_ttl_starts_after_body_finishes() {
assert_eq!(1, slow_once_timestamp_after_body(1));
assert_eq!(1, slow_once_timestamp_after_body(2));
}
#[cached(max_size = 1, ttl = 1)]
fn proc_timed_sized_sleeper(n: u64) -> u64 {
sleep(Duration::new(1, 0));
n
}
#[test]
fn test_proc_timed_sized_cache() {
proc_timed_sized_sleeper(1);
proc_timed_sized_sleeper(1);
{
let cache = PROC_TIMED_SIZED_SLEEPER.read();
assert_eq!(1, cache.cache_misses().unwrap());
assert_eq!(1, cache.cache_hits().unwrap());
}
sleep(Duration::new(1, 0));
proc_timed_sized_sleeper(1);
{
let cache = PROC_TIMED_SIZED_SLEEPER.read();
assert_eq!(2, cache.cache_misses().unwrap());
assert_eq!(1, cache.cache_hits().unwrap());
assert_eq!(cache.key_order(), vec![1]);
}
sleep(Duration::new(1, 0));
{
let cache = PROC_TIMED_SIZED_SLEEPER.read();
assert!(cache.key_order().is_empty());
}
proc_timed_sized_sleeper(1);
proc_timed_sized_sleeper(1);
{
let cache = PROC_TIMED_SIZED_SLEEPER.read();
assert_eq!(3, cache.cache_misses().unwrap());
assert_eq!(2, cache.cache_hits().unwrap());
assert_eq!(cache.key_order(), vec![1]);
}
proc_timed_sized_sleeper(2);
{
let cache = PROC_TIMED_SIZED_SLEEPER.read();
assert_eq!(4, cache.cache_misses().unwrap());
assert_eq!(2, cache.cache_hits().unwrap());
assert_eq!(cache.key_order(), vec![2]);
}
}
#[once(ttl = 1)]
fn only_cached_once_per_second(s: String) -> Vec<String> {
vec![s]
}
#[test]
fn test_only_cached_once_per_second() {
let a = only_cached_once_per_second("a".to_string());
let b = only_cached_once_per_second("b".to_string());
assert_eq!(a, b);
sleep(Duration::new(1, 0));
let b = only_cached_once_per_second("b".to_string());
assert_eq!(vec!["b".to_string()], b);
}
#[once(ttl = 1)]
fn only_cached_result_once_per_second(
s: String,
error: bool,
) -> std::result::Result<Vec<String>, u32> {
if error { Err(1) } else { Ok(vec![s]) }
}
#[test]
fn test_only_cached_result_once_per_second() {
assert!(only_cached_result_once_per_second("z".to_string(), true).is_err());
let a = only_cached_result_once_per_second("a".to_string(), false).unwrap();
let b = only_cached_result_once_per_second("b".to_string(), false).unwrap();
assert_eq!(a, b);
sleep(Duration::new(1, 0));
let b = only_cached_result_once_per_second("b".to_string(), false).unwrap();
assert_eq!(vec!["b".to_string()], b);
}
#[once(ttl = 1)]
fn only_cached_option_once_per_second(s: String, none: bool) -> Option<Vec<String>> {
if none { None } else { Some(vec![s]) }
}
#[test]
fn test_only_cached_option_once_per_second() {
assert!(only_cached_option_once_per_second("z".to_string(), true).is_none());
let a = only_cached_option_once_per_second("a".to_string(), false).unwrap();
let b = only_cached_option_once_per_second("b".to_string(), false).unwrap();
assert_eq!(a, b);
sleep(Duration::new(1, 0));
let b = only_cached_option_once_per_second("b".to_string(), false).unwrap();
assert_eq!(vec!["b".to_string()], b);
}
#[cached(ttl = 2, sync_writes = "default", key = "u32", convert = "{ 1 }")]
fn cached_sync_writes(s: String) -> Vec<String> {
vec![s]
}
#[test]
fn test_cached_sync_writes() {
let a = std::thread::spawn(|| cached_sync_writes("a".to_string()));
sleep(Duration::new(1, 0));
let b = std::thread::spawn(|| cached_sync_writes("b".to_string()));
let c = std::thread::spawn(|| cached_sync_writes("c".to_string()));
let a = a.join().unwrap();
let b = b.join().unwrap();
let c = c.join().unwrap();
assert_eq!(a, b);
assert_eq!(a, c);
}
#[cached(ttl = 2, sync_writes = true, key = "u32", convert = "{ 2 }")]
fn cached_sync_writes_true(s: String) -> Vec<String> {
vec![s]
}
#[test]
fn test_cached_sync_writes_true() {
let a = cached_sync_writes_true("a".to_string());
let b = cached_sync_writes_true("b".to_string());
assert_eq!(a, b);
}
#[cached(ttl = 2, sync_writes = false, key = "u32", convert = "{ 3 }")]
fn cached_sync_writes_false(s: String) -> Vec<String> {
vec![s]
}
#[test]
fn test_cached_sync_writes_false() {
let a = cached_sync_writes_false("a".to_string());
let b = cached_sync_writes_false("b".to_string());
assert_eq!(a, b);
}
#[cached(
ttl = 2,
sync_writes = "by_key",
sync_writes_buckets = 8,
key = "u32",
convert = "{ 1 }"
)]
fn cached_sync_writes_by_key(s: String) -> Vec<String> {
sleep(Duration::new(1, 0));
vec![s]
}
#[test]
fn test_cached_sync_writes_by_key() {
let a = std::thread::spawn(|| cached_sync_writes_by_key("a".to_string()));
let b = std::thread::spawn(|| cached_sync_writes_by_key("b".to_string()));
let c = std::thread::spawn(|| cached_sync_writes_by_key("c".to_string()));
let start = Instant::now();
let _ = a.join().unwrap();
let _ = b.join().unwrap();
let _ = c.join().unwrap();
assert!(start.elapsed() < Duration::from_secs(2));
}
#[cached(
ttl = 1,
refresh = true,
key = "String",
convert = r#"{ String::from(s) }"#
)]
fn cached_timed_refresh(s: &str) -> bool {
s == "true"
}
#[test]
fn test_cached_timed_refresh() {
assert!(cached_timed_refresh("true"));
{
let cache = CACHED_TIMED_REFRESH.read();
assert_eq!(cache.cache_hits(), Some(0));
assert_eq!(cache.cache_misses(), Some(1));
}
assert!(cached_timed_refresh("true"));
{
let cache = CACHED_TIMED_REFRESH.read();
assert_eq!(cache.cache_hits(), Some(1));
assert_eq!(cache.cache_misses(), Some(1));
}
std::thread::sleep(Duration::from_millis(500));
assert!(cached_timed_refresh("true"));
std::thread::sleep(Duration::from_millis(500));
assert!(cached_timed_refresh("true"));
std::thread::sleep(Duration::from_millis(500));
assert!(cached_timed_refresh("true"));
{
let cache = CACHED_TIMED_REFRESH.read();
assert_eq!(cache.cache_hits(), Some(4));
assert_eq!(cache.cache_misses(), Some(1));
}
}
#[cached(
max_size = 2,
ttl = 1,
refresh = true,
key = "String",
convert = r#"{ String::from(s) }"#
)]
fn cached_timed_sized_refresh(s: &str) -> bool {
s == "true"
}
#[test]
fn test_cached_timed_sized_refresh() {
assert!(cached_timed_sized_refresh("true"));
{
let cache = CACHED_TIMED_SIZED_REFRESH.read();
assert_eq!(cache.cache_hits(), Some(0));
assert_eq!(cache.cache_misses(), Some(1));
}
assert!(cached_timed_sized_refresh("true"));
{
let cache = CACHED_TIMED_SIZED_REFRESH.read();
assert_eq!(cache.cache_hits(), Some(1));
assert_eq!(cache.cache_misses(), Some(1));
}
std::thread::sleep(Duration::from_millis(500));
assert!(cached_timed_sized_refresh("true"));
std::thread::sleep(Duration::from_millis(500));
assert!(cached_timed_sized_refresh("true"));
std::thread::sleep(Duration::from_millis(500));
assert!(cached_timed_sized_refresh("true"));
{
let cache = CACHED_TIMED_SIZED_REFRESH.read();
assert_eq!(cache.cache_hits(), Some(4));
assert_eq!(cache.cache_misses(), Some(1));
}
}
#[cached(
max_size = 2,
ttl = 1,
refresh = true,
key = "String",
convert = r#"{ String::from(s) }"#
)]
fn cached_timed_sized_refresh_prime(s: &str) -> bool {
s == "true"
}
#[test]
fn test_cached_timed_sized_refresh_prime() {
assert!(cached_timed_sized_refresh_prime("true"));
{
let cache = CACHED_TIMED_SIZED_REFRESH_PRIME.read();
assert_eq!(cache.cache_hits(), Some(0));
assert_eq!(cache.cache_misses(), Some(1));
}
assert!(cached_timed_sized_refresh_prime("true"));
{
let cache = CACHED_TIMED_SIZED_REFRESH_PRIME.read();
assert_eq!(cache.cache_hits(), Some(1));
assert_eq!(cache.cache_misses(), Some(1));
}
std::thread::sleep(Duration::from_millis(500));
assert!(cached_timed_sized_refresh_prime_prime_cache("true"));
std::thread::sleep(Duration::from_millis(500));
assert!(cached_timed_sized_refresh_prime_prime_cache("true"));
std::thread::sleep(Duration::from_millis(500));
assert!(cached_timed_sized_refresh_prime_prime_cache("true"));
assert!(cached_timed_sized_refresh_prime("true"));
{
let cache = CACHED_TIMED_SIZED_REFRESH_PRIME.read();
assert_eq!(cache.cache_hits(), Some(2));
assert_eq!(cache.cache_misses(), Some(1));
}
}
#[cached(
max_size = 2,
ttl = 1,
key = "String",
convert = r#"{ String::from(s) }"#
)]
fn cached_timed_sized_prime(s: &str) -> bool {
s == "true"
}
#[test]
fn test_cached_timed_sized_prime() {
assert!(cached_timed_sized_prime("true"));
{
let cache = CACHED_TIMED_SIZED_PRIME.write();
assert_eq!(cache.cache_hits(), Some(0));
assert_eq!(cache.cache_misses(), Some(1));
}
assert!(cached_timed_sized_prime("true"));
{
let cache = CACHED_TIMED_SIZED_PRIME.write();
assert_eq!(cache.cache_hits(), Some(1));
assert_eq!(cache.cache_misses(), Some(1));
}
std::thread::sleep(Duration::from_millis(500));
assert!(cached_timed_sized_prime_prime_cache("true"));
std::thread::sleep(Duration::from_millis(500));
assert!(cached_timed_sized_prime_prime_cache("true"));
std::thread::sleep(Duration::from_millis(500));
assert!(cached_timed_sized_prime_prime_cache("true"));
assert!(cached_timed_sized_prime("true"));
{
let mut cache = CACHED_TIMED_SIZED_PRIME.write();
assert_eq!(cache.cache_hits(), Some(2));
assert_eq!(cache.cache_misses(), Some(1));
assert!(cache.cache_size() > 0);
std::thread::sleep(Duration::from_millis(1000));
cache.evict();
assert_eq!(cache.cache_size(), 0);
}
}
#[cached::macros::cached(ttl = 1, result_fallback = true)]
fn always_failing() -> Result<String, ()> {
Err(())
}
#[test]
fn test_result_fallback() {
assert!(always_failing().is_err());
{
let cache = ALWAYS_FAILING.write();
assert_eq!(cache.cache_hits(), Some(0));
assert_eq!(cache.cache_misses(), Some(1));
}
ALWAYS_FAILING.write().cache_set((), "abc".to_string());
assert_eq!(always_failing(), Ok("abc".to_string()));
{
let cache = ALWAYS_FAILING.write();
assert_eq!(cache.cache_hits(), Some(1));
assert_eq!(cache.cache_misses(), Some(1));
}
std::thread::sleep(Duration::from_millis(2000));
assert_eq!(always_failing(), Ok("abc".to_string()));
{
let cache = ALWAYS_FAILING.write();
assert_eq!(cache.cache_hits(), Some(1));
assert_eq!(cache.cache_misses(), Some(2));
}
assert_eq!(always_failing(), Ok("abc".to_string()));
{
let cache = ALWAYS_FAILING.write();
assert_eq!(cache.cache_hits(), Some(2));
assert_eq!(cache.cache_misses(), Some(2));
}
}
#[cfg(feature = "proc_macro")]
static CONCURRENT_RESULT_FALLBACK_SHOULD_SUCCEED: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(true);
#[cfg(feature = "proc_macro")]
#[cached::macros::concurrent_cached(ttl = 1, result_fallback = true)]
fn concurrent_result_fallback_fn() -> Result<u32, &'static str> {
if CONCURRENT_RESULT_FALLBACK_SHOULD_SUCCEED.load(std::sync::atomic::Ordering::SeqCst) {
Ok(42)
} else {
Err("backend down")
}
}
#[cfg(feature = "proc_macro")]
#[test]
fn test_concurrent_cached_result_fallback() {
std::thread::sleep(Duration::from_millis(1100));
CONCURRENT_RESULT_FALLBACK_SHOULD_SUCCEED.store(false, std::sync::atomic::Ordering::SeqCst);
assert!(
concurrent_result_fallback_fn().is_err(),
"no prior Ok → Err returned directly"
);
CONCURRENT_RESULT_FALLBACK_SHOULD_SUCCEED.store(true, std::sync::atomic::Ordering::SeqCst);
assert_eq!(concurrent_result_fallback_fn(), Ok(42));
std::thread::sleep(Duration::from_millis(1500));
CONCURRENT_RESULT_FALLBACK_SHOULD_SUCCEED.store(false, std::sync::atomic::Ordering::SeqCst);
assert_eq!(
concurrent_result_fallback_fn(),
Ok(42),
"expired Ok entry should be returned via fallback"
);
}
#[derive(Clone, Debug)]
struct OnceExpiredValue {
val: u32,
expired: bool,
}
impl Expires for OnceExpiredValue {
fn is_expired(&self) -> bool {
self.expired
}
}
#[once(expires = true)]
fn get_once_expired(val: u32, expired: bool) -> OnceExpiredValue {
OnceExpiredValue { val, expired }
}
#[test]
fn test_once_expires_sync() {
let r1 = get_once_expired(1, false);
assert_eq!(r1.val, 1);
let r2 = get_once_expired(2, false);
assert_eq!(r2.val, 1);
assert!(get_once_expired_prime_cache(3, true).expired);
let r3 = get_once_expired(4, false);
assert_eq!(r3.val, 4);
let r4 = get_once_expired(5, false);
assert_eq!(r4.val, 4);
}
#[once(expires = true)]
fn get_once_result_expired(val: u32, expired: bool) -> Result<OnceExpiredValue, String> {
Ok(OnceExpiredValue { val, expired })
}
#[test]
fn test_once_expires_result() {
assert_eq!(get_once_result_expired(1, false).unwrap().val, 1);
assert_eq!(get_once_result_expired(2, false).unwrap().val, 1);
assert!(
get_once_result_expired_prime_cache(3, true)
.unwrap()
.expired
);
assert_eq!(get_once_result_expired(4, false).unwrap().val, 4);
assert_eq!(get_once_result_expired(5, false).unwrap().val, 4);
}
#[once(expires = true)]
fn get_once_option_expired(val: u32, expired: bool) -> Option<OnceExpiredValue> {
Some(OnceExpiredValue { val, expired })
}
#[test]
fn test_once_expires_option() {
assert_eq!(get_once_option_expired(1, false).unwrap().val, 1);
assert_eq!(get_once_option_expired(2, false).unwrap().val, 1);
assert!(
get_once_option_expired_prime_cache(3, true)
.unwrap()
.expired
);
assert_eq!(get_once_option_expired(4, false).unwrap().val, 4);
assert_eq!(get_once_option_expired(5, false).unwrap().val, 4);
}
#[once(expires = true)]
fn get_once_result_expired_or_err(
val: u32,
expired: bool,
err: bool,
) -> Result<OnceExpiredValue, String> {
if err {
Err("forced error".to_string())
} else {
Ok(OnceExpiredValue { val, expired })
}
}
#[test]
fn test_once_expires_result_err_not_cached() {
assert!(get_once_result_expired_or_err(1, false, true).is_err());
assert_eq!(
get_once_result_expired_or_err(2, false, false).unwrap().val,
2
);
assert_eq!(
get_once_result_expired_or_err(3, false, false).unwrap().val,
2
);
}
#[once(expires = true)]
fn get_once_option_expired_or_none(
val: u32,
expired: bool,
none: bool,
) -> Option<OnceExpiredValue> {
if none {
None
} else {
Some(OnceExpiredValue { val, expired })
}
}
#[test]
fn test_once_expires_option_none_not_cached() {
assert!(get_once_option_expired_or_none(1, false, true).is_none());
assert_eq!(
get_once_option_expired_or_none(2, false, false)
.unwrap()
.val,
2
);
assert_eq!(
get_once_option_expired_or_none(3, false, false)
.unwrap()
.val,
2
);
}
#[cfg(all(feature = "async", feature = "proc_macro"))]
mod async_tests {
use super::*;
#[once(ttl = 1)]
async fn only_cached_once_per_second_a(s: String) -> Vec<String> {
vec![s]
}
#[tokio::test]
async fn test_only_cached_once_per_second_a() {
let a = only_cached_once_per_second_a("a".to_string()).await;
let b = only_cached_once_per_second_a("b".to_string()).await;
assert_eq!(a, b);
sleep(Duration::new(1, 0));
let b = only_cached_once_per_second_a("b".to_string()).await;
assert_eq!(vec!["b".to_string()], b);
}
#[once(ttl = 1)]
async fn only_cached_result_once_per_second_a(
s: String,
error: bool,
) -> std::result::Result<Vec<String>, u32> {
if error { Err(1) } else { Ok(vec![s]) }
}
#[tokio::test]
async fn test_only_cached_result_once_per_second_a() {
assert!(
only_cached_result_once_per_second_a("z".to_string(), true)
.await
.is_err()
);
let a = only_cached_result_once_per_second_a("a".to_string(), false)
.await
.unwrap();
let b = only_cached_result_once_per_second_a("b".to_string(), false)
.await
.unwrap();
assert_eq!(a, b);
sleep(Duration::new(1, 0));
let b = only_cached_result_once_per_second_a("b".to_string(), false)
.await
.unwrap();
assert_eq!(vec!["b".to_string()], b);
}
#[once(ttl = 1)]
async fn only_cached_option_once_per_second_a(
s: String,
none: bool,
) -> Option<Vec<String>> {
if none { None } else { Some(vec![s]) }
}
#[tokio::test]
async fn test_only_cached_option_once_per_second_a() {
assert!(
only_cached_option_once_per_second_a("z".to_string(), true)
.await
.is_none()
);
let a = only_cached_option_once_per_second_a("a".to_string(), false)
.await
.unwrap();
let b = only_cached_option_once_per_second_a("b".to_string(), false)
.await
.unwrap();
assert_eq!(a, b);
sleep(Duration::new(1, 0));
let b = only_cached_option_once_per_second_a("b".to_string(), false)
.await
.unwrap();
assert_eq!(vec!["b".to_string()], b);
}
#[once(ttl = 2, sync_writes)]
async fn only_cached_once_per_second_sync_writes(s: String) -> Vec<String> {
vec![s]
}
#[tokio::test]
async fn test_only_cached_once_per_second_sync_writes() {
let a = tokio::spawn(only_cached_once_per_second_sync_writes("a".to_string()));
tokio::time::sleep(Duration::new(1, 0)).await;
let b = tokio::spawn(only_cached_once_per_second_sync_writes("b".to_string()));
assert_eq!(a.await.unwrap(), b.await.unwrap());
}
#[cached(ttl = 2, sync_writes = "default", key = "u32", convert = "{ 1 }")]
async fn cached_sync_writes_a(s: String) -> Vec<String> {
vec![s]
}
#[tokio::test]
async fn test_cached_sync_writes_a() {
let a = tokio::spawn(cached_sync_writes_a("a".to_string()));
tokio::time::sleep(Duration::new(1, 0)).await;
let b = tokio::spawn(cached_sync_writes_a("b".to_string()));
let c = tokio::spawn(cached_sync_writes_a("c".to_string()));
let a = a.await.unwrap();
assert_eq!(a, b.await.unwrap());
assert_eq!(a, c.await.unwrap());
}
#[cached(
ttl = 5,
sync_writes = "by_key",
key = "String",
convert = r#"{ format!("{}", s) }"#
)]
async fn cached_sync_writes_by_key_a(s: String) -> Vec<String> {
tokio::time::sleep(Duration::from_secs(1)).await;
vec![s]
}
#[tokio::test]
async fn test_cached_sync_writes_by_key_a() {
let a = tokio::spawn(cached_sync_writes_by_key_a("a".to_string()));
let b = tokio::spawn(cached_sync_writes_by_key_a("b".to_string()));
let c = tokio::spawn(cached_sync_writes_by_key_a("c".to_string()));
let start = Instant::now();
a.await.unwrap();
b.await.unwrap();
c.await.unwrap();
assert!(start.elapsed() < Duration::from_secs(2));
}
#[derive(Clone, Debug)]
struct OnceExpiredValueAsync {
val: u32,
expired: bool,
}
impl Expires for OnceExpiredValueAsync {
fn is_expired(&self) -> bool {
self.expired
}
}
#[once(expires = true)]
async fn get_once_expired_async(val: u32, expired: bool) -> OnceExpiredValueAsync {
OnceExpiredValueAsync { val, expired }
}
#[tokio::test]
async fn test_once_expires_async() {
let r1 = get_once_expired_async(1, false).await;
assert_eq!(r1.val, 1);
let r2 = get_once_expired_async(2, false).await;
assert_eq!(r2.val, 1);
assert!(get_once_expired_async_prime_cache(3, true).await.expired);
let r3 = get_once_expired_async(4, false).await;
assert_eq!(r3.val, 4);
let r4 = get_once_expired_async(5, false).await;
assert_eq!(r4.val, 4);
}
#[once(expires = true)]
async fn get_once_result_expired_async(
val: u32,
expired: bool,
) -> Result<OnceExpiredValueAsync, String> {
Ok(OnceExpiredValueAsync { val, expired })
}
#[tokio::test]
async fn test_once_expires_result_async() {
assert_eq!(
get_once_result_expired_async(1, false).await.unwrap().val,
1
);
assert_eq!(
get_once_result_expired_async(2, false).await.unwrap().val,
1
);
assert!(
get_once_result_expired_async_prime_cache(3, true)
.await
.unwrap()
.expired
);
assert_eq!(
get_once_result_expired_async(4, false).await.unwrap().val,
4
);
assert_eq!(
get_once_result_expired_async(5, false).await.unwrap().val,
4
);
}
#[once(expires = true)]
async fn get_once_option_expired_async(
val: u32,
expired: bool,
) -> Option<OnceExpiredValueAsync> {
Some(OnceExpiredValueAsync { val, expired })
}
#[tokio::test]
async fn test_once_expires_option_async() {
assert_eq!(
get_once_option_expired_async(1, false).await.unwrap().val,
1
);
assert_eq!(
get_once_option_expired_async(2, false).await.unwrap().val,
1
);
assert!(
get_once_option_expired_async_prime_cache(3, true)
.await
.unwrap()
.expired
);
assert_eq!(
get_once_option_expired_async(4, false).await.unwrap().val,
4
);
assert_eq!(
get_once_option_expired_async(5, false).await.unwrap().val,
4
);
}
#[tokio::test]
async fn test_expiring_cache_async() {
use cached::CachedAsync;
#[derive(Clone, Debug)]
struct AsyncValue {
val: String,
expired: bool,
}
impl Expires for AsyncValue {
fn is_expired(&self) -> bool {
self.expired
}
}
let mut cache = ExpiringCache::builder().build().unwrap();
let r1 = cache
.async_get_or_set_with("key".to_string(), || async {
AsyncValue {
val: "hello".to_string(),
expired: false,
}
})
.await;
assert_eq!(r1.val, "hello");
let r2 = cache
.async_get_or_set_with("key".to_string(), || async {
AsyncValue {
val: "ignored".to_string(),
expired: false,
}
})
.await;
assert_eq!(r2.val, "hello");
cache.cache_set(
"key".to_string(),
AsyncValue {
val: "expired_val".to_string(),
expired: true,
},
);
let r3 = cache
.async_get_or_set_with("key".to_string(), || async {
AsyncValue {
val: "new_fresh".to_string(),
expired: false,
}
})
.await;
assert_eq!(r3.val, "new_fresh");
}
#[derive(Clone, Debug)]
struct AsyncCachedExpiresVal {
val: u32,
expired: bool,
}
impl Expires for AsyncCachedExpiresVal {
fn is_expired(&self) -> bool {
self.expired
}
}
#[cached(expires = true, key = "u32", convert = "{ k }")]
async fn async_cached_expires_basic(k: u32, expired: bool) -> AsyncCachedExpiresVal {
AsyncCachedExpiresVal { val: k, expired }
}
#[tokio::test]
async fn test_async_cached_expires_basic() {
assert_eq!(async_cached_expires_basic(1, false).await.val, 1);
assert_eq!(async_cached_expires_basic(1, false).await.val, 1);
async_cached_expires_basic_prime_cache(1, true).await;
let r = async_cached_expires_basic(1, false).await;
assert_eq!(r.val, 1);
assert!(!r.expired);
assert_eq!(async_cached_expires_basic(1, false).await.val, 1);
}
#[cached(expires = true, key = "u32", convert = "{ k }")]
async fn async_cached_expires_result(
k: u32,
expired: bool,
err: bool,
) -> Result<AsyncCachedExpiresVal, String> {
if err {
Err("forced error".to_string())
} else {
Ok(AsyncCachedExpiresVal { val: k, expired })
}
}
#[tokio::test]
async fn test_async_cached_expires_result() {
assert!(async_cached_expires_result(1, false, true).await.is_err());
assert!(async_cached_expires_result(1, false, true).await.is_err());
assert_eq!(
async_cached_expires_result(1, false, false)
.await
.unwrap()
.val,
1
);
assert_eq!(
async_cached_expires_result(1, false, false)
.await
.unwrap()
.val,
1
);
async_cached_expires_result_prime_cache(1, true, false)
.await
.unwrap();
let r = async_cached_expires_result(1, false, false).await.unwrap();
assert_eq!(r.val, 1);
assert!(!r.expired);
}
#[cached(expires = true, key = "u32", convert = "{ k }")]
async fn async_cached_expires_option(
k: u32,
expired: bool,
none: bool,
) -> Option<AsyncCachedExpiresVal> {
if none {
None
} else {
Some(AsyncCachedExpiresVal { val: k, expired })
}
}
#[tokio::test]
async fn test_async_cached_expires_option() {
assert!(async_cached_expires_option(1, false, true).await.is_none());
assert!(async_cached_expires_option(1, false, true).await.is_none());
assert_eq!(
async_cached_expires_option(1, false, false)
.await
.unwrap()
.val,
1
);
assert_eq!(
async_cached_expires_option(1, false, false)
.await
.unwrap()
.val,
1
);
async_cached_expires_option_prime_cache(1, true, false)
.await
.unwrap();
let r = async_cached_expires_option(1, false, false).await.unwrap();
assert_eq!(r.val, 1);
assert!(!r.expired);
}
#[cached(expires = true, result_fallback = true, key = "u32", convert = "{ k }")]
async fn async_cached_expires_result_fallback(
k: u32,
expired: bool,
err: bool,
) -> Result<AsyncCachedExpiresVal, String> {
if err {
Err("forced error".to_string())
} else {
Ok(AsyncCachedExpiresVal { val: k, expired })
}
}
#[tokio::test]
async fn test_async_cached_expires_result_fallback() {
async_cached_expires_result_fallback_prime_cache(1, false, false)
.await
.unwrap();
assert_eq!(
async_cached_expires_result_fallback(1, false, false)
.await
.unwrap()
.val,
1
);
async_cached_expires_result_fallback_prime_cache(1, true, false)
.await
.unwrap();
let r = async_cached_expires_result_fallback(1, false, true)
.await
.unwrap();
assert_eq!(r.val, 1);
assert!(r.expired);
}
}
use cached::stores::ExpiringCache;
#[derive(Clone, Debug)]
struct CachedExpiresVal {
val: u32,
expired: bool,
}
impl Expires for CachedExpiresVal {
fn is_expired(&self) -> bool {
self.expired
}
}
#[cached(expires = true, key = "u32", convert = "{ k }")]
fn cached_expires_basic(k: u32, expired: bool) -> CachedExpiresVal {
CachedExpiresVal { val: k, expired }
}
#[test]
fn test_cached_expires_basic() {
assert_eq!(cached_expires_basic(1, false).val, 1);
assert_eq!(cached_expires_basic(1, false).val, 1);
{
let c = CACHED_EXPIRES_BASIC.read();
assert_eq!(c.cache_hits(), Some(1));
assert_eq!(c.cache_misses(), Some(1));
}
cached_expires_basic_prime_cache(1, true);
let r = cached_expires_basic(1, false);
assert_eq!(r.val, 1);
assert!(!r.expired);
{
let c = CACHED_EXPIRES_BASIC.read();
assert_eq!(c.cache_hits(), Some(1));
assert_eq!(c.cache_misses(), Some(2));
assert_eq!(c.cache_evictions(), Some(1));
}
assert_eq!(cached_expires_basic(1, false).val, 1);
}
#[cached(expires = true, max_size = 4, key = "u32", convert = "{ k }")]
fn cached_expires_lru(k: u32, expired: bool) -> CachedExpiresVal {
CachedExpiresVal { val: k, expired }
}
#[test]
fn test_cached_expires_lru() {
assert_eq!(cached_expires_lru(10, false).val, 10);
assert_eq!(cached_expires_lru(10, false).val, 10);
{
let c = CACHED_EXPIRES_LRU.read();
assert_eq!(c.cache_hits(), Some(1));
assert_eq!(c.cache_misses(), Some(1));
}
cached_expires_lru_prime_cache(10, true);
let r = cached_expires_lru(10, false);
assert_eq!(r.val, 10);
assert!(!r.expired);
{
let c = CACHED_EXPIRES_LRU.read();
assert_eq!(c.cache_evictions(), Some(1));
}
}
#[cached(expires = true, key = "u32", convert = "{ k }")]
fn cached_expires_result(k: u32, expired: bool, err: bool) -> Result<CachedExpiresVal, String> {
if err {
Err("forced error".to_string())
} else {
Ok(CachedExpiresVal { val: k, expired })
}
}
#[test]
fn test_cached_expires_result() {
assert!(cached_expires_result(1, false, true).is_err());
assert!(cached_expires_result(1, false, true).is_err());
assert_eq!(cached_expires_result(1, false, false).unwrap().val, 1);
assert_eq!(cached_expires_result(1, false, false).unwrap().val, 1);
cached_expires_result_prime_cache(1, true, false).unwrap();
let r = cached_expires_result(1, false, false).unwrap();
assert_eq!(r.val, 1);
assert!(!r.expired);
{
let c = CACHED_EXPIRES_RESULT.read();
assert_eq!(c.cache_evictions(), Some(1));
}
}
#[cached(expires = true, key = "u32", convert = "{ k }")]
fn cached_expires_option(k: u32, expired: bool, none: bool) -> Option<CachedExpiresVal> {
if none {
None
} else {
Some(CachedExpiresVal { val: k, expired })
}
}
#[test]
fn test_cached_expires_option() {
assert!(cached_expires_option(1, false, true).is_none());
assert!(cached_expires_option(1, false, true).is_none());
assert_eq!(cached_expires_option(1, false, false).unwrap().val, 1);
assert_eq!(cached_expires_option(1, false, false).unwrap().val, 1);
cached_expires_option_prime_cache(1, true, false).unwrap();
let r = cached_expires_option(1, false, false).unwrap();
assert_eq!(r.val, 1);
assert!(!r.expired);
{
let c = CACHED_EXPIRES_OPTION.read();
assert_eq!(c.cache_evictions(), Some(1));
}
}
#[cached(expires = true, result_fallback = true, key = "u32", convert = "{ k }")]
fn cached_expires_result_fallback(
k: u32,
expired: bool,
err: bool,
) -> Result<CachedExpiresVal, String> {
if err {
Err("forced error".to_string())
} else {
Ok(CachedExpiresVal { val: k, expired })
}
}
#[test]
fn test_cached_expires_result_fallback() {
cached_expires_result_fallback_prime_cache(1, false, false).unwrap();
assert_eq!(
cached_expires_result_fallback(1, false, false).unwrap().val,
1
);
cached_expires_result_fallback_prime_cache(1, true, false).unwrap();
let r = cached_expires_result_fallback(1, false, true).unwrap();
assert_eq!(r.val, 1);
assert!(r.expired);
}
#[test]
fn test_expiring_cache_integration() {
#[derive(Clone, Debug, PartialEq, Eq)]
struct Value {
val: String,
expired: bool,
}
impl Expires for Value {
fn is_expired(&self) -> bool {
self.expired
}
}
let mut cache = ExpiringCache::builder().build().unwrap();
cache.cache_set(
"a".to_string(),
Value {
val: "hello".to_string(),
expired: false,
},
);
cache.cache_set(
"b".to_string(),
Value {
val: "world".to_string(),
expired: true,
},
);
assert_eq!(
cache.cache_get(&"a".to_string()).map(|v| &v.val),
Some(&"hello".to_string())
);
assert!(cache.cache_get(&"b".to_string()).is_none());
assert_eq!(cache.cache_hits(), Some(1));
assert_eq!(cache.cache_misses(), Some(1));
assert_eq!(cache.cache_evictions(), Some(1));
}
}
#[cfg(feature = "time_stores")]
mod sharded_ttl_tests {
#[test]
fn sharded_ttl_refresh_on_hit_extends_lifetime() {
use cached::ConcurrentCached;
use cached::ShardedTtlCache;
use cached::time::Duration;
let cache: ShardedTtlCache<u32, u32> = ShardedTtlCache::builder()
.ttl(Duration::from_millis(3_000))
.shards(4)
.refresh_on_hit(true)
.build()
.expect("valid config");
let _ = ConcurrentCached::cache_set(&cache, 1u32, 42u32);
std::thread::sleep(Duration::from_millis(500));
assert_eq!(
ConcurrentCached::cache_get(&cache, &1u32).expect("infallible"),
Some(42),
"entry should still be alive before TTL expires"
);
std::thread::sleep(Duration::from_millis(1_500));
assert_eq!(
ConcurrentCached::cache_get(&cache, &1u32).expect("infallible"),
Some(42),
"entry should still be alive after TTL was refreshed on the previous get"
);
std::thread::sleep(Duration::from_millis(3_200));
assert_eq!(
ConcurrentCached::cache_get(&cache, &1u32).expect("infallible"),
None,
"entry should be expired after TTL elapsed with no further refresh"
);
}
#[test]
fn sharded_ttl_stores_implement_cache_evict_trait() {
use cached::time::Duration;
use cached::{CacheEvict, ShardedLruTtlCache, ShardedTtlCache};
fn assert_cache_evict<C: CacheEvict>(cache: &mut C) -> usize {
cache.evict()
}
let mut ttl: ShardedTtlCache<u32, u32> = ShardedTtlCache::builder()
.ttl(Duration::from_secs(60))
.build()
.expect("valid config");
let mut lru_ttl: ShardedLruTtlCache<u32, u32> = ShardedLruTtlCache::builder()
.max_size(16)
.ttl(Duration::from_secs(60))
.build()
.expect("valid config");
assert_eq!(assert_cache_evict(&mut ttl), 0);
assert_eq!(assert_cache_evict(&mut lru_ttl), 0);
}
#[test]
fn sharded_ttl_builders_accept_refresh_alias() {
use cached::time::Duration;
use cached::{ShardedLruTtlCache, ShardedTtlCache};
let ttl = ShardedTtlCache::<u32, u32>::builder()
.ttl(Duration::from_secs(60))
.refresh(true)
.build()
.expect("valid config");
assert!(ttl.refresh_on_hit());
let lru_ttl = ShardedLruTtlCache::<u32, u32>::builder()
.max_size(64)
.ttl(Duration::from_secs(60))
.refresh(true)
.build()
.expect("valid config");
assert!(lru_ttl.refresh_on_hit());
}
#[test]
fn non_sharded_ttl_builders_accept_refresh_on_hit_and_refresh_alias() {
use cached::time::Duration;
use cached::{LruTtlCache, TtlCache};
let ttl = TtlCache::<u32, u32>::builder()
.ttl(Duration::from_secs(60))
.refresh_on_hit(true)
.build()
.expect("valid config");
assert!(ttl.refresh_on_hit());
let lru_ttl = LruTtlCache::<u32, u32>::builder()
.max_size(64)
.ttl(Duration::from_secs(60))
.refresh_on_hit(true)
.build()
.expect("valid config");
assert!(lru_ttl.refresh_on_hit());
let ttl_alias = TtlCache::<u32, u32>::builder()
.ttl(Duration::from_secs(60))
.refresh(true)
.build()
.expect("valid config");
assert!(ttl_alias.refresh_on_hit());
let lru_ttl_alias = LruTtlCache::<u32, u32>::builder()
.max_size(64)
.ttl(Duration::from_secs(60))
.refresh(true)
.build()
.expect("valid config");
assert!(lru_ttl_alias.refresh_on_hit());
let ttl_off = TtlCache::<u32, u32>::builder()
.ttl(Duration::from_secs(60))
.refresh_on_hit(false)
.build()
.expect("valid config");
assert!(!ttl_off.refresh_on_hit());
}
#[test]
fn sharded_lru_ttl_evict_does_not_double_count_evictions_or_double_fire_on_evict() {
use cached::ConcurrentCached;
use cached::ShardedLruTtlCache;
use cached::time::Duration;
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
let fired = Arc::new(AtomicU32::new(0));
let fired_clone = fired.clone();
let cache = ShardedLruTtlCache::builder()
.max_size(16)
.ttl(Duration::from_millis(50))
.on_evict(move |_k, _v| {
fired_clone.fetch_add(1, Ordering::Relaxed);
})
.build()
.unwrap();
cache.cache_set(1u32, 10u32).expect("infallible");
cache.cache_set(2u32, 20u32).expect("infallible");
cache.cache_set(3u32, 30u32).expect("infallible");
std::thread::sleep(Duration::from_millis(100));
assert_eq!(cache.evict(), 3);
assert_eq!(fired.load(Ordering::Relaxed), 3);
assert_eq!(cache.metrics().evictions, Some(3));
}
#[test]
fn sharded_ttl_evict_fires_on_evict_and_increments_evictions_counter() {
use cached::ConcurrentCached;
use cached::ShardedTtlCache;
use cached::time::Duration;
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
let fired = Arc::new(AtomicU32::new(0));
let fired_clone = fired.clone();
let cache = ShardedTtlCache::builder()
.ttl(Duration::from_millis(50))
.on_evict(move |_k, _v| {
fired_clone.fetch_add(1, Ordering::Relaxed);
})
.build()
.unwrap();
cache.cache_set(1u32, 10u32).expect("infallible");
cache.cache_set(2u32, 20u32).expect("infallible");
cache.cache_set(3u32, 30u32).expect("infallible");
std::thread::sleep(Duration::from_millis(100));
assert_eq!(cache.evict(), 3);
assert_eq!(fired.load(Ordering::Relaxed), 3);
assert_eq!(cache.metrics().evictions, Some(3));
}
}
#[cfg(all(feature = "async", feature = "proc_macro"))]
mod async_tests {
use super::*;
#[once]
async fn only_cached_result_once_a(
s: String,
error: bool,
) -> std::result::Result<Vec<String>, u32> {
if error { Err(1) } else { Ok(vec![s]) }
}
#[tokio::test]
async fn test_only_cached_result_once_a() {
assert!(
only_cached_result_once_a("z".to_string(), true)
.await
.is_err()
);
let a = only_cached_result_once_a("a".to_string(), false)
.await
.unwrap();
let b = only_cached_result_once_a("b".to_string(), false)
.await
.unwrap();
assert_eq!(a, b);
sleep(Duration::new(1, 0));
let b = only_cached_result_once_a("b".to_string(), false)
.await
.unwrap();
assert_eq!(a, b);
}
#[once]
async fn only_cached_option_once_a(s: String, none: bool) -> Option<Vec<String>> {
if none { None } else { Some(vec![s]) }
}
#[tokio::test]
async fn test_only_cached_option_once_a() {
assert!(
only_cached_option_once_a("z".to_string(), true)
.await
.is_none()
);
let a = only_cached_option_once_a("a".to_string(), false)
.await
.unwrap();
let b = only_cached_option_once_a("b".to_string(), false)
.await
.unwrap();
assert_eq!(a, b);
sleep(Duration::new(1, 0));
let b = only_cached_option_once_a("b".to_string(), false)
.await
.unwrap();
assert_eq!(a, b);
}
#[once(sync_writes = true)]
async fn once_sync_writes_a(s: &tokio::sync::Mutex<String>) -> String {
let mut guard = s.lock().await;
let results: String = (*guard).clone();
*guard = "consumed".to_string();
results
}
#[tokio::test]
async fn test_once_sync_writes_a() {
let a_mutex = tokio::sync::Mutex::new("a".to_string());
let b_mutex = tokio::sync::Mutex::new("b".to_string());
let fut_a = once_sync_writes_a(&a_mutex);
let fut_b = once_sync_writes_a(&b_mutex);
let a = fut_a.await;
let b = fut_b.await;
assert_eq!(a, b);
assert_eq!("a", a);
assert_eq!("consumed", a_mutex.lock().await.to_string());
assert_eq!("b", b_mutex.lock().await.to_string());
}
}
#[cfg(all(feature = "disk_store", feature = "proc_macro"))]
mod disk_tests {
use super::*;
use cached::DiskCache;
use cached::macros::concurrent_cached;
use thiserror::Error;
#[derive(Error, Debug, PartialEq, Clone)]
enum TestError {
#[error("error with disk cache `{0}`")]
DiskError(String),
#[error("count `{0}`")]
Count(u32),
}
#[concurrent_cached(
disk = true,
ttl = 1,
map_error = r##"|e| TestError::DiskError(format!("{:?}", e))"##
)]
fn cached_disk(n: u32) -> Result<u32, TestError> {
if n < 5 {
Ok(n)
} else {
Err(TestError::Count(n))
}
}
#[test]
fn test_cached_disk() {
assert_eq!(cached_disk(1), Ok(1));
assert_eq!(cached_disk(1), Ok(1));
assert_eq!(cached_disk(5), Err(TestError::Count(5)));
assert_eq!(cached_disk(6), Err(TestError::Count(6)));
}
#[concurrent_cached(
disk = true,
ttl = 1,
with_cached_flag = true,
map_error = r##"|e| TestError::DiskError(format!("{:?}", e))"##
)]
fn cached_disk_cached_flag(n: u32) -> Result<cached::Return<u32>, TestError> {
if n < 5 {
Ok(cached::Return::new(n))
} else {
Err(TestError::Count(n))
}
}
#[test]
fn test_cached_disk_cached_flag() {
assert!(!cached_disk_cached_flag(1).unwrap().was_cached);
assert!(cached_disk_cached_flag(1).unwrap().was_cached);
assert!(cached_disk_cached_flag(5).is_err());
assert!(cached_disk_cached_flag(6).is_err());
}
#[concurrent_cached(
map_error = r##"|e| TestError::DiskError(format!("{:?}", e))"##,
ty = "cached::DiskCache<u32, u32>",
create = r##" { DiskCache::new("cached_disk_cache_create").ttl(Duration::from_secs(1)).refresh(true).build().expect("error building disk cache") } "##
)]
fn cached_disk_cache_create(n: u32) -> Result<u32, TestError> {
if n < 5 {
Ok(n)
} else {
Err(TestError::Count(n))
}
}
#[test]
fn test_cached_disk_cache_create() {
assert_eq!(cached_disk_cache_create(1), Ok(1));
assert_eq!(cached_disk_cache_create(1), Ok(1));
assert_eq!(cached_disk_cache_create(5), Err(TestError::Count(5)));
assert_eq!(cached_disk_cache_create(6), Err(TestError::Count(6)));
}
#[concurrent_cached(
disk = true,
map_error = r##"|e| TestError::DiskError(format!("{:?}", e))"##,
connection_config = r##"sled::Config::new().flush_every_ms(None)"##
)]
fn cached_disk_connection_config(n: u32) -> Result<u32, TestError> {
if n < 5 {
Ok(n)
} else {
Err(TestError::Count(n))
}
}
#[concurrent_cached(
disk = true,
map_error = r##"|e| TestError::DiskError(format!("{:?}", e))"##,
sync_to_disk_on_cache_change = true
)]
fn cached_disk_sync_to_disk_on_cache_change(n: u32) -> Result<u32, TestError> {
if n < 5 {
Ok(n)
} else {
Err(TestError::Count(n))
}
}
#[cfg(all(feature = "async", feature = "proc_macro"))]
mod async_test {
use super::*;
#[concurrent_cached(
disk = true,
map_error = r##"|e| TestError::DiskError(format!("{:?}", e))"##
)]
async fn async_cached_disk(n: u32) -> Result<u32, TestError> {
if n < 5 {
Ok(n)
} else {
Err(TestError::Count(n))
}
}
#[tokio::test]
async fn test_async_cached_disk() {
assert_eq!(async_cached_disk(1).await, Ok(1));
assert_eq!(async_cached_disk(1).await, Ok(1));
assert_eq!(async_cached_disk(5).await, Err(TestError::Count(5)));
assert_eq!(async_cached_disk(6).await, Err(TestError::Count(6)));
}
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq)]
struct NotSyncValue {
c: std::cell::Cell<u32>,
}
#[concurrent_cached(
disk = true,
map_error = r##"|e| TestError::DiskError(format!("{:?}", e))"##
)]
async fn async_cached_disk_not_sync(n: u32) -> Result<NotSyncValue, TestError> {
Ok(NotSyncValue {
c: std::cell::Cell::new(n),
})
}
#[tokio::test]
async fn test_async_cached_disk_not_sync_value() {
fn assert_send<T: Send>() {}
assert_send::<NotSyncValue>(); assert_eq!(
async_cached_disk_not_sync(7).await.unwrap(),
NotSyncValue {
c: std::cell::Cell::new(7)
}
);
assert_eq!(
async_cached_disk_not_sync(7).await.unwrap(),
NotSyncValue {
c: std::cell::Cell::new(7)
}
);
}
}
}
#[cfg(feature = "proc_macro")]
mod concurrent_cached_plain_return {
use cached::macros::concurrent_cached;
use std::collections::HashMap;
use std::sync::atomic::{AtomicUsize, Ordering};
static PLAIN_CALLS: AtomicUsize = AtomicUsize::new(0);
static PLAIN_OPTION_CALLS: AtomicUsize = AtomicUsize::new(0);
static PLAIN_OPTION_NONE_CALLS: AtomicUsize = AtomicUsize::new(0);
static PLAIN_MAX_SIZE_CALLS: AtomicUsize = AtomicUsize::new(0);
#[concurrent_cached]
fn plain_double(x: u64) -> u64 {
PLAIN_CALLS.fetch_add(1, Ordering::Relaxed);
x * 2
}
#[concurrent_cached(max_size = 100)]
fn plain_double_lru(x: u64) -> u64 {
x * 2
}
#[concurrent_cached(max_size = 100)]
fn plain_double_lru_max_size(x: u64) -> u64 {
PLAIN_MAX_SIZE_CALLS.fetch_add(1, Ordering::Relaxed);
x * 2
}
#[concurrent_cached]
fn plain_option(x: u64) -> Option<u64> {
PLAIN_OPTION_CALLS.fetch_add(1, Ordering::Relaxed);
if x == 0 { None } else { Some(x * 2) }
}
#[concurrent_cached(cache_none = true)]
fn plain_option_cache_none(x: u64) -> Option<u64> {
PLAIN_OPTION_NONE_CALLS.fetch_add(1, Ordering::Relaxed);
if x == 0 { None } else { Some(x * 2) }
}
#[concurrent_cached]
fn plain_hash_map(x: u64) -> HashMap<u64, u64> {
let mut map = HashMap::new();
map.insert(x, x * 2);
map
}
#[test]
fn plain_return_compiles_and_caches() {
PLAIN_CALLS.store(0, Ordering::Relaxed);
assert_eq!(plain_double(21), 42);
assert_eq!(plain_double(21), 42); assert_eq!(plain_double(22), 44); assert_eq!(PLAIN_CALLS.load(Ordering::Relaxed), 2);
}
#[test]
fn plain_return_lru_compiles_and_caches() {
assert_eq!(plain_double_lru(10), 20);
assert_eq!(plain_double_lru(10), 20); }
#[test]
fn plain_return_max_size_alias_compiles_and_caches() {
PLAIN_MAX_SIZE_CALLS.store(0, Ordering::Relaxed);
assert_eq!(plain_double_lru_max_size(10), 20);
assert_eq!(plain_double_lru_max_size(10), 20); assert_eq!(plain_double_lru_max_size(11), 22); assert_eq!(PLAIN_MAX_SIZE_CALLS.load(Ordering::Relaxed), 2);
}
#[test]
fn plain_option_return_skips_none_caches_some() {
PLAIN_OPTION_CALLS.store(0, Ordering::Relaxed);
assert_eq!(plain_option(0), None);
assert_eq!(plain_option(0), None);
assert_eq!(
PLAIN_OPTION_CALLS.load(Ordering::Relaxed),
2,
"None should NOT be cached by default"
);
assert_eq!(plain_option(5), Some(10));
assert_eq!(plain_option(5), Some(10));
assert_eq!(
PLAIN_OPTION_CALLS.load(Ordering::Relaxed),
3,
"Some should be cached"
);
}
#[test]
fn plain_option_cache_none_caches_none_and_some() {
PLAIN_OPTION_NONE_CALLS.store(0, Ordering::Relaxed);
assert_eq!(plain_option_cache_none(0), None);
assert_eq!(plain_option_cache_none(0), None);
assert_eq!(
PLAIN_OPTION_NONE_CALLS.load(Ordering::Relaxed),
1,
"None should be cached with cache_none = true"
);
assert_eq!(plain_option_cache_none(5), Some(10));
assert_eq!(plain_option_cache_none(5), Some(10));
assert_eq!(
PLAIN_OPTION_NONE_CALLS.load(Ordering::Relaxed),
2,
"Some should be cached"
);
}
#[test]
fn plain_generic_return_is_not_misclassified_as_result() {
assert_eq!(plain_hash_map(7).get(&7), Some(&14));
assert_eq!(plain_hash_map(7).get(&7), Some(&14));
}
}
#[cfg(all(feature = "proc_macro", feature = "time_stores"))]
mod concurrent_cached_plain_return_ttl {
use cached::macros::concurrent_cached;
use std::sync::atomic::{AtomicUsize, Ordering};
static TTL_PLAIN_CALLS: AtomicUsize = AtomicUsize::new(0);
#[concurrent_cached(ttl = 60)]
fn plain_double_ttl(x: u64) -> u64 {
TTL_PLAIN_CALLS.fetch_add(1, Ordering::Relaxed);
x * 2
}
#[concurrent_cached(max_size = 50, ttl = 60)]
fn plain_double_lru_ttl(x: u64) -> u64 {
x * 2
}
#[test]
fn plain_ttl_compiles_and_caches() {
TTL_PLAIN_CALLS.store(0, Ordering::Relaxed);
assert_eq!(plain_double_ttl(21), 42);
assert_eq!(plain_double_ttl(21), 42); assert_eq!(TTL_PLAIN_CALLS.load(Ordering::Relaxed), 1);
}
#[test]
fn plain_lru_ttl_compiles_and_caches() {
assert_eq!(plain_double_lru_ttl(21), 42);
assert_eq!(plain_double_lru_ttl(21), 42); }
}
#[cfg(feature = "proc_macro")]
mod concurrent_cached_default_in_memory {
use cached::macros::concurrent_cached;
use std::sync::atomic::{AtomicUsize, Ordering};
static SLOW_DOUBLE_CALLS: AtomicUsize = AtomicUsize::new(0);
static FALLIBLE_CALLS: AtomicUsize = AtomicUsize::new(0);
static CUSTOM_RESULT_CALLS: AtomicUsize = AtomicUsize::new(0);
static PLAIN_ALIAS_CALLS: AtomicUsize = AtomicUsize::new(0);
static BARE_RESULT_ALIAS_CALLS: AtomicUsize = AtomicUsize::new(0);
#[concurrent_cached]
fn slow_double(x: u64) -> u64 {
SLOW_DOUBLE_CALLS.fetch_add(1, Ordering::Relaxed);
x * 2
}
#[concurrent_cached]
fn slow_double_fallible(x: u64) -> Result<u64, String> {
FALLIBLE_CALLS.fetch_add(1, Ordering::Relaxed);
if x == 0 {
Err("zero is not cacheable".to_string())
} else {
Ok(x * 2)
}
}
type MyResult<T> = Result<T, String>;
#[concurrent_cached]
fn slow_double_custom_result(x: u64) -> MyResult<u64> {
CUSTOM_RESULT_CALLS.fetch_add(1, Ordering::Relaxed);
if x == 0 {
Err("zero is cached (plain alias)".to_string())
} else {
Ok(x * 2)
}
}
type Api<T> = Result<T, String>;
#[concurrent_cached]
fn slow_double_plain_alias(x: u64) -> Api<u64> {
PLAIN_ALIAS_CALLS.fetch_add(1, Ordering::Relaxed);
if x == 0 {
Err("zero is cached for this plain alias".to_string())
} else {
Ok(x * 2)
}
}
type BareResult = Result<u64, String>;
#[concurrent_cached]
fn slow_double_bare_result_alias(x: u64) -> BareResult {
BARE_RESULT_ALIAS_CALLS.fetch_add(1, Ordering::Relaxed);
if x == 0 {
Err("zero is cached for this bare alias".to_string())
} else {
Ok(x * 2)
}
}
#[test]
fn bare_default_compiles_and_caches() {
SLOW_DOUBLE_CALLS.store(0, Ordering::Relaxed);
assert_eq!(slow_double(21), 42);
assert_eq!(slow_double(21), 42); assert_eq!(slow_double(22), 44); assert_eq!(SLOW_DOUBLE_CALLS.load(Ordering::Relaxed), 2); }
#[test]
fn result_return_skips_caching_on_err() {
FALLIBLE_CALLS.store(0, Ordering::Relaxed);
assert!(slow_double_fallible(0).is_err());
assert!(slow_double_fallible(0).is_err());
assert_eq!(FALLIBLE_CALLS.load(Ordering::Relaxed), 2);
assert_eq!(slow_double_fallible(5), Ok(10));
assert_eq!(slow_double_fallible(5), Ok(10)); assert_eq!(FALLIBLE_CALLS.load(Ordering::Relaxed), 3);
}
#[test]
fn custom_result_alias_treated_as_plain_return() {
CUSTOM_RESULT_CALLS.store(0, Ordering::Relaxed);
assert!(slow_double_custom_result(0).is_err());
assert!(slow_double_custom_result(0).is_err()); assert_eq!(CUSTOM_RESULT_CALLS.load(Ordering::Relaxed), 1);
assert_eq!(slow_double_custom_result(21), Ok(42));
assert_eq!(slow_double_custom_result(21), Ok(42)); assert_eq!(CUSTOM_RESULT_CALLS.load(Ordering::Relaxed), 2);
}
#[test]
fn result_alias_without_result_suffix_is_treated_as_plain_return() {
PLAIN_ALIAS_CALLS.store(0, Ordering::Relaxed);
assert!(slow_double_plain_alias(0).is_err());
assert!(slow_double_plain_alias(0).is_err());
assert_eq!(PLAIN_ALIAS_CALLS.load(Ordering::Relaxed), 1);
assert_eq!(slow_double_plain_alias(21), Ok(42));
assert_eq!(slow_double_plain_alias(21), Ok(42));
assert_eq!(PLAIN_ALIAS_CALLS.load(Ordering::Relaxed), 2);
}
#[test]
fn bare_result_alias_without_type_args_is_treated_as_plain_return() {
BARE_RESULT_ALIAS_CALLS.store(0, Ordering::Relaxed);
assert!(slow_double_bare_result_alias(0).is_err());
assert!(slow_double_bare_result_alias(0).is_err());
assert_eq!(BARE_RESULT_ALIAS_CALLS.load(Ordering::Relaxed), 1);
assert_eq!(slow_double_bare_result_alias(21), Ok(42));
assert_eq!(slow_double_bare_result_alias(21), Ok(42));
assert_eq!(BARE_RESULT_ALIAS_CALLS.load(Ordering::Relaxed), 2);
}
}
#[cfg(all(feature = "proc_macro", feature = "async_core"))]
mod concurrent_cached_default_with_both_traits_in_scope {
use cached::macros::concurrent_cached;
#[allow(unused_imports)]
use cached::{ConcurrentCached, ConcurrentCachedAsync};
#[concurrent_cached]
fn double_with_both_traits_in_scope(x: u64) -> u64 {
x * 2
}
#[test]
fn sync_macro_uses_ufcs_to_avoid_trait_method_ambiguity() {
assert_eq!(double_with_both_traits_in_scope(21), 42);
}
}
#[cfg(feature = "proc_macro")]
mod concurrent_cached_default_with_max_size {
use cached::macros::concurrent_cached;
use std::sync::atomic::{AtomicUsize, Ordering};
static SLOW_TRIPLE_CALLS: AtomicUsize = AtomicUsize::new(0);
#[concurrent_cached(max_size = 100)]
fn slow_triple(x: u64) -> u64 {
SLOW_TRIPLE_CALLS.fetch_add(1, Ordering::Relaxed);
x * 3
}
#[test]
fn size_attr_compiles_and_caches() {
SLOW_TRIPLE_CALLS.store(0, Ordering::Relaxed);
assert_eq!(slow_triple(21), 63);
assert_eq!(slow_triple(21), 63); assert_eq!(SLOW_TRIPLE_CALLS.load(Ordering::Relaxed), 1);
}
}
#[cfg(all(feature = "proc_macro", feature = "time_stores"))]
mod concurrent_cached_default_with_ttl {
use cached::macros::concurrent_cached;
use std::sync::atomic::{AtomicUsize, Ordering};
static SLOW_QUAD_CALLS: AtomicUsize = AtomicUsize::new(0);
#[concurrent_cached(ttl = 60)]
fn slow_quad(x: u64) -> u64 {
SLOW_QUAD_CALLS.fetch_add(1, Ordering::Relaxed);
x * 4
}
#[concurrent_cached(ttl = 60, refresh = true)]
fn slow_quad_refresh(x: u64) -> u64 {
x * 4
}
#[test]
fn ttl_attr_compiles_and_caches() {
SLOW_QUAD_CALLS.store(0, Ordering::Relaxed);
assert_eq!(slow_quad(21), 84);
assert_eq!(slow_quad(21), 84); assert_eq!(SLOW_QUAD_CALLS.load(Ordering::Relaxed), 1);
}
#[test]
fn ttl_refresh_attr_wired() {
assert_eq!(slow_quad_refresh(5), 20);
assert!(SLOW_QUAD_REFRESH.refresh_on_hit());
}
}
#[cfg(all(feature = "proc_macro", feature = "time_stores"))]
mod concurrent_cached_default_with_max_size_and_ttl {
use cached::macros::concurrent_cached;
use std::sync::atomic::{AtomicUsize, Ordering};
static SLOW_QUINT_CALLS: AtomicUsize = AtomicUsize::new(0);
#[concurrent_cached(max_size = 50, ttl = 60)]
fn slow_quint(x: u64) -> u64 {
SLOW_QUINT_CALLS.fetch_add(1, Ordering::Relaxed);
x * 5
}
#[concurrent_cached(max_size = 50, ttl = 60, refresh = true)]
fn slow_quint_refresh(x: u64) -> u64 {
x * 5
}
#[test]
fn size_and_ttl_compiles_and_caches() {
SLOW_QUINT_CALLS.store(0, Ordering::Relaxed);
assert_eq!(slow_quint(21), 105);
assert_eq!(slow_quint(21), 105); assert_eq!(SLOW_QUINT_CALLS.load(Ordering::Relaxed), 1);
}
#[test]
fn size_and_ttl_refresh_attr_wired() {
assert_eq!(slow_quint_refresh(5), 25);
assert!(SLOW_QUINT_REFRESH.refresh_on_hit());
}
}
#[cfg(feature = "proc_macro")]
mod concurrent_cached_default_with_shards {
use cached::macros::concurrent_cached;
use std::sync::atomic::{AtomicUsize, Ordering};
static DOUBLE_WITH_SHARDS_CALLS: AtomicUsize = AtomicUsize::new(0);
#[concurrent_cached(shards = 32)]
fn double_with_shards(x: u64) -> u64 {
DOUBLE_WITH_SHARDS_CALLS.fetch_add(1, Ordering::Relaxed);
x * 2
}
#[concurrent_cached(max_size = 100, shards = 32)]
fn double_with_max_size_shards(x: u64) -> u64 {
x * 2
}
#[test]
fn shards_attr_compiles_and_caches() {
DOUBLE_WITH_SHARDS_CALLS.store(0, Ordering::Relaxed);
assert_eq!(double_with_shards(21), 42);
assert_eq!(double_with_shards(21), 42); assert_eq!(DOUBLE_WITH_SHARDS_CALLS.load(Ordering::Relaxed), 1);
assert_eq!(double_with_max_size_shards(21), 42);
}
#[test]
fn shards_attr_produces_correct_shard_count() {
assert_eq!(DOUBLE_WITH_SHARDS.shards(), 32);
assert_eq!(DOUBLE_WITH_MAX_SIZE_SHARDS.shards(), 32);
}
}
#[cfg(all(feature = "proc_macro", feature = "time_stores"))]
mod concurrent_cached_default_with_ttl_and_shards {
use cached::macros::concurrent_cached;
use std::sync::atomic::{AtomicUsize, Ordering};
static TTL_SHARDS_CALLS: AtomicUsize = AtomicUsize::new(0);
#[concurrent_cached(ttl = 60, shards = 16)]
fn ttl_shards_double(x: u64) -> u64 {
TTL_SHARDS_CALLS.fetch_add(1, Ordering::Relaxed);
x * 2
}
#[test]
fn ttl_shards_compiles_and_caches() {
TTL_SHARDS_CALLS.store(0, Ordering::Relaxed);
assert_eq!(ttl_shards_double(21), 42);
assert_eq!(ttl_shards_double(21), 42); assert_eq!(TTL_SHARDS_CALLS.load(Ordering::Relaxed), 1);
}
}
#[cfg(all(feature = "proc_macro", feature = "time_stores"))]
mod concurrent_cached_default_with_max_size_and_ttl_and_shards {
use cached::macros::concurrent_cached;
use std::sync::atomic::{AtomicUsize, Ordering};
static SIZE_TTL_SHARDS_CALLS: AtomicUsize = AtomicUsize::new(0);
#[concurrent_cached(max_size = 100, ttl = 60, shards = 16)]
fn size_ttl_shards_double(x: u64) -> u64 {
SIZE_TTL_SHARDS_CALLS.fetch_add(1, Ordering::Relaxed);
x * 2
}
#[test]
fn size_ttl_shards_compiles_and_caches() {
SIZE_TTL_SHARDS_CALLS.store(0, Ordering::Relaxed);
assert_eq!(size_ttl_shards_double(21), 42);
assert_eq!(size_ttl_shards_double(21), 42); assert_eq!(SIZE_TTL_SHARDS_CALLS.load(Ordering::Relaxed), 1);
}
}
#[cfg(all(feature = "proc_macro", feature = "time_stores"))]
mod concurrent_cached_result_fallback {
use cached::macros::concurrent_cached;
use cached::time::Duration;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread::sleep;
static FAIL: AtomicBool = AtomicBool::new(false);
#[concurrent_cached(ttl = 1, result_fallback = true)]
fn maybe_double(x: u32) -> Result<u32, String> {
if FAIL.load(Ordering::Relaxed) {
Err("injected failure".to_string())
} else {
Ok(x * 2)
}
}
#[test]
fn result_fallback_returns_stale_ok_after_ttl_expiry() {
FAIL.store(false, Ordering::Relaxed);
assert_eq!(maybe_double(1), Ok(2));
FAIL.store(true, Ordering::Relaxed);
assert_eq!(maybe_double(1), Ok(2));
sleep(Duration::from_millis(1100));
assert_eq!(maybe_double(1), Ok(2));
assert_eq!(maybe_double(99), Err("injected failure".to_string()));
FAIL.store(false, Ordering::Relaxed);
}
static FAIL_METRIC: AtomicBool = AtomicBool::new(false);
#[concurrent_cached(ttl = 1, result_fallback = true)]
fn maybe_triple(x: u32) -> Result<u32, String> {
if FAIL_METRIC.load(Ordering::Relaxed) {
Err("metric test failure".to_string())
} else {
Ok(x * 3)
}
}
#[test]
fn result_fallback_expired_err_path_counts_miss_no_eviction() {
FAIL_METRIC.store(false, Ordering::Relaxed);
assert_eq!(maybe_triple(7), Ok(21));
assert_eq!(maybe_triple(7), Ok(21));
sleep(Duration::from_millis(1100));
FAIL_METRIC.store(true, Ordering::Relaxed);
assert_eq!(maybe_triple(7), Ok(21));
let m = MAYBE_TRIPLE.metrics();
assert_eq!(m.misses, Some(2), "expected 2 misses (absent + expired)");
assert_eq!(m.hits, Some(1), "expected 1 hit (within-TTL)");
assert_eq!(
m.evictions,
Some(0),
"no eviction must occur on expired→Err→stale path"
);
FAIL_METRIC.store(false, Ordering::Relaxed);
}
static FAIL_STR: AtomicBool = AtomicBool::new(false);
#[concurrent_cached(
ttl = 1,
result_fallback = true,
key = "String",
convert = r#"{ x.to_string() }"#
)]
fn maybe_echo(x: &str) -> Result<String, String> {
if FAIL_STR.load(Ordering::Relaxed) {
Err("injected failure".to_string())
} else {
Ok(x.to_uppercase())
}
}
#[test]
fn result_fallback_non_copy_key_compiles_and_works() {
FAIL_STR.store(false, Ordering::Relaxed);
assert_eq!(maybe_echo("hello"), Ok("HELLO".to_string()));
FAIL_STR.store(true, Ordering::Relaxed);
sleep(Duration::from_millis(1100));
assert_eq!(maybe_echo("hello"), Ok("HELLO".to_string()));
assert_eq!(maybe_echo("unknown"), Err("injected failure".to_string()));
FAIL_STR.store(false, Ordering::Relaxed);
}
static FAIL_PRIME: AtomicBool = AtomicBool::new(false);
#[concurrent_cached(ttl = 1, result_fallback = true)]
fn prime_fallback_fn(x: u32) -> Result<u32, String> {
if FAIL_PRIME.load(Ordering::Relaxed) {
Err("prime failure".to_string())
} else {
Ok(x * 2)
}
}
#[test]
fn result_fallback_prime_cache_skips_stale_fallback() {
FAIL_PRIME.store(false, Ordering::Relaxed);
assert_eq!(prime_fallback_fn(10), Ok(20));
sleep(Duration::from_millis(1100));
FAIL_PRIME.store(true, Ordering::Relaxed);
assert_eq!(
prime_fallback_fn_prime_cache(10),
Err("prime failure".to_string()),
"prime_cache must not substitute stale Ok for Err"
);
assert_eq!(prime_fallback_fn(10), Ok(20));
FAIL_PRIME.store(false, Ordering::Relaxed);
}
}
#[cfg(all(feature = "proc_macro", feature = "time_stores"))]
mod concurrent_cached_result_fallback_lru_ttl {
use cached::macros::concurrent_cached;
use cached::time::Duration;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread::sleep;
static FAIL_LRU: AtomicBool = AtomicBool::new(false);
#[concurrent_cached(ttl = 1, max_size = 100, result_fallback = true)]
fn lru_ttl_maybe_double(x: u32) -> Result<u32, String> {
if FAIL_LRU.load(Ordering::Relaxed) {
Err("lru_ttl failure".to_string())
} else {
Ok(x * 2)
}
}
#[test]
fn result_fallback_lru_ttl_returns_stale_ok_after_expiry() {
FAIL_LRU.store(false, Ordering::Relaxed);
assert_eq!(lru_ttl_maybe_double(5), Ok(10));
assert_eq!(lru_ttl_maybe_double(5), Ok(10));
sleep(Duration::from_millis(1100));
FAIL_LRU.store(true, Ordering::Relaxed);
assert_eq!(lru_ttl_maybe_double(5), Ok(10));
let m = LRU_TTL_MAYBE_DOUBLE.metrics();
assert_eq!(m.misses, Some(2), "expected 2 misses (absent + expired)");
assert_eq!(m.hits, Some(1), "expected 1 hit (within-TTL)");
assert_eq!(
m.evictions,
Some(0),
"no eviction must occur on expired→Err→stale path"
);
assert_eq!(lru_ttl_maybe_double(99), Err("lru_ttl failure".to_string()));
FAIL_LRU.store(false, Ordering::Relaxed);
}
}
#[cfg(all(
feature = "proc_macro",
feature = "time_stores",
feature = "async_tokio_rt_multi_thread"
))]
mod concurrent_cached_result_fallback_async {
use cached::macros::concurrent_cached;
use cached::time::Duration;
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::time::sleep;
static FAIL_ASYNC: AtomicBool = AtomicBool::new(false);
#[concurrent_cached(ttl = 1, result_fallback = true)]
async fn maybe_double_async(x: u32) -> Result<u32, String> {
if FAIL_ASYNC.load(Ordering::Relaxed) {
Err("async failure".to_string())
} else {
Ok(x * 2)
}
}
#[tokio::test(flavor = "multi_thread")]
async fn result_fallback_async_returns_stale_ok_after_ttl_expiry() {
FAIL_ASYNC.store(false, Ordering::Relaxed);
assert_eq!(maybe_double_async(5).await, Ok(10));
FAIL_ASYNC.store(true, Ordering::Relaxed);
sleep(Duration::from_millis(1100)).await;
assert_eq!(maybe_double_async(5).await, Ok(10));
assert_eq!(
maybe_double_async(99).await,
Err("async failure".to_string())
);
FAIL_ASYNC.store(false, Ordering::Relaxed);
}
}
#[cfg(all(
feature = "proc_macro",
feature = "time_stores",
feature = "async_tokio_rt_multi_thread"
))]
mod concurrent_cached_result_fallback_async_non_copy_key {
use cached::macros::concurrent_cached;
use cached::time::Duration;
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::time::sleep;
static FAIL_ASYNC_STR: AtomicBool = AtomicBool::new(false);
#[concurrent_cached(
ttl = 1,
result_fallback = true,
key = "String",
convert = r#"{ x.to_string() }"#
)]
async fn maybe_echo_async(x: &str) -> Result<String, String> {
if FAIL_ASYNC_STR.load(Ordering::Relaxed) {
Err("async failure".to_string())
} else {
Ok(x.to_uppercase())
}
}
#[tokio::test(flavor = "multi_thread")]
async fn result_fallback_async_non_copy_key_returns_stale_ok_after_ttl_expiry() {
FAIL_ASYNC_STR.store(false, Ordering::Relaxed);
assert_eq!(maybe_echo_async("hello").await, Ok("HELLO".to_string()));
FAIL_ASYNC_STR.store(true, Ordering::Relaxed);
sleep(Duration::from_millis(1100)).await;
assert_eq!(maybe_echo_async("hello").await, Ok("HELLO".to_string()));
assert_eq!(
maybe_echo_async("unknown").await,
Err("async failure".to_string())
);
FAIL_ASYNC_STR.store(false, Ordering::Relaxed);
}
}
#[cfg(all(
feature = "proc_macro",
feature = "time_stores",
feature = "async_tokio_rt_multi_thread"
))]
mod concurrent_cached_result_fallback_async_lru_ttl {
use cached::macros::concurrent_cached;
use cached::time::Duration;
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::time::sleep;
static FAIL_ASYNC_LRU: AtomicBool = AtomicBool::new(false);
#[concurrent_cached(ttl = 1, max_size = 100, result_fallback = true)]
async fn lru_ttl_maybe_double_async(x: u32) -> Result<u32, String> {
if FAIL_ASYNC_LRU.load(Ordering::Relaxed) {
Err("async lru_ttl failure".to_string())
} else {
Ok(x * 2)
}
}
#[tokio::test(flavor = "multi_thread")]
async fn result_fallback_async_lru_ttl_returns_stale_ok_after_expiry() {
FAIL_ASYNC_LRU.store(false, Ordering::Relaxed);
assert_eq!(lru_ttl_maybe_double_async(5).await, Ok(10));
assert_eq!(lru_ttl_maybe_double_async(5).await, Ok(10));
sleep(Duration::from_millis(1100)).await;
FAIL_ASYNC_LRU.store(true, Ordering::Relaxed);
assert_eq!(lru_ttl_maybe_double_async(5).await, Ok(10));
let m = LRU_TTL_MAYBE_DOUBLE_ASYNC
.get()
.expect("store initialized by first call")
.metrics();
assert_eq!(m.misses, Some(2), "expected 2 misses (absent + expired)");
assert_eq!(m.hits, Some(1), "expected 1 hit (within-TTL)");
assert_eq!(
m.evictions,
Some(0),
"no eviction must occur on expired→Err→stale path"
);
assert_eq!(
lru_ttl_maybe_double_async(99).await,
Err("async lru_ttl failure".to_string())
);
FAIL_ASYNC_LRU.store(false, Ordering::Relaxed);
}
}
#[cfg(feature = "proc_macro")]
mod concurrent_cached_cache_err {
use cached::macros::concurrent_cached;
use std::sync::atomic::{AtomicUsize, Ordering};
static ERR_CALLS: AtomicUsize = AtomicUsize::new(0);
#[concurrent_cached(cache_err = true)]
fn err_double(x: u32) -> Result<u32, u32> {
ERR_CALLS.fetch_add(1, Ordering::Relaxed);
Err(x)
}
#[test]
fn cache_err_caches_error_result() {
ERR_CALLS.store(0, Ordering::Relaxed);
assert_eq!(err_double(7), Err(7));
assert_eq!(ERR_CALLS.load(Ordering::Relaxed), 1);
assert_eq!(err_double(7), Err(7));
assert_eq!(ERR_CALLS.load(Ordering::Relaxed), 1);
assert_eq!(err_double(8), Err(8));
assert_eq!(ERR_CALLS.load(Ordering::Relaxed), 2);
}
}
#[cfg(all(feature = "proc_macro", feature = "async_tokio_rt_multi_thread"))]
mod concurrent_cached_default_async {
use cached::macros::concurrent_cached;
use std::sync::atomic::{AtomicUsize, Ordering};
static SLOW_DOUBLE_ASYNC_CALLS: AtomicUsize = AtomicUsize::new(0);
#[concurrent_cached]
async fn slow_double_async(x: u64) -> u64 {
SLOW_DOUBLE_ASYNC_CALLS.fetch_add(1, Ordering::Relaxed);
x * 2
}
#[tokio::test]
async fn async_default_compiles_and_caches() {
SLOW_DOUBLE_ASYNC_CALLS.store(0, Ordering::Relaxed);
assert_eq!(slow_double_async(21).await, 42);
assert_eq!(slow_double_async(21).await, 42); assert_eq!(SLOW_DOUBLE_ASYNC_CALLS.load(Ordering::Relaxed), 1);
}
}
#[cfg(feature = "proc_macro")]
mod concurrent_cached_default_with_cached_flag {
use cached::macros::concurrent_cached;
use std::sync::atomic::{AtomicUsize, Ordering};
static FLAG_CALLS: AtomicUsize = AtomicUsize::new(0);
static PLAIN_FLAG_CALLS: AtomicUsize = AtomicUsize::new(0);
#[concurrent_cached(with_cached_flag = true)]
fn flagged_double(x: u64) -> Result<cached::Return<u64>, std::convert::Infallible> {
FLAG_CALLS.fetch_add(1, Ordering::Relaxed);
Ok(cached::Return::new(x * 2))
}
#[concurrent_cached(with_cached_flag = true)]
fn flagged_plain_double(x: u64) -> cached::Return<u64> {
PLAIN_FLAG_CALLS.fetch_add(1, Ordering::Relaxed);
cached::Return::new(x * 2)
}
#[test]
fn with_cached_flag_reports_cache_state() {
FLAG_CALLS.store(0, Ordering::Relaxed);
let first = flagged_double(7).unwrap();
assert_eq!(*first, 14);
assert!(!first.was_cached, "first call should not be cached");
let second = flagged_double(7).unwrap();
assert_eq!(*second, 14);
assert!(second.was_cached, "second call should be cached");
assert_eq!(FLAG_CALLS.load(Ordering::Relaxed), 1);
}
#[test]
fn plain_return_with_cached_flag_reports_cache_state() {
PLAIN_FLAG_CALLS.store(0, Ordering::Relaxed);
let first = flagged_plain_double(8);
assert_eq!(*first, 16);
assert!(!first.was_cached, "first call should not be cached");
let second = flagged_plain_double(8);
assert_eq!(*second, 16);
assert!(second.was_cached, "second call should be cached");
assert_eq!(PLAIN_FLAG_CALLS.load(Ordering::Relaxed), 1);
}
}
#[cfg(feature = "proc_macro")]
mod concurrent_cached_option {
use cached::macros::concurrent_cached;
use std::sync::atomic::{AtomicUsize, Ordering};
static OPT_CALLS: AtomicUsize = AtomicUsize::new(0);
static OPT_FLAG_CALLS: AtomicUsize = AtomicUsize::new(0);
#[concurrent_cached]
fn maybe_double(x: u64) -> Option<u64> {
OPT_CALLS.fetch_add(1, Ordering::Relaxed);
if x == 0 { None } else { Some(x * 2) }
}
#[concurrent_cached(with_cached_flag = true)]
fn flagged_maybe_double(x: u64) -> Option<cached::Return<u64>> {
OPT_FLAG_CALLS.fetch_add(1, Ordering::Relaxed);
if x == 0 {
None
} else {
Some(cached::Return::new(x * 2))
}
}
#[test]
fn option_caches_some_not_none() {
OPT_CALLS.store(0, Ordering::Relaxed);
assert_eq!(maybe_double(0), None);
assert_eq!(maybe_double(0), None);
assert_eq!(
OPT_CALLS.load(Ordering::Relaxed),
2,
"None should not be cached"
);
assert_eq!(maybe_double(3), Some(6));
assert_eq!(maybe_double(3), Some(6));
assert_eq!(
OPT_CALLS.load(Ordering::Relaxed),
3,
"Some should be cached after first call"
);
}
#[test]
fn option_with_cached_flag_reports_cache_state() {
OPT_FLAG_CALLS.store(0, Ordering::Relaxed);
assert!(flagged_maybe_double(0).is_none());
assert!(flagged_maybe_double(0).is_none());
assert_eq!(
OPT_FLAG_CALLS.load(Ordering::Relaxed),
2,
"None should not be cached"
);
let first = flagged_maybe_double(5).expect("should return Some");
assert_eq!(*first, 10);
assert!(!first.was_cached, "first Some call should not be cached");
let second = flagged_maybe_double(5).expect("should return Some");
assert_eq!(*second, 10);
assert!(second.was_cached, "second Some call should be cached");
assert_eq!(OPT_FLAG_CALLS.load(Ordering::Relaxed), 3);
}
}
#[cfg(all(feature = "proc_macro", feature = "async_tokio_rt_multi_thread"))]
mod concurrent_cached_async_option {
use cached::macros::concurrent_cached;
use std::sync::atomic::{AtomicUsize, Ordering};
static ASYNC_OPT_CALLS: AtomicUsize = AtomicUsize::new(0);
static ASYNC_OPT_FLAG_CALLS: AtomicUsize = AtomicUsize::new(0);
#[concurrent_cached]
async fn async_maybe_double(x: u64) -> Option<u64> {
ASYNC_OPT_CALLS.fetch_add(1, Ordering::Relaxed);
if x == 0 { None } else { Some(x * 2) }
}
#[concurrent_cached(with_cached_flag = true)]
async fn async_flagged_maybe_double(x: u64) -> Option<cached::Return<u64>> {
ASYNC_OPT_FLAG_CALLS.fetch_add(1, Ordering::Relaxed);
if x == 0 {
None
} else {
Some(cached::Return::new(x * 2))
}
}
#[tokio::test]
async fn async_option_caches_some_not_none() {
ASYNC_OPT_CALLS.store(0, Ordering::Relaxed);
assert_eq!(async_maybe_double(0).await, None);
assert_eq!(async_maybe_double(0).await, None);
assert_eq!(
ASYNC_OPT_CALLS.load(Ordering::Relaxed),
2,
"None should not be cached"
);
assert_eq!(async_maybe_double(4).await, Some(8));
assert_eq!(async_maybe_double(4).await, Some(8));
assert_eq!(
ASYNC_OPT_CALLS.load(Ordering::Relaxed),
3,
"Some should be cached after first call"
);
}
#[tokio::test]
async fn async_option_with_cached_flag_reports_cache_state() {
ASYNC_OPT_FLAG_CALLS.store(0, Ordering::Relaxed);
assert!(async_flagged_maybe_double(0).await.is_none());
assert!(async_flagged_maybe_double(0).await.is_none());
assert_eq!(
ASYNC_OPT_FLAG_CALLS.load(Ordering::Relaxed),
2,
"None should not be cached"
);
let first = async_flagged_maybe_double(6)
.await
.expect("should return Some");
assert_eq!(*first, 12);
assert!(!first.was_cached, "first Some call should not be cached");
let second = async_flagged_maybe_double(6)
.await
.expect("should return Some");
assert_eq!(*second, 12);
assert!(second.was_cached, "second Some call should be cached");
assert_eq!(ASYNC_OPT_FLAG_CALLS.load(Ordering::Relaxed), 3);
}
}
#[cfg(all(feature = "proc_macro", feature = "async_tokio_rt_multi_thread"))]
mod concurrent_cached_default_async_with_cached_flag {
use cached::macros::concurrent_cached;
use std::sync::atomic::{AtomicUsize, Ordering};
static ASYNC_FLAG_CALLS: AtomicUsize = AtomicUsize::new(0);
static ASYNC_PLAIN_FLAG_CALLS: AtomicUsize = AtomicUsize::new(0);
#[concurrent_cached(with_cached_flag = true)]
async fn async_flagged_double(x: u64) -> Result<cached::Return<u64>, std::convert::Infallible> {
ASYNC_FLAG_CALLS.fetch_add(1, Ordering::Relaxed);
Ok(cached::Return::new(x * 2))
}
#[concurrent_cached(with_cached_flag = true)]
async fn async_flagged_plain_double(x: u64) -> cached::Return<u64> {
ASYNC_PLAIN_FLAG_CALLS.fetch_add(1, Ordering::Relaxed);
cached::Return::new(x * 2)
}
#[tokio::test]
async fn async_with_cached_flag_result_reports_cache_state() {
ASYNC_FLAG_CALLS.store(0, Ordering::Relaxed);
let first = async_flagged_double(7).await.unwrap();
assert_eq!(*first, 14);
assert!(!first.was_cached, "first call should not be cached");
let second = async_flagged_double(7).await.unwrap();
assert_eq!(*second, 14);
assert!(second.was_cached, "second call should be cached");
assert_eq!(ASYNC_FLAG_CALLS.load(Ordering::Relaxed), 1);
}
#[tokio::test]
async fn async_plain_return_with_cached_flag_reports_cache_state() {
ASYNC_PLAIN_FLAG_CALLS.store(0, Ordering::Relaxed);
let first = async_flagged_plain_double(8).await;
assert_eq!(*first, 16);
assert!(!first.was_cached, "first call should not be cached");
let second = async_flagged_plain_double(8).await;
assert_eq!(*second, 16);
assert!(second.was_cached, "second call should be cached");
assert_eq!(ASYNC_PLAIN_FLAG_CALLS.load(Ordering::Relaxed), 1);
}
}
#[cfg(feature = "proc_macro")]
#[allow(dead_code)]
mod sharded_send_sync_typecheck {
fn _typecheck_sync() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<cached::ShardedCache<String, u32>>();
assert_sync::<cached::ShardedCache<String, u32>>();
assert_send::<cached::ShardedLruCache<String, u32>>();
assert_sync::<cached::ShardedLruCache<String, u32>>();
}
#[cfg(feature = "time_stores")]
fn _typecheck_sync_timed() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<cached::ShardedTtlCache<String, u32>>();
assert_sync::<cached::ShardedTtlCache<String, u32>>();
assert_send::<cached::ShardedLruTtlCache<String, u32>>();
assert_sync::<cached::ShardedLruTtlCache<String, u32>>();
}
}
#[test]
fn concurrent_cached_trait_short_aliases_work() {
use cached::{ConcurrentCached, ShardedCache};
let cache = ShardedCache::<String, u32>::builder().build().unwrap();
assert_eq!(cache.set("a".to_string(), 1).unwrap(), None);
assert_eq!(cache.get(&"a".to_string()).unwrap(), Some(1));
assert_eq!(cache.remove(&"a".to_string()).unwrap(), Some(1));
assert!(!cache.delete(&"a".to_string()).unwrap());
}
#[test]
fn cache_clear_with_on_evict_no_callback_leaves_evictions_at_zero() {
use cached::{ConcurrentCached, ShardedCache, ShardedLruCache};
let cache = ShardedCache::<u32, u32>::builder().build().unwrap();
ConcurrentCached::cache_set(&cache, 1, 10).expect("infallible ShardedCache set");
ConcurrentCached::cache_set(&cache, 2, 20).expect("infallible ShardedCache set");
cache.cache_clear_with_on_evict();
assert_eq!(cache.len(), 0, "cache should be empty after clear");
assert_eq!(cache.metrics().evictions, None);
let lru = ShardedLruCache::<u32, u32>::builder()
.max_size(64)
.build()
.unwrap();
ConcurrentCached::cache_set(&lru, 1, 10).expect("infallible ShardedLruCache set");
ConcurrentCached::cache_set(&lru, 2, 20).expect("infallible ShardedLruCache set");
lru.cache_clear_with_on_evict();
assert_eq!(
lru.metrics().evictions,
Some(0),
"evictions should remain 0 when no on_evict callback is set"
);
assert_eq!(lru.len(), 0);
}
mod sharded_expiring_tests {
#[cfg(feature = "proc_macro")]
use cached::macros::concurrent_cached;
use cached::{
CacheEvict, ConcurrentCached, Expires, ShardedExpiringCache, ShardedExpiringLruCache,
};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
#[derive(Clone, Debug)]
struct ExpiringItem {
val: u32,
expired: Arc<AtomicBool>,
}
impl Expires for ExpiringItem {
fn is_expired(&self) -> bool {
self.expired.load(Ordering::Relaxed)
}
}
#[test]
fn sharded_expiring_cache_basic_ops() {
let flag1 = Arc::new(AtomicBool::new(false));
let flag2 = Arc::new(AtomicBool::new(true));
let cache = ShardedExpiringCache::<u32, ExpiringItem>::builder()
.build()
.unwrap();
let _ = ConcurrentCached::cache_set(
&cache,
1,
ExpiringItem {
val: 42,
expired: flag1.clone(),
},
);
let _ = ConcurrentCached::cache_set(
&cache,
2,
ExpiringItem {
val: 99,
expired: flag2.clone(),
},
);
assert_eq!(
ConcurrentCached::cache_get(&cache, &1)
.unwrap()
.map(|i| i.val),
Some(42)
);
assert_eq!(
ConcurrentCached::cache_get(&cache, &2)
.unwrap()
.map(|i| i.val),
None
); assert_eq!(cache.metrics().misses, Some(1));
assert_eq!(cache.metrics().hits, Some(1));
let lru = ShardedExpiringLruCache::<u32, ExpiringItem>::builder()
.max_size(64)
.build()
.unwrap();
let _ = ConcurrentCached::cache_set(
&lru,
1,
ExpiringItem {
val: 42,
expired: flag1.clone(),
},
);
let _ = ConcurrentCached::cache_set(
&lru,
2,
ExpiringItem {
val: 99,
expired: flag2.clone(),
},
);
assert_eq!(
ConcurrentCached::cache_get(&lru, &1)
.unwrap()
.map(|i| i.val),
Some(42)
);
assert_eq!(
ConcurrentCached::cache_get(&lru, &2)
.unwrap()
.map(|i| i.val),
None
); assert_eq!(lru.metrics().misses, Some(1));
assert_eq!(lru.metrics().hits, Some(1));
}
#[test]
fn sharded_expiring_cache_evict() {
let flag = Arc::new(AtomicBool::new(true));
let mut cache = ShardedExpiringCache::<u32, ExpiringItem>::builder()
.build()
.unwrap();
let _ = ConcurrentCached::cache_set(
&cache,
1,
ExpiringItem {
val: 42,
expired: flag.clone(),
},
);
let _ = ConcurrentCached::cache_set(
&cache,
2,
ExpiringItem {
val: 99,
expired: flag.clone(),
},
);
assert_eq!(cache.len(), 2);
let evicted = CacheEvict::evict(&mut cache);
assert_eq!(evicted, 2);
assert_eq!(cache.len(), 0);
let mut lru = ShardedExpiringLruCache::<u32, ExpiringItem>::builder()
.max_size(64)
.build()
.unwrap();
let _ = ConcurrentCached::cache_set(
&lru,
1,
ExpiringItem {
val: 42,
expired: flag.clone(),
},
);
let _ = ConcurrentCached::cache_set(
&lru,
2,
ExpiringItem {
val: 99,
expired: flag.clone(),
},
);
assert_eq!(lru.len(), 2);
let evicted_lru = CacheEvict::evict(&mut lru);
assert_eq!(evicted_lru, 2);
assert_eq!(lru.len(), 0);
}
#[test]
fn sharded_expiring_evict_fires_on_evict_and_increments_evictions_counter() {
use std::sync::atomic::AtomicU32;
let flag = Arc::new(AtomicBool::new(true));
let fired = Arc::new(AtomicU32::new(0));
let fired_clone = fired.clone();
let cache = ShardedExpiringCache::<u32, ExpiringItem>::builder()
.on_evict(move |_k, _v| {
fired_clone.fetch_add(1, Ordering::Relaxed);
})
.build()
.unwrap();
let _ = cache.cache_set(
1,
ExpiringItem {
val: 10,
expired: flag.clone(),
},
);
let _ = cache.cache_set(
2,
ExpiringItem {
val: 20,
expired: flag.clone(),
},
);
assert_eq!(cache.evict(), 2);
assert_eq!(fired.load(Ordering::Relaxed), 2);
assert_eq!(cache.metrics().evictions, Some(2));
let fired_lru = Arc::new(AtomicU32::new(0));
let fired_lru_clone = fired_lru.clone();
let lru = ShardedExpiringLruCache::<u32, ExpiringItem>::builder()
.max_size(64)
.on_evict(move |_k, _v| {
fired_lru_clone.fetch_add(1, Ordering::Relaxed);
})
.build()
.unwrap();
let _ = lru.cache_set(
1,
ExpiringItem {
val: 10,
expired: flag.clone(),
},
);
let _ = lru.cache_set(
2,
ExpiringItem {
val: 20,
expired: flag.clone(),
},
);
assert_eq!(lru.evict(), 2);
assert_eq!(fired_lru.load(Ordering::Relaxed), 2);
assert_eq!(lru.metrics().evictions, Some(2));
}
#[cfg(feature = "proc_macro")]
static BARE_EXPIRES_CALLS: AtomicUsize = AtomicUsize::new(0);
#[cfg(feature = "proc_macro")]
#[concurrent_cached(expires = true, key = "u32", convert = r#"{ x }"#)]
fn get_expiring_item(x: u32, flag: Arc<AtomicBool>) -> ExpiringItem {
BARE_EXPIRES_CALLS.fetch_add(1, Ordering::Relaxed);
ExpiringItem {
val: x * 10,
expired: flag,
}
}
#[cfg(feature = "proc_macro")]
static BARE_EXPIRES_LRU_CALLS: AtomicUsize = AtomicUsize::new(0);
#[cfg(feature = "proc_macro")]
#[concurrent_cached(expires = true, max_size = 64, key = "u32", convert = r#"{ x }"#)]
fn get_expiring_item_lru(x: u32, flag: Arc<AtomicBool>) -> ExpiringItem {
BARE_EXPIRES_LRU_CALLS.fetch_add(1, Ordering::Relaxed);
ExpiringItem {
val: x * 10,
expired: flag,
}
}
#[cfg(feature = "proc_macro")]
#[test]
fn concurrent_cached_expires_unbounded() {
BARE_EXPIRES_CALLS.store(0, Ordering::Relaxed);
let flag = Arc::new(AtomicBool::new(false));
let res1 = get_expiring_item(5, flag.clone());
assert_eq!(res1.val, 50);
let res2 = get_expiring_item(5, flag.clone());
assert_eq!(res2.val, 50);
assert_eq!(BARE_EXPIRES_CALLS.load(Ordering::Relaxed), 1);
flag.store(true, Ordering::Relaxed);
let res3 = get_expiring_item(5, flag.clone());
assert_eq!(res3.val, 50);
assert_eq!(BARE_EXPIRES_CALLS.load(Ordering::Relaxed), 2); }
#[cfg(feature = "proc_macro")]
#[test]
fn concurrent_cached_expires_lru() {
BARE_EXPIRES_LRU_CALLS.store(0, Ordering::Relaxed);
let flag = Arc::new(AtomicBool::new(false));
let res1 = get_expiring_item_lru(5, flag.clone());
assert_eq!(res1.val, 50);
let res2 = get_expiring_item_lru(5, flag.clone());
assert_eq!(res2.val, 50);
assert_eq!(BARE_EXPIRES_LRU_CALLS.load(Ordering::Relaxed), 1);
flag.store(true, Ordering::Relaxed);
let res3 = get_expiring_item_lru(5, flag.clone());
assert_eq!(res3.val, 50);
assert_eq!(BARE_EXPIRES_LRU_CALLS.load(Ordering::Relaxed), 2); }
#[test]
fn sharded_expiring_lru_on_evict_fires_on_lru_capacity_pressure() {
let evict_count = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let evict_count2 = evict_count.clone();
let not_expired = Arc::new(AtomicBool::new(false));
let cache = ShardedExpiringLruCache::<u32, ExpiringItem>::builder()
.max_size(8)
.shards(1)
.on_evict(move |_k, _v| {
evict_count2.fetch_add(1, Ordering::Relaxed);
})
.build()
.unwrap();
for i in 0..16 {
let _ = ConcurrentCached::cache_set(
&cache,
i,
ExpiringItem {
val: i,
expired: not_expired.clone(),
},
);
}
assert!(
evict_count.load(Ordering::Relaxed) >= 8,
"expected on_evict to fire for LRU evictions, got {}",
evict_count.load(Ordering::Relaxed)
);
let total_evictions = cache.metrics().evictions.unwrap_or(0);
assert!(
total_evictions >= 8,
"expected metrics().evictions >= 8, got {}",
total_evictions
);
}
#[test]
fn sharded_expiring_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<ShardedExpiringCache<u32, ExpiringItem>>();
assert_send_sync::<ShardedExpiringLruCache<u32, ExpiringItem>>();
}
#[cfg(feature = "proc_macro")]
mod concurrent_cached_expires_with_cached_flag {
use super::*;
use cached::macros::concurrent_cached;
use std::sync::atomic::AtomicUsize;
static EXPIRES_FLAG_CALLS: AtomicUsize = AtomicUsize::new(0);
#[concurrent_cached(
expires = true,
key = "u32",
convert = r#"{ x }"#,
with_cached_flag = true
)]
fn get_flagged_expiring(
x: u32,
expired: Arc<AtomicBool>,
) -> Result<cached::Return<ExpiringItem>, std::convert::Infallible> {
EXPIRES_FLAG_CALLS.fetch_add(1, Ordering::Relaxed);
Ok(cached::Return::new(ExpiringItem { val: x, expired }))
}
#[test]
fn expires_with_cached_flag_reports_cache_state() {
EXPIRES_FLAG_CALLS.store(0, Ordering::Relaxed);
let flag = Arc::new(AtomicBool::new(false));
let r1 = get_flagged_expiring(42, flag.clone()).unwrap();
assert!(!r1.was_cached, "first call should not be cached");
assert_eq!(r1.val, 42);
let r2 = get_flagged_expiring(42, flag.clone()).unwrap();
assert!(r2.was_cached, "second call should be a cache hit");
assert_eq!(EXPIRES_FLAG_CALLS.load(Ordering::Relaxed), 1);
flag.store(true, Ordering::Relaxed);
let r3 = get_flagged_expiring(42, flag.clone()).unwrap();
assert!(!r3.was_cached, "call after expiry should not be cached");
assert_eq!(EXPIRES_FLAG_CALLS.load(Ordering::Relaxed), 2);
}
}
}
#[cfg(feature = "redis_store")]
#[allow(dead_code)]
mod redis_not_sync_typecheck {
#[derive(serde::Serialize, serde::Deserialize, Clone)]
struct NotSyncValue {
c: std::cell::Cell<u32>,
}
fn _typecheck_sync() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<cached::RedisCache<String, NotSyncValue>>();
assert_sync::<cached::RedisCache<String, NotSyncValue>>();
}
#[cfg(any(feature = "redis_tokio", feature = "redis_smol"))]
fn _typecheck_async() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<cached::AsyncRedisCache<String, NotSyncValue>>();
assert_sync::<cached::AsyncRedisCache<String, NotSyncValue>>();
}
}
#[cfg(feature = "proc_macro")]
mod concurrent_cached_return_named_error {
use cached::macros::concurrent_cached;
#[derive(Debug, PartialEq)]
struct Return;
struct Store(std::sync::Mutex<std::collections::HashMap<String, String>>);
impl Store {
fn new() -> Self {
Self(std::sync::Mutex::new(std::collections::HashMap::new()))
}
}
impl cached::ConcurrentCached<String, String> for Store {
type Error = std::convert::Infallible;
fn cache_get(&self, k: &String) -> Result<Option<String>, Self::Error> {
Ok(self.0.lock().unwrap().get(k).cloned())
}
fn cache_set(&self, k: String, v: String) -> Result<Option<String>, Self::Error> {
Ok(self.0.lock().unwrap().insert(k, v))
}
fn cache_remove(&self, k: &String) -> Result<Option<String>, Self::Error> {
Ok(self.0.lock().unwrap().remove(k))
}
fn cache_remove_entry(&self, k: &String) -> Result<Option<(String, String)>, Self::Error> {
Ok(self.0.lock().unwrap().remove_entry(k))
}
fn set_refresh_on_hit(&self, _r: bool) -> bool {
false
}
}
#[concurrent_cached(
ty = "Store",
create = "{ Store::new() }",
key = "String",
convert = r#"{ k.to_string() }"#,
map_error = r#"|_e| Return"#
)]
fn fetch(k: u32) -> Result<String, Return> {
Ok(k.to_string())
}
#[test]
fn return_named_error_compiles_and_caches() {
assert_eq!(fetch(1), Ok("1".to_string()));
assert_eq!(fetch(1), Ok("1".to_string())); assert_eq!(fetch(2), Ok("2".to_string()));
}
}
#[cfg(all(feature = "redis_store", feature = "proc_macro"))]
mod redis_tests {
use super::*;
use cached::RedisCache;
use cached::macros::concurrent_cached;
use thiserror::Error;
#[derive(Error, Debug, PartialEq, Clone)]
enum TestError {
#[error("error with redis cache `{0}`")]
RedisError(String),
#[error("count `{0}`")]
Count(u32),
}
#[concurrent_cached(
redis = true,
ttl = 1,
cache_prefix_block = "{ \"__cached_redis_proc_macro_test_fn_cached_redis\" }",
map_error = r##"|e| TestError::RedisError(format!("{:?}", e))"##
)]
fn cached_redis(n: u32) -> Result<u32, TestError> {
if n < 5 {
Ok(n)
} else {
Err(TestError::Count(n))
}
}
#[test]
fn test_cached_redis() {
assert_eq!(cached_redis(1), Ok(1));
assert_eq!(cached_redis(1), Ok(1));
assert_eq!(cached_redis(5), Err(TestError::Count(5)));
assert_eq!(cached_redis(6), Err(TestError::Count(6)));
}
#[concurrent_cached(
redis = true,
ttl = 1,
with_cached_flag = true,
map_error = r##"|e| TestError::RedisError(format!("{:?}", e))"##
)]
fn cached_redis_cached_flag(n: u32) -> Result<cached::Return<u32>, TestError> {
if n < 5 {
Ok(cached::Return::new(n))
} else {
Err(TestError::Count(n))
}
}
#[test]
fn test_cached_redis_cached_flag() {
assert!(!cached_redis_cached_flag(1).unwrap().was_cached);
assert!(cached_redis_cached_flag(1).unwrap().was_cached);
assert!(cached_redis_cached_flag(5).is_err());
assert!(cached_redis_cached_flag(6).is_err());
}
#[concurrent_cached(
map_error = r##"|e| TestError::RedisError(format!("{:?}", e))"##,
ty = "cached::RedisCache<u32, u32>",
create = r##" { RedisCache::new("cache_redis_test_cache_create", Duration::from_secs(1)).refresh(true).build().expect("error building redis cache") } "##
)]
fn cached_redis_cache_create(n: u32) -> Result<u32, TestError> {
if n < 5 {
Ok(n)
} else {
Err(TestError::Count(n))
}
}
#[test]
fn test_cached_redis_cache_create() {
assert_eq!(cached_redis_cache_create(1), Ok(1));
assert_eq!(cached_redis_cache_create(1), Ok(1));
assert_eq!(cached_redis_cache_create(5), Err(TestError::Count(5)));
assert_eq!(cached_redis_cache_create(6), Err(TestError::Count(6)));
}
#[cfg(any(feature = "redis_smol", feature = "redis_tokio"))]
mod async_redis_tests {
use super::*;
#[concurrent_cached(
redis = true,
ttl = 1,
cache_prefix_block = "{ \"__cached_redis_proc_macro_test_fn_async_cached_redis\" }",
map_error = r##"|e| TestError::RedisError(format!("{:?}", e))"##
)]
async fn async_cached_redis(n: u32) -> Result<u32, TestError> {
if n < 5 {
Ok(n)
} else {
Err(TestError::Count(n))
}
}
#[tokio::test]
async fn test_async_cached_redis() {
assert_eq!(async_cached_redis(1).await, Ok(1));
assert_eq!(async_cached_redis(1).await, Ok(1));
assert_eq!(async_cached_redis(5).await, Err(TestError::Count(5)));
assert_eq!(async_cached_redis(6).await, Err(TestError::Count(6)));
}
#[concurrent_cached(
redis = true,
ttl = 1,
with_cached_flag = true,
map_error = r##"|e| TestError::RedisError(format!("{:?}", e))"##
)]
async fn async_cached_redis_cached_flag(n: u32) -> Result<cached::Return<u32>, TestError> {
if n < 5 {
Ok(cached::Return::new(n))
} else {
Err(TestError::Count(n))
}
}
#[tokio::test]
async fn test_async_cached_redis_cached_flag() {
assert!(!async_cached_redis_cached_flag(1).await.unwrap().was_cached);
assert!(async_cached_redis_cached_flag(1).await.unwrap().was_cached,);
assert!(async_cached_redis_cached_flag(5).await.is_err());
assert!(async_cached_redis_cached_flag(6).await.is_err());
}
use cached::AsyncRedisCache;
#[concurrent_cached(
map_error = r##"|e| TestError::RedisError(format!("{:?}", e))"##,
ty = "cached::AsyncRedisCache<u32, u32>",
create = r##" { AsyncRedisCache::new("async_cached_redis_test_cache_create", Duration::from_secs(1)).refresh(true).build().await.expect("error building async redis cache") } "##
)]
async fn async_cached_redis_cache_create(n: u32) -> Result<u32, TestError> {
if n < 5 {
Ok(n)
} else {
Err(TestError::Count(n))
}
}
#[tokio::test]
async fn test_async_cached_redis_cache_create() {
assert_eq!(async_cached_redis_cache_create(1).await, Ok(1));
assert_eq!(async_cached_redis_cache_create(1).await, Ok(1));
assert_eq!(
async_cached_redis_cache_create(5).await,
Err(TestError::Count(5))
);
assert_eq!(
async_cached_redis_cache_create(6).await,
Err(TestError::Count(6))
);
}
#[tokio::test]
async fn async_redis_builder_aliases_and_zero_ttl_validation() {
let result = cached::AsyncRedisCache::<String, String>::builder(
"async-zero-ttl",
Duration::ZERO,
)
.build()
.await;
assert!(matches!(
result,
Err(cached::RedisCacheBuildError::InvalidTtl(..))
));
}
}
}
#[cfg(feature = "proc_macro")]
#[derive(Clone)]
pub struct NewsArticle {
slug: String,
is_expired: bool,
}
#[cfg(feature = "proc_macro")]
impl Expires for NewsArticle {
fn is_expired(&self) -> bool {
self.is_expired
}
}
#[cfg(feature = "proc_macro")]
const EXPIRED_SLUG: &str = "expired_slug";
#[cfg(feature = "proc_macro")]
const UNEXPIRED_SLUG: &str = "unexpired_slug";
#[cfg(feature = "proc_macro")]
#[cached(
ty = "ExpiringLruCache<String, NewsArticle>",
create = "{ ExpiringLruCache::builder().max_size(3).build().unwrap() }"
)]
fn fetch_article(slug: String) -> Result<NewsArticle, ()> {
match slug.as_str() {
EXPIRED_SLUG => Ok(NewsArticle {
slug: String::from(EXPIRED_SLUG),
is_expired: true,
}),
UNEXPIRED_SLUG => Ok(NewsArticle {
slug: String::from(UNEXPIRED_SLUG),
is_expired: false,
}),
_ => Err(()),
}
}
#[cfg(feature = "proc_macro")]
#[test]
#[serial(ExpiringCacheTest)]
fn test_expiring_value_expired_article_returned_with_miss() {
{
let mut cache = FETCH_ARTICLE.write();
cache.cache_reset();
cache.cache_reset_metrics();
}
let expired_article = fetch_article(EXPIRED_SLUG.to_string());
assert!(expired_article.is_ok());
assert_eq!(EXPIRED_SLUG, expired_article.unwrap().slug.as_str());
{
let cache = FETCH_ARTICLE.write();
assert_eq!(1, cache.cache_size());
assert_eq!(cache.cache_hits(), Some(0));
assert_eq!(cache.cache_misses(), Some(1));
}
let _ = fetch_article(EXPIRED_SLUG.to_string());
{
let cache = FETCH_ARTICLE.write();
assert_eq!(1, cache.cache_size());
assert_eq!(cache.cache_hits(), Some(0));
assert_eq!(cache.cache_misses(), Some(2));
}
}
#[cfg(feature = "proc_macro")]
#[test]
#[serial(ExpiringCacheTest)]
fn test_expiring_value_unexpired_article_returned_with_hit() {
{
let mut cache = FETCH_ARTICLE.write();
cache.cache_reset();
cache.cache_reset_metrics();
}
let unexpired_article = fetch_article(UNEXPIRED_SLUG.to_string());
assert!(unexpired_article.is_ok());
assert_eq!(UNEXPIRED_SLUG, unexpired_article.unwrap().slug.as_str());
{
let cache = FETCH_ARTICLE.write();
assert_eq!(1, cache.cache_size());
assert_eq!(cache.cache_hits(), Some(0));
assert_eq!(cache.cache_misses(), Some(1));
}
let cached_article = fetch_article(UNEXPIRED_SLUG.to_string());
assert!(cached_article.is_ok());
assert_eq!(UNEXPIRED_SLUG, cached_article.unwrap().slug.as_str());
{
let cache = FETCH_ARTICLE.write();
assert_eq!(1, cache.cache_size());
assert_eq!(cache.cache_hits(), Some(1));
assert_eq!(cache.cache_misses(), Some(1));
}
}
#[test]
fn test_sized_cache_on_evict() {
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
let evicted_count = Arc::new(AtomicU32::new(0));
let evicted_count_clone = evicted_count.clone();
let mut cache = cached::LruCache::builder()
.max_size(2)
.on_evict(move |_k, _v| {
evicted_count_clone.fetch_add(1, Ordering::Relaxed);
})
.build()
.unwrap();
cache.set(1, 10);
cache.set(2, 20);
assert_eq!(evicted_count.load(Ordering::Relaxed), 0);
cache.set(3, 30);
assert_eq!(evicted_count.load(Ordering::Relaxed), 1);
}
#[test]
#[cfg(feature = "time_stores")]
fn test_timed_cache_on_evict() {
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
let evicted_count = Arc::new(AtomicU32::new(0));
let evicted_count_clone = evicted_count.clone();
let mut cache = cached::TtlCache::builder()
.ttl(cached::time::Duration::from_millis(100))
.on_evict(move |_k, _v| {
evicted_count_clone.fetch_add(1, Ordering::Relaxed);
})
.build()
.unwrap();
cache.set(1, 10);
assert_eq!(evicted_count.load(Ordering::Relaxed), 0);
std::thread::sleep(cached::time::Duration::from_millis(200));
assert_eq!(cache.evict(), 1);
assert_eq!(evicted_count.load(Ordering::Relaxed), 1);
assert_eq!(cache.cache_evictions(), Some(1));
}
#[test]
#[cfg(feature = "time_stores")]
fn test_cache_evict_trait_returns_count() {
use cached::CacheEvict;
let mut cache = cached::TtlCache::builder()
.ttl(cached::time::Duration::from_millis(20))
.build()
.unwrap();
cache.cache_set(1, 10);
cache.cache_set(2, 20);
std::thread::sleep(cached::time::Duration::from_millis(40));
assert_eq!(CacheEvict::evict(&mut cache), 2);
assert_eq!(cache.cache_size(), 0);
assert_eq!(cache.cache_evictions(), Some(2));
}
#[test]
#[cfg(feature = "time_stores")]
fn test_expiring_sized_cache_on_evict() {
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
let evicted_count = Arc::new(AtomicU32::new(0));
let evicted_count_clone = evicted_count.clone();
let mut cache = cached::stores::TtlSortedCache::builder()
.max_size(2)
.ttl(cached::time::Duration::from_secs(10))
.on_evict(move |_k, _v| {
evicted_count_clone.fetch_add(1, Ordering::Relaxed);
})
.build()
.unwrap();
cache.set(1, 10);
cache.set(2, 20);
assert_eq!(evicted_count.load(Ordering::Relaxed), 0);
cache.set(3, 30);
assert_eq!(evicted_count.load(Ordering::Relaxed), 1);
}
#[test]
#[cfg(feature = "time_stores")]
fn test_timed_sized_expired_get_does_not_pollute_inner_metrics() {
let mut cache = cached::LruTtlCache::builder()
.max_size(2)
.ttl(cached::time::Duration::from_millis(20))
.build()
.unwrap();
cache.cache_set(1, 10);
cache.cache_set(2, 20);
cache.cache_reset_metrics();
std::thread::sleep(cached::time::Duration::from_millis(40));
assert!(cache.cache_get(&1).is_none());
assert_eq!(cache.cache_hits(), Some(0));
assert_eq!(cache.cache_misses(), Some(1));
assert_eq!(cache.store().cache_hits(), Some(0));
assert_eq!(cache.store().cache_misses(), Some(0));
}
#[test]
#[cfg(feature = "time_stores")]
fn test_timed_sized_cache_expired_get_or_set_invokes_on_evict() {
use cached::Cached;
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
let evicted_count = Arc::new(AtomicU32::new(0));
let evicted_count_clone = evicted_count.clone();
let mut cache = cached::LruTtlCache::builder()
.max_size(4)
.ttl(cached::time::Duration::from_millis(50))
.on_evict(move |_k, _v| {
evicted_count_clone.fetch_add(1, Ordering::Relaxed);
})
.build()
.unwrap();
cache.cache_set(1, 10);
assert_eq!(evicted_count.load(Ordering::Relaxed), 0);
assert_eq!(cache.cache_evictions(), Some(0));
std::thread::sleep(cached::time::Duration::from_millis(100));
let val = cache.cache_get_or_set_with(1, || 99);
assert_eq!(*val, 99);
assert_eq!(
evicted_count.load(Ordering::Relaxed),
1,
"on_evict must fire for expired replacement"
);
assert!(
cache.cache_evictions().unwrap() >= 1,
"cache_evictions must be at least 1 after expired replacement"
);
}
#[test]
fn test_expiring_value_cache_expired_get_or_set_invokes_on_evict() {
use cached::Cached;
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
#[derive(Clone)]
struct Expirable {
value: i32,
expired: bool,
}
impl cached::Expires for Expirable {
fn is_expired(&self) -> bool {
self.expired
}
}
let evicted_count = Arc::new(AtomicU32::new(0));
let evicted_count_clone = evicted_count.clone();
let mut cache = cached::ExpiringLruCache::builder()
.max_size(4)
.on_evict(move |_k: &i32, _v: &Expirable| {
evicted_count_clone.fetch_add(1, Ordering::Relaxed);
})
.build()
.unwrap();
cache.cache_set(
1,
Expirable {
value: 10,
expired: true,
},
);
assert_eq!(evicted_count.load(Ordering::Relaxed), 0);
assert_eq!(cache.cache_evictions(), Some(0));
let val = cache.cache_get_or_set_with(1, || Expirable {
value: 99,
expired: false,
});
assert_eq!(val.value, 99);
assert_eq!(
evicted_count.load(Ordering::Relaxed),
1,
"on_evict must fire for expired replacement"
);
assert!(
cache.cache_evictions().unwrap() >= 1,
"cache_evictions must be at least 1 after expired replacement"
);
}
#[test]
fn test_expiring_value_cache_get_mut_expired_invokes_on_evict() {
use cached::Cached;
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
#[derive(Clone)]
struct Expirable {
expired: bool,
}
impl cached::Expires for Expirable {
fn is_expired(&self) -> bool {
self.expired
}
}
let evicted_count = Arc::new(AtomicU32::new(0));
let evicted_count_clone = evicted_count.clone();
let mut cache = cached::ExpiringLruCache::builder()
.max_size(4)
.on_evict(move |_k: &i32, _v: &Expirable| {
evicted_count_clone.fetch_add(1, Ordering::Relaxed);
})
.build()
.unwrap();
cache.cache_set(1, Expirable { expired: true });
assert!(cache.cache_get_mut(&1).is_none());
assert_eq!(evicted_count.load(Ordering::Relaxed), 1);
assert_eq!(cache.cache_evictions(), Some(1));
}
#[test]
fn test_fallible_builders_return_build_error() {
struct Expirable;
impl cached::Expires for Expirable {
fn is_expired(&self) -> bool {
false
}
}
let sized = cached::LruCache::<i32, i32>::builder().build();
assert!(
matches!(
sized.unwrap_err(),
cached::BuildError::MissingRequired("max_size")
),
"expected MissingRequired(max_size)"
);
let expiring = cached::ExpiringLruCache::<i32, Expirable>::builder().build();
assert!(
matches!(
expiring.unwrap_err(),
cached::BuildError::MissingRequired("max_size")
),
"expected MissingRequired(max_size)"
);
#[cfg(feature = "time_stores")]
{
let timed = cached::TtlCache::<i32, i32>::builder().build();
assert!(
matches!(
timed.unwrap_err(),
cached::BuildError::MissingRequired("ttl")
),
"expected MissingRequired(ttl)"
);
let timed_sized = cached::LruTtlCache::<i32, i32>::builder()
.ttl(cached::time::Duration::from_secs(1))
.build();
assert!(
matches!(
timed_sized.unwrap_err(),
cached::BuildError::MissingRequired("max_size")
),
"expected MissingRequired(max_size)"
);
let zero_ttl = cached::TtlCache::<i32, i32>::builder()
.ttl(cached::time::Duration::ZERO)
.build();
assert!(
matches!(zero_ttl.unwrap_err(), cached::BuildError::InvalidTtl { .. }),
"expected InvalidTtl"
);
let zero_lru_ttl = cached::LruTtlCache::<i32, i32>::builder()
.max_size(4)
.ttl(cached::time::Duration::ZERO)
.build();
assert!(
matches!(
zero_lru_ttl.unwrap_err(),
cached::BuildError::InvalidTtl { .. }
),
"expected InvalidTtl"
);
let zero_sorted_ttl = cached::TtlSortedCache::<i32, i32>::builder()
.ttl(cached::time::Duration::ZERO)
.build();
assert!(
matches!(
zero_sorted_ttl.unwrap_err(),
cached::BuildError::InvalidTtl { .. }
),
"expected InvalidTtl"
);
}
let sharded_unbound = cached::ShardedCache::<i32, i32>::builder()
.shards(0)
.build();
assert!(
matches!(
sharded_unbound.unwrap_err(),
cached::BuildError::InvalidValue {
field: "shards",
..
}
),
"expected InvalidValue(shards) for shards(0)"
);
}
#[cfg(feature = "disk_store")]
#[test]
fn disk_cache_builder_aliases_and_zero_ttl_validation() {
let result = cached::DiskCache::<String, String>::builder("zero-ttl")
.ttl(cached::time::Duration::ZERO)
.build();
assert!(matches!(
result,
Err(cached::DiskCacheBuildError::InvalidTtl(..))
));
}
#[cfg(feature = "redis_store")]
#[test]
fn redis_cache_builder_aliases_and_zero_ttl_validation() {
let result =
cached::RedisCache::<String, String>::builder("zero-ttl", cached::time::Duration::ZERO)
.build();
assert!(matches!(
result,
Err(cached::RedisCacheBuildError::InvalidTtl(..))
));
}
#[test]
fn test_expiring_value_cache_get_does_not_promote_expired_key() {
use cached::{Cached, CachedPeek};
#[derive(Clone)]
struct Expirable {
expired: bool,
}
impl cached::Expires for Expirable {
fn is_expired(&self) -> bool {
self.expired
}
}
let mut cache = cached::ExpiringLruCache::builder()
.max_size(2)
.build()
.unwrap();
cache.cache_set(1, Expirable { expired: false }); cache.cache_set(2, Expirable { expired: true });
assert!(cache.cache_get(&2).is_none());
cache.cache_set(3, Expirable { expired: false });
assert!(
cache.cache_peek(&1).is_some(),
"live entry must survive after expired entry is removed by cache_get"
);
}
#[test]
#[cfg(feature = "time_stores")]
fn test_timed_cache_on_evict_fires_on_cache_get() {
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
let evicted_count = Arc::new(AtomicU32::new(0));
let evicted_count_clone = evicted_count.clone();
let mut cache = cached::TtlCache::builder()
.ttl(cached::time::Duration::from_millis(50))
.on_evict(move |_k: &u32, _v: &u32| {
evicted_count_clone.fetch_add(1, Ordering::Relaxed);
})
.build()
.unwrap();
cache.cache_set(1, 10);
std::thread::sleep(cached::time::Duration::from_millis(100));
assert!(cache.cache_get(&1).is_none());
assert_eq!(
evicted_count.load(Ordering::Relaxed),
1,
"on_evict must fire when cache_get encounters an expired entry"
);
assert_eq!(cache.cache_evictions(), Some(1));
}
#[test]
#[cfg(feature = "time_stores")]
fn test_timed_cache_on_evict_fires_on_cache_get_or_set() {
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
let evicted_count = Arc::new(AtomicU32::new(0));
let evicted_count_clone = evicted_count.clone();
let mut cache = cached::TtlCache::builder()
.ttl(cached::time::Duration::from_millis(50))
.on_evict(move |_k: &u32, _v: &u32| {
evicted_count_clone.fetch_add(1, Ordering::Relaxed);
})
.build()
.unwrap();
cache.cache_set(1, 10);
std::thread::sleep(cached::time::Duration::from_millis(100));
let val = cache.cache_get_or_set_with(1, || 99);
assert_eq!(*val, 99);
assert_eq!(
evicted_count.load(Ordering::Relaxed),
1,
"on_evict must fire when cache_get_or_set_with replaces an expired entry"
);
assert_eq!(cache.cache_evictions(), Some(1));
}
#[cfg(all(feature = "time_stores", feature = "async"))]
#[tokio::test]
async fn test_timed_cache_async_on_evict_fires() {
use cached::CachedAsync;
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
let evicted_count = Arc::new(AtomicU32::new(0));
let evicted_count_clone = evicted_count.clone();
let mut cache = cached::TtlCache::builder()
.ttl(cached::time::Duration::from_millis(50))
.on_evict(move |_k: &u32, _v: &u32| {
evicted_count_clone.fetch_add(1, Ordering::Relaxed);
})
.build()
.unwrap();
cache.cache_set(1, 10);
tokio::time::sleep(cached::time::Duration::from_millis(100)).await;
let val = CachedAsync::async_get_or_set_with(&mut cache, 1, || async { 99u32 }).await;
assert_eq!(*val, 99);
assert_eq!(
evicted_count.load(Ordering::Relaxed),
1,
"on_evict must fire in async get_or_set_with when replacing an expired entry"
);
assert_eq!(cache.cache_evictions(), Some(1));
}
#[test]
#[cfg(feature = "time_stores")]
fn test_expiring_sized_cache_get_evicts_expired_and_fires_on_evict() {
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
let evicted_count = Arc::new(AtomicU32::new(0));
let evicted_count_clone = evicted_count.clone();
let mut cache = cached::stores::TtlSortedCache::builder()
.max_size(4)
.ttl(cached::time::Duration::from_millis(50))
.on_evict(move |_k: &u32, _v: &u32| {
evicted_count_clone.fetch_add(1, Ordering::Relaxed);
})
.build()
.unwrap();
cache.set(1, 10);
cache.set(2, 20);
std::thread::sleep(cached::time::Duration::from_millis(100));
assert!(cache.cache_get(&1).is_none());
assert_eq!(
evicted_count.load(Ordering::Relaxed),
1,
"on_evict must fire when cache_get encounters an expired TtlSortedCache entry"
);
assert_eq!(cache.cache_evictions(), Some(1));
assert_eq!(
cache.cache_size(),
1,
"expired entry must be removed from map"
);
}
#[test]
#[cfg(feature = "time_stores")]
fn test_timed_sized_cache_on_evict_fires_on_cache_get() {
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
let evicted_count = Arc::new(AtomicU32::new(0));
let evicted_count_clone = evicted_count.clone();
let mut cache = cached::LruTtlCache::builder()
.max_size(4)
.ttl(cached::time::Duration::from_millis(50))
.on_evict(move |_k: &u32, _v: &u32| {
evicted_count_clone.fetch_add(1, Ordering::Relaxed);
})
.build()
.unwrap();
cache.cache_set(1, 10);
std::thread::sleep(cached::time::Duration::from_millis(100));
assert!(cache.cache_get(&1).is_none());
assert_eq!(
evicted_count.load(Ordering::Relaxed),
1,
"on_evict must fire when LruTtlCache::cache_get encounters an expired entry"
);
assert_eq!(cache.cache_evictions(), Some(1));
}
#[test]
fn test_expiring_value_cache_on_evict_fires_on_cache_get() {
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
#[derive(Clone)]
struct Expirable {
expired: bool,
}
impl cached::Expires for Expirable {
fn is_expired(&self) -> bool {
self.expired
}
}
let evicted_count = Arc::new(AtomicU32::new(0));
let evicted_count_clone = evicted_count.clone();
let mut cache = cached::ExpiringLruCache::builder()
.max_size(4)
.on_evict(move |_k: &i32, _v: &Expirable| {
evicted_count_clone.fetch_add(1, Ordering::Relaxed);
})
.build()
.unwrap();
cache.cache_set(1, Expirable { expired: true });
assert_eq!(evicted_count.load(Ordering::Relaxed), 0);
assert!(cache.cache_get(&1).is_none());
assert_eq!(
evicted_count.load(Ordering::Relaxed),
1,
"on_evict must fire when ExpiringLruCache::cache_get encounters an expired entry"
);
assert_eq!(cache.cache_evictions(), Some(1));
}
#[cfg(feature = "proc_macro")]
#[test]
fn test_unsync_reads_unbound_cache() {
UNSYNC_DOUBLE.write().cache_reset();
assert_eq!(4, unsync_double(2));
assert_eq!(4, unsync_double(2));
assert_eq!(10, unsync_double(5));
let cache = UNSYNC_DOUBLE.read();
assert_eq!(2, cache.cache_size());
assert_eq!(1, cache.cache_hits().unwrap());
assert_eq!(2, cache.cache_misses().unwrap());
}
#[cfg(feature = "proc_macro")]
#[test]
fn test_unsync_reads_sync_writes_default_counts_single_miss() {
UNSYNC_DOUBLE_SYNC_WRITES.write().cache_reset();
assert_eq!(4, unsync_double_sync_writes(2));
assert_eq!(4, unsync_double_sync_writes(2));
let cache = UNSYNC_DOUBLE_SYNC_WRITES.read();
assert_eq!(1, cache.cache_size());
assert_eq!(1, cache.cache_hits().unwrap());
assert_eq!(1, cache.cache_misses().unwrap());
}
#[cfg(all(feature = "proc_macro", feature = "time_stores"))]
mod unsync_reads_ttl_sorted {
use cached::Cached;
use cached::macros::cached;
use cached::stores::TtlSortedCache;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
static CALL_COUNT: AtomicUsize = AtomicUsize::new(0);
#[cached(
ty = "TtlSortedCache<String, u32>",
create = "{ TtlSortedCache::builder().ttl(Duration::from_secs(60)).build().unwrap() }",
convert = r#"{ format!("{}", n) }"#,
unsync_reads = true
)]
fn unsync_ttl_sorted(n: u32) -> u32 {
CALL_COUNT.fetch_add(1, Ordering::SeqCst);
n * 3
}
#[test]
fn test_unsync_reads_ttl_sorted_cache() {
CALL_COUNT.store(0, Ordering::SeqCst);
UNSYNC_TTL_SORTED.write().cache_reset();
assert_eq!(unsync_ttl_sorted(4), 12);
assert_eq!(unsync_ttl_sorted(4), 12); assert_eq!(unsync_ttl_sorted(5), 15);
assert_eq!(CALL_COUNT.load(Ordering::SeqCst), 2);
let cache = UNSYNC_TTL_SORTED.read();
assert_eq!(cache.cache_size(), 2);
assert_eq!(cache.cache_hits(), Some(1));
assert_eq!(cache.cache_misses(), Some(2));
}
}
#[cfg(feature = "time_stores")]
#[test]
fn test_ttl_cache_zero_ttl() {
use cached::TtlCache;
let err = TtlCache::<u32, &str>::builder()
.ttl(Duration::from_nanos(0))
.build()
.unwrap_err();
assert!(matches!(err, cached::BuildError::InvalidTtl { .. }));
}
#[cfg(feature = "time_stores")]
#[test]
fn test_lru_ttl_cache_zero_ttl() {
use cached::LruTtlCache;
let err = LruTtlCache::<u32, &str>::builder()
.max_size(4)
.ttl(Duration::from_nanos(0))
.build()
.unwrap_err();
assert!(matches!(err, cached::BuildError::InvalidTtl { .. }));
}
#[cfg(feature = "time_stores")]
#[test]
fn test_ttl_sorted_cache_try_set_time_bounds() {
use cached::Cached;
use cached::stores::TtlSortedCache;
let ttl = Duration::from_secs(u64::MAX / 2);
let mut cache = TtlSortedCache::<u32, u32>::builder()
.ttl(ttl)
.build()
.unwrap();
cache.cache_set(1, 42);
let result = cache.cache_try_set(2, 99);
let _ = result;
}
#[test]
fn test_cache_reset_also_resets_metrics() {
use cached::Cached;
let mut c = UnboundCache::builder().build().unwrap();
c.cache_set(1u32, 1u32);
c.cache_get(&1u32);
c.cache_get(&99u32);
assert_eq!(c.cache_hits(), Some(1));
assert_eq!(c.cache_misses(), Some(1));
c.cache_reset();
assert_eq!(c.cache_hits(), Some(0));
assert_eq!(c.cache_misses(), Some(0));
assert_eq!(c.cache_size(), 0);
let mut lru = LruCache::builder().max_size(4).build().unwrap();
lru.cache_set(1u32, 1u32);
lru.cache_get(&1u32);
lru.cache_get(&99u32);
assert_eq!(lru.cache_hits(), Some(1));
assert_eq!(lru.cache_misses(), Some(1));
lru.cache_reset();
assert_eq!(lru.cache_hits(), Some(0));
assert_eq!(lru.cache_misses(), Some(0));
assert_eq!(lru.cache_size(), 0);
}
#[cfg(feature = "time_stores")]
#[test]
fn test_cache_reset_also_resets_metrics_time_stores() {
use cached::{Cached, LruTtlCache, TtlCache};
let mut tc = TtlCache::<u32, u32>::builder()
.ttl(Duration::from_secs(60))
.build()
.unwrap();
tc.cache_set(1, 1);
tc.cache_get(&1);
tc.cache_get(&99);
tc.cache_reset();
assert_eq!(tc.cache_hits(), Some(0));
assert_eq!(tc.cache_misses(), Some(0));
assert_eq!(tc.cache_size(), 0);
let mut ltu = LruTtlCache::<u32, u32>::builder()
.max_size(4)
.ttl(Duration::from_secs(60))
.build()
.unwrap();
ltu.cache_set(1, 1);
ltu.cache_get(&1);
ltu.cache_get(&99);
ltu.cache_reset();
assert_eq!(ltu.cache_hits(), Some(0));
assert_eq!(ltu.cache_misses(), Some(0));
assert_eq!(ltu.cache_size(), 0);
}
#[test]
fn test_cache_clear_preserves_metrics() {
use cached::Cached;
let mut c = UnboundCache::builder().build().unwrap();
c.cache_set(1u32, 1u32);
c.cache_get(&1u32);
c.cache_get(&99u32);
assert_eq!(c.cache_hits(), Some(1));
assert_eq!(c.cache_misses(), Some(1));
c.cache_clear();
assert_eq!(c.cache_size(), 0);
assert_eq!(c.cache_hits(), Some(1));
assert_eq!(c.cache_misses(), Some(1));
let mut lru = LruCache::builder().max_size(4).build().unwrap();
lru.cache_set(1u32, 1u32);
lru.cache_get(&1u32);
lru.cache_get(&99u32);
lru.cache_clear();
assert_eq!(lru.cache_size(), 0);
assert_eq!(lru.cache_hits(), Some(1));
assert_eq!(lru.cache_misses(), Some(1));
}
#[test]
fn test_unbound_cache_on_evict_fires_on_remove() {
use cached::Cached;
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
let fired = Arc::new(AtomicU32::new(0));
let fired_clone = fired.clone();
let mut cache = UnboundCache::<u32, u32>::builder()
.on_evict(move |_, _| {
fired_clone.fetch_add(1, Ordering::Relaxed);
})
.build()
.unwrap();
cache.cache_set(1, 100);
cache.cache_set(2, 200);
assert_eq!(fired.load(Ordering::Relaxed), 0);
cache.cache_remove(&1u32);
assert_eq!(fired.load(Ordering::Relaxed), 1);
cache.cache_remove(&99u32); assert_eq!(fired.load(Ordering::Relaxed), 1);
cache.cache_remove(&2u32);
assert_eq!(fired.load(Ordering::Relaxed), 2);
}
#[cfg(feature = "time_stores")]
#[test]
fn test_lru_ttl_cache_retain() {
use cached::{Cached, LruTtlCache};
let mut cache = LruTtlCache::<u32, u32>::builder()
.max_size(10)
.ttl(Duration::from_secs(60))
.build()
.unwrap();
cache.cache_set(1, 11); cache.cache_set(2, 20); cache.cache_set(3, 31); cache.cache_set(4, 40);
cache.retain(|_, v| v % 2 == 0);
assert!(cache.cache_get(&1).is_none()); assert!(cache.cache_get(&2).is_some()); assert!(cache.cache_get(&3).is_none()); assert!(cache.cache_get(&4).is_some()); assert_eq!(cache.cache_size(), 2);
}
#[test]
fn test_lru_retain_fires_on_evict_and_increments_evictions() {
use cached::{Cached, LruCache};
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
let fired = Arc::new(AtomicU32::new(0));
let fired_clone = fired.clone();
let mut cache = LruCache::builder()
.max_size(10)
.on_evict(move |_k, _v| {
fired_clone.fetch_add(1, Ordering::Relaxed);
})
.build()
.unwrap();
cache.cache_set(1u32, 10u32);
cache.cache_set(2u32, 20u32);
cache.cache_set(3u32, 30u32);
cache.cache_set(4u32, 40u32);
cache.retain(|k, _v| k % 2 == 0);
assert_eq!(fired.load(Ordering::Relaxed), 2); assert_eq!(cache.cache_evictions(), Some(2));
assert_eq!(cache.cache_size(), 2);
assert!(cache.cache_get(&1u32).is_none());
assert!(cache.cache_get(&2u32).is_some());
assert!(cache.cache_get(&3u32).is_none());
assert!(cache.cache_get(&4u32).is_some());
}
#[cfg(feature = "time_stores")]
#[test]
fn test_lru_ttl_evict_does_not_double_count_evictions() {
use cached::{Cached, LruTtlCache};
let mut cache = LruTtlCache::<u32, u32>::builder()
.max_size(10)
.ttl(Duration::from_millis(50))
.build()
.unwrap();
cache.cache_set(1u32, 10u32);
cache.cache_set(2u32, 20u32);
cache.cache_set(3u32, 30u32);
std::thread::sleep(Duration::from_millis(100));
assert_eq!(cache.evict(), 3);
assert_eq!(cache.cache_evictions(), Some(3));
}
#[cfg(feature = "time_stores")]
#[test]
fn test_ttl_sorted_cache_clone_cached() {
use cached::stores::TtlSortedCache;
use cached::{Cached, CloneCached};
let mut cache = TtlSortedCache::<u32, u32>::builder()
.ttl(Duration::from_secs(60))
.build()
.unwrap();
cache.cache_set(1, 100);
let (val, expired) = cache.cache_get_with_expiry_status(&1u32);
assert_eq!(val, Some(100));
assert!(!expired);
let (val, expired) = cache.cache_get_with_expiry_status(&99u32);
assert!(val.is_none());
assert!(!expired);
}
#[cfg(all(feature = "time_stores", feature = "async_tokio_rt_multi_thread"))]
#[tokio::test]
async fn test_ttl_sorted_cache_cached_async() {
use cached::CachedAsync;
use cached::stores::TtlSortedCache;
let mut cache = TtlSortedCache::<u32, u32>::builder()
.ttl(Duration::from_secs(60))
.build()
.unwrap();
let val = CachedAsync::async_get_or_set_with(&mut cache, 1u32, || async { 42u32 }).await;
assert_eq!(*val, 42);
let val2 = CachedAsync::async_get_or_set_with(&mut cache, 1u32, || async { 99u32 }).await;
assert_eq!(*val2, 42);
}
#[cfg(feature = "async_tokio_rt_multi_thread")]
#[tokio::test]
async fn test_expiring_lru_cache_cached_async() {
use cached::CachedAsync;
#[derive(Clone)]
struct NeverExpires(u32);
impl cached::Expires for NeverExpires {
fn is_expired(&self) -> bool {
false
}
}
let mut cache = ExpiringLruCache::<u32, NeverExpires>::builder()
.max_size(4)
.build()
.unwrap();
let val =
CachedAsync::async_get_or_set_with(&mut cache, 1u32, || async { NeverExpires(42) }).await;
assert_eq!(val.0, 42);
let val2 =
CachedAsync::async_get_or_set_with(&mut cache, 1u32, || async { NeverExpires(99) }).await;
assert_eq!(val2.0, 42);
assert_eq!(cache.cache_hits(), Some(1));
assert_eq!(cache.cache_misses(), Some(1));
}
#[test]
fn test_lru_cache_builder_build() {
use cached::Cached;
let mut cache = LruCache::<u32, u32>::builder().max_size(4).build().unwrap();
cache.cache_set(1, 10);
assert_eq!(cache.cache_get(&1), Some(&10));
assert_eq!(cache.cache_capacity(), Some(4));
}
#[test]
fn test_unbound_cache_builder_build() {
use cached::Cached;
let mut cache = UnboundCache::<u32, u32>::builder().build().unwrap();
cache.cache_set(1, 10);
assert_eq!(cache.cache_get(&1), Some(&10));
}
#[test]
fn test_expiring_lru_cache_builder_build() {
use cached::Cached;
#[derive(Clone)]
struct AlwaysFresh(u32);
impl Expires for AlwaysFresh {
fn is_expired(&self) -> bool {
false
}
}
let mut cache = ExpiringLruCache::<u32, AlwaysFresh>::builder()
.max_size(4)
.build()
.unwrap();
cache.cache_set(1, AlwaysFresh(42));
assert_eq!(cache.cache_get(&1).map(|v| v.0), Some(42));
}
#[test]
#[cfg(feature = "time_stores")]
fn test_ttl_cache_builder_build() {
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_get(&1), Some(&10));
}
#[test]
#[cfg(feature = "time_stores")]
fn test_lru_ttl_cache_builder_build() {
use cached::{Cached, LruTtlCache};
let mut cache = LruTtlCache::<u32, u32>::builder()
.max_size(4)
.ttl(Duration::from_secs(60))
.refresh(true)
.build()
.unwrap();
cache.cache_set(1, 10);
assert_eq!(cache.cache_get(&1), Some(&10));
assert!(cache.refresh_on_hit());
}
#[test]
#[cfg(feature = "time_stores")]
fn test_ttl_sorted_cache_builder_build() {
use cached::{Cached, stores::TtlSortedCache};
let mut cache = TtlSortedCache::<u32, u32>::builder()
.ttl(Duration::from_secs(60))
.max_size(4)
.build()
.unwrap();
cache.cache_set(1, 10);
assert_eq!(cache.cache_get(&1), Some(&10));
}
#[test]
#[cfg(feature = "time_stores")]
fn test_ttl_cache_store_getter() {
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.store().len(), 1);
assert!(cache.store().contains_key(&1));
}
#[test]
fn test_unbound_cache_store_getter() {
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.store().len(), 2);
}
#[test]
#[cfg(feature = "time_stores")]
fn test_ttl_cache_refresh_getter_and_setter() {
use cached::TtlCache;
let mut cache = TtlCache::<u32, u32>::builder()
.ttl(Duration::from_secs(60))
.refresh_on_hit(false)
.build()
.unwrap();
assert!(!cache.refresh_on_hit());
cache.set_refresh_on_hit(true);
assert!(cache.refresh_on_hit());
cache.set_refresh_on_hit(false);
assert!(!cache.refresh_on_hit());
}
#[test]
fn test_cached_iter_unbound() {
use cached::{Cached, CachedIter};
let mut cache = UnboundCache::<u32, u32>::builder().build().unwrap();
cache.cache_set(1, 10);
cache.cache_set(2, 20);
let mut pairs: Vec<_> = CachedIter::iter(&cache).collect();
pairs.sort_by_key(|(k, _)| *k);
assert_eq!(pairs, vec![(&1u32, &10u32), (&2u32, &20u32)]);
}
#[test]
fn test_cached_iter_lru() {
use cached::{Cached, CachedIter};
let mut cache = LruCache::<u32, u32>::builder().max_size(4).build().unwrap();
cache.cache_set(1, 10);
cache.cache_set(2, 20);
let mut pairs: Vec<_> = CachedIter::iter(&cache).collect();
pairs.sort_by_key(|(k, _)| *k);
assert_eq!(pairs, vec![(&1u32, &10u32), (&2u32, &20u32)]);
}
#[test]
#[cfg(feature = "time_stores")]
fn test_cached_iter_ttl_cache() {
use cached::{Cached, CachedIter, TtlCache};
let mut cache = TtlCache::<u32, u32>::builder()
.ttl(Duration::from_secs(60))
.build()
.unwrap();
cache.cache_set(1, 10);
cache.cache_set(2, 20);
let mut pairs: Vec<_> = CachedIter::iter(&cache).collect();
pairs.sort_by_key(|(k, _)| *k);
assert_eq!(pairs, vec![(&1u32, &10u32), (&2u32, &20u32)]);
}
#[test]
#[cfg(feature = "time_stores")]
fn test_cached_iter_ttl_cache_excludes_expired() {
use cached::{Cached, CachedIter, TtlCache};
let mut cache = TtlCache::<u32, u32>::builder()
.ttl(Duration::from_millis(20))
.build()
.unwrap();
cache.cache_set(1, 10);
cache.cache_set(2, 20);
sleep(Duration::from_millis(40));
assert_eq!(CachedIter::iter(&cache).count(), 0);
}
#[test]
fn test_cached_iter_expiring_lru() {
use cached::{Cached, CachedIter};
#[derive(Clone)]
struct Fresh;
impl Expires for Fresh {
fn is_expired(&self) -> bool {
false
}
}
let mut cache = ExpiringLruCache::<u32, Fresh>::builder()
.max_size(4)
.build()
.unwrap();
cache.cache_set(1, Fresh);
cache.cache_set(2, Fresh);
let mut keys: Vec<_> = CachedIter::iter(&cache).map(|(k, _)| *k).collect();
keys.sort();
assert_eq!(keys, vec![1u32, 2u32]);
}
#[test]
#[cfg(feature = "time_stores")]
fn test_cached_peek_ttl_cache() {
use cached::{Cached, CachedPeek, TtlCache};
let mut cache = TtlCache::<u32, u32>::builder()
.ttl(Duration::from_secs(60))
.build()
.unwrap();
cache.cache_set(1, 10);
cache.cache_reset_metrics();
assert_eq!(cache.cache_peek(&1), Some(&10));
assert_eq!(cache.cache_hits(), Some(0));
assert_eq!(cache.cache_misses(), Some(0));
assert_eq!(cache.cache_peek(&99), None);
assert_eq!(cache.cache_misses(), Some(0));
}
#[test]
#[cfg(feature = "time_stores")]
fn test_cached_peek_lru_ttl_cache() {
use cached::{Cached, CachedPeek, 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_eq!(cache.cache_peek(&1), Some(&10));
assert_eq!(cache.cache_hits(), Some(0));
assert_eq!(cache.cache_misses(), Some(0));
}
#[test]
#[cfg(feature = "time_stores")]
fn test_cached_peek_ttl_sorted_cache() {
use cached::{Cached, CachedPeek, stores::TtlSortedCache};
let mut cache = TtlSortedCache::<u32, u32>::builder()
.ttl(Duration::from_secs(60))
.build()
.unwrap();
cache.cache_set(1, 10);
cache.cache_reset_metrics();
assert_eq!(cache.cache_peek(&1), Some(&10));
assert_eq!(cache.cache_hits(), Some(0));
assert_eq!(cache.cache_misses(), Some(0));
}
#[test]
fn test_cached_peek_lru_cache() {
use cached::{Cached, CachedPeek};
let mut cache = LruCache::<u32, u32>::builder().max_size(4).build().unwrap();
cache.cache_set(1, 10);
cache.cache_reset_metrics();
assert_eq!(cache.cache_peek(&1), Some(&10));
assert_eq!(cache.cache_hits(), Some(0));
assert_eq!(cache.cache_misses(), Some(0));
}
#[test]
fn test_cached_peek_expiring_lru_cache() {
use cached::{Cached, CachedPeek};
#[derive(Clone)]
struct AlwaysFresh(u32);
impl Expires for AlwaysFresh {
fn is_expired(&self) -> bool {
false
}
}
#[derive(Clone)]
struct AlwaysExpired;
impl Expires for AlwaysExpired {
fn is_expired(&self) -> bool {
true
}
}
let mut cache = ExpiringLruCache::<u32, AlwaysFresh>::builder()
.max_size(4)
.build()
.unwrap();
cache.cache_set(1, AlwaysFresh(10));
cache.cache_reset_metrics();
assert_eq!(cache.cache_peek(&1).map(|v| v.0), Some(10));
assert_eq!(cache.cache_hits(), Some(0));
assert_eq!(cache.cache_misses(), Some(0));
assert!(cache.cache_peek(&99).is_none());
assert_eq!(cache.cache_misses(), Some(0));
let mut cache2 = ExpiringLruCache::<u32, AlwaysExpired>::builder()
.max_size(4)
.build()
.unwrap();
cache2.cache_set(1, AlwaysExpired);
cache2.cache_reset_metrics();
assert!(cache2.cache_peek(&1).is_none());
assert_eq!(cache2.cache_hits(), Some(0));
assert_eq!(cache2.cache_misses(), Some(0));
}
#[test]
fn test_cached_peek_unbound_cache() {
use cached::{Cached, CachedPeek, UnboundCache};
let mut cache = UnboundCache::<u32, u32>::builder().build().unwrap();
cache.cache_set(1, 10);
cache.cache_reset_metrics();
assert_eq!(cache.cache_peek(&1), Some(&10));
assert_eq!(cache.cache_hits(), Some(0));
assert_eq!(cache.cache_misses(), Some(0));
assert!(cache.cache_peek(&99).is_none());
assert_eq!(cache.cache_misses(), Some(0));
}
#[test]
fn test_cached_peek_hashmap() {
use cached::{Cached, CachedPeek};
use std::collections::HashMap;
let mut cache = HashMap::<u32, u32>::new();
cache.cache_set(1, 10);
assert_eq!(cache.cache_peek(&1), Some(&10));
assert!(cache.cache_peek(&99).is_none());
assert_eq!(cache.cache_size(), 1);
}
#[test]
fn test_cached_read_hashmap() {
use cached::{Cached, CachedRead};
use std::collections::HashMap;
let mut cache = HashMap::<u32, u32>::new();
cache.cache_set(1, 42);
let hits_before = cache.cache_hits();
assert_eq!(cache.cache_get_read(&1), Some(&42));
assert_eq!(cache.cache_get_read(&99), None);
assert_eq!(cache.cache_hits(), hits_before);
}
#[test]
#[cfg(feature = "time_stores")]
fn test_ttl_cache_clone_cached() {
use cached::{Cached, CloneCached, TtlCache};
let mut cache = TtlCache::<u32, u32>::builder()
.ttl(Duration::from_secs(60))
.build()
.unwrap();
cache.cache_set(1, 100);
let (val, expired) = cache.cache_get_with_expiry_status(&1u32);
assert_eq!(val, Some(100));
assert!(!expired);
let (val, expired) = cache.cache_get_with_expiry_status(&99u32);
assert!(val.is_none());
assert!(!expired);
}
#[test]
fn test_expiring_lru_cache_clone_cached() {
use cached::{Cached, CloneCached};
#[derive(Clone, PartialEq, Debug)]
struct Article {
content: String,
expired: bool,
}
impl Expires for Article {
fn is_expired(&self) -> bool {
self.expired
}
}
let mut cache = ExpiringLruCache::<u32, Article>::builder()
.max_size(4)
.build()
.unwrap();
cache.cache_set(
1,
Article {
content: "hello".into(),
expired: false,
},
);
cache.cache_set(
2,
Article {
content: "bye".into(),
expired: true,
},
);
let (val, is_exp) = cache.cache_get_with_expiry_status(&1u32);
assert_eq!(val.as_ref().map(|a| a.content.as_str()), Some("hello"));
assert!(!is_exp);
let (val, is_exp) = cache.cache_get_with_expiry_status(&2u32);
assert_eq!(val.as_ref().map(|a| a.content.as_str()), Some("bye"));
assert!(is_exp);
let (val, is_exp) = cache.cache_get_with_expiry_status(&99u32);
assert!(val.is_none());
assert!(!is_exp);
}
#[test]
#[cfg(feature = "time_stores")]
fn test_cache_evict_ttl_cache() {
use cached::{CacheEvict, Cached, TtlCache};
let mut cache = TtlCache::<u32, u32>::builder()
.ttl(Duration::from_millis(20))
.build()
.unwrap();
cache.cache_set(1, 10);
cache.cache_set(2, 20);
sleep(Duration::from_millis(40));
let evicted = CacheEvict::evict(&mut cache);
assert_eq!(evicted, 2);
assert_eq!(cache.cache_size(), 0);
assert_eq!(cache.cache_evictions(), Some(2));
}
#[test]
#[cfg(feature = "time_stores")]
fn test_cache_evict_lru_ttl_cache() {
use cached::{CacheEvict, Cached, LruTtlCache};
let mut cache = LruTtlCache::<u32, u32>::builder()
.max_size(4)
.ttl(Duration::from_millis(20))
.build()
.unwrap();
cache.cache_set(1, 10);
cache.cache_set(2, 20);
sleep(Duration::from_millis(40));
let evicted = CacheEvict::evict(&mut cache);
assert_eq!(evicted, 2);
assert_eq!(cache.cache_size(), 0);
}
#[test]
#[cfg(feature = "time_stores")]
fn test_ttl_sorted_cache_generic_get_str_key() {
use cached::stores::TtlSortedCache;
let mut cache = TtlSortedCache::<String, u32>::builder()
.ttl(Duration::from_secs(60))
.build()
.unwrap();
cache.insert(String::from("hello"), 1).unwrap();
cache.insert(String::from("world"), 2).unwrap();
assert_eq!(cache.get("hello"), Some(&1));
assert_eq!(cache.get("world"), Some(&2));
assert_eq!(cache.get("missing"), None);
}
#[test]
#[cfg(feature = "time_stores")]
fn test_ttl_sorted_cache_generic_get_slice_key() {
use cached::stores::TtlSortedCache;
let mut cache = TtlSortedCache::<Vec<u32>, &str>::builder()
.ttl(Duration::from_secs(60))
.build()
.unwrap();
cache.insert(vec![1, 2, 3], "abc").unwrap();
assert_eq!(cache.get([1u32, 2, 3].as_slice()), Some(&"abc"));
assert_eq!(cache.get([9u32].as_slice()), None);
}
#[cfg(feature = "proc_macro")]
mod generic_where_tests {
use cached::macros::{cached, concurrent_cached, once};
#[once]
fn generic_once_where<T>(x: T) -> String
where
T: std::string::ToString,
{
x.to_string()
}
#[test]
fn test_generic_once_where() {
assert_eq!(generic_once_where(1u32), "1");
assert_eq!(generic_once_where(2u32), "1");
}
#[cached(key = "String", convert = r#"{ x.to_string() }"#)]
fn generic_cached_where<T>(x: T) -> String
where
T: std::string::ToString + Clone,
{
x.to_string()
}
#[test]
fn test_generic_cached_where() {
assert_eq!(generic_cached_where(7u32), "7");
assert_eq!(generic_cached_where(7u32), "7");
assert_eq!(generic_cached_where(8u64), "8");
}
struct TestStore {
inner: std::sync::Mutex<std::collections::HashMap<String, String>>,
}
impl TestStore {
fn new() -> Self {
Self {
inner: std::sync::Mutex::new(std::collections::HashMap::new()),
}
}
}
impl cached::ConcurrentCached<String, String> for TestStore {
type Error = std::convert::Infallible;
fn cache_get(&self, k: &String) -> Result<Option<String>, Self::Error> {
Ok(self.inner.lock().unwrap().get(k).cloned())
}
fn cache_set(&self, k: String, v: String) -> Result<Option<String>, Self::Error> {
Ok(self.inner.lock().unwrap().insert(k, v))
}
fn cache_remove(&self, k: &String) -> Result<Option<String>, Self::Error> {
Ok(self.inner.lock().unwrap().remove(k))
}
fn cache_remove_entry(&self, k: &String) -> Result<Option<(String, String)>, Self::Error> {
Ok(self.inner.lock().unwrap().remove_entry(k))
}
fn set_refresh_on_hit(&self, _refresh: bool) -> bool {
false
}
}
#[concurrent_cached(
ty = "TestStore",
create = "{ TestStore::new() }",
key = "String",
convert = r#"{ x.to_string() }"#,
map_error = r#"|e| e"#
)]
fn generic_concurrent_cached_where<T>(x: T) -> Result<String, std::convert::Infallible>
where
T: std::string::ToString,
{
Ok(x.to_string())
}
#[test]
fn test_generic_concurrent_cached_where() {
assert_eq!(generic_concurrent_cached_where(3u32).unwrap(), "3");
assert_eq!(generic_concurrent_cached_where(3u32).unwrap(), "3");
assert_eq!(generic_concurrent_cached_where(4u64).unwrap(), "4");
}
#[cfg(feature = "async")]
mod async_generic {
use cached::macros::cached;
#[cached(key = "String", convert = r#"{ x.to_string() }"#)]
async fn generic_cached_where_async<T>(x: T) -> String
where
T: std::string::ToString + Clone,
{
x.to_string()
}
#[tokio::test]
async fn test_generic_cached_where_async() {
assert_eq!(generic_cached_where_async(5u32).await, "5");
assert_eq!(generic_cached_where_async(5u32).await, "5");
assert_eq!(generic_cached_where_async(6u64).await, "6");
}
}
}
#[test]
fn test_cache_metrics_and_hit_ratio() {
use cached::Cached;
let mut cache = UnboundCache::<u32, u32>::builder().build().unwrap();
let m = cache.metrics();
assert_eq!(m.hits, Some(0));
assert_eq!(m.misses, Some(0));
assert_eq!(m.size, 0);
assert!(m.capacity.is_none());
assert!(m.hit_ratio().is_none(), "no lookups yet → None");
cache.cache_set(1, 10);
cache.cache_get(&1); cache.cache_get(&2); cache.cache_get(&1);
let m = cache.metrics();
assert_eq!(m.hits, Some(2));
assert_eq!(m.misses, Some(1));
assert_eq!(m.size, 1);
let ratio = m.hit_ratio().expect("should have ratio after lookups");
assert!((ratio - 2.0 / 3.0).abs() < 1e-9);
let mut lru = LruCache::<u32, u32>::builder().max_size(4).build().unwrap();
lru.cache_set(1, 10);
lru.cache_get(&1);
lru.cache_get(&99);
let m = lru.metrics();
assert_eq!(m.capacity, Some(4));
assert_eq!(m.hits, Some(1));
assert_eq!(m.misses, Some(1));
let ratio = m.hit_ratio().unwrap();
assert!((ratio - 0.5).abs() < 1e-9);
}
#[test]
#[cfg(feature = "time_stores")]
fn test_ttl_sorted_cache_reserve() {
use cached::Cached;
use cached::stores::TtlSortedCache;
let mut cache = TtlSortedCache::<u32, u32>::builder()
.ttl(Duration::from_secs(60))
.build()
.unwrap();
cache.reserve(64);
cache.cache_set(1, 10);
assert_eq!(cache.cache_get(&1), Some(&10));
assert_eq!(cache.cache_size(), 1);
}
#[test]
#[cfg(feature = "time_stores")]
fn test_ttl_sorted_cache_try_size_limit() {
use cached::stores::TtlSortedCache;
let mut cache = TtlSortedCache::<u32, u32>::builder()
.ttl(Duration::from_secs(60))
.build()
.unwrap();
let prev = cache
.try_set_max_size(10)
.expect("non-zero limit should succeed");
assert!(prev.is_none(), "no previous limit");
let prev = cache.try_set_max_size(20).unwrap();
assert_eq!(prev, Some(10));
let err = cache.try_set_max_size(0);
assert!(err.is_err(), "zero size limit must fail");
assert_eq!(err.unwrap_err().kind(), std::io::ErrorKind::InvalidInput);
}
#[cfg(all(
feature = "proc_macro",
feature = "time_stores",
feature = "async_tokio_rt_multi_thread"
))]
mod result_fallback_async_tests {
use super::sleep;
use cached::time::Duration;
#[cached::macros::cached(ttl = 1, result_fallback = true)]
async fn async_always_failing() -> Result<String, ()> {
Err(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_result_fallback_async() {
use cached::Cached;
ASYNC_ALWAYS_FAILING
.write()
.await
.cache_set((), "hello".to_string());
assert_eq!(async_always_failing().await, Ok("hello".to_string()));
sleep(Duration::from_millis(1100));
assert_eq!(async_always_failing().await, Ok("hello".to_string()));
}
}
#[test]
#[cfg(feature = "time_stores")]
fn test_cache_evict_ttl_sorted_cache() {
use cached::stores::TtlSortedCache;
use cached::{CacheEvict, Cached};
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
let evicted_count = Arc::new(AtomicU32::new(0));
let evicted_clone = evicted_count.clone();
let mut cache = TtlSortedCache::<u32, u32>::builder()
.ttl(Duration::from_millis(20))
.on_evict(move |_k: &u32, _v: &u32| {
evicted_clone.fetch_add(1, Ordering::Relaxed);
})
.build()
.unwrap();
cache.cache_set(1, 10);
cache.cache_set(2, 20);
cache.cache_set(3, 30);
assert_eq!(cache.cache_size(), 3);
sleep(Duration::from_millis(40));
let evicted = CacheEvict::evict(&mut cache);
assert_eq!(evicted, 3);
assert_eq!(cache.cache_size(), 0);
assert_eq!(cache.cache_evictions(), Some(3));
assert_eq!(evicted_count.load(Ordering::Relaxed), 3);
}
#[test]
fn test_cache_evict_expiring_lru_cache() {
use cached::{CacheEvict, Cached};
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
#[derive(Clone)]
struct Expirable {
expired: bool,
}
impl cached::Expires for Expirable {
fn is_expired(&self) -> bool {
self.expired
}
}
let evicted_count = Arc::new(AtomicU32::new(0));
let evicted_clone = evicted_count.clone();
let mut cache = ExpiringLruCache::<u32, Expirable>::builder()
.max_size(10)
.on_evict(move |_k: &u32, _v: &Expirable| {
evicted_clone.fetch_add(1, Ordering::Relaxed);
})
.build()
.unwrap();
cache.cache_set(1, Expirable { expired: true });
cache.cache_set(2, Expirable { expired: false });
cache.cache_set(3, Expirable { expired: true });
assert_eq!(cache.cache_size(), 3);
let evicted = CacheEvict::evict(&mut cache);
assert_eq!(evicted, 2);
assert_eq!(cache.cache_size(), 1);
assert_eq!(cache.cache_evictions(), Some(2));
assert_eq!(evicted_count.load(Ordering::Relaxed), 2);
}
#[test]
fn test_expiring_lru_cache_get_does_not_inflate_inner_metrics() {
use cached::Cached;
#[derive(Clone)]
struct Fresh;
impl cached::Expires for Fresh {
fn is_expired(&self) -> bool {
false
}
}
let mut cache = ExpiringLruCache::<u32, Fresh>::builder()
.max_size(4)
.build()
.unwrap();
cache.cache_set(1, Fresh);
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));
assert_eq!(cache.store().cache_hits(), Some(0));
assert_eq!(cache.store().cache_misses(), Some(0));
}
#[test]
fn test_expiring_lru_cache_evictions_sum_lru_and_expiry() {
use cached::Cached;
#[derive(Clone)]
struct Expirable {
expired: bool,
}
impl cached::Expires for Expirable {
fn is_expired(&self) -> bool {
self.expired
}
}
let mut cache = ExpiringLruCache::<u32, Expirable>::builder()
.max_size(2)
.build()
.unwrap();
cache.cache_set(1, Expirable { expired: false });
cache.cache_set(2, Expirable { expired: false });
cache.cache_set(3, Expirable { expired: false });
assert_eq!(cache.cache_evictions(), Some(1));
cache.cache_set(2, Expirable { expired: true });
assert!(cache.cache_get(&2).is_none());
assert_eq!(cache.cache_evictions(), Some(2));
}
#[cfg(feature = "proc_macro")]
mod macro_arg_pairwise {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
#[cached(name = "PAIRWISE_NAMED_UNBOUND", unbound)]
fn named_unbound(n: u32) -> u32 {
n + 1
}
#[test]
fn test_name_with_unbound() {
assert_eq!(named_unbound(2), 3);
assert_eq!(named_unbound(2), 3);
let cache = PAIRWISE_NAMED_UNBOUND.write();
assert_eq!(cache.cache_hits(), Some(1));
assert_eq!(cache.cache_misses(), Some(1));
}
#[cached(max_size = 2, sync_lock = "mutex")]
fn sized_mutex(n: u32) -> u32 {
n * 2
}
#[test]
fn test_size_with_sync_lock_mutex() {
assert_eq!(sized_mutex(3), 6);
assert_eq!(sized_mutex(3), 6);
let cache = SIZED_MUTEX.lock();
assert_eq!(cache.cache_hits(), Some(1));
assert_eq!(cache.cache_misses(), Some(1));
}
static UNSYNC_CALLS: AtomicUsize = AtomicUsize::new(0);
#[cached(unsync_reads = true, sync_lock = "rwlock")]
fn unsync_rwlock(n: u32) -> u32 {
UNSYNC_CALLS.fetch_add(1, Ordering::SeqCst);
n + 100
}
#[test]
fn test_unsync_reads_with_sync_lock_rwlock() {
assert_eq!(unsync_rwlock(1), 101);
assert_eq!(unsync_rwlock(1), 101);
assert_eq!(unsync_rwlock(1), 101);
assert_eq!(UNSYNC_CALLS.load(Ordering::SeqCst), 1);
}
#[cached(name = "SYNC_LOCK_DOC_SPELLING", sync_lock = "rwlock")]
fn sync_lock_doc(n: u32) -> u32 {
n + 1
}
#[cached(name = "SYNC_LOCK_SNAKE_SPELLING", sync_lock = "rw_lock")]
fn sync_lock_snake(n: u32) -> u32 {
n + 1
}
#[test]
fn test_sync_lock_both_spellings_select_rwlock() {
assert_eq!(sync_lock_doc(1), 2);
assert_eq!(sync_lock_snake(1), 2);
assert_eq!(SYNC_LOCK_DOC_SPELLING.write().cache_misses(), Some(1));
assert_eq!(SYNC_LOCK_SNAKE_SPELLING.write().cache_misses(), Some(1));
}
static BY_KEY_CALLS: AtomicUsize = AtomicUsize::new(0);
#[cached(sync_writes = "by_key", sync_writes_buckets = 4)]
fn by_key_buckets(n: u32) -> u32 {
BY_KEY_CALLS.fetch_add(1, Ordering::SeqCst);
n + 7
}
#[test]
fn test_by_key_with_sync_writes_buckets() {
assert_eq!(by_key_buckets(5), 12);
assert_eq!(by_key_buckets(5), 12);
assert_eq!(by_key_buckets(5), 12);
assert_eq!(BY_KEY_CALLS.load(Ordering::SeqCst), 1);
}
#[once(with_cached_flag = true)]
fn once_flag(n: u32) -> cached::Return<u32> {
cached::Return::new(n + 1)
}
#[test]
fn test_once_with_cached_flag() {
let first = once_flag(10);
assert!(!first.was_cached);
assert_eq!(*first, 11);
let second = once_flag(999);
assert!(second.was_cached);
assert_eq!(*second, 11);
}
#[once(with_cached_flag = true)]
fn once_result_flag(ok: bool) -> Result<cached::Return<u32>, ()> {
if ok {
Ok(cached::Return::new(1))
} else {
Err(())
}
}
#[test]
fn test_once_result_with_cached_flag() {
assert!(once_result_flag(false).is_err());
let ok = once_result_flag(true).unwrap();
assert!(!ok.was_cached);
let cached_ok = once_result_flag(true).unwrap();
assert!(cached_ok.was_cached);
assert_eq!(*cached_ok, 1);
}
#[once(with_cached_flag = true)]
fn once_option_flag(some: bool) -> Option<cached::Return<u32>> {
if some {
Some(cached::Return::new(2))
} else {
None
}
}
#[test]
fn test_once_option_with_cached_flag() {
assert!(once_option_flag(false).is_none());
let s = once_option_flag(true).unwrap();
assert!(!s.was_cached);
let c = once_option_flag(true).unwrap();
assert!(c.was_cached);
assert_eq!(*c, 2);
}
#[cfg(feature = "time_stores")]
#[once(name = "PAIRWISE_ONCE_NAMED_TTL", ttl = 100)]
fn once_named_ttl(n: u32) -> u32 {
n + 3
}
#[cfg(feature = "time_stores")]
#[test]
fn test_once_name_with_ttl() {
assert_eq!(once_named_ttl(4), 7);
assert_eq!(once_named_ttl(123), 7);
let cache = PAIRWISE_ONCE_NAMED_TTL.read();
assert!(cache.is_some());
}
}
#[cfg(all(feature = "time_stores", feature = "async"))]
mod async_cache_store_tests {
use cached::Expires;
use cached::time::Duration;
use cached::{
CachedAsync, ExpiringLruCache, LruTtlCache, TtlCache, TtlSortedCache, UnboundCache,
};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
#[tokio::test]
async fn test_ttl_cache_async() {
let mut cache = TtlCache::builder()
.ttl(Duration::from_millis(50))
.build()
.unwrap();
let calls = Arc::new(AtomicUsize::new(0));
let calls_clone = calls.clone();
let val = cache
.async_get_or_set_with(1, || {
let calls = calls_clone.clone();
async move {
calls.fetch_add(1, Ordering::Relaxed);
"hello".to_string()
}
})
.await;
assert_eq!(val, "hello");
assert_eq!(calls.load(Ordering::Relaxed), 1);
let val = cache
.async_get_or_set_with(1, || async {
calls.fetch_add(1, Ordering::Relaxed);
"world".to_string()
})
.await;
assert_eq!(val, "hello");
assert_eq!(calls.load(Ordering::Relaxed), 1);
tokio::time::sleep(tokio::time::Duration::from_millis(60)).await;
let val = cache
.async_get_or_set_with(1, || async {
calls.fetch_add(1, Ordering::Relaxed);
"world".to_string()
})
.await;
assert_eq!(val, "world");
assert_eq!(calls.load(Ordering::Relaxed), 2);
}
#[tokio::test]
async fn test_ttl_cache_async_try_evict() {
let evicted = Arc::new(AtomicUsize::new(0));
let evicted_clone = evicted.clone();
let mut cache = TtlCache::builder()
.ttl(Duration::from_millis(50))
.on_evict(move |_, _| {
evicted_clone.fetch_add(1, Ordering::Relaxed);
})
.build()
.unwrap();
let val = cache
.async_try_get_or_set_with(1, || async { Ok::<_, ()>("hello".to_string()) })
.await
.unwrap();
assert_eq!(val, "hello");
assert_eq!(evicted.load(Ordering::Relaxed), 0);
tokio::time::sleep(tokio::time::Duration::from_millis(60)).await;
let val = cache
.async_try_get_or_set_with(1, || async { Ok::<_, ()>("world".to_string()) })
.await
.unwrap();
assert_eq!(val, "world");
assert_eq!(evicted.load(Ordering::Relaxed), 1);
}
#[tokio::test]
async fn test_lru_ttl_cache_async() {
let evicted = Arc::new(AtomicUsize::new(0));
let evicted_clone = evicted.clone();
let mut cache = LruTtlCache::builder()
.max_size(2)
.ttl(Duration::from_millis(50))
.on_evict(move |_, _| {
evicted_clone.fetch_add(1, Ordering::Relaxed);
})
.build()
.unwrap();
cache
.async_get_or_set_with(1, || async { "one".to_string() })
.await;
cache
.async_get_or_set_with(2, || async { "two".to_string() })
.await;
assert_eq!(evicted.load(Ordering::Relaxed), 0);
cache
.async_get_or_set_with(3, || async { "three".to_string() })
.await;
assert_eq!(evicted.load(Ordering::Relaxed), 1);
tokio::time::sleep(tokio::time::Duration::from_millis(60)).await;
cache
.async_get_or_set_with(3, || async { "new_three".to_string() })
.await;
assert_eq!(evicted.load(Ordering::Relaxed), 2);
}
#[tokio::test]
async fn test_ttl_sorted_cache_async() {
let evicted = Arc::new(AtomicUsize::new(0));
let evicted_clone = evicted.clone();
let mut cache = TtlSortedCache::builder()
.ttl(Duration::from_millis(50))
.on_evict(move |_, _| {
evicted_clone.fetch_add(1, Ordering::Relaxed);
})
.build()
.unwrap();
cache
.async_get_or_set_with(1, || async { "one".to_string() })
.await;
assert_eq!(evicted.load(Ordering::Relaxed), 0);
tokio::time::sleep(tokio::time::Duration::from_millis(60)).await;
cache
.async_get_or_set_with(1, || async { "new_one".to_string() })
.await;
assert_eq!(evicted.load(Ordering::Relaxed), 1);
}
#[derive(Clone)]
struct ExpiringVal {
expired: bool,
}
impl Expires for ExpiringVal {
fn is_expired(&self) -> bool {
self.expired
}
}
#[tokio::test]
async fn test_expiring_lru_cache_async() {
let evicted = Arc::new(AtomicUsize::new(0));
let evicted_clone = evicted.clone();
let mut cache = ExpiringLruCache::builder()
.max_size(2)
.on_evict(move |_, _| {
evicted_clone.fetch_add(1, Ordering::Relaxed);
})
.build()
.unwrap();
cache
.async_get_or_set_with(1, || async { ExpiringVal { expired: true } })
.await;
assert_eq!(evicted.load(Ordering::Relaxed), 0);
cache
.async_get_or_set_with(1, || async { ExpiringVal { expired: false } })
.await;
assert_eq!(evicted.load(Ordering::Relaxed), 1);
}
#[tokio::test]
async fn test_unbound_cache_async() {
let mut cache = UnboundCache::builder().build().unwrap();
let val = cache
.async_get_or_set_with(1, || async { "hello".to_string() })
.await;
assert_eq!(val, "hello");
}
}