Skip to content

Commit f4ff109

Browse files
feat: implement phase manager
1 parent a9fc9ee commit f4ff109

File tree

6 files changed

+266
-28
lines changed

6 files changed

+266
-28
lines changed

src/ethereum_test_execution/transaction_post.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,10 @@ def execute(
5353
tx = tx.with_signature_and_sender()
5454
to_address = tx.to
5555
label = to_address.label if isinstance(to_address, Address) else None
56+
phase = "testing" if tx.test_phase == "execution" else "setup"
5657
tx.metadata = TransactionTestMetadata(
5758
test_id=request.node.nodeid,
58-
phase="testing",
59+
phase=phase,
5960
target=label,
6061
tx_index=tx_index,
6162
)

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: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""Test phase management for Ethereum tests."""
2+
3+
from contextlib import contextmanager
4+
from enum import Enum
5+
from typing import ClassVar, Iterator, Optional
6+
7+
8+
class TestPhase(Enum):
9+
"""Test phase for state and blockchain tests."""
10+
11+
SETUP = "setup"
12+
EXECUTION = "execution"
13+
14+
15+
class TestPhaseManager:
16+
"""
17+
Manages test phases for transactions and blocks.
18+
19+
This singleton class provides context managers for "setup" and
20+
"execution" phases. Transactions automatically detect and tag
21+
themselves with the current phase.
22+
23+
Usage:
24+
with TestPhaseManager.setup():
25+
# Transactions created here have test_phase = "setup"
26+
setup_tx = Transaction(...)
27+
28+
with TestPhaseManager.execution():
29+
# Transactions created here have test_phase = "execution"
30+
benchmark_tx = Transaction(...)
31+
"""
32+
33+
_current_phase: ClassVar[Optional[TestPhase]] = None
34+
35+
@classmethod
36+
@contextmanager
37+
def setup(cls) -> Iterator[None]:
38+
"""Context manager for the setup phase of a benchmark test."""
39+
old_phase = cls._current_phase
40+
cls._current_phase = TestPhase.SETUP
41+
try:
42+
yield
43+
finally:
44+
cls._current_phase = old_phase
45+
46+
@classmethod
47+
@contextmanager
48+
def execution(cls) -> Iterator[None]:
49+
"""Context manager for the execution phase of a test."""
50+
old_phase = cls._current_phase
51+
cls._current_phase = TestPhase.EXECUTION
52+
try:
53+
yield
54+
finally:
55+
cls._current_phase = old_phase
56+
57+
@classmethod
58+
def get_current_phase(cls) -> Optional[TestPhase]:
59+
"""Get the current test phase."""
60+
return cls._current_phase
61+
62+
@classmethod
63+
def reset(cls) -> None:
64+
"""Reset the phase state to None (primarily for testing)."""
65+
cls._current_phase = None
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
"""Test suite for TestPhaseManager functionality."""
2+
3+
import pytest
4+
5+
from ethereum_test_base_types import Address
6+
from ethereum_test_tools import Transaction
7+
8+
from ..phase_manager import TestPhase, TestPhaseManager
9+
10+
11+
@pytest.fixture(autouse=True)
12+
def reset_phase_manager() -> None:
13+
"""Reset TestPhaseManager singleton state before each test."""
14+
TestPhaseManager.reset()
15+
16+
17+
def test_test_phase_enum_values() -> None:
18+
"""Test that TestPhase enum has correct values."""
19+
assert TestPhase.SETUP.value == "setup"
20+
assert TestPhase.EXECUTION.value == "execution"
21+
22+
23+
def test_phase_manager_class_state() -> None:
24+
"""Test TestPhaseManager uses class-level state."""
25+
# All access is through class methods, no instance needed
26+
assert TestPhaseManager.get_current_phase() is None
27+
28+
# Setting phase through class method
29+
with TestPhaseManager.setup():
30+
assert TestPhaseManager.get_current_phase() == TestPhase.SETUP
31+
32+
# Phase persists at class level
33+
assert TestPhaseManager.get_current_phase() is None
34+
35+
36+
def test_default_phase_is_none() -> None:
37+
"""Test that default phase is None (no context set)."""
38+
assert TestPhaseManager.get_current_phase() is None
39+
40+
41+
def test_transaction_auto_detects_execution_phase() -> None:
42+
"""Test that transactions default to 'execution' when no phase set."""
43+
tx = Transaction(to=Address(0x123), value=100, gas_limit=21000)
44+
assert tx.test_phase == "execution"
45+
46+
47+
def test_transaction_auto_detects_setup_phase() -> None:
48+
"""Test that transactions created in setup context get SETUP phase."""
49+
with TestPhaseManager.setup():
50+
tx = Transaction(to=Address(0x456), value=50, gas_limit=21000)
51+
assert tx.test_phase == "setup"
52+
53+
54+
def test_phase_context_switching() -> None:
55+
"""Test that phase switching works correctly."""
56+
# Start with no phase set (defaults to execution)
57+
tx1 = Transaction(to=Address(0x100), value=100, gas_limit=21000)
58+
assert tx1.test_phase == "execution"
59+
60+
# Switch to SETUP
61+
with TestPhaseManager.setup():
62+
assert TestPhaseManager.get_current_phase() == TestPhase.SETUP
63+
tx2 = Transaction(to=Address(0x200), value=200, gas_limit=21000)
64+
assert tx2.test_phase == "setup"
65+
66+
# Back to None after context (transactions default to execution)
67+
assert TestPhaseManager.get_current_phase() is None
68+
tx3 = Transaction(to=Address(0x300), value=300, gas_limit=21000)
69+
assert tx3.test_phase == "execution"
70+
71+
72+
def test_nested_phase_contexts() -> None:
73+
"""Test that nested phase contexts work correctly."""
74+
with TestPhaseManager.setup():
75+
tx1 = Transaction(to=Address(0x100), value=100, gas_limit=21000)
76+
assert tx1.test_phase == "setup"
77+
78+
# Nested execution context
79+
with TestPhaseManager.execution():
80+
tx2 = Transaction(to=Address(0x200), value=200, gas_limit=21000)
81+
assert tx2.test_phase == "execution"
82+
83+
# Back to setup after nested context
84+
tx3 = Transaction(to=Address(0x300), value=300, gas_limit=21000)
85+
assert tx3.test_phase == "setup"
86+
87+
88+
@pytest.mark.parametrize(
89+
["num_setup_txs", "num_exec_txs"],
90+
[
91+
pytest.param(0, 1, id="exec_only"),
92+
pytest.param(1, 0, id="setup_only"),
93+
pytest.param(3, 5, id="mixed"),
94+
pytest.param(10, 10, id="many"),
95+
],
96+
)
97+
def test_multiple_transactions_phase_tagging(num_setup_txs: int, num_exec_txs: int) -> None:
98+
"""Test that multiple transactions are correctly tagged by phase."""
99+
setup_txs = []
100+
exec_txs = []
101+
102+
# Create setup transactions
103+
with TestPhaseManager.setup():
104+
for i in range(num_setup_txs):
105+
tx = Transaction(to=Address(0x1000 + i), value=i * 10, gas_limit=21000)
106+
setup_txs.append(tx)
107+
108+
# Create execution transactions
109+
for i in range(num_exec_txs):
110+
tx = Transaction(to=Address(0x2000 + i), value=i * 20, gas_limit=21000)
111+
exec_txs.append(tx)
112+
113+
# Verify all setup transactions have SETUP phase
114+
for tx in setup_txs:
115+
assert tx.test_phase == "setup"
116+
117+
# Verify all execution transactions have EXECUTION phase
118+
for tx in exec_txs:
119+
assert tx.test_phase == "execution"
120+
121+
122+
def test_phase_reset() -> None:
123+
"""Test that reset() restores default phase."""
124+
# Change phase
125+
with TestPhaseManager.setup():
126+
pass
127+
128+
# Manually set to SETUP
129+
TestPhaseManager._current_phase = TestPhase.SETUP
130+
assert TestPhaseManager.get_current_phase() == TestPhase.SETUP
131+
132+
# Reset should restore None
133+
TestPhaseManager.reset()
134+
assert TestPhaseManager.get_current_phase() is None
135+
136+
137+
def test_class_state_shared() -> None:
138+
"""Test that phase state is shared at class level."""
139+
# Phase changes are visible globally since it's class-level state
140+
assert TestPhaseManager.get_current_phase() is None
141+
142+
with TestPhaseManager.setup():
143+
# All access to the class sees the same phase
144+
assert TestPhaseManager.get_current_phase() == TestPhase.SETUP
145+
146+
# Transactions created during this context get "setup" phase
147+
tx = Transaction(to=Address(0x789), value=75, gas_limit=21000)
148+
assert tx.test_phase == "setup"
149+
150+
# After context, phase returns to None
151+
assert TestPhaseManager.get_current_phase() is None

src/ethereum_test_types/transaction_types.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from .account_types import EOA
3939
from .blob_types import Blob
4040
from .chain_config_types import ChainConfigDefaults
41+
from .phase_manager import TestPhaseManager
4142
from .receipt_types import TransactionReceipt
4243
from .utils import int_to_bytes, keccak256
4344

@@ -292,6 +293,13 @@ class Transaction(
292293
zero: ClassVar[Literal[0]] = 0
293294

294295
metadata: TransactionTestMetadata | None = Field(None, exclude=True)
296+
test_phase: str | None = Field(
297+
default_factory=lambda: (
298+
phase.value
299+
if (phase := TestPhaseManager.get_current_phase()) is not None
300+
else "execution"
301+
)
302+
)
295303

296304
model_config = ConfigDict(validate_assignment=True)
297305

tests/benchmark/test_worst_stateful_opcodes.py

Lines changed: 37 additions & 27 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
@@ -434,23 +435,26 @@ def test_worst_storage_access_warm(
434435
)
435436
+ Op.RETURN(0, Op.MSIZE)
436437
)
437-
sender_addr = pre.fund_eoa()
438-
setup_tx = Transaction(
439-
to=None,
440-
gas_limit=env.gas_limit,
441-
data=creation_code,
442-
sender=sender_addr,
443-
)
444-
blocks.append(Block(txs=[setup_tx]))
438+
439+
with TestPhaseManager.setup():
440+
sender_addr = pre.fund_eoa()
441+
setup_tx = Transaction(
442+
to=None,
443+
gas_limit=env.gas_limit,
444+
data=creation_code,
445+
sender=sender_addr,
446+
)
447+
blocks.append(Block(txs=[setup_tx]))
445448

446449
contract_address = compute_create_address(address=sender_addr, nonce=0)
447450

448-
op_tx = Transaction(
449-
to=contract_address,
450-
gas_limit=attack_gas_limit,
451-
sender=pre.fund_eoa(),
452-
)
453-
blocks.append(Block(txs=[op_tx]))
451+
with TestPhaseManager.execution():
452+
op_tx = Transaction(
453+
to=contract_address,
454+
gas_limit=attack_gas_limit,
455+
sender=pre.fund_eoa(),
456+
)
457+
blocks.append(Block(txs=[op_tx]))
454458

455459
blockchain_test(
456460
pre=pre,
@@ -468,20 +472,26 @@ def test_worst_blockhash(
468472
Test running a block with as many blockhash accessing oldest allowed block
469473
as possible.
470474
"""
471-
# Create 256 dummy blocks to fill the blockhash window.
472-
blocks = [Block()] * 256
475+
blocks = []
473476

474-
# Always ask for the oldest allowed BLOCKHASH block.
475-
execution_code = Op.PUSH1(1) + While(
476-
body=Op.POP(Op.BLOCKHASH(Op.DUP1)),
477-
)
478-
execution_code_address = pre.deploy_contract(code=execution_code)
479-
op_tx = Transaction(
480-
to=execution_code_address,
481-
gas_limit=gas_benchmark_value,
482-
sender=pre.fund_eoa(),
483-
)
484-
blocks.append(Block(txs=[op_tx]))
477+
# Setup phase: Create 256 empty blocks
478+
with TestPhaseManager.setup():
479+
for _ in range(256):
480+
blocks.append(Block())
481+
482+
# Execution phase: Deploy contract and create benchmark transaction
483+
with TestPhaseManager.execution():
484+
# Always ask for the oldest allowed BLOCKHASH block.
485+
execution_code = Op.PUSH1(1) + While(
486+
body=Op.POP(Op.BLOCKHASH(Op.DUP1)),
487+
)
488+
execution_code_address = pre.deploy_contract(code=execution_code)
489+
op_tx = Transaction(
490+
to=execution_code_address,
491+
gas_limit=gas_benchmark_value,
492+
sender=pre.fund_eoa(),
493+
)
494+
blocks.append(Block(txs=[op_tx]))
485495

486496
blockchain_test(
487497
pre=pre,

0 commit comments

Comments
 (0)