Перейти к содержанию

Разработка и тесты

Вся логика блюпринта живёт в блоке variables: и шаблонных триггерах. Это позволяет проверять её без запущенного Home Assistant: тесты загружают YAML, подставляют !input, рендерят каждую переменную по порядку против фейкового набора сущностей и проверяют результат.

pip install -r requirements-dev.txt
pytest -m "not slow"    # весь набор кроме мутаций, около 15 секунд
pytest -m slow -n auto  # мутационное тестирование, несколько минут

Из чего состоит набор

Файл Что проверяет
test_blueprint_structure.py Схема документа: все входы объявлены и использованы, дефолты в границах селекторов, порядок объявления переменных, continue_on_error на всех вызовах служб
test_current_limits.py Жёсткие границы, дискретность, сужение диапазона атрибутами сущности, холодная погода, аварийный заряд с гистерезисом, число фаз, КПД
test_voltage_sources.py Приоритет источников напряжения и отбрасывание неправдоподобных значений
test_data_validity.py Три уровня достоверности данных и их приоритет, переключение плана, поправка на здоровье батареи, распознавание накопительного счётчика
test_schedule_window.py Переход через полночь, дни недели, бюджет времени
test_location.py Зоны, приезд посреди окна, отличие сбоя GPS от отъезда
test_decisions.py Матрица «заряжать / остановить», причины остановки, инвариант их взаимоисключения
test_throttle_and_alarms.py Троттлинг команд, зона нечувствительности, контроль исправности
test_triggers.py Шаблонные триггеры, в том числе на пустом наборе сущностей
test_night_simulation.py Сквозная симуляция ночи в замкнутом контуре, включая приезд и отъезд машины, неисправность, потерю связи, мороз и watchdog
test_actions.py Команды, уходящие станции: состав, порядок, троттлинг, хуки
test_diagnostics.py Вердикт, слепок diag, записи журнала — то, по чему потом разбирают сессию
test_first_night_regression.py Первая ночь на живой установке, воспроизведённая по её собственным трассировкам
test_second_night_regression.py Вторая ночь: обрыв связи со станцией, потеря и возврат владения сессией
test_third_night_regression.py Третья ночь: потерянные станцией команды, ложная недодача тока по залипшему сенсору
test_fourth_night_regression.py Четвёртая ночь: автоколебание тока под утро, дребезг шаблонных триггеров, сброшенный накопительный счётчик
test_trace_report.py Скрипт разбора трассировок: локали, битые файлы, часовые пояса, совместимость с Python 3.9
test_mutations.py Проверка самих тестов: ловят ли они намеренно сломанный блюпринт

Регрессии реальных ночей

Отдельно стоят четыре файла с ночными регрессиями. Они не выдумывают сценарий, а берут показания из трассировок реальных ночей и закрепляют каждый дефект, который те вскрыли. Синтетический тест этих багов не поймал бы: они проявились именно на сочетании реальных значений.

Первая ночь дала недодачу тока в 1.3 А, дрейф напряжения и кривую заряда — три дефекта, из-за которых машина остановилась на 83 %. Вторая принесла двухсекундный обрыв связи, разложенный по четырём тактам восстановления: именно на нём блюпринт терял собственную сессию и семь часов считал её чужой. Третья вскрыла команды, которые станция молча теряет, и ложную «недодачу» тока по сенсору, отстающему на минуты. Четвёртая — автоколебание уставки под утро (18 → 19 → 20 → 17 А за четыре минуты), когда план растёт от одного лишь таяния окна, а процент заряда ещё не обновился.

Симуляция ночи

Самый ценный из них — test_night_simulation.py. Он моделирует батарею и прогоняет регулятор тик за тиком: выбранный ток определяет скорость наполнения, скорость наполнения определяет следующий выбор тока. Так ловятся автоколебания, недозаряд и избыточное число команд — ровно то, что не видно в тестах отдельных переменных. Именно эта симуляция обнаружила, что при большой зоне нечувствительности ток мог застрять в паре ампер от максимума и не дотянуть до цели в холодную погоду.

Мутационное тестирование

Зелёный набор тестов сам по себе ничего не доказывает — тесты бывают бессодержательными. test_mutations.py намеренно ломает блюпринт по одному месту за раз (каждая мутация соответствует обещанию из документации) и убеждается, что остальной набор это замечает. Выжившая мутация означает, что соответствующее поведение не покрыто.

Именно эта проверка нашла, что зажим тока применялся дважды и внутренний маскировал внешний — ошибка проявлялась только когда максимум не делится на дискретность, например 28 А при шаге 5 А. Она же поймала недостижимое условие в щадящем финише: проверка достоверности заряда там дублировала то, что и так гарантировано, — мутация выжила, и условие убрали.

Почему прогон долгий и как его ускорить

Каждая мутация — это отдельный запуск всего остального набора (все обычные тесты), потому что заранее неизвестно, какой именно тест должен её поймать. Семь десятков мутаций подряд дают полчаса ожидания.

Мутации независимы друг от друга, и каждый воркер работает в собственной копии репозитория, поэтому прогон безопасно распараллеливается:

pytest -m slow -n auto

На восьми ядрах это несколько минут вместо получаса. Быстрому набору воркеры не нужны — он и так укладывается в десятки секунд, а запуск воркеров стоил бы дороже выигрыша.

Тестовый движок (tests/ha_sim.py) реализует только то подмножество шаблонного API Home Assistant, которое реально использует блюпринт. Если блюпринт начнёт использовать новую функцию, её нужно добавить в движок, а не ослаблять тесты.

Две проверки настоящим Home Assistant

Обе вынесены из pytest намеренно: установка Home Assistant тянет большое дерево зависимостей, а его внутренние API не имеют гарантий стабильности. Падение здесь означает «разобраться», а не «блюпринт сломан», поэтому в CI обе помечены информационными.

pip install homeassistant
python tests/validate_with_home_assistant.py
python tests/differential_against_home_assistant.py

validate_with_home_assistant.py проверяет структуру документа: метаданные, селекторы и объявленные входы прогоняются через ту же схему, которую применяет интерфейс при импорте блюпринта. Ни одного шаблона он не рендерит.

differential_against_home_assistant.py проверяет поведение движка. Весь набор работает на ha_sim.py, то есть на переписанной реализации шаблонного API, — а переписанная реализация со временем расходится с оригиналом. Сам набор этого заметить не может: он и есть тот, кто в неё верит. Скрипт рендерит один и тот же набор шаблонов обоими движками и сверяет результаты.

Опасность здесь односторонняя. Движок строже настоящего Home Assistant стоит недоумённого часа при отладке. Движок мягче настоящего Home Assistant стоит сессии без зарядки: набор зелёный, а в проде шаблон падает. Так уже было — is_state для отсутствующей сущности отвечал True там, где Home Assistant отвечает False.

Если добавляете в ha_sim.py новую функцию, добавьте её вызовы и в список шаблонов этого скрипта — иначе у новой реализации не будет эталона.

Дальше