multi-tier-cache 0.6.5

Customizable multi-tier cache with L1 (Moka in-memory) + L2 (Redis distributed) defaults, expandable to L3/L4+, cross-instance invalidation via Pub/Sub, stampede protection, and flexible TTL scaling
Documentation
#![allow(dead_code)]
//! Common utilities for integration tests
//!
//! This module provides shared test infrastructure including:
//! - Redis connection helpers
//! - Test data generators
//! - Cleanup utilities
//! - Test environment setup

use anyhow::Result;
use multi_tier_cache::backends::MokaCacheConfig;
use multi_tier_cache::{
    CacheManager, CacheSystem, CacheSystemBuilder, InvalidationConfig, L1Cache, L2Cache,
    L2CacheBackend, TierConfig,
};
use std::sync::Arc;
use std::sync::Once;

static INIT: Once = Once::new();

/// Initialize tracing for tests
pub fn init_test_tracing() {
    INIT.call_once(|| {
        tracing_subscriber::fmt()
            .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
            .init();
    });
}

/// Get Redis URL from environment or use default
pub fn redis_url() -> String {
    std::env::var("REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string())
}

/// Generate a unique test key prefix to avoid conflicts between tests
pub fn test_key_prefix() -> String {
    use std::time::{Duration, SystemTime, UNIX_EPOCH};
    let timestamp = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or(Duration::from_secs(0))
        .as_millis();
    format!("test:{timestamp}:")
}

/// Create a test key with unique prefix
pub fn test_key(name: &str) -> String {
    format!("test_{}_{}", name, rand::random::<u32>())
}

/// Initialize a basic cache system for testing
pub async fn setup_cache_system() -> Result<CacheSystem> {
    // TODO: Audit that the environment access only happens in single-threaded code.
    unsafe { std::env::set_var("REDIS_URL", redis_url()) };
    CacheSystem::new()
        .await
        .map_err(|e| anyhow::anyhow!(e.to_string()))
}

/// Initialize cache system with custom promotion frequency N for testing
pub async fn setup_cache_with_n(n: usize) -> Result<CacheSystem> {
    let l1 = Arc::new(L1Cache::new(MokaCacheConfig::default())?);
    let l2 = Arc::new(L2Cache::new().await?);

    let cache = CacheSystemBuilder::new()
        .with_tier(
            Arc::clone(&l1) as Arc<dyn L2CacheBackend>,
            TierConfig::as_l1(),
        )
        .with_tier(
            Arc::clone(&l2) as Arc<dyn L2CacheBackend>,
            TierConfig::as_l2().with_promotion_frequency(n),
        )
        .build()
        .await?;

    // Manually set the Option fields for backward compatibility in tests
    let mut cache = cache;
    cache.l1_cache = Some(l1);
    cache.l2_cache = Some(l2);

    Ok(cache)
}

/// Initialize cache manager with invalidation for testing
pub async fn setup_cache_with_invalidation() -> Result<Arc<CacheManager>> {
    let l1 = Arc::new(L1Cache::new(MokaCacheConfig::default())?);
    let l2 = Arc::new(L2Cache::new().await?);
    let config = InvalidationConfig::default();

    let manager = CacheManager::new_with_invalidation(l1, l2, &redis_url(), config)
        .await
        .map_err(|e| anyhow::anyhow!(e.to_string()))?;

    Ok(Arc::new(manager))
}

/// Cleanup test keys from Redis
pub async fn cleanup_test_keys(prefix: &str) -> Result<()> {
    let _cache = setup_cache_system().await?;
    let l2 = Arc::new(L2Cache::new().await?);

    // Find all test keys
    let pattern = format!("{prefix}*");
    let keys = l2.scan_keys(&pattern).await?;

    // Remove them
    if !keys.is_empty() {
        l2.remove_bulk(&keys).await?;
    }

    Ok(())
}

pub mod test_data {
    use serde::{Deserialize, Serialize};

    #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
    pub struct User {
        pub id: u64,
        pub name: String,
        pub email: String,
    }

    impl User {
        pub fn new(id: u64) -> Self {
            Self {
                id,
                name: format!("User {id}"),
                email: format!("user{id}@example.com"),
            }
        }
    }

    #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
    pub struct Product {
        pub id: u64,
        pub name: String,
        pub price: f64,
        pub category: String,
    }

    impl Product {
        pub fn new(id: u64) -> Self {
            Self {
                id,
                name: format!("Product {id}"),
                #[allow(clippy::cast_precision_loss)]
                price: 99.99 + (id as f64),
                category: format!("Category {}", id % 5),
            }
        }
    }

    /// Generate JSON test data
    pub fn json_user(id: u64) -> serde_json::Value {
        serde_json::json!({
            "id": id,
            "name": format!("User {}", id),
            "email": format!("user{}@example.com", id),
            "created_at": "2025-01-01T00:00:00Z"
        })
    }

    /// Generate Bytes test data (JSON)
    pub fn bytes_user(id: u64) -> bytes::Bytes {
        bytes::Bytes::from(json_user(id).to_string())
    }

    /// Generate JSON test data with specified size
    pub fn json_data_sized(size_kb: usize) -> serde_json::Value {
        let data_string = "x".repeat(size_kb * 1024);
        serde_json::json!({
            "data": data_string,
            "size_kb": size_kb
        })
    }

    /// Generate Bytes test data with specified size
    pub fn bytes_data_sized(size_kb: usize) -> bytes::Bytes {
        bytes::Bytes::from(json_data_sized(size_kb).to_string())
    }
}

/// Wait for a condition with timeout
pub async fn wait_for<F>(mut condition: F, timeout_ms: u64) -> bool
where
    F: FnMut() -> bool,
{
    use tokio::time::{Duration, sleep};

    let start = std::time::Instant::now();
    let timeout = Duration::from_millis(timeout_ms);

    while start.elapsed() < timeout {
        if condition() {
            return true;
        }
        sleep(Duration::from_millis(10)).await;
    }

    false
}

/// Assert that cache stats meet expectations
#[macro_export]
macro_rules! assert_cache_stats {
    ($cache:expr_2021, $field:ident > $value:expr_2021) => {
        let stats = $cache.cache_manager().get_stats();
        assert!(
            stats.$field > $value,
            "Expected {} > {}, got {}",
            stringify!($field),
            $value,
            stats.$field
        );
    };
    ($cache:expr_2021, $field:ident == $value:expr_2021) => {
        let stats = $cache.cache_manager().get_stats();
        assert_eq!(
            stats.$field,
            $value,
            "Expected {} == {}, got {}",
            stringify!($field),
            $value,
            stats.$field
        );
    };
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_key_generation() {
        let key1 = test_key("user");
        let key2 = test_key("user");
        assert_ne!(key1, key2, "Keys should be unique");
        assert!(key1.starts_with("test_user_"));
    }

    #[test]
    fn test_data_generation() {
        let user = test_data::User::new(123);
        assert_eq!(user.id, 123);
        assert_eq!(user.name, "User 123");
        assert_eq!(user.email, "user123@example.com");
    }
}