//! Comprehensive Test Fixtures System for Foxhunt HFT Trading System //! //! This module provides a production-ready test infrastructure with: //! - Standardized test symbols for all asset classes //! - Configurable test data generators //! - Reusable test scenarios and utilities //! - Mock services and database helpers //! //! ## Usage //! //! ```rust //! use tests::fixtures::{TEST_EQUITY_1, TEST_FOREX_1, generate_test_symbol}; //! use tests::fixtures::builders::PortfolioBuilder; //! //! // Use predefined test symbols //! let symbol = TEST_EQUITY_1; // "TEST_EQ_001" //! //! // Generate symbols dynamically //! let forex_symbol = generate_test_symbol(AssetClass::Currencies); //! //! // Use builders for complex data //! let portfolio = PortfolioBuilder::new() //! .with_symbol(TEST_EQUITY_1) //! .with_quantity(Decimal::from(100)) //! .build(); //! ``` use std::collections::HashMap; use std::sync::{Arc, atomic::{AtomicU16, AtomicU64, Ordering}}; use std::time::Duration; use tokio::sync::{mpsc, RwLock, Mutex}; use uuid::Uuid; use serde_json::json; use chrono::{DateTime, Utc}; use rust_decimal::Decimal; // Import TLI types explicitly (tli crate is available as dependency) use tli::error::{TliError, TliResult}; use tli::events::{Event, EventType, EventSeverity}; // Re-export sub-modules for easy access pub mod test_config; pub mod test_database; pub mod mock_services; pub mod test_data; pub mod builders; pub mod scenarios; pub mod helpers; // ============================================================================= // TEST SYMBOLS - PRODUCTION READY CONSTANTS // ============================================================================= /// Test equity symbols for consistent testing across all modules pub const TEST_EQUITY_1: &str = "TEST_EQ_001"; pub const TEST_EQUITY_2: &str = "TEST_EQ_002"; pub const TEST_EQUITY_3: &str = "TEST_EQ_003"; pub const TEST_EQUITY_LARGE_CAP: &str = "TEST_EQ_LC_001"; pub const TEST_EQUITY_MID_CAP: &str = "TEST_EQ_MC_001"; pub const TEST_EQUITY_SMALL_CAP: &str = "TEST_EQ_SC_001"; /// Test forex currency pairs for FX trading tests pub const TEST_FOREX_1: &str = "TEST_FX_EURUSD"; pub const TEST_FOREX_2: &str = "TEST_FX_GBPUSD"; pub const TEST_FOREX_3: &str = "TEST_FX_USDJPY"; pub const TEST_FOREX_EXOTIC: &str = "TEST_FX_USDTRY"; /// Test futures contracts for derivatives testing pub const TEST_FUTURE_1: &str = "TEST_FUT_ES001"; pub const TEST_FUTURE_2: &str = "TEST_FUT_NQ001"; pub const TEST_FUTURE_OIL: &str = "TEST_FUT_CL001"; pub const TEST_FUTURE_GOLD: &str = "TEST_FUT_GC001"; /// Test bond symbols for fixed income testing pub const TEST_BOND_1: &str = "TEST_BOND_UST10Y"; pub const TEST_BOND_2: &str = "TEST_BOND_UST2Y"; pub const TEST_BOND_CORP: &str = "TEST_BOND_CORP_AAA"; pub const TEST_BOND_HIGH_YIELD: &str = "TEST_BOND_HY_001"; /// Test commodity symbols pub const TEST_COMMODITY_1: &str = "TEST_COMM_GOLD"; pub const TEST_COMMODITY_2: &str = "TEST_COMM_SILVER"; pub const TEST_COMMODITY_OIL: &str = "TEST_COMM_OIL"; pub const TEST_COMMODITY_GAS: &str = "TEST_COMM_NATGAS"; /// Test cryptocurrency symbols pub const TEST_CRYPTO_1: &str = "TEST_CRYPTO_BTC"; pub const TEST_CRYPTO_2: &str = "TEST_CRYPTO_ETH"; pub const TEST_CRYPTO_ALT: &str = "TEST_CRYPTO_ADA"; /// Test option symbols pub const TEST_OPTION_CALL: &str = "TEST_OPT_CALL_001"; pub const TEST_OPTION_PUT: &str = "TEST_OPT_PUT_001"; /// Comprehensive symbol collections for batch testing pub const ALL_TEST_EQUITIES: &[&str] = &[ TEST_EQUITY_1, TEST_EQUITY_2, TEST_EQUITY_3, TEST_EQUITY_LARGE_CAP, TEST_EQUITY_MID_CAP, TEST_EQUITY_SMALL_CAP ]; pub const ALL_TEST_FX_PAIRS: &[&str] = &[ TEST_FOREX_1, TEST_FOREX_2, TEST_FOREX_3, TEST_FOREX_EXOTIC ]; pub const ALL_TEST_FUTURES: &[&str] = &[ TEST_FUTURE_1, TEST_FUTURE_2, TEST_FUTURE_OIL, TEST_FUTURE_GOLD ]; pub const ALL_TEST_BONDS: &[&str] = &[ TEST_BOND_1, TEST_BOND_2, TEST_BOND_CORP, TEST_BOND_HIGH_YIELD ]; pub const ALL_TEST_COMMODITIES: &[&str] = &[ TEST_COMMODITY_1, TEST_COMMODITY_2, TEST_COMMODITY_OIL, TEST_COMMODITY_GAS ]; pub const ALL_TEST_CRYPTOS: &[&str] = &[ TEST_CRYPTO_1, TEST_CRYPTO_2, TEST_CRYPTO_ALT ]; /// All test symbols combined for comprehensive testing pub const ALL_TEST_SYMBOLS: &[&str] = &[ // Equities TEST_EQUITY_1, TEST_EQUITY_2, TEST_EQUITY_3, TEST_EQUITY_LARGE_CAP, TEST_EQUITY_MID_CAP, TEST_EQUITY_SMALL_CAP, // Forex TEST_FOREX_1, TEST_FOREX_2, TEST_FOREX_3, TEST_FOREX_EXOTIC, // Futures TEST_FUTURE_1, TEST_FUTURE_2, TEST_FUTURE_OIL, TEST_FUTURE_GOLD, // Bonds TEST_BOND_1, TEST_BOND_2, TEST_BOND_CORP, TEST_BOND_HIGH_YIELD, // Commodities TEST_COMMODITY_1, TEST_COMMODITY_2, TEST_COMMODITY_OIL, TEST_COMMODITY_GAS, // Crypto TEST_CRYPTO_1, TEST_CRYPTO_2, TEST_CRYPTO_ALT, // Options TEST_OPTION_CALL, TEST_OPTION_PUT, ]; // ============================================================================= // TEST SYMBOL GENERATORS // ============================================================================= // Use local AssetClass definition for fixtures /// Asset class for test symbol generation #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum AssetClass { Equities, Currencies, Derivatives, FixedIncome, Commodities, Cash, Alternatives, } impl std::fmt::Display for AssetClass { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { AssetClass::Equities => write!(f, "Equities"), AssetClass::Currencies => write!(f, "Currencies"), AssetClass::Derivatives => write!(f, "Derivatives"), AssetClass::FixedIncome => write!(f, "FixedIncome"), AssetClass::Commodities => write!(f, "Commodities"), AssetClass::Cash => write!(f, "Cash"), AssetClass::Alternatives => write!(f, "Alternatives"), } } } /// Instrument type for test fixtures #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum InstrumentType { Equity, Currency, Future, Option, Bond, Commodity, Crypto, Swap, CDS, } impl std::fmt::Display for InstrumentType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { InstrumentType::Equity => write!(f, "Equity"), InstrumentType::Currency => write!(f, "Currency"), InstrumentType::Future => write!(f, "Future"), InstrumentType::Option => write!(f, "Option"), InstrumentType::Bond => write!(f, "Bond"), InstrumentType::Commodity => write!(f, "Commodity"), InstrumentType::Crypto => write!(f, "Crypto"), InstrumentType::Swap => write!(f, "Swap"), InstrumentType::CDS => write!(f, "CDS"), } } } /// Market sector for test fixtures #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum MarketSector { Technology, Healthcare, Financials, Energy, ConsumerDiscretionary, ConsumerStaples, Industrials, Materials, Utilities, RealEstate, Communication, } /// Test fixture Instrument type #[derive(Debug, Clone)] pub struct Instrument { pub id: Uuid, pub symbol: String, pub isin: Option, pub cusip: Option, pub bloomberg_id: Option, pub reuters_id: Option, pub name: String, pub instrument_type: InstrumentType, pub asset_class: AssetClass, pub sector: Option, pub currency: String, pub exchange: Option, pub tick_size: Option, pub lot_size: Option, pub multiplier: Option, pub maturity_date: Option>, pub strike_price: Option, pub option_type: Option, pub underlying_symbol: Option, pub is_active: bool, pub created_at: DateTime, pub updated_at: DateTime, pub metadata: serde_json::Value, } /// Test fixture Portfolio type #[derive(Debug, Clone)] pub struct Portfolio { pub id: String, pub name: String, pub description: Option, pub base_currency: String, pub portfolio_type: String, pub inception_date: DateTime, pub manager_id: String, pub benchmark: Option, pub risk_budget: Option, pub var_limit: Option, pub max_drawdown_limit: Option, pub is_active: bool, pub created_at: DateTime, pub updated_at: DateTime, pub metadata: serde_json::Value, } /// Test fixture Counterparty type #[derive(Debug, Clone)] pub struct Counterparty { pub id: String, pub name: String, pub counterparty_type: String, pub country: String, pub credit_rating: Option, pub lei_code: Option, pub parent_company: Option, pub is_active: bool, pub exposure_limit: Option, pub margin_requirement: Option, pub netting_agreement: bool, pub created_at: DateTime, pub updated_at: DateTime, pub metadata: serde_json::Value, } /// Generate a test symbol for the specified asset class pub fn generate_test_symbol(asset_class: AssetClass) -> String { use std::sync::atomic::{AtomicUsize, Ordering}; static EQUITY_COUNTER: AtomicUsize = AtomicUsize::new(1000); static FX_COUNTER: AtomicUsize = AtomicUsize::new(1000); static FUTURES_COUNTER: AtomicUsize = AtomicUsize::new(1000); static BOND_COUNTER: AtomicUsize = AtomicUsize::new(1000); static COMMODITY_COUNTER: AtomicUsize = AtomicUsize::new(1000); static CASH_COUNTER: AtomicUsize = AtomicUsize::new(1000); static ALT_COUNTER: AtomicUsize = AtomicUsize::new(1000); match asset_class { AssetClass::Equities => { let id = EQUITY_COUNTER.fetch_add(1, Ordering::Relaxed); format!("TEST_EQ_{:03}", id) }, AssetClass::Currencies => { let id = FX_COUNTER.fetch_add(1, Ordering::Relaxed); let pairs = ["EURUSD", "GBPUSD", "USDJPY", "AUDUSD", "USDCAD"]; let pair = pairs[id % pairs.len()]; format!("TEST_FX_{}", pair) }, AssetClass::Derivatives => { let id = FUTURES_COUNTER.fetch_add(1, Ordering::Relaxed); format!("TEST_FUT_{:03}", id) }, AssetClass::FixedIncome => { let id = BOND_COUNTER.fetch_add(1, Ordering::Relaxed); format!("TEST_BOND_{:03}", id) }, AssetClass::Commodities => { let id = COMMODITY_COUNTER.fetch_add(1, Ordering::Relaxed); format!("TEST_COMM_{:03}", id) }, AssetClass::Cash => { let id = CASH_COUNTER.fetch_add(1, Ordering::Relaxed); format!("TEST_CASH_{:03}", id) }, AssetClass::Alternatives => { let id = ALT_COUNTER.fetch_add(1, Ordering::Relaxed); // Assume crypto for alternatives format!("TEST_CRYPTO_{:03}", id) }, } } /// Generate multiple test symbols for an asset class pub fn generate_test_symbols(asset_class: AssetClass, count: usize) -> Vec { (0..count).map(|_| generate_test_symbol(asset_class)).collect() } /// Generate a random test symbol from all available symbols pub fn generate_random_test_symbol() -> &'static str { use rand::seq::SliceRandom; let mut rng = rand::thread_rng(); ALL_TEST_SYMBOLS.choose(&mut rng).unwrap_or(&TEST_EQUITY_1) } // ============================================================================= // TEST PRICE CONSTANTS // ============================================================================= /// Standard test prices for consistent testing pub const TEST_PRICE_EQUITY_BASE: f64 = 100.0; pub const TEST_PRICE_FX_BASE: f64 = 1.0; pub const TEST_PRICE_FUTURES_BASE: f64 = 4000.0; pub const TEST_PRICE_BOND_BASE: f64 = 100.0; pub const TEST_PRICE_COMMODITY_BASE: f64 = 2000.0; pub const TEST_PRICE_CRYPTO_BASE: f64 = 50000.0; /// Test quantity constants pub const TEST_QUANTITY_SMALL: i64 = 100; pub const TEST_QUANTITY_MEDIUM: i64 = 1000; pub const TEST_QUANTITY_LARGE: i64 = 10000; /// Test monetary amounts pub const TEST_AMOUNT_SMALL: f64 = 10000.0; pub const TEST_AMOUNT_MEDIUM: f64 = 100000.0; pub const TEST_AMOUNT_LARGE: f64 = 1000000.0; // ============================================================================= // TEST PORTFOLIO IDENTIFIERS // ============================================================================= /// Standard test portfolio IDs pub const TEST_PORTFOLIO_1: &str = "TEST_PORTFOLIO_001"; pub const TEST_PORTFOLIO_2: &str = "TEST_PORTFOLIO_002"; pub const TEST_PORTFOLIO_HEDGE: &str = "TEST_PORTFOLIO_HEDGE"; pub const TEST_PORTFOLIO_LONG_ONLY: &str = "TEST_PORTFOLIO_LONG"; pub const TEST_PORTFOLIO_MARKET_NEUTRAL: &str = "TEST_PORTFOLIO_NEUTRAL"; /// Test strategy identifiers pub const TEST_STRATEGY_MOMENTUM: &str = "TEST_STRAT_MOMENTUM"; pub const TEST_STRATEGY_MEAN_REVERT: &str = "TEST_STRAT_MEAN_REV"; pub const TEST_STRATEGY_ARBITRAGE: &str = "TEST_STRAT_ARBITRAGE"; // ============================================================================= // ORIGINAL FIXTURES INTEGRATION // ============================================================================= /// Integration test configuration #[derive(Debug, Clone)] pub struct IntegrationTestConfig { // Performance requirements pub max_latency_ns: u64, pub max_db_latency_ns: u64, pub max_risk_latency_ns: u64, pub max_ml_inference_latency_ns: u64, pub max_init_latency_ns: u64, pub min_throughput_ops_per_sec: f64, pub min_db_throughput_ops_per_sec: f64, pub min_backtest_throughput: f64, // Test parameters pub request_timeout_ms: u64, pub max_retry_attempts: u32, pub circuit_breaker_threshold: u32, pub concurrent_order_count: usize, pub parallel_backtest_count: usize, pub concurrent_operation_count: usize, pub batch_size: usize, pub stream_buffer_size: usize, pub stress_test_duration_secs: u64, // Database configuration pub test_db_url: String, pub test_db_max_connections: u32, pub enable_database_cleanup: bool, // Mock service configuration pub mock_service_latency_ms: u64, pub mock_failure_rate: f64, pub enable_chaos_testing: bool, } impl Default for IntegrationTestConfig { fn default() -> Self { Self { // HFT Performance requirements max_latency_ns: 50_000, // 50µs max latency max_db_latency_ns: 100_000, // 100µs max DB latency max_risk_latency_ns: 25_000, // 25µs max risk validation max_ml_inference_latency_ns: 50_000, // 50µs max ML inference max_init_latency_ns: 1_000_000, // 1ms max initialization min_throughput_ops_per_sec: 10_000.0, // 10K ops/sec minimum min_db_throughput_ops_per_sec: 5_000.0, // 5K DB ops/sec minimum min_backtest_throughput: 10.0, // 10 backtests/sec minimum // Test parameters request_timeout_ms: 5_000, // 5 second timeout max_retry_attempts: 3, circuit_breaker_threshold: 5, concurrent_order_count: 100, parallel_backtest_count: 20, concurrent_operation_count: 50, batch_size: 1000, stream_buffer_size: 10_000, stress_test_duration_secs: 30, // Database configuration test_db_url: std::env::var("TEST_DATABASE_URL") .unwrap_or_else(|_| "postgresql://foxhunt_test:test_password@localhost:5432/foxhunt_test".to_string()), test_db_max_connections: 20, enable_database_cleanup: true, // Mock service configuration mock_service_latency_ms: 10, mock_failure_rate: 0.01, // 1% failure rate enable_chaos_testing: false, } } } /// Base test result structure #[derive(Debug, Clone)] pub struct TestResult { pub name: String, pub passed: bool, pub execution_time: Duration, pub assertions: Vec, pub errors: Vec, pub metadata: HashMap, } impl TestResult { pub fn new(name: &str) -> Self { Self { name: name.to_string(), passed: false, execution_time: Duration::default(), assertions: Vec::new(), errors: Vec::new(), metadata: HashMap::new(), } } pub fn add_assertion(&mut self, description: &str, passed: bool) { self.assertions.push(Assertion { description: description.to_string(), passed, }); } pub fn add_error(&mut self, error: String) { self.errors.push(error); } pub fn set_passed(&mut self, passed: bool) { self.passed = passed; } } /// Individual test assertion #[derive(Debug, Clone)] pub struct Assertion { pub description: String, pub passed: bool, } /// Test suite containing multiple test results #[derive(Debug, Clone)] pub struct TestSuite { pub name: String, pub tests: Vec, pub passed_tests: usize, pub total_tests: usize, pub passed: bool, pub execution_time: Duration, pub metadata: HashMap, } impl TestSuite { pub fn new(name: &str) -> Self { Self { name: name.to_string(), tests: Vec::new(), passed_tests: 0, total_tests: 0, passed: false, execution_time: Duration::default(), metadata: HashMap::new(), } } pub fn add_test_result(&mut self, test: TestResult) { if test.passed { self.passed_tests += 1; } self.total_tests += 1; self.tests.push(test); } pub fn set_passed(&mut self, passed: bool) { self.passed = passed; } } /// Port manager for test services #[derive(Debug)] pub struct TestPortManager { next_port: AtomicU16, allocated_ports: RwLock>, } impl TestPortManager { pub fn new() -> Self { Self { next_port: AtomicU16::new(50000), // Start from port 50000 allocated_ports: RwLock::new(Vec::new()), } } pub async fn allocate_port(&self) -> u16 { loop { let port = self.next_port.fetch_add(1, Ordering::Relaxed); if port > 65000 { // Reset if we've used too many ports self.next_port.store(50000, Ordering::Relaxed); continue; } // Check if port is available if let Ok(listener) = tokio::net::TcpListener::bind(format!("127.0.0.1:{}", port)).await { drop(listener); // Release the port self.allocated_ports.write().await.push(port); return port; } } } pub async fn release_port(&self, port: u16) { let mut allocated = self.allocated_ports.write().await; if let Some(pos) = allocated.iter().position(|&p| p == port) { allocated.remove(pos); } } } impl Default for TestPortManager { fn default() -> Self { Self::new() } } // Global test port manager instance lazy_static::lazy_static! { pub static ref TEST_PORT_MANAGER: TestPortManager = TestPortManager::new(); } /// Test event publisher for streaming tests #[derive(Debug)] pub struct TestEventPublisher { _event_sender: mpsc::UnboundedSender, event_receiver: Arc>>, published_events: AtomicU64, } impl TestEventPublisher { pub async fn new() -> TliResult { let (sender, receiver) = mpsc::unbounded_channel(); Ok(Self { _event_sender: sender, event_receiver: Arc::new(Mutex::new(receiver)), published_events: AtomicU64::new(0), }) } pub async fn publish_event(&self, event: Event) -> TliResult<()> { self._event_sender.send(event) .map_err(|e| TliError::Other(format!("Failed to publish event: {}", e)))?; self.published_events.fetch_add(1, Ordering::Relaxed); Ok(()) } pub async fn publish_market_data_burst(&self, symbol: &str, count: usize) -> TliResult<()> { for i in 0..count { let event = Event { id: Uuid::new_v4(), event_type: EventType::MarketData, severity: EventSeverity::Info, source: "test_publisher".to_string(), timestamp_nanos: Utc::now().timestamp_nanos_opt().unwrap_or(0), sequence: i as u64, payload: json!({ "symbol": symbol, "price": 150.0 + (i as f64 * 0.01), "volume": 100 + i, "sequence": i }), correlation_id: None, metadata: HashMap::new(), ttl_seconds: 0, }; self.publish_event(event).await?; } Ok(()) } pub async fn publish_order_lifecycle(&self, order_id: &str) -> TliResult<()> { let states = vec!["pending", "partially_filled", "filled"]; for (i, state) in states.iter().enumerate() { let event = Event { id: Uuid::new_v4(), event_type: EventType::Trading, severity: EventSeverity::Info, source: "test_lifecycle".to_string(), timestamp_nanos: Utc::now().timestamp_nanos_opt().unwrap_or(0), sequence: i as u64, payload: json!({ "order_id": order_id, "status": state, "filled_quantity": (i + 1) * 50, "remaining_quantity": 100 - ((i + 1) * 50) }), correlation_id: None, metadata: HashMap::new(), ttl_seconds: 0, }; self.publish_event(event).await?; // Small delay between state changes tokio::time::sleep(Duration::from_millis(100)).await; } Ok(()) } pub fn get_published_count(&self) -> u64 { self.published_events.load(Ordering::Relaxed) } pub async fn receive_event(&self) -> Option { let mut receiver = self.event_receiver.lock().await; receiver.recv().await } } /// Performance metrics collector for tests #[derive(Debug, Default)] pub struct TestMetricsCollector { latency_measurements: RwLock>>, throughput_measurements: RwLock>>, error_counts: RwLock>, custom_metrics: RwLock>, } impl TestMetricsCollector { pub fn new() -> Self { Self::default() } pub async fn record_latency(&self, operation: &str, latency_ns: u64) { let mut latencies = self.latency_measurements.write().await; latencies.entry(operation.to_string()).or_insert_with(Vec::new).push(latency_ns); } pub async fn record_throughput(&self, operation: &str, ops_per_sec: f64) { let mut throughputs = self.throughput_measurements.write().await; throughputs.entry(operation.to_string()).or_insert_with(Vec::new).push(ops_per_sec); } pub async fn record_error(&self, operation: &str) { let mut errors = self.error_counts.write().await; *errors.entry(operation.to_string()).or_insert(0) += 1; } pub async fn record_custom_metric(&self, name: &str, value: serde_json::Value) { let mut metrics = self.custom_metrics.write().await; metrics.insert(name.to_string(), value); } pub async fn get_latency_stats(&self, operation: &str) -> Option { let latencies = self.latency_measurements.read().await; if let Some(measurements) = latencies.get(operation) { if measurements.is_empty() { return None; } let mut sorted = measurements.clone(); sorted.sort_unstable(); let len = sorted.len(); let avg = sorted.iter().sum::() / len as u64; let p50 = sorted[len * 50 / 100]; let p95 = sorted[len * 95 / 100]; let p99 = sorted[len * 99 / 100]; let max = sorted[len - 1]; Some(LatencyStats { avg, p50, p95, p99, max }) } else { None } } pub async fn get_summary(&self) -> serde_json::Value { let latencies = self.latency_measurements.read().await; let throughputs = self.throughput_measurements.read().await; let errors = self.error_counts.read().await; let custom = self.custom_metrics.read().await; let mut latency_summary = serde_json::Map::new(); for (operation, measurements) in latencies.iter() { if let Some(stats) = self.get_latency_stats(operation).await { latency_summary.insert(operation.clone(), json!({ "count": measurements.len(), "avg_ns": stats.avg, "p50_ns": stats.p50, "p95_ns": stats.p95, "p99_ns": stats.p99, "max_ns": stats.max })); } } let mut throughput_summary = serde_json::Map::new(); for (operation, measurements) in throughputs.iter() { if !measurements.is_empty() { let avg = measurements.iter().sum::() / measurements.len() as f64; let max = measurements.iter().fold(0.0f64, |a, &b| a.max(b)); let min = measurements.iter().fold(f64::INFINITY, |a, &b| a.min(b)); throughput_summary.insert(operation.clone(), json!({ "count": measurements.len(), "avg_ops_per_sec": avg, "max_ops_per_sec": max, "min_ops_per_sec": min })); } } json!({ "latencies": latency_summary, "throughput": throughput_summary, "errors": errors.clone(), "custom_metrics": custom.clone() }) } } /// Latency statistics structure #[derive(Debug, Clone)] pub struct LatencyStats { pub avg: u64, pub p50: u64, pub p95: u64, pub p99: u64, pub max: u64, } /// Test environment setup and cleanup pub struct TestEnvironment { pub config: IntegrationTestConfig, pub metrics: Arc, pub port_manager: Arc, pub event_publisher: Arc, cleanup_tasks: Vec std::pin::Pin + Send>> + Send + Sync>>, } impl std::fmt::Debug for TestEnvironment { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("TestEnvironment") .field("config", &self.config) .field("metrics", &self.metrics) .field("port_manager", &self.port_manager) .field("event_publisher", &self.event_publisher) .field("cleanup_tasks", &format!("<{} cleanup tasks>", self.cleanup_tasks.len())) .finish() } } impl TestEnvironment { pub async fn new(config: IntegrationTestConfig) -> TliResult { let metrics = Arc::new(TestMetricsCollector::new()); let port_manager = Arc::new(TestPortManager::new()); let event_publisher = Arc::new(TestEventPublisher::new().await?); Ok(Self { config, metrics, port_manager, event_publisher, cleanup_tasks: Vec::new(), }) } pub fn add_cleanup_task(&mut self, task: F) where F: Fn() -> Fut + Send + Sync + 'static, Fut: std::future::Future + Send + 'static, { let boxed_task = Box::new(move || Box::pin(task()) as std::pin::Pin + Send>>); self.cleanup_tasks.push(boxed_task); } pub async fn cleanup(self) { for task in self.cleanup_tasks { task().await; } } } // ============================================================================= // UTILITY FUNCTIONS // ============================================================================= /// Get test symbol for specific asset class by index pub fn get_test_symbol_by_class(asset_class: AssetClass, index: usize) -> Option<&'static str> { match asset_class { AssetClass::Equities => ALL_TEST_EQUITIES.get(index).copied(), AssetClass::Currencies => ALL_TEST_FX_PAIRS.get(index).copied(), AssetClass::Derivatives => ALL_TEST_FUTURES.get(index).copied(), AssetClass::FixedIncome => ALL_TEST_BONDS.get(index).copied(), AssetClass::Commodities => ALL_TEST_COMMODITIES.get(index).copied(), AssetClass::Alternatives => ALL_TEST_CRYPTOS.get(index).copied(), AssetClass::Cash => Some("TEST_CASH_USD"), } } /// Get all symbols for a specific asset class pub fn get_symbols_for_asset_class(asset_class: AssetClass) -> &'static [&'static str] { match asset_class { AssetClass::Equities => ALL_TEST_EQUITIES, AssetClass::Currencies => ALL_TEST_FX_PAIRS, AssetClass::Derivatives => ALL_TEST_FUTURES, AssetClass::FixedIncome => ALL_TEST_BONDS, AssetClass::Commodities => ALL_TEST_COMMODITIES, AssetClass::Alternatives => ALL_TEST_CRYPTOS, AssetClass::Cash => &["TEST_CASH_USD"], } } /// Generate test price for symbol based on asset class pub fn get_test_price_for_symbol(symbol: &str) -> f64 { if symbol.starts_with("TEST_EQ_") { TEST_PRICE_EQUITY_BASE } else if symbol.starts_with("TEST_FX_") { TEST_PRICE_FX_BASE } else if symbol.starts_with("TEST_FUT_") { TEST_PRICE_FUTURES_BASE } else if symbol.starts_with("TEST_BOND_") { TEST_PRICE_BOND_BASE } else if symbol.starts_with("TEST_COMM_") { TEST_PRICE_COMMODITY_BASE } else if symbol.starts_with("TEST_CRYPTO_") { TEST_PRICE_CRYPTO_BASE } else { 100.0 // Default price } } /// Create test symbol metadata pub fn create_test_symbol_metadata(symbol: &str) -> serde_json::Value { json!({ "symbol": symbol, "test_data": true, "created_at": Utc::now(), "fixture_version": "1.0" }) } #[cfg(test)] mod tests { use super::*; #[test] fn test_symbol_generation() { // Test predefined symbols assert_eq!(TEST_EQUITY_1, "TEST_EQ_001"); assert_eq!(TEST_FOREX_1, "TEST_FX_EURUSD"); assert_eq!(TEST_FUTURE_1, "TEST_FUT_ES001"); // Test dynamic generation let equity_symbol = generate_test_symbol(AssetClass::Equities); assert!(equity_symbol.starts_with("TEST_EQ_")); let fx_symbol = generate_test_symbol(AssetClass::Currencies); assert!(fx_symbol.starts_with("TEST_FX_")); } #[test] fn test_symbol_collections() { assert!(!ALL_TEST_SYMBOLS.is_empty()); assert!(ALL_TEST_SYMBOLS.contains(&TEST_EQUITY_1)); assert!(ALL_TEST_SYMBOLS.contains(&TEST_FOREX_1)); // Test asset class specific collections assert!(!ALL_TEST_EQUITIES.is_empty()); assert!(!ALL_TEST_FX_PAIRS.is_empty()); assert!(!ALL_TEST_FUTURES.is_empty()); } #[test] fn test_price_generation() { assert_eq!(get_test_price_for_symbol(TEST_EQUITY_1), TEST_PRICE_EQUITY_BASE); assert_eq!(get_test_price_for_symbol(TEST_FOREX_1), TEST_PRICE_FX_BASE); assert_eq!(get_test_price_for_symbol(TEST_FUTURE_1), TEST_PRICE_FUTURES_BASE); } #[test] fn test_symbol_metadata() { let metadata = create_test_symbol_metadata(TEST_EQUITY_1); assert_eq!(metadata["symbol"], TEST_EQUITY_1); assert_eq!(metadata["test_data"], true); } }