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

63 lines
10 KiB
Python

"""Проверка токовых графиков в настоящем DSView без аппаратного захвата.""" # Сценарий открывает готовую синтетическую DSL-запись и сохраняет изображение результата.
import argparse # Пути runtime, захвата и артефактов задаются явно.
import os # Чистое окружение исключает конфликт Qt и Python родительского процесса.
from pathlib import Path # Все артефакты сохраняются в выбранном каталоге проверки.
import re # Подтверждённый HWND извлекается из штатного протокола встраивания.
import subprocess # Проверяется собранный DSView.exe, а не заглушка графического движка.
import time # Ограниченные ожидания позволяют завершить зависший тест без вмешательства в пользовательскую сессию.
def main(): # Проверка управляет только дочерним экземпляром, который сама запустила.
parser = argparse.ArgumentParser(description=__doc__) # Назначение сценария видно при запуске с --help.
parser.add_argument('--runtime', required=True, type=Path) # Каталог должен содержать полный собранный компонент DSView.
parser.add_argument('--capture', required=True, type=Path) # Запись содержит назначения входов и все параметры двигателя.
parser.add_argument('--output', required=True, type=Path) # Изображение и журнал отделены от исходной записи.
parser.add_argument('--timeout', type=float, default=45) # Ожидание полного декодирования ограничено 45 секундами.
args = parser.parse_args() # Все пути читаются до создания окна или процесса.
from PySide6.QtWidgets import QApplication, QWidget # Qt используется только для тестового родительского окна и снимка дочернего компонента.
runtime, capture, output = args.runtime.resolve(), args.capture.resolve(), args.output.resolve() # Относительные параметры не зависят от рабочего каталога дочернего процесса.
output.mkdir(parents=True, exist_ok=True) # Каталог артефактов создаётся отдельно для каждого сценария.
app, window = QApplication([]), QWidget() # Окно воспроизводит штатное встраивание DSView в SETGUI.
window.resize(1500, 1000) # Высоты достаточно для двенадцати цифровых входов и токового графика NPC.
window.setWindowTitle('Induction motor current decoder verification') # Заголовок отличает автоматическую проверку от рабочего окна пользователя.
window.show() # Родитель получает настоящий HWND перед запуском встраиваемого компонента.
env = {key: value for key, value in os.environ.items() if not key.startswith(('PYTHON', 'QT_', 'QML'))} # Нативный компонент загружает собственные Qt и встроенный Python.
env['PATH'] = str(runtime) + os.pathsep + str(Path(os.environ['SystemRoot']) / 'System32') # DLL выбираются из проверяемой поставки и системного каталога Windows.
env['DSVIEW_HOST_SCREENSHOT'] = str(output / 'motor-loading.png') # Встроенный механизм подтверждает фактическое создание и отрисовку окна DSView.
with (output / 'motor-native.log').open('w', encoding='utf-8') as log: # Журнал сохраняет сообщения импорта декодера и завершения обработки записи.
process = subprocess.Popen([str(runtime / 'DSView.exe'), '--loglevel', '3', '--embed-parent', str(int(window.winId())), str(capture)], cwd=runtime, env=env, stdin=subprocess.PIPE, stdout=log, stderr=log, creationflags=subprocess.CREATE_NO_WINDOW) # Запускается только новый тестовый процесс без видимого терминала.
try: # Любая ошибка завершается очисткой именно этого дочернего процесса.
deadline, completed_at, report = time.monotonic() + args.timeout, None, '' # Дополнительная пауза после декодирования позволяет завершить очередную перерисовку.
while time.monotonic() < deadline and process.poll() is None: # Условие ограничивает ожидание по времени и учитывает преждевременное завершение DSView.
app.processEvents() # Родительское Qt-окно продолжает обрабатывать встраивание и отрисовку.
report = (output / 'motor-native.log').read_text(encoding='utf-8', errors='replace') # Чтение журнала не изменяет захват или состояние декодера.
if 'Traceback (most recent call last)' in report: # Ошибка Python-модели должна немедленно провалить нативную проверку.
raise AssertionError('Decoder failed; inspect motor-native.log') # Диагностика остаётся доступной в каталоге артефактов.
ready = re.search(r'DSVIEW_READY (\d+)', report) # Дескриптор принадлежит только созданному тестом компоненту.
if 'Decoded sample count:' in report and completed_at is None: # Сам факт открытия файла недостаточен без полного декодирования.
completed_at = time.monotonic() # Запоминается время фактического окончания расчёта.
window.resize(1501, 1000) # Изменение размера тестового родителя сбрасывает сохранённую Windows поверхность встраиваемого окна перед снимком.
if completed_at is not None and time.monotonic() - completed_at > 1 and ready: # Одна секунда после расчёта даёт Qt возможность показать все рассчитанные кривые.
image = window.screen().grabWindow(int(ready.group(1))) # Снимок берётся непосредственно с HWND проверяемого нативного окна.
assert not image.isNull() and image.save(str(output / 'motor-complete.png')), 'Native screenshot failed' # Пустой или несохранённый результат не считается визуальной проверкой.
break # После сохранения изображения можно штатно закрыть тестовый экземпляр.
time.sleep(0.02) # Двадцать миллисекунд ограничивают нагрузку опроса и не задерживают интерфейс надолго.
assert completed_at is not None and (output / 'motor-complete.png').is_file(), 'Motor decode did not finish; inspect motor-native.log' # Тайм-аут не маскируется успехом запуска EXE.
process.stdin.write(b'CLOSE\n') # Стандартный протокол встраивания закрывает только тестовую сессию.
process.stdin.flush() # Команда немедленно передаётся дочернему процессу.
deadline = time.monotonic() + 5 # Штатное закрытие ограничено пятью секундами.
while time.monotonic() < deadline and process.poll() is None: # Ожидание не блокирует Qt родительского окна.
app.processEvents() # Сообщения уничтожения дочерних окон обрабатываются обычным образом.
time.sleep(0.02) # Частый короткий опрос не создаёт длинной блокирующей паузы.
assert process.poll() == 0, 'DSView did not close cleanly' # Проверяется корректный жизненный цикл нового анализатора.
finally: # Ошибочный тест не должен оставлять свой DSView в фоне.
if process.poll() is None: # Чужие и уже завершённые процессы не затрагиваются.
process.terminate() # Завершается только дочерний процесс текущего сценария.
process.wait(timeout=5) # Освобождение дескриптора процесса ограничено по времени.
window.close() # Тестовое родительское окно больше не требуется.
print(output / 'motor-complete.png') # Готовый снимок можно открыть для визуальной проверки фазных кривых и шкалы ампер.
if __name__ == '__main__': # Импорт сценария не должен сам запускать графический компонент.
main() # Прямой запуск выполняет один нативный тест выбранной DSL-записи.