#[cfg(feature = "cache")]
mod cache_tests {
use dbnexus::CacheConfig;
use dbnexus::CacheKey;
use moka::sync::Cache;
use moka::sync::CacheBuilder;
use std::sync::Arc;
use std::time::Duration;
fn create_test_cache<V>(capacity: u64) -> Cache<String, V>
where
V: Send + Sync + 'static,
{
CacheBuilder::new(capacity)
}
fn create_test_cache_with_ttl<V>(capacity: u64, ttl: Duration) -> Cache<String, V>
where
V: Send + Sync + 'static,
{
CacheBuilder::new(capacity)
.time_to_live(ttl)
}
#[test]
fn test_lru_eviction_policy_capacity_exceeded() {
let cache = create_test_cache::<String>(3);
cache.insert("key1".to_string(), "value1".to_string());
cache.insert("key2".to_string(), "value2".to_string());
cache.insert("key3".to_string(), "value3".to_string());
assert!(cache.get(&"key1".to_string()).is_some(), "key1 should exist after initial insert");
assert!(cache.get(&"key2".to_string()).is_some(), "key2 should exist after initial insert");
assert!(cache.get(&"key3".to_string()).is_some(), "key3 should exist after initial insert");
cache.insert("key4".to_string(), "value4".to_string());
assert!(cache.get(&"key4".to_string()).is_some(), "key4 should exist after insert triggering eviction");
let remaining_count = [
cache.get(&"key1".to_string()).is_some(),
cache.get(&"key2".to_string()).is_some(),
cache.get(&"key3".to_string()).is_some(),
].iter().filter(|&&x| x).count();
assert!(remaining_count >= 2, "LRU 淘汰后应保留至少 2 个项目,实际保留: {}", remaining_count);
}
#[test]
fn test_lru_eviction_policy_access_updates_order() {
let cache = create_test_cache::<String>(3);
cache.insert("key1".to_string(), "value1".to_string());
cache.insert("key2".to_string(), "value2".to_string());
cache.insert("key3".to_string(), "value3".to_string());
let _ = cache.get(&"key1".to_string());
cache.insert("key4".to_string(), "value4".to_string());
let key1_exists = cache.get(&"key1".to_string()).is_some();
let remaining_old = [
cache.get(&"key2".to_string()).is_some(),
cache.get(&"key3".to_string()).is_some(),
].iter().filter(|&&x| x).count();
assert!(key1_exists, "key1 刚被访问过,不应被淘汰");
assert!(remaining_old <= 2, "旧项目中至少有一个被淘汰");
}
#[test]
fn test_lru_eviction_policy_write_updates_order() {
let cache = create_test_cache::<String>(2);
cache.insert("key1".to_string(), "value1".to_string());
cache.insert("key2".to_string(), "value2".to_string());
cache.insert("key1".to_string(), "value1_updated".to_string());
cache.insert("key3".to_string(), "value3".to_string());
assert!(cache.get(&"key1".to_string()).is_some());
}
#[test]
fn test_cache_preheat_batch_insert() {
let cache = create_test_cache::<String>(100);
let preheat_data = vec![
("config:max_connections".to_string(), "100".to_string()),
("config:timeout".to_string(), "30".to_string()),
("config:retries".to_string(), "3".to_string()),
];
for (key, value) in preheat_data.iter() {
cache.insert(key.clone(), value.clone());
}
let max_conn = cache.get(&"config:max_connections".to_string());
assert_eq!(max_conn, Some(&"100".to_string()));
let timeout = cache.get(&"config:timeout".to_string());
assert_eq!(timeout, Some(&"30".to_string()));
let retries = cache.get(&"config:retries".to_string());
assert_eq!(retries, Some(&"3".to_string()));
}
#[test]
fn test_cache_preheat_concurrent() {
let cache = Arc::new(create_test_cache::<String>(100));
let keys: Vec<String> = (0..20).map(|i| format!("preheat:{}", i)).collect();
let cache_clone1 = Arc::clone(&cache);
let cache_clone2 = Arc::clone(&cache);
std::thread::spawn(move || {
for key in keys.iter().take(10) {
cache_clone1.insert(key.clone(), format!("value_{}", key));
}
});
std::thread::spawn(move || {
for key in keys.iter().skip(10) {
cache_clone2.insert(key.clone(), format!("value_{}", key));
}
});
std::thread::sleep(Duration::from_millis(100));
for key in keys.iter() {
let value = cache.get(key);
assert!(value.is_some(), "预热 key {} 应该存在", key);
}
}
#[test]
fn test_cache_preheat_persistence() {
let cache = create_test_cache::<String>(10);
cache.insert("user:1".to_string(), "Alice".to_string());
cache.insert("user:2".to_string(), "Bob".to_string());
for _ in 0..5 {
assert_eq!(cache.get(&"user:1".to_string()), Some(&"Alice".to_string()));
assert_eq!(cache.get(&"user:2".to_string()), Some(&"Bob".to_string()));
}
}
#[test]
fn test_cache_penetration_protection_null_caching() {
let cache = create_test_cache::<String>(100);
let missing_key = "user:99999".to_string();
let result = cache.get(&missing_key);
assert!(result.is_none());
cache.insert(missing_key.clone(), "".to_string());
let cached_result = cache.get(&missing_key);
assert!(cached_result.is_some());
}
#[test]
fn test_cache_penetration_protection_sentinel_value() {
let cache = create_test_cache::<String>(100);
const NULL_MARKER: &str = "__CACHE_NULL__";
let missing_key = "product:不存在".to_string();
cache.insert(missing_key.clone(), NULL_MARKER.to_string());
let value = cache.get(&missing_key);
assert!(value.is_some());
assert_eq!(value.unwrap(), NULL_MARKER);
}
#[test]
fn test_cache_penetration_protection_miss_counting() {
let cache = create_test_cache::<String>(100);
let miss_key = "sensitive:data".to_string();
let mut miss_count = 0;
for _ in 0..10 {
if cache.get(&miss_key).is_none() {
miss_count += 1;
}
}
assert_eq!(miss_count, 10, "未缓存的键应该有 10 次未命中");
cache.insert(miss_key.clone(), "".to_string());
let hit_count = if cache.get(&miss_key).is_some() { 1 } else { 0 };
assert_eq!(hit_count, 1, "缓存后应该命中");
}
#[test]
fn test_cache_avalanche_protection_randomized_ttl() {
let cache1 = create_test_cache_with_ttl::<String>(100, Duration::from_secs(10));
let cache2 = create_test_cache_with_ttl::<String>(100, Duration::from_secs(15));
let cache3 = create_test_cache_with_ttl::<String>(100, Duration::from_secs(20));
let key = "shared:data".to_string();
cache1.insert(key.clone(), "value1".to_string());
cache2.insert(key.clone(), "value2".to_string());
cache3.insert(key.clone(), "value3".to_string());
assert!(cache1.get(&key).is_some());
assert!(cache2.get(&key).is_some());
assert!(cache3.get(&key).is_some());
}
#[test]
fn test_cache_avalanche_protection_gradual_expiration() {
let cache = create_test_cache::<String>(100);
cache.insert("key:1".to_string(), "value1".to_string());
cache.insert("key:2".to_string(), "value2".to_string());
cache.insert("key:3".to_string(), "value3".to_string());
assert!(cache.get(&"key:1".to_string()).is_some());
assert!(cache.get(&"key:2".to_string()).is_some());
assert!(cache.get(&"key:3".to_string()).is_some());
cache.insert("key:1".to_string(), "value1_v2".to_string());
assert_eq!(cache.get(&"key:1".to_string()), Some(&"value1_v2".to_string()));
}
#[test]
fn test_cache_avalanche_protection_rebuild() {
let cache = create_test_cache_with_ttl::<String>(10, Duration::from_millis(50));
let key = "avalanche:test".to_string();
cache.insert(key.clone(), "initial".to_string());
assert_eq!(cache.get(&key), Some(&"initial".to_string()));
std::thread::sleep(Duration::from_millis(100));
cache.insert(key.clone(), "rebuilt".to_string());
assert_eq!(cache.get(&key), Some(&"rebuilt".to_string()));
}
#[test]
fn test_cache_key_pattern_delete_prefix() {
let cache = create_test_cache::<String>(100);
let user_keys = vec![
"user:1".to_string(),
"user:2".to_string(),
"user:3".to_string(),
];
let product_keys = vec![
"product:1".to_string(),
"product:2".to_string(),
];
for key in user_keys.iter() {
cache.insert(key.clone(), format!("value_{}", key));
}
for key in product_keys.iter() {
cache.insert(key.clone(), format!("value_{}", key));
}
assert!(cache.get(&"user:1".to_string()).is_some());
assert!(cache.get(&"product:1".to_string()).is_some());
let keys_to_delete: Vec<String> = vec![
"user:1".to_string(),
"user:2".to_string(),
"user:3".to_string(),
];
for key in keys_to_delete {
cache.invalidate(&key);
}
assert!(cache.get(&"user:1".to_string()).is_none());
assert!(cache.get(&"user:2".to_string()).is_none());
assert!(cache.get(&"user:3".to_string()).is_none());
assert!(cache.get(&"product:1".to_string()).is_some());
assert!(cache.get(&"product:2".to_string()).is_some());
}
#[test]
fn test_cache_key_pattern_delete_exact() {
let cache = create_test_cache::<String>(100);
cache.insert("config:db".to_string(), "postgres".to_string());
cache.insert("config:cache".to_string(), "moka".to_string());
cache.insert("config:log".to_string(), "debug".to_string());
cache.invalidate(&"config:db".to_string());
assert!(cache.get(&"config:db".to_string()).is_none());
assert!(cache.get(&"config:cache".to_string()).is_some());
assert!(cache.get(&"config:log".to_string()).is_some());
}
#[test]
fn test_cache_key_pattern_delete_batch() {
let cache = create_test_cache::<String>(100);
let keys = vec![
"session:1".to_string(),
"session:2".to_string(),
"session:3".to_string(),
"cache:1".to_string(),
"cache:2".to_string(),
];
for key in keys.iter() {
cache.insert(key.clone(), "value".to_string());
}
let session_keys = vec![
"session:1".to_string(),
"session:2".to_string(),
"session:3".to_string(),
];
for key in session_keys.iter() {
cache.invalidate(key);
}
for key in session_keys.iter() {
assert!(cache.get(key).is_none(), "键 {} 应该被删除", key);
}
assert!(cache.get(&"cache:1".to_string()).is_some());
assert!(cache.get(&"cache:2".to_string()).is_some());
}
#[test]
fn test_cache_key_pattern_clear_all() {
let cache = create_test_cache::<String>(100);
cache.insert("key1".to_string(), "value1".to_string());
cache.insert("key2".to_string(), "value2".to_string());
cache.insert("key3".to_string(), "value3".to_string());
cache.invalidate(&"key1".to_string());
cache.invalidate(&"key2".to_string());
cache.invalidate(&"key3".to_string());
assert!(cache.get(&"key1".to_string()).is_none());
assert!(cache.get(&"key2".to_string()).is_none());
assert!(cache.get(&"key3".to_string()).is_none());
}
#[test]
fn test_cache_config_default() {
let config = CacheConfig::default();
assert_eq!(config.capacity, 1000);
assert_eq!(config.ttl, None);
}
#[test]
fn test_cache_config_custom() {
let config = CacheConfig::new(500, Some(60));
assert_eq!(config.capacity, 500);
assert_eq!(config.ttl, Some(60));
}
#[test]
fn test_cache_config_builder_pattern() {
let config = CacheConfig::new(1000, None)
.capacity(500)
.ttl(120);
assert_eq!(config.capacity, 500);
assert_eq!(config.ttl, Some(120));
}
#[test]
fn test_cache_key_trait_implementation() {
let key = "users:123".to_string();
let cache_key = key.to_cache_key();
assert_eq!(cache_key, "users:123");
}
#[test]
fn test_cache_empty_key() {
let cache = create_test_cache::<String>(10);
let empty_key = "".to_string();
cache.insert(empty_key.clone(), "empty_value".to_string());
let result = cache.get(&empty_key);
assert!(result.is_some());
}
#[test]
fn test_cache_very_long_key() {
let cache = create_test_cache::<String>(10);
let long_key = "x".repeat(10000);
let long_value = "y".repeat(10000);
cache.insert(long_key.clone(), long_value.clone());
let result = cache.get(&long_key);
assert!(result.is_some());
assert_eq!(result.unwrap(), long_value);
}
#[test]
fn test_cache_special_characters_key() {
let cache = create_test_cache::<String>(10);
let special_keys = vec![
"key:with:colons".to_string(),
"key-with-dashes".to_string(),
"key.with.dots".to_string(),
"key_under_score".to_string(),
"key with spaces".to_string(),
];
for key in special_keys.iter() {
cache.insert(key.clone(), format!("value_{}", key));
}
for key in special_keys.iter() {
let result = cache.get(key);
assert!(result.is_some(), "键 {:?} 应该存在", key);
}
}
#[test]
fn test_cache_capacity_boundary() {
let cache = create_test_cache::<String>(1);
cache.insert("key1".to_string(), "value1".to_string());
cache.insert("key2".to_string(), "value2".to_string());
let has_key = cache.get(&"key1".to_string()).is_some()
|| cache.get(&"key2".to_string()).is_some();
assert!(has_key);
}
#[test]
fn test_cache_update_existing_key() {
let cache = create_test_cache::<String>(10);
cache.insert("counter".to_string(), "0".to_string());
for i in 1..=5 {
cache.insert("counter".to_string(), i.to_string());
}
assert_eq!(cache.get(&"counter".to_string()), Some(&"5".to_string()));
}
#[test]
fn test_cache_different_value_types() {
let string_cache = create_test_cache::<String>(10);
let vec_cache = create_test_cache::<Vec<u8>>(10);
let num_cache = create_test_cache::<u64>(10);
string_cache.insert("str".to_string(), "hello".to_string());
vec_cache.insert("vec".to_string(), vec![1, 2, 3]);
num_cache.insert("num".to_string(), 42);
assert_eq!(string_cache.get(&"str".to_string()), Some(&"hello".to_string()));
assert_eq!(vec_cache.get(&"vec".to_string()), Some(&vec![1, 2, 3]));
assert_eq!(num_cache.get(&"num".to_string()), Some(&42));
}
}