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/13369.improvement.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Improved the error from :meth:`pytest.RunResult.assert_outcomes` when a plugin changes or removes pytest's standard terminal summary, and documented how to isolate output-changing plugins.
8 changes: 8 additions & 0 deletions doc/en/how-to/writing_plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,14 @@ return a result object, with which we can assert the tests' outcomes.
# check that all 4 tests passed
result.assert_outcomes(passed=4)

.. note::

:meth:`pytest.RunResult.assert_outcomes` parses pytest's standard terminal
summary. A plugin that changes or removes that summary can make outcome
parsing fail. Disable the output-changing plugin for the nested run with
``-p no:<plugin>``, or set ``PYTEST_DISABLE_PLUGIN_AUTOLOAD=1`` when the
nested run should not discover third-party plugins.


Additionally it is possible to copy examples to the ``pytester``'s isolated environment
before running pytest on it. This way we can abstract the tested logic to separate files,
Expand Down
14 changes: 13 additions & 1 deletion src/_pytest/pytester.py
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,9 @@ def parseoutcomes(self) -> dict[str, int]:
======= 1 failed, 1 passed, 1 warning, 1 error in 0.13s ====

Will return ``{"failed": 1, "passed": 1, "warnings": 1, "errors": 1}``.

This method parses pytest's standard terminal summary. Plugins that
modify or remove that summary can make the outcomes unavailable.
"""
return self.parse_summary_nouns(self.outlines)

Expand All @@ -582,7 +585,13 @@ def parse_summary_nouns(cls, lines) -> dict[str, int]:
ret = {noun: int(count) for (count, noun) in outcomes}
break
else:
raise ValueError("Pytest terminal summary report not found")
raise ValueError(
"Pytest terminal summary report not found. "
"Plugins that modify pytest's terminal output can break outcome "
"parsing. Disable the plugin for the test run, for example with "
"`-p no:<plugin>`, or disable plugin autoloading with "
"`PYTEST_DISABLE_PLUGIN_AUTOLOAD=1`."
)

to_plural = {
"warning": "warnings",
Expand All @@ -606,6 +615,9 @@ def assert_outcomes(
numbers (0 means it didn't occur) in the text output from a test run.

``warnings`` and ``deselected`` are only checked if not None.

This method requires pytest's standard terminal summary; see
:meth:`parseoutcomes`.
"""
__tracebackhide__ = True
from _pytest.pytester_assertions import assert_outcomes
Expand Down
6 changes: 5 additions & 1 deletion testing/test_pytester.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,9 +299,13 @@ def test_assert_outcomes_after_pytest_error(pytester: Pytester) -> None:
pytester.makepyfile("def test_foo(): assert True")

result = pytester.runpytest("--unexpected-argument")
with pytest.raises(ValueError, match="Pytest terminal summary report not found"):
with pytest.raises(ValueError) as exc_info:
result.assert_outcomes(passed=0)

message = str(exc_info.value)
assert "Plugins that modify pytest's terminal output" in message
assert "PYTEST_DISABLE_PLUGIN_AUTOLOAD=1" in message


class TestSysModulesSnapshot:
key = "my-test-module"
Expand Down