forked from Monitor-My-Solar/monitormysolar
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_recovery_snapshot.py
More file actions
106 lines (79 loc) · 4.19 KB
/
Copy pathtest_recovery_snapshot.py
File metadata and controls
106 lines (79 loc) · 4.19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
"""Tests for dongle-recovery snapshot re-requests.
When a dongle restarts and comes back, entities must repopulate. The coordinator
re-requests the full snapshot on: any /availability 'online' (after the first),
a boot.count change, or data resuming after a silent gap — all debounced so a
burst of triggers only sends one request.
"""
import asyncio
from unittest.mock import MagicMock
def _run(coro):
return asyncio.run(coro)
def _prep(coordinator, monkeypatch, published=True):
# Capture request_snapshot calls without publishing. `published` models its
# return value: True = the request actually went out, False = the publish
# failed or was suppressed.
calls = []
async def fake_request_snapshot(dongle_id, version="", force=False):
calls.append((dongle_id, force))
return published
monkeypatch.setattr(coordinator, "request_snapshot", fake_request_snapshot)
coordinator.current_fw_versions = {}
return calls
def test_recovery_snapshot_debounced(coordinator, monkeypatch):
calls = _prep(coordinator, monkeypatch)
coordinator._last_recovery_snapshot = {}
coordinator._recovery_snapshot_debounce = 30.0
# Two recovery requests in quick succession -> only one snapshot.
_run(coordinator.request_recovery_snapshot("dongle-A", "test1"))
_run(coordinator.request_recovery_snapshot("dongle-A", "test2"))
assert calls == [("dongle-A", True)]
def test_failed_publish_does_not_burn_debounce(coordinator, monkeypatch):
"""A request that never went out must not suppress the follow-up triggers.
Recovery triggers fire while the dongle is still rebooting, so the first
attempt can be published before it has resubscribed and is then lost. If
the debounce window were stamped regardless, the follow-up triggers would
be swallowed and — because FW >= 4.3.0 only streams change-data — the
dongle's settings entities would stay empty for the rest of the session.
This is the same rule the OTA-suppression path already follows; it just
also has to apply when the publish itself fails.
"""
calls = _prep(coordinator, monkeypatch, published=False)
coordinator._last_recovery_snapshot = {}
coordinator._recovery_snapshot_debounce = 30.0
_run(coordinator.request_recovery_snapshot("dongle-A", "reboot detected"))
_run(coordinator.request_recovery_snapshot("dongle-A", "availability online"))
assert calls == [("dongle-A", True), ("dongle-A", True)]
assert "dongle-A" not in coordinator._last_recovery_snapshot
def test_recovery_snapshot_per_dongle(coordinator, monkeypatch):
calls = _prep(coordinator, monkeypatch)
coordinator._last_recovery_snapshot = {}
coordinator._recovery_snapshot_debounce = 30.0
_run(coordinator.request_recovery_snapshot("dongle-A", "x"))
_run(coordinator.request_recovery_snapshot("dongle-B", "x"))
# Debounce is per-dongle: both fire.
assert ("dongle-A", True) in calls and ("dongle-B", True) in calls
def test_mark_dongle_seen_gap_triggers_recovery(coordinator, monkeypatch):
calls = _prep(coordinator, monkeypatch)
coordinator._last_recovery_snapshot = {}
coordinator._recovery_snapshot_debounce = 30.0
coordinator._dongle_stale_after = 90.0
# Simulate: last seen 200s ago (gone dark), now a message arrives.
import time
coordinator._dongle_last_seen = {"dongle-A": time.monotonic() - 200}
_run(coordinator.mark_dongle_seen("dongle-A"))
assert calls == [("dongle-A", True)] # gap recovery fired
def test_mark_dongle_seen_no_gap_no_recovery(coordinator, monkeypatch):
calls = _prep(coordinator, monkeypatch)
coordinator._last_recovery_snapshot = {}
coordinator._dongle_stale_after = 90.0
import time
# Seen 5s ago -> not stale -> no recovery.
coordinator._dongle_last_seen = {"dongle-A": time.monotonic() - 5}
_run(coordinator.mark_dongle_seen("dongle-A"))
assert calls == []
def test_first_sighting_no_recovery(coordinator, monkeypatch):
calls = _prep(coordinator, monkeypatch)
coordinator._last_recovery_snapshot = {}
coordinator._dongle_last_seen = {} # never seen before
_run(coordinator.mark_dongle_seen("dongle-A"))
assert calls == [] # first message doesn't trigger recovery