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#[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#[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
65fn skip_test(path: &Path) -> bool {
68 let path_str = path.to_str().unwrap_or_default();
69
70 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 "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 | "ValueOverflow.json"
94 | "ValueOverflowParis.json"
95
96 | "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), (Some(_), Err(_)) => Ok(true), _ => 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 let exception_expected = validate_exception(test, exec_result).inspect_err(|e| {
235 print_json(Some(e));
236 })?;
237
238 if exception_expected {
240 print_json(None);
241 return Ok(());
242 }
243
244 if let Ok(result) = exec_result {
246 validate_output(expected_output, result).inspect_err(|e| {
247 print_json(Some(e));
248 })?;
249 }
250
251 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 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
275pub 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 let cache_state = unit.state();
303
304 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 for (spec_name, tests) in &unit.post {
315 if *spec_name == SpecName::Constantinople {
317 continue;
318 }
319
320 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 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 let block = unit.block_env(&mut cfg);
340
341 for (index, test) in tests.iter().enumerate() {
342 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 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 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 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 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 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_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 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 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 let print_outcome = print_outcome || trace;
499 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
586pub 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 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 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 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}