Skip to content

Commit 510e3b8

Browse files
marioevzspencer-tb
andauthored
refactor(forks): refactor extract fork until from logic (#2051)
* feat(forks): Add helpers and adapters * refactor(plugins): Refactor validity marker logic * feat(forks): Add `children` method * feat(forks): Add BPO and transition forks to get_selected_fork_set * fix: tox * fix(forks): Rollback including BPO forks by default * chore(docs): update changelog. --------- Co-authored-by: spencer-tb <[email protected]>
1 parent 35829a4 commit 510e3b8

File tree

8 files changed

+290
-239
lines changed

8 files changed

+290
-239
lines changed

docs/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Test fixtures for use by clients are available for each release on the [Github r
1212
- Tests for the Prague fork are now marked as stable will be included in the `fixtures_stable.tar.gz` tarball from now on.
1313
- Tests for the Osaka fork are now included in the `fixtures_develop.tar.gz` tarball.
1414
- `fixtures_static.tar.gz` has been deprecated and filled static tests are now included in `fixtures_stable.tar.gz` and `fixtures_develop.tar.gz`.
15+
- When filling fixtures transition forks are included within there respective "to" fork, where `--fork Osaka` will now include `PragueToOsakaAtTime15k`. Previously transitions fork would only be included when filling with `--from Prague --until Osaka` flags.
1516

1617
#### 💥 Important Change for EEST developers
1718

@@ -64,6 +65,7 @@ Users can select any of the artifacts depending on their testing needs for their
6465
- ✨ Opcode classes now validate keyword arguments and raise `ValueError` with clear error messages.
6566
- 🔀 This PR removes the `solc` requirement to fill Python test cases. Regular test contributors no longer need to concern themselves with `solc` and, as such, the `solc-select` dependency has been removed. The remaining tests that used Yul have been ported to the EEST opcode wrapper mini-lang and the use of Yul in Python tests is no longer supported. Maintainers only: To fill the "static" JSON and YAML tests (`./tests/static/`) locally, `solc` (ideally v0.8.24) must be available in your PATH.
6667
- 🔀 Updated default block gas limit from 36M to 45M to match mainnet environment.
68+
- 🔀 Refactor fork logic to include transition forks within there "to" fork ([#2051](https://github.com/ethereum/execution-spec-tests/pull/2051)).
6769

6870
#### `fill`
6971

src/ethereum_test_forks/__init__.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,19 @@
3737
)
3838
from .gas_costs import GasCosts
3939
from .helpers import (
40+
ALL_FORKS,
41+
ALL_FORKS_WITH_TRANSITIONS,
42+
ALL_TRANSITION_FORKS,
4043
Fork,
44+
ForkAdapter,
45+
ForkOrNoneAdapter,
4146
ForkRangeDescriptor,
47+
ForkSet,
48+
ForkSetAdapter,
4249
InvalidForkError,
4350
TransitionFork,
51+
TransitionForkAdapter,
52+
TransitionForkOrNoneAdapter,
4453
forks_from,
4554
forks_from_until,
4655
get_closest_fork,
@@ -53,6 +62,7 @@
5362
get_from_until_fork_set,
5463
get_last_descendants,
5564
get_relative_fork_markers,
65+
get_selected_fork_set,
5666
get_transition_fork_predecessor,
5767
get_transition_fork_successor,
5868
get_transition_forks,
@@ -61,8 +71,17 @@
6171
)
6272

6373
__all__ = [
74+
"ALL_FORKS_WITH_TRANSITIONS",
75+
"ALL_FORKS",
76+
"ALL_TRANSITION_FORKS",
6477
"Fork",
78+
"ForkAdapter",
79+
"ForkOrNoneAdapter",
80+
"ForkSet",
81+
"ForkSetAdapter",
6582
"TransitionFork",
83+
"TransitionForkAdapter",
84+
"TransitionForkOrNoneAdapter",
6685
"ForkAttribute",
6786
"ArrowGlacier",
6887
"Berlin",
@@ -111,6 +130,7 @@
111130
"get_forks",
112131
"get_from_until_fork_set",
113132
"get_last_descendants",
133+
"get_selected_fork_set",
114134
"transition_fork_from_to",
115135
"transition_fork_to",
116136
"GasCosts",

src/ethereum_test_forks/base_fork.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
Mapping,
1111
Optional,
1212
Protocol,
13+
Set,
1314
Sized,
1415
Tuple,
1516
Type,
@@ -166,6 +167,7 @@ class BaseFork(ABC, metaclass=BaseForkMeta):
166167
_solc_name: ClassVar[Optional[str]] = None
167168
_ignore: ClassVar[bool] = False
168169
_bpo_fork: ClassVar[bool] = False
170+
_children: ClassVar[Set[Type["BaseFork"]]] = set()
169171

170172
# make mypy happy
171173
BLOB_CONSTANTS: ClassVar[Dict[str, Union[int, Literal["big"]]]] = {}
@@ -188,6 +190,11 @@ def __init_subclass__(
188190
cls._solc_name = solc_name
189191
cls._ignore = ignore
190192
cls._bpo_fork = bpo_fork
193+
cls._children = set()
194+
base_class = cls.__bases__[0]
195+
assert issubclass(base_class, BaseFork)
196+
if base_class != BaseFork:
197+
base_class._children.add(cls)
191198

192199
# Header information abstract methods
193200
@classmethod
@@ -630,3 +637,8 @@ def parent(cls) -> Type["BaseFork"] | None:
630637
if base_class == BaseFork:
631638
return None
632639
return base_class
640+
641+
@classmethod
642+
def children(cls) -> Set[Type["BaseFork"]]:
643+
"""Return the children forks."""
644+
return set(cls._children)

src/ethereum_test_forks/helpers.py

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
"""Helper methods to resolve forks during test filling."""
22

33
import re
4-
from typing import Annotated, Any, Callable, List, Optional, Set, Type
4+
from typing import Annotated, Any, Callable, FrozenSet, List, Optional, Set, Type
55

66
from pydantic import (
77
BaseModel,
8+
BeforeValidator,
89
ConfigDict,
910
PlainSerializer,
1011
PlainValidator,
12+
TypeAdapter,
1113
ValidatorFunctionWrapHandler,
1214
model_validator,
1315
)
@@ -43,13 +45,20 @@ def __init__(self, message):
4345
if issubclass(fork, TransitionBaseClass) and issubclass(fork, BaseFork):
4446
transition_forks.append(fork)
4547

48+
ALL_FORKS = frozenset(fork for fork in all_forks if not fork.ignore())
49+
50+
ALL_TRANSITION_FORKS = frozenset(transition_forks)
51+
ALL_FORKS_WITH_TRANSITIONS = frozenset(
52+
fork for fork in ALL_FORKS | ALL_TRANSITION_FORKS if not fork.ignore()
53+
)
54+
4655

4756
def get_forks() -> List[Type[BaseFork]]:
4857
"""
4958
Return list of all the fork classes implemented by
5059
`ethereum_test_forks` ordered chronologically by deployment.
5160
"""
52-
return all_forks
61+
return all_forks[:]
5362

5463

5564
def get_deployed_forks() -> List[Type[BaseFork]]:
@@ -86,7 +95,7 @@ def get_closest_fork(fork: Type[BaseFork], solc_version: Version) -> Optional[Ty
8695

8796
def get_transition_forks() -> Set[Type[BaseFork]]:
8897
"""Return all the transition forks."""
89-
return set(transition_forks)
98+
return set(ALL_TRANSITION_FORKS)
9099

91100

92101
def get_transition_fork_predecessor(transition_fork: Type[BaseFork]) -> Type[BaseFork]:
@@ -104,7 +113,9 @@ def get_transition_fork_successor(transition_fork: Type[BaseFork]) -> Type[BaseF
104113

105114

106115
def get_from_until_fork_set(
107-
forks: Set[Type[BaseFork]], forks_from: Set[Type[BaseFork]], forks_until: Set[Type[BaseFork]]
116+
forks: Set[Type[BaseFork]] | FrozenSet[Type[BaseFork]],
117+
forks_from: Set[Type[BaseFork]],
118+
forks_until: Set[Type[BaseFork]],
108119
) -> Set[Type[BaseFork]]:
109120
"""Get fork range from forks_from to forks_until."""
110121
resulting_set = set()
@@ -116,7 +127,9 @@ def get_from_until_fork_set(
116127
return resulting_set
117128

118129

119-
def get_forks_with_no_parents(forks: Set[Type[BaseFork]]) -> Set[Type[BaseFork]]:
130+
def get_forks_with_no_parents(
131+
forks: Set[Type[BaseFork]] | FrozenSet[Type[BaseFork]],
132+
) -> Set[Type[BaseFork]]:
120133
"""Get forks with no parents in the inheritance hierarchy."""
121134
resulting_forks: Set[Type[BaseFork]] = set()
122135
for fork in forks:
@@ -157,6 +170,30 @@ def get_last_descendants(
157170
return resulting_forks
158171

159172

173+
def get_selected_fork_set(
174+
single_fork: Set[Type[BaseFork]],
175+
forks_from: Set[Type[BaseFork]],
176+
forks_until: Set[Type[BaseFork]],
177+
) -> Set[Type[BaseFork]]:
178+
"""
179+
Process sets derived from `--fork`, `--until` and `--from` to return an unified fork
180+
set.
181+
"""
182+
selected_fork_set = set()
183+
if single_fork:
184+
selected_fork_set |= single_fork
185+
else:
186+
if not forks_from:
187+
forks_from = get_forks_with_no_parents(ALL_FORKS)
188+
if not forks_until:
189+
forks_until = get_last_descendants(set(get_deployed_forks()), forks_from)
190+
selected_fork_set = get_from_until_fork_set(ALL_FORKS, forks_from, forks_until)
191+
for fork in list(selected_fork_set):
192+
transition_fork_set = transition_fork_to(fork)
193+
selected_fork_set |= transition_fork_set
194+
return selected_fork_set
195+
196+
160197
def transition_fork_from_to(
161198
fork_from: Type[BaseFork], fork_to: Type[BaseFork]
162199
) -> Type[BaseFork] | None:
@@ -316,7 +353,7 @@ def fork_validator_generator(
316353
cls_name: str, forks: List[Type[BaseFork]]
317354
) -> Callable[[Any], Type[BaseFork]]:
318355
"""Generate a fork validator function."""
319-
forks_dict = {fork.name(): fork for fork in forks}
356+
forks_dict = {fork.name().lower(): fork for fork in forks}
320357

321358
def fork_validator(obj: Any) -> Type[BaseFork]:
322359
"""Get a fork by name or raise an error."""
@@ -325,21 +362,41 @@ def fork_validator(obj: Any) -> Type[BaseFork]:
325362
if isinstance(obj, type) and issubclass(obj, BaseFork):
326363
return obj
327364
if isinstance(obj, str):
328-
if obj in forks_dict:
329-
return forks_dict[obj]
365+
if obj.lower() in forks_dict:
366+
return forks_dict[obj.lower()]
367+
else:
368+
raise InvalidForkError(f"Invalid fork '{obj}' specified")
330369
raise InvalidForkError(f"Invalid {cls_name}: {obj} (type: {type(obj)})")
331370

332371
return fork_validator
333372

334373

374+
def set_before_validator(value: Any) -> Any:
375+
"""Convert a comma-separated string to a validation input for a set."""
376+
if isinstance(value, str):
377+
if value.strip() == "":
378+
return set()
379+
return {v.strip() for v in value.split(",")}
380+
return value
381+
382+
335383
# Annotated Pydantic-Friendly Fork Types
336384
Fork = Annotated[
337385
Type[BaseFork],
338386
PlainSerializer(str),
339387
PlainValidator(fork_validator_generator("Fork", all_forks + transition_forks)),
340388
]
389+
ForkAdapter: TypeAdapter = TypeAdapter(Fork)
390+
ForkOrNoneAdapter: TypeAdapter = TypeAdapter(Fork | None)
391+
ForkSet = Annotated[
392+
Set[Fork],
393+
BeforeValidator(set_before_validator),
394+
]
395+
ForkSetAdapter: TypeAdapter = TypeAdapter(ForkSet)
341396
TransitionFork = Annotated[
342397
Type[BaseFork],
343398
PlainSerializer(str),
344399
PlainValidator(fork_validator_generator("TransitionFork", transition_forks)),
345400
]
401+
TransitionForkAdapter: TypeAdapter = TypeAdapter(TransitionFork)
402+
TransitionForkOrNoneAdapter: TypeAdapter = TypeAdapter(TransitionFork | None)

src/ethereum_test_forks/tests/test_forks.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@
3636
)
3737
from ..helpers import (
3838
Fork,
39+
ForkAdapter,
40+
ForkOrNoneAdapter,
41+
ForkSetAdapter,
3942
forks_from,
4043
forks_from_until,
4144
get_deployed_forks,
@@ -492,3 +495,15 @@ def test_bpo_fork(): # noqa: D103
492495
assert BPO1ToBPO2AtTime15k.bpo_fork() is True
493496
assert BPO2ToBPO3AtTime15k.bpo_fork() is True
494497
assert BPO3ToBPO4AtTime15k.bpo_fork() is True
498+
499+
500+
def test_fork_adapters(): # noqa: D103
501+
assert Osaka == ForkAdapter.validate_python("Osaka")
502+
assert Osaka == ForkOrNoneAdapter.validate_python("Osaka")
503+
assert ForkOrNoneAdapter.validate_python(None) is None
504+
assert {Osaka, Prague} == ForkSetAdapter.validate_python("Osaka, Prague")
505+
assert {Osaka, Prague} == ForkSetAdapter.validate_python("osaka, Prague")
506+
assert {Osaka, Prague} == ForkSetAdapter.validate_python({"osaka", "Prague"})
507+
assert {Osaka} == ForkSetAdapter.validate_python("Osaka")
508+
assert {Osaka} == ForkSetAdapter.validate_python({Osaka})
509+
assert set() == ForkSetAdapter.validate_python("")

src/pytest_plugins/filler/static_filler.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from ethereum_test_specs import BaseStaticTest, BaseTest
2222
from ethereum_test_tools.code.yul import Yul
2323

24-
from ..forks.forks import ValidityMarker, get_intersection_set
24+
from ..forks.forks import ValidityMarker
2525
from ..shared.helpers import labeled_format_parameter_set
2626

2727

@@ -189,19 +189,24 @@ def collect(self: "FillerFile") -> Generator["FillerTestItem", None, None]:
189189
if session.should_generate_format(fixture_format)
190190
)
191191

192-
validity_markers: List[ValidityMarker] = (
193-
ValidityMarker.get_all_validity_markers(key, self.config, function_marks)
192+
test_fork_set = ValidityMarker.get_test_fork_set_from_markers(
193+
iter(function_marks)
194194
)
195-
intersection_set = get_intersection_set(key, validity_markers, self.config)
195+
if not test_fork_set:
196+
pytest.fail(
197+
"The test function's "
198+
f"'{key}' fork validity markers generate "
199+
"an empty fork range. Please check the arguments to its "
200+
f"markers: @pytest.mark.valid_from and "
201+
f"@pytest.mark.valid_until."
202+
)
203+
intersection_set = test_fork_set & self.config.selected_fork_set # type: ignore
196204

197205
extra_function_marks: List[pytest.Mark] = [
198206
mark
199207
for mark in function_marks
200208
if mark.name != "parametrize"
201-
and (
202-
mark.name
203-
not in [v.mark.name for v in validity_markers if v.mark is not None]
204-
)
209+
and not ValidityMarker.is_validity_or_filter_marker(mark.name)
205210
]
206211

207212
for format_with_or_without_label in fixture_formats:

0 commit comments

Comments
 (0)