diff --git a/AUTHORS b/AUTHORS index d1a2d3e7911..d1c2edd85fb 100644 --- a/AUTHORS +++ b/AUTHORS @@ -483,6 +483,7 @@ Terje Runde Thomas Grainger Thomas Hisch Tianyu Dongfang +Tim Anderson Tim Hoffmann Tim Strazny TJ Bruno diff --git a/changelog/14902.bugfix.rst b/changelog/14902.bugfix.rst new file mode 100644 index 00000000000..f44b04be487 --- /dev/null +++ b/changelog/14902.bugfix.rst @@ -0,0 +1,2 @@ +Disambiguated terminal report headings for tests with the same name by including +their node IDs when needed. diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 825435225b3..dae53e72444 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -1233,8 +1233,13 @@ def summary_failures_combined( self._outrep_summary(rep) self.write_line(line) else: + headline_counts = Counter( + self._getfailureheadline(rep) for rep in reports + ) for rep in reports: msg = self._getfailureheadline(rep) + if headline_counts[msg] > 1: + msg = self.config.cwd_relative_nodeid(rep.nodeid) or msg self.write_sep("_", msg, red=True, bold=True) self._outrep_summary(rep) self._handle_teardown_sections(rep.nodeid) @@ -1245,8 +1250,13 @@ def summary_errors(self) -> None: if not reports: return self.write_sep("=", "ERRORS") - for rep in self.stats["error"]: + headline_counts = Counter( + (rep.when, self._getfailureheadline(rep)) for rep in reports + ) + for rep in reports: msg = self._getfailureheadline(rep) + if headline_counts[rep.when, msg] > 1: + msg = self.config.cwd_relative_nodeid(rep.nodeid) or msg if rep.when == "collect": msg = "ERROR collecting " + msg else: diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 30208084ab2..1c402022ddc 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -653,6 +653,61 @@ def test_bar(): pass class TestFixtureReporting: + def test_duplicate_setup_error_headlines_are_disambiguated( + self, pytester: Pytester + ) -> None: + pytester.makeconftest( + """ + import pytest + + @pytest.fixture + def fixture(): + raise RuntimeError("fixture failed") + """ + ) + pytester.makepyfile( + test_1=""" + def test(fixture): + pass + """, + test_2=""" + def test(fixture): + pass + """, + ) + + result = pytester.runpytest("-q") + + result.stdout.fnmatch_lines( + [ + "*ERROR at setup of test_1.py::test*", + "*ERROR at setup of test_2.py::test*", + ] + ) + + def test_duplicate_failure_headlines_are_disambiguated( + self, pytester: Pytester + ) -> None: + pytester.makepyfile( + test_1=""" + def test(): + assert False + """, + test_2=""" + def test(): + assert False + """, + ) + + result = pytester.runpytest("-q") + + result.stdout.fnmatch_lines( + [ + "*_ test_1.py::test _*", + "*_ test_2.py::test _*", + ] + ) + def test_setup_fixture_error(self, pytester: Pytester) -> None: pytester.makepyfile( """