#![allow(dead_code)]
mod common;
use std::collections::BTreeMap;
use std::sync::Arc;
use thesix::{
BackendKind, CacheError, CacheManager, CacheTier, Cachelito, DefaultPolicy, FixedTierStub,
KeyRef, L0Stub, L1Stub, L2Stub, L3Stub, L4Stub, L5Stub, MemoryPool, TierHealth, TierId,
TierRegistry,
};
struct L6AuthorityStub;
#[async_trait::async_trait]
impl CacheTier<String> for L6AuthorityStub {
fn name(&self) -> String {
"L6-authority".to_string()
}
fn backend(&self) -> BackendKind {
BackendKind::Unavailable
}
async fn get(&self, _k: &KeyRef<'_>) -> Result<Option<String>, CacheError> {
Ok(None)
}
async fn set(
&self,
_k: &KeyRef<'_>,
_v: String,
_t: Option<std::time::Duration>,
) -> Result<(), CacheError> {
Ok(())
}
async fn remove(&self, _k: &KeyRef<'_>) -> Result<(), CacheError> {
Ok(())
}
async fn contains(&self, _k: &KeyRef<'_>) -> Result<bool, CacheError> {
Ok(false)
}
fn health(&self) -> TierHealth {
TierHealth::default()
}
fn tier_id(&self) -> TierId {
TierId::L6
}
}
fn seven_tier_manager() -> Arc<CacheManager<String, String, DefaultPolicy>> {
let tiers: Vec<Arc<dyn CacheTier<String>>> = vec![
Arc::new(L0Stub::new()),
Arc::new(L1Stub::new()),
Arc::new(L2Stub::new()),
Arc::new(L3Stub::new()),
Arc::new(L4Stub::new()),
Arc::new(L5Stub::new()),
Arc::new(L6AuthorityStub),
];
let pool = MemoryPool::new(1024).expect("pool");
Arc::new(CacheManager::new(
DefaultPolicy,
Cachelito::new(),
TierRegistry::new(),
tiers,
pool,
))
}
fn caps(m: &CacheManager<String, String, DefaultPolicy>) -> BTreeMap<TierId, BackendKind> {
m.capabilities()
}
#[test]
fn capabilities_cover_every_rung() {
let m = seven_tier_manager();
let c = caps(&m);
assert_eq!(
c.len(),
7,
"capabilities must report all seven rungs; a short report hides the ones it forgot"
);
for id in TierId::ALL {
assert!(c.contains_key(&id), "capabilities omitted {id}");
}
}
#[test]
fn default_build_reports_fallbacks_not_rich_backends() {
let m = seven_tier_manager();
let c = caps(&m);
for id in [TierId::L0, TierId::L1, TierId::L2] {
assert_eq!(
c[&id],
BackendKind::InMemory,
"{id} is a real in-memory tier and must report as one"
);
}
for id in [TierId::L3, TierId::L4, TierId::L5] {
assert_eq!(
c[&id],
BackendKind::InMemoryFallback,
"{id} is a stand-in and must not report as its advertised backend"
);
assert!(
!c[&id].is_native(),
"{id} is a fallback: is_native must be false or callers will rely on \
durability or cross-process sharing it does not provide"
);
}
assert_ne!(
c[&TierId::L3],
BackendKind::Redis,
"an unbound L3 must never claim to be redis"
);
assert!(
!c[&TierId::L3].is_shared(),
"an in-memory L3 is not shared across processes"
);
assert!(
!c[&TierId::L4].is_persistent(),
"an in-memory L4 does not survive a restart"
);
}
#[test]
fn unbound_tier_reports_unavailable_not_a_substitute() {
let m: Arc<CacheManager<String, String, DefaultPolicy>> = common::make_manager(DefaultPolicy);
let c = caps(&m);
assert_eq!(
c[&TierId::L6],
BackendKind::Unavailable,
"an unbound L6 must report Unavailable; tier_for would silently hand back L0"
);
assert!(!c[&TierId::L6].is_native());
assert!(
m.has_tier(&TierId::L5) && !m.has_tier(&TierId::L6),
"has_tier must agree with the capability report"
);
}
#[cfg(feature = "redis")]
#[test]
fn redis_backend_reports_redis() {
let c: BTreeMap<TierId, BackendKind> = BTreeMap::new();
assert!(c.is_empty());
assert!(BackendKind::Redis.is_native());
assert!(BackendKind::Redis.is_shared());
assert!(!BackendKind::Redis.is_persistent());
}
#[cfg(feature = "sled")]
#[test]
fn sled_backend_reports_sled() {
assert!(BackendKind::Sled.is_native());
assert!(BackendKind::Sled.is_persistent());
assert!(!BackendKind::Sled.is_shared());
}
#[test]
fn backend_classification_is_explicit() {
assert!(BackendKind::InMemory.is_native());
assert!(!BackendKind::InMemory.is_persistent());
assert!(!BackendKind::InMemory.is_shared());
assert!(!BackendKind::InMemoryFallback.is_native());
assert!(!BackendKind::Unavailable.is_native());
assert!(!BackendKind::Test.is_native());
assert!(BackendKind::Postgres.is_persistent());
assert!(BackendKind::Postgres.is_shared());
assert!(BackendKind::RocksDb.is_persistent());
assert!(!BackendKind::RocksDb.is_shared());
assert!(BackendKind::Oxigraph.is_native());
assert!(BackendKind::Helix.is_shared());
assert!(BackendKind::Neo4j.is_shared());
}
#[test]
fn backend_kind_displays_readably() {
assert_eq!(
BackendKind::InMemoryFallback.to_string(),
"in-memory-fallback"
);
assert_eq!(BackendKind::Unavailable.to_string(), "unavailable");
assert_eq!(BackendKind::Redis.to_string(), "redis");
assert_eq!(BackendKind::Oxigraph.to_string(), "oxigraph");
}
#[tokio::test]
async fn a_full_tier_reports_capacity_exhausted() {
let stub: FixedTierStub<String> = FixedTierStub::with_capacity(2).expect("capacity 2 is valid");
let tier: Arc<dyn CacheTier<String>> = Arc::new(StubAdapter(std::sync::Mutex::new(stub)));
tier.set(&KeyRef::from(&b"k1"[..]), "a".to_string(), None)
.await
.expect("first fits");
tier.set(&KeyRef::from(&b"k2"[..]), "b".to_string(), None)
.await
.expect("second fits");
let third = tier
.set(&KeyRef::from(&b"k3"[..]), "c".to_string(), None)
.await;
assert!(
matches!(third, Err(CacheError::CapacityExhausted)),
"a full tier must report CapacityExhausted, got {third:?}"
);
assert!(
!matches!(third, Err(CacheError::ConfigurationError)),
"capacity exhaustion must not masquerade as a configuration error"
);
}
struct StubAdapter<T>(std::sync::Mutex<FixedTierStub<T>>);
#[async_trait::async_trait]
impl<T: Clone + Send + Sync + 'static> CacheTier<T> for StubAdapter<T> {
fn name(&self) -> String {
"stub".to_string()
}
fn backend(&self) -> BackendKind {
BackendKind::InMemory
}
async fn get(&self, k: &KeyRef<'_>) -> Result<Option<T>, CacheError> {
self.0.lock().expect("lock").get(k)
}
async fn set(
&self,
k: &KeyRef<'_>,
v: T,
ttl: Option<std::time::Duration>,
) -> Result<(), CacheError> {
self.0.lock().expect("lock").set(k, v, ttl)
}
async fn remove(&self, k: &KeyRef<'_>) -> Result<(), CacheError> {
self.0.lock().expect("lock").remove(k)
}
async fn contains(&self, k: &KeyRef<'_>) -> Result<bool, CacheError> {
self.0.lock().expect("lock").contains(k)
}
fn health(&self) -> TierHealth {
TierHealth::default()
}
fn tier_id(&self) -> TierId {
TierId::L0
}
}
#[test]
fn a_consumer_can_fail_fast_on_an_unexpected_backend() {
let m = seven_tier_manager();
let c = caps(&m);
let distributed_l3_ok = matches!(
c.get(&TierId::L3),
Some(k) if k.is_shared()
);
assert!(
!distributed_l3_ok,
"with no redis feature, refusing to start is the correct outcome - and it is \
now reachable without issuing a single cache operation"
);
}
#[tokio::test]
async fn reported_fallbacks_actually_store() {
let tiers: Vec<Arc<dyn CacheTier<String>>> = vec![
Arc::new(L3Stub::new()),
Arc::new(L4Stub::new()),
Arc::new(L5Stub::new()),
];
for (i, tier) in tiers.iter().enumerate() {
let kind = tier.backend();
assert_eq!(
kind,
BackendKind::InMemoryFallback,
"tier {i} should report a fallback"
);
let key = format!("key-{i}");
let kr = || KeyRef::from(key.as_bytes());
let payload = format!("value-{i}");
assert!(
!tier.contains(&kr()).await.expect("contains must work"),
"tier {i} should start empty"
);
tier.set(&kr(), payload.clone(), None)
.await
.unwrap_or_else(|e| panic!("tier {i} reported {kind} but refused a write: {e:?}"));
assert_eq!(
tier.get(&kr()).await.expect("get must work"),
Some(payload.clone()),
"tier {i} reported {kind} but did not return what was written"
);
assert!(
tier.contains(&kr()).await.expect("contains must work"),
"tier {i} reported {kind} but does not hold what was written to it"
);
tier.remove(&kr()).await.expect("remove must work");
assert!(
!tier.contains(&kr()).await.expect("contains must work"),
"tier {i} reported {kind} but did not drop a removed key"
);
}
}
#[tokio::test]
async fn the_ladder_works_end_to_end_with_only_fallbacks() {
let m = seven_tier_manager();
let ctx = common::test_ctx();
m.set(&"warm".to_string(), "value".to_string(), &ctx)
.await
.expect("set must succeed against a default seven-rung manager");
let got = m
.get(&"warm".to_string(), &ctx)
.await
.expect("get must not error");
assert_eq!(
got.as_deref(),
Some("value"),
"a default manager could not read back what it just wrote"
);
}