diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index e569fd9..fb30120 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -51,7 +51,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ apt-get update && apt-get install -y --no-install-recommends \ curl git cmake ninja-build bear \ python3 python3-pip \ - clang-17 clangd-17 clang-tidy-17 clang-format-17 \ + clang-17 clangd-17 libclang-rt-17-dev clang-tidy-17 clang-format-17 \ gdb-multiarch \ build-essential \ && update-alternatives --install /usr/bin/clangd clangd /usr/bin/clangd-17 100 \ diff --git a/CMakePresets.json b/CMakePresets.json index 95c7ec0..eddb87c 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -128,8 +128,7 @@ "displayName": "Host Tests — Debug", "configurePreset": "host-debug", "targets": [ - "test_host_simple", - "test_host_second" + "test_bsp_led" ] }, { @@ -137,8 +136,7 @@ "displayName": "Host Tests — Release", "configurePreset": "host-release", "targets": [ - "test_host_simple", - "test_host_second" + "test_bsp_led" ] } ], diff --git a/bsp/CMakeLists.txt b/bsp/CMakeLists.txt index 419acb9..01f7af5 100644 --- a/bsp/CMakeLists.txt +++ b/bsp/CMakeLists.txt @@ -11,6 +11,8 @@ add_library( generated/board.c generated/pin_mux.c generated/clock_config.c generated/startup/startup_MIMXRT1052.S) +add_subdirectory(led) + # Подавляем предупреждения при компиляции собственных .c файлов библиотеки target_compile_options(bsp_board PRIVATE -w) @@ -23,24 +25,19 @@ target_compile_definitions( bsp_board PUBLIC CPU_MIMXRT1052CVJ5B SKIP_SYSCLK_INIT XIP_EXTERNAL_FLASH=1 XIP_BOOT_HEADER_ENABLE=1 XIP_BOOT_HEADER_DCD_ENABLE=1) -# Сценарий A: XIP — код исполняется прямо из Flash -add_library(bsp_boot_xip INTERFACE) -target_compile_definitions( - bsp_boot_xip INTERFACE XIP_EXTERNAL_FLASH=1 XIP_BOOT_HEADER_ENABLE=1 - XIP_BOOT_HEADER_DCD_ENABLE=1) - -# Сценарий B: копирование в ITCM (для bootloader или критичного кода) -add_library(bsp_boot_itcm INTERFACE) -target_compile_definitions( - bsp_boot_itcm INTERFACE XIP_BOOT_HEADER_ENABLE=1 XIP_BOOT_HEADER_DCD_ENABLE=1 - # XIP_EXTERNAL_FLASH намеренно отсутствует -) - -# Сценарий C: копирование в SDRAM (для app с графикой) -add_library(bsp_boot_sdram INTERFACE) -target_compile_definitions( - bsp_boot_sdram INTERFACE XIP_BOOT_HEADER_ENABLE=1 - XIP_BOOT_HEADER_DCD_ENABLE=1) - # sdk_device PUBLIC — транзитивно подтянется во все firmware таргеты target_link_libraries(bsp_board PUBLIC sdk_device sdk_clock sdk_common) + +# # Сценарий A: XIP — код исполняется прямо из Flash add_library(bsp_boot_xip +# INTERFACE) target_compile_definitions( bsp_boot_xip INTERFACE +# XIP_EXTERNAL_FLASH=1 XIP_BOOT_HEADER_ENABLE=1 XIP_BOOT_HEADER_DCD_ENABLE=1) + +# # Сценарий B: копирование в ITCM (для bootloader или критичного кода) +# add_library(bsp_boot_itcm INTERFACE) target_compile_definitions( bsp_boot_itcm +# INTERFACE XIP_BOOT_HEADER_ENABLE=1 XIP_BOOT_HEADER_DCD_ENABLE=1 # +# XIP_EXTERNAL_FLASH намеренно отсутствует ) + +# # Сценарий C: копирование в SDRAM (для app с графикой) +# add_library(bsp_boot_sdram INTERFACE) target_compile_definitions( +# bsp_boot_sdram INTERFACE XIP_BOOT_HEADER_ENABLE=1 +# XIP_BOOT_HEADER_DCD_ENABLE=1) diff --git a/bsp/generated/board.c b/bsp/generated/board.c index af2916e..30057ea 100644 --- a/bsp/generated/board.c +++ b/bsp/generated/board.c @@ -1,8 +1,10 @@ #include "board.h" + #include "clock_config.h" #include "pin_mux.h" -void BOARD_Init(void) { - BOARD_InitPins(); - BOARD_BootClockRUN(); +void board_hw_init(void) +{ + BOARD_InitPins(); + BOARD_BootClockRUN(); } diff --git a/bsp/generated/board.h b/bsp/generated/board.h index 9014149..275c12b 100644 --- a/bsp/generated/board.h +++ b/bsp/generated/board.h @@ -4,7 +4,7 @@ /*! @brief The board name */ #define BOARD_NAME "TFT4.3_UB_BOARD" -void BOARD_Init(void); - +/*! //TODO: @brief The board name */ +void board_hw_init(void); #endif /* _BOARD_H_ */ \ No newline at end of file diff --git a/bsp/led/CMakeLists.txt b/bsp/led/CMakeLists.txt new file mode 100644 index 0000000..cc58321 --- /dev/null +++ b/bsp/led/CMakeLists.txt @@ -0,0 +1,17 @@ +if(BUILD_TESTS_HOST) + return() +endif() + +add_library(bsp_led STATIC src/led.c) + +target_include_directories( + bsp_led + PUBLIC include/ # led.h доступен снаружи + PRIVATE src/ # ничего приватного сейчас, но структура готова +) + +target_link_libraries( + bsp_led + PUBLIC bsp_board # board.h, pin_mux.h — транзитивно + PRIVATE sdk_gpio # fsl_gpio.h — не торчит наружу +) diff --git a/bsp/led/README.md b/bsp/led/README.md new file mode 100644 index 0000000..07aecc4 --- /dev/null +++ b/bsp/led/README.md @@ -0,0 +1,70 @@ +# bsp_led + +Драйвер двух пользовательских светодиодов на плате. + +--- + +## Аппаратная часть + +| `led_id_t` | Сигнал | GPIO | Pin | Координата | Активный уровень | +|-----------------|------------|-------|-----|------------|-----------------| +| `LED_HEARTBEAT` | `UserLed1` | GPIO3 | 3 | M4 | LOW (0 = горит) | +| `LED_APP` | `UserLed2` | GPIO3 | 4 | P2 | LOW (0 = горит) | + +Пины сконфигурированы в `generated/pin_mux.h` (MCUXpresso Config Tools). +`INIT_GPIO_VALUE = 1U` — оба LED выключены сразу после `led_init()`. + +--- + +## API + +```c +void led_init(void); // вызвать один раз после BOARD_InitPins() + +void led_on(led_id_t id); +void led_off(led_id_t id); +void led_toggle(led_id_t id); +void led_set(led_id_t id, bool on); +bool led_get(led_id_t id); +``` + +--- + +## Использование + +```c +#include "bsp/led.h" + +// инициализация +led_init(); + +// heartbeat +led_toggle(LED_HEARTBEAT); + +// прикладная индикация +led_on(LED_APP); // пакет принят / тест запущен +led_off(LED_APP); // сброс +``` + +--- + +## CMake + +```cmake +target_link_libraries( PRIVATE bsp_led) +``` + +Зависимости: `bsp_board` (PUBLIC, транзитивно), `sdk_gpio` (PRIVATE). +При `BUILD_TESTS_HOST=ON` компонент не собирается — мокается через `fff` на уровне теста. + +--- + +## Файлы + +```bash +led/ +├── CMakeLists.txt +├── include/led.h # публичный API — без NXP хедеров +├── src/led.c # реализация, fsl_gpio.h только здесь +└── README.md # этот файл +``` diff --git a/bsp/led/include/bsp/led.h b/bsp/led/include/bsp/led.h new file mode 100644 index 0000000..a82309b --- /dev/null +++ b/bsp/led/include/bsp/led.h @@ -0,0 +1,49 @@ +#pragma once + +/** + * @file led.h + * @brief BSP LED driver — два светодиода на плате (active LOW) + * + * LED_HEARTBEAT — системный, мигает как признак жизни прошивки + * LED_APP — прикладной, управляется из firmware по ситуации + * + * Пины сконфигурированы в generated/pin_mux. Этот хедер не знает + * ни про GPIO-порты, ни про NXP SDK. + */ + +#include + +/* ── Идентификаторы светодиодов ─────────────────────────────────────── */ + +typedef enum +{ + LED_HEARTBEAT = 0, /**< системный heartbeat */ + LED_APP = 1, /**< прикладной индикатор */ +} led_id_t; + +/* ── API ────────────────────────────────────────────────────────────── */ + +/** + * @brief Инициализация обоих светодиодов. + * Вызвать один раз после BOARD_InitPins(). + * После вызова оба LED выключены. + */ +void led_init(void); + +/** @brief Включить светодиод. */ +void led_on(led_id_t led_id); + +/** @brief Выключить светодиод. */ +void led_off(led_id_t led_id); + +/** @brief Переключить состояние светодиода. */ +void led_toggle(led_id_t led_id); + +/** + * @brief Установить состояние светодиода явно. + * @param on true — включить, false — выключить + */ +void led_set(led_id_t led_id, bool is_enabled); + +/** @brief Получить текущее состояние (true — горит). */ +bool led_get(led_id_t led_id); \ No newline at end of file diff --git a/bsp/led/src/led.c b/bsp/led/src/led.c new file mode 100644 index 0000000..016b1ec --- /dev/null +++ b/bsp/led/src/led.c @@ -0,0 +1,101 @@ +#include "bsp/led.h" + +#include "fsl_gpio.h" +#include "pin_mux.h" + +#include + +/** + * Пины взяты из generated/pin_mux.h — имена макросов должны + * совпадать с тем что сгенерировал MCUXpresso Config Tools. + * + * Пины из generated/pin_mux.h (MCUXpresso Config Tools): + * + * UserLed1 → GPIO3 pin 3 (coord M4, GPIO_SD_B1_03) — LED_HEARTBEAT + * UserLed2 → GPIO3 pin 4 (coord P2, GPIO_SD_B1_04) — LED_APP + * + * INIT_GPIO_VALUE = 1U → active LOW подтверждён схемой. + */ + +/* ── Дескриптор одного светодиода ───────────────────────────────────── */ + +typedef struct +{ + GPIO_Type *gpio; + uint32_t pin; + bool state; /**< true = горит (логическое состояние) */ +} led_desc_t; + +/* ── Таблица светодиодов ───────────────────────────────────────────── + * Порядок должен совпадать с led_id_t. + * ─────────────────────────────────────────────────────────────────── */ +static led_desc_t s_leds[] = { + [LED_HEARTBEAT] = { + .gpio = BOARD_INITPINS_UserLed1_GPIO, + .pin = BOARD_INITPINS_UserLed1_GPIO_PIN, + .state = false, + }, + [LED_APP] = { + .gpio = BOARD_INITPINS_UserLed2_GPIO, + .pin = BOARD_INITPINS_UserLed2_GPIO_PIN, + .state = false, + }, +}; + +#define LED_COUNT (sizeof(s_leds) / sizeof(s_leds[0])) + +/* ── Приватные хелперы ───────────────────────────────────────────────── */ + +/** Перевести логическое состояние в физический уровень GPIO (active LOW). */ +static inline uint8_t led_to_gpio_level(bool is_enabled) +{ + return is_enabled ? 0U : 1U; +} + +static inline void led_apply(led_desc_t *p_led) +{ + GPIO_PinWrite(p_led->gpio, p_led->pin, led_to_gpio_level(p_led->state)); +} + +/* ── Публичный API ───────────────────────────────────────────────────── */ + +void led_init(void) +{ + gpio_pin_config_t cfg = { + .direction = kGPIO_DigitalOutput, + .outputLogic = 1U, /* active LOW → начинаем с 1 = LED выключен */ + .interruptMode = kGPIO_NoIntmode, + }; + + for (size_t i = 0; i < LED_COUNT; i++) + { + GPIO_PinInit(s_leds[i].gpio, s_leds[i].pin, &cfg); + s_leds[i].state = false; + } +} + +void led_on(led_id_t led_id) +{ + led_set(led_id, true); +} + +void led_off(led_id_t led_id) +{ + led_set(led_id, false); +} + +void led_toggle(led_id_t led_id) +{ + led_set(led_id, !s_leds[led_id].state); +} + +void led_set(led_id_t led_id, bool is_enabled) +{ + s_leds[led_id].state = is_enabled; + led_apply(&s_leds[led_id]); +} + +bool led_get(led_id_t led_id) +{ + return s_leds[led_id].state; +} \ No newline at end of file diff --git a/bsp/usb_cdc/CMakeLists.txt b/bsp/usb_cdc/CMakeLists.txt new file mode 100644 index 0000000..e69de29 diff --git a/firmware/test/CMakeLists.txt b/firmware/test/CMakeLists.txt index d4d9fc8..211b2f2 100644 --- a/firmware/test/CMakeLists.txt +++ b/firmware/test/CMakeLists.txt @@ -12,13 +12,7 @@ target_sources(firmware_test # ----------------------------------------------------------------------------- target_link_libraries( firmware_test - PRIVATE bsp_board - # SDK драйверы периферии — добавляй по мере написания тестов - sdk_lpuart # UART тест - sdk_flexcan # CAN тест - sdk_gpio # GPIO тест (кнопки, светодиоды, входы) - sdk_usdhc # uSD тест - sdk_semc # SDRAM тест + PRIVATE bsp_board bsp_led # Опциональные библиотеки lib_external # SEGGER RTT если включён через SEGGER_RTT_ENABLED ) diff --git a/firmware/test/main.c b/firmware/test/main.c index 5366beb..032d4da 100644 --- a/firmware/test/main.c +++ b/firmware/test/main.c @@ -1,36 +1,13 @@ -/* - * Copyright (c) 2013 - 2015, Freescale Semiconductor, Inc. - * Copyright 2016-2017, 2024 NXP - * All rights reserved. - * - * SPDX-License-Identifier: BSD-3-Clause - */ - #include "board.h" -#include "fsl_gpio.h" -#include "pin_mux.h" - -/******************************************************************************* - * Definitions - ******************************************************************************/ - -/******************************************************************************* - * Prototypes - ******************************************************************************/ - -/******************************************************************************* - * Variables - ******************************************************************************/ - -/******************************************************************************* - * Code - ******************************************************************************/ +#include "bsp/led.h" int main(void) { - BOARD_Init(); - GPIO_PinWrite(BOARD_INITPINS_UserLed1_PORT, BOARD_INITPINS_UserLed1_PIN, 0); - // GPIO_PinWrite(BOARD_INITPINS_UserLed2_PORT, BOARD_INITPINS_UserLed2_PIN, 0); + board_hw_init(); + led_init(); + + led_on(LED_HEARTBEAT); + led_off(LED_APP); while (1) { } diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index e9d90b1..71dc547 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -31,7 +31,7 @@ endif() # === FFF Mocking Framework === if(UNITY_TESTING_ENABLED) add_library(fff INTERFACE) - target_include_directories(fff INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/fff) + target_include_directories(fff INTERFACE fff) target_link_libraries(lib_external INTERFACE fff) message(STATUS "FFF ✅") else() diff --git a/tests/HostTestingGuide.md b/tests/HostTestingGuide.md deleted file mode 100644 index 100ddcd..0000000 --- a/tests/HostTestingGuide.md +++ /dev/null @@ -1,618 +0,0 @@ -# Юнит тестирование: Unity + FFF - -## Содержание - -- [Юнит тестирование: Unity + FFF](#юнит-тестирование-unity--fff) - - [Содержание](#содержание) - - [1. Архитектура тестирования](#1-архитектура-тестирования) - - [2. Структура тестового файла](#2-структура-тестового-файла) - - [3. Unity — assertion API](#3-unity--assertion-api) - - [Целые числа](#целые-числа) - - [Числа с плавающей точкой](#числа-с-плавающей-точкой) - - [Булевые значения](#булевые-значения) - - [Указатели](#указатели) - - [Строки и массивы](#строки-и-массивы) - - [Диапазоны](#диапазоны) - - [Явный провал / пропуск](#явный-провал--пропуск) - - [4. FFF — создание фейков](#4-fff--создание-фейков) - - [Макросы объявления](#макросы-объявления) - - [Где объявлять](#где-объявлять) - - [5. FFF — управление поведением](#5-fff--управление-поведением) - - [Задать возвращаемое значение](#задать-возвращаемое-значение) - - [Подставить кастомную реализацию](#подставить-кастомную-реализацию) - - [Захват аргументов через custom\_fake](#захват-аргументов-через-custom_fake) - - [6. FFF — проверка вызовов](#6-fff--проверка-вызовов) - - [Счётчики и аргументы последнего вызова](#счётчики-и-аргументы-последнего-вызова) - - [История всех вызовов](#история-всех-вызовов) - - [Порядок вызовов разных функций](#порядок-вызовов-разных-функций) - - [Функция не была вызвана](#функция-не-была-вызвана) - - [7. Паттерны работы с HAL](#7-паттерны-работы-с-hal) - - [Паттерн: заглушка HAL для host-сборки](#паттерн-заглушка-hal-для-host-сборки) - - [Паттерн: мок таймера](#паттерн-мок-таймера) - - [Паттерн: тест конечного автомата (FSM)](#паттерн-тест-конечного-автомата-fsm) - - [8. Организация фейков в проекте](#8-организация-фейков-в-проекте) - - [9. setUp / tearDown — правильный сброс](#9-setup--teardown--правильный-сброс) - - [10. Ограничения и обходные пути](#10-ограничения-и-обходные-пути) - - [`static` функции](#static-функции) - - [Настройка лимитов истории](#настройка-лимитов-истории) - - [Глобальные хендлеры HAL](#глобальные-хендлеры-hal) - - [11. CMakeLists.txt для host-тестов](#11-cmakeliststxt-для-host-тестов) - - [12. Запуск тестов](#12-запуск-тестов) - - [Пример вывода при успехе](#пример-вывода-при-успехе) - - [Пример вывода при провале](#пример-вывода-при-провале) - ---- - -## 1. Архитектура тестирования - -В bare-metal проекте тестируемый код вызывает HAL-функции, которые недоступны на хосте. -FFF подменяет эти вызовы фейками, Unity проверяет результаты. - -``` -┌─────────────────────────────────────────────┐ -│ Host Test Runner │ -│ │ -│ ┌──────────┐ ┌──────────┐ │ -│ │ Unity │ │ FFF │ │ -│ │ (assert) │ │ (fakes) │ │ -│ └──────────┘ └──────────┘ │ -│ ↓ ↓ │ -│ ┌─────────────────────────────┐ │ -│ │ Тестируемый код │ │ -│ │ App/Src/led.c │ │ -│ └─────────────────────────────┘ │ -│ ↓ вызывает │ -│ ┌─────────────────────────────┐ │ -│ │ HAL Fakes (вместо реального│ │ -│ │ stm32f4xx_hal.h) │ │ -│ └─────────────────────────────┘ │ -└─────────────────────────────────────────────┘ -``` - -**Принцип seam (шов):** тестируемый код не знает, что вызывает фейк, а не реальный HAL — линковщик подставляет нужную реализацию в зависимости от сборки. - ---- - -## 2. Структура тестового файла - -```c -// Tests/host/test_led.c - -#include "fff.h" // мок-фреймворк -#include "unity.h" // assertion фреймворк -#include "hal_fakes.h" // объявления фейков HAL -#include "led.h" // тестируемый модуль - -// Обязательно — ровно один раз на весь тестовый файл -DEFINE_FFF_GLOBALS; - -// ─── Жизненный цикл теста ───────────────────────────────────────────────── - -void setUp(void) -{ - // Сбрасываем фейки перед каждым тестом - RESET_FAKE(HAL_GPIO_WritePin); - RESET_FAKE(HAL_GetTick); - FFF_RESET_HISTORY(); -} - -void tearDown(void) {} - -// ─── Тесты ──────────────────────────────────────────────────────────────── - -void test_led_on_sets_gpio_high(void) -{ - led_set(LED_ON); - - TEST_ASSERT_EQUAL(1, HAL_GPIO_WritePin_fake.call_count); - TEST_ASSERT_EQUAL(GPIO_PIN_SET, HAL_GPIO_WritePin_fake.arg2_val); -} - -void test_led_off_sets_gpio_low(void) -{ - led_set(LED_OFF); - - TEST_ASSERT_EQUAL(GPIO_PIN_RESET, HAL_GPIO_WritePin_fake.arg2_val); -} - -// ─── Runner ─────────────────────────────────────────────────────────────── - -int main(void) -{ - UNITY_BEGIN(); - RUN_TEST(test_led_on_sets_gpio_high); - RUN_TEST(test_led_off_sets_gpio_low); - return UNITY_END(); -} -``` - ---- - -## 3. Unity — assertion API - -### Целые числа - -```c -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) -``` - -### Числа с плавающей точкой - -```c -TEST_ASSERT_EQUAL_FLOAT(expected, actual) -TEST_ASSERT_FLOAT_WITHIN(delta, expected, actual) // |actual - expected| < delta -TEST_ASSERT_EQUAL_DOUBLE(expected, actual) -``` - -### Булевые значения - -```c -TEST_ASSERT_TRUE(condition) -TEST_ASSERT_FALSE(condition) -TEST_ASSERT_NULL(pointer) -TEST_ASSERT_NOT_NULL(pointer) -``` - -### Указатели - -```c -TEST_ASSERT_EQUAL_PTR(expected, actual) -``` - -### Строки и массивы - -```c -TEST_ASSERT_EQUAL_STRING(expected, actual) -TEST_ASSERT_EQUAL_MEMORY(expected, actual, len) // побайтовое сравнение -TEST_ASSERT_EQUAL_INT_ARRAY(expected, actual, len) // сравнение массивов int -TEST_ASSERT_EQUAL_UINT8_ARRAY(expected, actual, len) -``` - -### Диапазоны - -```c -TEST_ASSERT_INT_WITHIN(delta, expected, actual) -TEST_ASSERT_UINT32_WITHIN(delta, expected, actual) -``` - -### Явный провал / пропуск - -```c -TEST_FAIL() // тест падает безусловно -TEST_FAIL_MESSAGE("причина") // с сообщением -TEST_IGNORE() // тест пропускается (помечается как ignored) -TEST_IGNORE_MESSAGE("в процессе") -``` - ---- - -## 4. FFF — создание фейков - -### Макросы объявления - -```c -// void функция без аргументов -FAKE_VOID_FUNC(HAL_Init); - -// void функция с аргументами -FAKE_VOID_FUNC(HAL_GPIO_WritePin, - GPIO_TypeDef*, // arg0 - uint16_t, // arg1 - GPIO_PinState); // arg2 - -// Функция с возвращаемым значением -FAKE_VALUE_FUNC(HAL_StatusTypeDef, - HAL_UART_Transmit, - UART_HandleTypeDef*, // arg0 - uint8_t*, // arg1 - uint16_t, // arg2 - uint32_t); // arg3 - -// Без аргументов, с возвращаемым значением -FAKE_VALUE_FUNC(uint32_t, HAL_GetTick); - -// Функция с переменным числом аргументов -FAKE_VOID_FUNC_VARARG(printf_fake, const char*, ...); -``` - -### Где объявлять - -| Место | Назначение | -|---|---| -| `hal_fakes.h` | `DECLARE_FAKE_*` — объявления для хедера | -| `hal_fakes.c` | `DEFINE_FAKE_*` — определения, компилируется один раз | -| Тестовый файл | `DEFINE_FFF_GLOBALS` — ровно в одном `.c` файле | - -```c -// hal_fakes.h -#pragma once -#include "fff.h" -#include "stm32f4xx_hal.h" // типы GPIO_TypeDef и т.д. - -DECLARE_FAKE_VOID_FUNC(HAL_GPIO_WritePin, GPIO_TypeDef*, uint16_t, GPIO_PinState); -DECLARE_FAKE_VALUE_FUNC(uint32_t, HAL_GetTick); -DECLARE_FAKE_VALUE_FUNC(HAL_StatusTypeDef, HAL_UART_Transmit, - UART_HandleTypeDef*, uint8_t*, uint16_t, uint32_t); -``` - -```c -// hal_fakes.c -#include "hal_fakes.h" - -DEFINE_FAKE_VOID_FUNC(HAL_GPIO_WritePin, GPIO_TypeDef*, uint16_t, GPIO_PinState); -DEFINE_FAKE_VALUE_FUNC(uint32_t, HAL_GetTick); -DEFINE_FAKE_VALUE_FUNC(HAL_StatusTypeDef, HAL_UART_Transmit, - UART_HandleTypeDef*, uint8_t*, uint16_t, uint32_t); -``` - ---- - -## 5. FFF — управление поведением - -### Задать возвращаемое значение - -```c -// Константное значение — возвращается при каждом вызове -HAL_UART_Transmit_fake.return_val = HAL_ERROR; - -// Последовательность значений — каждый вызов берёт следующее -HAL_StatusTypeDef seq[] = {HAL_OK, HAL_OK, HAL_TIMEOUT}; -SET_RETURN_SEQ(HAL_UART_Transmit, seq, 3); -// 1-й вызов → HAL_OK -// 2-й вызов → HAL_OK -// 3-й вызов → HAL_TIMEOUT -// 4-й и далее → последнее значение (HAL_TIMEOUT) -``` - -### Подставить кастомную реализацию - -```c -// Эмулируем тикающий таймер -static uint32_t s_tick_ms = 0; - -static uint32_t fake_get_tick_incrementing(void) -{ - s_tick_ms += 10; - return s_tick_ms; -} - -void test_timeout_after_100ms(void) -{ - HAL_GetTick_fake.custom_fake = fake_get_tick_incrementing; - s_tick_ms = 0; - - bool result = wait_with_timeout(100); - - TEST_ASSERT_TRUE(result); - TEST_ASSERT_EQUAL(11, HAL_GetTick_fake.call_count); -} -``` - -> `custom_fake` имеет наивысший приоритет — если задан, `return_val` и -> `return_val_seq` игнорируются. - -### Захват аргументов через custom_fake - -```c -static uint8_t s_captured_data[64]; -static uint16_t s_captured_size; - -static HAL_StatusTypeDef fake_uart_capture(UART_HandleTypeDef *p_huart, - uint8_t *p_data, - uint16_t size, - uint32_t timeout) -{ - s_captured_size = size; - memcpy(s_captured_data, p_data, size); - return HAL_OK; -} - -void test_uart_sends_correct_payload(void) -{ - HAL_UART_Transmit_fake.custom_fake = fake_uart_capture; - - protocol_send_ping(); - - TEST_ASSERT_EQUAL(4, s_captured_size); - TEST_ASSERT_EQUAL_UINT8_ARRAY("\xAA\x01\x00\xBB", s_captured_data, 4); -} -``` - ---- - -## 6. FFF — проверка вызовов - -### Счётчики и аргументы последнего вызова - -```c -// Количество вызовов -TEST_ASSERT_EQUAL(2, HAL_GPIO_WritePin_fake.call_count); - -// Аргументы последнего вызова -TEST_ASSERT_EQUAL_PTR(GPIOA, HAL_GPIO_WritePin_fake.arg0_val); -TEST_ASSERT_EQUAL(GPIO_PIN_5, HAL_GPIO_WritePin_fake.arg1_val); -TEST_ASSERT_EQUAL(GPIO_PIN_SET, HAL_GPIO_WritePin_fake.arg2_val); -``` - -### История всех вызовов - -```c -// История аргументов — по умолчанию хранит FFF_ARG_HISTORY_LEN=50 вызовов -TEST_ASSERT_EQUAL(GPIO_PIN_SET, HAL_GPIO_WritePin_fake.arg2_history[0]); -TEST_ASSERT_EQUAL(GPIO_PIN_RESET, HAL_GPIO_WritePin_fake.arg2_history[1]); -``` - -### Порядок вызовов разных функций - -```c -// FFF хранит глобальную историю вызовов всех фейков -void test_init_sequence_order(void) -{ - module_init(); - - // HAL_Init должен вызваться раньше HAL_GPIO_Init - TEST_ASSERT_EQUAL_PTR(HAL_Init, fff.call_history[0]); - TEST_ASSERT_EQUAL_PTR(HAL_GPIO_Init, fff.call_history[1]); -} -``` - -### Функция не была вызвана - -```c -void test_no_gpio_toggle_on_error(void) -{ - HAL_UART_Transmit_fake.return_val = HAL_ERROR; - - module_process(); - - TEST_ASSERT_EQUAL(0, HAL_GPIO_WritePin_fake.call_count); -} -``` - ---- - -## 7. Паттерны работы с HAL - -### Паттерн: заглушка HAL для host-сборки - -Создай `Tests/host/stubs/stm32f4xx_hal.h` — минимальный хедер с нужными типами: - -```c -// Tests/host/stubs/stm32f4xx_hal.h -#pragma once -#include - -typedef struct { uint32_t placeholder; } GPIO_TypeDef; -typedef struct { uint32_t placeholder; } UART_HandleTypeDef; - -typedef enum { GPIO_PIN_RESET = 0, GPIO_PIN_SET } GPIO_PinState; -typedef enum { HAL_OK = 0, HAL_ERROR, HAL_BUSY, HAL_TIMEOUT } HAL_StatusTypeDef; - -#define GPIOA ((GPIO_TypeDef *)0x40020000) -#define GPIOB ((GPIO_TypeDef *)0x40020400) - -#define GPIO_PIN_5 ((uint16_t)0x0020) -#define GPIO_PIN_13 ((uint16_t)0x2000) -``` - -### Паттерн: мок таймера - -```c -static uint32_t s_mock_tick = 0; - -void mock_tick_reset(void) { s_mock_tick = 0; } -void mock_tick_advance(uint32_t ms) { s_mock_tick += ms; } - -static uint32_t fake_hal_get_tick(void) { return s_mock_tick; } - -void setUp(void) -{ - RESET_FAKE(HAL_GetTick); - HAL_GetTick_fake.custom_fake = fake_hal_get_tick; - mock_tick_reset(); -} - -void test_retry_stops_after_timeout(void) -{ - HAL_UART_Transmit_fake.return_val = HAL_TIMEOUT; - - bool result = uart_send_with_retry("data", 4, /*timeout_ms=*/300); - - TEST_ASSERT_FALSE(result); - // За 300ms при retry каждые 100ms — ровно 3 попытки - TEST_ASSERT_EQUAL(3, HAL_UART_Transmit_fake.call_count); -} -``` - -### Паттерн: тест конечного автомата (FSM) - -```c -void test_fsm_transitions_on_uart_error(void) -{ - HAL_StatusTypeDef seq[] = {HAL_OK, HAL_ERROR}; - SET_RETURN_SEQ(HAL_UART_Transmit, seq, 2); - - fsm_init(); - fsm_step(); - fsm_step(); - - TEST_ASSERT_EQUAL(FSM_STATE_ERROR, fsm_get_state()); - TEST_ASSERT_EQUAL(2, HAL_UART_Transmit_fake.call_count); -} -``` - ---- - -## 8. Организация фейков в проекте - -``` -Tests/ -├── CMakeLists.txt -├── host/ -│ ├── CMakeLists.txt -│ ├── stubs/ -│ │ ├── stm32f4xx_hal.h # минимальные типы для компиляции на хосте -│ │ └── stm32f4xx_hal_conf.h -│ ├── fakes/ -│ │ ├── hal_fakes.h # DECLARE_FAKE_* для всех HAL функций -│ │ └── hal_fakes.c # DEFINE_FAKE_* — компилируется один раз -│ ├── test_led.c -│ ├── test_uart.c -│ └── test_fsm.c -└── target/ - └── ... -``` - ---- - -## 9. setUp / tearDown — правильный сброс - -`RESET_FAKE` сбрасывает для одного фейка: счётчик вызовов, историю аргументов, `return_val`, `custom_fake`. - -`FFF_RESET_HISTORY` сбрасывает глобальную историю порядка вызовов. - -```c -void setUp(void) -{ - RESET_FAKE(HAL_GPIO_WritePin); - RESET_FAKE(HAL_UART_Transmit); - RESET_FAKE(HAL_GetTick); - - FFF_RESET_HISTORY(); - - // Восстанавливаем дефолтное поведение - HAL_GetTick_fake.return_val = 0; - HAL_UART_Transmit_fake.return_val = HAL_OK; -} -``` - -> Никогда не полагайся на порядок выполнения тестов. Каждый тест должен -> работать независимо — `setUp` обязан полностью сбрасывать состояние. - ---- - -## 10. Ограничения и обходные пути - -### `static` функции - -FFF не может замокать `static` функции — они невидимы снаружи translation unit. - -```c -// ❌ Нельзя замокать напрямую -static void internal_process(void) { ... } - -// ✅ Решение — compile-time seam -#ifdef UNIT_TEST - void internal_process(void); // тест подставит свою реализацию -#else - static void internal_process(void) { ... } -#endif -``` - -### Настройка лимитов истории - -```c -// Переопределяется перед включением fff.h -#define FFF_ARG_HISTORY_LEN 100 // сколько вызовов хранить на фейк -#define FFF_CALL_HISTORY_LEN 100 // размер глобальной истории вызовов -#include "fff.h" -``` - -### Глобальные хендлеры HAL - -```c -// Тестовый файл — заглушка глобального хендлера -UART_HandleTypeDef huart1 = {0}; -``` - ---- - -## 11. CMakeLists.txt для host-тестов - -```cmake -# Tests/host/CMakeLists.txt - -# Библиотека фейков — общая для всех тестов -add_library(hal_fakes STATIC fakes/hal_fakes.c) -target_include_directories(hal_fakes PUBLIC fakes/ stubs/) -target_link_libraries(hal_fakes PUBLIC ThirdParty) - -# Макрос для регистрации тестов -function(add_host_test TEST_NAME TEST_SOURCES APP_SOURCES) - add_executable(${TEST_NAME} ${TEST_SOURCES} ${APP_SOURCES}) - target_include_directories(${TEST_NAME} PRIVATE - ${PROJECT_SOURCE_DIR}/App/Inc - ) - target_compile_definitions(${TEST_NAME} PRIVATE UNIT_TEST) - target_link_libraries(${TEST_NAME} PRIVATE hal_fakes ThirdParty) - add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME}) -endfunction() - -add_host_test(test_led - test_led.c - ${PROJECT_SOURCE_DIR}/App/Src/led.c -) - -add_host_test(test_uart - test_uart.c - ${PROJECT_SOURCE_DIR}/App/Src/uart.c -) -``` - -> `UNIT_TEST` define передаётся в тестируемый код — используй его для -> compile-time seam при работе со `static` функциями. - ---- - -## 12. Запуск тестов - -```bash -# 1. Конфигурация -cmake --preset host-debug - -# 2. Сборка -cmake --build --preset host-debug-build - -# 3. Запуск всех тестов -ctest --preset host-debug-test - -# Запуск конкретного теста с полным выводом -ctest --preset host-debug-test -R test_led -V - -# Запуск напрямую — полный вывод Unity -./build-host-debug/Tests/host/test_led - -# Одной командой -cmake --preset host-debug && \ -cmake --build --preset host-debug-build && \ -ctest --preset host-debug-test -``` - -### Пример вывода при успехе - -``` -test_led.c:28:test_led_on_sets_gpio_high:PASS -test_led.c:35:test_led_off_sets_gpio_low:PASS - ------------------------ -2 Tests 0 Failures 0 Ignored -OK -``` - -### Пример вывода при провале - -``` -test_led.c:29:test_led_on_sets_gpio_high:FAIL: - Expected 1 Was 0 - ------------------------ -2 Tests 1 Failures 0 Ignored -FAIL -``` \ No newline at end of file diff --git a/tests/README.md b/tests/README.md deleted file mode 100644 index 7a91099..0000000 --- a/tests/README.md +++ /dev/null @@ -1,312 +0,0 @@ -# Юнит тестирование CTest, Unity, CMakePresets - -- [Юнит тестирование CTest, Unity, CMakePresets](#юнит-тестирование-ctest-unity-cmakepresets) - - [1. Общая архитектура](#1-общая-архитектура) - - [1.1 Структура проекта](#11-структура-проекта) - - [1.2 CMake конфигурация](#12-cmake-конфигурация) - - [1.3 CMake Presets](#13-cmake-presets) - - [2. Использование](#2-использование) - - [2.1 CLI](#21-cli) - - [3. Ключевые принципы](#3-ключевые-принципы) - - [4. Итоговый Workflow](#4-итоговый-workflow) - -## 1. Общая архитектура - -| Компонент | Назначение | Где используется | -| ------------------------ | --------------------------------------------- | ---------------- | -| **Unity** | Фреймворк unit-тестирования (ThrowTheSwitch) | Host + Target | -| **SEGGER RTT** | Вывод логов через отладчик | Только Target | -| **ThirdParty INTERFACE** | Централизованная точка для всех зависимостей | Везде | -| **CMake Presets** | Конфигурационные профили для разных окружений | CLI + VSCode | -| **CTest** | Запуск и отчёты по тестам | Host тесты | - -### 1.1 Структура проекта - -```bash -ProjectRoot/ -├── CMakeLists.txt # Корневой CMake -├── CMakePresets.json # Presets для host/target -├── cmake/ -│ └── toolchain-stm32f4.cmake # ARM GCC toolchain -│ -├── App/ # Код приложения -│ ├── Src/ -│ │ └── app.c # Тестируемый код -│ └── Inc/ -│ └── app.h -│ -├── Tests/ # Все тесты -│ ├── CMakeLists.txt # enable_testing() + опции -│ ├── host/ # Host тесты (Mac) -│ │ ├── CMakeLists.txt -│ │ └── test_host_simple.c -│ └── target/ # Target тесты (STM32) -│ ├── CMakeLists.txt -│ ├── test_main.c # Unity runner для МК -│ └── test_gpio.c -│ -├── ThirdParty/ # Внешние зависимости -│ ├── CMakeLists.txt # INTERFACE библиотека -│ ├── Unity/ # git submodules -│ │ └── src/ -│ │ ├── unity.c -│ │ └── unity.h -│ └── SEGGER_RTT/ -│ ├── RTT/ -│ └── wrapper/ -│ └── CMakeLists.txt # PUBLIC include пути -│ -├── build-host/ # Сборка для Mac (генерируется) -└── build-target/ # Сборка для STM32 (генерируется) -``` - -### 1.2 CMake конфигурация - -1. Корневой **CMakeLists.txt** - -```cmake -cmake_minimum_required(VERSION 3.22) -project(MySTM32Project C ASM) - -# Добавляем все компоненты -add_subdirectory(ThirdParty) # Зависимости -add_subdirectory(App) # Код приложения -add_subdirectory(Tests) # Тесты -``` - -1. ThirdParty/CMakeLists.txt (**INTERFACE** библиотека) - - • INTERFACE библиотека НЕ генерирует .a/.so файлы - • Служит контейнером для передачи зависимостей - • Наследует PUBLIC свойства от связанных библиотек - • Передаёт всё потребителям одной строкой target_link_libraries(MyApp ThirdParty) - -Как работает **INTERFACE**: - -```txt -1. ThirdParty INTERFACE создана (без файлов) -2. target_link_libraries(ThirdParty INTERFACE SeggerRTT) - → ThirdParty наследует PUBLIC свойства SeggerRTT: - - include пути: RTT/ - - defines: RTT_MODE=1 - - библиотеку: libSeggerRTT.a -3. target_link_libraries(MyApp ThirdParty) - → MyApp автоматически получает ВСЁ от ThirdParty -``` - -1. **Tests/CMakeLists.txt** (входная точка для тестов) - -```cmake -# ⚠️ ОБЯЗАТЕЛЬНО! Регистрирует тесты в CTest -enable_testing() - -# Опции для выбора типа тестов -option(BUILD_TESTS_HOST "Build host (Mac) unit tests" ON) -option(BUILD_TESTS_TARGET "Build STM32 target tests" OFF) - -# Host тесты (Mac) -if(BUILD_TESTS_HOST) - message(STATUS "🔧 Building host tests") - add_subdirectory(host) -endif() - -# Target тесты (STM32) -if(BUILD_TESTS_TARGET) - message(STATUS "🔧 Building target tests") - add_subdirectory(target) -endif() -``` - -1. **Tests/host/CMakeLists.txt** (host тесты) - -```cmake -# Тестовый исполняемый файл -add_executable(test_host_simple - test_host_simple.c # Код тестов - ${PROJECT_SOURCE_DIR}/App/Src/app.c # Тестируемый код -) - -# Include пути -target_include_directories(test_host_simple PRIVATE - ${PROJECT_SOURCE_DIR}/App/Inc # Заголовки приложения - ${PROJECT_SOURCE_DIR}/ThirdParty/Unity/src # unity.h -) - -# Линковка с ThirdParty → автоматически получаем Unity -target_link_libraries(test_host_simple PRIVATE ThirdParty) - -# ⚠️ КРИТИЧНО! Регистрация в CTest -add_test(NAME test_host_simple - COMMAND test_host_simple - WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) # build-host/ -``` - -1. **Tests/target/CMakeLists.txt** (target тесты) - -```cmake -# Тестовый ELF для STM32 -add_executable(test_gpio.elf - test_main.c # Unity runner - test_gpio.c # Тесты GPIO - ${PROJECT_SOURCE_DIR}/Src/hw/gpio_driver.c -) - -target_include_directories(test_gpio.elf PRIVATE - ${PROJECT_SOURCE_DIR}/Src/hw -) - -# ThirdParty даёт Unity + RTT -target_link_libraries(test_gpio.elf PRIVATE ThirdParty) - -# Линкер скрипт для STM32 -target_link_options(test_gpio.elf PRIVATE - -T${PROJECT_SOURCE_DIR}/cmake/STM32F407VETx_flash.ld - -Wl,--gc-sections - -Wl,--print-memory-usage -) - -# Отчёт размера -add_custom_command(TARGET test_gpio.elf POST_BUILD - COMMAND ${CMAKE_SIZE} $ -) -``` - -### 1.3 CMake Presets - -```json -{ - "version": 6, - "configurePresets": [ - { - "name": "host-debug", - "displayName": "🖥️ Host Debug Tests", - "description": "Unit tests for Mac (AppleClang)", - "binaryDir": "${sourceDir}/build-host", - "generator": "Ninja", - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Debug", - "BUILD_TESTS_HOST": "ON", - "BUILD_TESTS_TARGET": "OFF", - "UNITY_ENABLED": "ON", - "SEGGER_RTT_ENABLED": "OFF" - } - }, - { - "name": "target-debug", - "displayName": "🎯 STM32F407 Target Tests", - "description": "Integration tests for STM32", - "binaryDir": "${sourceDir}/build-target", - "generator": "Ninja", - "toolchainFile": "${sourceDir}/cmake/toolchain-stm32f4.cmake", - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Debug", - "BUILD_TESTS_HOST": "OFF", - "BUILD_TESTS_TARGET": "ON", - "UNITY_ENABLED": "ON", - "SEGGER_RTT_ENABLED": "ON" - } - } - ], - "buildPresets": [ - { - "name": "host-debug-build", - "configurePreset": "host-debug", - "displayName": "Build Host Tests", - "targets": ["test_host_simple"] - }, - { - "name": "target-debug-flash", - "configurePreset": "target-debug", - "displayName": "Flash Target Tests", - "targets": ["test_gpio.elf"] - } - ], - "testPresets": [ - { - "name": "host-debug-test", - "configurePreset": "host-debug", - "displayName": "🧪 Run Host Tests", - "output": { - "outputOnFailure": true, - "verbosity": "detailed" - } - } - ] -} -``` - -Что делают **presets**: - -| Preset | Компилятор | Опции | Результат | -| ------------ | ---------- | --------------------- | --------------------------- | -| host-debug | AppleClang | BUILD_TESTS_HOST=ON | build-host/test_host_simple | -| target-debug | ARM GCC | BUILD_TESTS_TARGET=ON | build-target/test_gpio.elf | - -## 2. Использование - -### 2.1 CLI - -**Host** тесты - -```bash -# 1. Конфигурация -cmake --preset host-debug - -# 2. Сборка -cmake --build --preset host-debug-build - -# 3. Запуск тестов -ctest --preset host-debug-test - -# Или всё одной командой: -cmake --preset host-debug && \ -cmake --build --preset host-debug-build && \ -ctest --preset host-debug-test -``` - -**Target** тесты - -```bash -# 1. Конфигурация -cmake --preset target-debug - -# 2. Сборка -cmake --build --preset target-debug-flash - -# 3. Прошивка и запуск -probe-rs run --chip STM32F407VETx build-target/test_gpio.elf - -# Смотрим вывод RTT в терминале! -``` - -## 3. Ключевые принципы - -1. **INTERFACE** библиотека **ThirdParty** - • Контейнер зависимостей без генерации файлов - • Наследует **PUBLIC** свойства от связанных библиотек - • Передаёт всё одной строкой `target_link_libraries(App ThirdParty)` -2. **CMake Presets** - • Один файл для всех конфигураций (host/target) - • Опции управляют включением тестов ( BUILD_TESTS_HOST/TARGET ) - • Build presets с targets предотвращают сборку лишнего -3. **CTest** интеграция - • `enable_testing()` в `Tests/CMakeLists.txt` - • `add_test()` для каждого теста - • `WORKING_DIRECTORY ${CMAKE_BINARY_DIR}` критично! -4. Фиксы **Unity** - • Явное `target_include_directories` для `Unity/src` - • `Unity CMakeLists.txt` не экспортирует **PUBLIC** пути - -## 4. Итоговый Workflow - -| Шаг | Действие | -| :---- | ------------------------------------------------------------------------------------- | -| **1** | Написали код в `App/Src/app.c` | -| **2** | Написали `host` тест в `Tests/host/test_host_simple.c` | -| **3** | `cmake --preset host-debug` → AppleClang, build-host/, Unity enabled | -| **4** | `cmake --build --preset host-debug-build` → Только test_host_simple собирается | -| **5** | `ctest --preset host-debug-test` → Тесты выполняются, отчёт CTest | -| **6** | Всё работает! Добавили target тест в `Tests/target/` | -| **7** | `cmake --preset target-debug` → ARM GCC, build-target/, Unity + RTT enabled | -| **8** | `cmake --build --preset target-debug-flash` → `test_gpio.elf` создан | -| **9** | `probe-rs run --chip STM32F407VETx test_gpio.elf` → Прошивка, запуск, вывод через RTT | diff --git a/tests/host/CMakeLists.txt b/tests/host/CMakeLists.txt index 5acc32e..b33f895 100644 --- a/tests/host/CMakeLists.txt +++ b/tests/host/CMakeLists.txt @@ -1,16 +1,51 @@ -function(add_host_test TEST_NAME TEST_SOURCES) - add_executable(${TEST_NAME} ${TEST_SOURCES}) +# tests/host/CMakeLists.txt - target_link_libraries(${TEST_NAME} PRIVATE lib_external) +# ----------------------------------------------------------------------------- +# Вспомогательная функция добавления host-теста. +# +# Использование: add_host_test( NAME <имя теста> SOURCES <тест.c> +# <тестируемый_модуль.c> ... MOCKS <путь к папке с stub-хедерами> # +# опционально ) +# +# Каждый тест — отдельный исполняемый файл. CTest запускает их все. +# ----------------------------------------------------------------------------- +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) + + # stub-хедеры подключаются как PRIVATE — не торчат в других таргетах + foreach(mock_dir IN LISTS ARG_MOCKS) + target_include_directories(${ARG_NAME} PRIVATE ${mock_dir}) + endforeach() + + # публичные хедеры BSP нужны для #include "bsp/led.h" + target_include_directories(${ARG_NAME} + PRIVATE ${PROJECT_SOURCE_DIR}/bsp/led/include) add_test( - NAME ${TEST_NAME} - COMMAND ${TEST_NAME} + NAME ${ARG_NAME} + COMMAND ${ARG_NAME} WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) endfunction() -# Подключаешь тесты лаконично add_host_test( test_host_simple test_host_simple.c -# ${PROJECT_SOURCE_DIR}/lib/hal/src/app.c # тестируемый модуль ) +# ============================================================================= +# Host тесты +# ============================================================================= -# add_host_test(test_host_second test_host_second.c -# ${PROJECT_SOURCE_DIR}/lib/hal/src/app.c) +set(BSP_MOCKS_DIR ${CMAKE_CURRENT_SOURCE_DIR}/mocks) + +# ----------------------------------------------------------------------------- +# bsp_led +# ----------------------------------------------------------------------------- +add_host_test( + NAME + test_bsp_led + SOURCES + test_bsp_led.c + ${PROJECT_SOURCE_DIR}/bsp/led/src/led.c # тестируемый модуль напрямую + MOCKS + ${BSP_MOCKS_DIR} # fsl_gpio.h, pin_mux.h, board.h +) diff --git a/tests/host/README.md b/tests/host/README.md new file mode 100644 index 0000000..ec357fe --- /dev/null +++ b/tests/host/README.md @@ -0,0 +1,584 @@ +# Host unit-тесты: Unity + FFF + +## Содержание + +- [1. Две категории тестируемых модулей](#1-две-категории-тестируемых-модулей) +- [2. Структура директорий](#2-структура-директорий) +- [3. Структура тестового файла](#3-структура-тестового-файла) +- [4. Unity — assertion API](#4-unity--assertion-api) +- [5. FFF — создание фейков](#5-fff--создание-фейков) +- [6. FFF — управление поведением](#6-fff--управление-поведением) +- [7. FFF — проверка вызовов](#7-fff--проверка-вызовов) +- [8. Работа со stub-хедерами NXP SDK](#8-работа-со-stub-хедерами-nxp-sdk) +- [9. Ловушки и обходные пути](#9-ловушки-и-обходные-пути) +- [10. setUp / tearDown — правильный сброс](#10-setup--teardown--правильный-сброс) +- [11. CMakeLists.txt для host-тестов](#11-cmakeliststxt-для-host-тестов) +- [12. Запуск тестов](#12-запуск-тестов) + +--- + +## 1. Две категории тестируемых модулей + +Прежде чем писать тест — определи к какой категории относится модуль. От этого зависит какой инструментарий нужен. + +### Категория A — платформонезависимые модули + +Модули без единого вызова NXP SDK: парсеры, протоколы, конечные автоматы, алгоритмы, структуры данных. Зависят только от стандартной библиотеки C. + +**Инструментарий: только Unity.** + +```bash +тест (Unity assertions) + ↓ +тестируемый модуль (protocol.c, fsm.c, ...) + ↓ +stdlib (string.h, stdint.h, ...) ← всё доступно на хосте нативно +``` + +Пример: тест JSON-протокола `firmware_test`, тест логики Test runner, тест FSM. + +```c +/* 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-хедеры.** + +```bash +тест (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. Структура директорий + +```bash +tests/host/ +├── CMakeLists.txt +├── mocks/ # stub-хедеры, заменяющие NXP SDK на хосте +│ ├── fsl_gpio.h # минимальные типы + сигнатуры GPIO +│ ├── fsl_lpuart.h +│ ├── pin_mux.h # зеркало макросов пинов из generated/ +│ └── board.h +├── test_led.c # категория B — BSP-модуль +├── test_protocol.c # категория A — платформонезависимый +└── test_runner_logic.c # категория A +``` + +`mocks/` подключается как include path с более высоким приоритетом чем `sdk/`. Компилятор найдёт `fsl_gpio.h` из `mocks/` раньше чем из SDK — `led.c` компилируется на хосте без единого изменения в BSP-коде. + +--- + +## 3. Структура тестового файла + +### Категория A — без фейков + +```c +#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-фейками + +```c +#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 + +### Целые числа + +```c +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) +``` + +### Числа с плавающей точкой + +```c +TEST_ASSERT_EQUAL_FLOAT(expected, actual) +TEST_ASSERT_FLOAT_WITHIN(delta, expected, actual) /* |actual - expected| < delta */ +TEST_ASSERT_EQUAL_DOUBLE(expected, actual) +``` + +### Булевые значения + +```c +TEST_ASSERT_TRUE(condition) +TEST_ASSERT_FALSE(condition) +TEST_ASSERT_NULL(pointer) +TEST_ASSERT_NOT_NULL(pointer) +``` + +### Указатели + +```c +TEST_ASSERT_EQUAL_PTR(expected, actual) +``` + +### Строки и массивы + +```c +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) +``` + +### Диапазоны + +```c +TEST_ASSERT_INT_WITHIN(delta, expected, actual) +TEST_ASSERT_UINT32_WITHIN(delta, expected, actual) +``` + +### Явный провал / пропуск + +```c +TEST_FAIL() +TEST_FAIL_MESSAGE("причина") +TEST_IGNORE() +TEST_IGNORE_MESSAGE("в процессе") +``` + +--- + +## 5. FFF — создание фейков + +### Макросы объявления + +```c +/* 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` избыточен — каждый тестовый файл объявляет только те фейки, которые нужны именно ему. + +```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 — управление поведением + +### Задать возвращаемое значение + +```c +/* константа — при каждом вызове */ +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 — кастомная реализация + +```c +/* эмуляция тикающего таймера */ +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`). + +```c +/* НЕПРАВИЛЬНО — 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 — проверка вызовов + +### Счётчик и аргументы последнего вызова + +```c +TEST_ASSERT_EQUAL(2, GPIO_PinWrite_fake.call_count); +TEST_ASSERT_EQUAL_UINT8(0U, GPIO_PinWrite_fake.arg2_val); /* последний вызов */ +``` + +### История всех вызовов + +```c +/* 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]); +``` + +### Порядок вызовов разных функций + +```c +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]); +} +``` + +### Функция не была вызвана + +```c +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: + +```cmake +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 хедер. + +```c +/* tests/host/mocks/fsl_gpio.h */ +#pragma once +#include + +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 который зеркалит реальные значения: + +```c +/* 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. + +```c +/* ❌ нельзя замокать напрямую */ +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. + +### Настройка лимитов истории + +```c +/* переопределить перед включением fff.h */ +#define FFF_ARG_HISTORY_LEN 100 +#define FFF_CALL_HISTORY_LEN 100 +#include "fff.h" +``` + +### Стандартные хедеры в BSP + +BSP-модули должны явно включать ``, ``, `` — не полагаться на транзитивное подтягивание через NXP SDK. На хосте этот транзит отсутствует и компиляция упадёт с `undeclared identifier 'size_t'`. + +--- + +## 10. setUp / tearDown — правильный сброс + +`RESET_FAKE` сбрасывает для одного фейка: счётчик вызовов, историю аргументов, `return_val`, `custom_fake`. + +`FFF_RESET_HISTORY` сбрасывает глобальную историю порядка вызовов. + +```c +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-тестов + +```cmake +# 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. Запуск тестов + +```bash +# конфигурация (один раз или после изменения 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 +``` + +### Пример вывода при успехе + +```bash +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 +``` + +### Пример вывода при провале + +```bash +test_led.c:78:test_led_on_writes_gpio_low:FAIL: + Expected 0 Was 1 + +----------------------- +14 Tests 1 Failures 0 Ignored +FAIL +``` diff --git a/tests/host/mocks/board.h b/tests/host/mocks/board.h new file mode 100644 index 0000000..37f154d --- /dev/null +++ b/tests/host/mocks/board.h @@ -0,0 +1,2 @@ +#pragma once +/* Stub board.h для host-тестов — led.c включает его транзитивно через bsp_board */ \ No newline at end of file diff --git a/tests/host/mocks/fsl_gpio.h b/tests/host/mocks/fsl_gpio.h new file mode 100644 index 0000000..453d687 --- /dev/null +++ b/tests/host/mocks/fsl_gpio.h @@ -0,0 +1,40 @@ +#pragma once + +/** + * Stub fsl_gpio.h для host-тестов. + * Содержит только типы и сигнатуры используемые в bsp/led/src/led.c. + * fff предоставляет реализации через FAKE_VOID_FUNC. + */ + +#include + +/* GPIO peripheral base pointer type */ +typedef struct +{ + uint32_t reserved[64]; +} GPIO_Type; + +/* Pin direction */ +typedef enum +{ + kGPIO_DigitalInput = 0U, + kGPIO_DigitalOutput = 1U, +} gpio_pin_direction_t; + +/* Interrupt mode — не используется в LED, но нужен для компиляции */ +typedef enum +{ + kGPIO_NoIntmode = 0U, +} gpio_interrupt_mode_t; + +/* Pin config struct */ +typedef struct +{ + gpio_pin_direction_t direction; + uint8_t outputLogic; + gpio_interrupt_mode_t interruptMode; +} gpio_pin_config_t; + +/* Functions — реализуются через 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); \ No newline at end of file diff --git a/tests/host/mocks/pin_mux.h b/tests/host/mocks/pin_mux.h new file mode 100644 index 0000000..43bc046 --- /dev/null +++ b/tests/host/mocks/pin_mux.h @@ -0,0 +1,22 @@ +#pragma once + +/** + * Stub pin_mux.h для host-тестов. + * Макросы пинов зеркалят реальные значения из generated/pin_mux.h. + * GPIO-инстансы — статические объекты-заглушки, адреса уникальны. + */ + +#include "fsl_gpio.h" + +/* Заглушки GPIO-периферии */ +static GPIO_Type stub_GPIO3; + +/* UserLed1 → GPIO3 pin 3 (LED_HEARTBEAT) */ +#define BOARD_INITPINS_UserLed1_GPIO (&stub_GPIO3) +#define BOARD_INITPINS_UserLed1_GPIO_PIN 3U +#define BOARD_INITPINS_UserLed1_INIT_GPIO_VALUE 1U + +/* UserLed2 → GPIO3 pin 4 (LED_APP) */ +#define BOARD_INITPINS_UserLed2_GPIO (&stub_GPIO3) +#define BOARD_INITPINS_UserLed2_GPIO_PIN 4U +#define BOARD_INITPINS_UserLed2_INIT_GPIO_VALUE 1U \ No newline at end of file diff --git a/tests/host/test_bsp_led.c b/tests/host/test_bsp_led.c new file mode 100644 index 0000000..c9dfaaf --- /dev/null +++ b/tests/host/test_bsp_led.c @@ -0,0 +1,191 @@ +#include "fff.h" +#include "unity.h" + +DEFINE_FFF_GLOBALS; + +/* ── Моки GPIO-функций ─────────────────────────────────────────────── + * Должны быть объявлены ДО включения тестируемого модуля, + * чтобы fff-заглушки заменили реальные функции при линковке. + * ─────────────────────────────────────────────────────────────────── */ +#include "fsl_gpio.h" +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); + +/* ── Capture для gpio_pin_config_t ────────────────────────────────── + * fff хранит arg2_history как указатель — но cfg живёт на стеке + * led_init() и становится dangling после возврата. + * (ASAN правильно детектирует это как stack-use-after-return) + * + * Решение: custom_fake копирует структуру по значению в момент вызова, + * пока стек led_init() ещё жив. + * ─────────────────────────────────────────────────────────────────── */ +static gpio_pin_config_t s_captured_cfg[2]; +static int s_capture_idx = 0; + +static void GPIO_PinInit_capture(GPIO_Type *base, uint32_t pin, const gpio_pin_config_t *cfg) +{ + (void) base; + (void) pin; + if (s_capture_idx < 2) + { + s_captured_cfg[s_capture_idx++] = *cfg; /* копируем по значению */ + } +} + +/* ── Тестируемый модуль ──────────────────────────────────────────────── */ +#include "bsp/led.h" + +/* ── setUp / tearDown ────────────────────────────────────────────────── */ + +void setUp(void) +{ + RESET_FAKE(GPIO_PinInit); + RESET_FAKE(GPIO_PinWrite); + FFF_RESET_HISTORY(); + + /* подключаем capture до вызова led_init */ + s_capture_idx = 0; + GPIO_PinInit_fake.custom_fake = GPIO_PinInit_capture; + + led_init(); +} + +void tearDown(void) +{ +} + +/* ── Тесты инициализации ─────────────────────────────────────────────── */ + +void test_led_init_calls_gpio_init_for_each_led(void) +{ + TEST_ASSERT_EQUAL(2, GPIO_PinInit_fake.call_count); +} + +void test_led_init_configures_as_output(void) +{ + /* s_captured_cfg — копия по значению, стек led_init() не нужен */ + TEST_ASSERT_EQUAL(kGPIO_DigitalOutput, s_captured_cfg[0].direction); + TEST_ASSERT_EQUAL(kGPIO_DigitalOutput, s_captured_cfg[1].direction); +} + +void test_led_init_output_logic_is_high(void) +{ + /* active LOW — начальное состояние GPIO = 1 (LED выключен) */ + TEST_ASSERT_EQUAL_UINT8(1U, s_captured_cfg[0].outputLogic); + TEST_ASSERT_EQUAL_UINT8(1U, s_captured_cfg[1].outputLogic); +} + +void test_led_init_both_leds_off(void) +{ + TEST_ASSERT_FALSE(led_get(LED_HEARTBEAT)); + TEST_ASSERT_FALSE(led_get(LED_APP)); +} + +/* ── Тесты led_on / led_off ──────────────────────────────────────────── */ + +void test_led_on_sets_state_true(void) +{ + led_on(LED_HEARTBEAT); + TEST_ASSERT_TRUE(led_get(LED_HEARTBEAT)); +} + +void test_led_off_sets_state_false(void) +{ + led_on(LED_HEARTBEAT); + led_off(LED_HEARTBEAT); + TEST_ASSERT_FALSE(led_get(LED_HEARTBEAT)); +} + +void test_led_on_writes_gpio_low(void) +{ + /* active LOW: включить LED = записать 0 в GPIO */ + led_on(LED_HEARTBEAT); + TEST_ASSERT_EQUAL_UINT8(0U, GPIO_PinWrite_fake.arg2_val); +} + +void test_led_off_writes_gpio_high(void) +{ + /* active LOW: выключить LED = записать 1 в GPIO */ + led_off(LED_HEARTBEAT); + TEST_ASSERT_EQUAL_UINT8(1U, GPIO_PinWrite_fake.arg2_val); +} + +/* ── Тесты led_toggle ────────────────────────────────────────────────── */ + +void test_led_toggle_off_to_on(void) +{ + TEST_ASSERT_FALSE(led_get(LED_HEARTBEAT)); + led_toggle(LED_HEARTBEAT); + TEST_ASSERT_TRUE(led_get(LED_HEARTBEAT)); +} + +void test_led_toggle_on_to_off(void) +{ + led_on(LED_HEARTBEAT); + led_toggle(LED_HEARTBEAT); + TEST_ASSERT_FALSE(led_get(LED_HEARTBEAT)); +} + +void test_led_toggle_twice_returns_to_initial(void) +{ + led_toggle(LED_HEARTBEAT); + led_toggle(LED_HEARTBEAT); + TEST_ASSERT_FALSE(led_get(LED_HEARTBEAT)); +} + +/* ── Тесты led_set ───────────────────────────────────────────────────── */ + +void test_led_set_true_turns_on(void) +{ + led_set(LED_APP, true); + TEST_ASSERT_TRUE(led_get(LED_APP)); +} + +void test_led_set_false_turns_off(void) +{ + led_set(LED_APP, true); + led_set(LED_APP, false); + TEST_ASSERT_FALSE(led_get(LED_APP)); +} + +/* ── Тест независимости LED друг от друга ───────────────────────────── */ + +void test_leds_are_independent(void) +{ + led_on(LED_HEARTBEAT); + TEST_ASSERT_TRUE(led_get(LED_HEARTBEAT)); + TEST_ASSERT_FALSE(led_get(LED_APP)); + + led_on(LED_APP); + led_off(LED_HEARTBEAT); + TEST_ASSERT_FALSE(led_get(LED_HEARTBEAT)); + TEST_ASSERT_TRUE(led_get(LED_APP)); +} + +/* ── main ────────────────────────────────────────────────────────────── */ + +int main(void) +{ + UNITY_BEGIN(); + + RUN_TEST(test_led_init_calls_gpio_init_for_each_led); + RUN_TEST(test_led_init_configures_as_output); + RUN_TEST(test_led_init_output_logic_is_high); + RUN_TEST(test_led_init_both_leds_off); + + RUN_TEST(test_led_on_sets_state_true); + RUN_TEST(test_led_off_sets_state_false); + RUN_TEST(test_led_on_writes_gpio_low); + RUN_TEST(test_led_off_writes_gpio_high); + + RUN_TEST(test_led_toggle_off_to_on); + RUN_TEST(test_led_toggle_on_to_off); + RUN_TEST(test_led_toggle_twice_returns_to_initial); + + RUN_TEST(test_led_set_true_turns_on); + RUN_TEST(test_led_set_false_turns_off); + + RUN_TEST(test_leds_are_independent); + + return UNITY_END(); +} \ No newline at end of file diff --git a/tests/host/test_host_second.c b/tests/host/test_host_second.c deleted file mode 100644 index bd2f196..0000000 --- a/tests/host/test_host_second.c +++ /dev/null @@ -1,34 +0,0 @@ -#include "app.h" // Подключение заголовочного файла с тестируемым функционалом -#include "fff.h" -#include "unity.h" -void setUp(void) { - // Инициализация перед каждым тестом (опционально) -} - -void tearDown(void) { - // Очистка после теста (опционально) -} - -// Тест 1: Проверка сложения -void test_app_add_returns_correct_sum(void) { - TEST_ASSERT_EQUAL_INT(5, foo_add(2, 3)); - TEST_ASSERT_EQUAL_INT(0, foo_add(0, 0)); -} - -// Тест 2: Отрицательные числа -void test_app_add_handles_negative(void) { - TEST_ASSERT_EQUAL_INT(-1, foo_add(-2, 1)); -} - -// Тест 3: Переполнение (boundary case) -void test_app_add_overflow(void) { - TEST_ASSERT_INT_WITHIN(1, 2147483647, foo_add(2147483646, 1)); -} - -int main(void) { - UNITY_BEGIN(); // Запуск Unity - RUN_TEST(test_app_add_returns_correct_sum); - RUN_TEST(test_app_add_handles_negative); - RUN_TEST(test_app_add_overflow); - return UNITY_END(); // Отчёт -} \ No newline at end of file diff --git a/tests/host/test_host_simple.c b/tests/host/test_host_simple.c deleted file mode 100644 index e6740fe..0000000 --- a/tests/host/test_host_simple.c +++ /dev/null @@ -1,26 +0,0 @@ - -#include "app.h" // Подключение заголовочного файла с тестируемым функционалом -#include "unity.h" - -void setUp(void) { - // вызывется перед каждым тестом -} - -void tearDown(void) { - // после каждого теста -} - -void test_add_returns_correct_sum(void) { - TEST_ASSERT_EQUAL_INT(5, foo_add(2, 3)); -} - -void test_add_handles_negative(void) { - TEST_ASSERT_EQUAL_INT(1, foo_add(3, -2)); -} - -int main(void) { - UNITY_BEGIN(); - RUN_TEST(test_add_returns_correct_sum); - RUN_TEST(test_add_handles_negative); - return UNITY_END(); -} diff --git a/tests/target/README.md b/tests/target/README.md new file mode 100644 index 0000000..455f812 --- /dev/null +++ b/tests/target/README.md @@ -0,0 +1,32 @@ +# HIL Testing Toolchain — MIMXRT1052 + +> Документ описывает архитектуру и инструментальный стек для Hardware-in-the-Loop (HIL) тестирования на базе NXP MIMXRT1052. Тесты запускаются на рабочих станциях разработчиков, оркестрация — из DevContainer. + +--- + +## Оборудование + +| Компонент | Роль | +| :--- | :--- | +| NXP MCU-Link | Отладчик + VCOM-мост (один USB-кабель) | +| MIMXRT1052 (таргет) | Целевое устройство | + +MCU-Link предоставляет два логических канала по одному USB: + +- **CMSIS-DAP** — прошивка и сброс таргета (control plane) +- **VCOM (USB-UART)** — тестовый вывод с таргета (data plane) + +--- + +## Инструментальный стек + +| Задача | Инструмент | +| :--- | :--- | +| Прошивка и сброс таргета | `pyocd` Python API | +| Проброс зонда в DevContainer | `pyocd server --allow-remote` по TCP | +| Чтение тестового вывода | `pyserial` (VCOM) или `socat` TCP-мост | +| Оркестрация HIL-тестов | `pytest` + фикстуры в `conftest.py` | +| Сборка C-кода тестовой прошивки | `CMake` (вызывается из pytest fixture) | +| Тестовый фреймворк в прошивке | Unity + fff | +| Unit-тесты (без железа) | CMake + `CTest` (в DevContainer, без таргета) | +| Интерактивная отладка | `pyocd gdbserver` + VS Code **cortex-debug** |