Merge pull request #5441 from nicoddemus/faulthandler-5440
Integrate pytest-faulthandler into the core
This commit is contained in:
commit
3f5b078462
|
@ -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
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
----------------------------------------------------
|
----------------------------------------------------
|
||||||
|
|
||||||
|
|
|
@ -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, "
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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()
|
|
@ -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():
|
||||||
|
|
|
@ -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]
|
Loading…
Reference in New Issue