-
Notifications
You must be signed in to change notification settings - Fork 45
JSON reporting #131
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
Merged
Merged
JSON reporting #131
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
17b3eab
Reintroduce test_has_names
asmeurer fdb7ffd
Move test_has_names to its own file
asmeurer cfbef56
Add .report.json to .gitignore
asmeurer 7623741
Store the module name in the test metadata
asmeurer 3f25af0
Revert import change in test_signatures.py
asmeurer ed4d58e
Some work on adding additional metadata to the json report
asmeurer 7f25cec
Move reporting stuff to its own file, and add parametrize values to t…
asmeurer 1f5284e
Make more types of test parameters JSON serializable
asmeurer d094951
Include the array_api_tests version in the JSON report metadata
asmeurer 0c94174
Add hypothesis information to the JSON report metadata
asmeurer befbd80
Add a check that the custom JSON report metadata is always JSON seria…
asmeurer 05c7802
Add array_attributes to stubs.__all__
asmeurer 9497ecd
Don't enable the add_api_name_to_metadata fixture unless --json-repor…
asmeurer b5234ad
Use a better name for the fixture
asmeurer f8e562c
Fix undefined name
asmeurer 3dca2ad
Allow classes to be JSON serialized in the JSON report
asmeurer d62073a
Add pytest-json-report to requirements.txt
asmeurer 1c42d3a
Add a check that pytest-json-report is installed
asmeurer 98db893
Remove attempt to only enable the fixture when --json-report is used
asmeurer 775d25b
Add a repr() fallback for any non-JSON-serializable type
asmeurer f356ffd
Use shorter tracebacks in the JSON report
asmeurer a1ccd19
Revert "Use shorter tracebacks in the JSON report"
asmeurer 0841ef3
De-duplicate warnings metadata in the report JSON
asmeurer File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -127,3 +127,6 @@ dmypy.json | |
|
||
# Pyre type checker | ||
.pyre/ | ||
|
||
# pytest-json-report | ||
.report.json |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}()" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
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 collections import Counter | ||
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) | ||
|
||
def pytest_json_modifyreport(json_report): | ||
# Deduplicate warnings. These duplicate warnings can cause the file size | ||
# to become huge. For instance, a warning from np.bool which is emitted | ||
# every time hypothesis runs (over a million times) causes the warnings | ||
# JSON for a plain numpy namespace run to be over 500MB. | ||
|
||
# This will lose information about what order the warnings were issued in, | ||
# but that isn't particularly helpful anyway since the warning metadata | ||
# doesn't store a full stack of where it was issued from. The resulting | ||
# warnings will be in order of the first time each warning is issued since | ||
# collections.Counter is ordered just like dict(). | ||
counted_warnings = Counter([frozenset(i.items()) for i in json_report['warnings']]) | ||
deduped_warnings = [{**dict(i), 'count': counted_warnings[i]} for i in counted_warnings] | ||
|
||
json_report['warnings'] = deduped_warnings |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about
array_properties
?There was a problem hiding this comment.
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.