Skip to content

Commit 1819482

Browse files
committed
feat(consume): consume direct using evmone-statetest
1 parent 77edf42 commit 1819482

File tree

3 files changed

+198
-5
lines changed

3 files changed

+198
-5
lines changed

docs/running_tests/consume/direct.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ uv run consume direct --bin=<evm-binary> [OPTIONS]
1313

1414
Currently, only the following clients can be used with `consume direct`:
1515

16-
- go-ethereum `statetest` and `blocktest`.
17-
- Nethermind `nethtest`.
16+
- go-ethereum `statetest` and `blocktest`
17+
- Nethermind `nethtest`
18+
- evmone `evmone-statetest`
1819

1920
## Advantages
2021

@@ -24,7 +25,7 @@ uv run consume direct --bin=<evm-binary> [OPTIONS]
2425

2526
## Limitations
2627

27-
- **Limited client support**: Only go-ethereum and Nethermind.
28+
- **Limited client support**: Only go-ethereum, Nethermind and (partially) evmone
2829
- **Module scope**: Tests EVM, respectively block import, in isolation, not full client behavior.
2930
- **Interface dependency**: Requires client-specific test interfaces.
3031

@@ -42,6 +43,12 @@ or Nethermind:
4243
uv run consume direct --input ./fixtures -m state_test --bin=nethtest
4344
```
4445

46+
or evmone:
47+
48+
```bash
49+
uv run consume direct --input ./fixtures -m state_test --bin=evmone-statetest
50+
```
51+
4552
Run fixtures in the blockchain test format for the Prague fork:
4653

4754
```bash

src/ethereum_clis/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
)
1313
from .clis.besu import BesuTransitionTool
1414
from .clis.ethereumjs import EthereumJSTransitionTool
15-
from .clis.evmone import EvmoneExceptionMapper, EvmOneTransitionTool
15+
from .clis.evmone import EvmoneExceptionMapper, EvmOneFixtureConsumer, EvmOneTransitionTool
1616
from .clis.execution_specs import ExecutionSpecsTransitionTool
1717
from .clis.geth import GethFixtureConsumer, GethTransitionTool
1818
from .clis.nethermind import Nethtest, NethtestFixtureConsumer
@@ -31,6 +31,7 @@
3131
"EthereumJSTransitionTool",
3232
"EvmoneExceptionMapper",
3333
"EvmOneTransitionTool",
34+
"EvmOneFixtureConsumer",
3435
"ExecutionSpecsTransitionTool",
3536
"FixtureConsumerTool",
3637
"GethFixtureConsumer",

src/ethereum_clis/clis/evmone.py

Lines changed: 186 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,26 @@
11
"""Evmone Transition tool interface."""
22

3+
import json
34
import re
5+
import shlex
6+
import shutil
7+
import subprocess
8+
import tempfile
9+
import textwrap
10+
from functools import cache
411
from pathlib import Path
5-
from typing import ClassVar, Dict, Optional
12+
from typing import Any, ClassVar, Dict, List, Optional
613

14+
from ethereum_clis.file_utils import dump_files_to_directory
15+
from ethereum_clis.fixture_consumer_tool import FixtureConsumerTool
716
from ethereum_test_exceptions import (
817
EOFException,
918
ExceptionBase,
1019
ExceptionMapper,
1120
TransactionException,
1221
)
22+
from ethereum_test_fixtures.base import FixtureFormat
23+
from ethereum_test_fixtures.state import StateFixture
1324
from ethereum_test_forks import Fork
1425

1526
from ..transition_tool import TransitionTool
@@ -43,6 +54,180 @@ def is_fork_supported(self, fork: Fork) -> bool:
4354
return True
4455

4556

57+
class EvmOneFixtureConsumer(
58+
FixtureConsumerTool,
59+
fixture_formats=[StateFixture],
60+
):
61+
"""Evmone's implementation of the fixture consumer."""
62+
63+
default_binary = Path("evmone-statetest")
64+
detect_binary_pattern = re.compile(r"^evmone-statetest\b")
65+
version_flag: str = "--version"
66+
67+
binary: Path
68+
cached_version: Optional[str] = None
69+
70+
def __init__(
71+
self,
72+
binary: Optional[Path] = None,
73+
trace: bool = False,
74+
):
75+
"""Initialize the EvmOneFixtureConsumer class."""
76+
self.binary = binary if binary else self.default_binary
77+
self._info_metadata: Optional[Dict[str, Any]] = {}
78+
79+
def _run_command(self, command: List[str]) -> subprocess.CompletedProcess:
80+
try:
81+
return subprocess.run(
82+
command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
83+
)
84+
except subprocess.CalledProcessError as e:
85+
raise Exception("Command failed with non-zero status.") from e
86+
except Exception as e:
87+
raise Exception("Unexpected exception calling evm tool.") from e
88+
89+
# TODO: copied from geth.py, needs to be deduplicated, but nethermind.py
90+
# also has its version
91+
def _consume_debug_dump(
92+
self,
93+
command: List[str],
94+
result: subprocess.CompletedProcess,
95+
fixture_path: Path,
96+
debug_output_path: Path,
97+
):
98+
# our assumption is that each command element is a string
99+
assert all(isinstance(x, str) for x in command), (
100+
f"Not all elements of 'command' list are strings: {command}"
101+
)
102+
assert len(command) > 0
103+
104+
# replace last value with debug fixture path
105+
debug_fixture_path = str(debug_output_path / "fixtures.json")
106+
command[-1] = debug_fixture_path
107+
108+
# ensure that flags with spaces are wrapped in double-quotes
109+
consume_direct_call = " ".join(shlex.quote(arg) for arg in command)
110+
111+
consume_direct_script = textwrap.dedent(
112+
f"""\
113+
#!/bin/bash
114+
{consume_direct_call}
115+
"""
116+
)
117+
dump_files_to_directory(
118+
str(debug_output_path),
119+
{
120+
"consume_direct_args.py": command,
121+
"consume_direct_returncode.txt": result.returncode,
122+
"consume_direct_stdout.txt": result.stdout,
123+
"consume_direct_stderr.txt": result.stderr,
124+
"consume_direct.sh+x": consume_direct_script,
125+
},
126+
)
127+
shutil.copyfile(fixture_path, debug_fixture_path)
128+
129+
@cache # noqa
130+
def consume_state_test_file(
131+
self,
132+
fixture_path: Path,
133+
debug_output_path: Optional[Path] = None,
134+
) -> Dict[str, Any]:
135+
"""
136+
Consume an entire state test file.
137+
138+
The `evmone-statetest` will always execute all the tests contained in a
139+
file without the possibility of selecting a single test, so this
140+
function is cached in order to only call the command once and
141+
`consume_state_test` can simply select the result that was requested.
142+
"""
143+
global_options: List[str] = []
144+
if debug_output_path:
145+
global_options += ["--trace"]
146+
147+
with tempfile.NamedTemporaryFile() as tempfile_json:
148+
# `evmone` uses `gtest` and generates JSON output to a file,
149+
# c.f. https://google.github.io/googletest/advanced.html#generating-a-json-report
150+
# see there for the JSON schema.
151+
global_options += ["--gtest_output=json:{}".format(tempfile_json.name)]
152+
command = [str(self.binary)] + global_options + [str(fixture_path)]
153+
result = self._run_command(command)
154+
155+
if result.returncode not in [0, 1]:
156+
raise Exception(
157+
f"Unexpected exit code:\n{' '.join(command)}\n\n Error:\n{result.stderr}"
158+
)
159+
160+
try:
161+
output_data = json.load(tempfile_json)
162+
except json.JSONDecodeError as e:
163+
raise Exception(f"Failed to parse JSON output from evmone-statetest: {e}") from e
164+
165+
if debug_output_path:
166+
self._consume_debug_dump(command, result, fixture_path, debug_output_path)
167+
168+
return output_data
169+
170+
def _failure_msg(self, file_results: Dict[str, Any]) -> str:
171+
# Assumes only one test has run and there has been a failure,
172+
# as asserted before.
173+
failures = file_results["testsuites"][0]["testsuite"][0]["failures"]
174+
return ", ".join([f["failure"] for f in failures])
175+
176+
def consume_state_test(
177+
self,
178+
fixture_path: Path,
179+
fixture_name: Optional[str] = None,
180+
debug_output_path: Optional[Path] = None,
181+
):
182+
"""
183+
Consume a single state test.
184+
185+
Uses the cached result from `consume_state_test_file` in order to not
186+
call the command every time an select a single result from there.
187+
"""
188+
file_results = self.consume_state_test_file(
189+
fixture_path=fixture_path,
190+
debug_output_path=debug_output_path,
191+
)
192+
if not fixture_name:
193+
fixture_hint = fixture_path.stem
194+
else:
195+
fixture_hint = fixture_name
196+
assert file_results["tests"] == 1, f"Multiple tests ran for {fixture_hint}"
197+
assert file_results["disabled"] == 0, f"Disabled tests for {fixture_hint}"
198+
assert file_results["errors"] == 0, f"Errors during test for {fixture_hint}"
199+
assert file_results["failures"] == 0, (
200+
f"Failures for {fixture_hint}: {self._failure_msg(file_results)}"
201+
)
202+
203+
test_name = file_results["testsuites"][0]["testsuite"][0]["name"]
204+
assert test_name == fixture_path.stem, (
205+
f"Test name mismatch, expected {fixture_path.stem}, got {test_name}"
206+
)
207+
208+
def consume_fixture(
209+
self,
210+
fixture_format: FixtureFormat,
211+
fixture_path: Path,
212+
fixture_name: Optional[str] = None,
213+
debug_output_path: Optional[Path] = None,
214+
):
215+
"""
216+
Execute the appropriate geth fixture consumer for the fixture at
217+
`fixture_path`.
218+
"""
219+
if fixture_format == StateFixture:
220+
self.consume_state_test(
221+
fixture_path=fixture_path,
222+
fixture_name=fixture_name,
223+
debug_output_path=debug_output_path,
224+
)
225+
else:
226+
raise Exception(
227+
f"Fixture format {fixture_format.format_name} not supported by {self.binary}"
228+
)
229+
230+
46231
class EvmoneExceptionMapper(ExceptionMapper):
47232
"""
48233
Translate between EEST exceptions and error strings returned by Evmone.

0 commit comments

Comments
 (0)