Files
foxhunt/testing/integration/fixtures/mod.rs
jgrusewski db6462ba7a fix(clippy): resolve all clippy warnings across entire workspace (--all-targets)
Systematic fix of 360+ clippy errors across 37+ crates covering lib,
test, bench, and example targets. Key changes:

- Add targeted #[allow(...)] on #[cfg(test)] modules for test-only lints
  (assertions_on_result_states, float_cmp, str_to_string, indexing, etc.)
- Feature-gate broken integration tests behind __<crate>_integration flags
  where public APIs changed (trading-service, backtesting-service, etc.)
- Remove dead [[test]] entries from Cargo.toml files pointing to deleted files
- Fix production code: field_reassign_with_default, manual_range_contains,
  assert!(false) → panic!(), format!("{}") simplification, len() > 0 → !is_empty()
- Delete truly unused code (Order struct, unused methods/fields/variants)
- Convert sqlx::query!() to sqlx::query() for SQLX_OFFLINE compatibility

Result: cargo clippy --workspace --all-targets -- -D warnings = 0 errors, 0 warnings

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 10:18:35 +01:00

932 lines
29 KiB
Rust

//! 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::{
atomic::{AtomicU16, Ordering},
Arc,
};
use std::time::Duration;
use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde_json::json;
use tokio::sync::RwLock;
use uuid::Uuid;
// Import TLI types explicitly (tli crate is available as dependency)
use fxt::error::FxtResult;
// Re-export sub-modules for easy access
pub mod builders;
pub mod helpers;
pub mod mock_services;
pub mod scenarios;
pub mod test_config;
pub mod test_data;
pub mod test_database;
// =============================================================================
// 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<String>,
pub cusip: Option<String>,
pub bloomberg_id: Option<String>,
pub reuters_id: Option<String>,
pub name: String,
pub instrument_type: InstrumentType,
pub asset_class: AssetClass,
pub sector: Option<MarketSector>,
pub currency: String,
pub exchange: Option<String>,
pub tick_size: Option<Decimal>,
pub lot_size: Option<Decimal>,
pub multiplier: Option<Decimal>,
pub maturity_date: Option<DateTime<Utc>>,
pub strike_price: Option<Decimal>,
pub option_type: Option<String>,
pub underlying_symbol: Option<String>,
pub is_active: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub metadata: serde_json::Value,
}
/// Test fixture Portfolio type
#[derive(Debug, Clone)]
pub struct Portfolio {
pub id: String,
pub name: String,
pub description: Option<String>,
pub base_currency: String,
pub portfolio_type: String,
pub inception_date: DateTime<Utc>,
pub manager_id: String,
pub benchmark: Option<String>,
pub risk_budget: Option<Decimal>,
pub var_limit: Option<Decimal>,
pub max_drawdown_limit: Option<Decimal>,
pub is_active: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
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<String>,
pub lei_code: Option<String>,
pub parent_company: Option<String>,
pub is_active: bool,
pub exposure_limit: Option<Decimal>,
pub margin_requirement: Option<Decimal>,
pub netting_agreement: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
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<String> {
(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<Assertion>,
pub errors: Vec<String>,
pub metadata: HashMap<String, serde_json::Value>,
}
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<TestResult>,
pub passed_tests: usize,
pub total_tests: usize,
pub passed: bool,
pub execution_time: Duration,
pub metadata: HashMap<String, serde_json::Value>,
}
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<Vec<u16>>,
}
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();
}
// TestEventPublisher removed — event streaming moved to gRPC server-streaming in api service
/// Performance metrics collector for tests
#[derive(Debug, Default)]
pub struct TestMetricsCollector {
latency_measurements: RwLock<HashMap<String, Vec<u64>>>,
throughput_measurements: RwLock<HashMap<String, Vec<f64>>>,
error_counts: RwLock<HashMap<String, u64>>,
custom_metrics: RwLock<HashMap<String, serde_json::Value>>,
}
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<LatencyStats> {
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::<u64>() / 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.to_string(),
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::<f64>() / 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.to_string(),
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<TestMetricsCollector>,
pub port_manager: Arc<TestPortManager>,
cleanup_tasks: Vec<
Box<
dyn Fn() -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + 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(
"cleanup_tasks",
&format!("<{} cleanup tasks>", self.cleanup_tasks.len()),
)
.finish()
}
}
impl TestEnvironment {
pub async fn new(config: IntegrationTestConfig) -> FxtResult<Self> {
let metrics = Arc::new(TestMetricsCollector::new());
let port_manager = Arc::new(TestPortManager::new());
Ok(Self {
config,
metrics,
port_manager,
cleanup_tasks: Vec::new(),
})
}
pub fn add_cleanup_task<F, Fut>(&mut self, task: F)
where
F: Fn() -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = ()> + Send + 'static,
{
let boxed_task = Box::new(move || {
Box::pin(task()) as std::pin::Pin<Box<dyn std::future::Future<Output = ()> + 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]
#[allow(clippy::const_is_empty)]
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);
}
}