Skip to content

Commit 3ae38ba

Browse files
[8.0.x] Add summary for xfails with -rxX option (#11778)
Co-authored-by: Fabian Sturm <[email protected]>
1 parent 6123b24 commit 3ae38ba

File tree

5 files changed

+166
-19
lines changed

5 files changed

+166
-19
lines changed

AUTHORS

+1
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ Erik Hasse
138138
Erik M. Bray
139139
Evan Kepner
140140
Evgeny Seliverstov
141+
Fabian Sturm
141142
Fabien Zarifian
142143
Fabio Zadrozny
143144
Felix Hofstätter

changelog/11233.feature.rst

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Improvements to how ``-r`` for xfailures and xpasses:
2+
3+
* Report tracebacks for xfailures when ``-rx`` is set.
4+
* Report captured output for xpasses when ``-rX`` is set.
5+
* For xpasses, add ``-`` in summary between test name and reason, to match how xfail is displayed.

src/_pytest/terminal.py

+40-18
Original file line numberDiff line numberDiff line change
@@ -878,8 +878,10 @@ def pytest_sessionfinish(
878878
def pytest_terminal_summary(self) -> Generator[None, None, None]:
879879
self.summary_errors()
880880
self.summary_failures()
881+
self.summary_xfailures()
881882
self.summary_warnings()
882883
self.summary_passes()
884+
self.summary_xpasses()
883885
try:
884886
return (yield)
885887
finally:
@@ -1009,12 +1011,20 @@ def collapsed_location_report(reports: List[WarningReport]) -> str:
10091011
)
10101012

10111013
def summary_passes(self) -> None:
1014+
self.summary_passes_combined("passed", "PASSES", "P")
1015+
1016+
def summary_xpasses(self) -> None:
1017+
self.summary_passes_combined("xpassed", "XPASSES", "X")
1018+
1019+
def summary_passes_combined(
1020+
self, which_reports: str, sep_title: str, needed_opt: str
1021+
) -> None:
10121022
if self.config.option.tbstyle != "no":
1013-
if self.hasopt("P"):
1014-
reports: List[TestReport] = self.getreports("passed")
1023+
if self.hasopt(needed_opt):
1024+
reports: List[TestReport] = self.getreports(which_reports)
10151025
if not reports:
10161026
return
1017-
self.write_sep("=", "PASSES")
1027+
self.write_sep("=", sep_title)
10181028
for rep in reports:
10191029
if rep.sections:
10201030
msg = self._getfailureheadline(rep)
@@ -1048,21 +1058,30 @@ def print_teardown_sections(self, rep: TestReport) -> None:
10481058
self._tw.line(content)
10491059

10501060
def summary_failures(self) -> None:
1061+
self.summary_failures_combined("failed", "FAILURES")
1062+
1063+
def summary_xfailures(self) -> None:
1064+
self.summary_failures_combined("xfailed", "XFAILURES", "x")
1065+
1066+
def summary_failures_combined(
1067+
self, which_reports: str, sep_title: str, needed_opt: Optional[str] = None
1068+
) -> None:
10511069
if self.config.option.tbstyle != "no":
1052-
reports: List[BaseReport] = self.getreports("failed")
1053-
if not reports:
1054-
return
1055-
self.write_sep("=", "FAILURES")
1056-
if self.config.option.tbstyle == "line":
1057-
for rep in reports:
1058-
line = self._getcrashline(rep)
1059-
self.write_line(line)
1060-
else:
1061-
for rep in reports:
1062-
msg = self._getfailureheadline(rep)
1063-
self.write_sep("_", msg, red=True, bold=True)
1064-
self._outrep_summary(rep)
1065-
self._handle_teardown_sections(rep.nodeid)
1070+
if not needed_opt or self.hasopt(needed_opt):
1071+
reports: List[BaseReport] = self.getreports(which_reports)
1072+
if not reports:
1073+
return
1074+
self.write_sep("=", sep_title)
1075+
if self.config.option.tbstyle == "line":
1076+
for rep in reports:
1077+
line = self._getcrashline(rep)
1078+
self.write_line(line)
1079+
else:
1080+
for rep in reports:
1081+
msg = self._getfailureheadline(rep)
1082+
self.write_sep("_", msg, red=True, bold=True)
1083+
self._outrep_summary(rep)
1084+
self._handle_teardown_sections(rep.nodeid)
10661085

10671086
def summary_errors(self) -> None:
10681087
if self.config.option.tbstyle != "no":
@@ -1168,8 +1187,11 @@ def show_xpassed(lines: List[str]) -> None:
11681187
verbose_word, **{_color_for_type["warnings"]: True}
11691188
)
11701189
nodeid = _get_node_id_with_markup(self._tw, self.config, rep)
1190+
line = f"{markup_word} {nodeid}"
11711191
reason = rep.wasxfail
1172-
lines.append(f"{markup_word} {nodeid} {reason}")
1192+
if reason:
1193+
line += " - " + str(reason)
1194+
lines.append(line)
11731195

11741196
def show_skipped(lines: List[str]) -> None:
11751197
skipped: List[CollectReport] = self.stats.get("skipped", [])

testing/test_skipping.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -649,7 +649,7 @@ def test_foo():
649649
result.stdout.fnmatch_lines(
650650
[
651651
"*test_strict_xfail*",
652-
"XPASS test_strict_xfail.py::test_foo unsupported feature",
652+
"XPASS test_strict_xfail.py::test_foo - unsupported feature",
653653
]
654654
)
655655
assert result.ret == (1 if strict else 0)

testing/test_terminal.py

+119
Original file line numberDiff line numberDiff line change
@@ -2619,3 +2619,122 @@ def test_format_trimmed() -> None:
26192619

26202620
assert _format_trimmed(" ({}) ", msg, len(msg) + 4) == " (unconditional skip) "
26212621
assert _format_trimmed(" ({}) ", msg, len(msg) + 3) == " (unconditional ...) "
2622+
2623+
2624+
def test_summary_xfail_reason(pytester: Pytester) -> None:
2625+
pytester.makepyfile(
2626+
"""
2627+
import pytest
2628+
2629+
@pytest.mark.xfail
2630+
def test_xfail():
2631+
assert False
2632+
2633+
@pytest.mark.xfail(reason="foo")
2634+
def test_xfail_reason():
2635+
assert False
2636+
"""
2637+
)
2638+
result = pytester.runpytest("-rx")
2639+
expect1 = "XFAIL test_summary_xfail_reason.py::test_xfail"
2640+
expect2 = "XFAIL test_summary_xfail_reason.py::test_xfail_reason - foo"
2641+
result.stdout.fnmatch_lines([expect1, expect2])
2642+
assert result.stdout.lines.count(expect1) == 1
2643+
assert result.stdout.lines.count(expect2) == 1
2644+
2645+
2646+
def test_summary_xfail_tb(pytester: Pytester) -> None:
2647+
pytester.makepyfile(
2648+
"""
2649+
import pytest
2650+
2651+
@pytest.mark.xfail
2652+
def test_xfail():
2653+
a, b = 1, 2
2654+
assert a == b
2655+
"""
2656+
)
2657+
result = pytester.runpytest("-rx")
2658+
result.stdout.fnmatch_lines(
2659+
[
2660+
"*= XFAILURES =*",
2661+
"*_ test_xfail _*",
2662+
"* @pytest.mark.xfail*",
2663+
"* def test_xfail():*",
2664+
"* a, b = 1, 2*",
2665+
"> *assert a == b*",
2666+
"E *assert 1 == 2*",
2667+
"test_summary_xfail_tb.py:6: AssertionError*",
2668+
"*= short test summary info =*",
2669+
"XFAIL test_summary_xfail_tb.py::test_xfail",
2670+
"*= 1 xfailed in * =*",
2671+
]
2672+
)
2673+
2674+
2675+
def test_xfail_tb_line(pytester: Pytester) -> None:
2676+
pytester.makepyfile(
2677+
"""
2678+
import pytest
2679+
2680+
@pytest.mark.xfail
2681+
def test_xfail():
2682+
a, b = 1, 2
2683+
assert a == b
2684+
"""
2685+
)
2686+
result = pytester.runpytest("-rx", "--tb=line")
2687+
result.stdout.fnmatch_lines(
2688+
[
2689+
"*= XFAILURES =*",
2690+
"*test_xfail_tb_line.py:6: assert 1 == 2",
2691+
"*= short test summary info =*",
2692+
"XFAIL test_xfail_tb_line.py::test_xfail",
2693+
"*= 1 xfailed in * =*",
2694+
]
2695+
)
2696+
2697+
2698+
def test_summary_xpass_reason(pytester: Pytester) -> None:
2699+
pytester.makepyfile(
2700+
"""
2701+
import pytest
2702+
2703+
@pytest.mark.xfail
2704+
def test_pass():
2705+
...
2706+
2707+
@pytest.mark.xfail(reason="foo")
2708+
def test_reason():
2709+
...
2710+
"""
2711+
)
2712+
result = pytester.runpytest("-rX")
2713+
expect1 = "XPASS test_summary_xpass_reason.py::test_pass"
2714+
expect2 = "XPASS test_summary_xpass_reason.py::test_reason - foo"
2715+
result.stdout.fnmatch_lines([expect1, expect2])
2716+
assert result.stdout.lines.count(expect1) == 1
2717+
assert result.stdout.lines.count(expect2) == 1
2718+
2719+
2720+
def test_xpass_output(pytester: Pytester) -> None:
2721+
pytester.makepyfile(
2722+
"""
2723+
import pytest
2724+
2725+
@pytest.mark.xfail
2726+
def test_pass():
2727+
print('hi there')
2728+
"""
2729+
)
2730+
result = pytester.runpytest("-rX")
2731+
result.stdout.fnmatch_lines(
2732+
[
2733+
"*= XPASSES =*",
2734+
"*_ test_pass _*",
2735+
"*- Captured stdout call -*",
2736+
"*= short test summary info =*",
2737+
"XPASS test_xpass_output.py::test_pass*",
2738+
"*= 1 xpassed in * =*",
2739+
]
2740+
)

0 commit comments

Comments
 (0)