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_eip7708_delayed_burn_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(unit.transaction.secret_key),
171                        });
172                    }
173                };
174
175                let result = execute_single_test_runtime(
176                    &mut evm,
177                    RuntimeTestContext {
178                        test,
179                        expected_output: unit.out.as_ref(),
180                        name: &name,
181                        tx: &tx,
182                        cache_state: &cache_state,
183                        elapsed,
184                    },
185                );
186
187                if let Err(e) = result {
188                    return Err(TestError { name, path: path_str, kind: e });
189                }
190            }
191        }
192    }
193    Ok(())
194}
195
196// ── Top-level runner ────────────────────────────────────────────────────────
197
198fn run_test_worker(
199    state: TestRunnerState,
200    keep_going: bool,
201    mode: CompileMode,
202    backend: Option<&JitBackend>,
203) -> Result<(), TestError> {
204    loop {
205        if !keep_going && state.n_errors.load(Ordering::SeqCst) > 0 {
206            return Ok(());
207        }
208
209        let Some(test_path) = state.next_test() else {
210            return Ok(());
211        };
212
213        let t0 = Instant::now();
214        let result = match mode {
215            CompileMode::Interpreter => {
216                execute_test_suite(&test_path, &state.elapsed, false, false)
217            }
218            CompileMode::Jit | CompileMode::Aot => {
219                execute_test_suite_runtime(&test_path, &state.elapsed, backend.unwrap())
220            }
221        };
222        let elapsed = t0.elapsed();
223        if elapsed > Duration::from_secs(5) {
224            eprintln!("slow statetest file ({elapsed:?}): {}", test_path.display());
225        }
226
227        state.console_bar.inc(1);
228
229        if let Err(err) = result {
230            state.n_errors.fetch_add(1, Ordering::SeqCst);
231            if !keep_going {
232                return Err(err);
233            }
234        }
235    }
236}
237
238/// Run all test files.
239pub fn run(
240    test_files: Vec<PathBuf>,
241    single_thread: bool,
242    keep_going: bool,
243    mode: CompileMode,
244) -> Result<(), TestError> {
245    let _ = tracing_subscriber::fmt::try_init();
246
247    let n_files = test_files.len();
248    let state = TestRunnerState::new(test_files);
249
250    let backend = if matches!(mode, CompileMode::Aot | CompileMode::Jit) {
251        let cpus = thread::available_parallelism().map(|n| n.get()).unwrap_or(1);
252        let store = if mode == CompileMode::Aot {
253            let store = RuntimeArtifactStore::new().map_err(|e| TestError {
254                name: "backend".to_string(),
255                path: String::new(),
256                kind: TestErrorKind::CompilationError(format!("tempdir: {e}")),
257            })?;
258            Some(Arc::new(store) as Arc<dyn ArtifactStore>)
259        } else {
260            None
261        };
262        let config = RuntimeConfig {
263            enabled: true,
264            blocking: true,
265            aot: mode == CompileMode::Aot,
266            store,
267            tuning: RuntimeTuning {
268                jit_hot_threshold: 0,
269                jit_worker_count: cpus,
270                ..Default::default()
271            },
272            ..Default::default()
273        };
274        Some(JitBackend::new(config).map_err(|e| TestError {
275            name: "backend".to_string(),
276            path: String::new(),
277            kind: TestErrorKind::CompilationError(format!("backend start: {e}")),
278        })?)
279    } else {
280        None
281    };
282
283    let num_threads = if single_thread {
284        1
285    } else {
286        match thread::available_parallelism() {
287            Ok(n) => n.get().min(n_files),
288            Err(_) => 1,
289        }
290    };
291
292    let barrier = Arc::new(Barrier::new(num_threads));
293
294    let mut handles = Vec::with_capacity(num_threads);
295    for i in 0..num_threads {
296        let state = state.clone();
297        let backend = backend.clone();
298        let barrier = barrier.clone();
299
300        let thread = Builder::new()
301            .name(format!("runner-{i}"))
302            .spawn(move || {
303                // Catch panics so we always reach `barrier.wait()` below; otherwise a
304                // panicking worker would never advance the barrier and the remaining
305                // workers would deadlock waiting for it. Also flips the shared `stop`
306                // flag so siblings exit promptly instead of finishing the whole queue.
307                let stop = state.stop.clone();
308                let result = panic::catch_unwind(AssertUnwindSafe(|| {
309                    run_test_worker(state, keep_going, mode, backend.as_ref())
310                }));
311                if result.is_err() || (!keep_going && result.as_ref().is_ok_and(|r| r.is_err())) {
312                    stop.store(true, Ordering::SeqCst);
313                }
314                // Wait for all threads before exiting. Each thread holds a thread-local
315                // LLVM context that is destroyed on thread exit; concurrent context
316                // disposal crashes LLVM.
317                barrier.wait();
318                match result {
319                    Ok(r) => r,
320                    Err(payload) => panic::resume_unwind(payload),
321                }
322            })
323            .unwrap();
324
325        handles.push(thread);
326    }
327
328    let mut thread_errors = Vec::new();
329    for (i, handle) in handles.into_iter().enumerate() {
330        match handle.join() {
331            Ok(Ok(())) => {}
332            Ok(Err(e)) => thread_errors.push(e),
333            Err(_) => thread_errors.push(TestError {
334                name: format!("thread {i} panicked"),
335                path: String::new(),
336                kind: TestErrorKind::Panic,
337            }),
338        }
339    }
340
341    state.console_bar.finish();
342
343    println!(
344        "Finished execution. Total CPU time: {:.6}s",
345        state.elapsed.lock().unwrap().as_secs_f64()
346    );
347
348    if let Some(backend) = &backend {
349        let stats = backend.stats();
350        println!(
351            "Runtime backend: {} hits, {} misses, {} resident",
352            stats.lookup_hits, stats.lookup_misses, stats.resident_entries,
353        );
354    }
355
356    drop(backend);
357
358    let n_errors = state.n_errors.load(Ordering::SeqCst);
359    let n_thread_errors = thread_errors.len();
360
361    if n_errors == 0 && n_thread_errors == 0 {
362        println!("All tests passed!");
363        Ok(())
364    } else {
365        println!("Encountered {n_errors} errors out of {n_files} total tests");
366
367        if n_thread_errors == 0 {
368            process::exit(1);
369        }
370
371        if n_thread_errors > 1 {
372            println!("{n_thread_errors} threads returned an error, out of {num_threads} total:");
373            for error in &thread_errors {
374                println!("{error}");
375            }
376        }
377        Err(thread_errors.swap_remove(0))
378    }
379}