#![cfg(feature = "e2e-real-db")]
use std::sync::Arc;
use std::time::Duration;
mod common;
use common::cleanup::unique_table_name;
use sqlx::Row;
async fn pg_pool() -> Option<sqlx::PgPool> {
let url = std::env::var("POSTGRES_URL").ok()?;
sqlx::PgPool::connect(&url).await.ok()
}
#[tokio::test]
async fn test_l1_cache_put_get() {
use sz_orm_core::l1_cache::L1Cache;
let mut cache: L1Cache<String> = L1Cache::new(100);
cache.put(1, Arc::new("Alice".to_string()));
cache.put(2, Arc::new("Bob".to_string()));
let val1 = cache.get(&1).expect("key 1 应存在");
assert_eq!(*val1, "Alice");
let val2 = cache.get(&2).expect("key 2 应存在");
assert_eq!(*val2, "Bob");
assert!(cache.get(&3).is_none(), "key 3 不应存在");
}
#[tokio::test]
async fn test_l1_cache_lru_eviction() {
use sz_orm_core::l1_cache::L1Cache;
let mut cache: L1Cache<i32> = L1Cache::new(2);
cache.put(1, Arc::new(100));
cache.put(2, Arc::new(200));
let _ = cache.get(&1);
cache.put(3, Arc::new(300));
assert!(cache.get(&1).is_some(), "key 1 最近使用,不应被淘汰");
assert!(cache.get(&2).is_none(), "key 2 应被 LRU 淘汰");
assert!(cache.get(&3).is_some(), "key 3 刚插入,应存在");
}
#[tokio::test]
async fn test_l1_cache_evict_and_stats() {
use sz_orm_core::l1_cache::L1Cache;
let mut cache: L1Cache<String> = L1Cache::new(100);
cache.put(1, Arc::new("Alice".to_string()));
cache.put(2, Arc::new("Bob".to_string()));
let _ = cache.get(&1);
let _ = cache.get(&3);
let stats = cache.stats();
assert!(stats.hits >= 1, "应有至少 1 次命中");
assert!(stats.misses >= 1, "应有至少 1 次未命中");
cache.evict(&1);
assert!(cache.get(&1).is_none(), "evict 后 key 1 不应存在");
cache.clear();
assert!(cache.get(&2).is_none(), "clear 后所有键不应存在");
}
#[tokio::test]
async fn test_l2_cache_put_get() {
use sz_orm_core::l2_cache::{CacheKey, L2Cache};
use sz_orm_core::Value;
let cache = L2Cache::new();
let key = CacheKey::by_pk("users", 1);
cache.put(&key, Value::String("Alice".to_string()), None);
let val = cache.get(&key).expect("key 应存在");
match val {
Value::String(s) => assert_eq!(s, "Alice"),
_ => panic!("应为 String 类型"),
}
let missing_key = CacheKey::by_pk("users", 999);
assert!(
cache.get(&missing_key).is_none(),
"不存在的 key 应返回 None"
);
}
#[tokio::test]
async fn test_l2_cache_invalidate_table() {
use sz_orm_core::l2_cache::{CacheKey, L2Cache};
use sz_orm_core::Value;
let cache = L2Cache::new();
let key1 = CacheKey::by_pk("users", 1);
let key2 = CacheKey::by_pk("users", 2);
let key3 = CacheKey::by_pk("orders", 1);
cache.put(&key1, Value::String("Alice".to_string()), None);
cache.put(&key2, Value::String("Bob".to_string()), None);
cache.put(&key3, Value::String("Order1".to_string()), None);
cache.invalidate_table("users");
assert!(cache.get(&key1).is_none(), "users key1 应已失效");
assert!(cache.get(&key2).is_none(), "users key2 应已失效");
assert!(
cache.get(&key3).is_some(),
"orders 表缓存不应受 users 失效影响"
);
}
#[tokio::test]
async fn test_l2_cache_ttl_expiry() {
use sz_orm_core::l2_cache::{CacheKey, L2Cache};
use sz_orm_core::Value;
let cache = L2Cache::new();
let key = CacheKey::by_pk("users", 1);
cache.put(
&key,
Value::String("Alice".to_string()),
Some(Duration::from_millis(50)),
);
assert!(cache.get(&key).is_some(), "TTL 未过期应命中");
tokio::time::sleep(Duration::from_millis(100)).await;
assert!(cache.get(&key).is_none(), "TTL 过期后应返回 None");
}
#[tokio::test]
async fn test_l1l2_coordinator_three_level() {
use sz_orm_core::l1_cache::L1L2Coordinator;
use sz_orm_core::l2_cache::L2Cache;
use sz_orm_core::Value;
let l2 = Arc::new(L2Cache::new());
let mut coord: L1L2Coordinator<Value> = L1L2Coordinator::new(100).with_l2(l2);
let mut db_call_count = 0;
let result = coord.get_or_load("users", 1, || {
db_call_count += 1;
Some(Value::String("Alice".to_string()))
});
assert!(result.is_some());
assert_eq!(db_call_count, 1, "第一次应回源 DB");
let result = coord.get_or_load("users", 1, || {
db_call_count += 1;
Some(Value::String("Alice".to_string()))
});
assert!(result.is_some());
assert_eq!(db_call_count, 1, "第二次应命中 L1,不回源 DB");
}
#[tokio::test]
async fn test_pg_cache_db_consistency() {
use sz_orm_core::l2_cache::{CacheKey, L2Cache};
use sz_orm_core::Value;
let pool = match pg_pool().await {
Some(p) => p,
None => {
eprintln!("PostgreSQL 未配置,跳过");
return;
}
};
let table = unique_table_name("e2e_cache");
sqlx::query(sqlx::AssertSqlSafe(
format!(
"CREATE TABLE \"{}\" (id BIGSERIAL PRIMARY KEY, name TEXT)",
table
)
.as_str(),
))
.execute(&pool)
.await
.unwrap();
let insert_sql = format!(
"INSERT INTO \"{}\" (name) VALUES ($1) RETURNING id, name",
table
);
let row = sqlx::query(sqlx::AssertSqlSafe(insert_sql.as_str()))
.bind("Alice")
.fetch_one(&pool)
.await
.unwrap();
let id: i64 = row.try_get("id").unwrap();
let name: String = row.try_get("name").unwrap();
assert_eq!(name, "Alice");
let l2 = L2Cache::new();
let cache_key = CacheKey::by_pk(&table, id);
l2.put(&cache_key, Value::String(name.clone()), None);
let cached = l2.get(&cache_key).expect("缓存应命中");
match cached {
Value::String(s) => assert_eq!(s, "Alice"),
_ => panic!(),
}
let update_sql = format!("UPDATE \"{}\" SET name = $1 WHERE id = $2", table);
sqlx::query(sqlx::AssertSqlSafe(update_sql.as_str()))
.bind("AliceUpdated")
.bind(id)
.execute(&pool)
.await
.unwrap();
l2.invalidate(&cache_key);
assert!(l2.get(&cache_key).is_none(), "失效后缓存应为空");
let select_sql = format!("SELECT name FROM \"{}\" WHERE id = $1", table);
let row = sqlx::query(sqlx::AssertSqlSafe(select_sql.as_str()))
.bind(id)
.fetch_one(&pool)
.await
.unwrap();
let new_name: String = row.try_get("name").unwrap();
assert_eq!(new_name, "AliceUpdated");
l2.put(&cache_key, Value::String(new_name.clone()), None);
match l2.get(&cache_key).expect("回填后应命中") {
Value::String(s) => assert_eq!(s, "AliceUpdated"),
_ => panic!(),
}
sqlx::query(sqlx::AssertSqlSafe(
format!("DROP TABLE \"{}\"", table).as_str(),
))
.execute(&pool)
.await
.unwrap();
}