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>
713 lines
24 KiB
Rust
713 lines
24 KiB
Rust
#![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<String, String>,
|
||
pub working_directory: Option<String>,
|
||
pub timeout_seconds: Option<u64>,
|
||
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<i32>,
|
||
pub stdout: String,
|
||
pub stderr: String,
|
||
pub error_message: Option<String>,
|
||
}
|
||
|
||
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<TestExecutionResult> {
|
||
// 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<TestExecutionRequest>,
|
||
) -> Result<Vec<TestExecutionResult>> {
|
||
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<String> {
|
||
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::<String>("test-pattern").unwrap();
|
||
let parallel_count: usize = matches.get_one::<String>("parallel").unwrap().parse()?;
|
||
let timeout: u64 = matches.get_one::<String>("timeout").unwrap().parse()?;
|
||
let output_dir = matches.get_one::<String>("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::<String>("results-dir").unwrap();
|
||
let format = matches.get_one::<String>("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<TestExecutionResult> = 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<Vec<TestExecutionRequest>> {
|
||
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<String, String>,
|
||
) -> Vec<TestExecutionRequest> {
|
||
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<String, String>,
|
||
) -> Vec<TestExecutionRequest> {
|
||
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<String, String>,
|
||
) -> Vec<TestExecutionRequest> {
|
||
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<String, String>,
|
||
) -> Vec<TestExecutionRequest> {
|
||
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<String, String>,
|
||
) -> Vec<TestExecutionRequest> {
|
||
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<String> {
|
||
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("<!DOCTYPE html>\n<html><head><title>Foxhunt E2E Test Report</title>");
|
||
html.push_str("<style>body{font-family:Arial,sans-serif;margin:20px;}");
|
||
html.push_str(".pass{color:green;} .fail{color:red;} .summary{background:#f0f0f0;padding:10px;border-radius:5px;}");
|
||
html.push_str("table{border-collapse:collapse;width:100%;} th,td{border:1px solid #ddd;padding:8px;text-align:left;}");
|
||
html.push_str("</style></head><body>");
|
||
|
||
html.push_str(&"<h1>Foxhunt E2E Test Report</h1>".to_string());
|
||
html.push_str(&"<div class='summary'>".to_string());
|
||
html.push_str(&format!(
|
||
"<p>Generated: {}</p>",
|
||
chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
|
||
));
|
||
html.push_str(&format!("<p>Total Tests: {}</p>", total_tests));
|
||
html.push_str(&format!(
|
||
"<p>Passed: <span class='pass'>{}</span></p>",
|
||
passed_tests
|
||
));
|
||
html.push_str(&format!(
|
||
"<p>Failed: <span class='fail'>{}</span></p>",
|
||
failed_tests
|
||
));
|
||
html.push_str(&"</div>".to_string());
|
||
|
||
html.push_str("<h2>Test Results</h2><table>");
|
||
html.push_str("<tr><th>Test Name</th><th>Status</th><th>Duration</th><th>Exit Code</th></tr>");
|
||
|
||
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!(
|
||
"<tr><td>{}</td><td class='{}'>{}</td><td>{:?}</td><td>{}</td></tr>",
|
||
result.test_name, status_class, status_text, result.execution_time, exit_code
|
||
));
|
||
}
|
||
|
||
html.push_str("</table></body></html>");
|
||
|
||
Ok(html)
|
||
}
|