* TrafficManagement: flat unified cache + persistent next-hop overflow store Reworks the TrafficManagementModule cache layer (policing behaviour unchanged from upstream) and adds a routing-hint overflow store: - Flatten the ring: replace the cuckoo-hashed unified cache and the bucketed PSRAM NodeInfo index with plain flat arrays + linear scan (same idiom as WarmNodeStore). At LoRa packet rates an O(n) scan of the cache is negligible, and it removes a large amount of hashing/displacement complexity. The cache entry is 11 B; timestamps use a uniform +1 presence-offset so a 0 byte always means "empty" across every sub-store. Adds rebaseEpoch() so cached state survives the ~19 h relative-timestamp horizon instead of being flushed. - Next-hop overflow cache: setNextHop/getNextHopHint store a confirmed last-byte relay for a destination, written only from NextHopRouter's ACK-confirmed decision (and mirrored from TraceRoute). NextHopRouter::getNextHop falls back to this cache when the hot NodeDB has no hint, so DMs/relays to long-tail nodes keep routing after the node ages out of NodeInfoLite. - Persistence: preloadNextHopsFromNodeDB warm-starts the cache from persisted NodeInfoLite hints on first maintenance pass; next_hop entries are kept alive across the maintenance sweep (no TTL) and never clobbered by a stale preload. All packet-policing logic (rate limit, position dedup, unknown-packet drop, NodeInfo direct response, hop exhaustion) is the existing upstream behaviour, untouched. HAS_TRAFFIC_MANAGEMENT defaults on so the module is compiled in. (see note). Tests: upstream policing suite now actually runs (adds the MeshTypes.h include that gates HAS_TRAFFIC_MANAGEMENT) plus 4 next-hop tests. Role-aware throttles, politeness, precision clamp, port-interval and mesh-radius gating — and the rate-limit >255 saturation fix — are deferred to the advanced-TMM branch. Note: default dedup movement grid moves to ~91m, which also means 1.5km required to end up with the same signature position - coarser and therefore further than before. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * TrafficManagement: fix cppcheck constVariablePointer warning `node` in preloadNextHopsFromNodeDB() is never written through — mark it const to satisfy cppcheck's constVariablePointer check in CI. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add multi-hop NextHop recovery tests and unit tests for routing reliability - Introduced a new test suite for multi-hop NextHop directed-message delivery and relay recovery in `test_nexthop_multihop_recovery.py`. This includes tests for end-to-end delivery and recovery after relay drop. - Implemented unit tests in `test_main.cpp` for NextHop routing reliability mitigations, covering: - M1: Ambiguity-aware last-byte resolution. - M2: NextHopRouter's strict-neighbor gate and hop limit checks. - M3: Route-health freshness and failure decay. - Enhanced mock classes to facilitate controlled testing of node behaviors and routing logic. * grafting fixed * Address Copilot review for PR #10735 (NextHop improvements) - docs/nexthop-routing-reliability.md: update status from "no code changes yet" to reflect that mitigations and tests are implemented RAM pressure and MIGRATION_VERBOSE concerns addressed upstream in PR2.5 (per-platform TRAFFIC_MANAGEMENT_CACHE_SIZE) and PR2 (verbose default=0) respectively; (0,0) sentinel fixed in PR2.5. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * CI: fix cppcheck constVariablePointer and test include path - NextHopRouter.cpp: qualify two RouteHealth *h locals as const — only read for stale-route checks, never mutated through the pointer - Router.cpp: qualify meshtastic_NodeInfoLite *node as const in shouldDecrementHopLimit — only read for favorite/role predicate - test_position_module/test_main.cpp: change bare PositionModule.h to modules/PositionModule.h — build_flags sets -Isrc, not -Isrc/modules, so the bare form fails to resolve in the native PlatformIO test env Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * WarmStore: cache device role + protected category in last_heard low bits Steal the low 6 bits of WarmNodeEntry.last_heard to carry an evicted node's device role (4 bits) and a protected category (2 bits) for the hop-trim path, at zero record-size cost (entry stays 40 B; no RAM/flash growth). The high bits remain a real unix-seconds timestamp, quantised to 64 s — ample for warm LRU ordering of long-tail nodes. - absorb() packs role/protectedCat; place()/ring replay store the raw word so metadata round-trips through flash. LRU compares masked time (warmTimeOf). - take() rehydration masks the metadata bits and restores the cached role so a re-admitted node isn't stuck at CLIENT until its next NodeInfo. - NodeDB classifies the category (favorite/ignored/verified -> Flag; tracker/sensor/tak_tracker -> Role) at each eviction site. - WarmNodeStore::lookupMeta() exposes role/category to consumers. - Bump WARM_RING_MAGIC (WRNG->WRN2): old rings read as erased and rebuild; warm data is a non-critical evictee cache, so discard-on-upgrade is safe. Tests: test_warm_store 11/11 (new meta round-trip + quantisation-aware ordering); NodeDB compiles (test_nodedb_blocked 4/4). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * WarmStore: migrate v1 rings/files by discarding last_heard, not the data Previously the WRNG->WRN2 magic bump treated old rings as erased, discarding all warm entries — including the PKI public keys that let evicted nodes keep decrypting DMs. Instead, read v1 (WRNG / WRM1) records and keep each node's identity + public key, discarding only last_heard (its low bits would otherwise be misread as the new role/protected metadata). Records re-rank and re-learn their role on next contact. - Ring backend (nRF52840): ringReadHeader accepts both magics and reports v1 via an out-param; replay zeroes last_heard for v1 records. If the active head page is v1, force a rotation so new v2 records never land in a v1-headered page (which would discard their freshly-set role on the next load). Legacy pages convert to v2 as the ring rotates. - File backend (warm.dat): bump WARM_STORE_MAGIC WRM1->WRM2; accept WRM1, verify CRC against the stored bytes, then discard last_heard and mark dirty so the next save rewrites as v2. Tests: test_warm_store 12/12 (adds test_ws_v1_migration_discardsLastHeard: key survives, role/protected reset). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * WarmStore: guard role bit-width + test eviction carries role/protected - static_assert that the device role enum still fits the 4-bit warm metadata field (WARM_ROLE_MASK); fails the build loudly if a new role is added past 15 rather than silently truncating role on eviction. (Max role today = 12.) - Add test_migration_carriesRoleAndProtectedIntoWarm: a demoted TRACKER lands in the warm tier with its key, role=TRACKER and protected category=Role; a demoted CLIENT carries role=CLIENT/None. Exercises the NodeDB eviction path + warmProtectedCategory classification (the warm-store unit tests only cover absorb() directly). Tests: test_nodedb_blocked 5/5. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix copilot comments * fix(test): restore #if HAS_TRAFFIC_MANAGEMENT guard in TMM test The rebase onto PR1.5 lost the top-level HAS_TRAFFIC_MANAGEMENT guard that PR1.5 introduced, leaving the #else/#endif tail orphaned and causing compile errors on non-TMM builds. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
1299 lines
53 KiB
C++
1299 lines
53 KiB
C++
#include "MeshTypes.h" // Include BEFORE TestUtil.h — provides HAS_TRAFFIC_MANAGEMENT (via mesh-pb-constants.h)
|
|
#include "TestUtil.h"
|
|
#include <cstdlib>
|
|
#include <unity.h>
|
|
|
|
#if defined(ARCH_PORTDUINO)
|
|
#define TM_TEST_ENTRY extern "C"
|
|
#else
|
|
#define TM_TEST_ENTRY
|
|
#endif
|
|
|
|
#if HAS_TRAFFIC_MANAGEMENT
|
|
|
|
#include "airtime.h"
|
|
#include "mesh/CryptoEngine.h"
|
|
#include "mesh/MeshService.h"
|
|
#include "mesh/NodeDB.h"
|
|
#include "mesh/Router.h"
|
|
#include "modules/TrafficManagementModule.h"
|
|
#include <climits>
|
|
#include <cstring>
|
|
#include <memory>
|
|
#include <pb_encode.h>
|
|
#include <vector>
|
|
|
|
namespace
|
|
{
|
|
|
|
constexpr NodeNum kLocalNode = 0x11111111;
|
|
constexpr NodeNum kRemoteNode = 0x22222222;
|
|
constexpr NodeNum kTargetNode = 0x33333333;
|
|
|
|
// Telemetry hop exhaustion is gated on channel congestion (alterReceived checks
|
|
// airTime->isTxAllowedChannelUtil/isTxAllowedAirUtil). Installs a global
|
|
// airTime reporting 100% channel utilization for the enclosing scope.
|
|
class ScopedBusyAirTime
|
|
{
|
|
public:
|
|
ScopedBusyAirTime() : previous(airTime)
|
|
{
|
|
for (uint32_t i = 0; i < CHANNEL_UTILIZATION_PERIODS; i++)
|
|
busy.channelUtilization[i] = 10000; // 10 s of airtime per 10 s period
|
|
airTime = &busy;
|
|
}
|
|
~ScopedBusyAirTime() { airTime = previous; }
|
|
|
|
private:
|
|
AirTime busy;
|
|
AirTime *previous;
|
|
};
|
|
|
|
class MockNodeDB : public NodeDB
|
|
{
|
|
public:
|
|
meshtastic_NodeInfoLite *getMeshNode(NodeNum n) override
|
|
{
|
|
if (hasCachedNode && n == cachedNodeNum)
|
|
return &cachedNode;
|
|
return NodeDB::getMeshNode(n);
|
|
}
|
|
|
|
void clearCachedNode()
|
|
{
|
|
hasCachedNode = false;
|
|
cachedNodeNum = 0;
|
|
cachedNode = meshtastic_NodeInfoLite_init_zero;
|
|
}
|
|
|
|
void setCachedNode(NodeNum n)
|
|
{
|
|
clearCachedNode();
|
|
hasCachedNode = true;
|
|
cachedNodeNum = n;
|
|
cachedNode.num = n;
|
|
cachedNode.bitfield |= NODEINFO_BITFIELD_HAS_USER_MASK;
|
|
}
|
|
|
|
// Role the TMM should see for the cached node (sender-role-aware throttles).
|
|
void setCachedNodeRole(meshtastic_Config_DeviceConfig_Role role) { cachedNode.role = role; }
|
|
|
|
// Seed a node into the hot-store buffer at index 1 (index 0 is reserved for
|
|
// "self"). Respects the fixed-buffer invariant: `meshNodes` is a buffer of
|
|
// MAX_NUM_NODES slots with `numMeshNodes` as the logical count — we grow the
|
|
// buffer if needed and bump the count, never clear()/push_back() (which would
|
|
// shrink it and break NodeDB::resetNodes()'s begin()+1..end() fill).
|
|
void setHotNode(NodeNum n, uint8_t nextHop)
|
|
{
|
|
if (meshNodes->size() < 2)
|
|
meshNodes->resize(2);
|
|
(*meshNodes)[1] = meshtastic_NodeInfoLite_init_zero;
|
|
(*meshNodes)[1].num = n;
|
|
(*meshNodes)[1].next_hop = nextHop;
|
|
numMeshNodes = 2;
|
|
}
|
|
|
|
// Evict everything but "self" — simulates the hot DB rolling over. Logical
|
|
// count only; the buffer is left intact so the invariant holds.
|
|
void rollHotStore()
|
|
{
|
|
numMeshNodes = 1;
|
|
clearCachedNode();
|
|
}
|
|
|
|
private:
|
|
bool hasCachedNode = false;
|
|
NodeNum cachedNodeNum = 0;
|
|
meshtastic_NodeInfoLite cachedNode = meshtastic_NodeInfoLite_init_zero;
|
|
};
|
|
|
|
class MockRadioInterface : public RadioInterface
|
|
{
|
|
public:
|
|
ErrorCode send(meshtastic_MeshPacket *p) override
|
|
{
|
|
packetPool.release(p);
|
|
return ERRNO_OK;
|
|
}
|
|
|
|
uint32_t getPacketTime(uint32_t totalPacketLen, bool received = false) override
|
|
{
|
|
(void)totalPacketLen;
|
|
(void)received;
|
|
return 0;
|
|
}
|
|
};
|
|
|
|
class MockRouter : public Router
|
|
{
|
|
public:
|
|
~MockRouter()
|
|
{
|
|
// Router allocates a global crypt lock in its constructor.
|
|
// Clean it up here so each test can build a fresh mock router.
|
|
delete cryptLock;
|
|
cryptLock = nullptr;
|
|
}
|
|
|
|
ErrorCode send(meshtastic_MeshPacket *p) override
|
|
{
|
|
sentPackets.push_back(*p);
|
|
packetPool.release(p);
|
|
return ERRNO_OK;
|
|
}
|
|
|
|
std::vector<meshtastic_MeshPacket> sentPackets;
|
|
};
|
|
|
|
class TrafficManagementModuleTestShim : public TrafficManagementModule
|
|
{
|
|
public:
|
|
using TrafficManagementModule::alterReceived;
|
|
using TrafficManagementModule::handleReceived;
|
|
using TrafficManagementModule::resetEpoch;
|
|
using TrafficManagementModule::runOnce;
|
|
|
|
bool ignoreRequestFlag() const { return ignoreRequest; }
|
|
};
|
|
|
|
MockNodeDB *mockNodeDB = nullptr;
|
|
|
|
static void resetTrafficConfig()
|
|
{
|
|
moduleConfig = meshtastic_LocalModuleConfig_init_zero;
|
|
moduleConfig.has_traffic_management = true;
|
|
moduleConfig.traffic_management = meshtastic_ModuleConfig_TrafficManagementConfig_init_zero;
|
|
moduleConfig.traffic_management.enabled = true;
|
|
|
|
config = meshtastic_LocalConfig_init_zero;
|
|
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT;
|
|
|
|
channelFile = meshtastic_ChannelFile_init_zero;
|
|
owner.is_licensed = false;
|
|
|
|
myNodeInfo.my_node_num = kLocalNode;
|
|
|
|
router = nullptr;
|
|
service = nullptr;
|
|
|
|
mockNodeDB->resetNodes();
|
|
mockNodeDB->clearCachedNode();
|
|
nodeDB = mockNodeDB;
|
|
}
|
|
|
|
static meshtastic_MeshPacket makeDecodedPacket(meshtastic_PortNum port, NodeNum from, NodeNum to = NODENUM_BROADCAST)
|
|
{
|
|
meshtastic_MeshPacket packet = meshtastic_MeshPacket_init_zero;
|
|
packet.from = from;
|
|
packet.to = to;
|
|
packet.id = 0x1001;
|
|
packet.channel = 0;
|
|
packet.hop_start = 3;
|
|
packet.hop_limit = 3;
|
|
packet.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
|
packet.decoded.portnum = port;
|
|
packet.decoded.has_bitfield = true;
|
|
packet.decoded.bitfield = 0;
|
|
return packet;
|
|
}
|
|
|
|
static meshtastic_MeshPacket makeUnknownPacket(NodeNum from, NodeNum to = NODENUM_BROADCAST)
|
|
{
|
|
meshtastic_MeshPacket packet = meshtastic_MeshPacket_init_zero;
|
|
packet.from = from;
|
|
packet.to = to;
|
|
packet.id = 0x2001;
|
|
packet.channel = 0;
|
|
packet.hop_start = 3;
|
|
packet.hop_limit = 3;
|
|
packet.which_payload_variant = meshtastic_MeshPacket_encrypted_tag;
|
|
packet.encrypted.size = 0;
|
|
return packet;
|
|
}
|
|
|
|
static meshtastic_MeshPacket makePositionPacket(NodeNum from, int32_t lat, int32_t lon, NodeNum to = NODENUM_BROADCAST)
|
|
{
|
|
meshtastic_MeshPacket packet = makeDecodedPacket(meshtastic_PortNum_POSITION_APP, from, to);
|
|
meshtastic_Position pos = meshtastic_Position_init_zero;
|
|
pos.has_latitude_i = true;
|
|
pos.has_longitude_i = true;
|
|
pos.latitude_i = lat;
|
|
pos.longitude_i = lon;
|
|
|
|
packet.decoded.payload.size =
|
|
pb_encode_to_bytes(packet.decoded.payload.bytes, sizeof(packet.decoded.payload.bytes), &meshtastic_Position_msg, &pos);
|
|
return packet;
|
|
}
|
|
|
|
static meshtastic_MeshPacket makePositionPacketWithPrecision(NodeNum from, int32_t lat, int32_t lon, uint32_t precisionBits)
|
|
{
|
|
meshtastic_MeshPacket packet = makeDecodedPacket(meshtastic_PortNum_POSITION_APP, from, NODENUM_BROADCAST);
|
|
meshtastic_Position pos = meshtastic_Position_init_zero;
|
|
pos.has_latitude_i = true;
|
|
pos.has_longitude_i = true;
|
|
pos.latitude_i = lat;
|
|
pos.longitude_i = lon;
|
|
pos.precision_bits = precisionBits;
|
|
|
|
packet.decoded.payload.size =
|
|
pb_encode_to_bytes(packet.decoded.payload.bytes, sizeof(packet.decoded.payload.bytes), &meshtastic_Position_msg, &pos);
|
|
return packet;
|
|
}
|
|
|
|
static bool decodePositionPayload(const meshtastic_MeshPacket &packet, meshtastic_Position &out)
|
|
{
|
|
out = meshtastic_Position_init_zero;
|
|
return pb_decode_from_bytes(packet.decoded.payload.bytes, packet.decoded.payload.size, &meshtastic_Position_msg, &out);
|
|
}
|
|
|
|
// Primary channel with a well-known single-byte PSK and the (empty -> preset)
|
|
// default name, so Channels::isWellKnownChannel(0) is true.
|
|
static void installWellKnownPrimaryChannel()
|
|
{
|
|
channelFile = meshtastic_ChannelFile_init_zero;
|
|
channelFile.channels_count = 1;
|
|
channelFile.channels[0].index = 0;
|
|
channelFile.channels[0].has_settings = true;
|
|
channelFile.channels[0].role = meshtastic_Channel_Role_PRIMARY;
|
|
channelFile.channels[0].settings.psk.size = 1;
|
|
channelFile.channels[0].settings.psk.bytes[0] = 1;
|
|
config.lora.use_preset = true;
|
|
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
|
}
|
|
|
|
static meshtastic_MeshPacket makeNodeInfoPacket(NodeNum from, const char *longName, const char *shortName)
|
|
{
|
|
meshtastic_MeshPacket packet = makeDecodedPacket(meshtastic_PortNum_NODEINFO_APP, from, NODENUM_BROADCAST);
|
|
|
|
meshtastic_User user = meshtastic_User_init_zero;
|
|
snprintf(user.id, sizeof(user.id), "!%08x", from);
|
|
strncpy(user.long_name, longName, sizeof(user.long_name) - 1);
|
|
strncpy(user.short_name, shortName, sizeof(user.short_name) - 1);
|
|
|
|
packet.decoded.payload.size =
|
|
pb_encode_to_bytes(packet.decoded.payload.bytes, sizeof(packet.decoded.payload.bytes), &meshtastic_User_msg, &user);
|
|
return packet;
|
|
}
|
|
|
|
/**
|
|
* Verify the module is a no-op when traffic management is disabled.
|
|
* Important so config toggles cannot accidentally change routing behavior.
|
|
*/
|
|
static void test_tm_moduleDisabled_doesNothing(void)
|
|
{
|
|
moduleConfig.has_traffic_management = false;
|
|
TrafficManagementModuleTestShim module;
|
|
meshtastic_MeshPacket packet = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode);
|
|
|
|
ProcessMessage result = module.handleReceived(packet);
|
|
meshtastic_TrafficManagementStats stats = module.getStats();
|
|
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(result));
|
|
TEST_ASSERT_EQUAL_UINT32(0, stats.packets_inspected);
|
|
TEST_ASSERT_EQUAL_UINT32(0, stats.unknown_packet_drops);
|
|
TEST_ASSERT_FALSE(module.ignoreRequestFlag());
|
|
}
|
|
|
|
/**
|
|
* Verify unknown-packet dropping uses N+1 threshold semantics.
|
|
* Important to catch off-by-one regressions in drop decisions.
|
|
*/
|
|
static void test_tm_unknownPackets_dropOnNPlusOne(void)
|
|
{
|
|
moduleConfig.traffic_management.drop_unknown_enabled = true;
|
|
moduleConfig.traffic_management.unknown_packet_threshold = 2;
|
|
TrafficManagementModuleTestShim module;
|
|
meshtastic_MeshPacket packet = makeUnknownPacket(kRemoteNode);
|
|
|
|
ProcessMessage r1 = module.handleReceived(packet);
|
|
ProcessMessage r2 = module.handleReceived(packet);
|
|
ProcessMessage r3 = module.handleReceived(packet);
|
|
meshtastic_TrafficManagementStats stats = module.getStats();
|
|
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(r1));
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(r2));
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::STOP), static_cast<int>(r3));
|
|
TEST_ASSERT_EQUAL_UINT32(1, stats.unknown_packet_drops);
|
|
TEST_ASSERT_EQUAL_UINT32(3, stats.packets_inspected);
|
|
TEST_ASSERT_TRUE(module.ignoreRequestFlag());
|
|
}
|
|
|
|
/**
|
|
* Verify duplicate position broadcasts inside the dedup window are dropped.
|
|
* Important because this is the primary airtime-saving behavior.
|
|
*/
|
|
static void test_tm_positionDedup_dropsDuplicateWithinWindow(void)
|
|
{
|
|
moduleConfig.traffic_management.position_dedup_enabled = true;
|
|
moduleConfig.traffic_management.position_precision_bits = 16;
|
|
moduleConfig.traffic_management.position_min_interval_secs = 300;
|
|
TrafficManagementModuleTestShim module;
|
|
|
|
meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
|
meshtastic_MeshPacket second = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
|
|
|
ProcessMessage r1 = module.handleReceived(first);
|
|
ProcessMessage r2 = module.handleReceived(second);
|
|
meshtastic_TrafficManagementStats stats = module.getStats();
|
|
|
|
TEST_ASSERT_GREATER_THAN_UINT32(0, first.decoded.payload.size);
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(r1));
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::STOP), static_cast<int>(r2));
|
|
TEST_ASSERT_EQUAL_UINT32(1, stats.position_dedup_drops);
|
|
TEST_ASSERT_TRUE(module.ignoreRequestFlag());
|
|
}
|
|
|
|
/**
|
|
* Verify changed coordinates are forwarded even with dedup enabled.
|
|
* Important so real movement updates are never suppressed as duplicates.
|
|
*/
|
|
static void test_tm_positionDedup_allowsMovedPosition(void)
|
|
{
|
|
moduleConfig.traffic_management.position_dedup_enabled = true;
|
|
moduleConfig.traffic_management.position_precision_bits = 16;
|
|
moduleConfig.traffic_management.position_min_interval_secs = 300;
|
|
TrafficManagementModuleTestShim module;
|
|
|
|
meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
|
meshtastic_MeshPacket moved = makePositionPacket(kRemoteNode, 384221234, -1210845678);
|
|
|
|
ProcessMessage r1 = module.handleReceived(first);
|
|
ProcessMessage r2 = module.handleReceived(moved);
|
|
meshtastic_TrafficManagementStats stats = module.getStats();
|
|
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(r1));
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(r2));
|
|
TEST_ASSERT_EQUAL_UINT32(0, stats.position_dedup_drops);
|
|
}
|
|
|
|
/**
|
|
* Verify rate limiting drops only after exceeding the configured threshold.
|
|
* Important to protect threshold semantics from off-by-one regressions.
|
|
*/
|
|
static void test_tm_rateLimit_dropsOnlyAfterThreshold(void)
|
|
{
|
|
moduleConfig.traffic_management.rate_limit_enabled = true;
|
|
moduleConfig.traffic_management.rate_limit_window_secs = 60;
|
|
moduleConfig.traffic_management.rate_limit_max_packets = 3;
|
|
TrafficManagementModuleTestShim module;
|
|
meshtastic_MeshPacket packet = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kRemoteNode);
|
|
|
|
ProcessMessage r1 = module.handleReceived(packet);
|
|
ProcessMessage r2 = module.handleReceived(packet);
|
|
ProcessMessage r3 = module.handleReceived(packet);
|
|
ProcessMessage r4 = module.handleReceived(packet);
|
|
meshtastic_TrafficManagementStats stats = module.getStats();
|
|
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(r1));
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(r2));
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(r3));
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::STOP), static_cast<int>(r4));
|
|
TEST_ASSERT_EQUAL_UINT32(1, stats.rate_limit_drops);
|
|
TEST_ASSERT_TRUE(module.ignoreRequestFlag());
|
|
}
|
|
/**
|
|
* Verify packets sourced from this node bypass dedup and rate limiting.
|
|
* Important so local transmissions are not accidentally self-throttled.
|
|
*/
|
|
static void test_tm_fromUs_bypassesPositionAndRateFilters(void)
|
|
{
|
|
moduleConfig.traffic_management.position_dedup_enabled = true;
|
|
moduleConfig.traffic_management.position_precision_bits = 16;
|
|
moduleConfig.traffic_management.position_min_interval_secs = 300;
|
|
moduleConfig.traffic_management.rate_limit_enabled = true;
|
|
moduleConfig.traffic_management.rate_limit_window_secs = 60;
|
|
moduleConfig.traffic_management.rate_limit_max_packets = 1;
|
|
TrafficManagementModuleTestShim module;
|
|
|
|
meshtastic_MeshPacket positionPacket = makePositionPacket(kLocalNode, 374221234, -1220845678);
|
|
meshtastic_MeshPacket textPacket = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kLocalNode);
|
|
|
|
ProcessMessage p1 = module.handleReceived(positionPacket);
|
|
ProcessMessage p2 = module.handleReceived(positionPacket);
|
|
ProcessMessage t1 = module.handleReceived(textPacket);
|
|
ProcessMessage t2 = module.handleReceived(textPacket);
|
|
|
|
meshtastic_TrafficManagementStats stats = module.getStats();
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(p1));
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(p2));
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(t1));
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(t2));
|
|
TEST_ASSERT_EQUAL_UINT32(0, stats.position_dedup_drops);
|
|
TEST_ASSERT_EQUAL_UINT32(0, stats.rate_limit_drops);
|
|
}
|
|
|
|
/**
|
|
* Verify locally addressed packets are never dropped by transit shaping.
|
|
* Important so dedup/rate limiting do not suppress end-user delivery.
|
|
*/
|
|
static void test_tm_localDestination_bypassesTransitFilters(void)
|
|
{
|
|
moduleConfig.traffic_management.position_dedup_enabled = true;
|
|
moduleConfig.traffic_management.position_precision_bits = 16;
|
|
moduleConfig.traffic_management.position_min_interval_secs = 300;
|
|
moduleConfig.traffic_management.rate_limit_enabled = true;
|
|
moduleConfig.traffic_management.rate_limit_window_secs = 60;
|
|
moduleConfig.traffic_management.rate_limit_max_packets = 1;
|
|
TrafficManagementModuleTestShim module;
|
|
|
|
meshtastic_MeshPacket position1 = makePositionPacket(kRemoteNode, 374221234, -1220845678, kLocalNode);
|
|
meshtastic_MeshPacket position2 = makePositionPacket(kRemoteNode, 374221234, -1220845678, kLocalNode);
|
|
meshtastic_MeshPacket text1 = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode, kLocalNode);
|
|
meshtastic_MeshPacket text2 = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode, kLocalNode);
|
|
|
|
ProcessMessage p1 = module.handleReceived(position1);
|
|
ProcessMessage p2 = module.handleReceived(position2);
|
|
ProcessMessage t1 = module.handleReceived(text1);
|
|
ProcessMessage t2 = module.handleReceived(text2);
|
|
meshtastic_TrafficManagementStats stats = module.getStats();
|
|
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(p1));
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(p2));
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(t1));
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(t2));
|
|
TEST_ASSERT_EQUAL_UINT32(0, stats.position_dedup_drops);
|
|
TEST_ASSERT_EQUAL_UINT32(0, stats.rate_limit_drops);
|
|
}
|
|
|
|
/**
|
|
* Verify router role clamps NodeInfo response hops to router-safe maximum.
|
|
* Important so large config values cannot widen response scope unexpectedly.
|
|
*/
|
|
static void test_tm_nodeinfo_routerClamp_skipsWhenTooManyHops(void)
|
|
{
|
|
moduleConfig.traffic_management.nodeinfo_direct_response = true;
|
|
moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10;
|
|
config.device.role = meshtastic_Config_DeviceConfig_Role_ROUTER;
|
|
mockNodeDB->setCachedNode(kTargetNode);
|
|
|
|
TrafficManagementModuleTestShim module;
|
|
meshtastic_MeshPacket request = makeDecodedPacket(meshtastic_PortNum_NODEINFO_APP, kRemoteNode, kTargetNode);
|
|
request.decoded.want_response = true;
|
|
request.hop_start = 5;
|
|
request.hop_limit = 1; // 4 hops away; router clamp should cap max at 3
|
|
|
|
ProcessMessage result = module.handleReceived(request);
|
|
meshtastic_TrafficManagementStats stats = module.getStats();
|
|
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(result));
|
|
TEST_ASSERT_EQUAL_UINT32(0, stats.nodeinfo_cache_hits);
|
|
TEST_ASSERT_FALSE(module.ignoreRequestFlag());
|
|
}
|
|
|
|
/**
|
|
* Verify NodeInfo direct-response success path and reply packet fields.
|
|
* Important because this path consumes the request and generates a spoofed cached reply.
|
|
*/
|
|
static void test_tm_nodeinfo_directResponse_respondsFromCache(void)
|
|
{
|
|
moduleConfig.traffic_management.nodeinfo_direct_response = true;
|
|
moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10;
|
|
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT;
|
|
config.lora.config_ok_to_mqtt = true;
|
|
mockNodeDB->setCachedNode(kTargetNode);
|
|
|
|
MockRouter mockRouter;
|
|
mockRouter.addInterface(std::unique_ptr<RadioInterface>(new MockRadioInterface()));
|
|
MeshService mockService;
|
|
router = &mockRouter;
|
|
service = &mockService;
|
|
|
|
TrafficManagementModuleTestShim module;
|
|
meshtastic_MeshPacket request = makeDecodedPacket(meshtastic_PortNum_NODEINFO_APP, kRemoteNode, kTargetNode);
|
|
request.decoded.want_response = true;
|
|
request.id = 0x13572468;
|
|
request.hop_start = 3;
|
|
request.hop_limit = 3; // direct request (0 hops away)
|
|
|
|
ProcessMessage result = module.handleReceived(request);
|
|
meshtastic_TrafficManagementStats stats = module.getStats();
|
|
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::STOP), static_cast<int>(result));
|
|
TEST_ASSERT_TRUE(module.ignoreRequestFlag());
|
|
TEST_ASSERT_EQUAL_UINT32(1, stats.nodeinfo_cache_hits);
|
|
TEST_ASSERT_EQUAL_UINT32(1, static_cast<uint32_t>(mockRouter.sentPackets.size()));
|
|
|
|
const meshtastic_MeshPacket &reply = mockRouter.sentPackets.front();
|
|
TEST_ASSERT_EQUAL_INT(meshtastic_PortNum_NODEINFO_APP, reply.decoded.portnum);
|
|
TEST_ASSERT_EQUAL_UINT32(kTargetNode, reply.from);
|
|
TEST_ASSERT_EQUAL_UINT32(kRemoteNode, reply.to);
|
|
TEST_ASSERT_EQUAL_UINT32(request.id, reply.decoded.request_id);
|
|
TEST_ASSERT_FALSE(reply.decoded.want_response);
|
|
TEST_ASSERT_EQUAL_UINT8(0, reply.hop_limit);
|
|
TEST_ASSERT_EQUAL_UINT8(0, reply.hop_start);
|
|
TEST_ASSERT_EQUAL_UINT8(mockNodeDB->getLastByteOfNodeNum(kRemoteNode), reply.next_hop);
|
|
TEST_ASSERT_TRUE(reply.decoded.has_bitfield);
|
|
TEST_ASSERT_EQUAL_UINT8(BITFIELD_OK_TO_MQTT_MASK, reply.decoded.bitfield);
|
|
}
|
|
|
|
/**
|
|
* Verify cached direct replies still preserve requester NodeInfo learning.
|
|
* Important so consuming the request does not skip NodeDB refresh for observers.
|
|
*/
|
|
static void test_tm_nodeinfo_directResponse_learnsRequestorNodeInfo(void)
|
|
{
|
|
moduleConfig.traffic_management.nodeinfo_direct_response = true;
|
|
moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10;
|
|
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT;
|
|
mockNodeDB->setCachedNode(kTargetNode);
|
|
|
|
MockRouter mockRouter;
|
|
mockRouter.addInterface(std::unique_ptr<RadioInterface>(new MockRadioInterface()));
|
|
MeshService mockService;
|
|
router = &mockRouter;
|
|
service = &mockService;
|
|
|
|
TrafficManagementModuleTestShim module;
|
|
meshtastic_MeshPacket request = makeNodeInfoPacket(kRemoteNode, "requester-long", "rq");
|
|
request.to = kTargetNode;
|
|
request.decoded.want_response = true;
|
|
request.id = 0x01020304;
|
|
request.hop_start = 3;
|
|
request.hop_limit = 3;
|
|
|
|
ProcessMessage result = module.handleReceived(request);
|
|
meshtastic_NodeInfoLite *requestor = mockNodeDB->getMeshNode(kRemoteNode);
|
|
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::STOP), static_cast<int>(result));
|
|
TEST_ASSERT_NOT_NULL(requestor);
|
|
TEST_ASSERT_TRUE((requestor->bitfield & NODEINFO_BITFIELD_HAS_USER_MASK) != 0);
|
|
TEST_ASSERT_EQUAL_STRING("requester-long", requestor->long_name);
|
|
TEST_ASSERT_EQUAL_STRING("rq", requestor->short_name);
|
|
TEST_ASSERT_EQUAL_UINT8(request.channel, requestor->channel);
|
|
}
|
|
|
|
/**
|
|
* Verify client role only answers direct (0-hop) NodeInfo requests.
|
|
* Important so clients do not answer relayed requests outside intended scope.
|
|
*/
|
|
static void test_tm_nodeinfo_clientClamp_skipsWhenNotDirect(void)
|
|
{
|
|
moduleConfig.traffic_management.nodeinfo_direct_response = true;
|
|
moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10;
|
|
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT;
|
|
mockNodeDB->setCachedNode(kTargetNode);
|
|
|
|
TrafficManagementModuleTestShim module;
|
|
meshtastic_MeshPacket request = makeDecodedPacket(meshtastic_PortNum_NODEINFO_APP, kRemoteNode, kTargetNode);
|
|
request.decoded.want_response = true;
|
|
request.hop_start = 2;
|
|
request.hop_limit = 1; // 1 hop away; clients are clamped to max 0
|
|
|
|
ProcessMessage result = module.handleReceived(request);
|
|
meshtastic_TrafficManagementStats stats = module.getStats();
|
|
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(result));
|
|
TEST_ASSERT_EQUAL_UINT32(0, stats.nodeinfo_cache_hits);
|
|
TEST_ASSERT_FALSE(module.ignoreRequestFlag());
|
|
}
|
|
|
|
#if !(defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM))
|
|
/**
|
|
* Verify non-PSRAM builds require NodeDB for direct NodeInfo responses.
|
|
* Important because fallback should only happen through node-wide data when
|
|
* the dedicated PSRAM cache does not exist.
|
|
*/
|
|
static void test_tm_nodeinfo_directResponse_withoutNodeDbEntry_skips(void)
|
|
{
|
|
moduleConfig.traffic_management.nodeinfo_direct_response = true;
|
|
moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10;
|
|
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT;
|
|
mockNodeDB->clearCachedNode();
|
|
|
|
MockRouter mockRouter;
|
|
mockRouter.addInterface(std::unique_ptr<RadioInterface>(new MockRadioInterface()));
|
|
MeshService mockService;
|
|
router = &mockRouter;
|
|
service = &mockService;
|
|
|
|
TrafficManagementModuleTestShim module;
|
|
meshtastic_MeshPacket request = makeDecodedPacket(meshtastic_PortNum_NODEINFO_APP, kRemoteNode, kTargetNode);
|
|
request.decoded.want_response = true;
|
|
request.hop_start = 3;
|
|
request.hop_limit = 3;
|
|
|
|
ProcessMessage result = module.handleReceived(request);
|
|
meshtastic_TrafficManagementStats stats = module.getStats();
|
|
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(result));
|
|
TEST_ASSERT_FALSE(module.ignoreRequestFlag());
|
|
TEST_ASSERT_EQUAL_UINT32(0, stats.nodeinfo_cache_hits);
|
|
TEST_ASSERT_EQUAL_UINT32(0, static_cast<uint32_t>(mockRouter.sentPackets.size()));
|
|
}
|
|
#endif
|
|
|
|
#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM)
|
|
/**
|
|
* Verify PSRAM NodeInfo cache can answer requests without NodeDB and that
|
|
* shouldRespondToNodeInfo() uses cached bitfield metadata.
|
|
*/
|
|
static void test_tm_nodeinfo_directResponse_psramCacheRespondsAndPreservesBitfield(void)
|
|
{
|
|
moduleConfig.traffic_management.nodeinfo_direct_response = true;
|
|
moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10;
|
|
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT;
|
|
config.lora.config_ok_to_mqtt = true;
|
|
mockNodeDB->clearCachedNode();
|
|
|
|
MockRouter mockRouter;
|
|
mockRouter.addInterface(std::unique_ptr<RadioInterface>(new MockRadioInterface()));
|
|
MeshService mockService;
|
|
router = &mockRouter;
|
|
service = &mockService;
|
|
|
|
TrafficManagementModuleTestShim module;
|
|
|
|
meshtastic_MeshPacket observed = makeNodeInfoPacket(kTargetNode, "target-long", "tg");
|
|
observed.decoded.has_bitfield = true;
|
|
observed.decoded.bitfield = BITFIELD_WANT_RESPONSE_MASK;
|
|
observed.channel = 2;
|
|
observed.rx_time = 123456;
|
|
|
|
ProcessMessage observedResult = module.handleReceived(observed);
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(observedResult));
|
|
|
|
meshtastic_MeshPacket request = makeDecodedPacket(meshtastic_PortNum_NODEINFO_APP, kRemoteNode, kTargetNode);
|
|
request.decoded.want_response = true;
|
|
request.id = 0x24681357;
|
|
request.channel = 1;
|
|
request.hop_start = 3;
|
|
request.hop_limit = 3;
|
|
|
|
ProcessMessage result = module.handleReceived(request);
|
|
meshtastic_TrafficManagementStats stats = module.getStats();
|
|
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::STOP), static_cast<int>(result));
|
|
TEST_ASSERT_TRUE(module.ignoreRequestFlag());
|
|
TEST_ASSERT_EQUAL_UINT32(1, stats.nodeinfo_cache_hits);
|
|
TEST_ASSERT_EQUAL_UINT32(1, static_cast<uint32_t>(mockRouter.sentPackets.size()));
|
|
|
|
const meshtastic_MeshPacket &reply = mockRouter.sentPackets.front();
|
|
TEST_ASSERT_TRUE(reply.decoded.has_bitfield);
|
|
TEST_ASSERT_EQUAL_UINT8(static_cast<uint8_t>(BITFIELD_WANT_RESPONSE_MASK | BITFIELD_OK_TO_MQTT_MASK), reply.decoded.bitfield);
|
|
TEST_ASSERT_EQUAL_UINT32(kTargetNode, reply.from);
|
|
TEST_ASSERT_EQUAL_UINT32(kRemoteNode, reply.to);
|
|
TEST_ASSERT_EQUAL_UINT8(request.channel, reply.channel);
|
|
TEST_ASSERT_EQUAL_UINT32(request.id, reply.decoded.request_id);
|
|
}
|
|
|
|
/**
|
|
* Verify PSRAM cache misses do not fall back to NodeDB.
|
|
* Important so the dedicated PSRAM index stays logically separate from
|
|
* NodeInfoModule/NodeDB when PSRAM is available.
|
|
*/
|
|
static void test_tm_nodeinfo_directResponse_psramMissDoesNotFallbackToNodeDb(void)
|
|
{
|
|
moduleConfig.traffic_management.nodeinfo_direct_response = true;
|
|
moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10;
|
|
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT;
|
|
mockNodeDB->setCachedNode(kTargetNode);
|
|
|
|
MockRouter mockRouter;
|
|
mockRouter.addInterface(std::unique_ptr<RadioInterface>(new MockRadioInterface()));
|
|
MeshService mockService;
|
|
router = &mockRouter;
|
|
service = &mockService;
|
|
|
|
TrafficManagementModuleTestShim module;
|
|
meshtastic_MeshPacket request = makeDecodedPacket(meshtastic_PortNum_NODEINFO_APP, kRemoteNode, kTargetNode);
|
|
request.decoded.want_response = true;
|
|
request.hop_start = 3;
|
|
request.hop_limit = 3;
|
|
|
|
ProcessMessage result = module.handleReceived(request);
|
|
meshtastic_TrafficManagementStats stats = module.getStats();
|
|
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(result));
|
|
TEST_ASSERT_FALSE(module.ignoreRequestFlag());
|
|
TEST_ASSERT_EQUAL_UINT32(0, stats.nodeinfo_cache_hits);
|
|
TEST_ASSERT_EQUAL_UINT32(0, static_cast<uint32_t>(mockRouter.sentPackets.size()));
|
|
}
|
|
#endif
|
|
|
|
/**
|
|
* Verify relayed telemetry broadcasts are hop-exhausted when enabled AND the
|
|
* channel is congested (telemetry exhaustion is gated on channel utilization,
|
|
* unlike position exhaustion).
|
|
* Important to prevent further mesh propagation while still allowing one relay step.
|
|
*/
|
|
static void test_tm_alterReceived_exhaustsRelayedTelemetryBroadcast(void)
|
|
{
|
|
moduleConfig.traffic_management.exhaust_hop_telemetry = true;
|
|
ScopedBusyAirTime busyChannel;
|
|
TrafficManagementModuleTestShim module;
|
|
meshtastic_MeshPacket packet = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kRemoteNode, NODENUM_BROADCAST);
|
|
packet.hop_start = 5;
|
|
packet.hop_limit = 3;
|
|
|
|
module.alterReceived(packet);
|
|
meshtastic_TrafficManagementStats stats = module.getStats();
|
|
|
|
TEST_ASSERT_EQUAL_UINT8(0, packet.hop_limit);
|
|
TEST_ASSERT_EQUAL_UINT8(3, packet.hop_start);
|
|
TEST_ASSERT_TRUE(module.shouldExhaustHops(packet));
|
|
TEST_ASSERT_EQUAL_UINT32(1, stats.hop_exhausted_packets);
|
|
}
|
|
|
|
/**
|
|
* Verify hop exhaustion skips unicast and local-origin packets.
|
|
* Important to avoid mutating traffic that should retain normal forwarding behavior.
|
|
*/
|
|
static void test_tm_alterReceived_skipsLocalAndUnicast(void)
|
|
{
|
|
moduleConfig.traffic_management.exhaust_hop_telemetry = true;
|
|
ScopedBusyAirTime busyChannel; // congestion satisfied, so only the skip conditions are under test
|
|
TrafficManagementModuleTestShim module;
|
|
|
|
meshtastic_MeshPacket unicast = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kRemoteNode, kTargetNode);
|
|
unicast.hop_start = 5;
|
|
unicast.hop_limit = 3;
|
|
module.alterReceived(unicast);
|
|
TEST_ASSERT_EQUAL_UINT8(3, unicast.hop_limit);
|
|
TEST_ASSERT_FALSE(module.shouldExhaustHops(unicast));
|
|
|
|
meshtastic_MeshPacket fromUs = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kLocalNode, NODENUM_BROADCAST);
|
|
fromUs.hop_start = 5;
|
|
fromUs.hop_limit = 3;
|
|
module.alterReceived(fromUs);
|
|
TEST_ASSERT_EQUAL_UINT8(3, fromUs.hop_limit);
|
|
TEST_ASSERT_FALSE(module.shouldExhaustHops(fromUs));
|
|
|
|
meshtastic_TrafficManagementStats stats = module.getStats();
|
|
TEST_ASSERT_EQUAL_UINT32(0, stats.hop_exhausted_packets);
|
|
}
|
|
|
|
/**
|
|
* Verify position dedup window expires and later duplicates are allowed.
|
|
* Important so periodic identical reports can resume after cooldown.
|
|
*/
|
|
static void test_tm_positionDedup_allowsDuplicateAfterIntervalExpires(void)
|
|
{
|
|
moduleConfig.traffic_management.position_dedup_enabled = true;
|
|
moduleConfig.traffic_management.position_precision_bits = 16;
|
|
moduleConfig.traffic_management.position_min_interval_secs = 1;
|
|
TrafficManagementModuleTestShim module;
|
|
|
|
meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
|
meshtastic_MeshPacket second = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
|
meshtastic_MeshPacket third = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
|
|
|
ProcessMessage r1 = module.handleReceived(first);
|
|
ProcessMessage r2 = module.handleReceived(second);
|
|
testDelay(1200);
|
|
ProcessMessage r3 = module.handleReceived(third);
|
|
meshtastic_TrafficManagementStats stats = module.getStats();
|
|
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(r1));
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::STOP), static_cast<int>(r2));
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(r3));
|
|
TEST_ASSERT_EQUAL_UINT32(1, stats.position_dedup_drops);
|
|
}
|
|
|
|
/**
|
|
* Verify interval=0 disables position deduplication.
|
|
* Important because this is an explicit configuration escape hatch.
|
|
*/
|
|
static void test_tm_positionDedup_intervalZero_neverDrops(void)
|
|
{
|
|
moduleConfig.traffic_management.position_dedup_enabled = true;
|
|
moduleConfig.traffic_management.position_precision_bits = 16;
|
|
moduleConfig.traffic_management.position_min_interval_secs = 0;
|
|
TrafficManagementModuleTestShim module;
|
|
|
|
meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
|
meshtastic_MeshPacket second = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
|
|
|
ProcessMessage r1 = module.handleReceived(first);
|
|
ProcessMessage r2 = module.handleReceived(second);
|
|
meshtastic_TrafficManagementStats stats = module.getStats();
|
|
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(r1));
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(r2));
|
|
TEST_ASSERT_EQUAL_UINT32(0, stats.position_dedup_drops);
|
|
}
|
|
|
|
/**
|
|
* Verify precision values above 32 fall back to default precision.
|
|
* Important so invalid config uses the documented default behavior.
|
|
*/
|
|
static void test_tm_positionDedup_precisionAbove32_usesDefaultPrecision(void)
|
|
{
|
|
moduleConfig.traffic_management.position_dedup_enabled = true;
|
|
moduleConfig.traffic_management.position_precision_bits = 99;
|
|
moduleConfig.traffic_management.position_min_interval_secs = 300;
|
|
TrafficManagementModuleTestShim module;
|
|
|
|
meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
|
meshtastic_MeshPacket second = makePositionPacket(kRemoteNode, 384221234, -1210845678);
|
|
|
|
ProcessMessage r1 = module.handleReceived(first);
|
|
ProcessMessage r2 = module.handleReceived(second);
|
|
meshtastic_TrafficManagementStats stats = module.getStats();
|
|
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(r1));
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(r2));
|
|
TEST_ASSERT_EQUAL_UINT32(0, stats.position_dedup_drops);
|
|
}
|
|
|
|
/**
|
|
* Verify precision=32 does not collapse all positions to one fingerprint.
|
|
* Important to prevent false duplicate drops at the full-precision boundary.
|
|
*/
|
|
static void test_tm_positionDedup_precision32_allowsDistinctPositions(void)
|
|
{
|
|
moduleConfig.traffic_management.position_dedup_enabled = true;
|
|
moduleConfig.traffic_management.position_precision_bits = 32;
|
|
moduleConfig.traffic_management.position_min_interval_secs = 300;
|
|
TrafficManagementModuleTestShim module;
|
|
|
|
meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
|
meshtastic_MeshPacket second = makePositionPacket(kRemoteNode, 374221235, -1220845677);
|
|
|
|
ProcessMessage r1 = module.handleReceived(first);
|
|
ProcessMessage r2 = module.handleReceived(second);
|
|
meshtastic_TrafficManagementStats stats = module.getStats();
|
|
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(r1));
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(r2));
|
|
TEST_ASSERT_EQUAL_UINT32(0, stats.position_dedup_drops);
|
|
}
|
|
|
|
/**
|
|
* Verify precision=0 falls back to the default precision (same contract as
|
|
* >32: getConfiguredOrDefault + sanitizePositionPrecision treat 0 as unset).
|
|
* Important so invalid config does not collapse all positions into one
|
|
* fingerprint — positions in different default-precision grid cells must
|
|
* still be distinct.
|
|
*/
|
|
static void test_tm_positionDedup_precisionZero_allowsDistinctPositions(void)
|
|
{
|
|
moduleConfig.traffic_management.position_dedup_enabled = true;
|
|
moduleConfig.traffic_management.position_precision_bits = 0;
|
|
moduleConfig.traffic_management.position_min_interval_secs = 300;
|
|
TrafficManagementModuleTestShim module;
|
|
|
|
meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
|
meshtastic_MeshPacket second = makePositionPacket(kRemoteNode, 384221234, -1210845678);
|
|
|
|
ProcessMessage r1 = module.handleReceived(first);
|
|
ProcessMessage r2 = module.handleReceived(second);
|
|
meshtastic_TrafficManagementStats stats = module.getStats();
|
|
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(r1));
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(r2));
|
|
TEST_ASSERT_EQUAL_UINT32(0, stats.position_dedup_drops);
|
|
}
|
|
|
|
/**
|
|
* Verify epoch reset invalidates stale position identity for dedup.
|
|
* Important so reset paths cannot leak prior packet identity into new windows.
|
|
*/
|
|
static void test_tm_positionDedup_epochReset_doesNotDropFirstPacketAfterReset(void)
|
|
{
|
|
moduleConfig.traffic_management.position_dedup_enabled = true;
|
|
moduleConfig.traffic_management.position_precision_bits = 16;
|
|
moduleConfig.traffic_management.position_min_interval_secs = 300;
|
|
TrafficManagementModuleTestShim module;
|
|
|
|
meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
|
meshtastic_MeshPacket afterReset = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
|
meshtastic_MeshPacket duplicate = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
|
|
|
ProcessMessage r1 = module.handleReceived(first);
|
|
module.resetEpoch(millis());
|
|
ProcessMessage r2 = module.handleReceived(afterReset);
|
|
ProcessMessage r3 = module.handleReceived(duplicate);
|
|
meshtastic_TrafficManagementStats stats = module.getStats();
|
|
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(r1));
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(r2));
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::STOP), static_cast<int>(r3));
|
|
TEST_ASSERT_EQUAL_UINT32(1, stats.position_dedup_drops);
|
|
}
|
|
|
|
/**
|
|
* Verify non-position cache state does not make the first fingerprint-0 position look duplicated.
|
|
* Important so unified cache entries from other features cannot leak into dedup decisions.
|
|
*/
|
|
static void test_tm_positionDedup_priorRateState_doesNotDropFirstFingerprintZero(void)
|
|
{
|
|
moduleConfig.traffic_management.position_dedup_enabled = true;
|
|
moduleConfig.traffic_management.position_precision_bits = 16;
|
|
moduleConfig.traffic_management.position_min_interval_secs = 300;
|
|
moduleConfig.traffic_management.rate_limit_enabled = true;
|
|
moduleConfig.traffic_management.rate_limit_window_secs = 60;
|
|
moduleConfig.traffic_management.rate_limit_max_packets = 10;
|
|
TrafficManagementModuleTestShim module;
|
|
|
|
meshtastic_MeshPacket telemetry = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kRemoteNode);
|
|
meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 0x12300000, 0x45600000);
|
|
meshtastic_MeshPacket duplicate = makePositionPacket(kRemoteNode, 0x12300000, 0x45600000);
|
|
|
|
ProcessMessage seeded = module.handleReceived(telemetry);
|
|
ProcessMessage r1 = module.handleReceived(first);
|
|
ProcessMessage r2 = module.handleReceived(duplicate);
|
|
meshtastic_TrafficManagementStats stats = module.getStats();
|
|
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(seeded));
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(r1));
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::STOP), static_cast<int>(r2));
|
|
TEST_ASSERT_EQUAL_UINT32(1, stats.position_dedup_drops);
|
|
}
|
|
|
|
/**
|
|
* Verify rate-limit counters reset after the window expires.
|
|
* Important so temporary bursts do not cause persistent throttling.
|
|
*/
|
|
static void test_tm_rateLimit_resetsAfterWindowExpires(void)
|
|
{
|
|
moduleConfig.traffic_management.rate_limit_enabled = true;
|
|
moduleConfig.traffic_management.rate_limit_window_secs = 1;
|
|
moduleConfig.traffic_management.rate_limit_max_packets = 1;
|
|
TrafficManagementModuleTestShim module;
|
|
meshtastic_MeshPacket packet = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kRemoteNode);
|
|
|
|
ProcessMessage r1 = module.handleReceived(packet);
|
|
ProcessMessage r2 = module.handleReceived(packet);
|
|
testDelay(1200);
|
|
ProcessMessage r3 = module.handleReceived(packet);
|
|
meshtastic_TrafficManagementStats stats = module.getStats();
|
|
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(r1));
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::STOP), static_cast<int>(r2));
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(r3));
|
|
TEST_ASSERT_EQUAL_UINT32(1, stats.rate_limit_drops);
|
|
}
|
|
/**
|
|
* Verify unknown-packet tracking resets after its active window expires.
|
|
* Important so old unknown traffic does not trigger delayed drops.
|
|
*/
|
|
static void test_tm_unknownPackets_resetAfterWindowExpires(void)
|
|
{
|
|
moduleConfig.traffic_management.drop_unknown_enabled = true;
|
|
moduleConfig.traffic_management.unknown_packet_threshold = 1;
|
|
moduleConfig.traffic_management.rate_limit_window_secs = 1;
|
|
TrafficManagementModuleTestShim module;
|
|
meshtastic_MeshPacket packet = makeUnknownPacket(kRemoteNode);
|
|
|
|
ProcessMessage r1 = module.handleReceived(packet);
|
|
ProcessMessage r2 = module.handleReceived(packet);
|
|
testDelay(1200);
|
|
ProcessMessage r3 = module.handleReceived(packet);
|
|
meshtastic_TrafficManagementStats stats = module.getStats();
|
|
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(r1));
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::STOP), static_cast<int>(r2));
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(r3));
|
|
TEST_ASSERT_EQUAL_UINT32(1, stats.unknown_packet_drops);
|
|
}
|
|
|
|
/**
|
|
* Verify unknown threshold values above 255 clamp to the counter ceiling.
|
|
* Important to align config semantics with saturating counter storage.
|
|
*/
|
|
static void test_tm_unknownPackets_thresholdAbove255_clamps(void)
|
|
{
|
|
moduleConfig.traffic_management.drop_unknown_enabled = true;
|
|
moduleConfig.traffic_management.unknown_packet_threshold = 300;
|
|
TrafficManagementModuleTestShim module;
|
|
meshtastic_MeshPacket packet = makeUnknownPacket(kRemoteNode);
|
|
|
|
for (int i = 0; i < 255; i++) {
|
|
ProcessMessage result = module.handleReceived(packet);
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(result));
|
|
}
|
|
ProcessMessage dropped = module.handleReceived(packet);
|
|
meshtastic_TrafficManagementStats stats = module.getStats();
|
|
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::STOP), static_cast<int>(dropped));
|
|
TEST_ASSERT_EQUAL_UINT32(1, stats.unknown_packet_drops);
|
|
}
|
|
|
|
/**
|
|
* Verify relayed position broadcasts can also be hop-exhausted — under the
|
|
* same pressure gate as telemetry (here: channel congestion).
|
|
* Important because telemetry and position use separate exhaust flags.
|
|
*/
|
|
static void test_tm_alterReceived_exhaustsRelayedPositionBroadcast(void)
|
|
{
|
|
moduleConfig.traffic_management.exhaust_hop_position = true;
|
|
ScopedBusyAirTime busyChannel;
|
|
TrafficManagementModuleTestShim module;
|
|
meshtastic_MeshPacket packet = makePositionPacket(kRemoteNode, 374221234, -1220845678, NODENUM_BROADCAST);
|
|
packet.hop_start = 5;
|
|
packet.hop_limit = 2;
|
|
|
|
module.alterReceived(packet);
|
|
meshtastic_TrafficManagementStats stats = module.getStats();
|
|
|
|
TEST_ASSERT_EQUAL_UINT8(0, packet.hop_limit);
|
|
TEST_ASSERT_EQUAL_UINT8(4, packet.hop_start);
|
|
TEST_ASSERT_TRUE(module.shouldExhaustHops(packet));
|
|
TEST_ASSERT_EQUAL_UINT32(1, stats.hop_exhausted_packets);
|
|
}
|
|
/**
|
|
* Verify hop exhaustion ignores undecoded/encrypted packets.
|
|
* Important so we never mutate packets that were not decoded by this module.
|
|
*/
|
|
static void test_tm_alterReceived_skipsUndecodedPackets(void)
|
|
{
|
|
moduleConfig.traffic_management.exhaust_hop_telemetry = true;
|
|
ScopedBusyAirTime busyChannel; // congestion satisfied, so only the undecoded skip is under test
|
|
TrafficManagementModuleTestShim module;
|
|
meshtastic_MeshPacket packet = makeUnknownPacket(kRemoteNode, NODENUM_BROADCAST);
|
|
packet.hop_start = 5;
|
|
packet.hop_limit = 3;
|
|
|
|
module.alterReceived(packet);
|
|
meshtastic_TrafficManagementStats stats = module.getStats();
|
|
|
|
TEST_ASSERT_EQUAL_UINT8(5, packet.hop_start);
|
|
TEST_ASSERT_EQUAL_UINT8(3, packet.hop_limit);
|
|
TEST_ASSERT_FALSE(module.shouldExhaustHops(packet));
|
|
TEST_ASSERT_EQUAL_UINT32(0, stats.hop_exhausted_packets);
|
|
}
|
|
|
|
/**
|
|
* Verify exhaustRequested is per-packet and resets on next handleReceived().
|
|
* Important so a prior packet cannot leak hop-exhaust state into later packets.
|
|
*/
|
|
static void test_tm_alterReceived_resetExhaustFlagOnNextPacket(void)
|
|
{
|
|
moduleConfig.traffic_management.exhaust_hop_telemetry = true;
|
|
ScopedBusyAirTime busyChannel; // telemetry exhaust only fires under congestion
|
|
TrafficManagementModuleTestShim module;
|
|
|
|
meshtastic_MeshPacket telemetry = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kRemoteNode, NODENUM_BROADCAST);
|
|
telemetry.hop_start = 5;
|
|
telemetry.hop_limit = 3;
|
|
module.alterReceived(telemetry);
|
|
TEST_ASSERT_TRUE(module.shouldExhaustHops(telemetry));
|
|
|
|
meshtastic_MeshPacket text = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode);
|
|
ProcessMessage result = module.handleReceived(text);
|
|
meshtastic_TrafficManagementStats stats = module.getStats();
|
|
|
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(result));
|
|
TEST_ASSERT_FALSE(module.shouldExhaustHops(telemetry));
|
|
TEST_ASSERT_EQUAL_UINT32(1, stats.hop_exhausted_packets);
|
|
}
|
|
|
|
/**
|
|
* Verify exhaust requests are packet-scoped (from + id).
|
|
* Important so stale state from one packet cannot influence unrelated packets
|
|
* that pass through duplicate/rebroadcast paths before handleReceived().
|
|
*/
|
|
static void test_tm_alterReceived_exhaustFlag_isPacketScoped(void)
|
|
{
|
|
moduleConfig.traffic_management.exhaust_hop_telemetry = true;
|
|
ScopedBusyAirTime busyChannel; // telemetry exhaust only fires under congestion
|
|
TrafficManagementModuleTestShim module;
|
|
|
|
meshtastic_MeshPacket exhausted = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kRemoteNode, NODENUM_BROADCAST);
|
|
exhausted.id = 0x1010;
|
|
exhausted.hop_start = 5;
|
|
exhausted.hop_limit = 3;
|
|
module.alterReceived(exhausted);
|
|
|
|
meshtastic_MeshPacket unrelated = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kTargetNode, NODENUM_BROADCAST);
|
|
unrelated.id = 0x2020;
|
|
unrelated.hop_start = 4;
|
|
unrelated.hop_limit = 0;
|
|
|
|
TEST_ASSERT_TRUE(module.shouldExhaustHops(exhausted));
|
|
TEST_ASSERT_FALSE(module.shouldExhaustHops(unrelated));
|
|
}
|
|
|
|
/**
|
|
* Verify runOnce() returns sleep-forever interval when module is disabled.
|
|
* Important to ensure the maintenance thread is effectively inert when off.
|
|
*/
|
|
static void test_tm_runOnce_disabledReturnsMaxInterval(void)
|
|
{
|
|
moduleConfig.traffic_management.enabled = false;
|
|
TrafficManagementModuleTestShim module;
|
|
|
|
int32_t interval = module.runOnce();
|
|
|
|
TEST_ASSERT_EQUAL_INT32(INT32_MAX, interval);
|
|
}
|
|
|
|
/**
|
|
* Verify runOnce() returns the maintenance cadence when enabled.
|
|
* Important so periodic cache housekeeping continues at expected interval.
|
|
*/
|
|
static void test_tm_runOnce_enabledReturnsMaintenanceInterval(void)
|
|
{
|
|
TrafficManagementModuleTestShim module;
|
|
|
|
int32_t interval = module.runOnce();
|
|
|
|
TEST_ASSERT_EQUAL_INT32(60 * 1000, interval);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Next-hop overflow cache
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Round-trip set/get of a confirmed next hop, plus the input guards.
|
|
*/
|
|
static void test_tm_nextHop_setAndGetRoundTrip(void)
|
|
{
|
|
TrafficManagementModuleTestShim module;
|
|
|
|
// Unknown node yields no hint.
|
|
TEST_ASSERT_EQUAL_UINT8(0, module.getNextHopHint(kTargetNode));
|
|
|
|
// Store a confirmed hop and read it back.
|
|
module.setNextHop(kTargetNode, 0x42);
|
|
TEST_ASSERT_EQUAL_UINT8(0x42, module.getNextHopHint(kTargetNode));
|
|
|
|
// Zero dest and zero byte are rejected (no spurious entry created).
|
|
module.setNextHop(0, 0x42);
|
|
module.setNextHop(kRemoteNode, 0);
|
|
TEST_ASSERT_EQUAL_UINT8(0, module.getNextHopHint(kRemoteNode));
|
|
|
|
// Last-write-wins on re-confirmation.
|
|
module.setNextHop(kTargetNode, 0x99);
|
|
TEST_ASSERT_EQUAL_UINT8(0x99, module.getNextHopHint(kTargetNode));
|
|
}
|
|
|
|
/**
|
|
* The headline scenario: a node carrying a next hop in the hot NodeInfoLite DB
|
|
* is warm-loaded into the TMM cache, then the hot DB is "rolled" (the node ages
|
|
* out entirely). The hint must still be served — now exclusively from TMM.
|
|
*/
|
|
static void test_tm_nextHop_servedAfterNodeDbRoll(void)
|
|
{
|
|
TrafficManagementModuleTestShim module;
|
|
|
|
// Seed the hot NodeInfoLite DB with a node that has a confirmed next hop.
|
|
mockNodeDB->setHotNode(kTargetNode, 0x42);
|
|
|
|
// Warm-start the overflow cache from the hot DB.
|
|
module.preloadNextHopsFromNodeDB();
|
|
TEST_ASSERT_EQUAL_UINT8(0x42, module.getNextHopHint(kTargetNode));
|
|
|
|
// Roll the main NodeInfoLite DB: the node is evicted from the hot store.
|
|
mockNodeDB->rollHotStore();
|
|
TEST_ASSERT_NULL(nodeDB->getMeshNode(kTargetNode)); // gone from the hot store
|
|
|
|
// Hit is still served — proving it now comes from the TMM overflow cache.
|
|
TEST_ASSERT_EQUAL_UINT8(0x42, module.getNextHopHint(kTargetNode));
|
|
}
|
|
|
|
/**
|
|
* Preload must not clobber a freshly-learned (confirmed) hop with a possibly
|
|
* stale persisted one from NodeInfoLite.
|
|
*/
|
|
static void test_tm_nextHop_preloadDoesNotClobberLearned(void)
|
|
{
|
|
TrafficManagementModuleTestShim module;
|
|
|
|
// A fresher confirmed hop is already cached.
|
|
module.setNextHop(kTargetNode, 0x99);
|
|
|
|
// The hot DB carries an older next hop for the same node.
|
|
mockNodeDB->setHotNode(kTargetNode, 0x42);
|
|
|
|
module.preloadNextHopsFromNodeDB();
|
|
|
|
// The freshly-learned hop survives.
|
|
TEST_ASSERT_EQUAL_UINT8(0x99, module.getNextHopHint(kTargetNode));
|
|
}
|
|
|
|
/**
|
|
* A pure routing hint (no dedup/rate/unknown state) must survive the maintenance
|
|
* sweep — next_hop != 0 keeps the slot alive even though it has no TTL.
|
|
*/
|
|
static void test_tm_nextHop_keptAliveAcrossMaintenanceSweep(void)
|
|
{
|
|
TrafficManagementModuleTestShim module;
|
|
|
|
module.setNextHop(kTargetNode, 0x42);
|
|
|
|
// The sweep frees slots whose sub-stores are all empty; next_hop must veto that.
|
|
module.runOnce();
|
|
|
|
TEST_ASSERT_EQUAL_UINT8(0x42, module.getNextHopHint(kTargetNode));
|
|
}
|
|
} // namespace
|
|
|
|
void setUp(void)
|
|
{
|
|
resetTrafficConfig();
|
|
}
|
|
void tearDown(void) {}
|
|
|
|
TM_TEST_ENTRY void setup()
|
|
{
|
|
delay(10);
|
|
delay(2000);
|
|
|
|
initializeTestEnvironment();
|
|
mockNodeDB = new MockNodeDB();
|
|
nodeDB = mockNodeDB;
|
|
|
|
UNITY_BEGIN();
|
|
RUN_TEST(test_tm_moduleDisabled_doesNothing);
|
|
RUN_TEST(test_tm_unknownPackets_dropOnNPlusOne);
|
|
RUN_TEST(test_tm_positionDedup_dropsDuplicateWithinWindow);
|
|
RUN_TEST(test_tm_positionDedup_allowsMovedPosition);
|
|
RUN_TEST(test_tm_rateLimit_dropsOnlyAfterThreshold);
|
|
RUN_TEST(test_tm_fromUs_bypassesPositionAndRateFilters);
|
|
RUN_TEST(test_tm_localDestination_bypassesTransitFilters);
|
|
RUN_TEST(test_tm_nodeinfo_routerClamp_skipsWhenTooManyHops);
|
|
RUN_TEST(test_tm_nodeinfo_directResponse_respondsFromCache);
|
|
RUN_TEST(test_tm_nodeinfo_directResponse_learnsRequestorNodeInfo);
|
|
RUN_TEST(test_tm_nodeinfo_clientClamp_skipsWhenNotDirect);
|
|
#if !(defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM))
|
|
RUN_TEST(test_tm_nodeinfo_directResponse_withoutNodeDbEntry_skips);
|
|
#endif
|
|
#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM)
|
|
RUN_TEST(test_tm_nodeinfo_directResponse_psramCacheRespondsAndPreservesBitfield);
|
|
RUN_TEST(test_tm_nodeinfo_directResponse_psramMissDoesNotFallbackToNodeDb);
|
|
#endif
|
|
RUN_TEST(test_tm_alterReceived_exhaustsRelayedTelemetryBroadcast);
|
|
RUN_TEST(test_tm_alterReceived_skipsLocalAndUnicast);
|
|
RUN_TEST(test_tm_positionDedup_allowsDuplicateAfterIntervalExpires);
|
|
RUN_TEST(test_tm_positionDedup_intervalZero_neverDrops);
|
|
RUN_TEST(test_tm_positionDedup_precisionAbove32_usesDefaultPrecision);
|
|
RUN_TEST(test_tm_positionDedup_precision32_allowsDistinctPositions);
|
|
RUN_TEST(test_tm_positionDedup_precisionZero_allowsDistinctPositions);
|
|
RUN_TEST(test_tm_positionDedup_epochReset_doesNotDropFirstPacketAfterReset);
|
|
RUN_TEST(test_tm_positionDedup_priorRateState_doesNotDropFirstFingerprintZero);
|
|
RUN_TEST(test_tm_rateLimit_resetsAfterWindowExpires);
|
|
RUN_TEST(test_tm_unknownPackets_resetAfterWindowExpires);
|
|
RUN_TEST(test_tm_unknownPackets_thresholdAbove255_clamps);
|
|
RUN_TEST(test_tm_alterReceived_exhaustsRelayedPositionBroadcast);
|
|
RUN_TEST(test_tm_alterReceived_skipsUndecodedPackets);
|
|
RUN_TEST(test_tm_alterReceived_resetExhaustFlagOnNextPacket);
|
|
RUN_TEST(test_tm_alterReceived_exhaustFlag_isPacketScoped);
|
|
RUN_TEST(test_tm_runOnce_disabledReturnsMaxInterval);
|
|
RUN_TEST(test_tm_runOnce_enabledReturnsMaintenanceInterval);
|
|
RUN_TEST(test_tm_nextHop_setAndGetRoundTrip);
|
|
RUN_TEST(test_tm_nextHop_servedAfterNodeDbRoll);
|
|
RUN_TEST(test_tm_nextHop_preloadDoesNotClobberLearned);
|
|
RUN_TEST(test_tm_nextHop_keptAliveAcrossMaintenanceSweep);
|
|
exit(UNITY_END());
|
|
}
|
|
|
|
TM_TEST_ENTRY void loop() {}
|
|
|
|
#else
|
|
|
|
void setUp(void) {}
|
|
void tearDown(void) {}
|
|
|
|
TM_TEST_ENTRY void setup()
|
|
{
|
|
initializeTestEnvironment();
|
|
UNITY_BEGIN();
|
|
exit(UNITY_END());
|
|
}
|
|
|
|
TM_TEST_ENTRY void loop() {}
|
|
|
|
#endif
|