ci: pin the HA harness and stop tests from editing sys.path

Два независимых хвоста из разбора #389, оба про то, что «зелёный» значит не
то, что читается.

#392. Оба места, где поднимается HA-харнесс, ставили зависимости без единой
версии. Python при этом закреплён на 3.13, а pytest-homeassistant-custom-
component с 0.13.348 требует 3.14 — резолвер молча уезжал на 0.13.316, а та
тянет homeassistant 2026.2.3. Интеграция полгода проверялась против
февральского HA, и состав окружения мог измениться без нашего коммита.
Версии закреплены в tests_backend/requirements.txt ровно те, что резолвились
30.08; шаг печатает установленное в лог. Это остановка дрейфа, а не
обновление: переход на 3.14 и свежий phcc — отдельная задача с отдельным
измерением.

#393. test_trails.py клал каталог пакета в sys.path при коллекции — на всю
сессию, включая HA-харнесс. Модули интеграции становились импортируемыми ещё
и как модули верхнего уровня, то есть один файл мог оказаться в sys.modules
дважды. Вставка при этом не работала ни на что: TrailBook берётся чтением
текста и exec среза, а не импортом. Убрана вместе с мёртвым spec.

Гейт статический, по исходникам: он ловит намерение, а рантайм поймал бы
последствие и только при сегодняшнем порядке тестов.

Issue: #392
User-Visible: no
This commit is contained in:
Matysh
2026-08-30 19:49:37 +03:00
parent 162c8a3bef
commit eb5aa2a0f8
6 changed files with 89 additions and 10 deletions
+6 -1
View File
@@ -64,7 +64,12 @@ jobs:
python-version: '3.13'
- name: Установить backend test dependencies
run: pip install pytest voluptuous pytest-homeassistant-custom-component home-assistant-frontend
run: |
# Точные версии — в tests_backend/requirements.txt (#392): без них
# резолвер выбирает набор сам, и «зелёный backend» значит разное
# в разные дни.
pip install -r tests_backend/requirements.txt
pip list --format=columns | grep -Ei 'homeassistant|voluptuous|^pytest '
- name: Кэш браузеров Playwright
id: pw
+6 -1
View File
@@ -778,7 +778,12 @@ jobs:
with: { node-version: 22 }
- uses: actions/setup-python@v7
with: { python-version: "3.13" }
- run: pip install pytest voluptuous pytest-homeassistant-custom-component home-assistant-frontend
- run: |
# Точные версии — в tests_backend/requirements.txt (#392): без них
# резолвер выбирает набор сам, и «зелёный backend» значит разное
# в разные дни.
pip install -r tests_backend/requirements.txt
pip list --format=columns | grep -Ei 'homeassistant|voluptuous|^pytest '
- name: Backend unit tests (pure + HA harness)
run: python -m pytest tests_backend/ -q
# Маркер пишется последним шагом: он существует только если всё выше
+28
View File
@@ -0,0 +1,28 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync, readdirSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
// Гигиена backend-тестов (#393): тест не имеет права оставлять после себя
// изменённую импорт-машину. В #389 это
// стоило пяти часов красного dev: пустышка в sys.modules пережила свой тест,
// Home Assistant перестал видеть у интеграции async_setup, и 85 тестов
// харнесса падали с голым `assert False`, не намекая на причину.
//
// Проверка статическая — по исходникам, без запуска. Это намеренно: рантайм
// ловит последствие и только на том порядке тестов, который сегодня сложился,
// а исходник ловит намерение.
const BACKEND = fileURLToPath(new URL('../tests_backend/', import.meta.url));
const files = () => readdirSync(BACKEND).filter((name) => name.endsWith('.py'));
const read = (name) => readFileSync(new URL(name, `file://${BACKEND}`), 'utf8');
test('backend-тесты не правят sys.path (#393)', () => {
const offenders = files().filter((name) => /^\s*sys\.path\b/m.test(read(name)));
assert.deepEqual(offenders, [],
'sys.path, изменённый при коллекции, действует на всю сессию: модули пакета'
+ ' становятся импортируемыми ещё и как модули верхнего уровня, и один файл'
+ ' может оказаться в sys.modules дважды с разными объектами классов.'
+ ' Читать модуль без Home Assistant можно и без этого — см. test_trails.py:'
+ ' текст файла и exec нужного среза.');
});
+14
View File
@@ -206,3 +206,17 @@ test('гейты диапазона судят от доказанного пр
assert.match(frontend, /\[ "\$REF" = "refs\/heads\/dev" \]/);
assert.match(frontend, /git merge-base origin\/dev "\$HEAD_SHA"/);
});
test('HA-харнесс ставится по точным версиям, а не по воле резолвера (#392)', () => {
// Плавающие версии означают, что «зелёный backend» значит разное в разные
// дни: по SHA коммита нельзя сказать, чем его проверяли. Ровно так харнесс
// полгода тихо проверял интеграцию против февральского Home Assistant.
for (const file of ['validate.yml', 'mutation-gate.yml']) {
const workflow = read(file);
if (!/pytest-homeassistant-custom-component|tests_backend\/requirements\.txt/.test(workflow)) continue;
assert.match(workflow, /pip install -r tests_backend\/requirements\.txt/,
`${file}: зависимости харнесса ставятся мимо файла пинов`);
assert.equal(/pip install pytest /.test(workflow), false,
`${file}: остался установ без версий`);
}
});
+29
View File
@@ -0,0 +1,29 @@
# Зависимости HA-харнесса, закреплённые точно (issue #392).
#
# Было так: `pip install pytest voluptuous pytest-homeassistant-custom-component
# home-assistant-frontend` без единой версии. Резолвер молча выбирал набор сам,
# и выбор зависел не от нашего коммита, а от того, что в этот день лежит на
# PyPI. По SHA нельзя было сказать, чем его проверяли.
#
# Что здесь записано — ровно то, что резолвилось на 30.08.2026, снятое из лога
# шага `pip install`. Цель этого файла не «взять новое», а «перестать дрейфовать».
#
# Знать про эти версии нужно две вещи.
#
# 1. `pytest-homeassistant-custom-component` начиная с 0.13.348 (25.07.2026)
# требует Python >= 3.14, а job стоит на 3.13. Поэтому резолвер и уезжал на
# 0.13.316 — последнюю, которая ставится на 3.13.
# 2. Она тянет `homeassistant==2026.2.3`, то есть февральский. Харнесс проверяет
# интеграцию против HA, которого у пользователей давно нет. Это известный
# долг, а не оплошность: снимать его надо переходом на 3.14 и свежий phcc,
# и это отдельная задача с отдельным измерением.
#
# Менять версии здесь можно, но только вместе с прогоном полного набора: это
# не косметика, а смена того, что означает зелёный backend.
pytest==9.0.0
voluptuous==0.15.2
pytest-homeassistant-custom-component==0.13.316
home-assistant-frontend==20260128.6
# Ставится транзитивно из phcc; закреплён явно, чтобы версия была видна тому,
# кто читает этот файл, а не только тому, кто читает лог.
homeassistant==2026.2.3
+6 -8
View File
@@ -1,13 +1,11 @@
"""TrailBook: the pure part of the server-side vacuum trails."""
import math
import sys, pathlib
sys.path.insert(0, str(pathlib.Path(__file__).parent.parent / "custom_components" / "houseplan"))
import importlib.util
spec = importlib.util.spec_from_file_location(
"trailbook_pure",
pathlib.Path(__file__).parent.parent / "custom_components" / "houseplan" / "trails.py",
)
# import only TrailBook without HA deps: read the source and exec the class
import pathlib
# TrailBook берётся без импорта: из trails.py читается текст и исполняется срез
# до класса TrailRecorder — только он тянет Home Assistant. Ни sys.path, ни
# sys.modules при этом не трогаются намеренно (#393): правка импорт-машины
# переживает свой тест и достаётся всей сессии, а чем это кончается, показал
# #389 — 85 тестов харнесса падали с голым `assert False`.
src = (pathlib.Path(__file__).parent.parent / "custom_components" / "houseplan" / "trails.py").read_text(encoding="utf-8")
ns = {"Any": object, "annotations": None, "math": math}
exec(src[src.index("TRAIL_CAP"):src.index("class TrailRecorder")], ns)