Skip to content

Commit 5c1755e

Browse files
Pin AsyncAdaptedConnection.close OS-level suppression branches
The narrow try/except on close() rollback suppresses transport failures so a leader flip or peer reset does not leak out of the pool. The tuple covered the dbapi-level types (OperationalError, InterfaceError, DqliteConnectionError) plus OSError, TimeoutError, and ConnectionError — but only the dbapi-level types had direct tests, leaving the three OS-level branches unpinned. A refactor narrowing the tuple (e.g. dropping OSError) would silently re-raise real transport failures and leak the underlying AsyncConnection. Add a parametrized test across OSError, BrokenPipeError, ConnectionError, ConnectionResetError, and TimeoutError that asserts the DEBUG log breadcrumb fires and close() still runs. Add an inverse test that ValueError (outside the tuple) propagates. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 901fb11 commit 5c1755e

1 file changed

Lines changed: 76 additions & 0 deletions

File tree

tests/test_aio_close_rollback_log.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,3 +124,79 @@ def _fake_await_only(coro: object) -> object:
124124
]
125125
assert matching
126126
assert fake.close_calls == 1
127+
128+
129+
@pytest.mark.parametrize(
130+
"exc",
131+
[
132+
OSError(32, "broken pipe"),
133+
BrokenPipeError(32, "broken pipe"),
134+
ConnectionError("peer went away"),
135+
ConnectionResetError(104, "connection reset by peer"),
136+
TimeoutError("read timed out"),
137+
],
138+
)
139+
def test_close_suppresses_os_level_rollback_errors(
140+
caplog: pytest.LogCaptureFixture, exc: BaseException
141+
) -> None:
142+
"""The narrow suppression tuple on ``close()`` includes OSError,
143+
TimeoutError, and ConnectionError alongside the dbapi-level
144+
types. Pin each OS-level branch so a refactor dropping any one
145+
of them would fail this test rather than silently re-raising
146+
the exception and leaking the underlying AsyncConnection.
147+
"""
148+
fake = _FakeAsyncConn(exc)
149+
adapter = AsyncAdaptedConnection(fake) # type: ignore[arg-type]
150+
151+
from sqlalchemydqlite import aio as aio_module
152+
153+
def _fake_await_only(coro: object) -> object:
154+
import asyncio
155+
156+
return asyncio.new_event_loop().run_until_complete(coro) # type: ignore[arg-type]
157+
158+
orig = aio_module.await_only
159+
aio_module.await_only = _fake_await_only # type: ignore[assignment]
160+
try:
161+
with caplog.at_level(logging.DEBUG, logger="sqlalchemydqlite.aio"):
162+
adapter.close()
163+
finally:
164+
aio_module.await_only = orig # type: ignore[assignment]
165+
166+
matching = [
167+
r
168+
for r in caplog.records
169+
if r.levelno == logging.DEBUG and "rollback failed" in r.getMessage()
170+
]
171+
assert matching, f"no DEBUG log captured for {type(exc).__name__}: {caplog.records!r}"
172+
# Log line carries the exception type (so operators triaging a
173+
# noisy pool can correlate the cause without enabling exc_info
174+
# rendering). ``%s`` with ``type(exc).__name__`` in the format.
175+
assert type(exc).__name__ in matching[0].getMessage()
176+
# close() ran regardless of which transport error fired the
177+
# suppression branch.
178+
assert fake.close_calls == 1
179+
180+
181+
def test_close_propagates_value_error_out_of_tuple() -> None:
182+
"""Inverse pin: a ``ValueError`` from rollback is not in the
183+
narrow tuple and must propagate. Guards against a refactor
184+
widening the suppression back to ``except Exception``.
185+
"""
186+
fake = _FakeAsyncConn(ValueError("parameter out of range"))
187+
adapter = AsyncAdaptedConnection(fake) # type: ignore[arg-type]
188+
189+
from sqlalchemydqlite import aio as aio_module
190+
191+
def _fake_await_only(coro: object) -> object:
192+
import asyncio
193+
194+
return asyncio.new_event_loop().run_until_complete(coro) # type: ignore[arg-type]
195+
196+
orig = aio_module.await_only
197+
aio_module.await_only = _fake_await_only # type: ignore[assignment]
198+
try:
199+
with pytest.raises(ValueError, match="parameter out of range"):
200+
adapter.close()
201+
finally:
202+
aio_module.await_only = orig # type: ignore[assignment]

0 commit comments

Comments
 (0)