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#[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 | "CreateTransactionHighNonce.json"
81
82 | "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 | "ValueOverflow.json"
97 | "ValueOverflowParis.json"
98
99 | "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), (Some(_), Err(_)) => Ok(true), _ => 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 let exception_expected = validate_exception(test, exec_result).inspect_err(|e| {
238 print_json(Some(e));
239 })?;
240
241 if exception_expected {
243 print_json(None);
244 return Ok(());
245 }
246
247 if let Ok(result) = exec_result {
249 validate_output(expected_output, result).inspect_err(|e| {
250 print_json(Some(e));
251 })?;
252 }
253
254 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 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
278pub 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 let cache_state = unit.state();
306
307 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 for (spec_name, tests) in &unit.post {
318 if *spec_name == SpecName::Constantinople {
320 continue;
321 }
322
323 cfg.set_spec_and_mainnet_gas_params(spec_name.to_spec_id());
324
325 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 let block = unit.block_env(&mut cfg);
336
337 for (index, test) in tests.iter().enumerate() {
338 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 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 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 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 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 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_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 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 let print_outcome = print_outcome || trace;
492 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
575pub 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 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 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 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}