mirror of
https://github.com/meshtastic/firmware.git
synced 2026-10-09 06:31:35 -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>
373 lines
17 KiB
C++
373 lines
17 KiB
C++
// NodeInfoModule's send window and the routine-broadcast countdown: allocReply(),
|
|
// sendOurNodeInfo() and runOnce() in src/modules/NodeInfoModule.cpp.
|
|
//
|
|
// Two contracts, both properties of the module rather than of the scaler it calls:
|
|
//
|
|
// 1. The non-interactive window has a 30 minute floor. allocReply() passes 30 * 60 as the base to
|
|
// Default::getConfiguredOrDefaultMsScaled(); what that base becomes at mesh sizes over 40 nodes,
|
|
// per modem preset and per role, is Default's own contract and is covered in test_default -
|
|
// these tests pin the base and leave the multiplier alone. The interactive path (shorterTimeout,
|
|
// used by a user-triggered send, a PKI decrypt failure and a completed key verification) keeps
|
|
// its separate 60 second gate and must not inherit the floor.
|
|
//
|
|
// 2. A send that goes out re-arms the routine broadcast, and a send that is refused does not.
|
|
// sendOurNodeInfo() calls setIntervalFromNow() with the configured broadcast interval once the
|
|
// packet is queued, so an ad-hoc send is not followed minutes later by the periodic copy. The
|
|
// reset sits on the return-true path deliberately: if a refused send could re-arm it, a node
|
|
// that keeps attempting greetings inside the window would defer its broadcast indefinitely and
|
|
// go silent - the opposite of the intent.
|
|
//
|
|
// A preset or channel change (radioGeneration) rides on the same path: runOnce() asks for replies
|
|
// while currentGeneration != radioGeneration and copies the generation across only on a true
|
|
// return, so a refused send has to leave the request pending for the next attempt.
|
|
//
|
|
// Regressions guarded: reverting the base to 10 * 60, the value this branch replaced; hoisting the
|
|
// setIntervalFromNow() call above the veto checks or onto the false path; and moving the generation
|
|
// copy out of `if (sendOurNodeInfo(...))`, which loses a preset change to a throttled send so the
|
|
// mesh is never asked to re-introduce itself.
|
|
//
|
|
// The window probes step an injected clock (Time::setTestMillis) rather than sleeping;
|
|
// TransmitHistory, which is where allocReply() reads "last sent" from, reads the same clock.
|
|
#include "MeshTypes.h" // BEFORE TestUtil.h
|
|
#include "TestUtil.h"
|
|
#include <unity.h>
|
|
|
|
#if defined(ARCH_PORTDUINO)
|
|
#define NI_TEST_ENTRY extern "C"
|
|
#else
|
|
#define NI_TEST_ENTRY
|
|
#endif
|
|
|
|
#include "Default.h"
|
|
#include "NodeStatus.h"
|
|
#include "UptimeClock.h"
|
|
#include "airtime.h"
|
|
#include "mesh/NodeDB.h"
|
|
#include "mesh/RadioInterface.h"
|
|
#include "mesh/Router.h"
|
|
#include "mesh/TransmitHistory.h"
|
|
#include "modules/NodeInfoModule.h"
|
|
#include "support/MockMeshService.h"
|
|
#include <memory>
|
|
#include <vector>
|
|
|
|
// Exposes the protected periodic entry point. The countdown itself is read through NodeInfoModule's
|
|
// own ForTests accessors: OSThread is a private base, so a shim cannot reach it. Reading the interval
|
|
// rather than a deadline keeps these assertions off wall-clock millis(), which the test clock does not drive.
|
|
class NodeInfoModuleTestShim : public NodeInfoModule
|
|
{
|
|
public:
|
|
using NodeInfoModule::runOnce;
|
|
};
|
|
|
|
namespace
|
|
{
|
|
|
|
// sendLocal() is not virtual and refuses to send with no interface attached, so the mock router
|
|
// carries a stub one. Only send() and getPacketTime() are pure virtual, and init() - which is what
|
|
// observes config and sleep notifications - is never called.
|
|
class StubRadioInterface : 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 { return 100; }
|
|
};
|
|
|
|
class MockRouter : public Router
|
|
{
|
|
public:
|
|
MockRouter() { addInterface(std::unique_ptr<RadioInterface>(new StubRadioInterface())); }
|
|
|
|
ErrorCode send(meshtastic_MeshPacket *p) override
|
|
{
|
|
sentPackets.push_back(*p);
|
|
packetPool.release(p); // released here either way: the interface owns the packet it declined
|
|
return sendResult;
|
|
}
|
|
|
|
ErrorCode sendResult = ERRNO_OK;
|
|
|
|
// The broadcast loopback copy lands here; release rather than queue into fromRadioQueue, which
|
|
// nothing drains in tests.
|
|
void enqueueReceivedMessage(meshtastic_MeshPacket *p) override { packetPool.release(p); }
|
|
|
|
std::vector<meshtastic_MeshPacket> sentPackets;
|
|
};
|
|
|
|
NodeInfoModuleTestShim *mod = nullptr;
|
|
MockMeshService *mockSvc = nullptr;
|
|
MockRouter *mockRouter = nullptr;
|
|
AirTime *testAirTime = nullptr;
|
|
|
|
constexpr uint32_t kClockBaseMs = 60 * 60 * 1000; // an hour in, so no probe can underflow
|
|
constexpr uint32_t kThirtyMinMs = 30 * 60 * 1000;
|
|
constexpr uint32_t kThreeHoursMs = 3 * 60 * 60 * 1000;
|
|
|
|
// Stamp "we sent a NodeInfo just now", jump the clock forward, and report whether another send is
|
|
// allowed. A permitted send re-stamps the history, which is why every probe stamps first.
|
|
bool sendAllowedAfterMs(uint32_t elapsedMs, bool shorterTimeout = false)
|
|
{
|
|
transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP);
|
|
Time::advanceTestMillis(elapsedMs);
|
|
return mod->sendOurNodeInfo(NODENUM_BROADCAST, false, 0, shorterTimeout);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void setUp(void)
|
|
{
|
|
Time::resetMonotonicForTests();
|
|
Time::setTestMillis(kClockBaseMs);
|
|
Time::serviceMonotonic();
|
|
|
|
testAirTime = new AirTime();
|
|
airTime = testAirTime;
|
|
|
|
mockSvc = new MockMeshService();
|
|
service = mockSvc;
|
|
|
|
mockRouter = new MockRouter();
|
|
router = mockRouter;
|
|
|
|
if (transmitHistory) {
|
|
delete transmitHistory;
|
|
transmitHistory = nullptr;
|
|
}
|
|
transmitHistory = TransmitHistory::getInstance(); // fresh: loadFromDisk() is not called
|
|
|
|
// The congestion coefficient is 1.0 at or below 40 online nodes, so the window under test is
|
|
// the bare floor. Nothing wires the node-status observer in a test build, but assert it rather
|
|
// than assume it - a non-zero count here would silently stretch every boundary below.
|
|
TEST_ASSERT_NOT_NULL(nodeStatus);
|
|
TEST_ASSERT_EQUAL_UINT16_MESSAGE(0, nodeStatus->getNumOnline(), "these boundaries assume an unscaled window");
|
|
|
|
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT;
|
|
config.device.node_info_broadcast_secs = 0; // 0 selects the default, 3 hours
|
|
|
|
owner.is_licensed = false;
|
|
strncpy(owner.long_name, "send window", sizeof(owner.long_name) - 1);
|
|
strncpy(owner.short_name, "sw", sizeof(owner.short_name) - 1);
|
|
|
|
radioGeneration = 1;
|
|
mod = new NodeInfoModuleTestShim();
|
|
nodeInfoModule = mod;
|
|
|
|
// Settle the startup generation so no test inherits a pending "ask for replies", then drop the
|
|
// stamp that settling send left behind: every test starts unthrottled, and the ones that need a
|
|
// refusal arm the floor themselves.
|
|
mod->runOnce();
|
|
mockRouter->sentPackets.clear();
|
|
delete transmitHistory;
|
|
transmitHistory = nullptr;
|
|
transmitHistory = TransmitHistory::getInstance();
|
|
}
|
|
|
|
void tearDown(void)
|
|
{
|
|
nodeInfoModule = nullptr;
|
|
delete mod;
|
|
mod = nullptr;
|
|
|
|
// sendToMesh() copies a queue status to the phone on every send; toPhoneQueue takes ownership
|
|
// and nothing else drains it, so release them or LeakSanitizer aborts the run.
|
|
if (mockSvc) {
|
|
meshtastic_MeshPacket *p;
|
|
while ((p = mockSvc->getForPhone()) != nullptr)
|
|
mockSvc->releaseToPool(p);
|
|
}
|
|
|
|
service = nullptr;
|
|
delete mockSvc;
|
|
mockSvc = nullptr;
|
|
|
|
router = nullptr;
|
|
delete mockRouter;
|
|
mockRouter = nullptr;
|
|
|
|
airTime = nullptr;
|
|
delete testAirTime;
|
|
testAirTime = nullptr;
|
|
|
|
delete transmitHistory;
|
|
transmitHistory = nullptr;
|
|
|
|
Time::useRealClock();
|
|
}
|
|
|
|
// The floor is 30 minutes, not the 10 it used to be: 10 and 29:59 are refused, 30:01 is not.
|
|
static void test_sendWindow_floorIsThirtyMinutes(void)
|
|
{
|
|
TEST_ASSERT_FALSE_MESSAGE(sendAllowedAfterMs(10 * 60 * 1000), "10 min must be inside the window");
|
|
TEST_ASSERT_FALSE_MESSAGE(sendAllowedAfterMs(kThirtyMinMs - 1000), "29:59 must be inside the window");
|
|
TEST_ASSERT_TRUE_MESSAGE(sendAllowedAfterMs(kThirtyMinMs + 1000), "30:01 must be past the window");
|
|
}
|
|
|
|
// The interactive path keeps its own 60 second gate. Raising the routine floor must not have raised
|
|
// it, or a user-triggered send and a key verification would wait out half an hour.
|
|
static void test_sendWindow_interactiveSendKeepsItsSixtySecondGate(void)
|
|
{
|
|
TEST_ASSERT_FALSE_MESSAGE(sendAllowedAfterMs(30 * 1000, /*shorterTimeout=*/true), "30 s is inside the 60 s gate");
|
|
TEST_ASSERT_TRUE_MESSAGE(sendAllowedAfterMs(61 * 1000, /*shorterTimeout=*/true), "61 s is past the 60 s gate");
|
|
TEST_ASSERT_TRUE_MESSAGE(sendAllowedAfterMs(5 * 60 * 1000, /*shorterTimeout=*/true),
|
|
"5 min must pass the interactive gate while still inside the routine floor");
|
|
}
|
|
|
|
// A send that goes out re-arms the countdown to a full interval - the default, and the configured
|
|
// value when there is one. Arming 1 ms first means only the reset can produce the expected value.
|
|
static void test_broadcastTimer_aSendRearmsTheRoutineCountdown(void)
|
|
{
|
|
mod->armBroadcastCountdownForTests(1);
|
|
TEST_ASSERT_TRUE(mod->sendOurNodeInfo(NODENUM_BROADCAST, false, 0, false));
|
|
TEST_ASSERT_EQUAL_UINT32(Default::getConfiguredOrDefaultMs(0, default_node_info_broadcast_secs),
|
|
(uint32_t)mod->broadcastCountdownMsForTests());
|
|
TEST_ASSERT_EQUAL_UINT32(kThreeHoursMs, (uint32_t)mod->broadcastCountdownMsForTests());
|
|
|
|
config.device.node_info_broadcast_secs = 4 * 60 * 60;
|
|
mod->armBroadcastCountdownForTests(1);
|
|
Time::advanceTestMillis(kThirtyMinMs + 1000);
|
|
TEST_ASSERT_TRUE(mod->sendOurNodeInfo(NODENUM_BROADCAST, false, 0, false));
|
|
TEST_ASSERT_EQUAL_UINT32(4 * 60 * 60 * 1000, (uint32_t)mod->broadcastCountdownMsForTests());
|
|
}
|
|
|
|
// interval is not what the scheduler reads: shouldRun() keys off _cached_next_run, and
|
|
// Thread::setInterval() recomputes that from last_run while setIntervalFromNow() recomputes it from
|
|
// now. Age last_run by an hour first and the two answers differ by an hour, so this case fails if
|
|
// the send ever re-arms the period without moving the deadline - which would fire the routine copy
|
|
// straight after an ad-hoc send, the exact thing the reset exists to prevent.
|
|
static void test_broadcastTimer_aSendMovesTheDeadlineNotJustThePeriod(void)
|
|
{
|
|
const unsigned long ageMs = 60 * 60 * 1000;
|
|
mod->ageLastRunForTests(ageMs);
|
|
TEST_ASSERT_TRUE(mod->sendOurNodeInfo(NODENUM_BROADCAST, false, 0, false));
|
|
|
|
const unsigned long remaining = mod->broadcastDeadlineMsForTests() - millis();
|
|
TEST_ASSERT_UINT32_WITHIN_MESSAGE(5000, kThreeHoursMs, (uint32_t)remaining,
|
|
"the next routine broadcast is due a full interval from the send, not from the last tick");
|
|
}
|
|
|
|
// An ad-hoc unicast - the shape of a greeting, a PKI decrypt failure or a completed key
|
|
// verification - re-arms the countdown just as a broadcast does. Without it the routine copy
|
|
// follows the ad-hoc one within minutes, putting two NodeInfos on the air for no gain.
|
|
static void test_broadcastTimer_anAdHocUnicastRearmsItToo(void)
|
|
{
|
|
mod->armBroadcastCountdownForTests(1);
|
|
|
|
TEST_ASSERT_TRUE(mod->sendOurNodeInfo(0x12345678, true, 0, false));
|
|
TEST_ASSERT_EQUAL_UINT32(1, mockRouter->sentPackets.size());
|
|
TEST_ASSERT_EQUAL_HEX32(0x12345678, mockRouter->sentPackets[0].to);
|
|
TEST_ASSERT_EQUAL_UINT32(kThreeHoursMs, (uint32_t)mod->broadcastCountdownMsForTests());
|
|
}
|
|
|
|
// A refused send must leave the countdown exactly where it was, or a node that keeps attempting
|
|
// greetings inside the window defers its routine broadcast forever.
|
|
static void test_broadcastTimer_aRefusedSendLeavesTheCountdownAlone(void)
|
|
{
|
|
transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP);
|
|
Time::advanceTestMillis(60 * 1000); // a minute later: well inside the floor
|
|
|
|
const unsigned long sentinel = 4321;
|
|
mod->armBroadcastCountdownForTests(sentinel);
|
|
|
|
TEST_ASSERT_FALSE_MESSAGE(mod->sendOurNodeInfo(NODENUM_BROADCAST, false, 0, false), "the floor must refuse this send");
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRouter->sentPackets.size());
|
|
TEST_ASSERT_EQUAL_UINT32(sentinel, (uint32_t)mod->broadcastCountdownMsForTests());
|
|
}
|
|
|
|
// A licensed station announces its call sign on a regulatory interval - ham mode sets
|
|
// node_info_broadcast_secs to 600 s for the FCC minimum - and the floor must not stretch that to 30
|
|
// minutes. The unlicensed control below is the same configuration without the licence, so the
|
|
// assertion cannot pass by the floor quietly disappearing for everyone.
|
|
static void test_sendWindow_aLicensedStationKeepsItsCallSignInterval(void)
|
|
{
|
|
config.device.node_info_broadcast_secs = 600;
|
|
|
|
owner.is_licensed = true;
|
|
TEST_ASSERT_FALSE_MESSAGE(sendAllowedAfterMs(5 * 60 * 1000), "5 min is inside the station's own 10 min interval");
|
|
TEST_ASSERT_TRUE_MESSAGE(sendAllowedAfterMs(11 * 60 * 1000), "11 min is past it, and the floor must not override it");
|
|
|
|
owner.is_licensed = false;
|
|
TEST_ASSERT_FALSE_MESSAGE(sendAllowedAfterMs(11 * 60 * 1000), "without a licence the 30 minute floor still applies");
|
|
}
|
|
|
|
// A send the router declines never reached the air. It must not defer the routine broadcast, and it
|
|
// must report failure so runOnce() does not treat a pending channel change as delivered.
|
|
static void test_broadcastTimer_aRejectedSendLeavesTheCountdownAlone(void)
|
|
{
|
|
const unsigned long sentinel = 8765;
|
|
mod->armBroadcastCountdownForTests(sentinel);
|
|
mockRouter->sendResult = ERRNO_NO_INTERFACES;
|
|
|
|
TEST_ASSERT_FALSE_MESSAGE(mod->sendOurNodeInfo(NODENUM_BROADCAST, false, 0, false), "a declined send is not a send");
|
|
TEST_ASSERT_EQUAL_UINT32(sentinel, (uint32_t)mod->broadcastCountdownMsForTests());
|
|
}
|
|
|
|
// The countdown is only half of it: allocReply() used to stamp TransmitHistory when it built the
|
|
// packet, so a send the router then declined still started the window. With a 30 minute floor that
|
|
// silences the node for half an hour over a packet that never left. The retry immediately after must
|
|
// go out.
|
|
static void test_sendWindow_aRejectedSendDoesNotStartTheWindow(void)
|
|
{
|
|
mockRouter->sendResult = ERRNO_NO_INTERFACES;
|
|
TEST_ASSERT_FALSE_MESSAGE(mod->sendOurNodeInfo(NODENUM_BROADCAST, false, 0, false), "the router declined this one");
|
|
TEST_ASSERT_EQUAL_UINT32_MESSAGE(0, transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP),
|
|
"a declined send must leave no transmit stamp behind");
|
|
|
|
mockRouter->sendResult = ERRNO_OK;
|
|
mockRouter->sentPackets.clear();
|
|
TEST_ASSERT_TRUE_MESSAGE(mod->sendOurNodeInfo(NODENUM_BROADCAST, false, 0, false),
|
|
"the retry must not be throttled by the send that failed");
|
|
TEST_ASSERT_EQUAL_UINT32(1, mockRouter->sentPackets.size());
|
|
}
|
|
|
|
// A preset or channel change bumps radioGeneration, and only a send that goes out consumes it: the
|
|
// refused attempt leaves the ask pending, the next successful one carries want_response, and the
|
|
// one after that does not ask again.
|
|
static void test_presetChange_isConsumedOnlyByASendThatGoesOut(void)
|
|
{
|
|
radioGeneration++;
|
|
|
|
// Arm the floor so the first attempt is refused, which is the case under test.
|
|
transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP);
|
|
Time::advanceTestMillis(60 * 1000);
|
|
|
|
mod->runOnce();
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRouter->sentPackets.size());
|
|
|
|
Time::advanceTestMillis(kThirtyMinMs + 1000);
|
|
mod->runOnce();
|
|
TEST_ASSERT_EQUAL_UINT32(1, mockRouter->sentPackets.size());
|
|
TEST_ASSERT_TRUE_MESSAGE(mockRouter->sentPackets[0].decoded.want_response,
|
|
"a refused send must not consume the preset change");
|
|
TEST_ASSERT_EQUAL_HEX32(NODENUM_BROADCAST, mockRouter->sentPackets[0].to);
|
|
|
|
Time::advanceTestMillis(kThirtyMinMs + 1000);
|
|
mod->runOnce();
|
|
TEST_ASSERT_EQUAL_UINT32(2, mockRouter->sentPackets.size());
|
|
TEST_ASSERT_FALSE_MESSAGE(mockRouter->sentPackets[1].decoded.want_response,
|
|
"a settled generation must not keep asking for replies");
|
|
}
|
|
|
|
NI_TEST_ENTRY void setup()
|
|
{
|
|
initializeTestEnvironment();
|
|
nodeDB = new NodeDB();
|
|
|
|
UNITY_BEGIN();
|
|
RUN_TEST(test_sendWindow_floorIsThirtyMinutes);
|
|
RUN_TEST(test_sendWindow_interactiveSendKeepsItsSixtySecondGate);
|
|
RUN_TEST(test_broadcastTimer_aSendRearmsTheRoutineCountdown);
|
|
RUN_TEST(test_broadcastTimer_aSendMovesTheDeadlineNotJustThePeriod);
|
|
RUN_TEST(test_broadcastTimer_anAdHocUnicastRearmsItToo);
|
|
RUN_TEST(test_broadcastTimer_aRefusedSendLeavesTheCountdownAlone);
|
|
RUN_TEST(test_broadcastTimer_aRejectedSendLeavesTheCountdownAlone);
|
|
RUN_TEST(test_sendWindow_aRejectedSendDoesNotStartTheWindow);
|
|
RUN_TEST(test_sendWindow_aLicensedStationKeepsItsCallSignInterval);
|
|
RUN_TEST(test_presetChange_isConsumedOnlyByASendThatGoesOut);
|
|
exit(UNITY_END());
|
|
}
|
|
NI_TEST_ENTRY void loop() {}
|