Skip to content

Commit 4737c8c

Browse files
committed
add AbstractMatcher support to xfail
1 parent c93130c commit 4737c8c

File tree

3 files changed

+52
-5
lines changed

3 files changed

+52
-5
lines changed

src/_pytest/mark/structures.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from .._code import getfslineno
2424
from ..compat import NOTSET
2525
from ..compat import NotSetType
26+
from _pytest._raises_group import AbstractMatcher
2627
from _pytest.config import Config
2728
from _pytest.deprecated import check_ispytest
2829
from _pytest.deprecated import MARKED_FIXTURE
@@ -459,7 +460,10 @@ def __call__(
459460
*conditions: str | bool,
460461
reason: str = ...,
461462
run: bool = ...,
462-
raises: None | type[BaseException] | tuple[type[BaseException], ...] = ...,
463+
raises: None
464+
| type[BaseException]
465+
| tuple[type[BaseException], ...]
466+
| AbstractMatcher[BaseException] = ...,
463467
strict: bool = ...,
464468
) -> MarkDecorator: ...
465469

src/_pytest/skipping.py

+19-4
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import traceback
1313
from typing import Optional
1414

15+
from _pytest._raises_group import AbstractMatcher
1516
from _pytest.config import Config
1617
from _pytest.config import hookimpl
1718
from _pytest.config.argparsing import Parser
@@ -201,7 +202,12 @@ class Xfail:
201202
reason: str
202203
run: bool
203204
strict: bool
204-
raises: tuple[type[BaseException], ...] | None
205+
raises: (
206+
type[BaseException]
207+
| tuple[type[BaseException], ...]
208+
| AbstractMatcher[BaseException]
209+
| None
210+
)
205211

206212

207213
def evaluate_xfail_marks(item: Item) -> Xfail | None:
@@ -277,11 +283,20 @@ def pytest_runtest_makereport(
277283
elif not rep.skipped and xfailed:
278284
if call.excinfo:
279285
raises = xfailed.raises
280-
if raises is not None and not isinstance(call.excinfo.value, raises):
281-
rep.outcome = "failed"
282-
else:
286+
if raises is None or (
287+
(
288+
isinstance(raises, (type, tuple))
289+
and isinstance(call.excinfo.value, raises)
290+
)
291+
or (
292+
isinstance(raises, AbstractMatcher)
293+
and raises.matches(call.excinfo.value)
294+
)
295+
):
283296
rep.outcome = "skipped"
284297
rep.wasxfail = xfailed.reason
298+
else:
299+
rep.outcome = "failed"
285300
elif call.when == "call":
286301
if xfailed.strict:
287302
rep.outcome = "failed"

testing/python/raises_group.py

+28
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from _pytest._raises_group import RaisesGroup
1212
from _pytest._raises_group import repr_callable
1313
from _pytest.outcomes import Failed
14+
from _pytest.pytester import Pytester
1415
import pytest
1516

1617

@@ -1135,3 +1136,30 @@ def test_assert_matches() -> None:
11351136

11361137
# but even if we add assert_matches, will people remember to use it?
11371138
# other than writing a linter rule, I don't think we can catch `assert Matcher(...).matches`
1139+
1140+
1141+
# https://github.com/pytest-dev/pytest/issues/12504
1142+
def test_xfail_raisesgroup(pytester: Pytester) -> None:
1143+
pytester.makepyfile(
1144+
"""
1145+
import pytest
1146+
@pytest.mark.xfail(raises=pytest.RaisesGroup(ValueError))
1147+
def test_foo() -> None:
1148+
raise ExceptionGroup("foo", [ValueError()])
1149+
"""
1150+
)
1151+
result = pytester.runpytest()
1152+
result.assert_outcomes(xfailed=1)
1153+
1154+
1155+
def test_xfail_Matcher(pytester: Pytester) -> None:
1156+
pytester.makepyfile(
1157+
"""
1158+
import pytest
1159+
@pytest.mark.xfail(raises=pytest.Matcher(ValueError))
1160+
def test_foo() -> None:
1161+
raise ValueError
1162+
"""
1163+
)
1164+
result = pytester.runpytest()
1165+
result.assert_outcomes(xfailed=1)

0 commit comments

Comments
 (0)