88 lines
15 KiB
Python
88 lines
15 KiB
Python
"""Собирает нативную проверку и воспроизводит обрыв RJ2 на локальном UDP-эмуляторе.""" # Реальные GUI-объекты выявляют повторные окна, модальность и зависание закрытия.
|
||
import argparse # Явные пути позволяют проверить любой собранный runtime закреплённого DSView.
|
||
import json # Метаданные дисковой записи подтверждают точное число принятых до обрыва отсчётов.
|
||
import os # Окружение тестового процесса очищается от Python и Qt родительского приложения.
|
||
from pathlib import Path # Артефакты и исходники адресуются абсолютными путями.
|
||
import re # Выделяем объекты и библиотеки из созданного CMake файла Ninja.
|
||
import socket # Эмулятор использует тот же UDP-порт и little-endian протокол RJ2, что плата.
|
||
import struct # Заголовок ZLP состоит из четырёх 32-битных слов little endian.
|
||
import subprocess # Проверочный EXE линкуется с настоящими объектами текущей сборки.
|
||
import threading # Эмулятор отвечает параллельно с отдельным процессом Qt.
|
||
import time # Небольшой интервал между UDP-пакетами исключает случайное переполнение loopback.
|
||
import zipfile # После обрыва проверяем сохранённый непрерывный фрагмент аппаратной записи.
|
||
|
||
|
||
def main(): # Отдельный сценарий не требует PySide, тестового устройства и изменения рабочего EXE.
|
||
parser = argparse.ArgumentParser(description=__doc__) # Справка описывает назначение нативной проверки.
|
||
parser.add_argument('--source', type=Path, required=True) # Указываем уже собранный checkout DSView с build-win64.
|
||
parser.add_argument('--runtime', type=Path, required=True) # Полный runtime предоставляет DLL, ресурсы и декодеры.
|
||
parser.add_argument('--output', type=Path, required=True) # Все новые файлы и записи сохраняются в отдельном каталоге.
|
||
parser.add_argument('--msys-root', type=Path, default=Path('C:/msys64')) # SDK совпадает с использованным при основной сборке UCRT64.
|
||
args = parser.parse_args() # Ошибочные параметры отклоняются до запуска эмулятора и компилятора.
|
||
source, runtime, output = (path.resolve() for path in (args.source, args.runtime, args.output)) # Исключаем зависимость путей от cwd компилятора.
|
||
output.mkdir(parents=True, exist_ok=True) # Сценарий не удаляет прежние артефакты и не затрагивает записи пользователя.
|
||
build = source / 'build-win64' # Это штатный каталог сборки канонического рецепта.
|
||
sdk = args.msys_root.resolve() / 'ucrt64/bin' # В UCRT64 находятся компилятор и Ninja той же ABI.
|
||
env = dict(os.environ, PATH=str(sdk) + os.pathsep + os.environ.get('PATH', '')) # Компилятор должен найти свои DLL без изменения системного PATH.
|
||
commands = subprocess.check_output([str(sdk / 'ninja.exe'), '-t', 'commands', 'CMakeFiles/DSView.dir/DSView/main.cpp.obj'], cwd=build, env=env, text=True) # Берём настоящие include и define текущей сборки.
|
||
compile_flags = commands.strip().splitlines()[-1].split(' -MD ')[0] # Убираем генерацию зависимостей рабочего main.cpp, оставляя все флаги компилятора.
|
||
obj = output / 'check_link_loss.obj' # Проверочный main хранится отдельно от объектов рабочего приложения.
|
||
subprocess.run(compile_flags + ' -o ' + subprocess.list2cmdline([str(obj)]) + ' -c ' + subprocess.list2cmdline([str(Path(__file__).with_suffix('.cpp'))]), cwd=build, env=env, check=True) # Компилируем тест тем же C++11 и заголовками Qt5.
|
||
ninja = (build / 'build.ninja').read_text(encoding='utf-8') # Метаданные Ninja перечисляют все реальные объекты и библиотеки DSView.
|
||
edge = next(line for line in ninja.splitlines() if line.startswith('build ') and 'CXX_EXECUTABLE_LINKER' in line) # В рецепте есть единственный пользовательский EXE.
|
||
objects = re.findall(r'CMakeFiles/[^ ]+\.obj', edge.split(' | ')[0]) # Объекты включают ресурсы, moc, аппаратный драйвер и обработчик GUI.
|
||
objects.remove('CMakeFiles/DSView.dir/DSView/main.cpp.obj') # Рабочая точка входа заменяется проверочной без изменения остальных объектов.
|
||
libraries = re.search(r'^ LINK_LIBRARIES = (.+)$', ninja, re.MULTILINE).group(1) # Сохраняем полный штатный набор библиотек, включая Python и GIO.
|
||
executable = runtime / 'check_link_loss.exe' # Штатный поиск ресурсов DSView использует каталог EXE, поэтому тест располагается рядом с ресурсами отдельного проверочного runtime.
|
||
response = output / 'link-loss.rsp' # Response-файл обходит ограничение длины командной строки Windows.
|
||
response.write_text(' '.join(objects) + ' "' + obj.as_posix() + '" ' + libraries, encoding='utf-8') # Прямые слеши сохраняют Windows-путь при разборе response-файла GCC, где обратный слеш служит экранированием.
|
||
subprocess.run([str(sdk / 'c++.exe'), '@' + str(response), '-o', str(executable)], cwd=build, env=env, check=True) # Линкуем те же объекты, что вошли в исправленный DSView.
|
||
udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # Отдельный локальный сокет не подключается к реальной плате.
|
||
udp.bind(('127.0.0.1', 37575)) # Порт 37575 задан общим заголовком zynq_la_pl.h протокола RJ2.
|
||
udp.settimeout(0.1) # Таймаут 100 мс позволяет быстро остановить фоновую фикстуру после завершения EXE.
|
||
stopped, sessions, errors = threading.Event(), set(), [] # Уникальные nonce выявляют самопроизвольные повторные захваты.
|
||
def serve(): # Один поток отвечает на discovery и затем имитирует исчезновение платы во время захвата.
|
||
try: # Ошибки фикстуры передаются главному потоку, чтобы их нельзя было принять за штатный обрыв.
|
||
while not stopped.is_set(): # Внешний сценарий управляет временем жизни только собственного сокета.
|
||
try: request, peer = udp.recvfrom(2048) # Буфер вмещает обычный START и расширенную конфигурацию триггера.
|
||
except socket.timeout: continue # Отсутствие трафика не является ошибкой эмулятора.
|
||
if len(request) < 16: continue # Неполный заголовок не принадлежит протоколу тестовой платы.
|
||
magic, operation, nonce, value = struct.unpack_from('<4I', request) # Формат совпадает с четырьмя little-endian словами драйвера.
|
||
if operation == 0: # Discovery возвращает INFO только до фактического старта данной сессии.
|
||
if nonce not in sessions: udp.sendto(struct.pack('<4I', magic, 0, nonce, 200000000), peer) # Частота 200 МГц разрешена для ZLP2, ZLT и ZLR; после START heartbeat исчезает.
|
||
elif operation == 1 and nonce not in sessions: # Повторная отправка START с тем же nonce не считается новым захватом.
|
||
sessions.add(nonce) # Каждая новая сессия должна появляться только после явного запуска из нативного теста.
|
||
if len(sessions) == 2: # Второй захват обрывается после реально принятых данных.
|
||
for sequence in range(64): # 64 пакета по 1024 отсчёта дают непрерывный префикс длиной 65 536 отсчётов.
|
||
udp.sendto(struct.pack('<4I', magic, 1, nonce, sequence) + bytes([sequence & 255]) * 1024, peer) # Нумерация без пропусков проверяет сохранение хвоста до сетевого таймаута.
|
||
time.sleep(0.001) # Пауза 1 мс предотвращает переполнение очереди UDP при медленном тестовом хосте.
|
||
except Exception as error: errors.append(error) # Ошибка потока обязана завершить итоговую проверку неуспехом.
|
||
thread = threading.Thread(target=serve, daemon=True) # Поток обслуживает только локальную фикстуру текущего запуска.
|
||
thread.start() # Discovery рабочего драйвера выполняется синхронно ещё до первого Qt-таймера.
|
||
child_env = {key: value for key, value in os.environ.items() if not key.startswith(('PYTHON', 'QT_', 'QML'))} # Python и Qt пакета SETGUI не должны подменять ABI нативного DSView.
|
||
child_env['PATH'] = str(runtime) + os.pathsep + str(Path(os.environ['SystemRoot']) / 'System32') # Все DLL берутся из проверяемого runtime.
|
||
child_env['QT_PLUGIN_PATH'] = str(runtime) # EXE находится в каталоге артефактов, а плагины Qt остаются в runtime.
|
||
child_env['PYTHONHOME'] = str(runtime) # Встроенный Python читает стандартную библиотеку именно проверяемого компонента.
|
||
child_env['DSVIEW_CAPTURE_DIR'] = str(output) # Нативный тест сохраняет аппаратные фрагменты и скриншот рядом с отчётом.
|
||
try: # Фоновый сокет освобождается при любом исходе компиляции и запуска.
|
||
checked = subprocess.run([str(executable)], cwd=runtime, env=child_env, capture_output=True, text=True, errors='replace', timeout=40) # Дедлайн 40 секунд включает нативный предел 30 секунд и остановку декодеров.
|
||
(output / 'link-loss.log').write_text(checked.stdout + checked.stderr, encoding='utf-8') # Полный журнал остаётся доступным при ошибке теста.
|
||
print(checked.stdout, end='') # Показываем отдельные результаты трёх аппаратных таймаутов.
|
||
if checked.returncode: raise RuntimeError(f'Native link-loss check failed ({checked.returncode}): {checked.stderr}') # Падение процесса не маскируется успешной работой эмулятора.
|
||
if errors: raise errors[0] # Проверка требует корректного выполнения фоновой фикстуры.
|
||
assert len(sessions) == 3, f'Unexpected automatic captures: {len(sessions)}' # Три ручных запуска должны создать ровно три nonce.
|
||
recordings = list(output.glob('*.dsl')) # Запись после обрыва должна быть опубликована штатным дисковым писателем.
|
||
assert recordings, 'No preserved capture after partial receive' # Потеря связи не должна удалять принятый непрерывный фрагмент.
|
||
for path in recordings: # Проверяем целостность всех опубликованных аппаратных архивов текущего запуска.
|
||
with zipfile.ZipFile(path) as archive: # Метаданные и отсчёты должны относиться к одному завершённому архиву.
|
||
assert archive.testzip() is None # Контроль CRC выявляет незавершённый или повреждённый файл после обрыва.
|
||
assert json.loads(archive.read('zynq_recording.json'))['samples'] == 65536 # Ровно 64 пакета по 1024 отсчёта подтверждают сохранение всего непрерывного префикса.
|
||
print('PASS: three manual captures, preserved DSL, normal close with warning open') # Итог охватывает исходный пользовательский сценарий и сохранность записи.
|
||
finally: # Освобождение фикстуры не зависит от результата нативного процесса.
|
||
stopped.set() # Поток завершится после ближайшего короткого таймаута сокета.
|
||
thread.join(timeout=2) # Предел 2 секунды исключает бессрочное ожидание фонового recvfrom.
|
||
udp.close() # Закрываем только созданный этим сценарием loopback-сокет.
|
||
|
||
|
||
if __name__ == '__main__': main() # Импорт файла не запускает компилятор, сеть или приложение.
|