/** * Тесты библиотеки. Платформенных зависимостей нет, собирается любым C99: * * clang -std=c99 -Wall -Wextra -Iinclude tests/test_transport.c src/pcan_*.c -o test */ #include #include #include "protocan_transport.h" static int g_fail; #define CHECK(cond) \ do { \ if (!(cond)) { \ printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); \ g_fail++; \ } \ } while (0) /* -- CRC ------------------------------------------------------------------- */ static void test_crc(void) { /* Контрольное значение CRC-16/CCITT-FALSE. */ CHECK(pcan_crc16((const uint8_t *)"123456789", 9) == 0x29B1U); } /* -- Кадр ------------------------------------------------------------------ */ static void test_encode(void) { pcan_frame_t f; memset(&f, 0, sizeof f); f.seq = 1U; f.flags = PCAN_FLAG_IDE; f.id = 0x1234567UL; f.dlc = 2U; f.data[0] = 0xAAU; f.data[1] = 0xBBU; uint8_t raw[PCAN_FRAME_MAX]; size_t n = pcan_frame_encode(&f, raw, sizeof raw); /* Эталон согласован с gui/transport.py. */ static const uint8_t want[] = { 0xAA, 0x55, 0x08, 0x01, 0x01, 0x67, 0x45, 0x23, 0x01, 0xAA, 0xBB, 0xFE, 0x14 }; CHECK(n == sizeof want); CHECK(memcmp(raw, want, sizeof want) == 0); /* Маленький буфер должен отвергаться, а не портить память. */ CHECK(pcan_frame_encode(&f, raw, 4U) == 0U); } static void test_encode_max(void) { pcan_frame_t f; memset(&f, 0, sizeof f); f.seq = 255U; f.flags = PCAN_FLAG_IDE | PCAN_FLAG_DIR; f.id = 0x1FFFFFFFUL; f.dlc = 8U; for (int i = 0; i < 8; i++) { f.data[i] = (uint8_t)(0xF0 + i); } uint8_t raw[PCAN_FRAME_MAX]; size_t n = pcan_frame_encode(&f, raw, sizeof raw); static const uint8_t want[] = { 0xAA, 0x55, 0x0E, 0xFF, 0x05, 0xFF, 0xFF, 0xFF, 0x1F, 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0x6D, 0x0B }; CHECK(n == sizeof want); CHECK(n == PCAN_FRAME_MAX); CHECK(memcmp(raw, want, sizeof want) == 0); } static void test_roundtrip(void) { pcan_frame_t f; memset(&f, 0, sizeof f); f.flags = PCAN_FLAG_IDE | PCAN_FLAG_DIR; f.id = 0x0ABCDEFUL; f.dlc = 5U; memcpy(f.data, "HELLO", 5); uint8_t raw[PCAN_FRAME_MAX]; size_t n = pcan_frame_encode(&f, raw, sizeof raw); pcan_parser_t p; pcan_parser_init(&p); pcan_frame_t out; memset(&out, 0, sizeof out); int got = 0; for (size_t i = 0; i < n; i++) { if (pcan_parser_push(&p, raw[i], &out)) { got++; } } CHECK(got == 1); CHECK(out.id == f.id); CHECK(out.dlc == f.dlc); CHECK(out.flags == f.flags); CHECK(memcmp(out.data, "HELLO", 5) == 0); /* Хвост за DLC должен быть обнулён, а не содержать прошлый кадр. */ CHECK(out.data[5] == 0U && out.data[6] == 0U && out.data[7] == 0U); CHECK(p.stats.frames == 1U); } static void test_resync(void) { pcan_frame_t f; memset(&f, 0, sizeof f); f.flags = PCAN_FLAG_IDE; f.id = 0x100UL; f.dlc = 1U; f.data[0] = 0x42U; uint8_t raw[PCAN_FRAME_MAX]; size_t n = pcan_frame_encode(&f, raw, sizeof raw); /* Мусор, оборванная сигнатура и битый LEN перед настоящим кадром. */ static const uint8_t junk[] = { 0x00, 0xFF, 0xAA, 0xAA, 0x55, 0x03, 0x11, 0xAA }; pcan_parser_t p; pcan_parser_init(&p); pcan_frame_t out; int got = 0; for (size_t i = 0; i < sizeof junk; i++) { if (pcan_parser_push(&p, junk[i], &out)) { got++; } } for (size_t i = 0; i < n; i++) { if (pcan_parser_push(&p, raw[i], &out)) { got++; } } CHECK(got == 1); CHECK(out.id == 0x100UL); CHECK(out.data[0] == 0x42U); CHECK(p.stats.bad_len == 1U); } static void test_bad_crc(void) { pcan_frame_t f; memset(&f, 0, sizeof f); f.flags = PCAN_FLAG_IDE; f.id = 0x55UL; f.dlc = 3U; uint8_t raw[PCAN_FRAME_MAX]; size_t n = pcan_frame_encode(&f, raw, sizeof raw); raw[n - 1U] ^= 0xFFU; pcan_parser_t p; pcan_parser_init(&p); pcan_frame_t out; int got = 0; for (size_t i = 0; i < n; i++) { if (pcan_parser_push(&p, raw[i], &out)) { got++; } } CHECK(got == 0); CHECK(p.stats.crc_errors == 1U); } /* -- Идентификатор --------------------------------------------------------- */ static void test_id(void) { pcan_id_t id = { 0x1234U, PCAN_MSG_GAS, 2U, 1U, PCAN_ROUTE_FROM_DEVICE, PCAN_PRIORITY_STANDARD }; uint32_t raw = pcan_id_pack(&id); CHECK(raw == 0x19231234UL); pcan_id_t back; pcan_id_unpack(raw, &back); CHECK(back.msg_body == id.msg_body); CHECK(back.msg_type == id.msg_type); CHECK(back.device_id == id.device_id); CHECK(back.device_type == id.device_type); CHECK(back.route == id.route); CHECK(back.priority == id.priority); /* Раскладки MsgBody из protocan.h */ CHECK(pcan_body_broadcast(3U, 0U) == 0x0030U); CHECK(pcan_body_typed(4U, 17U) == 0x4011U); CHECK(pcan_body_modbus(0x100U, 3U) == 0x1003U); CHECK(pcan_body_error(0U, 0xFFU) == 0x00FFU); } /* -- Кольцевой буфер ------------------------------------------------------- */ static void test_ring(void) { uint8_t storage[16]; pcan_ring_t rb; CHECK(pcan_ring_init(&rb, storage, 16U)); /* Размер не степень двойки - отказ, а не тихая порча индексов. */ pcan_ring_t bad; CHECK(!pcan_ring_init(&bad, storage, 15U)); CHECK(pcan_ring_free(&rb) == 15U); CHECK(pcan_ring_write(&rb, (const uint8_t *)"0123456789", 10U)); CHECK(pcan_ring_count(&rb) == 10U); /* Не влезает - не должно попасть ни одного байта. */ CHECK(!pcan_ring_write(&rb, (const uint8_t *)"ABCDEF", 6U)); CHECK(pcan_ring_count(&rb) == 10U); CHECK(rb.dropped == 6U); /* Линейный участок для DMA. */ const uint8_t *ptr = NULL; uint16_t lin = pcan_ring_linear(&rb, &ptr); CHECK(lin == 10U); CHECK(ptr == storage); pcan_ring_consume(&rb, lin); CHECK(pcan_ring_empty(&rb)); /* Заворот: линейный участок обязан обрываться на конце буфера. */ CHECK(pcan_ring_write(&rb, (const uint8_t *)"ABCDEFGH", 8U)); lin = pcan_ring_linear(&rb, &ptr); CHECK(lin == 6U); /* от tail=10 до конца буфера (16) */ CHECK(ptr == &storage[10]); pcan_ring_consume(&rb, lin); lin = pcan_ring_linear(&rb, &ptr); CHECK(lin == 2U); CHECK(ptr == storage); } /* -- Общее адресное пространство ------------------------------------------- */ static uint16_t s_holding[8]; static uint16_t s_computed_calls; static pcan_gas_status_t computed_read(const pcan_gas_region_t *r, uint16_t offset, uint16_t *value) { (void)r; s_computed_calls++; *value = (uint16_t)(0xA000U + offset); return PCAN_GAS_OK; } static const pcan_gas_region_t s_regions[] = { { 0x0000U, 8U, s_holding, NULL, NULL, 0U, NULL, "holding" }, { 0xFF00U, 4U, NULL, computed_read, NULL, PCAN_GAS_RDONLY, NULL, "diag" }, }; static const pcan_gas_map_t s_map = { s_regions, 2U }; static void test_gas(void) { CHECK(pcan_gas_map_validate(&s_map)); /* Перекрытие регионов должно отвергаться. */ static const pcan_gas_region_t overlap[] = { { 0x0000U, 8U, s_holding, NULL, NULL, 0U, NULL, NULL }, { 0x0004U, 8U, s_holding, NULL, NULL, 0U, NULL, NULL }, }; const pcan_gas_map_t bad = { overlap, 2U }; CHECK(!pcan_gas_map_validate(&bad)); uint16_t v = 0U; CHECK(pcan_gas_write(&s_map, 0x0002U, 0x1234U) == PCAN_GAS_OK); CHECK(pcan_gas_read(&s_map, 0x0002U, &v) == PCAN_GAS_OK); CHECK(v == 0x1234U); CHECK(s_holding[2] == 0x1234U); /* Дырка в карте. */ CHECK(pcan_gas_read(&s_map, 0x0100U, &v) == PCAN_GAS_NO_REG); /* Регион только для чтения. */ CHECK(pcan_gas_write(&s_map, 0xFF00U, 1U) == PCAN_GAS_READ_ONLY); /* Колбэк вместо памяти. */ CHECK(pcan_gas_read(&s_map, 0xFF02U, &v) == PCAN_GAS_OK); CHECK(v == 0xA002U); CHECK(s_computed_calls == 1U); /* Блок обрывается на первом адресе вне карты. */ uint16_t block[8]; CHECK(pcan_gas_read_block(&s_map, 0x0006U, block, 8U) == 2U); } static void test_gas_frames(void) { for (int i = 0; i < 8; i++) { s_holding[i] = (uint16_t)(0x1000 + i); } /* Запрос на чтение: GAS, DLC = 0, адрес в MsgBody. */ pcan_id_t id = { 0x0002U, PCAN_MSG_GAS, 2U, 1U, PCAN_ROUTE_FROM_PM, PCAN_PRIORITY_STANDARD }; pcan_frame_t req; memset(&req, 0, sizeof req); req.id = pcan_id_pack(&id); req.flags = PCAN_FLAG_IDE; req.dlc = 0U; pcan_frame_t rsp; memset(&rsp, 0, sizeof rsp); CHECK(pcan_gas_handle(&s_map, &req, &rsp)); CHECK(rsp.dlc == 8U); /* 4 регистра */ pcan_id_t rid; pcan_id_unpack(rsp.id, &rid); CHECK(rid.msg_type == PCAN_MSG_GAS); CHECK(rid.msg_body == 0x0002U); CHECK(rid.route == PCAN_ROUTE_FROM_DEVICE); /* Регистры лежат младшим байтом вперёд - как в SETCAN. */ CHECK(rsp.data[0] == 0x02U && rsp.data[1] == 0x10U); uint16_t regs[4]; CHECK(pcan_gas_frame_to_regs(&rsp, regs, 4U) == 4U); CHECK(regs[0] == 0x1002U && regs[3] == 0x1005U); /* Запись: GAS с данными, ответа быть не должно. */ pcan_frame_t wr; memset(&wr, 0, sizeof wr); id.msg_body = 0x0000U; wr.id = pcan_id_pack(&id); wr.flags = PCAN_FLAG_IDE; wr.dlc = 4U; wr.data[0] = 0xEFU; wr.data[1] = 0xBEU; wr.data[2] = 0xADU; wr.data[3] = 0xDEU; CHECK(!pcan_gas_handle(&s_map, &wr, &rsp)); CHECK(s_holding[0] == 0xBEEFU); CHECK(s_holding[1] == 0xDEADU); /* Чтение по адресу вне карты не должно порождать ответ. */ id.msg_body = 0x0500U; req.id = pcan_id_pack(&id); CHECK(!pcan_gas_handle(&s_map, &req, &rsp)); } /* -- Канал связи ----------------------------------------------------------- */ static uint8_t s_line[256]; 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 int s_rx_count; static pcan_frame_t s_rx_last; static void on_frame(const pcan_frame_t *f, void *user) { (void)user; s_rx_count++; s_rx_last = *f; } static void test_link(void) { pcan_io_t io = { fake_write, fake_space, NULL }; pcan_link_t link; CHECK(pcan_link_init(&link, &io, on_frame, NULL)); s_line_len = 0U; s_rx_count = 0; /* SEQ должен инкрементироваться сам. */ for (int i = 0; i < 3; i++) { pcan_frame_t f; memset(&f, 0, sizeof f); f.flags = PCAN_FLAG_IDE; f.id = (uint32_t)(0x200 + i); f.dlc = 1U; f.data[0] = (uint8_t)i; CHECK(pcan_link_send(&link, &f)); CHECK(f.seq == (uint8_t)i); } CHECK(link.tx.tx_frames == 3U); /* Тот же поток скармливаем обратно, по кускам произвольной длины. */ size_t off = 0U; while (off < s_line_len) { size_t chunk = ((s_line_len - off) > 5U) ? 5U : (s_line_len - off); pcan_link_feed(&link, &s_line[off], chunk); off += chunk; } CHECK(s_rx_count == 3); CHECK(s_rx_last.id == 0x202UL); CHECK(s_rx_last.seq == 2U); CHECK(pcan_link_rx_stats(&link)->frames == 3U); /* Переполнение очереди: кадр не должен уйти обрывком. */ s_line_space = 4U; s_line_len = 0U; pcan_frame_t big; memset(&big, 0, sizeof big); big.flags = PCAN_FLAG_IDE; big.dlc = 8U; uint8_t seq_before = link.tx_seq; CHECK(!pcan_link_send(&link, &big)); CHECK(s_line_len == 0U); CHECK(link.tx.tx_dropped == 1U); /* SEQ не двигается на неотправленном кадре. */ CHECK(link.tx_seq == seq_before); CHECK(!pcan_link_can_send(&link)); s_line_space = sizeof s_line; } int main(void) { test_crc(); test_encode(); test_encode_max(); test_roundtrip(); test_resync(); test_bad_crc(); test_id(); test_ring(); test_gas(); test_gas_frames(); test_link(); if (g_fail == 0) { printf("all tests passed\n"); return 0; } printf("%d check(s) failed\n", g_fail); return 1; }