Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/9436.improvement.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Avoid formatting tracebacks for tests marked with ``xfail(run=False)``.
5 changes: 4 additions & 1 deletion src/_pytest/reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
11 changes: 9 additions & 2 deletions src/_pytest/skipping.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import platform
import sys
import traceback
from typing import NoReturn

from _pytest.config import Config
from _pytest.config import hookimpl
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down
57 changes: 57 additions & 0 deletions testing/test_skipping.py
Original file line number Diff line number Diff line change
Expand Up @@ -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="""
Expand Down
Loading