Removed 2.6GB of uncompressed .dbn files (derivable from .zst). Renamed directory to mbp10-fixtures to distinguish unit test fixtures from training data. Updated all test references to use .dbn.zst paths directly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
701 lines
24 KiB
Rust
701 lines
24 KiB
Rust
#![allow(
|
|
clippy::assertions_on_constants,
|
|
clippy::assertions_on_result_states,
|
|
clippy::clone_on_copy,
|
|
clippy::decimal_literal_representation,
|
|
clippy::doc_markdown,
|
|
clippy::empty_line_after_doc_comments,
|
|
clippy::field_reassign_with_default,
|
|
clippy::get_unwrap,
|
|
clippy::identity_op,
|
|
clippy::inconsistent_digit_grouping,
|
|
clippy::indexing_slicing,
|
|
clippy::integer_division,
|
|
clippy::len_zero,
|
|
clippy::let_underscore_must_use,
|
|
clippy::manual_div_ceil,
|
|
clippy::manual_let_else,
|
|
clippy::manual_range_contains,
|
|
clippy::modulo_arithmetic,
|
|
clippy::needless_range_loop,
|
|
clippy::non_ascii_literal,
|
|
clippy::redundant_clone,
|
|
clippy::shadow_reuse,
|
|
clippy::shadow_same,
|
|
clippy::shadow_unrelated,
|
|
clippy::single_match_else,
|
|
clippy::str_to_string,
|
|
clippy::string_slice,
|
|
clippy::tests_outside_test_module,
|
|
clippy::too_many_lines,
|
|
clippy::unnecessary_wraps,
|
|
clippy::unseparated_literal_suffix,
|
|
clippy::use_debug,
|
|
clippy::useless_vec,
|
|
clippy::wildcard_enum_match_arm,
|
|
clippy::else_if_without_else,
|
|
clippy::expect_used,
|
|
clippy::missing_const_for_fn,
|
|
clippy::similar_names,
|
|
clippy::type_complexity,
|
|
clippy::collapsible_else_if,
|
|
clippy::doc_lazy_continuation,
|
|
clippy::items_after_test_module,
|
|
clippy::map_clone,
|
|
clippy::multiple_unsafe_ops_per_block,
|
|
clippy::unwrap_or_default,
|
|
clippy::assign_op_pattern,
|
|
clippy::needless_borrow,
|
|
clippy::println_empty_string,
|
|
clippy::unnecessary_cast,
|
|
clippy::used_underscore_binding,
|
|
clippy::create_dir,
|
|
clippy::implicit_saturating_sub,
|
|
clippy::exit,
|
|
clippy::expect_fun_call,
|
|
clippy::too_many_arguments,
|
|
clippy::unnecessary_map_or,
|
|
clippy::unwrap_used,
|
|
dead_code,
|
|
unused_imports,
|
|
unused_variables,
|
|
clippy::cloned_ref_to_slice_refs,
|
|
clippy::neg_multiply,
|
|
clippy::while_let_loop,
|
|
clippy::bool_assert_comparison,
|
|
clippy::excessive_precision,
|
|
clippy::trivially_copy_pass_by_ref,
|
|
clippy::op_ref,
|
|
clippy::redundant_closure,
|
|
clippy::unnecessary_lazy_evaluations,
|
|
clippy::if_then_some_else_none,
|
|
clippy::unnecessary_to_owned,
|
|
clippy::single_component_path_imports,
|
|
)]
|
|
//! # DQN Feature Cache Integration Tests
|
|
//!
|
|
//! Comprehensive test suite for the DQN feature caching system that pre-computes
|
|
//! 51-feature vectors (43 base + 8 OFI) once and reuses them across hyperopt trials.
|
|
//!
|
|
//! ## Test Coverage
|
|
//!
|
|
//! 1. **Cache Creation Success**: Verify cache file creation with correct metadata
|
|
//! 2. **Cache Loading Matches Original**: Ensure cached features match non-cached computation
|
|
//! 3. **Cache Invalidation**: Test cache key changes when input data changes
|
|
//! 4. **Hyperopt Integration**: Multi-trial cache reuse verification
|
|
//! 5. **Performance Benchmark**: Validate 20s → <1s loading speedup
|
|
//! 6. **Edge Cases**: Missing cache, corrupted cache, multiple datasets
|
|
//!
|
|
//! ## Expected Performance Gains
|
|
//!
|
|
//! - Cache creation: ~20-30s (one-time setup)
|
|
//! - Cache loading: <1s per trial
|
|
//! - Time savings: 87-96% for 50-trial hyperopt
|
|
//! - Cache size: 3-5 MB compressed Parquet per dataset
|
|
//!
|
|
//! Design reference: `/tmp/MBP10_OFI_CACHING_DESIGN.md` section 8
|
|
|
|
use anyhow::{Context, Result};
|
|
use std::fs;
|
|
use std::path::Path;
|
|
use std::time::{Duration, Instant};
|
|
use tempfile::TempDir;
|
|
use std::ffi::OsStr;
|
|
use tracing::{info, warn};
|
|
|
|
|
|
// Cache functions (to be implemented based on design doc)
|
|
// These will initially fail compilation until implemented
|
|
|
|
/// Calculate SHA256-based cache key from input data
|
|
///
|
|
/// Components:
|
|
/// - Parquet file path + modification time
|
|
/// - MBP-10 directory path + file list + modification times
|
|
/// - Feature extraction version (code hash)
|
|
/// - Warmup period constant
|
|
fn calculate_cache_key(
|
|
parquet_path: &Path,
|
|
mbp10_dir: &Path,
|
|
warmup_period: usize,
|
|
) -> Result<String> {
|
|
use sha2::{Digest, Sha256};
|
|
use std::time::UNIX_EPOCH;
|
|
|
|
let mut hasher = Sha256::new();
|
|
|
|
// 1. Parquet file path + mtime
|
|
hasher.update(parquet_path.to_string_lossy().as_bytes());
|
|
let parquet_mtime = parquet_path
|
|
.metadata()
|
|
.context("Failed to read parquet metadata")?
|
|
.modified()
|
|
.context("Failed to get parquet mtime")?
|
|
.duration_since(UNIX_EPOCH)
|
|
.context("System time error")?
|
|
.as_secs();
|
|
hasher.update(&parquet_mtime.to_le_bytes());
|
|
|
|
// 2. MBP-10 directory + all .dbn files + mtimes
|
|
hasher.update(mbp10_dir.to_string_lossy().as_bytes());
|
|
|
|
if mbp10_dir.exists() {
|
|
let mut dbn_files: Vec<_> = fs::read_dir(mbp10_dir)
|
|
.context("Failed to read mbp10 directory")?
|
|
.filter_map(|e| e.ok())
|
|
.filter(|e| e.path().extension() == Some(OsStr::new("dbn")))
|
|
.collect();
|
|
|
|
dbn_files.sort_by_key(|e| e.path());
|
|
|
|
for entry in &dbn_files {
|
|
hasher.update(entry.path().to_string_lossy().as_bytes());
|
|
let mtime = entry
|
|
.metadata()
|
|
.context("Failed to read dbn metadata")?
|
|
.modified()
|
|
.context("Failed to get dbn mtime")?
|
|
.duration_since(UNIX_EPOCH)
|
|
.context("System time error")?
|
|
.as_secs();
|
|
hasher.update(&mtime.to_le_bytes());
|
|
}
|
|
}
|
|
|
|
// 3. Feature extraction version (simplified - use constant for now)
|
|
// In production, this would hash the extraction.rs source file
|
|
let feature_version = "v1.0.0";
|
|
hasher.update(feature_version.as_bytes());
|
|
|
|
// 4. Warmup period
|
|
hasher.update(&warmup_period.to_le_bytes());
|
|
|
|
Ok(format!("{:x}", hasher.finalize()))
|
|
}
|
|
|
|
/// Create feature cache from parquet and MBP-10 data
|
|
///
|
|
/// This is a placeholder that will be implemented based on the design doc.
|
|
/// Expected behavior:
|
|
/// - Load data from parquet_path and mbp10_dir
|
|
/// - Extract 51-feature vectors
|
|
/// - Save to cache_dir as Parquet file
|
|
/// - Create metadata JSON file
|
|
async fn create_feature_cache(
|
|
_parquet_path: &Path,
|
|
_mbp10_dir: &Path,
|
|
_cache_dir: &Path,
|
|
_warmup_period: usize,
|
|
) -> Result<String> {
|
|
// NOTE: This is a stub that will fail until implemented
|
|
// Implementation should follow design doc section 4 (Phase 1)
|
|
|
|
anyhow::bail!(
|
|
"create_feature_cache not yet implemented - see /tmp/MBP10_OFI_CACHING_DESIGN.md Phase 1"
|
|
)
|
|
}
|
|
|
|
/// Load features from cache
|
|
///
|
|
/// Returns (train_data, val_data) tuples matching DQN trainer format.
|
|
/// Each sample is (FeatureVector, Vec<f64>) where Vec<f64> contains target prices.
|
|
async fn load_features_from_cache(
|
|
_cache_dir: &Path,
|
|
_parquet_path: &Path,
|
|
_mbp10_dir: &Path,
|
|
_warmup_period: usize,
|
|
) -> Result<Option<(Vec<([f64; 42], Vec<f64>)>, Vec<([f64; 42], Vec<f64>)>)>> {
|
|
// NOTE: This is a stub that will fail until implemented
|
|
// Implementation should follow design doc section 4 (Phase 2)
|
|
|
|
anyhow::bail!(
|
|
"load_features_from_cache not yet implemented - see /tmp/MBP10_OFI_CACHING_DESIGN.md Phase 2"
|
|
)
|
|
}
|
|
|
|
// ============================================================================
|
|
// TEST 1: Cache Creation Success
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
#[ignore] // Ignore until cache creation is implemented
|
|
async fn test_cache_creation_success() -> Result<()> {
|
|
// Setup
|
|
let temp_dir = TempDir::new().context("Failed to create temp directory")?;
|
|
let cache_dir = temp_dir.path().join("cache");
|
|
fs::create_dir_all(&cache_dir).context("Failed to create cache directory")?;
|
|
|
|
// Create cache
|
|
let result = create_feature_cache(
|
|
Path::new("test_data/ES_FUT_180d.parquet"),
|
|
Path::new("test_data/mbp10-fixtures"),
|
|
&cache_dir,
|
|
50,
|
|
).await;
|
|
|
|
assert!(
|
|
result.is_ok(),
|
|
"Cache creation should succeed: {:?}",
|
|
result.err()
|
|
);
|
|
|
|
// Verify cache file exists
|
|
let cache_files: Vec<_> = fs::read_dir(&cache_dir)
|
|
.context("Failed to read cache directory")?
|
|
.filter_map(|e| e.ok())
|
|
.filter(|e| e.path().extension() == Some(OsStr::new("parquet")))
|
|
.collect();
|
|
|
|
assert_eq!(
|
|
cache_files.len(),
|
|
1,
|
|
"Should create exactly one cache file"
|
|
);
|
|
|
|
// Verify metadata exists
|
|
let metadata_path = cache_dir.join("cache_metadata.json");
|
|
assert!(
|
|
metadata_path.exists(),
|
|
"Metadata file should exist at {:?}",
|
|
metadata_path
|
|
);
|
|
|
|
// Verify file size (should be 3-5 MB compressed)
|
|
let file_size = fs::metadata(cache_files[0].path())
|
|
.context("Failed to read cache file metadata")?
|
|
.len();
|
|
|
|
assert!(
|
|
file_size > 2_000_000,
|
|
"Cache file too small: {} bytes (expected >2MB)",
|
|
file_size
|
|
);
|
|
assert!(
|
|
file_size < 10_000_000,
|
|
"Cache file too large: {} bytes (expected <10MB)",
|
|
file_size
|
|
);
|
|
|
|
info!(file_size_kb = file_size / 1024, "Cache created successfully");
|
|
Ok(())
|
|
}
|
|
|
|
// ============================================================================
|
|
// TEST 2: Cache Loading Matches Original
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
#[ignore] // Ignore until cache loading is implemented
|
|
async fn test_cache_loading_matches_original() -> Result<()> {
|
|
let temp_dir = TempDir::new().context("Failed to create temp directory")?;
|
|
let cache_dir = temp_dir.path().join("cache");
|
|
fs::create_dir_all(&cache_dir).context("Failed to create cache directory")?;
|
|
|
|
// Create cache
|
|
create_feature_cache(
|
|
Path::new("test_data/ES_FUT_180d.parquet"),
|
|
Path::new("test_data/mbp10-fixtures"),
|
|
&cache_dir,
|
|
50,
|
|
)
|
|
.await
|
|
.context("Failed to create cache")?;
|
|
|
|
// Load from cache
|
|
let (cached_train, cached_val) = load_features_from_cache(
|
|
&cache_dir,
|
|
Path::new("test_data/ES_FUT_180d.parquet"),
|
|
Path::new("test_data/mbp10-fixtures"),
|
|
50,
|
|
)
|
|
.await
|
|
.context("Failed to load from cache")?
|
|
.expect("Cache should exist");
|
|
|
|
// Load original way (no cache) - this requires DQN trainer modification
|
|
// For now, we'll verify the cached data structure is correct
|
|
|
|
assert!(
|
|
!cached_train.is_empty(),
|
|
"Training data should not be empty"
|
|
);
|
|
assert!(
|
|
!cached_val.is_empty(),
|
|
"Validation data should not be empty"
|
|
);
|
|
|
|
// Verify feature dimensions
|
|
let (features, targets) = &cached_train[0];
|
|
assert_eq!(
|
|
features.len(),
|
|
51,
|
|
"Each sample should have 51 features"
|
|
);
|
|
assert_eq!(
|
|
targets.len(),
|
|
4,
|
|
"Each sample should have 4 target prices"
|
|
);
|
|
|
|
// Verify train/val split is approximately 80/20
|
|
let total = cached_train.len() + cached_val.len();
|
|
let train_ratio = cached_train.len() as f64 / total as f64;
|
|
assert!(
|
|
(train_ratio - 0.8).abs() < 0.05,
|
|
"Train/val split should be ~80/20, got {:.2}",
|
|
train_ratio
|
|
);
|
|
|
|
// Compare first 10 samples (features should be deterministic)
|
|
for i in 0..10.min(cached_train.len()) {
|
|
let features = &cached_train[i].0;
|
|
|
|
// Verify no NaN or Inf values
|
|
for (j, &value) in features.iter().enumerate() {
|
|
assert!(
|
|
value.is_finite(),
|
|
"Feature {} in sample {} is not finite: {}",
|
|
j,
|
|
i,
|
|
value
|
|
);
|
|
}
|
|
}
|
|
|
|
info!(train_samples = cached_train.len(), val_samples = cached_val.len(), "Cache loaded successfully");
|
|
Ok(())
|
|
}
|
|
|
|
// ============================================================================
|
|
// TEST 3: Cache Invalidation on Data Change
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_cache_invalidation_on_data_change() -> Result<()> {
|
|
let temp_dir = TempDir::new().context("Failed to create temp directory")?;
|
|
|
|
// Create a temporary parquet file
|
|
let parquet_path = temp_dir.path().join("test.parquet");
|
|
fs::write(&parquet_path, b"mock parquet data v1")
|
|
.context("Failed to write parquet file")?;
|
|
|
|
let mbp10_dir = temp_dir.path().join("mbp10");
|
|
fs::create_dir_all(&mbp10_dir).context("Failed to create mbp10 directory")?;
|
|
|
|
// Calculate initial cache key
|
|
let cache_key1 = calculate_cache_key(&parquet_path, &mbp10_dir, 50)
|
|
.context("Failed to calculate initial cache key")?;
|
|
|
|
// Wait to ensure mtime changes (Linux has second-level granularity in most filesystems)
|
|
std::thread::sleep(Duration::from_secs(2));
|
|
|
|
// Modify file content to trigger mtime update
|
|
fs::write(&parquet_path, b"mock parquet data v2 - MODIFIED CONTENT")
|
|
.context("Failed to modify parquet file")?;
|
|
|
|
// Calculate new cache key
|
|
let cache_key2 = calculate_cache_key(&parquet_path, &mbp10_dir, 50)
|
|
.context("Failed to calculate new cache key")?;
|
|
|
|
info!(cache_key1 = &cache_key1[..16], "Cache key 1 (original)");
|
|
info!(cache_key2 = &cache_key2[..16], "Cache key 2 (after modification)");
|
|
|
|
// Keys should differ (cache invalidated)
|
|
assert_ne!(
|
|
cache_key1, cache_key2,
|
|
"Cache key should change when data file is modified.\nKey1: {}\nKey2: {}",
|
|
cache_key1, cache_key2
|
|
);
|
|
|
|
info!(old_key = &cache_key1[..16], new_key = &cache_key2[..16], "Cache invalidation works");
|
|
Ok(())
|
|
}
|
|
|
|
// ============================================================================
|
|
// TEST 4: Cache Key Stability
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_cache_key_stability() -> Result<()> {
|
|
let temp_dir = TempDir::new().context("Failed to create temp directory")?;
|
|
|
|
// Create test files
|
|
let parquet_path = temp_dir.path().join("test.parquet");
|
|
fs::write(&parquet_path, b"mock parquet data")
|
|
.context("Failed to write parquet file")?;
|
|
|
|
let mbp10_dir = temp_dir.path().join("mbp10");
|
|
fs::create_dir_all(&mbp10_dir).context("Failed to create mbp10 directory")?;
|
|
|
|
// Calculate cache key multiple times without modification
|
|
let key1 = calculate_cache_key(&parquet_path, &mbp10_dir, 50)?;
|
|
let key2 = calculate_cache_key(&parquet_path, &mbp10_dir, 50)?;
|
|
let key3 = calculate_cache_key(&parquet_path, &mbp10_dir, 50)?;
|
|
|
|
// Keys should be identical (deterministic)
|
|
assert_eq!(
|
|
key1, key2,
|
|
"Cache key should be deterministic"
|
|
);
|
|
assert_eq!(
|
|
key2, key3,
|
|
"Cache key should be deterministic"
|
|
);
|
|
|
|
info!(key = %key1, "Cache key is stable");
|
|
Ok(())
|
|
}
|
|
|
|
// ============================================================================
|
|
// TEST 5: Cache Key with Different Warmup Periods
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_cache_key_different_warmup() -> Result<()> {
|
|
let temp_dir = TempDir::new().context("Failed to create temp directory")?;
|
|
|
|
// Create test files
|
|
let parquet_path = temp_dir.path().join("test.parquet");
|
|
fs::write(&parquet_path, b"mock parquet data")
|
|
.context("Failed to write parquet file")?;
|
|
|
|
let mbp10_dir = temp_dir.path().join("mbp10");
|
|
fs::create_dir_all(&mbp10_dir).context("Failed to create mbp10 directory")?;
|
|
|
|
// Calculate cache keys with different warmup periods
|
|
let key_warmup_50 = calculate_cache_key(&parquet_path, &mbp10_dir, 50)?;
|
|
let key_warmup_100 = calculate_cache_key(&parquet_path, &mbp10_dir, 100)?;
|
|
|
|
// Keys should differ (different warmup periods)
|
|
assert_ne!(
|
|
key_warmup_50, key_warmup_100,
|
|
"Cache key should change when warmup period changes"
|
|
);
|
|
|
|
info!(key_warmup_50 = &key_warmup_50[..16], key_warmup_100 = &key_warmup_100[..16], "Different warmup periods produce different keys");
|
|
Ok(())
|
|
}
|
|
|
|
// ============================================================================
|
|
// TEST 6: Hyperopt Integration (Multi-Trial Cache Reuse)
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
#[ignore] // Ignore until hyperopt integration is implemented
|
|
async fn test_hyperopt_with_cache() -> Result<()> {
|
|
let temp_dir = TempDir::new().context("Failed to create temp directory")?;
|
|
let cache_dir = temp_dir.path().join("cache");
|
|
fs::create_dir_all(&cache_dir).context("Failed to create cache directory")?;
|
|
|
|
// Pre-create cache
|
|
create_feature_cache(
|
|
Path::new("test_data/ES_FUT_180d.parquet"),
|
|
Path::new("test_data/mbp10-fixtures"),
|
|
&cache_dir,
|
|
50,
|
|
)
|
|
.await
|
|
.context("Failed to create cache")?;
|
|
|
|
// Simulate 3 hyperopt trials (in production, this would use DQNHyperoptAdapter)
|
|
let start = Instant::now();
|
|
|
|
let mut trial_durations = Vec::new();
|
|
|
|
for trial in 1..=3 {
|
|
let trial_start = Instant::now();
|
|
|
|
// Load features from cache
|
|
let (_train, _val) = load_features_from_cache(
|
|
&cache_dir,
|
|
Path::new("test_data/ES_FUT_180d.parquet"),
|
|
Path::new("test_data/mbp10-fixtures"),
|
|
50,
|
|
)
|
|
.await
|
|
.context("Failed to load from cache")?
|
|
.expect("Cache should exist");
|
|
|
|
let trial_duration = trial_start.elapsed();
|
|
trial_durations.push(trial_duration);
|
|
|
|
info!(trial, duration = ?trial_duration, "Loaded features from cache");
|
|
}
|
|
|
|
let total_duration = start.elapsed();
|
|
|
|
// Verify results
|
|
assert_eq!(trial_durations.len(), 3, "Should complete all 3 trials");
|
|
|
|
// Each trial should load in <2s (with cache)
|
|
for (i, duration) in trial_durations.iter().enumerate() {
|
|
assert!(
|
|
duration < &Duration::from_secs(2),
|
|
"Trial {} took too long: {:?} (expected <2s with cache)",
|
|
i + 1,
|
|
duration
|
|
);
|
|
}
|
|
|
|
// Total should be <10s (generous, in practice should be ~3s)
|
|
assert!(
|
|
total_duration < Duration::from_secs(10),
|
|
"Total hyperopt time with cache took too long: {:?} (expected <10s)",
|
|
total_duration
|
|
);
|
|
|
|
info!(total_duration = ?total_duration, avg_per_trial = ?(total_duration / 3), "Hyperopt with cache completed");
|
|
Ok(())
|
|
}
|
|
|
|
// ============================================================================
|
|
// TEST 7: Performance Benchmark (Cache vs No Cache)
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
#[ignore] // Ignore by default - only run for performance validation
|
|
async fn test_cache_performance_benchmark() -> Result<()> {
|
|
let temp_dir = TempDir::new().context("Failed to create temp directory")?;
|
|
let cache_dir = temp_dir.path().join("cache");
|
|
fs::create_dir_all(&cache_dir).context("Failed to create cache directory")?;
|
|
|
|
info!("DQN Feature Cache Performance Benchmark");
|
|
|
|
// Benchmark: Cache creation
|
|
info!("Step 1: Cache creation (one-time setup)");
|
|
let start = Instant::now();
|
|
create_feature_cache(
|
|
Path::new("test_data/ES_FUT_180d.parquet"),
|
|
Path::new("test_data/mbp10-fixtures"),
|
|
&cache_dir,
|
|
50,
|
|
)
|
|
.await
|
|
.context("Failed to create cache")?;
|
|
let creation_time = start.elapsed();
|
|
info!(creation_time = ?creation_time, "Cache creation time");
|
|
|
|
// Verify creation time is reasonable
|
|
assert!(
|
|
creation_time < Duration::from_secs(60),
|
|
"Cache creation too slow: {:?} (expected <60s)",
|
|
creation_time
|
|
);
|
|
|
|
// Benchmark: Cache loading (3 trials)
|
|
info!("Step 2: Cache loading (simulating hyperopt trials)");
|
|
let mut load_times = Vec::new();
|
|
for trial in 1..=3 {
|
|
let start = Instant::now();
|
|
let (_train, _val) = load_features_from_cache(
|
|
&cache_dir,
|
|
Path::new("test_data/ES_FUT_180d.parquet"),
|
|
Path::new("test_data/mbp10-fixtures"),
|
|
50,
|
|
)
|
|
.await
|
|
.context("Failed to load from cache")?
|
|
.expect("Cache should exist");
|
|
let load_time = start.elapsed();
|
|
load_times.push(load_time);
|
|
info!(trial, load_time = ?load_time, "Cache load time for trial");
|
|
}
|
|
|
|
let avg_load_time = load_times.iter().sum::<Duration>() / load_times.len() as u32;
|
|
|
|
// Summary
|
|
let without_cache = Duration::from_secs(20 * 50); // 20s per trial
|
|
let with_cache = creation_time + (avg_load_time * 50);
|
|
let savings = without_cache.as_secs() - with_cache.as_secs();
|
|
let savings_pct = (savings as f64 / without_cache.as_secs() as f64) * 100.0;
|
|
|
|
info!(creation_time = ?creation_time, avg_load_time = ?avg_load_time, savings_pct, "Performance summary");
|
|
info!(without_cache_secs = without_cache.as_secs(), with_cache_secs = with_cache.as_secs(), savings_secs = savings, "Hyperopt impact (50 trials)");
|
|
|
|
// Assertions
|
|
assert!(
|
|
avg_load_time < Duration::from_secs(2),
|
|
"Cache loading too slow: {:?} (expected <2s)",
|
|
avg_load_time
|
|
);
|
|
|
|
assert!(
|
|
savings_pct > 80.0,
|
|
"Cache savings insufficient: {:.1}% (expected >80%)",
|
|
savings_pct
|
|
);
|
|
|
|
info!("Performance benchmark passed");
|
|
Ok(())
|
|
}
|
|
|
|
// ============================================================================
|
|
// TEST 8: Edge Case - Missing Cache Directory
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
#[ignore] // Ignore until cache loading is implemented
|
|
async fn test_missing_cache_graceful_fallback() -> Result<()> {
|
|
let temp_dir = TempDir::new().context("Failed to create temp directory")?;
|
|
let nonexistent_cache = temp_dir.path().join("nonexistent_cache");
|
|
|
|
// Try to load from non-existent cache
|
|
let result = load_features_from_cache(
|
|
&nonexistent_cache,
|
|
Path::new("test_data/ES_FUT_180d.parquet"),
|
|
Path::new("test_data/mbp10-fixtures"),
|
|
50,
|
|
)
|
|
.await;
|
|
|
|
// Should return None (cache miss) rather than error
|
|
match result {
|
|
Ok(None) => {
|
|
info!("Missing cache handled gracefully (returns None)");
|
|
Ok(())
|
|
}
|
|
Ok(Some(_)) => {
|
|
anyhow::bail!("Should not find cache in non-existent directory")
|
|
}
|
|
Err(e) => {
|
|
anyhow::bail!("Should return None for missing cache, got error: {}", e)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// TEST 9: Multiple Datasets (Separate Cache Files)
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
#[ignore] // Ignore until cache creation is implemented
|
|
async fn test_multiple_datasets_separate_caches() -> Result<()> {
|
|
let temp_dir = TempDir::new().context("Failed to create temp directory")?;
|
|
let cache_dir = temp_dir.path().join("cache");
|
|
fs::create_dir_all(&cache_dir).context("Failed to create cache directory")?;
|
|
|
|
// Create test files for two datasets
|
|
let dataset1 = temp_dir.path().join("ES_FUT_180d.parquet");
|
|
let dataset2 = temp_dir.path().join("NQ_FUT_180d.parquet");
|
|
|
|
fs::write(&dataset1, b"ES futures data").context("Failed to write dataset1")?;
|
|
fs::write(&dataset2, b"NQ futures data").context("Failed to write dataset2")?;
|
|
|
|
let mbp10_dir = temp_dir.path().join("mbp10");
|
|
fs::create_dir_all(&mbp10_dir).context("Failed to create mbp10 directory")?;
|
|
|
|
// Calculate cache keys for both datasets
|
|
let key1 = calculate_cache_key(&dataset1, &mbp10_dir, 50)?;
|
|
let key2 = calculate_cache_key(&dataset2, &mbp10_dir, 50)?;
|
|
|
|
// Keys should differ (different datasets)
|
|
assert_ne!(
|
|
key1, key2,
|
|
"Different datasets should produce different cache keys"
|
|
);
|
|
|
|
info!(es_key = &key1[..16], nq_key = &key2[..16], "Multiple datasets produce separate cache keys");
|
|
Ok(())
|
|
}
|
|
|