From ab6d2f1393ddec191355e7124307e4295c1b1e9a Mon Sep 17 00:00:00 2001 From: Ezra Maccabee Date: Mon, 6 Apr 2026 18:22:58 +0300 Subject: [PATCH] # Refactoring: conftest.py --- .env.example | 26 +++--- tools/hil/REFACTORED.md | 145 ++++++++++++++++++++++++++++++++ tools/hil/conftest.py | 179 ++++++++++++++++++---------------------- tools/hil/env_config.py | 14 +++- 4 files changed, 252 insertions(+), 112 deletions(-) create mode 100644 tools/hil/REFACTORED.md diff --git a/.env.example b/.env.example index 796c8bc..f96eebb 100644 --- a/.env.example +++ b/.env.example @@ -28,7 +28,6 @@ TARGET_CFG=target/imxrt.cfg GDB_PORT=3333 GDB_EXECUTABLE=arm-none-eabi-gdb - # pyOCD — таргет и частота для gdbserver и flash_swd.py PYOCD_TARGET=mimxrt1050_quadspi PYOCD_FREQUENCY=4000000 @@ -37,16 +36,23 @@ PYOCD_FREQUENCY=4000000 FCB_PATH=tools/host/dcd/w25q128_fdcb.bin # --- HIL (аппаратный стенд) --- -# Порты: macOS = /dev/cu.usbmodem*, Linux = /dev/ttyACM* -HIL_VCOM_PORT=/dev/cu.usbmodemGUXFBWDJBWTGQ3 -HIL_M5_PORT=/dev/cu.usbmodem211101 -HIL_VCOM_BAUD=115200 -HIL_M5_BAUD=115200 -HIL_READY_TIMEOUT=5.0 -HIL_M5_TIMEOUT=3.0 HIL_PYOCD_FREQUENCY=1000000 HIL_BUILD_DIR=build/target-debug -# Настройки порта USB CDC ACM на таргете -HIL_USB_CDC_PORT=/dev/cu.usbmodemZZZZ + +# Задержка после включения питания таргета перед очередным тестом (мс стабилизации + POR) +HIL_TARGET_POWER_SETTLE_S=1.5 + +# VCOM на программаторе NXP MCU-Link +HIL_VCOM_PORT=/dev/cu.usbmodemGUXFBWDJBWTGQ3 +HIL_VCOM_BAUD=115200 +HIL_READY_TIMEOUT=5.0 + +# M5Stack StamPLC (промежуточная платформа для HIL) +HIL_M5_PORT=/dev/cu.usbmodem211101 +HIL_M5_BAUD=115200 +HIL_M5_TIMEOUT=3.0 + +# VCOM на плате таргета +HIL_USB_CDC_PORT=/dev/cu.usbmodem21201 HIL_USB_CDC_BAUD=115200 HIL_USB_CDC_TIMEOUT=5.0 \ No newline at end of file diff --git a/tools/hil/REFACTORED.md b/tools/hil/REFACTORED.md new file mode 100644 index 0000000..a8c79d7 --- /dev/null +++ b/tools/hil/REFACTORED.md @@ -0,0 +1,145 @@ +# Рефакторинг `conftest.py` — HIL-тесты MIMXRT1052 + +## Обзор + +В ходе code review были выявлены и устранены 3 проблемы: +утечка ресурсов при исключениях, дублирование кода фикстур UART, +несогласованное чтение конфигурации. + +--- + +## 1. Утечка UART-порта при исключении + +**Проблема.** Функция `_open_uart_and_wait_ready` открывала `serial.Serial`, +но при исключении внутри цикла ожидания `READY` (например, `SerialException`) +порт не закрывался — операционная система удерживала дескриптор до завершения +процесса. + +**Решение.** Функция заменена на контекстный менеджер `_uart_context`, +реализованный через `@contextmanager`. Блок `finally` гарантирует вызов +`ser.close()` при любом исходе. + +```python +# До +def _open_uart_and_wait_ready(request) -> serial.Serial: + ser = serial.Serial(port=port, ...) + # ... если исключение здесь — ser не закрыт + return ser + +# После +```python +def _uart_context(port: str, baud: int, ready_timeout: float): + ser = serial.Serial(port=port, baudrate=baud, timeout=2.0, write_timeout=1.0) + try: + # … ожидание READY + yield ser + finally: + ser.close() # выполняется всегда +``` + +Аналогичная правка применена к фикстуре `usb_cdc_port` — добавлен +`try/finally` вокруг `yield ser`. + +--- + +## 2. Дублирование `uart_*` фикстур + +**Проблема.** Пять фикстур (`uart`, `uart_opto`, `uart_can`, `uart_button`, +`uart_hil_usb_cdc`) имели идентичное тело и отличались только зависимостью +`loaded_*`. При добавлении нового теста требовалось вручную копировать +очередную фикстуру. + +**Решение.** Введена фабричная функция `_make_uart_fixture` и словарь +`_UART_FIXTURE_MAP`. Все фикстуры генерируются в одну строку через `globals()`. +Зависимость `loaded_*` активируется через `request.getfixturevalue()` — +официальный pytest API (доступен с pytest 3.x). + +```python +# До — пять одинаковых блоков +def uart_opto(request, loaded_hil_opto): + ser = _open_uart_and_wait_ready(request) + yield ser + ser.close() + +def uart_can(request, loaded_hil_can): + ser = _open_uart_and_wait_ready(request) + yield ser\ + ser.close() + +# и т.д. + +# После Фабрика + словарь +_UART_FIXTURE_MAP = { + "uart": "loaded_host_uart", + "uart_opto": "loaded_hil_opto", + "uart_can": "loaded_hil_can", + "uart_button": "loaded_hil_button", + "uart_hil_usb_cdc": "loaded_hil_usb_cdc", +} +def _make_uart_fixture(loaded_name: str): + @pytest.fixture(scope="module") + def _fixture(request: pytest.FixtureRequest) -> Generator[serial.Serial, None, None]: + request.getfixturevalue(loaded_name) # триггерит зависимость явно + port = request.config.getoption("--vcom") + with _uart_context(port, cfg.VCOM_BAUD, cfg.READY_TIMEOUT) as ser: + yield ser + return _fixture + +# Регистрируем все фикстуры в пространстве имён модуля одной строкой +for _name, _dep in _UART_FIXTURE_MAP.items(): + globals()[_name] = _make_uart_fixture(_dep) +``` + +Для добавления поддержки нового теста теперь достаточно одной строки +в `_UART_FIXTURE_MAP`. + +--- + +## 3. Несогласованное чтение конфигурации + +**Проблема.** Большинство параметров читались через `env_config` (`cfg.*`), +но фикстура `usb_cdc_port` обращалась к `os.environ.get()` напрямую. +Это создавало три источника истины, рассыпало дефолтные значения по коду +и лишало возможности переопределить параметр через CLI pytest. + +```python +# До — нарушает единообразие конфигурации +port = os.environ.get("HIL_USB_CDC_PORT", "") +baud = int(os.environ.get("HIL_USB_CDC_BAUD", "115200")) +timeout = float(os.environ.get("HIL_USB_CDC_TIMEOUT", "5.0")) + +# После +@pytest.fixture(scope="module") +def usb_cdc_port( + uart_hil_usb_cdc, +) -> Generator[serial.Serial, None, None]: + """Открыть USB CDC порт таргета. Ждёт появления порта и DTR ready.""" + import time + + if not cfg.TARGET_VCOM_PORT: + pytest.skip("HIL_USB_CDC_PORT not set") + + # Ждём появления USB CDC порта (enumeration после загрузки ELF). + deadline = time.monotonic() + cfg.TARGET_VCOM_TIMEOUT + ser = None + while time.monotonic() < deadline: + try: + ser = serial.Serial(port=cfg.TARGET_VCOM_PORT, baudrate=cfg.TARGET_VCOM_BAUD, timeout=0.5) + break + except serial.SerialException: + time.sleep(0.3) + + if ser is None: + pytest.fail(f"USB CDC port {cfg.TARGET_VCOM_PORT} not available after {cfg.TARGET_VCOM_TIMEOUT}s") + + try: + # Установить DTR чтобы firmware увидела DTE presence. + ser.dtr = True + time.sleep(0.3) + # Сбросить входной буфер — могут быть мусорные байты от enumeration. + ser.reset_input_buffer() + yield ser + finally: + ser.close() + +``` diff --git a/tools/hil/conftest.py b/tools/hil/conftest.py index 7a1b6a4..190f928 100644 --- a/tools/hil/conftest.py +++ b/tools/hil/conftest.py @@ -11,17 +11,14 @@ import logging import time from pathlib import Path from typing import Generator - +from contextlib import contextmanager import pytest import serial import env_config as cfg from pyocd_utils import flexram_init, load_elf, open_target, run_from_vectors -import os -log = logging.getLogger(__name__) -# Задержка после включения питания таргета (мс стабилизации + POR) -_POWER_ON_SETTLE_S = 1.5 +log = logging.getLogger(__name__) # --------------------------------------------------------------------------- @@ -56,37 +53,26 @@ def _load_elf(request: pytest.FixtureRequest, default_elf: Path) -> None: # --------------------------------------------------------------------------- # Общая логика UART: открыть порт, дождаться READY # --------------------------------------------------------------------------- -def _open_uart_and_wait_ready(request: pytest.FixtureRequest) -> serial.Serial: - """Открыть VCOM, дождаться READY от прошивки.""" - port = request.config.getoption("--vcom") - log.info("UART %s @ %d baud", port, cfg.VCOM_BAUD) +@contextmanager +def _uart_context(port: str, baud: int, ready_timeout: float): + ser = serial.Serial(port=port, baudrate=baud, timeout=2.0, write_timeout=1.0) + try: + deadline = time.monotonic() + ready_timeout + ready = False + while time.monotonic() < deadline: + line = ser.readline().decode("ascii", errors="replace").strip() + if line == "READY": + ready = True + log.info("Получен READY от прошивки") + break - ser = serial.Serial( - port=port, - baudrate=cfg.VCOM_BAUD, - timeout=2.0, - write_timeout=1.0, - ) + if not ready: + pytest.fail(f"Прошивка не отправила READY за {ready_timeout} с") - # Ждём READY - deadline = time.monotonic() + cfg.READY_TIMEOUT - ready = False - while time.monotonic() < deadline: - line = ser.readline().decode("ascii", errors="replace").strip() - if line == "READY": - ready = True - log.info("Получен READY от прошивки") - break - - if not ready: - ser.close() - pytest.fail( - f"Прошивка не отправила READY за {cfg.READY_TIMEOUT} с — " - "проверьте VCOM-порт и bsp_uart_host_init()." - ) - - ser.reset_input_buffer() - return ser + ser.reset_input_buffer() + yield ser + finally: + ser.close() # гарантированно, всегда # --------------------------------------------------------------------------- @@ -196,8 +182,8 @@ def m5(request: pytest.FixtureRequest) -> Generator[M5Agent, None, None]: # Включаем питание таргета и ждём стабилизации agent.power(True) - log.info("Питание таргета включено, ждём %.1f с", _POWER_ON_SETTLE_S) - time.sleep(_POWER_ON_SETTLE_S) + log.info("Питание таргета включено, ждём %.1f с", cfg.POWER_ON_SETTLE_S) + time.sleep(cfg.POWER_ON_SETTLE_S) agent.opto_all_off() # безопасное начальное состояние yield agent @@ -221,6 +207,13 @@ def m5(request: pytest.FixtureRequest) -> Generator[M5Agent, None, None]: @pytest.fixture(scope="module") def loaded_host_uart(request: pytest.FixtureRequest, m5: M5Agent) -> None: + """ + Загрузить test_hil_button.elf на таргет по SWD + + Явная зависимость от m5 гарантирует порядок: + 1. m5 создаётся первым → питание таргета включено + 2. только потом pyOCD подключается и грузит ELF + """ _load_elf( request, Path(cfg.BUILD_DIR) / "tests/target/host_uart/test_host_uart.elf", @@ -228,6 +221,13 @@ def loaded_host_uart(request: pytest.FixtureRequest, m5: M5Agent) -> None: @pytest.fixture(scope="module") def loaded_hil_button(request: pytest.FixtureRequest, m5: M5Agent) -> None: + """ + Загрузить test_hil_button.elf на таргет по SWD + + Явная зависимость от m5 гарантирует порядок: + 1. m5 создаётся первым → питание таргета включено + 2. только потом pyOCD подключается и грузит ELF + """ _load_elf( request, Path(cfg.BUILD_DIR) / "tests/target/hil_button/test_hil_button.elf", @@ -236,7 +236,7 @@ def loaded_hil_button(request: pytest.FixtureRequest, m5: M5Agent) -> None: @pytest.fixture(scope="module") def loaded_hil_opto(request: pytest.FixtureRequest, m5: M5Agent) -> None: """ - Загрузить test_hil_opto.elf. + Загрузить test_hil_opto.elf на таргет по SWD Явная зависимость от фикстуры m5 гарантирует порядок: 1. m5 создаётся первым → питание таргета включено @@ -252,7 +252,7 @@ def loaded_hil_opto(request: pytest.FixtureRequest, m5: M5Agent) -> None: @pytest.fixture(scope="module") def loaded_hil_can(request: pytest.FixtureRequest, m5: M5Agent) -> None: """ - Загрузить test_hil_can.elf. + Загрузить test_hil_can.elf на таргет по SWD Явная зависимость от m5 гарантирует порядок: 1. m5 создаётся первым → питание таргета включено @@ -265,60 +265,43 @@ def loaded_hil_can(request: pytest.FixtureRequest, m5: M5Agent) -> None: @pytest.fixture(scope="module") def loaded_hil_usb_cdc(request: pytest.FixtureRequest, m5: M5Agent) -> None: + """ + Загрузить test_hil_usb_cdc.elf на таргет по SWD + + Явная зависимость от m5 гарантирует порядок: + 1. m5 создаётся первым → питание таргета включено + 2. только потом pyOCD подключается и грузит ELF + """ _load_elf( request, Path(cfg.BUILD_DIR) / "tests/target/hil_usb_cdc/test_hil_usb_cdc.elf", ) # --------------------------------------------------------------------------- -# Фикстуры UART +# Фикстуры UART (установление соединения с VCOM программатора NXP MCU-Link) +# Выполняется один раз на тест # --------------------------------------------------------------------------- -@pytest.fixture(scope="module") -def uart( - request: pytest.FixtureRequest, - loaded_host_uart, -) -> Generator[serial.Serial, None, None]: - ser = _open_uart_and_wait_ready(request) - yield ser - ser.close() +_UART_FIXTURE_MAP = { + "uart": "loaded_host_uart", + "uart_opto": "loaded_hil_opto", + "uart_can": "loaded_hil_can", + "uart_button": "loaded_hil_button", + "uart_hil_usb_cdc": "loaded_hil_usb_cdc", +} +def _make_uart_fixture(loaded_name: str): + @pytest.fixture(scope="module") + def _fixture(request: pytest.FixtureRequest) -> Generator[serial.Serial, None, None]: + request.getfixturevalue(loaded_name) # триггерит зависимость явно + port = request.config.getoption("--vcom") + with _uart_context(port, cfg.VCOM_BAUD, cfg.READY_TIMEOUT) as ser: + yield ser + return _fixture -@pytest.fixture(scope="module") -def uart_opto( - request: pytest.FixtureRequest, - loaded_hil_opto, -) -> Generator[serial.Serial, None, None]: - ser = _open_uart_and_wait_ready(request) - yield ser - ser.close() - -@pytest.fixture(scope="module") -def uart_can( - request: pytest.FixtureRequest, - loaded_hil_can, -) -> Generator[serial.Serial, None, None]: - ser = _open_uart_and_wait_ready(request) - yield ser - ser.close() - -@pytest.fixture(scope="module") -def uart_button( - request: pytest.FixtureRequest, - loaded_hil_button, -) -> Generator[serial.Serial, None, None]: - ser = _open_uart_and_wait_ready(request) - yield ser - ser.close() - -@pytest.fixture(scope="module") -def uart_hil_usb_cdc( - request: pytest.FixtureRequest, - loaded_hil_usb_cdc, -) -> Generator[serial.Serial, None, None]: - ser = _open_uart_and_wait_ready(request) - yield ser - ser.close() +# Регистрируем все фикстуры в пространстве имён модуля одной строкой +for _name, _dep in _UART_FIXTURE_MAP.items(): + globals()[_name] = _make_uart_fixture(_dep) # --------------------------------------------------------------------------- -# hil_usb_cdc — USB CDC ACM тест (два канала: UART + USB CDC) +# Фикстуры USB CDC (открытие порта, установка DTR) # --------------------------------------------------------------------------- @pytest.fixture(scope="module") def usb_cdc_port( @@ -327,35 +310,31 @@ def usb_cdc_port( """Открыть USB CDC порт таргета. Ждёт появления порта и DTR ready.""" import time - port = os.environ.get("HIL_USB_CDC_PORT", "") - if not port: + if not cfg.TARGET_VCOM_PORT: pytest.skip("HIL_USB_CDC_PORT not set") - baud = int(os.environ.get("HIL_USB_CDC_BAUD", "115200")) - timeout_s = float(os.environ.get("HIL_USB_CDC_TIMEOUT", "5.0")) - # Ждём появления USB CDC порта (enumeration после загрузки ELF). - deadline = time.monotonic() + timeout_s + deadline = time.monotonic() + cfg.TARGET_VCOM_TIMEOUT ser = None while time.monotonic() < deadline: try: - ser = serial.Serial(port, baud, timeout=0.5) + ser = serial.Serial(port=cfg.TARGET_VCOM_PORT, baudrate=cfg.TARGET_VCOM_BAUD, timeout=0.5) break except serial.SerialException: time.sleep(0.3) if ser is None: - pytest.fail(f"USB CDC port {port} not available after {timeout_s}s") + pytest.fail(f"USB CDC port {cfg.TARGET_VCOM_PORT} not available after {cfg.TARGET_VCOM_TIMEOUT}s") - # Установить DTR чтобы firmware увидела DTE presence. - ser.dtr = True - time.sleep(0.3) - - # Сбросить входной буфер — могут быть мусорные байты от enumeration. - ser.reset_input_buffer() - - yield ser - ser.close() + try: + # Установить DTR чтобы firmware увидела DTE presence. + ser.dtr = True + time.sleep(0.3) + # Сбросить входной буфер — могут быть мусорные байты от enumeration. + ser.reset_input_buffer() + yield ser + finally: + ser.close() # --------------------------------------------------------------------------- diff --git a/tools/hil/env_config.py b/tools/hil/env_config.py index 19cc159..99a866d 100644 --- a/tools/hil/env_config.py +++ b/tools/hil/env_config.py @@ -20,12 +20,22 @@ def _get(key: str, default: str) -> str: BUILD_DIR: str = _get("HIL_BUILD_DIR", str(_REPO_ROOT / "build" / "target-debug")) +PYOCD_FREQUENCY: int = int(_get("HIL_PYOCD_FREQUENCY", "1000000")) + + +POWER_ON_SETTLE_S = float(_get("HIL_TARGET_POWER_SETTLE_S", "1.5")) + +# VCOM на программаторе NXP MCU-Link VCOM_PORT: str = _get("HIL_VCOM_PORT", "/dev/ttyACM0") VCOM_BAUD: int = int(_get("HIL_VCOM_BAUD", "115200")) READY_TIMEOUT: float = float(_get("HIL_READY_TIMEOUT", "5.0")) -PYOCD_FREQUENCY: int = int(_get("HIL_PYOCD_FREQUENCY", "1000000")) # M5Stack StamPLC (промежуточная платформа для HIL) M5_PORT: str = _get("HIL_M5_PORT", "/dev/ttyACM1") M5_BAUD: int = int(_get("HIL_M5_BAUD", "115200")) -M5_TIMEOUT: float = float(_get("HIL_M5_TIMEOUT", "3.0")) \ No newline at end of file +M5_TIMEOUT: float = float(_get("HIL_M5_TIMEOUT", "3.0")) + +# VCOM на плате таргета +TARGET_VCOM_PORT:str = _get("HIL_USB_CDC_PORT", "/dev/ttyACM2") +TARGET_VCOM_BAUD:int = int(_get("HIL_USB_CDC_BAUD", "115200")) +TARGET_VCOM_TIMEOUT:float = float(_get("HIL_USB_CDC_TIMEOUT", "3.0")) \ No newline at end of file