From d44354aebe5c7936be5ca662ff09ab3473f1e98c Mon Sep 17 00:00:00 2001 From: harmin-parra Date: Thu, 25 Dec 2025 10:18:49 +0100 Subject: [PATCH 1/4] improve mark.xfail reason gathering --- src/_pytest/skipping.py | 4 +- testing/test_skipping.py | 123 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 1 deletion(-) diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index 3b067629de0..7e05cbca3c6 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -281,9 +281,11 @@ 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): + elif call.excinfo and isinstance(call.excinfo.value, (fail.Exception, xfail.Exception)): assert call.excinfo.value.msg is not None rep.wasxfail = 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..50fbd5665fc 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -1489,3 +1489,126 @@ 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 == "" From 3202f5d4922452620a4f556e227c5598889e20c1 Mon Sep 17 00:00:00 2001 From: harmin-parra Date: Wed, 31 Dec 2025 23:37:09 +0100 Subject: [PATCH 2/4] replace assert statement --- src/_pytest/skipping.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index 7e05cbca3c6..48365219a6c 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -282,8 +282,7 @@ def pytest_runtest_makereport( if item.config.option.runxfail: pass # don't interfere elif call.excinfo and isinstance(call.excinfo.value, (fail.Exception, xfail.Exception)): - assert call.excinfo.value.msg is not None - rep.wasxfail = call.excinfo.value.msg + rep.wasxfail = getattr(call.excinfo.value, "msg", '') if xfailed and xfailed.reason not in (None, ''): rep.wasxfail = xfailed.reason rep.outcome = "skipped" From 9d13a1c3936d0479efcb0ab54a2fb6d285829808 Mon Sep 17 00:00:00 2001 From: harmin-parra Date: Tue, 6 Jan 2026 23:04:12 +0100 Subject: [PATCH 3/4] changelog --- changelog/14090.improvement.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/14090.improvement.rst 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``. From 41871d2646124eedcda48d889aa58881254adad4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 6 Jan 2026 22:09:16 +0000 Subject: [PATCH 4/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/_pytest/skipping.py | 8 +++++--- testing/test_skipping.py | 33 ++++++++++++++++++++++++--------- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index 48365219a6c..13c433f9530 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -281,9 +281,11 @@ 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, (fail.Exception, xfail.Exception)): - rep.wasxfail = getattr(call.excinfo.value, "msg", '') - if xfailed and xfailed.reason not in (None, ''): + 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: diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 50fbd5665fc..f7278252098 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -1492,8 +1492,9 @@ def test_exit_reason_only(): class TestMarkXFailWithFailAndXFail: - - def test_marked_xfail_with_reason_pytest_fail_with_reason(self, pytester: Pytester) -> None: + def test_marked_xfail_with_reason_pytest_fail_with_reason( + self, pytester: Pytester + ) -> None: item = pytester.getitem( """ import pytest @@ -1508,7 +1509,9 @@ def test_func(): assert callreport.skipped assert callreport.wasxfail == "reason mark.xfail" - def test_marked_xfail_with_reason_pytest_fail_without_reason(self, pytester: Pytester) -> None: + def test_marked_xfail_with_reason_pytest_fail_without_reason( + self, pytester: Pytester + ) -> None: item = pytester.getitem( """ import pytest @@ -1523,7 +1526,9 @@ def test_func(): assert callreport.skipped assert callreport.wasxfail == "reason mark.xfail" - def test_marked_xfail_without_reason_pytest_fail_with_reason(self, pytester: Pytester) -> None: + def test_marked_xfail_without_reason_pytest_fail_with_reason( + self, pytester: Pytester + ) -> None: item = pytester.getitem( """ import pytest @@ -1538,7 +1543,9 @@ def test_func(): assert callreport.skipped assert callreport.wasxfail == "reason pytest.fail" - def test_marked_xfail_without_reason_pytest_fail_without_reason(self, pytester: Pytester) -> None: + def test_marked_xfail_without_reason_pytest_fail_without_reason( + self, pytester: Pytester + ) -> None: item = pytester.getitem( """ import pytest @@ -1553,7 +1560,9 @@ def test_func(): assert callreport.skipped assert callreport.wasxfail == "" - def test_marked_xfail_with_reason_pytest_xfail_with_reason(self, pytester: Pytester) -> None: + def test_marked_xfail_with_reason_pytest_xfail_with_reason( + self, pytester: Pytester + ) -> None: item = pytester.getitem( """ import pytest @@ -1568,7 +1577,9 @@ def test_func(): assert callreport.skipped assert callreport.wasxfail == "reason mark.xfail" - def test_marked_xfail_with_reason_pytest_xfail_without_reason(self, pytester: Pytester) -> None: + def test_marked_xfail_with_reason_pytest_xfail_without_reason( + self, pytester: Pytester + ) -> None: item = pytester.getitem( """ import pytest @@ -1583,7 +1594,9 @@ def test_func(): assert callreport.skipped assert callreport.wasxfail == "reason mark.xfail" - def test_marked_xfail_without_reason_pytest_xfail_with_reason(self, pytester: Pytester) -> None: + def test_marked_xfail_without_reason_pytest_xfail_with_reason( + self, pytester: Pytester + ) -> None: item = pytester.getitem( """ import pytest @@ -1598,7 +1611,9 @@ def test_func(): assert callreport.skipped assert callreport.wasxfail == "reason pytest.xfail" - def test_marked_xfail_without_reason_pytest_xfail_without_reason(self, pytester: Pytester) -> None: + def test_marked_xfail_without_reason_pytest_xfail_without_reason( + self, pytester: Pytester + ) -> None: item = pytester.getitem( """ import pytest