/** * test_offlog.c — offlog 环形事件日志 gcc 隔离单测 * * 编译: gcc -I../BLE/OnlyUpdateApp_Peripheral/APP/include \ * -o test_offlog test_offlog.c * 运行: ./test_offlog * * Mock: W25Q32 NOR 行为 (写=AND, 擦=0xFF), mstick/dev_time_now 固定值 * 覆盖: 全新初始化 / 环形回绕 / 掉电恢复 / 扇区切换 / 清空审计 */ #include #include #include /* ============ mock W25Q32 (4MB, NOR 语义) ============ */ static uint8_t flash[4 * 1024 * 1024]; void SPI_Flash_Erase_Sector(uint32_t sec) { memset(flash + sec * 4096, 0xFF, 4096); } void SPI_Flash_Read(uint8_t *buf, uint32_t addr, uint16_t size) { memcpy(buf, flash + addr, size); } void SPI_Flash_Write_NoCheck(uint8_t *buf, uint32_t addr, uint16_t size) { uint16_t i; for (i = 0; i < size; i++) flash[addr + i] &= buf[i]; /* NOR: 只能 1→0 */ } uint8_t SPI_Flash_ReadJEDEC_ID(void) { return 0x16; } /* mock W25Q32 */ void SPI_Flash_Write(uint8_t *buf, uint32_t addr, uint16_t size) { /* 模拟 storage.c 行为: 目标区域非全 FF 则擦整扇区再写 */ uint16_t i; int need_erase = 0; for (i = 0; i < size; i++) { if (flash[addr + i] != 0xFF) { need_erase = 1; break; } } if (need_erase) SPI_Flash_Erase_Sector(addr / 4096); SPI_Flash_Write_NoCheck(buf, addr, size); } /* ============ mock 时间 (已同步: >16亿) ============ */ static uint32_t mock_ms = 0; uint32_t mstick(void) { return mock_ms; } uint32_t dev_time_now(void) { return 1784768575UL; } /* ch32v20x 库类型 (单测环境无 SDK) */ typedef unsigned char u8; /* ============ 被测模块 (直接包含实现) ============ */ #include "../BLE/OnlyUpdateApp_Peripheral/APP/offlog.c" /* ============ 断言 ============ */ static int failures = 0; #define CHECK(cond) do { \ if (!(cond)) { printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); failures++; } \ } while (0) static void reset_flash(void) { memset(flash, 0xFF, sizeof(flash)); mock_ms = 0; } /* 测试1: 全新初始化 → BOOT 事件可读回 */ static void test_fresh_init(void) { OfflogEvt e; uint8_t payload[4] = {0x18, 0, 0, 0}; /* 假复位原因 (大端序写入 payload) */ reset_flash(); offlog_init(); CHECK(offlog_boot_seq() == 1); CHECK(offlog_count() == 0); /* init 不写事件 */ offlog_boot(0x18000000); /* bit27+bit24 之类 */ CHECK(offlog_count() == 1); CHECK(offlog_read_idx(0, &e) == 0); CHECK(e.magic == OFFLOG_EVT_MAGIC); CHECK(e.type == OFFLOG_EVT_BOOT); CHECK(e.seq == 1); CHECK(e.boot_seq == 1); CHECK(e.flags & OFFLOG_UNIX_VALID_FLAG); CHECK(e.len == 4); CHECK(memcmp(e.payload, payload, 4) == 0); CHECK(offlog_read_idx(1, &e) == -1); /* 越界 */ printf("PASS test_fresh_init\n"); } /* 测试2: 环形回绕 — 写满 8064+5 条, count 封顶, 序号单调 */ static void test_ring_wrap(void) { OfflogEvt e; uint32_t i, n = OFFLOG_MAX_RECORDS + 5; reset_flash(); offlog_init(); for (i = 0; i < n; i++) { offlog_evt(OFFLOG_EVT_IOT_RECONN, NULL, 0); } CHECK(offlog_count() == OFFLOG_MAX_RECORDS - OFFLOG_EVT_PER_SECTOR + 5); /* 8064-128+5 */ /* 最新一条是最后写入的, seq 应等于 n (从 1 开始) */ CHECK(offlog_read_idx(offlog_count() - 1, &e) == 0); CHECK(e.seq == n); /* 逻辑首: 回绕时整扇区(128条)被擦 + 写回5条 → 最旧 = 129 */ CHECK(offlog_read_idx(0, &e) == 0); CHECK(e.seq == n - OFFLOG_MAX_RECORDS + OFFLOG_EVT_PER_SECTOR - 4); printf("PASS test_ring_wrap (seq %lu..%lu, count %u)\n", (unsigned long)(n - OFFLOG_MAX_RECORDS + OFFLOG_EVT_PER_SECTOR - 4), (unsigned long)n, offlog_count()); } /* 测试3: 掉电恢复 — 写 10 条后重新 init, boot_seq 递增, seq 连续 */ static void test_power_loss_recovery(void) { OfflogEvt e; uint32_t i; reset_flash(); offlog_init(); for (i = 0; i < 10; i++) { offlog_evt(OFFLOG_EVT_COIL, NULL, 0); } CHECK(offlog_count() == 10); /* 模拟掉电重启 (flash 保留, RAM 全失) */ offlog_init(); CHECK(offlog_boot_seq() == 2); CHECK(offlog_count() == 10); /* 上电恢复出之前 10 条 */ offlog_evt(OFFLOG_EVT_IOT_CONNECT, NULL, 0); CHECK(offlog_count() == 11); CHECK(offlog_read_idx(10, &e) == 0); CHECK(e.type == OFFLOG_EVT_IOT_CONNECT); CHECK(e.seq == 11); /* seq 跨 boot 连续 */ CHECK(e.boot_seq == 2); printf("PASS test_power_loss_recovery\n"); } /* 测试4: 扇区切换 — 写满 128 条后头扇区更新, 数据完整 */ static void test_sector_switch(void) { OfflogEvt e; OfflogHead h; uint32_t i; reset_flash(); offlog_init(); for (i = 0; i < 128; i++) { offlog_evt(OFFLOG_EVT_BOOT, NULL, 0); } /* 第 129 条触发扇区切换 */ offlog_evt(OFFLOG_EVT_TIME_ANCHOR, NULL, 0); CHECK(offlog_count() == 129); SPI_Flash_Read((uint8_t *)&h, OFFLOG_AREA_BASE, sizeof(h)); CHECK(h.magic[0] == OFFLOG_HEAD_MAGIC0); CHECK(h.wr_sector == 2); /* 头在扇区切换时刷新: wr_off = 新扇区起点 (尚未写入第 129 条) */ CHECK(h.wr_off == OFFLOG_DATA_BASE + 4096); CHECK(offlog_read_idx(127, &e) == 0); CHECK(e.type == OFFLOG_EVT_BOOT); CHECK(e.seq == 128); CHECK(offlog_read_idx(128, &e) == 0); CHECK(e.type == OFFLOG_EVT_TIME_ANCHOR); CHECK(e.seq == 129); printf("PASS test_sector_switch\n"); } /* 测试5: 清空 — 擦数据区 + LOG_CLEAR 审计 */ static void test_clear(void) { OfflogEvt e; uint32_t i; reset_flash(); offlog_init(); for (i = 0; i < 5; i++) { offlog_evt(OFFLOG_EVT_IOT_READY, NULL, 0); } offlog_clear(); CHECK(offlog_count() == 1); /* 只剩审计 */ CHECK(offlog_read_idx(0, &e) == 0); CHECK(e.type == OFFLOG_EVT_LOG_CLEAR); CHECK(e.seq == 6); /* seq 不重置 */ printf("PASS test_clear\n"); } /* 测试6: 掉电恢复跨扇区 — 写到扇区 2 中间掉电 */ static void test_power_loss_mid_sector(void) { OfflogEvt e; uint32_t i; reset_flash(); offlog_init(); for (i = 0; i < 130; i++) { /* 已跨入扇区 2 */ offlog_evt(OFFLOG_EVT_COIL, NULL, 0); } CHECK(offlog_count() == 130); offlog_init(); /* 掉电重启 */ CHECK(offlog_boot_seq() == 2); CHECK(offlog_count() == 130); offlog_evt(OFFLOG_EVT_IOT_CONNECT, NULL, 0); CHECK(offlog_count() == 131); CHECK(offlog_read_idx(130, &e) == 0); CHECK(e.seq == 131); printf("PASS test_power_loss_mid_sector\n"); } /* 测试7: TIME_ANCHOR 严格使用平台下发 unix_ts (不走 dev_time_now) */ static void test_time_anchor_exact(void) { OfflogEvt e; reset_flash(); offlog_init(); offlog_time_anchor(1800000000UL); /* 平台下发值, 与 mock dev_time_now() 不同 */ CHECK(offlog_count() == 1); CHECK(offlog_read_idx(0, &e) == 0); CHECK(e.type == OFFLOG_EVT_TIME_ANCHOR); CHECK(e.unix_ts == 1800000000UL); /* 严格等于传参 */ CHECK(e.flags & OFFLOG_UNIX_VALID_FLAG); printf("PASS test_time_anchor_exact\n"); } /* 测试8: 协议导出 — enabled/seq_last/type_str/evt_to_json (log_stat/log_query 数据源) */ static void test_export_json(void) { OfflogEvt e; char buf[256]; reset_flash(); offlog_init(); CHECK(offlog_enabled() == 1); CHECK(offlog_seq_last() == 0); /* 尚无记录 */ offlog_boot(0x80000000UL); /* IWDG 复位 → rst=2147483648 */ CHECK(offlog_count() == 1); CHECK(offlog_seq_last() == 1); CHECK(offlog_read_idx(0, &e) == 0); CHECK(strcmp(offlog_type_str(e.type), "boot") == 0); offlog_evt_to_json(&e, buf, sizeof(buf)); CHECK(strstr(buf, "\"type\":\"boot\"") != NULL); CHECK(strstr(buf, "\"data\":{\"rst\":2147483648}") != NULL); /* coil: payload = sub(1=进) ch(2) value(97, 大端) */ { uint8_t p[6] = {1, 2, 0, 0, 0, 97}; offlog_evt(OFFLOG_EVT_COIL, p, 6); } CHECK(offlog_read_idx(1, &e) == 0); CHECK(strcmp(offlog_type_str(e.type), "coil") == 0); offlog_evt_to_json(&e, buf, sizeof(buf)); CHECK(strstr(buf, "\"type\":\"coil\"") != NULL); CHECK(strstr(buf, "\"sub\":\"car_enter\"") != NULL); CHECK(strstr(buf, "\"ch\":2") != NULL); CHECK(strstr(buf, "\"value\":97") != NULL); /* evt_retry: msg_id=5 retry=2 */ { uint8_t p[5] = {0, 0, 0, 5, 2}; offlog_evt(OFFLOG_EVT_EVT_RETRY, p, 5); } CHECK(offlog_read_idx(2, &e) == 0); offlog_evt_to_json(&e, buf, sizeof(buf)); CHECK(strstr(buf, "\"msg_id\":5") != NULL); CHECK(strstr(buf, "\"retry\":2") != NULL); /* 无 payload 事件 → data:null */ offlog_iot_ready(); CHECK(offlog_read_idx(3, &e) == 0); offlog_evt_to_json(&e, buf, sizeof(buf)); CHECK(strstr(buf, "\"type\":\"iot_ready\"") != NULL); CHECK(strstr(buf, "\"data\":null") != NULL); /* seq_first 公式: seq_last - count + 1 (4 条: seq 1..4) */ CHECK(offlog_seq_last() == 4); CHECK(offlog_count() == 4); printf("PASS test_export_json (seq_first=%lu seq_last=%lu)\n", (unsigned long)(offlog_seq_last() - offlog_count() + 1), (unsigned long)offlog_seq_last()); } int main(void) { test_fresh_init(); test_ring_wrap(); test_power_loss_recovery(); test_sector_switch(); test_clear(); test_power_loss_mid_sector(); test_time_anchor_exact(); test_export_json(); if (failures) { printf("\n%d FAILURE(S)\n", failures); return 1; } printf("\nALL PASS\n"); return 0; }