use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::Duration;
use bytes::Bytes;
use hydracache_core::{CacheError, CacheOptions, Result as CacheResult};
use std::sync::atomic::AtomicUsize;
use crate::tests::common::{user, LoaderError, User};
use crate::HydraCache;
#[tokio::test]
async fn put_then_get() {
let cache = HydraCache::local().build();
cache
.put("user:1", user(1), CacheOptions::new())
.await
.unwrap();
let cached: Option<User> = cache.get("user:1").await.unwrap();
assert_eq!(cached, Some(user(1)));
}
#[tokio::test]
async fn builder_options_accept_small_limits_and_custom_codec() {
let cache = HydraCache::local()
.max_capacity(0)
.max_entry_bytes(0)
.default_ttl(Duration::from_millis(20))
.codec(hydracache_core::PostcardCodec)
.build();
cache
.put("user:1", user(1), CacheOptions::new())
.await
.unwrap();
let cached: Option<User> = cache.get("user:1").await.unwrap();
assert_eq!(cached, Some(user(1)));
}
#[tokio::test]
async fn cache_and_builder_derived_impls_are_usable() {
let builder = HydraCache::local();
let builder_clone = builder.clone();
let cache = builder_clone.build();
assert!(format!("{builder:?}").contains("HydraCacheBuilder"));
assert!(format!("{cache:?}").contains("HydraCache"));
}
#[tokio::test]
async fn get_missing_returns_none() {
let cache = HydraCache::local().build();
let cached: Option<User> = cache.get("missing").await.unwrap();
assert_eq!(cached, None);
}
#[tokio::test]
async fn get_removes_expired_entry() {
let cache = HydraCache::local().build();
cache
.put(
"user:expired",
user(1),
CacheOptions::new().ttl(Duration::from_millis(20)),
)
.await
.unwrap();
tokio::time::sleep(Duration::from_millis(40)).await;
let cached: Option<User> = cache.get("user:expired").await.unwrap();
assert_eq!(cached, None);
assert!(!cache.contains_key("user:expired").await);
}
#[tokio::test]
async fn get_or_load_loads_on_miss_and_uses_hit_afterward() {
let cache = HydraCache::local().build();
let loaded = cache
.get_or_load("user:1", CacheOptions::new(), || async {
Ok::<_, LoaderError>(user(1))
})
.await
.unwrap();
let hit = cache
.get_or_load("user:1", CacheOptions::new(), || async {
Ok::<_, LoaderError>(user(2))
})
.await
.unwrap();
assert_eq!(loaded, user(1));
assert_eq!(hit, user(1));
assert_eq!(cache.stats().loads, 1);
}
#[tokio::test]
async fn loader_helpers_cover_infallible_and_fallible_paths() {
let cache = HydraCache::local().build();
let infallible = cache
.get_or_insert_with("user:1", CacheOptions::new(), || async { user(1) })
.await
.unwrap();
let fallible = cache
.try_get_or_insert_with("user:2", CacheOptions::new(), || async {
Ok::<_, LoaderError>(user(2))
})
.await
.unwrap();
let error = cache
.try_get_or_insert_with("user:error", CacheOptions::new(), || async {
Err::<User, _>(LoaderError)
})
.await;
assert_eq!(infallible, user(1));
assert_eq!(fallible, user(2));
assert!(matches!(error, Err(CacheError::Loader(_))));
assert_eq!(cache.stats().loads, 3);
}
#[tokio::test]
async fn ttl_expires_entry_and_contains_key_removes_it() {
let cache = HydraCache::local().build();
cache
.put(
"user:1",
user(1),
CacheOptions::new().ttl(Duration::from_millis(20)),
)
.await
.unwrap();
assert!(cache.contains_key("user:1").await);
tokio::time::sleep(Duration::from_millis(40)).await;
assert!(!cache.contains_key("user:1").await);
let cached: Option<User> = cache.get("user:1").await.unwrap();
assert_eq!(cached, None);
}
#[tokio::test]
async fn remove_invalidate_tag_and_flush_clear_expected_entries() {
let cache = HydraCache::local().build();
cache
.put("user:1", user(1), CacheOptions::new().tag("users"))
.await
.unwrap();
cache
.put("user:2", user(2), CacheOptions::new().tag("users"))
.await
.unwrap();
cache
.put("order:1", user(3), CacheOptions::new())
.await
.unwrap();
assert!(cache.remove("order:1").await.unwrap());
assert_eq!(cache.invalidate_tag("users").await.unwrap(), 2);
let user_1: Option<User> = cache.get("user:1").await.unwrap();
let order_1: Option<User> = cache.get("order:1").await.unwrap();
assert_eq!(user_1, None);
assert_eq!(order_1, None);
cache
.put("user:3", user(3), CacheOptions::new())
.await
.unwrap();
cache.flush().await.unwrap();
let user_3: Option<User> = cache.get("user:3").await.unwrap();
assert_eq!(user_3, None);
}
#[tokio::test]
async fn invalidate_key_alias_removes_one_entry() {
let cache = HydraCache::local().build();
cache
.put("user:1", user(1), CacheOptions::new())
.await
.unwrap();
assert!(cache.invalidate_key("user:1").await.unwrap());
assert!(!cache.invalidate_key("user:1").await.unwrap());
}
#[tokio::test]
async fn invalidate_tag_ignores_stale_tag_index_entries() {
let cache = HydraCache::local().build();
let tags = vec!["ghosts".to_owned()];
cache.inner.tag_index.register("missing:key", &tags).await;
assert_eq!(cache.invalidate_tag("ghosts").await.unwrap(), 0);
}
#[tokio::test]
async fn overwriting_entry_removes_old_tag_mapping() {
let cache = HydraCache::local().build();
cache
.put("user:1", user(1), CacheOptions::new().tag("old"))
.await
.unwrap();
cache
.put("user:1", user(2), CacheOptions::new().tag("new"))
.await
.unwrap();
assert_eq!(cache.invalidate_tag("old").await.unwrap(), 0);
assert!(cache.contains_key("user:1").await);
assert_eq!(cache.invalidate_tag("new").await.unwrap(), 1);
}
#[tokio::test]
async fn stats_track_hits_misses_loads_invalidations() {
let cache = HydraCache::local().build();
let _: Option<User> = cache.get("user:1").await.unwrap();
cache
.get_or_load("user:1", CacheOptions::new().tag("users"), || async {
Ok::<_, LoaderError>(user(1))
})
.await
.unwrap();
let _: Option<User> = cache.get("user:1").await.unwrap();
cache.invalidate_tag("users").await.unwrap();
let stats = cache.stats();
assert_eq!(stats.misses, 2);
assert_eq!(stats.loads, 1);
assert_eq!(stats.hits, 1);
assert_eq!(stats.invalidations, 1);
}
#[tokio::test]
async fn decode_error_invalidates_bad_entry() {
let cache = HydraCache::local().build();
cache
.put_bytes(
"user:bad",
Bytes::from_static(&[0xff, 0xff, 0xff]),
CacheOptions::new(),
)
.await
.unwrap();
let result: CacheResult<Option<User>> = cache.get("user:bad").await;
assert!(matches!(result, Err(CacheError::Decode(_))));
let cached: Option<User> = cache.get("user:bad").await.unwrap();
assert_eq!(cached, None);
}
#[tokio::test]
async fn cloned_cache_handles_share_state() {
let cache = HydraCache::local().build();
let clone = cache.clone();
cache
.put("user:1", user(1), CacheOptions::new())
.await
.unwrap();
let cached: Option<User> = clone.get("user:1").await.unwrap();
assert_eq!(cached, Some(user(1)));
}
#[tokio::test]
async fn concurrent_misses_share_one_loader_execution() {
let cache = HydraCache::local().build();
let calls = Arc::new(AtomicUsize::new(0));
let mut tasks = Vec::new();
for _ in 0..8 {
let cache = cache.clone();
let calls = calls.clone();
tasks.push(tokio::spawn(async move {
cache
.get_or_load("user:shared", CacheOptions::new(), move || {
let calls = calls.clone();
async move {
calls.fetch_add(1, Ordering::SeqCst);
tokio::time::sleep(Duration::from_millis(10)).await;
Ok::<_, LoaderError>(user(7))
}
})
.await
.unwrap()
}));
}
for task in tasks {
assert_eq!(task.await.unwrap(), user(7));
}
assert_eq!(calls.load(Ordering::SeqCst), 1);
assert_eq!(cache.stats().single_flight_joins, 7);
}