//! Roundtrip tests for the `.fxcache` binary format. //! //! Covers f64/bf16 serialization, cache-key lookup, header validation, //! and the empty-bars error path. use ml::fxcache::{find_fxcache, load_fxcache, write_fxcache, FxCacheData}; use tempfile::TempDir; // ── Helpers ───────────────────────────────────────────────────────────────── /// Deterministic feature vector for bar `i`. fn make_features(n: usize) -> Vec<[f64; 42]> { (0..n) .map(|i| { let mut row = [0.0_f64; 42]; for j in 0..42 { row[j] = (i * 42 + j) as f64 * 0.01; } row }) .collect() } /// Deterministic target vector for bar `i`. fn make_targets(n: usize) -> Vec<[f64; 4]> { (0..n) .map(|i| { let mut row = [0.0_f64; 4]; for j in 0..4 { row[j] = 5000.0 + (i * 4 + j) as f64 * 0.25; } row }) .collect() } /// Deterministic OFI vector for bar `i`. fn make_ofi(n: usize) -> Vec<[f64; 8]> { (0..n) .map(|i| { let mut row = [0.0_f64; 8]; for j in 0..8 { row[j] = (i * 8 + j) as f64 * 0.001; } row }) .collect() } /// Deterministic per-bar timestamps (1-minute intervals starting 2022-01-01). fn make_timestamps(n: usize) -> Vec { (0..n) .map(|i| 1640995200_000_000_000i64 + i as i64 * 60_000_000_000) .collect() } /// A fixed 32-byte cache key for tests. fn test_cache_key() -> [u8; 32] { let mut key = [0u8; 32]; for (i, slot) in key.iter_mut().enumerate() { *slot = (i as u8).wrapping_mul(7).wrapping_add(13); } key } // ── Tests ─────────────────────────────────────────────────────────────────── /// Write 100 bars as f64 (version 1), read back, verify bit-exact match. #[test] fn test_fxcache_f64_roundtrip() { let dir = TempDir::new().unwrap(); let path = dir.path().join("test_f64.fxcache"); let n = 100; let features = make_features(n); let targets = make_targets(n); let ofi = make_ofi(n); let timestamps = make_timestamps(n); let key = test_cache_key(); let bytes_written = write_fxcache(&path, &features, &targets, &ofi, ×tamps, key, false, false).unwrap(); assert!(bytes_written > 0, "expected nonzero bytes written"); let data: FxCacheData = load_fxcache(&path).unwrap(); assert_eq!(data.bar_count, n); assert_eq!(data.cache_key, key); assert_eq!(data.features.len(), n); assert_eq!(data.targets.len(), n); assert_eq!(data.ofi.len(), n); assert_eq!(data.timestamps.len(), n); // Bit-exact match for f64 roundtrip. for i in 0..n { assert_eq!( data.timestamps[i], timestamps[i], "timestamp mismatch at bar {i}" ); assert_eq!( data.features[i], features[i], "feature mismatch at bar {i}" ); assert_eq!( data.targets[i], targets[i], "target mismatch at bar {i}" ); assert_eq!(data.ofi[i], ofi[i], "ofi mismatch at bar {i}"); } } /// Write 50 bars as bf16 (version 2), read back, verify within tolerance. /// bf16 has limited precision: ~3 significant digits. #[test] fn test_fxcache_bf16_roundtrip() { let dir = TempDir::new().unwrap(); let path = dir.path().join("test_bf16.fxcache"); let n = 50; let features = make_features(n); let targets = make_targets(n); let ofi = make_ofi(n); let timestamps = make_timestamps(n); let key = test_cache_key(); let bytes_written = write_fxcache(&path, &features, &targets, &ofi, ×tamps, key, true, false).unwrap(); assert!(bytes_written > 0, "expected nonzero bytes written"); let data: FxCacheData = load_fxcache(&path).unwrap(); assert_eq!(data.bar_count, n); assert_eq!(data.cache_key, key); // Timestamps are always i64 — bit-exact even in bf16 mode. for i in 0..n { assert_eq!( data.timestamps[i], timestamps[i], "timestamp mismatch at bar {i}" ); } // bf16 tolerance checks. for i in 0..n { for j in 0..42 { let diff = (data.features[i][j] - features[i][j]).abs(); assert!( diff < 0.02, "feature[{i}][{j}]: expected {}, got {}, diff={diff}", features[i][j], data.features[i][j] ); } for j in 0..4 { let diff = (data.targets[i][j] - targets[i][j]).abs(); // bf16 at ~5000 has resolution of ~4, so allow up to 4.0. assert!( diff <= 4.0, "target[{i}][{j}]: expected {}, got {}, diff={diff}", targets[i][j], data.targets[i][j] ); } for j in 0..8 { let diff = (data.ofi[i][j] - ofi[i][j]).abs(); assert!( diff < 0.02, "ofi[{i}][{j}]: expected {}, got {}, diff={diff}", ofi[i][j], data.ofi[i][j] ); } } } /// Write a file with a known cache key, verify `find_fxcache` locates it. /// Also verify that a different key returns `None`. #[test] fn test_fxcache_find() { let dir = TempDir::new().unwrap(); let key = test_cache_key(); let hex_key = hex::encode(key); let path = dir.path().join(format!("{hex_key}.fxcache")); let features = make_features(5); let targets = make_targets(5); let ofi = make_ofi(5); let timestamps = make_timestamps(5); write_fxcache(&path, &features, &targets, &ofi, ×tamps, key, false, false).unwrap(); // Should find the file. let found = find_fxcache(dir.path(), &key); assert!(found.is_some(), "expected to find fxcache file"); assert_eq!(found.unwrap(), path); // Wrong key should miss. let mut wrong_key = [0xFFu8; 32]; wrong_key[0] = 0x00; let miss = find_fxcache(dir.path(), &wrong_key); assert!(miss.is_none(), "expected miss for wrong key"); } /// Write garbage bytes to a file, verify `load_fxcache` returns an error /// whose message contains "magic". #[test] fn test_fxcache_header_validation() { let dir = TempDir::new().unwrap(); let path = dir.path().join("garbage.fxcache"); // Write 128 bytes of garbage (more than header size). std::fs::write(&path, vec![0xABu8; 128]).unwrap(); let err = load_fxcache(&path).unwrap_err(); let msg = format!("{err:#}"); assert!( msg.to_lowercase().contains("magic"), "expected error about magic bytes, got: {msg}" ); } /// Attempting to write 0 bars should fail (writer rejects empty data). /// Attempting to read a hand-crafted header with bar_count=0 should also fail. #[test] fn test_fxcache_empty() { let dir = TempDir::new().unwrap(); let path = dir.path().join("empty.fxcache"); // Writer rejects 0 bars. let features: Vec<[f64; 42]> = vec![]; let targets: Vec<[f64; 4]> = vec![]; let ofi: Vec<[f64; 8]> = vec![]; let timestamps: Vec = vec![]; let key = test_cache_key(); let err = write_fxcache(&path, &features, &targets, &ofi, ×tamps, key, false, false).unwrap_err(); let msg = format!("{err:#}"); assert!( msg.contains("0 bars") || msg.contains("empty"), "expected empty/0-bars error from writer, got: {msg}" ); // Reader also rejects a header with bar_count=0. // Hand-craft a valid-magic header with bar_count=0. let mut header = [0u8; 64]; header[0..8].copy_from_slice(b"FXCACHE\0"); header[8..10].copy_from_slice(&1u16.to_le_bytes()); // version=1 header[10..12].copy_from_slice(&42u16.to_le_bytes()); // feat_dim header[12..14].copy_from_slice(&4u16.to_le_bytes()); // target_dim header[14..16].copy_from_slice(&8u16.to_le_bytes()); // ofi_dim header[16..24].copy_from_slice(&0u64.to_le_bytes()); // bar_count=0 // cache_key + reserved stay zeroed. let path_reader = dir.path().join("empty_header.fxcache"); std::fs::write(&path_reader, &header).unwrap(); let err = load_fxcache(&path_reader).unwrap_err(); let msg = format!("{err:#}"); assert!( msg.contains("zero") || msg.contains("bar_count"), "expected bar_count/zero error from reader, got: {msg}" ); }