diff --git a/changelog/12943.improvement.rst b/changelog/12943.improvement.rst new file mode 100644 index 00000000000..eb8ac63650a --- /dev/null +++ b/changelog/12943.improvement.rst @@ -0,0 +1 @@ +If a test fails with an exceptiongroup with a single exception, the contained exception will now be displayed in the short test summary info. diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 8fac39ea298..85ed3145e66 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -589,6 +589,23 @@ def exconly(self, tryshort: bool = False) -> str: representation is returned (so 'AssertionError: ' is removed from the beginning). """ + + def _get_single_subexc( + eg: BaseExceptionGroup[BaseException], + ) -> BaseException | None: + if len(eg.exceptions) != 1: + return None + if isinstance(e := eg.exceptions[0], BaseExceptionGroup): + return _get_single_subexc(e) + return e + + if ( + tryshort + and isinstance(self.value, BaseExceptionGroup) + and (subexc := _get_single_subexc(self.value)) is not None + ): + return f"{subexc!r} [single exception in {type(self.value).__name__}]" + lines = format_exception_only(self.type, self.value) text = "".join(lines) text = text.rstrip() diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index fc60ae9ac99..b049e0cf188 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -1703,6 +1703,83 @@ def test_exceptiongroup(pytester: Pytester, outer_chain, inner_chain) -> None: _exceptiongroup_common(pytester, outer_chain, inner_chain, native=False) +def test_exceptiongroup_short_summary_info(pytester: Pytester): + pytester.makepyfile( + """ + import sys + + if sys.version_info < (3, 11): + from exceptiongroup import BaseExceptionGroup, ExceptionGroup + + def test_base() -> None: + raise BaseExceptionGroup("NOT IN SUMMARY", [SystemExit("a" * 10)]) + + def test_nonbase() -> None: + raise ExceptionGroup("NOT IN SUMMARY", [ValueError("a" * 10)]) + + def test_nested() -> None: + raise ExceptionGroup( + "NOT DISPLAYED", [ + ExceptionGroup("NOT IN SUMMARY", [ValueError("a" * 10)]) + ] + ) + + def test_multiple() -> None: + raise ExceptionGroup( + "b" * 10, + [ + ValueError("NOT IN SUMMARY"), + TypeError("NOT IN SUMMARY"), + ] + ) + + def test_nested_multiple() -> None: + raise ExceptionGroup( + "b" * 10, + [ + ExceptionGroup( + "c" * 10, + [ + ValueError("NOT IN SUMMARY"), + TypeError("NOT IN SUMMARY"), + ] + ) + ] + ) + """ + ) + # run with -vv to not truncate summary info, default width in tests is very low + result = pytester.runpytest("-vv") + assert result.ret == 1 + backport_str = "exceptiongroup." if sys.version_info < (3, 11) else "" + result.stdout.fnmatch_lines( + [ + "*= short test summary info =*", + ( + "FAILED test_exceptiongroup_short_summary_info.py::test_base - " + "SystemExit('aaaaaaaaaa') [single exception in BaseExceptionGroup]" + ), + ( + "FAILED test_exceptiongroup_short_summary_info.py::test_nonbase - " + "ValueError('aaaaaaaaaa') [single exception in ExceptionGroup]" + ), + ( + "FAILED test_exceptiongroup_short_summary_info.py::test_nested - " + "ValueError('aaaaaaaaaa') [single exception in ExceptionGroup]" + ), + ( + "FAILED test_exceptiongroup_short_summary_info.py::test_multiple - " + f"{backport_str}ExceptionGroup: bbbbbbbbbb (2 sub-exceptions)" + ), + ( + "FAILED test_exceptiongroup_short_summary_info.py::test_nested_multiple - " + f"{backport_str}ExceptionGroup: bbbbbbbbbb (1 sub-exception)" + ), + "*= 5 failed in *", + ] + ) + + @pytest.mark.parametrize("tbstyle", ("long", "short", "auto", "line", "native")) def test_all_entries_hidden(pytester: Pytester, tbstyle: str) -> None: """Regression test for #10903."""