Skip to content

Commit 3b91c6b

Browse files
NomDeTomclaude
andcommitted
TMM: address review, fix dedup tests, inject fake time, fix fingerprint-0
Copilot review (PR #10706): - preloadNextHopsFromNodeDB() now returns bool; runOnce only latches nextHopPreloaded once the preload actually ran (retries if nodeDB wasn't ready), instead of skipping it forever. - Remove the empty `#if HAS_VARIABLE_HOPS` blocks in the test. Test correctness: - Three more position-dedup tests were missing installWellKnownPrimaryChannel() (dropsDuplicate/allowsMoved were fixed earlier; allowsDuplicateAfterInterval, cacheFlush, priorRateState were not) — without the well-known-channel gate the dedup path never runs, so their STOP assertions failed. Fake-time injection (no more real sleeps): - Add TrafficManagementModule::s_testNowMs + nowMs(), mirroring HopScalingModule; route all TMM tick/time reads through nowMs(). Tests advance a virtual clock via s_testNowMs instead of testDelay() sleeping real 5-6 min across a tick — the suite drops from ~15 min to ~30 s. Production behaviour is unchanged (nowMs() inlines to millis()). Fingerprint-0 fix: - computePositionFingerprint() never returns 0 now (remap 0 -> 0xFF, mirroring getLastByteOfNodeNum), so a real position that hashes to 0 doesn't collide with the "no position seen" sentinel and its duplicates dedup correctly. test_traffic_management: 34/34 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 3067e82 commit 3b91c6b

3 files changed

Lines changed: 50 additions & 32 deletions

File tree

src/modules/TrafficManagementModule.cpp

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,7 @@ TrafficManagementModule::NodeInfoPayloadEntry *TrafficManagementModule::findOrCr
390390
NodeInfoPayloadEntry *empty = nullptr;
391391
NodeInfoPayloadEntry *lru = nullptr;
392392
uint32_t lruAge = 0;
393-
const uint32_t now = millis();
393+
const uint32_t now = clockMs();
394394

395395
for (uint16_t i = 0; i < nodeInfoTargetEntries(); i++) {
396396
NodeInfoPayloadEntry &e = nodeInfoPayload[i];
@@ -466,7 +466,7 @@ void TrafficManagementModule::cacheNodeInfoPacket(const meshtastic_MeshPacket &m
466466
// richer context than "just the user protobuf" when PSRAM is present.
467467
// This path is intentionally independent from NodeInfoModule/NodeDB.
468468
entry->user = user;
469-
entry->lastObservedMs = millis();
469+
entry->lastObservedMs = clockMs();
470470
entry->lastObservedRxTime = mp.rx_time;
471471
entry->sourceChannel = mp.channel;
472472
entry->hasDecodedBitfield = mp.decoded.has_bitfield;
@@ -528,11 +528,11 @@ uint8_t TrafficManagementModule::getNextHopHint(NodeNum dest)
528528
#endif
529529
}
530530

531-
void TrafficManagementModule::preloadNextHopsFromNodeDB()
531+
bool TrafficManagementModule::preloadNextHopsFromNodeDB()
532532
{
533533
#if TRAFFIC_MANAGEMENT_CACHE_SIZE > 0
534534
if (!cache || !nodeDB)
535-
return;
535+
return false; // prerequisites not ready yet — caller should retry on a later pass
536536

537537
uint16_t seeded = 0;
538538
concurrency::LockGuard guard(&cacheLock);
@@ -552,6 +552,9 @@ void TrafficManagementModule::preloadNextHopsFromNodeDB()
552552
}
553553

554554
TM_LOG_INFO("Preloaded %u next-hop hints from NodeDB", static_cast<unsigned>(seeded));
555+
return true;
556+
#else
557+
return true; // nothing to preload on a cache-less build; don't keep retrying
555558
#endif
556559
}
557560

@@ -607,7 +610,12 @@ uint8_t TrafficManagementModule::computePositionFingerprint(int32_t lat_truncate
607610
uint8_t latBits = (static_cast<uint32_t>(lat_truncated) >> shift) & ((1u << bitsToTake) - 1);
608611
uint8_t lonBits = (static_cast<uint32_t>(lon_truncated) >> shift) & ((1u << bitsToTake) - 1);
609612

610-
return static_cast<uint8_t>((latBits << 4) | lonBits);
613+
const uint8_t fp = static_cast<uint8_t>((latBits << 4) | lonBits);
614+
// 0 is the "no position seen" sentinel for pos_fingerprint, so a real position that happens to
615+
// hash to 0 must not collide with it (otherwise its duplicates would never dedup). Remap 0 -> 0xFF,
616+
// mirroring NodeDB::getLastByteOfNodeNum()'s 0 -> 0xFF idiom. Cost: the 0x00 bucket merges into
617+
// 0xFF (one extra collision in 256 — negligible; the fingerprint already collides every 16 cells).
618+
return fp ? fp : 0xFF;
611619
}
612620

613621
// =============================================================================
@@ -632,7 +640,7 @@ ProcessMessage TrafficManagementModule::handleReceived(const meshtastic_MeshPack
632640
incrementStat(&stats.packets_inspected);
633641

634642
const auto &cfg = moduleConfig.traffic_management;
635-
const uint32_t nowMs = millis();
643+
const uint32_t nowMs = TrafficManagementModule::clockMs();
636644

637645
// -------------------------------------------------------------------------
638646
// Undecoded Packet Handling
@@ -811,15 +819,15 @@ int32_t TrafficManagementModule::runOnce()
811819
return INT32_MAX;
812820

813821
#if TRAFFIC_MANAGEMENT_CACHE_SIZE > 0
814-
const uint32_t nowMs = millis();
822+
const uint32_t nowMs = TrafficManagementModule::clockMs();
815823

816824
// Warm-start the next-hop cache from persisted NodeInfoLite hints once nodeDB
817825
// is populated. Done here (not in the constructor) so nodeDB has finished
818826
// loading. Takes its own lock, so call before acquiring the sweep guard below.
819-
if (!nextHopPreloaded) {
820-
preloadNextHopsFromNodeDB();
827+
// Only latch the one-shot guard once the preload actually ran; if nodeDB wasn't
828+
// ready yet, retry on the next maintenance pass instead of skipping it forever.
829+
if (!nextHopPreloaded && preloadNextHopsFromNodeDB())
821830
nextHopPreloaded = true;
822-
}
823831

824832
// Free-running tick counters (no epoch needed).
825833
// TTL expressed in ticks: pos 4× interval (default 44 ticks ≈ 4.4h),
@@ -843,7 +851,7 @@ int32_t TrafficManagementModule::runOnce()
843851
// Sweep cache and clear expired entries
844852
uint16_t activeEntries = 0;
845853
uint16_t expiredEntries = 0;
846-
const uint32_t sweepStartMs = millis();
854+
const uint32_t sweepStartMs = TrafficManagementModule::clockMs();
847855

848856
const auto &cfg = moduleConfig.traffic_management;
849857
concurrency::LockGuard guard(&cacheLock);
@@ -900,7 +908,7 @@ int32_t TrafficManagementModule::runOnce()
900908

901909
TM_LOG_DEBUG("Maintenance: %u active, %u expired, %u/%u slots, %lums elapsed", activeEntries, expiredEntries,
902910
static_cast<unsigned>(activeEntries), static_cast<unsigned>(cacheSize()),
903-
static_cast<unsigned long>(millis() - sweepStartMs));
911+
static_cast<unsigned long>(TrafficManagementModule::clockMs() - sweepStartMs));
904912

905913
#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM)
906914
if (nodeInfoPayload) {
@@ -1056,7 +1064,7 @@ bool TrafficManagementModule::shouldRespondToNodeInfo(const meshtastic_MeshPacke
10561064
reply->decoded.bitfield |= BITFIELD_OK_TO_MQTT_MASK;
10571065

10581066
if (hasCachedUser && cachedLastObservedMs != 0) {
1059-
uint32_t ageMs = millis() - cachedLastObservedMs;
1067+
uint32_t ageMs = clockMs() - cachedLastObservedMs;
10601068
TM_LOG_DEBUG("NodeInfo PSRAM hit node=0x%08x age=%lu ms src_ch=%u req_ch=%u rx_time=%lu", p->to,
10611069
static_cast<unsigned long>(ageMs), static_cast<unsigned>(cachedSourceChannel),
10621070
static_cast<unsigned>(p->channel), static_cast<unsigned long>(cachedLastObservedRxTime));

src/modules/TrafficManagementModule.h

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@ class TrafficManagementModule : public MeshModule, private concurrency::OSThread
5252
// Warm-start the next-hop cache from persisted NodeInfoLite hints so confirmed
5353
// hops survive later hot-store (NodeDB) eviction. Idempotent; runs once after
5454
// nodeDB is populated (lazily on first maintenance pass).
55-
void preloadNextHopsFromNodeDB();
55+
// @return true if it actually ran (prereqs met / nothing to do); false if
56+
// prerequisites (cache, nodeDB) weren't ready yet, so the caller should retry.
57+
bool preloadNextHopsFromNodeDB();
5658

5759
/**
5860
* Check if this packet should have its hops exhausted.
@@ -64,6 +66,17 @@ class TrafficManagementModule : public MeshModule, private concurrency::OSThread
6466
return exhaustRequested && exhaustRequestedFrom == getFrom(&mp) && exhaustRequestedId == mp.id;
6567
}
6668

69+
// Injectable monotonic clock (ms). All TMM time reads go through clockMs() so unit tests can
70+
// advance a virtual timebase instead of sleeping real seconds across the 6 min/360 s tick.
71+
// Mirrors HopScalingModule::s_testNowMs. Writable from tests as TrafficManagementModule::s_testNowMs;
72+
// ignored in production (clockMs() returns millis()).
73+
inline static uint32_t s_testNowMs = 0;
74+
#ifdef PIO_UNIT_TESTING
75+
static uint32_t clockMs() { return s_testNowMs; }
76+
#else
77+
static uint32_t clockMs() { return millis(); }
78+
#endif
79+
6780
protected:
6881
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
6982
bool wantPacket(const meshtastic_MeshPacket *p) override { return true; }
@@ -149,9 +162,9 @@ class TrafficManagementModule : public MeshModule, private concurrency::OSThread
149162
static constexpr uint32_t kRateTimeTickMs = 300'000UL; // 5 min/tick
150163
static constexpr uint32_t kUnknownTimeTickMs = 60'000UL; // 1 min/tick
151164

152-
static uint8_t currentPosTick() { return static_cast<uint8_t>(millis() / kPosTimeTickMs); }
153-
static uint8_t currentRateTick() { return static_cast<uint8_t>((millis() / kRateTimeTickMs) & 0x0F); }
154-
static uint8_t currentUnknownTick() { return static_cast<uint8_t>((millis() / kUnknownTimeTickMs) & 0x0F); }
165+
static uint8_t currentPosTick() { return static_cast<uint8_t>(clockMs() / kPosTimeTickMs); }
166+
static uint8_t currentRateTick() { return static_cast<uint8_t>((clockMs() / kRateTimeTickMs) & 0x0F); }
167+
static uint8_t currentUnknownTick() { return static_cast<uint8_t>((clockMs() / kUnknownTimeTickMs) & 0x0F); }
155168
// =========================================================================
156169
// Position Fingerprint
157170
// =========================================================================

test/test_traffic_management/test_main.cpp

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@
1212
#if HAS_TRAFFIC_MANAGEMENT
1313

1414
#include "airtime.h"
15-
#if HAS_VARIABLE_HOPS
16-
#endif
1715
#include "mesh/CryptoEngine.h"
1816
#include "mesh/MeshService.h"
1917
#include "mesh/NodeDB.h"
@@ -181,6 +179,10 @@ static void resetTrafficConfig()
181179
mockNodeDB->resetNodes();
182180
mockNodeDB->clearCachedNode();
183181
nodeDB = mockNodeDB;
182+
183+
// Virtual clock base (1 h in, so tick subtraction never underflows). Tests advance time by
184+
// bumping TrafficManagementModule::s_testNowMs instead of sleeping real seconds across a tick.
185+
TrafficManagementModule::s_testNowMs = 3600000;
184186
}
185187

186188
static meshtastic_MeshPacket makeDecodedPacket(meshtastic_PortNum port, NodeNum from, NodeNum to = NODENUM_BROADCAST)
@@ -738,8 +740,6 @@ static void test_tm_alterReceived_exhaustsRelayedTelemetryBroadcast(void)
738740
TEST_ASSERT_EQUAL_UINT32(1, stats.hop_exhausted_packets);
739741
}
740742

741-
#if HAS_VARIABLE_HOPS
742-
#endif // HAS_VARIABLE_HOPS
743743
/**
744744
* Verify hop exhaustion skips unicast and local-origin packets.
745745
* Important to avoid mutating traffic that should retain normal forwarding behavior.
@@ -776,8 +776,9 @@ static void test_tm_positionDedup_allowsDuplicateAfterIntervalExpires(void)
776776
{
777777
moduleConfig.traffic_management.position_dedup_enabled = true;
778778
moduleConfig.traffic_management.position_precision_bits = 16;
779-
// 360 s = 1 pos-tick (kPosTimeTickMs); testDelay advances past one tick period.
779+
// 360 s = 1 pos-tick (kPosTimeTickMs); advance the virtual clock past one tick period.
780780
moduleConfig.traffic_management.position_min_interval_secs = 360;
781+
installWellKnownPrimaryChannel(); // dedup only runs on a well-known channel (gate in handleReceived)
781782
TrafficManagementModuleTestShim module;
782783

783784
meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678);
@@ -786,7 +787,7 @@ static void test_tm_positionDedup_allowsDuplicateAfterIntervalExpires(void)
786787

787788
ProcessMessage r1 = module.handleReceived(first);
788789
ProcessMessage r2 = module.handleReceived(second);
789-
testDelay(360001); // advance past one 6-min pos-tick
790+
TrafficManagementModule::s_testNowMs += 360001; // advance past one 6-min pos-tick (virtual clock)
790791
ProcessMessage r3 = module.handleReceived(third);
791792
meshtastic_TrafficManagementStats stats = module.getStats();
792793

@@ -900,6 +901,7 @@ static void test_tm_positionDedup_cacheFlush_doesNotDropFirstPacketAfterFlush(vo
900901
moduleConfig.traffic_management.position_dedup_enabled = true;
901902
moduleConfig.traffic_management.position_precision_bits = 16;
902903
moduleConfig.traffic_management.position_min_interval_secs = 300;
904+
installWellKnownPrimaryChannel(); // dedup only runs on a well-known channel (gate in handleReceived)
903905
TrafficManagementModuleTestShim module;
904906

905907
meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678);
@@ -930,6 +932,7 @@ static void test_tm_positionDedup_priorRateState_doesNotDropFirstFingerprintZero
930932
moduleConfig.traffic_management.rate_limit_enabled = true;
931933
moduleConfig.traffic_management.rate_limit_window_secs = 60;
932934
moduleConfig.traffic_management.rate_limit_max_packets = 10;
935+
installWellKnownPrimaryChannel(); // dedup only runs on a well-known channel (gate in handleReceived)
933936
TrafficManagementModuleTestShim module;
934937

935938
meshtastic_MeshPacket telemetry = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kRemoteNode);
@@ -954,15 +957,15 @@ static void test_tm_positionDedup_priorRateState_doesNotDropFirstFingerprintZero
954957
static void test_tm_rateLimit_resetsAfterWindowExpires(void)
955958
{
956959
moduleConfig.traffic_management.rate_limit_enabled = true;
957-
// 300 s = 1 rate-tick (kRateTimeTickMs); testDelay advances past one tick period.
960+
// 300 s = 1 rate-tick (kRateTimeTickMs); advance the virtual clock past one tick period.
958961
moduleConfig.traffic_management.rate_limit_window_secs = 300;
959962
moduleConfig.traffic_management.rate_limit_max_packets = 1;
960963
TrafficManagementModuleTestShim module;
961964
meshtastic_MeshPacket packet = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kRemoteNode);
962965

963966
ProcessMessage r1 = module.handleReceived(packet);
964967
ProcessMessage r2 = module.handleReceived(packet);
965-
testDelay(300001); // advance past one 5-min rate-tick
968+
TrafficManagementModule::s_testNowMs += 300001; // advance past one 5-min rate-tick (virtual clock)
966969
ProcessMessage r3 = module.handleReceived(packet);
967970
meshtastic_TrafficManagementStats stats = module.getStats();
968971

@@ -984,7 +987,7 @@ static void test_tm_unknownPackets_resetAfterWindowExpires(void)
984987

985988
ProcessMessage r1 = module.handleReceived(packet);
986989
ProcessMessage r2 = module.handleReceived(packet);
987-
testDelay(300001); // advance past 5 unknown-ticks (kUnknownWindowTicks=5 × 60s)
990+
TrafficManagementModule::s_testNowMs += 300001; // advance past 5 unknown-ticks (5 × 60s) (virtual clock)
988991
ProcessMessage r3 = module.handleReceived(packet);
989992
meshtastic_TrafficManagementStats stats = module.getStats();
990993

@@ -1225,8 +1228,6 @@ static void test_tm_nextHop_keptAliveAcrossMaintenanceSweep(void)
12251228

12261229
TEST_ASSERT_EQUAL_UINT8(0x42, module.getNextHopHint(kTargetNode));
12271230
}
1228-
#if HAS_VARIABLE_HOPS
1229-
#endif // HAS_VARIABLE_HOPS
12301231
} // namespace
12311232

12321233
void setUp(void)
@@ -1264,8 +1265,6 @@ TM_TEST_ENTRY void setup()
12641265
RUN_TEST(test_tm_nodeinfo_directResponse_psramMissDoesNotFallbackToNodeDb);
12651266
#endif
12661267
RUN_TEST(test_tm_alterReceived_exhaustsRelayedTelemetryBroadcast);
1267-
#if HAS_VARIABLE_HOPS
1268-
#endif
12691268
RUN_TEST(test_tm_alterReceived_skipsLocalAndUnicast);
12701269
RUN_TEST(test_tm_positionDedup_allowsDuplicateAfterIntervalExpires);
12711270
RUN_TEST(test_tm_positionDedup_intervalZero_neverDrops);
@@ -1287,8 +1286,6 @@ TM_TEST_ENTRY void setup()
12871286
RUN_TEST(test_tm_nextHop_servedAfterNodeDbRoll);
12881287
RUN_TEST(test_tm_nextHop_preloadDoesNotClobberLearned);
12891288
RUN_TEST(test_tm_nextHop_keptAliveAcrossMaintenanceSweep);
1290-
#if HAS_VARIABLE_HOPS
1291-
#endif
12921289
exit(UNITY_END());
12931290
}
12941291

0 commit comments

Comments
 (0)