Skip to content

Commit 42a1e8c

Browse files
feat(tests): add phase manager to track testing phase
1 parent 10067fc commit 42a1e8c

File tree

8 files changed

+173
-9
lines changed

8 files changed

+173
-9
lines changed

src/ethereum_test_execution/transaction_post.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from ethereum_test_base_types import Address, Alloc, Hash
99
from ethereum_test_forks import Fork
1010
from ethereum_test_rpc import EngineRPC, EthRPC, SendTransactionExceptionError
11-
from ethereum_test_types import Transaction, TransactionTestMetadata
11+
from ethereum_test_types import TestPhase, Transaction, TransactionTestMetadata
1212

1313
from .base import BaseExecute
1414

@@ -40,9 +40,15 @@ def execute(
4040
tx = tx.with_signature_and_sender()
4141
to_address = tx.to
4242
label = to_address.label if isinstance(to_address, Address) else None
43+
phase = (
44+
"testing"
45+
if getattr(tx, "test_phase", None) == TestPhase.EXECUTION
46+
or getattr(block, "test_phase", None) == TestPhase.EXECUTION
47+
else "setup"
48+
)
4349
tx.metadata = TransactionTestMetadata(
4450
test_id=request.node.nodeid,
45-
phase="testing",
51+
phase=phase,
4652
target=label,
4753
tx_index=tx_index,
4854
)

src/ethereum_test_specs/blockchain.py

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,16 @@
5151
)
5252
from ethereum_test_fixtures.common import FixtureBlobSchedule
5353
from ethereum_test_forks import Fork
54-
from ethereum_test_types import Alloc, Environment, Removable, Requests, Transaction, Withdrawal
54+
from ethereum_test_types import (
55+
Alloc,
56+
Environment,
57+
Removable,
58+
Requests,
59+
TestPhase,
60+
TestPhaseManager,
61+
Transaction,
62+
Withdrawal,
63+
)
5564
from ethereum_test_types.block_access_list import BlockAccessList, BlockAccessListExpectation
5665

5766
from .base import BaseTest, OpMode, verify_result
@@ -232,7 +241,13 @@ class Block(Header):
232241
expected_post_state: Alloc | None = None
233242
"""Post state for verification after block execution in BlockchainTest"""
234243
block_access_list: Bytes | None = Field(None)
235-
"""EIP-7928: Block-level access lists (serialized)."""
244+
"""
245+
EIP-7928: Block-level access lists (serialized).
246+
"""
247+
test_phase: TestPhase | None = None
248+
"""
249+
Test phase for this block.
250+
"""
236251

237252
def set_environment(self, env: Environment) -> Environment:
238253
"""
@@ -404,10 +419,11 @@ class BlockchainTest(BaseTest):
404419

405420
pre: Alloc
406421
post: Alloc
407-
blocks: List[Block]
422+
blocks: List[Block] = Field(default_factory=list)
408423
genesis_environment: Environment = Field(default_factory=Environment)
409424
chain_id: int = 1
410425
exclude_full_post_state_in_output: bool = False
426+
test_phase_manager: TestPhaseManager | None = None
411427
"""
412428
Exclude the post state from the fixture output. In this case, the state
413429
verification is only performed based on the state root.
@@ -698,6 +714,31 @@ def verify_post_state(self, t8n, t8n_state: Alloc, expected_state: Alloc | None
698714
print_traces(t8n.get_traces())
699715
raise e
700716

717+
def _get_test_phase_blocks(self) -> List[Block]:
718+
"""Get additional blocks from benchmark manager setup and execution phases."""
719+
assert self.test_phase_manager is not None, "Test phase manager is not set"
720+
721+
blocks = []
722+
if self.test_phase_manager.setup_blocks:
723+
for block in self.test_phase_manager.setup_blocks:
724+
block.test_phase = TestPhase.SETUP
725+
blocks.extend(self.test_phase_manager.setup_blocks)
726+
elif self.test_phase_manager.setup_transactions:
727+
for tx in self.test_phase_manager.setup_transactions:
728+
tx.test_phase = TestPhase.SETUP
729+
blocks.append(Block(txs=self.test_phase_manager.setup_transactions))
730+
731+
if self.test_phase_manager.execution_blocks:
732+
for block in self.test_phase_manager.execution_blocks:
733+
block.test_phase = TestPhase.EXECUTION
734+
blocks.extend(self.test_phase_manager.execution_blocks)
735+
elif self.test_phase_manager.execution_transactions:
736+
for tx in self.test_phase_manager.execution_transactions:
737+
tx.test_phase = TestPhase.EXECUTION
738+
blocks.append(Block(txs=self.test_phase_manager.execution_transactions))
739+
740+
return blocks
741+
701742
def make_fixture(
702743
self,
703744
t8n: TransitionTool,
@@ -712,6 +753,10 @@ def make_fixture(
712753
env = environment_from_parent_header(genesis.header)
713754
head = genesis.header.block_hash
714755
invalid_blocks = 0
756+
757+
if self.test_phase_manager is not None:
758+
self.blocks.extend(self._get_test_phase_blocks())
759+
715760
for i, block in enumerate(self.blocks):
716761
# This is the most common case, the RLP needs to be constructed
717762
# based on the transactions to be included in the block.
@@ -776,6 +821,10 @@ def make_hive_fixture(
776821
env = environment_from_parent_header(genesis.header)
777822
head_hash = genesis.header.block_hash
778823
invalid_blocks = 0
824+
825+
if self.test_phase_manager is not None:
826+
self.blocks.extend(self._get_test_phase_blocks())
827+
779828
for i, block in enumerate(self.blocks):
780829
built_block = self.generate_block_data(
781830
t8n=t8n,
@@ -897,6 +946,8 @@ def execute(
897946
"""Generate the list of test fixtures."""
898947
if execute_format == TransactionPost:
899948
blocks: List[List[Transaction]] = []
949+
if self.test_phase_manager is not None:
950+
self.blocks.extend(self._get_test_phase_blocks())
900951
for block in self.blocks:
901952
blocks += [block.txs]
902953
return TransactionPost(

src/ethereum_test_types/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
compute_create_address,
2727
compute_eofcreate_address,
2828
)
29+
from .phase_manager import TestPhase, TestPhaseManager
2930
from .receipt_types import TransactionReceipt
3031
from .request_types import (
3132
ConsolidationRequest,
@@ -66,6 +67,8 @@
6667
"Removable",
6768
"Requests",
6869
"TestParameterGroup",
70+
"TestPhase",
71+
"TestPhaseManager",
6972
"Transaction",
7073
"TransactionDefaults",
7174
"TransactionReceipt",
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
"""Test phase management for Ethereum tests."""
2+
3+
from contextlib import contextmanager
4+
from contextvars import ContextVar
5+
from enum import Enum
6+
from typing import Iterator, Optional
7+
8+
from pydantic import BaseModel
9+
10+
11+
class TestPhase(Enum):
12+
"""Test phase for state and blockchain tests."""
13+
14+
SETUP = "setup"
15+
EXECUTION = "execution"
16+
17+
18+
_current_phase: ContextVar[Optional[TestPhase]] = ContextVar("test_phase", default=TestPhase.SETUP)
19+
20+
21+
class TestPhaseManager(BaseModel):
22+
"""
23+
Manages test phases and collects transactions and blocks by phase.
24+
This class provides a mechanism for tracking "setup" and "execution" phases,
25+
Only supports "setup" and "execution" phases now.
26+
"""
27+
28+
model_config = {"arbitrary_types_allowed": True}
29+
30+
setup_transactions: list = []
31+
setup_blocks: list = []
32+
execution_transactions: list = []
33+
execution_blocks: list = []
34+
35+
def __init__(self, **data):
36+
"""Initialize the TestPhaseManager with empty transaction and block lists."""
37+
super().__init__(**data)
38+
self.setup_transactions = []
39+
self.setup_blocks = []
40+
self.execution_transactions = []
41+
self.execution_blocks = []
42+
43+
@contextmanager
44+
def setup(self) -> Iterator["TestPhaseManager"]:
45+
"""Context manager for the setup phase of a benchmark test."""
46+
token = _current_phase.set(TestPhase.SETUP)
47+
try:
48+
yield self
49+
finally:
50+
_current_phase.reset(token)
51+
52+
@contextmanager
53+
def execution(self) -> Iterator["TestPhaseManager"]:
54+
"""Context manager for the execution phase of a test."""
55+
token = _current_phase.set(TestPhase.EXECUTION)
56+
try:
57+
yield self
58+
finally:
59+
_current_phase.reset(token)
60+
61+
def add_transaction(self, tx) -> None:
62+
"""Add a transaction to the current phase."""
63+
current_phase = _current_phase.get()
64+
tx.test_phase = current_phase
65+
66+
if current_phase == TestPhase.EXECUTION:
67+
self.execution_transactions.append(tx)
68+
else:
69+
self.setup_transactions.append(tx)
70+
71+
def add_block(self, block) -> None:
72+
"""Add a block to the current phase."""
73+
current_phase = _current_phase.get()
74+
for tx in block.txs:
75+
tx.test_phase = current_phase
76+
77+
if current_phase == TestPhase.EXECUTION:
78+
self.execution_blocks.append(block)
79+
else:
80+
self.setup_blocks.append(block)
81+
82+
def get_current_phase(self) -> Optional[TestPhase]:
83+
"""Get the current test phase."""
84+
return _current_phase.get()

src/ethereum_test_types/transaction_types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@ class Transaction(
292292
zero: ClassVar[Literal[0]] = 0
293293

294294
metadata: TransactionTestMetadata | None = Field(None, exclude=True)
295+
test_phase: str | None = None
295296

296297
model_config = ConfigDict(validate_assignment=True)
297298

src/pytest_plugins/shared/execute_fill.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from ..spec_version_checker.spec_version_checker import EIPSpecTestItem
1616

1717
ALL_FIXTURE_PARAMETERS = {
18+
"test_phase_manager",
1819
"gas_benchmark_value",
1920
"genesis_environment",
2021
"env",

src/pytest_plugins/shared/transaction_fixtures.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from ethereum_test_base_types import AccessList
1010
from ethereum_test_tools import Opcodes as Op
1111
from ethereum_test_types import AuthorizationTuple, Transaction, add_kzg_version
12+
from ethereum_test_types.phase_manager import TestPhaseManager
1213

1314

1415
@pytest.fixture
@@ -161,3 +162,15 @@ def typed_transaction(request, fork):
161162
f"Please add the missing fixture to "
162163
f"src/pytest_plugins/shared/transaction_fixtures.py"
163164
) from e
165+
166+
167+
@pytest.fixture
168+
def phase_manager():
169+
"""Fixture providing access to a test phase manager."""
170+
return TestPhaseManager()
171+
172+
173+
@pytest.fixture
174+
def test_phase_manager():
175+
"""Fixture providing access to a test phase manager for blockchain tests."""
176+
return TestPhaseManager()

tests/benchmark/test_worst_stateful_opcodes.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
compute_create2_address,
2323
compute_create_address,
2424
)
25+
from ethereum_test_types import TestPhaseManager
2526
from ethereum_test_vm import Opcodes as Op
2627

2728
from .helpers import code_loop_precompile_call
@@ -462,14 +463,16 @@ def test_worst_storage_access_warm(
462463
def test_worst_blockhash(
463464
blockchain_test: BlockchainTestFiller,
464465
pre: Alloc,
466+
phase_manager: TestPhaseManager,
465467
gas_benchmark_value: int,
466468
):
467469
"""
468470
Test running a block with as many blockhash accessing oldest allowed block
469471
as possible.
470472
"""
471-
# Create 256 dummy blocks to fill the blockhash window.
472-
blocks = [Block()] * 256
473+
with phase_manager.setup():
474+
for _ in range(256):
475+
phase_manager.add_block(Block())
473476

474477
# Always ask for the oldest allowed BLOCKHASH block.
475478
execution_code = Op.PUSH1(1) + While(
@@ -481,12 +484,14 @@ def test_worst_blockhash(
481484
gas_limit=gas_benchmark_value,
482485
sender=pre.fund_eoa(),
483486
)
484-
blocks.append(Block(txs=[op_tx]))
487+
488+
with phase_manager.execution():
489+
phase_manager.add_transaction(op_tx)
485490

486491
blockchain_test(
487492
pre=pre,
488493
post={},
489-
blocks=blocks,
494+
test_phase_manager=phase_manager,
490495
)
491496

492497

0 commit comments

Comments
 (0)