Files
houseplan-card/tests_backend/test_virtual_lights.py
T
Matysh e6366b6548 fix: load virtual_lights by path so offline collection survives
The file declared itself pure but imported the module through the package, and
the package __init__ unconditionally imports homeassistant. Without homeassistant
installed pytest did not skip the file — it stopped collecting the whole
tests_backend directory, taking the previously working pure suite down with it.
test_validation.py had already established the by-path pattern; virtual_lights.py
imports nothing beyond the standard library, so it loads cleanly.

The async tests also dropped their pytest-asyncio dependency in favour of
asyncio.run: the offline environment does not carry the plugin, and without it
the two tests failed as unsupported async defs. The offline gate has to be green,
or nobody runs it.

Verified in both environments: pytest+voluptuous only — 129 passed where
collection previously stopped dead; with pytest-asyncio as in CI — 129 passed.

Issue: #135
User-Visible: no
2026-08-14 10:10:34 +03:00

89 lines
3.5 KiB
Python

"""Pure operational-store rules independent of the HA WebSocket harness.
virtual_lights.py is loaded by path, without importing the HA integration
package: the ordinary `from custom_components.houseplan...` import executes the
package __init__, which unconditionally imports homeassistant — and a missing
homeassistant then breaks pytest collection for the whole tests_backend/
directory, not just this file (#135). test_validation.py established the
pattern; the module itself imports nothing beyond the standard library.
"""
import asyncio
import importlib.util
import os
_PATH = os.path.join(
os.path.dirname(os.path.dirname(__file__)),
"custom_components", "houseplan", "virtual_lights.py",
)
_spec = importlib.util.spec_from_file_location("hp_virtual_lights", _PATH)
_vl = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_vl)
async_reconcile_virtual_lights = _vl.async_reconcile_virtual_lights
async_toggle_virtual_light = _vl.async_toggle_virtual_light
async_virtual_light_snapshot = _vl.async_virtual_light_snapshot
eligible_virtual_light_ids = _vl.eligible_virtual_light_ids
class FakeStore:
def __init__(self, data=None):
self.data = data
self.writes = []
async def async_load(self):
return self.data
async def async_save(self, data):
self.data = data
self.writes.append(data)
def _config(*markers):
return {"spaces": [], "markers": list(markers), "settings": {}}
def _manual(marker_id, **extra):
return {
"id": marker_id, "binding": "virtual", "is_light": True,
"tap_action": "toggle", **extra,
}
def test_eligibility_is_the_exact_triple_and_hidden_is_not_lifecycle():
config = _config(
_manual("eligible", hidden=True),
_manual("wrong-action", tap_action="info"),
_manual("wrong-role", is_light=False),
_manual("removed", removed=True),
{"id": "ha", "binding": "entity:light.lamp", "is_light": True, "tap_action": "toggle"},
)
assert eligible_virtual_light_ids(config) == {"eligible"}
def test_revision_gap_fails_safe_on_and_known_transition_preserves_only_eligible():
# asyncio.run вместо pytest.mark.asyncio: маркер требует плагина
# pytest-asyncio, которого в офлайн-окружении без HA нет, и тесты падали бы
# как «async def not natively supported» — офлайн-гейт обязан быть зелёным.
store = FakeStore({"rev": 5, "config_rev": 2, "off": ["keep", "drop"]})
gap = asyncio.run(async_virtual_light_snapshot(store, _config(_manual("keep")), 4))
assert gap == {"rev": 6, "config_rev": 4, "off": []}
store = FakeStore({"rev": 8, "config_rev": 4, "off": ["keep", "drop"]})
carried = asyncio.run(async_reconcile_virtual_lights(
store,
_config(_manual("keep", hidden=True), _manual("drop", is_light=False)),
5,
previous_config_rev=4,
))
assert carried == {"rev": 9, "config_rev": 5, "off": ["keep"]}
def test_toggle_accepts_only_an_id_and_inverts_server_current_state():
store = FakeStore()
config = _config(_manual("lamp"))
first = asyncio.run(async_toggle_virtual_light(store, config, 1, "lamp"))
second = asyncio.run(async_toggle_virtual_light(store, config, 1, "lamp"))
assert first == {"marker_id": "lamp", "on": False, "rev": 1}
assert second == {"marker_id": "lamp", "on": True, "rev": 2}
assert asyncio.run(async_toggle_virtual_light(store, config, 1, "missing")) is None