Подключи ISE, Vivado и Quartus к общему итератору версии и HDL-паспорту. Проверяй новый bit/sof до создания manifest, SHA-256 и архива выпуска. Добавь установку локальных команд ISE, инструкции переноса и регрессионные проверки успешных и прерванных сборок.
139 lines
19 KiB
Python
139 lines
19 KiB
Python
"""Проверяет автоматическую установку ISE, сохранение версии и локальные BAT.""" # Тесты не изменяют реальные .xise пользователя.
|
|
import importlib.util # Загружает установщик без вызова его интерактивного CLI.
|
|
import json # Проверяет автоматически извлечённые и сохраняемые настройки проекта.
|
|
import os # Подготавливает окружение Windows для реального запуска BAT.
|
|
from pathlib import Path # Изолирует проекты в игнорируемом каталоге репозитория.
|
|
import subprocess # Проверяет установочный BAT и установленную команду --check.
|
|
import tempfile # Создаёт независимый проект для каждой проверки.
|
|
import unittest # Проверяет успешную установку, повторные вызовы и отказы до сборки.
|
|
from unittest.mock import patch # Заменяет только внешние команды ISE в тестах запуска.
|
|
HERE = Path(__file__).resolve().parents[1] # Находит общий установщик рядом с остальными FPGA-инструментами.
|
|
SPEC = importlib.util.spec_from_file_location("ise_project_setup", HERE / "ise_project.py") # Выделяет установщику отдельное пространство имён.
|
|
ise = importlib.util.module_from_spec(SPEC) # Создаёт модуль для проверки публичных функций.
|
|
SPEC.loader.exec_module(ise) # Загружает код без запросов к пользователю и без установки в чужие каталоги.
|
|
PROJECT = '''<?xml version="1.0" encoding="UTF-8"?>
|
|
<project xmlns="http://www.xilinx.com/XMLSchema" xmlns:x="http://www.xilinx.com/XMLSchema">
|
|
<properties>
|
|
<property x:name="Device" x:value="xc6slx16"/>
|
|
<property x:name="Device Family" x:value="Spartan6"/>
|
|
<property x:name="Package" x:value="ftg256"/>
|
|
<property x:name="Speed Grade" x:value="-3"/>
|
|
<property x:name="Implementation Top" x:value="Module|logic_top"/>
|
|
<property x:name="Implementation Top File" x:value="logic_top.v"/>
|
|
<property x:name="Preferred Language" x:value="Verilog"/>
|
|
<property x:name="Working Directory" x:value="impl"/>
|
|
<property x:name="Output File Name" x:value="programmer_top"/>
|
|
</properties>
|
|
</project>
|
|
''' # Фикстура воспроизводит namespace и свойства настоящего ISE 14.7 без исходников платы.
|
|
|
|
|
|
class SetupTests(unittest.TestCase): # Проверяет автоматическое подключение одного проекта без ручного копирования.
|
|
def setUp(self): # Каждый сценарий получает свой рабочий каталог с пробелами и кириллицей.
|
|
scratch = HERE.parents[2] / ".codex-build" # Держит тестовые проекты внутри разрешённой области и вне индекса Git.
|
|
scratch.mkdir(exist_ok=True) # Поддерживает запуск тестов из свежей копии библиотеки.
|
|
temporary = tempfile.TemporaryDirectory(prefix="ISE setup плата & ", dir=scratch) # Амперсанд проверяет передачу путей без вставки shell-команд.
|
|
self.addCleanup(temporary.cleanup) # Удаляет только собственный тестовый проект.
|
|
self.root = Path(temporary.name) # Хранит весь созданный комплект рядом с тестовым .xise.
|
|
self.project = self.root / "board.xise" # Отличается от имени top и результата BitGen для проверки автоподбора.
|
|
self.project.write_text(PROJECT, encoding="utf-8") # Не изменяет существующие проекты на диске.
|
|
|
|
def install(self, **arguments): # Вызывает установщик без поиска реальной САПР на каждом тесте.
|
|
with patch.object(ise, "find_settings", return_value=""): # Автоматическая установка допускается и до установки ISE.
|
|
return ise.install(self.project, **arguments) # Проверяет настоящую запись обвязок и предварительного HDL.
|
|
|
|
def test_install_detects_project_and_does_not_consume_version(self): # Установка должна завершиться готовым локальным BAT и исходниками паспорта.
|
|
before = self.project.read_bytes() # Автоматическое подключение не должно менять XML проекта без ISE.
|
|
settings = self.install() # Извлекает параметры и создаёт локальные файлы.
|
|
self.assertEqual(settings["image"], "impl/programmer_top.bit") # Output File Name и Working Directory важнее имени .xise.
|
|
self.assertEqual(settings["language"], "verilog") # Определяет язык по реальному верхнему исходнику.
|
|
identity = json.loads((self.root / "firmware_identity.json").read_text(encoding="utf-8")) # Читает рабочий, а не временный счётчик.
|
|
self.assertEqual(identity["target"], "xc6slx16-3ftg256") # Паспорт получает реальный кристалл с корпусом и скоростной группой.
|
|
self.assertEqual(identity["version"], [0, 1, 0]) # Установка не является попыткой синтеза и не расходует номер.
|
|
self.assertEqual(self.project.read_bytes(), before) # Исходный .xise сохранён побайтно.
|
|
for name in ("build_firmware.bat", "firmware_build.py", "firmware_build.json", "FIRMWARE_SETUP.md", "firmware_identity_instance.txt", "build/ise/generated/firmware_identity.v"): # Все ранее ручные файлы создаются автоматически.
|
|
self.assertTrue((self.root / name).is_file(), name) # Проверяет полноту установленного комплекта.
|
|
self.assertIn("build/ise/", (self.root / ".gitignore").read_text(encoding="utf-8")) # Сгенерированные исходники исключаются из статуса Git проекта.
|
|
|
|
def test_reinstall_preserves_configs_version_and_ignore(self): # Повторное подключение не должно сбросить уже выпущенный номер.
|
|
self.install() # Сначала создаёт штатный набор локальных файлов.
|
|
identity = self.root / "firmware_identity.json" # Рабочий счётчик изменяется только как часть тестового сценария.
|
|
identity.write_text('{"product":"Моя плата","target":"xc6slx16","version":[3,4,5],"increment_version":true}', encoding="utf-8") # Имитирует существующую версию и пользовательское форматирование.
|
|
config = self.root / "firmware_build.json" # Параметры установки также сохраняются при повторном запуске.
|
|
settings = json.loads(config.read_text(encoding="utf-8")) # Сохраняет автоматически определённые значения остальных полей.
|
|
settings["output"] = "build/custom" # Проект может поменять каталог архивов после установки.
|
|
config.write_text(json.dumps(settings), encoding="utf-8") # Создаёт пользовательский формат настройки.
|
|
originals = {path: path.read_bytes() for path in (identity, config)} # Проверяет побайтную сохранность обоих JSON.
|
|
self.install() # Повторный вызов должен использовать выбранный пользователем output.
|
|
for path, data in originals.items(): # Ни версия, ни пути не должны замениться значениями из .xise.
|
|
self.assertEqual(path.read_bytes(), data) # Не допускает переформатирования сохранённых настроек.
|
|
ignore = (self.root / ".gitignore").read_bytes() # Запоминает результат после добавления custom output.
|
|
self.install() # Третий вызов проверяет отсутствие повторяющихся правил.
|
|
self.assertEqual((self.root / ".gitignore").read_bytes(), ignore) # .gitignore не растёт при каждом запуске.
|
|
self.assertTrue((self.root / "build/custom/generated/firmware_identity.v").is_file()) # Предварительный HDL соответствует сохранённой настройке.
|
|
|
|
def test_existing_user_bat_is_preserved_without_partial_installation(self): # Установщик не должен заменять существующие проектные команды.
|
|
custom = self.root / "build_firmware.bat" # Создаёт пользовательскую команду с занятым именем.
|
|
custom.write_bytes(b"@echo custom build\n") # Содержимое отличается от управляемого шаблона.
|
|
with self.assertRaisesRegex(ValueError, "Existing file differs"): # Коллизия должна быть обнаружена до записи JSON и generated.
|
|
self.install() # Выполняет настоящую предварительную проверку устанавливаемых файлов.
|
|
self.assertEqual(custom.read_bytes(), b"@echo custom build\n") # Пользовательский файл остаётся прежним.
|
|
self.assertFalse((self.root / "firmware_identity.json").exists()) # Частичная установка не создаёт рабочий счётчик.
|
|
self.assertFalse((self.root / "build").exists()) # Отказ коллизии не оставляет предварительный паспорт.
|
|
|
|
def test_vhdl_override_and_ambiguous_or_cpld_project(self): # Поддерживает VHDL и останавливает неподходящие проекты до записи файлов.
|
|
self.project.write_text(PROJECT.replace("logic_top.v", "logic_top.vhd"), encoding="utf-8") # Расширение top имеет приоритет над Preferred Language.
|
|
project, image, language, target = ise.project_info(self.root) # Единственный .xise выбирается из переданной папки.
|
|
self.assertEqual(language, "vhdl") # Выбирает VHDL автоматически без ручного параметра.
|
|
settings = self.install(image=Path("custom/result.bit")) # Разрешает явно переопределить нестандартный путь BitGen.
|
|
self.assertEqual(settings["image"], "custom/result.bit") # Не теряет ручное уточнение при автоматическом заполнении.
|
|
self.assertIn("component firmware_identity", (self.root / "firmware_identity_instance.txt").read_text(encoding="utf-8")) # Выдаёт VHDL-заготовку для подключения до первого синтеза.
|
|
(self.root / "other.xise").write_text(PROJECT, encoding="utf-8") # Моделирует две платы в одной папке.
|
|
with self.assertRaisesRegex(ValueError, "exactly one"): # Установщик не должен угадывать нужную плату.
|
|
ise.project_info(self.root) # Требует явный файл .xise.
|
|
self.project.write_text(PROJECT.replace("xc6slx16", "xc9572xl"), encoding="utf-8") # CPLD требует .jed вместо .bit.
|
|
with self.assertRaisesRegex(ValueError, "FPGA project"): # Неподходящий FPGA-шаблон не устанавливается на CPLD.
|
|
ise.project_info(self.project) # Проверяет устройство до любых изменений проекта.
|
|
|
|
def test_build_preserves_arguments_exit_code_and_check_version(self): # Локальная настройка должна доходить до общего Tcl отдельными аргументами.
|
|
self.install() # Готовит настоящий набор настроек и исходную версию.
|
|
config = self.root / "firmware_build.json" # Выбирает установленную локальную настройку.
|
|
version = (self.root / "firmware_identity.json").read_bytes() # --check не имеет права занимать следующий номер.
|
|
with patch.object(ise, "ise_environment", return_value={"PATH": "test"}), patch.object(ise.shutil, "which", return_value="C:/ISE path/xtclsh.exe"), patch.object(ise.subprocess, "call", return_value=7) as invoke: # Не запускает лицензируемую САПР в тесте аргументов.
|
|
self.assertEqual(ise.build(config, check=True), 0) # Проверка должна завершиться до запуска Tcl-flow.
|
|
invoke.assert_not_called() # Подтверждает отсутствие скрытой сборки в --check.
|
|
self.assertEqual(ise.build(config), 7) # Код ошибки САПР передаётся пользователю без подмены.
|
|
arguments = invoke.call_args.args[0] # Каждый элемент списка соответствует одному аргументу процесса.
|
|
self.assertEqual(arguments[2], str(self.project)) # Пробелы, кириллица и амперсанд сохраняются в пути .xise.
|
|
self.assertEqual(arguments[-2], "verilog") # Общий Tcl получает выбранный при установке язык.
|
|
self.assertEqual(invoke.call_args.kwargs["cwd"], self.root) # Относительные исходники .xise разрешаются из папки проекта.
|
|
self.assertEqual((self.root / "firmware_identity.json").read_bytes(), version) # До настоящего pre-build номер не изменяется.
|
|
|
|
@unittest.skipUnless(os.name == "nt", "Windows BAT execution") # Реальные BAT проверяются только на Windows, остальные тесты переносимы.
|
|
def test_native_settings_environment_preserves_unicode(self): # Автоматическое окружение должно сохранить пути при любой кодовой странице консоли.
|
|
settings = self.root / "settings64.bat" # Путь содержит пробелы, кириллицу и амперсанд.
|
|
settings.write_bytes('@echo off\r\n@rem Передаёт переменную с русским путём из исходного окружения.\r\n@set "ISE_TEST_RESULT=%FW_TEST_UNICODE%"\r\n'.encode("utf-8")) # Моделирует штатный settings.bat без настоящей установки ISE.
|
|
expected = str(self.root / "русская папка") # Результат не должен зависеть от OEM-кодировки Windows.
|
|
with patch.dict(os.environ, {"FW_TEST_UNICODE": expected}), patch.object(ise.shutil, "which", return_value=None): # Принуждает настройку окружения через settings.bat.
|
|
environment = ise.ise_environment({"ise_settings": str(settings)}) # Исполняет настоящий CMD /u с захватом SET.
|
|
self.assertEqual(environment["ISE_TEST_RESULT"], expected) # Декодирование UTF-16LE должно сохранить каждый символ пути.
|
|
|
|
@unittest.skipUnless(os.name == "nt", "Windows BAT execution") # Реальный запуск обвязок требует командного процессора Windows.
|
|
def test_real_installer_and_local_bat_check(self): # Проверяет запуск пользователем без ручного копирования файлов или длинной Tcl-команды.
|
|
environment = os.environ.copy() # Наследует Python launcher и стандартные системные программы.
|
|
environment["FW_NO_PAUSE"] = "1" # Автотест не должен ожидать клавиши установщика.
|
|
setup = subprocess.run([environment.get("COMSPEC", "cmd.exe"), "/d", "/c", "call", str(HERE / "INSTALL_ISE.bat"), str(self.project)], env=environment, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # CALL сохраняет кавычки имени BAT; аргумент проекта содержит пробелы и амперсанд.
|
|
self.assertEqual(setup.returncode, 0, setup.stderr.decode(errors="replace")) # Файлы должен создать сам установщик.
|
|
stub = self.root / "stub bin" # Создаёт отдельный PATH для проверки наличия xtclsh.
|
|
stub.mkdir() # Не подменяет системную установку ISE.
|
|
(stub / "xtclsh.bat").write_text("@rem Только маркер наличия ISE для --check.\n@exit /b 99\n", encoding="utf-8") # Проверка не должна выполнять эту заглушку.
|
|
environment["PATH"] = str(stub) + os.pathsep + environment.get("PATH", "") # Добавляет тестовую САПР перед остальными программами.
|
|
before = (self.root / "firmware_identity.json").read_bytes() # Сохраняет номер перед запуском установленного BAT.
|
|
run = subprocess.run([environment.get("COMSPEC", "cmd.exe"), "/d", "/c", "call", str(self.root / "build_firmware.bat"), "--check"], cwd=HERE, env=environment, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # Рабочий каталог специально отличается от каталога проекта; CALL сохраняет кавычки пути BAT.
|
|
self.assertEqual(run.returncode, 0, run.stderr.decode(errors="replace")) # Локальная Python-обвязка должна найти настройку и библиотеку самостоятельно.
|
|
self.assertEqual((self.root / "firmware_identity.json").read_bytes(), before) # Проверка установленного комплекта не увеличивает версию.
|
|
|
|
|
|
if __name__ == "__main__": # Поддерживает прямой запуск тестов установщика.
|
|
unittest.main() # Возвращает ненулевой код при ошибке установки или потере настроек.
|