diff --git a/CMakePresets.json b/CMakePresets.json index 7409241..3c2f1f5 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -147,6 +147,7 @@ "uart_host_mock_example", "test_ring_buffer", "test_cli", + "test_prio_queue", "test_timeout_pattern", "uart_host_mock_example" ] @@ -162,6 +163,7 @@ "test_bsp_button", "test_bsp_can", "test_cli", + "test_prio_queue", "uart_host_mock_example", "test_ring_buffer", "test_timeout_pattern", diff --git a/bsp/button/README.md b/bsp/button/README.md index 0f8ee89..d667710 100644 --- a/bsp/button/README.md +++ b/bsp/button/README.md @@ -59,7 +59,7 @@ bsp_button_init(); bsp_button_poll(); /* В основном цикле: */ -if (bsp_button_get_event_pressed(BSP_BUTTON_1)) { +if (**bsp_button_get_event_pressed(BSP_BUTTON_1)**) { /* однократное срабатывание по нажатию */ } diff --git a/bsp/display/CMakeLists.txt b/bsp/display/CMakeLists.txt new file mode 100644 index 0000000..e69de29 diff --git a/bsp/display/README.md b/bsp/display/README.md new file mode 100644 index 0000000..e69de29 diff --git a/bsp/opto/include/bsp/opto.h b/bsp/opto/include/bsp/opto.h index a31d9a7..e0b98a8 100644 --- a/bsp/opto/include/bsp/opto.h +++ b/bsp/opto/include/bsp/opto.h @@ -198,6 +198,7 @@ extern "C" * Должна вызываться из main loop на каждой итерации. НЕ вызывать из ISR. * Каналы MODE_PROTO пропускаются — для них используется коллбэк из ISR. */ + //FIXME: poll void bsp_opto_process(void); /** diff --git a/firmware/test/arch.svg b/firmware/test/arch.svg new file mode 100644 index 0000000..da527f6 --- /dev/null +++ b/firmware/test/arch.svg @@ -0,0 +1,268 @@ + + + + + + + + + + +firmware_test · high-level architecture +NXP IMXRT1052CVJ5B + + +firmware_test + + + + + USB CDC + virtual serial port + +↔ Host PC +(терминал / GUI) + + + + + + + Protocol + JSON-lines · cJSON + + + + + + Test runner + sequencer + registry + + + + + + Local UI + LEDs + display + + + + + + + + + + +self-tests + + + SDRAM + 32 MB + + QSPI + W25Q128 + + uSD + SDIO + + RTC + BM8563 + + Display + operator ✓ + + IR + idle GPIO + + + +HIL tests · Firefly AIO-3588Q + + + CAN + frame exchange + + UART TTL + echo pattern + + UART ISO + +24V RX only + + Opto-in + +24V all ch. + + + + + + HAL / BSP + + + + +firmware_test · test runner flow + + + + + Boot + clock, USB CDC, HAL init + + + + + + + Wait for host + USB connect or timeout + + + + + +self-tests + + + + SDRAM + critical + write/read pattern + + + + QSPI Flash + critical + JEDEC ID + R/W + + + + uSD + critical + mount + R/W + + + + RTC + I2C presence + set / get + + + + Display + R/G/B/W fill + btn confirm + + + + + + + + + + + + Check critical failures + SDRAM || QSPI || uSD FAIL → abort HIL, report immediately + + + + + + +HIL tests · requires Firefly + + + + CAN + full-duplex + + + + UART TTL + echo + + + + UART ISO + RX only + + + + Opto-in + all channels + + + + IR burst + 38 kHz RX + + + + + + + + Summary report + JSON · pass/fail/skip per test · overall + + + + + + + + Provisioning + OCOTP chip_uid → host DB · only on pass + + +only if overall == pass + + + + +critical test + +HIL test + +operator + + diff --git a/tests/host/CMakeLists.txt b/tests/host/CMakeLists.txt index d92e7af..50db4e8 100644 --- a/tests/host/CMakeLists.txt +++ b/tests/host/CMakeLists.txt @@ -173,3 +173,15 @@ add_host_test( ${CMAKE_SOURCE_DIR}/bsp/usb_cdc/include MOCKS ${BSP_MOCKS_DIR}) + +# ----------------------------------------------------------------------------- +# utils/prio_queue +# ----------------------------------------------------------------------------- +add_host_test( + NAME + test_prio_queue + SOURCES + prio_queue/test_prio_queue.c + ${PROJECT_SOURCE_DIR}/utils/prio_queue/prio_queue.c + INCLUDES + ${PROJECT_SOURCE_DIR}/utils) diff --git a/tests/host/prio_queue/test_prio_queue.c b/tests/host/prio_queue/test_prio_queue.c new file mode 100644 index 0000000..a29773d --- /dev/null +++ b/tests/host/prio_queue/test_prio_queue.c @@ -0,0 +1,601 @@ +/** + * @file test_prio_queue.c + * @brief Host unit-тесты для prio_queue (Категория A — без NXP SDK). + * + * Фреймворк: Unity. + * Запуск: ctest --preset host-debug-test -R test_prio_queue -V + * + * Проверяемый инвариант: + * - peek()/remove_at(0) всегда отдаёт элемент с наивысшим приоритетом. + * - При равных приоритетах — FIFO (первым вошёл, первым вышел). + */ + +#include "prio_queue/prio_queue.h" +#include "unity.h" + +#include +#include +#include +/* ── Тестовые типы ─────────────────────────────────────────────────────── */ + +typedef struct +{ + int priority; + const char *name; +} task_t; + +/* cmp: меньший priority → ближе к голове (выше приоритет) */ +static int task_cmp(const void *p_a, const void *p_b) +{ + int pa = ((const task_t *) p_a)->priority; + int pb = ((const task_t *) p_b)->priority; + return (pa < pb) ? -1 : (pa > pb) ? 1 : 0; +} + +/* ── Общие фикстуры ────────────────────────────────────────────────────── */ + +#define CAPACITY 8U + +static task_t s_storage[CAPACITY]; +static prio_queue_t s_q; + +void setUp(void) +{ + memset(s_storage, 0, sizeof(s_storage)); + prio_queue_init(&s_q, s_storage, CAPACITY, sizeof(task_t), task_cmp); +} + +void tearDown(void) +{ +} + +/* ══════════════════════════════════════════════════════════════════════════ + * Группа: init + * ══════════════════════════════════════════════════════════════════════════ */ + +void test_init_size_is_zero(void) +{ + TEST_ASSERT_EQUAL_UINT8(0U, prio_queue_size(&s_q)); +} + +void test_init_peek_returns_null(void) +{ + TEST_ASSERT_NULL(prio_queue_peek(&s_q)); +} + +void test_init_at_returns_null_when_empty(void) +{ + TEST_ASSERT_NULL(prio_queue_at(&s_q, 0U)); +} + +/* ══════════════════════════════════════════════════════════════════════════ + * Группа: insert_order — правильная сортировка + * ══════════════════════════════════════════════════════════════════════════ */ + +void test_insert_single_element(void) +{ + task_t t = { .priority = 5, .name = "t5" }; + pq_status_t ret = prio_queue_insert(&s_q, &t); + + TEST_ASSERT_EQUAL(PQ_OK, ret); + TEST_ASSERT_EQUAL_UINT8(1U, prio_queue_size(&s_q)); +} + +void test_insert_two_ascending_order_peek_is_higher(void) +{ + /* Вставляем менее приоритетный первым */ + task_t low = { .priority = 10, .name = "low" }; + task_t high = { .priority = 1, .name = "high" }; + + prio_queue_insert(&s_q, &low); + prio_queue_insert(&s_q, &high); + + const task_t *p_top = (const task_t *) prio_queue_peek(&s_q); + TEST_ASSERT_NOT_NULL(p_top); + TEST_ASSERT_EQUAL_STRING("high", p_top->name); +} + +void test_insert_descending_input_peek_is_still_highest(void) +{ + task_t a = { .priority = 3, .name = "a" }; + task_t b = { .priority = 2, .name = "b" }; + task_t c = { .priority = 1, .name = "c" }; + + /* Вставляем в убывающем порядке приоритета */ + prio_queue_insert(&s_q, &a); + prio_queue_insert(&s_q, &b); + prio_queue_insert(&s_q, &c); + + const task_t *p_top = (const task_t *) prio_queue_peek(&s_q); + TEST_ASSERT_NOT_NULL(p_top); + TEST_ASSERT_EQUAL_STRING("c", p_top->name); +} + +void test_insert_ascending_input_order_preserved(void) +{ + task_t a = { .priority = 1, .name = "a" }; + task_t b = { .priority = 2, .name = "b" }; + task_t c = { .priority = 3, .name = "c" }; + + prio_queue_insert(&s_q, &a); + prio_queue_insert(&s_q, &b); + prio_queue_insert(&s_q, &c); + + /* Проверяем полный порядок */ + const task_t *p0 = (const task_t *) prio_queue_at(&s_q, 0U); + const task_t *p1 = (const task_t *) prio_queue_at(&s_q, 1U); + const task_t *p2 = (const task_t *) prio_queue_at(&s_q, 2U); + + TEST_ASSERT_EQUAL_STRING("a", p0->name); + TEST_ASSERT_EQUAL_STRING("b", p1->name); + TEST_ASSERT_EQUAL_STRING("c", p2->name); +} + +/* ══════════════════════════════════════════════════════════════════════════ + * Группа: fifo — FIFO при равных приоритетах + * ══════════════════════════════════════════════════════════════════════════ */ + +void test_fifo_two_equal_priority_first_in_first_out(void) +{ + task_t first = { .priority = 2, .name = "first" }; + task_t second = { .priority = 2, .name = "second" }; + + prio_queue_insert(&s_q, &first); + prio_queue_insert(&s_q, &second); + + const task_t *p_top = (const task_t *) prio_queue_peek(&s_q); + TEST_ASSERT_NOT_NULL(p_top); + TEST_ASSERT_EQUAL_STRING("first", p_top->name); +} + +void test_fifo_three_equal_priority_order_preserved(void) +{ + task_t a = { .priority = 5, .name = "a" }; + task_t b = { .priority = 5, .name = "b" }; + task_t c = { .priority = 5, .name = "c" }; + + prio_queue_insert(&s_q, &a); + prio_queue_insert(&s_q, &b); + prio_queue_insert(&s_q, &c); + + const task_t *p0 = (const task_t *) prio_queue_at(&s_q, 0U); + const task_t *p1 = (const task_t *) prio_queue_at(&s_q, 1U); + const task_t *p2 = (const task_t *) prio_queue_at(&s_q, 2U); + + TEST_ASSERT_EQUAL_STRING("a", p0->name); + TEST_ASSERT_EQUAL_STRING("b", p1->name); + TEST_ASSERT_EQUAL_STRING("c", p2->name); +} + +void test_fifo_mixed_priorities_equal_group_fifo(void) +{ + /* Вставляем: high(1), eq_a(2), eq_b(2), low(3) */ + task_t high = { .priority = 1, .name = "high" }; + task_t eq_a = { .priority = 2, .name = "eq_a" }; + task_t eq_b = { .priority = 2, .name = "eq_b" }; + task_t low = { .priority = 3, .name = "low" }; + + prio_queue_insert(&s_q, &high); + prio_queue_insert(&s_q, &eq_a); + prio_queue_insert(&s_q, &eq_b); + prio_queue_insert(&s_q, &low); + + /* Ожидаемый порядок: high, eq_a, eq_b, low */ + const task_t *p0 = (const task_t *) prio_queue_at(&s_q, 0U); + const task_t *p1 = (const task_t *) prio_queue_at(&s_q, 1U); + const task_t *p2 = (const task_t *) prio_queue_at(&s_q, 2U); + const task_t *p3 = (const task_t *) prio_queue_at(&s_q, 3U); + + TEST_ASSERT_EQUAL_STRING("high", p0->name); + TEST_ASSERT_EQUAL_STRING("eq_a", p1->name); + TEST_ASSERT_EQUAL_STRING("eq_b", p2->name); + TEST_ASSERT_EQUAL_STRING("low", p3->name); +} + +/* ══════════════════════════════════════════════════════════════════════════ + * Группа: full_eviction — статусы при заполнении + * ══════════════════════════════════════════════════════════════════════════ */ + +static void fill_queue_with_priority(int priority) +{ + for (uint8_t i = 0U; i < CAPACITY; i++) + { + task_t t = { .priority = priority, .name = "filler" }; + prio_queue_insert(&s_q, &t); + } +} + +void test_full_returns_pq_ok_until_capacity(void) +{ + for (uint8_t i = 0U; i < CAPACITY; i++) + { + task_t t = { .priority = (int) i, .name = "t" }; + pq_status_t ret = prio_queue_insert(&s_q, &t); + TEST_ASSERT_EQUAL(PQ_OK, ret); + } + TEST_ASSERT_EQUAL_UINT8(CAPACITY, prio_queue_size(&s_q)); +} + +void test_full_higher_priority_returns_pq_evicted(void) +{ + fill_queue_with_priority(10); + + task_t better = { .priority = 1, .name = "better" }; + pq_status_t ret = prio_queue_insert(&s_q, &better); + + TEST_ASSERT_EQUAL(PQ_EVICTED, ret); + /* Размер не изменился */ + TEST_ASSERT_EQUAL_UINT8(CAPACITY, prio_queue_size(&s_q)); +} + +void test_full_lower_priority_returns_pq_full(void) +{ + fill_queue_with_priority(1); + + task_t worst = { .priority = 100, .name = "worst" }; + pq_status_t ret = prio_queue_insert(&s_q, &worst); + + TEST_ASSERT_EQUAL(PQ_FULL, ret); + TEST_ASSERT_EQUAL_UINT8(CAPACITY, prio_queue_size(&s_q)); +} + +void test_full_equal_priority_to_all_returns_pq_full(void) +{ + /* Все элементы с prio=5; новый тоже prio=5 → FIFO: новый менее приоритетен */ + fill_queue_with_priority(5); + + task_t same = { .priority = 5, .name = "same" }; + pq_status_t ret = prio_queue_insert(&s_q, &same); + + TEST_ASSERT_EQUAL(PQ_FULL, ret); +} + +/* ══════════════════════════════════════════════════════════════════════════ + * Группа: evict_correct — правильный элемент вытесняется + * ══════════════════════════════════════════════════════════════════════════ */ + +void test_evict_removes_least_prioritized_element(void) +{ + /* Очередь: [1, 2, 3]; вставляем 2 → вытесняется 3 */ + task_t small_q_storage[3]; + memset(small_q_storage, 0, sizeof(small_q_storage)); + + prio_queue_t small_q; + prio_queue_init(&small_q, small_q_storage, 3U, sizeof(task_t), task_cmp); + + task_t a = { .priority = 1, .name = "a" }; + task_t b = { .priority = 2, .name = "b" }; + task_t c = { .priority = 3, .name = "c" }; + + prio_queue_insert(&small_q, &a); + prio_queue_insert(&small_q, &b); + prio_queue_insert(&small_q, &c); + + task_t d = { .priority = 2, .name = "d" }; + pq_status_t ret = prio_queue_insert(&small_q, &d); + + TEST_ASSERT_EQUAL(PQ_EVICTED, ret); + + /* c(3) должна была вытесниться; d(2) должна быть внутри */ + bool found_d = false; + bool found_c = false; + + for (uint8_t i = 0U; i < prio_queue_size(&small_q); i++) + { + const task_t *p_elem = (const task_t *) prio_queue_at(&small_q, i); + if (strcmp(p_elem->name, "d") == 0) + { + found_d = true; + } + if (strcmp(p_elem->name, "c") == 0) + { + found_c = true; + } + } + + TEST_ASSERT_TRUE(found_d); + TEST_ASSERT_FALSE(found_c); +} + +void test_evict_top_still_correct_after_eviction(void) +{ + task_t small_q_storage[2]; + memset(small_q_storage, 0, sizeof(small_q_storage)); + + prio_queue_t small_q; + prio_queue_init(&small_q, small_q_storage, 2U, sizeof(task_t), task_cmp); + + task_t lo = { .priority = 10, .name = "lo" }; + task_t hi = { .priority = 1, .name = "hi" }; + task_t best = { .priority = 0, .name = "best" }; + + prio_queue_insert(&small_q, &lo); + prio_queue_insert(&small_q, &hi); + /* best вытесняет lo */ + prio_queue_insert(&small_q, &best); + + const task_t *p_top = (const task_t *) prio_queue_peek(&small_q); + TEST_ASSERT_NOT_NULL(p_top); + TEST_ASSERT_EQUAL_STRING("best", p_top->name); +} + +/* ══════════════════════════════════════════════════════════════════════════ + * Группа: peek — стабильность указателя и edge-cases + * ══════════════════════════════════════════════════════════════════════════ */ + +void test_peek_null_when_empty(void) +{ + TEST_ASSERT_NULL(prio_queue_peek(&s_q)); +} + +void test_peek_does_not_remove_element(void) +{ + task_t t = { .priority = 3, .name = "t" }; + prio_queue_insert(&s_q, &t); + + prio_queue_peek(&s_q); + prio_queue_peek(&s_q); + + TEST_ASSERT_EQUAL_UINT8(1U, prio_queue_size(&s_q)); +} + +void test_peek_pointer_into_internal_storage(void) +{ + task_t t = { .priority = 7, .name = "stored" }; + prio_queue_insert(&s_q, &t); + + const task_t *p_top = (const task_t *) prio_queue_peek(&s_q); + TEST_ASSERT_EQUAL_STRING("stored", p_top->name); + TEST_ASSERT_EQUAL_INT(7, p_top->priority); +} + +/* ══════════════════════════════════════════════════════════════════════════ + * Группа: remove_at — корректность удаления и сдвига + * ══════════════════════════════════════════════════════════════════════════ */ + +void test_remove_at_first_element(void) +{ + task_t a = { .priority = 1, .name = "a" }; + task_t b = { .priority = 2, .name = "b" }; + task_t c = { .priority = 3, .name = "c" }; + + prio_queue_insert(&s_q, &a); + prio_queue_insert(&s_q, &b); + prio_queue_insert(&s_q, &c); + + prio_queue_remove_at(&s_q, 0U); + + TEST_ASSERT_EQUAL_UINT8(2U, prio_queue_size(&s_q)); + const task_t *p_top = (const task_t *) prio_queue_peek(&s_q); + TEST_ASSERT_EQUAL_STRING("b", p_top->name); +} + +void test_remove_at_middle_element(void) +{ + task_t a = { .priority = 1, .name = "a" }; + task_t b = { .priority = 2, .name = "b" }; + task_t c = { .priority = 3, .name = "c" }; + + prio_queue_insert(&s_q, &a); + prio_queue_insert(&s_q, &b); + prio_queue_insert(&s_q, &c); + + prio_queue_remove_at(&s_q, 1U); /* удаляем b */ + + TEST_ASSERT_EQUAL_UINT8(2U, prio_queue_size(&s_q)); + + const task_t *p0 = (const task_t *) prio_queue_at(&s_q, 0U); + const task_t *p1 = (const task_t *) prio_queue_at(&s_q, 1U); + TEST_ASSERT_EQUAL_STRING("a", p0->name); + TEST_ASSERT_EQUAL_STRING("c", p1->name); +} + +void test_remove_at_last_element(void) +{ + task_t a = { .priority = 1, .name = "a" }; + task_t b = { .priority = 2, .name = "b" }; + + prio_queue_insert(&s_q, &a); + prio_queue_insert(&s_q, &b); + + prio_queue_remove_at(&s_q, 1U); + + TEST_ASSERT_EQUAL_UINT8(1U, prio_queue_size(&s_q)); + const task_t *p_top = (const task_t *) prio_queue_peek(&s_q); + TEST_ASSERT_EQUAL_STRING("a", p_top->name); +} + +void test_remove_at_out_of_bounds_is_nop(void) +{ + task_t t = { .priority = 1, .name = "t" }; + prio_queue_insert(&s_q, &t); + + prio_queue_remove_at(&s_q, 5U); /* idx >= count — должна быть NOP */ + + TEST_ASSERT_EQUAL_UINT8(1U, prio_queue_size(&s_q)); +} + +void test_remove_at_on_empty_queue_is_nop(void) +{ + prio_queue_remove_at(&s_q, 0U); /* не должно упасть */ + TEST_ASSERT_EQUAL_UINT8(0U, prio_queue_size(&s_q)); +} + +/* ══════════════════════════════════════════════════════════════════════════ + * Группа: at — доступ по индексу + * ══════════════════════════════════════════════════════════════════════════ */ + +void test_at_returns_null_for_out_of_bounds(void) +{ + task_t t = { .priority = 1, .name = "t" }; + prio_queue_insert(&s_q, &t); + + TEST_ASSERT_NULL(prio_queue_at(&s_q, 1U)); + TEST_ASSERT_NULL(prio_queue_at(&s_q, 99U)); +} + +void test_at_returns_correct_element(void) +{ + task_t a = { .priority = 1, .name = "a" }; + task_t b = { .priority = 2, .name = "b" }; + + prio_queue_insert(&s_q, &a); + prio_queue_insert(&s_q, &b); + + task_t *p_elem = (task_t *) prio_queue_at(&s_q, 1U); + TEST_ASSERT_NOT_NULL(p_elem); + TEST_ASSERT_EQUAL_STRING("b", p_elem->name); +} + +/* ══════════════════════════════════════════════════════════════════════════ + * Группа: size — счётчик + * ══════════════════════════════════════════════════════════════════════════ */ + +void test_size_increments_on_insert(void) +{ + for (uint8_t i = 0U; i < 4U; i++) + { + task_t t = { .priority = (int) i, .name = "t" }; + prio_queue_insert(&s_q, &t); + TEST_ASSERT_EQUAL_UINT8((uint8_t) (i + 1U), prio_queue_size(&s_q)); + } +} + +void test_size_decrements_on_remove(void) +{ + task_t a = { .priority = 1, .name = "a" }; + task_t b = { .priority = 2, .name = "b" }; + + prio_queue_insert(&s_q, &a); + prio_queue_insert(&s_q, &b); + + prio_queue_remove_at(&s_q, 0U); + TEST_ASSERT_EQUAL_UINT8(1U, prio_queue_size(&s_q)); + + prio_queue_remove_at(&s_q, 0U); + TEST_ASSERT_EQUAL_UINT8(0U, prio_queue_size(&s_q)); +} + +void test_size_unchanged_on_pq_full(void) +{ + fill_queue_with_priority(5); + TEST_ASSERT_EQUAL_UINT8(CAPACITY, prio_queue_size(&s_q)); + + task_t worst = { .priority = 100, .name = "worst" }; + prio_queue_insert(&s_q, &worst); + + TEST_ASSERT_EQUAL_UINT8(CAPACITY, prio_queue_size(&s_q)); +} + +/* ══════════════════════════════════════════════════════════════════════════ + * Группа: stress — последовательность вставок/удалений + * ══════════════════════════════════════════════════════════════════════════ */ + +void test_stress_insert_all_remove_all_in_order(void) +{ + /* Вставляем 8 элементов с приоритетами 8..1 (убывающими) */ + for (int i = (int) CAPACITY; i >= 1; i--) + { + task_t t = { .priority = i, .name = "x" }; + prio_queue_insert(&s_q, &t); + } + + /* Вытаскиваем по одному: каждый раз первый должен быть минимальным приоритетом */ + for (int expected = 1; expected <= (int) CAPACITY; expected++) + { + const task_t *p_top = (const task_t *) prio_queue_peek(&s_q); + TEST_ASSERT_NOT_NULL(p_top); + TEST_ASSERT_EQUAL_INT(expected, p_top->priority); + prio_queue_remove_at(&s_q, 0U); + } + + TEST_ASSERT_EQUAL_UINT8(0U, prio_queue_size(&s_q)); + TEST_ASSERT_NULL(prio_queue_peek(&s_q)); +} + +void test_stress_fifo_across_removes(void) +{ + /* Вставляем поочерёдно prio=1 (a, b, c) и между ними prio=2 */ + task_t a = { .priority = 1, .name = "a" }; + task_t mid = { .priority = 2, .name = "mid" }; + task_t b = { .priority = 1, .name = "b" }; + task_t c = { .priority = 1, .name = "c" }; + + prio_queue_insert(&s_q, &a); + prio_queue_insert(&s_q, &mid); + prio_queue_insert(&s_q, &b); + prio_queue_insert(&s_q, &c); + + /* Порядок: a(1), b(1), c(1), mid(2) */ + const char *expected_names[] = { "a", "b", "c", "mid" }; + + for (uint8_t i = 0U; i < 4U; i++) + { + const task_t *p_top = (const task_t *) prio_queue_peek(&s_q); + TEST_ASSERT_NOT_NULL(p_top); + TEST_ASSERT_EQUAL_STRING(expected_names[i], p_top->name); + prio_queue_remove_at(&s_q, 0U); + } +} + +/* ══════════════════════════════════════════════════════════════════════════ + * main + * ══════════════════════════════════════════════════════════════════════════ */ + +int main(void) +{ + UNITY_BEGIN(); + + /* init */ + RUN_TEST(test_init_size_is_zero); + RUN_TEST(test_init_peek_returns_null); + RUN_TEST(test_init_at_returns_null_when_empty); + + /* insert_order */ + RUN_TEST(test_insert_single_element); + RUN_TEST(test_insert_two_ascending_order_peek_is_higher); + RUN_TEST(test_insert_descending_input_peek_is_still_highest); + RUN_TEST(test_insert_ascending_input_order_preserved); + + /* fifo */ + RUN_TEST(test_fifo_two_equal_priority_first_in_first_out); + RUN_TEST(test_fifo_three_equal_priority_order_preserved); + RUN_TEST(test_fifo_mixed_priorities_equal_group_fifo); + + /* full_eviction */ + RUN_TEST(test_full_returns_pq_ok_until_capacity); + RUN_TEST(test_full_higher_priority_returns_pq_evicted); + RUN_TEST(test_full_lower_priority_returns_pq_full); + RUN_TEST(test_full_equal_priority_to_all_returns_pq_full); + + /* evict_correct */ + RUN_TEST(test_evict_removes_least_prioritized_element); + RUN_TEST(test_evict_top_still_correct_after_eviction); + + /* peek */ + RUN_TEST(test_peek_null_when_empty); + RUN_TEST(test_peek_does_not_remove_element); + RUN_TEST(test_peek_pointer_into_internal_storage); + + /* remove_at */ + RUN_TEST(test_remove_at_first_element); + RUN_TEST(test_remove_at_middle_element); + RUN_TEST(test_remove_at_last_element); + RUN_TEST(test_remove_at_out_of_bounds_is_nop); + RUN_TEST(test_remove_at_on_empty_queue_is_nop); + + /* at */ + RUN_TEST(test_at_returns_null_for_out_of_bounds); + RUN_TEST(test_at_returns_correct_element); + + /* size */ + RUN_TEST(test_size_increments_on_insert); + RUN_TEST(test_size_decrements_on_remove); + RUN_TEST(test_size_unchanged_on_pq_full); + + /* stress */ + RUN_TEST(test_stress_insert_all_remove_all_in_order); + RUN_TEST(test_stress_fifo_across_removes); + + return UNITY_END(); +} \ No newline at end of file diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt index 16dd8a2..7122100 100644 --- a/utils/CMakeLists.txt +++ b/utils/CMakeLists.txt @@ -7,7 +7,8 @@ # Не зависит от конкретного железа (нет fsl_*, CMSIS, FreeRTOS). 2. Используется # более чем в одном месте проекта. -add_library(utils STATIC ring_buffer/ring_buffer.c log/log.c) +add_library(utils STATIC ring_buffer/ring_buffer.c log/log.c + prio_queue/prio_queue.c) # PUBLIC: любой таргет, слинкованный с utils, автоматически получает корень # utils/ в include path → #include "ring_buffer/ring_buffer.h" #include "log.h" diff --git a/utils/prio_queue/README.md b/utils/prio_queue/README.md new file mode 100644 index 0000000..6dea0a0 --- /dev/null +++ b/utils/prio_queue/README.md @@ -0,0 +1,92 @@ +# util/prio_queue + +Отсортированный массив с фиксированной ёмкостью — generic приоритетная очередь +с вытеснением. + +**Типичное использование:** очередь задач, событий или медиадорожек с приоритетами, +где число элементов заранее известно и невелико (≤ ~32). При полном буфере новый +элемент с более высоким приоритетом вытесняет наименее приоритетный. + +| Параметр | Значение | +|---|---| +| Элемент | любой тип, задаётся через `item_size` | +| Ёмкость | любая, задаётся при `prio_queue_init`; фиксирована на всё время жизни | +| Порядок | определяется `cmp`-функцией пользователя (аналог `qsort`) | +| Вставка | O(n) сдвиг; оптимально при n ≤ 32 | +| Peek-top | O(1) | +| Thread-safety | нет; при использовании из нескольких контекстов — внешняя синхронизация | +| Зависимости | ``, ``, `` | + +## Быстрый старт + +```c +#include "prio_queue/prio_queue.h" + +typedef struct { int priority; const char *name; } task_t; + +/* Comparator: меньший priority → ближе к голове */ +static int task_cmp(const void *a, const void *b) { + int pa = ((const task_t *)a)->priority; + int pb = ((const task_t *)b)->priority; + return (pa < pb) ? -1 : (pa > pb) ? 1 : 0; +} + +static task_t storage[8]; +static prio_queue_t q; + +/* Инициализация */ +prio_queue_init(&q, storage, 8, sizeof(task_t), task_cmp); + +/* Вставка */ +task_t t = { .priority = 2, .name = "send_data" }; +pq_status_t st = prio_queue_insert(&q, &t); +/* st == PQ_OK — вставлен + st == PQ_EVICTED — вставлен, наименее приоритетный вытеснен + st == PQ_FULL — отклонён, новый элемент наименее приоритетен из всех */ + +/* Чтение верхнего элемента без удаления */ +const task_t *top = prio_queue_peek(&q); +if (top != NULL) { /* обработать top */ } + +/* Удаление верхнего элемента после обработки */ +prio_queue_remove_at(&q, 0); +``` + +## Политика вытеснения при полном буфере + +``` +Очередь полна [A(1) B(2) C(3)], вставляем D(2): + → D приоритетнее C(3) → C вытесняется → [A(1) B(2) D(2)] PQ_EVICTED + +Очередь полна [A(1) B(2) C(3)], вставляем E(5): + → E менее приоритетен чем все → отклоняется PQ_FULL +``` + +## Comparator + +Сигнатура идентична `qsort`: + +```c +typedef int (*pq_cmp_fn)(const void *a, const void *b); +``` + +| Возврат | Смысл | +|---|---| +| `< 0` | `a` стоит перед `b` (a приоритетнее) | +| `> 0` | `b` стоит перед `a` | +| `0` | равнозначны; порядок вставки сохраняется | + +## API + +```c +void prio_queue_init(prio_queue_t *q, void *buf, uint8_t capacity, + size_t item_size, pq_cmp_fn cmp); + +pq_status_t prio_queue_insert(prio_queue_t *q, const void *item); +const void *prio_queue_peek(const prio_queue_t *q); +uint8_t prio_queue_size(const prio_queue_t *q); +void *prio_queue_at(prio_queue_t *q, uint8_t idx); +void prio_queue_remove_at(prio_queue_t *q, uint8_t idx); +``` + +Тесты: `tests/host/test_prio_queue.c` diff --git a/utils/prio_queue/prio_queue.c b/utils/prio_queue/prio_queue.c new file mode 100644 index 0000000..562e5f9 --- /dev/null +++ b/utils/prio_queue/prio_queue.c @@ -0,0 +1,111 @@ +/* + * prio_queue.c + */ + +#include "prio_queue.h" + +#include + +/* ---------- вспомогательные ---------- */ + +static void *pq_at(prio_queue_t *p_q, uint8_t i) +{ + return (uint8_t *) p_q->buf + (size_t) i * p_q->item_size; +} + +static const void *pq_at_const(const prio_queue_t *p_q, uint8_t i) +{ + return (const uint8_t *) p_q->buf + (size_t) i * p_q->item_size; +} + +/* ---------- публичный API ---------- */ + +void prio_queue_init(prio_queue_t *p_q, void *p_buf, uint8_t capacity, size_t item_size, + pq_cmp_fn p_cmp) +{ + p_q->buf = p_buf; + p_q->capacity = capacity; + p_q->count = 0; + p_q->item_size = item_size; + p_q->cmp = p_cmp; + memset(p_buf, 0, (size_t) capacity * item_size); +} + +pq_status_t prio_queue_insert(prio_queue_t *p_q, const void *p_item) +{ + /* Найти позицию вставки: первый элемент, перед которым новый стоит в очереди. + cmp(existing, new) > 0 означает: existing менее приоритетен, вставляем перед ним. */ + uint8_t insert_pos = p_q->count; + for (uint8_t i = 0; i < p_q->count; i++) + { + if (p_q->cmp(pq_at(p_q, i), p_item) > 0) + { + insert_pos = i; + break; + } + } + + if (p_q->count < p_q->capacity) + { + /* Есть свободное место — сдвигаем вправо и вставляем */ + for (uint8_t i = p_q->count; i > insert_pos; i--) + { + memcpy(pq_at(p_q, i), pq_at(p_q, i - 1), p_q->item_size); + } + memcpy(pq_at(p_q, insert_pos), p_item, p_q->item_size); + p_q->count++; + return PQ_OK; + } + + /* Буфер полон */ + if (insert_pos == p_q->capacity) + { + /* Новый элемент наименее приоритетен — отклоняем */ + return PQ_FULL; + } + + /* Вытесняем последний (наименее приоритетный), сдвигаем, вставляем */ + for (uint8_t i = p_q->capacity - 1U; i > insert_pos; i--) + { + memcpy(pq_at(p_q, i), pq_at(p_q, i - 1), p_q->item_size); + } + memcpy(pq_at(p_q, insert_pos), p_item, p_q->item_size); + return PQ_EVICTED; +} + +const void *prio_queue_peek(const prio_queue_t *p_q) +{ + if (p_q->count == 0U) + { + return NULL; + } + return pq_at_const(p_q, 0); +} + +uint8_t prio_queue_size(const prio_queue_t *p_q) +{ + return p_q->count; +} + +void *prio_queue_at(prio_queue_t *p_q, uint8_t idx) +{ + if (idx >= p_q->count) + { + return NULL; + } + return pq_at(p_q, idx); +} + +void prio_queue_remove_at(prio_queue_t *p_q, uint8_t idx) +{ + if (idx >= p_q->count) + { + return; + } + for (uint8_t i = idx; i < p_q->count - 1U; i++) + { + memcpy(pq_at(p_q, i), pq_at(p_q, i + 1), p_q->item_size); + } + memset(pq_at(p_q, p_q->count - 1U), 0, p_q->item_size); + p_q->count--; +} \ No newline at end of file diff --git a/utils/prio_queue/prio_queue.h b/utils/prio_queue/prio_queue.h new file mode 100644 index 0000000..f3a77da --- /dev/null +++ b/utils/prio_queue/prio_queue.h @@ -0,0 +1,70 @@ +/* + * prio_queue.h + * + * Generic sorted array с фиксированной ёмкостью. + * Не знает ничего про аудио, FreeRTOS или FatFS. + * + * Порядок сортировки задаётся comparator-функцией: + * cmp(a, b) < 0 → a стоит перед b (a приоритетнее) + * cmp(a, b) > 0 → b стоит перед a + * cmp(a, b) == 0 → равнозначны (порядок вставки сохраняется) + */ + +#ifndef PRIO_QUEUE_H_ +#define PRIO_QUEUE_H_ + +#include +#include + +typedef int (*pq_cmp_fn)(const void *a, const void *b); + +typedef enum +{ + PQ_OK, /*!< Вставка прошла успешно */ + PQ_EVICTED, /*!< Вставка прошла, наименее приоритетный элемент вытеснён */ + PQ_FULL, /*!< Очередь полна, новый элемент менее приоритетен чем все */ +} pq_status_t; + +typedef struct +{ + void *buf; /*!< Указатель на внешний буфер элементов */ + uint8_t capacity; /*!< Максимальное число элементов */ + uint8_t count; /*!< Текущее число элементов */ + size_t item_size; /*!< Размер одного элемента в байтах */ + pq_cmp_fn cmp; /*!< Функция сравнения */ +} prio_queue_t; + +/*! + * @brief Инициализация. Буфер @buf должен быть выделен вызывающей стороной + * и иметь размер не менее @capacity * @item_size байт. + */ +void prio_queue_init(prio_queue_t *p_q, void *p_buf, uint8_t capacity, size_t item_size, + pq_cmp_fn p_cmp); + +/*! + * @brief Вставить элемент с сохранением сортировки. + * При полном буфере и более высоком приоритете — вытесняет последний. + */ +pq_status_t prio_queue_insert(prio_queue_t *p_q, const void *p_item); + +/*! + * @brief Вернуть указатель на элемент с наивысшим приоритетом (index 0). + * NULL если очередь пуста. + */ +const void *prio_queue_peek(const prio_queue_t *p_q); + +/*! Текущее число элементов. */ +uint8_t prio_queue_size(const prio_queue_t *p_q); + +/*! + * @brief Изменяемый доступ к элементу по индексу. + * NULL если @idx >= count. + */ +void *prio_queue_at(prio_queue_t *p_q, uint8_t idx); + +/*! + * @brief Удалить элемент по индексу со сдвигом остальных влево. + */ +void prio_queue_remove_at(prio_queue_t *p_q, uint8_t idx); + +#endif /* PRIO_QUEUE_H_ */ \ No newline at end of file