Skip to content

Commit ef9acbe

Browse files
feat(tests): add phase manager to track testing phase
1 parent 1779001 commit ef9acbe

File tree

8 files changed

+170
-8
lines changed

8 files changed

+170
-8
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

@@ -38,9 +38,15 @@ def execute(
3838
tx = tx.with_signature_and_sender()
3939
to_address = tx.to
4040
label = to_address.label if isinstance(to_address, Address) else None
41+
phase = (
42+
"testing"
43+
if getattr(tx, "test_phase", None) == TestPhase.EXECUTION
44+
or getattr(block, "test_phase", None) == TestPhase.EXECUTION
45+
else "setup"
46+
)
4147
tx.metadata = TransactionTestMetadata(
4248
test_id=request.node.nodeid,
43-
phase="testing",
49+
phase=phase,
4450
target=label,
4551
tx_index=tx_index,
4652
)

src/ethereum_test_specs/blockchain.py

Lines changed: 51 additions & 2 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
@@ -250,6 +259,10 @@ class Block(Header):
250259
"""
251260
EIP-7928: Block-level access lists (serialized).
252261
"""
262+
test_phase: TestPhase | None = None
263+
"""
264+
Test phase for this block.
265+
"""
253266

254267
def set_environment(self, env: Environment) -> Environment:
255268
"""
@@ -423,10 +436,11 @@ class BlockchainTest(BaseTest):
423436

424437
pre: Alloc
425438
post: Alloc
426-
blocks: List[Block]
439+
blocks: List[Block] = Field(default_factory=list)
427440
genesis_environment: Environment = Field(default_factory=Environment)
428441
chain_id: int = 1
429442
exclude_full_post_state_in_output: bool = False
443+
test_phase_manager: TestPhaseManager | None = None
430444
"""
431445
Exclude the post state from the fixture output.
432446
In this case, the state verification is only performed based on the state root.
@@ -706,6 +720,31 @@ def verify_post_state(self, t8n, t8n_state: Alloc, expected_state: Alloc | None
706720
print_traces(t8n.get_traces())
707721
raise e
708722

723+
def _get_test_phase_blocks(self) -> List[Block]:
724+
"""Get additional blocks from benchmark manager setup and execution phases."""
725+
assert self.test_phase_manager is not None, "Test phase manager is not set"
726+
727+
blocks = []
728+
if self.test_phase_manager.setup_blocks:
729+
for block in self.test_phase_manager.setup_blocks:
730+
block.test_phase = TestPhase.SETUP
731+
blocks.extend(self.test_phase_manager.setup_blocks)
732+
elif self.test_phase_manager.setup_transactions:
733+
for tx in self.test_phase_manager.setup_transactions:
734+
tx.test_phase = TestPhase.SETUP
735+
blocks.append(Block(txs=self.test_phase_manager.setup_transactions))
736+
737+
if self.test_phase_manager.execution_blocks:
738+
for block in self.test_phase_manager.execution_blocks:
739+
block.test_phase = TestPhase.EXECUTION
740+
blocks.extend(self.test_phase_manager.execution_blocks)
741+
elif self.test_phase_manager.execution_transactions:
742+
for tx in self.test_phase_manager.execution_transactions:
743+
tx.test_phase = TestPhase.EXECUTION
744+
blocks.append(Block(txs=self.test_phase_manager.execution_transactions))
745+
746+
return blocks
747+
709748
def make_fixture(
710749
self,
711750
t8n: TransitionTool,
@@ -720,6 +759,10 @@ def make_fixture(
720759
env = environment_from_parent_header(genesis.header)
721760
head = genesis.header.block_hash
722761
invalid_blocks = 0
762+
763+
if self.test_phase_manager is not None:
764+
self.blocks.extend(self._get_test_phase_blocks())
765+
723766
for i, block in enumerate(self.blocks):
724767
# This is the most common case, the RLP needs to be constructed
725768
# based on the transactions to be included in the block.
@@ -783,6 +826,10 @@ def make_hive_fixture(
783826
env = environment_from_parent_header(genesis.header)
784827
head_hash = genesis.header.block_hash
785828
invalid_blocks = 0
829+
830+
if self.test_phase_manager is not None:
831+
self.blocks.extend(self._get_test_phase_blocks())
832+
786833
for i, block in enumerate(self.blocks):
787834
built_block = self.generate_block_data(
788835
t8n=t8n,
@@ -904,6 +951,8 @@ def execute(
904951
"""Generate the list of test fixtures."""
905952
if execute_format == TransactionPost:
906953
blocks: List[List[Transaction]] = []
954+
if self.test_phase_manager is not None:
955+
self.blocks.extend(self._get_test_phase_blocks())
907956
for block in self.blocks:
908957
blocks += [block.txs]
909958
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
@@ -279,6 +279,7 @@ class Transaction(
279279
zero: ClassVar[Literal[0]] = 0
280280

281281
metadata: TransactionTestMetadata | None = Field(None, exclude=True)
282+
test_phase: str | None = None
282283

283284
model_config = ConfigDict(validate_assignment=True)
284285

src/pytest_plugins/shared/execute_fill.py

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

1515
ALL_FIXTURE_PARAMETERS = {
16+
"test_phase_manager",
1617
"genesis_environment",
1718
"env",
1819
}

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
@@ -159,3 +160,15 @@ def typed_transaction(request, fork):
159160
f"Please add the missing fixture to "
160161
f"src/pytest_plugins/shared/transaction_fixtures.py"
161162
) from e
163+
164+
165+
@pytest.fixture
166+
def phase_manager():
167+
"""Fixture providing access to a test phase manager."""
168+
return TestPhaseManager()
169+
170+
171+
@pytest.fixture
172+
def test_phase_manager():
173+
"""Fixture providing access to a test phase manager for blockchain tests."""
174+
return TestPhaseManager()

tests/benchmark/test_worst_stateful_opcodes.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
compute_create_address,
2727
)
2828
from ethereum_test_tools.vm.opcode import Opcodes as Op
29+
from ethereum_test_types import TestPhaseManager
2930

3031
from .helpers import code_loop_precompile_call
3132

@@ -453,11 +454,13 @@ def test_worst_storage_access_warm(
453454
def test_worst_blockhash(
454455
blockchain_test: BlockchainTestFiller,
455456
pre: Alloc,
457+
phase_manager: TestPhaseManager,
456458
gas_benchmark_value: int,
457459
):
458460
"""Test running a block with as many blockhash accessing oldest allowed block as possible."""
459-
# Create 256 dummy blocks to fill the blockhash window.
460-
blocks = [Block()] * 256
461+
with phase_manager.setup():
462+
for _ in range(256):
463+
phase_manager.add_block(Block())
461464

462465
# Always ask for the oldest allowed BLOCKHASH block.
463466
execution_code = Op.PUSH1(1) + While(
@@ -469,12 +472,14 @@ def test_worst_blockhash(
469472
gas_limit=gas_benchmark_value,
470473
sender=pre.fund_eoa(),
471474
)
472-
blocks.append(Block(txs=[op_tx]))
475+
476+
with phase_manager.execution():
477+
phase_manager.add_transaction(op_tx)
473478

474479
blockchain_test(
475480
pre=pre,
476481
post={},
477-
blocks=blocks,
482+
test_phase_manager=phase_manager,
478483
)
479484

480485

0 commit comments

Comments
 (0)