#![allow(dead_code)] #![allow(unused_imports)] #![allow(unused_variables)] use anyhow::Result; use std::env; use std::path::PathBuf; use std::process::ExitCode; use tracing::{error, info, warn, Level}; use tracing_subscriber::FmtSubscriber; mod bot; mod desktop; mod fixtures; mod harness; mod mocks; mod ports; mod services; mod web; pub use harness::{TestConfig, TestContext, TestHarness}; pub use ports::PortAllocator; const CHROMEDRIVER_URL: &str = "https://storage.googleapis.com/chrome-for-testing-public"; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TestSuite { Unit, Integration, E2E, All, } impl std::str::FromStr for TestSuite { type Err = String; fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "unit" => Ok(Self::Unit), "integration" | "int" => Ok(Self::Integration), "e2e" | "end-to-end" => Ok(Self::E2E), "all" => Ok(Self::All), _ => Err(format!("Unknown test suite: {s}")), } } } #[derive(Debug, Clone)] pub struct RunnerConfig { pub suite: TestSuite, pub filter: Option, pub parallel: bool, pub verbose: bool, pub keep_env: bool, pub headed: bool, } impl Default for RunnerConfig { fn default() -> Self { Self { suite: TestSuite::All, filter: None, parallel: true, verbose: false, keep_env: env::var("KEEP_ENV").is_ok(), headed: env::var("HEADED").is_ok(), } } } fn print_usage() { eprintln!( r#" BotTest - Test Runner for General Bots USAGE: bottest [OPTIONS] [SUITE] SUITES: unit Run unit tests only (fast, no external services) integration Run integration tests (starts real services) e2e Run end-to-end browser tests all Run all test suites (default) OPTIONS: -f, --filter Filter tests by name pattern -p, --parallel Run tests in parallel (default) -s, --sequential Run tests sequentially -v, --verbose Enable verbose output -k, --keep-env Keep test environment after completion -h, --headed Run browser tests with visible browser --setup Download and install test dependencies --demo Run a quick browser demo (no database needed) --help Show this help message ENVIRONMENT VARIABLES: KEEP_ENV=1 Keep test environment for inspection HEADED=1 Run browser tests with visible browser DATABASE_URL Override test database URL TEST_THREADS Number of parallel test threads SKIP_E2E_TESTS Skip E2E tests SKIP_INTEGRATION_TESTS Skip integration tests EXAMPLES: bottest unit Run all unit tests bottest integration -f queue Run integration tests matching "queue" bottest e2e --headed Run E2E tests with visible browser bottest all -v Run all tests with verbose output bottest --setup Install ChromeDriver and dependencies bottest --demo Open browser and navigate to example.com "# ); } fn parse_args() -> Result<(RunnerConfig, bool, bool)> { let args: Vec = env::args().collect(); let mut config = RunnerConfig::default(); let mut setup_only = false; let mut demo_mode = false; let mut i = 1; while i < args.len() { match args[i].as_str() { "--help" => { print_usage(); std::process::exit(0); } "--setup" => { setup_only = true; } "--demo" => { demo_mode = true; config.headed = true; } "-f" | "--filter" => { i += 1; if i < args.len() { config.filter = Some(args[i].clone()); } else { anyhow::bail!("--filter requires a pattern argument"); } } "-p" | "--parallel" => { config.parallel = true; } "-s" | "--sequential" => { config.parallel = false; } "-v" | "--verbose" => { config.verbose = true; } "-k" | "--keep-env" => { config.keep_env = true; } "-h" | "--headed" => { config.headed = true; } arg if !arg.starts_with('-') => { config.suite = arg.parse().map_err(|e| anyhow::anyhow!("{e}"))?; } other => { anyhow::bail!("Unknown argument: {other}"); } } i += 1; } Ok((config, setup_only, demo_mode)) } fn setup_logging(verbose: bool) { let level = if verbose { Level::DEBUG } else { Level::INFO }; let subscriber = FmtSubscriber::builder() .with_max_level(level) .with_target(false) .with_thread_ids(false) .with_file(false) .with_line_number(false) .finish(); let _ = tracing::subscriber::set_global_default(subscriber); } #[derive(Debug, Clone)] pub struct TestResults { pub suite: String, pub passed: usize, pub failed: usize, pub skipped: usize, pub duration_ms: u64, pub errors: Vec, } impl TestResults { #[must_use] pub fn new(suite: &str) -> Self { Self { suite: suite.to_string(), passed: 0, failed: 0, skipped: 0, duration_ms: 0, errors: Vec::new(), } } #[must_use] pub const fn success(&self) -> bool { self.failed == 0 && self.errors.is_empty() } } fn get_cache_dir() -> PathBuf { let home = env::var("HOME").unwrap_or_else(|_| ".".to_string()); PathBuf::from(home).join(".cache").join("bottest") } fn get_chromedriver_path(version: &str) -> PathBuf { get_cache_dir().join(format!("chromedriver-{version}")) } fn get_chrome_path() -> PathBuf { get_cache_dir().join("chrome-linux64").join("chrome") } fn detect_existing_browser() -> Option { let browsers = [ "/usr/bin/brave-browser", "/usr/bin/brave", "/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/usr/bin/chromium", "/usr/bin/chromium-browser", ]; for browser in browsers { if std::path::Path::new(browser).exists() { return Some(browser.to_string()); } } let chrome_path = get_chrome_path(); if chrome_path.exists() { return Some(chrome_path.to_string_lossy().to_string()); } None } fn detect_browser_version(browser_path: &str) -> Option { let output = std::process::Command::new(browser_path) .arg("--version") .output() .ok()?; if !output.status.success() { return None; } let version_str = String::from_utf8_lossy(&output.stdout); let parts: Vec<&str> = version_str.split_whitespace().collect(); for part in parts { if part.contains('.') && part.chars().next().is_some_and(|c| c.is_ascii_digit()) { let major = part.split('.').next()?; return Some(major.to_string()); } } None } fn detect_chromedriver_for_version(major_version: &str) -> Option { let pattern = format!("chromedriver-{major_version}"); let cache_dir = get_cache_dir(); if let Ok(entries) = std::fs::read_dir(&cache_dir) { for entry in entries.flatten() { let name = entry.file_name().to_string_lossy().to_string(); if name.starts_with(&pattern) && entry.path().is_file() { return Some(entry.path()); } } } None } async fn download_file(url: &str, dest: &PathBuf) -> Result<()> { info!("Downloading: {}", url); let response = reqwest::get(url).await?; if !response.status().is_success() { anyhow::bail!("Download failed with status: {}", response.status()); } let bytes = response.bytes().await?; if let Some(parent) = dest.parent() { std::fs::create_dir_all(parent)?; } std::fs::write(dest, &bytes)?; info!("Downloaded to: {:?}", dest); Ok(()) } fn extract_zip(zip_path: &PathBuf, dest_dir: &PathBuf) -> Result<()> { info!("Extracting: {:?} to {:?}", zip_path, dest_dir); let file = std::fs::File::open(zip_path)?; let mut archive = zip::ZipArchive::new(file)?; std::fs::create_dir_all(dest_dir)?; for i in 0..archive.len() { let mut file = archive.by_index(i)?; let outpath = match file.enclosed_name() { Some(path) => dest_dir.join(path), None => continue, }; if file.name().ends_with('/') { std::fs::create_dir_all(&outpath)?; } else { if let Some(p) = outpath.parent() { if !p.exists() { std::fs::create_dir_all(p)?; } } let mut outfile = std::fs::File::create(&outpath)?; std::io::copy(&mut file, &mut outfile)?; } #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; if let Some(mode) = file.unix_mode() { std::fs::set_permissions(&outpath, std::fs::Permissions::from_mode(mode))?; } } } Ok(()) } async fn get_chromedriver_version_for_browser(major_version: &str) -> Result { let url = format!( "https://googlechromelabs.github.io/chrome-for-testing/LATEST_RELEASE_{major_version}" ); info!("Fetching ChromeDriver version for Chrome {}", major_version); let response = reqwest::get(&url).await?; if !response.status().is_success() { anyhow::bail!("Failed to get ChromeDriver version: {}", response.status()); } let version = response.text().await?.trim().to_string(); info!("Found ChromeDriver version: {}", version); Ok(version) } async fn setup_chromedriver(browser_path: &str) -> Result { let major_version = detect_browser_version(browser_path).unwrap_or_else(|| "131".to_string()); info!("Detected browser major version: {}", major_version); if let Some(existing) = detect_chromedriver_for_version(&major_version) { info!("Found existing ChromeDriver: {:?}", existing); return Ok(existing); } info!( "ChromeDriver for version {} not found, downloading...", major_version ); let cache_dir = get_cache_dir(); std::fs::create_dir_all(&cache_dir)?; let chrome_version = get_chromedriver_version_for_browser(&major_version).await?; let chromedriver_url = format!("{CHROMEDRIVER_URL}/{chrome_version}/linux64/chromedriver-linux64.zip"); let zip_path = cache_dir.join("chromedriver.zip"); download_file(&chromedriver_url, &zip_path).await?; extract_zip(&zip_path, &cache_dir)?; let extracted_driver = cache_dir.join("chromedriver-linux64").join("chromedriver"); let final_path = get_chromedriver_path(&major_version); if extracted_driver.exists() { std::fs::rename(&extracted_driver, &final_path)?; #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; std::fs::set_permissions(&final_path, std::fs::Permissions::from_mode(0o755))?; } } std::fs::remove_file(&zip_path).ok(); std::fs::remove_dir_all(cache_dir.join("chromedriver-linux64")).ok(); if final_path.exists() { info!( "ChromeDriver {} installed: {:?}", chrome_version, final_path ); Ok(final_path) } else { anyhow::bail!("Failed to install ChromeDriver"); } } async fn setup_chrome_for_testing() -> Result { if let Some(browser) = detect_existing_browser() { info!("Found existing browser: {}", browser); return Ok(PathBuf::from(browser)); } info!("No compatible browser found, downloading Chrome for Testing..."); let cache_dir = get_cache_dir(); std::fs::create_dir_all(&cache_dir)?; let chrome_version = get_chromedriver_version_for_browser("131") .await .unwrap_or_else(|_| "131.0.6778.204".to_string()); let chrome_url = format!("{CHROMEDRIVER_URL}/{chrome_version}/linux64/chrome-linux64.zip"); let zip_path = cache_dir.join("chrome.zip"); download_file(&chrome_url, &zip_path).await?; extract_zip(&zip_path, &cache_dir)?; std::fs::remove_file(&zip_path).ok(); let chrome_path = get_chrome_path(); if chrome_path.exists() { #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; std::fs::set_permissions(&chrome_path, std::fs::Permissions::from_mode(0o755))?; } info!("Chrome installed: {:?}", chrome_path); Ok(chrome_path) } else { anyhow::bail!("Failed to install Chrome for Testing"); } } async fn setup_test_dependencies() -> Result<(PathBuf, PathBuf)> { info!("Setting up test dependencies..."); let chrome = setup_chrome_for_testing().await?; let chrome_str = chrome.to_string_lossy().to_string(); let chromedriver = setup_chromedriver(&chrome_str).await?; info!("Dependencies ready:"); info!(" ChromeDriver: {:?}", chromedriver); info!(" Browser: {:?}", chrome); Ok((chromedriver, chrome)) } async fn start_chromedriver(chromedriver_path: &PathBuf, port: u16) -> Result { info!("Starting ChromeDriver on port {}...", port); let child = std::process::Command::new(chromedriver_path) .arg(format!("--port={port}")) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .spawn()?; for _ in 0..30 { tokio::time::sleep(std::time::Duration::from_millis(100)).await; if check_webdriver_available(port).await { info!("ChromeDriver started successfully"); return Ok(child); } } anyhow::bail!("ChromeDriver failed to start"); } async fn check_webdriver_available(port: u16) -> bool { let url = format!("http://localhost:{port}/status"); let Ok(client) = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(2)) .build() else { return false; }; client.get(&url).send().await.is_ok() } async fn run_browser_demo() -> Result<()> { info!("Running browser demo..."); let debug_port = 9222u16; let mut browser_service = match services::BrowserService::start(debug_port).await { Ok(bs) => bs, Err(e) => { anyhow::bail!("Failed to start browser: {e}"); } }; info!("Browser started on CDP port {}", debug_port); let config = web::BrowserConfig::default() .with_browser(web::BrowserType::Chrome) .with_debug_port(debug_port) .headless(false) .with_timeout(std::time::Duration::from_secs(30)); let browser = match web::Browser::new(config).await { Ok(b) => b, Err(e) => { let _ = browser_service.stop().await; anyhow::bail!("Failed to connect to browser CDP: {e}"); } }; info!("Browser CDP connection established!"); info!("Navigating to example.com..."); browser.goto("https://example.com").await?; info!("Waiting 5 seconds so you can see the browser..."); tokio::time::sleep(std::time::Duration::from_secs(5)).await; info!("Navigating to Google..."); browser.goto("https://www.google.com").await?; info!("Waiting 5 seconds..."); tokio::time::sleep(std::time::Duration::from_secs(5)).await; info!("Closing browser..."); let _ = browser.close(); let _ = browser_service.stop().await; info!("Demo complete!"); Ok(()) } fn discover_test_files(test_dir: &str) -> Vec { let path = std::path::PathBuf::from(test_dir); if !path.exists() { return Vec::new(); } let mut test_files = Vec::new(); if let Ok(entries) = std::fs::read_dir(&path) { for entry in entries.flatten() { let file_path = entry.path(); if file_path.extension().is_some_and(|e| e == "rs") { if let Some(name) = file_path.file_stem() { let name_str = name.to_string_lossy().to_string(); if name_str != "mod" { test_files.push(name_str); } } } } } test_files.sort(); test_files } fn run_cargo_test( test_type: &str, filter: Option<&str>, parallel: bool, env_vars: Vec<(&str, &str)>, features: Option<&str>, ) -> Result<(usize, usize, usize)> { let mut cmd = std::process::Command::new("cargo"); cmd.arg("test"); cmd.arg("-p").arg("bottest"); if let Some(feat) = features { cmd.arg("--features").arg(feat); } cmd.arg("--test").arg(test_type); if let Some(pattern) = filter { cmd.arg(pattern); } cmd.arg("--"); if !parallel { cmd.arg("--test-threads=1"); } cmd.arg("--nocapture"); for (key, value) in env_vars { cmd.env(key, value); } let output = cmd.output()?; let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); let combined = format!("{stdout}\n{stderr}"); let mut passed = 0usize; let mut failed = 0usize; let mut skipped = 0usize; for line in combined.lines() { if line.contains("test result:") { let parts: Vec<&str> = line.split_whitespace().collect(); for (i, part) in parts.iter().enumerate() { if *part == "passed;" && i > 0 { passed = parts[i - 1].parse().unwrap_or(0); } if *part == "failed;" && i > 0 { failed = parts[i - 1].parse().unwrap_or(0); } if *part == "ignored;" && i > 0 { skipped = parts[i - 1].parse().unwrap_or(0); } } } } Ok((passed, failed, skipped)) } fn run_unit_tests(config: &RunnerConfig) -> Result { info!("Running unit tests..."); let mut results = TestResults::new("unit"); let start = std::time::Instant::now(); let test_files = discover_test_files("tests/unit"); if test_files.is_empty() { info!("No unit test files found in tests/unit/"); results.skipped = 1; return Ok(results); } info!("Discovered unit test modules: {:?}", test_files); let filter = config.filter.as_deref(); let env_vars: Vec<(&str, &str)> = vec![]; match run_cargo_test("unit", filter, config.parallel, env_vars, None) { Ok((passed, failed, skipped)) => { results.passed = passed; results.failed = failed; results.skipped = skipped; } Err(e) => { results .errors .push(format!("Failed to run unit tests: {e}")); results.failed = 1; } } results.duration_ms = start.elapsed().as_millis() as u64; info!( "Unit tests completed: {} passed, {} failed, {} skipped ({} ms)", results.passed, results.failed, results.skipped, results.duration_ms ); Ok(results) } async fn run_integration_tests(config: &RunnerConfig) -> Result { info!("Running integration tests..."); let mut results = TestResults::new("integration"); let start = std::time::Instant::now(); if env::var("SKIP_INTEGRATION_TESTS").is_ok() { info!("Integration tests skipped (SKIP_INTEGRATION_TESTS is set)"); results.skipped = 1; return Ok(results); } let test_config = TestConfig::full(); let ctx = match TestHarness::setup(test_config).await { Ok(c) => c, Err(e) => { error!("Failed to set up test harness: {}", e); results.failed = 1; results.errors.push(format!("Harness setup failed: {e}")); return Ok(results); } }; info!("Test harness ready:"); info!(" PostgreSQL: {}", ctx.database_url()); info!(" MinIO: {}", ctx.minio_endpoint()); info!(" Redis: {}", ctx.redis_url()); info!(" Mock Zitadel: {}", ctx.zitadel_url()); info!(" Mock LLM: {}", ctx.llm_url()); let test_files = discover_test_files("tests/integration"); if test_files.is_empty() { info!("No integration test files found in tests/integration/"); results.skipped = 1; return Ok(results); } info!("Discovered integration test modules: {:?}", test_files); let filter = config.filter.as_deref(); let db_url = ctx.database_url(); let directory_url = ctx.zitadel_url(); let env_vars: Vec<(&str, &str)> = vec![ ("DATABASE_URL", &db_url), ("DIRECTORY_URL", &directory_url), ("ZITADEL_CLIENT_ID", "test-client-id"), ("ZITADEL_CLIENT_SECRET", "test-client-secret"), ("DRIVE_ACCESSKEY", "minioadmin"), ("DRIVE_SECRET", "minioadmin"), ]; match run_cargo_test( "integration", filter, config.parallel, env_vars, Some("integration"), ) { Ok((passed, failed, skipped)) => { results.passed = passed; results.failed = failed; results.skipped = skipped; } Err(e) => { results .errors .push(format!("Failed to run integration tests: {e}")); results.failed = 1; } } if config.keep_env { info!("Keeping test environment for inspection (KEEP_ENV=1)"); info!(" Data dir: {:?}", ctx.data_dir); } else { info!("Cleaning up test environment..."); } results.duration_ms = start.elapsed().as_millis() as u64; info!( "Integration tests completed: {} passed, {} failed, {} skipped ({} ms)", results.passed, results.failed, results.skipped, results.duration_ms ); Ok(results) } async fn run_e2e_tests(config: &RunnerConfig) -> Result { info!("Running E2E tests..."); let mut results = TestResults::new("e2e"); let start = std::time::Instant::now(); if env::var("SKIP_E2E_TESTS").is_ok() { info!("E2E tests skipped (SKIP_E2E_TESTS is set)"); results.skipped = 1; return Ok(results); } if config.headed { info!("Running with visible browser (HEADED mode)"); } else { info!("Running headless"); } let (chromedriver_path, chrome_path) = match setup_test_dependencies().await { Ok(deps) => deps, Err(e) => { warn!("Failed to setup dependencies: {}", e); let browser = detect_existing_browser(); if browser.is_none() { info!("No WebDriver available, skipping E2E tests"); info!("Run 'bottest --setup' to install dependencies"); results.skipped = 1; return Ok(results); } let browser_path = browser.unwrap(); let major = detect_browser_version(&browser_path).unwrap_or_else(|| "131".to_string()); if let Some(driver) = detect_chromedriver_for_version(&major) { (driver, PathBuf::from(browser_path)) } else { info!("No matching ChromeDriver, skipping E2E tests"); results.skipped = 1; return Ok(results); } } }; let webdriver_port = 4444u16; let mut chromedriver_process = None; if !check_webdriver_available(webdriver_port).await { match start_chromedriver(&chromedriver_path, webdriver_port).await { Ok(child) => { chromedriver_process = Some(child); } Err(e) => { error!("Failed to start ChromeDriver: {}", e); results.failed = 1; results .errors .push(format!("ChromeDriver start failed: {e}")); return Ok(results); } } } let test_config = TestConfig::full(); let ctx = match TestHarness::setup(test_config).await { Ok(c) => c, Err(e) => { error!("Failed to set up test harness: {}", e); if let Some(mut child) = chromedriver_process { let _ = child.kill(); } results.failed = 1; results.errors.push(format!("Harness setup failed: {e}")); return Ok(results); } }; info!("Test harness ready for E2E tests"); let server = match ctx.start_botserver().await { Ok(s) => s, Err(e) => { error!("Failed to start botserver: {}", e); if let Some(mut child) = chromedriver_process { let _ = child.kill(); } results.failed = 1; results.errors.push(format!("Botserver start failed: {e}")); return Ok(results); } }; if server.is_running() { info!("Botserver started at: {}", server.url); } else { info!("Botserver not running, E2E tests may fail"); } let test_files = discover_test_files("tests/e2e"); if test_files.is_empty() { info!("No E2E test files found in tests/e2e/"); if let Some(mut child) = chromedriver_process { let _ = child.kill(); } results.skipped = 1; return Ok(results); } info!("Discovered E2E test modules: {:?}", test_files); let filter = config.filter.as_deref(); let headed = if config.headed { "1" } else { "" }; let db_url = ctx.database_url(); let directory_url = ctx.zitadel_url(); let server_url = server.url.clone(); let chrome_binary = chrome_path.to_string_lossy().to_string(); let webdriver_url = format!("http://localhost:{webdriver_port}"); let env_vars: Vec<(&str, &str)> = vec![ ("DATABASE_URL", &db_url), ("DIRECTORY_URL", &directory_url), ("ZITADEL_CLIENT_ID", "test-client-id"), ("ZITADEL_CLIENT_SECRET", "test-client-secret"), ("DRIVE_ACCESSKEY", "minioadmin"), ("DRIVE_SECRET", "minioadmin"), ("BOTSERVER_URL", &server_url), ("HEADED", headed), ("CHROME_BINARY", &chrome_binary), ("WEBDRIVER_URL", &webdriver_url), ]; match run_cargo_test("e2e", filter, false, env_vars, Some("e2e")) { Ok((passed, failed, skipped)) => { results.passed = passed; results.failed = failed; results.skipped = skipped; } Err(e) => { results.errors.push(format!("Failed to run E2E tests: {e}")); results.failed = 1; } } if let Some(mut child) = chromedriver_process { info!("Stopping ChromeDriver..."); let _ = child.kill(); let _ = child.wait(); } if config.keep_env { info!("Keeping test environment for inspection (KEEP_ENV=1)"); info!(" Server URL: {}", server.url); info!(" Data dir: {:?}", ctx.data_dir); } else { info!("Cleaning up test environment..."); } results.duration_ms = start.elapsed().as_millis() as u64; info!( "E2E tests completed: {} passed, {} failed, {} skipped ({} ms)", results.passed, results.failed, results.skipped, results.duration_ms ); Ok(results) } fn print_summary(results: &[TestResults]) { println!("\n{}", "=".repeat(60)); println!("TEST SUMMARY"); println!("{}", "=".repeat(60)); let mut total_passed = 0; let mut total_failed = 0; let mut total_skipped = 0; let mut total_duration = 0; for result in results { println!( "\n{} tests: {} passed, {} failed, {} skipped ({} ms)", result.suite, result.passed, result.failed, result.skipped, result.duration_ms ); for error in &result.errors { println!(" ERROR: {error}"); } total_passed += result.passed; total_failed += result.failed; total_skipped += result.skipped; total_duration += result.duration_ms; } println!("\n{}", "-".repeat(60)); println!( "TOTAL: {total_passed} passed, {total_failed} failed, {total_skipped} skipped ({total_duration} ms)" ); println!("{}", "=".repeat(60)); if total_failed > 0 { println!("\n❌ TESTS FAILED"); } else { println!("\n✅ ALL TESTS PASSED"); } } #[tokio::main] async fn main() -> ExitCode { let (config, setup_only, demo_mode) = match parse_args() { Ok(c) => c, Err(e) => { eprintln!("Error: {e}"); print_usage(); return ExitCode::from(1); } }; setup_logging(config.verbose); info!( "BotTest - General Bots Test Suite v{}", env!("CARGO_PKG_VERSION") ); if setup_only { info!("Setting up test dependencies..."); match setup_test_dependencies().await { Ok((chromedriver, chrome)) => { println!("\n✅ Dependencies installed successfully!"); println!(" ChromeDriver: {}", chromedriver.display()); println!(" Browser: {}", chrome.display()); return ExitCode::SUCCESS; } Err(e) => { eprintln!("\n❌ Setup failed: {e}"); return ExitCode::from(1); } } } if demo_mode { info!("Running browser demo..."); match run_browser_demo().await { Ok(()) => { println!("\n✅ Browser demo completed successfully!"); return ExitCode::SUCCESS; } Err(e) => { eprintln!("\n❌ Browser demo failed: {e}"); return ExitCode::from(1); } } } info!("Running {:?} tests", config.suite); let start = std::time::Instant::now(); let mut all_results = Vec::new(); let result = match config.suite { TestSuite::Unit => run_unit_tests(&config), TestSuite::Integration => run_integration_tests(&config).await, TestSuite::E2E => run_e2e_tests(&config).await, TestSuite::All => { let unit = run_unit_tests(&config); let integration = run_integration_tests(&config).await; let e2e = run_e2e_tests(&config).await; match (unit, integration, e2e) { (Ok(u), Ok(i), Ok(e)) => { all_results.push(u); all_results.push(i); all_results.push(e); Ok(TestResults::new("all")) } (Err(e), _, _) | (_, Err(e), _) | (_, _, Err(e)) => Err(e), } } }; match result { Ok(results) => { if all_results.is_empty() { all_results.push(results); } } Err(e) => { error!("Test execution failed: {}", e); return ExitCode::from(1); } } let total_duration = start.elapsed(); for result in &mut all_results { if result.duration_ms == 0 { result.duration_ms = total_duration.as_millis() as u64; } } print_summary(&all_results); let all_passed = all_results.iter().all(TestResults::success); if all_passed { ExitCode::SUCCESS } else { ExitCode::from(1) } }