test: sound backoff-jitter assertions (per-delay band + non-overlapping growth) and scoped import-stub teardown in the pairing regression tests (F5 residual)
This commit is contained in:
1 parent
75fff11d04
commit
d07820cde8
2 files changed
+44
-4
No files matched your search
@@ -28,6 +28,20 @@ from nsh.const import (
|
|||||||
_KEY_PATH = "/tmp/nsh-test-adbkey"
|
_KEY_PATH = "/tmp/nsh-test-adbkey"
|
||||||
|
|
||||||
|
|
||||||
|
_STUB_NAMES = (
|
||||||
|
"voluptuous",
|
||||||
|
"homeassistant",
|
||||||
|
"homeassistant.config_entries",
|
||||||
|
"homeassistant.core",
|
||||||
|
"homeassistant.helpers",
|
||||||
|
"homeassistant.helpers.entity_registry",
|
||||||
|
"homeassistant.helpers.selector",
|
||||||
|
# Loaded execution runs beneath the stubs; never escape this file either.
|
||||||
|
"nsh.helpers",
|
||||||
|
"nsh.config_flow",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _install_import_stubs() -> None:
|
def _install_import_stubs() -> None:
|
||||||
"""Register import-time stand-ins for HA/voluptuous (idempotent)."""
|
"""Register import-time stand-ins for HA/voluptuous (idempotent)."""
|
||||||
if sys.modules.get("voluptuous") is not None:
|
if sys.modules.get("voluptuous") is not None:
|
||||||
@@ -139,6 +153,25 @@ def _flow(user_input_host: str, monkeypatch=None):
|
|||||||
return flow
|
return flow
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def scoped_stubs():
|
||||||
|
"""Contain the import stubs to this module's tests (t7 INFO).
|
||||||
|
|
||||||
|
Snapshots every injected sys.modules entry and restores or removes it on
|
||||||
|
teardown, so later tests never observe the fake homeassistant/voluptuous
|
||||||
|
(nor the stub-loaded nsh.helpers/nsh.config_flow) modules.
|
||||||
|
"""
|
||||||
|
snapshot = {name: sys.modules.get(name) for name in _STUB_NAMES}
|
||||||
|
_install_import_stubs()
|
||||||
|
yield
|
||||||
|
for name in _STUB_NAMES:
|
||||||
|
original = snapshot[name]
|
||||||
|
if original is None:
|
||||||
|
sys.modules.pop(name, None)
|
||||||
|
else:
|
||||||
|
sys.modules[name] = original
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mocked_test_connect(monkeypatch):
|
def mocked_test_connect(monkeypatch):
|
||||||
from nsh.connection import ShieldConnectionManager
|
from nsh.connection import ShieldConnectionManager
|
||||||
|
|||||||
@@ -211,15 +211,22 @@ class TestReconnectBackoff:
|
|||||||
manager = _manager(poller)
|
manager = _manager(poller)
|
||||||
|
|
||||||
delays = []
|
delays = []
|
||||||
|
backoffs = []
|
||||||
backoff = 5.0
|
backoff = 5.0
|
||||||
for _ in range(7):
|
for _ in range(7):
|
||||||
|
backoffs.append(backoff)
|
||||||
delays.append(await manager._watchdog_cycle(backoff))
|
delays.append(await manager._watchdog_cycle(backoff))
|
||||||
backoff = min(backoff * 2, 300.0)
|
backoff = min(backoff * 2, 300.0)
|
||||||
|
|
||||||
# 5→10→20→40→80→160→300 growth with easing; the last must not exceed
|
# F5 soundness: the previous ±25 % jitter could flake the claim
|
||||||
# the cap (with jitter).
|
# delays[1] > delays[0] * 1.5: BACKOFF=10 jittered to 9.17 vs
|
||||||
assert max(delays) <= 300.0 * 1.25
|
# BACKOFF=5 jittered to 6.25 → 6.25*1.5 = 9.375). Assert only what
|
||||||
assert delays[1] >= delays[0] * 1.5
|
# jitter guarantees: each delay sits inside ITS OWN backoff's band
|
||||||
|
# (non-overlapping consecutive bands ⇒ delays[1] >= delays[0],
|
||||||
|
# since min next = 7.5 >= max first = 6.25), and never above the cap.
|
||||||
|
for backoff, delay in zip(backoffs, delays, strict=True):
|
||||||
|
assert 0.75 * backoff <= delay <= 1.25 * backoff
|
||||||
|
assert delays[1] >= delays[0]
|
||||||
assert delays[-1] <= 300.0 * 1.25
|
assert delays[-1] <= 300.0 * 1.25
|
||||||
|
|
||||||
async def test_repeated_failures_do_not_spam_listeners(self):
|
async def test_repeated_failures_do_not_spam_listeners(self):
|
||||||
|
|||||||
Reference in new issue
Block a user