diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/forks/forks.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/forks/forks.py index 2dc7336f6e8..cd95676b3f3 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/forks/forks.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/forks/forks.py @@ -38,6 +38,7 @@ get_transition_forks, transition_fork_to, ) +from execution_testing.forks.forks.eips import ALL_EIPS from execution_testing.logging import ( get_logger, ) @@ -513,6 +514,14 @@ def pytest_configure(config: pytest.Config) -> None: "shown in verbose collection output." ), ) + for eip in sorted(ALL_EIPS): + config.addinivalue_line( + "markers", + ( + f"{eip.name()}: tests explicitly enabled by " + f"{eip.name()} via valid_from(...)" + ), + ) for d in fork_covariant_decorators: config.addinivalue_line("markers", f"{d.marker_name}: {d.description}") @@ -1485,6 +1494,31 @@ def blob_params_changed_at_transition(fork: Fork | TransitionFork) -> bool: return False +def get_valid_from_eip_marker_names(markers: Iterator[pytest.Mark]) -> Set[str]: + """ + Return explicit EIP names from ``valid_from`` markers. + + Only positive EIP-based selectors are promoted to plain pytest markers so + ``-m EIP1234`` matches tests explicitly enabled by that EIP. + """ + eip_marker_names: Set[str] = set() + for marker in markers: + if not marker.args: + continue + for fork_or_eip in ForkEIPSetAdapter.validate_python(marker.args): + if not fork_or_eip.is_transition_fork and fork_or_eip.is_eip(): + eip_marker_names.add(fork_or_eip.name()) + return eip_marker_names + + +def add_explicit_eip_markers(item: pytest.Item) -> None: + """Attach plain pytest EIP markers derived from ``valid_from``.""" + for eip_marker_name in sorted( + get_valid_from_eip_marker_names(item.iter_markers("valid_from")) + ): + item.add_marker(getattr(pytest.mark, eip_marker_name)) + + def _get_item_params( item: pytest.Item, ) -> Dict[str, Any] | None: @@ -1543,6 +1577,14 @@ def pytest_collection_modifyitems( filter_stats: Dict[str, Tuple[str, int, int]] = {} for i, item in enumerate(items): + try: + add_explicit_eip_markers(item) + except Exception as e: + pytest.exit( + f"Error in test '{item.name}': {e}", + returncode=pytest.ExitCode.USAGE_ERROR, + ) + params = _get_item_params(item) if not params: continue diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/forks/tests/test_markers.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/forks/tests/test_markers.py index 34e84cb1b7e..1eb7d28799a 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/forks/tests/test_markers.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/forks/tests/test_markers.py @@ -521,3 +521,128 @@ def test_param_level_validity_markers( *pytest_args, ) result.assert_outcomes(**outcomes) + + +def generate_function_level_eip_marker_test() -> str: + """Generate a test with a function-level EIP validity marker.""" + return """ +import pytest + +@pytest.mark.valid_from("EIP3675") +@pytest.mark.state_test_only +def test_function_level_eip_marker(state_test): + pass +""" + + +def generate_param_level_eip_marker_test() -> str: + """Generate a test with mixed param-level EIP and fork markers.""" + return """ +import pytest + +@pytest.mark.parametrize( + "value", + [ + pytest.param( + True, + id="post_eip", + marks=pytest.mark.valid_from("EIP7928"), + ), + pytest.param( + False, + id="post_paris", + marks=pytest.mark.valid_from("Paris"), + ), + ], +) +@pytest.mark.state_test_only +def test_param_level_eip_marker(state_test, value): + pass +""" + + +def generate_negative_eip_marker_test() -> str: + """Generate a test that mentions an EIP only in a negative selector.""" + return """ +import pytest + +@pytest.mark.valid_before("EIP7928") +@pytest.mark.state_test_only +def test_negative_eip_marker(state_test): + pass +""" + + +def test_function_level_valid_from_eip_is_selectable_by_mark( + pytester: pytest.Pytester, +) -> None: + """``-m EIP####`` should select tests enabled via function-level valid_from.""" + pytester.makepyfile(generate_function_level_eip_marker_test()) + pytester.copy_example( + name="src/execution_testing/cli/pytest_commands/pytest_ini_files/pytest-fill.ini" + ) + result = pytester.runpytest( + "-c", + "pytest-fill.ini", + "--until=Prague", + "--collect-only", + "-q", + "-m", + "EIP3675", + ) + assert result.ret == pytest.ExitCode.OK + stdout = "\n".join(result.stdout.lines) + assert "test_function_level_eip_marker[fork_Paris-state_test]" in stdout + assert ( + "test_function_level_eip_marker[fork_Shanghai-state_test]" in stdout + ) + assert "test_function_level_eip_marker[fork_Cancun-state_test]" in stdout + assert "test_function_level_eip_marker[fork_Prague-state_test]" in stdout + + +def test_param_level_valid_from_eip_is_selectable_by_mark( + pytester: pytest.Pytester, +) -> None: + """``-m EIP####`` should select only matching param-level variants.""" + pytester.makepyfile(generate_param_level_eip_marker_test()) + pytester.copy_example( + name="src/execution_testing/cli/pytest_commands/pytest_ini_files/pytest-fill.ini" + ) + result = pytester.runpytest( + "-c", + "pytest-fill.ini", + "--until=Amsterdam", + "--collect-only", + "-q", + "-m", + "EIP7928", + ) + assert result.ret == pytest.ExitCode.OK + stdout = "\n".join(result.stdout.lines) + assert "test_param_level_eip_marker[fork_Amsterdam-state_test-post_eip]" in stdout + assert ( + "test_param_level_eip_marker[fork_Amsterdam-state_test-post_paris]" + not in stdout + ) + + +def test_negative_eip_selectors_do_not_add_eip_markers( + pytester: pytest.Pytester, +) -> None: + """``valid_before(EIP####)`` should not make a test selectable by that EIP.""" + pytester.makepyfile(generate_negative_eip_marker_test()) + pytester.copy_example( + name="src/execution_testing/cli/pytest_commands/pytest_ini_files/pytest-fill.ini" + ) + result = pytester.runpytest( + "-c", + "pytest-fill.ini", + "--until=Amsterdam", + "--collect-only", + "-q", + "-m", + "EIP7928", + ) + assert result.ret == pytest.ExitCode.NO_TESTS_COLLECTED + stdout = "\n".join(result.stdout.lines) + assert "test_negative_eip_marker" not in stdout