Files
ledgrab/server/tests/storage/test_value_source_factories.py
alexei.dolgolyov 888f8fd16e refactor(types): PEP-604 union sweep + UP007/UP045 enforcement
ruff --select UP007,UP045 --fix converted ~1760 sites across the
backend: `Optional[T]` → `T | None`, `Union[X, Y]` → `X | Y`. The
remaining module-level alias targets that ruff conservatively skips
(BindableFloatInput, ColorList, DeviceConfig) were converted by hand
earlier in the pass. black -formatted the result so the wider unions
fit cleanly under the 100-char line budget.

pyproject.toml now sets [tool.ruff.lint] extend-select = ["UP007",
"UP045"] so future legacy imports fire CI on every push. The
pre-commit ruff hook was bumped from v0.8.0 -> v0.15.12 to recognise
UP045 (split off from UP007 in v0.13).
2026-05-23 01:21:44 +03:00

271 lines
7.7 KiB
Python

"""Tests for the per-type ValueSource factory registry.
Locks in the contract that ``CREATE_BUILDERS`` and ``UPDATE_APPLIERS``
stay symmetric with storage's ``_VALUE_SOURCE_MAP``, and exercises a
couple of representative builders end-to-end.
"""
from __future__ import annotations
from datetime import datetime, timezone
import pytest
from ledgrab.storage import value_source_factories as vsf
from ledgrab.storage.value_source import (
AdaptiveValueSource,
HAEntityValueSource,
StaticValueSource,
_VALUE_SOURCE_MAP,
)
# ---------------------------------------------------------------------------
# Coverage / shape
# ---------------------------------------------------------------------------
def test_create_builders_cover_every_storage_kind():
assert set(vsf.CREATE_BUILDERS.keys()) == set(_VALUE_SOURCE_MAP.keys())
def test_update_appliers_cover_every_storage_kind():
assert set(vsf.UPDATE_APPLIERS.keys()) == set(_VALUE_SOURCE_MAP.keys())
def test_coverage_assertion_raises_when_create_drifts(monkeypatch):
"""Removing a builder makes _assert_factory_coverage() fail loudly."""
pruned = {k: v for k, v in vsf.CREATE_BUILDERS.items() if k != "static"}
monkeypatch.setattr(vsf, "CREATE_BUILDERS", pruned)
with pytest.raises(RuntimeError, match="static"):
vsf._assert_factory_coverage()
def test_coverage_assertion_raises_when_update_drifts(monkeypatch):
pruned = {k: v for k, v in vsf.UPDATE_APPLIERS.items() if k != "http"}
monkeypatch.setattr(vsf, "UPDATE_APPLIERS", pruned)
with pytest.raises(RuntimeError, match="http"):
vsf._assert_factory_coverage()
# ---------------------------------------------------------------------------
# build_source — representative paths
# ---------------------------------------------------------------------------
def _now() -> datetime:
return datetime.now(timezone.utc)
def test_build_source_static_uses_value_default():
src = vsf.build_source(
source_type="static",
sid="vs_t",
name="Test",
now=_now(),
description=None,
tags=None,
icon=None,
icon_color=None,
)
assert isinstance(src, StaticValueSource)
assert src.value == 1.0
assert src.source_type == "static"
def test_build_source_static_honours_supplied_value():
src = vsf.build_source(
source_type="static",
sid="vs_t",
name="Test",
now=_now(),
description=None,
tags=None,
icon=None,
icon_color=None,
value=0.42,
)
assert src.value == 0.42
def test_build_source_adaptive_time_requires_schedule_with_two_points():
with pytest.raises(ValueError, match="Time of day schedule"):
vsf.build_source(
source_type="adaptive_time",
sid="vs_t",
name="Test",
now=_now(),
description=None,
tags=None,
icon=None,
icon_color=None,
schedule=[{"time": "00:00", "value": 0.5}], # only one point
)
def test_build_source_adaptive_scene_routes_to_adaptive_class_with_scene_type():
"""``adaptive_scene`` and ``adaptive_time`` share AdaptiveValueSource but
differ in source_type — the builder must set the right discriminator."""
src = vsf.build_source(
source_type="adaptive_scene",
sid="vs_t",
name="Test",
now=_now(),
description=None,
tags=None,
icon=None,
icon_color=None,
picture_source_id="pic_42",
)
assert isinstance(src, AdaptiveValueSource)
assert src.source_type == "adaptive_scene"
assert src.picture_source_id == "pic_42"
def test_build_source_ha_entity_requires_ha_source_id_and_entity_id():
with pytest.raises(ValueError, match="HA source ID"):
vsf.build_source(
source_type="ha_entity",
sid="vs_t",
name="Test",
now=_now(),
description=None,
tags=None,
icon=None,
icon_color=None,
entity_id="light.foo", # missing ha_source_id
)
with pytest.raises(ValueError, match="Entity ID"):
vsf.build_source(
source_type="ha_entity",
sid="vs_t",
name="Test",
now=_now(),
description=None,
tags=None,
icon=None,
icon_color=None,
ha_source_id="ha_main", # missing entity_id
)
def test_build_source_http_validates_interval():
with pytest.raises(ValueError, match="interval_s"):
vsf.build_source(
source_type="http",
sid="vs_t",
name="Test",
now=_now(),
description=None,
tags=None,
icon=None,
icon_color=None,
http_endpoint_id="ep_1",
interval_s=0,
)
def test_build_source_unknown_type_raises():
with pytest.raises(ValueError, match="Invalid source type"):
vsf.build_source(
source_type="totally_unregistered",
sid="vs_t",
name="Test",
now=_now(),
description=None,
tags=None,
icon=None,
icon_color=None,
)
def test_build_source_game_event_is_not_creatable_via_store():
"""game_event sources were never creatable through the store API.
The builder exists for coverage-assertion symmetry but raises so the
historical contract holds (game-event value sources are wired up by
the game-integration setup path, not user CRUD).
"""
with pytest.raises(NotImplementedError, match="game_event"):
vsf.build_source(
source_type="game_event",
sid="vs_t",
name="Test",
now=_now(),
description=None,
tags=None,
icon=None,
icon_color=None,
)
# ---------------------------------------------------------------------------
# apply_update — representative paths
# ---------------------------------------------------------------------------
def test_apply_update_static_mutates_value():
src = vsf.build_source(
source_type="static",
sid="vs_t",
name="Test",
now=_now(),
description=None,
tags=None,
icon=None,
icon_color=None,
)
vsf.apply_update(src, value=0.75)
assert src.value == 0.75
def test_apply_update_ignores_unknown_kwargs():
"""Stray kwargs are silently dropped by each applier's ``**_``."""
src = vsf.build_source(
source_type="static",
sid="vs_t",
name="Test",
now=_now(),
description=None,
tags=None,
icon=None,
icon_color=None,
)
vsf.apply_update(src, value=0.5, totally_made_up_field="ignored")
assert src.value == 0.5
def test_apply_update_ha_entity_validates_smoothing_path():
src = vsf.build_source(
source_type="ha_entity",
sid="vs_t",
name="Test",
now=_now(),
description=None,
tags=None,
icon=None,
icon_color=None,
ha_source_id="ha_main",
entity_id="light.foo",
)
assert isinstance(src, HAEntityValueSource)
vsf.apply_update(src, smoothing=0.5, attribute="brightness")
assert src.smoothing == 0.5
assert src.attribute == "brightness"
def test_apply_update_http_rejects_zero_interval():
src = vsf.build_source(
source_type="http",
sid="vs_t",
name="Test",
now=_now(),
description=None,
tags=None,
icon=None,
icon_color=None,
http_endpoint_id="ep_1",
)
with pytest.raises(ValueError, match="interval_s"):
vsf.apply_update(src, interval_s=0)