88 lines
13 KiB
Python
88 lines
13 KiB
Python
"""Проверяет совместимость 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() # Возвращает ненулевой код при нарушении контракта упаковки или экспорта.
|