Skip to content

Commit 50d6bc2

Browse files
authored
feat(consume): consume direct using evmone-state and blockchaintest (#2243)
* feat(consume): `consume direct` using evmone-statetest * feat(consume): `consume direct` using evmone-blockchaintest
1 parent c06786c commit 50d6bc2

File tree

4 files changed

+264
-5
lines changed

4 files changed

+264
-5
lines changed

docs/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Test fixtures for use by clients are available for each release on the [Github r
2323

2424
- ✨ Add retry logic to RPC requests to fix flaky connection issues in Hive ([#2205](https://github.com/ethereum/execution-spec-tests/pull/2205)).
2525
- 🛠️ Mark `consume sync` tests as `flaky` with 3 retires due to client sync inconsistencies ([#2252](https://github.com/ethereum/execution-spec-tests/pull/2252)).
26+
- ✨ Add `consume direct` using `evmone-statetest` and `evmone-blockchaintest` ([#2243](https://github.com/ethereum/execution-spec-tests/pull/2243)).
2627

2728
### 📋 Misc
2829

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` and `evmone-blockchaintest`
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 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 --bin=evmone-statetest --bin=evmone-blockchaintest
50+
```
51+
4552
Run fixtures in the blockchain test format for the Prague fork:
4653

4754
```bash

src/ethereum_clis/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@
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 (
16+
EvmOneBlockchainFixtureConsumer,
17+
EvmoneExceptionMapper,
18+
EvmOneStateFixtureConsumer,
19+
EvmOneTransitionTool,
20+
)
1621
from .clis.execution_specs import ExecutionSpecsTransitionTool
1722
from .clis.geth import GethFixtureConsumer, GethTransitionTool
1823
from .clis.nethermind import Nethtest, NethtestFixtureConsumer
@@ -31,6 +36,8 @@
3136
"EthereumJSTransitionTool",
3237
"EvmoneExceptionMapper",
3338
"EvmOneTransitionTool",
39+
"EvmOneStateFixtureConsumer",
40+
"EvmOneBlockchainFixtureConsumer",
3441
"ExecutionSpecsTransitionTool",
3542
"FixtureConsumerTool",
3643
"GethFixtureConsumer",

src/ethereum_clis/clis/evmone.py

Lines changed: 245 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,29 @@
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+
import pytest
15+
16+
from ethereum_clis.file_utils import dump_files_to_directory
17+
from ethereum_clis.fixture_consumer_tool import FixtureConsumerTool
718
from ethereum_test_exceptions import (
819
EOFException,
920
ExceptionBase,
1021
ExceptionMapper,
1122
TransactionException,
1223
)
24+
from ethereum_test_fixtures.base import FixtureFormat
25+
from ethereum_test_fixtures.blockchain import BlockchainFixture
26+
from ethereum_test_fixtures.state import StateFixture
1327
from ethereum_test_forks import Fork
1428

1529
from ..transition_tool import TransitionTool
@@ -43,6 +57,236 @@ def is_fork_supported(self, fork: Fork) -> bool:
4357
return True
4458

4559

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

0 commit comments

Comments
 (0)