mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
139 lines
5.6 KiB
Python
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,
|
|
]
|