Skip to content

Commit

Permalink
Integrate pytest-faulthandler into the core
Browse files Browse the repository at this point in the history
* Add pytest-faulthandler files unchanged
* Adapt imports and tests
* Add code to skip registration of the external `pytest_faulthandler`
  to avoid conflicts

Fix pytest-dev#5440
  • Loading branch information
nicoddemus committed Jun 12, 2019
1 parent ede8b87 commit b0c51a3
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 5 deletions.
4 changes: 4 additions & 0 deletions changelog/5440.feature.rst
@@ -0,0 +1,4 @@
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.

For more information see the docs: https://docs.pytest.org/en/latest/usage.html#fault-handler
19 changes: 18 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,24 @@ 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 ``--no-faulthandler`` is given
on the command-line.

Also the ``--faulthandler-timeout=X`` can be used to dump the traceback of all threads if a test
takes longer than ``X`` seconds to finish (not available on Windows).


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
81 changes: 81 additions & 0 deletions src/_pytest/faulthandler.py
@@ -0,0 +1,81 @@
import io
import os
import sys

import pytest


def pytest_addoption(parser):
group = parser.getgroup("terminal reporting")
group.addoption(
"--no-faulthandler",
action="store_false",
dest="fault_handler",
default=True,
help="Disable faulthandler module.",
)

group.addoption(
"--faulthandler-timeout",
type=int,
dest="fault_handler_timeout",
metavar="TIMEOUT",
default=0,
help="Dump the traceback of all threads if a test takes "
"more than TIMEOUT seconds to finish.\n"
"Not available on Windows.",
)


def pytest_configure(config):
if config.getoption("fault_handler"):
import faulthandler

try:
stderr_fileno = 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.
stderr_fileno = sys.__stderr__.fileno()
stderr_fd_copy = os.dup(stderr_fileno)
config.fault_handler_stderr = os.fdopen(stderr_fd_copy, "w")
faulthandler.enable(config.fault_handler_stderr)
# we never disable faulthandler after it was enabled
# see pytest-dev/pytest-faulthandler#3


@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_protocol(item):
enabled = item.config.getoption("fault_handler")
timeout = item.config.getoption("fault_handler_timeout")
if enabled and 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()
9 changes: 6 additions & 3 deletions testing/deprecated_test.py
Expand Up @@ -69,9 +69,9 @@ 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", ["catchlog", "capturelog", "faulthandler"])
@pytest.mark.filterwarnings("default")
def test_pytest_catchlog_deprecated(testdir, plugin):
def test_external_plugins_integrated(testdir, plugin):
testdir.makepyfile(
"""
def test_func(pytestconfig):
Expand All @@ -83,7 +83,10 @@ def test_func(pytestconfig):
res = testdir.runpytest()
assert res.ret == 0
res.stdout.fnmatch_lines(
["*pytest-*log plugin has been merged into the core*", "*1 passed, 1 warnings*"]
[
"*pytest-{} plugin has been merged into the core*".format(plugin),
"*1 passed, 1 warnings*",
]
)


Expand Down
99 changes: 99 additions & 0 deletions testing/test_faulthandler.py
@@ -0,0 +1,99 @@
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("--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)
"""
)
args = ["--faulthandler-timeout=1"]
if not enabled:
args.append("--no-faulthandler")

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]

0 comments on commit b0c51a3

Please sign in to comment.