diff --git a/changelog/15047.bugfix.rst b/changelog/15047.bugfix.rst new file mode 100644 index 00000000000..df2c2408db3 --- /dev/null +++ b/changelog/15047.bugfix.rst @@ -0,0 +1 @@ +Fixed :option:`--fixtures` and :option:`--fixtures-per-test` exiting with status 0 even when collection had errors. diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 7656fca2f5b..e596576d78b 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -63,6 +63,7 @@ from _pytest.deprecated import FIXTUREDEF_HAS_LOCATION_DEPRECATED from _pytest.deprecated import PARSEFACTORIES_NODEID_DEPRECATED from _pytest.deprecated import YIELD_FIXTURE +from _pytest.main import _raise_on_collection_errors from _pytest.main import Session from _pytest.mark import Mark from _pytest.mark import ParameterSet @@ -1674,11 +1675,9 @@ def pytest_addoption(parser: Parser) -> None: def pytest_cmdline_main(config: Config) -> int | ExitCode | None: if config.option.showfixtures: - showfixtures(config) - return 0 + return showfixtures(config) if config.option.show_fixtures_per_test: - show_fixtures_per_test(config) - return 0 + return show_fixtures_per_test(config) return None @@ -2447,7 +2446,7 @@ def _get_fixtures_per_test(test: nodes.Item) -> Iterator[FixtureDef[object]]: yield fixturedef -def _show_fixtures_per_test(config: Config, session: Session) -> None: +def _show_fixtures_per_test(config: Config, session: Session) -> int | ExitCode: import _pytest.config session.perform_collect() @@ -2495,6 +2494,9 @@ def write_item(item: nodes.Item) -> None: for session_item in session.items: write_item(session_item) + _raise_on_collection_errors(session) + return ExitCode.TESTS_FAILED if session.testsfailed else ExitCode.OK + def showfixtures(config: Config) -> int | ExitCode: from _pytest.main import wrap_session @@ -2502,7 +2504,7 @@ def showfixtures(config: Config) -> int | ExitCode: return wrap_session(config, _showfixtures_main) -def _showfixtures_main(config: Config, session: Session) -> None: +def _showfixtures_main(config: Config, session: Session) -> int | ExitCode: import _pytest.config session.perform_collect() @@ -2552,6 +2554,9 @@ def _showfixtures_main(config: Config, session: Session) -> None: tw.line(" no docstring available", red=True) tw.line() + _raise_on_collection_errors(session) + return ExitCode.TESTS_FAILED if session.testsfailed else ExitCode.OK + def write_docstring(tw: TerminalWriter, doc: str, indent: str = " ") -> None: for line in doc.split("\n"): diff --git a/src/_pytest/main.py b/src/_pytest/main.py index d43a68b4679..0a292126dae 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -397,12 +397,16 @@ def pytest_collection(session: Session) -> None: session.perform_collect() -def pytest_runtestloop(session: Session) -> bool: +def _raise_on_collection_errors(session: Session) -> None: if session.testsfailed and not session.config.option.continue_on_collection_errors: raise session.Interrupted( f"{session.testsfailed} error{'s' if session.testsfailed != 1 else ''} during collection" ) + +def pytest_runtestloop(session: Session) -> bool: + _raise_on_collection_errors(session) + if session.config.option.collectonly: return True diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index bc7b5a40cc2..3996b86f37a 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -3855,6 +3855,34 @@ def test_show_fixtures(self, pytester: Pytester) -> None: ] ) + def test_show_fixtures_collection_error(self, pytester: Pytester) -> None: + pytester.makepyfile( + """ + def test_broken(: + pass + """ + ) + + result = pytester.runpytest("--fixtures") + + assert result.ret == ExitCode.INTERRUPTED + result.stdout.fnmatch_lines(["*Interrupted: 1 error during collection*"]) + + def test_show_fixtures_continue_on_collection_errors( + self, pytester: Pytester + ) -> None: + pytester.makepyfile( + """ + def test_broken(: + pass + """ + ) + + result = pytester.runpytest("--fixtures", "--continue-on-collection-errors") + + assert result.ret == ExitCode.TESTS_FAILED + result.stdout.no_fnmatch_line("*Interrupted:*") + def test_show_fixtures_verbose(self, pytester: Pytester) -> None: result = pytester.runpytest("--fixtures", "-v") result.stdout.fnmatch_lines( diff --git a/testing/python/show_fixtures_per_test.py b/testing/python/show_fixtures_per_test.py index 2362847f338..11a9b44d406 100644 --- a/testing/python/show_fixtures_per_test.py +++ b/testing/python/show_fixtures_per_test.py @@ -1,5 +1,6 @@ from __future__ import annotations +from _pytest.config import ExitCode from _pytest.pytester import Pytester @@ -9,6 +10,38 @@ def test_should_show_no_output_when_zero_items(pytester: Pytester) -> None: assert result.ret == 0 +def test_fixtures_per_test_collection_error(pytester: Pytester) -> None: + pytester.makepyfile( + """ + def test_broken(: + pass + """ + ) + + result = pytester.runpytest("--fixtures-per-test") + + assert result.ret == ExitCode.INTERRUPTED + result.stdout.fnmatch_lines(["*Interrupted: 1 error during collection*"]) + + +def test_fixtures_per_test_continue_on_collection_errors( + pytester: Pytester, +) -> None: + pytester.makepyfile( + """ + def test_broken(: + pass + """ + ) + + result = pytester.runpytest( + "--fixtures-per-test", "--continue-on-collection-errors" + ) + + assert result.ret == ExitCode.TESTS_FAILED + result.stdout.no_fnmatch_line("*Interrupted:*") + + def test_fixtures_in_module(pytester: Pytester) -> None: p = pytester.makepyfile( '''