File tree Expand file tree Collapse file tree
Expand file tree Collapse file tree Original file line number Diff line number Diff line change 1212
1313from __future__ import annotations
1414
15+ import asyncio
1516from pathlib import Path
1617import sys
1718from typing import Any
3334)
3435
3536
37+ @pytest .fixture (autouse = True )
38+ def isolated_listener_loop (monkeypatch ):
39+ """Model a fresh HA process's lock for each isolated test event loop.
40+
41+ Production HA uses one loop. Pytest creates a new one for each case while
42+ retaining imported modules. A contended module-level asyncio.Lock otherwise
43+ stays bound to a previous case's loop. Keep the real lock and registry
44+ behavior; do not clear leaked listeners or suppress task errors.
45+ """
46+ from custom_components .eybond_local .collector .transport import listener
47+
48+ assert not listener ._LISTENERS , "previous HA test leaked a shared listener"
49+ lock = asyncio .Lock ()
50+ monkeypatch .setattr (listener , "_LISTENERS_LOCK" , lock )
51+ yield
52+ assert not lock .locked (), "HA test leaked the shared-listener lock"
53+ assert not listener ._LISTENERS , "HA test leaked a shared listener"
54+
55+
3656@pytest .fixture (autouse = True )
3757def auto_enable_custom_integrations (enable_custom_integrations ):
3858 """Let Home Assistant load `custom_components/eybond_local` for real."""
Original file line number Diff line number Diff line change 1+ """Each isolated HA test owns its event-loop-bound listener lock."""
2+ import asyncio
3+
4+ import pytest
5+
6+ from custom_components .eybond_local .collector .transport import listener
7+
8+
9+ @pytest .mark .parametrize ("separate_ha_loop" , [1 , 2 ])
10+ async def test_listener_lock_contention_is_local_to_each_ha_loop (hass , separate_ha_loop ):
11+ # Both parametrized cases must actually contend. An uncontended Lock can
12+ # appear to work on a different loop, hiding cross-test state until CI.
13+ lock = listener ._LISTENERS_LOCK
14+ queued = asyncio .Event ()
15+ acquired = asyncio .Event ()
16+
17+ async def contender ():
18+ queued .set ()
19+ async with lock :
20+ acquired .set ()
21+
22+ async with lock :
23+ task = asyncio .create_task (contender ())
24+ await queued .wait ()
25+ assert not acquired .is_set ()
26+ await task
27+ assert acquired .is_set ()
28+ assert not lock .locked ()
You can’t perform that action at this time.
0 commit comments