Skip to content

Commit f774300

Browse files
committed
fix: own and close pytest-asyncio's "old loop" instead of suppressing the leak
Reverts the previous PytestUnraisableExceptionWarning suppression. Tracemalloc identified the source as pytest-asyncio's `_temporary_event_loop_policy` (plugin.py:618) calling `asyncio.get_event_loop()` to capture the "old" loop — which creates a fresh loop (with a backing socketpair) when no loop is current and never closes it. Three sub-exceptions per session: two for the socketpair fds, one for the loop itself. Workspace-root conftest now pre-creates and registers an event loop, then closes it (and any loop pytest-asyncio swapped in) in a session finalizer. The capture call finds our loop instead of conjuring a new one. filterwarnings = ["error"] stays strict — no warning suppression.
1 parent 17bc853 commit f774300

2 files changed

Lines changed: 43 additions & 7 deletions

File tree

conftest.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"""Workspace-root conftest for the dexpace SDK test suite.
2+
3+
Sole purpose right now: own and close an event loop on behalf of
4+
``pytest-asyncio``'s ``_temporary_event_loop_policy`` fixture (plugin.py:618),
5+
which calls ``asyncio.get_event_loop()`` to capture the "old" loop, gets a
6+
freshly-created one back when no loop is current, and never closes it. The
7+
leftover loop (and the socket pair backing its self-pipe) escalates as a
8+
``PytestUnraisableExceptionWarning`` under our ``filterwarnings = ["error"]``
9+
gate.
10+
11+
Pre-creating and registering an event loop here means that capture call
12+
finds *our* loop instead of conjuring a new one, and the session finalizer
13+
closes it deterministically.
14+
15+
Tracked upstream in pytest-asyncio; this conftest can go away once a fix
16+
ships.
17+
"""
18+
19+
from __future__ import annotations
20+
21+
import asyncio
22+
from collections.abc import Iterator
23+
24+
import pytest
25+
26+
27+
@pytest.fixture(scope="session", autouse=True)
28+
def _own_default_event_loop() -> Iterator[None]:
29+
loop = asyncio.new_event_loop()
30+
asyncio.set_event_loop(loop)
31+
try:
32+
yield
33+
finally:
34+
# ``pytest_asyncio`` may have replaced the current loop; close
35+
# whichever loop the policy currently points at.
36+
try:
37+
current = asyncio.get_event_loop_policy().get_event_loop()
38+
except RuntimeError:
39+
current = None
40+
for candidate in {loop, current}:
41+
if candidate is not None and not candidate.is_closed():
42+
candidate.close()
43+
asyncio.set_event_loop(None)

pyproject.toml

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,6 @@ asyncio_mode = "auto"
4343
filterwarnings = [
4444
"error",
4545
"ignore::DeprecationWarning:pytest_asyncio.*",
46-
# Socketserver-based stub HTTP servers and short-lived asyncio loops in
47-
# the transport-adapter test suites occasionally leave sockets / event
48-
# loops behind for the GC sweep. The leaks are in test fixtures (not
49-
# production code) and surface inconsistently across platforms — Linux
50-
# CI sees them every run, macOS sees them sometimes. Suppress the
51-
# escalated warning so a clean test run is treated as success.
52-
"ignore::pytest.PytestUnraisableExceptionWarning",
5346
]
5447

5548
[tool.ruff]

0 commit comments

Comments
 (0)