Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JSON reporting #131

Merged
merged 23 commits into from
Aug 19, 2022
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
17b3eab
Reintroduce test_has_names
asmeurer Jun 24, 2022
fdb7ffd
Move test_has_names to its own file
asmeurer Jun 24, 2022
cfbef56
Add .report.json to .gitignore
asmeurer Jun 24, 2022
7623741
Store the module name in the test metadata
asmeurer Jun 24, 2022
3f25af0
Revert import change in test_signatures.py
asmeurer Jun 29, 2022
ed4d58e
Some work on adding additional metadata to the json report
asmeurer Aug 1, 2022
7f25cec
Move reporting stuff to its own file, and add parametrize values to t…
asmeurer Aug 2, 2022
1f5284e
Make more types of test parameters JSON serializable
asmeurer Aug 2, 2022
d094951
Include the array_api_tests version in the JSON report metadata
asmeurer Aug 2, 2022
0c94174
Add hypothesis information to the JSON report metadata
asmeurer Aug 2, 2022
befbd80
Add a check that the custom JSON report metadata is always JSON seria…
asmeurer Aug 2, 2022
05c7802
Add array_attributes to stubs.__all__
asmeurer Aug 2, 2022
9497ecd
Don't enable the add_api_name_to_metadata fixture unless --json-repor…
asmeurer Aug 2, 2022
b5234ad
Use a better name for the fixture
asmeurer Aug 2, 2022
f8e562c
Fix undefined name
asmeurer Aug 2, 2022
3dca2ad
Allow classes to be JSON serialized in the JSON report
asmeurer Aug 2, 2022
d62073a
Add pytest-json-report to requirements.txt
asmeurer Aug 2, 2022
1c42d3a
Add a check that pytest-json-report is installed
asmeurer Aug 2, 2022
98db893
Remove attempt to only enable the fixture when --json-report is used
asmeurer Aug 2, 2022
775d25b
Add a repr() fallback for any non-JSON-serializable type
asmeurer Aug 2, 2022
f356ffd
Use shorter tracebacks in the JSON report
asmeurer Aug 3, 2022
a1ccd19
Revert "Use shorter tracebacks in the JSON report"
asmeurer Aug 3, 2022
0841ef3
De-duplicate warnings metadata in the report JSON
asmeurer Aug 4, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,6 @@ dmypy.json

# Pyre type checker
.pyre/

# pytest-json-report
.report.json
5 changes: 5 additions & 0 deletions array_api_tests/stubs.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
__all__ = [
"name_to_func",
"array_methods",
"array_attributes",
"category_to_funcs",
"EXTENSIONS",
"extension_to_funcs",
Expand All @@ -34,6 +35,10 @@
f for n, f in inspect.getmembers(array, predicate=inspect.isfunction)
if n != "__init__" # probably exists for Sphinx
]
array_attributes = [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about array_properties?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And in any case, if you could add this to __all__. Maybe this is just a weird side product of how I learnt Python, but I like using it to denote the internally-public API of a module.

n for n, f in inspect.getmembers(array, predicate=lambda x: not inspect.isfunction(x))
if n != "__init__" # probably exists for Sphinx
]

category_to_funcs: Dict[str, List[FunctionType]] = {}
for name, mod in name_to_mod.items():
Expand Down
37 changes: 37 additions & 0 deletions array_api_tests/test_has_names.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""
This is a very basic test to see what names are defined in a library. It
does not even require functioning hypothesis array_api support.
"""

import pytest

from ._array_module import mod as xp, mod_name
from .stubs import (array_attributes, array_methods, category_to_funcs,
extension_to_funcs, EXTENSIONS)

has_name_params = []
for ext, stubs in extension_to_funcs.items():
for stub in stubs:
has_name_params.append(pytest.param(ext, stub.__name__))
for cat, stubs in category_to_funcs.items():
for stub in stubs:
has_name_params.append(pytest.param(cat, stub.__name__))
for meth in array_methods:
has_name_params.append(pytest.param('array_method', meth.__name__))
for attr in array_attributes:
has_name_params.append(pytest.param('array_attribute', attr))

@pytest.mark.parametrize("category, name", has_name_params)
def test_has_names(category, name):
if category in EXTENSIONS:
ext_mod = getattr(xp, category)
assert hasattr(ext_mod, name), f"{mod_name} is missing the {category} extension function {name}()"
elif category.startswith('array_'):
# TODO: This would fail if ones() is missing.
arr = xp.ones((1, 1))
if category == 'array_attribute':
assert hasattr(arr, name), f"The {mod_name} array object is missing the attribute {name}"
else:
assert hasattr(arr, name), f"The {mod_name} array object is missing the method {name}()"
else:
assert hasattr(xp, name), f"{mod_name} is missing the {category} function {name}()"
5 changes: 3 additions & 2 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
from array_api_tests import _array_module as xp
from array_api_tests._array_module import _UndefinedStub

settings.register_profile("xp_default", deadline=800)
from reporting import pytest_metadata, add_extra_json_metadata # noqa
Copy link
Member

@honno honno Aug 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a small comment explaining what's being imported here and why? I assume it's doing some magic when imported. Oh import reporting is importing the file you've introduced. I'd lean towards just deleting reporting.py and putting those fixtures in conftest.py, but I see value in not bloating conftest.py.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the names have to be defined here so that pytest will see them as fixtures. I decided to keep the reporting in a separate file to keep things clean, since it's already a good bit of code and could potentially be more if we decide to add more stuff to the report in the future.


settings.register_profile("xp_default", deadline=800)

def pytest_addoption(parser):
# Hypothesis max examples
Expand Down Expand Up @@ -120,7 +121,7 @@ def pytest_collection_modifyitems(config, items):
mark.skip(reason="disabled via --disable-data-dependent-shapes")
)
break
# skip if test not appropiate for CI
# skip if test not appropriate for CI
if ci:
ci_mark = next((m for m in markers if m.name == "ci"), None)
if ci_mark is None:
Expand Down
91 changes: 91 additions & 0 deletions reporting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
from array_api_tests.dtype_helpers import dtype_to_name
from array_api_tests import _array_module as xp
from array_api_tests import __version__

from types import BuiltinFunctionType, FunctionType
import dataclasses
import json
import warnings

from hypothesis.strategies import SearchStrategy

from pytest import mark, fixture
try:
import pytest_jsonreport # noqa
except ImportError:
raise ImportError("pytest-json-report is required to run the array API tests")

def to_json_serializable(o):
if o in dtype_to_name:
return dtype_to_name[o]
if isinstance(o, (BuiltinFunctionType, FunctionType, type)):
return o.__name__
if dataclasses.is_dataclass(o):
return to_json_serializable(dataclasses.asdict(o))
if isinstance(o, SearchStrategy):
return repr(o)
if isinstance(o, dict):
return {to_json_serializable(k): to_json_serializable(v) for k, v in o.items()}
if isinstance(o, tuple):
if hasattr(o, '_asdict'): # namedtuple
return to_json_serializable(o._asdict())
return tuple(to_json_serializable(i) for i in o)
if isinstance(o, list):
return [to_json_serializable(i) for i in o]

# Ensure everything is JSON serializable. If this warning is issued, it
# means the given type needs to be added above if possible.
try:
json.dumps(o)
except TypeError:
warnings.warn(f"{o!r} (of type {type(o)}) is not JSON-serializable. Using the repr instead.")
return repr(o)

return o

@mark.optionalhook
def pytest_metadata(metadata):
"""
Additional global metadata for --json-report.
"""
metadata['array_api_tests_module'] = xp.mod_name
metadata['array_api_tests_version'] = __version__

@fixture(autouse=True)
def add_extra_json_metadata(request, json_metadata):
"""
Additional per-test metadata for --json-report
"""
def add_metadata(name, obj):
obj = to_json_serializable(obj)
json_metadata[name] = obj

test_module = request.module.__name__
if test_module.startswith('array_api_tests.meta'):
return

test_function = request.function.__name__
assert test_function.startswith('test_'), 'unexpected test function name'

if test_module == 'array_api_tests.test_has_names':
array_api_function_name = None
else:
array_api_function_name = test_function[len('test_'):]

add_metadata('test_module', test_module)
add_metadata('test_function', test_function)
add_metadata('array_api_function_name', array_api_function_name)

if hasattr(request.node, 'callspec'):
params = request.node.callspec.params
add_metadata('params', params)

def finalizer():
# TODO: This metadata is all in the form of error strings. It might be
# nice to extract the hypothesis failing inputs directly somehow.
if hasattr(request.node, 'hypothesis_report_information'):
add_metadata('hypothesis_report_information', request.node.hypothesis_report_information)
if hasattr(request.node, 'hypothesis_statistics'):
add_metadata('hypothesis_statistics', request.node.hypothesis_statistics)

request.addfinalizer(finalizer)
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pytest
pytest-json-report
hypothesis>=6.45.0
ndindex>=1.6