test_asyncio_handlers.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. """Tests for the Windows asyncio Proactor cleanup-RST filter (#1113)."""
  2. from __future__ import annotations
  3. import asyncio
  4. from unittest.mock import patch
  5. import pytest
  6. from backend.app.core.asyncio_handlers import (
  7. _is_proactor_connection_reset,
  8. _proactor_reset_filter,
  9. install_proactor_reset_filter,
  10. )
  11. # `_is_proactor_connection_reset` short-circuits on non-Windows; pretend we're
  12. # on Windows for the discrimination tests so they exercise the actual logic.
  13. @pytest.fixture
  14. def fake_windows():
  15. with patch("backend.app.core.asyncio_handlers.sys.platform", "win32"):
  16. yield
  17. class TestIsProactorConnectionReset:
  18. """The discriminator that decides whether a context is the noise we silence."""
  19. def test_matches_proactor_cleanup_reset(self, fake_windows):
  20. ctx = {
  21. "exception": ConnectionResetError(10054, "An existing connection was forcibly closed"),
  22. "message": "Exception in callback _ProactorBasePipeTransport._call_connection_lost()",
  23. }
  24. assert _is_proactor_connection_reset(ctx) is True
  25. def test_rejects_when_not_on_windows(self):
  26. # No `fake_windows` fixture — sys.platform reflects the real OS.
  27. ctx = {
  28. "exception": ConnectionResetError(10054, "irrelevant"),
  29. "message": "Exception in callback _ProactorBasePipeTransport._call_connection_lost()",
  30. }
  31. # The whole point of the filter is to be a Windows-only no-op.
  32. with patch("backend.app.core.asyncio_handlers.sys.platform", "linux"):
  33. assert _is_proactor_connection_reset(ctx) is False
  34. def test_rejects_unrelated_connection_reset(self, fake_windows):
  35. """A real `ConnectionResetError` raised inside an app coroutine —
  36. not from the Proactor cleanup path — must NOT be suppressed.
  37. Otherwise we'd hide genuine connectivity bugs."""
  38. ctx = {
  39. "exception": ConnectionResetError(),
  40. "message": "Task exception was never retrieved",
  41. }
  42. assert _is_proactor_connection_reset(ctx) is False
  43. def test_rejects_other_exception_types(self, fake_windows):
  44. """Other OSErrors (BrokenPipeError, ConnectionAbortedError) might
  45. share the cleanup path but they're a different signal worth
  46. keeping visible — we only silence the specific 10054 family."""
  47. ctx = {
  48. "exception": BrokenPipeError(),
  49. "message": "Exception in callback _ProactorBasePipeTransport._call_connection_lost()",
  50. }
  51. assert _is_proactor_connection_reset(ctx) is False
  52. def test_rejects_when_no_exception(self, fake_windows):
  53. """asyncio sometimes invokes the handler with no exception object
  54. (e.g. resource warnings) — those shouldn't blanket-match."""
  55. ctx = {"message": "_call_connection_lost was slow"}
  56. assert _is_proactor_connection_reset(ctx) is False
  57. class TestProactorResetFilter:
  58. """The handler glue itself — does it suppress the right ones and
  59. pass everything else through to the default handler?"""
  60. @pytest.mark.asyncio
  61. async def test_suppresses_proactor_reset(self, fake_windows):
  62. loop = asyncio.get_running_loop()
  63. with patch.object(loop, "default_exception_handler") as default:
  64. _proactor_reset_filter(
  65. loop,
  66. {
  67. "exception": ConnectionResetError(10054, "forcibly closed"),
  68. "message": "Exception in callback _ProactorBasePipeTransport._call_connection_lost()",
  69. },
  70. )
  71. # Suppression = default handler is never reached.
  72. default.assert_not_called()
  73. @pytest.mark.asyncio
  74. async def test_passes_unrelated_through_to_default(self, fake_windows):
  75. """A different uncaught exception must go through asyncio's normal
  76. path so it surfaces in logs and tests as an actual problem."""
  77. loop = asyncio.get_running_loop()
  78. ctx = {
  79. "exception": ValueError("real bug"),
  80. "message": "Task exception was never retrieved",
  81. }
  82. with patch.object(loop, "default_exception_handler") as default:
  83. _proactor_reset_filter(loop, ctx)
  84. default.assert_called_once_with(ctx)
  85. class TestInstallation:
  86. """Wiring: install_proactor_reset_filter only runs on Windows."""
  87. @pytest.mark.asyncio
  88. async def test_install_is_no_op_on_non_windows(self):
  89. """Linux/macOS use the Selector loop, which doesn't hit this code
  90. path — the install must be inert so the Linux production path
  91. keeps the default exception handler untouched."""
  92. loop = asyncio.get_running_loop()
  93. with (
  94. patch("backend.app.core.asyncio_handlers.sys.platform", "linux"),
  95. patch.object(loop, "set_exception_handler") as setter,
  96. ):
  97. installed = install_proactor_reset_filter(loop)
  98. assert installed is False
  99. setter.assert_not_called()
  100. @pytest.mark.asyncio
  101. async def test_install_attaches_handler_on_windows(self, fake_windows):
  102. loop = asyncio.get_running_loop()
  103. with patch.object(loop, "set_exception_handler") as setter:
  104. installed = install_proactor_reset_filter(loop)
  105. assert installed is True
  106. setter.assert_called_once_with(_proactor_reset_filter)