-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
1 parent
e3dcf1f
commit a37b902
Showing
7 changed files
with
245 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
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=float, | ||
dest="fault_handler_timeout", | ||
metavar="TIMEOUT", | ||
default=0.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 | ||
|
||
# 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): | ||
if config.getoption("fault_handler"): | ||
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): | ||
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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] |