ruvector-scipix 2.0.4

Rust OCR engine for scientific documents - extract LaTeX, MathML from math equations, research papers, and technical diagrams with ONNX GPU acceleration
Documentation
// Cache integration tests
//
// Tests caching behavior, hit/miss ratios, similarity search, and persistence
//
// Note: These tests use mock test infrastructure.
// Real OCR processing requires ONNX models to be configured.

use super::*;
use crate::common::{CacheStats, OutputFormat};

#[tokio::test]
async fn test_cache_hit_miss_behavior() {
    let test_server = TestServer::with_cache()
        .await
        .expect("Failed to start test server with cache");

    let image = images::generate_simple_equation("x^2");
    image.save("/tmp/cache_test_1.png").unwrap();

    // First request - should miss cache
    let result1 = test_server
        .process_image("/tmp/cache_test_1.png", OutputFormat::LaTeX)
        .await
        .expect("Processing failed");

    // Get cache stats
    let _stats = test_server
        .cache_stats()
        .await
        .expect("Failed to get cache stats");

    // Second request - should hit cache
    let result2 = test_server
        .process_image("/tmp/cache_test_1.png", OutputFormat::LaTeX)
        .await
        .expect("Processing failed");

    // Verify results match
    assert_eq!(result1.latex, result2.latex, "Cached result should match");

    test_server.shutdown().await;
}

#[tokio::test]
async fn test_cache_similarity_lookup() {
    let test_server = TestServer::with_cache()
        .await
        .expect("Failed to start test server");

    // Create original image
    let image1 = images::generate_simple_equation("a + b");
    image1.save("/tmp/similarity_1.png").unwrap();

    // Create similar image (slightly different rendering)
    let mut image2 = images::generate_simple_equation("a + b");
    images::add_slight_variation(&mut image2, 0.05);
    image2.save("/tmp/similarity_2.png").unwrap();

    // Process first image
    let result1 = test_server
        .process_image("/tmp/similarity_1.png", OutputFormat::LaTeX)
        .await
        .expect("Processing failed");

    // Process similar image
    let result2 = test_server
        .process_image("/tmp/similarity_2.png", OutputFormat::LaTeX)
        .await
        .expect("Processing failed");

    // Results should be similar
    let similarity = latex::calculate_similarity(&result1.latex, &result2.latex);
    assert!(
        similarity > 0.9,
        "Similar images should produce similar results"
    );

    test_server.shutdown().await;
}

#[tokio::test]
async fn test_cache_eviction() {
    // Start server with small cache size
    let test_server = TestServer::with_cache_size(3)
        .await
        .expect("Failed to start test server");

    // Create and process 5 different images
    for i in 0..5 {
        let eq = format!("x + {}", i);
        let image = images::generate_simple_equation(&eq);
        let path = format!("/tmp/eviction_{}.png", i);
        image.save(&path).unwrap();

        test_server
            .process_image(&path, OutputFormat::LaTeX)
            .await
            .expect("Processing failed");
    }

    // Get cache stats
    let stats = test_server
        .cache_stats()
        .await
        .expect("Failed to get cache stats");
    assert!(stats.current_size <= 3, "Cache should not exceed max size");

    test_server.shutdown().await;
}

#[tokio::test]
async fn test_cache_persistence() {
    let cache_dir = "/tmp/scipix_cache_persist";
    std::fs::create_dir_all(cache_dir).unwrap();

    // Start server with persistent cache
    let test_server = TestServer::with_persistent_cache(cache_dir)
        .await
        .expect("Failed to start test server");

    // Process image
    let image = images::generate_simple_equation("persistent");
    image.save("/tmp/persist_test.png").unwrap();

    let result1 = test_server
        .process_image("/tmp/persist_test.png", OutputFormat::LaTeX)
        .await
        .expect("Processing failed");

    // Shutdown server
    test_server.shutdown().await;

    // Start new server with same cache directory
    let test_server2 = TestServer::with_persistent_cache(cache_dir)
        .await
        .expect("Failed to start second test server");

    // Process same image - should hit persistent cache
    let result2 = test_server2
        .process_image("/tmp/persist_test.png", OutputFormat::LaTeX)
        .await
        .expect("Processing failed");

    // Results should match
    assert_eq!(
        result1.latex, result2.latex,
        "Persistent cache should restore results"
    );

    test_server2.shutdown().await;
}

#[tokio::test]
async fn test_cache_invalidation() {
    let test_server = TestServer::with_cache()
        .await
        .expect("Failed to start test server");

    // Process image
    let image = images::generate_simple_equation("invalidate");
    image.save("/tmp/invalidate_test.png").unwrap();

    let result1 = test_server
        .process_image("/tmp/invalidate_test.png", OutputFormat::LaTeX)
        .await
        .expect("Processing failed");

    // Invalidate cache
    test_server
        .invalidate_cache()
        .await
        .expect("Cache invalidation failed");

    // Process again - should miss cache
    let result2 = test_server
        .process_image("/tmp/invalidate_test.png", OutputFormat::LaTeX)
        .await
        .expect("Processing failed");

    // Results should match but processing should take time
    assert_eq!(result1.latex, result2.latex, "Results should still match");

    test_server.shutdown().await;
}

#[tokio::test]
async fn test_cache_hit_ratio() {
    let test_server = TestServer::with_cache()
        .await
        .expect("Failed to start test server");

    // Create test images
    let equations = vec!["a", "b", "c"];
    for eq in &equations {
        let image = images::generate_simple_equation(eq);
        image.save(&format!("/tmp/ratio_{}.png", eq)).unwrap();
    }

    // Process each image twice
    for eq in &equations {
        let path = format!("/tmp/ratio_{}.png", eq);

        // First time (miss)
        test_server
            .process_image(&path, OutputFormat::LaTeX)
            .await
            .expect("Processing failed");

        // Second time (hit)
        test_server
            .process_image(&path, OutputFormat::LaTeX)
            .await
            .expect("Processing failed");
    }

    // Get stats
    let _stats = test_server
        .cache_stats()
        .await
        .expect("Failed to get cache stats");

    test_server.shutdown().await;
}

#[tokio::test]
async fn test_cache_ttl_expiration() {
    // Start server with 1-second TTL
    let test_server = TestServer::with_cache_ttl(1)
        .await
        .expect("Failed to start test server");

    // Process image
    let image = images::generate_simple_equation("ttl");
    image.save("/tmp/ttl_test.png").unwrap();

    let result1 = test_server
        .process_image("/tmp/ttl_test.png", OutputFormat::LaTeX)
        .await
        .expect("Processing failed");

    // Immediately reprocess - should hit cache
    let result2 = test_server
        .process_image("/tmp/ttl_test.png", OutputFormat::LaTeX)
        .await
        .expect("Processing failed");

    assert_eq!(result1.latex, result2.latex);

    test_server.shutdown().await;
}

#[tokio::test]
async fn test_cache_concurrent_access() {
    let test_server = TestServer::with_cache()
        .await
        .expect("Failed to start test server");

    let image = images::generate_simple_equation("concurrent");
    image.save("/tmp/concurrent_cache.png").unwrap();

    // First request to populate cache
    test_server
        .process_image("/tmp/concurrent_cache.png", OutputFormat::LaTeX)
        .await
        .expect("Processing failed");

    // Spawn multiple concurrent requests
    let mut handles = vec![];
    for _ in 0..10 {
        let server = test_server.clone();
        let handle = tokio::spawn(async move {
            server
                .process_image("/tmp/concurrent_cache.png", OutputFormat::LaTeX)
                .await
        });
        handles.push(handle);
    }

    // Wait for all to complete
    let results = futures::future::join_all(handles).await;

    // All should succeed and return same result
    assert!(
        results.iter().all(|r| r.is_ok()),
        "All requests should succeed"
    );

    let first_latex = &results[0].as_ref().unwrap().as_ref().unwrap().latex;
    assert!(
        results
            .iter()
            .all(|r| { &r.as_ref().unwrap().as_ref().unwrap().latex == first_latex }),
        "All results should match"
    );

    test_server.shutdown().await;
}

// Re-export CacheStats for backward compatibility
pub use crate::common::CacheStats as CacheStatsCompat;