Добавить защищённое обновление приложения через RS и CAN с сохранением загрузчика, проверкой CRC, восстановлением передачи и защитой EEPROM. Подключить общий процесс сборки C2000 и автоматический счётчик версии для согласованных RAM, Flash, CAN и паспортов бинарных образов. Уточнить запуск ЦАП, обработку измерений и параметров нагрузки, начальную индикацию и документацию регистров GAS. Дополнить модели аппаратуры и изолировать заголовки CAN в тестах. Проверка: 26 тестов выпуска, 4 теста восстановления CAN и восемь программных моделей CAN, Flash, EEPROM, ЦАП, параметров и TESTLAMP.
344 lines
62 KiB
Python
344 lines
62 KiB
Python
"""Проверяет выпуск 167: старый OUT, EEPROM, обрезанный SCI8 и отсутствие публикации при отказе.""" # Использует производственные проверки и настоящий TI-конвертор при доступном toolchain.
|
||
from datetime import datetime # Независимо восстанавливает регистры даты для контрольного образа.
|
||
import hashlib # Сравнивает постоянные каталоги до и после отклонённого выпуска.
|
||
import importlib.util # Загружает существующий общий seal/archive без изменения templates.
|
||
import json # Создаёт минимальный проект и проверяет поля конечного manifest.
|
||
import os # Позволяет указать установленный TI toolchain переменной TI_CGT_ROOT.
|
||
from pathlib import Path # Изолирует все временные файлы от настоящих generated и Bin проекта.
|
||
import struct # Формирует независимые контрольные SCI8-блоки согласно формату загрузчика TI.
|
||
import subprocess # Компилирует минимальный настоящий COFF для проверки интеграции с hex2000.
|
||
import sys # Подключает локальные prebuild/postbuild из каталога CCS-проекта.
|
||
import tempfile # Удаляет только временные каталоги этой проверки после завершения тестов.
|
||
import unittest # Группирует успешные и отказные сценарии с отдельными результатами.
|
||
from unittest.mock import patch # Подменяет только конверсию для испытания повреждённых результатов инструментов.
|
||
|
||
ROOT = Path(__file__).resolve().parents[2] # Находит исходники проверяемой 167 независимо от рабочего каталога запуска.
|
||
sys.path.insert(0, str(ROOT / "UKSSTMS320F28335")) # Импортирует производственные функции без запуска их CLI.
|
||
import prepare_identity as prebuild # Проверяет фактическую генерацию ручной версии и физического профиля.
|
||
import publish_identity as release # Вызывает именно обязательный защищённый postbuild проекта.
|
||
VENDOR = ROOT.parent / "newProject/SETGUI/third_party/templates" # Совпадает с include paths обеих конфигураций CCS.
|
||
TOOLS = ROOT.parent / "newProject/templates" # Полная рабочая библиотека содержит общий BAT-конвертор и архивирование.
|
||
COMPILER = Path(os.environ.get("TI_CGT_ROOT", "C:/ti/ccs1230/ccs/tools/compiler/ti-cgt-c2000_22.6.0.LTS")) # Проверяет установленный TI без загрузки нового toolchain.
|
||
SPEC = importlib.util.spec_from_file_location("test_common_identity", TOOLS / "c/firmware-info/tools/identity.py") # Подключает тот же seal/archive, который вызывает postcompile.
|
||
IDENTITY = importlib.util.module_from_spec(SPEC) # Отделяет общие инструменты тестов от производственного prebuild.
|
||
SPEC.loader.exec_module(IDENTITY) # Неполная зависимость явно останавливает тест вместо пропуска проверок выпуска.
|
||
|
||
|
||
def identity_words(metadata): # Независимый контрольный вектор проверяет общий 12-словный контракт, а не копирует поиск production-кода.
|
||
stamp = datetime.fromisoformat(metadata["built_at_utc"]) # Использует фиксированное время того же тестового prebuild.
|
||
result = [1, *metadata["version"], stamp.year, stamp.month * 256 + stamp.day, stamp.hour * 256 + stamp.minute, stamp.second] # Первые восемь слов содержат формат, ручную версию и UTC.
|
||
result += [ord(metadata["build_id"][index]) * 256 + ord(metadata["build_id"][index + 1]) for index in range(0, 8, 2)] # Последние четыре слова хранят ASCII Build ID в сетевом порядке.
|
||
return result # Возвращает числа для независимой SCI8-таблицы и настоящего COFF-фикстура.
|
||
|
||
|
||
def block(address, words): # Строит минимальный загрузочный блок с адресом в словах C28x.
|
||
return struct.pack("<HHH", len(words), address >> 16, address & 65535) + struct.pack("<" + "H" * len(words), *words) # Поля длины, старшего/младшего адресов и данных соответствуют документации SCI8.
|
||
|
||
|
||
def image_for(metadata): # Формирует два непересекающихся блока RAM с загруженной точкой входа и неизменяемым паспортом.
|
||
return b"\xaa\x08" + bytes(16) + struct.pack("<HH", 0, 0x8000) + block(0x8000, [0x761F, 0x8000]) + block(0x8100, identity_words(metadata)) + bytes(2) # Заголовок занимает 22 октета, нулевая длина завершает таблицу.
|
||
|
||
|
||
def snapshot(path): # Доказывает сохранность прежних выпусков и каталога GUI при любом отказе валидации.
|
||
return {str(file.relative_to(path)): hashlib.sha256(file.read_bytes()).hexdigest() for file in path.rglob("*") if file.is_file()} if path.exists() else {} # Сравнивает имена и точные байты всех постоянных файлов.
|
||
|
||
|
||
class ReleaseTests(unittest.TestCase): # Испытывает реальную последовательность prebuild, проверок и публикации.
|
||
def setUp(self): # Каждый сценарий получает собственные project/generated/output/export.
|
||
self.temporary = tempfile.TemporaryDirectory(prefix="balsam-release-test-") # Временные результаты не попадают в настоящее дерево Bin.
|
||
self.directory = Path(self.temporary.name) # Задаёт корень только текущего тестового проекта.
|
||
self.project = self.directory / "Project" # Сохраняет отношение project.parent/Source, используемое настоящим prebuild.
|
||
self.project.mkdir() # Создаёт отдельный каталог настроек CCS.
|
||
policy = json.loads((ROOT / "UKSSTMS320F28335/firmware_build_profile.json").read_text(encoding="utf-8")) # Сохраняет фактический аппаратный профиль рабочей платы для всех сценариев выпуска.
|
||
policy["version_header"] = "firmware_version.h" # Существующие проверки выпуска дополнительно сохраняют совместимость ручного режима других проектов; автоматические сценарии явно отключают его.
|
||
(self.project / "firmware_build_profile.json").write_text(json.dumps(policy), encoding="utf-8") # Изолирует выбор источника версии в настройках временного проекта.
|
||
self.build = self.project / "Debug" # Использует штатное соглашение выбранной конфигурации.
|
||
self.build.mkdir() # Позволяет postbuild изолировать конверсию внутри этой конфигурации.
|
||
self.config = self.directory / "Source/Internal/Include/eeprom_config.h" # Отделяет выбор физической EEPROM от настоящей платы пользователя.
|
||
self.config.parent.mkdir(parents=True) # Создаёт только тестовую иерархию Source.
|
||
self.config.write_text("#define EEPROM_M95M02 0 /* Тестовая плата с AT25256B. */\n", encoding="utf-8") # Начальный профиль соответствует совместимой малой EEPROM 167.
|
||
self.version_text = "#define FIRMWARE_VERSION_MAJOR 1U /* Основной выпуск. */\n#define FIRMWARE_VERSION_MINOR 1U /* Дополнительный выпуск. */\n#define FIRMWARE_VERSION_PATCH 2U /* Ручной номер исправления. */\n" # Весь тест сохраняет пользовательский выпуск 1.1.2.
|
||
(self.project / "firmware_version.h").write_text(self.version_text, encoding="utf-8") # Источником версии остаётся C-заголовок.
|
||
self.settings = {"product": "Balsam-167-PM35", "target": "TMS320F28335", "increment_version": True} # Старый автоинкремент не должен преодолеть новый ручной контракт.
|
||
(self.project / "firmware_identity.json").write_text(json.dumps(self.settings), encoding="utf-8") # Проверяет совместимость со случайно сохранённым старым полем JSON.
|
||
self.output, self.export = self.directory / "Published", self.directory / "Gui" # Выбирает отдельные постоянные назначения для проверки порядка публикации.
|
||
self.metadata = prebuild.prepare(self.project, VENDOR, self.build / "generated") # Выполняет настоящий prebuild с фактической компилируемой библиотекой.
|
||
self.map_path = self.build / "application.map" # Каждая попытка обязана представить карту активного линкера.
|
||
self.write_map(self.metadata) # Начальные абсолютные символы согласованы с текущей конфигурацией платы.
|
||
|
||
def tearDown(self): # Завершение теста не затрагивает настоящее состояние Git или прошивки.
|
||
self.temporary.cleanup() # Удаляет только созданный TemporaryDirectory после закрытия всех файлов.
|
||
|
||
def write_map(self, metadata): # Формирует формат TI MAP для испытания аппаратной согласованности до конверсии.
|
||
self.map_path.write_text(f"abs {metadata['eeprom_bytes']:08x} _EEPROM_TOTAL_BYTES\nabs {metadata['max_image_bytes']:08x} _EEPROM_FIRMWARE_LIMIT_BYTES\nabs {metadata['max_image_bytes'] // 2:08x} _EEPROM_PARAMS_START_WORD\n", encoding="utf-8") # Повторяет реальные абсолютные символы F28335.cmd с единицами октеты/слова.
|
||
if metadata.get("can_boot_profile"): # Только профиль M95M02 A/B должен доказывать наличие прикладной интеграции CAN.
|
||
with self.map_path.open("a", encoding="utf-8") as stream: # Дополняет только временную MAP, не меняя реальные символы проекта.
|
||
for name in ("_F28335CanBoot_Init", "_F28335CanBoot_Ready", "_F28335CanBoot_Service", "_F28335CanBoot_RxISR"): # Контрольные hooks представлены экспортированными символами, как в настоящей карте TI.
|
||
stream.write(f"0 00008000 {name}\n") # Адрес 0x8000 действительно присутствует в payload независимого SCI8-фикстура image_for.
|
||
|
||
def invoke(self, **arguments): # Передаёт стандартные пути в реальный postbuild, сохраняя возможность выбора EEPROM.
|
||
return release.publish(self.project, VENDOR, TOOLS, COMPILER, self.build, "application", self.map_path, self.output, self.export, **arguments) # Обязательные проверки и копирование выполняет production-код.
|
||
|
||
def converter(self, payload=None, wrong_eeprom=False): # Имитирует только результат инструмента, оставляя seal/archive и все проверки настоящими.
|
||
def produce(compiler, build_dir, artifact, metadata_path, stage, tools_templates): # Получает временное назначение так же, как общий postcompile.
|
||
image = stage / (artifact + "_" + self.metadata["build_id"] + ".bin") # Создаёт имя только текущей попытки выпуска.
|
||
image.write_bytes(payload if payload is not None else image_for(self.metadata)) # Позволяет испытать обрезанный, старый и слишком большой BIN после успешной конверсии.
|
||
IDENTITY.seal(image, metadata_path) # Создаёт настоящий SHA/manifest, чтобы проверки содержимого не подменялись проверкой хеша.
|
||
IDENTITY.archive_build(image, metadata_path, stage) # Формирует настоящий архив только во временном каталоге без export.
|
||
if wrong_eeprom: # Повреждённый аппаратный паспорт должен отклоняться даже при правильном хеше файла.
|
||
manifest_path = image.with_suffix(".bin.manifest.json") # Изменяет лишь паспорт текущего staged-BIN.
|
||
manifest = json.loads(manifest_path.read_text(encoding="utf-8")) # Сохраняет остальные корректные поля, включая SHA и версию.
|
||
manifest["eeprom"] = "M95M02" if manifest["eeprom"] == "AT25256B" else "AT25256B" # Переключает ровно один аппаратный признак для адресной проверки.
|
||
manifest_path.write_text(json.dumps(manifest), encoding="utf-8") # Оставляет целый JSON с намеренно неверным профилем.
|
||
return produce # Подмена ограничивается внешним инструментом конверсии; проверки выпуска остаются производственными.
|
||
|
||
def assert_rejected_without_publication(self, converter, **arguments): # Проверяет не только отказ, но и сохранность уже выпущенных файлов.
|
||
self.output.mkdir(exist_ok=True) # Моделирует существующий успешный выпуск до неудачной новой попытки.
|
||
self.export.mkdir(exist_ok=True) # Моделирует каталог GUI с ранее проверенными файлами.
|
||
(self.output / "previous.bin").write_bytes(b"previous verified release") # Контрольный файл должен остаться побайтно прежним.
|
||
(self.export / "previous.bin").write_bytes(b"previous GUI release") # Ошибка не должна обновить внешнее приложение.
|
||
before = snapshot(self.output), snapshot(self.export) # Фиксирует полный состав обоих постоянных каталогов.
|
||
with patch.object(release, "convert", converter), self.assertRaises((ValueError, OSError)): # Подмена внешнего инструмента не ослабляет настоящие проверки SCI8 и паспорта.
|
||
self.invoke(**arguments) # Ошибочная попытка обязана завершиться до копирования staged-файлов.
|
||
self.assertEqual(before, (snapshot(self.output), snapshot(self.export))) # Новые архивы и BIN не появляются ни локально, ни в GUI.
|
||
|
||
def test_manual_version_and_compiled_templates(self): # Проверяет исходный дефект происхождения зависимости и ручной номер выпуска.
|
||
self.assertEqual(self.metadata["version"], [1, 1, 2]) # Старый increment_version=true не увеличивает версию из заголовка.
|
||
self.assertEqual((self.project / "firmware_version.h").read_text(encoding="utf-8"), self.version_text) # Prebuild не редактирует исходный C-заголовок.
|
||
self.assertEqual(json.loads((self.project / "firmware_identity.json").read_text()), self.settings) # Старый JSON также остаётся пользовательской настройкой без скрытой записи.
|
||
self.assertEqual(self.metadata["templates_root"], str(VENDOR.resolve())) # Паспорт привязан к компилируемой submodule-копии, а не другой checkout инструментов.
|
||
self.assertEqual(self.metadata["templates"], prebuild.git_state(VENDOR)) # Full SHA и dirty соответствуют именно фактической зависимости C.
|
||
|
||
def enable_auto_version(self, version=(1, 1, 2)): # Переводит только временный проект на тот же сохраняемый счётчик, который включён в настоящем проекте.
|
||
policy_path = self.project / "firmware_build_profile.json" # Настройка источника версии принадлежит конкретному CCS-проекту.
|
||
policy = json.loads(policy_path.read_text(encoding="utf-8")) # Сохраняет аппаратные поля существующего тестового профиля.
|
||
policy.pop("version_header", None) # Отсутствие ручного источника включает стандартный JSON-режим и допускает повторную настройку контрольного номера в одном тесте.
|
||
policy_path.write_text(json.dumps(policy), encoding="utf-8") # Изменяет только временный профиль, не настройки рабочей платы.
|
||
self.settings.update(version=list(version), increment_version=True, extra={"keep": "настройка"}) # Начальный номер и дополнительное поле проверяют увеличение без потери пользовательских настроек.
|
||
(self.project / "firmware_identity.json").write_text(json.dumps(self.settings, ensure_ascii=False), encoding="utf-8") # Создаёт постоянный источник номера для нескольких проверочных попыток.
|
||
(self.project / "firmware_version.h").write_bytes((ROOT / "UKSSTMS320F28335/firmware_version.h").read_bytes()) # Проверяет настоящий интерфейс версии, который подключает сгенерированные константы.
|
||
|
||
def test_real_project_enables_json_iterator(self): # Проверяет включение механизма в рабочем проекте, а не только поведение временной фикстуры.
|
||
settings = json.loads((ROOT / "UKSSTMS320F28335/firmware_identity.json").read_text(encoding="utf-8")) # Читает реальные настройки без резервирования номера сборки.
|
||
policy = json.loads((ROOT / "UKSSTMS320F28335/firmware_build_profile.json").read_text(encoding="utf-8")) # Проверяет, что старый ручной источник больше не подавляет итератор.
|
||
self.assertIs(settings["increment_version"], True) # Автоматическое увеличение требует настоящего логического true.
|
||
self.assertNotIn("version_header", policy) # Prebuild должен сохранять новый номер в исходном JSON проекта.
|
||
self.assertEqual(len(settings["version"]), 3) # Сохраняемый счётчик содержит major, minor и patch общего контракта.
|
||
|
||
def test_auto_version_sequence_in_debug_and_release(self): # Каждый основной prebuild должен резервировать ровно один номер независимо от конфигурации CCS.
|
||
self.enable_auto_version() # Отправной номер 1.1.2 совпадает с последним ручным выпуском проекта.
|
||
for configuration, expected in (("Debug", [1, 1, 3]), ("Release", [1, 1, 4])): # Последовательные сборки разных конфигураций используют один сохраняемый счётчик.
|
||
with self.subTest(configuration=configuration): # Различает ошибки увеличения Debug и Release в отчёте.
|
||
generated = self.directory / configuration / "generated" # Каждая конфигурация получает собственный заголовок и паспорт.
|
||
metadata = prebuild.prepare(self.project, VENDOR, generated) # Запускает производственный адаптер и подключённую библиотеку templates без подмены итератора.
|
||
saved = json.loads((self.project / "firmware_identity.json").read_text(encoding="utf-8")) # Считывает источник следующего номера после завершения подготовки.
|
||
self.assertEqual((metadata["version"], saved["version"]), (expected, expected)) # Не допускает разного номера в сохраняемом счётчике и паспорте текущей сборки.
|
||
self.assertEqual(saved["extra"], self.settings["extra"]) # Атомарная перезапись сохраняет посторонние поля и русский UTF-8 текст.
|
||
header = (generated / "firmware_identity.h").read_text(encoding="utf-8") # Проверяет константы, которые действительно подключаются к коду прошивки.
|
||
for part, value in zip(("MAJOR", "MINOR", "PATCH"), expected): # Контракт C и JSON должен совпадать по всем трём разрядам.
|
||
self.assertIn(f"#define FIRMWARE_VERSION_{part} {value}U", header) # Проверяет подготовленные для компилятора числовые определения версии.
|
||
self.assertEqual(json.loads((generated / "firmware_identity.json").read_text(encoding="utf-8"))["version"], expected) # Сопроводительный паспорт относится к тому же номеру.
|
||
|
||
def test_auto_version_is_not_incremented_by_postbuild(self): # EEPROM, Flash и упаковка одной попытки не должны резервировать дополнительные номера.
|
||
self.enable_auto_version() # Настраивает JSON-итератор настоящего проекта вместо ручного режима фикстуры.
|
||
self.metadata = prebuild.prepare(self.project, VENDOR, self.build / "generated") # Единственный основной prebuild резервирует 1.1.3.
|
||
self.write_map(self.metadata) # Карта соответствует аппаратному профилю подготовленного паспорта.
|
||
saved = (self.project / "firmware_identity.json").read_bytes() # Фиксирует счётчик перед конверсией и публикацией.
|
||
with patch.object(release, "convert", self.converter()): # Подменяет только внешний конвертор; проверки встроенной версии и выпуск паспортов остаются настоящими.
|
||
image = self.invoke() # Полный postbuild должен опубликовать BIN текущей попытки без нового prebuild.
|
||
manifest = json.loads(image.with_suffix(".bin.manifest.json").read_text(encoding="utf-8")) # Читает паспорт реально выпущенного контрольного BIN.
|
||
self.assertEqual(manifest["version"], [1, 1, 3]) # Подписанная версия совпадает со словами внутри SCI8, проверенными production-процессом.
|
||
self.assertEqual((self.project / "firmware_identity.json").read_bytes(), saved) # Postbuild не меняет сохранённый источник номера.
|
||
|
||
def test_auto_version_failed_generation_reserves_number(self): # Следующая попытка не повторяет номер после ошибки формирования заголовка или будущей компиляции.
|
||
self.enable_auto_version() # Начинает с 1.1.2, не меняя рабочий счётчик настоящего проекта.
|
||
generator = prebuild.load_identity(VENDOR) # Загружает фактический генератор компилируемой зависимости для изолированной имитации отказа.
|
||
with patch.object(prebuild, "load_identity", return_value=generator), patch.object(generator, "write_atomic", side_effect=OSError("generated unavailable")): # Прерывает подготовку уже после атомарного сохранения номера 1.1.3.
|
||
with self.assertRaises(OSError): # Ошибка должна остановить вызывающую сборку, сохранив занятую версию.
|
||
prebuild.prepare(self.project, VENDOR, self.build / "generated") # Производственный prebuild не скрывает отказ создания выходов.
|
||
self.assertEqual(json.loads((self.project / "firmware_identity.json").read_text(encoding="utf-8"))["version"], [1, 1, 3]) # Неудачная попытка занимает свой номер, как в ветке 166.
|
||
self.assertEqual(prebuild.prepare(self.project, VENDOR, self.build / "generated")["version"], [1, 1, 4]) # Следующая подготовка использует новый номер вместо повторного 1.1.3.
|
||
|
||
def test_auto_version_rollover_and_exhaustion(self): # Пределы общего 12-словного контракта должны сохраняться после включения счётчика.
|
||
for initial, expected in (((1, 1, 999), [1, 2, 0]), ((1, 255, 999), [2, 0, 0])): # Patch переносится после 999, minor после 255; major также ограничен 255.
|
||
with self.subTest(initial=initial): # Различает два уровня переноса в отчёте проверки.
|
||
self.enable_auto_version(initial) # Устанавливает контрольный номер только во временном исходном JSON.
|
||
self.assertEqual(prebuild.prepare(self.project, VENDOR, self.build / "generated")["version"], expected) # Проверяет перенос через производственный prebuild, а не отдельную копию алгоритма.
|
||
self.enable_auto_version((255, 255, 999)) # Задаёт наибольшую представимую версию firmware-info v1.
|
||
original = (self.project / "firmware_identity.json").read_bytes() # Полное переполнение не должно обнулить или повредить счётчик.
|
||
with self.assertRaisesRegex(ValueError, "exhausted"): # Требует явного отказа вместо повторного использования версии 0.0.0.
|
||
prebuild.prepare(self.project, VENDOR, self.build / "generated") # Не допускает начало компиляции за пределами контракта версии.
|
||
self.assertEqual((self.project / "firmware_identity.json").read_bytes(), original) # Исходная максимальная версия остаётся доступной для диагностики.
|
||
|
||
def test_auto_version_failed_save_preserves_source(self): # Отказ фиксации счётчика должен оставлять исходный JSON целым.
|
||
self.enable_auto_version() # Включает автоматический режим только у временного проекта.
|
||
original = (self.project / "firmware_identity.json").read_bytes() # Сохраняет точные байты исходных настроек до ожидаемого отказа.
|
||
with patch.object(Path, "replace", side_effect=PermissionError("counter unavailable")): # Имитирует невозможность атомарно заменить JSON до генерации паспорта.
|
||
with self.assertRaises(PermissionError): # Не разрешает компиляцию с номером, который не удалось надёжно занять.
|
||
prebuild.prepare(self.project, VENDOR, self.build / "generated") # Вызывает настоящий порядок чтения профиля и резервирования версии.
|
||
self.assertEqual((self.project / "firmware_identity.json").read_bytes(), original) # Ошибка файловой системы не оставляет частично записанные настройки.
|
||
self.assertEqual(list(self.project.glob(".firmware-version-*")), []) # Собственный временный файл счётчика удаляется после неудачной замены.
|
||
|
||
@unittest.skipUnless((COMPILER / "bin/cl2000.exe").is_file(), "TI C2000 compiler is unavailable") # Сверяет реальную C-компиляцию при доступном установленном toolchain.
|
||
def test_auto_version_is_shared_with_can_and_flash(self): # Ручной интерфейс версии не должен оставлять CAN или Flash на старом номере после prebuild.
|
||
self.enable_auto_version() # Копирует production firmware_version.h, подключающий единственный generated-заголовок.
|
||
self.config.write_text("#define EEPROM_M95M02 1 /* Выбирает M95M02 для проверки включённого CAN A/B. */\n", encoding="utf-8") # Активирует аппаратную ветку, использующую номер прошивки в ProtoCAN.
|
||
(self.config.parent / "f28335_can_application_config.h").write_bytes((ROOT / "Source/Internal/Include/f28335_can_application_config.h").read_bytes()) # Проверяет реальную упаковку версии CAN с изолированной EEPROM-конфигурацией.
|
||
self.metadata = prebuild.prepare(self.project, VENDOR, self.build / "generated") # Подготавливает единую версию 1.1.3 для всех вариантов текущей попытки.
|
||
source = self.build / "version_probe.c" # Малый исходник проверяет интерфейс версии без записи платы и без компиляции всего приложения.
|
||
source.write_text('#include "f28335_can_application_config.h" /* Подключает production-формулу версии ProtoCAN и generated-паспорт. */\n#if FIRMWARE_VERSION_MAJOR != 1 || FIRMWARE_VERSION_MINOR != 1 || FIRMWARE_VERSION_PATCH != 3 /* Требует номер этой попытки 1.1.3. */\n#error Version_does_not_match_prebuild /* Останавливает компиляцию при старом ручном заголовке. */\n#endif /* Завершает сверку трёх разрядов версии. */\ntypedef char can_version_matches[(F28335_CAN_BOOT_FIRMWARE_VERSION == 0x01010003UL) ? 1 : -1]; /* 0x01:major, 0x01:minor, 0x0003:patch повторяют 32-битный контракт ProtoCAN. */\n', encoding="utf-8") # Проверяет номер заголовка и поле FW настоящим компилятором C28x.
|
||
subprocess.run([str(COMPILER / "bin/cl2000.exe"), "-v28", "--abi=coffabi", "-c", "--include_path=" + str(self.config.parent), "--include_path=" + str(self.project), "--include_path=" + str(self.build / "generated"), "--output_file=" + str(self.build / "version_probe.obj"), str(source)], check=True, capture_output=True) # Реальная компиляция подтверждает отсутствие несовпадения CAN и паспорта.
|
||
import build_flash # Загружает реальный дополнительный сборщик, который не должен резервировать второй номер.
|
||
saved = (self.project / "firmware_identity.json").read_bytes() # Фиксирует счётчик перед проверкой Flash-конфигурации.
|
||
with patch.object(build_flash, "PROJECT", self.project), patch.object(build_flash, "configuration_paths", side_effect=RuntimeError("version accepted")): # Останавливает сборщик сразу после реальной проверки версии и EEPROM, до дорогостоящей компиляции приложения.
|
||
with self.assertRaisesRegex(RuntimeError, "version accepted"): # Положительный результат доказывает, что ручное чтение старого C-заголовка больше не применяется.
|
||
build_flash.build_flash(COMPILER, self.build, self.build / "flash.out") # Flash получает паспорт основной попытки и проходит проверку JSON-версии.
|
||
self.assertEqual((self.project / "firmware_identity.json").read_bytes(), saved) # Проверка и подготовка Flash не увеличивают счётчик повторно.
|
||
|
||
def test_success_for_both_eeprom_profiles(self): # Проверяет полноценные publish/archive/export для обеих физических ёмкостей.
|
||
for eeprom, limit in (("AT25256B", 32508), ("M95M02", 261884)): # Пределы включают исторический резерв 129 параметров и последнее свободное слово.
|
||
with self.subTest(eeprom=eeprom): # Различает ошибки аппаратных вариантов в отчёте тестов.
|
||
self.metadata = prebuild.prepare(self.project, VENDOR, self.build / "generated", eeprom) # CLI-профиль также попадёт в паспорт до компиляции.
|
||
self.write_map(self.metadata) # Моделирует согласованный define выбранного линкера.
|
||
with patch.object(release, "convert", self.converter()): # Имитирует только внешний инструмент и использует настоящие проверки staged-комплекта.
|
||
image = self.invoke(eeprom=eeprom) # Успешный выпуск обязан завершить локальное и GUI-копирование.
|
||
manifest = json.loads(image.with_suffix(".bin.manifest.json").read_text(encoding="utf-8")) # Проверяет опубликованный, а не промежуточный паспорт.
|
||
self.assertEqual((manifest["eeprom"], manifest["max_image_bytes"], manifest["version"]), (eeprom, limit, [1, 1, 2])) # Связывает аппаратную ёмкость, границу и неизменённый номер версии.
|
||
self.assertTrue(any(self.export.rglob(image.name))) # Каталог GUI получает только проверенный архив текущей сборки.
|
||
|
||
def test_stale_embedded_identity(self): # Воспроизводит опасную подпись старого OUT новым manifest при правильном SHA.
|
||
stale = dict(self.metadata, version=[1, 1, 1]) # Единственным дефектом образа остаётся предыдущий номер встроенного паспорта.
|
||
self.assert_rejected_without_publication(self.converter(image_for(stale))) # Проверка содержимого обязана остановить публикацию до export.
|
||
|
||
def test_oversize_image(self): # Воспроизводит выход нового кода за начало сохраняемых параметров малой EEPROM.
|
||
self.assert_rejected_without_publication(self.converter(image_for(self.metadata) + bytes(32508))) # Даже корректно подписанный SHA большого файла не разрешает выпуск.
|
||
|
||
def enable_can(self): # Переводит только тестовую плату в профиль реального CAN-загрузчика M95M02.
|
||
self.config.write_text("#define EEPROM_M95M02 1 /* Тестовый профиль M95M02 с CAN A/B. */\n", encoding="utf-8") # Настоящий конфигурационный заголовок рабочей платы остаётся неизменным.
|
||
self.metadata = prebuild.prepare(self.project, VENDOR, self.build / "generated") # CAN-профиль должен попасть в паспорт до компиляции и последующей проверки выпуска.
|
||
self.write_map(self.metadata) # Согласует аппаратные символы и hooks с новой тестовой конфигурацией.
|
||
|
||
def test_can_bin_is_created_archived_and_exported_automatically(self): # Общий postbuild обязан выпускать CAN-комплект без отдельного ручного вызова конвертора.
|
||
self.enable_can() # Использует тот же явный профиль, который добавлен в проект настоящей 167.
|
||
with patch.object(release, "convert", self.converter()): # Подмена ограничена внешней конверсией; выпуск, проверки и экспорт выполняет production-код.
|
||
image = self.invoke() # Обычный защищённый вызов publish должен сам создать дополнительный CAN BIN.
|
||
can_image = image.parent.parent / "can" / (image.stem + "_can.bin") # CAN-вариант сохраняет артефакт и Build ID в собственной папке назначения рядом с eeprom.
|
||
self.assertEqual(can_image.read_bytes(), image.read_bytes()) # CAN использует тот же SCI8 RAM payload без перенумерации адресов или изменения контейнера.
|
||
manifest = IDENTITY.verify(can_image, can_image.with_suffix(".bin.manifest.json")) # Контрольная сумма и размер должны относиться к собственному имени CAN-файла.
|
||
self.assertEqual((manifest["transport"], manifest["protocol"], manifest["execution"], manifest["image_format"]), ("can", "ProtoCAN Boot", "RAM", "SCI8-RAM")) # CAN BIN явно отделён от обычного EEPROM и внутреннего Flash RS.
|
||
folder = IDENTITY.build_folder_name(self.metadata) # Использует единственный архив именно этой попытки компиляции.
|
||
for destination in (self.output / folder / "can", self.export / folder / "can"): # CAN BIN и sidecar сохраняют одну папку назначения в локальном архиве и штатном экспорте GUI.
|
||
self.assertEqual((destination / can_image.name).read_bytes(), can_image.read_bytes()) # Сравнивает полные байты, исключая подмену результата старым файлом.
|
||
IDENTITY.verify(destination / can_image.name, destination / (can_image.name + ".manifest.json")) # Проверяет реальные сопроводительные данные скопированного комплекта.
|
||
self.assertTrue((destination / (can_image.name + ".sha256")).is_file()) # Одного BIN без отдельного SHA sidecar недостаточно для полного выпуска.
|
||
|
||
def test_can_rejects_oversize_without_publishing_any_variant(self): # Предел EEPROM больше CAN-слота, поэтому CAN требует собственной проверки длины.
|
||
self.enable_can() # Выбирает физическую EEPROM на 256 КиБ и профиль A/B с меньшим логическим слотом.
|
||
payload = image_for(self.metadata)[:-2] + b"".join(block(0x8200 + index, [index]) for index in range(16320)) + bytes(2) # Валидные непересекающиеся RAM-блоки помещаются в EEPROM, но их SCI8-обрамление превышает CAN-лимит 130560 октетов.
|
||
probe = self.build / "oversize_ram.bin" # Отдельный временный образ доказывает пригодность таблицы для общего EEPROM/RAM-контракта.
|
||
probe.write_bytes(payload) # Сохраняет полный корректный SCI8 с большим числом коротких секций.
|
||
release.verify_embedded(probe, self.metadata) # Проверка обычного RAM должна пройти, чтобы отказ действительно относился к ограничениям CAN.
|
||
self.assert_rejected_without_publication(self.converter(payload)) # Отказ CAN не должен публиковать даже обычный RAM BIN из той же неуспешной попытки.
|
||
|
||
def test_can_rejects_missing_application_hooks(self): # Правильный RAM BIN старого приложения не обеспечивает READY/CONFIRM нового CAN-загрузчика.
|
||
self.enable_can() # Подготавливает согласованные паспорт и карту полноценной CAN-платы.
|
||
self.map_path.write_text(self.map_path.read_text(encoding="utf-8").replace("_F28335CanBoot_Ready", "_OldApplicationReady"), encoding="utf-8") # Удаляет обязательный READY из экспортированных символов, сохраняя EEPROM-профиль.
|
||
self.assert_rejected_without_publication(self.converter()) # Непригодное приложение не должно получить CAN-суффикс и попасть в GUI.
|
||
|
||
def test_can_rejects_more_than_64_sections(self): # Аппаратный parser имеет отдельный предел числа SCI8-блоков независимо от размера файла.
|
||
self.enable_can() # Активирует ограничения порта M95M02 A/B.
|
||
prefix = image_for(self.metadata)[:-2] # Сохраняет корректные заголовок, entry и паспорт, убирая лишь конечный терминатор.
|
||
payload = prefix + b"".join(block(0x8200 + index, [index]) for index in range(63)) + bytes(2) # Две исходные секции и 63 короткие дополнительные дают 65 непересекающихся блоков.
|
||
self.assert_rejected_without_publication(self.converter(payload)) # Проверка должна отклонить только CAN-несовместимое число секций до постоянного копирования.
|
||
|
||
def test_at25256_does_not_emit_can_variant(self): # Малый аппаратный вариант остаётся совместимым с прежней сборкой без обещания неподдерживаемого A/B.
|
||
with patch.object(release, "convert", self.converter()): # Оставляет настоящий постпроцесс и паспорт тестовой AT25256B.
|
||
image = self.invoke() # Обычный EEPROM-выпуск должен завершаться успешно без CAN-файла.
|
||
self.assertFalse((image.parent.parent / "can").exists()) # Папка CAN не должна обещать неподдержанную прошивку платы AT25256B.
|
||
|
||
def test_receipt_and_export_keep_purpose_paths(self): # Публикатор и CCS должны получать одинаковые проверенные пути вместо плоской смеси артефактов.
|
||
self.enable_can() # Включает реальный профиль M95M02 A/B с отдельным CAN BIN.
|
||
with patch.object(release, "convert", self.converter()): # Сохраняет настоящие проверки и архивирование, подменяя только TI-конверсию.
|
||
image = self.invoke() # Выпускает файлы в локальный каталог и экспорт SETGUI.
|
||
receipt = json.loads((self.output / "latest_build.json").read_text(encoding="utf-8")) # Читает точный указатель последней завершённой сборки.
|
||
self.assertEqual(image.parent.name, "eeprom") # Возвращаемый основной образ находится в папке внешней EEPROM.
|
||
self.assertEqual(receipt["manifest"], "eeprom/" + image.name + ".manifest.json") # Относительный путь остаётся переносимым между проектом CCS и SETGUI.
|
||
for root in (self.output, self.output / receipt["folder"], self.export / receipt["folder"]): # Проверяет текущую локальную раскладку CCS, её архив и экспортированный комплект SETGUI.
|
||
self.assertTrue((root / receipt["manifest"]).is_file()) # Относительный путь receipt разрешается как в текущих папках, так и в каждом архиве.
|
||
self.assertTrue((root / "firmware_identity.json").is_file()) # Общий паспорт остаётся в корне и связывает назначения одного выпуска.
|
||
self.assertFalse(list(root.glob("*.bin"))) # BIN не смешиваются в корне архива, выбранного для импорта.
|
||
|
||
def test_disabling_can_after_prebuild_requires_rebuild(self): # Удаление CAN-профиля не должно оставлять молча опубликованный файл по устаревшему паспорту.
|
||
self.enable_can() # Подготавливает реальный паспорт с включённым обязательным CAN-вариантом.
|
||
path = self.project / "firmware_build_profile.json" # Изменение ограничено декларативным профилем временного проекта теста.
|
||
policy = json.loads(path.read_text(encoding="utf-8")) # Сохраняет остальные RAM/EEPROM-настройки проекта.
|
||
del policy["can_boot_profile"] # Моделирует явное отключение CAN пользователем после уже завершённого prebuild.
|
||
path.write_text(json.dumps(policy), encoding="utf-8") # Не подменяет старые metadata и бинарный образ новым паспортом.
|
||
self.assert_rejected_without_publication(self.converter()) # Устаревшая конфигурация обязана остановить выпуск до конверсии и экспорта.
|
||
|
||
def test_truncated_image(self): # Воспроизводит неполную загрузочную секцию после сбоя конверсии или копирования.
|
||
self.assert_rejected_without_publication(self.converter(image_for(self.metadata)[:-5])) # Обрезает и часть данных, и терминатор, сохраняя заново вычисленный manifest.
|
||
|
||
def test_wrong_manifest_eeprom(self): # Проверяет требуемое различение аппаратных вариантов по полю manifest.eeprom.
|
||
self.assert_rejected_without_publication(self.converter(wrong_eeprom=True)) # Правильные BIN, версия и SHA не маскируют аппаратное несоответствие паспорта.
|
||
|
||
def test_flash_requires_protected_application_and_current_identity(self): # Проверяет паспорт текущей сборки внутри Flash-приложения, не затрагивающего постоянный сектор A.
|
||
from protocan.f28335_flash import descriptor # Использует действующий контракт FAB1, который проверяет production-парсер защищённого Flash.
|
||
entry_address = release.APP_DATA # Код приложения начинается после 16-словного описания FAB1 в секторе H.
|
||
header = b"\xaa\x08" + bytes(16) + struct.pack("<HH", entry_address >> 16, entry_address & 65535) # Точка входа SCI8 указывает на загруженный код приложения, а не аппаратный вектор ROM.
|
||
loaded = {entry_address: 0x761F, entry_address + 1: 0x8000} # Минимальная исполняемая секция содержит два контрольных слова приложения.
|
||
loaded.update((0x308000 + index, value) for index, value in enumerate(identity_words(self.metadata))) # Встроенные 12 слов содержат версию и Build ID текущего prebuild вне служебного заголовка.
|
||
application = block(entry_address, [0x761F, 0x8000]) + block(0x308000, identity_words(self.metadata)) # Создаёт непересекающиеся Flash-секции кода и неизменяемого паспорта.
|
||
boot = block(release.APP_START, struct.unpack("<16H", descriptor(entry_address, loaded))) # Описание FAB1 хранит границы, точку запуска и CRC точных слов приложения ниже сектора A.
|
||
body = boot + application # Полный Flash-поток включает единственное описание FAB1 и проверяемые данные приложения.
|
||
image = self.build / "flash.bin" # Изолирует проверки структуры от постоянных каталогов выпуска и GUI.
|
||
image.write_bytes(header + body + bytes(2)) # Проверяемая таблица не содержит запрещённого аппаратного перехода 0x33FFF6.
|
||
entry, loaded = release.verify_embedded(image, self.metadata, flash=True) # Вызывает production-парсер с отдельными ограничениями Flash и RS-буфера.
|
||
self.assertEqual((entry, loaded[entry_address]), (entry_address, 0x761F)) # Подтверждает реальную точку запуска и данные исполняемой секции приложения.
|
||
self.assertNotIn(0x33FFF6, loaded) # Постоянный аппаратный вход остаётся вне таблицы записи защищённого приложения.
|
||
failures = [image_for(self.metadata), header + application + bytes(2), header + body + boot + bytes(2)] # RAM-таблица, отсутствующий и дублированный FAB1 не разрешают Flash-выпуск.
|
||
failures += [header + body + block(address, [1, 2]) + bytes(2) for address in (0x2FFFFF, release.APP_STOP, 0x33FF80, 0x33FFF6, 0x33FFF8, 0x380400)] # Запрещает нижнюю границу, весь сектор A, аппаратный вход, CSM, пароли и OTP до стирания.
|
||
for payload in failures: # Все негативные сценарии проверяются настоящими границами production-парсера.
|
||
image.write_bytes(payload) # Корректный ключ/паспорт не должны маскировать запрещённые адреса либо аппаратный вход.
|
||
with self.assertRaises(ValueError): # Отказ является обязательным, даже если файл заново подписать корректным SHA.
|
||
release.verify_embedded(image, self.metadata, flash=True) # Проверяет конкретные Flash-условия независимо от общих RAM-тестов.
|
||
|
||
def test_wrong_linker_eeprom(self): # Проверяет случай define EEPROM_M95M02, переданного не всем этапам сборки.
|
||
self.write_map(dict(self.metadata, eeprom_bytes=262144, max_image_bytes=261884)) # MAP описывает большую EEPROM при AT25256B в prebuild.
|
||
self.assert_rejected_without_publication(self.converter()) # Отказ обязан наступить до конверсии и публикации.
|
||
|
||
def test_changed_profile_after_prebuild(self): # Проверяет изменение заголовка аппаратной конфигурации между prebuild и выпуском.
|
||
self.config.write_text("#define EEPROM_M95M02 1 /* После prebuild установили другой аппаратный профиль. */\n", encoding="utf-8") # Сохраняет прежние metadata и карту для обнаружения устаревшего комплекта.
|
||
self.assert_rejected_without_publication(self.converter()) # Требует повторной подготовки паспорта и пересборки вместо тихой смены аппаратного типа.
|
||
|
||
def test_required_flash_variant_cannot_be_omitted(self): # Проверяет, что неполная попытка Flash-build не публикует даже корректный новый RAM BIN.
|
||
(self.project / "build_flash.py").write_text("# Тестовый признак обязательного отдельного Flash-варианта проекта.\n", encoding="utf-8") # Повторяет соглашение автоматического обнаружения общего postcompile.
|
||
self.assert_rejected_without_publication(self.converter()) # Отсутствующие Flash OUT/SCI8 должны остановить выпуск до output и GUI.
|
||
|
||
@unittest.skipUnless((COMPILER / "bin/cl2000.exe").is_file(), "TI C2000 compiler is unavailable") # Настоящий препроцессор TI проверяет global compiler override без изменения прошивки.
|
||
def test_compiler_only_eeprom_override(self): # Закрывает случай M95M02-компилятора при паспорте и линкере AT25256B.
|
||
self.config.write_text("#ifndef EEPROM_M95M02 /* Разрешает штатное переопределение через define компилятора. */\n#define EEPROM_M95M02 0 /* Аппаратный профиль по умолчанию совпадает с prebuild. */\n#endif /* Завершает конфигурацию тестовой платы. */\n", encoding="utf-8") # Повторяет реальную семантику eeprom_config.h, при которой ошибка раньше оставалась незамеченной.
|
||
source = self.build / "profile_guard.c" # Контрольный C-модуль включает тот же generated-заголовок, что shared_ump.c.
|
||
source.write_text('#include "firmware_identity.h" /* Заставляет компилятор проверить аппаратный профиль паспорта. */\n', encoding="utf-8") # Не добавляет исполняемые данные в проверяемый BIN.
|
||
result = subprocess.run([str(COMPILER / "bin/cl2000.exe"), "-v28", "--preproc_only", "--define=EEPROM_M95M02=1", "--include_path=" + str(self.config.parent), "--include_path=" + str(self.build / "generated"), str(source)], cwd=self.build, text=True, capture_output=True) # Намеренно передаёт M95M02 только компилятору, оставляя паспорт AT25256B.
|
||
self.assertNotEqual(result.returncode, 0) # Несогласованный define обязан останавливать компиляцию до появления OUT.
|
||
self.assertIn("EEPROM_profile_differs_from_prebuild", result.stdout + result.stderr) # Отказ вызван профильной защитой, а не посторонней ошибкой установленного TI.
|
||
|
||
@unittest.skipUnless((COMPILER / "bin/cl2000.exe").is_file(), "TI C2000 compiler is unavailable") # Реальный конвертор проверяется на установленном toolchain; host-проверки обязательны в любом случае.
|
||
def test_real_ti_postcompile(self): # Проверяет общий BAT, TI COFF, hex2000, staged-архив и защищённую публикацию без полной прошивки платы.
|
||
assembly = self.build / "fixture.asm" # Минимальная таблица содержит реальную исполняемую секцию и настоящий встроенный паспорт.
|
||
assembly.write_text(' .sect ".text" ; Тестовая секция исполняется из внутренней RAM.\n .global _fixture_entry ; Экспортирует точку входа контрольного COFF.\n_fixture_entry: ; Начало двухсловного перехода в загруженном RAM-блоке.\n LB _fixture_entry ; Не завершает тестовый код при случайном запуске под отладчиком.\n .sect ".econst" ; Неизменяемые слова паспорта загружаются отдельным блоком.\n .word ' + ','.join(str(value) for value in identity_words(self.metadata)) + ' ; Точные 12 слов текущего prebuild.\n .end ; Завершает минимальный модуль C28x.\n', encoding="utf-8") # Создаёт настоящий исходник TI с русскими комментариями каждой строки.
|
||
linker = self.build / "fixture.cmd" # Карта фикстура соответствует тем RAM-диапазонам, которые разрешает производственный проверяющий код.
|
||
linker.write_text('MEMORY { PAGE 0: CODE: origin=0x8000,length=0x100 DATA: origin=0x8100,length=0x100 } /* Отдельные RAM-области тестового кода и паспорта. */\nSECTIONS { .text: > CODE PAGE=0 .econst: > DATA PAGE=0 } /* Создаёт два полных непересекающихся блока SCI8. */\n' + f'_EEPROM_TOTAL_BYTES={self.metadata["eeprom_bytes"]}; /* Физическая ёмкость выбранной EEPROM в октетах. */\n_EEPROM_FIRMWARE_LIMIT_BYTES={self.metadata["max_image_bytes"]}; /* Конец области прошивки до параметров, в октетах. */\n_EEPROM_PARAMS_START_WORD={self.metadata["max_image_bytes"] // 2}; /* Адрес первого слова сохранённых параметров. */\n', encoding="utf-8") # Абсолютные символы проходят ту же проверку, что F28335.cmd настоящего проекта.
|
||
subprocess.run([str(COMPILER / "bin/cl2000.exe"), "-v28", "--abi=coffabi", "-c", "--output_file=" + str(self.build / "fixture.obj"), str(assembly)], check=True) # Получает настоящий объект TI без аппаратного программирования.
|
||
subprocess.run([str(COMPILER / "bin/cl2000.exe"), "-v28", "--abi=coffabi", "-z", "--entry_point=_fixture_entry", "-m" + str(self.map_path), "-o", str(self.build / "application.out"), str(self.build / "fixture.obj"), str(linker)], check=True) # Линкует настоящий COFF и абсолютную карту выбранного EEPROM.
|
||
with self.assertRaises(ValueError): # Настоящий RAM COFF нельзя выдать за пригодный JTAG Flash OUT только из-за одинакового расширения.
|
||
release.verify_flash_coff(self.build / "application.out", COMPILER) # Проверяет отказ на фактических секциях TI вместо поддельной тестовой MAP.
|
||
image = self.invoke() # Запускает реальный общий BAT и hex2000, сохраняя обязательные производственные проверки и публикацию.
|
||
release.verify_embedded(image, self.metadata) # Дополнительно проверяет фактические байты, опубликованные после конверсии TI.
|
||
release.verify_package(image, self.metadata) # Подтверждает совпадение аппаратного manifest и контрольной суммы конечного файла.
|
||
|
||
|
||
if __name__ == "__main__": # Позволяет запускать всю проверку обычной командой Python из корня проекта.
|
||
unittest.main(verbosity=2) # Каждый отказный сценарий явно виден в отчёте без зависимости от pytest.
|