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

Integrate pytest-faulthandler into the core #5441

Merged
merged 2 commits into from Jun 24, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 8 additions & 0 deletions changelog/5440.feature.rst
@@ -0,0 +1,8 @@
The `faulthandler <https://docs.python.org/3/library/faulthandler.html>`__ standard library
module is now enabled by default to help users diagnose crashes in C modules.

This functionality was provided by integrating the external
`pytest-faulthandler <https://github.com/pytest-dev/pytest-faulthandler>`__ plugin into the core,
so users should remove that plugin from their requirements if used.

For more information see the docs: https://docs.pytest.org/en/latest/usage.html#fault-handler
17 changes: 17 additions & 0 deletions doc/en/reference.rst
Expand Up @@ -1076,6 +1076,23 @@ passed multiple times. The expected format is ``name=value``. For example::
for more details.


.. confval:: faulthandler_timeout

Dumps the tracebacks of all threads if a test takes longer than ``X`` seconds to run (including
fixture setup and teardown). Implemented using the `faulthandler.dump_traceback_later`_ function,
so all caveats there apply.

.. code-block:: ini

# content of pytest.ini
[pytest]
faulthandler_timeout=5

For more information please refer to :ref:`faulthandler`.

.. _`faulthandler.dump_traceback_later`: https://docs.python.org/3/library/faulthandler.html#faulthandler.dump_traceback_later


.. confval:: filterwarnings


Expand Down
33 changes: 32 additions & 1 deletion doc/en/usage.rst
Expand Up @@ -408,7 +408,6 @@ Pytest supports the use of ``breakpoint()`` with the following behaviours:
Profiling test execution duration
-------------------------------------

.. versionadded: 2.2

To get a list of the slowest 10 test durations:

Expand All @@ -418,6 +417,38 @@ To get a list of the slowest 10 test durations:

By default, pytest will not show test durations that are too small (<0.01s) unless ``-vv`` is passed on the command-line.


.. _faulthandler:

Fault Handler
-------------

.. versionadded:: 5.0

The `faulthandler <https://docs.python.org/3/library/faulthandler.html>`__ standard module
can be used to dump Python tracebacks on a segfault or after a timeout.

The module is automatically enabled for pytest runs, unless the ``-p no:faulthandler`` is given
on the command-line.

Also the :confval:`faulthandler_timeout=X<faulthandler_timeout>` configuration option can be used
to dump the traceback of all threads if a test takes longer than ``X``
seconds to finish (not available on Windows).

.. note::

This functionality has been integrated from the external
`pytest-faulthandler <https://github.com/pytest-dev/pytest-faulthandler>`__ plugin, with two
small differences:

* To disable it, use ``-p no:faulthandler`` instead of ``--no-faulthandler``: the former
can be used with any plugin, so it saves one option.

* The ``--faulthandler-timeout`` command-line option has become the
:confval:`faulthandler_timeout` configuration option. It can still be configured from
the command-line using ``-o faulthandler_timeout=X``.


Creating JUnitXML format files
----------------------------------------------------

Expand Down
3 changes: 2 additions & 1 deletion src/_pytest/config/__init__.py
Expand Up @@ -141,6 +141,7 @@ def directory_arg(path, optname):
"warnings",
"logging",
"reports",
"faulthandler",
)

builtin_plugins = set(default_plugins)
Expand Down Expand Up @@ -299,7 +300,7 @@ def parse_hookspec_opts(self, module_or_class, name):
return opts

def register(self, plugin, name=None):
if name in ["pytest_catchlog", "pytest_capturelog"]:
if name in _pytest.deprecated.DEPRECATED_EXTERNAL_PLUGINS:
warnings.warn(
PytestConfigWarning(
"{} plugin has been merged into the core, "
Expand Down
8 changes: 8 additions & 0 deletions src/_pytest/deprecated.py
Expand Up @@ -14,6 +14,14 @@

YIELD_TESTS = "yield tests were removed in pytest 4.0 - {name} will be ignored"

# set of plugins which have been integrated into the core; we use this list to ignore
# them during registration to avoid conflicts
DEPRECATED_EXTERNAL_PLUGINS = {
"pytest_catchlog",
"pytest_capturelog",
"pytest_faulthandler",
}


FIXTURE_FUNCTION_CALL = (
'Fixture "{name}" called directly. Fixtures are not meant to be called directly,\n'
Expand Down
86 changes: 86 additions & 0 deletions src/_pytest/faulthandler.py
@@ -0,0 +1,86 @@
import io
import os
import sys

import pytest


def pytest_addoption(parser):
help = (
"Dump the traceback of all threads if a test takes "
"more than TIMEOUT seconds to finish.\n"
"Not available on Windows."
)
parser.addini("faulthandler_timeout", help, default=0.0)


def pytest_configure(config):
import faulthandler

# avoid trying to dup sys.stderr if faulthandler is already enabled
if faulthandler.is_enabled():
return

stderr_fd_copy = os.dup(_get_stderr_fileno())
config.fault_handler_stderr = os.fdopen(stderr_fd_copy, "w")
faulthandler.enable(file=config.fault_handler_stderr)


def _get_stderr_fileno():
try:
return sys.stderr.fileno()
except (AttributeError, io.UnsupportedOperation):
# python-xdist monkeypatches sys.stderr with an object that is not an actual file.
# https://docs.python.org/3/library/faulthandler.html#issue-with-file-descriptors
# This is potentially dangerous, but the best we can do.
return sys.__stderr__.fileno()


def pytest_unconfigure(config):
import faulthandler

faulthandler.disable()
# close our dup file installed during pytest_configure
f = getattr(config, "fault_handler_stderr", None)
if f is not None:
# re-enable the faulthandler, attaching it to the default sys.stderr
# so we can see crashes after pytest has finished, usually during
# garbage collection during interpreter shutdown
config.fault_handler_stderr.close()
del config.fault_handler_stderr
faulthandler.enable(file=_get_stderr_fileno())


@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_protocol(item):
timeout = float(item.config.getini("faulthandler_timeout") or 0.0)
if timeout > 0:
import faulthandler

stderr = item.config.fault_handler_stderr
faulthandler.dump_traceback_later(timeout, file=stderr)
try:
yield
finally:
faulthandler.cancel_dump_traceback_later()
else:
yield


@pytest.hookimpl(tryfirst=True)
def pytest_enter_pdb():
"""Cancel any traceback dumping due to timeout before entering pdb.
"""
import faulthandler

faulthandler.cancel_dump_traceback_later()


@pytest.hookimpl(tryfirst=True)
def pytest_exception_interact():
"""Cancel any traceback dumping due to an interactive exception being
raised.
"""
import faulthandler

faulthandler.cancel_dump_traceback_later()
23 changes: 8 additions & 15 deletions testing/deprecated_test.py
@@ -1,6 +1,7 @@
import os

import pytest
from _pytest import deprecated
from _pytest.warning_types import PytestDeprecationWarning
from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG

Expand Down Expand Up @@ -69,22 +70,14 @@ def test_terminal_reporter_writer_attr(pytestconfig):
assert terminal_reporter.writer is terminal_reporter._tw


@pytest.mark.parametrize("plugin", ["catchlog", "capturelog"])
@pytest.mark.parametrize("plugin", deprecated.DEPRECATED_EXTERNAL_PLUGINS)
@pytest.mark.filterwarnings("default")
def test_pytest_catchlog_deprecated(testdir, plugin):
testdir.makepyfile(
"""
def test_func(pytestconfig):
pytestconfig.pluginmanager.register(None, 'pytest_{}')
""".format(
plugin
)
)
res = testdir.runpytest()
assert res.ret == 0
res.stdout.fnmatch_lines(
["*pytest-*log plugin has been merged into the core*", "*1 passed, 1 warnings*"]
)
def test_external_plugins_integrated(testdir, plugin):
testdir.syspathinsert()
testdir.makepyfile(**{plugin: ""})

with pytest.warns(pytest.PytestConfigWarning):
testdir.parseconfig("-p", plugin)


def test_raises_message_argument_deprecated():
Expand Down
103 changes: 103 additions & 0 deletions testing/test_faulthandler.py
@@ -0,0 +1,103 @@
import sys

import pytest


def test_enabled(testdir):
"""Test single crashing test displays a traceback."""
testdir.makepyfile(
"""
import faulthandler
def test_crash():
faulthandler._sigabrt()
"""
)
result = testdir.runpytest_subprocess()
result.stderr.fnmatch_lines(["*Fatal Python error*"])
assert result.ret != 0


def test_crash_near_exit(testdir):
"""Test that fault handler displays crashes that happen even after
pytest is exiting (for example, when the interpreter is shutting down).
"""
testdir.makepyfile(
"""
import faulthandler
import atexit
def test_ok():
atexit.register(faulthandler._sigabrt)
"""
)
result = testdir.runpytest_subprocess()
result.stderr.fnmatch_lines(["*Fatal Python error*"])
assert result.ret != 0


def test_disabled(testdir):
"""Test option to disable fault handler in the command line.
"""
testdir.makepyfile(
"""
import faulthandler
def test_disabled():
assert not faulthandler.is_enabled()
"""
)
result = testdir.runpytest_subprocess("-p", "no:faulthandler")
result.stdout.fnmatch_lines(["*1 passed*"])
assert result.ret == 0


@pytest.mark.parametrize("enabled", [True, False])
def test_timeout(testdir, enabled):
"""Test option to dump tracebacks after a certain timeout.
If faulthandler is disabled, no traceback will be dumped.
"""
testdir.makepyfile(
"""
import time
def test_timeout():
time.sleep(2.0)
"""
)
testdir.makeini(
"""
[pytest]
faulthandler_timeout = 1
"""
)
args = ["-p", "no:faulthandler"] if not enabled else []

result = testdir.runpytest_subprocess(*args)
tb_output = "most recent call first"
if sys.version_info[:2] == (3, 3):
tb_output = "Thread"
if enabled:
result.stderr.fnmatch_lines(["*%s*" % tb_output])
else:
assert tb_output not in result.stderr.str()
result.stdout.fnmatch_lines(["*1 passed*"])
assert result.ret == 0


@pytest.mark.parametrize("hook_name", ["pytest_enter_pdb", "pytest_exception_interact"])
def test_cancel_timeout_on_hook(monkeypatch, pytestconfig, hook_name):
"""Make sure that we are cancelling any scheduled traceback dumping due
to timeout before entering pdb (pytest-dev/pytest-faulthandler#12) or any other interactive
exception (pytest-dev/pytest-faulthandler#14).
"""
import faulthandler
from _pytest import faulthandler as plugin_module

called = []

monkeypatch.setattr(
faulthandler, "cancel_dump_traceback_later", lambda: called.append(1)
)

# call our hook explicitly, we can trust that pytest will call the hook
# for us at the appropriate moment
hook_func = getattr(plugin_module, hook_name)
hook_func()
assert called == [1]