fix(discovery): a sweep that could not run is not an answer (t62 F1)
The installed-apps sweep stamped an EMPTY answer on every failure path, so a Shield that was asleep at the entity's startup sweep was cached as "0 apps" for APP_LIST_TTL (6 h): `homeassistant.update_entity` then issued zero shells and the sensor kept reporting 0/"pending" until the TTL or the Configure row's force. A genuinely empty device was indistinguishable from it, so "ok" was unreachable for an empty list. Only a sweep that RAN stamps the cache now. Every failure records the outcome as `unknown` (`_apps_unavailable`), stores no table, and is retried by the next non-forced refresh; a sweep that reached the device and failed is spaced by APP_SWEEP_RETRY_SECONDS (30 s, force=True bypasses) so a silent socket is not hammered once per poll, while an unavailable transport costs nothing and is retried immediately. `installed_apps_status` exposes ok/unknown/pending, the sensor surfaces it, and "ok" + an empty table is now the honest empty answer. Docstrings, the README vocabulary and the docs on what a refresh actually re-runs are corrected; the "update_entity re-runs only without a fresh table, the Configure row forces one" wording stays true. Red proof (pre-fix sources, current tests): the five new/updated tests fail on HEAD, e.g. `installed_apps_stale` is False after the ADB-down startup sweep, and the sensor reports 'pending' where it must say 'unknown'. Same class already fixed for app labels: a fetch that could not run is not an answer. Also records that the `-3`/resolve-activity/pm-dump output shapes are documented, not observed.
This commit is contained in:
1 parent
ef42682250
commit
c05e6fa929
4 files changed
+217
-43
No files matched your search
@@ -393,7 +393,11 @@ class TestDiscoverySweep:
|
||||
|
||||
|
||||
class TestDiscoveryDegradesGracefully:
|
||||
"""ADB off, down or mute: an empty list and a note, never a crash or a loop."""
|
||||
"""ADB off, down or mute: an empty list and a note, never a crash or a loop.
|
||||
|
||||
t62 F1: "could not ask" and "asked, nothing there" are different answers.
|
||||
Only the second one is cached; the first is retried.
|
||||
"""
|
||||
|
||||
async def test_no_poller_is_an_empty_list(self):
|
||||
manager = ShieldConnectionManager(hass=None, entry=None, poller=None)
|
||||
@@ -416,19 +420,64 @@ class TestDiscoveryDegradesGracefully:
|
||||
assert rows == []
|
||||
assert poller.shell_calls == [] # nothing was even attempted
|
||||
|
||||
async def test_a_mute_transport_reports_empty_without_raising(self):
|
||||
async def test_a_mute_transport_is_spaced_but_not_stamped(self):
|
||||
"""t62 F1: a failed sweep is retried — but not once per state poll."""
|
||||
now = [1000.0]
|
||||
poller = _DiscoveryPoller(failure=True)
|
||||
manager = _manager(poller)
|
||||
manager = ShieldConnectionManager(
|
||||
hass=None, entry=None, poller=poller, clock=lambda: now[0]
|
||||
)
|
||||
|
||||
rows = await manager.async_refresh_installed_apps()
|
||||
|
||||
assert rows == []
|
||||
assert poller.shell_calls # it tried
|
||||
# and a second call does NOT re-shell in a loop: the empty answer is
|
||||
# stamped, and only an explicit force/TTL asks again
|
||||
assert manager.installed_apps_status == "unknown" # NOT an empty answer
|
||||
# the next poll inside the window does NOT re-shell (no hammering)..
|
||||
calls = len(poller.shell_calls)
|
||||
assert await manager.async_refresh_installed_apps() == []
|
||||
assert len(poller.shell_calls) == calls
|
||||
# ..but nothing was stamped as an answer, so once the window passes the
|
||||
# sweep runs again. Pre-fix it waited out APP_LIST_TTL (6 h) instead.
|
||||
now[0] += manager.APP_SWEEP_RETRY_SECONDS + 1
|
||||
assert await manager.async_refresh_installed_apps() == []
|
||||
assert len(poller.shell_calls) > calls
|
||||
|
||||
async def test_a_sweep_that_could_not_run_is_not_a_fresh_answer(self):
|
||||
"""t62 F1 (red proof): startup with ADB down must not BE the answer.
|
||||
|
||||
The entity's one startup sweep runs with the poller down. Once ADB comes
|
||||
back, ``homeassistant.update_entity`` (``sensor.async_update``) must
|
||||
recover the table. Pre-fix the startup sweep stamped an empty answer, so
|
||||
this call issued ZERO shells and kept reporting 0/"pending" for
|
||||
APP_LIST_TTL = 6 h.
|
||||
"""
|
||||
poller = _DiscoveryPoller(dumps={"ar.tvplayer.tv": DUMP_TIVIMATE})
|
||||
poller.connected = False
|
||||
manager = _manager(poller)
|
||||
|
||||
assert await manager.async_refresh_installed_apps() == [] # the startup
|
||||
assert poller.shell_calls == [] # it was not even attempted
|
||||
assert manager.installed_apps_stale is True # ...so it is not an answer
|
||||
assert manager.installed_apps_status == "unknown"
|
||||
|
||||
poller.connected = True # ADB is back
|
||||
rows = await manager.async_refresh_installed_apps()
|
||||
|
||||
assert rows, "update_entity must recover the table once ADB returns"
|
||||
assert "pm list packages -3" in poller.shell_calls
|
||||
assert manager.installed_apps_status == "ok"
|
||||
|
||||
async def test_a_genuinely_empty_answer_is_ok(self):
|
||||
"""t62 F1: 'ok' must be reachable for a really empty device."""
|
||||
poller = _DiscoveryPoller(packages="", launcher="")
|
||||
manager = _manager(poller)
|
||||
|
||||
assert await manager.async_refresh_installed_apps() == []
|
||||
|
||||
assert poller.shell_calls # the commands RAN and answered
|
||||
assert manager.installed_apps_stale is False # stamped: a real answer
|
||||
assert manager.installed_apps_status == "ok"
|
||||
|
||||
async def test_the_accessor_never_shells(self):
|
||||
poller = _DiscoveryPoller()
|
||||
@@ -534,6 +583,31 @@ class TestInstalledAppsSensor:
|
||||
assert attrs["installed_apps"] == []
|
||||
assert attrs["discovery"] in ("pending", "not_connected", "adb_disabled")
|
||||
|
||||
async def test_an_empty_discovery_is_ok_not_pending(self):
|
||||
"""t62 F1: a sweep that RAN and found nothing is the answer 'ok'."""
|
||||
poller = _DiscoveryPoller(packages="", launcher="")
|
||||
manager = _manager(poller)
|
||||
await manager.async_refresh_installed_apps()
|
||||
sensor = self._sensor(manager, {"adb_enabled": True})
|
||||
|
||||
assert sensor.native_value == 0
|
||||
assert sensor.extra_state_attributes == {
|
||||
"installed_apps": [],
|
||||
"discovery": "ok",
|
||||
}
|
||||
|
||||
async def test_a_sweep_that_could_not_run_is_unknown_not_pending(self):
|
||||
"""t62 F1: 'pending' used to cover both "running" and "could not run"."""
|
||||
poller = _DiscoveryPoller(failure=True)
|
||||
manager = _manager(poller)
|
||||
await manager.async_refresh_installed_apps()
|
||||
sensor = self._sensor(manager, {"adb_enabled": True})
|
||||
|
||||
assert sensor.extra_state_attributes == {
|
||||
"installed_apps": [],
|
||||
"discovery": "unknown",
|
||||
}
|
||||
|
||||
def test_adb_disabled_is_reported_as_such(self):
|
||||
poller = _DiscoveryPoller()
|
||||
manager = _manager(poller)
|
||||
|
||||
Reference in new issue
Block a user