Обеспечь единый процесс обновления для STM32F103/F407/G474 и TMS320F2812/F28335: discovery, служебные ответы, BUSY, тайм-аут сессии и подтверждение приложения. Добавь самостоятельные проекты CCS, аппаратные хранилища и Python-клиент. Совместимость: структуры конфигурации и порта расширены, поэтому потребители должны пересобраться и обнулить новые поля. Для C28x callbacks используют pcan_boot_byte_t; периодический pcan_boot_poll обслуживает discovery и настроенный тайм-аут.
67 lines
11 KiB
Python
67 lines
11 KiB
Python
"""Проверки локального экспорта постоянных CAN-загрузчиков из CCS и CLI.""" # Аппаратное программирование и сеть в сценариях не используются.
|
|
import hashlib # Сверяет контрольные суммы экспортированных байтов OUT.
|
|
import importlib.util # Загружает настоящий сборщик проектов без запуска компиляции.
|
|
import json # Проверяет manifest, подготовку и указатель последнего выпуска.
|
|
from pathlib import Path # Все результаты проверок находятся во временных каталогах.
|
|
import tempfile # Изолирует тесты от пользовательских Debug/Release и SETGUI.
|
|
import unittest # Проверки запускаются штатным Python без Qt и дополнительных пакетов.
|
|
from unittest.mock import patch # Заменяет только аппаратный OFD-анализ тестового OUT, сохраняя настоящий экспорт и SHA-256.
|
|
|
|
SPEC = importlib.util.spec_from_file_location("checked_canboot_projects", Path(__file__).resolve().parents[1] / "projects/build.py") # Выбирает сборщик из того же checkout templates.
|
|
BUILD = importlib.util.module_from_spec(SPEC) # Создаёт модуль без вызова CLI и без доступа к плате.
|
|
SPEC.loader.exec_module(BUILD) # Выполняет реальные функции prebuild/postbuild, проверяемые ниже.
|
|
|
|
|
|
class ProjectReleaseTests(unittest.TestCase): # Проверяет маршруты обоих контроллеров и отказ копирования при неполной или несогласованной сборке.
|
|
def test_current_out_map_and_hashes_are_exported_for_both_configurations(self): # Debug/Release F28335/F2812 обязаны экспортировать только свой комплект.
|
|
for device in ("f28335", "f2812"): # Покрывает отдельные имена OUT и каталоги двух контроллеров.
|
|
for configuration in ("Debug", "Release"): # Проверяет обе конфигурации каждого CCS-проекта.
|
|
with self.subTest(device=device, configuration=configuration), tempfile.TemporaryDirectory(prefix="canboot release ") as temporary: # Пробелы в путях проверяют передачу каталогов без shell-подстановок.
|
|
output = Path(temporary) / configuration; export = Path(temporary) / "SETGUI/canboot" / device[1:] # Тестовый SETGUI не затрагивает реальные файлы прошивок.
|
|
BUILD.prepare(device, configuration, BUILD.DEFAULT_COMPILER, output) # Настоящий prebuild сохраняет ревизию, конфигурацию и SHA исходников.
|
|
name = BUILD.settings(device, configuration, BUILD.DEFAULT_COMPILER)[1]; image = output / (name + ".out") # Имя артефакта читается из действующего CCS-проекта.
|
|
image.write_bytes(b"current OUT " + device.encode("ascii")); image.with_suffix(".map").write_text("current map", encoding="utf-8") # Создаёт различимые артефакты текущего выбранного контроллера.
|
|
(output / "unrelated.out").write_bytes(b"old build") # Старый соседний OUT не должен попасть в экспорт текущего выпуска.
|
|
with patch.object(BUILD, "verify", return_value={"entry_word": 1, "boot_word": 2, "loaded_words": 3}): # Модель заменяет лишь TI-декодирование искусственных байтов OUT.
|
|
BUILD.postbuild(device, configuration, BUILD.DEFAULT_COMPILER, output, export) # Архивирование, копирование и проверка SHA выполняются настоящим общим кодом.
|
|
receipt = json.loads((output / "firmware/latest_build.json").read_text(encoding="utf-8")); copied = Path(receipt["exported_directory"]) # Указатель должен вести на конкретный скопированный выпуск.
|
|
self.assertEqual(copied.parent, export) # F28335 и F2812 не смешиваются в одном каталоге контроллера.
|
|
self.assertEqual({file.name for file in copied.iterdir()}, {image.name, image.with_suffix(".map").name, image.name + ".manifest.json", image.name + ".sha256", "firmware_identity.json"}) # Экспорт содержит полный текущий комплект без старых OUT или объектных файлов.
|
|
self.assertEqual((copied / image.name).read_bytes(), image.read_bytes()) # OUT сохраняется побайтно, включая COFF-символы для JTAG.
|
|
manifest = json.loads((copied / (image.name + ".manifest.json")).read_text(encoding="utf-8")) # Проверяет внешний паспорт именно экспортированного файла.
|
|
self.assertEqual((manifest["device"], manifest["configuration"], manifest["purpose"]), (device, configuration, "canboot")) # Паспорт сохраняет выбранный контроллер, режим сборки и назначение загрузчика.
|
|
self.assertEqual(manifest["sha256"], hashlib.sha256(image.read_bytes()).hexdigest()) # Контрольная сумма относится к точным байтам OUT.
|
|
|
|
def test_changed_sources_or_configuration_prevent_export(self): # Несогласованная подготовка не должна выпускать даже формально существующий OUT.
|
|
with tempfile.TemporaryDirectory() as temporary: # Изолирует изменение тестовых метаданных от реального prebuild.
|
|
output = Path(temporary) / "Debug"; export = Path(temporary) / "SETGUI" # Каталог назначения заранее отсутствует для явной проверки отказа копирования.
|
|
BUILD.prepare("f28335", "Debug", BUILD.DEFAULT_COMPILER, output) # Создаёт согласованный паспорт до моделируемого изменения исходников.
|
|
with patch.object(BUILD, "source_hashes", return_value={}): # Моделирует изменение дерева после подготовки без записи production-файлов.
|
|
with self.assertRaisesRegex(ValueError, "changed"): BUILD.postbuild("f28335", "Debug", BUILD.DEFAULT_COMPILER, output, export) # Несовпадение SHA останавливает выпуск до проверки и копирования OUT.
|
|
with self.assertRaisesRegex(ValueError, "changed"): BUILD.postbuild("f28335", "Release", BUILD.DEFAULT_COMPILER, output, export) # Debug-подготовку нельзя выдать за Release даже при одинаковых исходниках.
|
|
self.assertFalse(export.exists()) # Ошибочные подготовки не создают папку успешного выпуска в GUI.
|
|
|
|
def test_missing_map_or_invalid_coff_prevent_export(self): # Наличие OUT без карты или с запрещёнными LOAD-адресами не разрешает экспорт.
|
|
with tempfile.TemporaryDirectory() as temporary: # Тестовые файлы удаляются вместе с изолированным каталогом.
|
|
output = Path(temporary) / "Debug"; export = Path(temporary) / "SETGUI" # Не использует аппаратные каталоги реальной установки.
|
|
BUILD.prepare("f2812", "Debug", BUILD.DEFAULT_COMPILER, output) # Сохраняет штатные входные данные выбранного F2812.
|
|
image = output / "F2812_CAN_NOR.out"; image.write_bytes(b"invalid COFF") # Искусственный OUT не должен считаться проверенным загрузчиком.
|
|
with self.assertRaisesRegex(ValueError, "MAP"): BUILD.postbuild("f2812", "Debug", BUILD.DEFAULT_COMPILER, output, export) # Отсутствие карты обнаруживается до запуска OFD.
|
|
image.with_suffix(".map").write_text("map", encoding="utf-8") # Дополняет комплект, чтобы отдельно проверить отказ COFF-анализа.
|
|
with patch.object(BUILD, "verify", side_effect=ValueError("invalid COFF")): # Моделирует диагностированный TI-декодером неверный образ.
|
|
with self.assertRaisesRegex(ValueError, "invalid COFF"): BUILD.postbuild("f2812", "Debug", BUILD.DEFAULT_COMPILER, output, export) # Неверные Flash-данные блокируются до любой публикации файлов.
|
|
self.assertFalse(export.exists()) # Ни один путь ошибки не оставляет ложный новый выпуск в SETGUI.
|
|
|
|
def test_ccs_steps_match_device_and_configuration(self): # Проверяет все четыре реальные настройки Build Steps, которые вызывает CCS.
|
|
for device in ("f28335", "f2812"): # Отдельно проверяет маршруты 28335 и 2812.
|
|
for configuration in BUILD.ET.parse(BUILD.ROOT / "projects/ccs" / device / ".cproject").findall(".//configuration"): # Читает действующий XML, включая Debug и Release.
|
|
prepare = configuration.get("prebuildStep"); postbuild = configuration.get("postbuildStep") # Извлекает уже раскодированные XML-команды со штатными кавычками.
|
|
self.assertIn("--step prepare", prepare); self.assertIn("--step postbuild", postbuild) # Обе фазы обязаны вызывать общий сборщик в соответствующем режиме.
|
|
for command in (prepare, postbuild): # Контроллер и конфигурация должны совпадать на обеих фазах.
|
|
self.assertIn("--device " + device, command); self.assertIn("--configuration " + configuration.get("name"), command) # Предотвращает случайный паспорт другого проекта либо Debug вместо Release.
|
|
self.assertIn("balzam167/canboot/" + device[1:], postbuild) # Конечный каталог соответствует контроллеру постоянного загрузчика.
|
|
|
|
|
|
if __name__ == "__main__": # Импорт тестов не запускает их побочным эффектом.
|
|
unittest.main() # Поддерживает прямой запуск штатным Python из любого рабочего каталога.
|