Skip to content

Commit 16201e0

Browse files
committed
Merge branch 'main' into eof/eip663_dupn_swapn
Required to resolve a dependency conflict with upstream dependency, cf ethereum#510.
2 parents a7b0965 + 3a6520e commit 16201e0

32 files changed

+3315
-2912
lines changed

docs/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Test fixtures for use by clients are available for each release on the [Github r
1515
- 🐞 Fix incorrect `!=` operator for `FixedSizeBytes` ([#477](https://github.com/ethereum/execution-spec-tests/pull/477)).
1616
- ✨ Add Macro enum that represents byte sequence of Op instructions ([#457](https://github.com/ethereum/execution-spec-tests/pull/457))
1717
- ✨ Number of parameters used to call opcodes (to generate bytecode) is now checked ([#492](https://github.com/ethereum/execution-spec-tests/pull/492)).
18-
- ✨ Libraries have been refactored to use `pydantic` for type checking in most test types ([#486](https://github.com/ethereum/execution-spec-tests/pull/486)).
18+
- ✨ Libraries have been refactored to use `pydantic` for type checking in most test types ([#486](https://github.com/ethereum/execution-spec-tests/pull/486), [#501](https://github.com/ethereum/execution-spec-tests/pull/501), [#508](https://github.com/ethereum/execution-spec-tests/pull/508)).
1919

2020
### 🔧 EVM Tools
2121

@@ -27,6 +27,7 @@ Test fixtures for use by clients are available for each release on the [Github r
2727
- 💥 As part of the pydantic conversion, the fixtures have the following (possibly breaking) changes ([#486](https://github.com/ethereum/execution-spec-tests/pull/486)):
2828
- State test field `transaction` now uses the proper zero-padded hex number format for fields `maxPriorityFeePerGas`, `maxFeePerGas`, and `maxFeePerBlobGas`
2929
- Fixtures' hashes (in the `_info` field) are now calculated by removing the "_info" field entirely instead of it being set to an empty dict.
30+
- 🐞 Relax minor and patch dependency requirements to avoid conflicting package dependencies ([#510](https://github.com/ethereum/execution-spec-tests/pull/510)).
3031

3132
### 💥 Breaking Change
3233

setup.cfg

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,16 @@ install_requires =
2626
ethereum@git+https://github.com/ethereum/execution-specs.git
2727
setuptools
2828
types-setuptools
29-
requests>=2.31.0
30-
colorlog>=6.7.0
31-
pytest==7.3.2
29+
requests>=2.31.0,<3
30+
colorlog>=6.7.0,<7
31+
pytest>7.3.2,<8
3232
pytest-xdist>=3.3.1,<4
3333
coincurve>=18.0.0,<19
34-
trie==2.1.1
35-
semver==3.0.1
34+
trie>=2.0.2,<3
35+
semver>=3.0.1,<4
3636
click>=8.0.0,<9
37-
pydantic>=2.6.3
37+
pydantic>=2.6.3,<3
38+
rich>=13.7.0,<14
3839

3940
[options.package_data]
4041
ethereum_test_tools =
@@ -48,6 +49,7 @@ evm_transition_tool =
4849
console_scripts =
4950
fill = cli.pytest_commands:fill
5051
tf = cli.pytest_commands:tf
52+
checkfixtures = cli.check_fixtures:check_fixtures
5153
gentest = cli.gentest:make_test
5254
pyspelling_soft_fail = cli.tox_helpers:pyspelling
5355
markdownlintcli2_soft_fail = cli.tox_helpers:markdownlint

src/cli/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""
2+
Ethereum execution spec tests command-line tools.
3+
"""

src/cli/check_fixtures.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
"""
2+
Perform sanity checks on the framework's pydantic serialization and
3+
deserialization using generated json fixtures files.
4+
"""
5+
6+
from pathlib import Path
7+
8+
import click
9+
from rich.progress import BarColumn, Progress, TaskProgressColumn, TextColumn, TimeElapsedColumn
10+
11+
from ethereum_test_tools.common.json import to_json
12+
from ethereum_test_tools.spec.base.base_test import HashMismatchException
13+
from ethereum_test_tools.spec.file.types import Fixtures
14+
15+
16+
def count_json_files_exclude_index(start_path: Path) -> int:
17+
"""
18+
Return the number of json files in the specified directory, excluding
19+
index.json files.
20+
"""
21+
json_file_count = sum(1 for file in start_path.rglob("*.json") if file.name != "index.json")
22+
return json_file_count
23+
24+
25+
def check_json(json_file_path: Path):
26+
"""
27+
Check all fixtures in the specified json file:
28+
29+
1. Load the json file into a pydantic model. This checks there are no
30+
Validation errors when loading fixtures into EEST models.
31+
2. Serialize the loaded pydantic model to "json" (actually python data
32+
structures, ready to written as json).
33+
3. Load the serialized data back into a pydantic model (to get an updated
34+
hash) from step 2.
35+
4. Compare hashes:
36+
a. Compare the newly calculated hashes from step 2. and 3.and
37+
b. If present, compare info["hash"] with the calculated hash from step 2.
38+
"""
39+
fixtures = Fixtures.from_file(json_file_path, fixture_format=None)
40+
fixtures_json = to_json(fixtures)
41+
fixtures_deserialized = Fixtures.from_json_data(fixtures_json, fixture_format=None)
42+
for fixture_name, fixture in fixtures.items():
43+
new_hash = fixtures_deserialized[fixture_name].hash
44+
if (original_hash := fixture.hash) != new_hash:
45+
raise HashMismatchException(
46+
original_hash,
47+
new_hash,
48+
message=f"Fixture hash attributes do not match for {fixture_name}",
49+
)
50+
if "hash" in fixture.info and fixture.info["hash"] != original_hash:
51+
raise HashMismatchException(
52+
original_hash,
53+
fixture.info["hash"],
54+
message=f"Fixture info['hash'] does not match calculated hash for {fixture_name}",
55+
)
56+
57+
58+
@click.command()
59+
@click.option(
60+
"--input",
61+
"-i",
62+
"input_dir",
63+
type=click.Path(exists=True, file_okay=False, dir_okay=True, readable=True),
64+
required=True,
65+
help="The input directory containing json fixture files",
66+
)
67+
@click.option(
68+
"--quiet",
69+
"-q",
70+
"quiet_mode",
71+
is_flag=True,
72+
default=False,
73+
expose_value=True,
74+
help="Don't show the progress bar while processing fixture files.",
75+
)
76+
@click.option(
77+
"--stop-on-error",
78+
"--raise-on-error",
79+
"-s",
80+
"stop_on_error",
81+
is_flag=True,
82+
default=False,
83+
expose_value=True,
84+
help="Stop and raise any exceptions encountered while checking fixtures.",
85+
)
86+
def check_fixtures(input_dir: str, quiet_mode: bool, stop_on_error: bool):
87+
"""
88+
Perform some checks on the fixtures contained in the specified directory.
89+
"""
90+
input_path = Path(input_dir)
91+
success = True
92+
file_count = 0
93+
filename_display_width = 25
94+
if not quiet_mode:
95+
file_count = count_json_files_exclude_index(input_path)
96+
97+
with Progress(
98+
TextColumn(
99+
f"[bold cyan]{{task.fields[filename]:<{filename_display_width}}}[/]", justify="left"
100+
),
101+
BarColumn(bar_width=None, complete_style="green3", finished_style="bold green3"),
102+
TaskProgressColumn(),
103+
TimeElapsedColumn(),
104+
expand=True,
105+
disable=quiet_mode,
106+
) as progress:
107+
108+
task_id = progress.add_task("Checking fixtures", total=file_count, filename="...")
109+
for json_file_path in input_path.rglob("*.json"):
110+
if json_file_path.name == "index.json":
111+
continue
112+
113+
display_filename = json_file_path.name
114+
if len(display_filename) > filename_display_width:
115+
display_filename = display_filename[: filename_display_width - 3] + "..."
116+
else:
117+
display_filename = display_filename.ljust(filename_display_width)
118+
119+
try:
120+
progress.update(task_id, advance=1, filename=f"Checking {display_filename}")
121+
check_json(json_file_path)
122+
except Exception as e:
123+
success = False
124+
if stop_on_error:
125+
raise e
126+
else:
127+
progress.console.print(f"\nError checking {json_file_path}:")
128+
progress.console.print(f" {e}")
129+
130+
reward_string = "🦄" if success else "🐢"
131+
progress.update(
132+
task_id, completed=file_count, filename=f"Completed checking all files {reward_string}"
133+
)
134+
135+
return success
136+
137+
138+
if __name__ == "__main__":
139+
check_fixtures()

src/cli/tests/test_evm_bytes_to_python.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
"""
44

55
import pytest
6-
from evm_bytes_to_python import process_evm_bytes
76

87
from ethereum_test_tools import Macro
98
from ethereum_test_tools import Opcodes as Op
109

10+
from ..evm_bytes_to_python import process_evm_bytes
11+
1112
basic_vector = [
1213
"0x60008080808061AAAA612d5ff1600055",
1314
'Op.PUSH1("0x00") + Op.DUP1 + Op.DUP1 + Op.DUP1 + Op.DUP1 + Op.PUSH2("0xaaaa") + Op.PUSH2("0x2d5f") + Op.CALL + Op.PUSH1("0x00") + Op.SSTORE', # noqa: E501

src/cli/tests/test_order_fixtures.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88

99
import pytest
1010
from click.testing import CliRunner
11-
from order_fixtures import order_fixtures, process_directory
11+
12+
from ..order_fixtures import order_fixtures, process_directory
1213

1314

1415
def create_temp_json_file(directory, name, content): # noqa: D103

src/cli/tests/test_pytest_commands.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
import pytest
66
from click.testing import CliRunner
7-
from pytest_commands import fill
7+
8+
from ..pytest_commands import fill
89

910

1011
@pytest.fixture
@@ -47,7 +48,7 @@ def test_tf_deprecation(runner):
4748
"""
4849
Test the deprecation message of the `tf` command.
4950
"""
50-
from pytest_commands import tf
51+
from ..pytest_commands import tf
5152

5253
result = runner.invoke(tf, [])
5354
assert result.exit_code == 1

src/ethereum_test_tools/common/json.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44

55
from typing import Any, Dict
66

7-
from pydantic import BaseModel
7+
from pydantic import BaseModel, RootModel
88

99

10-
def to_json(input: BaseModel) -> Dict[str, Any]:
10+
def to_json(input: BaseModel | RootModel) -> Dict[str, Any]:
1111
"""
12-
Converts a value to its json representation.
12+
Converts a model to its json data representation.
1313
"""
1414
return input.model_dump(mode="json", by_alias=True, exclude_none=True)

src/ethereum_test_tools/common/types.py

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -826,31 +826,63 @@ class TransactionGeneric(BaseModel, Generic[NumberBoundTypeVar]):
826826
sender: Address | None = None
827827

828828

829-
class TransactionToEmptyStringHandler(CamelModel):
830-
"""Handler for serializing and validating the `to` field as an empty string."""
829+
class TransactionFixtureConverter(CamelModel):
830+
"""
831+
Handler for serializing and validating the `to` field as an empty string.
832+
"""
831833

832834
@model_validator(mode="before")
833835
@classmethod
834836
def validate_to_as_empty_string(cls, data: Any) -> Any:
835837
"""
836-
Validates the field `to` is an empty string if the value is None.
838+
If the `to` field is an empty string, set the model value to None.
837839
"""
838840
if isinstance(data, dict) and "to" in data and data["to"] == "":
839-
del data["to"]
841+
data["to"] = None
840842
return data
841843

842844
@model_serializer(mode="wrap", when_used="json-unless-none")
843845
def serialize_to_as_empty_string(self, serializer):
844846
"""
845-
Serializes the field `to` an empty string if the value is None.
847+
Serialize the `to` field as the empty string if the model value is None.
846848
"""
847849
default = serializer(self)
848850
if default is not None and "to" not in default:
849851
default["to"] = ""
850852
return default
851853

852854

853-
class Transaction(CamelModel, TransactionGeneric[HexNumber]):
855+
class TransactionTransitionToolConverter(CamelModel):
856+
"""
857+
Handler for serializing and validating the `to` field as an empty string.
858+
"""
859+
860+
@model_validator(mode="before")
861+
@classmethod
862+
def validate_to_as_empty_string(cls, data: Any) -> Any:
863+
"""
864+
If the `to` field is an empty string, set the model value to None.
865+
"""
866+
if isinstance(data, dict) and "to" in data and data["to"] == "":
867+
data["to"] = None
868+
return data
869+
870+
@model_serializer(mode="wrap", when_used="json-unless-none")
871+
def serialize_to_as_none(self, serializer):
872+
"""
873+
Serialize the `to` field as `None` if the model value is None.
874+
875+
This is required as we use `exclude_none=True` when serializing, but the
876+
t8n tool explicitly requires a value of `None` (respectively null), for
877+
if the `to` field should be unset (contract creation).
878+
"""
879+
default = serializer(self)
880+
if default is not None and "to" not in default:
881+
default["to"] = None
882+
return default
883+
884+
885+
class Transaction(TransactionGeneric[HexNumber], TransactionTransitionToolConverter):
854886
"""
855887
Generic object that can represent all Ethereum transaction types.
856888
"""

src/ethereum_test_tools/exceptions/__init__.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@
44

55
from .exceptions import (
66
BlockException,
7-
BlockExceptionList,
8-
ExceptionList,
7+
BlockExceptionInstanceOrList,
8+
ExceptionInstanceOrList,
99
TransactionException,
10-
TransactionExceptionList,
10+
TransactionExceptionInstanceOrList,
1111
)
1212

1313
__all__ = [
1414
"BlockException",
15-
"BlockExceptionList",
16-
"ExceptionList",
15+
"BlockExceptionInstanceOrList",
16+
"ExceptionInstanceOrList",
1717
"TransactionException",
18-
"TransactionExceptionList",
18+
"TransactionExceptionInstanceOrList",
1919
]

0 commit comments

Comments
 (0)