Merge pull request #5441 from nicoddemus/faulthandler-5440

Integrate pytest-faulthandler into the core
This commit is contained in:
Bruno Oliveira 2019-06-24 20:01:15 -03:00 committed by GitHub
commit 3f5b078462
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 264 additions and 17 deletions

View File

@ -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

View File

@ -1084,6 +1084,23 @@ passed multiple times. The expected format is ``name=value``. For example::
for more details. 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 .. confval:: filterwarnings

View File

@ -410,7 +410,6 @@ Pytest supports the use of ``breakpoint()`` with the following behaviours:
Profiling test execution duration Profiling test execution duration
------------------------------------- -------------------------------------
.. versionadded: 2.2
To get a list of the slowest 10 test durations: To get a list of the slowest 10 test durations:
@ -420,6 +419,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. 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 Creating JUnitXML format files
---------------------------------------------------- ----------------------------------------------------

View File

@ -140,6 +140,7 @@ default_plugins = essential_plugins + (
"warnings", "warnings",
"logging", "logging",
"reports", "reports",
"faulthandler",
) )
builtin_plugins = set(default_plugins) builtin_plugins = set(default_plugins)
@ -288,7 +289,7 @@ class PytestPluginManager(PluginManager):
return opts return opts
def register(self, plugin, name=None): def register(self, plugin, name=None):
if name in ["pytest_catchlog", "pytest_capturelog"]: if name in _pytest.deprecated.DEPRECATED_EXTERNAL_PLUGINS:
warnings.warn( warnings.warn(
PytestConfigWarning( PytestConfigWarning(
"{} plugin has been merged into the core, " "{} plugin has been merged into the core, "

View File

@ -14,6 +14,14 @@ from _pytest.warning_types import UnformattedWarning
YIELD_TESTS = "yield tests were removed in pytest 4.0 - {name} will be ignored" 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_FUNCTION_CALL = (
'Fixture "{name}" called directly. Fixtures are not meant to be called directly,\n' 'Fixture "{name}" called directly. Fixtures are not meant to be called directly,\n'

View File

@ -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()

View File

@ -1,6 +1,7 @@
import os import os
import pytest import pytest
from _pytest import deprecated
from _pytest.warning_types import PytestDeprecationWarning from _pytest.warning_types import PytestDeprecationWarning
from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG
@ -69,22 +70,14 @@ def test_terminal_reporter_writer_attr(pytestconfig):
assert terminal_reporter.writer is terminal_reporter._tw 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") @pytest.mark.filterwarnings("default")
def test_pytest_catchlog_deprecated(testdir, plugin): def test_external_plugins_integrated(testdir, plugin):
testdir.makepyfile( testdir.syspathinsert()
""" testdir.makepyfile(**{plugin: ""})
def test_func(pytestconfig):
pytestconfig.pluginmanager.register(None, 'pytest_{}') with pytest.warns(pytest.PytestConfigWarning):
""".format( testdir.parseconfig("-p", plugin)
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_raises_message_argument_deprecated(): def test_raises_message_argument_deprecated():

View File

@ -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]