Skip to content

Commit 2fe8ca5

Browse files
marioevzspencer-tb
andauthored
feat(specs,tests): exception_test marker (#1436)
* feat(specs): Introduce/enforce `negative` marker * feat(tests): Add marker to all invalid transaction/block tests * changelog * refactor(specs): Rename marker to `pytest.mark.exception_test` * refactor(tests): Rename marker to `pytest.mark.exception_test` * refactor(specs): Make `request` a `PrivateAttr` * nit * fix(specs): Review suggestions Co-authored-by: spencer <spencer.taylor-brown@ethereum.org> --------- Co-authored-by: spencer <spencer.taylor-brown@ethereum.org>
1 parent 23a354e commit 2fe8ca5

File tree

30 files changed

+177
-112
lines changed

30 files changed

+177
-112
lines changed

docs/CHANGELOG.md

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

1515
- ✨ The `static_filler` plug-in now has support for static state tests (from [GeneralStateTests](https://github.com/ethereum/tests/tree/develop/src/GeneralStateTestsFiller)) ([#1362](https://github.com/ethereum/execution-spec-tests/pull/1362)).
16+
- ✨ Introduce `pytest.mark.exception_test` to mark tests that contain an invalid transaction or block ([#1436](https://github.com/ethereum/execution-spec-tests/pull/1436)).
1617
- 🐞 Fix `DeprecationWarning: Pickle, copy, and deepcopy support will be removed from itertools in Python 3.14.` by avoiding use `itertools` object in the spec `BaseTest` pydantic model ([#1414](https://github.com/ethereum/execution-spec-tests/pull/1414)).
1718

1819
#### `consume`

src/cli/eofwrap.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,6 @@ def _wrap_fixture(self, fixture: BlockchainFixture, traces: bool):
308308
raise TypeError("not a FixtureBlock")
309309

310310
result = test.generate(
311-
request=None, # type: ignore
312311
t8n=t8n,
313312
fork=Osaka,
314313
fixture_format=BlockchainFixture,

src/ethereum_clis/tests/test_transition_tools_support.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,6 @@ def test_t8n_support(fork: Fork, installed_t8n: TransitionTool):
236236
blocks=[block_1, block_2],
237237
)
238238
test.generate(
239-
request=None, # type: ignore
240239
t8n=installed_t8n,
241240
fork=fork,
242241
fixture_format=BlockchainFixture,

src/ethereum_test_specs/base.py

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
from functools import reduce
55
from os import path
66
from pathlib import Path
7-
from typing import Callable, ClassVar, Dict, Generator, List, Optional, Sequence
7+
from typing import Callable, ClassVar, Dict, Generator, List, Optional, Sequence, Type, TypeVar
88

99
import pytest
10-
from pydantic import BaseModel, Field
10+
from pydantic import BaseModel, Field, PrivateAttr
1111

1212
from ethereum_clis import Result, TransitionTool
1313
from ethereum_test_base_types import to_hex
@@ -41,11 +41,16 @@ def verify_result(result: Result, env: Environment):
4141
assert result.withdrawals_root == to_hex(Withdrawal.list_root(env.withdrawals))
4242

4343

44+
T = TypeVar("T", bound="BaseTest")
45+
46+
4447
class BaseTest(BaseModel):
4548
"""Represents a base Ethereum test which must return a single test fixture."""
4649

4750
tag: str = ""
4851

52+
_request: pytest.FixtureRequest | None = PrivateAttr(None)
53+
4954
# Transition tool specific fields
5055
t8n_dump_dir: Path | None = Field(None, exclude=True)
5156
t8n_call_counter: int = Field(0, exclude=True)
@@ -65,6 +70,22 @@ def discard_fixture_format_by_marks(
6570
"""Discard a fixture format from filling if the appropriate marker is used."""
6671
return False
6772

73+
@classmethod
74+
def from_test(
75+
cls: Type[T],
76+
*,
77+
base_test: "BaseTest",
78+
**kwargs,
79+
) -> T:
80+
"""Create a test in a different format from a base test."""
81+
new_instance = cls(
82+
tag=base_test.tag,
83+
t8n_dump_dir=base_test.t8n_dump_dir,
84+
**kwargs,
85+
)
86+
new_instance._request = base_test._request
87+
return new_instance
88+
6889
@classmethod
6990
def discard_execute_format_by_marks(
7091
cls,
@@ -79,7 +100,6 @@ def discard_execute_format_by_marks(
79100
def generate(
80101
self,
81102
*,
82-
request: pytest.FixtureRequest,
83103
t8n: TransitionTool,
84104
fork: Fork,
85105
fixture_format: FixtureFormat,
@@ -119,5 +139,51 @@ def get_next_transition_tool_output_path(self) -> str:
119139
str(current_value),
120140
)
121141

142+
def is_slow_test(self) -> bool:
143+
"""Check if the test is slow."""
144+
if self._request is not None and hasattr(self._request, "node"):
145+
return self._request.node.get_closest_marker("slow") is not None
146+
return False
147+
148+
def is_exception_test(self) -> bool | None:
149+
"""
150+
Check if the test is an exception test (invalid block, invalid transaction).
151+
152+
`None` is returned if it's not possible to determine if the test is negative or not.
153+
This is the case when the test is not run in pytest.
154+
"""
155+
if self._request is not None and hasattr(self._request, "node"):
156+
return self._request.node.get_closest_marker("exception_test") is not None
157+
return None
158+
159+
def node_id(self) -> str:
160+
"""Return the node ID of the test."""
161+
if self._request is not None and hasattr(self._request, "node"):
162+
return self._request.node.nodeid
163+
return ""
164+
165+
def check_exception_test(
166+
self,
167+
*,
168+
exception: bool,
169+
):
170+
"""Compare the test marker against the outcome of the test."""
171+
negative_test_marker = self.is_exception_test()
172+
if negative_test_marker is None:
173+
return
174+
if negative_test_marker != exception:
175+
if exception:
176+
raise Exception(
177+
"Test produced an invalid block or transaction but was not marked with the "
178+
"`exception_test` marker. Add the `@pytest.mark.exception_test` decorator "
179+
"to the test."
180+
)
181+
else:
182+
raise Exception(
183+
"Test didn't produce an invalid block or transaction but was marked with the "
184+
"`exception_test` marker. Remove the `@pytest.mark.exception_test` decorator "
185+
"from the test."
186+
)
187+
122188

123189
TestSpec = Callable[[Fork], Generator[BaseTest, None, None]]

src/ethereum_test_specs/blockchain.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949

5050
from .base import BaseTest, verify_result
5151
from .debugging import print_traces
52-
from .helpers import is_slow_test, verify_block, verify_transactions
52+
from .helpers import verify_block, verify_transactions
5353

5454

5555
def environment_from_parent_header(parent: "FixtureHeader") -> "Environment":
@@ -562,7 +562,6 @@ def make_fixture(
562562
t8n: TransitionTool,
563563
fork: Fork,
564564
eips: Optional[List[int]] = None,
565-
slow: bool = False,
566565
) -> BlockchainFixture:
567566
"""Create a fixture from the blockchain test definition."""
568567
fixture_blocks: List[FixtureBlock | InvalidFixtureBlock] = []
@@ -572,7 +571,7 @@ def make_fixture(
572571
alloc = pre
573572
env = environment_from_parent_header(genesis.header)
574573
head = genesis.header.block_hash
575-
574+
invalid_blocks = 0
576575
for block in self.blocks:
577576
if block.rlp is None:
578577
# This is the most common case, the RLP needs to be constructed
@@ -585,7 +584,7 @@ def make_fixture(
585584
previous_env=env,
586585
previous_alloc=alloc,
587586
eips=eips,
588-
slow=slow,
587+
slow=self.is_slow_test(),
589588
)
590589
fixture_block = FixtureBlockBase(
591590
header=header,
@@ -616,6 +615,7 @@ def make_fixture(
616615
),
617616
),
618617
)
618+
invalid_blocks += 1
619619
else:
620620
assert block.exception is not None, (
621621
"test correctness: if the block's rlp is hard-coded, "
@@ -627,12 +627,13 @@ def make_fixture(
627627
expect_exception=block.exception,
628628
),
629629
)
630+
invalid_blocks += 1
630631

631632
if block.expected_post_state:
632633
self.verify_post_state(
633634
t8n, t8n_state=alloc, expected_state=block.expected_post_state
634635
)
635-
636+
self.check_exception_test(exception=invalid_blocks > 0)
636637
self.verify_post_state(t8n, t8n_state=alloc)
637638
network_info = BlockchainTest.network_info(fork, eips)
638639
return BlockchainFixture(
@@ -655,7 +656,6 @@ def make_hive_fixture(
655656
t8n: TransitionTool,
656657
fork: Fork,
657658
eips: Optional[List[int]] = None,
658-
slow: bool = False,
659659
) -> BlockchainEngineFixture:
660660
"""Create a hive fixture from the blocktest definition."""
661661
fixture_payloads: List[FixtureEngineNewPayload] = []
@@ -664,7 +664,7 @@ def make_hive_fixture(
664664
alloc = pre
665665
env = environment_from_parent_header(genesis.header)
666666
head_hash = genesis.header.block_hash
667-
667+
invalid_blocks = 0
668668
for block in self.blocks:
669669
header, txs, requests, new_alloc, new_env = self.generate_block_data(
670670
t8n=t8n,
@@ -673,7 +673,7 @@ def make_hive_fixture(
673673
previous_env=env,
674674
previous_alloc=alloc,
675675
eips=eips,
676-
slow=slow,
676+
slow=self.is_slow_test(),
677677
)
678678
if block.rlp is None:
679679
fixture_payloads.append(
@@ -691,12 +691,14 @@ def make_hive_fixture(
691691
alloc = new_alloc
692692
env = apply_new_parent(env, header)
693693
head_hash = header.block_hash
694+
else:
695+
invalid_blocks += 1
694696

695697
if block.expected_post_state:
696698
self.verify_post_state(
697699
t8n, t8n_state=alloc, expected_state=block.expected_post_state
698700
)
699-
701+
self.check_exception_test(exception=invalid_blocks > 0)
700702
fcu_version = fork.engine_forkchoice_updated_version(header.number, header.timestamp)
701703
assert fcu_version is not None, (
702704
"A hive fixture was requested but no forkchoice update is defined."
@@ -752,7 +754,6 @@ def make_hive_fixture(
752754

753755
def generate(
754756
self,
755-
request: pytest.FixtureRequest,
756757
t8n: TransitionTool,
757758
fork: Fork,
758759
fixture_format: FixtureFormat,
@@ -761,9 +762,9 @@ def generate(
761762
"""Generate the BlockchainTest fixture."""
762763
t8n.reset_traces()
763764
if fixture_format == BlockchainEngineFixture:
764-
return self.make_hive_fixture(t8n, fork, eips, slow=is_slow_test(request))
765+
return self.make_hive_fixture(t8n, fork, eips)
765766
elif fixture_format == BlockchainFixture:
766-
return self.make_fixture(t8n, fork, eips, slow=is_slow_test(request))
767+
return self.make_fixture(t8n, fork, eips)
767768

768769
raise Exception(f"Unknown fixture format: {fixture_format}")
769770

src/ethereum_test_specs/eof.py

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,6 @@ def model_post_init(self, __context):
305305
def make_eof_test_fixture(
306306
self,
307307
*,
308-
request: pytest.FixtureRequest,
309308
fork: Fork,
310309
eips: Optional[List[int]],
311310
) -> EOFFixture:
@@ -316,7 +315,7 @@ def make_eof_test_fixture(
316315
f"Duplicate EOF test: {container_bytes}, "
317316
f"existing test: {existing_tests[container_bytes]}"
318317
)
319-
existing_tests[container_bytes] = request.node.nodeid
318+
existing_tests[container_bytes] = self.node_id()
320319
vectors = [
321320
Vector(
322321
code=container_bytes,
@@ -438,18 +437,17 @@ def generate_eof_contract_create_transaction(self) -> Transaction:
438437

439438
def generate_state_test(self, fork: Fork) -> StateTest:
440439
"""Generate the StateTest filler."""
441-
return StateTest(
440+
return StateTest.from_test(
441+
base_test=self,
442442
pre=self.pre,
443443
tx=self.generate_eof_contract_create_transaction(),
444444
env=Environment(),
445445
post=self.post,
446-
t8n_dump_dir=self.t8n_dump_dir,
447446
)
448447

449448
def generate(
450449
self,
451450
*,
452-
request: pytest.FixtureRequest,
453451
t8n: TransitionTool,
454452
fork: Fork,
455453
eips: Optional[List[int]] = None,
@@ -458,10 +456,10 @@ def generate(
458456
) -> BaseFixture:
459457
"""Generate the BlockchainTest fixture."""
460458
if fixture_format == EOFFixture:
461-
return self.make_eof_test_fixture(request=request, fork=fork, eips=eips)
459+
return self.make_eof_test_fixture(fork=fork, eips=eips)
462460
elif fixture_format in StateTest.supported_fixture_formats:
463461
return self.generate_state_test(fork).generate(
464-
request=request, t8n=t8n, fork=fork, fixture_format=fixture_format, eips=eips
462+
t8n=t8n, fork=fork, fixture_format=fixture_format, eips=eips
465463
)
466464
raise Exception(f"Unknown fixture format: {fixture_format}")
467465

@@ -583,18 +581,17 @@ def generate_state_test(self, fork: Fork) -> StateTest:
583581
assert self.pre is not None, "pre must be set to generate a StateTest."
584582
assert self.post is not None, "post must be set to generate a StateTest."
585583

586-
return StateTest(
584+
return StateTest.from_test(
585+
base_test=self,
587586
pre=self.pre,
588587
tx=self,
589588
env=self.env,
590589
post=self.post,
591-
t8n_dump_dir=self.t8n_dump_dir,
592590
)
593591

594592
def generate(
595593
self,
596594
*,
597-
request: pytest.FixtureRequest,
598595
t8n: TransitionTool,
599596
fork: Fork,
600597
eips: Optional[List[int]] = None,
@@ -606,11 +603,11 @@ def generate(
606603
if Bytes(self.container) in existing_tests:
607604
# Gracefully skip duplicate tests because one EOFStateTest can generate multiple
608605
# state fixtures with the same data.
609-
pytest.skip(f"Duplicate EOF container on EOFStateTest: {request.node.nodeid}")
610-
return self.make_eof_test_fixture(request=request, fork=fork, eips=eips)
606+
pytest.skip(f"Duplicate EOF container on EOFStateTest: {self.node_id()}")
607+
return self.make_eof_test_fixture(fork=fork, eips=eips)
611608
elif fixture_format in StateTest.supported_fixture_formats:
612609
return self.generate_state_test(fork).generate(
613-
request=request, t8n=t8n, fork=fork, fixture_format=fixture_format, eips=eips
610+
t8n=t8n, fork=fork, fixture_format=fixture_format, eips=eips
614611
)
615612

616613
raise Exception(f"Unknown fixture format: {fixture_format}")

src/ethereum_test_specs/helpers.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
from enum import Enum
55
from typing import Any, Dict, List
66

7-
import pytest
8-
97
from ethereum_clis import Result
108
from ethereum_test_exceptions import (
119
BlockException,
@@ -306,10 +304,3 @@ def verify_block(
306304
got_exception=result.block_exception,
307305
)
308306
info.verify(strict_match=transition_tool_exceptions_reliable)
309-
310-
311-
def is_slow_test(request: pytest.FixtureRequest) -> bool:
312-
"""Check if the test is slow."""
313-
if hasattr(request, "node"):
314-
return request.node.get_closest_marker("slow") is not None
315-
return False

0 commit comments

Comments
 (0)