Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/15028.breaking.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Collection errors now abort from the default :hook:`pytest_collection` hook rather than :hook:`pytest_runtestloop`. A plugin that replaces ``pytest_runtestloop`` still sees the abort. A plugin that replaces ``pytest_collection`` entirely has to perform the ``testsfailed`` check itself if it wants the same guard.
6 changes: 4 additions & 2 deletions src/_pytest/hookspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,8 @@ def pytest_collection(session: Session) -> object | None:
3. Set ``session.items`` to the list of collected items
4. ``pytest_collection_finish(session)``
5. Set ``session.testscollected`` to the number of collected items
6. Abort with :class:`~pytest.Interrupted` if collection reported errors
(unless ``--continue-on-collection-errors`` is set)

You can implement this hook to only perform some action before collection,
for example the terminal plugin uses it to start displaying the collection
Expand Down Expand Up @@ -595,8 +597,8 @@ def pytest_runtestloop(session: Session) -> object | None:
"""Perform the main runtest loop (after collection finished).

The default hook implementation performs the runtest protocol for all items
collected in the session (``session.items``), unless the collection failed
or the ``collectonly`` pytest option is set.
collected in the session (``session.items``), unless the
``collectonly`` pytest option is set.

If at any point :py:func:`pytest.exit` is called, the loop is
terminated immediately.
Expand Down
5 changes: 2 additions & 3 deletions src/_pytest/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -392,14 +392,13 @@ def _main(config: Config, session: Session) -> int | ExitCode | None:

def pytest_collection(session: Session) -> None:
session.perform_collect()


def pytest_runtestloop(session: Session) -> bool:
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:
if session.config.option.collectonly:
return True

Expand Down
17 changes: 17 additions & 0 deletions testing/test_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -1132,6 +1132,23 @@ def test_continue_on_collection_errors_maxfail(pytester: Pytester) -> None:
res.stdout.fnmatch_lines(["collected 2 items / 2 errors", "*1 failed, 2 errors*"])


def test_collection_error_aborts_before_custom_runtestloop(pytester: Pytester) -> None:
pytester.makeconftest(
"""
def pytest_runtestloop(session):
raise AssertionError("runtestloop must not run after a collection error")
"""
)
pytester.makepyfile(
"""
import nosuchmodule
"""
)
res = pytester.runpytest()
assert res.ret == ExitCode.INTERRUPTED
res.stdout.fnmatch_lines(["*! Interrupted: 1 error during collection !*"])


def test_fixture_scope_sibling_conftests(pytester: Pytester) -> None:
"""Regression test case for https://github.com/pytest-dev/pytest/issues/2836"""
foo_path = pytester.mkdir("foo")
Expand Down
Loading