diff --git a/docs/DEV_ARCH.md b/docs/DEV_ARCH.md index 669d51f..6b7ed7b 100644 --- a/docs/DEV_ARCH.md +++ b/docs/DEV_ARCH.md @@ -108,14 +108,24 @@ PYOCD_FREQUENCY=4000000 FCB_PATH=tools/host/dcd/w25q128_fdcb.bin # HIL — аппаратный стенд -HIL_VCOM_PORT=/dev/cu.usbmodemXXXX # MCU-Link VCOM (macOS: cu.usbmodem*, Linux: ttyACM*) -HIL_M5_PORT=/dev/cu.usbmodemYYYY # M5StampPLC USB CDC -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 +HIL_TARGET_POWER_SETTLE_S=1.5 # задержка после включения питания (POR + стабилизация) + +# VCOM на программаторе NXP MCU-Link +HIL_VCOM_PORT=/dev/cu.usbmodemXXXX # macOS: cu.usbmodem*, Linux: ttyACM* +HIL_VCOM_BAUD=115200 +HIL_READY_TIMEOUT=5.0 + +# M5Stack StamPLC (промежуточная платформа для HIL) +HIL_M5_PORT=/dev/cu.usbmodemYYYY # M5StampPLC USB CDC +HIL_M5_BAUD=115200 +HIL_M5_TIMEOUT=3.0 + +# VCOM на плате таргета (USB CDC, появляется после загрузки ELF) +HIL_USB_CDC_PORT=/dev/cu.usbmodemZZZZ +HIL_USB_CDC_BAUD=115200 +HIL_USB_CDC_TIMEOUT=5.0 ``` **Как значения попадают в инструменты:** diff --git a/docs/testing/hil/HIL_BENCH.md b/docs/testing/hil/HIL_BENCH.md index f8615ec..aec37dd 100644 --- a/docs/testing/hil/HIL_BENCH.md +++ b/docs/testing/hil/HIL_BENCH.md @@ -75,7 +75,7 @@ _OPTO_TO_RELAY = {1: 3, 2: 4, 3: 2} | Файл | Назначение | |--------------|----------------------------------------------------------------| -| `agent.py` | MicroPython агент на M5. Принимает JSON-команды через USB CDC, управляет реле через I2C экспандер AW9523B | +| `agent.py` | MicroPython агент на M5. Принимает JSON-команды через USB CDC, подает сигналы на таргет | | `cli.py` | Интерактивный CLI для ручного тестирования агента | | `power.py` | Скрипт управления питанием таргета (RLY1) из командной строки | @@ -109,7 +109,6 @@ just host::m5-deploy ``` > ⚠️ После изменения `agent.py` обязательно задеплоить перед запуском тестов. -> Иначе на M5 работает старая версия. ### MCU-Link — загрузка ELF и UART CLI diff --git a/docs/testing/hil/HIL_CREATE_TEST.md b/docs/testing/hil/HIL_CREATE_TEST.md index 87aa1c1..73955fc 100644 --- a/docs/testing/hil/HIL_CREATE_TEST.md +++ b/docs/testing/hil/HIL_CREATE_TEST.md @@ -167,55 +167,59 @@ ls build/target-debug/tests/target//test_.elf ## Шаг 5 — `conftest.py`: добавить фикстуры -Открыть `tools/hil/conftest.py` и добавить в конец раздела с фикстурами загрузки. +Открыть `tools/hil/conftest.py` и добавить: + +1. Фикстуру загрузки `loaded_` в конец раздела загрузок. +2. Одну строку в `_UART_FIXTURE_MAP` — фабрика `_make_uart_fixture` автоматически + создаст фикстуру `uart_` через контекстный менеджер `_uart_context`. ### Базовый тест (без M5) ```python +# 1. Фикстура загрузки — добавить в раздел loaded_* @pytest.fixture(scope="module") -def loaded_(request: pytest.FixtureRequest) -> None: +def loaded_(request: pytest.FixtureRequest) -> None: _load_elf( request, - Path(cfg.BUILD_DIR) / "tests/target//test_.elf", + Path(cfg.BUILD_DIR) / "tests/target//test_.elf", ) -@pytest.fixture(scope="module") -def uart_( - request: pytest.FixtureRequest, - loaded_, # ← гарантирует порядок: ELF раньше UART -) -> Generator[serial.Serial, None, None]: - ser = _open_uart_and_wait_ready(request) - yield ser - ser.close() +# 2. UART-фикстура — добавить одну строку в словарь +_UART_FIXTURE_MAP = { + ... + "uart_": "loaded_", # ← добавить +} ``` ### Тест с M5 (GPIO, реле, питание) ```python +# 1. Фикстура загрузки — зависимость от m5 гарантирует питание @pytest.fixture(scope="module") -def loaded_(request: pytest.FixtureRequest, m5: M5Agent) -> None: +def loaded_(request: pytest.FixtureRequest, m5: M5Agent) -> None: """ Зависит от m5 — питание таргета уже включено к моменту загрузки ELF. """ _load_elf( request, - Path(cfg.BUILD_DIR) / "tests/target//test_.elf", + Path(cfg.BUILD_DIR) / "tests/target//test_.elf", ) -@pytest.fixture(scope="module") -def uart_( - request: pytest.FixtureRequest, - loaded_, -) -> Generator[serial.Serial, None, None]: - ser = _open_uart_and_wait_ready(request) - yield ser - ser.close() +# 2. UART-фикстура — та же одна строка +_UART_FIXTURE_MAP = { + ... + "uart_": "loaded_", # ← добавить +} ``` -**Правило:** если тест управляет железом через M5 — `loaded_` должен явно +**Правило:** если тест управляет железом через M5 — `loaded_` должен явно зависеть от `m5`. Это гарантирует что питание включено до того как pyOCD попытается подключиться к MCU. +> Ручное написание `uart_` фикстур больше не требуется — фабрика +> `_make_uart_fixture` создаёт фикстуру с `_uart_context` (контекстный менеджер, +> гарантирует `ser.close()` при любом исходе). + --- ## Шаг 6 — `tools/hil/test_.py` @@ -429,7 +433,7 @@ uart.close() test_something [ ] tests/target//CMakeLists.txt — сборка с bsp_boot_ram [ ] tests/target/CMakeLists.txt — add_subdirectory() [ ] CMakePresets.json — добавить test_ в targets -[ ] tools/hil/conftest.py — loaded_ + uart_ +[ ] tools/hil/conftest.py — loaded_ + строка в _UART_FIXTURE_MAP [ ] tools/hil/test_.py — pytest-тесты [ ] just/host.just — рецепт hil- (опционально) [ ] just build::build-hil — зелёная сборка diff --git a/docs/testing/hil/HIL_FIXTURES.md b/docs/testing/hil/HIL_FIXTURES.md index c0f2bcb..054da53 100644 --- a/docs/testing/hil/HIL_FIXTURES.md +++ b/docs/testing/hil/HIL_FIXTURES.md @@ -138,14 +138,14 @@ def flush_uart_buffer(uart): ``` **Правило:** `autouse` в `conftest.py` действует на **все** тесты в директории -и поддиректориях. Используй осторожно — можно случайно затронуть тесты, +и поддиректориях. Используется осторожно — можно случайно затронуть тесты, которым эта фикстура не нужна. --- ## 5. Зависимости между фикстурами -Зависимость объявляется **в сигнатуре** фикстуры. pytest строит DAG и гарантирует +Зависимость объявляется **в сигнатуре** фикстуры. pytest строит `DAG` и гарантирует порядок создания. ```python @@ -200,15 +200,18 @@ tools/hil/ └── conftest.py ← (если бы был) виден только в m5/ ``` -### Что живёт в `conftest.py` нашего проекта +### Что живёт в `conftest.py` проекта | Фикстура / функция | Scope | Назначение | |---------------------|-------|------------| | `_load_elf()` | вспомогательная | pyOCD: halt → FLEXRAM → load ELF → run | -| `_open_uart_and_wait_ready()` | вспомогательная | Открыть VCOM, дождаться `READY\r\n` | +| `_uart_context()` | контекстный менеджер | Открыть VCOM, дождаться `READY\r\n`, гарантировать `close()` | +| `_make_uart_fixture()` | фабрика | Генерирует `uart_*` фикстуры из `_UART_FIXTURE_MAP` | +| `_UART_FIXTURE_MAP` | словарь | Связь `uart_` → `loaded_` для всех тестов | | `loaded_host_uart` | module | Загрузить `test_host_uart.elf` | | `loaded_hil_opto` | module | Загрузить `test_hil_opto.elf`, зависит от `m5` | -| `uart` / `uart_hil_opto` | module | Открыть UART CLI, зависит от `loaded_*` | +| `uart` / `uart_opto` / ... | module | Создаются автоматически через `_UART_FIXTURE_MAP` | +| `usb_cdc_port` | module | USB CDC порт таргета, использует `cfg.TARGET_VCOM_*` | | `m5` | module | Подключиться к M5, включить питание | | `uart_cmd()` | обычная функция | Отправить команду, прочитать ответ | @@ -276,17 +279,37 @@ def loaded_(request, m5): # m5 — только если нужно п Teardown не нужен — MCU будет перезагружен при следующей загрузке ELF. -### 9.2. UART-сессия (с teardown) +### 9.2. UART-сессия (через фабрику) + +Для создания UART-фикстур используется фабрика +`_make_uart_fixture` и словарь `_UART_FIXTURE_MAP`. Иначе, пришлось бы +вручную писать фикстуры`uart` / `uart_opto` / ... для каждого таргета. ```python -@pytest.fixture(scope="module") -def uart_(request, loaded_): - 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) ``` -`yield` обязателен — порт нужно закрыть, иначе следующий файл не сможет его открыть. +Для нового теста достаточно одной строки в `_UART_FIXTURE_MAP`. +Контекстный менеджер `_uart_context` гарантирует `ser.close()` при любом исходе +(исключение, `pytest.fail`, `KeyboardInterrupt`). ### 9.3. Управление стендом (M5 — с teardown) @@ -352,21 +375,33 @@ def test_opto_ch1(self): ### 9.6. Ожидание `READY` от прошивки -Паттерн из проекта — прошивка шлёт `READY\r\n` в цикле, пока хост не откроет порт: +Паттерн из проекта — прошивка шлёт `READY\r\n` в цикле, пока хост не откроет порт. +Реализован как контекстный менеджер `_uart_context`, который гарантирует закрытие +порта при любом исходе: ```python -def _open_uart_and_wait_ready(request, timeout_s=5.0): - port = request.config.getoption("--vcom") - ser = serial.Serial(port=port, baudrate=115200, timeout=2.0) - deadline = time.monotonic() + timeout_s - while time.monotonic() < deadline: - line = ser.readline().decode(errors="replace").strip() - if line == "READY": - return ser - ser.close() - raise TimeoutError("Прошивка не отправила READY") +@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 + break + if not ready: + pytest.fail(f"Прошивка не отправила READY за {ready_timeout} с") + ser.reset_input_buffer() + yield ser + finally: + ser.close() # выполняется всегда ``` +Все `uart_*` фикстуры используют `_uart_context` через фабрику `_make_uart_fixture` +(см. секцию 9.2). + ### 9.7. Параметризация фикстур Один тест — несколько входных наборов. Полезно для проверки всех каналов: @@ -402,12 +437,10 @@ def uart(request, loaded_host_uart): ser = _open_uart_and_wait_ready(request) return ser # ser.close() никогда не вызовется! -# ✅ yield + close -@pytest.fixture(scope="module") -def uart(request, loaded_host_uart): - ser = _open_uart_and_wait_ready(request) - yield ser - ser.close() +# ✅ Актуальное решение: _uart_context (контекстный менеджер) +# ser.close() гарантирован блоком finally внутри _uart_context. +# Все uart_* фикстуры создаются через _make_uart_fixture, +# который использует _uart_context — ручной close() не нужен. ``` ### Нет зависимости от `loaded_*` — UART открывается раньше ELF @@ -478,21 +511,22 @@ def uart(fresh_state): # ScopeMismatch! ```python # 1. В conftest.py — загрузка ELF @pytest.fixture(scope="module") -def loaded_(request, m5): # m5 только если нужен - _load_elf(request, Path(cfg.BUILD_DIR) / "tests/target//test_.elf") +def loaded_(request, m5): # m5 только если нужен + _load_elf(request, Path(cfg.BUILD_DIR) / "tests/target//test_.elf") -# 2. В conftest.py — UART-сессия -@pytest.fixture(scope="module") -def uart_(request, loaded_): - ser = _open_uart_and_wait_ready(request) - yield ser - ser.close() +# 2. В conftest.py — одна строка в _UART_FIXTURE_MAP +_UART_FIXTURE_MAP = { + ... + "uart_": "loaded_", # ← добавить +} +# Фабрика _make_uart_fixture создаст фикстуру автоматически +# с _uart_context (гарантирует ser.close()) -# 3. В test_.py — autouse setup -class Test: +# 3. В test_.py — autouse setup +class Test: @pytest.fixture(autouse=True) - def _setup(self, uart_, m5): # m5 только если нужен - self.ser = uart_ + def _setup(self, uart_, m5): # m5 только если нужен + self.ser = uart_ self.m5 = m5 def test_ping(self): diff --git a/docs/testing/hil/HIL_HOW_TO.md b/docs/testing/hil/HIL_HOW_TO.md index 5eb657f..1549414 100644 --- a/docs/testing/hil/HIL_HOW_TO.md +++ b/docs/testing/hil/HIL_HOW_TO.md @@ -26,7 +26,8 @@ just host::setup-tools ```bash HIL_VCOM_PORT=/dev/cu.usbmodemXXX # MCU-Link VCOM HIL_M5_PORT=/dev/cu.usbmodemYYY # M5StampPLC -HIL_BUILD_DIR=build/target-debug # путь к собранным ELF (обычно не менять) +HIL_USB_CDC_PORT=/dev/cu.usbmodemZZZ # USB CDC порт на плате таргета (если тест использует USB CDC) +HIL_BUILD_DIR=build/target-debug # путь к собранным ELF (по умолчанию) ``` Как найти нужные порты: @@ -55,6 +56,12 @@ just host::m5-deploy just host::m5-cli # интерактивный CLI для ручной отправки команд агенту ``` +Также можно запустить агент с интерактивным режимом CLI на хосте с помощью команды: + +```bash +just host::m5-start +``` + --- ## Шаг 2 — Физическое подключение стенда @@ -82,8 +89,7 @@ just host::m5-power off ## Шаг 3 — Сборка HIL-прошивок (в devcontainer) -HIL-прошивки компилируются под ARM и собираются **внутри devcontainer**, -потому что там есть `arm-none-eabi-gcc` и весь SDK. +HIL-прошивки компилируются под ARM и собираются **внутри devcontainer**. ```bash # Открыть проект в VSCode → Reopen in Container @@ -103,9 +109,6 @@ ls build/target-debug/tests/target/ # ... ``` -> Пересобирать нужно только при изменении C-кода прошивок (`tests/target/*/main.c` -> или BSP). Изменения в Python-тестах (`tools/hil/test_*.py`) сборки не требуют. - --- ## Шаг 4 — Запуск тестов (на хосте, вне контейнера) @@ -116,7 +119,8 @@ ls build/target-debug/tests/target/ just host::hil-run ``` -pytest обходит все `test_*.py` в `tools/hil/`, **исключая** помеченные `@pytest.mark.interactive`. +pytest обходит все `test_*.py` в `tools/hil/`, **исключая** помеченные `@pytest.mark.interactive` и +`@pytest.mark.usb_vcom`. Каждый файл — своя загрузка ELF, свой UART-сеанс, MCU перезагружается между файлами. ### Интерактивные HIL-тесты (требуют оператора)