diff --git a/AUTHORS b/AUTHORS index f3cf1b0facb..c1b0117df64 100644 --- a/AUTHORS +++ b/AUTHORS @@ -503,6 +503,7 @@ Warren Markham Wei Lin Wil Cooley Will Riley +Willem Adnet William Lee Wim Glenn Wouter van Ackooy diff --git a/changelog/14443.feature.rst b/changelog/14443.feature.rst new file mode 100644 index 00000000000..bfdf1cfc153 --- /dev/null +++ b/changelog/14443.feature.rst @@ -0,0 +1 @@ +Added new CLI argument ``--name-only`` to only display the name of the test when it fails, suppressing tracebacks and other detailed information. diff --git a/doc/en/reference/reference.rst b/doc/en/reference/reference.rst index a69aa2c7887..6e527eb84ba 100644 --- a/doc/en/reference/reference.rst +++ b/doc/en/reference/reference.rst @@ -3074,6 +3074,10 @@ Output and Reporting * ``log``: show captured logging * ``all`` (default): show all captured output +.. option:: --name-only + + Only display the name of the test when it fails, suppressing tracebacks and other detailed information. + .. option:: --color=WHEN Color terminal output: diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index b9a65ff191e..79f3a6d12ce 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -247,6 +247,13 @@ def pytest_addoption(parser: Parser) -> None: help="Controls how captured stdout/stderr/log is shown on failed tests. " "Default: all.", ) + group.addoption( + "--name-only", + action="store_true", + default=False, + dest="name_only", + help="Only display the name of the test when it fails.", + ) group.addoption( "--fulltrace", "--full-trace", @@ -1212,14 +1219,18 @@ def summary_failures_combined( if style == "line": for rep in reports: line = self._getcrashline(rep) - self._outrep_summary(rep) + if not self.config.option.name_only: + self._outrep_summary(rep) self.write_line(line) else: for rep in reports: msg = self._getfailureheadline(rep) - self.write_sep("_", msg, red=True, bold=True) - self._outrep_summary(rep) - self._handle_teardown_sections(rep.nodeid) + if self.config.option.name_only: + self._tw.line(msg, red=True, bold=True) + else: + self.write_sep("_", msg, red=True, bold=True) + self._outrep_summary(rep) + self._handle_teardown_sections(rep.nodeid) def summary_errors(self) -> None: if self.config.option.tbstyle != "no": @@ -1233,8 +1244,11 @@ def summary_errors(self) -> None: msg = "ERROR collecting " + msg else: msg = f"ERROR at {rep.when} of {msg}" - self.write_sep("_", msg, red=True, bold=True) - self._outrep_summary(rep) + if self.config.option.name_only: + self._tw.line(msg, red=True, bold=True) + else: + self.write_sep("_", msg, red=True, bold=True) + self._outrep_summary(rep) def _outrep_summary(self, rep: BaseReport) -> None: rep.toterminal(self._tw) diff --git a/testing/test_name_only.py b/testing/test_name_only.py new file mode 100644 index 00000000000..11a32ed880c --- /dev/null +++ b/testing/test_name_only.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +import pytest + + +class TestNameOnly: + def test_name_only_failures(self, pytester: pytest.Pytester) -> None: + pytester.makepyfile( + """ + def test_fail1(): + assert False + def test_fail2(): + assert False + def test_pass(): + assert True + """ + ) + result = pytester.runpytest("--name-only") + result.stdout.fnmatch_lines( + [ + "=* FAILURES *=", + "test_fail1", + "test_fail2", + "=* short test summary info *=", + ] + ) + + output = result.stdout.str() + failures_part = output.split("FAILURES")[1].split("short test summary info")[0] + assert "test_fail1" in failures_part + assert "test_fail2" in failures_part + assert "assert False" not in failures_part + assert "E assert False" not in failures_part + + def test_name_only_errors(self, pytester: pytest.Pytester) -> None: + p = pytester.makepyfile( + test_error=""" + import pytest + @pytest.fixture + def bad_fixture(): + raise RuntimeError("error in fixture") + def test_error(bad_fixture): + pass + """ + ) + result = pytester.runpytest(p, "--name-only") + result.stdout.fnmatch_lines( + [ + "=* ERRORS *=", + "ERROR at setup of test_error", + "=* short test summary info *=", + "ERROR test_error.py::test_error - RuntimeError: error in fixture", + ] + ) + output = result.stdout.str() + errors_part = output.split("ERRORS")[1].split("short test summary info")[0] + assert "ERROR at setup of test_error" in errors_part + assert "RuntimeError: error in fixture" not in errors_part + + def test_name_only_collection_error(self, pytester: pytest.Pytester) -> None: + pytester.makepyfile( + """ + def test_syntax(): + assert + """ + ) + result = pytester.runpytest("--name-only") + result.stdout.fnmatch_lines( + [ + "=* ERRORS *=", + "ERROR collecting test_name_only_collection_error.py", + "=* short test summary info *=", + "ERROR test_name_only_collection_error.py", + ] + ) + output = result.stdout.str() + errors_part = output.split("ERRORS")[1].split("short test summary info")[0] + assert "ERROR collecting test_name_only_collection_error.py" in errors_part + assert "SyntaxError" not in errors_part + + def test_name_only_with_tb_line(self, pytester: pytest.Pytester) -> None: + pytester.makepyfile( + """ + def test_fail(): + assert False + """ + ) + result = pytester.runpytest("--name-only", "--tb=line") + result.stdout.fnmatch_lines( + [ + "=* FAILURES *=", + "*test_name_only_with_tb_line.py*: assert False", + ] + )