Fix TransmitHistory to improve epoch handling (#10017)
* Fix TransmitHistory to improve epoch handling * Enable epoch handling in unit tests * Improve comments and test handling for epoch persistence in TransmitHistory * Add boot-relative timestamp handling and unit tests for TransmitHistory * loadFromDisk should handle legacy entries and clean up old v1 files after migration * Revert "loadFromDisk should handle legacy entries and clean up old v1 files after migration" This reverts commit eb7e5c7acfa4ac077fe50980be752e4b42a739b8. * Add NodeInfoModule integration for RTC quality changes and trigger immediate checks * Update test conditions for RTC quality checks
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
#include "TestUtil.h"
|
||||
#include "TransmitHistory.h"
|
||||
#include "gps/RTC.h"
|
||||
#include <Throttle.h>
|
||||
#include <unity.h>
|
||||
|
||||
@@ -161,44 +162,141 @@ static void test_save_and_load_round_trip()
|
||||
// After loadFromDisk, millis should be seeded (non-zero) for stored entries
|
||||
uint32_t restoredMillis = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
|
||||
if (restoredNodeInfo > 0) {
|
||||
// If epoch was stored, millis should be seeded from load
|
||||
// If epoch was stored (set seconds ago), epoch-conversion gives elapsed ≈ 0 s,
|
||||
// so getLastSentToMeshMillis() should return a non-zero value.
|
||||
TEST_ASSERT_NOT_EQUAL(0, restoredMillis);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Boot without RTC scenario ---
|
||||
|
||||
static void test_load_seeds_millis_even_without_rtc()
|
||||
// Crash-reboot protection: a send that happened moments before the reboot must still
|
||||
// throttle after reload. This works because getLastSentToMeshMillis() reconstructs
|
||||
// a millis()-relative timestamp from the stored epoch, and Throttle uses unsigned
|
||||
// subtraction so the age survives wraparound even when uptime is near zero.
|
||||
static void test_boot_after_recent_send_still_throttles()
|
||||
{
|
||||
// This tests the critical crash-reboot scenario:
|
||||
// After loadFromDisk(), even if getTime() returns 0 (no RTC),
|
||||
// lastMillis should be seeded so throttle blocks immediate re-broadcast.
|
||||
|
||||
transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP);
|
||||
transmitHistory->saveToDisk();
|
||||
|
||||
// Simulate reboot: destroy and recreate
|
||||
// Simulate reboot
|
||||
delete transmitHistory;
|
||||
transmitHistory = nullptr;
|
||||
transmitHistory = TransmitHistory::getInstance();
|
||||
transmitHistory->loadFromDisk();
|
||||
|
||||
// The key insight: after load, getLastSentToMeshMillis should return non-zero
|
||||
// because loadFromDisk seeds lastMillis[key] = millis() for every loaded entry.
|
||||
// This ensures throttle works even without RTC.
|
||||
// Epoch was set seconds ago; reconstructed age is still within the 10-min window.
|
||||
uint32_t result = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
|
||||
|
||||
uint32_t epoch = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_NODEINFO_APP);
|
||||
if (epoch > 0) {
|
||||
// Data was persisted — millis must be seeded
|
||||
TEST_ASSERT_NOT_EQUAL(0, result);
|
||||
|
||||
// And it should cause throttle to block (treating as "just sent")
|
||||
bool withinInterval = Throttle::isWithinTimespanMs(result, 10 * 60 * 1000);
|
||||
TEST_ASSERT_TRUE(withinInterval);
|
||||
if (epoch == 0) {
|
||||
TEST_IGNORE_MESSAGE("Epoch not persisted; skipping");
|
||||
return;
|
||||
}
|
||||
// If epoch == 0, RTC wasn't available — no data was saved, so nothing to restore.
|
||||
// This is expected on platforms without RTC during the very first boot.
|
||||
|
||||
TEST_ASSERT_NOT_EQUAL(0, result);
|
||||
bool withinInterval = Throttle::isWithinTimespanMs(result, 10 * 60 * 1000);
|
||||
TEST_ASSERT_TRUE(withinInterval);
|
||||
}
|
||||
|
||||
// Regression test for issue #9901:
|
||||
// A device powered off for longer than the throttle window must broadcast NodeInfo
|
||||
// on its next boot — it must not be silenced because loadFromDisk() once treated
|
||||
// every loaded entry as "just sent" by seeding lastMillis to millis() at boot.
|
||||
static void test_boot_after_long_gap_allows_nodeinfo()
|
||||
{
|
||||
if (getRTCQuality() <= RTCQualityNone) {
|
||||
TEST_IGNORE_MESSAGE("No RTC available; skipping epoch-dependent test");
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t now = getTime();
|
||||
|
||||
// Simulate: last NodeInfo sent 30 minutes ago (outside the 10-min throttle window)
|
||||
transmitHistory->setLastSentAtEpoch(meshtastic_PortNum_NODEINFO_APP, now - (30 * 60));
|
||||
transmitHistory->saveToDisk();
|
||||
|
||||
// Simulate reboot
|
||||
delete transmitHistory;
|
||||
transmitHistory = nullptr;
|
||||
transmitHistory = TransmitHistory::getInstance();
|
||||
transmitHistory->loadFromDisk();
|
||||
|
||||
uint32_t restoredEpoch = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_NODEINFO_APP);
|
||||
if (restoredEpoch == 0) {
|
||||
TEST_IGNORE_MESSAGE("Epoch not persisted; skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t restoredMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
|
||||
bool throttled = (restoredMs != 0) && Throttle::isWithinTimespanMs(restoredMs, 10 * 60 * 1000);
|
||||
TEST_ASSERT_FALSE_MESSAGE(throttled, "NodeInfo must not be throttled after a 30-min gap (#9901)");
|
||||
}
|
||||
|
||||
// Complementary: a rapid reboot must still throttle (crash-loop protection), even
|
||||
// though the reconstructed lastMs may wrap because current uptime is small.
|
||||
static void test_boot_within_throttle_window_still_throttles()
|
||||
{
|
||||
if (getRTCQuality() <= RTCQualityNone) {
|
||||
TEST_IGNORE_MESSAGE("No RTC available; skipping epoch-dependent test");
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t now = getTime();
|
||||
|
||||
// Simulate: last NodeInfo sent 5 minutes ago (inside the 10-min throttle window)
|
||||
transmitHistory->setLastSentAtEpoch(meshtastic_PortNum_NODEINFO_APP, now - (5 * 60));
|
||||
transmitHistory->saveToDisk();
|
||||
|
||||
// Simulate reboot
|
||||
delete transmitHistory;
|
||||
transmitHistory = nullptr;
|
||||
transmitHistory = TransmitHistory::getInstance();
|
||||
transmitHistory->loadFromDisk();
|
||||
|
||||
uint32_t restoredEpoch = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_NODEINFO_APP);
|
||||
if (restoredEpoch == 0) {
|
||||
TEST_IGNORE_MESSAGE("Epoch not persisted; skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t restoredMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
|
||||
bool throttled = (restoredMs != 0) && Throttle::isWithinTimespanMs(restoredMs, 10 * 60 * 1000);
|
||||
TEST_ASSERT_TRUE_MESSAGE(throttled, "NodeInfo must still be throttled when last send was within the 10-min window");
|
||||
}
|
||||
|
||||
static void test_boot_without_time_source_still_throttles_recent_restart()
|
||||
{
|
||||
setBootRelativeTimeForUnitTest(32);
|
||||
transmitHistory->setLastSentAtBootRelative(meshtastic_PortNum_NODEINFO_APP, 32);
|
||||
transmitHistory->saveToDisk();
|
||||
|
||||
delete transmitHistory;
|
||||
transmitHistory = nullptr;
|
||||
transmitHistory = TransmitHistory::getInstance();
|
||||
|
||||
setBootRelativeTimeForUnitTest(31);
|
||||
transmitHistory->loadFromDisk();
|
||||
|
||||
uint32_t restoredMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
|
||||
bool throttled = (restoredMs != 0) && Throttle::isWithinTimespanMs(restoredMs, 10 * 60 * 1000);
|
||||
TEST_ASSERT_TRUE_MESSAGE(throttled, "Recent no-RTC reboots should still suppress duplicate NodeInfo");
|
||||
}
|
||||
|
||||
static void test_boot_without_time_source_expires_boot_relative_history()
|
||||
{
|
||||
setBootRelativeTimeForUnitTest(32);
|
||||
transmitHistory->setLastSentAtBootRelative(meshtastic_PortNum_NODEINFO_APP, 32);
|
||||
transmitHistory->saveToDisk();
|
||||
|
||||
delete transmitHistory;
|
||||
transmitHistory = nullptr;
|
||||
transmitHistory = TransmitHistory::getInstance();
|
||||
|
||||
setBootRelativeTimeForUnitTest(400);
|
||||
transmitHistory->loadFromDisk();
|
||||
|
||||
uint32_t restoredMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
|
||||
TEST_ASSERT_EQUAL_UINT32_MESSAGE(0, restoredMs, "Boot-relative history should only suppress near-term restarts");
|
||||
}
|
||||
|
||||
void setup()
|
||||
@@ -222,7 +320,15 @@ void setup()
|
||||
|
||||
// Persistence
|
||||
RUN_TEST(test_save_and_load_round_trip);
|
||||
RUN_TEST(test_load_seeds_millis_even_without_rtc);
|
||||
RUN_TEST(test_boot_after_recent_send_still_throttles);
|
||||
|
||||
// Issue #9901 regression tests
|
||||
RUN_TEST(test_boot_after_long_gap_allows_nodeinfo);
|
||||
RUN_TEST(test_boot_within_throttle_window_still_throttles);
|
||||
|
||||
// No-RTC regression tests
|
||||
RUN_TEST(test_boot_without_time_source_still_throttles_recent_restart);
|
||||
RUN_TEST(test_boot_without_time_source_expires_boot_relative_history);
|
||||
|
||||
exit(UNITY_END());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user