Skip to content

Commit 47ae439

Browse files
authored
refactor(specs/static_state): Fork string parsing (#1977)
* refactor(specs/static_state): Fork string parsing * fix: tox * docs: Changelog * fix: review comments * fix: spelling again
1 parent ea1a927 commit 47ae439

File tree

3 files changed

+176
-102
lines changed

3 files changed

+176
-102
lines changed

docs/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ Users can select any of the artifacts depending on their testing needs for their
7878
- 🔀 Disabled writing debugging information to the EVM "dump directory" to improve performance. To obtain debug output, the `--evm-dump-dir` flag must now be explicitly set. As a consequence, the now redundant `--skip-evm-dump` option was removed ([#1874](https://github.com/ethereum/execution-spec-tests/pull/1874)).
7979
- ✨ Generate unique addresses with Python for compatible static tests, instead of using hard-coded addresses from legacy static test fillers ([#1781](https://github.com/ethereum/execution-spec-tests/pull/1781)).
8080
- ✨ Added support for the `--benchmark-gas-values` flag in the `fill` command, allowing a single genesis file to be used across different gas limit settings when generating fixtures. ([#1895](https://github.com/ethereum/execution-spec-tests/pull/1895)).
81+
- ✨ Static tests can now specify a maximum fork where they should be filled for ([#1977](https://github.com/ethereum/execution-spec-tests/pull/1977)).
8182

8283
#### `consume`
8384

src/ethereum_test_specs/static_state/expect_section.py

Lines changed: 169 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
"""Expect section structure of ethereum/tests fillers."""
22

3-
from enum import Enum
4-
from typing import Annotated, Any, Dict, List, Mapping, Union
3+
import re
4+
from enum import StrEnum
5+
from typing import Annotated, Any, Dict, Iterator, List, Mapping, Set, Union
56

67
from pydantic import (
78
BaseModel,
89
BeforeValidator,
910
Field,
10-
ValidationInfo,
1111
ValidatorFunctionWrapHandler,
1212
field_validator,
1313
model_validator,
@@ -22,7 +22,7 @@
2222
Storage,
2323
)
2424
from ethereum_test_exceptions import TransactionExceptionInstanceOrList
25-
from ethereum_test_forks import get_forks
25+
from ethereum_test_forks import Fork, get_forks
2626
from ethereum_test_types import Alloc
2727

2828
from .common import (
@@ -86,6 +86,14 @@ def resolve(self, tags: TagDict) -> Storage:
8686
storage[resolved_key] = value
8787
return storage
8888

89+
def __contains__(self, key: Address) -> bool:
90+
"""Check if the storage contains a key."""
91+
return key in self.root
92+
93+
def __iter__(self) -> Iterator[ValueOrCreateTagInFiller]: # type: ignore[override]
94+
"""Iterate over the storage."""
95+
return iter(self.root)
96+
8997

9098
class AccountInExpectSection(BaseModel, TagDependentData):
9199
"""Class that represents an account in expect section filler."""
@@ -127,66 +135,116 @@ def resolve(self, tags: TagDict) -> Account:
127135
return Account(**account_kwargs)
128136

129137

130-
class CMP(Enum):
138+
class CMP(StrEnum):
131139
"""Comparison action."""
132140

133-
GT = 1
134-
LT = 2
135-
LE = 3
136-
GE = 4
137-
EQ = 5
138-
139-
140-
def parse_networks(fork_with_operand: str) -> List[str]:
141-
"""Parse fork_with_operand `>=Cancun` into [Cancun, Prague, ...]."""
142-
parsed_forks: List[str] = []
143-
all_forks_by_name = [fork.name() for fork in get_forks()]
144-
145-
action: CMP = CMP.EQ
146-
fork: str = fork_with_operand
147-
if fork_with_operand[:1] == "<":
148-
action = CMP.LT
149-
fork = fork_with_operand[1:]
150-
if fork_with_operand[:1] == ">":
151-
action = CMP.GT
152-
fork = fork_with_operand[1:]
153-
if fork_with_operand[:2] == "<=":
154-
action = CMP.LE
155-
fork = fork_with_operand[2:]
156-
if fork_with_operand[:2] == ">=":
157-
action = CMP.GE
158-
fork = fork_with_operand[2:]
159-
160-
if action == CMP.EQ:
161-
fork = fork_with_operand
162-
163-
# translate unsupported fork names
164-
if fork == "EIP158":
165-
fork = "Byzantium"
166-
167-
if action == CMP.EQ:
168-
parsed_forks.append(fork)
169-
return parsed_forks
170-
171-
try:
172-
# print(all_forks_by_name)
173-
idx = all_forks_by_name.index(fork)
174-
# ['Frontier', 'Homestead', 'Byzantium', 'Constantinople', 'ConstantinopleFix',
175-
# 'Istanbul', 'MuirGlacier', 'Berlin', 'London', 'ArrowGlacier', 'GrayGlacier',
176-
# 'Paris', 'Shanghai', 'Cancun', 'Prague', 'Osaka']
177-
except ValueError:
178-
raise ValueError(f"Unsupported fork: {fork}") from Exception
179-
180-
if action == CMP.GE:
181-
parsed_forks = all_forks_by_name[idx:]
182-
elif action == CMP.GT:
183-
parsed_forks = all_forks_by_name[idx + 1 :]
184-
elif action == CMP.LE:
185-
parsed_forks = all_forks_by_name[: idx + 1]
186-
elif action == CMP.LT:
187-
parsed_forks = all_forks_by_name[:idx]
188-
189-
return parsed_forks
141+
LE = "<="
142+
GE = ">="
143+
LT = "<"
144+
GT = ">"
145+
EQ = "="
146+
147+
148+
class ForkConstraint(BaseModel):
149+
"""Single fork with an operand."""
150+
151+
operand: CMP
152+
fork: Fork
153+
154+
@field_validator("fork", mode="before")
155+
@classmethod
156+
def parse_fork_synonyms(cls, value: Any):
157+
"""Resolve fork synonyms."""
158+
if value == "EIP158":
159+
value = "Byzantium"
160+
return value
161+
162+
@model_validator(mode="before")
163+
@classmethod
164+
def parse_from_string(cls, data: Any) -> Any:
165+
"""Parse a fork with operand from a string."""
166+
if isinstance(data, str):
167+
for cmp in CMP:
168+
if data.startswith(cmp):
169+
fork = data.removeprefix(cmp)
170+
return {
171+
"operand": cmp,
172+
"fork": fork,
173+
}
174+
return {
175+
"operand": CMP.EQ,
176+
"fork": data,
177+
}
178+
return data
179+
180+
def match(self, fork: Fork) -> bool:
181+
"""Return whether the fork satisfies the operand evaluation."""
182+
match self.operand:
183+
case CMP.LE:
184+
return fork <= self.fork
185+
case CMP.GE:
186+
return fork >= self.fork
187+
case CMP.LT:
188+
return fork < self.fork
189+
case CMP.GT:
190+
return fork > self.fork
191+
case CMP.EQ:
192+
return fork == self.fork
193+
case _:
194+
raise ValueError(f"Invalid operand: {self.operand}")
195+
196+
197+
class ForkSet(EthereumTestRootModel):
198+
"""Set of forks."""
199+
200+
root: Set[Fork]
201+
202+
@model_validator(mode="before")
203+
@classmethod
204+
def parse_from_list_or_string(cls, value: Any) -> Set[Fork]:
205+
"""Parse fork_with_operand `>=Cancun` into {Cancun, Prague, ...}."""
206+
fork_set: Set[Fork] = set()
207+
if not isinstance(value, list):
208+
value = [value]
209+
210+
for fork_with_operand in value:
211+
matches = re.findall(r"(<=|<|>=|>|=)([^<>=]+)", fork_with_operand)
212+
if matches:
213+
all_fork_constraints = [
214+
ForkConstraint.model_validate(f"{op}{fork.strip()}") for op, fork in matches
215+
]
216+
else:
217+
all_fork_constraints = [ForkConstraint.model_validate(fork_with_operand.strip())]
218+
219+
for fork in get_forks():
220+
for f in all_fork_constraints:
221+
if not f.match(fork):
222+
# If any constraint does not match, skip adding
223+
break
224+
else:
225+
# All constraints match, add the fork to the set
226+
fork_set.add(fork)
227+
228+
return fork_set
229+
230+
def __hash__(self) -> int:
231+
"""Return the hash of the fork set."""
232+
h = hash(None)
233+
for fork in sorted([str(f) for f in self]):
234+
h ^= hash(fork)
235+
return h
236+
237+
def __contains__(self, fork: Fork) -> bool:
238+
"""Check if the fork set contains a fork."""
239+
return fork in self.root
240+
241+
def __iter__(self) -> Iterator[Fork]: # type: ignore[override]
242+
"""Iterate over the fork set."""
243+
return iter(self.root)
244+
245+
def __len__(self) -> int:
246+
"""Return the length of the fork set."""
247+
return len(self.root)
190248

191249

192250
class ResultInFiller(EthereumTestRootModel, TagDependentData):
@@ -228,44 +286,61 @@ def resolve(self, tags: TagDict) -> Alloc:
228286
post[resolved_address] = account.resolve(tags)
229287
return post
230288

289+
def __contains__(self, address: Address) -> bool:
290+
"""Check if the result contains an address."""
291+
return address in self.root
292+
293+
def __iter__(self) -> Iterator[AddressOrCreateTagInFiller]: # type: ignore[override]
294+
"""Iterate over the result."""
295+
return iter(self.root)
296+
297+
def __len__(self) -> int:
298+
"""Return the length of the result."""
299+
return len(self.root)
300+
301+
302+
class ExpectException(EthereumTestRootModel):
303+
"""Expect exception model."""
304+
305+
root: Dict[ForkSet, TransactionExceptionInstanceOrList]
306+
307+
def __getitem__(self, fork: Fork) -> TransactionExceptionInstanceOrList:
308+
"""Get an expectation for a given fork."""
309+
for k in self.root:
310+
if fork in k:
311+
return self.root[k]
312+
raise KeyError(f"Fork {fork} not found in expectations.")
313+
314+
def __contains__(self, fork: Fork) -> bool:
315+
"""Check if the expect exception contains a fork."""
316+
return fork in self.root
317+
318+
def __iter__(self) -> Iterator[ForkSet]: # type: ignore[override]
319+
"""Iterate over the expect exception."""
320+
return iter(self.root)
321+
322+
def __len__(self) -> int:
323+
"""Return the length of the expect exception."""
324+
return len(self.root)
325+
231326

232327
class ExpectSectionInStateTestFiller(CamelModel):
233328
"""Expect section in state test filler."""
234329

235330
indexes: Indexes = Field(default_factory=Indexes)
236-
network: List[str]
331+
network: ForkSet
237332
result: ResultInFiller
238-
expect_exception: Dict[str, TransactionExceptionInstanceOrList] | None = None
239-
240-
@field_validator("network", mode="before")
241-
@classmethod
242-
def parse_networks(cls, network: List[str], info: ValidationInfo) -> List[str]:
243-
"""Parse networks into array of forks."""
244-
forks: List[str] = []
245-
for net in network:
246-
forks.extend(parse_networks(net))
247-
return forks
248-
249-
@field_validator("expect_exception", mode="before")
250-
@classmethod
251-
def parse_expect_exception(
252-
cls, expect_exception: Dict[str, str] | None, info: ValidationInfo
253-
) -> Dict[str, str] | None:
254-
"""Parse operand networks in exceptions."""
255-
if expect_exception is None:
256-
return expect_exception
257-
258-
parsed_expect_exception: Dict[str, str] = {}
259-
for fork_with_operand, exception in expect_exception.items():
260-
forks: List[str] = parse_networks(fork_with_operand)
261-
for fork in forks:
262-
if fork in parsed_expect_exception:
263-
raise ValueError(
264-
"Expect exception has redundant fork with multiple exceptions!"
265-
)
266-
parsed_expect_exception[fork] = exception
267-
268-
return parsed_expect_exception
333+
expect_exception: ExpectException | None = None
334+
335+
def model_post_init(self, __context):
336+
"""Validate that the expectation is coherent."""
337+
if self.expect_exception is None:
338+
return
339+
all_forks: Set[Fork] = set()
340+
for current_fork_set in self.expect_exception:
341+
for fork in current_fork_set:
342+
assert fork not in all_forks
343+
all_forks.add(fork)
269344

270345
def has_index(self, d: int, g: int, v: int) -> bool:
271346
"""Check if there is index set in indexes."""

src/ethereum_test_specs/static_state/state_static.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Ethereum General State Test filler static test spec parser."""
22

3-
from typing import Callable, ClassVar, List, Self, Union
3+
from typing import Callable, ClassVar, List, Self, Set, Union
44

55
import pytest
66
from _pytest.mark.structures import ParameterSet
@@ -156,7 +156,7 @@ def test_state_vectors(
156156
):
157157
for expect in self.expect:
158158
if expect.has_index(d, g, v):
159-
if fork.name() in expect.network:
159+
if fork in expect.network:
160160
tx_tag_dependencies = self.transaction.tag_dependencies()
161161
result_tag_dependencies = expect.result.tag_dependencies()
162162
all_dependencies = {**tx_tag_dependencies, **result_tag_dependencies}
@@ -165,7 +165,7 @@ def test_state_vectors(
165165
exception = (
166166
None
167167
if expect.expect_exception is None
168-
else expect.expect_exception[fork.name()]
168+
else expect.expect_exception[fork]
169169
)
170170
tx = self.transaction.get_transaction(tags, d, g, v, exception)
171171
post = expect.result.resolve(tags)
@@ -193,9 +193,7 @@ def test_state_vectors(
193193

194194
def get_valid_at_forks(self) -> List[str]:
195195
"""Return list of forks that are valid for this test."""
196-
fork_list: List[str] = []
196+
fork_set: Set[Fork] = set()
197197
for expect in self.expect:
198-
for fork in expect.network:
199-
if fork not in fork_list:
200-
fork_list.append(fork)
201-
return fork_list
198+
fork_set.update(expect.network)
199+
return sorted([str(f) for f in fork_set])

0 commit comments

Comments
 (0)