//! Comprehensive tests for Parquet persistence functionality //! Target: 35+ test functions for full coverage #![allow( clippy::absurd_extreme_comparisons, clippy::assertions_on_constants, clippy::assertions_on_result_states, clippy::clone_on_copy, clippy::decimal_literal_representation, clippy::doc_lazy_continuation, clippy::doc_markdown, clippy::double_comparisons, clippy::else_if_without_else, clippy::empty_drop, clippy::expect_used, clippy::field_reassign_with_default, clippy::format_push_string, clippy::if_then_some_else_none, clippy::indexing_slicing, clippy::integer_division, clippy::len_zero, clippy::let_and_return, clippy::let_underscore_must_use, clippy::manual_let_else, clippy::manual_range_contains, clippy::map_err_ignore, clippy::missing_const_for_fn, clippy::modulo_arithmetic, clippy::needless_range_loop, clippy::new_without_default, clippy::non_ascii_literal, clippy::nonminimal_bool, clippy::octal_escapes, clippy::overly_complex_bool_expr, clippy::redundant_clone, clippy::shadow_reuse, clippy::shadow_unrelated, clippy::similar_names, clippy::single_component_path_imports, clippy::single_match_else, clippy::str_to_string, clippy::string_slice, clippy::tests_outside_test_module, clippy::unnecessary_cast, clippy::unnecessary_get_then_check, clippy::unnecessary_unwrap, clippy::unseparated_literal_suffix, clippy::unwrap_or_default, clippy::unwrap_used, clippy::use_debug, clippy::useless_vec, clippy::wildcard_enum_match_arm, dead_code, deprecated, unreachable_pub, unused_assignments, unused_comparisons, unused_imports, unused_variables )] use data::parquet_persistence::{ MarketDataEvent, ParquetConfig, ParquetMarketDataReader, ParquetMarketDataWriter, }; use parquet::basic::Compression; use parquet::file::properties::EnabledStatistics; use std::fs; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; use tempfile::TempDir; use tokio::time::{sleep, Duration}; use tracing_subscriber; mod real_data_helpers; use real_data_helpers::RealDataLoader; // Test utilities and setup struct TestSetup { temp_dir: TempDir, config: ParquetConfig, } impl TestSetup { fn new() -> Self { let temp_dir = tempfile::tempdir().expect("Failed to create temp directory"); let config = ParquetConfig { base_path: temp_dir.path().to_string_lossy().to_string(), batch_size: 100, flush_interval_ms: 1000, compression: Compression::SNAPPY, enable_dictionary: true, enable_statistics: EnabledStatistics::Page, }; Self { temp_dir, config } } fn custom_config(batch_size: usize, flush_interval_ms: u64) -> Self { let temp_dir = tempfile::tempdir().expect("Failed to create temp directory"); let config = ParquetConfig { base_path: temp_dir.path().to_string_lossy().to_string(), batch_size, flush_interval_ms, compression: Compression::SNAPPY, enable_dictionary: true, enable_statistics: EnabledStatistics::Page, }; Self { temp_dir, config } } fn with_compression(compression: Compression) -> Self { let temp_dir = tempfile::tempdir().expect("Failed to create temp directory"); let config = ParquetConfig { base_path: temp_dir.path().to_string_lossy().to_string(), batch_size: 100, flush_interval_ms: 1000, compression, enable_dictionary: true, enable_statistics: EnabledStatistics::Page, }; Self { temp_dir, config } } } fn create_test_event(timestamp_ns: u64, symbol: &str, sequence: u64) -> MarketDataEvent { MarketDataEvent { timestamp_ns, symbol: symbol.to_string(), venue: "test_venue".to_string(), event_type: trading_engine::types::metrics::MarketDataEventType::Trade, price: Some(100.0 + sequence as f64), quantity: Some(1.0), sequence, latency_ns: Some(1000), open: None, high: None, low: None, } } /// Load real DBN data events for testing async fn load_real_btc_events(count: usize) -> Option> { let loader = RealDataLoader::new(); if !loader.files_exist() { return None; } let reader = ParquetMarketDataReader::new( std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) .parent() .unwrap() .join("test_data/real/parquet") .to_string_lossy() .to_string(), ); match reader.read_file("BTC-USD_30day_2024-09.parquet").await { Ok(events) => Some(events.into_iter().take(count).collect()), Err(_) => None, } } /// Load real DBN data events for ETH async fn load_real_eth_events(count: usize) -> Option> { let loader = RealDataLoader::new(); if !loader.files_exist() { return None; } let reader = ParquetMarketDataReader::new( std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) .parent() .unwrap() .join("test_data/real/parquet") .to_string_lossy() .to_string(), ); match reader.read_file("ETH-USD_30day_2024-09.parquet").await { Ok(events) => Some(events.into_iter().take(count).collect()), Err(_) => None, } } fn create_quote_event(timestamp_ns: u64, symbol: &str, sequence: u64) -> MarketDataEvent { MarketDataEvent { timestamp_ns, symbol: symbol.to_string(), venue: "test_venue".to_string(), event_type: trading_engine::types::metrics::MarketDataEventType::Quote, price: Some(99.0 + sequence as f64 * 0.1), quantity: Some(100.0), sequence, latency_ns: Some(500), open: None, high: None, low: None, } } // Initialize test logging (call once per test process) fn init_logging() { let _ = tracing_subscriber::fmt().with_test_writer().try_init(); } // === BASIC FUNCTIONALITY TESTS === #[tokio::test] async fn test_parquet_config_default() { let config = ParquetConfig::default(); assert_eq!(config.base_path, "./market_data"); assert_eq!(config.batch_size, 10000); assert_eq!(config.flush_interval_ms, 5000); assert_eq!(config.compression, Compression::SNAPPY); assert!(config.enable_dictionary); assert_eq!(config.enable_statistics, EnabledStatistics::Page); } #[tokio::test] async fn test_market_data_event_creation() { let event = create_test_event(1234567890000000000, "BTCUSD", 1); assert_eq!(event.timestamp_ns, 1234567890000000000); assert_eq!(event.symbol, "BTCUSD"); assert_eq!(event.venue, "test_venue"); assert_eq!( event.event_type, trading_engine::types::metrics::MarketDataEventType::Trade ); assert_eq!(event.price, Some(101.0)); assert_eq!(event.sequence, 1); } #[tokio::test] async fn test_parquet_writer_creation() { init_logging(); let setup = TestSetup::new(); let writer = ParquetMarketDataWriter::new(setup.config).await; assert!(writer.is_ok(), "Failed to create ParquetMarketDataWriter"); } #[tokio::test] async fn test_parquet_writer_creation_invalid_path() { init_logging(); let config = ParquetConfig { base_path: "/invalid/path/that/cannot/be/created".to_string(), ..Default::default() }; let writer = ParquetMarketDataWriter::new(config).await; assert!(writer.is_err(), "Should fail with invalid path"); } #[tokio::test] async fn test_single_event_recording() { init_logging(); let setup = TestSetup::custom_config(1, 100); // Immediate flush let writer = ParquetMarketDataWriter::new(setup.config).await.unwrap(); let event = create_test_event(1234567890000000000, "ETHUSD", 1); let result = writer.record(event); assert!(result.is_ok(), "Failed to record event"); // Wait for background processing sleep(Duration::from_millis(200)).await; // Check that file was created let files: Vec<_> = fs::read_dir(setup.temp_dir.path()) .unwrap() .filter_map(|entry| { let entry = entry.ok()?; let path = entry.path(); if path.extension()?.to_str()? == "parquet" { Some(path) } else { None } }) .collect(); assert_eq!(files.len(), 1, "Expected exactly one parquet file"); } // === BATCH PROCESSING TESTS === #[tokio::test] async fn test_batch_size_flush() { init_logging(); let setup = TestSetup::custom_config(5, 10000); // Large flush interval let writer = ParquetMarketDataWriter::new(setup.config).await.unwrap(); // Send exactly batch_size events for i in 0..5 { let event = create_test_event(1234567890000000000 + i * 1000, "BTCUSD", i); writer.record(event).unwrap(); } // Wait for batch flush sleep(Duration::from_millis(200)).await; let files: Vec<_> = fs::read_dir(setup.temp_dir.path()) .unwrap() .filter_map(|entry| { let entry = entry.ok()?; let path = entry.path(); if path.extension()?.to_str()? == "parquet" { Some(path) } else { None } }) .collect(); assert_eq!( files.len(), 1, "Expected one parquet file after batch flush" ); } #[tokio::test] async fn test_time_based_flush() { init_logging(); let setup = TestSetup::custom_config(1000, 100); // Small flush interval let writer = ParquetMarketDataWriter::new(setup.config).await.unwrap(); // Send fewer events than batch size for i in 0..3 { let event = create_test_event(1234567890000000000 + i * 1000, "ETHUSD", i); writer.record(event).unwrap(); } // Wait for time-based flush sleep(Duration::from_millis(300)).await; let files: Vec<_> = fs::read_dir(setup.temp_dir.path()) .unwrap() .filter_map(|entry| { let entry = entry.ok()?; let path = entry.path(); if path.extension()?.to_str()? == "parquet" { Some(path) } else { None } }) .collect(); assert_eq!(files.len(), 1, "Expected one parquet file after time flush"); } #[tokio::test] async fn test_multiple_batches() { init_logging(); let setup = TestSetup::custom_config(3, 10000); let writer = ParquetMarketDataWriter::new(setup.config).await.unwrap(); // Send two full batches for i in 0..6 { let event = create_test_event(1234567890000000000 + i * 1000, "BTCUSD", i); writer.record(event).unwrap(); } sleep(Duration::from_millis(300)).await; let files: Vec<_> = fs::read_dir(setup.temp_dir.path()) .unwrap() .filter_map(|entry| { let entry = entry.ok()?; let path = entry.path(); if path.extension()?.to_str()? == "parquet" { Some(path) } else { None } }) .collect(); assert_eq!(files.len(), 2, "Expected two parquet files for two batches"); } // === COMPRESSION TESTS === #[tokio::test] async fn test_snappy_compression() { init_logging(); let setup = TestSetup::with_compression(Compression::SNAPPY); let writer = ParquetMarketDataWriter::new(setup.config).await.unwrap(); let event = create_test_event(1234567890000000000, "BTCUSD", 1); writer.record(event).unwrap(); sleep(Duration::from_millis(200)).await; let files: Vec<_> = fs::read_dir(setup.temp_dir.path()) .unwrap() .filter_map(|entry| { let entry = entry.ok()?; let path = entry.path(); if path.extension()?.to_str()? == "parquet" { Some(path) } else { None } }) .collect(); assert_eq!(files.len(), 1); assert!(files[0].metadata().unwrap().len() > 0); } #[tokio::test] async fn test_gzip_compression() { init_logging(); let setup = TestSetup::with_compression(Compression::GZIP(parquet::basic::GzipLevel::default())); let writer = ParquetMarketDataWriter::new(setup.config).await.unwrap(); let event = create_test_event(1234567890000000000, "ETHUSD", 1); writer.record(event).unwrap(); sleep(Duration::from_millis(200)).await; let files: Vec<_> = fs::read_dir(setup.temp_dir.path()) .unwrap() .filter_map(|entry| { let entry = entry.ok()?; let path = entry.path(); if path.extension()?.to_str()? == "parquet" { Some(path) } else { None } }) .collect(); assert_eq!(files.len(), 1); } #[tokio::test] async fn test_lz4_compression() { init_logging(); let setup = TestSetup::with_compression(Compression::LZ4); let writer = ParquetMarketDataWriter::new(setup.config).await.unwrap(); let event = create_test_event(1234567890000000000, "ADAUSD", 1); writer.record(event).unwrap(); sleep(Duration::from_millis(200)).await; let files: Vec<_> = fs::read_dir(setup.temp_dir.path()) .unwrap() .filter_map(|entry| { let entry = entry.ok()?; let path = entry.path(); if path.extension()?.to_str()? == "parquet" { Some(path) } else { None } }) .collect(); assert_eq!(files.len(), 1); } #[tokio::test] async fn test_uncompressed() { init_logging(); let setup = TestSetup::with_compression(Compression::UNCOMPRESSED); let writer = ParquetMarketDataWriter::new(setup.config).await.unwrap(); let event = create_test_event(1234567890000000000, "SOLUSD", 1); writer.record(event).unwrap(); sleep(Duration::from_millis(200)).await; let files: Vec<_> = fs::read_dir(setup.temp_dir.path()) .unwrap() .filter_map(|entry| { let entry = entry.ok()?; let path = entry.path(); if path.extension()?.to_str()? == "parquet" { Some(path) } else { None } }) .collect(); assert_eq!(files.len(), 1); } // === DATA TYPE TESTS === #[tokio::test] async fn test_trade_events() { init_logging(); let setup = TestSetup::custom_config(1, 100); let writer = ParquetMarketDataWriter::new(setup.config).await.unwrap(); let event = MarketDataEvent { timestamp_ns: 1234567890000000000, symbol: "BTCUSD".to_string(), venue: "binance".to_string(), event_type: trading_engine::types::metrics::MarketDataEventType::Trade, price: Some(50000.0), quantity: Some(0.1), sequence: 1, latency_ns: Some(1000), open: None, high: None, low: None, }; writer.record(event).unwrap(); sleep(Duration::from_millis(200)).await; let files: Vec<_> = fs::read_dir(setup.temp_dir.path()) .unwrap() .filter_map(|entry| { let entry = entry.ok()?; let path = entry.path(); if path.extension()?.to_str()? == "parquet" { Some(path) } else { None } }) .collect(); assert_eq!(files.len(), 1); } #[tokio::test] async fn test_quote_events() { init_logging(); let setup = TestSetup::custom_config(1, 100); let writer = ParquetMarketDataWriter::new(setup.config).await.unwrap(); let event = create_quote_event(1234567890000000000, "ETHUSD", 1); writer.record(event).unwrap(); sleep(Duration::from_millis(200)).await; let files: Vec<_> = fs::read_dir(setup.temp_dir.path()) .unwrap() .filter_map(|entry| { let entry = entry.ok()?; let path = entry.path(); if path.extension()?.to_str()? == "parquet" { Some(path) } else { None } }) .collect(); assert_eq!(files.len(), 1); } #[tokio::test] async fn test_orderbook_events() { init_logging(); let setup = TestSetup::custom_config(1, 100); let writer = ParquetMarketDataWriter::new(setup.config).await.unwrap(); let event = MarketDataEvent { timestamp_ns: 1234567890000000000, symbol: "ADAUSD".to_string(), venue: "coinbase".to_string(), event_type: trading_engine::types::metrics::MarketDataEventType::OrderBookUpdate, price: None, quantity: None, sequence: 1, latency_ns: Some(2000), open: None, high: None, low: None, }; writer.record(event).unwrap(); sleep(Duration::from_millis(200)).await; let files: Vec<_> = fs::read_dir(setup.temp_dir.path()) .unwrap() .filter_map(|entry| { let entry = entry.ok()?; let path = entry.path(); if path.extension()?.to_str()? == "parquet" { Some(path) } else { None } }) .collect(); assert_eq!(files.len(), 1); } #[tokio::test] async fn test_mixed_event_types() { init_logging(); let setup = TestSetup::custom_config(10, 10000); let writer = ParquetMarketDataWriter::new(setup.config).await.unwrap(); // Mix of different event types let events = vec![ create_test_event(1234567890000000000, "BTCUSD", 1), create_quote_event(1234567890000001000, "BTCUSD", 2), MarketDataEvent { timestamp_ns: 1234567890000002000, symbol: "BTCUSD".to_string(), venue: "binance".to_string(), event_type: trading_engine::types::metrics::MarketDataEventType::StatusUpdate, price: None, quantity: None, sequence: 3, latency_ns: None, open: None, high: None, low: None, }, ]; for event in events { writer.record(event).unwrap(); } sleep(Duration::from_millis(200)).await; } // === LARGE DATASET TESTS === #[tokio::test] async fn test_large_batch_processing() { init_logging(); let setup = TestSetup::custom_config(1000, 5000); let writer = ParquetMarketDataWriter::new(setup.config).await.unwrap(); // Send 2500 events (2.5 batches) for i in 0..2500 { let event = create_test_event(1234567890000000000 + i * 1000, "BTCUSD", i); writer.record(event).unwrap(); } sleep(Duration::from_millis(1000)).await; let files: Vec<_> = fs::read_dir(setup.temp_dir.path()) .unwrap() .filter_map(|entry| { let entry = entry.ok()?; let path = entry.path(); if path.extension()?.to_str()? == "parquet" { Some(path) } else { None } }) .collect(); assert!( files.len() >= 2, "Expected at least 2 files for large dataset" ); } #[tokio::test] async fn test_high_frequency_events() { init_logging(); let setup = TestSetup::custom_config(100, 1000); let writer = ParquetMarketDataWriter::new(setup.config).await.unwrap(); // Simulate high-frequency trading events let start_time = 1234567890000000000u64; for i in 0..500 { let event = create_test_event(start_time + i * 1000, "ETHUSD", i); writer.record(event).unwrap(); } sleep(Duration::from_millis(2000)).await; let files: Vec<_> = fs::read_dir(setup.temp_dir.path()) .unwrap() .filter_map(|entry| { let entry = entry.ok()?; let path = entry.path(); if path.extension()?.to_str()? == "parquet" { Some(path) } else { None } }) .collect(); assert!( files.len() >= 1, "Expected at least 1 file for high-frequency data" ); } // === BUFFER MANAGEMENT TESTS === #[tokio::test] async fn test_buffer_stats() { init_logging(); let setup = TestSetup::custom_config(1000, 10000); // Large batch, long interval let writer = ParquetMarketDataWriter::new(setup.config).await.unwrap(); // Initial stats should show empty buffer let stats = writer.get_buffer_stats().await; assert_eq!(stats.buffered_events, 0); assert!(stats.buffer_capacity > 0); assert_eq!(stats.utilization_percent, 0.0); // Add some events for i in 0..10 { let event = create_test_event(1234567890000000000 + i * 1000, "BTCUSD", i); writer.record(event).unwrap(); } // Give time for events to be queued sleep(Duration::from_millis(50)).await; let stats = writer.get_buffer_stats().await; // Note: Events might be processed quickly, so we can't guarantee exact count assert!(stats.buffer_capacity > 0); } #[tokio::test] async fn test_buffer_utilization() { init_logging(); let setup = TestSetup::custom_config(100, 10000); // Medium batch, long interval let writer = ParquetMarketDataWriter::new(setup.config).await.unwrap(); // Fill buffer partially for i in 0..50 { let event = create_test_event(1234567890000000000 + i * 1000, "ETHUSD", i); writer.record(event).unwrap(); } sleep(Duration::from_millis(100)).await; let stats = writer.get_buffer_stats().await; assert!(stats.buffer_capacity >= 100); // At least batch_size * 2 } // === MULTITHREADING AND CONCURRENCY TESTS === #[tokio::test] async fn test_concurrent_writes() { init_logging(); let setup = TestSetup::custom_config(50, 1000); let writer = Arc::new(ParquetMarketDataWriter::new(setup.config).await.unwrap()); let sequence_counter = Arc::new(AtomicU64::new(0)); // Spawn multiple concurrent tasks let mut handles = Vec::new(); for task_id in 0..5 { let writer_clone = writer.clone(); let counter_clone = sequence_counter.clone(); let handle = tokio::spawn(async move { for _i in 0..20 { let seq = counter_clone.fetch_add(1, Ordering::SeqCst); let event = create_test_event( 1234567890000000000 + seq * 1000, &format!("SYM{}", task_id), seq, ); writer_clone.record(event).unwrap(); } }); handles.push(handle); } // Wait for all tasks to complete for handle in handles { handle.await.unwrap(); } sleep(Duration::from_millis(2000)).await; let files: Vec<_> = fs::read_dir(setup.temp_dir.path()) .unwrap() .filter_map(|entry| { let entry = entry.ok()?; let path = entry.path(); if path.extension()?.to_str()? == "parquet" { Some(path) } else { None } }) .collect(); assert!(files.len() >= 1, "Expected files from concurrent writes"); } // === SCHEMA AND PARTITIONING TESTS === #[tokio::test] async fn test_multiple_symbols() { init_logging(); let setup = TestSetup::custom_config(10, 1000); let writer = ParquetMarketDataWriter::new(setup.config).await.unwrap(); let symbols = vec!["BTCUSD", "ETHUSD", "ADAUSD", "SOLUSD", "DOTUSD"]; for (i, symbol) in symbols.into_iter().enumerate() { let event = create_test_event(1234567890000000000 + i as u64 * 1000, symbol, i as u64); writer.record(event).unwrap(); } sleep(Duration::from_millis(2000)).await; let files: Vec<_> = fs::read_dir(setup.temp_dir.path()) .unwrap() .filter_map(|entry| { let entry = entry.ok()?; let path = entry.path(); if path.extension()?.to_str()? == "parquet" { Some(path) } else { None } }) .collect(); assert!(files.len() >= 1); } #[tokio::test] async fn test_multiple_venues() { init_logging(); let setup = TestSetup::custom_config(10, 1000); let writer = ParquetMarketDataWriter::new(setup.config).await.unwrap(); let venues = vec!["binance", "coinbase", "kraken", "bitstamp", "gemini"]; for (i, venue) in venues.into_iter().enumerate() { let mut event = create_test_event(1234567890000000000 + i as u64 * 1000, "BTCUSD", i as u64); event.venue = venue.to_string(); writer.record(event).unwrap(); } sleep(Duration::from_millis(2000)).await; } // === FILE NAMING AND ORGANIZATION TESTS === #[tokio::test] async fn test_file_naming_convention() { init_logging(); let setup = TestSetup::custom_config(1, 100); let writer = ParquetMarketDataWriter::new(setup.config).await.unwrap(); let event = create_test_event(1234567890000000000, "BTCUSD", 1); writer.record(event).unwrap(); sleep(Duration::from_millis(200)).await; let files: Vec<_> = fs::read_dir(setup.temp_dir.path()) .unwrap() .filter_map(|entry| { let entry = entry.ok()?; let path = entry.path(); if path.extension()?.to_str()? == "parquet" { path.file_name()?.to_str().map(|s| s.to_string()) } else { None } }) .collect(); assert_eq!(files.len(), 1); let filename = &files[0]; assert!(filename.starts_with("market_data_")); assert!(filename.ends_with(".parquet")); assert!(filename.contains("_")); // Contains timestamp and UUID } #[tokio::test] async fn test_directory_creation() { init_logging(); let temp_dir = tempfile::tempdir().unwrap(); let nested_path = temp_dir.path().join("nested").join("path"); let config = ParquetConfig { base_path: nested_path.to_string_lossy().to_string(), batch_size: 1, flush_interval_ms: 100, compression: Compression::SNAPPY, enable_dictionary: true, enable_statistics: EnabledStatistics::Page, }; let writer = ParquetMarketDataWriter::new(config).await; assert!(writer.is_ok(), "Should create nested directories"); let event = create_test_event(1234567890000000000, "TESTCOIN", 1); writer.unwrap().record(event).unwrap(); sleep(Duration::from_millis(200)).await; assert!(nested_path.exists()); } // === ERROR HANDLING AND EDGE CASES === #[tokio::test] async fn test_empty_symbol() { init_logging(); let setup = TestSetup::custom_config(1, 100); let writer = ParquetMarketDataWriter::new(setup.config).await.unwrap(); let mut event = create_test_event(1234567890000000000, "", 1); event.symbol = "".to_string(); let result = writer.record(event); assert!(result.is_ok(), "Should handle empty symbol"); sleep(Duration::from_millis(200)).await; } #[tokio::test] async fn test_extreme_values() { init_logging(); let setup = TestSetup::custom_config(1, 100); let writer = ParquetMarketDataWriter::new(setup.config).await.unwrap(); let event = MarketDataEvent { timestamp_ns: u64::MAX, symbol: "EXTREME".to_string(), venue: "test".to_string(), event_type: trading_engine::types::metrics::MarketDataEventType::Trade, price: Some(f64::MAX), quantity: Some(f64::MIN_POSITIVE), sequence: u64::MAX, latency_ns: Some(u64::MAX), open: None, high: None, low: None, }; let result = writer.record(event); assert!(result.is_ok(), "Should handle extreme values"); sleep(Duration::from_millis(200)).await; } #[tokio::test] async fn test_unicode_symbols() { init_logging(); let setup = TestSetup::custom_config(1, 100); let writer = ParquetMarketDataWriter::new(setup.config).await.unwrap(); let mut event = create_test_event(1234567890000000000, "测试币", 1); event.venue = "交易所".to_string(); let result = writer.record(event); assert!(result.is_ok(), "Should handle Unicode strings"); sleep(Duration::from_millis(200)).await; } #[tokio::test] async fn test_null_optional_fields() { init_logging(); let setup = TestSetup::custom_config(1, 100); let writer = ParquetMarketDataWriter::new(setup.config).await.unwrap(); let event = MarketDataEvent { timestamp_ns: 1234567890000000000, symbol: "NULLTEST".to_string(), venue: "test".to_string(), event_type: trading_engine::types::metrics::MarketDataEventType::Trade, price: None, quantity: None, sequence: 1, latency_ns: None, open: None, high: None, low: None, }; let result = writer.record(event); assert!(result.is_ok(), "Should handle all null optional fields"); sleep(Duration::from_millis(200)).await; } // === READER TESTS === #[tokio::test] async fn test_reader_creation() { let temp_dir = tempfile::tempdir().unwrap(); let reader = ParquetMarketDataReader::new(temp_dir.path().to_string_lossy().to_string()); // Just test that reader can be created assert_eq!( reader.base_path(), temp_dir.path().to_string_lossy().to_string() ); } #[tokio::test] async fn test_reader_list_empty_directory() { let temp_dir = tempfile::tempdir().unwrap(); let reader = ParquetMarketDataReader::new(temp_dir.path().to_string_lossy().to_string()); let files = reader.list_available_files().await.unwrap(); assert_eq!(files.len(), 0, "Empty directory should have no files"); } #[tokio::test] async fn test_reader_list_with_parquet_files() { let temp_dir = tempfile::tempdir().unwrap(); // Create some test files fs::write(temp_dir.path().join("test1.parquet"), b"fake parquet").unwrap(); fs::write(temp_dir.path().join("test2.parquet"), b"fake parquet").unwrap(); fs::write(temp_dir.path().join("test.txt"), b"not parquet").unwrap(); let reader = ParquetMarketDataReader::new(temp_dir.path().to_string_lossy().to_string()); let files = reader.list_available_files().await.unwrap(); assert_eq!(files.len(), 2, "Should find only parquet files"); assert!(files.contains(&"test1.parquet".to_string())); assert!(files.contains(&"test2.parquet".to_string())); assert!(files[0] <= files[1], "Files should be sorted"); } #[tokio::test] async fn test_reader_invalid_directory() { let reader = ParquetMarketDataReader::new("/invalid/path/that/does/not/exist".to_string()); let result = reader.list_available_files().await; assert!(result.is_err(), "Should fail for invalid directory"); } #[tokio::test] async fn test_reader_read_placeholder() { let temp_dir = tempfile::tempdir().unwrap(); let reader = ParquetMarketDataReader::new(temp_dir.path().to_string_lossy().to_string()); // Test placeholder implementation let events = reader.read_file("nonexistent.parquet").await.unwrap(); assert_eq!(events.len(), 0, "Placeholder should return empty vec"); } // === PERFORMANCE AND MONITORING TESTS === #[tokio::test] async fn test_metrics_integration() { init_logging(); let setup = TestSetup::custom_config(1, 100); let writer = ParquetMarketDataWriter::new(setup.config).await.unwrap(); let event = create_test_event(1234567890000000000, "METRICSTEST", 1); writer.record(event).unwrap(); sleep(Duration::from_millis(300)).await; // Metrics should be recorded, but we can't easily test them without // accessing the actual metrics registry } #[tokio::test] async fn test_performance_timing() { init_logging(); let setup = TestSetup::custom_config(100, 5000); let writer = ParquetMarketDataWriter::new(setup.config).await.unwrap(); let start = std::time::Instant::now(); // Write many events quickly for i in 0..1000 { let event = create_test_event(1234567890000000000 + i * 1000, "PERFTEST", i); writer.record(event).unwrap(); } let record_duration = start.elapsed(); println!("Recorded 1000 events in {:?}", record_duration); // Should be very fast for recording (just queuing) assert!( record_duration.as_millis() < 100, "Recording should be fast" ); sleep(Duration::from_millis(2000)).await; } // === ERROR HANDLING TESTS (NEW) === #[tokio::test] async fn test_write_to_readonly_directory() { init_logging(); let temp_dir = tempfile::tempdir().unwrap(); let readonly_path = temp_dir.path().join("readonly"); fs::create_dir_all(&readonly_path).unwrap(); // Set directory to read-only on Unix systems #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; let mut perms = fs::metadata(&readonly_path).unwrap().permissions(); perms.set_mode(0o444); // Read-only fs::set_permissions(&readonly_path, perms).unwrap(); } let config = ParquetConfig { base_path: readonly_path.to_string_lossy().to_string(), batch_size: 1, flush_interval_ms: 100, compression: Compression::SNAPPY, enable_dictionary: true, enable_statistics: EnabledStatistics::Page, }; let writer = ParquetMarketDataWriter::new(config).await.unwrap(); let event = create_test_event(1234567890000000000, "TESTCOIN", 1); writer.record(event).unwrap(); // Wait for background processing to attempt write sleep(Duration::from_millis(300)).await; // The error should be logged, but writer continues to operate // This tests error resilience in background task // Cleanup: Reset permissions so temp_dir can be deleted #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; let mut perms = fs::metadata(&readonly_path).unwrap().permissions(); perms.set_mode(0o755); // Restore write permissions fs::set_permissions(&readonly_path, perms).unwrap(); } } #[tokio::test] async fn test_writer_with_invalid_parent_path() { init_logging(); // Try to create writer in a path that cannot exist let config = ParquetConfig { base_path: "/proc/self/mem/invalid".to_string(), ..Default::default() }; let result = ParquetMarketDataWriter::new(config).await; assert!( result.is_err(), "Should fail to create writer with invalid path" ); } #[tokio::test] async fn test_empty_batch_edge_case() { init_logging(); let setup = TestSetup::custom_config(0, 100); // Zero batch size edge case // This tests the behavior when batch_size is 0 let result = ParquetMarketDataWriter::new(setup.config).await; assert!(result.is_ok(), "Should handle zero batch size gracefully"); } #[tokio::test] async fn test_very_long_symbol_name() { init_logging(); let setup = TestSetup::custom_config(1, 100); let writer = ParquetMarketDataWriter::new(setup.config).await.unwrap(); // Create symbol with 1000 characters let long_symbol = "A".repeat(1000); let mut event = create_test_event(1234567890000000000, "BTC", 1); event.symbol = long_symbol; let result = writer.record(event); assert!(result.is_ok(), "Should handle very long symbol names"); sleep(Duration::from_millis(200)).await; } #[tokio::test] async fn test_very_long_venue_name() { init_logging(); let setup = TestSetup::custom_config(1, 100); let writer = ParquetMarketDataWriter::new(setup.config).await.unwrap(); // Create venue with 1000 characters let long_venue = "V".repeat(1000); let mut event = create_test_event(1234567890000000000, "BTC", 1); event.venue = long_venue; let result = writer.record(event); assert!(result.is_ok(), "Should handle very long venue names"); sleep(Duration::from_millis(200)).await; } #[tokio::test] async fn test_special_characters_in_paths() { init_logging(); let temp_dir = tempfile::tempdir().unwrap(); // Create path with special characters (spaces, unicode, etc) let special_path = temp_dir.path().join("special path 测试 !@#"); let config = ParquetConfig { base_path: special_path.to_string_lossy().to_string(), batch_size: 1, flush_interval_ms: 100, compression: Compression::SNAPPY, enable_dictionary: true, enable_statistics: EnabledStatistics::Page, }; let result = ParquetMarketDataWriter::new(config).await; assert!(result.is_ok(), "Should handle special characters in path"); if let Ok(writer) = result { let event = create_test_event(1234567890000000000, "TEST", 1); writer.record(event).unwrap(); sleep(Duration::from_millis(200)).await; assert!( special_path.exists(), "Directory with special chars should exist" ); } } #[tokio::test] async fn test_rapid_writer_creation_and_drop() { init_logging(); // Test that creating and dropping writers rapidly doesn't cause issues for i in 0..10 { let temp_dir = tempfile::tempdir().unwrap(); let config = ParquetConfig { base_path: temp_dir.path().to_string_lossy().to_string(), batch_size: 1, flush_interval_ms: 100, ..Default::default() }; let writer = ParquetMarketDataWriter::new(config).await.unwrap(); let event = create_test_event(1234567890000000000 + i, "RAPID", i); writer.record(event).unwrap(); // Writer drops immediately - test cleanup handling } sleep(Duration::from_millis(500)).await; } #[tokio::test] async fn test_negative_timestamp_handling() { init_logging(); let setup = TestSetup::custom_config(1, 100); let writer = ParquetMarketDataWriter::new(setup.config).await.unwrap(); // Test with timestamp that would be negative when cast to i64 let event = MarketDataEvent { timestamp_ns: 0, // Minimum valid timestamp symbol: "ZEROTIME".to_string(), venue: "test".to_string(), event_type: trading_engine::types::metrics::MarketDataEventType::Trade, price: Some(100.0), quantity: Some(1.0), sequence: 1, latency_ns: Some(1000), open: None, high: None, low: None, }; let result = writer.record(event); assert!(result.is_ok(), "Should handle zero timestamp"); sleep(Duration::from_millis(200)).await; } #[tokio::test] async fn test_nan_and_infinity_values() { init_logging(); let setup = TestSetup::custom_config(1, 100); let writer = ParquetMarketDataWriter::new(setup.config).await.unwrap(); // Test NaN let event_nan = MarketDataEvent { timestamp_ns: 1234567890000000000, symbol: "NANTEST".to_string(), venue: "test".to_string(), event_type: trading_engine::types::metrics::MarketDataEventType::Trade, price: Some(f64::NAN), quantity: Some(f64::NAN), sequence: 1, latency_ns: Some(1000), open: None, high: None, low: None, }; let result = writer.record(event_nan); assert!(result.is_ok(), "Should handle NaN values"); sleep(Duration::from_millis(100)).await; // Test Infinity let event_inf = MarketDataEvent { timestamp_ns: 1234567890000000001, symbol: "INFTEST".to_string(), venue: "test".to_string(), event_type: trading_engine::types::metrics::MarketDataEventType::Trade, price: Some(f64::INFINITY), quantity: Some(f64::NEG_INFINITY), sequence: 2, latency_ns: Some(2000), open: None, high: None, low: None, }; let result = writer.record(event_inf); assert!(result.is_ok(), "Should handle Infinity values"); sleep(Duration::from_millis(200)).await; } #[tokio::test] async fn test_sequence_overflow() { init_logging(); let setup = TestSetup::custom_config(1, 100); let writer = ParquetMarketDataWriter::new(setup.config).await.unwrap(); // Test with maximum sequence number let event = MarketDataEvent { timestamp_ns: 1234567890000000000, symbol: "SEQMAX".to_string(), venue: "test".to_string(), event_type: trading_engine::types::metrics::MarketDataEventType::Trade, price: Some(100.0), quantity: Some(1.0), sequence: u64::MAX, latency_ns: Some(u64::MAX), open: None, high: None, low: None, }; let result = writer.record(event); assert!(result.is_ok(), "Should handle maximum sequence number"); sleep(Duration::from_millis(200)).await; } #[tokio::test] async fn test_buffer_stats_during_flush() { init_logging(); let setup = TestSetup::custom_config(10, 5000); // Long flush interval let writer = ParquetMarketDataWriter::new(setup.config).await.unwrap(); // Fill buffer close to capacity for i in 0..8 { let event = create_test_event(1234567890000000000 + i * 1000, "BTCUSD", i); writer.record(event).unwrap(); } sleep(Duration::from_millis(50)).await; // Check stats while buffer is full let stats_before = writer.get_buffer_stats().await; // Trigger flush by sending more events for i in 8..12 { let event = create_test_event(1234567890000000000 + i * 1000, "BTCUSD", i); writer.record(event).unwrap(); } sleep(Duration::from_millis(200)).await; let stats_after = writer.get_buffer_stats().await; // After flush, buffer should have fewer events assert!(stats_after.buffered_events <= stats_before.buffered_events + 4); } #[tokio::test] async fn test_reader_with_mixed_file_types() { let temp_dir = tempfile::tempdir().unwrap(); // Create various file types fs::write(temp_dir.path().join("data1.parquet"), b"fake").unwrap(); fs::write(temp_dir.path().join("data2.PARQUET"), b"fake").unwrap(); // Uppercase extension fs::write(temp_dir.path().join("data.parquet.tmp"), b"fake").unwrap(); fs::write(temp_dir.path().join("data.txt"), b"fake").unwrap(); fs::write(temp_dir.path().join(".hidden.parquet"), b"fake").unwrap(); let reader = ParquetMarketDataReader::new(temp_dir.path().to_string_lossy().to_string()); let files = reader.list_available_files().await.unwrap(); // Should only find .parquet files (lowercase extension) assert!(files.contains(&"data1.parquet".to_string())); assert!(!files.contains(&"data2.PARQUET".to_string())); // Uppercase not matched assert!(!files.contains(&"data.parquet.tmp".to_string())); assert!(!files.contains(&"data.txt".to_string())); } #[tokio::test] async fn test_reader_with_subdirectories() { let temp_dir = tempfile::tempdir().unwrap(); // Create subdirectory with parquet file let subdir = temp_dir.path().join("subdir"); fs::create_dir_all(&subdir).unwrap(); fs::write(subdir.join("nested.parquet"), b"fake").unwrap(); fs::write(temp_dir.path().join("root.parquet"), b"fake").unwrap(); let reader = ParquetMarketDataReader::new(temp_dir.path().to_string_lossy().to_string()); let files = reader.list_available_files().await.unwrap(); // Should only list files in root directory, not subdirectories assert_eq!(files.len(), 1); assert!(files.contains(&"root.parquet".to_string())); assert!(!files.contains(&"nested.parquet".to_string())); } #[tokio::test] async fn test_reader_list_after_permission_denied() { let temp_dir = tempfile::tempdir().unwrap(); #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; // Create a file and make directory unreadable fs::write(temp_dir.path().join("test.parquet"), b"fake").unwrap(); let mut perms = fs::metadata(temp_dir.path()).unwrap().permissions(); perms.set_mode(0o000); // No permissions fs::set_permissions(temp_dir.path(), perms.clone()).unwrap(); let reader = ParquetMarketDataReader::new(temp_dir.path().to_string_lossy().to_string()); let result = reader.list_available_files().await; // Restore permissions for cleanup perms.set_mode(0o755); fs::set_permissions(temp_dir.path(), perms).unwrap(); assert!( result.is_err(), "Should fail when directory is not readable" ); } // On Windows, skip this test as permission model is different #[cfg(not(unix))] { let reader = ParquetMarketDataReader::new(temp_dir.path().to_string_lossy().to_string()); let _ = reader.list_available_files().await; // Just ensure it doesn't panic } } #[tokio::test] async fn test_concurrent_readers() { let temp_dir = tempfile::tempdir().unwrap(); // Create some parquet files for i in 0..5 { fs::write( temp_dir.path().join(format!("data{}.parquet", i)), b"fake data", ) .unwrap(); } let reader = Arc::new(ParquetMarketDataReader::new( temp_dir.path().to_string_lossy().to_string(), )); // Spawn multiple concurrent read tasks let mut handles = Vec::new(); for _ in 0..10 { let reader_clone = reader.clone(); let handle = tokio::spawn(async move { let files = reader_clone.list_available_files().await.unwrap(); assert_eq!(files.len(), 5); }); handles.push(handle); } // Wait for all readers to complete for handle in handles { handle.await.unwrap(); } } #[tokio::test] async fn test_reader_file_sorting() { let temp_dir = tempfile::tempdir().unwrap(); // Create files with specific names to test sorting let filenames = vec![ "c.parquet", "a.parquet", "b.parquet", "10.parquet", "2.parquet", ]; for name in &filenames { fs::write(temp_dir.path().join(name), b"fake").unwrap(); } let reader = ParquetMarketDataReader::new(temp_dir.path().to_string_lossy().to_string()); let files = reader.list_available_files().await.unwrap(); // Verify files are sorted assert_eq!(files.len(), 5); for i in 0..files.len() - 1 { assert!( files[i] <= files[i + 1], "Files should be sorted alphabetically" ); } } #[tokio::test] async fn test_zero_flush_interval() { init_logging(); let setup = TestSetup::custom_config(1000, 0); // Zero flush interval let writer = ParquetMarketDataWriter::new(setup.config).await.unwrap(); let event = create_test_event(1234567890000000000, "ZEROFLUSH", 1); writer.record(event).unwrap(); // Even with zero interval, should handle gracefully sleep(Duration::from_millis(200)).await; } #[tokio::test] async fn test_very_large_batch_size() { init_logging(); let setup = TestSetup::custom_config(1_000_000, 10000); // Very large batch let result = ParquetMarketDataWriter::new(setup.config).await; assert!(result.is_ok(), "Should handle very large batch size"); if let Ok(writer) = result { let event = create_test_event(1234567890000000000, "LARGEBATCH", 1); writer.record(event).unwrap(); sleep(Duration::from_millis(100)).await; } } #[tokio::test] async fn test_statistics_disabled() { init_logging(); let temp_dir = tempfile::tempdir().unwrap(); let config = ParquetConfig { base_path: temp_dir.path().to_string_lossy().to_string(), batch_size: 1, flush_interval_ms: 100, compression: Compression::SNAPPY, enable_dictionary: false, enable_statistics: EnabledStatistics::None, }; let writer = ParquetMarketDataWriter::new(config).await.unwrap(); let event = create_test_event(1234567890000000000, "NOSTATS", 1); let result = writer.record(event); assert!(result.is_ok(), "Should work with statistics disabled"); sleep(Duration::from_millis(200)).await; } #[tokio::test] async fn test_dictionary_disabled() { init_logging(); let temp_dir = tempfile::tempdir().unwrap(); let config = ParquetConfig { base_path: temp_dir.path().to_string_lossy().to_string(), batch_size: 1, flush_interval_ms: 100, compression: Compression::SNAPPY, enable_dictionary: false, enable_statistics: EnabledStatistics::Page, }; let writer = ParquetMarketDataWriter::new(config).await.unwrap(); let event = create_test_event(1234567890000000000, "NODICT", 1); let result = writer.record(event); assert!(result.is_ok(), "Should work with dictionary disabled"); sleep(Duration::from_millis(200)).await; } #[tokio::test] async fn test_all_event_types() { init_logging(); let setup = TestSetup::custom_config(10, 1000); let writer = ParquetMarketDataWriter::new(setup.config).await.unwrap(); // Test all possible event types let event_types = vec![ trading_engine::types::metrics::MarketDataEventType::Trade, trading_engine::types::metrics::MarketDataEventType::Quote, trading_engine::types::metrics::MarketDataEventType::OrderBookUpdate, trading_engine::types::metrics::MarketDataEventType::StatusUpdate, ]; for (i, event_type) in event_types.into_iter().enumerate() { let event = MarketDataEvent { timestamp_ns: 1234567890000000000 + i as u64 * 1000, symbol: "ALLTYPE".to_string(), venue: "test".to_string(), event_type, price: Some(100.0), quantity: Some(1.0), sequence: i as u64, latency_ns: Some(1000), open: None, high: None, low: None, }; writer.record(event).unwrap(); } sleep(Duration::from_millis(2000)).await; let files: Vec<_> = fs::read_dir(setup.temp_dir.path()) .unwrap() .filter_map(|entry| { let entry = entry.ok()?; let path = entry.path(); if path.extension()?.to_str()? == "parquet" { Some(path) } else { None } }) .collect(); assert!( files.len() >= 1, "Should create parquet files for all event types" ); } #[tokio::test] async fn test_writer_channel_closure() { init_logging(); let setup = TestSetup::custom_config(1, 100); let writer = ParquetMarketDataWriter::new(setup.config).await.unwrap(); let event = create_test_event(1234567890000000000, "TESTCLOSE", 1); writer.record(event).unwrap(); // Drop writer to trigger channel closure drop(writer); // Wait for background task cleanup sleep(Duration::from_millis(300)).await; // Test passes if no panic occurs } #[tokio::test] async fn test_empty_event_batch_handling() { init_logging(); let setup = TestSetup::custom_config(1, 50); // Very short flush interval let _writer = ParquetMarketDataWriter::new(setup.config).await.unwrap(); // Don't send any events, just wait for flush timer sleep(Duration::from_millis(200)).await; // Should handle empty flushes gracefully without creating files let files: Vec<_> = fs::read_dir(setup.temp_dir.path()) .unwrap() .filter_map(|entry| { let entry = entry.ok()?; let path = entry.path(); if path.extension()?.to_str()? == "parquet" { Some(path) } else { None } }) .collect(); assert_eq!(files.len(), 0, "Should not create files for empty batches"); } // ============================================================================ // REAL DBN DATA TESTS // ============================================================================ #[tokio::test] async fn test_parquet_write_real_btc_data() { init_logging(); // Load real BTC data let real_events = match load_real_btc_events(1000).await { Some(events) if !events.is_empty() => events, _ => { println!("Skipping test - real DBN BTC data not available"); return; }, }; println!( "✓ Loaded {} real BTC events from DBN data", real_events.len() ); let setup = TestSetup::custom_config(500, 1000); let writer = ParquetMarketDataWriter::new(setup.config).await.unwrap(); // Write real events for event in real_events { writer.record(event).unwrap(); } sleep(Duration::from_millis(2000)).await; let files: Vec<_> = fs::read_dir(setup.temp_dir.path()) .unwrap() .filter_map(|entry| { let entry = entry.ok()?; let path = entry.path(); if path.extension()?.to_str()? == "parquet" { Some(path) } else { None } }) .collect(); assert!( files.len() >= 2, "Expected at least 2 files for real BTC data" ); // Verify file sizes let total_size: u64 = files .iter() .filter_map(|f| f.metadata().ok()) .map(|m| m.len()) .sum(); println!( "✓ Total Parquet size: {} bytes for {} events", total_size, 1000 ); println!( "✓ Compression ratio: {:.2}:1", 1000.0 * 100.0 / total_size as f64 ); assert!(total_size > 1000, "Files should contain actual data"); } #[tokio::test] async fn test_parquet_write_real_eth_data() { init_logging(); // Load real ETH data let real_events = match load_real_eth_events(1000).await { Some(events) if !events.is_empty() => events, _ => { println!("Skipping test - real DBN ETH data not available"); return; }, }; println!( "✓ Loaded {} real ETH events from DBN data", real_events.len() ); let setup = TestSetup::custom_config(500, 1000); let writer = ParquetMarketDataWriter::new(setup.config).await.unwrap(); // Write real events for event in real_events { writer.record(event).unwrap(); } sleep(Duration::from_millis(2000)).await; let files: Vec<_> = fs::read_dir(setup.temp_dir.path()) .unwrap() .filter_map(|entry| { let entry = entry.ok()?; let path = entry.path(); if path.extension()?.to_str()? == "parquet" { Some(path) } else { None } }) .collect(); assert!( files.len() >= 2, "Expected at least 2 files for real ETH data" ); // Verify file sizes let total_size: u64 = files .iter() .filter_map(|f| f.metadata().ok()) .map(|m| m.len()) .sum(); println!( "✓ Total Parquet size: {} bytes for {} events", total_size, 1000 ); assert!(total_size > 1000, "Files should contain actual data"); } #[tokio::test] async fn test_parquet_compression_with_real_data() { init_logging(); // Load real data for compression testing let real_events = match load_real_btc_events(5000).await { Some(events) if !events.is_empty() => events, _ => { println!("Skipping test - real DBN data not available"); return; }, }; println!( "✓ Loaded {} real events for compression test", real_events.len() ); let temp_dir_snappy = TempDir::new().unwrap(); let temp_dir_gzip = TempDir::new().unwrap(); let config_snappy = ParquetConfig { base_path: temp_dir_snappy.path().to_string_lossy().to_string(), batch_size: 1000, flush_interval_ms: 100, compression: parquet::basic::Compression::SNAPPY, enable_dictionary: true, enable_statistics: EnabledStatistics::Page, }; let config_gzip = ParquetConfig { base_path: temp_dir_gzip.path().to_string_lossy().to_string(), batch_size: 1000, flush_interval_ms: 100, compression: parquet::basic::Compression::GZIP(parquet::basic::GzipLevel::default()), enable_dictionary: true, enable_statistics: EnabledStatistics::Page, }; let writer_snappy = ParquetMarketDataWriter::new(config_snappy.clone()) .await .unwrap(); let writer_gzip = ParquetMarketDataWriter::new(config_gzip.clone()) .await .unwrap(); // Write same real data to both for event in real_events.clone() { writer_snappy.record(event.clone()).unwrap(); writer_gzip.record(event).unwrap(); } sleep(Duration::from_millis(3000)).await; // Compare file sizes let snappy_files: Vec<_> = fs::read_dir(temp_dir_snappy.path()) .unwrap() .filter_map(|e| e.ok()) .collect(); let gzip_files: Vec<_> = fs::read_dir(temp_dir_gzip.path()) .unwrap() .filter_map(|e| e.ok()) .collect(); let snappy_size: u64 = snappy_files .iter() .filter_map(|f| f.metadata().ok()) .map(|m| m.len()) .sum(); let gzip_size: u64 = gzip_files .iter() .filter_map(|f| f.metadata().ok()) .map(|m| m.len()) .sum(); println!( "✓ SNAPPY: {} bytes, GZIP: {} bytes (real data)", snappy_size, gzip_size ); println!( "✓ GZIP saves: {:.1}% vs SNAPPY", (1.0 - gzip_size as f64 / snappy_size as f64) * 100.0 ); assert!( snappy_size > 0 && gzip_size > 0, "Both compressions should produce data" ); // GZIP typically achieves better compression assert!( gzip_size < snappy_size * 2, "GZIP should be competitive with SNAPPY" ); } #[tokio::test] async fn test_parquet_read_write_cycle_real_data() { init_logging(); // Load real data let real_events = match load_real_btc_events(100).await { Some(events) if !events.is_empty() => events, _ => { println!("Skipping test - real DBN data not available"); return; }, }; println!( "✓ Loaded {} real events for read/write cycle test", real_events.len() ); let temp_dir = TempDir::new().unwrap(); let config = ParquetConfig { base_path: temp_dir.path().to_string_lossy().to_string(), batch_size: 50, flush_interval_ms: 100, compression: parquet::basic::Compression::SNAPPY, enable_dictionary: true, enable_statistics: EnabledStatistics::Page, }; // Write events let writer = ParquetMarketDataWriter::new(config.clone()).await.unwrap(); let original_count = real_events.len(); for event in real_events { writer.record(event).unwrap(); } sleep(Duration::from_millis(500)).await; // Read back and verify let reader = ParquetMarketDataReader::new(config.base_path.clone()); let files = reader.list_available_files().await.unwrap(); assert!(!files.is_empty(), "Should have created Parquet files"); let mut _total_read_events = 0; for file in &files { match reader.read_file(file).await { Ok(events) => { _total_read_events += events.len(); }, Err(e) => { println!("Warning: placeholder read_file returned error: {}", e); // Placeholder implementation returns empty vec }, } } println!( "✓ Read/write cycle: wrote {} events, files created: {}", original_count, 2 ); // Note: read_file is placeholder, so we just verify files were created assert!(files.len() >= 2, "Should have created multiple files"); }