-
-
Notifications
You must be signed in to change notification settings - Fork 3k
Serialization hooks #4965
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
Serialization hooks #4965
Changes from all commits
0c63f99
7b9a414
d856f4e
f2e0c74
6457742
e4eec34
ceef0af
2d77018
9311d82
65c8e8a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| New ``pytest_report_to_serializable`` and ``pytest_report_from_serializable`` **experimental** hooks. | ||
|
|
||
| These hooks will be used by ``pytest-xdist``, ``pytest-subtests``, and the replacement for | ||
| resultlog to serialize and customize reports. | ||
|
|
||
| They are experimental, meaning that their details might change or even be removed | ||
| completely in future patch releases without warning. | ||
|
|
||
| Feedback is welcome from plugin authors and users alike. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -140,6 +140,7 @@ def directory_arg(path, optname): | |
| "stepwise", | ||
| "warnings", | ||
| "logging", | ||
| "reports", | ||
| ) | ||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,19 @@ | ||
| from pprint import pprint | ||
|
|
||
| import py | ||
| import six | ||
|
|
||
| from _pytest._code.code import ExceptionInfo | ||
| from _pytest._code.code import ReprEntry | ||
| from _pytest._code.code import ReprEntryNative | ||
| from _pytest._code.code import ReprExceptionInfo | ||
| from _pytest._code.code import ReprFileLocation | ||
| from _pytest._code.code import ReprFuncArgs | ||
| from _pytest._code.code import ReprLocals | ||
| from _pytest._code.code import ReprTraceback | ||
| from _pytest._code.code import TerminalRepr | ||
| from _pytest.outcomes import skip | ||
| from _pytest.pathlib import Path | ||
|
|
||
|
|
||
| def getslaveinfoline(node): | ||
|
|
@@ -137,12 +148,136 @@ def head_line(self): | |
| fspath, lineno, domain = self.location | ||
| return domain | ||
|
|
||
| def _to_json(self): | ||
| """ | ||
| This was originally the serialize_report() function from xdist (ca03269). | ||
|
|
||
| Returns the contents of this report as a dict of builtin entries, suitable for | ||
| serialization. | ||
|
|
||
| Experimental method. | ||
| """ | ||
|
|
||
| def disassembled_report(rep): | ||
| reprtraceback = rep.longrepr.reprtraceback.__dict__.copy() | ||
| reprcrash = rep.longrepr.reprcrash.__dict__.copy() | ||
|
|
||
| new_entries = [] | ||
| for entry in reprtraceback["reprentries"]: | ||
| entry_data = { | ||
| "type": type(entry).__name__, | ||
| "data": entry.__dict__.copy(), | ||
| } | ||
| for key, value in entry_data["data"].items(): | ||
| if hasattr(value, "__dict__"): | ||
| entry_data["data"][key] = value.__dict__.copy() | ||
| new_entries.append(entry_data) | ||
|
|
||
| reprtraceback["reprentries"] = new_entries | ||
|
|
||
| return { | ||
| "reprcrash": reprcrash, | ||
| "reprtraceback": reprtraceback, | ||
| "sections": rep.longrepr.sections, | ||
| } | ||
|
|
||
| d = self.__dict__.copy() | ||
| if hasattr(self.longrepr, "toterminal"): | ||
| if hasattr(self.longrepr, "reprtraceback") and hasattr( | ||
| self.longrepr, "reprcrash" | ||
| ): | ||
| d["longrepr"] = disassembled_report(self) | ||
| else: | ||
| d["longrepr"] = six.text_type(self.longrepr) | ||
| else: | ||
| d["longrepr"] = self.longrepr | ||
| for name in d: | ||
| if isinstance(d[name], (py.path.local, Path)): | ||
| d[name] = str(d[name]) | ||
| elif name == "result": | ||
| d[name] = None # for now | ||
| return d | ||
|
|
||
| @classmethod | ||
| def _from_json(cls, reportdict): | ||
| """ | ||
| This was originally the serialize_report() function from xdist (ca03269). | ||
|
|
||
| Factory method that returns either a TestReport or CollectReport, depending on the calling | ||
| class. It's the callers responsibility to know which class to pass here. | ||
|
|
||
| Experimental method. | ||
| """ | ||
| if reportdict["longrepr"]: | ||
| if ( | ||
| "reprcrash" in reportdict["longrepr"] | ||
| and "reprtraceback" in reportdict["longrepr"] | ||
| ): | ||
|
|
||
| reprtraceback = reportdict["longrepr"]["reprtraceback"] | ||
| reprcrash = reportdict["longrepr"]["reprcrash"] | ||
|
|
||
| unserialized_entries = [] | ||
| reprentry = None | ||
| for entry_data in reprtraceback["reprentries"]: | ||
| data = entry_data["data"] | ||
| entry_type = entry_data["type"] | ||
| if entry_type == "ReprEntry": | ||
| reprfuncargs = None | ||
| reprfileloc = None | ||
| reprlocals = None | ||
| if data["reprfuncargs"]: | ||
| reprfuncargs = ReprFuncArgs(**data["reprfuncargs"]) | ||
| if data["reprfileloc"]: | ||
| reprfileloc = ReprFileLocation(**data["reprfileloc"]) | ||
| if data["reprlocals"]: | ||
| reprlocals = ReprLocals(data["reprlocals"]["lines"]) | ||
|
|
||
| reprentry = ReprEntry( | ||
| lines=data["lines"], | ||
| reprfuncargs=reprfuncargs, | ||
| reprlocals=reprlocals, | ||
| filelocrepr=reprfileloc, | ||
| style=data["style"], | ||
| ) | ||
| elif entry_type == "ReprEntryNative": | ||
| reprentry = ReprEntryNative(data["lines"]) | ||
| else: | ||
| _report_unserialization_failure(entry_type, cls, reportdict) | ||
| unserialized_entries.append(reprentry) | ||
| reprtraceback["reprentries"] = unserialized_entries | ||
|
|
||
| exception_info = ReprExceptionInfo( | ||
| reprtraceback=ReprTraceback(**reprtraceback), | ||
| reprcrash=ReprFileLocation(**reprcrash), | ||
| ) | ||
|
|
||
| for section in reportdict["longrepr"]["sections"]: | ||
| exception_info.addsection(*section) | ||
| reportdict["longrepr"] = exception_info | ||
|
|
||
| return cls(**reportdict) | ||
|
|
||
|
|
||
| def _report_unserialization_failure(type_name, report_class, reportdict): | ||
| url = "https://github.com/pytest-dev/pytest/issues" | ||
| stream = py.io.TextIO() | ||
| pprint("-" * 100, stream=stream) | ||
| pprint("INTERNALERROR: Unknown entry type returned: %s" % type_name, stream=stream) | ||
| pprint("report_name: %s" % report_class, stream=stream) | ||
| pprint(reportdict, stream=stream) | ||
| pprint("Please report this bug at %s" % url, stream=stream) | ||
| pprint("-" * 100, stream=stream) | ||
| raise RuntimeError(stream.getvalue()) | ||
|
|
||
|
|
||
| class TestReport(BaseReport): | ||
| """ Basic test report object (also used for setup and teardown calls if | ||
| they fail). | ||
| """ | ||
|
|
||
| __test__ = False | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. heh neat, I think this oddity could really be purged if tests were required to be defined in the module they appear in, and then importing names wouldn't add them to discovery -- always was a weird thing about |
||
|
|
||
| def __init__( | ||
| self, | ||
| nodeid, | ||
|
|
@@ -272,3 +407,21 @@ def __init__(self, msg): | |
|
|
||
| def toterminal(self, out): | ||
| out.line(self.longrepr, red=True) | ||
|
|
||
|
|
||
| def pytest_report_to_serializable(report): | ||
| if isinstance(report, (TestReport, CollectReport)): | ||
| data = report._to_json() | ||
| data["_report_type"] = report.__class__.__name__ | ||
| return data | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nitpick: these implementations don't actually serialize/unserialize the objects: they convert them to/from native Python types which can be serialised/deserialised as JSON. Serialisation, to my mind, means converting something to bytes (or at least str). That's a fine API if that's what you want, but maybe the names should reflect it. Sorry for bikeshedding. ;-)
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not at all, thanks for chipping in. I agree with you, they are not actually serializing anything. I'm fine with changing those names now, easier to do this before merging. Do you have a suggestion?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe Otherwise:
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would like to avoid restricting this to "json", because we might change the actual representation to something else. I like Thanks a lot for the input! I will change to
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. at $previous_job we called this
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. JSON-compatible is a stricter standard than built-in types (e.g. no sets in JSON), but so long as the requirements are documented, I think the
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like @RonnyPfannschmidt's, we describe the intent rather what the hook returns in detail. 👍
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done, If nobody objects I will merge this later then. |
||
|
|
||
|
|
||
| def pytest_report_from_serializable(data): | ||
| if "_report_type" in data: | ||
| if data["_report_type"] == "TestReport": | ||
| return TestReport._from_json(data) | ||
| elif data["_report_type"] == "CollectReport": | ||
| return CollectReport._from_json(data) | ||
| assert False, "Unknown report_type unserialize data: {}".format( | ||
| data["_report_type"] | ||
| ) | ||
Uh oh!
There was an error while loading. Please reload this page.