Wave 64-65 cleanup: Proto regeneration and build system updates from Tonic 0.12→0.14 upgrade Files updated: - Cargo.lock: Dependency resolution for Tonic 0.14.2 - All build.rs: Updated for tonic-prost-build - Proto files: Regenerated with tonic-prost 0.14 - Examples/tests: Updated for new gRPC API 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
519 lines
17 KiB
Rust
519 lines
17 KiB
Rust
//! Test Configuration for Foxhunt HFT Trading System
|
|
//!
|
|
//! This module provides configuration utilities for test environments,
|
|
//! including database setup, service configuration, and test parameters.
|
|
|
|
use std::collections::HashMap;
|
|
use std::env;
|
|
use serde::{Deserialize, Serialize};
|
|
use rust_decimal::Decimal;
|
|
|
|
/// Test environment configuration
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct TestConfig {
|
|
pub database: DatabaseConfig,
|
|
pub services: ServiceConfig,
|
|
pub performance: PerformanceConfig,
|
|
pub market_data: MarketDataConfig,
|
|
pub risk: RiskConfig,
|
|
}
|
|
|
|
/// Database configuration for tests
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct DatabaseConfig {
|
|
pub url: String,
|
|
pub max_connections: u32,
|
|
pub connection_timeout_ms: u64,
|
|
pub query_timeout_ms: u64,
|
|
pub enable_logging: bool,
|
|
pub auto_migrate: bool,
|
|
pub cleanup_on_drop: bool,
|
|
}
|
|
|
|
/// Service configuration for tests
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ServiceConfig {
|
|
pub trading_service: ServiceEndpoint,
|
|
pub ml_training_service: ServiceEndpoint,
|
|
pub backtesting_service: ServiceEndpoint,
|
|
pub enable_mocks: bool,
|
|
pub mock_latency_ms: u64,
|
|
pub mock_failure_rate: f64,
|
|
}
|
|
|
|
/// Service endpoint configuration
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ServiceEndpoint {
|
|
pub host: String,
|
|
pub port: u16,
|
|
pub use_tls: bool,
|
|
pub timeout_ms: u64,
|
|
pub max_retries: u32,
|
|
}
|
|
|
|
/// Performance testing configuration
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct PerformanceConfig {
|
|
pub max_latency_ns: u64,
|
|
pub min_throughput_ops_per_sec: f64,
|
|
pub test_duration_secs: u64,
|
|
pub warmup_duration_secs: u64,
|
|
pub concurrent_operations: usize,
|
|
pub stress_test_multiplier: f64,
|
|
}
|
|
|
|
/// Market data configuration for tests
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct MarketDataConfig {
|
|
pub default_symbols: Vec<String>,
|
|
pub tick_rate_hz: u32,
|
|
pub price_precision: u32,
|
|
pub volume_range: (u64, u64),
|
|
pub volatility_range: (f64, f64),
|
|
pub enable_realistic_data: bool,
|
|
}
|
|
|
|
/// Risk management configuration for tests
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct RiskConfig {
|
|
pub default_var_limit: Decimal,
|
|
pub default_position_limit: Decimal,
|
|
pub default_concentration_limit: Decimal,
|
|
pub stress_test_scenarios: Vec<String>,
|
|
pub enable_circuit_breakers: bool,
|
|
pub max_drawdown_limit: Decimal,
|
|
}
|
|
|
|
impl Default for TestConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
database: DatabaseConfig::default(),
|
|
services: ServiceConfig::default(),
|
|
performance: PerformanceConfig::default(),
|
|
market_data: MarketDataConfig::default(),
|
|
risk: RiskConfig::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for DatabaseConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
url: env::var("TEST_DATABASE_URL")
|
|
.unwrap_or_else(|_| "postgresql://foxhunt_test:test_password@localhost:5432/foxhunt_test".to_string()),
|
|
max_connections: 10,
|
|
connection_timeout_ms: 5000,
|
|
query_timeout_ms: 30000,
|
|
enable_logging: false, // Disable SQL logging in tests by default
|
|
auto_migrate: true,
|
|
cleanup_on_drop: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for ServiceConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
trading_service: ServiceEndpoint {
|
|
host: "localhost".to_string(),
|
|
port: 8001,
|
|
use_tls: false,
|
|
timeout_ms: 5000,
|
|
max_retries: 3,
|
|
},
|
|
ml_training_service: ServiceEndpoint {
|
|
host: "localhost".to_string(),
|
|
port: 8002,
|
|
use_tls: false,
|
|
timeout_ms: 10000,
|
|
max_retries: 3,
|
|
},
|
|
backtesting_service: ServiceEndpoint {
|
|
host: "localhost".to_string(),
|
|
port: 8003,
|
|
use_tls: false,
|
|
timeout_ms: 30000,
|
|
max_retries: 3,
|
|
},
|
|
enable_mocks: true, // Enable mocks by default for unit tests
|
|
mock_latency_ms: 1,
|
|
mock_failure_rate: 0.0, // No failures by default
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for PerformanceConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
max_latency_ns: 50_000, // 50µs
|
|
min_throughput_ops_per_sec: 1000.0,
|
|
test_duration_secs: 10,
|
|
warmup_duration_secs: 2,
|
|
concurrent_operations: 10,
|
|
stress_test_multiplier: 2.0,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for MarketDataConfig {
|
|
fn default() -> Self {
|
|
use super::{ALL_TEST_SYMBOLS};
|
|
|
|
Self {
|
|
default_symbols: ALL_TEST_SYMBOLS.iter().map(|&s| s.to_string()).collect(),
|
|
tick_rate_hz: 1000, // 1000 ticks per second
|
|
price_precision: 2, // 2 decimal places
|
|
volume_range: (100, 10000),
|
|
volatility_range: (0.01, 0.05), // 1% to 5% daily volatility
|
|
enable_realistic_data: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for RiskConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
default_var_limit: Decimal::from(100000), // $100k
|
|
default_position_limit: Decimal::from(1000000), // $1M
|
|
default_concentration_limit: Decimal::new(25, 2), // 25%
|
|
stress_test_scenarios: vec![
|
|
"market_crash".to_string(),
|
|
"interest_rate_shock".to_string(),
|
|
"liquidity_crisis".to_string(),
|
|
],
|
|
enable_circuit_breakers: true,
|
|
max_drawdown_limit: Decimal::new(20, 2), // 20%
|
|
}
|
|
}
|
|
}
|
|
|
|
impl TestConfig {
|
|
/// Load configuration from environment variables and defaults
|
|
pub fn from_env() -> Self {
|
|
let mut config = Self::default();
|
|
|
|
// Override with environment variables if present
|
|
if let Ok(db_url) = env::var("TEST_DATABASE_URL") {
|
|
config.database.url = db_url;
|
|
}
|
|
|
|
if let Ok(max_connections) = env::var("TEST_DB_MAX_CONNECTIONS") {
|
|
if let Ok(connections) = max_connections.parse() {
|
|
config.database.max_connections = connections;
|
|
}
|
|
}
|
|
|
|
if let Ok(enable_mocks) = env::var("TEST_ENABLE_MOCKS") {
|
|
config.services.enable_mocks = enable_mocks.to_lowercase() == "true";
|
|
}
|
|
|
|
if let Ok(max_latency) = env::var("TEST_MAX_LATENCY_NS") {
|
|
if let Ok(latency) = max_latency.parse() {
|
|
config.performance.max_latency_ns = latency;
|
|
}
|
|
}
|
|
|
|
config
|
|
}
|
|
|
|
/// Create configuration for unit tests (fast, mocked)
|
|
pub fn for_unit_tests() -> Self {
|
|
Self {
|
|
database: DatabaseConfig {
|
|
max_connections: 5,
|
|
connection_timeout_ms: 1000,
|
|
query_timeout_ms: 5000,
|
|
enable_logging: false,
|
|
cleanup_on_drop: true,
|
|
..DatabaseConfig::default()
|
|
},
|
|
services: ServiceConfig {
|
|
enable_mocks: true,
|
|
mock_latency_ms: 0, // No artificial latency for unit tests
|
|
mock_failure_rate: 0.0,
|
|
..ServiceConfig::default()
|
|
},
|
|
performance: PerformanceConfig {
|
|
test_duration_secs: 1,
|
|
warmup_duration_secs: 0,
|
|
concurrent_operations: 1,
|
|
..PerformanceConfig::default()
|
|
},
|
|
market_data: MarketDataConfig {
|
|
tick_rate_hz: 10, // Low tick rate for fast tests
|
|
..MarketDataConfig::default()
|
|
},
|
|
..Self::default()
|
|
}
|
|
}
|
|
|
|
/// Create configuration for integration tests (realistic)
|
|
pub fn for_integration_tests() -> Self {
|
|
Self {
|
|
services: ServiceConfig {
|
|
enable_mocks: false, // Use real services
|
|
..ServiceConfig::default()
|
|
},
|
|
performance: PerformanceConfig {
|
|
test_duration_secs: 30,
|
|
warmup_duration_secs: 5,
|
|
concurrent_operations: 50,
|
|
..PerformanceConfig::default()
|
|
},
|
|
..Self::default()
|
|
}
|
|
}
|
|
|
|
/// Create configuration for performance tests (demanding)
|
|
pub fn for_performance_tests() -> Self {
|
|
Self {
|
|
services: ServiceConfig {
|
|
enable_mocks: false,
|
|
..ServiceConfig::default()
|
|
},
|
|
performance: PerformanceConfig {
|
|
max_latency_ns: 14_000, // 14µs target for HFT
|
|
min_throughput_ops_per_sec: 10000.0,
|
|
test_duration_secs: 60,
|
|
warmup_duration_secs: 10,
|
|
concurrent_operations: 100,
|
|
stress_test_multiplier: 5.0,
|
|
..PerformanceConfig::default()
|
|
},
|
|
market_data: MarketDataConfig {
|
|
tick_rate_hz: 10000, // High frequency data
|
|
..MarketDataConfig::default()
|
|
},
|
|
..Self::default()
|
|
}
|
|
}
|
|
|
|
/// Create configuration for stress tests (extreme)
|
|
pub fn for_stress_tests() -> Self {
|
|
Self {
|
|
performance: PerformanceConfig {
|
|
test_duration_secs: 300, // 5 minutes
|
|
concurrent_operations: 1000,
|
|
stress_test_multiplier: 10.0,
|
|
..PerformanceConfig::default()
|
|
},
|
|
market_data: MarketDataConfig {
|
|
tick_rate_hz: 50000, // Extreme tick rate
|
|
volatility_range: (0.05, 0.20), // Higher volatility
|
|
..MarketDataConfig::default()
|
|
},
|
|
..Self::default()
|
|
}
|
|
}
|
|
|
|
/// Validate configuration values
|
|
pub fn validate(&self) -> Result<(), String> {
|
|
if self.database.max_connections == 0 {
|
|
return Err("Database max_connections must be greater than 0".to_string());
|
|
}
|
|
|
|
if self.performance.max_latency_ns == 0 {
|
|
return Err("Performance max_latency_ns must be greater than 0".to_string());
|
|
}
|
|
|
|
if self.performance.min_throughput_ops_per_sec <= 0.0 {
|
|
return Err("Performance min_throughput_ops_per_sec must be positive".to_string());
|
|
}
|
|
|
|
if self.services.mock_failure_rate < 0.0 || self.services.mock_failure_rate > 1.0 {
|
|
return Err("Services mock_failure_rate must be between 0.0 and 1.0".to_string());
|
|
}
|
|
|
|
if self.market_data.tick_rate_hz == 0 {
|
|
return Err("Market data tick_rate_hz must be greater than 0".to_string());
|
|
}
|
|
|
|
if self.risk.default_var_limit <= Decimal::ZERO {
|
|
return Err("Risk default_var_limit must be positive".to_string());
|
|
}
|
|
|
|
if self.risk.default_concentration_limit <= Decimal::ZERO || self.risk.default_concentration_limit > Decimal::ONE {
|
|
return Err("Risk default_concentration_limit must be between 0 and 1".to_string());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Get database URL with test database suffix if not already present
|
|
pub fn get_test_database_url(&self) -> String {
|
|
let url = &self.database.url;
|
|
if url.contains("_test") {
|
|
url.clone()
|
|
} else {
|
|
// Append _test to database name
|
|
if let Some(last_slash) = url.rfind('/') {
|
|
let (base, db_name) = url.split_at(last_slash + 1);
|
|
format!("{}{}_test", base, db_name)
|
|
} else {
|
|
format!("{}_test", url)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Create environment variables map for child processes
|
|
pub fn to_env_vars(&self) -> HashMap<String, String> {
|
|
let mut env_vars = HashMap::new();
|
|
|
|
env_vars.insert("TEST_DATABASE_URL".to_string(), self.get_test_database_url());
|
|
env_vars.insert("TEST_DB_MAX_CONNECTIONS".to_string(), self.database.max_connections.to_string());
|
|
env_vars.insert("TEST_ENABLE_MOCKS".to_string(), self.services.enable_mocks.to_string());
|
|
env_vars.insert("TEST_MAX_LATENCY_NS".to_string(), self.performance.max_latency_ns.to_string());
|
|
env_vars.insert("TEST_MIN_THROUGHPUT".to_string(), self.performance.min_throughput_ops_per_sec.to_string());
|
|
env_vars.insert("RUST_LOG".to_string(), if self.database.enable_logging { "debug" } else { "warn" }.to_string());
|
|
|
|
env_vars
|
|
}
|
|
}
|
|
|
|
/// Test configuration builder for fluent configuration
|
|
#[derive(Debug)]
|
|
pub struct TestConfigBuilder {
|
|
config: TestConfig,
|
|
}
|
|
|
|
impl TestConfigBuilder {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
config: TestConfig::default(),
|
|
}
|
|
}
|
|
|
|
pub fn with_database_url(mut self, url: impl Into<String>) -> Self {
|
|
self.config.database.url = url.into();
|
|
self
|
|
}
|
|
|
|
pub fn with_max_connections(mut self, max_connections: u32) -> Self {
|
|
self.config.database.max_connections = max_connections;
|
|
self
|
|
}
|
|
|
|
pub fn with_mocks_enabled(mut self, enabled: bool) -> Self {
|
|
self.config.services.enable_mocks = enabled;
|
|
self
|
|
}
|
|
|
|
pub fn with_max_latency_ns(mut self, latency: u64) -> Self {
|
|
self.config.performance.max_latency_ns = latency;
|
|
self
|
|
}
|
|
|
|
pub fn with_test_duration(mut self, duration_secs: u64) -> Self {
|
|
self.config.performance.test_duration_secs = duration_secs;
|
|
self
|
|
}
|
|
|
|
pub fn with_concurrent_operations(mut self, operations: usize) -> Self {
|
|
self.config.performance.concurrent_operations = operations;
|
|
self
|
|
}
|
|
|
|
pub fn with_var_limit(mut self, limit: Decimal) -> Self {
|
|
self.config.risk.default_var_limit = limit;
|
|
self
|
|
}
|
|
|
|
pub fn build(self) -> Result<TestConfig, String> {
|
|
self.config.validate()?;
|
|
Ok(self.config)
|
|
}
|
|
}
|
|
|
|
impl Default for TestConfigBuilder {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_default_config() {
|
|
let config = TestConfig::default();
|
|
assert!(config.validate().is_ok());
|
|
assert!(config.database.max_connections > 0);
|
|
assert!(config.performance.max_latency_ns > 0);
|
|
assert!(!config.market_data.default_symbols.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_unit_test_config() {
|
|
let config = TestConfig::for_unit_tests();
|
|
assert!(config.validate().is_ok());
|
|
assert!(config.services.enable_mocks);
|
|
assert_eq!(config.services.mock_latency_ms, 0);
|
|
assert_eq!(config.performance.test_duration_secs, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_performance_test_config() {
|
|
let config = TestConfig::for_performance_tests();
|
|
assert!(config.validate().is_ok());
|
|
assert!(!config.services.enable_mocks);
|
|
assert_eq!(config.performance.max_latency_ns, 14_000);
|
|
assert!(config.performance.concurrent_operations >= 100);
|
|
}
|
|
|
|
#[test]
|
|
fn test_config_builder() {
|
|
let config = TestConfigBuilder::new()
|
|
.with_max_connections(20)
|
|
.with_mocks_enabled(false)
|
|
.with_max_latency_ns(10_000)
|
|
.build()
|
|
.unwrap();
|
|
|
|
assert_eq!(config.database.max_connections, 20);
|
|
assert!(!config.services.enable_mocks);
|
|
assert_eq!(config.performance.max_latency_ns, 10_000);
|
|
}
|
|
|
|
#[test]
|
|
fn test_config_validation() {
|
|
let mut config = TestConfig::default();
|
|
|
|
// Test invalid max_connections
|
|
config.database.max_connections = 0;
|
|
assert!(config.validate().is_err());
|
|
|
|
// Test invalid failure rate
|
|
config.database.max_connections = 10;
|
|
config.services.mock_failure_rate = 1.5;
|
|
assert!(config.validate().is_err());
|
|
|
|
// Test valid config
|
|
config.services.mock_failure_rate = 0.1;
|
|
assert!(config.validate().is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_test_database_url() {
|
|
let config = TestConfig::default();
|
|
let test_url = config.get_test_database_url();
|
|
assert!(test_url.contains("_test"));
|
|
|
|
// Test with already test database
|
|
let mut config_with_test = config.clone();
|
|
config_with_test.database.url = "postgresql://user:pass@localhost/db_test".to_string();
|
|
let test_url2 = config_with_test.get_test_database_url();
|
|
assert_eq!(test_url2, config_with_test.database.url);
|
|
}
|
|
|
|
#[test]
|
|
fn test_env_vars_generation() {
|
|
let config = TestConfig::default();
|
|
let env_vars = config.to_env_vars();
|
|
|
|
assert!(env_vars.contains_key("TEST_DATABASE_URL"));
|
|
assert!(env_vars.contains_key("TEST_DB_MAX_CONNECTIONS"));
|
|
assert!(env_vars.contains_key("TEST_ENABLE_MOCKS"));
|
|
assert!(env_vars.contains_key("RUST_LOG"));
|
|
}
|
|
} |