test_suppress_eio_on_interrupt.py
1 """Tests for OSError EIO suppression during interrupt shutdown (#13710). 2 3 When the user interrupts a running task, prompt_toolkit tries to flush 4 stdout during emergency shutdown. If stdout is already in a broken state 5 (redirected to /dev/null, pipe closed, etc.), the flush raises 6 ``OSError: [Errno 5] Input/output error``. 7 8 The ``_suppress_closed_loop_errors`` asyncio exception handler and the 9 outer ``except (KeyError, OSError)`` block must both suppress this error 10 to prevent a hard crash. 11 """ 12 13 from __future__ import annotations 14 15 import errno 16 import os 17 from unittest.mock import MagicMock 18 19 import pytest 20 21 22 # --------------------------------------------------------------------------- 23 # _suppress_closed_loop_errors – asyncio exception handler 24 # --------------------------------------------------------------------------- 25 26 def _make_suppress_fn(): 27 """Build a standalone copy of ``_suppress_closed_loop_errors``. 28 29 The real function is defined as a closure inside 30 ``CLI._run_interactive``; we reconstruct an equivalent here so the 31 unit tests don't need a full CLI instance. 32 """ 33 def _suppress_closed_loop_errors(loop, context): 34 exc = context.get("exception") 35 if isinstance(exc, RuntimeError) and "Event loop is closed" in str(exc): 36 return 37 if isinstance(exc, KeyError) and "is not registered" in str(exc): 38 return 39 if isinstance(exc, OSError) and getattr(exc, "errno", None) == errno.EIO: 40 return 41 loop.default_exception_handler(context) 42 return _suppress_closed_loop_errors 43 44 45 class TestSuppressClosedLoopErrors: 46 """Verify the asyncio exception handler suppresses expected errors.""" 47 48 def test_suppresses_event_loop_closed(self): 49 handler = _make_suppress_fn() 50 loop = MagicMock() 51 handler(loop, {"exception": RuntimeError("Event loop is closed")}) 52 loop.default_exception_handler.assert_not_called() 53 54 def test_suppresses_key_not_registered(self): 55 handler = _make_suppress_fn() 56 loop = MagicMock() 57 handler(loop, {"exception": KeyError("0 is not registered")}) 58 loop.default_exception_handler.assert_not_called() 59 60 def test_suppresses_oserror_eio(self): 61 """OSError with errno.EIO must be suppressed (#13710).""" 62 handler = _make_suppress_fn() 63 loop = MagicMock() 64 exc = OSError(errno.EIO, "Input/output error") 65 handler(loop, {"exception": exc}) 66 loop.default_exception_handler.assert_not_called() 67 68 def test_does_not_suppress_oserror_other_errno(self): 69 """OSError with a different errno must still propagate.""" 70 handler = _make_suppress_fn() 71 loop = MagicMock() 72 exc = OSError(errno.EACCES, "Permission denied") 73 handler(loop, {"exception": exc}) 74 loop.default_exception_handler.assert_called_once() 75 76 def test_does_not_suppress_unrelated_exception(self): 77 """Unrelated exceptions must still propagate.""" 78 handler = _make_suppress_fn() 79 loop = MagicMock() 80 handler(loop, {"exception": ValueError("something else")}) 81 loop.default_exception_handler.assert_called_once() 82 83 def test_no_exception_key(self): 84 """Context without 'exception' must propagate to default handler.""" 85 handler = _make_suppress_fn() 86 loop = MagicMock() 87 handler(loop, {"message": "some log"}) 88 loop.default_exception_handler.assert_called_once() 89 90 91 # --------------------------------------------------------------------------- 92 # Outer except block – EIO handling 93 # --------------------------------------------------------------------------- 94 95 class TestOuterExceptEIO: 96 """Verify the outer ``except (KeyError, OSError)`` block logic.""" 97 98 def test_eio_does_not_reraise(self): 99 """OSError with errno.EIO should be silently suppressed.""" 100 exc = OSError(errno.EIO, "Input/output error") 101 # Simulate the condition check from the outer except block: 102 assert isinstance(exc, OSError) 103 assert getattr(exc, "errno", None) == errno.EIO 104 105 def test_bad_file_descriptor_matches(self): 106 """'Bad file descriptor' string should be caught.""" 107 exc = OSError(errno.EBADF, "Bad file descriptor") 108 assert "Bad file descriptor" in str(exc) 109 110 def test_other_oserror_reraises(self): 111 """Other OSError variants must not match the EIO guard.""" 112 exc = OSError(errno.EACCES, "Permission denied") 113 assert not (getattr(exc, "errno", None) == errno.EIO) 114 assert "is not registered" not in str(exc) 115 assert "Bad file descriptor" not in str(exc)