Skip to content

Commit 66ab89c

Browse files
authored
Merge pull request #131 from asmeurer/json-reporting
JSON reporting
2 parents 4d9d7b4 + 0841ef3 commit 66ab89c

File tree

6 files changed

+157
-2
lines changed

6 files changed

+157
-2
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,6 @@ dmypy.json
127127

128128
# Pyre type checker
129129
.pyre/
130+
131+
# pytest-json-report
132+
.report.json

array_api_tests/stubs.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
__all__ = [
1010
"name_to_func",
1111
"array_methods",
12+
"array_attributes",
1213
"category_to_funcs",
1314
"EXTENSIONS",
1415
"extension_to_funcs",
@@ -34,6 +35,10 @@
3435
f for n, f in inspect.getmembers(array, predicate=inspect.isfunction)
3536
if n != "__init__" # probably exists for Sphinx
3637
]
38+
array_attributes = [
39+
n for n, f in inspect.getmembers(array, predicate=lambda x: not inspect.isfunction(x))
40+
if n != "__init__" # probably exists for Sphinx
41+
]
3742

3843
category_to_funcs: Dict[str, List[FunctionType]] = {}
3944
for name, mod in name_to_mod.items():

array_api_tests/test_has_names.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""
2+
This is a very basic test to see what names are defined in a library. It
3+
does not even require functioning hypothesis array_api support.
4+
"""
5+
6+
import pytest
7+
8+
from ._array_module import mod as xp, mod_name
9+
from .stubs import (array_attributes, array_methods, category_to_funcs,
10+
extension_to_funcs, EXTENSIONS)
11+
12+
has_name_params = []
13+
for ext, stubs in extension_to_funcs.items():
14+
for stub in stubs:
15+
has_name_params.append(pytest.param(ext, stub.__name__))
16+
for cat, stubs in category_to_funcs.items():
17+
for stub in stubs:
18+
has_name_params.append(pytest.param(cat, stub.__name__))
19+
for meth in array_methods:
20+
has_name_params.append(pytest.param('array_method', meth.__name__))
21+
for attr in array_attributes:
22+
has_name_params.append(pytest.param('array_attribute', attr))
23+
24+
@pytest.mark.parametrize("category, name", has_name_params)
25+
def test_has_names(category, name):
26+
if category in EXTENSIONS:
27+
ext_mod = getattr(xp, category)
28+
assert hasattr(ext_mod, name), f"{mod_name} is missing the {category} extension function {name}()"
29+
elif category.startswith('array_'):
30+
# TODO: This would fail if ones() is missing.
31+
arr = xp.ones((1, 1))
32+
if category == 'array_attribute':
33+
assert hasattr(arr, name), f"The {mod_name} array object is missing the attribute {name}"
34+
else:
35+
assert hasattr(arr, name), f"The {mod_name} array object is missing the method {name}()"
36+
else:
37+
assert hasattr(xp, name), f"{mod_name} is missing the {category} function {name}()"

conftest.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
from array_api_tests import _array_module as xp
88
from array_api_tests._array_module import _UndefinedStub
99

10-
settings.register_profile("xp_default", deadline=800)
10+
from reporting import pytest_metadata, pytest_json_modifyreport, add_extra_json_metadata # noqa
1111

12+
settings.register_profile("xp_default", deadline=800)
1213

1314
def pytest_addoption(parser):
1415
# Hypothesis max examples
@@ -120,7 +121,7 @@ def pytest_collection_modifyitems(config, items):
120121
mark.skip(reason="disabled via --disable-data-dependent-shapes")
121122
)
122123
break
123-
# skip if test not appropiate for CI
124+
# skip if test not appropriate for CI
124125
if ci:
125126
ci_mark = next((m for m in markers if m.name == "ci"), None)
126127
if ci_mark is None:

reporting.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
from array_api_tests.dtype_helpers import dtype_to_name
2+
from array_api_tests import _array_module as xp
3+
from array_api_tests import __version__
4+
5+
from collections import Counter
6+
from types import BuiltinFunctionType, FunctionType
7+
import dataclasses
8+
import json
9+
import warnings
10+
11+
from hypothesis.strategies import SearchStrategy
12+
13+
from pytest import mark, fixture
14+
try:
15+
import pytest_jsonreport # noqa
16+
except ImportError:
17+
raise ImportError("pytest-json-report is required to run the array API tests")
18+
19+
def to_json_serializable(o):
20+
if o in dtype_to_name:
21+
return dtype_to_name[o]
22+
if isinstance(o, (BuiltinFunctionType, FunctionType, type)):
23+
return o.__name__
24+
if dataclasses.is_dataclass(o):
25+
return to_json_serializable(dataclasses.asdict(o))
26+
if isinstance(o, SearchStrategy):
27+
return repr(o)
28+
if isinstance(o, dict):
29+
return {to_json_serializable(k): to_json_serializable(v) for k, v in o.items()}
30+
if isinstance(o, tuple):
31+
if hasattr(o, '_asdict'): # namedtuple
32+
return to_json_serializable(o._asdict())
33+
return tuple(to_json_serializable(i) for i in o)
34+
if isinstance(o, list):
35+
return [to_json_serializable(i) for i in o]
36+
37+
# Ensure everything is JSON serializable. If this warning is issued, it
38+
# means the given type needs to be added above if possible.
39+
try:
40+
json.dumps(o)
41+
except TypeError:
42+
warnings.warn(f"{o!r} (of type {type(o)}) is not JSON-serializable. Using the repr instead.")
43+
return repr(o)
44+
45+
return o
46+
47+
@mark.optionalhook
48+
def pytest_metadata(metadata):
49+
"""
50+
Additional global metadata for --json-report.
51+
"""
52+
metadata['array_api_tests_module'] = xp.mod_name
53+
metadata['array_api_tests_version'] = __version__
54+
55+
@fixture(autouse=True)
56+
def add_extra_json_metadata(request, json_metadata):
57+
"""
58+
Additional per-test metadata for --json-report
59+
"""
60+
def add_metadata(name, obj):
61+
obj = to_json_serializable(obj)
62+
json_metadata[name] = obj
63+
64+
test_module = request.module.__name__
65+
if test_module.startswith('array_api_tests.meta'):
66+
return
67+
68+
test_function = request.function.__name__
69+
assert test_function.startswith('test_'), 'unexpected test function name'
70+
71+
if test_module == 'array_api_tests.test_has_names':
72+
array_api_function_name = None
73+
else:
74+
array_api_function_name = test_function[len('test_'):]
75+
76+
add_metadata('test_module', test_module)
77+
add_metadata('test_function', test_function)
78+
add_metadata('array_api_function_name', array_api_function_name)
79+
80+
if hasattr(request.node, 'callspec'):
81+
params = request.node.callspec.params
82+
add_metadata('params', params)
83+
84+
def finalizer():
85+
# TODO: This metadata is all in the form of error strings. It might be
86+
# nice to extract the hypothesis failing inputs directly somehow.
87+
if hasattr(request.node, 'hypothesis_report_information'):
88+
add_metadata('hypothesis_report_information', request.node.hypothesis_report_information)
89+
if hasattr(request.node, 'hypothesis_statistics'):
90+
add_metadata('hypothesis_statistics', request.node.hypothesis_statistics)
91+
92+
request.addfinalizer(finalizer)
93+
94+
def pytest_json_modifyreport(json_report):
95+
# Deduplicate warnings. These duplicate warnings can cause the file size
96+
# to become huge. For instance, a warning from np.bool which is emitted
97+
# every time hypothesis runs (over a million times) causes the warnings
98+
# JSON for a plain numpy namespace run to be over 500MB.
99+
100+
# This will lose information about what order the warnings were issued in,
101+
# but that isn't particularly helpful anyway since the warning metadata
102+
# doesn't store a full stack of where it was issued from. The resulting
103+
# warnings will be in order of the first time each warning is issued since
104+
# collections.Counter is ordered just like dict().
105+
counted_warnings = Counter([frozenset(i.items()) for i in json_report['warnings']])
106+
deduped_warnings = [{**dict(i), 'count': counted_warnings[i]} for i in counted_warnings]
107+
108+
json_report['warnings'] = deduped_warnings

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pytest
2+
pytest-json-report
23
hypothesis>=6.45.0
34
ndindex>=1.6

0 commit comments

Comments
 (0)