Skip to content

Commit 9c9326a

Browse files
authored
TST: xfail_xp_backend(strict=False) (#269)
1 parent 6e3ad0f commit 9c9326a

File tree

7 files changed

+44
-22
lines changed

7 files changed

+44
-22
lines changed

pixi.lock

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

+2-2
Original file line numberDiff line numberDiff line change
@@ -213,8 +213,8 @@ filterwarnings = ["error"]
213213
log_cli_level = "INFO"
214214
testpaths = ["tests"]
215215
markers = [
216-
"skip_xp_backend(library, *, reason=None): Skip test for a specific backend",
217-
"xfail_xp_backend(library, *, reason=None): Xfail test for a specific backend",
216+
"skip_xp_backend(library, /, *, reason=None): Skip test for a specific backend",
217+
"xfail_xp_backend(library, /, *, reason=None, strict=None): Xfail test for a specific backend",
218218
]
219219

220220

src/array_api_extra/_lib/_testing.py

+12-2
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,9 @@ def xp_assert_close(
195195
)
196196

197197

198-
def xfail(request: pytest.FixtureRequest, reason: str) -> None:
198+
def xfail(
199+
request: pytest.FixtureRequest, *, reason: str, strict: bool | None = None
200+
) -> None:
199201
"""
200202
XFAIL the currently running test.
201203
@@ -209,5 +211,13 @@ def xfail(request: pytest.FixtureRequest, reason: str) -> None:
209211
``request`` argument of the test function.
210212
reason : str
211213
Reason for the expected failure.
214+
strict: bool, optional
215+
If True, the test will be marked as failed if it passes.
216+
If False, the test will be marked as passed if it fails.
217+
Default: ``xfail_strict`` value in ``pyproject.toml``, or False if absent.
212218
"""
213-
request.node.add_marker(pytest.mark.xfail(reason=reason))
219+
if strict is not None:
220+
marker = pytest.mark.xfail(reason=reason, strict=strict)
221+
else:
222+
marker = pytest.mark.xfail(reason=reason)
223+
request.node.add_marker(marker)

tests/conftest.py

+19-11
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""Pytest fixtures."""
22

33
from collections.abc import Callable, Generator
4-
from contextlib import suppress
54
from functools import partial, wraps
65
from types import ModuleType
76
from typing import ParamSpec, TypeVar, cast
@@ -34,20 +33,29 @@ def library(request: pytest.FixtureRequest) -> Backend: # numpydoc ignore=PR01,
3433
"""
3534
elem = cast(Backend, request.param)
3635

37-
for marker_name, skip_or_xfail in (
38-
("skip_xp_backend", pytest.skip),
39-
("xfail_xp_backend", partial(xfail, request)),
36+
for marker_name, skip_or_xfail, allow_kwargs in (
37+
("skip_xp_backend", pytest.skip, {"reason"}),
38+
("xfail_xp_backend", partial(xfail, request), {"reason", "strict"}),
4039
):
4140
for marker in request.node.iter_markers(marker_name):
42-
library = marker.kwargs.get("library") or marker.args[0] # type: ignore[no-untyped-usage]
43-
if not isinstance(library, Backend):
44-
msg = f"argument of {marker_name} must be a Backend enum"
41+
if len(marker.args) != 1: # pyright: ignore[reportUnknownArgumentType]
42+
msg = f"Expected exactly one positional argument; got {marker.args}"
4543
raise TypeError(msg)
44+
if not isinstance(marker.args[0], Backend):
45+
msg = f"Argument of {marker_name} must be a Backend enum"
46+
raise TypeError(msg)
47+
if invalid_kwargs := set(marker.kwargs) - allow_kwargs: # pyright: ignore[reportUnknownArgumentType]
48+
msg = f"Unexpected kwarg(s): {invalid_kwargs}"
49+
raise TypeError(msg)
50+
51+
library: Backend = marker.args[0]
52+
reason: str | None = marker.kwargs.get("reason", None)
53+
strict: bool | None = marker.kwargs.get("strict", None)
54+
4655
if library == elem:
47-
reason = str(library)
48-
with suppress(KeyError):
49-
reason += ":" + cast(str, marker.kwargs["reason"])
50-
skip_or_xfail(reason=reason)
56+
reason = f"{library}: {reason}" if reason else str(library) # pyright: ignore[reportUnknownArgumentType]
57+
kwargs = {"strict": strict} if strict is not None else {}
58+
skip_or_xfail(reason=reason, **kwargs) # pyright: ignore[reportUnknownArgumentType]
5159

5260
return elem
5361

tests/test_at.py

+8-4
Original file line numberDiff line numberDiff line change
@@ -115,11 +115,15 @@ def assert_copy(
115115
pytest.param(
116116
*(True, 1, 1),
117117
marks=(
118-
pytest.mark.skip_xp_backend( # test passes when copy=False
119-
Backend.JAX, reason="bool mask update with shaped rhs"
118+
pytest.mark.xfail_xp_backend(
119+
Backend.JAX,
120+
reason="bool mask update with shaped rhs",
121+
strict=False, # test passes when copy=False
120122
),
121-
pytest.mark.skip_xp_backend( # test passes when copy=False
122-
Backend.JAX_GPU, reason="bool mask update with shaped rhs"
123+
pytest.mark.xfail_xp_backend(
124+
Backend.JAX_GPU,
125+
reason="bool mask update with shaped rhs",
126+
strict=False, # test passes when copy=False
123127
),
124128
pytest.mark.xfail_xp_backend(
125129
Backend.DASK, reason="bool mask update with shaped rhs"

tests/test_funcs.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ def test_device(self, xp: ModuleType, device: Device):
196196
y = apply_where(x % 2 == 0, x, self.f1, fill_value=x)
197197
assert get_device(y) == device
198198

199-
@pytest.mark.skip_xp_backend(Backend.SPARSE, reason="no isdtype")
199+
@pytest.mark.xfail_xp_backend(Backend.SPARSE, reason="no isdtype")
200200
@pytest.mark.filterwarnings("ignore::RuntimeWarning") # overflows, etc.
201201
@hypothesis.settings(
202202
# The xp and library fixtures are not regenerated between hypothesis iterations

tests/test_helpers.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
lazy_xp_function(in1d, jax_jit=False, static_argnames=("assume_unique", "invert", "xp"))
2828

2929

30-
@pytest.mark.xfail_xp_backend(Backend.SPARSE, reason="no unique_inverse")
30+
@pytest.mark.skip_xp_backend(Backend.SPARSE, reason="no unique_inverse")
3131
@pytest.mark.skip_xp_backend(Backend.ARRAY_API_STRICTEST, reason="no unique_inverse")
3232
class TestIn1D:
3333
# cover both code paths

0 commit comments

Comments
 (0)