"""Проверяет перенос трёх файлов сборки, итератор JSON и запрет выпуска неверного BIN.""" # Не меняет рабочую версию или файлы прошивки пользователя. import hashlib # Создаёт независимый эталон SHA-256 для проверяемого транспортного файла. import importlib.util # Загружает production-сценарий без запуска его CLI. import json # Готовит конфигурацию другого изделия и проверяет сохраняемый счётчик. import os # Задаёт относительный путь к templates для временного проекта на том же диске. from pathlib import Path # Находит файлы проекта и изолирует пути с пробелами. import shutil # Копирует только три файла, необходимые для подключения сборки. import struct # Формирует независимый минимальный SCI8 с известным паспортом. import subprocess # Запускает настоящий BAT из постороннего рабочего каталога. import tempfile # Удаляет только временные файлы проверяемого проекта. import unittest # Проверяет успешные сборки, переполнение и ошибки выпуска. from unittest.mock import patch # Подменяет внешнюю конверсию для воспроизведения неверных результатов. import xml.etree.ElementTree as ET # Читает реальные команды Debug и Release из настроек CCS. ROOT = Path(__file__).resolve().parents[2] # Использует репозиторий только для поиска переносимых исходников и библиотеки. PROJECT = ROOT / "UKSSTMS320F28335" # Здесь лежат три копируемых файла подключения сборки. TEMPLATES = ROOT.parent / "newProject/templates" # Настоящая общая библиотека используется тестами prebuild без копирования реализации. spec = importlib.util.spec_from_file_location("portable_build", PROJECT / "build_identity.py") # Проверяет тот же код, который запускает BAT проекта. builder = importlib.util.module_from_spec(spec) # Создаёт отдельный импорт без изменения версии. spec.loader.exec_module(builder) # Загружает реальные функции счётчика и проверки выпуска. class BuildStepsTest(unittest.TestCase): # Проверяет контракт переноса независимо от названия текущего изделия. def setUp(self): # Создаёт другой проект только из трёх переносимых файлов. self.temporary = tempfile.TemporaryDirectory(prefix="portable build ", dir=ROOT) # Тот же диск позволяет проверить относительный путь к templates, а пробелы проверяют кавычки BAT. self.addCleanup(self.temporary.cleanup) # Удаляет только собственные временные данные даже при отказе проверки. self.project = Path(self.temporary.name) / "another controller" # Имя временного проекта отличается от имени production-проекта. self.project.mkdir() # Создаёт каталог назначения до копирования переносимых файлов. for name in ("firmware_build.bat", "build_identity.py", "firmware_identity.json"): # Никакие вспомогательные заголовки версии или prebuild-скрипты не требуются. shutil.copy2(PROJECT / name, self.project / name) # Копирует исходные сценарии без исправления их кода. self.config = self.project / "firmware_identity.json" # Единственный изменяемый файл настройки переносимого подключения. self.settings = {"product": "Portable-Test-Controller", "target": "TMS320F28335", "version": [1, 0, 0], "increment_version": True, "templates": os.path.relpath(TEMPLATES, self.project), "max_image_bytes": 64, "copy_to": "../images copy"} # Предел 64 байта относится только к минимальному тестовому BIN, а путь копирования проверяет разрешение от JSON. self.save_settings() # Записывает другое изделие и размещение зависимости без изменения Python или BAT. self.build_dir = self.project / "Debug with spaces" # Все generated и firmware отделены от пользовательских результатов. self.metadata = {"product": "Portable-Test-Controller", "target": "TMS320F28335", "version": [1, 0, 0], "build_id": "1234abcd", "built_at_utc": "2026-10-17T09:00:00+00:00"} # Независимый паспорт для сценариев конверсии и отказов. def save_settings(self): # Позволяет каждому сценарию явно задать входную конфигурацию. self.config.write_text(json.dumps(self.settings), encoding="utf-8") # Хранит проверочные настройки в требуемой кодировке. def prepare_metadata(self): # Готовит результат prebuild без изменения рабочего проекта. generated = self.build_dir / "generated" # Использует стандартный путь, который ожидает production-postbuild. generated.mkdir(parents=True) # Создаёт изолированный контекст компиляции. (generated / "firmware_identity.json").write_text(json.dumps(self.metadata), encoding="utf-8") # Фиксирует паспорт компиляции независимо от поведения проверяемого кода. def image_bytes(self, patch_number=0): # Создаёт эталон SCI8 без использования production-упаковщика. words = [1, 1, 0, patch_number, 2026, 0x0A11, 0x0900, 0, 0x3132, 0x3334, 0x6162, 0x6364] # Двенадцать слов задают контракт, версию, UTC и строку 1234abcd в регистровом порядке. return b"\xaa\x08" + bytes(20) + struct.pack(" RAM .econst > RAM .const > RAM .cinit > RAM .bss > RAM .ebss > RAM .stack > RAM .data > RAM } /* Размещает все секции примера в загрузочном блоке RAM. */\n', encoding="utf-8") # Проверяет транспорт SCI8, не меняя размещение пользовательской прошивки. subprocess.run([str(compiler), "-v28", "--abi=coffabi", "--compile_only", "--include_path=" + str(self.build_dir / "generated"), "--obj_directory=" + str(self.build_dir), str(source)], check=True) # Компилирует настоящий паспорт из generated в объект C28x COFF. artifact = self.project / "external output/PortableFirmware" # Полный путь OUT с пробелами проверяет стандартный макрос BuildArtifactFileBaseName. artifact.parent.mkdir() # Выходной OUT специально находится за пределами BuildDirectory. subprocess.run([str(compiler), "-v28", "--abi=coffabi", "--run_linker", "--rom_model", "--entry_point=_entry", "--output_file=" + str(artifact) + ".out", "--map_file=" + str(artifact) + ".map", str(self.build_dir / "fixture.obj"), str(linker)], check=True) # Линкует минимальный пример без библиотек конкретной платы и сохраняет секцию паспорта. result = subprocess.run(["cmd.exe", "/d", "/c", "call", str(self.project / "firmware_build.bat"), "build", "--compiler", str(compiler.parents[1]), "--build-dir", str(self.build_dir), "--artifact", str(artifact)], cwd=ROOT, capture_output=True) # Полностью проходит BAT → Python → общий postcompile → hex2000 → проверки → копирование. self.assertEqual(result.returncode, 0, repr(result.stdout) + repr(result.stderr)) # При ошибке показывает фактический вывод общей конверсии без потери диагностики. image = self.build_dir / "firmware" / (artifact.name + "_" + metadata["build_id"] + ".bin") # Выбирает точный результат фактического prebuild вместо поиска старого BIN. builder.verify_embedded(image, metadata, 4096) # Проверяет реальную транспортную таблицу и встроенные слова паспорта. builder.verify_package(self.project.parent / "images copy" / image.name, metadata) # Проверяет SHA-256 и метаданные дополнительной копии. self.assertEqual(json.loads(self.config.read_text(encoding="utf-8"))["version"], [1, 0, 1]) # Postbuild не увеличивает номер повторно после prebuild. def test_ccs_prebuild_increments_in_both_configurations(self): # Проверяет фактический итератор и при прямом подключении templates, и через проектную обёртку CCS. configurations = ET.parse(PROJECT / ".cproject").findall("./storageModule/cconfiguration/storageModule/configuration") # Проверяет реальные настройки проекта. self.assertEqual({item.get("name") for item in configurations}, {"Debug", "Release"}) # Требует обнаружения обеих конфигураций. self.settings["version"] = [1, 0, 4] # Воспроизводит рабочий номер, на котором прямое подключение templates перестало увеличивать версию. self.save_settings() # Не меняет настоящий JSON и не занимает номера пользовательских прошивок. for configuration, expected in zip(configurations, ([1, 0, 5], [1, 0, 6])): # Два запуска с одним JSON должны увеличивать номер ровно по одному разу. build_dir = self.build_dir / configuration.get("name") # Каждая конфигурация получает собственные generated, как при сборке в IDE. command = configuration.get("prebuildStep").rstrip(";").replace("${PROJECT_LOC}", str(self.project)).replace("${BuildDirectory}", str(build_dir)) # Подставляет макросы CCS и удаляет завершающий разделитель списка команд IDE. result = subprocess.run(command, cwd=ROOT, capture_output=True) # Выполняет команду из настоящего XML через cmd с путями, содержащими пробелы. self.assertEqual(result.returncode, 0, repr(result.stdout) + repr(result.stderr)) # Показывает диагностику фактического BAT, если подключение templates неработоспособно. self.assertEqual(json.loads(self.config.read_text(encoding="utf-8"))["version"], expected) # Проверяет сохраняемый счётчик после прямого вызова и обёртки без двойного увеличения. self.assertEqual(json.loads((build_dir / "generated/firmware_identity.json").read_text(encoding="utf-8"))["version"], expected) # Паспорт компиляции обязан содержать тот же номер, который зарезервирован в JSON. self.assertIn(f"#define FIRMWARE_VERSION_PATCH {expected[2]}U", (build_dir / "generated/firmware_identity.h").read_text(encoding="utf-8")) # Проверяет, что компилятор получит обновлённую версию, а не только новый manifest. if __name__ == "__main__": unittest.main() # Любое нарушение версии, переносимости или защиты BIN возвращает ненулевой код.