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