diff --git a/changelog/12231.feature.rst b/changelog/12231.feature.rst index 6369c92f7..594cf063a 100644 --- a/changelog/12231.feature.rst +++ b/changelog/12231.feature.rst @@ -1,10 +1,8 @@ -Add `--xfail-tb` flag, which acts a lot like `--tb`, but controls the traceback output for XFAIL results. +Add `--xfail-tb` flag, which turns traceback output for XFAIL results. -* The `--xfail-tb` flag takes one argument, `auto|long|short|line|native|no`, and defaults to `no`. -* To turn on xfail tracebacks, pass in `-rx` or `-ra` along with `--xfail-tb=short` (or any of `auto`, `long`, `short`, `line`, or `native`). - -The `--tb` flag, that controls normal test failure tracebacks, defaults to `auto`. -This change really only separates the traceback behavior of normal vs xfail failures. +* If the `--xfail-tb` flag is not sent, tracebacks for XFAIL results are NOT shown. +* The style of traceback for XFAIL is set with `--tb`, and can be `auto|long|short|line|native|no`. +* Note: Even if you have `--xfail-tb` set, you won't see them if `--tb=no`. Some history: @@ -12,5 +10,4 @@ Some history: * With this change, default `-rx`/ `-ra` behavior is identical to pre-8.0 with respect to xfail tracebacks. * With pytest 8.0, `-rx` or `-ra` would not only turn on summary reports for xfail, but also report the tracebacks for xfail results. * This caused issues with some projects that utilize xfail, but don't want to see all of the xfail tracebacks. - - +* This change detaches xfail tracebacks from `-rx`, and now we turn on xfail tracebacks with `--xfail-tb`. \ No newline at end of file diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index bd12e56cf..5dfed31c3 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -218,12 +218,10 @@ def pytest_addoption(parser: Parser) -> None: ) group._addoption( "--xfail-tb", - metavar="style", - action="store", - dest="xfail_tbstyle", - default="no", - choices=["auto", "long", "short", "no", "line", "native"], - help="Traceback print mode for xfail (auto/long/short/line/native/no)", + action="store_true", + dest="xfail_tb", + default=False, + help="Show tracebacks for xfail (as long as --tb != no)", ) group._addoption( "--show-capture", @@ -1080,11 +1078,13 @@ class TerminalReporter: self._tw.line(content) def summary_failures(self) -> None: - self.summary_failures_combined("failed", "FAILURES") + style = self.config.option.tbstyle + self.summary_failures_combined("failed", "FAILURES", style=style) def summary_xfailures(self) -> None: - style = self.config.option.xfail_tbstyle - self.summary_failures_combined("xfailed", "XFAILURES", "x", style=style) + show_tb = self.config.option.xfail_tb + style = self.config.option.tbstyle if show_tb else "no" + self.summary_failures_combined("xfailed", "XFAILURES", style=style) def summary_failures_combined( self, @@ -1093,8 +1093,6 @@ class TerminalReporter: needed_opt: Optional[str] = None, style: Optional[str] = None, ) -> None: - if style is None: - style = self.config.option.tbstyle if style != "no": if not needed_opt or self.hasopt(needed_opt): reports: List[BaseReport] = self.getreports(which_reports) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 4957c0e02..bfe853858 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -2909,79 +2909,74 @@ def test_summary_xfail_reason(pytester: Pytester) -> None: assert result.stdout.lines.count(expect2) == 1 -def test_xfail_tb_default(pytester: Pytester) -> None: - pytester.makepyfile( +@pytest.fixture() +def xfail_testfile(pytester: Pytester) -> Path: + return pytester.makepyfile( """ import pytest - @pytest.mark.xfail - def test_xfail(): + def test_fail(): a, b = 1, 2 assert a == b + + @pytest.mark.xfail + def test_xfail(): + c, d = 3, 4 + assert c == d """ ) - # defaults to "no", so this is the same as `runpytest("-rx", "--xfail-tb=no")` - result = pytester.runpytest("-rx") - # Don't show traceback - result.stdout.no_fnmatch_line("*= XFAILURES =*") - result.stdout.no_fnmatch_line("*test_xfail_tb_line.py:6: assert 1 == 2") - # still print summary + +def test_xfail_tb_default(xfail_testfile, pytester: Pytester) -> None: + result = pytester.runpytest(xfail_testfile) + + # test_fail, show traceback result.stdout.fnmatch_lines( [ - "*= short test summary info =*", - "XFAIL test_xfail_tb_default.py::test_xfail", - "*= 1 xfailed in * =*", + "*= FAILURES =*", + "*_ test_fail _*", + "*def test_fail():*", + "* a, b = 1, 2*", + "*> assert a == b*", + "*E assert 1 == 2*", + ] + ) + + # test_xfail, don't show traceback + result.stdout.no_fnmatch_line("*= XFAILURES =*") + + +def test_xfail_tb_true(xfail_testfile, pytester: Pytester) -> None: + result = pytester.runpytest(xfail_testfile, "--xfail-tb") + + # both test_fail and test_xfail, show traceback + result.stdout.fnmatch_lines( + [ + "*= FAILURES =*", + "*_ test_fail _*", + "*def test_fail():*", + "* a, b = 1, 2*", + "*> assert a == b*", + "*E assert 1 == 2*", + "*= XFAILURES =*", + "*_ test_xfail _*", + "*def test_xfail():*", + "* c, d = 3, 4*", + "*> assert c == d*", + "*E assert 3 == 4*", ] ) -def test_xfail_tb_short(pytester: Pytester) -> None: - pytester.makepyfile( - """ - import pytest +def test_xfail_tb_line(xfail_testfile, pytester: Pytester) -> None: + result = pytester.runpytest(xfail_testfile, "--xfail-tb", "--tb=line") - @pytest.mark.xfail - def test_xfail(): - a, b = 1, 2 - assert a == b - """ - ) - result = pytester.runpytest("-rx", "--xfail-tb=short") + # both test_fail and test_xfail, show line result.stdout.fnmatch_lines( [ + "*= FAILURES =*", + "*test_xfail_tb_line.py:5: assert 1 == 2", "*= XFAILURES =*", - "*_ test_xfail _*", - "*@pytest.mark.xfail*", - "*def test_xfail():*", - "* a, b = 1, 2*", - "*> *assert a == b*", - "*E *assert 1 == 2*", - "*= short test summary info =*", - "XFAIL test_xfail_tb_short.py::test_xfail", - "*= 1 xfailed in * =*", - ] - ) - - -def test_xfail_tb_line(pytester: Pytester) -> None: - pytester.makepyfile( - """ - import pytest - - @pytest.mark.xfail - def test_xfail(): - a, b = 1, 2 - assert a == b - """ - ) - result = pytester.runpytest("-rx", "--xfail-tb=line") - result.stdout.fnmatch_lines( - [ - "*= XFAILURES =*", - "*test_xfail_tb_line.py:6: assert 1 == 2", - "*= short test summary info =*", - "XFAIL test_xfail_tb_line.py::test_xfail", - "*= 1 xfailed in * =*", + "*test_xfail_tb_line.py:10: assert 3 == 4", ] )