# Fixes: unit test mocks + small fixes

This commit is contained in:
Dmitry Akimov 2026-07-06 18:10:56 +03:00
parent bfe4dd6033
commit 2dbe3e61ed
12 changed files with 226 additions and 111 deletions

View file

@ -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)

View file

@ -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)
{

View file

@ -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
/* ── Типы результата ───────────────────────────────────────────────────── */
/**

View file

@ -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);
/* ── Внутренние вспомогательные функции ────────────────────────────────── */
/**

View file

@ -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})

View file

@ -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)

View file

@ -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 <stdbool.h>
@ -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();
}

View file

@ -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);

View file

@ -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"

View file

@ -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;

View file

@ -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",
)
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,
)

View file

@ -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" },
]