Skip to content

Commit d2d43a7

Browse files
authored
Merge pull request #157 from asmeurer/xfails-file
Add support for an xfails file, and allow the files to be specified with a flag
2 parents 2093a12 + 54c7141 commit d2d43a7

File tree

5 files changed

+197
-85
lines changed

5 files changed

+197
-85
lines changed

.github/workflows/numpy.yml

+1-48
Original file line numberDiff line numberDiff line change
@@ -28,51 +28,4 @@ jobs:
2828
env:
2929
ARRAY_API_TESTS_MODULE: numpy.array_api
3030
run: |
31-
# Skip testing functions with known issues
32-
cat << EOF >> skips.txt
33-
34-
# copy not implemented
35-
array_api_tests/test_creation_functions.py::test_asarray_arrays
36-
# https://github.com/numpy/numpy/issues/20870
37-
array_api_tests/test_data_type_functions.py::test_can_cast
38-
# The return dtype for trace is not consistent in the spec
39-
# https://github.com/data-apis/array-api/issues/202#issuecomment-952529197
40-
array_api_tests/test_linalg.py::test_trace
41-
# waiting on NumPy to allow/revert distinct NaNs for np.unique
42-
# https://github.com/numpy/numpy/issues/20326#issuecomment-1012380448
43-
array_api_tests/test_set_functions.py
44-
45-
# https://github.com/numpy/numpy/issues/21373
46-
array_api_tests/test_array_object.py::test_getitem
47-
48-
# missing copy arg
49-
array_api_tests/test_signatures.py::test_func_signature[reshape]
50-
51-
# https://github.com/numpy/numpy/issues/21211
52-
array_api_tests/test_special_cases.py::test_iop[__iadd__(x1_i is -0 and x2_i is -0) -> -0]
53-
# https://github.com/numpy/numpy/issues/21213
54-
array_api_tests/test_special_cases.py::test_iop[__ipow__(x1_i is -infinity and x2_i > 0 and not (x2_i.is_integer() and x2_i % 2 == 1)) -> +infinity]
55-
array_api_tests/test_special_cases.py::test_iop[__ipow__(x1_i is -0 and x2_i > 0 and not (x2_i.is_integer() and x2_i % 2 == 1)) -> +0]
56-
# noted diversions from spec
57-
array_api_tests/test_special_cases.py::test_binary[floor_divide(x1_i is +infinity and isfinite(x2_i) and x2_i > 0) -> +infinity]
58-
array_api_tests/test_special_cases.py::test_binary[floor_divide(x1_i is +infinity and isfinite(x2_i) and x2_i < 0) -> -infinity]
59-
array_api_tests/test_special_cases.py::test_binary[floor_divide(x1_i is -infinity and isfinite(x2_i) and x2_i > 0) -> -infinity]
60-
array_api_tests/test_special_cases.py::test_binary[floor_divide(x1_i is -infinity and isfinite(x2_i) and x2_i < 0) -> +infinity]
61-
array_api_tests/test_special_cases.py::test_binary[floor_divide(isfinite(x1_i) and x1_i > 0 and x2_i is -infinity) -> -0]
62-
array_api_tests/test_special_cases.py::test_binary[floor_divide(isfinite(x1_i) and x1_i < 0 and x2_i is +infinity) -> -0]
63-
array_api_tests/test_special_cases.py::test_binary[__floordiv__(x1_i is +infinity and isfinite(x2_i) and x2_i > 0) -> +infinity]
64-
array_api_tests/test_special_cases.py::test_binary[__floordiv__(x1_i is +infinity and isfinite(x2_i) and x2_i < 0) -> -infinity]
65-
array_api_tests/test_special_cases.py::test_binary[__floordiv__(x1_i is -infinity and isfinite(x2_i) and x2_i > 0) -> -infinity]
66-
array_api_tests/test_special_cases.py::test_binary[__floordiv__(x1_i is -infinity and isfinite(x2_i) and x2_i < 0) -> +infinity]
67-
array_api_tests/test_special_cases.py::test_binary[__floordiv__(isfinite(x1_i) and x1_i > 0 and x2_i is -infinity) -> -0]
68-
array_api_tests/test_special_cases.py::test_binary[__floordiv__(isfinite(x1_i) and x1_i < 0 and x2_i is +infinity) -> -0]
69-
array_api_tests/test_special_cases.py::test_iop[__ifloordiv__(x1_i is +infinity and isfinite(x2_i) and x2_i > 0) -> +infinity]
70-
array_api_tests/test_special_cases.py::test_iop[__ifloordiv__(x1_i is +infinity and isfinite(x2_i) and x2_i < 0) -> -infinity]
71-
array_api_tests/test_special_cases.py::test_iop[__ifloordiv__(x1_i is -infinity and isfinite(x2_i) and x2_i > 0) -> -infinity]
72-
array_api_tests/test_special_cases.py::test_iop[__ifloordiv__(x1_i is -infinity and isfinite(x2_i) and x2_i < 0) -> +infinity]
73-
array_api_tests/test_special_cases.py::test_iop[__ifloordiv__(isfinite(x1_i) and x1_i > 0 and x2_i is -infinity) -> -0]
74-
array_api_tests/test_special_cases.py::test_iop[__ifloordiv__(isfinite(x1_i) and x1_i < 0 and x2_i is +infinity) -> -0]
75-
76-
EOF
77-
78-
pytest -v -rxXfE --ci
31+
pytest -v -rxXfE --ci --skips-file numpy-skips.txt

README.md

+76-25
Original file line numberDiff line numberDiff line change
@@ -185,13 +185,22 @@ By default, tests for the optional Array API extensions such as
185185
will be skipped if not present in the specified array module. You can purposely
186186
skip testing extension(s) via the `--disable-extension` option.
187187

188-
#### Skip test cases
188+
#### Skip or XFAIL test cases
189189

190-
Test cases you want to skip can be specified in a `skips.txt` file in the root
191-
of this repository, e.g.:
190+
Test cases you want to skip can be specified in a skips or XFAILS file. The
191+
difference between skip and XFAIL is that XFAIL tests are still run and
192+
reported as XPASS if they pass.
193+
194+
By default, the skips and xfails files are `skips.txt` and `fails.txt` in the root
195+
of this repository, but any file can be specified with the `--skips-file` and
196+
`--xfails-file` command line flags.
197+
198+
The files should list the test ids to be skipped/xfailed. Empty lines and
199+
lines starting with `#` are ignored. The test id can be any substring of the
200+
test ids to skip/xfail.
192201

193202
```
194-
# ./skips.txt
203+
# skips.txt or xfails.txt
195204
# Line comments can be denoted with the hash symbol (#)
196205
197206
# Skip specific test case, e.g. when argsort() does not respect relative order
@@ -207,39 +216,81 @@ array_api_tests/test_add[__iadd__(x, s)]
207216
array_api_tests/test_set_functions.py
208217
```
209218

210-
For GitHub Actions, you might like to keep everything in the workflow config
211-
instead of having a seperate `skips.txt` file, e.g.:
219+
Here is an example GitHub Actions workflow file, where the xfails are stored
220+
in `array-api-tests.xfails.txt` in the base of the `your-array-library` repo.
221+
222+
If you want, you can use `-o xfail_strict=True`, which causes XPASS tests (XFAIL
223+
tests that actually pass) to fail the test suite. However, be aware that
224+
XFAILures can be flaky (see below, so this may not be a good idea unless you
225+
use some other mitigation of such flakyness).
226+
227+
If you don't want this behavior, you can remove it, or use `--skips-file`
228+
instead of `--xfails-file`.
212229

213230
```yaml
214231
# ./.github/workflows/array_api.yml
215-
...
216-
...
217-
- name: Run the test suite
232+
jobs:
233+
tests:
234+
runs-on: ubuntu-latest
235+
strategy:
236+
matrix:
237+
python-version: ['3.8', '3.9', '3.10', '3.11']
238+
239+
steps:
240+
- name: Checkout <your array library>
241+
uses: actions/checkout@v3
242+
with:
243+
path: your-array-library
244+
245+
- name: Checkout array-api-tests
246+
uses: actions/checkout@v3
247+
with:
248+
repository: data-apis/array-api-tests
249+
submodules: 'true'
250+
path: array-api-tests
251+
252+
- name: Run the array API test suite
218253
env:
219254
ARRAY_API_TESTS_MODULE: your.array.api.namespace
220255
run: |
221-
# Skip test cases with known issues
222-
cat << EOF >> skips.txt
223-
224-
# Comments can still work here
225-
array_api_tests/test_sorting_functions.py::test_argsort
226-
array_api_tests/test_add[__iadd__(x1, x2)]
227-
array_api_tests/test_add[__iadd__(x, s)]
228-
array_api_tests/test_set_functions.py
229-
230-
EOF
231-
232-
pytest -v -rxXfE --ci
256+
export PYTHONPATH="${GITHUB_WORKSPACE}/your-array-library"
257+
cd ${GITHUB_WORKSPACE}/array-api-tests
258+
pytest -v -rxXfE --ci --xfails-file ${GITHUB_WORKSPACE}/your-array-library/array-api-tests-xfails.txt array_api_tests/
233259
```
234260
261+
> **Warning**
262+
>
263+
> XFAIL tests that use Hypothesis (basically every test in the test suite except
264+
> those in test_has_names.py) can be flaky, due to the fact that Hypothesis
265+
> might not always run the test with an input that causes the test to fail.
266+
> There are several ways to avoid this problem:
267+
>
268+
> - Increase the maximum number of examples, e.g., by adding `--max-examples
269+
> 200` to the test command (the default is `100`, see below). This will
270+
> make it more likely that the failing case will be found, but it will also
271+
> make the tests take longer to run.
272+
> - Don't use `-o xfail_strict=True`. This will make it so that if an XFAIL
273+
> test passes, it will alert you in the test summary but will not cause the
274+
> test run to register as failed.
275+
> - Use skips instead of XFAILS. The difference between XFAIL and skip is that
276+
> a skipped test is never run at all, whereas an XFAIL test is always run
277+
> but ignored if it fails.
278+
> - Save the [Hypothesis examples
279+
> database](https://hypothesis.readthedocs.io/en/latest/database.html)
280+
> persistently on CI. That way as soon as a run finds one failing example,
281+
> it will always re-run future runs with that example. But note that the
282+
> Hypothesis examples database may be cleared when a new version of
283+
> Hypothesis or the test suite is released.
284+
235285
#### Max examples
236286

237287
The tests make heavy use
238288
[Hypothesis](https://hypothesis.readthedocs.io/en/latest/). You can configure
239-
how many examples are generated using the `--max-examples` flag, which defaults
240-
to 100. Lower values can be useful for quick checks, and larger values should
241-
result in more rigorous runs. For example, `--max-examples 10_000` may find bugs
242-
where default runs don't but will take much longer to run.
289+
how many examples are generated using the `--max-examples` flag, which
290+
defaults to `100`. Lower values can be useful for quick checks, and larger
291+
values should result in more rigorous runs. For example, `--max-examples
292+
10_000` may find bugs where default runs don't but will take much longer to
293+
run.
243294

244295

245296
## Contributing

array_api_tests/test_has_names.py

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
from .stubs import (array_attributes, array_methods, category_to_funcs,
1010
extension_to_funcs, EXTENSIONS)
1111

12+
pytestmark = pytest.mark.ci
13+
1214
has_name_params = []
1315
for ext, stubs in extension_to_funcs.items():
1416
for stub in stubs:

conftest.py

+77-12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from functools import lru_cache
22
from pathlib import Path
3+
import warnings
4+
import os
35

46
from hypothesis import settings
57
from pytest import mark
@@ -48,7 +50,17 @@ def pytest_addoption(parser):
4850
parser.addoption(
4951
"--ci",
5052
action="store_true",
51-
help="run just the tests appropiate for CI",
53+
help="run just the tests appropriate for CI",
54+
)
55+
parser.addoption(
56+
"--skips-file",
57+
action="store",
58+
help="file with tests to skip. Defaults to skips.txt"
59+
)
60+
parser.addoption(
61+
"--xfails-file",
62+
action="store",
63+
help="file with tests to skip. Defaults to xfails.txt"
5264
)
5365

5466

@@ -87,26 +99,56 @@ def xp_has_ext(ext: str) -> bool:
8799
return False
88100

89101

90-
skip_ids = []
91-
skips_path = Path(__file__).parent / "skips.txt"
92-
if skips_path.exists():
93-
with open(skips_path) as f:
94-
for line in f:
95-
if line.startswith("array_api_tests"):
102+
def pytest_collection_modifyitems(config, items):
103+
skips_file = skips_path = config.getoption('--skips-file')
104+
if skips_file is None:
105+
skips_file = Path(__file__).parent / "skips.txt"
106+
if skips_file.exists():
107+
skips_path = skips_file
108+
109+
skip_ids = []
110+
if skips_path:
111+
with open(os.path.expanduser(skips_path)) as f:
112+
for line in f:
113+
if line.startswith("array_api_tests"):
114+
id_ = line.strip("\n")
115+
skip_ids.append(id_)
116+
117+
xfails_file = xfails_path = config.getoption('--xfails-file')
118+
if xfails_file is None:
119+
xfails_file = Path(__file__).parent / "xfails.txt"
120+
if xfails_file.exists():
121+
xfails_path = xfails_file
122+
123+
xfail_ids = []
124+
if xfails_path:
125+
with open(os.path.expanduser(xfails_path)) as f:
126+
for line in f:
127+
if not line.strip() or line.startswith('#'):
128+
continue
96129
id_ = line.strip("\n")
97-
skip_ids.append(id_)
130+
xfail_ids.append(id_)
98131

132+
skip_id_matched = {id_: False for id_ in skip_ids}
133+
xfail_id_matched = {id_: False for id_ in xfail_ids}
99134

100-
def pytest_collection_modifyitems(config, items):
101135
disabled_exts = config.getoption("--disable-extension")
102136
disabled_dds = config.getoption("--disable-data-dependent-shapes")
103137
ci = config.getoption("--ci")
138+
104139
for item in items:
105140
markers = list(item.iter_markers())
106-
# skip if specified in skips.txt
141+
# skip if specified in skips file
107142
for id_ in skip_ids:
108-
if item.nodeid.startswith(id_):
109-
item.add_marker(mark.skip(reason="skips.txt"))
143+
if id_ in item.nodeid:
144+
item.add_marker(mark.skip(reason=f"--skips-file ({skips_file})"))
145+
skip_id_matched[id_] = True
146+
break
147+
# xfail if specified in xfails file
148+
for id_ in xfail_ids:
149+
if id_ in item.nodeid:
150+
item.add_marker(mark.xfail(reason=f"--xfails-file ({xfails_file})"))
151+
xfail_id_matched[id_] = True
110152
break
111153
# skip if disabled or non-existent extension
112154
ext_mark = next((m for m in markers if m.name == "xp_extension"), None)
@@ -141,3 +183,26 @@ def pytest_collection_modifyitems(config, items):
141183
reason=f"requires ARRAY_API_TESTS_VERSION=>{min_version}"
142184
)
143185
)
186+
187+
bad_ids_end_msg = (
188+
"Note the relevant tests might not of been collected by pytest, or "
189+
"another specified id might have already matched a test."
190+
)
191+
bad_skip_ids = [id_ for id_, matched in skip_id_matched.items() if not matched]
192+
if bad_skip_ids:
193+
f_bad_ids = "\n".join(f" {id_}" for id_ in bad_skip_ids)
194+
warnings.warn(
195+
f"{len(bad_skip_ids)} ids in skips file don't match any collected tests: \n"
196+
f"{f_bad_ids}\n"
197+
f"(skips file: {skips_file})\n"
198+
f"{bad_ids_end_msg}"
199+
)
200+
bad_xfail_ids = [id_ for id_, matched in xfail_id_matched.items() if not matched]
201+
if bad_xfail_ids:
202+
f_bad_ids = "\n".join(f" {id_}" for id_ in bad_xfail_ids)
203+
warnings.warn(
204+
f"{len(bad_xfail_ids)} ids in xfails file don't match any collected tests: \n"
205+
f"{f_bad_ids}\n"
206+
f"(xfails file: {xfails_file})\n"
207+
f"{bad_ids_end_msg}"
208+
)

numpy-skips.txt

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# copy not implemented
2+
array_api_tests/test_creation_functions.py::test_asarray_arrays
3+
# https://github.com/numpy/numpy/issues/20870
4+
array_api_tests/test_data_type_functions.py::test_can_cast
5+
# The return dtype for trace is not consistent in the spec
6+
# https://github.com/data-apis/array-api/issues/202#issuecomment-952529197
7+
array_api_tests/test_linalg.py::test_trace
8+
# waiting on NumPy to allow/revert distinct NaNs for np.unique
9+
# https://github.com/numpy/numpy/issues/20326#issuecomment-1012380448
10+
array_api_tests/test_set_functions.py
11+
12+
# https://github.com/numpy/numpy/issues/21373
13+
array_api_tests/test_array_object.py::test_getitem
14+
15+
# missing copy arg
16+
array_api_tests/test_signatures.py::test_func_signature[reshape]
17+
18+
# https://github.com/numpy/numpy/issues/21211
19+
array_api_tests/test_special_cases.py::test_iop[__iadd__(x1_i is -0 and x2_i is -0) -> -0]
20+
# https://github.com/numpy/numpy/issues/21213
21+
array_api_tests/test_special_cases.py::test_iop[__ipow__(x1_i is -infinity and x2_i > 0 and not (x2_i.is_integer() and x2_i % 2 == 1)) -> +infinity]
22+
array_api_tests/test_special_cases.py::test_iop[__ipow__(x1_i is -0 and x2_i > 0 and not (x2_i.is_integer() and x2_i % 2 == 1)) -> +0]
23+
# noted diversions from spec
24+
array_api_tests/test_special_cases.py::test_binary[floor_divide(x1_i is +infinity and isfinite(x2_i) and x2_i > 0) -> +infinity]
25+
array_api_tests/test_special_cases.py::test_binary[floor_divide(x1_i is +infinity and isfinite(x2_i) and x2_i < 0) -> -infinity]
26+
array_api_tests/test_special_cases.py::test_binary[floor_divide(x1_i is -infinity and isfinite(x2_i) and x2_i > 0) -> -infinity]
27+
array_api_tests/test_special_cases.py::test_binary[floor_divide(x1_i is -infinity and isfinite(x2_i) and x2_i < 0) -> +infinity]
28+
array_api_tests/test_special_cases.py::test_binary[floor_divide(isfinite(x1_i) and x1_i > 0 and x2_i is -infinity) -> -0]
29+
array_api_tests/test_special_cases.py::test_binary[floor_divide(isfinite(x1_i) and x1_i < 0 and x2_i is +infinity) -> -0]
30+
array_api_tests/test_special_cases.py::test_binary[__floordiv__(x1_i is +infinity and isfinite(x2_i) and x2_i > 0) -> +infinity]
31+
array_api_tests/test_special_cases.py::test_binary[__floordiv__(x1_i is +infinity and isfinite(x2_i) and x2_i < 0) -> -infinity]
32+
array_api_tests/test_special_cases.py::test_binary[__floordiv__(x1_i is -infinity and isfinite(x2_i) and x2_i > 0) -> -infinity]
33+
array_api_tests/test_special_cases.py::test_binary[__floordiv__(x1_i is -infinity and isfinite(x2_i) and x2_i < 0) -> +infinity]
34+
array_api_tests/test_special_cases.py::test_binary[__floordiv__(isfinite(x1_i) and x1_i > 0 and x2_i is -infinity) -> -0]
35+
array_api_tests/test_special_cases.py::test_binary[__floordiv__(isfinite(x1_i) and x1_i < 0 and x2_i is +infinity) -> -0]
36+
array_api_tests/test_special_cases.py::test_iop[__ifloordiv__(x1_i is +infinity and isfinite(x2_i) and x2_i > 0) -> +infinity]
37+
array_api_tests/test_special_cases.py::test_iop[__ifloordiv__(x1_i is +infinity and isfinite(x2_i) and x2_i < 0) -> -infinity]
38+
array_api_tests/test_special_cases.py::test_iop[__ifloordiv__(x1_i is -infinity and isfinite(x2_i) and x2_i > 0) -> -infinity]
39+
array_api_tests/test_special_cases.py::test_iop[__ifloordiv__(x1_i is -infinity and isfinite(x2_i) and x2_i < 0) -> +infinity]
40+
array_api_tests/test_special_cases.py::test_iop[__ifloordiv__(isfinite(x1_i) and x1_i > 0 and x2_i is -infinity) -> -0]
41+
array_api_tests/test_special_cases.py::test_iop[__ifloordiv__(isfinite(x1_i) and x1_i < 0 and x2_i is +infinity) -> -0]

0 commit comments

Comments
 (0)