Files
templates/tools/dsview/tests/check_link_loss.py
T

88 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Собирает нативную проверку и воспроизводит обрыв 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() # Импорт файла не запускает компилятор, сеть или приложение.