From eb5aa2a0f8dea5514ee2fee2dbd609457dd0fb4d Mon Sep 17 00:00:00 2001 From: Matysh Date: Sun, 30 Aug 2026 19:49:37 +0300 Subject: [PATCH] ci: pin the HA harness and stop tests from editing sys.path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Два независимых хвоста из разбора #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 --- .github/workflows/mutation-gate.yml | 7 ++++++- .github/workflows/validate.yml | 7 ++++++- test/backend-test-hygiene.test.mjs | 28 ++++++++++++++++++++++++++++ test/validate-workflow.test.mjs | 14 ++++++++++++++ tests_backend/requirements.txt | 29 +++++++++++++++++++++++++++++ tests_backend/test_trails.py | 14 ++++++-------- 6 files changed, 89 insertions(+), 10 deletions(-) create mode 100644 test/backend-test-hygiene.test.mjs create mode 100644 tests_backend/requirements.txt diff --git a/.github/workflows/mutation-gate.yml b/.github/workflows/mutation-gate.yml index 34af413c..54fc8920 100644 --- a/.github/workflows/mutation-gate.yml +++ b/.github/workflows/mutation-gate.yml @@ -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 diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 6aa6b0ff..71f094e9 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -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 # Маркер пишется последним шагом: он существует только если всё выше diff --git a/test/backend-test-hygiene.test.mjs b/test/backend-test-hygiene.test.mjs new file mode 100644 index 00000000..feca4371 --- /dev/null +++ b/test/backend-test-hygiene.test.mjs @@ -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 нужного среза.'); +}); diff --git a/test/validate-workflow.test.mjs b/test/validate-workflow.test.mjs index 47b4958f..b2a8aabf 100644 --- a/test/validate-workflow.test.mjs +++ b/test/validate-workflow.test.mjs @@ -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}: остался установ без версий`); + } +}); diff --git a/tests_backend/requirements.txt b/tests_backend/requirements.txt new file mode 100644 index 00000000..cd3261e8 --- /dev/null +++ b/tests_backend/requirements.txt @@ -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 diff --git a/tests_backend/test_trails.py b/tests_backend/test_trails.py index 608ab996..f5ea9f9c 100644 --- a/tests_backend/test_trails.py +++ b/tests_backend/test_trails.py @@ -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)