mirror of
https://github.com/meshtastic/firmware.git
synced 2026-04-10 02:07:51 -04:00
* 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 eb7e5c7acf.
* Add NodeInfoModule integration for RTC quality changes and trigger immediate checks
* Update test conditions for RTC quality checks
337 lines
12 KiB
C++
337 lines
12 KiB
C++
#include "TestUtil.h"
|
|
#include "TransmitHistory.h"
|
|
#include "gps/RTC.h"
|
|
#include <Throttle.h>
|
|
#include <unity.h>
|
|
|
|
// Reset the singleton between tests
|
|
static void resetTransmitHistory()
|
|
{
|
|
if (transmitHistory) {
|
|
delete transmitHistory;
|
|
transmitHistory = nullptr;
|
|
}
|
|
transmitHistory = TransmitHistory::getInstance();
|
|
}
|
|
|
|
void setUp(void)
|
|
{
|
|
resetTransmitHistory();
|
|
}
|
|
|
|
void tearDown(void) {}
|
|
|
|
static void test_setLastSentToMesh_stores_millis()
|
|
{
|
|
transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP);
|
|
|
|
uint32_t result = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
|
|
TEST_ASSERT_NOT_EQUAL(0, result);
|
|
|
|
// The stored millis value should be very close to current millis()
|
|
uint32_t diff = millis() - result;
|
|
TEST_ASSERT_LESS_OR_EQUAL(100, diff); // Within 100ms
|
|
}
|
|
|
|
static void test_set_overwrites_previous_value()
|
|
{
|
|
transmitHistory->setLastSentToMesh(meshtastic_PortNum_TELEMETRY_APP);
|
|
uint32_t first = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_TELEMETRY_APP);
|
|
|
|
testDelay(50);
|
|
|
|
transmitHistory->setLastSentToMesh(meshtastic_PortNum_TELEMETRY_APP);
|
|
uint32_t second = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_TELEMETRY_APP);
|
|
|
|
// The second value should be newer (larger millis)
|
|
TEST_ASSERT_GREATER_THAN(first, second);
|
|
}
|
|
|
|
// --- Throttle integration ---
|
|
|
|
static void test_throttle_blocks_within_interval()
|
|
{
|
|
transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP);
|
|
uint32_t lastMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
|
|
|
|
// Should be within a 10-minute interval (just set it)
|
|
bool withinInterval = Throttle::isWithinTimespanMs(lastMs, 10 * 60 * 1000);
|
|
TEST_ASSERT_TRUE(withinInterval);
|
|
}
|
|
|
|
static void test_throttle_allows_after_interval()
|
|
{
|
|
// Unknown key returns 0 — throttle should NOT block
|
|
uint32_t lastMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
|
|
TEST_ASSERT_EQUAL_UINT32(0, lastMs);
|
|
|
|
// When lastMs == 0, the module check `lastMs == 0 || !isWithinTimespan` allows sending
|
|
bool shouldSend = (lastMs == 0) || !Throttle::isWithinTimespanMs(lastMs, 10 * 60 * 1000);
|
|
TEST_ASSERT_TRUE(shouldSend);
|
|
}
|
|
|
|
static void test_throttle_blocks_after_set_then_zero_does_not()
|
|
{
|
|
// Set it — now throttle should block
|
|
transmitHistory->setLastSentToMesh(meshtastic_PortNum_TELEMETRY_APP);
|
|
uint32_t lastMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_TELEMETRY_APP);
|
|
bool shouldSend = (lastMs == 0) || !Throttle::isWithinTimespanMs(lastMs, 60 * 60 * 1000);
|
|
TEST_ASSERT_FALSE(shouldSend); // Should be blocked (within 1hr interval)
|
|
|
|
// Different key — should allow
|
|
uint32_t otherMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_POSITION_APP);
|
|
bool otherShouldSend = (otherMs == 0) || !Throttle::isWithinTimespanMs(otherMs, 60 * 60 * 1000);
|
|
TEST_ASSERT_TRUE(otherShouldSend);
|
|
}
|
|
|
|
// --- Multiple keys ---
|
|
|
|
static void test_multiple_keys_stored_independently()
|
|
{
|
|
transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP);
|
|
uint32_t nodeInfoInitial = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
|
|
testDelay(20);
|
|
transmitHistory->setLastSentToMesh(meshtastic_PortNum_POSITION_APP);
|
|
uint32_t positionInitial = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_POSITION_APP);
|
|
testDelay(20);
|
|
transmitHistory->setLastSentToMesh(meshtastic_PortNum_TELEMETRY_APP);
|
|
|
|
uint32_t nodeInfo = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
|
|
uint32_t position = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_POSITION_APP);
|
|
uint32_t telemetry = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_TELEMETRY_APP);
|
|
|
|
// All should be non-zero
|
|
TEST_ASSERT_NOT_EQUAL(0, nodeInfo);
|
|
TEST_ASSERT_NOT_EQUAL(0, position);
|
|
TEST_ASSERT_NOT_EQUAL(0, telemetry);
|
|
|
|
// Updating other keys should not overwrite earlier key timestamps
|
|
TEST_ASSERT_EQUAL_UINT32(nodeInfoInitial, nodeInfo);
|
|
TEST_ASSERT_EQUAL_UINT32(positionInitial, position);
|
|
}
|
|
|
|
// --- Singleton ---
|
|
|
|
static void test_getInstance_returns_same_instance()
|
|
{
|
|
TransmitHistory *a = TransmitHistory::getInstance();
|
|
TransmitHistory *b = TransmitHistory::getInstance();
|
|
TEST_ASSERT_EQUAL_PTR(a, b);
|
|
}
|
|
|
|
static void test_getInstance_creates_global()
|
|
{
|
|
if (transmitHistory) {
|
|
delete transmitHistory;
|
|
transmitHistory = nullptr;
|
|
}
|
|
TEST_ASSERT_NULL(transmitHistory);
|
|
|
|
TransmitHistory::getInstance();
|
|
TEST_ASSERT_NOT_NULL(transmitHistory);
|
|
}
|
|
|
|
// --- Persistence round-trip (loadFromDisk / saveToDisk) ---
|
|
|
|
static void test_save_and_load_round_trip()
|
|
{
|
|
// Set some values
|
|
transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP);
|
|
testDelay(10);
|
|
transmitHistory->setLastSentToMesh(meshtastic_PortNum_POSITION_APP);
|
|
|
|
uint32_t nodeInfoEpoch = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_NODEINFO_APP);
|
|
uint32_t positionEpoch = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_POSITION_APP);
|
|
|
|
// Force save
|
|
transmitHistory->saveToDisk();
|
|
|
|
// Reset and reload
|
|
delete transmitHistory;
|
|
transmitHistory = nullptr;
|
|
transmitHistory = TransmitHistory::getInstance();
|
|
transmitHistory->loadFromDisk();
|
|
|
|
// Epoch values should be restored (if RTC was available when set)
|
|
uint32_t restoredNodeInfo = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_NODEINFO_APP);
|
|
uint32_t restoredPosition = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_POSITION_APP);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(nodeInfoEpoch, restoredNodeInfo);
|
|
TEST_ASSERT_EQUAL_UINT32(positionEpoch, restoredPosition);
|
|
|
|
// 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 (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 ---
|
|
|
|
// 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()
|
|
{
|
|
transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP);
|
|
transmitHistory->saveToDisk();
|
|
|
|
// Simulate reboot
|
|
delete transmitHistory;
|
|
transmitHistory = nullptr;
|
|
transmitHistory = TransmitHistory::getInstance();
|
|
transmitHistory->loadFromDisk();
|
|
|
|
// 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) {
|
|
TEST_IGNORE_MESSAGE("Epoch not persisted; skipping");
|
|
return;
|
|
}
|
|
|
|
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()
|
|
{
|
|
initializeTestEnvironment();
|
|
|
|
UNITY_BEGIN();
|
|
|
|
RUN_TEST(test_setLastSentToMesh_stores_millis);
|
|
RUN_TEST(test_set_overwrites_previous_value);
|
|
|
|
RUN_TEST(test_throttle_blocks_within_interval);
|
|
RUN_TEST(test_throttle_allows_after_interval);
|
|
RUN_TEST(test_throttle_blocks_after_set_then_zero_does_not);
|
|
|
|
RUN_TEST(test_multiple_keys_stored_independently);
|
|
|
|
// Singleton
|
|
RUN_TEST(test_getInstance_returns_same_instance);
|
|
RUN_TEST(test_getInstance_creates_global);
|
|
|
|
// Persistence
|
|
RUN_TEST(test_save_and_load_round_trip);
|
|
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());
|
|
}
|
|
|
|
void loop() {}
|