diff --git a/crates/core/src/differential_tests/driver.rs b/crates/core/src/differential_tests/driver.rs index 6d0998c..a2dbac9 100644 --- a/crates/core/src/differential_tests/driver.rs +++ b/crates/core/src/differential_tests/driver.rs @@ -373,7 +373,7 @@ where if !expects_exception { return Err(err).context("Failed to handle the function call execution"); } - tracing::info!("Transaction failed as expected: {err:?}"); + tracing::info!("Transaction failed as expected"); None }, }; diff --git a/crates/ml-test-runner/src/main.rs b/crates/ml-test-runner/src/main.rs index c904258..bb3e35b 100644 --- a/crates/ml-test-runner/src/main.rs +++ b/crates/ml-test-runner/src/main.rs @@ -21,7 +21,7 @@ use std::{ io::{BufRead, BufReader, BufWriter, Write}, path::{Path, PathBuf}, sync::Arc, - time::Instant, + time::{Duration, Instant}, }; use temp_dir::TempDir; use tokio::sync::Mutex; @@ -40,6 +40,10 @@ struct MlTestRunnerArgs { #[arg(long = "cached-passed")] cached_passed: Option, + /// File to store tests that have failed (defaults to .-failed) + #[arg(long = "cached-failed")] + cached_failed: Option, + /// Stop after the first file failure #[arg(long = "bail")] bail: bool, @@ -92,6 +96,40 @@ fn main() -> anyhow::Result<()> { .block_on(run(args)) } +/// Wait for HTTP server to be ready by attempting to connect to the specified port +async fn wait_for_http_server(port: u16) -> anyhow::Result<()> { + const MAX_RETRIES: u32 = 60; + const RETRY_DELAY: Duration = Duration::from_secs(1); + + for attempt in 1..=MAX_RETRIES { + match tokio::net::TcpStream::connect(format!("127.0.0.1:{}", port)).await { + Ok(_) => { + info!("Successfully connected to HTTP server on port {} (attempt {})", port, attempt); + return Ok(()); + }, + Err(e) => { + if attempt == MAX_RETRIES { + anyhow::bail!( + "Failed to connect to HTTP server on port {} after {} attempts: {}", + port, + MAX_RETRIES, + e + ); + } + if attempt % 10 == 0 { + info!( + "Still waiting for HTTP server on port {} (attempt {}/{})", + port, attempt, MAX_RETRIES + ); + } + tokio::time::sleep(RETRY_DELAY).await; + }, + } + } + + unreachable!() +} + async fn run(args: MlTestRunnerArgs) -> anyhow::Result<()> { let start_time = Instant::now(); @@ -109,6 +147,14 @@ async fn run(args: MlTestRunnerArgs) -> anyhow::Result<()> { let cached_passed = Arc::new(Mutex::new(cached_passed)); + // Set up cached-failed file (defaults to .-failed) + let cached_failed_path = args + .cached_failed + .clone() + .unwrap_or_else(|| PathBuf::from(format!(".{:?}-failed", args.platform))); + + let cached_failed = Arc::new(Mutex::new(HashSet::::new())); + // Get the platform based on CLI args let platform: &dyn Platform = match args.platform { PlatformIdentifier::GethEvmSolc => &revive_dt_core::GethEvmSolcPlatform, @@ -147,7 +193,13 @@ async fn run(args: MlTestRunnerArgs) -> anyhow::Result<()> { node } else { - info!("Using existing node"); + info!("Using existing node at port {}", args.rpc_port); + + // Wait for the HTTP server to be ready + info!("Waiting for HTTP server to be ready on port {}...", args.rpc_port); + wait_for_http_server(args.rpc_port).await?; + info!("HTTP server is ready"); + let existing_node: Box = match args.platform { PlatformIdentifier::GethEvmSolc | PlatformIdentifier::LighthouseGethEvmSolc => Box::new( @@ -208,18 +260,27 @@ async fn run(args: MlTestRunnerArgs) -> anyhow::Result<()> { mf }, Err(e) => { - println!("test {} ... {RED}FAILED{COLOUR_RESET}", file_display); - println!(" Error loading metadata: {}", e); - failed_files += 1; - failures.push((file_display.clone(), format!("Error loading metadata: {}", e))); - if args.bail { - break; - } + // Skip files without metadata instead of treating them as failures + info!("Skipping {} (no metadata): {}", file_display, e); + skipped_files += 1; continue; }, }; - match execute_test_file(&metadata_file, platform, node, &context).await { + // Execute test with 10 second timeout + let test_result = tokio::time::timeout( + Duration::from_secs(20), + execute_test_file(&metadata_file, platform, node, &context), + ) + .await; + + let result = match test_result { + Ok(Ok(_)) => Ok(()), + Ok(Err(e)) => Err(e), + Err(_) => Err(anyhow::anyhow!("Test timed out after 20 seconds")), + }; + + match result { Ok(_) => { println!("test {file_display} ... {GREEN}ok{COLOUR_RESET}"); passed_files += 1; @@ -237,7 +298,16 @@ async fn run(args: MlTestRunnerArgs) -> anyhow::Result<()> { println!("test {file_display} ... {RED}FAILED{COLOUR_RESET}"); failed_files += 1; let error_detail = if args.verbose { format!("{:?}", e) } else { format!("{}", e) }; - failures.push((file_display, error_detail)); + failures.push((file_display.clone(), error_detail)); + + // Update cached-failed + { + let mut cache = cached_failed.lock().await; + cache.insert(file_display); + if let Err(e) = save_cached_failed(&cached_failed_path, &cache) { + info!("Failed to save cached-failed: {}", e); + } + } if args.bail { info!("Bailing after first failure"); @@ -551,3 +621,19 @@ fn save_cached_passed(path: &Path, cache: &HashSet) -> anyhow::Result<() writer.flush()?; Ok(()) } + +/// Save cached failed tests to file +fn save_cached_failed(path: &Path, cache: &HashSet) -> anyhow::Result<()> { + let file = File::create(path).context("Failed to create cached-failed file")?; + let mut writer = BufWriter::new(file); + + let mut entries: Vec<_> = cache.iter().collect(); + entries.sort(); + + for entry in entries { + writeln!(writer, "{}", entry)?; + } + + writer.flush()?; + Ok(()) +} diff --git a/crates/node/src/provider_utils/fallback_gas_provider.rs b/crates/node/src/provider_utils/fallback_gas_provider.rs index 78bc94a..960d71c 100644 --- a/crates/node/src/provider_utils/fallback_gas_provider.rs +++ b/crates/node/src/provider_utils/fallback_gas_provider.rs @@ -51,12 +51,9 @@ where provider: &P, tx: &::TransactionRequest, ) -> TransportResult { - // Try to fetch GasFiller’s “fillable” (gas_price, base_fee, estimate_gas, …) - // If it errors (i.e. tx would revert under eth_estimateGas), swallow it. - match self.inner.prepare(provider, tx).await { - Ok(fill) => Ok(Some(fill)), - Err(_) => Ok(None), - } + // Try to fetch GasFiller's "fillable" (gas_price, base_fee, estimate_gas, …) + // Propagate errors so caller can handle them appropriately + self.inner.prepare(provider, tx).await.map(Some) } async fn fill(