test(firmware): event_log boot recovery — partial fill and post-wrap

Exercises the slot-scan logic in event_log_init(): after a simulated
reboot (RAM state cleared, NVS slots preserved) the module must
resume with the correct head/cnt so newest-first read order is
unchanged and subsequent writes continue the seq monotonically.

Adds native-only event_log_test_simulate_reboot() helper. Lifts the
slot-scan loop out of the #ifdef ARDUINO guard so the native stub
exercises the same recovery path as production; the platform-specific
NVS setup remains guarded.
This commit is contained in:
2026-04-23 13:18:08 -07:00
parent 95f91d3656
commit 9eb1e19651
3 changed files with 61 additions and 3 deletions

View File

@@ -27,6 +27,11 @@
g_head = 0;
g_cnt = 0;
}
extern "C" void event_log_test_simulate_reboot() {
// Simulate device reboot: clear in-RAM state, keep persistent slots.
g_head = 0;
g_cnt = 0;
}
#endif
static const size_t SLOTS = 32;
@@ -69,6 +74,7 @@ void event_log_init() {
Serial.println("[evlog] NVS begin failed");
return;
}
#endif
// Scan all 32 slots; locate the one with the largest seq.
// Empty log: every slot tag == 0 (not a valid EventLogTag, which starts at 1).
uint32_t max_seq = 0;
@@ -91,9 +97,6 @@ void event_log_init() {
g_head = 0;
g_cnt = 0;
}
#else
// nothing
#endif
}
void event_log_write(EventLogTag tag, uint16_t data0, uint16_t data1) {

View File

@@ -33,6 +33,7 @@ struct EventLogEntry {
static_assert(sizeof(EventLogEntry) == 32, "EventLogEntry must be 32 bytes");
// NVS-backed 32-slot ring buffer. Safe to call before NTP sync.
// Call exactly once from application setup, before any task writes events.
void event_log_init();
void event_log_write(EventLogTag tag, uint16_t data0 = 0, uint16_t data1 = 0);
size_t event_log_read_recent(EventLogEntry* out, size_t max_entries);

View File

@@ -74,6 +74,58 @@ void test_path_hash_distinguishes_real_api_paths() {
TEST_ASSERT_NOT_EQUAL(h2, h3);
}
extern "C" void event_log_test_simulate_reboot();
void test_boot_recovery_after_partial_fill() {
// Phase 1: write 5 entries before "reboot"
event_log_init();
for (uint16_t i = 0; i < 5; i++) event_log_write(EVT_HTTP_OK, i, 0);
// Phase 2: simulate reboot (clear RAM state, keep slots), re-init, verify
event_log_test_simulate_reboot();
event_log_init();
// All 5 original entries should still be readable, newest first
EventLogEntry buf[8];
size_t n = event_log_read_recent(buf, 8);
TEST_ASSERT_EQUAL(5, n);
TEST_ASSERT_EQUAL(4, buf[0].data0); // newest
TEST_ASSERT_EQUAL(0, buf[4].data0); // oldest
// Phase 3: write one more — seq must continue (not restart at 0),
// so the new entry is the newest and slot index 5 holds it
event_log_write(EVT_HTTP_OK, 99, 0);
n = event_log_read_recent(buf, 8);
TEST_ASSERT_EQUAL(6, n);
TEST_ASSERT_EQUAL(99, buf[0].data0);
TEST_ASSERT_EQUAL(4, buf[1].data0);
}
void test_boot_recovery_after_wrap() {
// Phase 1: write 40 entries (wraps the 32-slot ring once; oldest 8 dropped)
event_log_init();
for (uint16_t i = 0; i < 40; i++) event_log_write(EVT_HTTP_OK, i, 0);
// Phase 2: simulate reboot, re-init
event_log_test_simulate_reboot();
event_log_init();
// Still 32 entries visible, newest=39, oldest=8
EventLogEntry buf[32];
size_t n = event_log_read_recent(buf, 32);
TEST_ASSERT_EQUAL(32, n);
TEST_ASSERT_EQUAL(39, buf[0].data0);
TEST_ASSERT_EQUAL(8, buf[31].data0);
// Phase 3: one more write — newest becomes 100, head advances past
// wherever the max-seq slot was, oldest drops to data0=9
event_log_write(EVT_HTTP_OK, 100, 0);
n = event_log_read_recent(buf, 32);
TEST_ASSERT_EQUAL(32, n);
TEST_ASSERT_EQUAL(100, buf[0].data0);
TEST_ASSERT_EQUAL(9, buf[31].data0);
}
int main() {
UNITY_BEGIN();
RUN_TEST(test_entry_is_32_bytes);
@@ -83,5 +135,7 @@ int main() {
RUN_TEST(test_empty_log_read_returns_zero);
RUN_TEST(test_read_recent_truncates_to_max_entries);
RUN_TEST(test_path_hash_distinguishes_real_api_paths);
RUN_TEST(test_boot_recovery_after_partial_fill);
RUN_TEST(test_boot_recovery_after_wrap);
return UNITY_END();
}