You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

faulthandler.py 4.3KB

2 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. import io
  2. import os
  3. import sys
  4. from typing import Generator
  5. from typing import TextIO
  6. import pytest
  7. from _pytest.config import Config
  8. from _pytest.config.argparsing import Parser
  9. from _pytest.nodes import Item
  10. from _pytest.store import StoreKey
  11. fault_handler_stderr_key = StoreKey[TextIO]()
  12. def pytest_addoption(parser: Parser) -> None:
  13. help = (
  14. "Dump the traceback of all threads if a test takes "
  15. "more than TIMEOUT seconds to finish."
  16. )
  17. parser.addini("faulthandler_timeout", help, default=0.0)
  18. def pytest_configure(config: Config) -> None:
  19. import faulthandler
  20. if not faulthandler.is_enabled():
  21. # faulthhandler is not enabled, so install plugin that does the actual work
  22. # of enabling faulthandler before each test executes.
  23. config.pluginmanager.register(FaultHandlerHooks(), "faulthandler-hooks")
  24. else:
  25. # Do not handle dumping to stderr if faulthandler is already enabled, so warn
  26. # users that the option is being ignored.
  27. timeout = FaultHandlerHooks.get_timeout_config_value(config)
  28. if timeout > 0:
  29. config.issue_config_time_warning(
  30. pytest.PytestConfigWarning(
  31. "faulthandler module enabled before pytest configuration step, "
  32. "'faulthandler_timeout' option ignored"
  33. ),
  34. stacklevel=2,
  35. )
  36. class FaultHandlerHooks:
  37. """Implements hooks that will actually install fault handler before tests execute,
  38. as well as correctly handle pdb and internal errors."""
  39. def pytest_configure(self, config: Config) -> None:
  40. import faulthandler
  41. stderr_fd_copy = os.dup(self._get_stderr_fileno())
  42. config._store[fault_handler_stderr_key] = open(stderr_fd_copy, "w")
  43. faulthandler.enable(file=config._store[fault_handler_stderr_key])
  44. def pytest_unconfigure(self, config: Config) -> None:
  45. import faulthandler
  46. faulthandler.disable()
  47. # close our dup file installed during pytest_configure
  48. # re-enable the faulthandler, attaching it to the default sys.stderr
  49. # so we can see crashes after pytest has finished, usually during
  50. # garbage collection during interpreter shutdown
  51. config._store[fault_handler_stderr_key].close()
  52. del config._store[fault_handler_stderr_key]
  53. faulthandler.enable(file=self._get_stderr_fileno())
  54. @staticmethod
  55. def _get_stderr_fileno():
  56. try:
  57. fileno = sys.stderr.fileno()
  58. # The Twisted Logger will return an invalid file descriptor since it is not backed
  59. # by an FD. So, let's also forward this to the same code path as with pytest-xdist.
  60. if fileno == -1:
  61. raise AttributeError()
  62. return fileno
  63. except (AttributeError, io.UnsupportedOperation):
  64. # pytest-xdist monkeypatches sys.stderr with an object that is not an actual file.
  65. # https://docs.python.org/3/library/faulthandler.html#issue-with-file-descriptors
  66. # This is potentially dangerous, but the best we can do.
  67. return sys.__stderr__.fileno()
  68. @staticmethod
  69. def get_timeout_config_value(config):
  70. return float(config.getini("faulthandler_timeout") or 0.0)
  71. @pytest.hookimpl(hookwrapper=True, trylast=True)
  72. def pytest_runtest_protocol(self, item: Item) -> Generator[None, None, None]:
  73. timeout = self.get_timeout_config_value(item.config)
  74. stderr = item.config._store[fault_handler_stderr_key]
  75. if timeout > 0 and stderr is not None:
  76. import faulthandler
  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. @pytest.hookimpl(tryfirst=True)
  85. def pytest_enter_pdb(self) -> None:
  86. """Cancel any traceback dumping due to timeout before entering pdb."""
  87. import faulthandler
  88. faulthandler.cancel_dump_traceback_later()
  89. @pytest.hookimpl(tryfirst=True)
  90. def pytest_exception_interact(self) -> None:
  91. """Cancel any traceback dumping due to an interactive exception being
  92. raised."""
  93. import faulthandler
  94. faulthandler.cancel_dump_traceback_later()