mirror of
https://github.com/meshtastic/firmware.git
synced 2026-09-13 14:57:13 -04:00
* test(native): add 14 suites for routing, persistence, parsing and identity gaps Coverage audit of the native test tree; adds the highest-value untested logic as 11 new suites and extends 3 existing ones (200 test functions). New: test_stream_framing, test_nodedb_boot_recovery, test_nodedb_legacy_migration, test_nodedb_v25_roundtrip, test_nodedb_identity_hygiene, test_channel_keys, test_reliable_ack_matrix, test_hop_start_policy, test_routing_response_hops, test_phone_api_config_dump, test_observer. Extended: test_rtc, test_mqtt, test_xmodem. Two source changes the audit produced: - StreamAPI::handleRecStream copied stream->read()'s `cInt < 0` EOF check into the buffer-fed path, where there is no EOF sentinel; with signed char any byte >= 0x80 (START1 is 0x94) aborted the parse. Read the byte as uint8_t directly. Latent on develop (no callers), pinned by test_stream_framing. - Extract the post-decode pre-hop predicate from Router::handleReceived into shouldSkipHandleForPostDecodeHop() (NodeDB.h) so test_hop_start_policy drives the exact expression the router calls. No behavior change. test/state-manifest.tsv declares the suites that construct a NodeDB. Full 68-suite Docker coverage run matches the pre-change baseline. * test(native): address review - harden observer dispatch, trim comments Review follow-ups on the coverage-audit suites: - Observable::notifyObservers() erased list nodes while holding an iterator into them, so an observer that unobserves itself from onNotify corrupted the dispatch. Today the only self-detacher (PhoneAPI::onNotify -> checkConnectionTimeout -> close -> unobserve) survives solely because it returns -1 and aborts the chain before the increment; that unwritten contract is now gone. Removal during a dispatch nulls the entry and the outermost notify sweeps afterwards, which keeps self-detach, next-detach and destruction-during-notify all safe without an allocation. Hoisting the next iterator instead would have inverted the hazard and broken the existing next-detach case. Two regression tests added. - Correct the documented caller of shouldSkipHandleForPostDecodeHop: the call is in Router::dispatchReceived, not handleReceived. - Cast hop fields to unsigned at the %u call site in test_hop_start_policy. - Trim the new suites' file headers to the one-or-two-line rule in AGENTS.md. - Rename eight test functions whose names were exactly `test_` + 35 chars: that is the shape of a Lob API key, so trufflehog flagged them as secrets and failed the Trunk CI check. Full 68-suite Docker coverage run matches the pre-change baseline. * test(native): revert the observer dispatch change, keep the contract test Backs out the notifyObservers() deferred-removal hardening from the previous commit. It was reviewer-driven scope creep: nothing in the coverage audit needed it, no test required it, and it changes dispatch semantics in a header with ~76 observe() call sites on native verification alone. The hazard it addressed is not reachable today. The only observer that unobserves itself from onNotify is PhoneAPI (onNotify -> checkConnectionTimeout -> close -> unobserve), and it returns -1, which aborts the chain before the iterator is advanced past the erased node. test_self_detach_with_abort_during_notify stays: it passes against the unmodified dispatch and pins that the -1 is load-bearing, so a later cleanup that "simplifies" it away goes red. The unsafe variant (self-detach returning 0) is documented in a comment rather than tested, since asserting it would be asserting UB. * fix(serial): recover the frame behind a stray framing marker A byte that failed the START2 check was discarded rather than re-tested as a possible START1, so 0x94 0x94 0xc3 ... lost the real frame: one corrupted byte on a noisy UART silently dropped the frame behind it. Re-test the byte in place instead. Applied to both copies of the receive state machine. readStream() is the one that matters in the field - it is the serial path every phone client uses - while handleRecStream() still has no callers on develop. Strictly widens what the parser accepts; no frame that parsed before parses differently. test_stream_framing covers it on both receive paths, plus a run of stray markers and a START1-then-unrelated-byte resync. This was originally documented as a known gap in the framing suite. Fixing it instead was NomDeTom's call on review: a passing test asserting the bad behavior is what makes it hard to change later, and it is the same defect shape as the signedness fix three functions away. Also: use Throttle::deadlinePassed() in test_reliable_ack_matrix rather than a bare millis() compare, matching the house deadline rule. * test(native): cover the stray-marker resync on the buffer path too The stray-marker fix went into both copies of the receive state machine, but only test_stray_start1_before_frame_still_delivers drove both. The repeated- marker and unrelated-byte cases drove readStream() alone, so a regression in handleRecStream() would have gone unnoticed by two of the three. Verified load-bearing: reverting only the handleRecStream() half of the fix turns test_repeated_stray_start1_before_frame_still_delivers red on the new assertion. test_start1_then_unrelated_byte_resyncs stays green under that mutation by design - its failing byte is 0x00, where both branches reset to 0 - and covers the other half of the ternary. Also drops the stale header on test_stray_start1_before_frame_still_delivers, which still described the gap as pinned-as-is after the fix landed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(native): make the hop-start truth table assert the rows it prints test_truth_table_summary was six TEST_MESSAGE lines and no assertion, so it reported as a case that could not fail - the anti-pattern #11517 names in its unfinished assertion-presence lint, and the one exception to NomDeTom's "no RUN_TEST without an assertion" pass over this PR. The printed row and the checked expectation now come from one struct, so the summary cannot narrate a table the predicates no longer implement. It also covers the consequence columns the per-row tests do not assert together: classifyHopStart, shouldDropPacketForPreHop and shouldSkipHandleForPostDecodeHop for the same packet, with the expectations gated on MESHTASTIC_PREHOP_DROP. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
854 lines
35 KiB
C++
854 lines
35 KiB
C++
// ReliableRouter ACK/NAK decision matrix: which ACK or NAK sniffReceived() emits per inbound
|
|
// shape, retransmission bookkeeping, the #11502 implicit ACK for our own overheard opaque DM
|
|
// (Group 5b drives the real OPAQUE_RELAY_ONLY ingress path), and the pending-timer extensions.
|
|
// Harness copied from test_nexthop_routing (ReliableRouterTestShim + MockRoutingModule).
|
|
|
|
#include "MeshTypes.h" // before TestUtil.h: provides NodeNum etc.
|
|
#include "TestUtil.h"
|
|
#include <unity.h>
|
|
|
|
#include "airtime.h"
|
|
#include "configuration.h"
|
|
#include "gps/RTC.h"
|
|
#include "mesh/Channels.h"
|
|
#include "mesh/NodeDB.h"
|
|
#include "mesh/RadioInterface.h"
|
|
#include "mesh/ReliableRouter.h"
|
|
#include "mesh/Throttle.h"
|
|
#include "modules/RoutingModule.h"
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <list>
|
|
#include <memory>
|
|
#include <tuple>
|
|
#include <vector>
|
|
|
|
static constexpr NodeNum kLocalNode = 0x11111111; // last byte 0x11
|
|
static constexpr NodeNum kRemoteNode = 0x22222222;
|
|
static constexpr NodeNum kThirdNode = 0x33333333;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// MockNodeDB - inject sender records with a controlled public-key size, so the PKI_UNKNOWN_PUBKEY
|
|
// vs NO_CHANNEL discrimination in sniffReceived() can be driven per test.
|
|
// ---------------------------------------------------------------------------
|
|
class MockNodeDB : public NodeDB
|
|
{
|
|
public:
|
|
void clearTestNodes()
|
|
{
|
|
testNodes.clear();
|
|
meshNodes = &testNodes;
|
|
numMeshNodes = 0;
|
|
}
|
|
|
|
void addNode(NodeNum num, uint8_t publicKeySize = 0)
|
|
{
|
|
meshtastic_NodeInfoLite node = meshtastic_NodeInfoLite_init_zero;
|
|
node.num = num;
|
|
node.last_heard = getTime();
|
|
node.public_key.size = publicKeySize;
|
|
if (publicKeySize)
|
|
memset(node.public_key.bytes, 0x5C, publicKeySize);
|
|
nodeInfoLiteSetBit(&node, NODEINFO_BITFIELD_HAS_USER_MASK, true);
|
|
testNodes.push_back(node);
|
|
meshNodes = &testNodes;
|
|
numMeshNodes = testNodes.size();
|
|
}
|
|
|
|
std::vector<meshtastic_NodeInfoLite> testNodes;
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Test shim - expose the protected sniff/filter entry points and the pending/route-health state.
|
|
// ---------------------------------------------------------------------------
|
|
class ReliableRouterTestShim : public ReliableRouter
|
|
{
|
|
public:
|
|
ReliableRouterTestShim() : ReliableRouter() {}
|
|
|
|
using NextHopRouter::findRouteHealth;
|
|
using NextHopRouter::noteRouteFailure;
|
|
using NextHopRouter::noteRouteLearned;
|
|
|
|
size_t pendingCount() const { return pending.size(); }
|
|
|
|
void seedRetry(const meshtastic_MeshPacket &p, uint8_t attempts)
|
|
{
|
|
auto *copy = packetPool.allocCopy(p);
|
|
TEST_ASSERT_NOT_NULL(copy);
|
|
startRetransmission(copy, attempts);
|
|
}
|
|
|
|
void sniffForTest(const meshtastic_MeshPacket *p, const meshtastic_Routing *routing)
|
|
{
|
|
ReliableRouter::sniffReceived(p, routing);
|
|
}
|
|
|
|
bool filterForTest(const meshtastic_MeshPacket *p) { return ReliableRouter::shouldFilterReceived(p); }
|
|
|
|
bool hasPending(NodeNum from, PacketId id) { return findPendingPacket(from, id) != nullptr; }
|
|
|
|
uint32_t pendingNextTx(NodeNum from, PacketId id)
|
|
{
|
|
PendingPacket *entry = findPendingPacket(from, id);
|
|
TEST_ASSERT_NOT_NULL(entry);
|
|
return entry->nextTxMsec;
|
|
}
|
|
|
|
const meshtastic_MeshPacket *pendingPacket(NodeNum from, PacketId id)
|
|
{
|
|
PendingPacket *entry = findPendingPacket(from, id);
|
|
TEST_ASSERT_NOT_NULL(entry);
|
|
return entry->packet;
|
|
}
|
|
|
|
void clearPendingForTest()
|
|
{
|
|
while (!pending.empty())
|
|
stopRetransmission(pending.begin()->first);
|
|
}
|
|
|
|
void resetRouteHealthForTest()
|
|
{
|
|
for (auto &h : routeHealth)
|
|
h = RouteHealth{};
|
|
}
|
|
};
|
|
|
|
// Capture radio with a configurable per-packet airtime, so the pending-timer extension loops
|
|
// (which are no-ops with a 0-returning stub) become observable.
|
|
class TimedCaptureRadio : public RadioInterface
|
|
{
|
|
public:
|
|
ErrorCode send(meshtastic_MeshPacket *p) override
|
|
{
|
|
sentPackets.push_back(*p);
|
|
packetPool.release(p);
|
|
return ERRNO_OK;
|
|
}
|
|
|
|
bool cancelSending(NodeNum from, PacketId id) override
|
|
{
|
|
(void)from;
|
|
(void)id;
|
|
cancelCount++;
|
|
return false;
|
|
}
|
|
|
|
bool findInTxQueue(NodeNum from, PacketId id) override
|
|
{
|
|
(void)from;
|
|
(void)id;
|
|
return false;
|
|
}
|
|
|
|
uint32_t getPacketTime(uint32_t totalPacketLen, bool received = false) override
|
|
{
|
|
(void)totalPacketLen;
|
|
(void)received;
|
|
return packetTimeMsec;
|
|
}
|
|
|
|
void reset()
|
|
{
|
|
sentPackets.clear();
|
|
cancelCount = 0;
|
|
packetTimeMsec = 0;
|
|
}
|
|
|
|
std::vector<meshtastic_MeshPacket> sentPackets;
|
|
uint32_t cancelCount = 0;
|
|
uint32_t packetTimeMsec = 0;
|
|
};
|
|
|
|
class MockRoutingModule : public RoutingModule
|
|
{
|
|
public:
|
|
void sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit = 0,
|
|
bool ackWantsAck = false) override
|
|
{
|
|
ackNaks.emplace_back(err, to, idFrom, chIndex, hopLimit, ackWantsAck);
|
|
}
|
|
|
|
std::list<std::tuple<meshtastic_Routing_Error, NodeNum, PacketId, ChannelIndex, uint8_t, bool>> ackNaks;
|
|
};
|
|
|
|
class ScopedAirTimeFixture
|
|
{
|
|
public:
|
|
ScopedAirTimeFixture() : previous(airTime) { airTime = &instance; }
|
|
~ScopedAirTimeFixture() { airTime = previous; }
|
|
|
|
private:
|
|
AirTime instance;
|
|
AirTime *previous;
|
|
};
|
|
|
|
static MockNodeDB *mockNodeDB = nullptr;
|
|
static ReliableRouterTestShim *reliableShim = nullptr;
|
|
static TimedCaptureRadio *radio = nullptr;
|
|
static MockRoutingModule *mockRoutingModule = nullptr;
|
|
static std::unique_ptr<ScopedAirTimeFixture> airTimeFixture;
|
|
static PacketId nextTestPacketId = 0x7A000000;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Packet builders
|
|
// ---------------------------------------------------------------------------
|
|
|
|
static meshtastic_MeshPacket makeDecodedPacket(meshtastic_PortNum portnum, NodeNum from, NodeNum to, uint8_t channel,
|
|
bool wantAck = false)
|
|
{
|
|
meshtastic_MeshPacket p = meshtastic_MeshPacket_init_zero;
|
|
p.from = from;
|
|
p.to = to;
|
|
p.id = nextTestPacketId++;
|
|
p.channel = channel;
|
|
p.hop_start = 3;
|
|
p.hop_limit = 3; // hop_start == hop_limit -> getHopsAway() == 0 ("heard directly")
|
|
p.relay_node = 0x22;
|
|
p.next_hop = NO_NEXT_HOP_PREFERENCE;
|
|
p.want_ack = wantAck;
|
|
p.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA;
|
|
p.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
|
p.decoded.portnum = portnum;
|
|
return p;
|
|
}
|
|
|
|
static meshtastic_MeshPacket makeEncryptedToUs(uint8_t channel, bool wantAck)
|
|
{
|
|
meshtastic_MeshPacket p = meshtastic_MeshPacket_init_zero;
|
|
p.from = kRemoteNode;
|
|
p.to = kLocalNode;
|
|
p.id = nextTestPacketId++;
|
|
p.channel = channel;
|
|
p.hop_start = 3;
|
|
p.hop_limit = 3;
|
|
p.relay_node = 0x22;
|
|
p.next_hop = NO_NEXT_HOP_PREFERENCE;
|
|
p.want_ack = wantAck;
|
|
p.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA;
|
|
p.which_payload_variant = meshtastic_MeshPacket_encrypted_tag;
|
|
p.encrypted.size = 32;
|
|
return p;
|
|
}
|
|
|
|
static void expectSingleAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId id, ChannelIndex chIndex, uint8_t hopLimit,
|
|
bool ackWantsAck)
|
|
{
|
|
TEST_ASSERT_EQUAL_UINT32(1, mockRoutingModule->ackNaks.size());
|
|
const auto &ack = mockRoutingModule->ackNaks.front();
|
|
TEST_ASSERT_EQUAL_INT(err, std::get<0>(ack));
|
|
TEST_ASSERT_EQUAL_HEX32(to, std::get<1>(ack));
|
|
TEST_ASSERT_EQUAL_HEX32(id, std::get<2>(ack));
|
|
TEST_ASSERT_EQUAL_UINT8(chIndex, std::get<3>(ack));
|
|
TEST_ASSERT_EQUAL_UINT8(hopLimit, std::get<4>(ack));
|
|
TEST_ASSERT_EQUAL(ackWantsAck, std::get<5>(ack));
|
|
}
|
|
|
|
static void configureChannels()
|
|
{
|
|
memset(&channelFile, 0, sizeof(channelFile));
|
|
channelFile.channels_count = 2;
|
|
|
|
meshtastic_Channel primary = meshtastic_Channel_init_default;
|
|
primary.index = 0;
|
|
primary.has_settings = true;
|
|
primary.role = meshtastic_Channel_Role_PRIMARY;
|
|
strncpy(primary.settings.name, "primary", sizeof(primary.settings.name) - 1);
|
|
|
|
meshtastic_Channel secondary = meshtastic_Channel_init_default;
|
|
secondary.index = 1;
|
|
secondary.has_settings = true;
|
|
secondary.role = meshtastic_Channel_Role_SECONDARY;
|
|
strncpy(secondary.settings.name, "second", sizeof(secondary.settings.name) - 1);
|
|
secondary.settings.psk.size = 32;
|
|
memset(secondary.settings.psk.bytes, 0xAB, secondary.settings.psk.size);
|
|
|
|
channelFile.channels[0] = primary;
|
|
channelFile.channels[1] = secondary;
|
|
channels.onConfigChanged();
|
|
}
|
|
|
|
void setUp(void)
|
|
{
|
|
myNodeInfo.my_node_num = kLocalNode;
|
|
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT;
|
|
config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_ALL;
|
|
config.lora.override_duty_cycle = true;
|
|
config.lora.hop_limit = 3; // keep getHopLimitForResponse() deterministic across tests
|
|
config.security.private_key.size = 0;
|
|
owner.is_licensed = false;
|
|
// Keep our own key unset: the PKI_UNKNOWN_PUBKEY NAK handler dereferences nodeInfoModule (a null
|
|
// global here) only when owner.public_key.size == 32.
|
|
owner.public_key.size = 0;
|
|
mockNodeDB->clearTestNodes();
|
|
reliableShim->clearPendingForTest();
|
|
reliableShim->resetRouteHealthForTest();
|
|
radio->reset();
|
|
mockRoutingModule->ackNaks.clear();
|
|
configureChannels();
|
|
}
|
|
|
|
void tearDown(void) {}
|
|
|
|
// ===========================================================================
|
|
// Group 1 - want_ack ACK variants (decoded packets to us)
|
|
// ===========================================================================
|
|
|
|
void test_text_dm_want_ack_gets_want_ack_ack(void)
|
|
{
|
|
auto p = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode, kLocalNode, 1, /*wantAck=*/true);
|
|
uint8_t expectedHop = mockRoutingModule->getHopLimitForResponse(p);
|
|
TEST_ASSERT_NOT_EQUAL(0, expectedHop); // must be distinguishable from the 0-hop ACK branch
|
|
|
|
reliableShim->sniffForTest(&p, nullptr);
|
|
|
|
expectSingleAckNak(meshtastic_Routing_Error_NONE, kRemoteNode, p.id, 1, expectedHop, /*ackWantsAck=*/true);
|
|
}
|
|
|
|
void test_text_reply_still_gets_want_ack_ack(void)
|
|
{
|
|
// shouldSuccessAckWithWantAck() runs before the response branch, so a text DM that is itself a
|
|
// reply still gets the reliable want-ack ACK (not the 0-hop response treatment).
|
|
auto p = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode, kLocalNode, 1, /*wantAck=*/true);
|
|
p.decoded.reply_id = 0x1234;
|
|
uint8_t expectedHop = mockRoutingModule->getHopLimitForResponse(p);
|
|
|
|
reliableShim->sniffForTest(&p, nullptr);
|
|
|
|
expectSingleAckNak(meshtastic_Routing_Error_NONE, kRemoteNode, p.id, 1, expectedHop, /*ackWantsAck=*/true);
|
|
}
|
|
|
|
void test_nontext_dm_want_ack_gets_plain_ack(void)
|
|
{
|
|
auto p = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kRemoteNode, kLocalNode, 1, /*wantAck=*/true);
|
|
uint8_t expectedHop = mockRoutingModule->getHopLimitForResponse(p);
|
|
TEST_ASSERT_NOT_EQUAL(0, expectedHop);
|
|
|
|
reliableShim->sniffForTest(&p, nullptr);
|
|
|
|
expectSingleAckNak(meshtastic_Routing_Error_NONE, kRemoteNode, p.id, 1, expectedHop, /*ackWantsAck=*/false);
|
|
}
|
|
|
|
void test_response_heard_directly_gets_zero_hop_ack(void)
|
|
{
|
|
// A response (request_id set) heard at 0 hops: the original sender cannot overhear an implicit
|
|
// ACK, so we ACK - but only with hop limit 0.
|
|
auto p = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kRemoteNode, kLocalNode, 1, /*wantAck=*/true);
|
|
p.decoded.request_id = 0x4242;
|
|
|
|
reliableShim->sniffForTest(&p, nullptr);
|
|
|
|
expectSingleAckNak(meshtastic_Routing_Error_NONE, kRemoteNode, p.id, 1, /*hopLimit=*/0, /*ackWantsAck=*/false);
|
|
}
|
|
|
|
void test_response_relayed_gets_no_ack(void)
|
|
{
|
|
// A relayed response with no next-hop addressing already got its implicit ACK from the
|
|
// rebroadcast; ACKing again would only burn airtime.
|
|
auto p = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kRemoteNode, kLocalNode, 1, /*wantAck=*/true);
|
|
p.decoded.request_id = 0x4242;
|
|
p.hop_limit = 2; // hop_start 3 -> 1 hop away
|
|
|
|
reliableShim->sniffForTest(&p, nullptr);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRoutingModule->ackNaks.size());
|
|
}
|
|
|
|
void test_response_relayed_via_next_hop_gets_zero_hop_ack(void)
|
|
{
|
|
// Relayed, but directed at a next_hop: the immediate relayer retransmits until stopped, so a
|
|
// 0-hop ACK is still required.
|
|
auto p = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kRemoteNode, kLocalNode, 1, /*wantAck=*/true);
|
|
p.decoded.request_id = 0x4242;
|
|
p.hop_limit = 2;
|
|
p.next_hop = 0x77;
|
|
|
|
reliableShim->sniffForTest(&p, nullptr);
|
|
|
|
expectSingleAckNak(meshtastic_Routing_Error_NONE, kRemoteNode, p.id, 1, /*hopLimit=*/0, /*ackWantsAck=*/false);
|
|
}
|
|
|
|
void test_broadcast_want_ack_gets_no_ack(void)
|
|
{
|
|
// 0-hop reliability is unicast-only: a want_ack broadcast is never ACKed (isToUs() is false).
|
|
auto p = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode, NODENUM_BROADCAST, 0, /*wantAck=*/true);
|
|
|
|
reliableShim->sniffForTest(&p, nullptr);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRoutingModule->ackNaks.size());
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Group 2 - undecodable want_ack NAKs (encrypted packets to us)
|
|
// ===========================================================================
|
|
|
|
void test_pki_unknown_sender_gets_pki_unknown_pubkey_nak(void)
|
|
{
|
|
// channel==0 + sender absent from NodeDB -> the PKI key-amnesia NAK, on the primary channel.
|
|
auto p = makeEncryptedToUs(/*channel=*/0, /*wantAck=*/true);
|
|
uint8_t expectedHop = mockRoutingModule->getHopLimitForResponse(p);
|
|
|
|
reliableShim->sniffForTest(&p, nullptr);
|
|
|
|
expectSingleAckNak(meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY, kRemoteNode, p.id, channels.getPrimaryIndex(), expectedHop,
|
|
/*ackWantsAck=*/false);
|
|
}
|
|
|
|
void test_pki_keyless_sender_record_gets_pki_unknown_pubkey_nak(void)
|
|
{
|
|
// The sender is in the DB but we hold no key for it - same NAK as a fully unknown node.
|
|
mockNodeDB->addNode(kRemoteNode, /*publicKeySize=*/0);
|
|
auto p = makeEncryptedToUs(/*channel=*/0, /*wantAck=*/true);
|
|
uint8_t expectedHop = mockRoutingModule->getHopLimitForResponse(p);
|
|
|
|
reliableShim->sniffForTest(&p, nullptr);
|
|
|
|
expectSingleAckNak(meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY, kRemoteNode, p.id, channels.getPrimaryIndex(), expectedHop,
|
|
/*ackWantsAck=*/false);
|
|
}
|
|
|
|
void test_pki_known_key_sender_gets_no_channel_nak(void)
|
|
{
|
|
// Discriminator: with the sender's key on hand an undecodable channel-0 want_ack packet is NOT a
|
|
// key problem, so it falls through to the generic NO_CHANNEL NAK.
|
|
mockNodeDB->addNode(kRemoteNode, /*publicKeySize=*/32);
|
|
auto p = makeEncryptedToUs(/*channel=*/0, /*wantAck=*/true);
|
|
uint8_t expectedHop = mockRoutingModule->getHopLimitForResponse(p);
|
|
|
|
reliableShim->sniffForTest(&p, nullptr);
|
|
|
|
expectSingleAckNak(meshtastic_Routing_Error_NO_CHANNEL, kRemoteNode, p.id, channels.getPrimaryIndex(), expectedHop,
|
|
/*ackWantsAck=*/false);
|
|
}
|
|
|
|
void test_unknown_channel_hash_gets_no_channel_nak(void)
|
|
{
|
|
// Nonzero channel hash we cannot decode -> NO_CHANNEL on the primary channel (not the hash).
|
|
auto p = makeEncryptedToUs(/*channel=*/0x5A, /*wantAck=*/true);
|
|
uint8_t expectedHop = mockRoutingModule->getHopLimitForResponse(p);
|
|
|
|
reliableShim->sniffForTest(&p, nullptr);
|
|
|
|
expectSingleAckNak(meshtastic_Routing_Error_NO_CHANNEL, kRemoteNode, p.id, channels.getPrimaryIndex(), expectedHop,
|
|
/*ackWantsAck=*/false);
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Group 3 - no want_ack, but we are the addressed next hop
|
|
// ===========================================================================
|
|
|
|
void test_next_hop_addressed_to_us_gets_zero_hop_ack(void)
|
|
{
|
|
// We were the addressed next hop: a 0-hop ACK stops the relayer's retransmissions even though
|
|
// the packet itself did not ask for an ACK.
|
|
auto p = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kRemoteNode, kLocalNode, 1, /*wantAck=*/false);
|
|
p.next_hop = 0x11; // our last byte
|
|
p.hop_limit = 1;
|
|
|
|
reliableShim->sniffForTest(&p, nullptr);
|
|
|
|
expectSingleAckNak(meshtastic_Routing_Error_NONE, kRemoteNode, p.id, 1, /*hopLimit=*/0, /*ackWantsAck=*/false);
|
|
}
|
|
|
|
void test_next_hop_with_hop_limit_zero_gets_no_ack(void)
|
|
{
|
|
auto p = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kRemoteNode, kLocalNode, 1, /*wantAck=*/false);
|
|
p.next_hop = 0x11;
|
|
p.hop_limit = 0;
|
|
|
|
reliableShim->sniffForTest(&p, nullptr);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRoutingModule->ackNaks.size());
|
|
}
|
|
|
|
void test_next_hop_other_byte_gets_no_ack(void)
|
|
{
|
|
auto p = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kRemoteNode, kLocalNode, 1, /*wantAck=*/false);
|
|
p.next_hop = 0x22; // someone else's byte
|
|
p.hop_limit = 1;
|
|
|
|
reliableShim->sniffForTest(&p, nullptr);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRoutingModule->ackNaks.size());
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Group 4 - explicit ACK/NAK vs pending retransmissions, MQTT gate, route health
|
|
// ===========================================================================
|
|
|
|
void test_explicit_ack_stops_retransmissions_and_clears_route_failures(void)
|
|
{
|
|
auto original = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kLocalNode, kRemoteNode, 1, /*wantAck=*/true);
|
|
reliableShim->seedRetry(original, NextHopRouter::NUM_RELIABLE_UNICAST_ATTEMPTS);
|
|
reliableShim->noteRouteLearned(kRemoteNode, 0xAB, millis());
|
|
reliableShim->noteRouteFailure(kRemoteNode);
|
|
reliableShim->noteRouteFailure(kRemoteNode);
|
|
TEST_ASSERT_EQUAL_UINT32(1, reliableShim->pendingCount());
|
|
|
|
auto ack = makeDecodedPacket(meshtastic_PortNum_ROUTING_APP, kRemoteNode, kLocalNode, 1);
|
|
ack.decoded.request_id = original.id;
|
|
meshtastic_Routing routing = meshtastic_Routing_init_zero;
|
|
routing.error_reason = meshtastic_Routing_Error_NONE;
|
|
|
|
reliableShim->sniffForTest(&ack, &routing);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(0, reliableShim->pendingCount());
|
|
// The end-to-end ACK proves the route to its sender works -> noteRouteSuccess clears failures.
|
|
RouteHealth *h = reliableShim->findRouteHealth(kRemoteNode);
|
|
TEST_ASSERT_NOT_NULL(h);
|
|
TEST_ASSERT_EQUAL_UINT8(0, h->consecutiveFailures);
|
|
}
|
|
|
|
void test_nak_stops_retransmissions_but_keeps_route_failures(void)
|
|
{
|
|
auto original = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kLocalNode, kRemoteNode, 1, /*wantAck=*/true);
|
|
reliableShim->seedRetry(original, NextHopRouter::NUM_RELIABLE_UNICAST_ATTEMPTS);
|
|
reliableShim->noteRouteLearned(kRemoteNode, 0xAB, millis());
|
|
reliableShim->noteRouteFailure(kRemoteNode);
|
|
reliableShim->noteRouteFailure(kRemoteNode);
|
|
|
|
auto nak = makeDecodedPacket(meshtastic_PortNum_ROUTING_APP, kRemoteNode, kLocalNode, 1);
|
|
nak.decoded.request_id = original.id;
|
|
meshtastic_Routing routing = meshtastic_Routing_init_zero;
|
|
routing.error_reason = meshtastic_Routing_Error_MAX_RETRANSMIT;
|
|
|
|
reliableShim->sniffForTest(&nak, &routing);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(0, reliableShim->pendingCount());
|
|
// A NAK is not a delivery success: the failure count must survive.
|
|
RouteHealth *h = reliableShim->findRouteHealth(kRemoteNode);
|
|
TEST_ASSERT_NOT_NULL(h);
|
|
TEST_ASSERT_EQUAL_UINT8(2, h->consecutiveFailures);
|
|
}
|
|
|
|
void test_pki_unknown_pubkey_nak_stops_retransmissions(void)
|
|
{
|
|
// The remote lost our key: its PKI_UNKNOWN_PUBKEY NAK must still clear the pending record.
|
|
// owner.public_key.size == 0 (setUp) keeps the NodeInfo re-send branch (a nodeInfoModule
|
|
// dereference, null in this harness) out of the path.
|
|
auto original = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kLocalNode, kRemoteNode, 1, /*wantAck=*/true);
|
|
reliableShim->seedRetry(original, NextHopRouter::NUM_RELIABLE_UNICAST_ATTEMPTS);
|
|
|
|
auto nak = makeDecodedPacket(meshtastic_PortNum_ROUTING_APP, kRemoteNode, kLocalNode, 1);
|
|
nak.decoded.request_id = original.id;
|
|
meshtastic_Routing routing = meshtastic_Routing_init_zero;
|
|
routing.error_reason = meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY;
|
|
|
|
reliableShim->sniffForTest(&nak, &routing);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(0, reliableShim->pendingCount());
|
|
}
|
|
|
|
void test_own_ack_echo_via_mqtt_keeps_retransmissions(void)
|
|
{
|
|
// An implicit ACK that is our own traffic echoed back via MQTT must not stop LoRa retries.
|
|
auto original = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kLocalNode, kRemoteNode, 1, /*wantAck=*/true);
|
|
reliableShim->seedRetry(original, NextHopRouter::NUM_RELIABLE_UNICAST_ATTEMPTS);
|
|
|
|
auto echo = makeDecodedPacket(meshtastic_PortNum_ROUTING_APP, kLocalNode, kLocalNode, 1);
|
|
echo.decoded.request_id = original.id;
|
|
echo.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MQTT;
|
|
|
|
reliableShim->sniffForTest(&echo, nullptr);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(1, reliableShim->pendingCount());
|
|
TEST_ASSERT_TRUE(reliableShim->hasPending(kLocalNode, original.id));
|
|
}
|
|
|
|
void test_own_ack_echo_via_lora_stops_retransmissions(void)
|
|
{
|
|
// Control for the MQTT gate: the identical from-us echo via LoRa does stop the retries.
|
|
auto original = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kLocalNode, kRemoteNode, 1, /*wantAck=*/true);
|
|
reliableShim->seedRetry(original, NextHopRouter::NUM_RELIABLE_UNICAST_ATTEMPTS);
|
|
|
|
auto echo = makeDecodedPacket(meshtastic_PortNum_ROUTING_APP, kLocalNode, kLocalNode, 1);
|
|
echo.decoded.request_id = original.id;
|
|
echo.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA;
|
|
|
|
reliableShim->sniffForTest(&echo, nullptr);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(0, reliableShim->pendingCount());
|
|
}
|
|
|
|
void test_remote_ack_via_mqtt_still_stops_retransmissions(void)
|
|
{
|
|
// The gate is scoped to from-us echoes: a genuine end-to-end ACK arriving over MQTT counts.
|
|
auto original = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kLocalNode, kRemoteNode, 1, /*wantAck=*/true);
|
|
reliableShim->seedRetry(original, NextHopRouter::NUM_RELIABLE_UNICAST_ATTEMPTS);
|
|
|
|
auto ack = makeDecodedPacket(meshtastic_PortNum_ROUTING_APP, kRemoteNode, kLocalNode, 1);
|
|
ack.decoded.request_id = original.id;
|
|
ack.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MQTT;
|
|
meshtastic_Routing routing = meshtastic_Routing_init_zero;
|
|
routing.error_reason = meshtastic_Routing_Error_NONE;
|
|
|
|
reliableShim->sniffForTest(&ack, &routing);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(0, reliableShim->pendingCount());
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Group 5 - implicit ACK for our own overheard DM through shouldFilterReceived. This is the
|
|
// pre-existing route (a decodable copy still in encrypted wire form reaches it); the #11502
|
|
// opaque short-circuit is exercised separately in Group 5b.
|
|
// ===========================================================================
|
|
|
|
void test_overheard_own_dm_rebroadcast_mints_implicit_ack(void)
|
|
{
|
|
// The implicit ACK is minted from the header alone (from/id), so this route must work on a
|
|
// still-encrypted packet, and the LoRa copy stops the retransmissions.
|
|
auto original = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kLocalNode, kRemoteNode, 1, /*wantAck=*/true);
|
|
reliableShim->seedRetry(original, NextHopRouter::NUM_RELIABLE_UNICAST_ATTEMPTS);
|
|
|
|
meshtastic_MeshPacket overheard = meshtastic_MeshPacket_init_zero;
|
|
overheard.from = kLocalNode;
|
|
overheard.to = kRemoteNode;
|
|
overheard.id = original.id;
|
|
overheard.hop_start = 3;
|
|
overheard.hop_limit = 2;
|
|
overheard.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA;
|
|
overheard.which_payload_variant = meshtastic_MeshPacket_encrypted_tag;
|
|
overheard.encrypted.size = 32;
|
|
|
|
reliableShim->filterForTest(&overheard);
|
|
|
|
// ACK is addressed to us (so it reaches the phone) on the pending copy's channel.
|
|
expectSingleAckNak(meshtastic_Routing_Error_NONE, kLocalNode, original.id, 1, /*hopLimit=*/0, /*ackWantsAck=*/false);
|
|
TEST_ASSERT_EQUAL_UINT32(0, reliableShim->pendingCount());
|
|
}
|
|
|
|
void test_overheard_own_dm_via_mqtt_acks_but_keeps_retransmissions(void)
|
|
{
|
|
// The MQTT copy still surfaces "Delivered to mesh" but must not cancel the LoRa retries.
|
|
auto original = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kLocalNode, kRemoteNode, 1, /*wantAck=*/true);
|
|
reliableShim->seedRetry(original, NextHopRouter::NUM_RELIABLE_UNICAST_ATTEMPTS);
|
|
|
|
meshtastic_MeshPacket overheard = meshtastic_MeshPacket_init_zero;
|
|
overheard.from = kLocalNode;
|
|
overheard.to = kRemoteNode;
|
|
overheard.id = original.id;
|
|
overheard.hop_start = 3;
|
|
overheard.hop_limit = 2;
|
|
overheard.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MQTT;
|
|
overheard.which_payload_variant = meshtastic_MeshPacket_encrypted_tag;
|
|
overheard.encrypted.size = 32;
|
|
|
|
reliableShim->filterForTest(&overheard);
|
|
|
|
expectSingleAckNak(meshtastic_Routing_Error_NONE, kLocalNode, original.id, 1, /*hopLimit=*/0, /*ackWantsAck=*/false);
|
|
TEST_ASSERT_EQUAL_UINT32(1, reliableShim->pendingCount());
|
|
}
|
|
|
|
void test_overheard_foreign_packet_mints_no_implicit_ack(void)
|
|
{
|
|
// Someone else's traffic must never mint an ACK, even with a colliding packet id.
|
|
auto original = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kLocalNode, kRemoteNode, 1, /*wantAck=*/true);
|
|
reliableShim->seedRetry(original, NextHopRouter::NUM_RELIABLE_UNICAST_ATTEMPTS);
|
|
|
|
meshtastic_MeshPacket foreign = meshtastic_MeshPacket_init_zero;
|
|
foreign.from = kRemoteNode;
|
|
foreign.to = kThirdNode;
|
|
foreign.id = original.id;
|
|
foreign.hop_start = 3;
|
|
foreign.hop_limit = 2;
|
|
foreign.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA;
|
|
foreign.which_payload_variant = meshtastic_MeshPacket_encrypted_tag;
|
|
foreign.encrypted.size = 32;
|
|
|
|
reliableShim->filterForTest(&foreign);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRoutingModule->ackNaks.size());
|
|
TEST_ASSERT_EQUAL_UINT32(1, reliableShim->pendingCount());
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Group 5b - the real #11502 wiring: an overheard own DM under a channel hash we cannot decode
|
|
// (a PKI DM we sent) is OPAQUE_RELAY_ONLY in Router::perhapsHandleReceived and returns BEFORE
|
|
// shouldFilterReceived; the fix is the isFromUs branch there. Driven through the public ingress
|
|
// queue (enqueueReceivedMessage + runOnce), so deleting that branch fails these tests.
|
|
// ===========================================================================
|
|
|
|
// An encrypted copy of our own DM under an unknown channel hash: not to us (no PKI attempt), no
|
|
// hash match -> DECODE_OPAQUE -> OPAQUE_RELAY_ONLY. hop_limit > 0 so the opaque relay does not
|
|
// short-circuit before the ACK branch.
|
|
static meshtastic_MeshPacket makeOpaqueOwnOverheard(PacketId id, meshtastic_MeshPacket_TransportMechanism transport)
|
|
{
|
|
meshtastic_MeshPacket p = meshtastic_MeshPacket_init_zero;
|
|
p.from = kLocalNode;
|
|
p.to = kRemoteNode;
|
|
p.id = id;
|
|
p.channel = 0x5A;
|
|
p.hop_start = 3;
|
|
p.hop_limit = 2;
|
|
p.transport_mechanism = transport;
|
|
p.which_payload_variant = meshtastic_MeshPacket_encrypted_tag;
|
|
p.encrypted.size = 32;
|
|
memset(p.encrypted.bytes, 0xC3, p.encrypted.size);
|
|
return p;
|
|
}
|
|
|
|
static void ingressOverheard(const meshtastic_MeshPacket &p)
|
|
{
|
|
meshtastic_MeshPacket *copy = packetPool.allocCopy(p);
|
|
TEST_ASSERT_NOT_NULL(copy);
|
|
reliableShim->enqueueReceivedMessage(copy);
|
|
reliableShim->runOnce();
|
|
}
|
|
|
|
void test_ingress_opaque_own_dm_lora_mints_implicit_ack_and_stops_retries(void)
|
|
{
|
|
auto original = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kLocalNode, kRemoteNode, 1, /*wantAck=*/true);
|
|
reliableShim->seedRetry(original, NextHopRouter::NUM_RELIABLE_UNICAST_ATTEMPTS);
|
|
|
|
ingressOverheard(makeOpaqueOwnOverheard(original.id, meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA));
|
|
|
|
expectSingleAckNak(meshtastic_Routing_Error_NONE, kLocalNode, original.id, 1, /*hopLimit=*/0, /*ackWantsAck=*/false);
|
|
TEST_ASSERT_EQUAL_UINT32(0, reliableShim->pendingCount());
|
|
}
|
|
|
|
void test_ingress_opaque_own_dm_mqtt_acks_but_keeps_retries(void)
|
|
{
|
|
auto original = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kLocalNode, kRemoteNode, 1, /*wantAck=*/true);
|
|
reliableShim->seedRetry(original, NextHopRouter::NUM_RELIABLE_UNICAST_ATTEMPTS);
|
|
|
|
ingressOverheard(makeOpaqueOwnOverheard(original.id, meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MQTT));
|
|
|
|
expectSingleAckNak(meshtastic_Routing_Error_NONE, kLocalNode, original.id, 1, /*hopLimit=*/0, /*ackWantsAck=*/false);
|
|
TEST_ASSERT_EQUAL_UINT32(1, reliableShim->pendingCount());
|
|
}
|
|
|
|
void test_ingress_opaque_foreign_packet_mints_no_implicit_ack(void)
|
|
{
|
|
// The isFromUs guard on the opaque branch: someone else's opaque traffic with a colliding id
|
|
// is relayed but never ACKed.
|
|
auto original = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kLocalNode, kRemoteNode, 1, /*wantAck=*/true);
|
|
reliableShim->seedRetry(original, NextHopRouter::NUM_RELIABLE_UNICAST_ATTEMPTS);
|
|
|
|
auto foreign = makeOpaqueOwnOverheard(original.id, meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA);
|
|
foreign.from = kRemoteNode;
|
|
foreign.to = kThirdNode;
|
|
ingressOverheard(foreign);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRoutingModule->ackNaks.size());
|
|
TEST_ASSERT_EQUAL_UINT32(1, reliableShim->pendingCount());
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Group 6 - pending-timer airtime extension in send() and shouldFilterReceived()
|
|
// ===========================================================================
|
|
|
|
void test_send_extends_other_pending_deadlines_not_own(void)
|
|
{
|
|
// While we transmit packet B we cannot hear an (implicit) ACK for pending A, so A's deadline
|
|
// must move out by B's airtime. B's own fresh record must not be self-extended.
|
|
radio->packetTimeMsec = 50000; // dwarfs any real time elapsed inside the test
|
|
|
|
auto a = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kLocalNode, kRemoteNode, 1, /*wantAck=*/true);
|
|
reliableShim->seedRetry(a, NextHopRouter::NUM_RELIABLE_UNICAST_ATTEMPTS);
|
|
uint32_t aBefore = reliableShim->pendingNextTx(kLocalNode, a.id);
|
|
|
|
auto b = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kLocalNode, NODENUM_BROADCAST, 0, /*wantAck=*/true);
|
|
auto *allocated = packetPool.allocCopy(b);
|
|
TEST_ASSERT_NOT_NULL(allocated);
|
|
TEST_ASSERT_EQUAL_INT(ERRNO_OK, reliableShim->send(allocated));
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(2, reliableShim->pendingCount());
|
|
TEST_ASSERT_EQUAL_UINT32(aBefore + 50000, reliableShim->pendingNextTx(kLocalNode, a.id));
|
|
|
|
// B's deadline is millis-at-set + getRetransmissionMsec(B); a self-extension would push it a
|
|
// further 50s out, past anything the wall clock could account for.
|
|
uint32_t bTx = reliableShim->pendingNextTx(kLocalNode, b.id);
|
|
uint32_t retrans = radio->getRetransmissionMsec(reliableShim->pendingPacket(kLocalNode, b.id));
|
|
// Via Throttle rather than a bare millis() compare, per the house deadline rule.
|
|
TEST_ASSERT_TRUE_MESSAGE(Throttle::deadlinePassed(bTx - retrans), "own record must not be extended by its own send");
|
|
}
|
|
|
|
void test_receive_extends_all_pending_deadlines(void)
|
|
{
|
|
// While receiving any packet we cannot hear an ACK either: every pending deadline moves out by
|
|
// the received packet's airtime.
|
|
radio->packetTimeMsec = 40000;
|
|
|
|
auto a = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kLocalNode, kRemoteNode, 1, /*wantAck=*/true);
|
|
reliableShim->seedRetry(a, NextHopRouter::NUM_RELIABLE_UNICAST_ATTEMPTS);
|
|
auto b = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kLocalNode, kThirdNode, 1, /*wantAck=*/true);
|
|
reliableShim->seedRetry(b, NextHopRouter::NUM_RELIABLE_UNICAST_ATTEMPTS);
|
|
uint32_t aBefore = reliableShim->pendingNextTx(kLocalNode, a.id);
|
|
uint32_t bBefore = reliableShim->pendingNextTx(kLocalNode, b.id);
|
|
|
|
auto inbound = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kRemoteNode, kLocalNode, 1, /*wantAck=*/false);
|
|
reliableShim->filterForTest(&inbound);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(aBefore + 40000, reliableShim->pendingNextTx(kLocalNode, a.id));
|
|
TEST_ASSERT_EQUAL_UINT32(bBefore + 40000, reliableShim->pendingNextTx(kLocalNode, b.id));
|
|
}
|
|
|
|
// ===========================================================================
|
|
|
|
void setup()
|
|
{
|
|
initializeTestEnvironment();
|
|
UNITY_BEGIN();
|
|
|
|
airTimeFixture = std::make_unique<ScopedAirTimeFixture>();
|
|
mockNodeDB = new MockNodeDB();
|
|
nodeDB = mockNodeDB;
|
|
reliableShim = new ReliableRouterTestShim();
|
|
|
|
auto capture = std::make_unique<TimedCaptureRadio>();
|
|
radio = capture.get();
|
|
reliableShim->addInterface(std::move(capture));
|
|
|
|
mockRoutingModule = new MockRoutingModule();
|
|
routingModule = mockRoutingModule;
|
|
|
|
printf("\n=== want_ack ACK variants ===\n");
|
|
RUN_TEST(test_text_dm_want_ack_gets_want_ack_ack);
|
|
RUN_TEST(test_text_reply_still_gets_want_ack_ack);
|
|
RUN_TEST(test_nontext_dm_want_ack_gets_plain_ack);
|
|
RUN_TEST(test_response_heard_directly_gets_zero_hop_ack);
|
|
RUN_TEST(test_response_relayed_gets_no_ack);
|
|
RUN_TEST(test_response_relayed_via_next_hop_gets_zero_hop_ack);
|
|
RUN_TEST(test_broadcast_want_ack_gets_no_ack);
|
|
|
|
printf("\n=== undecodable want_ack NAKs ===\n");
|
|
RUN_TEST(test_pki_unknown_sender_gets_pki_unknown_pubkey_nak);
|
|
RUN_TEST(test_pki_keyless_sender_record_gets_pki_unknown_pubkey_nak);
|
|
RUN_TEST(test_pki_known_key_sender_gets_no_channel_nak);
|
|
RUN_TEST(test_unknown_channel_hash_gets_no_channel_nak);
|
|
|
|
printf("\n=== next-hop 0-hop ACK without want_ack ===\n");
|
|
RUN_TEST(test_next_hop_addressed_to_us_gets_zero_hop_ack);
|
|
RUN_TEST(test_next_hop_with_hop_limit_zero_gets_no_ack);
|
|
RUN_TEST(test_next_hop_other_byte_gets_no_ack);
|
|
|
|
printf("\n=== ACK/NAK vs pending retransmissions ===\n");
|
|
RUN_TEST(test_explicit_ack_stops_retransmissions_and_clears_route_failures);
|
|
RUN_TEST(test_nak_stops_retransmissions_but_keeps_route_failures);
|
|
RUN_TEST(test_pki_unknown_pubkey_nak_stops_retransmissions);
|
|
RUN_TEST(test_own_ack_echo_via_mqtt_keeps_retransmissions);
|
|
RUN_TEST(test_own_ack_echo_via_lora_stops_retransmissions);
|
|
RUN_TEST(test_remote_ack_via_mqtt_still_stops_retransmissions);
|
|
|
|
printf("\n=== implicit ACK for our own overheard DM ===\n");
|
|
RUN_TEST(test_overheard_own_dm_rebroadcast_mints_implicit_ack);
|
|
RUN_TEST(test_overheard_own_dm_via_mqtt_acks_but_keeps_retransmissions);
|
|
RUN_TEST(test_overheard_foreign_packet_mints_no_implicit_ack);
|
|
|
|
printf("\n=== implicit ACK through the opaque ingress short-circuit (#11502) ===\n");
|
|
RUN_TEST(test_ingress_opaque_own_dm_lora_mints_implicit_ack_and_stops_retries);
|
|
RUN_TEST(test_ingress_opaque_own_dm_mqtt_acks_but_keeps_retries);
|
|
RUN_TEST(test_ingress_opaque_foreign_packet_mints_no_implicit_ack);
|
|
|
|
printf("\n=== pending-timer airtime extension ===\n");
|
|
RUN_TEST(test_send_extends_other_pending_deadlines_not_own);
|
|
RUN_TEST(test_receive_extends_all_pending_deadlines);
|
|
|
|
int result = UNITY_END();
|
|
airTimeFixture.reset();
|
|
exit(result);
|
|
}
|
|
|
|
void loop() {}
|