Skip to main content

revme/cmd/statetest/
runner.rs

1use crate::cmd::statetest::merkle_trie::{compute_test_roots, TestValidationResult};
2use indicatif::{ProgressBar, ProgressDrawTarget};
3use revm::{
4    context::{block::BlockEnv, cfg::CfgEnv, tx::TxEnv},
5    context_interface::result::{EVMError, ExecutionResult, HaltReason, InvalidTransaction},
6    database::{self, bal::EvmDatabaseError},
7    database_interface::EmptyDB,
8    inspector::{inspectors::TracerEip3155, InspectCommitEvm},
9    primitives::{hardfork::SpecId, Bytes, B256, U256},
10    statetest_types::{SpecName, Test, TestSuite, TestUnit},
11    Context, ExecuteCommitEvm, InspectEvm, MainBuilder, MainContext,
12};
13use serde_json::json;
14use std::{
15    convert::Infallible,
16    fmt::Debug,
17    io::stderr,
18    path::{Path, PathBuf},
19    sync::{
20        atomic::{AtomicBool, AtomicUsize, Ordering},
21        Arc, Mutex,
22    },
23    time::{Duration, Instant},
24};
25use thiserror::Error;
26
27/// Error that occurs during test execution
28#[derive(Debug, Error)]
29#[error("Path: {path}\nName: {name}\nError: {kind}")]
30pub struct TestError {
31    pub name: String,
32    pub path: String,
33    pub kind: TestErrorKind,
34}
35
36/// Specific kind of error that occurred during test execution
37#[derive(Debug, Error)]
38pub enum TestErrorKind {
39    #[error("logs root mismatch: got {got}, expected {expected}")]
40    LogsRootMismatch { got: B256, expected: B256 },
41    #[error("state root mismatch: got {got}, expected {expected}")]
42    StateRootMismatch { got: B256, expected: B256 },
43    #[error("unknown private key: {0:?}")]
44    UnknownPrivateKey(B256),
45    #[error("unexpected exception: got {got_exception:?}, expected {expected_exception:?}")]
46    UnexpectedException {
47        expected_exception: Option<String>,
48        got_exception: Option<String>,
49    },
50    #[error("unexpected output: got {got_output:?}, expected {expected_output:?}")]
51    UnexpectedOutput {
52        expected_output: Option<Bytes>,
53        got_output: Option<Bytes>,
54    },
55    #[error(transparent)]
56    SerdeDeserialize(#[from] serde_json::Error),
57    #[error("thread panicked")]
58    Panic,
59    #[error("path does not exist")]
60    InvalidPath,
61    #[error("no JSON test files found in path")]
62    NoJsonFiles,
63}
64
65/// Check if a test should be skipped based on its filename
66/// Some tests are known to be problematic or take too long
67fn skip_test(path: &Path) -> bool {
68    let path_str = path.to_str().unwrap_or_default();
69
70    // Skip tets that have storage for newly created account.
71    if path_str.contains("paris/eip7610_create_collision") {
72        return true;
73    }
74
75    let name = path.file_name().unwrap().to_str().unwrap_or_default();
76
77    matches!(
78        name,
79        // Test with some storage check.
80        "RevertInCreateInInit_Paris.json"
81        | "RevertInCreateInInit.json"
82        | "dynamicAccountOverwriteEmpty.json"
83        | "dynamicAccountOverwriteEmpty_Paris.json"
84        | "RevertInCreateInInitCreate2Paris.json"
85        | "create2collisionStorage.json"
86        | "RevertInCreateInInitCreate2.json"
87        | "create2collisionStorageParis.json"
88        | "InitCollision.json"
89        | "InitCollisionParis.json"
90        | "test_init_collision_create_opcode.json"
91
92        // Malformed value.
93        | "ValueOverflow.json"
94        | "ValueOverflowParis.json"
95
96        // These tests are passing, but they take a lot of time to execute so we are going to skip them.
97        | "Call50000_sha256.json"
98        | "static_Call50000_sha256.json"
99        | "loopMul.json"
100        | "CALLBlake2f_MaxRounds.json"
101    )
102}
103
104struct TestExecutionContext<'a> {
105    name: &'a str,
106    unit: &'a TestUnit,
107    test: &'a Test,
108    cfg: &'a CfgEnv,
109    block: &'a BlockEnv,
110    tx: &'a TxEnv,
111    cache_state: &'a database::CacheState,
112    elapsed: &'a Arc<Mutex<Duration>>,
113    trace: bool,
114    print_json_outcome: bool,
115}
116
117struct DebugContext<'a> {
118    name: &'a str,
119    path: &'a str,
120    index: usize,
121    test: &'a Test,
122    cfg: &'a CfgEnv,
123    block: &'a BlockEnv,
124    tx: &'a TxEnv,
125    cache_state: &'a database::CacheState,
126    error: &'a TestErrorKind,
127}
128
129fn build_json_output(
130    test: &Test,
131    test_name: &str,
132    exec_result: &Result<
133        ExecutionResult<HaltReason>,
134        EVMError<EvmDatabaseError<Infallible>, InvalidTransaction>,
135    >,
136    validation: &TestValidationResult,
137    spec: SpecId,
138    error: Option<String>,
139) -> serde_json::Value {
140    json!({
141        "stateRoot": validation.state_root,
142        "logsRoot": validation.logs_root,
143        "output": exec_result.as_ref().ok().and_then(|r| r.output().cloned()).unwrap_or_default(),
144        "gasUsed": exec_result.as_ref().ok().map(|r| r.tx_gas_used()).unwrap_or_default(),
145        "pass": error.is_none(),
146        "errorMsg": error.unwrap_or_default(),
147        "evmResult": format_evm_result(exec_result),
148        "postLogsHash": validation.logs_root,
149        "fork": spec,
150        "test": test_name,
151        "d": test.indexes.data,
152        "g": test.indexes.gas,
153        "v": test.indexes.value,
154    })
155}
156
157fn format_evm_result(
158    exec_result: &Result<
159        ExecutionResult<HaltReason>,
160        EVMError<EvmDatabaseError<Infallible>, InvalidTransaction>,
161    >,
162) -> String {
163    match exec_result {
164        Ok(r) => match r {
165            ExecutionResult::Success { reason, .. } => format!("Success: {reason:?}"),
166            ExecutionResult::Revert { .. } => "Revert".to_string(),
167            ExecutionResult::Halt { reason, .. } => format!("Halt: {reason:?}"),
168        },
169        Err(e) => e.to_string(),
170    }
171}
172
173fn validate_exception(
174    test: &Test,
175    exec_result: &Result<
176        ExecutionResult<HaltReason>,
177        EVMError<EvmDatabaseError<Infallible>, InvalidTransaction>,
178    >,
179) -> Result<bool, TestErrorKind> {
180    match (&test.expect_exception, exec_result) {
181        (None, Ok(_)) => Ok(false), // No exception expected, execution succeeded
182        (Some(_), Err(_)) => Ok(true), // Exception expected and occurred
183        _ => Err(TestErrorKind::UnexpectedException {
184            expected_exception: test.expect_exception.clone(),
185            got_exception: exec_result.as_ref().err().map(|e| e.to_string()),
186        }),
187    }
188}
189
190fn validate_output(
191    expected_output: Option<&Bytes>,
192    actual_result: &ExecutionResult<HaltReason>,
193) -> Result<(), TestErrorKind> {
194    if let Some((expected, actual)) = expected_output.zip(actual_result.output()) {
195        if expected != actual {
196            return Err(TestErrorKind::UnexpectedOutput {
197                expected_output: Some(expected.clone()),
198                got_output: actual_result.output().cloned(),
199            });
200        }
201    }
202    Ok(())
203}
204
205fn check_evm_execution(
206    test: &Test,
207    expected_output: Option<&Bytes>,
208    test_name: &str,
209    exec_result: &Result<
210        ExecutionResult<HaltReason>,
211        EVMError<EvmDatabaseError<Infallible>, InvalidTransaction>,
212    >,
213    db: &mut database::State<EmptyDB>,
214    spec: SpecId,
215    print_json_outcome: bool,
216) -> Result<(), TestErrorKind> {
217    let validation = compute_test_roots(exec_result, db);
218
219    let print_json = |error: Option<&TestErrorKind>| {
220        if print_json_outcome {
221            let json = build_json_output(
222                test,
223                test_name,
224                exec_result,
225                &validation,
226                spec,
227                error.map(|e| e.to_string()),
228            );
229            eprintln!("{json}");
230        }
231    };
232
233    // Check if exception handling is correct
234    let exception_expected = validate_exception(test, exec_result).inspect_err(|e| {
235        print_json(Some(e));
236    })?;
237
238    // If exception was expected and occurred, we're done
239    if exception_expected {
240        print_json(None);
241        return Ok(());
242    }
243
244    // Validate output if execution succeeded
245    if let Ok(result) = exec_result {
246        validate_output(expected_output, result).inspect_err(|e| {
247            print_json(Some(e));
248        })?;
249    }
250
251    // Validate logs root
252    if validation.logs_root != test.logs {
253        let error = TestErrorKind::LogsRootMismatch {
254            got: validation.logs_root,
255            expected: test.logs,
256        };
257        print_json(Some(&error));
258        return Err(error);
259    }
260
261    // Validate state root
262    if validation.state_root != test.hash {
263        let error = TestErrorKind::StateRootMismatch {
264            got: validation.state_root,
265            expected: test.hash,
266        };
267        print_json(Some(&error));
268        return Err(error);
269    }
270
271    print_json(None);
272    Ok(())
273}
274
275/// Execute a single test suite file containing multiple tests
276///
277/// # Arguments
278/// * `path` - Path to the JSON test file
279/// * `elapsed` - Shared counter for total execution time
280/// * `trace` - Whether to enable EVM tracing
281/// * `print_json_outcome` - Whether to print JSON formatted results
282pub fn execute_test_suite(
283    path: &Path,
284    elapsed: &Arc<Mutex<Duration>>,
285    trace: bool,
286    print_json_outcome: bool,
287) -> Result<(), TestError> {
288    if skip_test(path) {
289        return Ok(());
290    }
291
292    let s = std::fs::read_to_string(path).unwrap();
293    let path = path.to_string_lossy().into_owned();
294    let suite: TestSuite = serde_json::from_str(&s).map_err(|e| TestError {
295        name: "Unknown".to_string(),
296        path: path.clone(),
297        kind: e.into(),
298    })?;
299
300    for (name, unit) in suite.0 {
301        // Prepare initial state
302        let cache_state = unit.state();
303
304        // Setup base configuration
305        let mut cfg = CfgEnv::default();
306        cfg.chain_id = unit
307            .env
308            .current_chain_id
309            .unwrap_or(U256::ONE)
310            .try_into()
311            .unwrap_or(1);
312
313        // Post and execution
314        for (spec_name, tests) in &unit.post {
315            // Skip Constantinople spec
316            if *spec_name == SpecName::Constantinople {
317                continue;
318            }
319
320            // Unknown/unsupported spec (e.g. a transition fork not yet mapped to a
321            // `SpecId`). Report it and skip rather than panicking in `to_spec_id`.
322            if *spec_name == SpecName::Unknown {
323                eprintln!("Error: unknown spec in post state, skipping: path={path}");
324                continue;
325            }
326
327            cfg.set_spec_and_mainnet_gas_params(spec_name.to_spec_id());
328
329            // Configure max blobs per spec
330            if cfg.spec().is_enabled_in(SpecId::OSAKA) {
331                cfg.set_max_blobs_per_tx(6);
332            } else if cfg.spec().is_enabled_in(SpecId::PRAGUE) {
333                cfg.set_max_blobs_per_tx(9);
334            } else {
335                cfg.set_max_blobs_per_tx(6);
336            }
337
338            // Setup block environment for this spec
339            let block = unit.block_env(&mut cfg);
340
341            for (index, test) in tests.iter().enumerate() {
342                // Setup transaction environment
343                let tx = match test.tx_env(&unit) {
344                    Ok(tx) => tx,
345                    Err(_) if test.expect_exception.is_some() => continue,
346                    Err(_) => {
347                        return Err(TestError {
348                            name,
349                            path,
350                            kind: TestErrorKind::UnknownPrivateKey(unit.transaction.secret_key),
351                        });
352                    }
353                };
354
355                // Execute the test
356                let result = execute_single_test(TestExecutionContext {
357                    name: &name,
358                    unit: &unit,
359                    test,
360                    cfg: &cfg,
361                    block: &block,
362                    tx: &tx,
363                    cache_state: &cache_state,
364                    elapsed,
365                    trace,
366                    print_json_outcome,
367                });
368
369                if let Err(e) = result {
370                    // Handle error with debug trace if needed
371                    static FAILED: AtomicBool = AtomicBool::new(false);
372                    if print_json_outcome || FAILED.swap(true, Ordering::SeqCst) {
373                        return Err(TestError {
374                            name,
375                            path,
376                            kind: e,
377                        });
378                    }
379
380                    // Re-run with trace for debugging
381                    debug_failed_test(DebugContext {
382                        name: &name,
383                        path: &path,
384                        index,
385                        test,
386                        cfg: &cfg,
387                        block: &block,
388                        tx: &tx,
389                        cache_state: &cache_state,
390                        error: &e,
391                    });
392
393                    return Err(TestError {
394                        path,
395                        name,
396                        kind: e,
397                    });
398                }
399            }
400        }
401    }
402    Ok(())
403}
404
405fn execute_single_test(ctx: TestExecutionContext) -> Result<(), TestErrorKind> {
406    // Prepare state
407    let cache = ctx.cache_state.clone();
408    let mut state = database::State::builder()
409        .with_cached_prestate(cache)
410        .with_bundle_update()
411        .build();
412
413    let evm_context = Context::mainnet()
414        .with_block(ctx.block)
415        .with_tx(ctx.tx)
416        .with_cfg(ctx.cfg.clone())
417        .with_db(&mut state);
418
419    // Execute
420    let timer = Instant::now();
421    let (db, exec_result) = if ctx.trace {
422        let mut evm = evm_context
423            .build_mainnet_with_inspector(TracerEip3155::buffered(stderr()).without_summary());
424        let res = evm.inspect_tx_commit(ctx.tx);
425        let db = evm.ctx.journaled_state.database;
426        (db, res)
427    } else {
428        let mut evm = evm_context.build_mainnet();
429        let res = evm.transact_commit(ctx.tx);
430        let db = evm.ctx.journaled_state.database;
431        (db, res)
432    };
433    *ctx.elapsed.lock().unwrap() += timer.elapsed();
434
435    let exec_result = exec_result;
436    // Check results
437    check_evm_execution(
438        ctx.test,
439        ctx.unit.out.as_ref(),
440        ctx.name,
441        &exec_result,
442        db,
443        *ctx.cfg.spec(),
444        ctx.print_json_outcome,
445    )
446}
447
448fn debug_failed_test(ctx: DebugContext) {
449    println!("\nTraces:");
450
451    // Re-run with tracing
452    let cache = ctx.cache_state.clone();
453    let mut state = database::State::builder()
454        .with_cached_prestate(cache)
455        .with_bundle_update()
456        .build();
457
458    let mut evm = Context::mainnet()
459        .with_db(&mut state)
460        .with_block(ctx.block)
461        .with_tx(ctx.tx)
462        .with_cfg(ctx.cfg.clone())
463        .build_mainnet_with_inspector(TracerEip3155::buffered(stderr()).without_summary());
464
465    let _ = evm.inspect_tx(ctx.tx);
466
467    // Execute the transaction without tracing
468    let exec_result = evm.transact_commit(ctx.tx);
469
470    println!("\nExecution result: {exec_result:#?}");
471    println!("\nExpected exception: {:?}", ctx.test.expect_exception);
472    println!("\nState before:\n{}", ctx.cache_state.pretty_print());
473    println!(
474        "\nState after:\n{}",
475        evm.ctx.journaled_state.database.cache.pretty_print()
476    );
477    println!("\nSpecification: {:?}", ctx.cfg.spec());
478    println!("\nTx: {:#?}", ctx.tx);
479    println!("Block: {:#?}", ctx.block);
480    println!("Cfg: {:#?}", ctx.cfg);
481    println!(
482        "\nTest name: {:?} (index: {}, path: {:?}) failed:\n{}",
483        ctx.name, ctx.index, ctx.path, ctx.error
484    );
485}
486
487#[derive(Clone, Copy)]
488struct TestRunnerConfig {
489    single_thread: bool,
490    trace: bool,
491    print_outcome: bool,
492    keep_going: bool,
493}
494
495impl TestRunnerConfig {
496    fn new(single_thread: bool, trace: bool, print_outcome: bool, keep_going: bool) -> Self {
497        // Trace implies print_outcome
498        let print_outcome = print_outcome || trace;
499        // print_outcome or trace implies single_thread
500        let single_thread = single_thread || print_outcome;
501
502        Self {
503            single_thread,
504            trace,
505            print_outcome,
506            keep_going,
507        }
508    }
509}
510
511#[derive(Clone)]
512struct TestRunnerState {
513    n_errors: Arc<AtomicUsize>,
514    console_bar: Arc<ProgressBar>,
515    queue: Arc<Mutex<(usize, Vec<PathBuf>)>>,
516    elapsed: Arc<Mutex<Duration>>,
517    errors: Arc<Mutex<Vec<TestError>>>,
518}
519
520impl TestRunnerState {
521    fn new(test_files: Vec<PathBuf>, omit_progress: bool) -> Self {
522        let n_files = test_files.len();
523        let draw_target = if omit_progress {
524            ProgressDrawTarget::hidden()
525        } else {
526            ProgressDrawTarget::stdout()
527        };
528        Self {
529            n_errors: Arc::new(AtomicUsize::new(0)),
530            console_bar: Arc::new(ProgressBar::with_draw_target(
531                Some(n_files as u64),
532                draw_target,
533            )),
534            queue: Arc::new(Mutex::new((0usize, test_files))),
535            elapsed: Arc::new(Mutex::new(Duration::ZERO)),
536            errors: Arc::new(Mutex::new(Vec::new())),
537        }
538    }
539
540    fn next_test(&self) -> Option<PathBuf> {
541        let (current_idx, queue) = &mut *self.queue.lock().unwrap();
542        let idx = *current_idx;
543        let test_path = queue.get(idx).cloned()?;
544        *current_idx = idx + 1;
545        Some(test_path)
546    }
547}
548
549fn run_test_worker(state: TestRunnerState, config: TestRunnerConfig) -> Result<(), TestError> {
550    loop {
551        if !config.keep_going && state.n_errors.load(Ordering::SeqCst) > 0 {
552            return Ok(());
553        }
554
555        let Some(test_path) = state.next_test() else {
556            return Ok(());
557        };
558
559        let result = execute_test_suite(
560            &test_path,
561            &state.elapsed,
562            config.trace,
563            config.print_outcome,
564        );
565
566        state.console_bar.inc(1);
567
568        if let Err(err) = result {
569            state.n_errors.fetch_add(1, Ordering::SeqCst);
570            if config.keep_going {
571                state.errors.lock().unwrap().push(err);
572            } else {
573                return Err(err);
574            }
575        }
576    }
577}
578
579fn determine_thread_count(single_thread: bool, n_files: usize) -> usize {
580    match (single_thread, std::thread::available_parallelism()) {
581        (true, _) | (false, Err(_)) => 1,
582        (false, Ok(n)) => n.get().min(n_files),
583    }
584}
585
586/// Run all test files in parallel or single-threaded mode
587///
588/// # Arguments
589/// * `test_files` - List of test files to execute
590/// * `single_thread` - Force single-threaded execution
591/// * `trace` - Enable EVM execution tracing
592/// * `print_outcome` - Print test outcomes in JSON format
593/// * `keep_going` - Continue running tests even if some fail
594pub fn run(
595    test_files: Vec<PathBuf>,
596    single_thread: bool,
597    trace: bool,
598    print_outcome: bool,
599    keep_going: bool,
600    omit_progress: bool,
601) -> Result<(), TestError> {
602    let config = TestRunnerConfig::new(single_thread, trace, print_outcome, keep_going);
603    let n_files = test_files.len();
604    let state = TestRunnerState::new(test_files, omit_progress);
605    let num_threads = determine_thread_count(config.single_thread, n_files);
606
607    // Spawn worker threads
608    let mut handles = Vec::with_capacity(num_threads);
609    for i in 0..num_threads {
610        let state = state.clone();
611
612        let thread = std::thread::Builder::new()
613            .name(format!("runner-{i}"))
614            .spawn(move || run_test_worker(state, config))
615            .unwrap();
616
617        handles.push(thread);
618    }
619
620    // Collect results from all threads
621    let mut thread_errors = Vec::new();
622    for (i, handle) in handles.into_iter().enumerate() {
623        match handle.join() {
624            Ok(Ok(())) => {}
625            Ok(Err(e)) => thread_errors.push(e),
626            Err(_) => thread_errors.push(TestError {
627                name: format!("thread {i} panicked"),
628                path: String::new(),
629                kind: TestErrorKind::Panic,
630            }),
631        }
632    }
633
634    state.console_bar.finish();
635
636    // Print summary
637    println!(
638        "Finished execution. Total CPU time: {:.6}s",
639        state.elapsed.lock().unwrap().as_secs_f64()
640    );
641
642    let n_errors = state.n_errors.load(Ordering::SeqCst);
643    let n_thread_errors = thread_errors.len();
644
645    if n_errors == 0 && n_thread_errors == 0 {
646        println!("All tests passed!");
647        Ok(())
648    } else {
649        println!("Encountered {n_errors} errors out of {n_files} total tests");
650
651        let collected_errors = state.errors.lock().unwrap();
652        if !collected_errors.is_empty() {
653            println!("\nFailed tests:");
654            for error in collected_errors.iter() {
655                println!("  {error}");
656            }
657        }
658        drop(collected_errors);
659
660        if n_thread_errors == 0 {
661            std::process::exit(1);
662        }
663
664        if n_thread_errors > 1 {
665            println!("{n_thread_errors} threads returned an error, out of {num_threads} total:");
666            for error in &thread_errors {
667                println!("{error}");
668            }
669        }
670        Err(thread_errors.swap_remove(0))
671    }
672}