diff --git a/changelog/14090.improvement.rst b/changelog/14090.improvement.rst new file mode 100644 index 00000000000..9706ef1e1b1 --- /dev/null +++ b/changelog/14090.improvement.rst @@ -0,0 +1 @@ +Improvement gathering the ``wasxfail`` value when the ``pytest.mark.xfail`` fixture is combined with explicit calls to ``pytest.fail`` or ``pytest.xfail``. diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index 3b067629de0..13c433f9530 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -281,9 +281,12 @@ def pytest_runtest_makereport( xfailed = item.stash.get(xfailed_key, None) if item.config.option.runxfail: pass # don't interfere - elif call.excinfo and isinstance(call.excinfo.value, xfail.Exception): - assert call.excinfo.value.msg is not None - rep.wasxfail = call.excinfo.value.msg + elif call.excinfo and isinstance( + call.excinfo.value, (fail.Exception, xfail.Exception) + ): + rep.wasxfail = getattr(call.excinfo.value, "msg", "") + if xfailed and xfailed.reason not in (None, ""): + rep.wasxfail = xfailed.reason rep.outcome = "skipped" elif not rep.skipped and xfailed: if call.excinfo: diff --git a/testing/test_skipping.py b/testing/test_skipping.py index e1e25e45468..f7278252098 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -1489,3 +1489,141 @@ def test_exit_reason_only(): ) result = pytester.runpytest(p) result.stdout.fnmatch_lines("*_pytest.outcomes.Exit: foo*") + + +class TestMarkXFailWithFailAndXFail: + def test_marked_xfail_with_reason_pytest_fail_with_reason( + self, pytester: Pytester + ) -> None: + item = pytester.getitem( + """ + import pytest + @pytest.mark.xfail(reason="reason mark.xfail") + def test_func(): + pytest.fail("reason pytest.fail") + """ + ) + reports = runtestprotocol(item, log=False) + assert len(reports) == 3 + callreport = reports[1] + assert callreport.skipped + assert callreport.wasxfail == "reason mark.xfail" + + def test_marked_xfail_with_reason_pytest_fail_without_reason( + self, pytester: Pytester + ) -> None: + item = pytester.getitem( + """ + import pytest + @pytest.mark.xfail(reason="reason mark.xfail") + def test_func(): + pytest.fail() + """ + ) + reports = runtestprotocol(item, log=False) + assert len(reports) == 3 + callreport = reports[1] + assert callreport.skipped + assert callreport.wasxfail == "reason mark.xfail" + + def test_marked_xfail_without_reason_pytest_fail_with_reason( + self, pytester: Pytester + ) -> None: + item = pytester.getitem( + """ + import pytest + @pytest.mark.xfail + def test_func(): + pytest.fail("reason pytest.fail") + """ + ) + reports = runtestprotocol(item, log=False) + assert len(reports) == 3 + callreport = reports[1] + assert callreport.skipped + assert callreport.wasxfail == "reason pytest.fail" + + def test_marked_xfail_without_reason_pytest_fail_without_reason( + self, pytester: Pytester + ) -> None: + item = pytester.getitem( + """ + import pytest + @pytest.mark.xfail + def test_func(): + pytest.fail() + """ + ) + reports = runtestprotocol(item, log=False) + assert len(reports) == 3 + callreport = reports[1] + assert callreport.skipped + assert callreport.wasxfail == "" + + def test_marked_xfail_with_reason_pytest_xfail_with_reason( + self, pytester: Pytester + ) -> None: + item = pytester.getitem( + """ + import pytest + @pytest.mark.xfail(reason="reason mark.xfail") + def test_func(): + pytest.xfail("reason pytest.xfail") + """ + ) + reports = runtestprotocol(item, log=False) + assert len(reports) == 3 + callreport = reports[1] + assert callreport.skipped + assert callreport.wasxfail == "reason mark.xfail" + + def test_marked_xfail_with_reason_pytest_xfail_without_reason( + self, pytester: Pytester + ) -> None: + item = pytester.getitem( + """ + import pytest + @pytest.mark.xfail(reason="reason mark.xfail") + def test_func(): + pytest.xfail() + """ + ) + reports = runtestprotocol(item, log=False) + assert len(reports) == 3 + callreport = reports[1] + assert callreport.skipped + assert callreport.wasxfail == "reason mark.xfail" + + def test_marked_xfail_without_reason_pytest_xfail_with_reason( + self, pytester: Pytester + ) -> None: + item = pytester.getitem( + """ + import pytest + @pytest.mark.xfail + def test_func(): + pytest.xfail("reason pytest.xfail") + """ + ) + reports = runtestprotocol(item, log=False) + assert len(reports) == 3 + callreport = reports[1] + assert callreport.skipped + assert callreport.wasxfail == "reason pytest.xfail" + + def test_marked_xfail_without_reason_pytest_xfail_without_reason( + self, pytester: Pytester + ) -> None: + item = pytester.getitem( + """ + import pytest + @pytest.mark.xfail + def test_func(): + pytest.xfail() + """ + ) + reports = runtestprotocol(item, log=False) + assert len(reports) == 3 + callreport = reports[1] + assert callreport.skipped + assert callreport.wasxfail == ""