diff --git a/changelog/9436.improvement.rst b/changelog/9436.improvement.rst new file mode 100644 index 00000000000..ec966a2981d --- /dev/null +++ b/changelog/9436.improvement.rst @@ -0,0 +1 @@ +Avoid formatting tracebacks for tests marked with ``xfail(run=False)``. diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 011a69db001..5fbe591ebd3 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -435,7 +435,10 @@ def from_item_and_call(cls, item: Item, call: CallInfo[None]) -> TestReport: longrepr = _format_exception_group_all_skipped_longrepr(item, excinfo) else: outcome = "failed" - longrepr = _format_failed_longrepr(item, call, excinfo) + if getattr(excinfo.value, "_pytest_xfail_not_run", False): + longrepr = str(excinfo.value) + else: + longrepr = _format_failed_longrepr(item, call, excinfo) for rwhen, key, content in item._report_sections: sections.append((f"Captured {key} {rwhen}", content)) return cls( diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index 3b067629de0..30b26e6cf28 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -10,6 +10,7 @@ import platform import sys import traceback +from typing import NoReturn from _pytest.config import Config from _pytest.config import hookimpl @@ -244,6 +245,12 @@ def evaluate_xfail_marks(item: Item) -> Xfail | None: xfailed_key = StashKey[Xfail | None]() +def _xfail_not_run(reason: str) -> NoReturn: + exc = xfail.Exception("[NOTRUN] " + reason) + exc._pytest_xfail_not_run = True # type: ignore[attr-defined] + raise exc + + @hookimpl(tryfirst=True) def pytest_runtest_setup(item: Item) -> None: skipped = evaluate_skip_marks(item) @@ -252,7 +259,7 @@ def pytest_runtest_setup(item: Item) -> None: item.stash[xfailed_key] = xfailed = evaluate_xfail_marks(item) if xfailed and not item.config.option.runxfail and not xfailed.run: - xfail("[NOTRUN] " + xfailed.reason) + _xfail_not_run(xfailed.reason) @hookimpl(wrapper=True) @@ -262,7 +269,7 @@ def pytest_runtest_call(item: Item) -> Generator[None]: item.stash[xfailed_key] = xfailed = evaluate_xfail_marks(item) if xfailed and not item.config.option.runxfail and not xfailed.run: - xfail("[NOTRUN] " + xfailed.reason) + _xfail_not_run(xfailed.reason) try: return (yield) diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 5bb641aed3c..fab1ca5b5ab 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -453,6 +453,63 @@ def test_this_false(): ] ) + def test_xfail_not_run_does_not_format_traceback(self, pytester: Pytester) -> None: + item = pytester.getitem( + """ + import pytest + @pytest.mark.xfail(run=False, reason="noway") + def test_func(): + assert 0 + """ + ) + + def repr_failure(*args, **kwargs): + raise AssertionError( # pragma: no cover + "xfail(run=False) should not format a traceback" + ) + + item.repr_failure = repr_failure # type: ignore[method-assign] + item._repr_failure_py = repr_failure # type: ignore[method-assign] + + reports = runtestprotocol(item, log=False) + + assert reports[0].skipped + assert reports[0].wasxfail == "[NOTRUN] noway" + assert reports[0].longrepr == "[NOTRUN] noway" + + def test_regular_failure_still_formats_traceback(self, pytester: Pytester) -> None: + item = pytester.getitem( + """ + def test_func(): + raise ValueError("boom") + """ + ) + + reports = runtestprotocol(item, log=False) + + assert reports[1].failed + assert "ValueError: boom" in reports[1].longreprtext + + def test_xfail_not_run_call_phase_marks_exception(self, pytester: Pytester) -> None: + from _pytest.skipping import pytest_runtest_call + + item = pytester.getitem( + """ + import pytest + @pytest.mark.xfail(run=False, reason="call phase") + def test_func(): + assert 0 + """ + ) + + runtest_call = pytest_runtest_call(item) + + with pytest.raises(pytest.xfail.Exception) as excinfo: + next(runtest_call) + + assert excinfo.value.msg == "[NOTRUN] call phase" + assert excinfo.value._pytest_xfail_not_run is True # type: ignore[attr-defined] + def test_xfail_not_run_no_setup_run(self, pytester: Pytester) -> None: p = pytester.makepyfile( test_one="""