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