mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 11:49:16 +00:00
test: put the package stub in one place, under an honest condition
Чистые тесты подменяли custom_components и custom_components.houseplan пустышками и не убирали их никогда. В CI это не стреляло только потому, что настоящий пакет успевал импортироваться из файла, который идёт раньше по алфавиту: условие «если ещё не импортирован» оказывалось ложным. То есть корректность HA-харнесса держалась на именах файлов в каталоге, и хватило бы переименования, чтобы получить #389 заново. Подмена переехала в conftest и стала условной по единственному честному признаку: есть Home Assistant — работаем с настоящим пакетом и не подменяем ничего; нет — HA-тесты и так пропущены, ломать нечего. Первым заходом я делал подмену обратимой прямо в тестах, контекстным менеджером. Замер показал, что так теряется работоспособность test_wall_segment_model в песочнице: он импортирует пакет обычным способом и жил как раз за счёт чужой пустышки. Развилка в conftest сохраняет оба окружения и убирает зависимость от порядка файлов. Проверено в обе стороны: в песочнице 240 passed, а в эмуляции «HA есть» объект пакета после прогона тот же, что был до. Issue: #394 User-Visible: no
This commit is contained in:
@@ -26,3 +26,26 @@ test('backend-тесты не правят sys.path (#393)', () => {
|
||||
+ ' Читать модуль без Home Assistant можно и без этого — см. test_trails.py:'
|
||||
+ ' текст файла и exec нужного среза.');
|
||||
});
|
||||
|
||||
test('пакет интеграции подменяет только conftest и только без HA (#394)', () => {
|
||||
// Пустышка вместо `custom_components.houseplan` — единственный способ читать
|
||||
// подмодули без Home Assistant, и запретить её нельзя. Но место у неё одно:
|
||||
// conftest, где решение принимается по честному признаку «есть ли HA».
|
||||
//
|
||||
// Когда её ставили сами тесты под условием «если ещё не импортирован», в CI
|
||||
// она не срабатывала лишь потому, что настоящий пакет успевал импортироваться
|
||||
// из файла, который идёт раньше по алфавиту. Корректность харнесса держалась
|
||||
// на именах файлов; чем это кончается, показал #389.
|
||||
const assigns = /sys\.modules\[\s*(['"])custom_components/;
|
||||
const offenders = files().filter((name) => name !== 'conftest.py' && assigns.test(read(name)));
|
||||
assert.deepEqual(offenders, [],
|
||||
'подмена пакета интеграции живёт в tests_backend/conftest.py и только там:'
|
||||
+ ' там она условная (нет Home Assistant — нечего ломать), а в тесте она'
|
||||
+ ' переживает свой тест и достаётся всей сессии (#389, #394).');
|
||||
|
||||
const conftest = read('conftest.py');
|
||||
assert.match(conftest, /if not HAS_HA:/,
|
||||
'подмена в conftest обязана быть под условием отсутствия Home Assistant');
|
||||
const stub = conftest.slice(conftest.indexOf('if not HAS_HA:'));
|
||||
assert.match(stub, /sys\.modules\[_name\] = _module/);
|
||||
});
|
||||
|
||||
@@ -10,6 +10,36 @@ except ImportError:
|
||||
|
||||
collect_ignore_glob = [] if HAS_HA else ["test_ha_*.py"]
|
||||
|
||||
# Пакет интеграции без Home Assistant не импортируется: его __init__.py тянет
|
||||
# весь HA. Чистым тестам нужны только подмодули, поэтому в окружении БЕЗ HA
|
||||
# родительские пакеты подменяются пустышками с настоящим __path__ — подмодули
|
||||
# после этого читаются, а тяжёлый __init__.py не исполняется.
|
||||
#
|
||||
# Подмена живёт здесь, а не в тестах, и это главное решение issue #394. Раньше
|
||||
# её ставили сами тесты, под условием «если ещё не импортирован», и в CI она не
|
||||
# срабатывала лишь потому, что настоящий пакет успевал импортироваться из файла,
|
||||
# который идёт раньше по алфавиту. Корректность HA-харнесса держалась на именах
|
||||
# файлов в каталоге; чем это кончается, показал #389 — 85 тестов упали с голым
|
||||
# `assert False`, потому что HA получил пустышку вместо интеграции.
|
||||
#
|
||||
# Теперь развилка явная и по единственному честному признаку: есть Home
|
||||
# Assistant — работаем с настоящим пакетом и ничего не подменяем; нет — значит
|
||||
# HA-тесты и так пропущены, и подменять безопасно, ломать нечего.
|
||||
if not HAS_HA:
|
||||
import sys
|
||||
import types
|
||||
from pathlib import Path
|
||||
|
||||
_ROOT = Path(__file__).resolve().parent.parent
|
||||
for _name, _path in (
|
||||
("custom_components", _ROOT / "custom_components"),
|
||||
("custom_components.houseplan", _ROOT / "custom_components" / "houseplan"),
|
||||
):
|
||||
if _name not in sys.modules:
|
||||
_module = types.ModuleType(_name)
|
||||
_module.__path__ = [str(_path)]
|
||||
sys.modules[_name] = _module
|
||||
|
||||
if HAS_HA:
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Загрузка модуля интеграции по пути, без Home Assistant (issue #394).
|
||||
|
||||
Зачем не обычный импорт: часть чистых тестов читает модуль под своим именем
|
||||
(`hp_validation`) либо под каноническим, чтобы работали относительные импорты
|
||||
внутри него. Родительские пакеты при этом должны существовать — в окружении с
|
||||
Home Assistant это настоящий пакет, без него пустышки ставит `conftest.py`.
|
||||
|
||||
Ставить пустышки здесь, в момент загрузки, было бы ошибкой: именно так они
|
||||
переживали свой тест и доставались всей сессии. В #389 это стоило пяти часов
|
||||
красного `dev` — HA получал пустышку вместо интеграции и не мог её поднять.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
PACKAGE_ROOT = REPO / "custom_components"
|
||||
HOUSEPLAN_ROOT = PACKAGE_ROOT / "houseplan"
|
||||
|
||||
|
||||
def load_pure(name: str, file: Path):
|
||||
"""Загрузить модуль по пути под указанным именем.
|
||||
|
||||
Имя значимо: относительные импорты внутри модуля резолвятся только тогда,
|
||||
когда модуль знает, какому пакету принадлежит.
|
||||
"""
|
||||
spec = importlib.util.spec_from_file_location(name, file)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
@@ -6,38 +6,29 @@ fixtures to the TypeScript checks and to this module and demands the same
|
||||
verdict, because two implementations of one rule are worth nothing if they can
|
||||
disagree.
|
||||
"""
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import types
|
||||
|
||||
import pytest
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from pure_imports import load_pure
|
||||
|
||||
_ROOT = os.path.dirname(os.path.dirname(__file__))
|
||||
_PACKAGE_ROOT = os.path.join(_ROOT, "custom_components")
|
||||
_HOUSEPLAN_ROOT = os.path.join(_PACKAGE_ROOT, "houseplan")
|
||||
|
||||
if "custom_components" not in sys.modules:
|
||||
package = types.ModuleType("custom_components")
|
||||
package.__path__ = [_PACKAGE_ROOT]
|
||||
sys.modules["custom_components"] = package
|
||||
if "custom_components.houseplan" not in sys.modules:
|
||||
package = types.ModuleType("custom_components.houseplan")
|
||||
package.__path__ = [_HOUSEPLAN_ROOT]
|
||||
sys.modules["custom_components.houseplan"] = package
|
||||
|
||||
# Loaded under its canonical package name: the module imports the migration
|
||||
# mirror relatively (`from .wall_segment_model import ...`), which only resolves
|
||||
# when the module knows the package it belongs to.
|
||||
_PATH = os.path.join(_HOUSEPLAN_ROOT, "junction_limits.py")
|
||||
_spec = importlib.util.spec_from_file_location(
|
||||
"custom_components.houseplan.junction_limits", _PATH,
|
||||
# Каноническое имя обязательно: модуль импортирует зеркало миграции
|
||||
# относительным путём (`from .wall_segment_model import ...`), а это резолвится
|
||||
# только когда модуль знает свой пакет. Подмена родительских пакетов живёт
|
||||
# ровно на время загрузки — почему именно так, см. tests_backend/pure_imports.py
|
||||
# и #394.
|
||||
jl = load_pure(
|
||||
"custom_components.houseplan.junction_limits",
|
||||
Path(_HOUSEPLAN_ROOT) / "junction_limits.py",
|
||||
)
|
||||
jl = importlib.util.module_from_spec(_spec)
|
||||
sys.modules[_spec.name] = jl
|
||||
_spec.loader.exec_module(jl)
|
||||
|
||||
CELL = 5.0
|
||||
|
||||
|
||||
@@ -10,28 +10,20 @@ import types
|
||||
import pytest
|
||||
import voluptuous as vol
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from pure_imports import load_pure
|
||||
|
||||
_ROOT = os.path.dirname(os.path.dirname(__file__))
|
||||
_PACKAGE_ROOT = os.path.join(_ROOT, "custom_components")
|
||||
_HOUSEPLAN_ROOT = os.path.join(_PACKAGE_ROOT, "houseplan")
|
||||
|
||||
# Keep this pure test independent of Home Assistant even though Python normally
|
||||
# executes package __init__.py before resolving the validation submodule.
|
||||
if "custom_components" not in sys.modules:
|
||||
package = types.ModuleType("custom_components")
|
||||
package.__path__ = [_PACKAGE_ROOT]
|
||||
sys.modules["custom_components"] = package
|
||||
if "custom_components.houseplan" not in sys.modules:
|
||||
package = types.ModuleType("custom_components.houseplan")
|
||||
package.__path__ = [_HOUSEPLAN_ROOT]
|
||||
sys.modules["custom_components.houseplan"] = package
|
||||
|
||||
_PATH = os.path.join(
|
||||
_ROOT,
|
||||
"custom_components", "houseplan", "validation.py",
|
||||
)
|
||||
_spec = importlib.util.spec_from_file_location("hp_validation", _PATH)
|
||||
v = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(v)
|
||||
# Тест обходится без Home Assistant, хотя Python выполнил бы __init__.py пакета
|
||||
# прежде, чем добрался до validation. Подмена родительских пакетов живёт ровно
|
||||
# на время загрузки: прежняя редакция ставила пустышки насовсем и не падала
|
||||
# только потому, что настоящий пакет успевал импортироваться из файла, который
|
||||
# идёт раньше по алфавиту (#394). См. tests_backend/pure_imports.py.
|
||||
v = load_pure("hp_validation", Path(_HOUSEPLAN_ROOT) / "validation.py")
|
||||
|
||||
|
||||
def test_backend_model_version_matches_frontend_constant():
|
||||
|
||||
Reference in New Issue
Block a user