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(
351                                unit.transaction.secret_key.unwrap_or_default(),
352                            ),
353                        });
354                    }
355                };
356
357                // Execute the test
358                let result = execute_single_test(TestExecutionContext {
359                    name: &name,
360                    unit: &unit,
361                    test,
362                    cfg: &cfg,
363                    block: &block,
364                    tx: &tx,
365                    cache_state: &cache_state,
366                    elapsed,
367                    trace,
368                    print_json_outcome,
369                });
370
371                if let Err(e) = result {
372                    // Handle error with debug trace if needed
373                    static FAILED: AtomicBool = AtomicBool::new(false);
374                    if print_json_outcome || FAILED.swap(true, Ordering::SeqCst) {
375                        return Err(TestError {
376                            name,
377                            path,
378                            kind: e,
379                        });
380                    }
381
382                    // Re-run with trace for debugging
383                    debug_failed_test(DebugContext {
384                        name: &name,
385                        path: &path,
386                        index,
387                        test,
388                        cfg: &cfg,
389                        block: &block,
390                        tx: &tx,
391                        cache_state: &cache_state,
392                        error: &e,
393                    });
394
395                    return Err(TestError {
396                        path,
397                        name,
398                        kind: e,
399                    });
400                }
401            }
402        }
403    }
404    Ok(())
405}
406
407fn execute_single_test(ctx: TestExecutionContext) -> Result<(), TestErrorKind> {
408    // Prepare state
409    let cache = ctx.cache_state.clone();
410    let mut state = database::State::builder()
411        .with_cached_prestate(cache)
412        .with_bundle_update()
413        .build();
414
415    let evm_context = Context::mainnet()
416        .with_block(ctx.block)
417        .with_tx(ctx.tx)
418        .with_cfg(ctx.cfg.clone())
419        .with_db(&mut state);
420
421    // Execute
422    let timer = Instant::now();
423    let (db, exec_result) = if ctx.trace {
424        let mut evm = evm_context
425            .build_mainnet_with_inspector(TracerEip3155::buffered(stderr()).without_summary());
426        let res = evm.inspect_tx_commit(ctx.tx);
427        let db = evm.ctx.journaled_state.database;
428        (db, res)
429    } else {
430        let mut evm = evm_context.build_mainnet();
431        let res = evm.transact_commit(ctx.tx);
432        let db = evm.ctx.journaled_state.database;
433        (db, res)
434    };
435    *ctx.elapsed.lock().unwrap() += timer.elapsed();
436
437    let exec_result = exec_result;
438    // Check results
439    check_evm_execution(
440        ctx.test,
441        ctx.unit.out.as_ref(),
442        ctx.name,
443        &exec_result,
444        db,
445        *ctx.cfg.spec(),
446        ctx.print_json_outcome,
447    )
448}
449
450fn debug_failed_test(ctx: DebugContext) {
451    println!("\nTraces:");
452
453    // Re-run with tracing
454    let cache = ctx.cache_state.clone();
455    let mut state = database::State::builder()
456        .with_cached_prestate(cache)
457        .with_bundle_update()
458        .build();
459
460    let mut evm = Context::mainnet()
461        .with_db(&mut state)
462        .with_block(ctx.block)
463        .with_tx(ctx.tx)
464        .with_cfg(ctx.cfg.clone())
465        .build_mainnet_with_inspector(TracerEip3155::buffered(stderr()).without_summary());
466
467    let _ = evm.inspect_tx(ctx.tx);
468
469    // Execute the transaction without tracing
470    let exec_result = evm.transact_commit(ctx.tx);
471
472    println!("\nExecution result: {exec_result:#?}");
473    println!("\nExpected exception: {:?}", ctx.test.expect_exception);
474    println!("\nState before:\n{}", ctx.cache_state.pretty_print());
475    println!(
476        "\nState after:\n{}",
477        evm.ctx.journaled_state.database.cache.pretty_print()
478    );
479    println!("\nSpecification: {:?}", ctx.cfg.spec());
480    println!("\nTx: {:#?}", ctx.tx);
481    println!("Block: {:#?}", ctx.block);
482    println!("Cfg: {:#?}", ctx.cfg);
483    println!(
484        "\nTest name: {:?} (index: {}, path: {:?}) failed:\n{}",
485        ctx.name, ctx.index, ctx.path, ctx.error
486    );
487}
488
489#[derive(Clone, Copy)]
490struct TestRunnerConfig {
491    single_thread: bool,
492    trace: bool,
493    print_outcome: bool,
494    keep_going: bool,
495}
496
497impl TestRunnerConfig {
498    fn new(single_thread: bool, trace: bool, print_outcome: bool, keep_going: bool) -> Self {
499        // Trace implies print_outcome
500        let print_outcome = print_outcome || trace;
501        // print_outcome or trace implies single_thread
502        let single_thread = single_thread || print_outcome;
503
504        Self {
505            single_thread,
506            trace,
507            print_outcome,
508            keep_going,
509        }
510    }
511}
512
513#[derive(Clone)]
514struct TestRunnerState {
515    n_errors: Arc<AtomicUsize>,
516    console_bar: Arc<ProgressBar>,
517    queue: Arc<Mutex<(usize, Vec<PathBuf>)>>,
518    elapsed: Arc<Mutex<Duration>>,
519    errors: Arc<Mutex<Vec<TestError>>>,
520}
521
522impl TestRunnerState {
523    fn new(test_files: Vec<PathBuf>, omit_progress: bool) -> Self {
524        let n_files = test_files.len();
525        let draw_target = if omit_progress {
526            ProgressDrawTarget::hidden()
527        } else {
528            ProgressDrawTarget::stdout()
529        };
530        Self {
531            n_errors: Arc::new(AtomicUsize::new(0)),
532            console_bar: Arc::new(ProgressBar::with_draw_target(
533                Some(n_files as u64),
534                draw_target,
535            )),
536            queue: Arc::new(Mutex::new((0usize, test_files))),
537            elapsed: Arc::new(Mutex::new(Duration::ZERO)),
538            errors: Arc::new(Mutex::new(Vec::new())),
539        }
540    }
541
542    fn next_test(&self) -> Option<PathBuf> {
543        let (current_idx, queue) = &mut *self.queue.lock().unwrap();
544        let idx = *current_idx;
545        let test_path = queue.get(idx).cloned()?;
546        *current_idx = idx + 1;
547        Some(test_path)
548    }
549}
550
551fn run_test_worker(state: TestRunnerState, config: TestRunnerConfig) -> Result<(), TestError> {
552    loop {
553        if !config.keep_going && state.n_errors.load(Ordering::SeqCst) > 0 {
554            return Ok(());
555        }
556
557        let Some(test_path) = state.next_test() else {
558            return Ok(());
559        };
560
561        let result = execute_test_suite(
562            &test_path,
563            &state.elapsed,
564            config.trace,
565            config.print_outcome,
566        );
567
568        state.console_bar.inc(1);
569
570        if let Err(err) = result {
571            state.n_errors.fetch_add(1, Ordering::SeqCst);
572            if config.keep_going {
573                state.errors.lock().unwrap().push(err);
574            } else {
575                return Err(err);
576            }
577        }
578    }
579}
580
581fn determine_thread_count(single_thread: bool, n_files: usize) -> usize {
582    match (single_thread, std::thread::available_parallelism()) {
583        (true, _) | (false, Err(_)) => 1,
584        (false, Ok(n)) => n.get().min(n_files),
585    }
586}
587
588/// Run all test files in parallel or single-threaded mode
589///
590/// # Arguments
591/// * `test_files` - List of test files to execute
592/// * `single_thread` - Force single-threaded execution
593/// * `trace` - Enable EVM execution tracing
594/// * `print_outcome` - Print test outcomes in JSON format
595/// * `keep_going` - Continue running tests even if some fail
596pub fn run(
597    test_files: Vec<PathBuf>,
598    single_thread: bool,
599    trace: bool,
600    print_outcome: bool,
601    keep_going: bool,
602    omit_progress: bool,
603) -> Result<(), TestError> {
604    let config = TestRunnerConfig::new(single_thread, trace, print_outcome, keep_going);
605    let n_files = test_files.len();
606    let state = TestRunnerState::new(test_files, omit_progress);
607    let num_threads = determine_thread_count(config.single_thread, n_files);
608
609    // Spawn worker threads
610    let mut handles = Vec::with_capacity(num_threads);
611    for i in 0..num_threads {
612        let state = state.clone();
613
614        let thread = std::thread::Builder::new()
615            .name(format!("runner-{i}"))
616            .spawn(move || run_test_worker(state, config))
617            .unwrap();
618
619        handles.push(thread);
620    }
621
622    // Collect results from all threads
623    let mut thread_errors = Vec::new();
624    for (i, handle) in handles.into_iter().enumerate() {
625        match handle.join() {
626            Ok(Ok(())) => {}
627            Ok(Err(e)) => thread_errors.push(e),
628            Err(_) => thread_errors.push(TestError {
629                name: format!("thread {i} panicked"),
630                path: String::new(),
631                kind: TestErrorKind::Panic,
632            }),
633        }
634    }
635
636    state.console_bar.finish();
637
638    // Print summary
639    println!(
640        "Finished execution. Total CPU time: {:.6}s",
641        state.elapsed.lock().unwrap().as_secs_f64()
642    );
643
644    let n_errors = state.n_errors.load(Ordering::SeqCst);
645    let n_thread_errors = thread_errors.len();
646
647    if n_errors == 0 && n_thread_errors == 0 {
648        println!("All tests passed!");
649        Ok(())
650    } else {
651        println!("Encountered {n_errors} errors out of {n_files} total tests");
652
653        let collected_errors = state.errors.lock().unwrap();
654        if !collected_errors.is_empty() {
655            println!("\nFailed tests:");
656            for error in collected_errors.iter() {
657                println!("  {error}");
658            }
659        }
660        drop(collected_errors);
661
662        if n_thread_errors == 0 {
663            std::process::exit(1);
664        }
665
666        if n_thread_errors > 1 {
667            println!("{n_thread_errors} threads returned an error, out of {num_threads} total:");
668            for error in &thread_errors {
669                println!("{error}");
670            }
671        }
672        Err(thread_errors.swap_remove(0))
673    }
674}