diff --git a/firmware/test/CMakeLists.txt b/firmware/test/CMakeLists.txt index 6b7e74a..0ca0953 100644 --- a/firmware/test/CMakeLists.txt +++ b/firmware/test/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.20) project( firmware_test - VERSION 0.1.1 + VERSION 0.1.2 LANGUAGES C ASM) set(TARGET_NAME firmware_test) diff --git a/firmware/test/src/cli.c b/firmware/test/src/cli.c index 70ff248..fc12a60 100644 --- a/firmware/test/src/cli.c +++ b/firmware/test/src/cli.c @@ -254,10 +254,10 @@ static void handle_cmd_run(const char *p_line) */ static void handle_cmd_run_selected(const char *p_line) { - /* Максимальный размер реестра — не более 16 тестов */ - const size_t MAX_SELECTED = 16U; - char id_bufs[16][TEST_ID_MAX_SIZE]; - const char *id_ptrs[16]; + /* Общий "потолок системы" — TEST_REGISTRY_MAX_SIZE (test_module.h), + * тот же, что ограничивает test_runner.c::g_s_selected[]. */ + char id_bufs[TEST_REGISTRY_MAX_SIZE][TEST_ID_MAX_SIZE]; + const char *id_ptrs[TEST_REGISTRY_MAX_SIZE]; const char *tests_key = strstr(p_line, K_FIELD_TESTS); if (tests_key == NULL) @@ -267,7 +267,7 @@ static void handle_cmd_run_selected(const char *p_line) } size_t count = - parse_string_array(tests_key + sizeof(K_FIELD_TESTS) - 1U, id_bufs, MAX_SELECTED); + parse_string_array(tests_key + sizeof(K_FIELD_TESTS) - 1U, id_bufs, TEST_REGISTRY_MAX_SIZE); if (count == 0U) { diff --git a/firmware/test/src/test_module.h b/firmware/test/src/test_module.h index 9c78e63..6ee22bd 100644 --- a/firmware/test/src/test_module.h +++ b/firmware/test/src/test_module.h @@ -21,6 +21,23 @@ /** @brief Максимальная длина идентификатора теста (включая NUL). */ #define TEST_ID_MAX_SIZE 24U +/** + * @brief Верхняя граница числа тест-модулей в системе. + * + * Общий "потолок" для двух независимых мест: + * - test_runner.c: compile-time ёмкость g_s_selected[] (маска выбора + * для RUNNER_MODE_SELECTED). REGISTRY_SIZE в UNIT_TEST-сборке — + * runtime-значение (g_unit_test_registry_size), не может быть + * границей массива со static storage duration. + * - cli.c: максимальный размер входящего массива "tests" в команде + * run_selected (handle_cmd_run_selected()). + * + * Реальный реестр (non-UNIT_TEST, k_registry) — 8 модулей, запас есть. + * При добавлении нового теста, если REGISTRY_SIZE превысит эту границу, + * сборка упадёт на _Static_assert в test_runner.c — увеличивать здесь. + */ +#define TEST_REGISTRY_MAX_SIZE 16U + /* ── Типы результата ───────────────────────────────────────────────────── */ /** diff --git a/firmware/test/src/test_runner.c b/firmware/test/src/test_runner.c index c7d98e7..cf53770 100644 --- a/firmware/test/src/test_runner.c +++ b/firmware/test/src/test_runner.c @@ -58,6 +58,12 @@ extern size_t g_unit_test_registry_size; #define RUNNER_CONFIRM_ID_SIZE 32U +#ifndef UNIT_TEST +_Static_assert( + REGISTRY_SIZE <= TEST_REGISTRY_MAX_SIZE, + "k_registry вырос за пределы TEST_REGISTRY_MAX_SIZE (test_module.h) — увеличь константу"); +#endif + /* ── Типы ──────────────────────────────────────────────────────────────── */ typedef enum runner_state_e @@ -92,12 +98,10 @@ static uint8_t g_s_skipped; static bool g_s_critical_failed; /* Маска выбранных тестов для RUNNER_MODE_SELECTED */ -static bool g_s_selected[REGISTRY_SIZE]; +static bool g_s_selected[TEST_REGISTRY_MAX_SIZE]; /* ── Forward declaration ───────────────────────────────────────────────── */ - static void start_test_at(size_t idx); - /* ── Внутренние вспомогательные функции ────────────────────────────────── */ /** diff --git a/tests/host/CMakeLists.txt b/tests/host/CMakeLists.txt index 469044a..0fd3bc2 100644 --- a/tests/host/CMakeLists.txt +++ b/tests/host/CMakeLists.txt @@ -171,6 +171,7 @@ add_host_test( INCLUDES ${CMAKE_SOURCE_DIR}/firmware/test/src/ ${CMAKE_SOURCE_DIR}/bsp/common/include + ${CMAKE_SOURCE_DIR}/bsp/provisioning/include MOCKS ${BSP_MOCKS_DIR}) diff --git a/tests/host/can/test_bsp_can.c b/tests/host/can/test_bsp_can.c index c4fe4be..cdebe1e 100644 --- a/tests/host/can/test_bsp_can.c +++ b/tests/host/can/test_bsp_can.c @@ -18,8 +18,8 @@ DEFINE_FFF_GLOBALS; #include "clock_config.h" +#include "fsl_clock.h" #include "fsl_flexcan.h" - /* ══════════════════════════════════════════════════════════════════════ * fff-фейки SDK-функций * ══════════════════════════════════════════════════════════════════════ */ @@ -47,6 +47,9 @@ FAKE_VOID_FUNC(FLEXCAN_ClearMbStatusFlags, CAN_Type *, uint64_t); /* bsp_tick — управляемый «таймер» для тестов таймаутов */ FAKE_VALUE_FUNC(uint32_t, bsp_tick_get_ms); +/* ERRATA 50235 workaround (см. bsp_can_init()) */ +FAKE_VOID_FUNC(CLOCK_EnableClock, clock_ip_name_t); + /* ══════════════════════════════════════════════════════════════════════ * Тестируемый модуль (после всех фейков!) * ══════════════════════════════════════════════════════════════════════ */ @@ -132,6 +135,7 @@ void setUp(void) RESET_FAKE(FLEXCAN_GetMbStatusFlags); RESET_FAKE(FLEXCAN_ClearMbStatusFlags); RESET_FAKE(bsp_tick_get_ms); + RESET_FAKE(CLOCK_EnableClock); FFF_RESET_HISTORY(); s_mb_flags_call_count = 0U; @@ -162,6 +166,8 @@ void test_init_success(void) TEST_ASSERT_EQUAL(1, FLEXCAN_Init_fake.call_count); /* MB0 = reserved (ERR005829), MB1 = TX */ TEST_ASSERT_EQUAL(1, FLEXCAN_SetTxMbConfig_fake.call_count); + TEST_ASSERT_EQUAL(1, CLOCK_EnableClock_fake.call_count); + TEST_ASSERT_EQUAL(kCLOCK_Lpuart1, CLOCK_EnableClock_fake.arg0_val); } void test_init_null_config(void) diff --git a/tests/host/cli/test_cli.c b/tests/host/cli/test_cli.c index bffb3fe..3409bb2 100644 --- a/tests/host/cli/test_cli.c +++ b/tests/host/cli/test_cli.c @@ -11,7 +11,10 @@ * пока стек ещё жив (аргументы — указатели на локальные буферы cli.c). */ +#include "bsp/provisioning.h" +#include "bsp/status.h" #include "fff.h" +#include "test_module.h" #include "unity.h" #include @@ -28,7 +31,11 @@ FAKE_VOID_FUNC(protocol_send_error, const char *); FAKE_VOID_FUNC(test_runner_run_all); FAKE_VOID_FUNC(test_runner_run_single, const char *); FAKE_VOID_FUNC(test_runner_on_confirm, const char *, bool); - +FAKE_VOID_FUNC(protocol_send_uid_response, const uint8_t *); +FAKE_VOID_FUNC(protocol_send_version_response); +FAKE_VOID_FUNC(test_runner_run_selected, const char *const *, size_t); +FAKE_VOID_FUNC(test_runner_send_list); +FAKE_VALUE_FUNC(bsp_status_t, bsp_prov_read_uid, uint8_t *, size_t); /* ── Модуль под тестом ─────────────────────────────────────────────────── */ #include "cli.h" @@ -72,6 +79,16 @@ static char s_run_single_id[32U]; static char s_confirm_id[32U]; static bool s_confirm_value; +/* get_uid: cli.c передаёт указатель на СВОЙ стековый буфер — копируем + * по значению внутри custom_fake, иначе после возврата cli_process() + * указатель уже dangling (см. "Ловушка" в HOST_CREATE_TEST.md). */ +static uint8_t s_captured_uid[BSP_PROV_UID_LEN]; + +/* run_selected: id_ptrs[] в cli.c указывает на локальный id_bufs[][] — + * та же ловушка, копируем строки, а не указатели. */ +static size_t s_run_selected_count; +static char s_run_selected_ids[TEST_REGISTRY_MAX_SIZE][TEST_ID_MAX_SIZE]; + static void capture_run_single(const char *p_id) { (void) snprintf(s_run_single_id, sizeof(s_run_single_id), "%s", p_id); @@ -83,6 +100,31 @@ static void capture_on_confirm(const char *p_id, bool confirmed) s_confirm_value = confirmed; } +static void capture_uid_response(const uint8_t *p_uid) +{ + memcpy(s_captured_uid, p_uid, BSP_PROV_UID_LEN); +} + +static void capture_run_selected(const char *const *pp_ids, size_t count) +{ + s_run_selected_count = count; + for (size_t i = 0U; i < count && i < TEST_REGISTRY_MAX_SIZE; i++) + { + (void) snprintf(s_run_selected_ids[i], sizeof(s_run_selected_ids[i]), "%s", pp_ids[i]); + } +} + +/* Фиксированный UID, который "читает" bsp_prov_read_uid в тестах успеха. */ +static const uint8_t K_TEST_UID[BSP_PROV_UID_LEN] = { + 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22 +}; + +static bsp_status_t prov_read_uid_success(uint8_t *p_uid, size_t len) +{ + (void) len; + memcpy(p_uid, K_TEST_UID, BSP_PROV_UID_LEN); + return BSP_OK; +} /* ── setUp / tearDown ──────────────────────────────────────────────────── */ void setUp(void) @@ -94,16 +136,27 @@ void setUp(void) RESET_FAKE(test_runner_run_all); RESET_FAKE(test_runner_run_single); RESET_FAKE(test_runner_on_confirm); + RESET_FAKE(protocol_send_uid_response); + RESET_FAKE(protocol_send_version_response); + RESET_FAKE(bsp_prov_read_uid); + RESET_FAKE(protocol_send_version_response); + RESET_FAKE(bsp_prov_read_uid); + RESET_FAKE(test_runner_run_selected); + RESET_FAKE(test_runner_send_list); FFF_RESET_HISTORY(); - bsp_usb_cdc_read_fake.custom_fake = fake_usb_read; - test_runner_run_single_fake.custom_fake = capture_run_single; - test_runner_on_confirm_fake.custom_fake = capture_on_confirm; + bsp_usb_cdc_read_fake.custom_fake = fake_usb_read; + test_runner_run_single_fake.custom_fake = capture_run_single; + test_runner_on_confirm_fake.custom_fake = capture_on_confirm; + protocol_send_uid_response_fake.custom_fake = capture_uid_response; + test_runner_run_selected_fake.custom_fake = capture_run_selected; - s_inject_len = 0U; - s_run_single_id[0] = '\0'; - s_confirm_id[0] = '\0'; - s_confirm_value = false; + s_inject_len = 0U; + s_run_single_id[0] = '\0'; + s_confirm_id[0] = '\0'; + s_confirm_value = false; + s_run_selected_count = 0U; + (void) memset(s_captured_uid, 0, sizeof(s_captured_uid)); cli_init(); } @@ -138,6 +191,62 @@ void test_run_single_dispatches_with_id(void) TEST_ASSERT_EQUAL_STRING("sdram", s_run_single_id); } +void test_get_uid_success(void) +{ + bsp_prov_read_uid_fake.custom_fake = prov_read_uid_success; + + inject("{\"type\":\"cmd\",\"cmd\":\"get_uid\"}\n"); + + TEST_ASSERT_EQUAL_INT(1, bsp_prov_read_uid_fake.call_count); + TEST_ASSERT_EQUAL_INT(1, protocol_send_uid_response_fake.call_count); + TEST_ASSERT_EQUAL_INT(0, protocol_send_error_fake.call_count); + TEST_ASSERT_EQUAL_UINT8_ARRAY(K_TEST_UID, s_captured_uid, BSP_PROV_UID_LEN); +} + +void test_get_uid_read_failure_sends_error(void) +{ + bsp_prov_read_uid_fake.return_val = BSP_ERR_PARAM; + + inject("{\"type\":\"cmd\",\"cmd\":\"get_uid\"}\n"); + + TEST_ASSERT_EQUAL_INT(0, protocol_send_uid_response_fake.call_count); + TEST_ASSERT_EQUAL_INT(1, protocol_send_error_fake.call_count); + TEST_ASSERT_EQUAL_STRING("UID_READ_ERR", protocol_send_error_fake.arg0_val); +} + +void test_get_version_dispatches(void) +{ + inject("{\"type\":\"cmd\",\"cmd\":\"get_version\"}\n"); + + TEST_ASSERT_EQUAL_INT(1, protocol_send_version_response_fake.call_count); + TEST_ASSERT_EQUAL_INT(0, protocol_send_error_fake.call_count); +} + +void test_list_tests_dispatches(void) +{ + inject("{\"type\":\"cmd\",\"cmd\":\"list_tests\"}\n"); + + TEST_ASSERT_EQUAL_INT(1, test_runner_send_list_fake.call_count); +} + +void test_run_selected_dispatches_with_ids(void) +{ + inject("{\"type\":\"cmd\",\"cmd\":\"run_selected\",\"tests\":[\"sdram\",\"opto\"]}\n"); + + TEST_ASSERT_EQUAL_INT(1, test_runner_run_selected_fake.call_count); + TEST_ASSERT_EQUAL(2, s_run_selected_count); + TEST_ASSERT_EQUAL_STRING("sdram", s_run_selected_ids[0]); + TEST_ASSERT_EQUAL_STRING("opto", s_run_selected_ids[1]); +} + +void test_run_selected_missing_tests_field_sends_parse_err(void) +{ + inject("{\"type\":\"cmd\",\"cmd\":\"run_selected\"}\n"); + + TEST_ASSERT_EQUAL_INT(1, protocol_send_error_fake.call_count); + TEST_ASSERT_EQUAL_STRING("PARSE_ERR", protocol_send_error_fake.arg0_val); +} + void test_unknown_cmd_sends_unknown_cmd_error(void) { inject("{\"type\":\"cmd\",\"cmd\":\"reboot\"}\n"); @@ -262,5 +371,12 @@ int main(void) RUN_TEST(test_crlf_handled_same_as_lf); RUN_TEST(test_two_lines_in_one_chunk_both_dispatched); + RUN_TEST(test_get_uid_success); + RUN_TEST(test_get_uid_read_failure_sends_error); + RUN_TEST(test_get_version_dispatches); + RUN_TEST(test_list_tests_dispatches); + RUN_TEST(test_run_selected_dispatches_with_ids); + RUN_TEST(test_run_selected_missing_tests_field_sends_parse_err); + return UNITY_END(); } \ No newline at end of file diff --git a/tests/host/mocks/fsl_clock.h b/tests/host/mocks/fsl_clock.h new file mode 100644 index 0000000..4668539 --- /dev/null +++ b/tests/host/mocks/fsl_clock.h @@ -0,0 +1,24 @@ +#pragma once + +/** + * @file fsl_clock.h + * @brief Stub fsl_clock.h для host-тестов. + * + * Содержит только то, что реально используется в: + * bsp/can/src/can.c — ERRATA 50235 workaround, CLOCK_EnableClock(kCLOCK_Lpuart1) + * + * fff предоставляет реализацию через FAKE_VOID_FUNC в тестовом файле. + * + * Оригинал: sdk/devices/MIMXRT1052/drivers/fsl_clock.h + */ + +/* ── clock_ip_name_t — только используемый член ──────────────────────────── */ + +typedef enum _clock_ip_name +{ + kCLOCK_Lpuart1 = 0, +} clock_ip_name_t; + +/* ── Function signature — реализуется через fff ──────────────────────────── */ + +void CLOCK_EnableClock(clock_ip_name_t name); \ No newline at end of file diff --git a/tests/host/mocks/version.h b/tests/host/mocks/version.h new file mode 100644 index 0000000..767f73d --- /dev/null +++ b/tests/host/mocks/version.h @@ -0,0 +1,19 @@ +#pragma once + +/** + * @file version.h + * @brief Stub version.h для host-тестов. + * + * Реальный version.h генерируется CMake (configure_file) из + * firmware/test/src/version.h.in только при конфигурации ARM-таргета + * firmware_test (BUILD_TESTS_TARGET=ON). В host-debug/host-release + * firmware/test/CMakeLists.txt не подключается — generated/version.h + * не существует. Значения ниже фиксированные, участвуют только + * в сериализации протокола (session_start/version_response), не в логике. + */ + +#define FIRMWARE_TEST_VERSION_MAJOR 0 +#define FIRMWARE_TEST_VERSION_MINOR 0 +#define FIRMWARE_TEST_VERSION_PATCH 0 + +#define FIRMWARE_TEST_VERSION_STR "0.0.0-host-test" \ No newline at end of file diff --git a/tests/host/runner/test_firmware_runner.c b/tests/host/runner/test_firmware_runner.c index bf93ba2..6073a06 100644 --- a/tests/host/runner/test_firmware_runner.c +++ b/tests/host/runner/test_firmware_runner.c @@ -36,7 +36,7 @@ FAKE_VOID_FUNC(protocol_send_confirm_request, const confirm_params_t *); FAKE_VOID_FUNC(protocol_send_error, const char *); FAKE_VOID_FUNC(protocol_send_pong); FAKE_VOID_FUNC(protocol_send_session_start); - +FAKE_VOID_FUNC(protocol_send_test_list, const test_module_t *const *, size_t); /* ── Модуль под тестом ─────────────────────────────────────────────────── */ #include "test_runner.h" @@ -131,6 +131,7 @@ void setUp(void) RESET_FAKE(protocol_send_summary); RESET_FAKE(protocol_send_confirm_request); RESET_FAKE(protocol_send_error); + RESET_FAKE(protocol_send_test_list); FFF_RESET_HISTORY(); protocol_send_test_result_fake.custom_fake = capture_test_result; diff --git a/tools/production/service_tui.spec b/tools/production/service_tui.spec index 06a8c70..ff9b8c9 100644 --- a/tools/production/service_tui.spec +++ b/tools/production/service_tui.spec @@ -1,81 +1,12 @@ # -*- mode: python ; coding: utf-8 -*- -""" -service_tui.spec — PyInstaller spec для service-tui (Фаза 5, RELEASE_ROADMAP.md). -Собирает onedir-бандл (не onefile — onefile замедляет старт распаковкой, -см. MONOLITH_APP_PLAN.md §Фаза 5). Итоговая структура: - - dist/service_tui/ - ├── service_tui[.exe] - ├── _internal/ - │ ├── data/ ← dcd.bin, *_fdcb.bin, ivt_flashloader.bin, spsdk data - │ └── ... ← рантайм PyInstaller, libusbsio (из Analysis) - ├── firmware/ ← НЕ создаётся этим spec — см. just-рецепт (post-build copy, - │ firmware_hab_path() во frozen ждёт его рядом с exe, - │ а не внутри _internal — PyInstaller datas всегда - │ кладёт файлы внутрь _internal, сюда достать не может) - └── custom_binaries/ ← создаётся приложением само при первом запуске - (flasher.py::_resolve_custom_binaries_dir), не этим spec - -Запуск: uv run --directory tools/production pyinstaller service_tui.spec -(все относительные пути ниже считаются от расположения этого файла — SPECPATH). -""" - -from pathlib import Path - -from PyInstaller.utils.hooks import collect_data_files, collect_dynamic_libs - -_SPEC_DIR = Path(SPECPATH) # tools/production/ — переменная предоставлена PyInstaller -_REPO_ROOT = _SPEC_DIR.parents[1] # tools/production -> tools -> корень репозитория -_DCD_DIR = _REPO_ROOT / "tools" / "host" / "dcd" - -# ── datas ──────────────────────────────────────────────────────────────── - -# spsdk: ~380 файлов данных (data/devices/*/database.yaml и т.п.) — проверено, -# реально нужны (HabImage/Config резолвят family="mimxrt1050" через них), -# не декоративная предосторожность. -datas = collect_data_files("spsdk") - -# app.tcss — Textual резолвит CSS_PATH относительно __file__ модуля, где -# определён App (app/app.py); во frozen эта директория виртуальна, но должна -# физически существовать в бандле по тому же относительному пути. -datas += [(str(_SPEC_DIR / "app" / "app.tcss"), "app")] - -# pyproject.toml — для _read_app_version() (tomllib) во frozen; waiting.py -# резолвит parents[2] от __file__, что во frozen указывает на корень бандла. -datas += [(str(_SPEC_DIR / "pyproject.toml"), ".")] - -# tools/host/dcd/*.bin — единый источник (Р6), тот же, что использует -# нетронутый flash_usb.py. Кладём в data/ внутри _internal — резолвер -# flash_backend.py (см. правку в этом же ответе) ждёт их именно там. -for _blob in sorted(_DCD_DIR.glob("*.bin")): - datas.append((str(_blob), "data")) - -# ── binaries ───────────────────────────────────────────────────────────── - -# libusbsio: нативный HID-транспорт (следствие Р7); libusb-1.0.* в бандле -# отсутствует. Заберёт бинарники всех платформ (см. пояснение в ответе, -# где предложен этот spec) — раздувает бандл, но не ломает. -binaries = collect_dynamic_libs("libusbsio") - -# ── hiddenimports ──────────────────────────────────────────────────────── - -# app/ — implicit namespace package (нет __init__.py, в отличие от -# app/screens/). Перечисляем явно — цена нулевая, класс "тихо потерянного -# модуля" снимается целиком. -hiddenimports = [ - "app", - "app.app", - "app.screens", - "app.widgets", -] a = Analysis( - [str(_SPEC_DIR / "main.py")], - pathex=[str(_SPEC_DIR)], - binaries=binaries, - datas=datas, - hiddenimports=hiddenimports, + ['main.py'], + pathex=[], + binaries=[], + datas=[('../shared', 'shared')], + hiddenimports=[], hookspath=[], hooksconfig={}, runtime_hooks=[], @@ -83,27 +14,25 @@ a = Analysis( noarchive=False, optimize=0, ) - pyz = PYZ(a.pure) exe = EXE( pyz, a.scripts, - [], - exclude_binaries=True, - name="service_tui", - debug=False, - strip=False, - upx=False, # UPX + нативные HID-либы (libusbsio) — известный источник - # проблем с загрузкой; отключаю явно, не полагаюсь на дефолт - console=True, # Textual — терминальное приложение, без консоли не отрисуется -) - -coll = COLLECT( - exe, a.binaries, a.datas, + [], + name='service_tui', + debug=False, + bootloader_ignore_signals=False, strip=False, - upx=False, - name="service_tui", -) \ No newline at end of file + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=True, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) diff --git a/tools/production/uv.lock b/tools/production/uv.lock index bcc0d54..af4ab47 100644 --- a/tools/production/uv.lock +++ b/tools/production/uv.lock @@ -1132,7 +1132,6 @@ dependencies = [ { name = "pyinstaller" }, { name = "pyserial" }, { name = "python-dotenv" }, - { name = "pyusb" }, { name = "spsdk" }, { name = "textual" }, ] @@ -1147,7 +1146,6 @@ requires-dist = [ { name = "pyinstaller", specifier = ">=6.0.0" }, { name = "pyserial", specifier = ">=3.5" }, { name = "python-dotenv", specifier = ">=1.0.0" }, - { name = "pyusb", specifier = ">=1.0.0" }, { name = "spsdk", specifier = "==3.7.0" }, { name = "textual", specifier = ">=0.80.0" }, ]