Skip to content

Commit a37b902

Browse files
committed
Integrate pytest-faulthandler into the core
* Add pytest-faulthandler files unchanged * Adapt imports and tests * Add code to skip registration of the external `pytest_faulthandler` to avoid conflicts Fix #5440
1 parent e3dcf1f commit a37b902

File tree

7 files changed

+245
-17
lines changed

7 files changed

+245
-17
lines changed

changelog/5440.feature.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
The `faulthandler <https://docs.python.org/3/library/faulthandler.html>`__ standard library
2+
module is now enabled by default to help users diagnose crashes in C modules.
3+
4+
This functionality was provided by integrating the external
5+
`pytest-faulthandler <https://github.com/pytest-dev/pytest-faulthandler>`__ plugin into the core,
6+
so users should remove that plugin from their requirements if used.
7+
8+
For more information see the docs: https://docs.pytest.org/en/latest/usage.html#fault-handler

doc/en/usage.rst

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,6 @@ Pytest supports the use of ``breakpoint()`` with the following behaviours:
408408
Profiling test execution duration
409409
-------------------------------------
410410

411-
.. versionadded: 2.2
412411

413412
To get a list of the slowest 10 test durations:
414413

@@ -418,6 +417,24 @@ To get a list of the slowest 10 test durations:
418417
419418
By default, pytest will not show test durations that are too small (<0.01s) unless ``-vv`` is passed on the command-line.
420419

420+
421+
.. _faulthandler:
422+
423+
Fault Handler
424+
-------------
425+
426+
.. versionadded:: 5.0
427+
428+
The `faulthandler <https://docs.python.org/3/library/faulthandler.html>`__ standard module
429+
can be used to dump Python tracebacks on a segfault or after a timeout.
430+
431+
The module is automatically enabled for pytest runs, unless the ``--no-faulthandler`` is given
432+
on the command-line.
433+
434+
Also the ``--faulthandler-timeout=X`` can be used to dump the traceback of all threads if a test
435+
takes longer than ``X`` seconds to finish (not available on Windows).
436+
437+
421438
Creating JUnitXML format files
422439
----------------------------------------------------
423440

src/_pytest/config/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ def directory_arg(path, optname):
141141
"warnings",
142142
"logging",
143143
"reports",
144+
"faulthandler",
144145
)
145146

146147
builtin_plugins = set(default_plugins)
@@ -299,7 +300,7 @@ def parse_hookspec_opts(self, module_or_class, name):
299300
return opts
300301

301302
def register(self, plugin, name=None):
302-
if name in ["pytest_catchlog", "pytest_capturelog"]:
303+
if name in _pytest.deprecated.DEPRECATED_EXTERNAL_PLUGINS:
303304
warnings.warn(
304305
PytestConfigWarning(
305306
"{} plugin has been merged into the core, "

src/_pytest/deprecated.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@
1414

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

17+
# set of plugins which have been integrated into the core; we use this list to ignore
18+
# them during registration to avoid conflicts
19+
DEPRECATED_EXTERNAL_PLUGINS = {
20+
"pytest_catchlog",
21+
"pytest_capturelog",
22+
"pytest_faulthandler",
23+
}
24+
1725

1826
FIXTURE_FUNCTION_CALL = (
1927
'Fixture "{name}" called directly. Fixtures are not meant to be called directly,\n'

src/_pytest/faulthandler.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import io
2+
import os
3+
import sys
4+
5+
import pytest
6+
7+
8+
def pytest_addoption(parser):
9+
group = parser.getgroup("terminal reporting")
10+
group.addoption(
11+
"--no-faulthandler",
12+
action="store_false",
13+
dest="fault_handler",
14+
default=True,
15+
help="Disable faulthandler module.",
16+
)
17+
18+
group.addoption(
19+
"--faulthandler-timeout",
20+
type=float,
21+
dest="fault_handler_timeout",
22+
metavar="TIMEOUT",
23+
default=0.0,
24+
help="Dump the traceback of all threads if a test takes "
25+
"more than TIMEOUT seconds to finish.\n"
26+
"Not available on Windows.",
27+
)
28+
29+
30+
def pytest_configure(config):
31+
if config.getoption("fault_handler"):
32+
import faulthandler
33+
34+
# avoid trying to dup sys.stderr if faulthandler is already enabled
35+
if faulthandler.is_enabled():
36+
return
37+
38+
stderr_fd_copy = os.dup(_get_stderr_fileno())
39+
config.fault_handler_stderr = os.fdopen(stderr_fd_copy, "w")
40+
faulthandler.enable(file=config.fault_handler_stderr)
41+
42+
43+
def _get_stderr_fileno():
44+
try:
45+
return sys.stderr.fileno()
46+
except (AttributeError, io.UnsupportedOperation):
47+
# python-xdist monkeypatches sys.stderr with an object that is not an actual file.
48+
# https://docs.python.org/3/library/faulthandler.html#issue-with-file-descriptors
49+
# This is potentially dangerous, but the best we can do.
50+
return sys.__stderr__.fileno()
51+
52+
53+
def pytest_unconfigure(config):
54+
if config.getoption("fault_handler"):
55+
import faulthandler
56+
57+
faulthandler.disable()
58+
# close our dup file installed during pytest_configure
59+
f = getattr(config, "fault_handler_stderr", None)
60+
if f is not None:
61+
# re-enable the faulthandler, attaching it to the default sys.stderr
62+
# so we can see crashes after pytest has finished, usually during
63+
# garbage collection during interpreter shutdown
64+
config.fault_handler_stderr.close()
65+
del config.fault_handler_stderr
66+
faulthandler.enable(file=_get_stderr_fileno())
67+
68+
69+
@pytest.hookimpl(hookwrapper=True)
70+
def pytest_runtest_protocol(item):
71+
enabled = item.config.getoption("fault_handler")
72+
timeout = item.config.getoption("fault_handler_timeout")
73+
if enabled and timeout > 0:
74+
import faulthandler
75+
76+
stderr = item.config.fault_handler_stderr
77+
faulthandler.dump_traceback_later(timeout, file=stderr)
78+
try:
79+
yield
80+
finally:
81+
faulthandler.cancel_dump_traceback_later()
82+
else:
83+
yield
84+
85+
86+
@pytest.hookimpl(tryfirst=True)
87+
def pytest_enter_pdb():
88+
"""Cancel any traceback dumping due to timeout before entering pdb.
89+
"""
90+
import faulthandler
91+
92+
faulthandler.cancel_dump_traceback_later()
93+
94+
95+
@pytest.hookimpl(tryfirst=True)
96+
def pytest_exception_interact():
97+
"""Cancel any traceback dumping due to an interactive exception being
98+
raised.
99+
"""
100+
import faulthandler
101+
102+
faulthandler.cancel_dump_traceback_later()

testing/deprecated_test.py

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import os
22

33
import pytest
4+
from _pytest import deprecated
45
from _pytest.warning_types import PytestDeprecationWarning
56
from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG
67

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

7172

72-
@pytest.mark.parametrize("plugin", ["catchlog", "capturelog"])
73+
@pytest.mark.parametrize("plugin", deprecated.DEPRECATED_EXTERNAL_PLUGINS)
7374
@pytest.mark.filterwarnings("default")
74-
def test_pytest_catchlog_deprecated(testdir, plugin):
75-
testdir.makepyfile(
76-
"""
77-
def test_func(pytestconfig):
78-
pytestconfig.pluginmanager.register(None, 'pytest_{}')
79-
""".format(
80-
plugin
81-
)
82-
)
83-
res = testdir.runpytest()
84-
assert res.ret == 0
85-
res.stdout.fnmatch_lines(
86-
["*pytest-*log plugin has been merged into the core*", "*1 passed, 1 warnings*"]
87-
)
75+
def test_external_plugins_integrated(testdir, plugin):
76+
testdir.syspathinsert()
77+
testdir.makepyfile(**{plugin: ""})
78+
79+
with pytest.warns(pytest.PytestConfigWarning):
80+
testdir.parseconfig("-p", plugin)
8881

8982

9083
def test_raises_message_argument_deprecated():

testing/test_faulthandler.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import sys
2+
3+
import pytest
4+
5+
6+
def test_enabled(testdir):
7+
"""Test single crashing test displays a traceback."""
8+
testdir.makepyfile(
9+
"""
10+
import faulthandler
11+
def test_crash():
12+
faulthandler._sigabrt()
13+
"""
14+
)
15+
result = testdir.runpytest_subprocess()
16+
result.stderr.fnmatch_lines(["*Fatal Python error*"])
17+
assert result.ret != 0
18+
19+
20+
def test_crash_near_exit(testdir):
21+
"""Test that fault handler displays crashes that happen even after
22+
pytest is exiting (for example, when the interpreter is shutting down).
23+
"""
24+
testdir.makepyfile(
25+
"""
26+
import faulthandler
27+
import atexit
28+
def test_ok():
29+
atexit.register(faulthandler._sigabrt)
30+
"""
31+
)
32+
result = testdir.runpytest_subprocess()
33+
result.stderr.fnmatch_lines(["*Fatal Python error*"])
34+
assert result.ret != 0
35+
36+
37+
def test_disabled(testdir):
38+
"""Test option to disable fault handler in the command line.
39+
"""
40+
testdir.makepyfile(
41+
"""
42+
import faulthandler
43+
def test_disabled():
44+
assert not faulthandler.is_enabled()
45+
"""
46+
)
47+
result = testdir.runpytest_subprocess("--no-faulthandler")
48+
result.stdout.fnmatch_lines(["*1 passed*"])
49+
assert result.ret == 0
50+
51+
52+
@pytest.mark.parametrize("enabled", [True, False])
53+
def test_timeout(testdir, enabled):
54+
"""Test option to dump tracebacks after a certain timeout.
55+
If faulthandler is disabled, no traceback will be dumped.
56+
"""
57+
testdir.makepyfile(
58+
"""
59+
import time
60+
def test_timeout():
61+
time.sleep(2.0)
62+
"""
63+
)
64+
args = ["--faulthandler-timeout=1"]
65+
if not enabled:
66+
args.append("--no-faulthandler")
67+
68+
result = testdir.runpytest_subprocess(*args)
69+
tb_output = "most recent call first"
70+
if sys.version_info[:2] == (3, 3):
71+
tb_output = "Thread"
72+
if enabled:
73+
result.stderr.fnmatch_lines(["*%s*" % tb_output])
74+
else:
75+
assert tb_output not in result.stderr.str()
76+
result.stdout.fnmatch_lines(["*1 passed*"])
77+
assert result.ret == 0
78+
79+
80+
@pytest.mark.parametrize("hook_name", ["pytest_enter_pdb", "pytest_exception_interact"])
81+
def test_cancel_timeout_on_hook(monkeypatch, pytestconfig, hook_name):
82+
"""Make sure that we are cancelling any scheduled traceback dumping due
83+
to timeout before entering pdb (pytest-dev/pytest-faulthandler#12) or any other interactive
84+
exception (pytest-dev/pytest-faulthandler#14).
85+
"""
86+
import faulthandler
87+
from _pytest import faulthandler as plugin_module
88+
89+
called = []
90+
91+
monkeypatch.setattr(
92+
faulthandler, "cancel_dump_traceback_later", lambda: called.append(1)
93+
)
94+
95+
# call our hook explicitly, we can trust that pytest will call the hook
96+
# for us at the appropriate moment
97+
hook_func = getattr(plugin_module, hook_name)
98+
hook_func()
99+
assert called == [1]

0 commit comments

Comments
 (0)