Skip to content

Commit 5e0bd80

Browse files
committed
feat(consume): allow geth to validate eof test vectors via direct (ethereum#1232)
1 parent b19b4ca commit 5e0bd80

File tree

1 file changed

+154
-21
lines changed

1 file changed

+154
-21
lines changed

src/ethereum_clis/clis/geth.py

Lines changed: 154 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
ExceptionMessage,
1616
TransactionException,
1717
)
18-
from ethereum_test_fixtures import BlockchainFixture, FixtureFormat, StateFixture
18+
from ethereum_test_fixtures import BlockchainFixture, EOFFixture, FixtureFormat, StateFixture
1919
from ethereum_test_forks import Fork
2020

2121
from ..ethereum_cli import EthereumCLI
@@ -246,10 +246,88 @@ def is_fork_supported(self, fork: Fork) -> bool:
246246
class GethFixtureConsumer(
247247
GethEvm,
248248
FixtureConsumerTool,
249-
fixture_formats=[StateFixture, BlockchainFixture],
249+
fixture_formats=[StateFixture, BlockchainFixture, EOFFixture],
250250
):
251251
"""Geth's implementation of the fixture consumer."""
252252

253+
# ------------------------------------ state test ---------------------------------------------
254+
@cache # noqa: B019
255+
def consume_state_test_file(
256+
self,
257+
fixture_path: Path,
258+
debug_output_path: Optional[Path] = None,
259+
) -> List[Dict[str, Any]]:
260+
"""
261+
Consume an entire state test file.
262+
263+
The `evm statetest` will always execute all the tests contained in a file without the
264+
possibility of selecting a single test, so this function is cached in order to only call
265+
the command once and `consume_state_test` can simply select the result that
266+
was requested.
267+
"""
268+
subcommand = "statetest"
269+
global_options: List[str] = []
270+
subcommand_options: List[str] = []
271+
if debug_output_path:
272+
global_options += ["--verbosity", "100"]
273+
subcommand_options += ["--trace"]
274+
275+
command = (
276+
[str(self.binary)]
277+
+ global_options
278+
+ [subcommand]
279+
+ subcommand_options
280+
+ [str(fixture_path)]
281+
)
282+
result = self._run_command(command)
283+
284+
if debug_output_path:
285+
self._consume_debug_dump(command, result, fixture_path, debug_output_path)
286+
287+
if result.returncode != 0:
288+
raise Exception(
289+
f"Unexpected exit code:\n{' '.join(command)}\n\n Error:\n{result.stderr}"
290+
)
291+
292+
result_json = json.loads(result.stdout)
293+
print("Output from json.loads(result.stdout):", result_json)
294+
if not isinstance(result_json, list):
295+
raise Exception(f"Unexpected result from evm statetest: {result_json}")
296+
return result_json
297+
298+
def consume_state_test(
299+
self,
300+
fixture_path: Path,
301+
fixture_name: Optional[str] = None,
302+
debug_output_path: Optional[Path] = None,
303+
):
304+
"""
305+
Consume a single state test.
306+
307+
Uses the cached result from `consume_state_test_file` in order to not call the command
308+
every time an select a single result from there.
309+
"""
310+
file_results = self.consume_state_test_file(
311+
fixture_path=fixture_path,
312+
debug_output_path=debug_output_path,
313+
)
314+
if fixture_name:
315+
test_result = [
316+
test_result for test_result in file_results if test_result["name"] == fixture_name
317+
]
318+
assert len(test_result) < 2, f"Multiple test results for {fixture_name}"
319+
assert len(test_result) == 1, f"Test result for {fixture_name} missing"
320+
assert test_result[0]["pass"], f"State test failed: {test_result[0]['error']}"
321+
else:
322+
if any(not test_result["pass"] for test_result in file_results):
323+
exception_text = "State test failed: \n" + "\n".join(
324+
f"{test_result['name']}: " + test_result["error"]
325+
for test_result in file_results
326+
if not test_result["pass"]
327+
)
328+
raise Exception(exception_text)
329+
330+
# ---------------------------------- blockchain test ------------------------------------------
253331
def consume_blockchain_test(
254332
self,
255333
fixture_path: Path,
@@ -290,26 +368,71 @@ def consume_blockchain_test(
290368
f"Unexpected exit code:\n{' '.join(command)}\n\n Error:\n{result.stderr}"
291369
)
292370

293-
@cache # noqa
294-
def consume_state_test_file(
371+
# ------------------------------------ eof test -----------------------------------------------
372+
def extract_int_from_subprocess_string(
373+
self, s: str, substring_of_interest_prefix: str
374+
) -> int | None:
375+
"""Hacky method to extract relevant substring from string returned by subprocess execution.""" # noqa: E501
376+
# get everything after this substring occurrence, then remove everything not in [0,9]
377+
relevant_substring = re.sub(r"\D", "", s.split('total executed"=', 1)[1])
378+
if relevant_substring.isdigit():
379+
return int(relevant_substring)
380+
else:
381+
return None
382+
383+
def eofparse_subprocess_output_to_dict(
384+
self, process_result: str, fixture_name: str | None, fork_name: str
385+
) -> Dict:
386+
"""Take subprocess output from geth's eofparse, a fixture_name and a fork name and return relevant data as dict.""" # noqa: E501
387+
# remove whitespaces if more than 1 whitespace occurs at a time
388+
process_result_cleaned: str = re.sub(r"\s+", " ", process_result).strip()
389+
390+
# determine amount of total tests
391+
total_tests_amount: int | None = self.extract_int_from_subprocess_string(
392+
process_result_cleaned, 'total executed"='
393+
)
394+
395+
# determine amount of passed tests
396+
passed_tests_amount: int | None = self.extract_int_from_subprocess_string(
397+
process_result_cleaned, "tests passed="
398+
)
399+
400+
# determine if all tests of this test vector passed
401+
all_tests_passed: bool = False
402+
if total_tests_amount is not None and passed_tests_amount is not None:
403+
if total_tests_amount == passed_tests_amount:
404+
all_tests_passed = True
405+
406+
# return relevant data as dict
407+
return {
408+
"name": fixture_name,
409+
"pass": all_tests_passed,
410+
"stateRoot": None,
411+
"fork": fork_name,
412+
}
413+
414+
@cache # noqa: B019
415+
def consume_eof_test_file(
295416
self,
296417
fixture_path: Path,
418+
fixture_name: Optional[str] = None,
297419
debug_output_path: Optional[Path] = None,
298420
) -> List[Dict[str, Any]]:
299421
"""
300-
Consume an entire state test file.
422+
Consume an entire EOF validation test file.
301423
302-
The `evm statetest` will always execute all the tests contained in a file without the
303-
possibility of selecting a single test, so this function is cached in order to only call
304-
the command once and `consume_state_test` can simply select the result that
305-
was requested.
424+
425+
The `evm eofparse` will always validate all the eof byte codes contained in a file without
426+
the possibility of selecting a single test, so this function is cached in order to only
427+
call the command once and `consume_eof_test` can simply select the result that was
428+
requested.
306429
"""
307-
subcommand = "statetest"
308-
global_options: List[str] = []
309-
subcommand_options: List[str] = []
430+
subcommand = "eofparse"
431+
global_options = []
432+
subcommand_options = ["--test"]
310433
if debug_output_path:
311434
global_options += ["--verbosity", "100"]
312-
subcommand_options += ["--trace"]
435+
# --trace does not exist for eofparse
313436

314437
command = (
315438
[str(self.binary)]
@@ -318,6 +441,7 @@ def consume_state_test_file(
318441
+ subcommand_options
319442
+ [str(fixture_path)]
320443
)
444+
321445
result = self._run_command(command)
322446

323447
if debug_output_path:
@@ -328,25 +452,28 @@ def consume_state_test_file(
328452
f"Unexpected exit code:\n{' '.join(command)}\n\n Error:\n{result.stderr}"
329453
)
330454

331-
result_json = json.loads(result.stdout)
455+
result_json = [
456+
self.eofparse_subprocess_output_to_dict(result.stderr, fixture_name, "Osaka")
457+
]
332458
if not isinstance(result_json, list):
333-
raise Exception(f"Unexpected result from evm statetest: {result_json}")
459+
raise Exception(f"Unexpected result from evm eofparse: {result_json}")
334460
return result_json
335461

336-
def consume_state_test(
462+
def consume_eof_test(
337463
self,
338464
fixture_path: Path,
339465
fixture_name: Optional[str] = None,
340466
debug_output_path: Optional[Path] = None,
341467
):
342468
"""
343-
Consume a single state test.
469+
Consume a single eof test.
344470
345-
Uses the cached result from `consume_state_test_file` in order to not call the command
471+
Uses the cached result from `consume_eof_test_file` in order to not call the command
346472
every time an select a single result from there.
347473
"""
348-
file_results = self.consume_state_test_file(
474+
file_results = self.consume_eof_test_file(
349475
fixture_path=fixture_path,
476+
fixture_name=fixture_name,
350477
debug_output_path=debug_output_path,
351478
)
352479
if fixture_name:
@@ -355,10 +482,10 @@ def consume_state_test(
355482
]
356483
assert len(test_result) < 2, f"Multiple test results for {fixture_name}"
357484
assert len(test_result) == 1, f"Test result for {fixture_name} missing"
358-
assert test_result[0]["pass"], f"State test failed: {test_result[0]['error']}"
485+
assert test_result[0]["pass"], f"EOF test failed: {test_result[0]['error']}"
359486
else:
360487
if any(not test_result["pass"] for test_result in file_results):
361-
exception_text = "State test failed: \n" + "\n".join(
488+
exception_text = "EOF test failed: \n" + "\n".join(
362489
f"{test_result['name']}: " + test_result["error"]
363490
for test_result in file_results
364491
if not test_result["pass"]
@@ -385,6 +512,12 @@ def consume_fixture(
385512
fixture_name=fixture_name,
386513
debug_output_path=debug_output_path,
387514
)
515+
elif fixture_format == EOFFixture:
516+
self.consume_eof_test(
517+
fixture_path=fixture_path,
518+
fixture_name=fixture_name,
519+
debug_output_path=debug_output_path,
520+
)
388521
else:
389522
raise Exception(
390523
f"Fixture format {fixture_format.format_name} not supported by {self.binary}"

0 commit comments

Comments
 (0)