Skip to content

Commit bf1eda4

Browse files
feat(fill, execute): track execution & setup testing phase (#2157)
* feat: implement phase manager * feat(tests): add testPhase attribute to transaction test cases * fix: update transaction phase handling * fix failing test
1 parent 7d469c3 commit bf1eda4

File tree

6 files changed

+247
-15
lines changed

6 files changed

+247
-15
lines changed

src/ethereum_test_execution/transaction_post.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,14 @@ 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 = (
57+
"testing"
58+
if (tx.test_phase == "execution" or tx.test_phase is None)
59+
else "setup"
60+
)
5661
tx.metadata = TransactionTestMetadata(
5762
test_id=request.node.nodeid,
58-
phase="testing",
63+
phase=phase,
5964
target=label,
6065
tx_index=tx_index,
6166
)

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_default_phase() -> None:
42+
"""Test that transactions default to None when no phase set."""
43+
tx = Transaction(to=Address(0x123), value=100, gas_limit=21000)
44+
assert tx.test_phase is None
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 == TestPhase.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 None)
57+
tx1 = Transaction(to=Address(0x100), value=100, gas_limit=21000)
58+
assert tx1.test_phase is None
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 == TestPhase.SETUP
65+
66+
# Back to None after context (transactions default to None)
67+
assert TestPhaseManager.get_current_phase() is None
68+
tx3 = Transaction(to=Address(0x300), value=300, gas_limit=21000)
69+
assert tx3.test_phase is None
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 == TestPhase.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 == TestPhase.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 == TestPhase.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 == TestPhase.SETUP
116+
117+
# Verify all execution transactions have None phase (no context set)
118+
for tx in exec_txs:
119+
assert tx.test_phase is None
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 == TestPhase.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: 4 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 TestPhase, TestPhaseManager
4142
from .receipt_types import TransactionReceipt
4243
from .utils import int_to_bytes, keccak256
4344

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

294295
metadata: TransactionTestMetadata | None = Field(None, exclude=True)
296+
test_phase: TestPhase | None = Field(
297+
default_factory=TestPhaseManager.get_current_phase, exclude=True
298+
)
295299

296300
model_config = ConfigDict(validate_assignment=True)
297301

tests/benchmark/test_worst_stateful_opcodes.py

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
compute_create2_address,
2525
compute_create_address,
2626
)
27+
from ethereum_test_types import TestPhaseManager
2728
from ethereum_test_vm import Opcodes as Op
2829

2930
REFERENCE_SPEC_GIT_PATH = "TODO"
@@ -405,23 +406,26 @@ def test_worst_storage_access_warm(
405406
)
406407
+ Op.RETURN(0, Op.MSIZE)
407408
)
408-
sender_addr = pre.fund_eoa()
409-
setup_tx = Transaction(
410-
to=None,
411-
gas_limit=env.gas_limit,
412-
data=creation_code,
413-
sender=sender_addr,
414-
)
415-
blocks.append(Block(txs=[setup_tx]))
409+
410+
with TestPhaseManager.setup():
411+
sender_addr = pre.fund_eoa()
412+
setup_tx = Transaction(
413+
to=None,
414+
gas_limit=env.gas_limit,
415+
data=creation_code,
416+
sender=sender_addr,
417+
)
418+
blocks.append(Block(txs=[setup_tx]))
416419

417420
contract_address = compute_create_address(address=sender_addr, nonce=0)
418421

419-
op_tx = Transaction(
420-
to=contract_address,
421-
gas_limit=gas_benchmark_value,
422-
sender=pre.fund_eoa(),
423-
)
424-
blocks.append(Block(txs=[op_tx]))
422+
with TestPhaseManager.execution():
423+
op_tx = Transaction(
424+
to=contract_address,
425+
gas_limit=gas_benchmark_value,
426+
sender=pre.fund_eoa(),
427+
)
428+
blocks.append(Block(txs=[op_tx]))
425429

426430
benchmark_test(blocks=blocks)
427431

0 commit comments

Comments
 (0)