Перенести итератор версии прошивки в общий генератор
Увеличивать версию при прямом подключении precompile из CCS и Keil. Сохранять счётчик атомарно и оставлять ручной режим без включённого флага. Добавить проверки итератора и обновить примеры подключения.
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
"""Проверяет общий сохраняемый итератор версии до компиляции CCS, Keil и CLI.""" # Запуск: python -m unittest discover -s c/firmware-info/tests -p test_version_iterator.py.
|
||||
import importlib.util # Загружает тот же генератор, который вызывается штатными BAT templates.
|
||||
import json # Проверяет исходный JSON и метаданные после резервирования версии.
|
||||
from pathlib import Path # Изолирует проверочные настройки и generated во временном каталоге.
|
||||
import tempfile # Удаляет только собственные файлы после каждой проверки.
|
||||
import unittest # Проверяет последовательность номеров, ручной режим и отказы записи.
|
||||
from unittest.mock import patch # Воспроизводит ошибки файловой системы без изменения прав пользователя.
|
||||
TOOL = Path(__file__).resolve().parents[1] / "tools/identity.py" # Находит общую реализацию относительно файла теста.
|
||||
spec = importlib.util.spec_from_file_location("shared_iterator", TOOL) # Не импортирует проектные обёртки или другой генератор из PATH.
|
||||
identity = importlib.util.module_from_spec(spec) # Создаёт отдельный экземпляр модуля для изолированной подмены отказов.
|
||||
spec.loader.exec_module(identity) # Загружает функции без выполнения CLI и без изменения рабочих версий.
|
||||
|
||||
|
||||
class VersionIteratorTests(unittest.TestCase): # Проверяет поведение общего prepare независимо от IDE.
|
||||
def setUp(self): # Готовит настройки другого изделия только во временном каталоге.
|
||||
temporary = tempfile.TemporaryDirectory(prefix="shared iterator ") # Пробелы в пути не должны влиять на атомарную запись настроек.
|
||||
self.addCleanup(temporary.cleanup) # Удаляет временные файлы даже после ожидаемого отказа генерации.
|
||||
self.root = Path(temporary.name) # Ограничивает все изменения проверки собственным каталогом.
|
||||
self.config = self.root / "config.json" # Отделяет исходную настройку от generated/firmware_identity.json.
|
||||
self.output = self.root / "generated" # Моделирует отдельный каталог текущей конфигурации сборки.
|
||||
self.settings = {"product": "Другое изделие", "target": "host", "version": [1, 0, 4], "increment_version": True, "extra": {"keep": "настройка"}} # Дополнительное поле проверяет сохранность проектных настроек в UTF-8.
|
||||
self.save() # Устанавливает номер, на котором обнаружилась остановка прямого подключения.
|
||||
|
||||
def save(self): # Сохраняет выбранные входные настройки для текущего сценария.
|
||||
self.config.write_text(json.dumps(self.settings, ensure_ascii=False), encoding="utf-8") # Не использует рабочий счётчик какого-либо проекта.
|
||||
|
||||
def prepare(self): # Запускает публичный общий API с детерминированным состоянием Git.
|
||||
with patch.object(identity, "git_state", return_value={"commit": None, "dirty": None}): # Доступ к Git не влияет на проверку счётчика.
|
||||
return identity.prepare(self.root, self.root, self.config, self.output) # Вызывает тот же prepare, что CCS, Keil и CLI.
|
||||
|
||||
def test_sequence_and_matching_outputs(self): # Воспроизводит увеличение после 1.0.4 и отсутствие повторного номера.
|
||||
for expected in ([1, 0, 5], [1, 0, 6]): # Каждый общий pre-build должен занимать ровно один номер.
|
||||
metadata = self.prepare() # Генерирует реальный заголовок и JSON после атомарного резервирования.
|
||||
settings = json.loads(self.config.read_text(encoding="utf-8")) # Читает сохранённый источник следующей версии.
|
||||
self.assertEqual(settings["version"], expected) # Проверяет увеличение исходного JSON, а не только отображаемой версии.
|
||||
self.assertEqual(settings["extra"], self.settings["extra"]) # Сохраняет неизвестные общему генератору поля проекта.
|
||||
self.assertEqual(metadata["version"], expected) # Возвращаемые метаданные содержат уже занятый номер.
|
||||
self.assertEqual(json.loads((self.output / "firmware_identity.json").read_text(encoding="utf-8"))["version"], expected) # Сохранённый паспорт совпадает с источником версии.
|
||||
self.assertIn(f"#define FIRMWARE_VERSION_PATCH {expected[2]}U", (self.output / "firmware_identity.h").read_text(encoding="utf-8")) # Компилятор получает тот же patch, что manifest.
|
||||
|
||||
def test_manual_mode_preserves_config_bytes(self): # Старые проекты без флага и явно отключённый итератор сохраняют ручную версию.
|
||||
for enabled in (None, False): # None обозначает отсутствие поля, а не допустимое значение JSON-флага.
|
||||
self.settings.pop("increment_version", None) # Сначала моделирует старую конфигурацию без нового переключателя.
|
||||
if enabled is False: self.settings["increment_version"] = False # Затем проверяет явное отключение без удаления поля.
|
||||
self.save() # Записывает исходный формат, который генератор не должен переформатировать.
|
||||
original = self.config.read_bytes() # Сравнивает байты, чтобы заметить любую лишнюю запись.
|
||||
self.assertEqual(self.prepare()["version"], [1, 0, 4]) # Ручной номер используется без увеличения.
|
||||
self.assertEqual(self.config.read_bytes(), original) # Исходный JSON остаётся побайтно прежним.
|
||||
|
||||
def test_carry_and_exhaustion(self): # Проверяет ограничения трёх разрядов контракта firmware-info v1.
|
||||
for previous, expected in (([1, 0, 999], [1, 1, 0]), ([1, 255, 999], [2, 0, 0])): # Patch переносится после 999, minor после 255.
|
||||
self.settings["version"] = previous # Задаёт последнюю версию перед границей разряда.
|
||||
self.save() # Передаёт граничное значение публичному prepare через JSON.
|
||||
self.assertEqual(self.prepare()["version"], expected) # Проверяет перенос вместе с реальной записью настроек.
|
||||
self.settings["version"] = [255, 255, 999] # Устанавливает последний допустимый номер всего контракта.
|
||||
self.save() # Сохраняет исходный предел перед ожидаемым отказом.
|
||||
original = self.config.read_bytes() # Переполнение не должно сбрасывать номер в JSON.
|
||||
with self.assertRaises(ValueError): self.prepare() # Требует явную ошибку вместо повторного использования версии.
|
||||
self.assertEqual(self.config.read_bytes(), original) # Сохраняет последний занятый номер без частичной записи.
|
||||
|
||||
def test_invalid_flag_does_not_change_config(self): # Не допускает неявное включение счётчика строками, числами или null.
|
||||
for value in ("true", "false", 1, 0, None, [], {}): # Только JSON true и false являются допустимыми значениями переключателя.
|
||||
self.settings["increment_version"] = value # Передаёт неверный тип через настоящий JSON.
|
||||
self.save() # Фиксирует исходные байты для проверки отказа до резервирования.
|
||||
original = self.config.read_bytes() # Ошибка настройки не должна занимать номер версии.
|
||||
with self.assertRaises(ValueError): self.prepare() # Останавливает генерацию до изменения счётчика.
|
||||
self.assertEqual(self.config.read_bytes(), original) # Подтверждает сохранение исходного JSON при отказе проверки.
|
||||
|
||||
def test_generation_failure_consumes_number(self): # Номер неудачной попытки не должен использоваться следующей сборкой.
|
||||
with patch.object(identity, "write_atomic", side_effect=OSError("header unavailable")): # Воспроизводит отказ записи заголовка после сохранения следующей версии.
|
||||
with self.assertRaises(OSError): self.prepare() # Передаёт ошибку вызывающему BAT без маскировки успешным завершением.
|
||||
self.assertEqual(json.loads(self.config.read_text(encoding="utf-8"))["version"], [1, 0, 5]) # Резервирование фиксируется до генерации выходных файлов.
|
||||
self.assertEqual(self.prepare()["version"], [1, 0, 6]) # Следующая попытка занимает следующий номер, даже если прежняя не дошла до компиляции.
|
||||
|
||||
def test_failed_config_replace_preserves_original(self): # Атомарная замена не должна портить настройки при отказе файловой системы.
|
||||
original = self.config.read_bytes() # Сохраняет эталон исходного номера и остальных полей.
|
||||
with patch.object(identity.Path, "replace", side_effect=PermissionError("config unavailable")): # Имитирует отказ фиксации временного файла вместо записи нового JSON.
|
||||
with self.assertRaises(PermissionError): self.prepare() # Останавливает pre-build, если номер нельзя надёжно сохранить.
|
||||
self.assertEqual(self.config.read_bytes(), original) # Исходные настройки не заменены частичным или неверным файлом.
|
||||
self.assertEqual(list(self.root.glob(".firmware-version-*")), []) # При отказе не остаются собственные временные файлы счётчика.
|
||||
|
||||
|
||||
if __name__ == "__main__": unittest.main() # Любое нарушение общего итератора возвращает ненулевой код процесса.
|
||||
Reference in New Issue
Block a user