#![allow(
clippy::arithmetic_side_effects,
clippy::indexing_slicing,
clippy::unwrap_used,
clippy::expect_used,
clippy::panic
)]
use super::*;
use crate::proxy::types::{CacheStatus, SearchResult};
fn make_response(content: &str) -> QueryResponse {
QueryResponse {
results: vec![SearchResult {
id: "test".to_string(),
score: 1.0,
content: content.to_string(),
metadata: None,
upstream_id: None,
}],
cache_status: CacheStatus::Miss,
took_ms: 10,
generated_at: None,
miss_reason: None,
}
}
#[test]
fn test_normalize_query() {
assert_eq!(
CacheStore::normalize_query(" Hello World "),
"hello world"
);
assert_eq!(CacheStore::normalize_query("TEST"), "test");
assert_eq!(CacheStore::normalize_query("a\t\nb"), "a b");
assert_eq!(CacheStore::normalize_query(" "), "");
}
#[test]
fn test_hash_query_consistency() {
let h1 = CacheStore::hash_query("hello world");
let h2 = CacheStore::hash_query(" HELLO WORLD ");
assert_eq!(h1, h2, "Normalized queries should have the same hash");
let h3 = CacheStore::hash_query("different query");
assert_ne!(h1, h3, "Different queries should have different hashes");
}
#[test]
fn test_insert_and_get() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
store.insert(
"test query",
make_response("result"),
"upstream1".to_string(),
);
let entry = store.get("test query");
assert!(entry.is_some());
assert_eq!(entry.unwrap().upstream_id, "upstream1");
}
#[test]
fn test_evict_from_upstream_broadcasts_cdc_remove() {
let sender = std::sync::Arc::new(crate::proxy::cdc::EventSender::new(
64,
"test-node".to_string(),
));
let mut rx = sender.subscribe();
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000)
.with_cdc_sender(sender);
for q in ["query one", "query two", "query three"] {
store.insert(q, make_response("result"), "up1".to_string());
}
while rx.try_recv().is_ok() {}
let evicted = store.evict_from_upstream("up1", 10);
assert_eq!(evicted, 3);
let mut removes = std::collections::HashSet::new();
while let Ok(ev) = rx.try_recv() {
if ev.event_type == CdcEventType::Remove {
removes.insert(ev.query_key);
}
}
assert_eq!(removes.len(), 3);
assert!(removes.contains("query one"));
assert!(removes.contains("query two"));
assert!(removes.contains("query three"));
}
#[test]
fn test_evict_from_upstream_no_cdc_without_sender() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
store.insert("q", make_response("r"), "up1".to_string());
assert_eq!(store.evict_from_upstream("up1", 5), 1);
}
#[test]
fn test_get_normalized() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
store.insert(
"hello world",
make_response("result"),
"upstream1".to_string(),
);
assert!(store.get(" HELLO WORLD ").is_some());
assert!(store.get("Hello World").is_some());
}
#[test]
fn test_freshness() {
let mut store = CacheStore::new(
Duration::from_millis(500), Duration::from_millis(1000), 1000,
);
store.set_max_frozen_duration(Duration::from_millis(2500));
store.insert("test", make_response("result"), "upstream1".to_string());
assert_eq!(store.check_freshness("test"), Some(Freshness::Fresh));
std::thread::sleep(Duration::from_millis(700));
assert_eq!(store.check_freshness("test"), Some(Freshness::Stale));
std::thread::sleep(Duration::from_millis(1000));
assert_eq!(store.check_freshness("test"), Some(Freshness::Frozen));
std::thread::sleep(Duration::from_millis(1500));
assert_eq!(store.check_freshness("test"), Some(Freshness::Expired));
}
#[test]
fn test_extend_ttl() {
let store = CacheStore::new(Duration::from_millis(100), Duration::from_millis(200), 1000);
store.insert("test", make_response("result"), "upstream1".to_string());
std::thread::sleep(Duration::from_millis(80));
store.extend_ttl("test");
assert_eq!(store.check_freshness("test"), Some(Freshness::Fresh));
let entry = store.get("test").unwrap();
assert_eq!(entry.extended_count, 1);
}
#[test]
fn test_remove() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
store.insert("test", make_response("result"), "upstream1".to_string());
assert!(store.get("test").is_some());
store.remove("test");
assert!(store.get("test").is_none());
}
#[test]
fn test_evict_truly_expired() {
let mut store = CacheStore::new(
Duration::from_millis(10), Duration::from_millis(20), 1000,
);
store.set_max_frozen_duration(Duration::from_millis(50));
store.insert("test1", make_response("r1"), "upstream".to_string());
store.insert("test2", make_response("r2"), "upstream".to_string());
assert_eq!(store.len(), 2);
std::thread::sleep(Duration::from_millis(60));
let evicted = store.evict_truly_expired();
assert_eq!(evicted, 2);
assert_eq!(store.len(), 0);
}
#[test]
fn test_eviction() {
let store = CacheStore::new(
Duration::from_secs(300),
Duration::from_secs(3600),
10, );
for i in 0..15 {
store.insert(
&format!("query{}", i),
make_response(&format!("result{}", i)),
"upstream".to_string(),
);
std::thread::sleep(Duration::from_millis(10)); }
assert!(store.len() <= 10);
}
#[test]
fn test_stats() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
store.insert("q1", make_response("r1"), "upstream".to_string());
store.insert("q2", make_response("r2"), "upstream".to_string());
let stats = store.stats();
assert_eq!(stats.total, 2);
assert_eq!(stats.fresh, 2);
assert_eq!(stats.stale, 0);
assert_eq!(stats.expired, 0);
}
#[test]
fn test_clear() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
store.insert("q1", make_response("r1"), "upstream".to_string());
store.insert("q2", make_response("r2"), "upstream".to_string());
assert_eq!(store.len(), 2);
store.clear();
assert!(store.is_empty());
}
#[test]
fn test_with_jitter_creation() {
let store = CacheStore::with_jitter(
Duration::from_secs(300),
Duration::from_secs(3600),
1000,
0.2, );
assert_eq!(store.ttl_jitter_percent(), 0.2);
}
#[test]
fn test_jitter_clamped() {
let store1 = CacheStore::with_jitter(
Duration::from_secs(300),
Duration::from_secs(3600),
1000,
-0.5, );
assert_eq!(store1.ttl_jitter_percent(), 0.0);
let store2 = CacheStore::with_jitter(
Duration::from_secs(300),
Duration::from_secs(3600),
1000,
1.5, );
assert_eq!(store2.ttl_jitter_percent(), 1.0);
}
#[test]
fn test_jittered_ttl_deterministic() {
let store = CacheStore::with_jitter(
Duration::from_secs(300),
Duration::from_secs(3600),
1000,
0.1,
);
let hash = CacheStore::hash_query("test query");
let base_ttl = Duration::from_secs(300);
let jittered1 = store.jittered_ttl(base_ttl, &hash);
let jittered2 = store.jittered_ttl(base_ttl, &hash);
assert_eq!(jittered1, jittered2);
assert!(jittered1 >= base_ttl);
}
#[test]
fn test_jittered_ttl_varies_by_query() {
let store = CacheStore::with_jitter(
Duration::from_secs(300),
Duration::from_secs(3600),
1000,
0.1,
);
let hash1 = CacheStore::hash_query("query one");
let hash2 = CacheStore::hash_query("query two");
let base_ttl = Duration::from_secs(300);
let jittered1 = store.jittered_ttl(base_ttl, &hash1);
let jittered2 = store.jittered_ttl(base_ttl, &hash2);
assert!(jittered1 >= base_ttl);
assert!(jittered2 >= base_ttl);
assert!(jittered1 <= base_ttl + Duration::from_secs(30)); assert!(jittered2 <= base_ttl + Duration::from_secs(30));
}
#[test]
fn test_jittered_ttl_zero_jitter() {
let store_no_jitter = CacheStore::with_jitter(
Duration::from_secs(300),
Duration::from_secs(3600),
1000,
0.0,
);
let hash = CacheStore::hash_query("test");
let base_ttl = Duration::from_secs(300);
let jittered = store_no_jitter.jittered_ttl(base_ttl, &hash);
assert_eq!(jittered, base_ttl);
}
#[test]
fn test_config_fingerprint_changes() {
let fp1 = CacheStore::compute_config_fingerprint(Some("http://a.com"), &[]);
let fp2 = CacheStore::compute_config_fingerprint(Some("http://b.com"), &[]);
assert_ne!(fp1, fp2);
let fp3 = CacheStore::compute_config_fingerprint(Some("http://a.com"), &["seed1".to_string()]);
let fp4 = CacheStore::compute_config_fingerprint(Some("http://a.com"), &["seed2".to_string()]);
assert_ne!(fp1, fp3);
assert_ne!(fp3, fp4);
}
#[test]
fn test_config_fingerprint_deterministic() {
let fp1 = CacheStore::compute_config_fingerprint(
Some("http://example.com"),
&["seed1".to_string(), "seed2".to_string()],
);
let fp2 = CacheStore::compute_config_fingerprint(
Some("http://example.com"),
&["seed1".to_string(), "seed2".to_string()],
);
assert_eq!(fp1, fp2);
}
#[test]
fn test_update_config_fingerprint_invalidates() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
store.insert("q1", make_response("r1"), "upstream".to_string());
store.insert("q2", make_response("r2"), "upstream".to_string());
assert_eq!(store.len(), 2);
let invalidated = store.update_config_fingerprint(Some("http://a.com"), &[]);
assert!(!invalidated);
assert_eq!(store.len(), 2);
let invalidated = store.update_config_fingerprint(Some("http://b.com"), &[]);
assert!(invalidated);
assert_eq!(store.len(), 0);
}
#[test]
fn test_update_config_fingerprint_no_change() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
store.insert("q1", make_response("r1"), "upstream".to_string());
store.update_config_fingerprint(Some("http://a.com"), &["seed".to_string()]);
let invalidated = store.update_config_fingerprint(Some("http://a.com"), &["seed".to_string()]);
assert!(!invalidated);
assert_eq!(store.len(), 1);
}
#[test]
fn test_get_stale_hashes() {
let store = CacheStore::with_jitter(
Duration::from_millis(300), Duration::from_millis(1000), 1000,
0.0, );
store.insert("q1", make_response("r1"), "upstream".to_string());
store.insert("q2", make_response("r2"), "upstream".to_string());
let stale = store.get_stale_hashes();
assert!(stale.is_empty());
std::thread::sleep(Duration::from_millis(500));
let stale = store.get_stale_hashes();
assert_eq!(stale.len(), 2);
}
#[test]
fn test_get_by_hash() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
let hash = CacheStore::hash_query("test query");
store.insert(
"test query",
make_response("result"),
"upstream".to_string(),
);
let entry = store.get_by_hash(&hash);
assert!(entry.is_some());
assert_eq!(entry.unwrap().response.results[0].content, "result");
}
#[test]
fn test_insert_by_hash() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
let hash = CacheStore::hash_query("test query");
store.insert_by_hash(hash, make_response("result"), "upstream".to_string(), None);
let entry = store.get_by_hash(&hash);
assert!(entry.is_some());
}
#[test]
fn test_warmup() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 100);
let entries: Vec<(String, QueryResponse)> = (0..10)
.map(|i| {
(
format!("query{}", i),
make_response(&format!("result{}", i)),
)
})
.collect();
let inserted = store.warmup(entries, "warmup".to_string());
assert_eq!(inserted, 10);
assert_eq!(store.len(), 10);
let entry = store.get("query0");
assert!(entry.is_some());
assert_eq!(entry.unwrap().response.results[0].content, "result0");
}
#[test]
fn test_warmup_respects_max_entries() {
let store = CacheStore::new(
Duration::from_secs(300),
Duration::from_secs(3600),
5, );
let entries: Vec<(String, QueryResponse)> = (0..10)
.map(|i| {
(
format!("query{}", i),
make_response(&format!("result{}", i)),
)
})
.collect();
let inserted = store.warmup(entries, "warmup".to_string());
assert_eq!(inserted, 5); assert_eq!(store.len(), 5);
}
#[test]
fn test_warmup_skips_invalid_responses() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 100);
let entries = vec![
("valid".to_string(), make_response("valid content")),
(
"invalid".to_string(),
QueryResponse {
results: vec![SearchResult {
id: "".to_string(), score: 0.9,
content: "content".to_string(),
metadata: None,
upstream_id: None,
}],
cache_status: CacheStatus::Miss,
took_ms: 10,
generated_at: None,
miss_reason: None,
},
),
("also_valid".to_string(), make_response("also valid")),
];
let inserted = store.warmup(entries, "warmup".to_string());
assert_eq!(inserted, 2); assert!(store.get("valid").is_some());
assert!(store.get("invalid").is_none());
assert!(store.get("also_valid").is_some());
}
#[test]
fn test_verify_integrity() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
store.insert("q1", make_response("r1"), "upstream".to_string());
store.insert("q2", make_response("r2"), "upstream".to_string());
let report = store.verify_integrity();
assert_eq!(report.total, 2);
assert_eq!(report.valid, 2);
assert_eq!(report.invalid, 0);
}
#[test]
fn test_content_hash_computed_on_insert() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
store.insert("test", make_response("content"), "upstream".to_string());
let entry = store.get("test").unwrap();
assert!(entry.content_hash.is_some());
assert!(entry.verify_integrity());
}
#[test]
fn test_get_verified_valid_entry() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
store.insert("test", make_response("content"), "upstream".to_string());
let entry = store.get_verified("test");
assert!(entry.is_some());
assert_eq!(entry.unwrap().response.results[0].content, "content");
}
#[test]
fn test_get_verified_nonexistent() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
assert!(store.get_verified("nonexistent").is_none());
}
#[test]
fn test_entry_approximate_size() {
let response = make_response("this is test content");
let entry = CacheEntry::new_with_hash(
response,
"upstream".to_string(),
SchemaFingerprint::default(),
"default".to_string(),
);
let size = entry.approximate_size();
assert!(size > 0);
assert!(size > 20); }
#[test]
fn test_memory_usage() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
let initial = store.memory_usage();
assert_eq!(initial, 0);
store.insert(
"q1",
make_response("some content here"),
"upstream".to_string(),
);
let after_insert = store.memory_usage();
assert!(after_insert > 0);
}
#[test]
fn test_entries_by_upstream() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
store.insert("q1", make_response("r1"), "upstream_a".to_string());
store.insert("q2", make_response("r2"), "upstream_a".to_string());
store.insert("q3", make_response("r3"), "upstream_b".to_string());
let by_upstream = store.entries_by_upstream();
assert_eq!(by_upstream.get("upstream_a"), Some(&2));
assert_eq!(by_upstream.get("upstream_b"), Some(&1));
assert_eq!(by_upstream.get("upstream_c"), None);
}
#[test]
fn test_count_for_upstream() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
store.insert("q1", make_response("r1"), "upstream_a".to_string());
store.insert("q2", make_response("r2"), "upstream_a".to_string());
store.insert("q3", make_response("r3"), "upstream_b".to_string());
assert_eq!(store.count_for_upstream("upstream_a"), 2);
assert_eq!(store.count_for_upstream("upstream_b"), 1);
assert_eq!(store.count_for_upstream("nonexistent"), 0);
}
#[test]
fn test_evict_from_upstream() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
store.insert("q1", make_response("r1"), "upstream_a".to_string());
std::thread::sleep(Duration::from_millis(10));
store.insert("q2", make_response("r2"), "upstream_a".to_string());
std::thread::sleep(Duration::from_millis(10));
store.insert("q3", make_response("r3"), "upstream_a".to_string());
store.insert("q4", make_response("r4"), "upstream_b".to_string());
let evicted = store.evict_from_upstream("upstream_a", 2);
assert_eq!(evicted, 2);
assert_eq!(store.count_for_upstream("upstream_a"), 1);
assert_eq!(store.count_for_upstream("upstream_b"), 1);
assert!(store.get("q3").is_some());
}
#[test]
fn test_evict_from_upstream_more_than_exists() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
store.insert("q1", make_response("r1"), "upstream_a".to_string());
store.insert("q2", make_response("r2"), "upstream_a".to_string());
let evicted = store.evict_from_upstream("upstream_a", 10);
assert_eq!(evicted, 2);
assert_eq!(store.count_for_upstream("upstream_a"), 0);
}
#[test]
fn test_evict_from_nonexistent_upstream() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
store.insert("q1", make_response("r1"), "upstream_a".to_string());
let evicted = store.evict_from_upstream("nonexistent", 5);
assert_eq!(evicted, 0);
assert_eq!(store.len(), 1); }
#[test]
fn test_enforce_per_upstream_limit() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
for i in 0..5 {
store.insert(
&format!("q{}", i),
make_response(&format!("r{}", i)),
"upstream_a".to_string(),
);
std::thread::sleep(Duration::from_millis(10));
}
store.insert("other", make_response("other"), "upstream_b".to_string());
let evicted = store.enforce_per_upstream_limit("upstream_a", 2);
assert_eq!(evicted, 3); assert_eq!(store.count_for_upstream("upstream_a"), 2);
assert_eq!(store.count_for_upstream("upstream_b"), 1);
assert!(store.get("q3").is_some());
assert!(store.get("q4").is_some());
}
#[test]
fn test_enforce_per_upstream_limit_already_under() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
store.insert("q1", make_response("r1"), "upstream_a".to_string());
store.insert("q2", make_response("r2"), "upstream_a".to_string());
let evicted = store.enforce_per_upstream_limit("upstream_a", 5);
assert_eq!(evicted, 0);
assert_eq!(store.count_for_upstream("upstream_a"), 2);
}
#[test]
fn test_stats_by_upstream() {
let store = CacheStore::new(
Duration::from_secs(30), Duration::from_secs(60), 1000,
);
store.insert("q1", make_response("r1"), "upstream_a".to_string());
store.insert("q2", make_response("r2"), "upstream_a".to_string());
store.insert("q3", make_response("r3"), "upstream_b".to_string());
let stats = store.stats_by_upstream();
let stats_a = stats.get("upstream_a").unwrap();
let stats_b = stats.get("upstream_b").unwrap();
assert_eq!(stats_a.total, 2);
assert_eq!(stats_a.fresh, 2);
assert_eq!(stats_a.stale, 0);
assert!(stats_a.memory_bytes > 0);
assert_eq!(stats_b.total, 1);
assert_eq!(stats_b.fresh, 1);
}
#[test]
fn test_stats_by_upstream_with_stale() {
let store = CacheStore::new(
Duration::from_millis(20), Duration::from_millis(200), 1000,
);
store.insert("q1", make_response("r1"), "upstream_a".to_string());
std::thread::sleep(Duration::from_millis(30));
let stats = store.stats_by_upstream();
let stats_a = stats.get("upstream_a").unwrap();
assert_eq!(stats_a.total, 1);
assert_eq!(stats_a.fresh, 0);
assert_eq!(stats_a.stale, 1);
}
#[test]
fn test_per_upstream_limit() {
let mut store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
store.set_per_upstream_limit(Some(3));
for i in 0..5 {
store.insert(
&format!("query_a_{}", i),
make_response(&format!("result_a_{}", i)),
"upstream_a".to_string(),
);
std::thread::sleep(Duration::from_millis(5)); }
assert_eq!(store.count_for_upstream("upstream_a"), 3);
for i in 0..5 {
store.insert(
&format!("query_b_{}", i),
make_response(&format!("result_b_{}", i)),
"upstream_b".to_string(),
);
std::thread::sleep(Duration::from_millis(5));
}
assert_eq!(store.count_for_upstream("upstream_b"), 3);
assert_eq!(store.len(), 6);
let eviction_stats = store.eviction_stats();
assert!(
eviction_stats.per_upstream > 0,
"Should have per-upstream evictions"
);
}
#[test]
fn test_per_upstream_limit_getter_setter() {
let mut store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
assert!(store.per_upstream_limit().is_none());
store.set_per_upstream_limit(Some(10));
assert_eq!(store.per_upstream_limit(), Some(10));
store.set_per_upstream_limit(None);
assert!(store.per_upstream_limit().is_none());
}
#[test]
fn test_mitm_modified_response_detected() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
let response = make_response("original content");
store.insert("test query", response.clone(), "upstream".to_string());
let verified = store.get_verified("test query");
assert!(verified.is_some(), "Should retrieve valid entry");
let hash = CacheStore::hash_query("test query");
let guard = store.entries.pin();
if let Some(old) = guard.get(&hash) {
let mut modified_response = old.response.clone();
modified_response.results[0].content = "MODIFIED malicious content".to_string();
let poisoned = Arc::new(CacheEntry {
response: modified_response,
upstream_id: old.upstream_id.clone(),
cached_at: old.cached_at,
cached_at_wall: old.cached_at_wall,
extended_count: old.extended_count,
schema: old.schema.clone(),
content_hash: old.content_hash, query_text: old.query_text.clone(),
context_id: old.context_id.clone(),
freq: std::sync::atomic::AtomicU8::new(0),
});
guard.insert(hash, poisoned);
}
drop(guard);
let after_modification = store.get_verified("test query");
assert!(
after_modification.is_none(),
"Modified entry should fail integrity check"
);
assert!(
store.get("test query").is_none(),
"Poisoned entry should be evicted"
);
let stats = store.eviction_stats();
assert!(
stats.integrity_failures > 0,
"Should record integrity failure"
);
}
#[test]
fn test_concurrent_poison_attempt() {
use std::sync::Arc;
use std::thread;
let store = Arc::new(CacheStore::new(
Duration::from_secs(300),
Duration::from_secs(3600),
1000,
));
let query = "concurrent test query";
let num_threads = 10;
let iterations = 100;
let handles: Vec<_> = (0..num_threads)
.map(|thread_id| {
let store_clone = store.clone();
let query_string = query.to_string();
thread::spawn(move || {
for i in 0..iterations {
let content = format!("thread {} iteration {}", thread_id, i);
let response = QueryResponse {
results: vec![SearchResult {
id: format!("t{}i{}", thread_id, i),
score: 1.0,
content,
metadata: None,
upstream_id: None,
}],
cache_status: CacheStatus::Miss,
took_ms: 10,
generated_at: None,
miss_reason: None,
};
store_clone.insert(&query_string, response, "upstream".to_string());
}
})
})
.collect();
for handle in handles {
handle.join().expect("Thread should not panic");
}
assert!(store.get(query).is_some(), "Cache should have the entry");
let verified = store.get_verified(query);
assert!(
verified.is_some(),
"Entry should pass integrity check after concurrent access"
);
if let Some(entry) = store.get(query) {
let computed_hash = CacheEntry::compute_content_hash(&entry.response);
assert_eq!(
entry.content_hash,
Some(computed_hash),
"Content hash should be consistent"
);
}
}
#[test]
fn test_recovery_replaces_poisoned_entries() {
let store = CacheStore::new(
Duration::from_millis(10), Duration::from_secs(60), 1000,
);
let query = "test recovery query";
let poisoned_response = QueryResponse {
results: vec![SearchResult {
id: "old".to_string(),
score: 0.5,
content: "potentially poisoned content".to_string(),
metadata: None,
upstream_id: None,
}],
cache_status: CacheStatus::Miss,
took_ms: 10,
generated_at: None,
miss_reason: None,
};
store.insert(query, poisoned_response, "suspect_upstream".to_string());
let hash = CacheStore::hash_query(query);
std::thread::sleep(Duration::from_millis(20));
let stale_hashes = store.get_stale_hashes();
assert!(
stale_hashes.contains(&hash),
"Stale entry should be in recovery queue"
);
let fresh_response = QueryResponse {
results: vec![SearchResult {
id: "fresh".to_string(),
score: 0.9,
content: "verified fresh content".to_string(),
metadata: None,
upstream_id: None,
}],
cache_status: CacheStatus::Miss,
took_ms: 5,
generated_at: Some(QueryResponse::current_time_ms()),
miss_reason: None,
};
store.insert_by_hash(hash, fresh_response, "verified_upstream".to_string(), None);
let freshness = store.check_freshness(query);
assert_eq!(
freshness,
Some(Freshness::Fresh),
"Recovered entry should be fresh"
);
let verified = store.get_verified(query);
assert!(
verified.is_some(),
"Recovered entry should pass integrity check"
);
if let Some(entry) = verified {
assert_eq!(entry.response.results[0].id, "fresh");
assert_eq!(entry.upstream_id, "verified_upstream");
}
let stale_after = store.get_stale_hashes();
assert!(
!stale_after.contains(&hash),
"Recovered entry should not be in recovery queue"
);
}
#[test]
fn test_two_tier_exact_hash_method() {
let h1 = CacheStore::hash_query_exact("Hello World");
let h2 = CacheStore::hash_query_exact("hello world");
let h3 = CacheStore::hash_query_exact("Hello World");
assert_ne!(h1, h2, "Exact hash should be case-sensitive");
assert_eq!(h1, h3, "Same string should produce same exact hash");
let exact = CacheStore::hash_query_exact(" Hello World ");
let normalized = CacheStore::hash_query(" Hello World ");
assert_ne!(
exact, normalized,
"Exact and normalized should differ for non-normalized query"
);
}
#[test]
fn test_two_tier_exact_hit_on_repeated_query() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
let query = "Test Query";
store.insert(query, make_response("result"), "upstream".to_string());
assert_eq!(store.exact_hits(), 0);
assert_eq!(store.normalized_hits(), 0);
let _ = store.get(query);
assert_eq!(store.exact_hits(), 1);
assert_eq!(store.normalized_hits(), 0);
let _ = store.get(query);
assert_eq!(store.exact_hits(), 2);
assert_eq!(store.normalized_hits(), 0);
let _ = store.get(query);
assert_eq!(store.exact_hits(), 3);
assert_eq!(store.normalized_hits(), 0);
}
#[test]
fn test_two_tier_normalized_hit_different_case() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000)
.with_normalized_matching(true);
store.insert(
"test query",
make_response("result"),
"upstream".to_string(),
);
let _ = store.get("test query");
assert_eq!(store.exact_hits(), 1);
assert_eq!(store.normalized_hits(), 0);
let _ = store.get("TEST QUERY");
assert_eq!(store.exact_hits(), 1);
assert_eq!(store.normalized_hits(), 1);
let _ = store.get("TEST QUERY");
assert_eq!(store.exact_hits(), 2);
assert_eq!(store.normalized_hits(), 1);
}
#[test]
fn test_two_tier_normalized_hit_different_whitespace() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000)
.with_normalized_matching(true);
store.insert(
"hello world",
make_response("result"),
"upstream".to_string(),
);
let _ = store.get("hello world");
assert_eq!(store.exact_hits(), 1);
assert_eq!(store.normalized_hits(), 0);
let _ = store.get(" hello world ");
assert_eq!(store.exact_hits(), 1);
assert_eq!(store.normalized_hits(), 1);
let _ = store.get(" hello world ");
assert_eq!(store.exact_hits(), 2);
assert_eq!(store.normalized_hits(), 1);
}
#[test]
fn test_get_with_hit_type() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000)
.with_normalized_matching(true);
let query = "Test Query";
store.insert(query, make_response("result"), "upstream".to_string());
let result = store.get_with_hit_type(query);
assert!(result.is_some());
let (_, hit_type) = result.unwrap();
assert_eq!(hit_type, CacheHitType::Exact);
let result = store.get_with_hit_type(query);
assert!(result.is_some());
let (_, hit_type) = result.unwrap();
assert_eq!(hit_type, CacheHitType::Exact);
let result = store.get_with_hit_type("TEST QUERY");
assert!(result.is_some());
let (entry, hit_type) = result.unwrap();
assert_eq!(hit_type, CacheHitType::Normalized);
assert_eq!(entry.response.results[0].content, "result");
let result = store.get_with_hit_type("TEST QUERY");
assert!(result.is_some());
let (_, hit_type) = result.unwrap();
assert_eq!(hit_type, CacheHitType::Exact);
}
#[test]
fn test_get_with_hit_type_miss() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
let result = store.get_with_hit_type("nonexistent");
assert!(result.is_none());
}
#[test]
fn test_two_tier_clear_clears_mappings() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
store.insert("query", make_response("result"), "upstream".to_string());
let _ = store.get("query");
assert!(store.get("query").is_some());
store.clear();
assert!(store.is_empty());
assert!(store.get("query").is_none());
}
#[test]
fn test_two_tier_remove_clears_mapping() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
store.insert("query", make_response("result"), "upstream".to_string());
let _ = store.get("query");
assert_eq!(store.exact_hits(), 1);
let _ = store.get("query");
assert_eq!(store.exact_hits(), 2);
store.remove("query");
assert!(store.get("query").is_none());
let exact_before = store.exact_hits();
let _ = store.get("query");
assert_eq!(store.exact_hits(), exact_before);
}
#[test]
fn test_two_tier_insert_stores_mapping() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
store.insert(
"Test Query",
make_response("result"),
"upstream".to_string(),
);
let _ = store.get("Test Query");
assert_eq!(
store.exact_hits(),
1,
"Insert should pre-populate exact→normalized mapping"
);
assert_eq!(store.normalized_hits(), 0);
}
#[cfg(feature = "persistence")]
mod persistence_tests {
use super::*;
use crate::proxy::persistence::PersistentCache;
use tempfile::tempdir;
#[test]
fn test_set_persistence() {
let dir = tempdir().unwrap();
let persistent = Arc::new(PersistentCache::open(dir.path().join("cache")).unwrap());
let mut store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
assert!(store.persistence().is_none());
store.set_persistence(persistent.clone());
assert!(store.persistence().is_some());
}
#[test]
fn test_insert_persists_entry() {
let dir = tempdir().unwrap();
let persistent = Arc::new(PersistentCache::open(dir.path().join("cache")).unwrap());
let mut store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
store.set_persistence(persistent.clone());
store.insert(
"test query",
make_response("result"),
"upstream".to_string(),
);
let hash = CacheStore::hash_query("test query");
let persisted = persistent.load_entry(&hash).unwrap();
assert!(persisted.is_some());
assert_eq!(persisted.unwrap().query, "test query");
}
#[test]
fn test_remove_unpersists_entry() {
let dir = tempdir().unwrap();
let persistent = Arc::new(PersistentCache::open(dir.path().join("cache")).unwrap());
let mut store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
store.set_persistence(persistent.clone());
store.insert(
"test query",
make_response("result"),
"upstream".to_string(),
);
let hash = CacheStore::hash_query("test query");
assert!(persistent.load_entry(&hash).unwrap().is_some());
store.remove("test query");
assert!(persistent.load_entry(&hash).unwrap().is_none());
}
#[test]
fn test_clear_clears_persistence() {
let dir = tempdir().unwrap();
let persistent = Arc::new(PersistentCache::open(dir.path().join("cache")).unwrap());
let mut store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
store.set_persistence(persistent.clone());
store.insert("query1", make_response("r1"), "upstream".to_string());
store.insert("query2", make_response("r2"), "upstream".to_string());
store.insert("query3", make_response("r3"), "upstream".to_string());
assert_eq!(persistent.entry_count(), 3);
store.clear();
assert_eq!(persistent.entry_count(), 0);
}
#[test]
fn test_restore_from_persistence() {
let dir = tempdir().unwrap();
let persistent = Arc::new(PersistentCache::open(dir.path().join("cache")).unwrap());
{
let mut store =
CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
store.set_persistence(persistent.clone());
store.insert("query1", make_response("r1"), "upstream".to_string());
store.insert("query2", make_response("r2"), "upstream".to_string());
}
{
let mut store =
CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
store.set_persistence(persistent.clone());
assert_eq!(store.len(), 0);
let restored = store.restore_from_persistence();
assert_eq!(restored, 2);
assert_eq!(store.len(), 2);
}
}
#[test]
fn test_flush_to_persistence() {
let dir = tempdir().unwrap();
let persistent = Arc::new(PersistentCache::open(dir.path().join("cache")).unwrap());
let mut store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
store.set_persistence(persistent.clone());
store.insert("query1", make_response("r1"), "upstream".to_string());
store.insert("query2", make_response("r2"), "upstream".to_string());
let flushed = store.flush_to_persistence();
assert_eq!(flushed, 2);
}
#[test]
fn test_persistence_stats() {
let dir = tempdir().unwrap();
let persistent = Arc::new(PersistentCache::open(dir.path().join("cache")).unwrap());
let mut store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
store.set_persistence(persistent.clone());
store.insert("query", make_response("result"), "upstream".to_string());
let stats = store.persistence_stats();
assert!(stats.is_some());
assert_eq!(stats.unwrap().entry_count, 1);
}
#[test]
fn test_insert_by_hash_persists() {
let dir = tempdir().unwrap();
let persistent = Arc::new(PersistentCache::open(dir.path().join("cache")).unwrap());
let mut store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
store.set_persistence(persistent.clone());
let hash = [42u8; 32];
store.insert_by_hash(hash, make_response("result"), "upstream".to_string(), None);
let persisted = persistent.load_entry(&hash).unwrap();
assert!(persisted.is_some());
}
#[test]
fn test_restore_respects_max_entries() {
let dir = tempdir().unwrap();
let persistent = Arc::new(PersistentCache::open(dir.path().join("cache")).unwrap());
{
let mut store =
CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 100);
store.set_persistence(persistent.clone());
for i in 0..50 {
store.insert(
&format!("query{}", i),
make_response(&format!("r{}", i)),
"upstream".to_string(),
);
}
}
{
let mut store =
CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 10); store.set_persistence(persistent.clone());
let restored = store.restore_from_persistence();
assert_eq!(restored, 10); assert_eq!(store.len(), 10);
}
}
#[test]
fn test_sync_persistence() {
let dir = tempdir().unwrap();
let persistent = Arc::new(PersistentCache::open(dir.path().join("cache")).unwrap());
let mut store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
store.set_persistence(persistent.clone());
store.insert("query", make_response("result"), "upstream".to_string());
assert!(store.sync_persistence());
}
#[test]
fn test_no_persistence_operations_without_config() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
store.insert("query", make_response("result"), "upstream".to_string());
store.remove("query");
store.clear();
let restored = store.restore_from_persistence();
assert_eq!(restored, 0);
let flushed = store.flush_to_persistence();
assert_eq!(flushed, 0);
assert!(store.persistence_stats().is_none());
assert!(store.sync_persistence());
}
}
#[test]
fn test_s3fifo_promotes_accessed_entries() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(600), 5);
for i in 0..5 {
store.insert(
&format!("query-{}", i),
make_response(&format!("content-{}", i)),
"up".to_string(),
);
}
assert_eq!(store.len(), 5);
for _ in 0..3 {
store.get("query-0");
store.get("query-1");
}
for i in 5..8 {
store.insert(
&format!("query-{}", i),
make_response(&format!("content-{}", i)),
"up".to_string(),
);
}
assert!(
store.get("query-0").is_some(),
"Frequently accessed entry should survive eviction"
);
assert!(
store.get("query-1").is_some(),
"Frequently accessed entry should survive eviction"
);
assert!(store.len() <= 5, "Cache should respect max_entries bound");
}
#[test]
fn test_s3fifo_ghost_readmission() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(600), 5);
for i in 0..5 {
store.insert(
&format!("query-{}", i),
make_response(&format!("content-{}", i)),
"up".to_string(),
);
}
for i in 5..10 {
store.insert(
&format!("query-{}", i),
make_response(&format!("content-{}", i)),
"up".to_string(),
);
}
let stats = store.eviction_stats();
assert!(
stats.small_evictions + stats.main_evictions > 0,
"Evictions should have occurred"
);
store.insert(
"query-0",
make_response("content-0-readmit"),
"up".to_string(),
);
let stats = store.eviction_stats();
assert!(stats.ghost_hits > 0, "Re-inserted key should hit ghost set");
}
#[test]
fn test_s3fifo_scan_resistance() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(600), 20);
for i in 0..10 {
store.insert(
&format!("hot-{}", i),
make_response(&format!("hot-content-{}", i)),
"up".to_string(),
);
}
for _ in 0..3 {
for i in 0..10 {
store.get(&format!("hot-{}", i));
}
}
for i in 0..30 {
store.insert(
&format!("scan-{}", i),
make_response(&format!("scan-content-{}", i)),
"up".to_string(),
);
}
let mut hot_survived = 0;
for i in 0..10 {
if store.get(&format!("hot-{}", i)).is_some() {
hot_survived += 1;
}
}
assert!(
hot_survived >= 7,
"S3-FIFO should protect hot entries from sequential scan (survived: {}/10)",
hot_survived
);
assert!(store.len() <= 20, "Cache should respect max_entries");
}
#[test]
fn test_s3fifo_clear_resets_queues() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(600), 10);
for i in 0..8 {
store.insert(
&format!("query-{}", i),
make_response(&format!("content-{}", i)),
"up".to_string(),
);
}
store.clear();
assert_eq!(store.len(), 0);
for i in 0..8 {
store.insert(
&format!("new-query-{}", i),
make_response(&format!("new-content-{}", i)),
"up".to_string(),
);
}
assert_eq!(store.len(), 8);
}
#[test]
fn test_s3fifo_remove_lazy_cleanup() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(600), 5);
for i in 0..5 {
store.insert(
&format!("query-{}", i),
make_response(&format!("content-{}", i)),
"up".to_string(),
);
}
store.remove("query-1");
store.remove("query-3");
assert_eq!(store.len(), 3);
for i in 5..10 {
store.insert(
&format!("query-{}", i),
make_response(&format!("content-{}", i)),
"up".to_string(),
);
}
assert!(store.len() <= 5);
}
#[test]
fn test_s3fifo_extend_ttl_sets_freq() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(600), 10);
store.insert("query", make_response("content"), "up".to_string());
let entry = store.get("query").unwrap();
drop(entry);
store.extend_ttl("query");
let entry = store.get("query").unwrap();
assert!(entry.extended_count > 0, "Entry should be extended");
}
#[test]
fn test_s3fifo_update_existing_no_duplicate() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(600), 5);
for version in 0..5 {
store.insert(
"same-query",
make_response(&format!("content-v{}", version)),
"up".to_string(),
);
}
assert_eq!(store.len(), 1);
for i in 0..6 {
store.insert(
&format!("other-{}", i),
make_response(&format!("other-content-{}", i)),
"up".to_string(),
);
}
assert!(store.len() <= 5, "Cache should respect max_entries");
}
#[test]
fn test_tracked_remove_cleans_exact_to_normalized() {
let store = CacheStore::new(
Duration::from_secs(300),
Duration::from_secs(600),
5, )
.with_normalized_matching(true);
for i in 0..10 {
let query = format!("test query number {}", i);
let response = QueryResponse {
results: vec![],
cache_status: CacheStatus::Miss,
took_ms: 1,
generated_at: None,
miss_reason: None,
};
store.insert(&query, response, "upstream".to_string());
}
let stats = store.stats();
assert!(
stats.total <= 5,
"Cache should have at most 5 entries, got {}",
stats.total
);
}
#[test]
fn test_s3fifo_ghost_set_readmission() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(600), 10);
let hash0 = CacheStore::hash_query("query-0");
for i in 0..10 {
store.insert(
&format!("query-{}", i),
make_response(&format!("content-{}", i)),
"up".to_string(),
);
}
{
let s = store.s3fifo.lock();
assert_eq!(s.ghost_set_size(), 0, "No evictions yet");
assert!(!s.is_in_ghost(&hash0), "query-0 not ghosted yet");
assert!(s.small_queue_size() > 0, "Small queue has entries");
}
store.insert(
"query-overflow",
make_response("overflow"),
"up".to_string(),
);
{
let s = store.s3fifo.lock();
assert!(
s.is_in_ghost(&hash0),
"Evicted entry should appear in ghost set"
);
assert_eq!(s.ghost_set_size(), 1, "One entry in ghost set");
}
store.insert(
"query-0",
make_response("content-0-readmitted"),
"up".to_string(),
);
{
let s = store.s3fifo.lock();
assert!(
!s.is_in_ghost(&hash0),
"Re-admitted entry should no longer be in ghost set"
);
assert_eq!(
s.main_queue_size(),
1,
"Ghost re-admission goes to main queue"
);
}
}
#[test]
fn test_s3fifo_small_to_main_promotion() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(600), 3);
store.insert("q0", make_response("content-0"), "up".to_string());
store.get("q0");
store.insert("q1", make_response("content-1"), "up".to_string());
store.insert("q2", make_response("content-2"), "up".to_string());
let small_before = store.s3fifo.lock().small_queue_size();
let main_before = store.s3fifo.lock().main_queue_size();
assert_eq!(small_before, 3, "All 3 entries start in small queue");
assert_eq!(main_before, 0, "Main queue empty");
store.insert("q3", make_response("content-3"), "up".to_string());
let small_after = store.s3fifo.lock().small_queue_size();
let main_after = store.s3fifo.lock().main_queue_size();
assert!(
small_after < small_before,
"Small queue shrank after eviction ({} < {})",
small_after,
small_before
);
assert!(
main_after > main_before,
"Main queue grew after promotion ({} > {})",
main_after,
main_before
);
assert_eq!(main_after, 1, "q0 promoted to main queue");
let entry = store.get("q0");
assert!(entry.is_some(), "Promoted entry remains in cache");
}
#[test]
fn test_s3fifo_demotion_on_stale() {
let store = CacheStore::new(
Duration::from_millis(50), Duration::from_millis(200), 5,
);
for i in 0..5 {
store.insert(
&format!("q-{}", i),
make_response(&format!("content-{}", i)),
"up".to_string(),
);
store.get(&format!("q-{}", i));
}
store.insert("q-5", make_response("content-5"), "up".to_string());
std::thread::sleep(Duration::from_millis(60));
for i in 6..10 {
store.insert(
&format!("q-{}", i),
make_response(&format!("content-{}", i)),
"up".to_string(),
);
}
let ghost_size = store.s3fifo.lock().ghost_set_size();
assert!(
ghost_size > 0,
"Evictions should populate ghost set (got {} )",
ghost_size
);
let main_size = store.s3fifo.lock().main_queue_size();
assert!(
main_size > 0,
"Main queue should retain frequently accessed entries"
);
}
#[test]
fn test_empty_results_cached_as_hit() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
let empty_response = QueryResponse {
results: vec![],
cache_status: CacheStatus::Miss,
took_ms: 0,
generated_at: None,
miss_reason: None,
};
store.insert("empty-query", empty_response, "upstream".to_string());
let entry = store.get("empty-query");
assert!(
entry.is_some(),
"Empty-result entry should be found (cache hit)"
);
assert!(
entry.unwrap().response.results.is_empty(),
"Stored response has zero results"
);
}
#[test]
fn test_empty_results_respected_by_stats() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
let empty_response = QueryResponse {
results: vec![],
cache_status: CacheStatus::Miss,
took_ms: 0,
generated_at: None,
miss_reason: None,
};
store.insert("empty-query", empty_response, "upstream".to_string());
let stats = store.stats();
assert_eq!(stats.total, 1, "Empty-result entry counted in stats");
assert_eq!(store.len(), 1, "Empty-result entry counted in len()");
store.insert(
"normal-query",
make_response("normal"),
"upstream".to_string(),
);
let stats = store.stats();
assert_eq!(stats.total, 2, "Both entries counted after normal insert");
store.clear();
assert!(
store.is_empty(),
"Clear still works after empty-result insert"
);
}
#[test]
fn test_evict_filter_by_context_and_prefix() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
store.insert_with_context(
"error handling rust",
make_response("r"),
"upstream".to_string(),
"tenant_a",
);
store.insert_with_context(
"error handling python",
make_response("r"),
"upstream".to_string(),
"tenant_a",
);
store.insert_with_context(
"error handling ruby",
make_response("r"),
"upstream".to_string(),
"tenant_b",
);
store.insert_with_context(
"async patterns",
make_response("r"),
"upstream".to_string(),
"tenant_a",
);
let evicted = store.evict_filter(Some("tenant_a"), None, None, None);
assert_eq!(evicted, 3);
assert_eq!(store.len(), 1);
let evicted = store.evict_filter(None, Some("error"), None, None);
assert_eq!(evicted, 1);
assert_eq!(store.len(), 0);
}
#[test]
fn test_evict_filter_max_entries_caps() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
for i in 0..5 {
store.insert_with_context(
&format!("query {i}"),
make_response("r"),
"u".to_string(),
"c",
);
}
let evicted = store.evict_filter(Some("c"), None, None, Some(2));
assert_eq!(evicted, 2);
assert_eq!(store.len(), 3);
}
#[test]
fn test_evict_filter_older_than_secs() {
let store = CacheStore::new(Duration::from_secs(300), Duration::from_secs(3600), 1000);
store.insert_with_context("old", make_response("r"), "u".to_string(), "c");
let evicted = store.evict_filter(None, None, Some(0), None);
assert_eq!(evicted, 1);
}