Skip to content

Set timed_out attrib on reports for logging customization #90

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

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
34 changes: 34 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,40 @@ debugging frameworks modules OR if pytest itself drops you into a pdb
session using ```--pdb``` or similar.


Logging Customization
=====================

This plugin sets a new ``timed_out`` attribute on report objects via
the ``pytest_runtest_makereport`` hook. This way logging can be
customized in your ``conftest.py`` by implementing
``pytest_report_teststatus``::

@pytest.hookimpl(tryfirst=True)
def pytest_report_teststatus(report, **kwargs):
"""Adapted from
https://github.com/pytest-dev/pytest/blob/38d8deb74d95077ebf189440ca047e14f8197da1/src/_pytest/runner.py#L202
"""
d = "%.2f" % getattr(report, "duration", -1.0)
if report.passed:
return "passed", "P", "PASSED (%s)" % d
if getattr(report, "timed_out", False):
return "failed", "T", "TIMEOUT (%s)" % d
if report.failed:
return "failed", "F", "FAILED (%s)" % d
return None

This will print ``T`` (or ``TIMEOUT (5.00)`` if verbose) in case a test
times out. You might want to restrict this to the *call* phase because
the above code would print three symbols (lines) per test; one for each of
the three test phases *setup*, *call* and *teardown* (use ``report.when``).
Remark: If a ``report`` has the ``timed_out`` attribute, it can have three
values: ``True`` or ``False`` if the test ran and a timeout appeared or did
not appear. If the value is ``None``, the item might have been run with
timeout being disabled or something unexpected has happened. This way all
checks are reliable, and ``report.timed_out`` still works, because
``bool(None)`` is ``False``.


Copy link
Member

Choose a reason for hiding this comment

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

Great docs update! ❤️

Changelog
=========

Expand Down
42 changes: 42 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/usr/bin/env python3
"""Define pytest hooks as part of the pytest-timeout test suite."""
import pytest


@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_call(item):
"""Create a conftest.py file.

This should be considered a test, not a fixture!
By creating a conftest.py file before actually running the test,
it can be tested/ensured that the functionality implemented in
#90 works reliably.
With untypical test, all other tests are employed to ensure correct
behaviour in terms of #90 apart from their designated checks.

The implementation is inspired by ``_pytest.python.pytest_pyfunc_call``:
https://github.com/pytest-dev/pytest/blob/
f28421cc7068b13ba63c1f60cc21f898cccea36c/src/_pytest/python.py#L179
"""
testdir = item.funcargs.get("testdir", None)
if hasattr(testdir, "makepyfile"):
testdir.makepyfile(
conftest="""
import pytest

@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
r = yield
if not hasattr(r, "get_result"):
return
report = r.get_result()
timed_out = False
if hasattr(call.excinfo, "value"):
msg = getattr(call.excinfo.value, "msg", None)
if isinstance(msg, str) and msg.startswith("Timeout >"):
timed_out = True
ref_timed_out = bool(getattr(report, "timed_out", None))
assert ref_timed_out == timed_out, "#90 customisation broken!"
"""
)
yield
18 changes: 18 additions & 0 deletions pytest_timeout.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ def pytest_runtest_protocol(item):
teardown, then this hook installs the timeout. Otherwise
pytest_runtest_call is used.
"""
item.pytest_timeout__timed_out = None
func_only = get_func_only_setting(item)
if func_only is False:
timeout_setup(item)
Expand Down Expand Up @@ -136,6 +137,20 @@ def pytest_report_header(config):
]


@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
"""Set report.timed_out after it is generated by pytest_runtest_makereport.

This method is a wrapper that sets timed_out if the call has a matching excinfo
value. Note that in that case report.failed and report.timed_out are true (both!).
"""
r = yield
if not hasattr(r, "get_result"):
return
report = r.get_result()
report.timed_out = getattr(item, "pytest_timeout__timed_out", None)


@pytest.hookimpl(tryfirst=True)
def pytest_exception_interact(node):
"""Stop the timeout when pytest enters pdb in post-mortem mode."""
Expand Down Expand Up @@ -227,6 +242,7 @@ def timeout_teardown(item):
cancel = getattr(item, "cancel_timeout", None)
if cancel:
cancel()
item.pytest_timeout__timed_out = False


def get_env_settings(config):
Expand Down Expand Up @@ -366,6 +382,7 @@ def timeout_sigalrm(item, timeout):
if is_debugging():
return
__tracebackhide__ = True
item.pytest_timeout__timed_out = True
nthreads = len(threading.enumerate())
if nthreads > 1:
write_title("Timeout", sep="+")
Expand All @@ -383,6 +400,7 @@ def timeout_timer(item, timeout):
"""
if is_debugging():
return
item.pytest_timeout__timed_out = True # for consistency
try:
capman = item.config.pluginmanager.getplugin("capturemanager")
if capman:
Expand Down