aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/python/pytest/py3/_pytest/faulthandler.py
blob: 43eb5f3ff2c7e970838f771b90d17b5bf741a7e9 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
import io 
import os 
import sys 
from typing import Generator 
from typing import TextIO 
 
import pytest 
from _pytest.config import Config 
from _pytest.config.argparsing import Parser 
from _pytest.nodes import Item 
from _pytest.store import StoreKey 
 
 
fault_handler_stderr_key = StoreKey[TextIO]() 
 
 
def pytest_addoption(parser: Parser) -> None: 
    help = ( 
        "Dump the traceback of all threads if a test takes " 
        "more than TIMEOUT seconds to finish." 
    ) 
    parser.addini("faulthandler_timeout", help, default=0.0) 
 
 
def pytest_configure(config: Config) -> None: 
    import faulthandler 
 
    if not faulthandler.is_enabled(): 
        # faulthhandler is not enabled, so install plugin that does the actual work 
        # of enabling faulthandler before each test executes. 
        config.pluginmanager.register(FaultHandlerHooks(), "faulthandler-hooks") 
    else: 
        # Do not handle dumping to stderr if faulthandler is already enabled, so warn 
        # users that the option is being ignored. 
        timeout = FaultHandlerHooks.get_timeout_config_value(config) 
        if timeout > 0: 
            config.issue_config_time_warning( 
                pytest.PytestConfigWarning( 
                    "faulthandler module enabled before pytest configuration step, " 
                    "'faulthandler_timeout' option ignored" 
                ), 
                stacklevel=2, 
            ) 
 
 
class FaultHandlerHooks: 
    """Implements hooks that will actually install fault handler before tests execute, 
    as well as correctly handle pdb and internal errors.""" 
 
    def pytest_configure(self, config: Config) -> None: 
        import faulthandler 
 
        stderr_fd_copy = os.dup(self._get_stderr_fileno()) 
        config._store[fault_handler_stderr_key] = open(stderr_fd_copy, "w") 
        faulthandler.enable(file=config._store[fault_handler_stderr_key]) 
 
    def pytest_unconfigure(self, config: Config) -> None: 
        import faulthandler 
 
        faulthandler.disable() 
        # close our dup file installed during pytest_configure 
        # 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._store[fault_handler_stderr_key].close() 
        del config._store[fault_handler_stderr_key] 
        faulthandler.enable(file=self._get_stderr_fileno()) 
 
    @staticmethod 
    def _get_stderr_fileno(): 
        try: 
            fileno = sys.stderr.fileno() 
            # The Twisted Logger will return an invalid file descriptor since it is not backed 
            # by an FD. So, let's also forward this to the same code path as with pytest-xdist. 
            if fileno == -1: 
                raise AttributeError() 
            return fileno 
        except (AttributeError, io.UnsupportedOperation): 
            # pytest-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() 
 
    @staticmethod 
    def get_timeout_config_value(config): 
        return float(config.getini("faulthandler_timeout") or 0.0) 
 
    @pytest.hookimpl(hookwrapper=True, trylast=True) 
    def pytest_runtest_protocol(self, item: Item) -> Generator[None, None, None]: 
        timeout = self.get_timeout_config_value(item.config) 
        stderr = item.config._store[fault_handler_stderr_key] 
        if timeout > 0 and stderr is not None: 
            import faulthandler 
 
            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(self) -> None: 
        """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(self) -> None: 
        """Cancel any traceback dumping due to an interactive exception being 
        raised.""" 
        import faulthandler 
 
        faulthandler.cancel_dump_traceback_later()