Files
houseplan-card/tests_backend/test_projection.py
2026-08-23 10:22:51 +03:00

139 lines
5.6 KiB
Python

"""Pure tests for the read-only projections (#256).
The main invariant is negative: without parameters nothing may change. Every
other case is measured against that one, because a projection that quietly
reshapes the default response would break clients that never asked for it.
"""
from __future__ import annotations
import copy
import importlib.util
import os
# Модуль грузится по пути, а не импортом пакета: `custom_components.houseplan`
# исполняет `__init__`, который безусловно тянет homeassistant, и на машине без
# HA ломается сбор всего каталога tests_backend, а не только этого файла (#135).
# Тот же приём, что в test_virtual_lights.py; сам модуль не зависит ни от чего,
# кроме стандартной библиотеки.
_PATH = os.path.join(
os.path.dirname(os.path.dirname(__file__)),
"custom_components", "houseplan", "projection.py",
)
_spec = importlib.util.spec_from_file_location("hp_projection", _PATH)
_projection = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_projection)
project_config = _projection.project_config
project_layout = _projection.project_layout
project_markers = _projection.project_markers
CONFIG = {
"spaces": [
{"id": "f1", "title": "First", "rooms": [{"id": "r1"}]},
{"id": "f2", "title": "Second", "rooms": []},
],
"markers": [
{"id": "m1", "binding": "virtual", "space": "f1", "icon": "mdi:lamp", "pdfs": []},
{"id": "m2", "binding": "device:abc", "space": "f2", "icon": None},
],
"settings": {"glow_radius_cm": 200},
"model_version": 6,
}
LAYOUT = {
"m1": {"s": "f1", "x": 0.1, "y": 0.2},
"m2": {"s": "f2", "x": 0.3, "y": 0.4},
"rl_r1": {"s": "f1", "x": 0.5, "y": 0.6},
}
def test_no_parameters_change_nothing():
original = copy.deepcopy(CONFIG)
assert project_config(CONFIG) == original
assert project_config(CONFIG) is CONFIG
assert project_layout(LAYOUT) is LAYOUT
assert CONFIG == original
def test_space_id_narrows_spaces_only():
projected = project_config(CONFIG, space_id="f1")
assert [space["id"] for space in projected["spaces"]] == ["f1"]
# Остальные разделы не урезаются: клиент просил меньше пространств, а не
# меньше конфигурации.
assert projected["markers"] == CONFIG["markers"]
assert projected["settings"] == CONFIG["settings"]
assert projected["model_version"] == 6
# Исходный документ не тронут.
assert len(CONFIG["spaces"]) == 2
def test_unknown_space_returns_empty_list_not_an_error():
projected = project_config(CONFIG, space_id="nope")
assert projected["spaces"] == []
assert projected["markers"] == CONFIG["markers"]
def test_marker_fields_keep_id_even_when_not_asked():
projected = project_config(CONFIG, marker_fields=["binding", "space"])
assert projected["markers"] == [
{"id": "m1", "binding": "virtual", "space": "f1"},
{"id": "m2", "binding": "device:abc", "space": "f2"},
]
assert CONFIG["markers"][0]["icon"] == "mdi:lamp"
def test_unknown_marker_field_adds_nothing():
projected = project_config(CONFIG, marker_fields=["binding", "does_not_exist"])
assert projected["markers"][0] == {"id": "m1", "binding": "virtual"}
def test_fields_narrow_top_level_keys():
projected = project_config(CONFIG, fields=["spaces"])
assert set(projected) == {"spaces"}
projected = project_config(CONFIG, fields=["markers", "settings"])
assert set(projected) == {"markers", "settings"}
def test_fields_and_marker_fields_combine():
projected = project_config(CONFIG, fields=["markers"], marker_fields=["space"])
assert projected == {"markers": [{"id": "m1", "space": "f1"}, {"id": "m2", "space": "f2"}]}
def test_empty_or_malformed_lists_mean_no_projection():
# Пустой список — это «я ничего не выбрал», а не «оставь ноль полей»:
# второе трактование превращает опечатку клиента в пустой ответ.
assert project_config(CONFIG, fields=[]) is CONFIG
assert project_config(CONFIG, marker_fields=[]) is CONFIG
assert project_config(CONFIG, fields="spaces") is CONFIG
assert project_markers(CONFIG["markers"], None) == CONFIG["markers"]
def test_layout_space_filter():
assert project_layout(LAYOUT, space_id="f1") == {
"m1": {"s": "f1", "x": 0.1, "y": 0.2},
"rl_r1": {"s": "f1", "x": 0.5, "y": 0.6},
}
assert project_layout(LAYOUT, space_id="nope") == {}
assert LAYOUT == {
"m1": {"s": "f1", "x": 0.1, "y": 0.2},
"m2": {"s": "f2", "x": 0.3, "y": 0.4},
"rl_r1": {"s": "f1", "x": 0.5, "y": 0.6},
}
def test_non_dict_documents_pass_through():
# Хранилище может отдать что угодно после ручной правки файла: проекция —
# не место для валидации, она обязана не мешать читать то, что есть.
assert project_config(None) is None
assert project_config([1, 2], space_id="f1") == [1, 2]
assert project_layout("nope", space_id="f1") == "nope"
assert project_markers("nope", ["id"]) == "nope"
def test_markers_survive_unexpected_entries():
markers = [{"id": "m1", "binding": "virtual"}, "junk", None]
assert project_markers(markers, ["binding"]) == [
{"id": "m1", "binding": "virtual"}, "junk", None,
]