#![allow(clippy::unnecessary_to_owned)] // String literals in HTML generation use anyhow::Result; use clap::{Arg, ArgMatches, Command}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::time::Duration; use tokio::fs; use tracing::{error, info}; // Corrode integration structures (to be implemented) #[derive(Debug, Clone)] pub struct CorrodeConfig { pub executable_path: String, pub workspace_path: String, pub timeout_seconds: u64, pub max_parallel_sessions: usize, pub log_level: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TestExecutionRequest { pub test_name: String, pub test_command: String, pub environment: HashMap, pub working_directory: Option, pub timeout_seconds: Option, pub capture_output: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TestExecutionResult { pub test_name: String, pub success: bool, pub execution_time: Duration, pub exit_code: Option, pub stdout: String, pub stderr: String, pub error_message: Option, } pub struct CorrodeTestRunner { } impl CorrodeTestRunner { pub fn new(_config: CorrodeConfig) -> Self { Self {} } pub fn with_default_config() -> Self { Self::new(CorrodeConfig { executable_path: "corrode".to_string(), workspace_path: std::env::current_dir() .unwrap() .to_string_lossy() .to_string(), timeout_seconds: 600, max_parallel_sessions: 4, log_level: "info".to_string(), }) } pub async fn execute_test( &mut self, request: TestExecutionRequest, ) -> Result { // Stub implementation - would integrate with corrode-mcp info!("Executing test: {}", request.test_name); Ok(TestExecutionResult { test_name: request.test_name, success: false, execution_time: Duration::from_secs(0), exit_code: Some(1), stdout: String::new(), stderr: "Test runner not fully implemented".to_string(), error_message: Some("Corrode integration pending".to_string()), }) } pub async fn execute_test_suite( &mut self, requests: Vec, ) -> Result> { let mut results = Vec::new(); for request in requests { results.push(self.execute_test(request).await?); } Ok(results) } pub async fn generate_test_report(&self, results: &[TestExecutionResult]) -> Result { let total = results.len(); let passed = results.iter().filter(|r| r.success).count(); let failed = total - passed; let mut report = "# E2E Test Report\n\n".to_string(); report.push_str(&format!("**Total Tests:** {}\n", total)); report.push_str(&format!("**Passed:** {}\n", passed)); report.push_str(&format!("**Failed:** {}\n\n", failed)); for result in results { let status = if result.success { "PASS" } else { "FAIL" }; report.push_str(&format!( "- {} - {} ({:?})\n", result.test_name, status, result.execution_time )); } Ok(report) } } #[tokio::main] async fn main() -> Result<()> { // Setup logging tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), ) .init(); let matches = build_cli().get_matches(); match matches.subcommand() { Some(("run", sub_matches)) => run_tests(sub_matches).await, Some(("list", _)) => list_available_tests().await, Some(("report", sub_matches)) => generate_report(sub_matches).await, Some(("validate", _)) => validate_environment().await, _ => { eprintln!("Use --help for available commands"); Ok(()) }, } } fn build_cli() -> Command { Command::new("foxhunt-e2e-runner") .version("1.0.0") .about("Foxhunt E2E Test Runner with Corrode-MCP Integration") .subcommand( Command::new("run") .about("Run E2E tests") .arg( Arg::new("test-pattern") .long("test") .short('t') .value_name("PATTERN") .help("Test pattern to run (e.g., 'trading', 'ml', 'all')") .default_value("all"), ) .arg( Arg::new("parallel") .long("parallel") .short('j') .value_name("COUNT") .help("Number of parallel test sessions") .default_value("4"), ) .arg( Arg::new("timeout") .long("timeout") .value_name("SECONDS") .help("Test timeout in seconds") .default_value("600"), ) .arg( Arg::new("output-dir") .long("output-dir") .short('o') .value_name("DIR") .help("Output directory for test results") .default_value("./test-results"), ) .arg( Arg::new("fail-fast") .long("fail-fast") .action(clap::ArgAction::SetTrue) .help("Stop on first test failure"), ) .arg( Arg::new("verbose") .long("verbose") .short('v') .action(clap::ArgAction::SetTrue) .help("Enable verbose output"), ), ) .subcommand(Command::new("list").about("List available E2E tests")) .subcommand( Command::new("report") .about("Generate test report from previous run") .arg( Arg::new("results-dir") .long("results-dir") .short('r') .value_name("DIR") .help("Directory containing test results") .default_value("./test-results"), ) .arg( Arg::new("format") .long("format") .short('f') .value_name("FORMAT") .help("Report format (markdown, json, html)") .default_value("markdown"), ), ) .subcommand(Command::new("validate").about("Validate test environment and dependencies")) } async fn run_tests(matches: &ArgMatches) -> Result<()> { let test_pattern = matches.get_one::("test-pattern").unwrap(); let parallel_count: usize = matches.get_one::("parallel").unwrap().parse()?; let timeout: u64 = matches.get_one::("timeout").unwrap().parse()?; let output_dir = matches.get_one::("output-dir").unwrap(); let fail_fast = matches.get_flag("fail-fast"); let verbose = matches.get_flag("verbose"); info!("Starting E2E test execution"); info!("Test pattern: {}", test_pattern); info!("Parallel sessions: {}", parallel_count); info!("Timeout: {}s", timeout); info!("Output directory: {}", output_dir); // Create output directory fs::create_dir_all(output_dir).await?; // Initialize corrode test runner let config = CorrodeConfig { executable_path: "corrode".to_string(), workspace_path: std::env::current_dir()?.to_string_lossy().to_string(), timeout_seconds: timeout, max_parallel_sessions: parallel_count, log_level: if verbose { "debug".to_string() } else { "info".to_string() }, }; let mut runner = CorrodeTestRunner::new(config); // Generate test execution plan let test_requests = generate_test_plan(test_pattern, timeout)?; info!("Generated {} test requests", test_requests.len()); // Execute tests let start_time = tokio::time::Instant::now(); let mut results = Vec::new(); if fail_fast { // Execute tests sequentially, stopping on first failure for request in test_requests { info!("Executing test: {}", request.test_name); let result = runner.execute_test(request).await?; if verbose { println!( "Test: {} - {}", result.test_name, if result.success { "PASSED" } else { "FAILED" } ); if !result.stdout.is_empty() { println!("STDOUT:\n{}", result.stdout); } if !result.stderr.is_empty() { println!("STDERR:\n{}", result.stderr); } } let success = result.success; results.push(result); if !success { error!("Test failed, stopping execution due to --fail-fast"); break; } } } else { // Execute tests in parallel results = runner.execute_test_suite(test_requests).await?; } let total_duration = start_time.elapsed(); // Generate and save report let report = runner.generate_test_report(&results).await?; let report_path = format!("{}/test_report.md", output_dir); fs::write(&report_path, &report).await?; // Save detailed results as JSON let json_results = serde_json::to_string_pretty(&results)?; let json_path = format!("{}/test_results.json", output_dir); fs::write(&json_path, &json_results).await?; // Print summary let total_tests = results.len(); let passed_tests = results.iter().filter(|r| r.success).count(); let failed_tests = total_tests - passed_tests; println!("\n=== E2E Test Summary ==="); println!("Total Tests: {}", total_tests); println!( "Passed: {} ({}%)", passed_tests, (passed_tests as f64 / total_tests as f64 * 100.0) as i32 ); println!( "Failed: {} ({}%)", failed_tests, (failed_tests as f64 / total_tests as f64 * 100.0) as i32 ); println!("Total Duration: {:?}", total_duration); println!("Report saved to: {}", report_path); println!("Detailed results: {}", json_path); if failed_tests > 0 { println!("\nFailed Tests:"); for result in &results { if !result.success { println!(" - {} ({:?})", result.test_name, result.execution_time); if let Some(error) = &result.error_message { println!(" Error: {}", error); } } } std::process::exit(1); } Ok(()) } async fn list_available_tests() -> Result<()> { println!("Available E2E Test Categories:\n"); println!("šŸ”§ Service Tests:"); println!(" - service_startup: Test all services start and respond to health checks"); println!(" - service_shutdown: Test graceful service shutdown"); println!(" - service_recovery: Test service recovery after failures\n"); println!("šŸ—„ļø Database Tests:"); println!(" - database_integration: Test PostgreSQL integration and queries"); println!(" - database_migrations: Test database schema migrations"); println!(" - database_performance: Test database query performance\n"); println!("šŸ“” gRPC Tests:"); println!(" - grpc_clients: Test all gRPC client connections and authentication"); println!(" - grpc_streaming: Test streaming gRPC calls"); println!(" - grpc_error_handling: Test gRPC error scenarios\n"); println!("šŸ¤– ML Pipeline Tests:"); println!(" - ml_inference: Test ML model inference pipelines"); println!(" - ml_training: Test ML model training workflows"); println!(" - ml_ensemble: Test ensemble prediction workflows\n"); println!("šŸ’¼ Trading Tests:"); println!(" - trading_workflows: Complete trading workflow tests"); println!(" - order_lifecycle: Order submission to execution lifecycle"); println!(" - risk_management: Risk management and safety mechanisms"); println!(" - emergency_stop: Emergency stop and kill switch tests\n"); println!("šŸŽÆ Full Suite:"); println!(" - all: Run complete E2E test suite"); println!(" - smoke: Run smoke tests for quick validation"); println!(" - performance: Run performance and load tests\n"); println!("Usage Examples:"); println!(" ./test_runner run --test all # Run all tests"); println!(" ./test_runner run --test trading # Run trading tests only"); println!(" ./test_runner run --test ml --parallel 2 # Run ML tests with 2 parallel sessions"); println!( " ./test_runner run --test smoke --fail-fast # Run smoke tests, stop on first failure" ); Ok(()) } async fn generate_report(matches: &ArgMatches) -> Result<()> { let results_dir = matches.get_one::("results-dir").unwrap(); let format = matches.get_one::("format").unwrap(); info!("Generating test report from: {}", results_dir); let json_path = format!("{}/test_results.json", results_dir); let json_content = fs::read_to_string(&json_path) .await .map_err(|_| anyhow::anyhow!("Could not read test results from {}", json_path))?; let results: Vec = serde_json::from_str(&json_content)?; let runner = CorrodeTestRunner::with_default_config(); let report = runner.generate_test_report(&results).await?; match format.as_str() { "markdown" | "md" => { let output_path = format!("{}/report.md", results_dir); fs::write(&output_path, &report).await?; println!("Markdown report saved to: {}", output_path); }, "json" => { let json_report = serde_json::to_string_pretty(&results)?; let output_path = format!("{}/report.json", results_dir); fs::write(&output_path, &json_report).await?; println!("JSON report saved to: {}", output_path); }, "html" => { let html_report = generate_html_report(&results).await?; let output_path = format!("{}/report.html", results_dir); fs::write(&output_path, &html_report).await?; println!("HTML report saved to: {}", output_path); }, _ => { return Err(anyhow::anyhow!("Unsupported format: {}", format)); }, } Ok(()) } async fn validate_environment() -> Result<()> { println!("šŸ” Validating E2E Test Environment\n"); // Check corrode executable print!("Checking corrode executable... "); match tokio::process::Command::new("corrode") .arg("--version") .output() .await { Ok(output) => { if output.status.success() { println!("āœ… Found"); let version = String::from_utf8_lossy(&output.stdout); println!(" Version: {}", version.trim()); } else { println!("āŒ Error running corrode"); } }, Err(_) => { println!("āŒ Not found"); println!(" Please install corrode-mcp for test execution"); }, } // Check Rust toolchain print!("Checking Rust toolchain... "); match tokio::process::Command::new("cargo") .arg("--version") .output() .await { Ok(output) => { if output.status.success() { println!("āœ… Found"); let version = String::from_utf8_lossy(&output.stdout); println!(" {}", version.trim()); } else { println!("āŒ Error running cargo"); } }, Err(_) => { println!("āŒ Not found"); }, } // Check PostgreSQL print!("Checking PostgreSQL... "); match tokio::process::Command::new("psql") .arg("--version") .output() .await { Ok(output) => { if output.status.success() { println!("āœ… Found"); let version = String::from_utf8_lossy(&output.stdout); println!(" {}", version.trim()); } else { println!("āŒ Error running psql"); } }, Err(_) => { println!("āŒ Not found"); println!(" PostgreSQL is required for database tests"); }, } // Check environment variables println!("\nEnvironment Variables:"); let required_vars = ["DATABASE_URL", "RUST_LOG"]; let optional_vars = ["CUDA_VISIBLE_DEVICES", "TORCH_DEVICE"]; for var in required_vars { match std::env::var(var) { Ok(value) => println!(" āœ… {}: {}", var, value), Err(_) => println!(" āŒ {} (required)", var), } } for var in optional_vars { match std::env::var(var) { Ok(value) => println!(" āœ… {}: {}", var, value), Err(_) => println!(" āž– {} (optional)", var), } } // Check test compilation print!("\nChecking E2E test compilation... "); match tokio::process::Command::new("cargo") .args(["check", "--package", "e2e_tests"]) .output() .await { Ok(output) => { if output.status.success() { println!("āœ… Compiles successfully"); } else { println!("āŒ Compilation errors"); let stderr = String::from_utf8_lossy(&output.stderr); println!("Errors:\n{}", stderr); } }, Err(e) => { println!("āŒ Error checking compilation: {}", e); }, } println!("\nšŸŽÆ Environment validation complete!"); println!("Run './test_runner run --test smoke' to perform a quick smoke test."); Ok(()) } fn generate_test_plan(pattern: &str, timeout: u64) -> Result> { let mut requests = Vec::new(); let base_env = HashMap::from([ ("RUST_BACKTRACE".to_string(), "1".to_string()), ("FOXHUNT_TEST_MODE".to_string(), "true".to_string()), ( "DATABASE_URL".to_string(), "postgresql://localhost/foxhunt_test".to_string(), ), ]); match pattern { "all" => { // Complete test suite requests.extend(generate_service_tests(timeout, &base_env)); requests.extend(generate_database_tests(timeout, &base_env)); requests.extend(generate_grpc_tests(timeout, &base_env)); requests.extend(generate_ml_tests(timeout, &base_env)); requests.extend(generate_trading_tests(timeout, &base_env)); }, "smoke" => { // Quick validation tests requests.push(TestExecutionRequest { test_name: "smoke_service_startup".to_string(), test_command: "cargo test --package e2e_tests test_service_startup --timeout 30" .to_string(), environment: base_env.clone(), working_directory: None, timeout_seconds: Some(60), capture_output: true, }); }, "services" | "service" => { requests.extend(generate_service_tests(timeout, &base_env)); }, "database" | "db" => { requests.extend(generate_database_tests(timeout, &base_env)); }, "grpc" => { requests.extend(generate_grpc_tests(timeout, &base_env)); }, "ml" => { requests.extend(generate_ml_tests(timeout, &base_env)); }, "trading" => { requests.extend(generate_trading_tests(timeout, &base_env)); }, _ => { return Err(anyhow::anyhow!("Unknown test pattern: {}", pattern)); }, } Ok(requests) } fn generate_service_tests( timeout: u64, base_env: &HashMap, ) -> Vec { vec![TestExecutionRequest { test_name: "service_startup".to_string(), test_command: "cargo test --package e2e_tests test_service_startup".to_string(), environment: base_env.clone(), working_directory: None, timeout_seconds: Some(timeout), capture_output: true, }] } fn generate_database_tests( timeout: u64, base_env: &HashMap, ) -> Vec { vec![TestExecutionRequest { test_name: "database_integration".to_string(), test_command: "cargo test --package e2e_tests test_database_integration".to_string(), environment: base_env.clone(), working_directory: None, timeout_seconds: Some(timeout), capture_output: true, }] } fn generate_grpc_tests( timeout: u64, base_env: &HashMap, ) -> Vec { vec![TestExecutionRequest { test_name: "grpc_clients".to_string(), test_command: "cargo test --package e2e_tests test_grpc_clients".to_string(), environment: base_env.clone(), working_directory: None, timeout_seconds: Some(timeout), capture_output: true, }] } fn generate_ml_tests( timeout: u64, base_env: &HashMap, ) -> Vec { vec![TestExecutionRequest { test_name: "ml_pipeline".to_string(), test_command: "cargo test --package e2e_tests test_ml_pipeline".to_string(), environment: base_env.clone(), working_directory: None, timeout_seconds: Some(timeout), capture_output: true, }] } fn generate_trading_tests( timeout: u64, base_env: &HashMap, ) -> Vec { vec![TestExecutionRequest { test_name: "trading_workflows".to_string(), test_command: "cargo test --package e2e_tests test_trading_workflows".to_string(), environment: base_env.clone(), working_directory: None, timeout_seconds: Some(timeout), capture_output: true, }] } async fn generate_html_report(results: &[TestExecutionResult]) -> Result { let total_tests = results.len(); let passed_tests = results.iter().filter(|r| r.success).count(); let failed_tests = total_tests - passed_tests; let mut html = String::new(); html.push_str("\nFoxhunt E2E Test Report"); html.push_str(""); html.push_str(&"

Foxhunt E2E Test Report

".to_string()); html.push_str(&"
".to_string()); html.push_str(&format!( "

Generated: {}

", chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC") )); html.push_str(&format!("

Total Tests: {}

", total_tests)); html.push_str(&format!( "

Passed: {}

", passed_tests )); html.push_str(&format!( "

Failed: {}

", failed_tests )); html.push_str(&"
".to_string()); html.push_str("

Test Results

"); html.push_str(""); for result in results { let status_class = if result.success { "pass" } else { "fail" }; let status_text = if result.success { "PASS" } else { "FAIL" }; let exit_code = result .exit_code .map(|c| c.to_string()) .unwrap_or_else(|| "N/A".to_string()); html.push_str(&format!( "", result.test_name, status_class, status_text, result.execution_time, exit_code )); } html.push_str("
Test NameStatusDurationExit Code
{}{}{:?}{}
"); Ok(html) }