Skip to main content

revmc_statetest/
compiled.rs

1// revmc-specific code: compilation, handler integration, and test orchestration.
2
3use crate::runner::{
4    TestError, TestErrorKind, TestRunnerState, check_evm_execution, execute_test_suite, skip_test,
5};
6use revm_context::{Cfg, Context, Journal, cfg::CfgEnv, tx::TxEnv};
7use revm_context_interface::journaled_state::JournalTr;
8use revm_database::{self as database};
9use revm_database_interface::{DatabaseCommit, EmptyDB};
10use revm_handler::{Handler, MainBuilder, MainContext, MainnetContext, MainnetEvm};
11use revm_primitives::{U256, hardfork::SpecId};
12use revm_statetest_types::{SpecName, TestSuite};
13use std::{
14    fs,
15    panic::{self, AssertUnwindSafe},
16    path::{Path, PathBuf},
17    process,
18    sync::{Arc, Barrier, Mutex, atomic::Ordering},
19    thread::{self, Builder},
20    time::{Duration, Instant},
21};
22
23// ── Compile mode ────────────────────────────────────────────────────────────
24
25/// How to compile and execute bytecodes in the test suite.
26#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
27pub enum CompileMode {
28    /// Standard interpreter execution (no compilation).
29    #[default]
30    Interpreter,
31    /// Use the runtime backend and look up JIT-compiled functions via `JitBackend::lookup()`.
32    Jit,
33    /// AOT-compile all bytecodes to a shared library, then load and execute.
34    Aot,
35}
36
37// ── Runtime backend mode ─────────────────────────────────────────────────────
38
39use revmc::{
40    revm_evm::JitEvm,
41    runtime::{ArtifactStore, JitBackend, RuntimeArtifactStore, RuntimeConfig, RuntimeTuning},
42};
43
44type RuntimeState = database::State<EmptyDB>;
45type RuntimeEvm = JitEvm<MainnetEvm<MainnetContext<RuntimeState>>>;
46
47/// Execute a single test using the runtime backend via [`JitEvm`].
48fn execute_single_test_runtime(
49    evm: &mut RuntimeEvm,
50    ctx: RuntimeTestContext<'_>,
51) -> Result<(), TestErrorKind> {
52    let prestate = ctx.cache_state.clone();
53    let state =
54        database::State::builder().with_cached_prestate(prestate).with_bundle_update().build();
55    let mut journal = Journal::new(state);
56    journal.set_spec_id(*evm.ctx.cfg.spec());
57    journal.set_eip7708_config(
58        evm.ctx.cfg.is_eip7708_disabled(),
59        evm.ctx.cfg.is_eip8246_delayed_clear_disabled(),
60    );
61
62    let timer = Instant::now();
63    evm.ctx.tx = ctx.tx.clone();
64    evm.ctx.journaled_state = journal;
65
66    let mut handler = revm_handler::MainnetHandler::default();
67    let exec_result = handler.run(evm);
68    if exec_result.is_ok() {
69        let s = evm.ctx.journaled_state.finalize();
70        DatabaseCommit::commit(&mut evm.ctx.journaled_state.database, s);
71    }
72    *ctx.elapsed.lock().unwrap() += timer.elapsed();
73
74    let spec = *evm.ctx.cfg.spec();
75    check_evm_execution(
76        ctx.test,
77        ctx.expected_output,
78        ctx.name,
79        &exec_result,
80        &mut evm.ctx.journaled_state.database,
81        spec,
82        false,
83    )
84}
85
86struct RuntimeTestContext<'a> {
87    test: &'a revm_statetest_types::Test,
88    expected_output: Option<&'a revm_primitives::Bytes>,
89    name: &'a str,
90    tx: &'a TxEnv,
91    cache_state: &'a database::CacheState,
92    elapsed: &'a Arc<Mutex<Duration>>,
93}
94
95fn skip_runtime_test(path: &Path) -> bool {
96    if skip_test(path) {
97        return true;
98    }
99
100    // TODO: Remove this once runtime compilation handles these cases fast enough.
101    // These generated execution-spec tests are interpreter coverage, but runtime
102    // mode has to compile hundreds of large/duplicate variants and can exceed CI
103    // timeouts on slower targets.
104    path.file_name().is_some_and(|name| {
105        name == "test_stack_overflow.json" || name == "precompsEIP2929Cancun.json"
106    })
107}
108
109/// Execute a test suite file using the runtime backend.
110///
111/// For each test unit, enqueue JIT compilation via the backend before executing.
112fn execute_test_suite_runtime(
113    path: &Path,
114    elapsed: &Arc<Mutex<Duration>>,
115    backend: &JitBackend,
116) -> Result<(), TestError> {
117    if skip_runtime_test(path) {
118        return Ok(());
119    }
120
121    let s = fs::read_to_string(path).unwrap();
122    let path_str = path.to_string_lossy().into_owned();
123    let suite: TestSuite = serde_json::from_str(&s).map_err(|e| TestError {
124        name: "Unknown".to_string(),
125        path: path_str.clone(),
126        kind: e.into(),
127    })?;
128
129    for (name, unit) in suite.0 {
130        let cache_state = unit.state();
131        let mut cfg = CfgEnv::default();
132        cfg.chain_id = unit.env.current_chain_id.unwrap_or(U256::ONE).try_into().unwrap_or(1);
133
134        for (spec_name, tests) in &unit.post {
135            if *spec_name == SpecName::Constantinople {
136                continue;
137            }
138
139            let spec_id = spec_name.to_spec_id();
140            cfg.set_spec_and_mainnet_gas_params(spec_id);
141
142            if cfg.spec().is_enabled_in(SpecId::OSAKA) {
143                cfg.set_max_blobs_per_tx(6);
144            } else if cfg.spec().is_enabled_in(SpecId::PRAGUE) {
145                cfg.set_max_blobs_per_tx(9);
146            } else {
147                cfg.set_max_blobs_per_tx(6);
148            }
149
150            let block = unit.block_env(&mut cfg);
151            let initial_state = database::State::builder()
152                .with_cached_prestate(cache_state.clone())
153                .with_bundle_update()
154                .build();
155            let evm_context = Context::mainnet()
156                .with_block(block.clone())
157                .with_cfg(cfg.clone())
158                .with_db(initial_state);
159            let inner = evm_context.build_mainnet();
160            let mut evm = JitEvm::new(inner, backend.clone());
161
162            for test in tests.iter() {
163                let tx = match test.tx_env(&unit) {
164                    Ok(tx) => tx,
165                    Err(_) if test.expect_exception.is_some() => continue,
166                    Err(_) => {
167                        return Err(TestError {
168                            name,
169                            path: path_str,
170                            kind: TestErrorKind::UnknownPrivateKey(
171                                unit.transaction.secret_key.unwrap_or_default(),
172                            ),
173                        });
174                    }
175                };
176
177                let result = execute_single_test_runtime(
178                    &mut evm,
179                    RuntimeTestContext {
180                        test,
181                        expected_output: unit.out.as_ref(),
182                        name: &name,
183                        tx: &tx,
184                        cache_state: &cache_state,
185                        elapsed,
186                    },
187                );
188
189                if let Err(e) = result {
190                    return Err(TestError { name, path: path_str, kind: e });
191                }
192            }
193        }
194    }
195    Ok(())
196}
197
198// ── Top-level runner ────────────────────────────────────────────────────────
199
200fn run_test_worker(
201    state: TestRunnerState,
202    keep_going: bool,
203    mode: CompileMode,
204    backend: Option<&JitBackend>,
205) -> Result<(), TestError> {
206    loop {
207        if !keep_going && state.n_errors.load(Ordering::SeqCst) > 0 {
208            return Ok(());
209        }
210
211        let Some(test_path) = state.next_test() else {
212            return Ok(());
213        };
214
215        let t0 = Instant::now();
216        let result = match mode {
217            CompileMode::Interpreter => {
218                execute_test_suite(&test_path, &state.elapsed, false, false)
219            }
220            CompileMode::Jit | CompileMode::Aot => {
221                execute_test_suite_runtime(&test_path, &state.elapsed, backend.unwrap())
222            }
223        };
224        let elapsed = t0.elapsed();
225        if elapsed > Duration::from_secs(5) {
226            eprintln!("slow statetest file ({elapsed:?}): {}", test_path.display());
227        }
228
229        state.console_bar.inc(1);
230
231        if let Err(err) = result {
232            state.n_errors.fetch_add(1, Ordering::SeqCst);
233            if !keep_going {
234                return Err(err);
235            }
236        }
237    }
238}
239
240/// Run all test files.
241pub fn run(
242    test_files: Vec<PathBuf>,
243    single_thread: bool,
244    keep_going: bool,
245    mode: CompileMode,
246) -> Result<(), TestError> {
247    let _ = tracing_subscriber::fmt::try_init();
248
249    let n_files = test_files.len();
250    let state = TestRunnerState::new(test_files);
251
252    let backend = if matches!(mode, CompileMode::Aot | CompileMode::Jit) {
253        let cpus = thread::available_parallelism().map(|n| n.get()).unwrap_or(1);
254        let store = if mode == CompileMode::Aot {
255            let store = RuntimeArtifactStore::new().map_err(|e| TestError {
256                name: "backend".to_string(),
257                path: String::new(),
258                kind: TestErrorKind::CompilationError(format!("tempdir: {e}")),
259            })?;
260            Some(Arc::new(store) as Arc<dyn ArtifactStore>)
261        } else {
262            None
263        };
264        let config = RuntimeConfig {
265            enabled: true,
266            blocking: true,
267            aot: mode == CompileMode::Aot,
268            store,
269            tuning: RuntimeTuning {
270                jit_hot_threshold: 0,
271                jit_worker_count: cpus,
272                ..Default::default()
273            },
274            ..Default::default()
275        };
276        Some(JitBackend::new(config).map_err(|e| TestError {
277            name: "backend".to_string(),
278            path: String::new(),
279            kind: TestErrorKind::CompilationError(format!("backend start: {e}")),
280        })?)
281    } else {
282        None
283    };
284
285    let num_threads = if single_thread {
286        1
287    } else {
288        match thread::available_parallelism() {
289            Ok(n) => n.get().min(n_files),
290            Err(_) => 1,
291        }
292    };
293
294    let barrier = Arc::new(Barrier::new(num_threads));
295
296    let mut handles = Vec::with_capacity(num_threads);
297    for i in 0..num_threads {
298        let state = state.clone();
299        let backend = backend.clone();
300        let barrier = barrier.clone();
301
302        let thread = Builder::new()
303            .name(format!("runner-{i}"))
304            .spawn(move || {
305                // Catch panics so we always reach `barrier.wait()` below; otherwise a
306                // panicking worker would never advance the barrier and the remaining
307                // workers would deadlock waiting for it. Also flips the shared `stop`
308                // flag so siblings exit promptly instead of finishing the whole queue.
309                let stop = state.stop.clone();
310                let result = panic::catch_unwind(AssertUnwindSafe(|| {
311                    run_test_worker(state, keep_going, mode, backend.as_ref())
312                }));
313                if result.is_err() || (!keep_going && result.as_ref().is_ok_and(|r| r.is_err())) {
314                    stop.store(true, Ordering::SeqCst);
315                }
316                // Wait for all threads before exiting. Each thread holds a thread-local
317                // LLVM context that is destroyed on thread exit; concurrent context
318                // disposal crashes LLVM.
319                barrier.wait();
320                match result {
321                    Ok(r) => r,
322                    Err(payload) => panic::resume_unwind(payload),
323                }
324            })
325            .unwrap();
326
327        handles.push(thread);
328    }
329
330    let mut thread_errors = Vec::new();
331    for (i, handle) in handles.into_iter().enumerate() {
332        match handle.join() {
333            Ok(Ok(())) => {}
334            Ok(Err(e)) => thread_errors.push(e),
335            Err(_) => thread_errors.push(TestError {
336                name: format!("thread {i} panicked"),
337                path: String::new(),
338                kind: TestErrorKind::Panic,
339            }),
340        }
341    }
342
343    state.console_bar.finish();
344
345    println!(
346        "Finished execution. Total CPU time: {:.6}s",
347        state.elapsed.lock().unwrap().as_secs_f64()
348    );
349
350    if let Some(backend) = &backend {
351        let stats = backend.stats();
352        println!(
353            "Runtime backend: {} hits, {} misses, {} resident",
354            stats.lookup_hits, stats.lookup_misses, stats.resident_entries,
355        );
356    }
357
358    drop(backend);
359
360    let n_errors = state.n_errors.load(Ordering::SeqCst);
361    let n_thread_errors = thread_errors.len();
362
363    if n_errors == 0 && n_thread_errors == 0 {
364        println!("All tests passed!");
365        Ok(())
366    } else {
367        println!("Encountered {n_errors} errors out of {n_files} total tests");
368
369        if n_thread_errors == 0 {
370            process::exit(1);
371        }
372
373        if n_thread_errors > 1 {
374            println!("{n_thread_errors} threads returned an error, out of {num_threads} total:");
375            for error in &thread_errors {
376                println!("{error}");
377            }
378        }
379        Err(thread_errors.swap_remove(0))
380    }
381}