Skip to content

Commit 7976006

Browse files
authored
Clean up repair listeners on deactivate (#1314)
1 parent 6bb2fd6 commit 7976006

2 files changed

Lines changed: 108 additions & 10 deletions

File tree

custom_components/spook/repairs.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ async def async_inspect(self) -> None:
123123

124124
async def async_deactivate(self) -> None:
125125
"""Unregister the repair."""
126-
for issue_id in self.issue_ids:
126+
for issue_id in self.issue_ids.copy():
127127
self.async_delete_issue(issue_id)
128128

129129

@@ -211,10 +211,12 @@ def _filter_event(data: Mapping[str, Any] | Event) -> bool:
211211
return True
212212
return self.inspect_on_reload == event_data.get("domain")
213213

214-
self.hass.bus.async_listen(
215-
"call_service",
216-
_async_call_inspect_debouncer,
217-
event_filter=_filter_event,
214+
self._event_subs.add(
215+
self.hass.bus.async_listen(
216+
"call_service",
217+
_async_call_inspect_debouncer,
218+
event_filter=_filter_event,
219+
),
218220
)
219221

220222
if self.inspect_config_entry_changed:
@@ -231,16 +233,20 @@ async def _async_config_entry_changed( # pylint: disable=unused-argument
231233
return
232234
await self.inspect_debouncer.async_call()
233235

234-
async_dispatcher_connect(
235-
self.hass,
236-
SIGNAL_CONFIG_ENTRY_CHANGED,
237-
_async_config_entry_changed,
236+
self._event_subs.add(
237+
async_dispatcher_connect(
238+
self.hass,
239+
SIGNAL_CONFIG_ENTRY_CHANGED,
240+
_async_config_entry_changed,
241+
),
238242
)
239243

240244
async def async_deactivate(self) -> None:
241245
"""Unregister the repair."""
242-
for sub in self._event_subs:
246+
for sub in self._event_subs.copy():
243247
sub()
248+
self._event_subs.discard(sub)
249+
self.inspect_debouncer.async_shutdown()
244250
await super().async_deactivate()
245251

246252

tests/test_repairs_cleanup.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
"""Tests for Spook repair cleanup."""
2+
# ruff: noqa: SLF001
3+
# pylint: disable=protected-access
4+
5+
from __future__ import annotations
6+
7+
from typing import TYPE_CHECKING, Any
8+
9+
from custom_components.spook import repairs
10+
from custom_components.spook.repairs import AbstractSpookRepair, AbstractSpookRepairBase
11+
12+
EXPECTED_UNSUBSCRIBE_COUNT = 3
13+
14+
if TYPE_CHECKING:
15+
from collections.abc import Callable
16+
17+
from homeassistant.config_entries import ConfigEntry, ConfigEntryChange
18+
from homeassistant.core import HomeAssistant
19+
import pytest
20+
21+
22+
class MockRepairBase(AbstractSpookRepairBase):
23+
"""Mock base repair."""
24+
25+
domain = "mock"
26+
repair = "mock_repair"
27+
28+
async def async_activate(self) -> None:
29+
"""Activate the repair."""
30+
31+
async def async_inspect(self) -> None:
32+
"""Inspect the repair."""
33+
34+
35+
class MockRepair(AbstractSpookRepair):
36+
"""Mock repair."""
37+
38+
domain = "mock"
39+
repair = "mock_repair"
40+
inspect_events = {"mock_event"}
41+
inspect_config_entry_changed = True
42+
inspect_on_reload = True
43+
44+
inspections = 0
45+
46+
async def async_inspect(self) -> None:
47+
"""Inspect the repair."""
48+
self.inspections += 1
49+
50+
51+
async def test_deactivate_deletes_issues_from_snapshot(hass: HomeAssistant) -> None:
52+
"""Test deactivation can delete issues while mutating the issue ID set."""
53+
repair = MockRepairBase(hass)
54+
repair.issue_ids = {"one", "two"}
55+
56+
await repair.async_deactivate()
57+
58+
assert not repair.issue_ids
59+
60+
61+
async def test_deactivate_unsubscribes_all_activation_listeners(
62+
hass: HomeAssistant,
63+
monkeypatch: pytest.MonkeyPatch,
64+
) -> None:
65+
"""Test repair deactivation unsubscribes every activation listener."""
66+
unsubscribed = []
67+
68+
def async_dispatcher_connect(
69+
hass: HomeAssistant,
70+
signal: str,
71+
target: Callable[[ConfigEntryChange, ConfigEntry], Any],
72+
) -> Callable[[], None]:
73+
"""Connect a dispatcher listener."""
74+
del hass, signal, target
75+
76+
def unsubscribe() -> None:
77+
"""Unsubscribe the listener."""
78+
unsubscribed.append("dispatcher")
79+
80+
return unsubscribe
81+
82+
monkeypatch.setattr(repairs, "async_dispatcher_connect", async_dispatcher_connect)
83+
84+
repair = MockRepair(hass)
85+
await repair.async_activate()
86+
87+
assert len(repair._event_subs) == EXPECTED_UNSUBSCRIBE_COUNT
88+
89+
await repair.async_deactivate()
90+
91+
assert len(unsubscribed) == 1
92+
assert not repair._event_subs

0 commit comments

Comments
 (0)