# bootloader: phase 2 + host test docs
This commit is contained in:
parent
4644f21f07
commit
6a643ae790
33 changed files with 1569 additions and 5 deletions
|
|
@ -150,7 +150,8 @@
|
||||||
"test_prio_queue",
|
"test_prio_queue",
|
||||||
"uart_host_mock_example",
|
"uart_host_mock_example",
|
||||||
"test_ring_buffer",
|
"test_ring_buffer",
|
||||||
"test_timeout_pattern"
|
"test_timeout_pattern",
|
||||||
|
"test_mcuboot_boot_select"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -169,7 +170,8 @@
|
||||||
"test_prio_queue",
|
"test_prio_queue",
|
||||||
"uart_host_mock_example",
|
"uart_host_mock_example",
|
||||||
"test_ring_buffer",
|
"test_ring_buffer",
|
||||||
"test_timeout_pattern"
|
"test_timeout_pattern",
|
||||||
|
"test_mcuboot_boot_select"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 0 — Карта Flash | ✅ завершена | [docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md](../../docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md) |
|
| 0 — Карта Flash | ✅ завершена | [docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md](../../docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md) |
|
||||||
| 1 — Скелет (CDC + LED) | ✅ завершена | сборка/HAB/SWD-прошивка/ping-pong/debug — все пункты верификации пройдены на реальной плате, детали ниже |
|
| 1 — Скелет (CDC + LED) | ✅ завершена | сборка/HAB/SWD-прошивка/ping-pong/debug — все пункты верификации пройдены на реальной плате, детали ниже |
|
||||||
| 2 — bootutil (Direct-XIP) | не начата | |
|
| 2 — bootutil (Direct-XIP) | 🟡 host-тесты завершены | реальный bootutil+TinyCrypt+imgtool-фикстуры, 5/5 тестов зелёные; ARM-сторона (flash_map_backend над bsp_qspi_flash, boot_select.c/jump, сборка/железо) — впереди |
|
||||||
| 3 — SD-путь установки | не начата | |
|
| 3 — SD-путь установки | не начата | |
|
||||||
| 4 — SDRAM/W25Q smoke-test + LED-паттерны | не начата | |
|
| 4 — SDRAM/W25Q smoke-test + LED-паттерны | не начата | |
|
||||||
| 5 — HAB Release + service-tui | не начата | |
|
| 5 — HAB Release + service-tui | не начата | |
|
||||||
|
|
@ -150,7 +150,79 @@
|
||||||
- `firmware/bootloader/src/boot_select.c` — вызов `boot_go` (Direct-XIP путь), получение адреса entry
|
- `firmware/bootloader/src/boot_select.c` — вызов `boot_go` (Direct-XIP путь), получение адреса entry
|
||||||
point выбранного слота.
|
point выбранного слота.
|
||||||
|
|
||||||
**Верификация — до всякого железа**:
|
**Решения, принятые в обсуждении Фазы 2 (не пересматриваются):**
|
||||||
|
- Крипто-бэкенд — **TinyCrypt**, не mbedTLS: для ECDSA-P256+SHA-256 нужно 6 файлов (~2142 строк),
|
||||||
|
всё уже вендорено (`sdk/middleware/mcuboot_opensource/ext/tinycrypt` + минимальный ASN.1-парсер из
|
||||||
|
`ext/mbedtls-asn1`). Полноценный mbedTLS не вендорен вообще — потребовал бы ~8000+ новых строк.
|
||||||
|
- FIH-профиль — **MCUBOOT_FIH_PROFILE_LOW** (double-read защита + CFI-счётчики), без RNG-задержки
|
||||||
|
(та требует профиль HIGH и реальную mbedTLS-энтропию — не наш случай).
|
||||||
|
- **MCUBOOT_DIRECT_XIP_REVERT — включён.** Если новый образ ни разу не подтверждён (`boot_set_confirmed()`,
|
||||||
|
вызов — будущая ответственность tft_app), следующая загрузка стирает его и откатывается. Проверено
|
||||||
|
host-тестом (`test_boot_go_reverts_unconfirmed_image`).
|
||||||
|
- Heap **не нужен** — `malloc`/`free` в `loader.c` компилируются только под `!MCUBOOT_DIRECT_XIP`.
|
||||||
|
Линкер-скрипт бюджет 256 КБ (Фаза 1) не трогаем.
|
||||||
|
- Тестовый ключ — уже вендоренный публичный sample-ключ MCUboot
|
||||||
|
(`sdk/middleware/mcuboot_opensource/root-ec-p256.pem`), публичная часть встроена как C-массив через
|
||||||
|
`imgtool.py getpub --lang c` (`mcuboot_port/keys/bootloader_test_ecdsa_pub.c`). Не production-секрет —
|
||||||
|
для серийного производства нужен отдельный ключ вне репозитория (аналог HAB SRK-церемонии).
|
||||||
|
|
||||||
|
**Итог host-части (выполнено):**
|
||||||
|
- `firmware/bootloader/mcuboot_port/` — `sysflash/sysflash.h`, `mcuboot_config/{mcuboot_config.h,
|
||||||
|
mcuboot_logging.h}`, `flash_map_backend/flash_map_backend.h`, `flash_map.h`, `keys.c` +
|
||||||
|
`keys/bootloader_test_ecdsa_pub.c`, `bootutil_sources.cmake` (общий список исходников bootutil +
|
||||||
|
TinyCrypt + ASN.1, `include()`-ится и ARM-таргетом, и host-тестами — не дублируется).
|
||||||
|
- Финальный набор файлов bootutil для нашего режима (Direct-XIP + Revert, ECDSA-only, без
|
||||||
|
measured-boot/encryption): `loader.c, bootutil_misc.c, bootutil_public.c, tlv.c, image_validate.c,
|
||||||
|
image_ecdsa.c, fault_injection_hardening.c, swap_scratch.c`. Важная находка:
|
||||||
|
**`swap_scratch.c` нужен несмотря на название** — `boot_read_image_header()` внутри него обёрнут в
|
||||||
|
`#if !defined(MCUBOOT_SWAP_USING_MOVE)`, то есть это и есть дефолтная (не swap-move) реализация чтения
|
||||||
|
заголовка слота, которую `loader.c` вызывает безусловно для любого режима, включая Direct-XIP.
|
||||||
|
`swap_move.c` (с альтернативной версией той же функции под `MCUBOOT_SWAP_USING_MOVE`) не нужен.
|
||||||
|
- `tests/host/mcuboot_port/` — `fake_flash_map_backend.c/.h` (in-memory реализация контракта
|
||||||
|
`flash_map.h` вместо `bsp_qspi_flash`), `host_link_shims.c` (см. ниже), `test_boot_select.c`
|
||||||
|
(5 тестов), `fixtures/` (imgtool-подписанные `valid_v1.bin`/`valid_v2.bin`/`valid_v2_unconfirmed.bin`/
|
||||||
|
`corrupt_v1.bin`, 32 КБ каждый, slot-size уменьшен относительно реальных 2 МБ — тестируем логику
|
||||||
|
выбора слота, не абсолютные размеры).
|
||||||
|
- **Обход двух host-специфичных проблем линковки** (не существуют на реальном ARM-таргете,
|
||||||
|
`arm-none-eabi-gcc` не использует leading-underscore mangling и `--gc-sections` вырезает мёртвый код):
|
||||||
|
- `fih_panic_loop()` в `fault_injection_hardening.c` — ARM inline-asm self-reference по имени без
|
||||||
|
подчёркивания. Зависит от ABI хоста, не просто от "это host-тест":
|
||||||
|
**Mach-O (macOS)** — C-функция манглится в `_fih_panic_loop`, inline asm ищет
|
||||||
|
`fih_panic_loop` без подчёркивания и не находит → нужен отдельный символ через
|
||||||
|
`asm("fih_panic_loop")`-label.
|
||||||
|
**ELF (Linux, напр. clang-17 в devcontainer)** — C-символы НЕ манглятся, `fih_panic_loop`
|
||||||
|
резолвится сам на себя нативно, как на реальном ARM — наш шим здесь не нужен и ломает сборку
|
||||||
|
(`multiple definition of 'fih_panic_loop'`, поймано на Release/Debug пресетах в devcontainer).
|
||||||
|
Шим в `host_link_shims.c` обёрнут в `#if defined(__APPLE__)` — активен только там, где реально нужен.
|
||||||
|
- `mbedtls_mpi_read_binary` — недостижимый RSA-путь ASN.1 (`mbedtls_asn1_get_mpi`), попадающий в
|
||||||
|
объектный файл `asn1parse.c` целиком; на host (оба ABI) без `--gc-sections` требует явной
|
||||||
|
(недостижимой по рантайму) заглушки — платформенно-независимая часть `host_link_shims.c`.
|
||||||
|
- Проверено на обеих платформах: macOS (Homebrew clang, локально) и Linux/devcontainer (clang-17,
|
||||||
|
Debug + Release пресеты) — `just build::test-host` и `test-host-release` зелёные 13/13 на обеих.
|
||||||
|
- **Обход бага clang 22.1.8 (Homebrew)**: `-fsanitize=address,undefined` ломает генерацию CFI-директив
|
||||||
|
на больших функциях `loader.c` ("invalid CFI advance_loc expression" на этапе ассемблирования; без
|
||||||
|
санитайзеров те же файлы собираются чисто). Отключены санитайзеры точечно для вендоренных файлов
|
||||||
|
bootutil/TinyCrypt/ASN.1 через `set_source_files_properties` в `bootutil_sources.cmake` — для
|
||||||
|
собственного кода (`fake_flash_map_backend.c`, тест) ASan/UBSan остаются включены.
|
||||||
|
- `just build::test-host` — **13/13 тестов зелёные**, включая 5/5 новых
|
||||||
|
(`test_boot_go_slot_a_only_valid`, `test_boot_go_picks_higher_version`,
|
||||||
|
`test_boot_go_ignores_corrupted_slot`, `test_boot_go_no_valid_image`,
|
||||||
|
`test_boot_go_reverts_unconfirmed_image`).
|
||||||
|
|
||||||
|
**Осталось для Фазы 2 (ARM-сторона, ещё не сделано):**
|
||||||
|
- `firmware/bootloader/mcuboot_port/flash_map_backend.c` — реальный шим над `bsp_qspi_flash`
|
||||||
|
(`bsp_qspi_read`/`bsp_qspi_write_page`/`bsp_qspi_erase_sector`), по образцу
|
||||||
|
`sdk/middleware/mcuboot_opensource/boot/nxp_mcux_sdk/flashapi/flash_api.c`.
|
||||||
|
- `firmware/bootloader/src/boot_select.c` — `boot_go()` + прыжок в выбранный образ. Референс —
|
||||||
|
`sdk/middleware/mcuboot_opensource/boot/nxp_mcux_sdk/boot.c::do_boot()`: `flash_device_base()` →
|
||||||
|
`vt = flash_base + rsp->br_image_off + rsp->br_hdr->ih_hdr_size` → `__set_MSP(vt->msp)` →
|
||||||
|
`((void(*)(void))vt->reset)()`. CMSIS-интринсики, ассемблер не нужен.
|
||||||
|
- Подключить `bsp_qspi_flash` в `firmware/bootloader/CMakeLists.txt`, собрать под ARM (проверить что
|
||||||
|
`m_text` укладывается в 247 КБ бюджет Фазы 1).
|
||||||
|
- Аппаратная проверка: образ-заглушка (просто зажигает LED), подписанный тем же тестовым ключом, залит
|
||||||
|
вручную через SWD в Slot A (`0x60040000`) — подтвердить что bootloader реально в него прыгает.
|
||||||
|
|
||||||
|
**Верификация — до всякого железа** (план, для истории):
|
||||||
- Host-юнит-тесты (`tests/host/`, Unity + fff, по образцу существующих `tests/host/protocol/`,
|
- Host-юнит-тесты (`tests/host/`, Unity + fff, по образцу существующих `tests/host/protocol/`,
|
||||||
`tests/host/cli/`) с фейковым flash-буфером в памяти вместо `bsp_qspi_flash`: валидный образ в
|
`tests/host/cli/`) с фейковым flash-буфером в памяти вместо `bsp_qspi_flash`: валидный образ в
|
||||||
Slot A только → выбран A; оба слота валидны, версия Б выше → выбран Б; повреждённый TLV/подпись в
|
Slot A только → выбран A; оба слота валидны, версия Б выше → выбран Б; повреждённый TLV/подпись в
|
||||||
|
|
|
||||||
67
firmware/bootloader/mcuboot_port/bootutil_sources.cmake
Normal file
67
firmware/bootloader/mcuboot_port/bootutil_sources.cmake
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
# mcuboot_port/bootutil_sources.cmake
|
||||||
|
#
|
||||||
|
# Общий список файлов bootutil (MCUboot) + TinyCrypt + ASN.1-парсер,
|
||||||
|
# используемых и реальным ARM-таргетом (firmware/bootloader), и host-тестами
|
||||||
|
# (tests/host/mcuboot_port/) — чтобы не дублировать ~13 путей в двух местах.
|
||||||
|
#
|
||||||
|
# Состав определён вручную (не через .cmake файлы NXP-порта — см.
|
||||||
|
# firmware/bootloader/PLAN.md, Фаза 2) под конкретный режим:
|
||||||
|
# MCUBOOT_DIRECT_XIP + MCUBOOT_DIRECT_XIP_REVERT, MCUBOOT_IMAGE_NUMBER=1,
|
||||||
|
# MCUBOOT_SIGN_EC256 + MCUBOOT_USE_TINYCRYPT, без encryption, без
|
||||||
|
# measured-boot/shared-data (поэтому НЕ включены: boot_record.c, caps.c,
|
||||||
|
# encrypted.c, image_rsa.c, image_ed25519.c, swap_move.c, swap_misc.c,
|
||||||
|
# fault_injection_hardening_delay_rng_mbedtls.c — последний нужен только
|
||||||
|
# профилю MCUBOOT_FIH_PROFILE_HIGH, мы используем LOW).
|
||||||
|
#
|
||||||
|
# swap_scratch.c — ВКЛЮЧЁН, несмотря на название: boot_read_image_header()
|
||||||
|
# внутри него обёрнут в `#if !defined(MCUBOOT_SWAP_USING_MOVE)` — то есть
|
||||||
|
# это и есть дефолтная (не swap-move) реализация чтения заголовка слота,
|
||||||
|
# которую loader.c вызывает безусловно для ЛЮБОГО режима, включая
|
||||||
|
# Direct-XIP. swap_move.c (со своей версией той же функции под
|
||||||
|
# MCUBOOT_SWAP_USING_MOVE) не нужен. Остальные функции swap_scratch.c
|
||||||
|
# (собственно swap-логика) не достижимы в Direct-XIP и вырезаются линкером
|
||||||
|
# ARM-таргета через --gc-sections; для host-тестов — мёртвый, но безобидный
|
||||||
|
# код.
|
||||||
|
|
||||||
|
set(MCUBOOT_OPENSOURCE_DIR ${CMAKE_SOURCE_DIR}/sdk/middleware/mcuboot_opensource)
|
||||||
|
set(MCUBOOT_BOOTUTIL_DIR ${MCUBOOT_OPENSOURCE_DIR}/boot/bootutil)
|
||||||
|
set(MCUBOOT_EXT_DIR ${MCUBOOT_OPENSOURCE_DIR}/ext)
|
||||||
|
|
||||||
|
set(MCUBOOT_BOOTUTIL_SOURCES
|
||||||
|
${MCUBOOT_BOOTUTIL_DIR}/src/loader.c
|
||||||
|
${MCUBOOT_BOOTUTIL_DIR}/src/bootutil_misc.c
|
||||||
|
${MCUBOOT_BOOTUTIL_DIR}/src/bootutil_public.c
|
||||||
|
${MCUBOOT_BOOTUTIL_DIR}/src/tlv.c
|
||||||
|
${MCUBOOT_BOOTUTIL_DIR}/src/image_validate.c
|
||||||
|
${MCUBOOT_BOOTUTIL_DIR}/src/image_ecdsa.c
|
||||||
|
${MCUBOOT_BOOTUTIL_DIR}/src/fault_injection_hardening.c
|
||||||
|
${MCUBOOT_BOOTUTIL_DIR}/src/swap_scratch.c
|
||||||
|
${MCUBOOT_EXT_DIR}/tinycrypt/lib/source/ecc.c
|
||||||
|
${MCUBOOT_EXT_DIR}/tinycrypt/lib/source/ecc_dsa.c
|
||||||
|
${MCUBOOT_EXT_DIR}/tinycrypt/lib/source/sha256.c
|
||||||
|
${MCUBOOT_EXT_DIR}/tinycrypt/lib/source/utils.c
|
||||||
|
${MCUBOOT_EXT_DIR}/mbedtls-asn1/src/asn1parse.c
|
||||||
|
${MCUBOOT_EXT_DIR}/mbedtls-asn1/src/platform_util.c)
|
||||||
|
|
||||||
|
set(MCUBOOT_BOOTUTIL_INCLUDES
|
||||||
|
${MCUBOOT_BOOTUTIL_DIR}/include
|
||||||
|
${MCUBOOT_BOOTUTIL_DIR}/src
|
||||||
|
${MCUBOOT_EXT_DIR}/tinycrypt/lib/include
|
||||||
|
${MCUBOOT_EXT_DIR}/mbedtls-asn1/include
|
||||||
|
${CMAKE_CURRENT_LIST_DIR} # sysflash.h, mcuboot_config.h, flash_map.h, flash_map_backend.h
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------
|
||||||
|
# Обход бага clang 22.1.8 (Homebrew): -fsanitize=address,undefined ломает
|
||||||
|
# генерацию CFI-директив на некоторых больших функциях bootutil (напр.
|
||||||
|
# loader.c::context_boot_go) — "invalid CFI advance_loc expression" на
|
||||||
|
# этапе ассемблирования. Без санитайзеров те же файлы собираются чисто —
|
||||||
|
# похоже на баг конкретной версии тулчейна, а не проблему в bootutil или
|
||||||
|
# нашем коде. Отключаем санитайзеры только для вендоренных исходников
|
||||||
|
# bootutil/TinyCrypt/ASN.1 (не для нашего кода — там ASan/UBSan остаются).
|
||||||
|
# ------------------------------------------------------------------------
|
||||||
|
if(CMAKE_C_COMPILER_ID MATCHES "Clang")
|
||||||
|
set_source_files_properties(${MCUBOOT_BOOTUTIL_SOURCES}
|
||||||
|
PROPERTIES COMPILE_OPTIONS
|
||||||
|
"-fno-sanitize=address,undefined")
|
||||||
|
endif()
|
||||||
66
firmware/bootloader/mcuboot_port/flash_map.h
Normal file
66
firmware/bootloader/mcuboot_port/flash_map.h
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
/**
|
||||||
|
* @file flash_map.h
|
||||||
|
* @brief Контракт bootutil на "область флеша" — реализуется
|
||||||
|
* flash_map_backend.c (реальный, над bsp_qspi_flash) или
|
||||||
|
* fake_flash_map_backend.c (host-тесты, in-memory буфер).
|
||||||
|
*
|
||||||
|
* Урезано под наш случай относительно оригинального Apache-2.0 заголовка
|
||||||
|
* MCUboot (sdk/middleware/mcuboot_opensource/boot/nxp_mcux_sdk/include/flash_map.h):
|
||||||
|
* без sblconfig.h, без encrypted_xip externs, без boot_image_names[] — этого
|
||||||
|
* NXP-специфичного слоя у нас нет.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef FLASH_MAP_H_
|
||||||
|
#define FLASH_MAP_H_
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Область на flash-устройстве.
|
||||||
|
*
|
||||||
|
* Несколько устройств в системе не предполагается (см. FLASH_DEVICE_ID в
|
||||||
|
* sysflash.h) — fa_device_id всегда равен FLASH_DEVICE_ID.
|
||||||
|
*/
|
||||||
|
struct flash_area
|
||||||
|
{
|
||||||
|
uint8_t fa_id; /**< ID области, уникален в системе. */
|
||||||
|
uint8_t fa_device_id; /**< ID flash-устройства. */
|
||||||
|
uint16_t pad16;
|
||||||
|
uint32_t fa_off; /**< Смещение области от начала устройства. */
|
||||||
|
uint32_t fa_size; /**< Размер области, байт. */
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @brief Сектор внутри области (смещение относительно начала области). */
|
||||||
|
struct flash_sector
|
||||||
|
{
|
||||||
|
uint32_t fs_off;
|
||||||
|
uint32_t fs_size;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @brief Базовый адрес flash-устройства в адресном пространстве MCU (XIP). */
|
||||||
|
int flash_device_base(uint8_t fd_id, uintptr_t *ret);
|
||||||
|
|
||||||
|
int flash_area_open(uint8_t id, const struct flash_area **area);
|
||||||
|
void flash_area_close(const struct flash_area *area);
|
||||||
|
|
||||||
|
/* Read/write/erase — смещение относительно начала области. */
|
||||||
|
int flash_area_read(const struct flash_area *area, uint32_t off, void *dst, uint32_t len);
|
||||||
|
int flash_area_write(const struct flash_area *area, uint32_t off, const void *src, uint32_t len);
|
||||||
|
int flash_area_erase(const struct flash_area *area, uint32_t off, uint32_t len);
|
||||||
|
|
||||||
|
/** @brief Минимальное выравнивание записи. */
|
||||||
|
uint8_t flash_area_align(const struct flash_area *area);
|
||||||
|
|
||||||
|
/** @brief Значение стёртого байта (0xFF для NOR). */
|
||||||
|
uint8_t flash_area_erased_val(const struct flash_area *area);
|
||||||
|
|
||||||
|
/** @brief Прочитать len байт с off и проверить что это стёртая область. */
|
||||||
|
int flash_area_read_is_empty(const struct flash_area *area, uint32_t off, void *dst, uint32_t len);
|
||||||
|
|
||||||
|
int flash_area_get_sectors(int fa_id, uint32_t *count, struct flash_sector *sectors);
|
||||||
|
int flash_area_get_sector(const struct flash_area *fa, uint32_t off, struct flash_sector *sector);
|
||||||
|
|
||||||
|
int flash_area_id_from_image_slot(int slot);
|
||||||
|
int flash_area_id_to_multi_image_slot(int image_index, int area_id);
|
||||||
|
|
||||||
|
#endif /* FLASH_MAP_H_ */
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
/**
|
||||||
|
* @file flash_map_backend.h
|
||||||
|
* @brief Инлайн-аксессоры flash_area/flash_sector, требуемые bootutil.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef FLASH_MAP_BACKEND_H_
|
||||||
|
#define FLASH_MAP_BACKEND_H_
|
||||||
|
|
||||||
|
#include "flash_map.h"
|
||||||
|
|
||||||
|
static inline uint8_t flash_area_get_id(const struct flash_area *fa)
|
||||||
|
{
|
||||||
|
return fa->fa_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline uint8_t flash_area_get_device_id(const struct flash_area *fa)
|
||||||
|
{
|
||||||
|
return fa->fa_device_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline uint32_t flash_area_get_off(const struct flash_area *fa)
|
||||||
|
{
|
||||||
|
return fa->fa_off;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline uint32_t flash_area_get_size(const struct flash_area *fa)
|
||||||
|
{
|
||||||
|
return fa->fa_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline uint32_t flash_sector_get_off(const struct flash_sector *fs)
|
||||||
|
{
|
||||||
|
return fs->fs_off;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline uint32_t flash_sector_get_size(const struct flash_sector *fs)
|
||||||
|
{
|
||||||
|
return fs->fs_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* FLASH_MAP_BACKEND_H_ */
|
||||||
27
firmware/bootloader/mcuboot_port/keys.c
Normal file
27
firmware/bootloader/mcuboot_port/keys.c
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
/**
|
||||||
|
* @file keys.c
|
||||||
|
* @brief Таблица публичных ключей bootutil.
|
||||||
|
*
|
||||||
|
* MCUBOOT_HW_KEY / MCUBOOT_BUILTIN_KEY не определены (см. mcuboot_config.h)
|
||||||
|
* — bootutil использует стандартный путь: TLV образа несёт хэш ключа,
|
||||||
|
* bootutil ищет совпадение в bootutil_keys[] и проверяет подпись найденным
|
||||||
|
* ключом. По образцу sdk/middleware/mcuboot_opensource/boot/nxp_mcux_sdk/keys.c.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <bootutil/sign_key.h>
|
||||||
|
#include <mcuboot_config/mcuboot_config.h>
|
||||||
|
|
||||||
|
#if defined(MCUBOOT_SIGN_EC256)
|
||||||
|
#include "keys/bootloader_test_ecdsa_pub.c"
|
||||||
|
#else
|
||||||
|
#error "No public key available for given signing algorithm."
|
||||||
|
#endif
|
||||||
|
|
||||||
|
const struct bootutil_key bootutil_keys[] = {
|
||||||
|
{
|
||||||
|
.key = ecdsa_pub_key,
|
||||||
|
.len = &ecdsa_pub_key_len,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const int bootutil_key_cnt = 1;
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
/**
|
||||||
|
* @file bootloader_test_ecdsa_pub.c
|
||||||
|
* @brief Публичный ключ ECDSA P-256 — сгенерирован из тестового
|
||||||
|
* sample-ключа MCUboot (sdk/middleware/mcuboot_opensource/root-ec-p256.pem)
|
||||||
|
* командой `imgtool.py getpub --lang c`.
|
||||||
|
*
|
||||||
|
* ВНИМАНИЕ: это публичный, широко известный sample-ключ проекта MCUboot,
|
||||||
|
* НЕ производственный секрет. Используется для Фазы 2 (host-тесты + первая
|
||||||
|
* проверка на железе). Перед серийным производством должен быть заменён
|
||||||
|
* на реальный production-ключ (приватная часть — вне репозитория, см.
|
||||||
|
* жизненный цикл ключей HAB в docs/mimxrt1052/HAB_GUIDE.md — аналогичная
|
||||||
|
* процедура нужна для ключа подписи tft_app-образов).
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Autogenerated by imgtool.py, do not edit. */
|
||||||
|
const unsigned char ecdsa_pub_key[] = {
|
||||||
|
0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06,
|
||||||
|
0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0x2a,
|
||||||
|
0xcb, 0x40, 0x3c, 0xe8, 0xfe, 0xed, 0x5b, 0xa4, 0x49, 0x95, 0xa1, 0xa9, 0x1d, 0xae,
|
||||||
|
0xe8, 0xdb, 0xbe, 0x19, 0x37, 0xcd, 0x14, 0xfb, 0x2f, 0x24, 0x57, 0x37, 0xe5, 0x95,
|
||||||
|
0x39, 0x88, 0xd9, 0x94, 0xb9, 0xd6, 0x5a, 0xeb, 0xd7, 0xcd, 0xd5, 0x30, 0x8a, 0xd6,
|
||||||
|
0xfe, 0x48, 0xb2, 0x4a, 0x6a, 0x81, 0x0e, 0xe5, 0xf0, 0x7d, 0x8b, 0x68, 0x34, 0xcc,
|
||||||
|
0x3a, 0x6a, 0xfc, 0x53, 0x8e, 0xfa, 0xc1,
|
||||||
|
};
|
||||||
|
const unsigned int ecdsa_pub_key_len = 91;
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
/**
|
||||||
|
* @file mcuboot_config.h
|
||||||
|
* @brief Конфигурация bootutil (MCUboot) для загрузчика TFT.
|
||||||
|
*
|
||||||
|
* В отличие от шаблона NXP (nxp_mcux_sdk/include/mcuboot_config/mcuboot_config.h)
|
||||||
|
* задаёт финальные макросы напрямую, без Kconfig-подобной прослойки —
|
||||||
|
* решения зафиксированы в firmware/bootloader/PLAN.md, Фаза 2:
|
||||||
|
*
|
||||||
|
* - Direct-XIP с revert: два слота, оба могут содержать валидный образ,
|
||||||
|
* bootutil выбирает более новую валидную версию; если она ни разу не
|
||||||
|
* подтверждена (confirm) — следующая загрузка откатится на предыдущую
|
||||||
|
* (см. MCUBOOT_DIRECT_XIP_REVERT).
|
||||||
|
* - ECDSA P-256 + TinyCrypt — компактный, полностью вендорен в репозитории
|
||||||
|
* (sdk/middleware/mcuboot_opensource/ext/tinycrypt), в отличие от
|
||||||
|
* mbedTLS (не вендорен, потребовал бы ~8000+ новых строк).
|
||||||
|
* - FIH профиль LOW — часть защиты bootutil (double-read сравнений) без
|
||||||
|
* RNG-задержки (та требует mbedTLS-энтропию, доступно только в профиле
|
||||||
|
* HIGH — не наш случай).
|
||||||
|
* - Heap НЕ нужен: malloc/free в bootutil (loader.c) вызываются только в
|
||||||
|
* swap-режиме, недостижимы под MCUBOOT_DIRECT_XIP.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef MCUBOOT_CONFIG_H_
|
||||||
|
#define MCUBOOT_CONFIG_H_
|
||||||
|
|
||||||
|
/* ── Схема подписи ─────────────────────────────────────────────────────── */
|
||||||
|
#define MCUBOOT_SIGN_EC256
|
||||||
|
|
||||||
|
/* ── Крипто-бэкенд ─────────────────────────────────────────────────────── */
|
||||||
|
#define MCUBOOT_USE_TINYCRYPT
|
||||||
|
|
||||||
|
/* ── Режим обновления ─────────────────────────────────────────────────── */
|
||||||
|
#define MCUBOOT_DIRECT_XIP
|
||||||
|
#define MCUBOOT_DIRECT_XIP_REVERT
|
||||||
|
|
||||||
|
/* Проверять подпись активного слота при каждой загрузке, не только при
|
||||||
|
* установке нового образа. */
|
||||||
|
#define MCUBOOT_VALIDATE_PRIMARY_SLOT
|
||||||
|
|
||||||
|
/* ── Образы ────────────────────────────────────────────────────────────── */
|
||||||
|
#define MCUBOOT_IMAGE_NUMBER 1
|
||||||
|
|
||||||
|
/* ── Flash-абстракция ─────────────────────────────────────────────────── */
|
||||||
|
#define MCUBOOT_USE_FLASH_AREA_GET_SECTORS
|
||||||
|
|
||||||
|
/* Slot A/Б = 2 МБ (см. docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md), сектор
|
||||||
|
* W25Qxx = 4 КБ → 2 МБ / 4 КБ = 512 секторов на слот. */
|
||||||
|
#define MCUBOOT_MAX_IMG_SECTORS 512
|
||||||
|
|
||||||
|
/* ── Fault injection hardening ────────────────────────────────────────── */
|
||||||
|
#define MCUBOOT_FIH_PROFILE_LOW
|
||||||
|
|
||||||
|
/* ── Логирование — отключено, BOOT_LOG_* становятся no-op (bootutil_log.h) */
|
||||||
|
|
||||||
|
/* ── Watchdog — не используется в bootloader ─────────────────────────── */
|
||||||
|
#define MCUBOOT_WATCHDOG_FEED() \
|
||||||
|
do \
|
||||||
|
{ \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
#endif /* MCUBOOT_CONFIG_H_ */
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
/**
|
||||||
|
* @file mcuboot_logging.h
|
||||||
|
* @brief Логирование bootutil — не используется (MCUBOOT_HAVE_LOGGING не
|
||||||
|
* определён в mcuboot_config.h, BOOT_LOG_* становятся no-op через
|
||||||
|
* bootutil_log.h). Этот заголовок обязателен к существованию —
|
||||||
|
* часть заголовков bootutil (bootutil/crypto/sha.h) включает его
|
||||||
|
* безусловно, независимо от MCUBOOT_HAVE_LOGGING.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef MCUBOOT_LOGGING_H_
|
||||||
|
#define MCUBOOT_LOGGING_H_
|
||||||
|
|
||||||
|
#define MCUBOOT_LOG_MODULE_DECLARE(domain)
|
||||||
|
#define MCUBOOT_LOG_MODULE_REGISTER(domain)
|
||||||
|
|
||||||
|
#define MCUBOOT_LOG_ERR(...)
|
||||||
|
#define MCUBOOT_LOG_WRN(...)
|
||||||
|
#define MCUBOOT_LOG_INF(...)
|
||||||
|
#define MCUBOOT_LOG_DBG(...)
|
||||||
|
|
||||||
|
#endif /* MCUBOOT_LOGGING_H_ */
|
||||||
26
firmware/bootloader/mcuboot_port/sysflash/sysflash.h
Normal file
26
firmware/bootloader/mcuboot_port/sysflash/sysflash.h
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
/**
|
||||||
|
* @file sysflash.h
|
||||||
|
* @brief Отображение логических слотов bootutil на flash-area ID.
|
||||||
|
*
|
||||||
|
* MCUBOOT_IMAGE_NUMBER=1 → два ID: Slot A (primary=0), Slot Б (secondary=1).
|
||||||
|
* Без scratch — Direct-XIP не использует область подкачки. Смещения/размеры
|
||||||
|
* самих областей заданы в flash_map_backend.c (см.
|
||||||
|
* docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef SYSFLASH_H_
|
||||||
|
#define SYSFLASH_H_
|
||||||
|
|
||||||
|
#include "mcuboot_config/mcuboot_config.h"
|
||||||
|
|
||||||
|
#define FLASH_AREA_IMAGE_PRIMARY(x) (((x) == 0) ? 0 : 255)
|
||||||
|
#define FLASH_AREA_IMAGE_SECONDARY(x) (((x) == 0) ? 1 : 255)
|
||||||
|
|
||||||
|
#define MCUBOOT_IMAGE_SLOT_NUMBER (MCUBOOT_IMAGE_NUMBER * 2)
|
||||||
|
|
||||||
|
/** @brief Единственное flash-устройство в системе — W25Qxx через FlexSPI. */
|
||||||
|
#define FLASH_DEVICE_ID 1
|
||||||
|
|
||||||
|
int flash_area_id_from_multi_image_slot(int image_index, int slot);
|
||||||
|
|
||||||
|
#endif /* SYSFLASH_H_ */
|
||||||
|
|
@ -218,3 +218,29 @@ add_host_test(
|
||||||
${BSP_MOCKS_DIR})
|
${BSP_MOCKS_DIR})
|
||||||
|
|
||||||
target_compile_definitions(test_firmware_runner PRIVATE UNIT_TEST)
|
target_compile_definitions(test_firmware_runner PRIVATE UNIT_TEST)
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# bootutil (MCUboot Direct-XIP) — выбор слота. Реальный bootutil + TinyCrypt
|
||||||
|
# поверх fake_flash_map_backend.c (in-memory буфер вместо bsp_qspi_flash).
|
||||||
|
# См. firmware/bootloader/PLAN.md, Фаза 2.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
include(${CMAKE_SOURCE_DIR}/firmware/bootloader/mcuboot_port/bootutil_sources.cmake)
|
||||||
|
|
||||||
|
add_host_test(
|
||||||
|
NAME
|
||||||
|
test_mcuboot_boot_select
|
||||||
|
SOURCES
|
||||||
|
mcuboot_port/test_boot_select.c
|
||||||
|
mcuboot_port/fake_flash_map_backend.c
|
||||||
|
mcuboot_port/host_link_shims.c
|
||||||
|
${CMAKE_SOURCE_DIR}/firmware/bootloader/mcuboot_port/keys.c
|
||||||
|
${MCUBOOT_BOOTUTIL_SOURCES}
|
||||||
|
INCLUDES
|
||||||
|
${MCUBOOT_BOOTUTIL_INCLUDES}
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/mcuboot_port)
|
||||||
|
|
||||||
|
target_compile_definitions(
|
||||||
|
test_mcuboot_boot_select
|
||||||
|
PRIVATE FIXTURES_DIR="${CMAKE_CURRENT_SOURCE_DIR}/mcuboot_port/fixtures")
|
||||||
|
|
||||||
|
target_compile_options(test_mcuboot_boot_select PRIVATE -w)
|
||||||
|
|
|
||||||
56
tests/host/button/README.md
Normal file
56
tests/host/button/README.md
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
# test_bsp_button
|
||||||
|
|
||||||
|
## Модуль под тестом
|
||||||
|
|
||||||
|
`bsp/button/src/button.c` (`bsp/button/include/bsp/button.h`)
|
||||||
|
|
||||||
|
## Категория
|
||||||
|
|
||||||
|
B — BSP-модуль, зависит от `fsl_gpio.h`. Unity + fff.
|
||||||
|
|
||||||
|
## Моки
|
||||||
|
|
||||||
|
- `GPIO_ReadPinInput` — единственная SDK-функция, вызываемая `button.c`,
|
||||||
|
fff value-фейк. Управляется напрямую (`return_val`) для одной кнопки или
|
||||||
|
через `SET_RETURN_SEQ` для чередования значений нескольких кнопок в одном
|
||||||
|
`bsp_button_poll()`.
|
||||||
|
- `GPIO_PinInit` не мокируется — `bsp_button_init()` не трогает GPIO
|
||||||
|
напрямую.
|
||||||
|
|
||||||
|
## Что проверяется
|
||||||
|
|
||||||
|
- **Инициализация** — `bsp_button_init()` возвращает `BSP_OK`; повторный
|
||||||
|
вызов сбрасывает накопленное состояние (состояние нажатия и события).
|
||||||
|
- **Сырое чтение** (`bsp_button_read`) — LOW = нажата, HIGH = не нажата,
|
||||||
|
невалидный индекс безопасно возвращает `false`.
|
||||||
|
- **Debounce на нажатие** — событие не срабатывает до порога в 4 стабильных
|
||||||
|
сэмпла, срабатывает на 4-м, состояние остаётся стабильным при удержании,
|
||||||
|
повторных событий на удержании нет.
|
||||||
|
- **Потребление события** — флаг события сбрасывается после первого чтения
|
||||||
|
(`get_event_pressed`/`get_event_released`).
|
||||||
|
- **Debounce на отпускание** — аналогичная логика для перехода в
|
||||||
|
«отпущено».
|
||||||
|
- **Сброс счётчика при глитче** — единичный сэмпл противоположного уровня
|
||||||
|
посреди серии сбрасывает накопленный счётчик debounce.
|
||||||
|
- **Независимость кнопок** — `BSP_BUTTON_1` и `BSP_BUTTON_2` не влияют друг
|
||||||
|
на друга при чередующихся значениях GPIO.
|
||||||
|
- **Граничные индексы** — некорректный `bsp_button_t` не приводит к падению
|
||||||
|
ни в одной публичной функции.
|
||||||
|
|
||||||
|
## Гарантии
|
||||||
|
|
||||||
|
- Debounce требует ровно 4 подряд идущих одинаковых сэмпла для регистрации
|
||||||
|
перехода состояния.
|
||||||
|
- Любой единичный «дребезг» (глитч) сбрасывает накопленный счётчик — не
|
||||||
|
засчитывается частично.
|
||||||
|
- События нажатия/отпускания одноразовые: второй подряд вызов `get_event_*`
|
||||||
|
без нового перехода возвращает `false`.
|
||||||
|
- Кнопки полностью независимы друг от друга.
|
||||||
|
- Невалидный индекс кнопки не вызывает падения — все геттеры возвращают
|
||||||
|
безопасное значение по умолчанию.
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ctest --preset host-debug-test -R test_bsp_button -V
|
||||||
|
```
|
||||||
66
tests/host/can/README.md
Normal file
66
tests/host/can/README.md
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
# test_bsp_can
|
||||||
|
|
||||||
|
## Модуль под тестом
|
||||||
|
|
||||||
|
`bsp/can/src/can.c` + `utils/ring_buffer/ring_buffer.c` (реальный, не
|
||||||
|
мокается — используется как внутренняя зависимость `can.c`).
|
||||||
|
|
||||||
|
## Категория
|
||||||
|
|
||||||
|
B — BSP-модуль, зависит от FlexCAN SDK (`fsl_flexcan.h`, `fsl_clock.h`).
|
||||||
|
Unity + fff.
|
||||||
|
|
||||||
|
## Моки
|
||||||
|
|
||||||
|
Набор fff-фейков SDK FlexCAN: `FLEXCAN_GetDefaultConfig`,
|
||||||
|
`FLEXCAN_CalculateImprovedTimingValues`, `FLEXCAN_Init`, `FLEXCAN_Deinit`,
|
||||||
|
`FLEXCAN_SetTxMbConfig`, `FLEXCAN_SetRxMbConfig`,
|
||||||
|
`FLEXCAN_SetRxIndividualMask`, `FLEXCAN_WriteTxMb`, `FLEXCAN_ReadRxMb`,
|
||||||
|
`FLEXCAN_GetMbStatusFlags`, `FLEXCAN_ClearMbStatusFlags`, а также
|
||||||
|
`bsp_tick_get_ms` (управляемое время для тестов таймаутов) и
|
||||||
|
`CLOCK_EnableClock` (ERRATA 50235 workaround в `bsp_can_init()`).
|
||||||
|
Кастомные `custom_fake`: `read_rx_mb_inject` (подставляет заранее
|
||||||
|
подготовленный RX-фрейм), `get_mb_flags_once` (флаг готовности только на
|
||||||
|
первый вызов), `tick_advancing` (линейно растущее время для таймаутов),
|
||||||
|
`capture_tx_frame` (захват TX-фрейма для проверки конвертации ID/данных).
|
||||||
|
|
||||||
|
## Что проверяется
|
||||||
|
|
||||||
|
- **Init / Deinit** — успешная инициализация; отклонение `NULL`-конфига,
|
||||||
|
нулевого и слишком высокого bitrate, ошибки расчёта тайминга; повторная
|
||||||
|
инициализация вызывает `Deinit` перед новым `Init`; `deinit` без `init`
|
||||||
|
— no-op.
|
||||||
|
- **TX** (`bsp_can_send`) — успешная отправка, `NULL`-фрейм, `dlc > 8`,
|
||||||
|
отправка без инициализации, занятый MB (`BSP_ERR_BUSY`), таймаут
|
||||||
|
ожидания готовности (`BSP_ERR_TIMEOUT`).
|
||||||
|
- **Фильтрация** (`bsp_can_set_filter` / `bsp_can_accept_all`) — STD/EXT
|
||||||
|
ID, индекс фильтра вне диапазона, вызов без инициализации,
|
||||||
|
`accept_all` настраивает минимум 2 RX MB и деактивирует ранее
|
||||||
|
настроенные фильтры.
|
||||||
|
- **RX** (`bsp_can_receive`) — успешный приём STD/EXT фрейма, таймаут,
|
||||||
|
`NULL`-указатель, приём без инициализации, неблокирующий опрос при
|
||||||
|
`timeout_ms == 0`.
|
||||||
|
- **Callback-заглушка** — `bsp_can_register_rx_callback` возвращает
|
||||||
|
`BSP_ERR_NOT_SUPPORTED`.
|
||||||
|
- **Конвертация фреймов** — корректность кодирования STD/EXT ID
|
||||||
|
(`FLEXCAN_ID_STD`/`FLEXCAN_ID_EXT`) и порядка байт данных при переводе
|
||||||
|
между `bsp_can_frame_t` и SDK `flexcan_frame_t`.
|
||||||
|
|
||||||
|
## Гарантии
|
||||||
|
|
||||||
|
- Все публичные функции проверяют, что модуль инициализирован, и
|
||||||
|
возвращают `BSP_ERR_PARAM` в противном случае.
|
||||||
|
- Параметры валидируются перед обращением к SDK: `dlc <= 8`, bitrate в
|
||||||
|
допустимом диапазоне, индекс фильтра в пределах `BSP_CAN_FILTER_MAX`.
|
||||||
|
- TX и RX корректно завершаются по таймауту (`BSP_ERR_TIMEOUT`), если MB не
|
||||||
|
становится готовым.
|
||||||
|
- STD/EXT кодирование идентификатора и порядок байт данных не искажаются
|
||||||
|
при конвертации между форматами приложения и SDK.
|
||||||
|
- `bsp_can_accept_all()` деактивирует ранее настроенные пользовательские
|
||||||
|
фильтры перед установкой приёма «всех кадров».
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ctest --preset host-debug-test -R test_bsp_can -V
|
||||||
|
```
|
||||||
55
tests/host/cli/README.md
Normal file
55
tests/host/cli/README.md
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
# test_cli
|
||||||
|
|
||||||
|
## Модуль под тестом
|
||||||
|
|
||||||
|
`firmware/test/src/cli.c` — построчный разбор JSON-протокола от USB CDC и
|
||||||
|
диспетчеризация команд.
|
||||||
|
|
||||||
|
## Категория
|
||||||
|
|
||||||
|
B — зависит от `bsp_usb_cdc`, `protocol_send_*`, `test_runner_*`,
|
||||||
|
`bsp_prov_read_uid`, подменяемых через fff.
|
||||||
|
|
||||||
|
## Моки
|
||||||
|
|
||||||
|
`bsp_usb_cdc_write`/`bsp_usb_cdc_read` (чтение эмулируется хелпером
|
||||||
|
`inject()`, который наполняет внутренний буфер «как будто USB» и вызывает
|
||||||
|
`cli_process()`), `protocol_send_pong`, `protocol_send_error`,
|
||||||
|
`protocol_send_uid_response`, `protocol_send_version_response`,
|
||||||
|
`test_runner_run_all`, `test_runner_run_single`, `test_runner_run_selected`,
|
||||||
|
`test_runner_on_confirm`, `test_runner_send_list`, `bsp_prov_read_uid`.
|
||||||
|
Кастомные `custom_fake` (`capture_run_single`, `capture_on_confirm`,
|
||||||
|
`capture_uid_response`, `capture_run_selected`) копируют строковые
|
||||||
|
аргументы по значению, т.к. `cli.c` передаёt указатели на собственные
|
||||||
|
локальные буферы, которые становятся dangling после возврата
|
||||||
|
`cli_process()`.
|
||||||
|
|
||||||
|
## Что проверяется
|
||||||
|
|
||||||
|
- **Команды** (`type: cmd`) — `ping`→pong, `run_all`→запуск всех,
|
||||||
|
`run`+`id`→запуск одного с передачей id, `get_uid` (успех и ошибка
|
||||||
|
чтения UID), `get_version`, `list_tests`, `run_selected` с массивом id.
|
||||||
|
- **Подтверждения** (`type: confirm`) — диспетчеризация `confirmed:
|
||||||
|
true/false` с корректным id.
|
||||||
|
- **Ошибки протокола** — отсутствует `type` или обязательное поле
|
||||||
|
(`id`/`confirmed`/`tests`) → `PARSE_ERR`; неизвестный `type`/`cmd` →
|
||||||
|
`UNKNOWN_CMD`; строка длиннее `CLI_LINE_BUF_SIZE` → `LINE_TOO_LONG`.
|
||||||
|
- **Построчный ввод** — пустая строка игнорируется, `\r\n` обрабатывается
|
||||||
|
как `\n`, две команды в одном чтении диспетчеризуются обе по отдельности.
|
||||||
|
|
||||||
|
## Гарантии
|
||||||
|
|
||||||
|
- Каждая валидная команда приводит ровно к одному вызову соответствующего
|
||||||
|
обработчика (`test_runner_*`/`protocol_send_*`).
|
||||||
|
- Любая ошибка парсинга или неизвестная команда отправляет ровно один
|
||||||
|
`protocol_send_error()` с точным, стабильным кодом ошибки.
|
||||||
|
- Построчный парсер корректно разделяет несколько команд в одном чтении и
|
||||||
|
не путает `\r\n` и `\n`.
|
||||||
|
- Переполнение буфера строки не приводит к падению — возвращается
|
||||||
|
контролируемая ошибка `LINE_TOO_LONG`.
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ctest --preset host-debug-test -R test_cli -V
|
||||||
|
```
|
||||||
42
tests/host/led/README.md
Normal file
42
tests/host/led/README.md
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
# test_bsp_led
|
||||||
|
|
||||||
|
## Модуль под тестом
|
||||||
|
|
||||||
|
`bsp/led/src/led.c` (`bsp/led/include/bsp/led.h`)
|
||||||
|
|
||||||
|
## Категория
|
||||||
|
|
||||||
|
B — BSP-модуль, зависит от `fsl_gpio.h`. Unity + fff.
|
||||||
|
|
||||||
|
## Моки
|
||||||
|
|
||||||
|
- `GPIO_PinInit` — фейк, конфигурация захватывается через `custom_fake`
|
||||||
|
(`GPIO_PinInit_capture`) и копируется по значению, т.к. `gpio_pin_config_t`
|
||||||
|
живёт на стеке `bsp_led_init()` и становится dangling после возврата.
|
||||||
|
- `GPIO_PinWrite` — фейк, проверяется последний переданный уровень
|
||||||
|
(`arg2_val`).
|
||||||
|
|
||||||
|
## Что проверяется
|
||||||
|
|
||||||
|
- **Инициализация** — `GPIO_PinInit` вызывается для каждого LED, пины
|
||||||
|
настраиваются как output, начальный уровень HIGH (LED выключен), оба LED
|
||||||
|
выключены сразу после `bsp_led_init()`.
|
||||||
|
- **on / off** — `bsp_led_on()`/`bsp_led_off()` меняют логическое состояние и
|
||||||
|
пишут в GPIO инвертированный уровень.
|
||||||
|
- **toggle** — переключение из off→on, on→off, двойной toggle возвращает
|
||||||
|
исходное состояние.
|
||||||
|
- **set** — `bsp_led_set(led, bool)` включает/выключает по явному флагу.
|
||||||
|
- **Независимость** — состояние `LED_HEARTBEAT` и `LED_APP` не пересекается.
|
||||||
|
|
||||||
|
## Гарантии
|
||||||
|
|
||||||
|
- Полярность active-LOW: `on` → GPIO=0, `off` → GPIO=1.
|
||||||
|
- После инициализации оба LED гарантированно выключены.
|
||||||
|
- `toggle`, применённый чётное число раз, возвращает исходное состояние.
|
||||||
|
- LED-каналы независимы друг от друга.
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ctest --preset host-debug-test -R test_bsp_led -V
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
# test_log
|
||||||
|
|
||||||
|
## Модуль под тестом
|
||||||
|
|
||||||
|
`utils/log/log.c` (`utils/log/log.h`)
|
||||||
|
|
||||||
|
## Категория
|
||||||
|
|
||||||
|
A с fff-хуками — `log.c` не зависит от `fsl_*.h`, но имеет weak-хуки
|
||||||
|
(mutex, timestamp), которые в тесте переопределяются строгими fff-фейками;
|
||||||
|
линковщик выбирает их поверх слабых реализаций из `log.c` автоматически.
|
||||||
|
|
||||||
|
## Моки
|
||||||
|
|
||||||
|
- `log_mutex_init` / `log_mutex_lock` / `log_mutex_unlock` — fff void-фейки,
|
||||||
|
переопределяющие weak-реализации. Для проверки порядка вызовов lock/unlock
|
||||||
|
используют `custom_fake`, инкрементирующий общий монотонный счётчик.
|
||||||
|
- `log_get_timestamp_ms` — fff value-фейк, управляет значением метки времени
|
||||||
|
в выводе.
|
||||||
|
- `capture_cb` — тестовый callback вместо реального UART-транспорта:
|
||||||
|
сохраняет содержимое и длину каждого вызова в `g_s_capture` для проверки
|
||||||
|
через `strstr()`.
|
||||||
|
|
||||||
|
## Что проверяется
|
||||||
|
|
||||||
|
- **Init** — отсутствие вывода до `log_init()` и при `NULL`-callback;
|
||||||
|
callback вызывается один раз на каждый `LOG_*`; повторный `log_init()`
|
||||||
|
заменяет предыдущий callback.
|
||||||
|
- **Output** — наличие символа уровня (`[E]`/`[W]`/`[I]`/`[D]`/`[V]`), тега,
|
||||||
|
текста сообщения (включая форматирование с аргументами) и `\r\n` в конце
|
||||||
|
строки.
|
||||||
|
- **Timestamp** — `log_get_timestamp_ms()` вызывается ровно один раз на
|
||||||
|
каждый `LOG_*`, возвращённое значение попадает в вывод.
|
||||||
|
- **Overflow** — строка длиннее `LOG_BUF_SIZE` (256 байт) обрезается,
|
||||||
|
callback не получает больше `LOG_BUF_SIZE` байт, `\r\n` сохраняется в
|
||||||
|
конце даже при обрезке.
|
||||||
|
- **Mutex** — порядок `lock → callback → unlock` проверяется через
|
||||||
|
монотонный счётчик последовательности; lock/unlock вызываются ровно один
|
||||||
|
раз на лог.
|
||||||
|
- **Context** — указатель контекста, переданный в `log_init()`, доходит до
|
||||||
|
callback без изменений (`NULL` и ненулевой указатель).
|
||||||
|
|
||||||
|
## Гарантии
|
||||||
|
|
||||||
|
- Без вызова `log_init()` (или с `NULL`-callback) логгер не производит
|
||||||
|
вывод.
|
||||||
|
- Вывод никогда не превышает `LOG_BUF_SIZE` байт и всегда заканчивается
|
||||||
|
`\r\n`, даже при переполнении входной строки.
|
||||||
|
- Каждый лог-вызов оборачивается ровно одной парой lock/unlock, callback
|
||||||
|
вызывается строго между ними.
|
||||||
|
- Контекст, переданный в `log_init()`, не искажается при передаче в
|
||||||
|
callback.
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ctest --preset host-debug-test -R test_log -V
|
||||||
|
```
|
||||||
60
tests/host/mcuboot_port/README.md
Normal file
60
tests/host/mcuboot_port/README.md
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
# test_mcuboot_boot_select
|
||||||
|
|
||||||
|
## Модуль под тестом
|
||||||
|
|
||||||
|
Выбор загрузочного слота реальным `bootutil` (MCUboot Direct-XIP + Revert,
|
||||||
|
см. `firmware/bootloader/PLAN.md`, Фаза 2). В тесте линкуется настоящий
|
||||||
|
`bootutil` (`loader.c`, `image_validate.c`, `tlv.c`, ...) и TinyCrypt —
|
||||||
|
тестируется реальная проверка подписи/версии/TLV, а не заглушка
|
||||||
|
криптографии.
|
||||||
|
|
||||||
|
## Категория
|
||||||
|
|
||||||
|
B — интеграционный host-тест: реальная библиотека `bootutil` собирается
|
||||||
|
поверх фейкового flash-бэкенда вместо `bsp_qspi_flash`.
|
||||||
|
|
||||||
|
## Моки
|
||||||
|
|
||||||
|
- `fake_flash_map_backend.c/.h` — полноценная тестовая реализация контракта
|
||||||
|
`flash_map.h` поверх статического буфера в памяти хоста (два смежных
|
||||||
|
слота по `FAKE_FLASH_SLOT_SIZE` = 32 KB) вместо `bsp_qspi_flash`. Это
|
||||||
|
фейк, а не fff-мок: `fake_flash_reset()` стирает оба слота в `0xFF`,
|
||||||
|
`fake_flash_write_slot()` записывает подготовленный образ в нужный слот.
|
||||||
|
- `host_link_shims.c` — заглушки символов, недостающих только при
|
||||||
|
линковке `bootutil` на хосте (`fih_panic_loop` под Mach-O ABI на macOS,
|
||||||
|
недостижимый в ECDSA-only сборке `mbedtls_mpi_read_binary`); не участвуют
|
||||||
|
в тестируемой логике выбора слота.
|
||||||
|
- `fixtures/*.bin` — реальные, подписанные `imgtool` тестовым ключом
|
||||||
|
MCUboot образы (`valid_v1`, `valid_v2`, `valid_v2_unconfirmed`,
|
||||||
|
`corrupt_v1`), загружаются в фейковый flash перед вызовом `boot_go()`.
|
||||||
|
|
||||||
|
## Что проверяется
|
||||||
|
|
||||||
|
- Валиден только Slot A → выбирается Slot A.
|
||||||
|
- Валидны оба слота, версия образа в Slot Б выше → выбирается Slot Б.
|
||||||
|
- Slot Б повреждён (битый хэш/подпись) → игнорируется, выбирается
|
||||||
|
валидный Slot A.
|
||||||
|
- Оба слота пусты/невалидны → `boot_go()` возвращает ошибку.
|
||||||
|
- Direct-XIP Revert: образ выбран, но ни разу не подтверждён
|
||||||
|
(`boot_set_confirmed()` не вызывался) → при следующей загрузке `bootutil`
|
||||||
|
стирает слот, повторный `boot_go()` не находит образ.
|
||||||
|
|
||||||
|
## Гарантии
|
||||||
|
|
||||||
|
- `bootutil` выбирает валидный образ с максимальной версией среди слотов.
|
||||||
|
- Образы с некорректной подписью/хэшем не выбираются и не приводят к
|
||||||
|
падению — они просто игнорируются в пользу валидного слота.
|
||||||
|
- Отсутствие валидного образа в обоих слотах даёт явную ошибку `boot_go()`,
|
||||||
|
а не неопределённое поведение.
|
||||||
|
- Неподтверждённый (unconfirmed) Direct-XIP образ откатывается (слот
|
||||||
|
стирается) при следующей загрузке — anti-brick гарантия механизма
|
||||||
|
Revert.
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ctest --preset host-debug-test -R test_mcuboot_boot_select -V
|
||||||
|
```
|
||||||
|
|
||||||
|
Путь к фикстурам (`FIXTURES_DIR`) прокидывается автоматически через
|
||||||
|
`target_compile_definitions` в `tests/host/CMakeLists.txt`.
|
||||||
189
tests/host/mcuboot_port/fake_flash_map_backend.c
Normal file
189
tests/host/mcuboot_port/fake_flash_map_backend.c
Normal file
|
|
@ -0,0 +1,189 @@
|
||||||
|
/**
|
||||||
|
* @file fake_flash_map_backend.c
|
||||||
|
* @brief Реализация flash_map.h поверх статического буфера в памяти хоста.
|
||||||
|
* См. fake_flash_map_backend.h.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "fake_flash_map_backend.h"
|
||||||
|
|
||||||
|
#include "flash_map.h"
|
||||||
|
#include "sysflash/sysflash.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#define ERASED_VAL 0xFFU
|
||||||
|
|
||||||
|
/** @brief Slot A на [0, FAKE_FLASH_SLOT_SIZE), Slot Б сразу за ним. */
|
||||||
|
static uint8_t g_s_flash_buf[2U * FAKE_FLASH_SLOT_SIZE];
|
||||||
|
|
||||||
|
static const struct flash_area g_s_areas[2] = {
|
||||||
|
{ .fa_id = 0U, .fa_device_id = FLASH_DEVICE_ID, .pad16 = 0U,
|
||||||
|
.fa_off = 0U, .fa_size = FAKE_FLASH_SLOT_SIZE },
|
||||||
|
{ .fa_id = 1U, .fa_device_id = FLASH_DEVICE_ID, .pad16 = 0U,
|
||||||
|
.fa_off = FAKE_FLASH_SLOT_SIZE, .fa_size = FAKE_FLASH_SLOT_SIZE },
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ── Test-only helpers ────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
void fake_flash_reset(void)
|
||||||
|
{
|
||||||
|
memset(g_s_flash_buf, ERASED_VAL, sizeof(g_s_flash_buf));
|
||||||
|
}
|
||||||
|
|
||||||
|
void fake_flash_write_slot(int slot_idx, const uint8_t *p_data, size_t len)
|
||||||
|
{
|
||||||
|
uint8_t *p_dst = g_s_flash_buf + ((size_t) slot_idx * FAKE_FLASH_SLOT_SIZE);
|
||||||
|
memcpy(p_dst, p_data, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── flash_map.h contract ─────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
int flash_device_base(uint8_t fd_id, uintptr_t *ret)
|
||||||
|
{
|
||||||
|
if (fd_id != FLASH_DEVICE_ID)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
*ret = (uintptr_t) g_s_flash_buf;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int flash_area_open(uint8_t id, const struct flash_area **area)
|
||||||
|
{
|
||||||
|
if (id >= 2U)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
*area = &g_s_areas[id];
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void flash_area_close(const struct flash_area *area)
|
||||||
|
{
|
||||||
|
(void) area;
|
||||||
|
}
|
||||||
|
|
||||||
|
int flash_area_read(const struct flash_area *area, uint32_t off, void *dst, uint32_t len)
|
||||||
|
{
|
||||||
|
if (off + len > area->fa_size)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
memcpy(dst, g_s_flash_buf + area->fa_off + off, len);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int flash_area_write(const struct flash_area *area, uint32_t off, const void *src, uint32_t len)
|
||||||
|
{
|
||||||
|
if (off + len > area->fa_size)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
memcpy(g_s_flash_buf + area->fa_off + off, src, len);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int flash_area_erase(const struct flash_area *area, uint32_t off, uint32_t len)
|
||||||
|
{
|
||||||
|
if (off + len > area->fa_size)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
memset(g_s_flash_buf + area->fa_off + off, ERASED_VAL, len);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t flash_area_align(const struct flash_area *area)
|
||||||
|
{
|
||||||
|
(void) area;
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t flash_area_erased_val(const struct flash_area *area)
|
||||||
|
{
|
||||||
|
(void) area;
|
||||||
|
return ERASED_VAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int flash_area_read_is_empty(const struct flash_area *area, uint32_t off, void *dst, uint32_t len)
|
||||||
|
{
|
||||||
|
if (flash_area_read(area, off, dst, len) != 0)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint8_t *p_buf = (const uint8_t *) dst;
|
||||||
|
for (uint32_t i = 0U; i < len; i++)
|
||||||
|
{
|
||||||
|
if (p_buf[i] != ERASED_VAL)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int flash_area_get_sector(const struct flash_area *fa, uint32_t off, struct flash_sector *sector)
|
||||||
|
{
|
||||||
|
if (off >= fa->fa_size)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
sector->fs_off = (off / FAKE_FLASH_SECTOR_SIZE) * FAKE_FLASH_SECTOR_SIZE;
|
||||||
|
sector->fs_size = FAKE_FLASH_SECTOR_SIZE;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int flash_area_get_sectors(int fa_id, uint32_t *count, struct flash_sector *sectors)
|
||||||
|
{
|
||||||
|
const struct flash_area *fa;
|
||||||
|
uint32_t max_cnt = *count;
|
||||||
|
|
||||||
|
if (flash_area_open((uint8_t) fa_id, &fa) != 0)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t rem_len = fa->fa_size;
|
||||||
|
*count = 0U;
|
||||||
|
while ((rem_len > 0U) && (*count < max_cnt))
|
||||||
|
{
|
||||||
|
sectors[*count].fs_off = FAKE_FLASH_SECTOR_SIZE * (*count);
|
||||||
|
sectors[*count].fs_size = FAKE_FLASH_SECTOR_SIZE;
|
||||||
|
(*count)++;
|
||||||
|
rem_len -= FAKE_FLASH_SECTOR_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int flash_area_id_from_multi_image_slot(int image_index, int slot)
|
||||||
|
{
|
||||||
|
switch (slot)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
return FLASH_AREA_IMAGE_PRIMARY(image_index);
|
||||||
|
case 1:
|
||||||
|
return FLASH_AREA_IMAGE_SECONDARY(image_index);
|
||||||
|
default:
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int flash_area_id_from_image_slot(int slot)
|
||||||
|
{
|
||||||
|
return flash_area_id_from_multi_image_slot(0, slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
int flash_area_id_to_multi_image_slot(int image_index, int area_id)
|
||||||
|
{
|
||||||
|
if (area_id == FLASH_AREA_IMAGE_PRIMARY(image_index))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (area_id == FLASH_AREA_IMAGE_SECONDARY(image_index))
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
34
tests/host/mcuboot_port/fake_flash_map_backend.h
Normal file
34
tests/host/mcuboot_port/fake_flash_map_backend.h
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
/**
|
||||||
|
* @file fake_flash_map_backend.h
|
||||||
|
* @brief Test-only реализация контракта flash_map.h поверх обычной памяти
|
||||||
|
* хоста — заменяет bsp_qspi_flash для host-тестов bootutil (Direct-XIP
|
||||||
|
* выбор слота, см. firmware/bootloader/PLAN.md, Фаза 2).
|
||||||
|
*
|
||||||
|
* Слот A (fa_id=0) и слот Б (fa_id=1) — смежные регионы одного статического
|
||||||
|
* буфера FAKE_SLOT_SIZE байт каждый, имитируют один flash-девайс с двумя
|
||||||
|
* областями (как и на реальном железе — см. BOOTLOADER_FLASH_MAP.md).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef FAKE_FLASH_MAP_BACKEND_H_
|
||||||
|
#define FAKE_FLASH_MAP_BACKEND_H_
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#define FAKE_FLASH_SECTOR_SIZE 0x1000U /* 4 KB — как реальный W25Qxx */
|
||||||
|
#define FAKE_FLASH_SLOT_SIZE 0x8000U /* 32 KB — уменьшенный тестовый слот */
|
||||||
|
|
||||||
|
/** @brief Заполнить всю fake-флеш 0xFF (оба слота — "стёрты"). */
|
||||||
|
void fake_flash_reset(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Записать содержимое (например, imgtool-подписанный фикстур-образ)
|
||||||
|
* в начало указанного слота.
|
||||||
|
*
|
||||||
|
* @param[in] slot_idx 0 — Slot A, 1 — Slot Б.
|
||||||
|
* @param[in] p_data Данные образа.
|
||||||
|
* @param[in] len Длина данных, <= FAKE_FLASH_SLOT_SIZE.
|
||||||
|
*/
|
||||||
|
void fake_flash_write_slot(int slot_idx, const uint8_t *p_data, size_t len);
|
||||||
|
|
||||||
|
#endif /* FAKE_FLASH_MAP_BACKEND_H_ */
|
||||||
BIN
tests/host/mcuboot_port/fixtures/corrupt_v1.bin
Normal file
BIN
tests/host/mcuboot_port/fixtures/corrupt_v1.bin
Normal file
Binary file not shown.
BIN
tests/host/mcuboot_port/fixtures/valid_v1.bin
Normal file
BIN
tests/host/mcuboot_port/fixtures/valid_v1.bin
Normal file
Binary file not shown.
BIN
tests/host/mcuboot_port/fixtures/valid_v2.bin
Normal file
BIN
tests/host/mcuboot_port/fixtures/valid_v2.bin
Normal file
Binary file not shown.
BIN
tests/host/mcuboot_port/fixtures/valid_v2_unconfirmed.bin
Normal file
BIN
tests/host/mcuboot_port/fixtures/valid_v2_unconfirmed.bin
Normal file
Binary file not shown.
47
tests/host/mcuboot_port/host_link_shims.c
Normal file
47
tests/host/mcuboot_port/host_link_shims.c
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
/**
|
||||||
|
* @file host_link_shims.c
|
||||||
|
* @brief Заглушки символов, недостающих только при линковке bootutil на
|
||||||
|
* хосте, но не на реальном ARM-таргете. Не часть mcuboot_port/
|
||||||
|
* (портируемого слоя) — специфично для host-тестов.
|
||||||
|
*
|
||||||
|
* 1. fih_panic_loop() — тело в bootutil/src/fault_injection_hardening.c
|
||||||
|
* написано как ARM inline asm, self-reference по имени без подчёркивания
|
||||||
|
* ("b fih_panic_loop"). На arm-none-eabi-gcc (реальный таргет) это
|
||||||
|
* резолвится нативно — там C-символы не манглятся подчёркиванием.
|
||||||
|
*
|
||||||
|
* На хосте картина зависит от ABI, а не просто от "это host-тест":
|
||||||
|
* - Mach-O (macOS): C-функция "fih_panic_loop" компилируется в символ
|
||||||
|
* "_fih_panic_loop" — inline asm ищет ровно "fih_panic_loop" без
|
||||||
|
* подчёркивания и не находит. Нужен явный символ через GNU asm-label
|
||||||
|
* (см. ниже, только под __APPLE__).
|
||||||
|
* - ELF (Linux, напр. devcontainer clang-17): C-символы НЕ манглятся
|
||||||
|
* подчёркиванием — "fih_panic_loop" резолвится сам на себя нативно,
|
||||||
|
* как и на ARM. Наш шим здесь не нужен и создаёт konфликт
|
||||||
|
* ("multiple definition") с уже существующим определением в
|
||||||
|
* fault_injection_hardening.c — поэтому строго под __APPLE__.
|
||||||
|
*
|
||||||
|
* 2. mbedtls_mpi_read_binary() — используется только mbedtls_asn1_get_mpi()
|
||||||
|
* (RSA-путь ASN.1, ext/mbedtls-asn1/src/asn1parse.c), недостижимо в нашей
|
||||||
|
* ECDSA-only конфигурации. На ARM-таргете --gc-sections вырезает мёртвый
|
||||||
|
* вызов до того, как он потребует символ; host-линковка (Mach-O и ELF
|
||||||
|
* одинаково) без --gc-sections требует явного разрешения — недостижимая
|
||||||
|
* по рантайму заглушка, платформенно-независима.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#if defined(__APPLE__)
|
||||||
|
void fih_panic_loop_impl(void) asm("fih_panic_loop");
|
||||||
|
void fih_panic_loop_impl(void)
|
||||||
|
{
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif /* __APPLE__ */
|
||||||
|
|
||||||
|
int mbedtls_mpi_read_binary(void *X, const unsigned char *buf, unsigned long len)
|
||||||
|
{
|
||||||
|
(void) X;
|
||||||
|
(void) buf;
|
||||||
|
(void) len;
|
||||||
|
return -1; /* недостижимо — ECDSA-only, RSA-путь ASN.1 не вызывается */
|
||||||
|
}
|
||||||
149
tests/host/mcuboot_port/test_boot_select.c
Normal file
149
tests/host/mcuboot_port/test_boot_select.c
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
/**
|
||||||
|
* @file test_boot_select.c
|
||||||
|
* @brief Host-тесты выбора слота bootutil (MCUboot Direct-XIP + Revert).
|
||||||
|
*
|
||||||
|
* Реальный bootutil (loader.c, image_validate.c, tlv.c, ...) + TinyCrypt
|
||||||
|
* поверх fake_flash_map_backend.c (in-memory буфер вместо bsp_qspi_flash).
|
||||||
|
* Фикстуры в fixtures/ подписаны настоящим imgtool тестовым ключом
|
||||||
|
* MCUboot (root-ec-p256.pem, см. mcuboot_port/keys/bootloader_test_ecdsa_pub.c)
|
||||||
|
* — тестируется реальная проверка подписи/версии/TLV, не мок.
|
||||||
|
*
|
||||||
|
* Сценарии — firmware/bootloader/PLAN.md, Фаза 2:
|
||||||
|
* 1. Валиден только Slot A → выбран A.
|
||||||
|
* 2. Оба валидны, версия Б выше → выбран Б.
|
||||||
|
* 3. Slot Б повреждён (битый хэш/подпись) → игнорируется, выбран A.
|
||||||
|
* 4. Оба слота пусты/невалидны → boot_go() возвращает ошибку (триггер
|
||||||
|
* top-level состояния "нет образа" из Фазы 3).
|
||||||
|
* 5. Direct-XIP Revert: образ выбран, но ни разу не confirmed → при
|
||||||
|
* следующей загрузке bootutil стирает слот и boot_go() проваливается.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "unity.h"
|
||||||
|
|
||||||
|
#include "fake_flash_map_backend.h"
|
||||||
|
|
||||||
|
#include "bootutil/bootutil.h"
|
||||||
|
#include "bootutil/fault_injection_hardening.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#ifndef FIXTURES_DIR
|
||||||
|
#error "FIXTURES_DIR must be defined by CMake (see tests/host/CMakeLists.txt)"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static uint8_t g_s_fixture_buf[FAKE_FLASH_SLOT_SIZE];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Загрузить фикстур-файл в g_s_fixture_buf.
|
||||||
|
* @return Число прочитанных байт.
|
||||||
|
*/
|
||||||
|
static size_t load_fixture(const char *p_name)
|
||||||
|
{
|
||||||
|
char path[256];
|
||||||
|
(void) snprintf(path, sizeof(path), "%s/%s", FIXTURES_DIR, p_name);
|
||||||
|
|
||||||
|
FILE *p_file = fopen(path, "rb");
|
||||||
|
TEST_ASSERT_NOT_NULL_MESSAGE(p_file, path);
|
||||||
|
|
||||||
|
size_t n = fread(g_s_fixture_buf, 1U, sizeof(g_s_fixture_buf), p_file);
|
||||||
|
(void) fclose(p_file);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(FAKE_FLASH_SLOT_SIZE, n);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setUp(void)
|
||||||
|
{
|
||||||
|
fake_flash_reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void tearDown(void)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Сценарий 1 — валиден только Slot A ──────────────────────────────── */
|
||||||
|
|
||||||
|
void test_boot_go_slot_a_only_valid(void)
|
||||||
|
{
|
||||||
|
fake_flash_write_slot(0, g_s_fixture_buf, load_fixture("valid_v1.bin"));
|
||||||
|
|
||||||
|
struct boot_rsp rsp;
|
||||||
|
fih_ret fih_rc = boot_go(&rsp);
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(FIH_EQ(fih_rc, FIH_SUCCESS));
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(0U, rsp.br_image_off);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Сценарий 2 — оба валидны, побеждает более новая версия ──────────── */
|
||||||
|
|
||||||
|
void test_boot_go_picks_higher_version(void)
|
||||||
|
{
|
||||||
|
fake_flash_write_slot(0, g_s_fixture_buf, load_fixture("valid_v1.bin"));
|
||||||
|
fake_flash_write_slot(1, g_s_fixture_buf, load_fixture("valid_v2.bin"));
|
||||||
|
|
||||||
|
struct boot_rsp rsp;
|
||||||
|
fih_ret fih_rc = boot_go(&rsp);
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(FIH_EQ(fih_rc, FIH_SUCCESS));
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(FAKE_FLASH_SLOT_SIZE, rsp.br_image_off);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Сценарий 3 — повреждённый Slot Б игнорируется ───────────────────── */
|
||||||
|
|
||||||
|
void test_boot_go_ignores_corrupted_slot(void)
|
||||||
|
{
|
||||||
|
fake_flash_write_slot(0, g_s_fixture_buf, load_fixture("valid_v1.bin"));
|
||||||
|
fake_flash_write_slot(1, g_s_fixture_buf, load_fixture("corrupt_v1.bin"));
|
||||||
|
|
||||||
|
struct boot_rsp rsp;
|
||||||
|
fih_ret fih_rc = boot_go(&rsp);
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(FIH_EQ(fih_rc, FIH_SUCCESS));
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(0U, rsp.br_image_off);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Сценарий 4 — оба слота пусты → нет загружаемого образа ──────────── */
|
||||||
|
|
||||||
|
void test_boot_go_no_valid_image(void)
|
||||||
|
{
|
||||||
|
/* fake_flash_reset() в setUp уже оставил оба слота стёртыми (0xFF) */
|
||||||
|
struct boot_rsp rsp;
|
||||||
|
fih_ret fih_rc = boot_go(&rsp);
|
||||||
|
|
||||||
|
TEST_ASSERT_FALSE(FIH_EQ(fih_rc, FIH_SUCCESS));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Сценарий 5 — Direct-XIP Revert: неподтверждённый образ стирается ── */
|
||||||
|
|
||||||
|
void test_boot_go_reverts_unconfirmed_image(void)
|
||||||
|
{
|
||||||
|
fake_flash_write_slot(0, g_s_fixture_buf, load_fixture("valid_v2_unconfirmed.bin"));
|
||||||
|
|
||||||
|
struct boot_rsp rsp;
|
||||||
|
|
||||||
|
/* Первая загрузка: образ валиден, выбран, но boot_set_confirmed() никто
|
||||||
|
* не вызвал (симулируем что tft_app не подтвердила себя). */
|
||||||
|
fih_ret fih_rc = boot_go(&rsp);
|
||||||
|
TEST_ASSERT_TRUE(FIH_EQ(fih_rc, FIH_SUCCESS));
|
||||||
|
|
||||||
|
/* Вторая загрузка "после перезагрузки" — bootutil видит copy_done=SET,
|
||||||
|
* image_ok не SET → стирает слот и не находит образ. */
|
||||||
|
fih_rc = boot_go(&rsp);
|
||||||
|
TEST_ASSERT_FALSE(FIH_EQ(fih_rc, FIH_SUCCESS));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Точка входа ───────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
UNITY_BEGIN();
|
||||||
|
|
||||||
|
RUN_TEST(test_boot_go_slot_a_only_valid);
|
||||||
|
RUN_TEST(test_boot_go_picks_higher_version);
|
||||||
|
RUN_TEST(test_boot_go_ignores_corrupted_slot);
|
||||||
|
RUN_TEST(test_boot_go_no_valid_image);
|
||||||
|
RUN_TEST(test_boot_go_reverts_unconfirmed_image);
|
||||||
|
|
||||||
|
return UNITY_END();
|
||||||
|
}
|
||||||
68
tests/host/opto/README.md
Normal file
68
tests/host/opto/README.md
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
# test_bsp_opto
|
||||||
|
|
||||||
|
## Модуль под тестом
|
||||||
|
|
||||||
|
`bsp/opto/src/opto.c` (`bsp/opto/include/bsp/opto.h`)
|
||||||
|
|
||||||
|
## Категория
|
||||||
|
|
||||||
|
B — BSP-модуль, зависит от `fsl_gpio.h` и `bsp/tick.h`. Unity + fff.
|
||||||
|
|
||||||
|
## Моки
|
||||||
|
|
||||||
|
SDK GPIO/NVIC: `GPIO_PinInit`, `GPIO_PinWrite`, `GPIO_PinRead`,
|
||||||
|
`GPIO_SetPinInterruptConfig`, `GPIO_EnableInterrupts`,
|
||||||
|
`GPIO_DisableInterrupts`, `GPIO_GetPinsInterruptFlags`,
|
||||||
|
`GPIO_ClearPinsInterruptFlags`, `EnableIRQ`. Плюс BSP-зависимости:
|
||||||
|
`BOARD_InitRS_GPIO` (`board.h`-стаб), `bsp_tick_get_ms` (`bsp/tick.h`-стаб).
|
||||||
|
Кастомные `custom_fake`: захват конфигурации `GPIO_PinInit` и
|
||||||
|
`GPIO_SetPinInterruptConfig` по значению (пин + структура конфигурации).
|
||||||
|
ISR-обработчик `GPIO1_Combined_16_31_IRQHandler()` вызывается напрямую из
|
||||||
|
теста (`simulate_isr`) с заранее выставленными
|
||||||
|
`GPIO_GetPinsInterruptFlags`/`GPIO_PinRead` — реальное прерывание не
|
||||||
|
эмулируется, дёргается тот же код, что вызвало бы железо.
|
||||||
|
|
||||||
|
## Что проверяется
|
||||||
|
|
||||||
|
- **Инициализация** — число и параметры настраиваемых каналов в
|
||||||
|
зависимости от `rs_as_gpio` (2 канала IN1/IN2, либо 3 с RS), направление
|
||||||
|
всегда input, корректные номера пинов, IRQ включается на каждый канал и
|
||||||
|
глобально, `BOARD_InitRS_GPIO()` вызывается только при `rs_as_gpio ==
|
||||||
|
true`.
|
||||||
|
- **Начальный фронт для MODE_LEVEL** — если пин LOW (INACTIVE) при init,
|
||||||
|
выбирается RISING; если HIGH (ACTIVE) — FALLING.
|
||||||
|
- **Переключение фронта в ISR** (MODE_LEVEL) — после RISING фронта ISR
|
||||||
|
переключает ожидание на FALLING и обратно.
|
||||||
|
- **bsp_opto_read** — начальное состояние по уровню пина, отключённый
|
||||||
|
RS-канал и некорректный номер канала всегда дают INACTIVE.
|
||||||
|
- **bsp_opto_process — дебаунс (MODE_LEVEL)** — коллбэк не срабатывает до
|
||||||
|
истечения `debounce_ms`, срабатывает после с правильными
|
||||||
|
каналом/состоянием, не срабатывает повторно если состояние не изменилось
|
||||||
|
или уже обработано предыдущим `process()`.
|
||||||
|
- **Независимость каналов** — срабатывание одного канала не влияет на
|
||||||
|
состояние и коллбэки другого.
|
||||||
|
- **MODE_PROTO** — коллбэк вызывается синхронно прямо в ISR (не в
|
||||||
|
`process()`), после срабатывания IRQ канала отключается,
|
||||||
|
`bsp_opto_process()` не генерирует для него коллбэков,
|
||||||
|
`bsp_opto_proto_arm()` перевзводит прерывание (для PROTO) и является
|
||||||
|
no-op для LEVEL-канала, `bsp_opto_read()` для PROTO-канала всегда
|
||||||
|
INACTIVE.
|
||||||
|
|
||||||
|
## Гарантии
|
||||||
|
|
||||||
|
- Полярность active-HIGH: `raw=1` → `ACTIVE`, `raw=0` → `INACTIVE`.
|
||||||
|
- MODE_LEVEL: переходы состояния дебаунсятся (`debounce_ms`) и
|
||||||
|
подтверждаются только в `bsp_opto_process()`; ISR лишь фиксирует
|
||||||
|
«pending» и переключает направление ожидаемого фронта.
|
||||||
|
- MODE_PROTO: обрабатывается целиком в ISR, коллбэк синхронный,
|
||||||
|
`bsp_opto_process()` его не трогает.
|
||||||
|
- Каналы полностью независимы — событие на одном не искажает состояние
|
||||||
|
другого.
|
||||||
|
- Некорректный номер канала не приводит к падению, все геттеры
|
||||||
|
возвращают безопасное значение по умолчанию (`INACTIVE`).
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ctest --preset host-debug-test -R test_bsp_opto -V
|
||||||
|
```
|
||||||
56
tests/host/prio_queue/README.md
Normal file
56
tests/host/prio_queue/README.md
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
# test_prio_queue
|
||||||
|
|
||||||
|
## Модуль под тестом
|
||||||
|
|
||||||
|
`utils/prio_queue/prio_queue.c` (`utils/prio_queue/prio_queue.h`) — очередь
|
||||||
|
с приоритетами на отсортированном массиве фиксированной ёмкости, с
|
||||||
|
вытеснением наименее приоритетного элемента при переполнении.
|
||||||
|
|
||||||
|
## Категория
|
||||||
|
|
||||||
|
A — платформонезависимый модуль. Только Unity.
|
||||||
|
|
||||||
|
## Моки
|
||||||
|
|
||||||
|
Нет.
|
||||||
|
|
||||||
|
## Что проверяется
|
||||||
|
|
||||||
|
- **init** — размер 0, `peek`/`at` на пустой очереди возвращают `NULL`.
|
||||||
|
- **insert_order** — сортировка по приоритету не зависит от порядка
|
||||||
|
вставки (по возрастанию, по убыванию, вперемешку).
|
||||||
|
- **fifo** — при равных приоритетах порядок вставки сохраняется
|
||||||
|
(стабильная сортировка), включая перемешанные группы разных приоритетов.
|
||||||
|
- **full_eviction** — вставка в заполненную очередь: более приоритетный
|
||||||
|
элемент вытесняет худший (`PQ_EVICTED`), менее или равно приоритетный
|
||||||
|
отклоняется (`PQ_FULL`); размер очереди не меняется ни в одном из
|
||||||
|
случаев.
|
||||||
|
- **evict_correct** — вытесняется именно наименее приоритетный элемент,
|
||||||
|
верхушка (`peek`) остаётся корректной после вытеснения.
|
||||||
|
- **peek** — не удаляет элемент при повторных вызовах, указывает на
|
||||||
|
элемент во внутреннем хранилище очереди.
|
||||||
|
- **remove_at** — удаление первого/среднего/последнего элемента со сдвигом
|
||||||
|
остальных, no-op при индексе вне диапазона или на пустой очереди.
|
||||||
|
- **at** — доступ по индексу, `NULL` для индекса вне диапазона.
|
||||||
|
- **size** — инкремент при вставке, декремент при удалении, не меняется
|
||||||
|
при `PQ_FULL`.
|
||||||
|
- **stress** — полный цикл вставки всех элементов и последовательного
|
||||||
|
извлечения с проверкой сохранения порядка; чередование вставок с
|
||||||
|
одинаковыми и разными приоритетами.
|
||||||
|
|
||||||
|
## Гарантии
|
||||||
|
|
||||||
|
- `peek()` / `at(0)` всегда возвращает элемент с наивысшим приоритетом
|
||||||
|
(наименьшим значением согласно переданной `cmp`-функции).
|
||||||
|
- При равных приоритетах порядок извлечения строго FIFO.
|
||||||
|
- При переполнении: более приоритетная вставка вытесняет наименее
|
||||||
|
приоритетный существующий элемент (`PQ_EVICTED`), иначе отклоняется
|
||||||
|
(`PQ_FULL`) без изменения состояния очереди.
|
||||||
|
- `remove_at()` на некорректном индексе или пустой очереди — безопасный
|
||||||
|
no-op, не приводит к падению или порче данных.
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ctest --preset host-debug-test -R test_prio_queue -V
|
||||||
|
```
|
||||||
43
tests/host/protocol/README.md
Normal file
43
tests/host/protocol/README.md
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
# test_protocol
|
||||||
|
|
||||||
|
## Модуль под тестом
|
||||||
|
|
||||||
|
`firmware/test/src/protocol.c` — сериализация JSON-событий протокола
|
||||||
|
(`session_start`, `test_begin`, `test_result`, `summary`, `confirm_request`,
|
||||||
|
`pong`, `error`).
|
||||||
|
|
||||||
|
## Категория
|
||||||
|
|
||||||
|
B — зависит от `cli_send()` и `bsp_tick_get_ms()`, подменяемых через fff.
|
||||||
|
|
||||||
|
## Моки
|
||||||
|
|
||||||
|
- `cli_send` — fff void-фейк; итоговая строка захватывается через
|
||||||
|
`custom_fake` (`capture_cli_send`) в статический буфер, т.к. аргумент —
|
||||||
|
указатель на стековый буфер `protocol_send_*()`, живой только до возврата.
|
||||||
|
- `bsp_tick_get_ms` — fff value-фейк, управляет значением `uptime_ms` в
|
||||||
|
`session_start`.
|
||||||
|
- `bsp_delay` — fff void-фейк-заглушка (не используется в проверках).
|
||||||
|
|
||||||
|
## Что проверяется
|
||||||
|
|
||||||
|
Точный JSON-текст, передаваемый в `cli_send()`, для каждого типа сообщения:
|
||||||
|
`session_start` (нулевой и ненулевой uptime, версия прошивки), `test_begin`
|
||||||
|
(`critical: true/false`), `test_result` (`pass`, `fail` с деталями, `skip`),
|
||||||
|
`summary` (общий результат pass/fail), `confirm_request` (кастомный таймаут
|
||||||
|
и подстановка таймаута по умолчанию при `timeout_ms == 0`), `pong`, `error`
|
||||||
|
(разные коды ошибок).
|
||||||
|
|
||||||
|
## Гарантии
|
||||||
|
|
||||||
|
- Формат JSON точно соответствует протоколу — сравнивается побайтово с
|
||||||
|
эталонной строкой, а не только наличием отдельных полей.
|
||||||
|
- Каждый `protocol_send_*()` вызывает `cli_send()` ровно один раз.
|
||||||
|
- `confirm_request` с `timeout_ms == 0` всегда получает
|
||||||
|
`PROTOCOL_CONFIRM_TIMEOUT_MS` вместо нуля.
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ctest --preset host-debug-test -R test_protocol -V
|
||||||
|
```
|
||||||
51
tests/host/ring_buffer/README.md
Normal file
51
tests/host/ring_buffer/README.md
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
# test_ring_buffer
|
||||||
|
|
||||||
|
## Модуль под тестом
|
||||||
|
|
||||||
|
`utils/ring_buffer/ring_buffer.c` (`utils/ring_buffer/ring_buffer.h`)
|
||||||
|
|
||||||
|
## Категория
|
||||||
|
|
||||||
|
A — платформонезависимый модуль, только стандартная библиотека C. Только Unity.
|
||||||
|
|
||||||
|
## Моки
|
||||||
|
|
||||||
|
Нет — модуль не имеет внешних зависимостей, тестируется напрямую.
|
||||||
|
|
||||||
|
## Что проверяется
|
||||||
|
|
||||||
|
- **Init** — корректная инициализация с валидными параметрами; отклонение
|
||||||
|
`NULL`-указателей, нулевого размера и размера, не являющегося степенью
|
||||||
|
двойки; приём всех степеней двойки от 1 до 256.
|
||||||
|
- **Put / Get** — запись/чтение одного байта, `get` из пустого буфера, `put` в
|
||||||
|
полный буфер, порядок FIFO.
|
||||||
|
- **Write / Read** — блочная запись/чтение, частичная запись при нехватке
|
||||||
|
места, частичное чтение при нехватке данных, чтение из пустого буфера.
|
||||||
|
- **Full / Empty** — граничные состояния и счётчики (`count`, `free`) до и
|
||||||
|
после заполнения/опустошения.
|
||||||
|
- **Wraparound** — несколько циклов заполнения/опустошения, переход индексов
|
||||||
|
через границу массива, сохранение целостности данных при частичном
|
||||||
|
перекрытии индексов после сдвига к краю буфера.
|
||||||
|
- **SPSC simulation** — 200 итераций чередующихся `put`/`get` (имитация
|
||||||
|
ISR-producer / task-consumer в один поток) с проверкой сохранения порядка
|
||||||
|
данных.
|
||||||
|
- **Reset** — сброс состояния и корректное повторное использование буфера
|
||||||
|
без остаточных артефактов.
|
||||||
|
|
||||||
|
## Гарантии
|
||||||
|
|
||||||
|
- `ring_buffer_init()` отклоняет `NULL`-указатели, нулевой и не
|
||||||
|
степень-двойки размер.
|
||||||
|
- Данные извлекаются в порядке FIFO для любых сочетаний put/get и write/read.
|
||||||
|
- Индексация корректно оборачивается через границу массива (маска `&`) на
|
||||||
|
любом протестированном размере — данные не повреждаются при wraparound.
|
||||||
|
- `put`/`write` в полный буфер и `get`/`read` из пустого не повреждают
|
||||||
|
состояние и не пишут за границы переданных буферов.
|
||||||
|
- `ring_buffer_reset()` приводит буфер в состояние, идентичное состоянию
|
||||||
|
сразу после `init`.
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ctest --preset host-debug-test -R test_ring_buffer -V
|
||||||
|
```
|
||||||
61
tests/host/runner/README.md
Normal file
61
tests/host/runner/README.md
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
# test_firmware_runner
|
||||||
|
|
||||||
|
## Модуль под тестом
|
||||||
|
|
||||||
|
`firmware/test/src/test_runner.c` — state machine выполнения реестра
|
||||||
|
тестовых модулей.
|
||||||
|
|
||||||
|
## Категория
|
||||||
|
|
||||||
|
B — зависит от `bsp_tick_get_ms`, `bsp_delay`, `bsp_usb_cdc_poll`,
|
||||||
|
`cli_process`, `protocol_send_*`, подменяемых через fff.
|
||||||
|
|
||||||
|
## Моки
|
||||||
|
|
||||||
|
`bsp_tick_get_ms`, `bsp_delay`, `bsp_usb_cdc_poll`, `cli_process`,
|
||||||
|
`protocol_send_test_begin`, `protocol_send_test_result`,
|
||||||
|
`protocol_send_summary`, `protocol_send_confirm_request`,
|
||||||
|
`protocol_send_error`, `protocol_send_pong`, `protocol_send_session_start`,
|
||||||
|
`protocol_send_test_list`. UNIT_TEST seam: реестр тестовых модулей
|
||||||
|
(`g_unit_test_registry` / `g_unit_test_registry_size`) подставляется этим
|
||||||
|
тестовым файлом вместо реального реестра прошивки — `set_registry()`
|
||||||
|
меняет состав между тестами (сборка выполняется с `-DUNIT_TEST`). Кастомный
|
||||||
|
`custom_fake` `capture_test_result` копирует `test_result_t` по значению,
|
||||||
|
т.к. `protocol_send_test_result` получает указатель на стековую переменную
|
||||||
|
`execute_test()`.
|
||||||
|
|
||||||
|
## Что проверяется
|
||||||
|
|
||||||
|
- **Базовое состояние** — runner не занят изначально; `run_all` на пустом
|
||||||
|
реестре сразу шлёт summary с нулевыми счётчиками; `run_single` с
|
||||||
|
неизвестным id шлёт `UNKNOWN_TEST`.
|
||||||
|
- **Одиночный запуск** — успешный тест шлёт `test_begin` + `test_result` и
|
||||||
|
освобождает runner; проваленный тест шлёт результат со статусом FAIL.
|
||||||
|
- **run_all** — несколько успешных тестов дают верный summary;
|
||||||
|
критический (`critical=true`) провал прерывает выполнение — оставшиеся
|
||||||
|
тесты получают статус SKIP, а `summary` отражает pass/fail/skip и
|
||||||
|
итоговый `overall`.
|
||||||
|
- **pre_confirm state machine** — модуль с `pre_confirm_prompt` переводит
|
||||||
|
runner в состояние ожидания (busy) и шлёт `confirm_request`, не запуская
|
||||||
|
тест; повторный запуск во время ожидания получает ошибку `BUSY`;
|
||||||
|
`on_confirm(id, true)` выполняет тест; `on_confirm(id, false)` помечает
|
||||||
|
тест как SKIP; истечение `PROTOCOL_CONFIRM_TIMEOUT_MS` без ответа тоже
|
||||||
|
даёт SKIP; подтверждение с чужим id игнорируется как устаревшее (stale).
|
||||||
|
|
||||||
|
## Гарантии
|
||||||
|
|
||||||
|
- Критический провал теста в `run_all` прерывает выполнение остальных —
|
||||||
|
они получают статус SKIP, а не PASS/FAIL.
|
||||||
|
- Runner не принимает новый запуск, пока занят (`BUSY`), в частности во
|
||||||
|
время ожидания pre-confirm.
|
||||||
|
- Тест с pre-confirm не выполняется без явного положительного
|
||||||
|
подтверждения: отказ, таймаут ожидания или подтверждение с чужим id не
|
||||||
|
приводят к выполнению теста.
|
||||||
|
- `summary` всегда отражает фактические счётчики passed/failed/skipped и
|
||||||
|
корректный итоговый `overall`.
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ctest --preset host-debug-test -R test_firmware_runner -V
|
||||||
|
```
|
||||||
43
tests/host/timeout/README.md
Normal file
43
tests/host/timeout/README.md
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
# test_timeout_pattern
|
||||||
|
|
||||||
|
## Модуль под тестом
|
||||||
|
|
||||||
|
Не отдельный файл, а паттерн таймаута
|
||||||
|
`(uint32_t)(now - start) >= (uint32_t)timeout`, используемый в
|
||||||
|
`bsp_uart_read()`, `bsp_delay()` и других BSP-модулях (счётчик —
|
||||||
|
`bsp_tick_get_ms()`). Тестируется сама математика, изолированно от железа.
|
||||||
|
|
||||||
|
## Категория
|
||||||
|
|
||||||
|
A — чистая математика, без внешних зависимостей. Только Unity.
|
||||||
|
|
||||||
|
## Моки
|
||||||
|
|
||||||
|
Нет.
|
||||||
|
|
||||||
|
## Что проверяется
|
||||||
|
|
||||||
|
- **Нормальные случаи** — истёкший/неистёкший таймаут при `elapsed <
|
||||||
|
timeout`, `elapsed == timeout`, `elapsed > timeout`; нулевой `elapsed`;
|
||||||
|
`timeout == 0` (истекает немедленно).
|
||||||
|
- **Wraparound** — счётчик `now`/`start` переполняется через `UINT32_MAX`;
|
||||||
|
беззнаковая арифметика корректно вычисляет `elapsed` в этих случаях
|
||||||
|
(проверено на нескольких комбинациях старта у границы `UINT32_MAX`).
|
||||||
|
- **Антипаттерн** — отдельный тест демонстрирует, что альтернативная формула
|
||||||
|
`now >= start + timeout` ломается при переполнении `start + timeout`, в
|
||||||
|
отличие от используемого паттерна.
|
||||||
|
|
||||||
|
## Гарантии
|
||||||
|
|
||||||
|
- Формула `(uint32_t)(now - start) >= timeout` корректна при любом взаимном
|
||||||
|
положении `now` и `start`, включая переполнение 32-битного счётчика.
|
||||||
|
- `elapsed == timeout` считается истёкшим таймаутом (граница включительно).
|
||||||
|
- `timeout == 0` всегда трактуется как немедленно истёкший.
|
||||||
|
- Задокументировано и подтверждено тестом, почему альтернативная формула
|
||||||
|
`now >= start + timeout` небезопасна и не должна использоваться.
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ctest --preset host-debug-test -R test_timeout_pattern -V
|
||||||
|
```
|
||||||
52
tests/host/uart_host/README.md
Normal file
52
tests/host/uart_host/README.md
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# uart_host_mock_example
|
||||||
|
|
||||||
|
## Модуль под тестом
|
||||||
|
|
||||||
|
Не сам `bsp/uart_host/src/uart_host.c` (он верифицируется отдельными
|
||||||
|
HIL-тестами через pyserial на реальном железе), а корректность вызова его
|
||||||
|
публичного API (`bsp/uart_host/include/bsp/uart_host.h`) кодом верхнего
|
||||||
|
уровня — через готовый мок `bsp/uart_host/mocks/uart_host_mock.c`.
|
||||||
|
|
||||||
|
## Категория
|
||||||
|
|
||||||
|
B — зависит от API `bsp_uart_host`, подменяемого через fff.
|
||||||
|
|
||||||
|
## Моки
|
||||||
|
|
||||||
|
Полный fff-мок API `bsp_uart_host`, поставляемый модулем
|
||||||
|
`bsp/uart_host/mocks/uart_host_mock.{c,h}`:
|
||||||
|
`bsp_uart_host_init`, `bsp_uart_host_deinit`, `bsp_uart_host_write`,
|
||||||
|
`bsp_uart_host_write_str`, `bsp_uart_host_read`, `bsp_uart_host_read_byte`,
|
||||||
|
`bsp_uart_host_rx_available`, `bsp_uart_host_rx_flush`. Сброс всех фейков —
|
||||||
|
макросом `UART_HOST_MOCK_RESET_ALL()` в `setUp()`.
|
||||||
|
|
||||||
|
## Что проверяется
|
||||||
|
|
||||||
|
- **init** — baud rate передаётся без искажений, ошибка инициализации
|
||||||
|
(`BSP_ERR_INIT`) пробрасывается наверх.
|
||||||
|
- **write / write_str** — корректные буфер/длина или строка передаются в
|
||||||
|
API, ошибка при неинициализированном UART пробрасывается.
|
||||||
|
- **read_byte** — успешное значение, таймаут (`-1`), конвертация
|
||||||
|
`BSP_UART_HOST_WAIT_FOREVER` → `UINT32_MAX`.
|
||||||
|
- **read** — корректные буфер/размер/таймаут передаются, частичное чтение
|
||||||
|
(меньше запрошенного) не трактуется как ошибка.
|
||||||
|
- **rx helpers** — `rx_available` возвращает счётчик, `rx_flush`
|
||||||
|
вызывается.
|
||||||
|
- **Сброс мока** — `UART_HOST_MOCK_RESET_ALL()` обнуляет счётчики вызовов и
|
||||||
|
`return_val` между тестами.
|
||||||
|
|
||||||
|
## Гарантии
|
||||||
|
|
||||||
|
- Код верхнего уровня передаёт в `bsp_uart_host_*` ровно те аргументы,
|
||||||
|
которые получил сам (baud rate, буфер, длина, таймаут).
|
||||||
|
- Коды ошибок и специальные значения (таймаут `-1`,
|
||||||
|
`BSP_UART_HOST_WAIT_FOREVER`) не теряются и не искажаются на пути через
|
||||||
|
API.
|
||||||
|
- Частичное чтение — штатный случай, а не ошибка.
|
||||||
|
- Между тестами мок гарантированно возвращается в чистое состояние.
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ctest --preset host-debug-test -R uart_host_mock_example -V
|
||||||
|
```
|
||||||
|
|
@ -71,7 +71,7 @@ prio_queue_remove_at(&q, 0); /* удалить верхний после об
|
||||||
|
|
||||||
**Политика вытеснения при полном буфере:**
|
**Политика вытеснения при полном буфере:**
|
||||||
|
|
||||||
```
|
```bash
|
||||||
Очередь полна [A(1) B(2) C(3)], вставляем D(2):
|
Очередь полна [A(1) B(2) C(3)], вставляем D(2):
|
||||||
→ D приоритетнее C(3) → C вытесняется → [A(1) B(2) D(2)] PQ_EVICTED
|
→ D приоритетнее C(3) → C вытесняется → [A(1) B(2) D(2)] PQ_EVICTED
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue