Files
foxhunt/tests/e2e/src/framework.rs
jgrusewski 6093eac7bf 🔧 Tonic 0.14 Upgrade: Auto-generated and build system changes
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>
2025-10-03 07:34:26 +02:00

354 lines
11 KiB
Rust

//! E2E Test Framework Core
//!
//! Provides the main E2ETestFramework struct that orchestrates all testing components
//! including service management, gRPC clients, database connections, and ML pipeline testing.
use anyhow::{Context, Result};
use std::time::Duration;
use tokio::time::sleep;
use tracing::{debug, info, warn};
use crate::proto::backtesting::backtesting_service_client::BacktestingServiceClient;
use crate::proto::config::config_service_client::ConfigServiceClient;
use crate::proto::trading::trading_service_client::TradingServiceClient;
use crate::{
database::TestDatabase, ml_pipeline::MLPipelineTestHarness, performance::PerformanceTracker,
services::ServiceManager,
};
use tonic::transport::Channel;
/// Main E2E Test Framework
///
/// Provides centralized orchestration for all testing components:
/// - Service lifecycle management
/// - gRPC client connections
/// - Database testing harness
/// - ML pipeline testing
/// - Performance monitoring
/// - Test data management
#[derive(Debug)]
pub struct E2ETestFramework {
pub service_manager: ServiceManager,
pub database_harness: TestDatabase,
pub ml_pipeline: MLPipelineTestHarness,
pub performance_tracker: PerformanceTracker,
// gRPC clients (initialized on demand)
pub trading_client: Option<TradingServiceClient<Channel>>,
pub backtesting_client: Option<BacktestingServiceClient<Channel>>,
pub config_client: Option<ConfigServiceClient<Channel>>,
// Framework state
pub services_started: bool,
pub test_session_id: String,
}
impl E2ETestFramework {
/// Create a new E2E test framework instance
///
/// This initializes all components but does not start services.
/// Call `start_services()` to begin service orchestration.
pub async fn new() -> Result<Self> {
info!("🔧 Initializing E2E Test Framework...");
let test_session_id = format!(
"test_session_{}",
chrono::Utc::now().format("%Y%m%d_%H%M%S")
);
debug!("Generated test session ID: {}", test_session_id);
// Initialize database harness
let database_harness = TestDatabase::new("postgresql://localhost/foxhunt_test".to_string());
// Initialize ML pipeline testing
let ml_pipeline = MLPipelineTestHarness::new()
.await
.context("Failed to initialize ML pipeline test harness")?;
// Initialize service manager
let service_manager = ServiceManager::new();
// Initialize performance tracker
let performance_tracker = PerformanceTracker::new(&test_session_id)
.context("Failed to initialize performance tracker")?;
info!("✅ E2E Test Framework initialized successfully");
Ok(Self {
service_manager,
database_harness,
ml_pipeline,
performance_tracker,
trading_client: None,
backtesting_client: None,
config_client: None,
services_started: false,
test_session_id,
})
}
/// Start all services needed for testing
pub async fn start_services(&mut self) -> Result<()> {
if self.services_started {
debug!("Services already started, skipping startup");
return Ok(());
}
info!("🚀 Starting services for E2E testing...");
// Start services in proper order
self.service_manager
.start_all_services()
.await
.context("Failed to start services")?;
// Wait for services to be ready
info!("⏳ Waiting for services to be ready...");
self.wait_for_services_ready()
.await
.context("Services failed to become ready")?;
self.services_started = true;
info!("✅ All services started and ready");
Ok(())
}
/// Stop all services and cleanup
pub async fn stop_services(&mut self) -> Result<()> {
if !self.services_started {
debug!("Services not started, skipping shutdown");
return Ok(());
}
info!("🛑 Stopping services...");
// Close client connections first
self.trading_client = None;
self.backtesting_client = None;
self.config_client = None;
// Stop services
self.service_manager
.stop_all_services()
.await
.context("Failed to stop services")?;
self.services_started = false;
info!("✅ All services stopped");
Ok(())
}
/// Get Trading Service gRPC client
pub async fn get_trading_client(&mut self) -> Result<&mut TradingServiceClient<Channel>> {
if self.trading_client.is_none() {
info!("🔌 Connecting to Trading Service...");
let client = TradingServiceClient::connect("http://[::1]:50051")
.await
.context("Failed to connect to Trading Service")?;
self.trading_client = Some(client);
info!("✅ Connected to Trading Service");
}
Ok(self.trading_client.as_mut().unwrap())
}
/// Get Backtesting Service gRPC client
pub async fn get_backtesting_client(
&mut self,
) -> Result<&mut BacktestingServiceClient<Channel>> {
if self.backtesting_client.is_none() {
info!("🔌 Connecting to Backtesting Service...");
let client = BacktestingServiceClient::connect("http://[::1]:50052")
.await
.context("Failed to connect to Backtesting Service")?;
self.backtesting_client = Some(client);
info!("✅ Connected to Backtesting Service");
}
Ok(self.backtesting_client.as_mut().unwrap())
}
/// Get Configuration Service client
pub async fn get_config_client(&mut self) -> Result<&mut ConfigServiceClient<Channel>> {
if self.config_client.is_none() {
info!("🔌 Connecting to Configuration Service...");
let client = ConfigServiceClient::connect("http://[::1]:50053")
.await
.context("Failed to connect to Configuration Service")?;
self.config_client = Some(client);
info!("✅ Connected to Configuration Service");
}
Ok(self.config_client.as_mut().unwrap())
}
/// Check health status of all services
pub async fn check_services_health(&self) -> Result<ServicesHealthStatus> {
info!("🩺 Checking services health...");
let mut status = ServicesHealthStatus {
trading_service: ServiceHealth::Unknown,
config_service: ServiceHealth::Unknown,
database: ServiceHealth::Unknown,
all_healthy: false,
};
// Check Trading Service
status.trading_service = match self.check_trading_service_health().await {
Ok(_) => ServiceHealth::Healthy,
Err(e) => {
warn!("Trading Service health check failed: {}", e);
ServiceHealth::Unhealthy
},
};
// Note: Backtesting Service health check removed - service not available yet
// Check Database (simplified for now)
status.database = ServiceHealth::Healthy;
// Configuration service is database-backed, so use database status
status.config_service = status.database.clone();
// All services are healthy if no service is unhealthy
status.all_healthy = ![
&status.trading_service,
&status.config_service,
&status.database,
]
.iter()
.any(|s| matches!(s, ServiceHealth::Unhealthy));
if status.all_healthy {
info!("✅ All services are healthy");
} else {
warn!("⚠️ Some services are not healthy: {:#?}", status);
}
Ok(status)
}
/// Wait for all services to be ready with retries
async fn wait_for_services_ready(&self) -> Result<()> {
let max_retries = 30; // 30 attempts
let retry_delay = Duration::from_secs(2); // 2 seconds between attempts
for attempt in 1..=max_retries {
debug!("Health check attempt {}/{}", attempt, max_retries);
let health = self
.check_services_health()
.await
.context("Failed to check services health")?;
if health.all_healthy {
info!("✅ All services are ready after {} attempts", attempt);
return Ok(());
}
if attempt < max_retries {
debug!("Some services not ready, retrying in {:?}...", retry_delay);
sleep(retry_delay).await;
}
}
Err(anyhow::anyhow!(
"Services failed to become ready after {} attempts",
max_retries
))
}
/// Check Trading Service health
async fn check_trading_service_health(&self) -> Result<()> {
// Simple TCP connection check
use tokio::net::TcpStream;
let _stream = TcpStream::connect("127.0.0.1:50051")
.await
.context("Could not connect to Trading Service port 50051")?;
Ok(())
}
// Note: Backtesting Service health check removed - service not available yet
/// Get the test session ID
pub fn get_test_session_id(&self) -> &str {
&self.test_session_id
}
/// Setup test database
pub async fn setup_test_database(&self) -> Result<()> {
self.database_harness.setup().await
}
/// Teardown test database
pub async fn teardown_test_database(&self) -> Result<()> {
self.database_harness.teardown().await
}
}
/// Service health status enumeration
#[derive(Debug, Clone, PartialEq)]
pub enum ServiceHealth {
Healthy,
Unhealthy,
Unknown,
}
/// Overall services health status
#[derive(Debug, Clone)]
pub struct ServicesHealthStatus {
pub trading_service: ServiceHealth,
pub config_service: ServiceHealth,
pub database: ServiceHealth,
pub all_healthy: bool,
}
impl ServicesHealthStatus {
/// Get a summary of health status as a string
pub fn summary(&self) -> String {
let services = [
("Trading", &self.trading_service),
("Config", &self.config_service),
("Database", &self.database),
];
let healthy_count = services
.iter()
.filter(|(_, health)| matches!(health, ServiceHealth::Healthy))
.count();
let total_count = services.len();
format!("{}/{} services healthy", healthy_count, total_count)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_framework_creation() {
let framework = E2ETestFramework::new().await;
assert!(framework.is_ok());
let framework = framework.unwrap();
assert!(!framework.services_started);
assert!(!framework.test_session_id.is_empty());
}
#[test]
fn test_service_health_summary() {
let status = ServicesHealthStatus {
trading_service: ServiceHealth::Healthy,
config_service: ServiceHealth::Unhealthy,
database: ServiceHealth::Healthy,
all_healthy: false,
};
let summary = status.summary();
assert_eq!(summary, "2/3 services healthy");
}
}