diff --git a/changelog/11572.bugfix.rst b/changelog/11572.bugfix.rst new file mode 100644 index 00000000000..7a235a071fe --- /dev/null +++ b/changelog/11572.bugfix.rst @@ -0,0 +1 @@ +Handle an edge case where :data:`sys.stderr` and :data:`sys.__stderr__` might already be closed when :ref:`faulthandler` is tearing down. diff --git a/src/_pytest/faulthandler.py b/src/_pytest/faulthandler.py index 36040bffffc..d8c7e9fd3b6 100644 --- a/src/_pytest/faulthandler.py +++ b/src/_pytest/faulthandler.py @@ -9,8 +9,8 @@ from _pytest.stash import StashKey +fault_handler_original_stderr_fd_key = StashKey[int]() fault_handler_stderr_fd_key = StashKey[int]() -fault_handler_originally_enabled_key = StashKey[bool]() def pytest_addoption(parser: Parser) -> None: @@ -24,8 +24,15 @@ def pytest_addoption(parser: Parser) -> None: def pytest_configure(config: Config) -> None: import faulthandler - config.stash[fault_handler_stderr_fd_key] = os.dup(get_stderr_fileno()) - config.stash[fault_handler_originally_enabled_key] = faulthandler.is_enabled() + # at teardown we want to restore the original faulthandler fileno + # but faulthandler has no api to return the original fileno + # so here we stash the stderr fileno to be used at teardown + # sys.stderr and sys.__stderr__ may be closed or patched during the session + # so we can't rely on their values being good at that point (#11572). + stderr_fileno = get_stderr_fileno() + if faulthandler.is_enabled(): + config.stash[fault_handler_original_stderr_fd_key] = stderr_fileno + config.stash[fault_handler_stderr_fd_key] = os.dup(stderr_fileno) faulthandler.enable(file=config.stash[fault_handler_stderr_fd_key]) @@ -37,9 +44,10 @@ def pytest_unconfigure(config: Config) -> None: if fault_handler_stderr_fd_key in config.stash: os.close(config.stash[fault_handler_stderr_fd_key]) del config.stash[fault_handler_stderr_fd_key] - if config.stash.get(fault_handler_originally_enabled_key, False): - # Re-enable the faulthandler if it was originally enabled. - faulthandler.enable(file=get_stderr_fileno()) + # Re-enable the faulthandler if it was originally enabled. + if fault_handler_original_stderr_fd_key in config.stash: + faulthandler.enable(config.stash[fault_handler_original_stderr_fd_key]) + del config.stash[fault_handler_original_stderr_fd_key] def get_stderr_fileno() -> int: