lift_indicator_suite/tests/host
2026-05-08 10:50:58 +03:00
..
button # Created HIL & Host tests for bsp_button module 2026-03-31 18:25:45 +03:00
can # Created HIL and HOST tests for bsp_can module 2026-03-31 13:07:39 +03:00
cli # Added: cli, protocol, runner modules for firmware_test + host tests 2026-04-17 17:24:22 +03:00
led # 4 2026-03-16 16:53:16 +03:00
log # 10 2026-03-23 12:19:39 +03:00
mocks # Added: cli, protocol, runner modules for firmware_test + host tests 2026-04-17 17:24:22 +03:00
opto # 14 - Шаблон HIL-теста 2026-03-27 14:36:12 +03:00
prio_queue # Added: prio_queue structure + bsp/display start 2026-04-09 14:18:18 +03:00
protocol # test_display: bsp_display + test_template 2026-05-08 10:50:58 +03:00
ring_buffer # 4 2026-03-16 16:53:16 +03:00
runner # Added: cli, protocol, runner modules for firmware_test + host tests 2026-04-17 17:24:22 +03:00
timeout # 4 2026-03-16 16:53:16 +03:00
uart_host # 5 2026-03-17 13:18:59 +03:00
CMakeLists.txt # Added: cli, protocol, runner modules for firmware_test + host tests 2026-04-17 17:24:22 +03:00
README.md # 4 2026-03-16 16:53:16 +03:00

Host unit-тесты: Unity + FFF

Содержание


1. Две категории тестируемых модулей

Прежде чем писать тест — определи к какой категории относится модуль. От этого зависит какой инструментарий нужен.

Категория A — платформонезависимые модули

Модули без единого вызова NXP SDK: парсеры, протоколы, конечные автоматы, алгоритмы, структуры данных. Зависят только от стандартной библиотеки C.

Инструментарий: только Unity.

тест (Unity assertions)
        ↓
тестируемый модуль (protocol.c, fsm.c, ...)
        ↓
stdlib (string.h, stdint.h, ...)   ← всё доступно на хосте нативно

Пример: тест JSON-протокола firmware_test, тест логики Test runner, тест FSM.

/* tests/host/test_protocol.c */
#include "unity.h"
#include "protocol.h"   /* платформонезависимый модуль */

void test_parse_run_all_command(void) {
    proto_cmd_t cmd;
    int r = proto_parse("{\"type\":\"cmd\",\"cmd\":\"run_all\"}\n", &cmd);
    TEST_ASSERT_EQUAL(0, r);
    TEST_ASSERT_EQUAL(CMD_RUN_ALL, cmd.type);
}

Категория B — BSP-модули (привязка к NXP SDK)

Модули из bsp/ которые вызывают fsl_gpio.h, fsl_lpuart.h, NXP USB stack и т.д. На хосте этих хедеров нет — нужны stub-хедеры и fff-фейки.

Инструментарий: Unity + fff + stub-хедеры.

тест (Unity assertions)
        ↓
тестируемый модуль (bsp/led/src/led.c)
        ↓
fff-фейки (FAKE_VOID_FUNC, FAKE_VALUE_FUNC)  ← подменяют NXP SDK функции
        ↓
stub-хедеры (tests/host/mocks/fsl_gpio.h)    ← подменяют NXP SDK хедеры

Принцип seam (шов): тестируемый код не знает что вызывает фейк — линковщик и include path подставляют нужную реализацию в зависимости от сборки.


2. Структура директорий

tests/host/
├── CMakeLists.txt
├── mocks/                        # stub-хедеры, заменяющие NXP SDK на хосте
│   ├── fsl_gpio.h                # минимальные типы + сигнатуры GPIO
│   ├── fsl_lpuart.h
│   ├── pin_mux.h                 # зеркало макросов пинов из generated/
│   └── board.h
├── led/test_led.c                    # категория B — BSP-модуль
├── protocol/test_protocol.c               # категория A — платформонезависимый
└── runner/test_runner_logic.c           # категория A

mocks/ подключается как include path с более высоким приоритетом чем sdk/. Компилятор найдёт fsl_gpio.h из mocks/ раньше чем из SDK — led.c компилируется на хосте без единого изменения в BSP-коде.


3. Структура тестового файла

Категория A — без фейков

#include "unity.h"
#include "protocol.h"   /* тестируемый модуль */

void setUp(void)    { /* сброс состояния если нужен */ }
void tearDown(void) { }

void test_something(void) {
    TEST_ASSERT_EQUAL(expected, actual);
}

int main(void) {
    UNITY_BEGIN();
    RUN_TEST(test_something);
    return UNITY_END();
}

Категория B — с fff-фейками

#include "unity.h"
#include "fff.h"

DEFINE_FFF_GLOBALS;  /* ровно один раз на весь .c файл */

/* 1. Подключаем stub-хедер с типами */
#include "fsl_gpio.h"

/* 2. Объявляем фейки для функций которые вызывает тестируемый модуль */
FAKE_VOID_FUNC(GPIO_PinInit,  GPIO_Type *, uint32_t, const gpio_pin_config_t *);
FAKE_VOID_FUNC(GPIO_PinWrite, GPIO_Type *, uint32_t, uint8_t);

/* 3. Подключаем тестируемый модуль — ПОСЛЕ фейков */
#include "bsp/led.h"

void setUp(void) {
    RESET_FAKE(GPIO_PinInit);
    RESET_FAKE(GPIO_PinWrite);
    FFF_RESET_HISTORY();
    led_init();
}

void tearDown(void) { }

void test_led_on_writes_gpio_low(void) {
    led_on(LED_HEARTBEAT);
    TEST_ASSERT_EQUAL_UINT8(0U, GPIO_PinWrite_fake.arg2_val); /* active LOW */
}

int main(void) {
    UNITY_BEGIN();
    RUN_TEST(test_led_on_writes_gpio_low);
    return UNITY_END();
}

4. Unity — assertion API

Целые числа

TEST_ASSERT_EQUAL(expected, actual)
TEST_ASSERT_EQUAL_INT(expected, actual)
TEST_ASSERT_EQUAL_INT8(expected, actual)
TEST_ASSERT_EQUAL_INT16(expected, actual)
TEST_ASSERT_EQUAL_INT32(expected, actual)
TEST_ASSERT_EQUAL_UINT8(expected, actual)
TEST_ASSERT_EQUAL_UINT32(expected, actual)
TEST_ASSERT_NOT_EQUAL(expected, actual)

Числа с плавающей точкой

TEST_ASSERT_EQUAL_FLOAT(expected, actual)
TEST_ASSERT_FLOAT_WITHIN(delta, expected, actual)  /* |actual - expected| < delta */
TEST_ASSERT_EQUAL_DOUBLE(expected, actual)

Булевые значения

TEST_ASSERT_TRUE(condition)
TEST_ASSERT_FALSE(condition)
TEST_ASSERT_NULL(pointer)
TEST_ASSERT_NOT_NULL(pointer)

Указатели

TEST_ASSERT_EQUAL_PTR(expected, actual)

Строки и массивы

TEST_ASSERT_EQUAL_STRING(expected, actual)
TEST_ASSERT_EQUAL_MEMORY(expected, actual, len)
TEST_ASSERT_EQUAL_INT_ARRAY(expected, actual, len)
TEST_ASSERT_EQUAL_UINT8_ARRAY(expected, actual, len)

Диапазоны

TEST_ASSERT_INT_WITHIN(delta, expected, actual)
TEST_ASSERT_UINT32_WITHIN(delta, expected, actual)

Явный провал / пропуск

TEST_FAIL()
TEST_FAIL_MESSAGE("причина")
TEST_IGNORE()
TEST_IGNORE_MESSAGE("в процессе")

5. FFF — создание фейков

Макросы объявления

/* void-функция без аргументов */
FAKE_VOID_FUNC(HAL_Init);

/* void-функция с аргументами */
FAKE_VOID_FUNC(GPIO_PinInit, GPIO_Type *, uint32_t, const gpio_pin_config_t *);

/* функция с возвращаемым значением */
FAKE_VALUE_FUNC(status_t, LPUART_WriteBlocking, LPUART_Type *, const uint8_t *, size_t);

/* без аргументов с возвращаемым значением */
FAKE_VALUE_FUNC(uint32_t, get_tick_ms);

/* переменное число аргументов */
FAKE_VOID_FUNC_VARARG(debug_printf, const char *, ...);

Расположение фейков в файле

Фейки объявляются в тестовом .c файле прямо перед #include тестируемого модуля. Для небольших проектов отдельный fakes.h/fakes.c избыточен — каждый тестовый файл объявляет только те фейки, которые нужны именно ему.

/* Правильный порядок в тестовом файле */
#include "unity.h"
#include "fff.h"
DEFINE_FFF_GLOBALS;               /* 1. глобальный контекст fff */

#include "fsl_gpio.h"             /* 2. stub-хедер с типами */
FAKE_VOID_FUNC(GPIO_PinWrite, GPIO_Type *, uint32_t, uint8_t); /* 3. фейк */

#include "bsp/led.h"              /* 4. тестируемый модуль — последним */

6. FFF — управление поведением

Задать возвращаемое значение

/* константа — при каждом вызове */
LPUART_WriteBlocking_fake.return_val = kStatus_Fail;

/* последовательность — каждый вызов берёт следующее */
status_t seq[] = {kStatus_Success, kStatus_Success, kStatus_Timeout};
SET_RETURN_SEQ(LPUART_WriteBlocking, seq, 3);
/* 1-й вызов → kStatus_Success */
/* 2-й вызов → kStatus_Success */
/* 3-й вызов → kStatus_Timeout */
/* 4-й и далее → последнее (kStatus_Timeout) */

custom_fake — кастомная реализация

/* эмуляция тикающего таймера */
static uint32_t s_tick = 0;
static uint32_t fake_tick_inc(void) { s_tick += 10; return s_tick; }

void test_timeout_fires_after_100ms(void) {
    get_tick_ms_fake.custom_fake = fake_tick_inc;
    s_tick = 0;

    bool result = wait_with_timeout(100);

    TEST_ASSERT_TRUE(result);
    TEST_ASSERT_EQUAL(11, get_tick_ms_fake.call_count);
}

custom_fake имеет наивысший приоритет — если задан, return_val и return_val_seq игнорируются.

custom_fake — захват аргументов по значению

Используй когда нужно проверить содержимое структуры, переданной по указателю. Не используй arg_history[] для указателей на стековые переменные — после возврата из тестируемой функции это dangling pointer (ASAN: stack-use-after-return).

/* НЕПРАВИЛЬНО — cfg живёт на стеке led_init(), после return — dangling */
led_init();
const gpio_pin_config_t *cfg = GPIO_PinInit_fake.arg2_history[0]; /* UB! */
TEST_ASSERT_EQUAL(kGPIO_DigitalOutput, cfg->direction);

/* ПРАВИЛЬНО — копируем по значению пока стек ещё жив */
static gpio_pin_config_t s_captured[2];
static int s_idx = 0;

static void capture_cfg(GPIO_Type *base, uint32_t pin,
                        const gpio_pin_config_t *cfg) {
    (void)base; (void)pin;
    if (s_idx < 2) s_captured[s_idx++] = *cfg;  /* копия по значению */
}

void setUp(void) {
    RESET_FAKE(GPIO_PinInit);
    s_idx = 0;
    GPIO_PinInit_fake.custom_fake = capture_cfg;  /* подключить ДО вызова */
    led_init();
}

void test_init_configures_as_output(void) {
    TEST_ASSERT_EQUAL(kGPIO_DigitalOutput, s_captured[0].direction);
}

7. FFF — проверка вызовов

Счётчик и аргументы последнего вызова

TEST_ASSERT_EQUAL(2, GPIO_PinWrite_fake.call_count);
TEST_ASSERT_EQUAL_UINT8(0U, GPIO_PinWrite_fake.arg2_val);  /* последний вызов */

История всех вызовов

/* arg_history хранит FFF_ARG_HISTORY_LEN (по умолчанию 50) последних вызовов */
/* Используй только для скалярных типов и не-стековых указателей */
TEST_ASSERT_EQUAL_UINT8(0U, GPIO_PinWrite_fake.arg2_history[0]);
TEST_ASSERT_EQUAL_UINT8(1U, GPIO_PinWrite_fake.arg2_history[1]);

Порядок вызовов разных функций

void test_init_sequence_order(void) {
    board_init();
    /* clock_init должен вызваться раньше gpio_init */
    TEST_ASSERT_EQUAL_PTR(clock_init, fff.call_history[0]);
    TEST_ASSERT_EQUAL_PTR(gpio_init,  fff.call_history[1]);
}

Функция не была вызвана

void test_no_gpio_write_on_error(void) {
    LPUART_WriteBlocking_fake.return_val = kStatus_Fail;
    module_process();
    TEST_ASSERT_EQUAL(0, GPIO_PinWrite_fake.call_count);
}

8. Работа со stub-хедерами NXP SDK

Зачем нужны stub-хедеры

NXP SDK хедеры (fsl_gpio.h, fsl_lpuart.h и т.д.) не компилируются на хосте — они тянут платформенные регистровые определения для Cortex-M7. Stub-хедер в tests/host/mocks/ содержит только минимально необходимые типы и сигнатуры функций.

Как stub-хедер «перекрывает» SDK

В CMakeLists для тестового таргета mocks/ добавляется в include path до SDK:

target_include_directories(test_led PRIVATE
    ${CMAKE_SOURCE_DIR}/tests/host/mocks   # ← ищется первым
    ${CMAKE_SOURCE_DIR}/bsp/led/include
)

Компилятор найдёт fsl_gpio.h из mocks/ раньше чем из sdk/led.c компилируется без изменений.

Что должно быть в stub-хедере

Только то, что реально используется в тестируемом .c файле. Не копировать весь SDK хедер.

/* tests/host/mocks/fsl_gpio.h */
#pragma once
#include <stdint.h>

typedef struct { uint32_t reserved[64]; } GPIO_Type;

typedef enum { kGPIO_DigitalInput = 0U, kGPIO_DigitalOutput = 1U } gpio_pin_direction_t;
typedef enum { kGPIO_NoIntmode = 0U } gpio_interrupt_mode_t;

typedef struct {
    gpio_pin_direction_t  direction;
    uint8_t               outputLogic;
    gpio_interrupt_mode_t interruptMode;
} gpio_pin_config_t;

/* сигнатуры — реализации предоставляет fff */
void GPIO_PinInit(GPIO_Type *base, uint32_t pin, const gpio_pin_config_t *config);
void GPIO_PinWrite(GPIO_Type *base, uint32_t pin, uint8_t output);

stub pin_mux.h — зеркало макросов пинов

pin_mux.h из generated/ тоже недоступен на хосте. Создаём stub который зеркалит реальные значения:

/* tests/host/mocks/pin_mux.h */
#pragma once
#include "fsl_gpio.h"

static GPIO_Type stub_GPIO3;

#define BOARD_INITPINS_UserLed1_GPIO      (&stub_GPIO3)
#define BOARD_INITPINS_UserLed1_GPIO_PIN  3U
#define BOARD_INITPINS_UserLed2_GPIO      (&stub_GPIO3)
#define BOARD_INITPINS_UserLed2_GPIO_PIN  4U

При изменении пинов в generated/pin_mux.h — обновить соответствующий stub вручную.


9. Ловушки и обходные пути

static функции

FFF не может замокать static функции — они невидимы снаружи translation unit.

/* ❌ нельзя замокать напрямую */
static void internal_process(void) { ... }

/* ✅ compile-time seam */
#ifdef UNIT_TEST
    void internal_process(void);   /* тест подставит свою реализацию */
#else
    static void internal_process(void) { ... }
#endif

Dangling pointer из arg_history

arg_history[] хранит указатели — не копии. Для структур передаваемых по указателю из функций с коротким временем жизни (локальные переменные) использовать custom_fake с копированием по значению. Подробнее — в разделе 6.

Настройка лимитов истории

/* переопределить перед включением fff.h */
#define FFF_ARG_HISTORY_LEN  100
#define FFF_CALL_HISTORY_LEN 100
#include "fff.h"

Стандартные хедеры в BSP

BSP-модули должны явно включать <stddef.h>, <stdint.h>, <stdbool.h> — не полагаться на транзитивное подтягивание через NXP SDK. На хосте этот транзит отсутствует и компиляция упадёт с undeclared identifier 'size_t'.


10. setUp / tearDown — правильный сброс

RESET_FAKE сбрасывает для одного фейка: счётчик вызовов, историю аргументов, return_val, custom_fake.

FFF_RESET_HISTORY сбрасывает глобальную историю порядка вызовов.

void setUp(void)
{
    RESET_FAKE(GPIO_PinInit);
    RESET_FAKE(GPIO_PinWrite);
    FFF_RESET_HISTORY();

    /* восстановить дефолтное поведение если нужно */
    LPUART_WriteBlocking_fake.return_val = kStatus_Success;
}

Никогда не полагайся на порядок выполнения тестов. Каждый тест должен работать независимо — setUp обязан полностью сбрасывать состояние.


11. CMakeLists.txt для host-тестов

# tests/host/CMakeLists.txt

function(add_host_test)
  cmake_parse_arguments(ARG "" "NAME" "SOURCES;MOCKS" ${ARGN})

  add_executable(${ARG_NAME} ${ARG_SOURCES})

  target_link_libraries(${ARG_NAME} PRIVATE lib_external)

  # mocks/ подключается первым — перекрывает SDK хедеры
  foreach(mock_dir IN LISTS ARG_MOCKS)
    target_include_directories(${ARG_NAME} PRIVATE ${mock_dir})
  endforeach()

  add_test(
    NAME    ${ARG_NAME}
    COMMAND ${ARG_NAME}
    WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
  )
endfunction()

set(MOCKS ${CMAKE_CURRENT_SOURCE_DIR}/mocks)

# категория A — платформонезависимый, без mocks
add_host_test(
  NAME    test_protocol
  SOURCES test_protocol.c
          ${PROJECT_SOURCE_DIR}/firmware/test/src/protocol.c
)

# категория B — BSP-модуль, нужны mocks
add_host_test(
  NAME    test_led
  SOURCES test_led.c
          ${PROJECT_SOURCE_DIR}/bsp/led/src/led.c
  MOCKS   ${MOCKS}
)

12. Запуск тестов

# конфигурация (один раз или после изменения CMakeLists)
cmake --preset host-debug

# сборка + тесты одной командой
just build::test-host

# или по шагам:
cmake --build --preset host-debug-build
ctest --preset host-debug-test

# конкретный тест с полным выводом Unity
ctest --preset host-debug-test -R test_led -V

# напрямую — видно весь вывод без CTest-обёртки
./build/host-debug/tests/host/test_led

Пример вывода при успехе

test_led.c:58:test_led_init_calls_gpio_init_for_each_led:PASS
test_led.c:64:test_led_init_configures_as_output:PASS
test_led.c:71:test_led_init_output_logic_is_high:PASS
test_led.c:77:test_led_on_writes_gpio_low:PASS

-----------------------
14 Tests 0 Failures 0 Ignored
OK

Пример вывода при провале

test_led.c:78:test_led_on_writes_gpio_low:FAIL:
  Expected 0 Was 1

-----------------------
14 Tests 1 Failures 0 Ignored
FAIL