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(
351 unit.transaction.secret_key.unwrap_or_default(),
352 ),
353 });
354 }
355 };
356
357 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 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 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 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 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_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 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 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 let print_outcome = print_outcome || trace;
501 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
588pub 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 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 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 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}