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.

117 lines
4.3KB

  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()