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