Files
foxhunt/testing/e2e/src/utils.rs
jgrusewski 9c3d741a08 refactor: restructure repo — crates/, bin/, testing/ layout
Move 17 library crates into crates/, CLI binary into bin/fxt,
consolidate 10 test crates into testing/, split config crate
from deployment config files.

Root directory reduced from 38+ to ~17 directories.
All Cargo.toml paths and build.rs proto refs updated.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 11:56:00 +01:00

443 lines
13 KiB
Rust

use chrono::{DateTime, Utc};
use common::{OrderSide, OrderType, Price, Quantity, Symbol, TimeInForce};
use rand::{thread_rng, Rng};
use std::collections::HashMap;
use uuid::Uuid;
/// Test market data event structure
///
/// Note: This is a test-specific type, not the proto MarketDataEvent
#[derive(Debug, Clone)]
pub struct MarketDataEvent {
pub id: Uuid,
pub symbol: Symbol,
pub timestamp: DateTime<Utc>,
pub bid: Price,
pub ask: Price,
pub bid_size: Quantity,
pub ask_size: Quantity,
pub last_price: Option<Price>,
pub volume: Option<Quantity>,
}
/// Test utilities for generating market data and orders
pub struct TestDataGenerator {
symbols: Vec<Symbol>,
prices: HashMap<Symbol, Price>,
}
impl Default for TestDataGenerator {
fn default() -> Self {
Self::new()
}
}
impl TestDataGenerator {
pub fn new() -> Self {
let symbols = vec![
Symbol::new("EURUSD".to_string()),
Symbol::new("GBPUSD".to_string()),
Symbol::new("USDJPY".to_string()),
Symbol::new("USDCHF".to_string()),
Symbol::new("AUDUSD".to_string()),
];
let mut prices = HashMap::new();
prices.insert(
Symbol::new("EURUSD".to_string()),
Price::new(1.0520).unwrap(),
); // 1.0520
prices.insert(
Symbol::new("GBPUSD".to_string()),
Price::new(1.2845).unwrap(),
); // 1.2845
prices.insert(
Symbol::new("USDJPY".to_string()),
Price::new(148.55).unwrap(),
); // 148.55
prices.insert(
Symbol::new("USDCHF".to_string()),
Price::new(0.8750).unwrap(),
); // 0.8750
prices.insert(
Symbol::new("AUDUSD".to_string()),
Price::new(0.6750).unwrap(),
); // 0.6750
Self { symbols, prices }
}
pub fn generate_market_data(&mut self, symbol: &Symbol) -> MarketDataEvent {
let mut rng = thread_rng();
let current_price = *self.prices.get(symbol).unwrap();
// Generate small random price movement
let change_pct = rng.gen_range(-0.001..0.001); // ±0.1%
let change = current_price.to_f64() * change_pct;
let new_price = Price::new(current_price.to_f64() + change).unwrap();
self.prices.insert(symbol.clone(), new_price);
MarketDataEvent {
id: Uuid::new_v4(),
symbol: symbol.clone(),
timestamp: Utc::now(),
bid: Price::new(new_price.to_f64() - 0.0002).unwrap(), // 2 pip spread
ask: new_price,
bid_size: Quantity::new(rng.gen_range(100000..1000000) as f64).unwrap(),
ask_size: Quantity::new(rng.gen_range(100000..1000000) as f64).unwrap(),
last_price: Some(new_price),
volume: Some(Quantity::new(rng.gen_range(50000..500000) as f64).unwrap()),
}
}
pub fn generate_order_request(&self, symbol: &Symbol) -> OrderRequest {
let mut rng = thread_rng();
let current_price = self.prices.get(symbol).unwrap();
OrderRequest {
id: Uuid::new_v4(),
symbol: symbol.clone(),
side: if rng.gen_bool(0.5) {
OrderSide::Buy
} else {
OrderSide::Sell
},
quantity: Quantity::new(rng.gen_range(10000..100000) as f64).unwrap(), // 10K to 100K units
order_type: OrderType::Market,
price: Some(*current_price),
time_in_force: TimeInForce::ImmediateOrCancel,
timestamp: Utc::now(),
}
}
pub fn get_random_symbol(&self) -> &Symbol {
let mut rng = thread_rng();
&self.symbols[rng.gen_range(0..self.symbols.len())]
}
pub fn get_all_symbols(&self) -> &[Symbol] {
&self.symbols
}
}
#[derive(Debug, Clone)]
pub struct OrderRequest {
pub id: Uuid,
pub symbol: Symbol,
pub side: OrderSide,
pub quantity: Quantity,
pub order_type: OrderType,
pub price: Option<Price>,
pub time_in_force: TimeInForce,
pub timestamp: DateTime<Utc>,
}
/// Trading event for database persistence testing
#[derive(Debug, Clone)]
pub struct TradingEvent {
pub id: Uuid,
pub event_type: String,
pub symbol: String,
pub timestamp: DateTime<Utc>,
pub data: serde_json::Value,
}
/// Test assertion helpers
pub mod assertions {
use super::*;
use std::time::Duration;
pub fn assert_within_tolerance(actual: f64, expected: f64, tolerance_pct: f64) {
let tolerance = expected * tolerance_pct;
assert!(
(actual - expected).abs() <= tolerance,
"Value {} is not within {}% tolerance of expected {}",
actual,
expected,
tolerance_pct * 100.0
);
}
pub fn assert_latency_under(duration: Duration, max_latency: Duration) {
assert!(
duration <= max_latency,
"Latency {:?} exceeds maximum allowed {:?}",
duration,
max_latency
);
}
pub fn assert_price_reasonable(price: &Price, symbol: &Symbol) {
let value = price.to_f64();
match symbol.as_str() {
"EURUSD" | "GBPUSD" | "AUDUSD" => {
assert!(
value > 0.5 && value < 2.0,
"Price {} unreasonable for {}",
value,
symbol
);
},
"USDJPY" => {
assert!(
value > 100.0 && value < 200.0,
"Price {} unreasonable for {}",
value,
symbol
);
},
"USDCHF" => {
assert!(
value > 0.7 && value < 1.2,
"Price {} unreasonable for {}",
value,
symbol
);
},
_ => {}, // Skip validation for unknown symbols
}
}
}
/// Performance profiling utilities for E2E tests
pub struct PerformanceProfiler {
start_time: std::time::Instant,
checkpoints: Vec<(String, std::time::Duration)>,
}
impl Default for PerformanceProfiler {
fn default() -> Self {
Self::new()
}
}
impl PerformanceProfiler {
pub fn new() -> Self {
Self {
start_time: std::time::Instant::now(),
checkpoints: Vec::new(),
}
}
pub fn checkpoint(&mut self, name: &str) {
let elapsed = self.start_time.elapsed();
self.checkpoints.push((name.to_string(), elapsed));
}
pub fn print_summary(&self) {
println!("\n=== Performance Profile ===");
let mut last_duration = std::time::Duration::ZERO;
for (name, duration) in &self.checkpoints {
let delta = *duration - last_duration;
println!("{}: {:?} (delta: {:?})", name, duration, delta);
last_duration = *duration;
}
println!("Total: {:?}\n", self.start_time.elapsed());
}
}
/// Test utility functions
pub struct TestUtils;
impl TestUtils {
/// Setup test logging
pub fn setup_test_logging() {
let _ = tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.try_init();
}
/// Wait for a condition to be true with timeout
pub async fn wait_for_condition<F, Fut>(
condition: F,
timeout_secs: u64,
check_interval_ms: u64,
) -> anyhow::Result<()>
where
F: Fn() -> Fut,
Fut: std::future::Future<Output = bool>,
{
use tokio::time::{sleep, Duration, Instant};
let start = Instant::now();
let timeout = Duration::from_secs(timeout_secs);
let interval = Duration::from_millis(check_interval_ms);
while start.elapsed() < timeout {
if condition().await {
return Ok(());
}
sleep(interval).await;
}
Err(anyhow::anyhow!(
"Condition not met within {} seconds",
timeout_secs
))
}
/// Check if a service is healthy via HTTP
pub async fn check_service_health(endpoint: &str) -> anyhow::Result<bool> {
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(5))
.build()?;
match client.get(endpoint).send().await {
Ok(response) => Ok(response.status().is_success()),
Err(_) => Ok(false),
}
}
}
/// Stub types for E2E testing - These are lightweight test doubles
/// for performance-critical components that are tested separately
/// Trading operations tracker for latency testing
pub struct TradingOperations {
operations: std::sync::Arc<std::sync::Mutex<Vec<(String, String, std::time::Instant)>>>,
}
impl Default for TradingOperations {
fn default() -> Self {
Self::new()
}
}
impl TradingOperations {
pub fn new() -> Self {
Self {
operations: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
}
}
pub fn record_order_submission(&self, order_id: String, symbol: String) {
let mut ops = self.operations.lock().unwrap();
ops.push((order_id, symbol, std::time::Instant::now()));
}
}
/// SIMD price operations stub for testing (actual implementation in trading_engine)
pub struct SimdPriceOps;
impl SimdPriceOps {
pub fn new() -> anyhow::Result<Self> {
Ok(Self)
}
pub fn vectorized_mean(&self, prices: &[f64]) -> anyhow::Result<f64> {
if prices.is_empty() {
return Err(anyhow::anyhow!("Empty price array"));
}
Ok(prices.iter().sum::<f64>() / prices.len() as f64)
}
}
/// Lock-free ring buffer stub for testing
pub struct LockFreeRingBuffer<T> {
buffer: std::sync::Arc<std::sync::Mutex<Vec<T>>>,
capacity: usize,
}
impl<T: Clone> LockFreeRingBuffer<T> {
pub fn new(capacity: usize) -> Self {
Self {
buffer: std::sync::Arc::new(std::sync::Mutex::new(Vec::with_capacity(capacity))),
capacity,
}
}
pub fn try_push(&self, item: T) -> bool {
let mut buffer = self.buffer.lock().unwrap();
if buffer.len() < self.capacity {
buffer.push(item);
true
} else {
false
}
}
}
/// Small batch processor for testing
pub struct SmallBatchProcessor;
impl Default for SmallBatchProcessor {
fn default() -> Self {
Self::new()
}
}
impl SmallBatchProcessor {
pub fn new() -> Self {
Self
}
pub fn process_batch<T>(&self, orders: Vec<T>) -> anyhow::Result<usize> {
// Simulate minimal processing - generic to accept any order type
Ok(orders.len())
}
}
/// Environment setup utilities
pub mod env {
use std::env;
pub fn setup_test_environment() {
// Set up test-specific environment variables
env::set_var("RUST_LOG", "debug");
env::set_var("DATABASE_URL", get_test_database_url());
env::set_var("TRADING_SERVICE_PORT", "50051");
env::set_var("BACKTESTING_SERVICE_PORT", "50052");
env::set_var("CONFIG_SERVICE_PORT", "50053");
}
pub fn get_test_database_url() -> String {
env::var("TEST_DATABASE_URL")
.unwrap_or_else(|_| "postgresql://localhost/foxhunt_test".to_string())
}
pub fn is_ci_environment() -> bool {
env::var("CI").is_ok() || env::var("GITHUB_ACTIONS").is_ok()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn test_data_generator_creates_valid_market_data() {
let mut generator = TestDataGenerator::new();
let symbol = Symbol::new("EURUSD".to_string());
let market_data = generator.generate_market_data(&symbol);
assert_eq!(market_data.symbol, symbol);
assert!(market_data.bid < market_data.ask);
assertions::assert_price_reasonable(&market_data.bid, &symbol);
assertions::assert_price_reasonable(&market_data.ask, &symbol);
}
#[test]
fn test_order_request_generation() {
let generator = TestDataGenerator::new();
let symbol = Symbol::new("GBPUSD".to_string());
let order = generator.generate_order_request(&symbol);
assert_eq!(order.symbol, symbol);
assert!(order.quantity.to_f64() > 0.0);
if let Some(price) = &order.price {
assertions::assert_price_reasonable(price, &symbol);
}
}
#[test]
fn test_assertion_helpers() {
assertions::assert_within_tolerance(100.0, 99.0, 0.02); // 2% tolerance
assertions::assert_latency_under(Duration::from_millis(5), Duration::from_millis(10));
}
}