Skip to content

Commit 19a54d6

Browse files
geofffranksclaude
andcommitted
test: prime pycares shutdown thread at session scope; revert oauth band-aid
The CI Python 3.12 leg backtracks to homeassistant 2025.1.4 / pytest-HA-CC 0.13.205, whose lingering-thread check has no whitelist for the _run_safe_shutdown_loop daemon that aiohttp's aiodns resolver (pycares) spawns the first time a test builds a clientsession. Whichever session-creating test runs first gets blamed -- so the previous per-test hermetic fix in test_oauth.py just moved the failure to test_oauth_wiring.py. Root fix: a session-scoped autouse fixture in conftest.py starts pycares' shared shutdown-manager thread once, before verify_cleanup snapshots threads_before. The thread is then in every test's baseline and never counted as leaked -- covering all current and future clientsession tests without lowering fidelity. pycares' start() is idempotent and its loop only blocks on a queue (no socket/DNS/event loop), so it is safe under pytest-socket. Validated against the exact CI pycares version (5.0.1). Revert the test_oauth.py boundary-mock band-aid now that the fixture makes the original aioresponses test (which exercises the real envelope parse) safe again. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ff57549 commit 19a54d6

2 files changed

Lines changed: 29 additions & 18 deletions

File tree

tests/conftest.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,31 @@
3535
MockConfigEntry = None # type: ignore
3636

3737

38+
@pytest.fixture(autouse=True, scope="session")
39+
def _prime_pycares_shutdown_thread():
40+
"""Pre-spawn pycares' ``_run_safe_shutdown_loop`` daemon before per-test
41+
thread snapshots, so the lingering-thread check never blames a test for it.
42+
43+
aiohttp's aiodns resolver lazily starts this shared daemon the first time
44+
any test builds a clientsession (e.g. a real OAuth token refresh). Older
45+
pytest-homeassistant-custom-component builds have no whitelist for it in
46+
their cleanup check, so the *first* such test fails teardown — on the
47+
Python 3.12 CI leg, pip backtracks to homeassistant 2025.1.4 /
48+
pytest-HA-CC 0.13.205, which predates the whitelist. Starting the thread
49+
once at session scope (this fixture runs before the function-scoped
50+
``verify_cleanup``) puts it in every test's ``threads_before`` baseline, so
51+
it is never counted as leaked. The loop only blocks on a queue — no socket,
52+
DNS, or event loop — so it is safe under pytest-socket.
53+
"""
54+
try:
55+
import pycares
56+
57+
pycares._shutdown_manager.start()
58+
except (ImportError, AttributeError): # pragma: no cover — pycares internals shifted
59+
pass
60+
yield
61+
62+
3863
@pytest.fixture(autouse=True)
3964
def auto_enable_custom_integrations(enable_custom_integrations):
4065
"""Automatically enable loading custom components in tests."""

tests/test_oauth.py

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,9 @@
77
goes stale. These cover the unwrap and its wiring into the implementations.
88
"""
99

10-
from unittest.mock import AsyncMock, patch
11-
10+
from aioresponses import aioresponses
1211
import pytest
1312

14-
from homeassistant.helpers import config_entry_oauth2_flow
15-
1613
from custom_components.u_tec.const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN
1714
from custom_components.u_tec.oauth import (
1815
UtecLocalOAuth2Implementation,
@@ -46,23 +43,12 @@ def test_unwrap_raises_when_no_access_token_present():
4643

4744

4845
async def test_local_impl_token_request_unwraps(hass):
49-
"""The mixin unwraps whatever the underlying HA token request returns.
50-
51-
We patch the superclass ``_token_request`` (HA's HTTP boundary) rather than
52-
mock the network: hitting it for real spins up an aiohttp clientsession whose
53-
pycares DNS resolver leaves a daemon ``_run_safe_shutdown_loop`` thread that
54-
older pytest-homeassistant-custom-component builds flag as a lingering-thread
55-
failure. Patching the boundary keeps the test hermetic and version-agnostic
56-
while still proving the mixin is wired in and unwraps the {code,data} envelope.
57-
"""
46+
"""_token_request against the real {code,data} envelope returns standard fields."""
5847
impl = UtecLocalOAuth2Implementation(
5948
hass, DOMAIN, "client-id", "client-secret", OAUTH2_AUTHORIZE, OAUTH2_TOKEN,
6049
)
61-
with patch.object(
62-
config_entry_oauth2_flow.LocalOAuth2Implementation,
63-
"_token_request",
64-
new=AsyncMock(return_value=_WRAPPED),
65-
):
50+
with aioresponses() as mock:
51+
mock.post(OAUTH2_TOKEN, payload=_WRAPPED)
6652
result = await impl._token_request(
6753
{"grant_type": "refresh_token", "refresh_token": "r"}
6854
)

0 commit comments

Comments
 (0)