mirror of
https://github.com/meshtastic/firmware.git
synced 2026-10-09 14:41:19 -04:00
* Lock: give Portduino a real mutex instead of the empty fallback Lock.cpp has a FreeRTOS implementation and an empty one, and Portduino takes the empty one: every lock() and unlock() on a Linux build is a no-op, so concurrency::Lock protects nothing there. TrafficManagementModule's cacheLock and SPILock are both built on it, and native meshtasticd runs the radio and the API on separate threads. Add a pthread implementation under ARCH_PORTDUINO. The timed lock(uint32_t) blocks rather than returning early, because there is no portable timed pthread_mutex_lock across Linux and macOS and returning true without acquiring would leave a caller such as SPILock unlocking a mutex it never took. Targets that have neither FreeRTOS nor pthreads, such as STM32WL, keep the existing empty implementation byte for byte. Co-Authored-By: Jonathan Bennett <jbennett@incomsystems.biz> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01REkPVFh6kvG4AZJ5A8AtM9 * test: create spiLock in the shared setup, which NodeDB needs and no test had Making Portduino's Lock real turns a latent null dereference into a crash. spiLock is a bare pointer that initSPI() fills in, and only main.cpp calls that, so in a test binary it stays null. NodeDB's constructor reaches it through loadFromDisk(), and while Lock::lock() was an empty function the call never touched `this`, so 23 suites have been calling a method on a null pointer and getting away with it. With a pthread mutex behind it the same call reads through the null pointer and takes SIGSEGV at offset 0x10, which is what test_phone_api_config_dump, test_muted_source, test_nodeinfo_send_window and test_module_config hit. Create it once in initializeTestEnvironment(), which every affected suite calls as the first statement of setup(), before any of them constructs a NodeDB. The guard is the idiom test_xmodem and test_nodedb_identity_hygiene already use; theirs stay correct and become no-ops. test_safefile called initSPI() bare right after the harness, which would now trip its assert, so that call goes away. No firmware behaviour changes: main.cpp still calls initSPI() exactly once, and nothing outside the test harness is touched. Co-Authored-By: Jonathan Bennett <jbennett@incomsystems.biz> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01REkPVFh6kvG4AZJ5A8AtM9 * test: create cryptLock where a suite reaches it without building a Router Second instance of the same latent null dereference the previous commit fixed for spiLock. cryptLock is a bare pointer that Router's constructor creates (Router.cpp:246); AdminModule::setPassKey takes a LockGuard on it, and a suite that exercises an admin path without standing up a Router leaves it null. While Lock::lock() was empty on Portduino the guard never touched `this`; with a pthread mutex it reads through null, which is test_tak_config's SIGSEGV in handleGetModuleConfig. It cannot go in initializeTestEnvironment() the way spiLock did, because Router asserts cryptLock is unset before allocating its own, so creating it for every suite would break the ones that do build a Router. It is a named helper instead, testEnsureCryptLock(), called by the six suites that reach a cryptLock path with no Router: test_ack_proof, test_admin_session_repro, test_fuzz_packets, test_hop_scaling, test_module_config and test_tak_config. The three that define setup() twice behind a PKI #if get the call only in the branch that compiles the tests in. No firmware behaviour changes; nothing outside test/ is touched. Co-Authored-By: Jonathan Bennett <jbennett@incomsystems.biz> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01REkPVFh6kvG4AZJ5A8AtM9 * test: create cryptLock in the harness instead of chasing suites Router's constructor asserted cryptLock was unset, then allocated it. That assert is why ten suites carry a mock-router destructor whose only job is to delete the global and null it so the next router can be built. While Lock::lock() was an empty function on Portduino a null cryptLock cost nothing, so those null windows were invisible; with a real mutex, anything reaching perhapsDecode() or the ack-proof paths after one of those destructors runs dereferences null. The fix is the idiom already on the next line of the same constructor, which routingAuthCacheLock has used all along: reuse the lock if one exists. Nothing in src/ ever deleted cryptLock, so a Router that finds one is finding the process's only one. initializeTestEnvironment() can then create it for every suite, the way it now does for spiLock, and the ten teardowns and the per-suite helper from the previous commit all go away. Replaces the six testEnsureCryptLock() call sites with one creation point, and removes the null windows in test_admin_radio, test_mesh_beacon, test_mesh_module, test_mqtt, test_nexthop_routing, test_nodeinfo_send_window, test_traffic_management, test_event_channel_phone_api, test_event_channel_router and test_phone_api_config_dump. Co-Authored-By: Jonathan Bennett <jbennett@incomsystems.biz> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01REkPVFh6kvG4AZJ5A8AtM9 --------- Co-authored-by: Claude <noreply@anthropic.com>
909 lines
32 KiB
C++
909 lines
32 KiB
C++
#include "MeshModule.h"
|
|
#include "MeshTypes.h"
|
|
#include "TestUtil.h"
|
|
#include <unity.h>
|
|
|
|
#include "configuration.h"
|
|
#include "mesh/CryptoEngine.h"
|
|
#include "mesh/MeshService.h"
|
|
#include "mesh/NodeDB.h"
|
|
#include "mesh/RadioInterface.h"
|
|
#include "mesh/Router.h"
|
|
#include "modules/NeighborInfoModule.h"
|
|
#include "modules/RoutingModule.h"
|
|
#include "support/MockMeshService.h"
|
|
#include <memory>
|
|
#include <vector>
|
|
|
|
namespace
|
|
{
|
|
constexpr NodeNum LOCAL_NODE = 0x11111111;
|
|
constexpr NodeNum REMOTE_NODE = 0x22222222;
|
|
|
|
// Minimal concrete subclass for testing the base class helper
|
|
class TestModule : public MeshModule
|
|
{
|
|
public:
|
|
TestModule() : MeshModule("TestModule") {}
|
|
virtual bool wantPacket(const meshtastic_MeshPacket *p) override { return true; }
|
|
using MeshModule::currentRequest;
|
|
using MeshModule::isMultiHopBroadcastRequest;
|
|
};
|
|
|
|
class MockNodeDB : public NodeDB
|
|
{
|
|
};
|
|
|
|
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:
|
|
ErrorCode send(meshtastic_MeshPacket *p) override
|
|
{
|
|
sentPackets.push_back(*p);
|
|
packetPool.release(p);
|
|
return ERRNO_OK;
|
|
}
|
|
|
|
std::vector<meshtastic_MeshPacket> sentPackets;
|
|
};
|
|
|
|
struct AckNak {
|
|
meshtastic_Routing_Error error;
|
|
NodeNum to;
|
|
PacketId requestId;
|
|
ChannelIndex channel;
|
|
};
|
|
|
|
class MockRoutingModule : public RoutingModule
|
|
{
|
|
public:
|
|
void sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit = 0,
|
|
bool ackWantsAck = false, const meshtastic_MeshPacket *relaySource = nullptr) override
|
|
{
|
|
(void)hopLimit;
|
|
(void)ackWantsAck;
|
|
(void)relaySource;
|
|
ackNaks.push_back({err, to, idFrom, chIndex});
|
|
}
|
|
|
|
std::vector<AckNak> ackNaks;
|
|
|
|
protected:
|
|
bool wantPacket(const meshtastic_MeshPacket *p) override
|
|
{
|
|
(void)p;
|
|
return false;
|
|
}
|
|
};
|
|
|
|
class SyntheticReplyModule : public MeshModule
|
|
{
|
|
public:
|
|
SyntheticReplyModule(const char *name, meshtastic_PortNum modulePort, meshtastic_PortNum replyPort,
|
|
bool acceptsEveryPort = false)
|
|
: MeshModule(name, modulePort), replyPort(replyPort), acceptsEveryPort(acceptsEveryPort)
|
|
{
|
|
isPromiscuous = acceptsEveryPort;
|
|
}
|
|
|
|
uint32_t allocReplyCalls = 0;
|
|
|
|
protected:
|
|
bool wantPacket(const meshtastic_MeshPacket *p) override { return acceptsEveryPort || p->decoded.portnum == ourPortNum; }
|
|
|
|
meshtastic_MeshPacket *allocReply() override
|
|
{
|
|
allocReplyCalls++;
|
|
meshtastic_MeshPacket *reply = router->allocForSending();
|
|
reply->decoded.portnum = replyPort;
|
|
return reply;
|
|
}
|
|
|
|
private:
|
|
meshtastic_PortNum replyPort;
|
|
bool acceptsEveryPort;
|
|
};
|
|
|
|
class ObservingIgnoreModule : public MeshModule
|
|
{
|
|
public:
|
|
ObservingIgnoreModule() : MeshModule("storeforward-shaped", meshtastic_PortNum_STORE_FORWARD_APP) {}
|
|
|
|
uint32_t allocReplyCalls = 0;
|
|
|
|
protected:
|
|
bool wantPacket(const meshtastic_MeshPacket *p) override { return p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP; }
|
|
|
|
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override
|
|
{
|
|
(void)mp;
|
|
ignoreRequest = true;
|
|
return ProcessMessage::CONTINUE;
|
|
}
|
|
|
|
meshtastic_MeshPacket *allocReply() override
|
|
{
|
|
allocReplyCalls++;
|
|
return nullptr;
|
|
}
|
|
};
|
|
|
|
class ReplyIgnoreModule : public MeshModule
|
|
{
|
|
public:
|
|
ReplyIgnoreModule() : MeshModule("reply-ignore", meshtastic_PortNum_NEIGHBORINFO_APP) {}
|
|
|
|
uint32_t allocReplyCalls = 0;
|
|
|
|
protected:
|
|
bool wantPacket(const meshtastic_MeshPacket *p) override { return p->decoded.portnum == ourPortNum; }
|
|
|
|
meshtastic_MeshPacket *allocReply() override
|
|
{
|
|
allocReplyCalls++;
|
|
ignoreRequest = true;
|
|
return nullptr;
|
|
}
|
|
};
|
|
|
|
// Sends, from inside callModules(), a self-addressed reply and a local broadcast - the fan-out an
|
|
// AdminModule config save performs. Both go out through service->sendToMesh() while a
|
|
// handleReceived() frame is live, so both must be deferred rather than handled re-entrantly.
|
|
class NestedFanoutModule : public MeshModule
|
|
{
|
|
public:
|
|
NestedFanoutModule() : MeshModule("nested-fanout", meshtastic_PortNum_PRIVATE_APP) {}
|
|
uint32_t handleCalls = 0;
|
|
|
|
protected:
|
|
bool wantPacket(const meshtastic_MeshPacket *p) override { return p->decoded.portnum == ourPortNum; }
|
|
|
|
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override
|
|
{
|
|
(void)mp;
|
|
handleCalls++;
|
|
|
|
// (a) a reply addressed to ourselves - exercises the isToUs() deferral branch
|
|
meshtastic_MeshPacket *reply = router->allocForSending();
|
|
reply->to = LOCAL_NODE;
|
|
reply->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP;
|
|
reply->decoded.request_id = 0xBEEF;
|
|
service->sendToMesh(reply); // default src == RX_SRC_LOCAL
|
|
|
|
// (b) a local broadcast - exercises the broadcast branch (loopback deferred, TX immediate)
|
|
meshtastic_MeshPacket *bcast = router->allocForSending();
|
|
bcast->to = NODENUM_BROADCAST;
|
|
bcast->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP;
|
|
service->sendToMesh(bcast);
|
|
|
|
return ProcessMessage::STOP;
|
|
}
|
|
};
|
|
|
|
// loopbackOk so it also runs for drained RX_SRC_LOCAL packets; each generation sends the next, so
|
|
// the drain must keep processing a chain that grows while it is draining.
|
|
class ChainSendModule : public MeshModule
|
|
{
|
|
public:
|
|
ChainSendModule() : MeshModule("chain-send", meshtastic_PortNum_PRIVATE_APP) { loopbackOk = true; }
|
|
uint32_t handleCalls = 0;
|
|
uint32_t maxGeneration = 0;
|
|
|
|
protected:
|
|
bool wantPacket(const meshtastic_MeshPacket *p) override { return p->decoded.portnum == ourPortNum; }
|
|
|
|
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override
|
|
{
|
|
handleCalls++;
|
|
uint32_t generation = mp.decoded.request_id;
|
|
if (generation > maxGeneration)
|
|
maxGeneration = generation;
|
|
|
|
if (generation < 3) {
|
|
meshtastic_MeshPacket *next = router->allocForSending();
|
|
next->to = LOCAL_NODE;
|
|
next->decoded.portnum = ourPortNum;
|
|
next->decoded.request_id = generation + 1;
|
|
service->sendToMesh(next); // default src == RX_SRC_LOCAL
|
|
}
|
|
return ProcessMessage::STOP;
|
|
}
|
|
};
|
|
|
|
// Sends a burst of self-addressed replies from one dispatch, to overflow the deferred queue.
|
|
class BurstSendModule : public MeshModule
|
|
{
|
|
public:
|
|
explicit BurstSendModule(uint32_t count) : MeshModule("burst-send", meshtastic_PortNum_PRIVATE_APP), count(count) {}
|
|
|
|
protected:
|
|
bool wantPacket(const meshtastic_MeshPacket *p) override { return p->decoded.portnum == ourPortNum; }
|
|
|
|
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override
|
|
{
|
|
(void)mp;
|
|
for (uint32_t i = 0; i < count; i++) {
|
|
meshtastic_MeshPacket *reply = router->allocForSending();
|
|
reply->to = LOCAL_NODE;
|
|
reply->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP;
|
|
reply->decoded.request_id = 0x100 + i;
|
|
service->sendToMesh(reply); // default src == RX_SRC_LOCAL
|
|
}
|
|
return ProcessMessage::STOP;
|
|
}
|
|
|
|
private:
|
|
uint32_t count;
|
|
};
|
|
|
|
static TestModule *testModule;
|
|
static meshtastic_MeshPacket testPacket;
|
|
static MockNodeDB *mockNodeDB;
|
|
static MockMeshService *mockService;
|
|
static MockRouter *mockRouter;
|
|
static MockRoutingModule *mockRoutingModule;
|
|
static NeighborInfoModule *realNeighborInfoModule;
|
|
static RoutingModule *realRoutingModule;
|
|
static std::vector<MeshModule *> dispatchModules;
|
|
|
|
template <typename T> static T *registerDispatchModule(T *module)
|
|
{
|
|
dispatchModules.push_back(module);
|
|
return module;
|
|
}
|
|
|
|
// Swap the mocked RoutingModule for a real one. tearDown() owns the cleanup because a failed
|
|
// assertion longjmps out of the test, which would otherwise leave it registered in MeshModule::modules.
|
|
static void installRealRoutingModule()
|
|
{
|
|
realRoutingModule = new RoutingModule();
|
|
routingModule = realRoutingModule;
|
|
}
|
|
|
|
static meshtastic_MeshPacket makeRequest(meshtastic_PortNum port)
|
|
{
|
|
meshtastic_MeshPacket packet = meshtastic_MeshPacket_init_zero;
|
|
packet.from = REMOTE_NODE;
|
|
packet.to = LOCAL_NODE;
|
|
packet.id = 0x12345678;
|
|
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.want_response = true;
|
|
return packet;
|
|
}
|
|
|
|
static void dispatch(meshtastic_PortNum port)
|
|
{
|
|
meshtastic_MeshPacket request = makeRequest(port);
|
|
MeshModule::callModules(request);
|
|
}
|
|
|
|
// Dispatch a want_response==false trigger addressed to us through the real router, so a module's
|
|
// handler runs inside a live handleReceived() frame (handleDepth == 1) and any packet it sends is
|
|
// deferred. requestId is carried in decoded.request_id for modules that key on a generation.
|
|
static void dispatchTrigger(meshtastic_PortNum port, uint32_t requestId = 0)
|
|
{
|
|
meshtastic_MeshPacket trigger = meshtastic_MeshPacket_init_zero;
|
|
trigger.from = LOCAL_NODE;
|
|
trigger.to = LOCAL_NODE;
|
|
trigger.id = 0x7A190000 + requestId;
|
|
trigger.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
|
trigger.decoded.portnum = port;
|
|
trigger.decoded.request_id = requestId;
|
|
service->sendToMesh(packetPool.allocCopy(trigger), RX_SRC_USER);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void setUp(void)
|
|
{
|
|
config = meshtastic_LocalConfig_init_zero;
|
|
moduleConfig = meshtastic_LocalModuleConfig_init_zero;
|
|
channelFile = meshtastic_ChannelFile_init_zero;
|
|
owner = meshtastic_User_init_zero;
|
|
myNodeInfo.my_node_num = LOCAL_NODE;
|
|
|
|
mockNodeDB = new MockNodeDB();
|
|
nodeDB = mockNodeDB;
|
|
myNodeInfo.my_node_num = LOCAL_NODE;
|
|
|
|
mockService = new MockMeshService();
|
|
service = mockService;
|
|
|
|
channels.initDefaults();
|
|
channels.onConfigChanged();
|
|
|
|
mockRouter = new MockRouter();
|
|
mockRouter->addInterface(std::unique_ptr<RadioInterface>(new MockRadioInterface()));
|
|
router = mockRouter;
|
|
|
|
mockRoutingModule = new MockRoutingModule();
|
|
routingModule = mockRoutingModule;
|
|
realRoutingModule = nullptr;
|
|
|
|
testModule = new TestModule();
|
|
memset(&testPacket, 0, sizeof(testPacket));
|
|
TestModule::currentRequest = &testPacket;
|
|
}
|
|
|
|
void tearDown(void)
|
|
{
|
|
TestModule::currentRequest = NULL;
|
|
|
|
for (auto it = dispatchModules.rbegin(); it != dispatchModules.rend(); ++it)
|
|
delete *it;
|
|
dispatchModules.clear();
|
|
|
|
delete realNeighborInfoModule;
|
|
realNeighborInfoModule = nullptr;
|
|
|
|
delete testModule;
|
|
testModule = nullptr;
|
|
|
|
delete realRoutingModule;
|
|
realRoutingModule = nullptr;
|
|
|
|
delete mockRoutingModule;
|
|
mockRoutingModule = nullptr;
|
|
routingModule = nullptr;
|
|
|
|
while (auto *status = mockService->getQueueStatusForPhone())
|
|
mockService->releaseQueueStatusToPool(status);
|
|
while (auto *toPhone = mockService->getForPhone())
|
|
mockService->releaseToPool(toPhone);
|
|
delete mockService;
|
|
mockService = nullptr;
|
|
service = nullptr;
|
|
|
|
delete mockRouter;
|
|
mockRouter = nullptr;
|
|
router = nullptr;
|
|
|
|
delete mockNodeDB;
|
|
mockNodeDB = nullptr;
|
|
nodeDB = nullptr;
|
|
}
|
|
|
|
// Zero-hop broadcast (hop_limit == hop_start): should be allowed
|
|
static void test_zeroHopBroadcast_isAllowed()
|
|
{
|
|
testPacket.to = NODENUM_BROADCAST;
|
|
testPacket.hop_start = 3;
|
|
testPacket.hop_limit = 3; // Not yet relayed
|
|
|
|
TEST_ASSERT_FALSE(testModule->isMultiHopBroadcastRequest());
|
|
}
|
|
|
|
// Multi-hop broadcast (hop_limit < hop_start): should be blocked
|
|
static void test_multiHopBroadcast_isBlocked()
|
|
{
|
|
testPacket.to = NODENUM_BROADCAST;
|
|
testPacket.hop_start = 7;
|
|
testPacket.hop_limit = 4; // Already relayed 3 hops
|
|
|
|
TEST_ASSERT_TRUE(testModule->isMultiHopBroadcastRequest());
|
|
}
|
|
|
|
// Direct message (not broadcast): should always be allowed regardless of hops
|
|
static void test_directMessage_isAllowed()
|
|
{
|
|
testPacket.to = 0x12345678; // Specific node
|
|
testPacket.hop_start = 7;
|
|
testPacket.hop_limit = 4;
|
|
|
|
TEST_ASSERT_FALSE(testModule->isMultiHopBroadcastRequest());
|
|
}
|
|
|
|
// Broadcast with hop_limit == 0 (fully relayed): should be blocked
|
|
static void test_fullyRelayedBroadcast_isBlocked()
|
|
{
|
|
testPacket.to = NODENUM_BROADCAST;
|
|
testPacket.hop_start = 3;
|
|
testPacket.hop_limit = 0;
|
|
|
|
TEST_ASSERT_TRUE(testModule->isMultiHopBroadcastRequest());
|
|
}
|
|
|
|
// No current request: should not crash, should return false
|
|
static void test_noCurrentRequest_isAllowed()
|
|
{
|
|
TestModule::currentRequest = NULL;
|
|
|
|
TEST_ASSERT_FALSE(testModule->isMultiHopBroadcastRequest());
|
|
}
|
|
|
|
// Broadcast with hop_start == 0 (legacy or local): should be allowed
|
|
static void test_legacyPacket_zeroHopStart_isAllowed()
|
|
{
|
|
testPacket.to = NODENUM_BROADCAST;
|
|
testPacket.hop_start = 0;
|
|
testPacket.hop_limit = 0;
|
|
|
|
// hop_limit == hop_start, so not multi-hop
|
|
TEST_ASSERT_FALSE(testModule->isMultiHopBroadcastRequest());
|
|
}
|
|
|
|
// Single hop relayed broadcast (hop_limit = hop_start - 1): should be blocked
|
|
static void test_singleHopRelayedBroadcast_isBlocked()
|
|
{
|
|
testPacket.to = NODENUM_BROADCAST;
|
|
testPacket.hop_start = 3;
|
|
testPacket.hop_limit = 2;
|
|
|
|
TEST_ASSERT_TRUE(testModule->isMultiHopBroadcastRequest());
|
|
}
|
|
|
|
static void test_replyPortMatches_ownPort()
|
|
{
|
|
meshtastic_MeshPacket request = makeRequest(meshtastic_PortNum_TELEMETRY_APP);
|
|
|
|
TEST_ASSERT_TRUE(MeshModule::replyPortMatches(meshtastic_PortNum_TELEMETRY_APP, request));
|
|
}
|
|
|
|
static void test_replyPortMatches_neighborInfoVsTelemetry()
|
|
{
|
|
meshtastic_MeshPacket request = makeRequest(meshtastic_PortNum_TELEMETRY_APP);
|
|
|
|
TEST_ASSERT_FALSE(MeshModule::replyPortMatches(meshtastic_PortNum_NEIGHBORINFO_APP, request));
|
|
}
|
|
|
|
static void test_replyPortMatches_positionRequest()
|
|
{
|
|
meshtastic_MeshPacket request = makeRequest(meshtastic_PortNum_POSITION_APP);
|
|
|
|
TEST_ASSERT_TRUE(MeshModule::replyPortMatches(meshtastic_PortNum_POSITION_APP, request));
|
|
}
|
|
|
|
static void test_replyPortMatches_unknownModulePort()
|
|
{
|
|
meshtastic_MeshPacket request = makeRequest(meshtastic_PortNum_TELEMETRY_APP);
|
|
|
|
TEST_ASSERT_FALSE(MeshModule::replyPortMatches(meshtastic_PortNum_UNKNOWN_APP, request));
|
|
}
|
|
|
|
static void test_replyPortMatches_unknownRequestPort()
|
|
{
|
|
meshtastic_MeshPacket request = makeRequest(meshtastic_PortNum_UNKNOWN_APP);
|
|
|
|
TEST_ASSERT_FALSE(MeshModule::replyPortMatches(meshtastic_PortNum_TELEMETRY_APP, request));
|
|
}
|
|
|
|
static void test_dispatch_foreignPortOffenderCannotShadowOwner()
|
|
{
|
|
auto *offender = registerDispatchModule(new SyntheticReplyModule("neighbor-shaped", meshtastic_PortNum_NEIGHBORINFO_APP,
|
|
meshtastic_PortNum_NEIGHBORINFO_APP, true));
|
|
auto *owner = registerDispatchModule(
|
|
new SyntheticReplyModule("telemetry-owner", meshtastic_PortNum_TELEMETRY_APP, meshtastic_PortNum_TELEMETRY_APP));
|
|
|
|
dispatch(meshtastic_PortNum_TELEMETRY_APP);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(0, offender->allocReplyCalls);
|
|
TEST_ASSERT_EQUAL_UINT32(1, owner->allocReplyCalls);
|
|
TEST_ASSERT_EQUAL_UINT32(1, mockRouter->sentPackets.size());
|
|
TEST_ASSERT_EQUAL(meshtastic_PortNum_TELEMETRY_APP, mockRouter->sentPackets[0].decoded.portnum);
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRoutingModule->ackNaks.size());
|
|
}
|
|
|
|
static void test_dispatch_ownerPortStillReplies()
|
|
{
|
|
auto *offender = registerDispatchModule(new SyntheticReplyModule("neighbor-shaped", meshtastic_PortNum_NEIGHBORINFO_APP,
|
|
meshtastic_PortNum_NEIGHBORINFO_APP, true));
|
|
auto *owner = registerDispatchModule(
|
|
new SyntheticReplyModule("telemetry-owner", meshtastic_PortNum_TELEMETRY_APP, meshtastic_PortNum_TELEMETRY_APP));
|
|
|
|
dispatch(meshtastic_PortNum_NEIGHBORINFO_APP);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(1, offender->allocReplyCalls);
|
|
TEST_ASSERT_EQUAL_UINT32(0, owner->allocReplyCalls);
|
|
TEST_ASSERT_EQUAL_UINT32(1, mockRouter->sentPackets.size());
|
|
TEST_ASSERT_EQUAL(meshtastic_PortNum_NEIGHBORINFO_APP, mockRouter->sentPackets[0].decoded.portnum);
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRoutingModule->ackNaks.size());
|
|
}
|
|
|
|
static void test_dispatch_crossPortReplyUsesRequestOwner()
|
|
{
|
|
auto *position = registerDispatchModule(
|
|
new SyntheticReplyModule("position-owner", meshtastic_PortNum_POSITION_APP, meshtastic_PortNum_ATAK_PLUGIN_V2));
|
|
|
|
dispatch(meshtastic_PortNum_POSITION_APP);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(1, position->allocReplyCalls);
|
|
TEST_ASSERT_EQUAL_UINT32(1, mockRouter->sentPackets.size());
|
|
TEST_ASSERT_EQUAL(meshtastic_PortNum_ATAK_PLUGIN_V2, mockRouter->sentPackets[0].decoded.portnum);
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRoutingModule->ackNaks.size());
|
|
}
|
|
|
|
static void test_dispatch_foreignPortObserverCanSuppressNak()
|
|
{
|
|
auto *observer = registerDispatchModule(new ObservingIgnoreModule());
|
|
|
|
dispatch(meshtastic_PortNum_TEXT_MESSAGE_APP);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(0, observer->allocReplyCalls);
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRouter->sentPackets.size());
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRoutingModule->ackNaks.size());
|
|
}
|
|
|
|
static void test_dispatch_noResponderSendsNak()
|
|
{
|
|
dispatch(meshtastic_PortNum_TELEMETRY_APP);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRouter->sentPackets.size());
|
|
TEST_ASSERT_EQUAL_UINT32(1, mockRoutingModule->ackNaks.size());
|
|
TEST_ASSERT_EQUAL(meshtastic_Routing_Error_NO_RESPONSE, mockRoutingModule->ackNaks[0].error);
|
|
TEST_ASSERT_EQUAL_HEX32(REMOTE_NODE, mockRoutingModule->ackNaks[0].to);
|
|
}
|
|
|
|
static void test_dispatch_ignoreRequestIsClearedPerPacket()
|
|
{
|
|
auto *ignoring = registerDispatchModule(new ReplyIgnoreModule());
|
|
|
|
dispatch(meshtastic_PortNum_NEIGHBORINFO_APP);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(1, ignoring->allocReplyCalls);
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRouter->sentPackets.size());
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRoutingModule->ackNaks.size());
|
|
|
|
dispatch(meshtastic_PortNum_TELEMETRY_APP);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(1, ignoring->allocReplyCalls);
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRouter->sentPackets.size());
|
|
TEST_ASSERT_EQUAL_UINT32(1, mockRoutingModule->ackNaks.size());
|
|
TEST_ASSERT_EQUAL(meshtastic_Routing_Error_NO_RESPONSE, mockRoutingModule->ackNaks[0].error);
|
|
}
|
|
|
|
static void test_dispatch_realNeighborInfoCannotShadowTelemetryOwner()
|
|
{
|
|
moduleConfig.neighbor_info.enabled = true;
|
|
realNeighborInfoModule = new NeighborInfoModule();
|
|
registerDispatchModule(
|
|
new SyntheticReplyModule("telemetry-owner", meshtastic_PortNum_TELEMETRY_APP, meshtastic_PortNum_TELEMETRY_APP));
|
|
|
|
dispatch(meshtastic_PortNum_TELEMETRY_APP);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(1, mockRouter->sentPackets.size());
|
|
TEST_ASSERT_EQUAL(meshtastic_PortNum_TELEMETRY_APP, mockRouter->sentPackets[0].decoded.portnum);
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRoutingModule->ackNaks.size());
|
|
}
|
|
|
|
// A reply addressed to ourselves reaches the phone queue, with SHOULD_RELEASE reported as success.
|
|
static void test_localReplyToSelf_isDeliveredToPhone()
|
|
{
|
|
meshtastic_MeshPacket reply = meshtastic_MeshPacket_init_zero;
|
|
reply.from = LOCAL_NODE;
|
|
reply.to = LOCAL_NODE;
|
|
reply.id = 0xABCD1234;
|
|
reply.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
|
reply.decoded.portnum = meshtastic_PortNum_ADMIN_APP;
|
|
reply.decoded.request_id = 0x12345678;
|
|
|
|
service->sendToMesh(packetPool.allocCopy(reply)); // default src == RX_SRC_LOCAL, as callModules sends replies
|
|
|
|
meshtastic_MeshPacket *toPhone = mockService->getForPhone();
|
|
TEST_ASSERT_NOT_NULL(toPhone);
|
|
TEST_ASSERT_EQUAL_UINT32(0xABCD1234, toPhone->id);
|
|
TEST_ASSERT_EQUAL_UINT32(0x12345678, toPhone->decoded.request_id);
|
|
mockService->releaseToPool(toPhone);
|
|
TEST_ASSERT_NULL(mockService->getForPhone()); // exactly one delivery
|
|
|
|
meshtastic_QueueStatus *qs = mockService->getQueueStatusForPhone();
|
|
TEST_ASSERT_NOT_NULL(qs);
|
|
TEST_ASSERT_EQUAL(ERRNO_OK, qs->res);
|
|
mockService->releaseQueueStatusToPool(qs);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRouter->sentPackets.size()); // nothing went toward the radio
|
|
}
|
|
|
|
// handleFromRadio() is private to MeshService, which befriends RoutingModule and, under
|
|
// PIO_UNIT_TESTING, this seam.
|
|
class MeshServicePhoneDeliveryTest
|
|
{
|
|
public:
|
|
static void deliver(const meshtastic_MeshPacket &p) { service->handleFromRadio(&p); }
|
|
};
|
|
|
|
static void test_handleFromRadio_remotePacketReachesPhone()
|
|
{
|
|
meshtastic_MeshPacket rx = meshtastic_MeshPacket_init_zero;
|
|
rx.from = REMOTE_NODE;
|
|
rx.to = NODENUM_BROADCAST;
|
|
rx.id = 0x0BADF00D;
|
|
rx.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
|
rx.decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP;
|
|
|
|
MeshServicePhoneDeliveryTest::deliver(rx);
|
|
|
|
meshtastic_MeshPacket *toPhone = mockService->getForPhone();
|
|
TEST_ASSERT_NOT_NULL(toPhone);
|
|
TEST_ASSERT_EQUAL_UINT32(0x0BADF00D, toPhone->id);
|
|
mockService->releaseToPool(toPhone);
|
|
TEST_ASSERT_NULL(mockService->getForPhone());
|
|
}
|
|
|
|
// A packet we originated, coming back around, must not be echoed to the client that sent it.
|
|
static void test_handleFromRadio_ownPacketIsNotEchoedToPhone()
|
|
{
|
|
meshtastic_MeshPacket ours = meshtastic_MeshPacket_init_zero;
|
|
ours.from = LOCAL_NODE;
|
|
ours.to = NODENUM_BROADCAST;
|
|
ours.id = 0x5E1F0001;
|
|
ours.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
|
ours.decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP;
|
|
|
|
MeshServicePhoneDeliveryTest::deliver(ours);
|
|
TEST_ASSERT_NULL(mockService->getForPhone());
|
|
|
|
// Same for the from==0 spelling handleToRadio stamps on phone-originated packets.
|
|
ours.from = 0;
|
|
ours.id = 0x5E1F0002;
|
|
MeshServicePhoneDeliveryTest::deliver(ours);
|
|
TEST_ASSERT_NULL(mockService->getForPhone());
|
|
}
|
|
|
|
// A packet from us *addressed to us* is locally-generated feedback, not an echo, and must still be
|
|
// delivered - suppressing it would silently drop every ACK/NAK the client relies on.
|
|
static void test_handleFromRadio_ownPacketAddressedToUsReachesPhone()
|
|
{
|
|
meshtastic_MeshPacket ack = meshtastic_MeshPacket_init_zero;
|
|
ack.from = LOCAL_NODE;
|
|
ack.to = LOCAL_NODE;
|
|
ack.id = 0x5E1F0003;
|
|
ack.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
|
ack.decoded.portnum = meshtastic_PortNum_ROUTING_APP;
|
|
ack.decoded.request_id = 0x0C0FFEE0;
|
|
|
|
MeshServicePhoneDeliveryTest::deliver(ack);
|
|
|
|
meshtastic_MeshPacket *toPhone = mockService->getForPhone();
|
|
TEST_ASSERT_NOT_NULL(toPhone);
|
|
TEST_ASSERT_EQUAL_UINT32(0x0C0FFEE0, toPhone->decoded.request_id);
|
|
mockService->releaseToPool(toPhone);
|
|
TEST_ASSERT_NULL(mockService->getForPhone());
|
|
}
|
|
|
|
// sendAckNak stamps from == our nodenum and to == us, and sendLocal defaults to RX_SRC_RADIO, so the
|
|
// loopback gate never applies and only handleFromRadio's filter gates the implicit ACK / NAK path.
|
|
static void test_localAckNak_reachesPhoneViaRealRoutingModule()
|
|
{
|
|
installRealRoutingModule();
|
|
|
|
realRoutingModule->sendAckNak(meshtastic_Routing_Error_NONE, LOCAL_NODE, 0xFEEDBEEF, 0);
|
|
|
|
meshtastic_MeshPacket *toPhone = mockService->getForPhone();
|
|
TEST_ASSERT_NOT_NULL(toPhone);
|
|
TEST_ASSERT_EQUAL(meshtastic_PortNum_ROUTING_APP, toPhone->decoded.portnum);
|
|
TEST_ASSERT_EQUAL_UINT32(0xFEEDBEEF, toPhone->decoded.request_id);
|
|
TEST_ASSERT_EQUAL_UINT32(LOCAL_NODE, toPhone->to);
|
|
TEST_ASSERT_EQUAL_UINT32(LOCAL_NODE, toPhone->from);
|
|
mockService->releaseToPool(toPhone);
|
|
}
|
|
|
|
// #10767: the implicit ACK for an overheard rebroadcast of our own packet carries that copy's
|
|
// relaying node and the link metrics we heard it at, so the phone can attribute them to the relayer.
|
|
// rx_rssi has explicit presence, so has_rx_rssi has to travel with it or the reading never encodes.
|
|
static void test_localAck_carriesRelaySourceToPhone()
|
|
{
|
|
installRealRoutingModule();
|
|
|
|
meshtastic_MeshPacket overheard = meshtastic_MeshPacket_init_zero;
|
|
overheard.from = LOCAL_NODE;
|
|
overheard.to = REMOTE_NODE;
|
|
overheard.id = 0xFEEDBEEF;
|
|
overheard.relay_node = 0xAB;
|
|
overheard.has_rx_rssi = true;
|
|
overheard.rx_rssi = -93;
|
|
overheard.rx_snr = 4.75f;
|
|
|
|
realRoutingModule->sendAckNak(meshtastic_Routing_Error_NONE, LOCAL_NODE, overheard.id, 0, /*hopLimit=*/0,
|
|
/*ackWantsAck=*/false, &overheard);
|
|
|
|
meshtastic_MeshPacket *toPhone = mockService->getForPhone();
|
|
TEST_ASSERT_NOT_NULL(toPhone);
|
|
TEST_ASSERT_EQUAL_UINT32(0xFEEDBEEF, toPhone->decoded.request_id);
|
|
TEST_ASSERT_EQUAL_HEX8(0xAB, toPhone->relay_node);
|
|
TEST_ASSERT_TRUE(toPhone->has_rx_rssi);
|
|
TEST_ASSERT_EQUAL_INT32(-93, toPhone->rx_rssi);
|
|
TEST_ASSERT_EQUAL_FLOAT(4.75f, toPhone->rx_snr);
|
|
mockService->releaseToPool(toPhone);
|
|
}
|
|
|
|
// Every other ACK/NAK passes no relay source and must reach the phone with the relay fields clear,
|
|
// so the client is never told a relayer we did not hear.
|
|
static void test_localAck_withoutRelaySource_leavesRelayFieldsUnset()
|
|
{
|
|
installRealRoutingModule();
|
|
|
|
realRoutingModule->sendAckNak(meshtastic_Routing_Error_NONE, LOCAL_NODE, 0x0BADF00D, 0);
|
|
|
|
meshtastic_MeshPacket *toPhone = mockService->getForPhone();
|
|
TEST_ASSERT_NOT_NULL(toPhone);
|
|
TEST_ASSERT_EQUAL_UINT32(0x0BADF00D, toPhone->decoded.request_id);
|
|
TEST_ASSERT_EQUAL_HEX8(NO_RELAY_NODE, toPhone->relay_node);
|
|
TEST_ASSERT_FALSE(toPhone->has_rx_rssi);
|
|
mockService->releaseToPool(toPhone);
|
|
}
|
|
|
|
// The mirror of the above: a broadcast we originated, heard back off the mesh, must not reach the
|
|
// phone even though it travels the same RoutingModule path.
|
|
static void test_ownBroadcastEcho_isDroppedByRealRoutingModule()
|
|
{
|
|
installRealRoutingModule();
|
|
|
|
meshtastic_MeshPacket echo = meshtastic_MeshPacket_init_zero;
|
|
echo.from = LOCAL_NODE;
|
|
echo.to = NODENUM_BROADCAST;
|
|
echo.id = 0x5E1F0004;
|
|
echo.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
|
echo.decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP;
|
|
|
|
MeshModule::callModules(echo, RX_SRC_RADIO);
|
|
|
|
TEST_ASSERT_NULL(mockService->getForPhone());
|
|
}
|
|
|
|
// Full loop: a phone-originated want_response request (from == 0, RX_SRC_USER) dispatched
|
|
// through the real router must produce a module reply that reaches the phone queue.
|
|
static void test_phoneRequest_replyReachesPhone()
|
|
{
|
|
auto *replyOwner = registerDispatchModule(
|
|
new SyntheticReplyModule("private-owner", meshtastic_PortNum_PRIVATE_APP, meshtastic_PortNum_PRIVATE_APP));
|
|
|
|
meshtastic_MeshPacket request = meshtastic_MeshPacket_init_zero;
|
|
request.from = 0; // phone-originated, as MeshService::handleToRadio stamps it
|
|
request.to = LOCAL_NODE;
|
|
request.id = 0x5EED0001;
|
|
request.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
|
request.decoded.portnum = meshtastic_PortNum_PRIVATE_APP;
|
|
request.decoded.want_response = true;
|
|
|
|
service->sendToMesh(packetPool.allocCopy(request), RX_SRC_USER);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(1, replyOwner->allocReplyCalls);
|
|
|
|
meshtastic_MeshPacket *toPhone = mockService->getForPhone();
|
|
TEST_ASSERT_NOT_NULL(toPhone);
|
|
TEST_ASSERT_EQUAL(meshtastic_PortNum_PRIVATE_APP, toPhone->decoded.portnum);
|
|
TEST_ASSERT_EQUAL_UINT32(0x5EED0001, toPhone->decoded.request_id);
|
|
TEST_ASSERT_EQUAL_UINT32(LOCAL_NODE, toPhone->to);
|
|
mockService->releaseToPool(toPhone);
|
|
TEST_ASSERT_NULL(mockService->getForPhone()); // the request itself must not echo back
|
|
|
|
// One QueueStatus for the request, one for the reply, both reporting success
|
|
uint32_t statuses = 0;
|
|
while (auto *qs = mockService->getQueueStatusForPhone()) {
|
|
TEST_ASSERT_EQUAL(ERRNO_OK, qs->res);
|
|
mockService->releaseQueueStatusToPool(qs);
|
|
statuses++;
|
|
}
|
|
TEST_ASSERT_EQUAL_UINT32(2, statuses);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRouter->sentPackets.size());
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRoutingModule->ackNaks.size());
|
|
}
|
|
|
|
// A module that fans out a self-addressed reply and a local broadcast from inside callModules()
|
|
// must not re-enter handleReceived(): the sends are deferred and drained flat (max depth 1).
|
|
static void test_nestedLocalSend_isDeferred_notReentrant()
|
|
{
|
|
auto *mod = registerDispatchModule(new NestedFanoutModule());
|
|
|
|
dispatchTrigger(meshtastic_PortNum_PRIVATE_APP);
|
|
|
|
// The module ran once and was never re-entered; the drain left nothing behind.
|
|
TEST_ASSERT_EQUAL_UINT32(1, mod->handleCalls);
|
|
TEST_ASSERT_EQUAL_UINT8(1, mockRouter->maxHandleDepthObserved);
|
|
TEST_ASSERT_EQUAL_UINT8(0, mockRouter->deferredLocalPending());
|
|
|
|
// The self-addressed reply reached the phone exactly once (composition with #11185's ccToPhone).
|
|
meshtastic_MeshPacket *toPhone = mockService->getForPhone();
|
|
TEST_ASSERT_NOT_NULL(toPhone);
|
|
TEST_ASSERT_EQUAL_UINT32(LOCAL_NODE, toPhone->to);
|
|
TEST_ASSERT_EQUAL_UINT32(0xBEEF, toPhone->decoded.request_id);
|
|
mockService->releaseToPool(toPhone);
|
|
TEST_ASSERT_NULL(mockService->getForPhone()); // the broadcast did not also go to the phone
|
|
|
|
// The broadcast still reached the radio TX path exactly once.
|
|
TEST_ASSERT_EQUAL_UINT32(1, mockRouter->sentPackets.size());
|
|
TEST_ASSERT_TRUE(isBroadcast(mockRouter->sentPackets[0].to));
|
|
}
|
|
|
|
// A drained packet whose own module sends again is picked up by the same drain pass, so a chain of
|
|
// local sends is processed breadth-first with the stack held flat (max depth 1).
|
|
static void test_deferredChain_drainsBreadthFirst()
|
|
{
|
|
auto *mod = registerDispatchModule(new ChainSendModule());
|
|
|
|
dispatchTrigger(meshtastic_PortNum_PRIVATE_APP, 1); // generation 1
|
|
|
|
// Generations 1 -> 2 -> 3 were all processed in the single outermost drain, never nesting.
|
|
TEST_ASSERT_EQUAL_UINT32(3, mod->handleCalls);
|
|
TEST_ASSERT_EQUAL_UINT32(3, mod->maxGeneration);
|
|
TEST_ASSERT_EQUAL_UINT8(1, mockRouter->maxHandleDepthObserved);
|
|
TEST_ASSERT_EQUAL_UINT8(0, mockRouter->deferredLocalPending());
|
|
}
|
|
|
|
// Overflowing the fixed deferred queue drops the excess deferrals gracefully: no crash, no leak,
|
|
// depth still capped, and every send's phone cc still happens (return codes unchanged).
|
|
static void test_deferredQueueOverflow_dropsGracefully()
|
|
{
|
|
const uint32_t burst = 6; // deferredLocalCapacity (4) + 2 - keep in sync with Router.h
|
|
|
|
registerDispatchModule(new BurstSendModule(burst));
|
|
|
|
dispatchTrigger(meshtastic_PortNum_PRIVATE_APP);
|
|
|
|
// Two deferrals were dropped (queue full) and nothing re-entered handleReceived().
|
|
TEST_ASSERT_EQUAL_UINT32(2, mockRouter->deferredLocalDropped);
|
|
TEST_ASSERT_EQUAL_UINT8(1, mockRouter->maxHandleDepthObserved);
|
|
TEST_ASSERT_EQUAL_UINT8(0, mockRouter->deferredLocalPending()); // drained clean
|
|
|
|
// Return codes were unchanged: every reply still cc'd to the phone, dropped deferral or not.
|
|
uint32_t delivered = 0;
|
|
while (auto *toPhone = mockService->getForPhone()) {
|
|
mockService->releaseToPool(toPhone);
|
|
delivered++;
|
|
}
|
|
TEST_ASSERT_EQUAL_UINT32(burst, delivered);
|
|
}
|
|
|
|
void setup()
|
|
{
|
|
initializeTestEnvironment();
|
|
|
|
UNITY_BEGIN();
|
|
RUN_TEST(test_zeroHopBroadcast_isAllowed);
|
|
RUN_TEST(test_multiHopBroadcast_isBlocked);
|
|
RUN_TEST(test_directMessage_isAllowed);
|
|
RUN_TEST(test_fullyRelayedBroadcast_isBlocked);
|
|
RUN_TEST(test_noCurrentRequest_isAllowed);
|
|
RUN_TEST(test_legacyPacket_zeroHopStart_isAllowed);
|
|
RUN_TEST(test_singleHopRelayedBroadcast_isBlocked);
|
|
RUN_TEST(test_replyPortMatches_ownPort);
|
|
RUN_TEST(test_replyPortMatches_neighborInfoVsTelemetry);
|
|
RUN_TEST(test_replyPortMatches_positionRequest);
|
|
RUN_TEST(test_replyPortMatches_unknownModulePort);
|
|
RUN_TEST(test_replyPortMatches_unknownRequestPort);
|
|
RUN_TEST(test_dispatch_foreignPortOffenderCannotShadowOwner);
|
|
RUN_TEST(test_dispatch_ownerPortStillReplies);
|
|
RUN_TEST(test_dispatch_crossPortReplyUsesRequestOwner);
|
|
RUN_TEST(test_dispatch_foreignPortObserverCanSuppressNak);
|
|
RUN_TEST(test_dispatch_noResponderSendsNak);
|
|
RUN_TEST(test_dispatch_ignoreRequestIsClearedPerPacket);
|
|
RUN_TEST(test_dispatch_realNeighborInfoCannotShadowTelemetryOwner);
|
|
RUN_TEST(test_localReplyToSelf_isDeliveredToPhone);
|
|
RUN_TEST(test_handleFromRadio_remotePacketReachesPhone);
|
|
RUN_TEST(test_handleFromRadio_ownPacketIsNotEchoedToPhone);
|
|
RUN_TEST(test_handleFromRadio_ownPacketAddressedToUsReachesPhone);
|
|
RUN_TEST(test_localAckNak_reachesPhoneViaRealRoutingModule);
|
|
RUN_TEST(test_localAck_carriesRelaySourceToPhone);
|
|
RUN_TEST(test_localAck_withoutRelaySource_leavesRelayFieldsUnset);
|
|
RUN_TEST(test_ownBroadcastEcho_isDroppedByRealRoutingModule);
|
|
RUN_TEST(test_phoneRequest_replyReachesPhone);
|
|
RUN_TEST(test_nestedLocalSend_isDeferred_notReentrant);
|
|
RUN_TEST(test_deferredChain_drainsBreadthFirst);
|
|
RUN_TEST(test_deferredQueueOverflow_dropsGracefully);
|
|
exit(UNITY_END());
|
|
}
|
|
|
|
void loop() {}
|