Skip to content

Commit 7b9a414

Browse files
committed
Add pytest_report_serialize and pytest_report_unserialize hooks
These hooks will be used by pytest-xdist and pytest-subtests to serialize and customize reports.
1 parent 0c63f99 commit 7b9a414

File tree

4 files changed

+102
-0
lines changed

4 files changed

+102
-0
lines changed

src/_pytest/config/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ def directory_arg(path, optname):
140140
"stepwise",
141141
"warnings",
142142
"logging",
143+
"reports",
143144
)
144145

145146

src/_pytest/hookspec.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,41 @@ def pytest_runtest_logreport(report):
375375
the respective phase of executing a test. """
376376

377377

378+
@hookspec(firstresult=True)
379+
def pytest_report_serialize(config, report):
380+
"""
381+
.. warning::
382+
This hook is experimental and subject to change between pytest releases, even
383+
bug fixes.
384+
385+
The intent is for this to be used by plugins maintained by the core-devs, such
386+
as ``pytest-xdist``, ``pytest-subtests``, and as a replacement for the internal
387+
'resultlog' plugin.
388+
389+
In the future it might become part of the public hook API.
390+
391+
Serializes the given report object into a data structure suitable for sending
392+
over the wire, or converted to JSON.
393+
"""
394+
395+
396+
@hookspec(firstresult=True)
397+
def pytest_report_unserialize(config, data):
398+
"""
399+
.. warning::
400+
This hook is experimental and subject to change between pytest releases, even
401+
bug fixes.
402+
403+
The intent is for this to be used by plugins maintained by the core-devs, such
404+
as ``pytest-xdist``, ``pytest-subtests``, and as a replacement for the internal
405+
'resultlog' plugin.
406+
407+
In the future it might become part of the public hook API.
408+
409+
Restores a report object previously serialized with pytest_report_serialize().;
410+
"""
411+
412+
378413
# -------------------------------------------------------------------------
379414
# Fixture related hooks
380415
# -------------------------------------------------------------------------

src/_pytest/reports.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,3 +404,19 @@ def __init__(self, msg):
404404

405405
def toterminal(self, out):
406406
out.line(self.longrepr, red=True)
407+
408+
409+
def pytest_report_serialize(report):
410+
if isinstance(report, (TestReport, CollectReport)):
411+
data = report._to_json()
412+
data["_report_type"] = report.__class__.__name__
413+
return data
414+
415+
416+
def pytest_report_unserialize(data):
417+
if "_report_type" in data:
418+
if data["_report_type"] == "TestReport":
419+
return TestReport._from_json(data)
420+
elif data["_report_type"] == "CollectReport":
421+
return CollectReport._from_json(data)
422+
assert "Unknown report_type unserialize data: {}".format(data["_report_type"])

testing/test_reports.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,53 @@ def test_extended_report_deserialization(self, testdir):
181181
assert newrep.skipped == rep.skipped
182182
if rep.failed:
183183
assert newrep.longrepr == str(rep.longrepr)
184+
185+
186+
class TestHooks:
187+
"""Test that the hooks are working correctly for plugins"""
188+
189+
def test_test_report(self, testdir, pytestconfig):
190+
testdir.makepyfile(
191+
"""
192+
import os
193+
def test_a(): assert False
194+
def test_b(): pass
195+
"""
196+
)
197+
reprec = testdir.inline_run()
198+
reports = reprec.getreports("pytest_runtest_logreport")
199+
assert len(reports) == 6
200+
for rep in reports:
201+
data = pytestconfig.hook.pytest_report_serialize(
202+
config=pytestconfig, report=rep
203+
)
204+
assert data["_report_type"] == "TestReport"
205+
new_rep = pytestconfig.hook.pytest_report_unserialize(
206+
config=pytestconfig, data=data
207+
)
208+
assert new_rep.nodeid == rep.nodeid
209+
assert new_rep.when == rep.when
210+
assert new_rep.outcome == rep.outcome
211+
212+
def test_collect_report(self, testdir, pytestconfig):
213+
testdir.makepyfile(
214+
"""
215+
import os
216+
def test_a(): assert False
217+
def test_b(): pass
218+
"""
219+
)
220+
reprec = testdir.inline_run()
221+
reports = reprec.getreports("pytest_collectreport")
222+
assert len(reports) == 2
223+
for rep in reports:
224+
data = pytestconfig.hook.pytest_report_serialize(
225+
config=pytestconfig, report=rep
226+
)
227+
assert data["_report_type"] == "CollectReport"
228+
new_rep = pytestconfig.hook.pytest_report_unserialize(
229+
config=pytestconfig, data=data
230+
)
231+
assert new_rep.nodeid == rep.nodeid
232+
assert new_rep.when == "collect"
233+
assert new_rep.outcome == rep.outcome

0 commit comments

Comments
 (0)