/** * Тесты GUI-протокола и каталога общего адресного пространства. * Эталонные байты совпадают с SETGUI/tests/test_gas_catalog.py. * * clang -std=c99 -Wall -Wextra -Iinclude tests/test_gui.c src/pcan_*.c src/gui_*.c -o test_gui */ #include #include #include "gui_catalog.h" #include "gui_frame.h" #include "pcan_gas.h" static int g_fail; #define CHECK(cond) \ do { \ if (!(cond)) { \ printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); \ g_fail++; \ } \ } while (0) static void dump(const char *tag, const uint8_t *data, size_t len) { printf("%s:", tag); for (size_t i = 0; i < len; i++) { printf(" %02X", data[i]); } printf("\n"); } /* -- Транспорт ------------------------------------------------------------- */ static void test_crc32(void) { /* Контрольное значение CRC32/ISO-HDLC, оно же zlib.crc32. */ CHECK(gui_crc32((const uint8_t *)"123456789", 9) == 0xCBF43926UL); CHECK(gui_crc32((const uint8_t *)"", 0) == 0U); } static void test_ping_reference(void) { /* Эталон из PROJECT_RELATIONS.md: A5 5A 01 01 12 34 00 00 EE 89 8C 9E */ static const uint8_t want[] = { 0xA5, 0x5A, 0x01, 0x01, 0x12, 0x34, 0x00, 0x00, 0xEE, 0x89, 0x8C, 0x9E }; uint8_t out[GUI_FRAME_MAX]; size_t n = gui_frame_encode(GUI_MSG_PING, 0x1234, NULL, 0, out, sizeof out); if ((n != sizeof want) || (memcmp(out, want, sizeof want) != 0)) { dump("получено", out, n); dump("ожидалось", want, sizeof want); } CHECK(n == sizeof want); CHECK(memcmp(out, want, sizeof want) == 0); } static int s_rx_count; static uint8_t s_rx_type; static uint16_t s_rx_seq; static uint16_t s_rx_size; static uint8_t s_rx_payload[64]; static void on_frame(const gui_frame_t *frame, void *user) { (void)user; s_rx_count++; s_rx_type = frame->type; s_rx_seq = frame->sequence; s_rx_size = frame->size; if (frame->size <= sizeof s_rx_payload) { memcpy(s_rx_payload, frame->payload, frame->size); } } static void test_parser(void) { uint8_t payload[5] = { 1, 2, 3, 4, 5 }; uint8_t out[GUI_FRAME_MAX]; size_t n = gui_frame_encode(GUI_MSG_GAS_WATCH_DATA, 0, payload, 5, out, sizeof out); gui_parser_t p; gui_parser_init(&p); s_rx_count = 0; /* Мусор и оборванная сигнатура перед настоящим кадром. */ static const uint8_t junk[] = { 0x00, 0xFF, 0xA5, 0xA5, 0x5A, 0x02 }; gui_parser_feed(&p, junk, sizeof junk, on_frame, NULL); gui_parser_feed(&p, out, n, on_frame, NULL); CHECK(s_rx_count == 1); CHECK(s_rx_type == GUI_MSG_GAS_WATCH_DATA); CHECK(s_rx_size == 5U); CHECK(memcmp(s_rx_payload, payload, 5) == 0); CHECK(p.stats.version_errors == 1U); /* 0x02 вместо версии 0x01 */ CHECK(p.stats.frames == 1U); /* Битый CRC отвергается. */ out[n - 1U] ^= 0xFFU; gui_parser_init(&p); s_rx_count = 0; gui_parser_feed(&p, out, n, on_frame, NULL); CHECK(s_rx_count == 0); CHECK(p.stats.crc_errors == 1U); } static void test_max_payload(void) { static uint8_t payload[GUI_MAX_PAYLOAD]; for (size_t i = 0; i < sizeof payload; i++) { payload[i] = (uint8_t)i; } static uint8_t out[GUI_FRAME_MAX]; size_t n = gui_frame_encode(GUI_MSG_WRITE_REGISTERS, 0xFFFF, payload, GUI_MAX_PAYLOAD, out, sizeof out); CHECK(n == GUI_FRAME_MAX); CHECK(out[6] == 0x02U && out[7] == 0x00U); /* size = 512 big-endian */ /* Больше максимума не кодируем и буфер не переполняем. */ CHECK(gui_frame_encode(GUI_MSG_PING, 0, payload, GUI_MAX_PAYLOAD + 1U, out, sizeof out) == 0U); CHECK(gui_frame_encode(GUI_MSG_PING, 0, payload, 100U, out, 50U) == 0U); } /* -- Каталог --------------------------------------------------------------- */ static uint16_t s_settings[4]; static uint16_t s_diag[4] = { 0x0100, 0x003C, 0x0000, 0x0002 }; static const pcan_gas_region_t s_regions[] = { { 0x0000U, 4U, s_settings, NULL, NULL, 0U, NULL, "settings" }, { 0xFF00U, 4U, s_diag, NULL, NULL, PCAN_GAS_RDONLY, NULL, "diag" }, }; static const pcan_gas_map_t s_map = { s_regions, 2U }; static const gui_object_t s_objects[] = { { 0xFF01U, GUI_OBJ_U16, GUI_OBJ_READABLE | GUI_OBJ_DEFAULT_WATCH, 0, GUI_UNIT_SECOND, "Uptime" }, { 0x0000U, GUI_OBJ_I16, GUI_OBJ_READABLE | GUI_OBJ_WRITABLE, -2, GUI_UNIT_VOLT, "Uставка" }, { 0xFF02U, GUI_OBJ_U32, GUI_OBJ_READABLE, 0, GUI_UNIT_COUNT, "Кадров" }, }; static const gui_catalog_t s_catalog = { s_objects, 3U }; static void test_catalog_validate(void) { CHECK(gui_catalog_validate(&s_catalog, &s_map)); /* Адрес вне карты GAS. */ static const gui_object_t bad_addr[] = { { 0x8000U, GUI_OBJ_U16, GUI_OBJ_READABLE, 0, 0, "нет" }, }; const gui_catalog_t c1 = { bad_addr, 1U }; CHECK(!gui_catalog_validate(&c1, &s_map)); /* U32 требует двух подряд идущих адресов; 0xFF03 есть, 0xFF04 - нет. */ static const gui_object_t split[] = { { 0xFF03U, GUI_OBJ_U32, GUI_OBJ_READABLE, 0, 0, "хвост" }, }; const gui_catalog_t c2 = { split, 1U }; CHECK(!gui_catalog_validate(&c2, &s_map)); /* Имя длиннее поля: 13 кириллических символов - это 26 байт. */ static const gui_object_t long_name[] = { { 0x0000U, GUI_OBJ_U16, GUI_OBJ_READABLE, 0, 0, "тринадцать сим" }, }; const gui_catalog_t c3 = { long_name, 1U }; CHECK(!gui_catalog_validate(&c3, &s_map)); } static void test_catalog_encode(void) { uint8_t out[GUI_MAX_PAYLOAD]; size_t n = gui_catalog_encode(&s_catalog, 0U, 1U, out, sizeof out); CHECK(n == GUI_CATALOG_HEADER + GUI_ENTRY_SIZE); CHECK(gui_get_u16(&out[0]) == 3U); /* total */ CHECK(gui_get_u16(&out[2]) == 0U); /* start_index */ CHECK(gui_get_u16(&out[4]) == 1U); /* count */ /* Запись Uptime совпадает с эталоном из Python-тестов. */ static const uint8_t want[] = { 0x01, 0xFF, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 'U', 'p', 't', 'i', 'm', 'e', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; if (memcmp(&out[GUI_CATALOG_HEADER], want, sizeof want) != 0) { dump("запись", &out[GUI_CATALOG_HEADER], GUI_ENTRY_SIZE); } CHECK(sizeof want == GUI_ENTRY_SIZE); CHECK(memcmp(&out[GUI_CATALOG_HEADER], want, sizeof want) == 0); /* Отрицательный масштаб кодируется как int8. */ n = gui_catalog_encode(&s_catalog, 1U, 1U, out, sizeof out); CHECK(n == GUI_CATALOG_HEADER + GUI_ENTRY_SIZE); CHECK(out[GUI_CATALOG_HEADER + 4U] == 0xFEU); /* -2 */ /* max_count = 0 отдаёт всё, что влезает. */ n = gui_catalog_encode(&s_catalog, 0U, 0U, out, sizeof out); CHECK(n == GUI_CATALOG_HEADER + (3U * GUI_ENTRY_SIZE)); CHECK(gui_get_u16(&out[4]) == 3U); /* Индекс за концом каталога - пустой ответ, а не мусор. */ CHECK(gui_catalog_encode(&s_catalog, 3U, 0U, out, sizeof out) == 0U); /* Маленький буфер отвергается. */ CHECK(gui_catalog_encode(&s_catalog, 0U, 1U, out, 8U) == 0U); CHECK(GUI_ENTRIES_PER_FRAME == 15U); } /* -- Подписка -------------------------------------------------------------- */ static void test_watch(void) { gui_watch_t watch; gui_watch_init(&watch); /* period = 100, два адреса: один есть в карте, второй нет. */ static const uint8_t request[] = { 0x64, 0x00, 0x02, 0x00, 0x01, 0xFF, 0x00, 0x80 }; uint16_t accepted = gui_watch_apply(&watch, &s_map, request, sizeof request); CHECK(accepted == 1U); CHECK(watch.count == 1U); CHECK(watch.address[0] == 0xFF01U); CHECK(watch.period_ms == 100U); uint8_t ack[4]; CHECK(gui_watch_encode_ack(&watch, ack, sizeof ack) == 4U); CHECK(gui_get_u16(&ack[0]) == 100U); CHECK(gui_get_u16(&ack[2]) == 1U); /* Значение берётся из карты GAS. */ uint8_t data[GUI_MAX_PAYLOAD]; size_t n = gui_watch_encode_data(&watch, &s_map, 0x12345678UL, data, sizeof data); CHECK(n == 8U); CHECK(data[0] == 0x78U && data[3] == 0x12U); /* время little-endian */ CHECK(gui_get_u16(&data[4]) == 1U); CHECK(gui_get_u16(&data[6]) == 0x003CU); /* s_diag[1] = uptime */ /* Битый payload не должен ломать действующую подписку. */ static const uint8_t broken[] = { 0x64, 0x00, 0x05, 0x00, 0x01, 0xFF }; CHECK(gui_watch_apply(&watch, &s_map, broken, sizeof broken) == 0xFFFFU); CHECK(watch.count == 1U); CHECK(watch.period_ms == 100U); /* Слишком длинная подписка отвергается целиком. */ static uint8_t huge[4U + ((GUI_WATCH_MAX + 1U) * 2U)]; gui_put_u16(&huge[0], 10U); gui_put_u16(&huge[2], GUI_WATCH_MAX + 1U); CHECK(gui_watch_apply(&watch, &s_map, huge, sizeof huge) == 0xFFFFU); CHECK(watch.count == 1U); /* period = 0 останавливает поток. */ static const uint8_t stop[] = { 0x00, 0x00, 0x00, 0x00 }; CHECK(gui_watch_apply(&watch, &s_map, stop, sizeof stop) == 0U); CHECK(watch.period_ms == 0U); CHECK(!gui_watch_due(&watch, 1000U)); CHECK(gui_watch_encode_data(&watch, &s_map, 0U, data, sizeof data) == 0U); } static void test_watch_timing(void) { gui_watch_t watch; gui_watch_init(&watch); static const uint8_t request[] = { 0x0A, 0x00, 0x01, 0x00, 0x01, 0xFF }; CHECK(gui_watch_apply(&watch, &s_map, request, sizeof request) == 1U); watch.next_ms = 1000U; CHECK(!gui_watch_due(&watch, 999U)); CHECK(gui_watch_due(&watch, 1000U)); CHECK(gui_watch_due(&watch, 1500U)); /* Идём в такт, пока успеваем. */ gui_watch_advance(&watch, 1000U); CHECK(watch.next_ms == 1010U); /* Отстали сильно - не нагоняем пачкой, а идём от текущего момента. */ watch.next_ms = 1000U; gui_watch_advance(&watch, 5000U); CHECK(watch.next_ms == 5010U); /* Переполнение счётчика миллисекунд не должно вешать поток. */ watch.next_ms = 0xFFFFFFF0UL; CHECK(!gui_watch_due(&watch, 0xFFFFFFE0UL)); CHECK(gui_watch_due(&watch, 0x00000010UL)); } /* -- Канал ----------------------------------------------------------------- */ static uint8_t s_line[1024]; static size_t s_line_len; static size_t s_line_space = sizeof s_line; static size_t fake_write(void *ctx, const uint8_t *data, size_t len) { (void)ctx; if ((s_line_len + len) > s_line_space) { return 0U; } memcpy(&s_line[s_line_len], data, len); s_line_len += len; return len; } static size_t fake_space(void *ctx) { (void)ctx; return s_line_space - s_line_len; } static void test_link(void) { pcan_io_t io = { fake_write, fake_space, NULL }; static gui_link_t link; CHECK(gui_link_init(&link, &io, on_frame, NULL)); s_line_len = 0U; s_rx_count = 0; uint8_t payload[3] = { 0xDE, 0xAD, 0xBE }; CHECK(gui_link_send(&link, GUI_MSG_GAS_CATALOG, 7U, payload, 3U)); CHECK(link.tx_frames == 1U); /* Поток обратно кусками произвольной длины. */ size_t off = 0U; while (off < s_line_len) { size_t chunk = ((s_line_len - off) > 4U) ? 4U : (s_line_len - off); gui_link_feed(&link, &s_line[off], chunk); off += chunk; } CHECK(s_rx_count == 1); CHECK(s_rx_type == GUI_MSG_GAS_CATALOG); CHECK(s_rx_seq == 7U); CHECK(s_rx_size == 3U); /* NACK - два байта кода результата. */ s_line_len = 0U; s_rx_count = 0; CHECK(gui_link_nack(&link, 9U, GUI_RESULT_NOT_FOUND)); gui_link_feed(&link, s_line, s_line_len); CHECK(s_rx_count == 1); CHECK(s_rx_type == GUI_MSG_NACK); CHECK(gui_get_u16(s_rx_payload) == GUI_RESULT_NOT_FOUND); /* Не хватило места - кадр не уходит обрывком. */ s_line_space = 8U; s_line_len = 0U; CHECK(!gui_link_can_send(&link, 100U)); CHECK(!gui_link_send(&link, GUI_MSG_PING, 1U, payload, 3U)); CHECK(s_line_len == 0U); CHECK(link.tx_dropped == 1U); s_line_space = sizeof s_line; } int main(void) { test_crc32(); test_ping_reference(); test_parser(); test_max_payload(); test_catalog_validate(); test_catalog_encode(); test_watch(); test_watch_timing(); test_link(); if (g_fail == 0) { printf("all gui tests passed\n"); return 0; } printf("%d check(s) failed\n", g_fail); return 1; }