faulthandler.py 3.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  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.stash import StashKey
  11. fault_handler_stderr_key = StashKey[TextIO]()
  12. fault_handler_originally_enabled_key = StashKey[bool]()
  13. def pytest_addoption(parser: Parser) -> None:
  14. help = (
  15. "Dump the traceback of all threads if a test takes "
  16. "more than TIMEOUT seconds to finish."
  17. )
  18. parser.addini("faulthandler_timeout", help, default=0.0)
  19. def pytest_configure(config: Config) -> None:
  20. import faulthandler
  21. stderr_fd_copy = os.dup(get_stderr_fileno())
  22. config.stash[fault_handler_stderr_key] = open(stderr_fd_copy, "w")
  23. config.stash[fault_handler_originally_enabled_key] = faulthandler.is_enabled()
  24. faulthandler.enable(file=config.stash[fault_handler_stderr_key])
  25. def pytest_unconfigure(config: Config) -> None:
  26. import faulthandler
  27. faulthandler.disable()
  28. # Close the dup file installed during pytest_configure.
  29. if fault_handler_stderr_key in config.stash:
  30. config.stash[fault_handler_stderr_key].close()
  31. del config.stash[fault_handler_stderr_key]
  32. if config.stash.get(fault_handler_originally_enabled_key, False):
  33. # Re-enable the faulthandler if it was originally enabled.
  34. faulthandler.enable(file=get_stderr_fileno())
  35. def get_stderr_fileno() -> int:
  36. try:
  37. fileno = sys.stderr.fileno()
  38. # The Twisted Logger will return an invalid file descriptor since it is not backed
  39. # by an FD. So, let's also forward this to the same code path as with pytest-xdist.
  40. if fileno == -1:
  41. raise AttributeError()
  42. return fileno
  43. except (AttributeError, io.UnsupportedOperation):
  44. # pytest-xdist monkeypatches sys.stderr with an object that is not an actual file.
  45. # https://docs.python.org/3/library/faulthandler.html#issue-with-file-descriptors
  46. # This is potentially dangerous, but the best we can do.
  47. return sys.__stderr__.fileno()
  48. def get_timeout_config_value(config: Config) -> float:
  49. return float(config.getini("faulthandler_timeout") or 0.0)
  50. @pytest.hookimpl(hookwrapper=True, trylast=True)
  51. def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
  52. timeout = get_timeout_config_value(item.config)
  53. stderr = item.config.stash[fault_handler_stderr_key]
  54. if timeout > 0 and stderr is not None:
  55. import faulthandler
  56. faulthandler.dump_traceback_later(timeout, file=stderr)
  57. try:
  58. yield
  59. finally:
  60. faulthandler.cancel_dump_traceback_later()
  61. else:
  62. yield
  63. @pytest.hookimpl(tryfirst=True)
  64. def pytest_enter_pdb() -> None:
  65. """Cancel any traceback dumping due to timeout before entering pdb."""
  66. import faulthandler
  67. faulthandler.cancel_dump_traceback_later()
  68. @pytest.hookimpl(tryfirst=True)
  69. def pytest_exception_interact() -> None:
  70. """Cancel any traceback dumping due to an interactive exception being
  71. raised."""
  72. import faulthandler
  73. faulthandler.cancel_dump_traceback_later()