diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index cc70dabd34..2de96d870f 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -56,8 +56,28 @@ jobs: uvx --from actionlint-py actionlint py3: + name: py3 (${{ matrix.label }}) runs-on: [self-hosted-ghr, size-xl-x64] needs: static + strategy: + fail-fast: false + matrix: + include: + - label: pre-cancun + from_fork: Frontier + until_fork: Shanghai + - label: cancun + from_fork: Cancun + until_fork: Cancun + - label: prague + from_fork: Prague + until_fork: Prague + - label: osaka + from_fork: Osaka + until_fork: Osaka + - label: amsterdam + from_fork: Amsterdam + until_fork: Amsterdam steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: @@ -66,8 +86,8 @@ jobs: with: python-version: "3.14" - uses: ./.github/actions/setup-env - - name: Run py3 tests - run: tox -e py3 + - name: Run py3 tests (${{ matrix.label }}) + run: tox -e py3 -- --from ${{ matrix.from_fork }} --until ${{ matrix.until_fork }} env: PYTEST_XDIST_AUTO_NUM_WORKERS: auto - name: Upload coverage reports to Codecov diff --git a/packages/testing/pyproject.toml b/packages/testing/pyproject.toml index ee5c2c7ddf..f9b5960a7a 100644 --- a/packages/testing/pyproject.toml +++ b/packages/testing/pyproject.toml @@ -20,7 +20,7 @@ classifiers = [ ] dependencies = [ "click>=8.1.0,<9", - "ethereum-hive>=0.1.0a1,<1.0.0", + "ethereum-hive>=0.1.0a5,<1.0.0", "ethereum-execution", "gitpython>=3.1.31,<4", "PyJWT>=2.3.0,<3", diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/simulators/enginex/conftest.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/simulators/enginex/conftest.py index 80ad3405f2..78c57a4fb7 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/simulators/enginex/conftest.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/simulators/enginex/conftest.py @@ -122,6 +122,26 @@ def test_suite_description() -> str: ) +@pytest.fixture(scope="function", autouse=True) +def _per_test_reporting( + client: Client, + hive_test: HiveTest, +) -> None: + """ + Register a test for execution against a multi-test client. + + Activate log segment capturing in the Hive backend for correct + client log reporting in the multi-test client case. + + Parameter order matters: `client` listed before `hive_test` + ensures pytest sets up `client` first and tears it down last. + This guarantees `hive_test` teardown (`test.end()`) runs while + the hive node still exists, before `client` teardown calls + `mark_test_completed` / `client.stop()`. + """ + hive_test.register_multi_test_client(client) + + @pytest.fixture(scope="function") def client( multi_test_hive_test: HiveTest, @@ -179,6 +199,7 @@ def client( multi_test_client_manager.register_client( group_identifier, resolved_client ) + resolved_client.multi_test = True try: yield resolved_client diff --git a/packages/testing/src/execution_testing/specs/base.py b/packages/testing/src/execution_testing/specs/base.py index c37237b1b4..b9c566cd08 100644 --- a/packages/testing/src/execution_testing/specs/base.py +++ b/packages/testing/src/execution_testing/specs/base.py @@ -36,6 +36,9 @@ from execution_testing.forks import Fork from execution_testing.forks.base_fork import BaseFork from execution_testing.test_types import Environment, Withdrawal +from execution_testing.test_types.receipt_types import ( + TransactionReceipt, +) class HashMismatchExceptionError(Exception): @@ -109,6 +112,7 @@ class BaseTest(BaseModel): gas_optimization_max_gas_limit: int | None = None expected_benchmark_gas_used: int | None = None skip_gas_used_validation: bool = False + expected_receipt_status: int | None = None is_tx_gas_heavy_test: bool = False is_exception_test: bool = False @@ -170,7 +174,7 @@ def from_test( ) -> Self: """Create a test in a different format from a base test.""" for k in BaseTest.model_fields.keys(): - if k not in kwargs: + if k not in kwargs and k in base_test.model_fields_set: kwargs[k] = getattr(base_test, k) return cls(**kwargs) @@ -288,5 +292,32 @@ def validate_benchmark_gas( f"{gas_benchmark_value}" ) + def validate_receipt_status( + self, + *, + receipts: List[TransactionReceipt], + block_number: int, + ) -> None: + """ + Validate receipt status for every transaction in a block. + + When expected_receipt_status is set, verify that all + receipts match. Catches silent OOG failures that roll + back state and invalidate benchmarks. + """ + if "expected_receipt_status" not in self.model_fields_set: + return + for i, receipt in enumerate(receipts): + if receipt.status is not None and ( + int(receipt.status) != self.expected_receipt_status + ): + raise Exception( + f"Transaction {i} in block " + f"{block_number} has receipt " + f"status {int(receipt.status)}, " + f"expected " + f"{self.expected_receipt_status}." + ) + TestSpec = Callable[[Fork], Generator[BaseTest, None, None]] diff --git a/packages/testing/src/execution_testing/specs/blockchain.py b/packages/testing/src/execution_testing/specs/blockchain.py index 9b922b970e..1c9b5b7fa0 100644 --- a/packages/testing/src/execution_testing/specs/blockchain.py +++ b/packages/testing/src/execution_testing/specs/blockchain.py @@ -858,8 +858,7 @@ def make_fixture( invalid_blocks = 0 benchmark_gas_used: int | None = None benchmark_opcode_count: OpcodeCount | None = None - for i, block in enumerate(self.blocks): - is_last_block = i == len(self.blocks) - 1 + for block in self.blocks: # This is the most common case, the RLP needs to be constructed # based on the transactions to be included in the block. # Set the environment according to the block to execute. @@ -869,9 +868,16 @@ def make_fixture( previous_env=env, previous_alloc=alloc, ) + block_number = int(built_block.header.number) + is_last_block = block is self.blocks[-1] if is_last_block and self.operation_mode == OpMode.BENCHMARKING: benchmark_gas_used = int(built_block.result.gas_used) benchmark_opcode_count = built_block.result.opcode_count + if built_block.result.receipts: + self.validate_receipt_status( + receipts=built_block.result.receipts, + block_number=block_number, + ) include_receipts = ( block.include_receipts_in_output if block.include_receipts_in_output is not None @@ -953,17 +959,23 @@ def make_hive_fixture( invalid_blocks = 0 benchmark_gas_used: int | None = None benchmark_opcode_count: OpcodeCount | None = None - for i, block in enumerate(self.blocks): - is_last_block = i == len(self.blocks) - 1 + for block in self.blocks: built_block = self.generate_block_data( t8n=t8n, block=block, previous_env=env, previous_alloc=alloc, ) + block_number = int(built_block.header.number) + is_last_block = block is self.blocks[-1] if is_last_block and self.operation_mode == OpMode.BENCHMARKING: benchmark_gas_used = int(built_block.result.gas_used) benchmark_opcode_count = built_block.result.opcode_count + if built_block.result.receipts: + self.validate_receipt_status( + receipts=built_block.result.receipts, + block_number=block_number, + ) fixture_payloads.append( built_block.get_fixture_engine_new_payload() ) diff --git a/tox.ini b/tox.ini index 2cd27d0b5c..6c1fd50b89 100644 --- a/tox.ini +++ b/tox.ini @@ -125,7 +125,6 @@ commands = --clean \ --until Amsterdam \ --durations=50 \ - --ignore=tests/ported_static \ {posargs} \ tests diff --git a/uv.lock b/uv.lock index e4aada63d5..b606382f62 100644 --- a/uv.lock +++ b/uv.lock @@ -1110,7 +1110,7 @@ requires-dist = [ { name = "colorlog", specifier = ">=6.7.0,<7" }, { name = "eth-abi", specifier = ">=5.2.0" }, { name = "ethereum-execution", editable = "." }, - { name = "ethereum-hive", specifier = ">=0.1.0a1,<1.0.0" }, + { name = "ethereum-hive", specifier = ">=0.1.0a5,<1.0.0" }, { name = "ethereum-rlp", specifier = ">=0.1.5,<0.2" }, { name = "ethereum-types", specifier = ">=0.3.0,<0.4" }, { name = "filelock", specifier = ">=3.15.1,<4" }, @@ -1155,14 +1155,14 @@ test = [{ name = "pytest-cov", specifier = ">=4.1.0,<5" }] [[package]] name = "ethereum-hive" -version = "0.1.0a2" +version = "0.1.0a5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b3/9b/f15c86866245ad5c22dbc98f433158fdeaab3e96eae4efd1bf6a12a7717c/ethereum_hive-0.1.0a2.tar.gz", hash = "sha256:9ddc08a8dfe2828a9a4abfca1c0972f88882fa06fb6a3e111ec013b66906f843", size = 74796, upload-time = "2025-07-15T05:38:36.983Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/a8/95676acd86095a5dcf5dacfa5a991175b3a547dcd5729735fea777b8feec/ethereum_hive-0.1.0a5.tar.gz", hash = "sha256:bf91d3144c263a1a6407c931b3864ab3edd40bc5c34b812e571e7c49cc70f9eb", size = 75064, upload-time = "2026-03-11T07:36:24.532Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/74/60/1daf13c49bbefed6b67dd8a5d1280386e1ad22c9ec61737d44047064632e/ethereum_hive-0.1.0a2-py3-none-any.whl", hash = "sha256:0b0253146428240725e2b83a86bbc9eab975c460b8d76ce3937ba5295aa2448b", size = 37578, upload-time = "2025-07-15T05:38:35.334Z" }, + { url = "https://files.pythonhosted.org/packages/73/95/986727018ac0401562d91961e2f46462ed65a08e2d5162a255d9f871a7ad/ethereum_hive-0.1.0a5-py3-none-any.whl", hash = "sha256:3225747ed83a9124a697db0ab8d736c4820117462e64aa3395bbc1588c4d40d0", size = 37589, upload-time = "2026-03-11T07:36:23.286Z" }, ] [[package]]