Подключи ISE, Vivado и Quartus к общему итератору версии и HDL-паспорту. Проверяй новый bit/sof до создания manifest, SHA-256 и архива выпуска. Добавь установку локальных команд ISE, инструкции переноса и регрессионные проверки успешных и прерванных сборок.
97 lines
14 KiB
Python
97 lines
14 KiB
Python
"""Исполняет реальные Tcl-сценарии с заменой только команд производителей САПР.""" # Проверяет порядок, пути с пробелами и блокирование post-build.
|
|
import json # Проверяет, сколько номеров занял каждый запуск сценария.
|
|
import os # Временно задаёт путь минимальной инициализации Tcl без GUI.
|
|
from pathlib import Path # Находит сценарии относительно тестов.
|
|
import sys # Передаёт Tcl путь к тому же Python 3, которым выполняется тест.
|
|
import tempfile # Изолирует проекты и готовые образы каждого запуска.
|
|
import unittest # Организует успешные и ошибочные сценарии трёх САПР.
|
|
import tkinter # Предоставляет настоящий Tcl-интерпретатор без открытия окна Tk.
|
|
SCRIPTS = Path(__file__).resolve().parents[1] # Находит поставку FPGA-сценариев.
|
|
|
|
|
|
class FlowTests(unittest.TestCase): # Проверяет Tcl-управление без подмены генерации паспорта и архивирования.
|
|
def run_flow(self, toolchain, language="verilog", failure="", revision=""): # Исполняет один полный flow во временном проекте.
|
|
scratch = SCRIPTS.parents[2] / ".codex-build" # Каталог внутри репозитория исключает внешние junction AppData при проверке Tcl на Windows.
|
|
scratch.mkdir(exist_ok=True) # Хранит временные проекты в уже игнорируемой области сборки.
|
|
with tempfile.TemporaryDirectory(prefix="FPGA flow пробел ", dir=scratch) as temporary: # Кириллица и пробелы должны пережить Tcl exec и Python CLI.
|
|
root = Path(temporary) # Не использует настоящие проекты или настройки САПР.
|
|
project = root / {"ise": "board.xise", "vivado": "board.xpr", "quartus": "board.qpf"}[toolchain] # Создаёт только входной маркер нужного типа проекта.
|
|
project.touch() # Открытие моделируется командами производителя, наличие проверяет реальный common.tcl.
|
|
config = root / "identity.json" # Хранит реальный сохраняемый счётчик.
|
|
config.write_text(json.dumps({"product": "Плата", "target": "test", "version": [0, 1, 0], "increment_version": "true" if failure == "prepare" else True}), encoding="utf-8") # Ошибка типа флага должна остановить prepare до резервирования; штатный flow занимает 0.1.1.
|
|
image = root / ("board.sof" if toolchain == "quartus" else "board.bit") # Совпадает с нативным форматом сценария.
|
|
image.write_bytes(b"old image") # Позволяет проверить отказ упаковки старого файла после сбоя сборки.
|
|
previous = Path.cwd() # Настоящий Tcl cd меняет текущий каталог процесса Python.
|
|
script = os.path.relpath(SCRIPTS / (toolchain + ".tcl"), previous) # Относительный путь проверяет нахождение Python-helper после перехода в другой каталог проекта.
|
|
interpreter = tkinter.Tcl() # Выполняет установленный Tcl без окна и без запуска xtclsh/Vivado/Quartus.
|
|
calls = [] # Собирает последовательность только вендорных команд.
|
|
def vendor(name, *arguments): # Моделирует интерфейс САПР, сохраняя настоящий exec Python-инструмента.
|
|
calls.append((name, *arguments)) # Позволяет проверить порядок подготовки и запуска дочерних шагов.
|
|
if name == "current_run": # В Vivado выбирается активная реализация с нестандартным именем.
|
|
return "impl_board" # Проверяет отсутствие жёсткой привязки к impl_1.
|
|
if name == "get_runs": # Возвращает имя объекта run без лишних преобразований.
|
|
return arguments[-1] # Достаточно для чтения PARENT, SRCSET и PROGRESS.
|
|
if name == "get_property": # Моделирует свойства выбранного синтеза и реализации.
|
|
field, target = arguments # Сохраняет различие родительского синтеза и активного в GUI.
|
|
if field == "PARENT": # Реализация может зависеть от другого синтеза.
|
|
return "synth_board" # Сценарий должен выбрать этого родителя.
|
|
if field == "SRCSET": # Новый модуль должен попасть именно в исходники родителя реализации.
|
|
return "sources_board" # Проверяет выбор набора вместо предположения sources_1.
|
|
return "0%" if failure == target else "100%" # Ожидание завершения при ошибке не считается успехом.
|
|
finished = name == "execute_flow" or name == "process" or (name == "wait_on_run" and arguments[0] == "impl_board") # Определяет место записи результата компиляции.
|
|
if finished and not failure: # Только успешный flow создаёт новый файл.
|
|
image.write_bytes(b"new image from " + toolchain.encode("ascii")) # Имитирует точные байты нового .bit/.sof.
|
|
if name == "process": # ISE сообщает отказ логическим результатом без Tcl error.
|
|
return "false" if failure else "true" # Проверяет особую обработку boolean в ise.tcl.
|
|
if name == "execute_flow" and failure: # Quartus сообщает ошибку компиляции Tcl-исключением.
|
|
raise RuntimeError("test compiler failure") # Вызывает catch в настоящем quartus.tcl.
|
|
return "" # Остальные команды производителя успешно завершаются без значения.
|
|
for name in ("project", "xfile", "process", "open_project", "current_run", "get_property", "get_runs", "add_files", "update_compile_order", "reset_run", "launch_runs", "wait_on_run", "close_project", "load_package", "project_open", "set_global_assignment", "execute_flow", "project_close"): # Подменяет только API производителя, не команды ядра Tcl.
|
|
interpreter.createcommand(name, lambda *arguments, name=name: vendor(name, *arguments)) # Привязывает имя при создании каждой callback-функции.
|
|
interpreter.eval('proc exit {code} {error "flow-exit:$code"}') # Не позволяет Tcl exit завершить процесс тестов.
|
|
interpreter.call("package", "provide", "::quartus::project", "1.0") # Удовлетворяет require пакета, чьи команды заменены заглушками.
|
|
arguments = [str(project), str(config), str(root / "build"), str(image), language, sys.executable] # Передаёт пути списком, без shell-экранирования.
|
|
if revision: # Quartus допускает дополнительное имя аппаратной ревизии.
|
|
arguments.append(revision) # Проверяет передачу значения с пробелами одним аргументом.
|
|
interpreter.call("set", "quartus(args)" if toolchain == "quartus" else "argv", tuple(arguments)) # Создаёт штатный источник аргументов соответствующей САПР.
|
|
try: # Гарантирует восстановление cwd даже при ожидаемом exit 1.
|
|
if failure: # Неудачная компиляция должна прервать сценарий до упаковки.
|
|
with self.assertRaisesRegex(tkinter.TclError, "flow-exit:1"): # Проверяет внешнее состояние отказа.
|
|
interpreter.call("source", script) # Исполняет настоящий сценарий со всеми проверками.
|
|
else: # Успешный сценарий обязан дойти до архива.
|
|
interpreter.call("source", script) # Реальный exec создаёт HDL, manifest и архив.
|
|
self.assertEqual(json.loads(config.read_text(encoding="utf-8"))["version"], [0, 1, 0] if failure == "prepare" else [0, 1, 1]) # Ошибка конфигурации не занимает номер; после начала сборки номер резервируется ровно один раз.
|
|
manifests = list((root / "build/firmware").glob("*/*.manifest.json")) # Ищет только выпущенные архивы текущего запуска.
|
|
self.assertEqual(len(manifests), 0 if failure else 1) # Ошибка синтеза/реализации не должна выпустить старый образ.
|
|
if failure == "prepare": # Ошибка общего Python pre-build должна блокировать запуск САПР.
|
|
self.assertFalse(any(call[0] in ("process", "launch_runs", "execute_flow") for call in calls)) # Не допускает синтез с прежним паспортом после ошибки настроек.
|
|
if not failure: # Дополнительно проверяет выбранный HDL после реального prepare.
|
|
suffix = "v" if language == "verilog" else "vhd" # В проект регистрируется только выбранный вариант.
|
|
self.assertTrue(any(str(root / "build/generated" / ("firmware_identity." + suffix)).replace("\\", "/") in str(call).replace("\\\\", "/") for call in calls)) # Учитывает Windows-разделители в строковом представлении кортежей.
|
|
finally: # Tcl cd не должен влиять на следующий тест или временную очистку.
|
|
os.chdir(previous) # Возвращает исходный каталог процесса до удаления проекта.
|
|
return calls # Отдаёт последовательность команд для специфических проверок САПР.
|
|
|
|
def test_all_toolchains_and_languages(self): # Каждый сценарий должен выпустить файл для обеих HDL-реализаций.
|
|
for toolchain in ("ise", "vivado", "quartus"): # Покрывает три независимых набора Tcl-команд.
|
|
for language in ("verilog", "vhdl"): # Проверяет правильное расширение и тип назначения исходника.
|
|
with self.subTest(toolchain=toolchain, language=language): # Показывает точную конфигурацию возможного отказа.
|
|
self.run_flow(toolchain, language) # Создаёт реальный паспорт и архив без лицензии производителя.
|
|
|
|
def test_failures_do_not_archive_old_images(self): # Проверяет false ISE, ошибку Quartus и неуспешные runs Vivado.
|
|
for toolchain, failure in (("ise", "compile"), ("quartus", "compile"), ("vivado", "synth_board"), ("vivado", "impl_board"), ("ise", "prepare"), ("vivado", "prepare"), ("quartus", "prepare")): # Отдельно покрывает обе стадии Vivado и отказ Python pre-build во всех САПР.
|
|
with self.subTest(toolchain=toolchain, failure=failure): # Сохраняет контекст ожидаемой ошибки.
|
|
self.run_flow(toolchain, failure=failure) # Старый файл существует, но архив не должен появиться.
|
|
|
|
def test_revision_and_parent_synthesis(self): # Проверяет нестандартные имена конфигураций проектов.
|
|
quartus = self.run_flow("quartus", revision="board revision B") # Явная ревизия содержит пробелы и передаётся единым аргументом.
|
|
self.assertTrue(any(call[:3] == ("project_open", "-revision", "board revision B") for call in quartus)) # Не допускает случайной сборки default revision.
|
|
vivado = self.run_flow("vivado") # Активная реализация зависит от синтеза с нестандартным именем.
|
|
self.assertIn(("launch_runs", "synth_board"), vivado) # Собирается именно родитель выбранной реализации.
|
|
self.assertIn(("update_compile_order", "-fileset", "sources_board"), vivado) # Паспорт относится к набору исходников этого синтеза.
|
|
self.assertLess(vivado.index(("wait_on_run", "synth_board")), vivado.index(("launch_runs", "impl_board", "-to_step", "write_bitstream"))) # Реализация начинается только после ожидания синтеза.
|
|
|
|
|
|
if __name__ == "__main__": # Поддерживает прямой запуск файла проверки.
|
|
unittest.main() # Возвращает ненулевой код при нарушении порядка сборки.
|