From a9780ff5867509748fd773ada53f17795917c55f Mon Sep 17 00:00:00 2001 From: Audel Rouhi Date: Wed, 7 Oct 2026 22:04:38 -0500 Subject: [PATCH] test: verify sync client teardown by owned resources --- tests/test_sync_client_lifecycle.py | 43 +++++------------------------ 1 file changed, 7 insertions(+), 36 deletions(-) diff --git a/tests/test_sync_client_lifecycle.py b/tests/test_sync_client_lifecycle.py index 7da67c5e3..4d8209286 100644 --- a/tests/test_sync_client_lifecycle.py +++ b/tests/test_sync_client_lifecycle.py @@ -3,8 +3,6 @@ from __future__ import annotations import asyncio -import collections -import os import threading import time @@ -20,24 +18,6 @@ pytestmark = pytest.mark.no_engine -def _resources() -> tuple[int, int, int, collections.Counter[str]]: - sockets = 0 - epolls = 0 - for fd in os.listdir("/proc/self/fd"): - try: - target = os.readlink(f"/proc/self/fd/{fd}") - except OSError: - continue - sockets += target.startswith("socket:") - epolls += "eventpoll" in target - return ( - len(os.listdir("/proc/self/fd")), - sockets, - epolls, - collections.Counter(thread.name for thread in threading.enumerate()), - ) - - class _AsyncClient(EpistemicGraphClient): def __init__(self) -> None: self.close_calls = 0 @@ -124,10 +104,6 @@ async def probe() -> None: } == baseline_workers -@pytest.mark.skipif( - not os.path.isdir("/proc/self/fd"), - reason="requires Linux /proc file-descriptor accounting", -) def test_failed_sync_connect_releases_its_loop_resources(monkeypatch) -> None: """A failed async dial must not strand the loop thread or selector FDs.""" @@ -155,10 +131,6 @@ def track_stop(loop: asyncio.AbstractEventLoop, thread: threading.Thread) -> Non assert not thread.is_alive(), "owned loop thread remains alive" -@pytest.mark.skipif( - not os.path.isdir("/proc/self/fd"), - reason="requires Linux /proc file-descriptor accounting", -) def test_successful_sync_close_releases_loop_resources_once(monkeypatch) -> None: """The successful path closes the selector and remains idempotent.""" clients: list[_AsyncClient] = [] @@ -169,21 +141,20 @@ async def connect(**_kwargs: object) -> _AsyncClient: return client monkeypatch.setattr(EpistemicGraphClient, "connect", connect) - baseline = _resources() + # Unrelated cleanup changes process totals but cannot change client ownership. + unrelated_loop = asyncio.new_event_loop() for _ in range(32): client = SyncEpistemicGraphClient.connect(verified_context={}) + unrelated_loop.close() client.close() client.close() - assert _resources() == baseline + assert client._loop.is_closed(), "owned selector remains open" + assert not client._thread.is_alive(), "owned loop thread remains alive" assert all(client.close_calls == 1 for client in clients) -@pytest.mark.skipif( - not os.path.isdir("/proc/self/fd"), - reason="requires Linux /proc file-descriptor accounting", -) def test_sync_close_retries_loop_teardown_without_reclosing_transport( monkeypatch, ) -> None: @@ -208,15 +179,15 @@ def fail_once(loop, thread) -> None: "_stop_loop", staticmethod(fail_once), ) - baseline = _resources() client = SyncEpistemicGraphClient.connect(verified_context={}) client.close() assert not client._loop.is_closed() + assert client._thread.is_alive() assert async_client.close_calls == 1 client.close() assert client._loop.is_closed() assert async_client.close_calls == 1 assert stop_calls == 2 - assert _resources() == baseline + assert not client._thread.is_alive(), "owned loop thread remains alive"