diff --git a/changelog/8986.bugfix.rst b/changelog/8986.bugfix.rst new file mode 100644 index 00000000000..e1c6544ded1 --- /dev/null +++ b/changelog/8986.bugfix.rst @@ -0,0 +1,2 @@ +Warn when a plugin raises :class:`SystemExit` from the +:hook:`pytest_cmdline_main` hook. diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index e0038e26ce6..06046f50f16 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -234,7 +234,15 @@ def _main( return ExitCode.USAGE_ERROR try: - ret: ExitCode | int = config.hook.pytest_cmdline_main(config=config) + try: + ret: ExitCode | int = config.hook.pytest_cmdline_main(config=config) + except SystemExit: + warnings.warn( + PytestConfigWarning( + "A plugin raised SystemExit from the pytest_cmdline_main hook" + ) + ) + raise try: return ExitCode(ret) except ValueError: diff --git a/testing/test_config.py b/testing/test_config.py index 1d2aceb0d99..5645d2b5233 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -32,6 +32,7 @@ from _pytest.monkeypatch import MonkeyPatch from _pytest.pathlib import absolutepath from _pytest.pytester import Pytester +from _pytest.warning_types import PytestConfigWarning from _pytest.warning_types import PytestDeprecationWarning import pytest @@ -852,6 +853,23 @@ def test_absolute_win32_path(self, pytester: Pytester) -> None: class TestConfigAPI: + @pytest.mark.parametrize("code", [0, 42]) + def test_cmdline_main_system_exit(self, code: int) -> None: + class Plugin: + def pytest_cmdline_main(self, config: Config) -> None: + raise SystemExit(code) + + with ( + pytest.warns( + PytestConfigWarning, + match="plugin raised SystemExit from the pytest_cmdline_main hook", + ), + pytest.raises(SystemExit) as exc_info, + ): + pytest.main([], plugins=[Plugin()]) + + assert exc_info.value.code == code + def test_config_trace(self, pytester: Pytester) -> None: config = pytester.parseconfig() values: list[str] = []