faulthandler.py 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. import os
  2. import sys
  3. from typing import Generator
  4. import pytest
  5. from _pytest.config import Config
  6. from _pytest.config.argparsing import Parser
  7. from _pytest.nodes import Item
  8. from _pytest.stash import StashKey
  9. fault_handler_original_stderr_fd_key = StashKey[int]()
  10. fault_handler_stderr_fd_key = StashKey[int]()
  11. def pytest_addoption(parser: Parser) -> None:
  12. help = (
  13. "Dump the traceback of all threads if a test takes "
  14. "more than TIMEOUT seconds to finish"
  15. )
  16. parser.addini("faulthandler_timeout", help, default=0.0)
  17. def pytest_configure(config: Config) -> None:
  18. import faulthandler
  19. # at teardown we want to restore the original faulthandler fileno
  20. # but faulthandler has no api to return the original fileno
  21. # so here we stash the stderr fileno to be used at teardown
  22. # sys.stderr and sys.__stderr__ may be closed or patched during the session
  23. # so we can't rely on their values being good at that point (#11572).
  24. stderr_fileno = get_stderr_fileno()
  25. if faulthandler.is_enabled():
  26. config.stash[fault_handler_original_stderr_fd_key] = stderr_fileno
  27. config.stash[fault_handler_stderr_fd_key] = os.dup(stderr_fileno)
  28. faulthandler.enable(file=config.stash[fault_handler_stderr_fd_key])
  29. def pytest_unconfigure(config: Config) -> None:
  30. import faulthandler
  31. faulthandler.disable()
  32. # Close the dup file installed during pytest_configure.
  33. if fault_handler_stderr_fd_key in config.stash:
  34. os.close(config.stash[fault_handler_stderr_fd_key])
  35. del config.stash[fault_handler_stderr_fd_key]
  36. # Re-enable the faulthandler if it was originally enabled.
  37. if fault_handler_original_stderr_fd_key in config.stash:
  38. faulthandler.enable(config.stash[fault_handler_original_stderr_fd_key])
  39. del config.stash[fault_handler_original_stderr_fd_key]
  40. def get_stderr_fileno() -> int:
  41. try:
  42. fileno = sys.stderr.fileno()
  43. # The Twisted Logger will return an invalid file descriptor since it is not backed
  44. # by an FD. So, let's also forward this to the same code path as with pytest-xdist.
  45. if fileno == -1:
  46. raise AttributeError()
  47. return fileno
  48. except (AttributeError, ValueError):
  49. # pytest-xdist monkeypatches sys.stderr with an object that is not an actual file.
  50. # https://docs.python.org/3/library/faulthandler.html#issue-with-file-descriptors
  51. # This is potentially dangerous, but the best we can do.
  52. return sys.__stderr__.fileno()
  53. def get_timeout_config_value(config: Config) -> float:
  54. return float(config.getini("faulthandler_timeout") or 0.0)
  55. @pytest.hookimpl(hookwrapper=True, trylast=True)
  56. def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
  57. timeout = get_timeout_config_value(item.config)
  58. if timeout > 0:
  59. import faulthandler
  60. stderr = item.config.stash[fault_handler_stderr_fd_key]
  61. faulthandler.dump_traceback_later(timeout, file=stderr)
  62. try:
  63. yield
  64. finally:
  65. faulthandler.cancel_dump_traceback_later()
  66. else:
  67. yield
  68. @pytest.hookimpl(tryfirst=True)
  69. def pytest_enter_pdb() -> None:
  70. """Cancel any traceback dumping due to timeout before entering pdb."""
  71. import faulthandler
  72. faulthandler.cancel_dump_traceback_later()
  73. @pytest.hookimpl(tryfirst=True)
  74. def pytest_exception_interact() -> None:
  75. """Cancel any traceback dumping due to an interactive exception being
  76. raised."""
  77. import faulthandler
  78. faulthandler.cancel_dump_traceback_later()