Files
templates/c/firmware-info/tests/test_c2000_postbuild.py

88 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Проверяет совместимость postbuild, экспорт двух образов и отказ от устаревшего Flash OUT.""" # Фиксирует контракт общего выпуска без зависимости от установленного TI-компилятора.
import json # Формирует паспорт текущей сборки для проверки архива и контрольных сумм.
from pathlib import Path # Создаёт независимые пути проекта, компилятора и результатов в каждом тесте.
import subprocess # Использует штатное исключение ненулевого кода проектного сборщика.
import sys # Подключает общий модуль из каталога tools текущей копии templates.
import tempfile # Изолирует проверочные файлы от реальных прошивок и каталогов GUI.
import unittest # Организует проверки публичного контракта postbuild стандартной библиотекой Python.
from unittest.mock import patch # Подменяет только внешние инструменты, оставляя настоящее архивирование и SHA-256.
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'tools')) # Находит проверяемую реализацию относительно теста без путей конкретной рабочей станции.
import c2000_postbuild as c2000 # Вызывает тот же postbuild, который запускается штатным postcompile.bat.
from identity import verify # Проверяет байты скопированных образов по настоящему manifest.
class PostbuildTests(unittest.TestCase): # Проверяет общую цепочку от готового OUT до экспортированной папки выпуска.
def setUp(self): # Для каждой проверки создаёт отдельный проект с типичной структурой CCS.
temporary = tempfile.TemporaryDirectory() # Все файлы сценария принадлежат временному каталогу теста.
self.addCleanup(temporary.cleanup) # Освобождает только каталог данной проверки при любом результате.
self.root = Path(temporary.name) # Корень не зависит от реальной прошивки и установленного templates.
self.build = self.root / 'project/Debug' # Проверяет соглашение о расположении сборщика рядом с конфигурациями CCS.
self.build.mkdir(parents=True) # Подготавливает рабочий каталог внешних инструментов.
self.compiler = self.root / 'compiler' # Позволяет проверить передачу компилятора текущего проекта вместо фиксированного пути.
(self.compiler / 'bin').mkdir(parents=True) # Воспроизводит структуру установленного C2000 compiler.
(self.compiler / 'bin/hex2000.exe').write_bytes(b'fake tool') # Проверка наличия инструмента реальна, а выполнение будет подменено.
self.source = self.root / 'Bin/Firmware.out' # Основной OUT размещён вне Debug, как в рабочем проекте пользователя.
self.source.parent.mkdir() # Создаёт отдельный каталог исходного COFF и итогового Flash COFF.
self.source.write_bytes(b'RAM image') # Даёт узнаваемые байты для проверки, что основной образ не подменён Flash-вариантом.
self.metadata = self.build / 'metadata.json' # Один паспорт используется обоими вариантами текущего выпуска.
self.metadata.write_text(json.dumps({'build_id': 'NOGIT000', 'built_at_utc': '2026-10-14T13:00:00+00:00', 'source': {'commit': None}, 'version': [1, 2, 3]}), encoding='utf-8') # Фиксированное время проверяет выбор папки по prebuild, а не времени файлов.
self.output = self.build / 'firmware' # Локальный выпуск получает оба образа и их сопроводительные файлы.
self.export = self.root / 'GUI' # Экспорт изолирован от настоящего GUI и сетевых ресурсов.
def fake_tool(self, command, **kwargs): # Имитирует границу внешних инструментов, оставляя реальную логику упаковки и проверки.
if '--postbuild' in command: # Вложенная Flash-сборка обязана использовать режим без повторного Make.
self.assertEqual(Path(command[command.index('--compiler') + 1]), self.compiler.resolve()) # Проверяет выбор текущей версии TI compiler.
self.assertEqual(Path(command[command.index('--build-dir') + 1]), self.build.resolve()) # Проверяет использование текущих объектов и generated-паспорта.
image = Path(command[command.index('--output') + 1]) # Результат должен создаваться по переданному временному пути.
self.assertNotEqual(image.parent, self.source.parent) # Запрещает выдавать существующий OUT из Bin за результат текущего вызова.
image.write_bytes(b'current Flash image') # Создаёт узнаваемый новый COFF для сравнения всех копий.
image.with_suffix('.map').write_text('codestart 0033fff6', encoding='utf-8') # Воспроизводит обязательный второй результат сборщика.
else: # Обычная SCI8-конверсия должна продолжать использовать основной RAM OUT.
expected = 'Firmware_flash.out' if '_flash_rs.bin' in command[command.index('-o') + 1] else self.source.name # Только отдельная RS-конверсия должна брать Flash COFF.
self.assertEqual(Path(command[1]).name, expected) # Проверяет, что EEPROM BIN и Flash RS BIN создаются из разных исходных образов.
Path(command[command.index('-o') + 1]).write_bytes(b'EEPROM image') # Выпускает HEX/BIN с отличимыми байтами основного варианта.
Path(command[command.index('-map') + 1]).write_text('RAM map', encoding='utf-8') # Сохраняет карты, необходимые настоящему архиватору.
return subprocess.CompletedProcess(command, 0) # Подтверждает только имитируемый инструмент, не подменяя проверки выпуска.
def run_postbuild(self): # Вызывает публичную функцию с теми же путями, что передаёт BAT из CCS.
return c2000.postbuild(self.compiler, self.build, str(self.source.with_suffix('')), self.metadata, self.output, self.export) # Включает настоящий экспорт и проверку manifest.
def test_project_without_flash_builder_keeps_eeprom_workflow(self): # Старые проекты должны работать без новых настроек или Flash-файлов.
with patch.object(c2000.subprocess, 'run', side_effect=self.fake_tool) as calls: # Подменяет только TI Hex Utility.
result = self.run_postbuild() # Выполняет полный прежний выпуск EEPROM.
self.assertEqual(calls.call_count, 2) # Для HEX и BIN выполнены два вызова; дополнительного сборщика не было.
self.assertEqual(self.source.read_bytes(), b'RAM image') # Основной OUT не изменён упаковкой.
self.assertFalse(self.source.with_name('Firmware_flash.out').exists()) # Отсутствие builder не создаёт фиктивный Flash-образ.
self.assertEqual(verify(result, Path(str(result) + '.manifest.json'))['size_bytes'], len(b'EEPROM image')) # Проверяет настоящее содержимое EEPROM BIN и его паспорт.
def test_flash_and_eeprom_share_current_export_with_separate_checksums(self): # Новый выпуск должен содержать оба варианта под своими именами.
(self.build.parent / 'build_flash.py').write_text('# Сборщик проекта подключён явно.\n', encoding='utf-8') # Наличие сценария включает дополнительный шаг postbuild.
with patch.object(c2000.subprocess, 'run', side_effect=self.fake_tool) as calls: # Flash и hex2000 имитируются на границе процессов.
self.run_postbuild() # Архиватор и экспорт работают без подмены.
self.assertEqual(calls.call_count, 4) # Один Flash-вызов, две EEPROM-конверсии и отдельный RS BIN подтверждают отсутствие рекурсивного Make.
folder = self.export / 'NOGIT000_2026-10-14_16-00-00_UTC+3' # Имя следует времени prebuild с переводом в московский UTC+3.
for directory in (self.source.parent, self.output, folder): # Проверяет копию для CCS, плоский выпуск и архив для GUI.
self.assertEqual((directory / 'Firmware_flash.out').read_bytes(), b'current Flash image') # Каждая копия содержит новый Flash COFF.
self.assertTrue((directory / 'Firmware_flash.map').is_file()) # Карта Flash не теряется при сохранении или экспорте.
for name in ('Firmware_flash.out', 'Firmware_NOGIT000.bin', 'Firmware_flash_rs.bin'): # Контрольные суммы каждого варианта должны проверяться независимо.
verify(folder / name, folder / (name + '.manifest.json')) # Использует реальные SHA-256 экспортированных байтов.
self.assertEqual((folder / self.source.name).read_bytes(), b'RAM image') # Архив сохраняет исходный RAM OUT вместе с новым Flash OUT.
def test_failed_or_incomplete_builder_cannot_export_stale_flash(self): # Ошибка или отсутствие карты должны остановить выпуск до экспорта старого COFF.
(self.build.parent / 'build_flash.py').write_text('# Подключён неисправный сборщик для проверки отказа.\n', encoding='utf-8') # Включает путь Flash-проверки.
stale = self.source.with_name('Firmware_flash.out') # В Bin уже есть образ от предыдущей успешной сборки.
stale.write_bytes(b'old Flash image') # Отличимые байты выявят ошибочную повторную публикацию.
for failure in (subprocess.CalledProcessError(1, 'builder'), None): # Проверяет ненулевой код и ложный успех без обязательных результатов.
with self.subTest(failure=failure): # Даёт отдельную диагностику двум независимым отказам.
with patch.object(c2000.subprocess, 'run', side_effect=failure) as calls: # Сборщик либо падает, либо не создаёт файлов.
with self.assertRaises((subprocess.CalledProcessError, ValueError)): # Оба отказа должны выйти наружу как ошибка postbuild.
self.run_postbuild() # Не разрешает продолжение EEPROM-конверсии после сбоя Flash.
self.assertEqual(calls.call_count, 1) # После отказа Flash ни hex2000, ни новый сборщик не запускаются.
self.assertEqual(stale.read_bytes(), b'old Flash image') # Предыдущий образ сохраняется, но не считается новым результатом.
self.assertFalse(self.export.exists()) # Неполная сборка не создаёт выпуск для GUI.
if __name__ == '__main__': # Позволяет запускать проверки отдельно или через unittest discover.
unittest.main() # Возвращает ненулевой код при нарушении контракта упаковки или экспорта.