Files
firmware/test/test_nodeinfo_send_window/test_main.cpp
T
Tom 332c4d7c6f Narrow the ad-hoc NodeInfo greeting (#11897)
* feat(nodedb): greet only while the node store is under half full

The ad-hoc greeting in MeshService::handleFromRadio() was gated on
!isFull(), so a node kept sending unsolicited NodeInfo right up to the
last free slot - on a dense mesh that is the regime where the store is
already churning and the greeting is least likely to buy a lasting
entry.

Add NodeDB::isHalfEmpty(), true only when strictly more than half the
slots are free, and gate the greeting on it instead. The comparison is
written as 2 * numMeshNodes < cap so a half-full store reads false with
no integer rounding, and MAX_NUM_NODES is read into a local because
portduino resolves it through a runtime call.

The helper keeps the MINIMUM_SAFE_FREE_HEAP term that !isFull() used to
contribute: low heap disqualifies the store regardless of occupancy, so
a sparse database on a memory-starved device still does not transmit.

Admission is untouched - updateFrom() and getOrCreateMeshNode() still
fill to capacity. Only greeting stops early.

* fix(nodeinfo): raise the minimum greeting window to 30 minutes

The !shorterTimeout branch of NodeInfoModule::allocReply() used a
10-minute base, so a node that had just greeted one neighbour could
greet the next ten minutes later. Raise the base to 30 minutes.

This is the floor, not the window: getConfiguredOrDefaultMsScaled()
still multiplies by the congestion coefficient for the roles that scale,
so a busy mesh stretches it further. ROUTER/ROUTER_LATE and the
tracker/sensor roles bypass the scaling and get a flat 30 minutes.

The interactive paths are unaffected - they pass shorterTimeout and keep
their own 60-second gate. The periodic broadcast is unaffected too:
default_node_info_broadcast_secs is 3 hours with a 1-hour minimum, both
clear of the new floor, so the timer is not swallowed by the throttle.

* fix(nodeinfo): a send restarts the routine broadcast countdown

sendOurNodeInfo() left the OSThread schedule alone, so an ad-hoc send
had no effect on the periodic broadcast: run() anchors the next run at
runned() + interval, and nothing re-anchored it when the send came from
a greeting, a PKI decrypt failure or a completed key verification. The
routine copy could follow minutes behind an ad-hoc one, putting two
NodeInfos on the air for no gain.

Call setIntervalFromNow() with the configured broadcast interval once
the packet is queued, so the next periodic copy is a full interval from
the send rather than from the last tick.

It sits on the return-true path only: a send vetoed by allocReply() -
throttle, airtime ceiling, reply suppression - must not be able to
silence the routine broadcast. Calling it from inside runOnce() is
harmless, since run() then applies the same interval from a last_run of
effectively now.

* test(nodeinfo): cover the send window, the countdown reset and the greeting gate

Three behaviours from this branch had no coverage: isHalfEmpty()'s exclusive
boundary, the 30-minute send floor, and the countdown reset on a send.

isHalfEmpty() goes to test_nodedb_blocked, which already owns the full-store
cases and clears the hot store per test. Three tests sweep the cap over the
sizes real deployments have - portduino resolves MAX_NUM_NODES from
General.MaxNodes on every read, so a predicate that cached it would greet at the
wrong occupancy - and pin the band where admission outlives greeting. That suite
had no tearDown; it has one now, restoring the cap so an assertion firing
mid-sweep cannot leak a 2-node cap into the tests after it.

test_nodeinfo_send_window is new because nothing in the tree stands up
NodeInfoModule's send path. Six tests: the floor at 30 minutes with 10 refused,
the interactive 60-second gate staying separate, the countdown re-armed by a
broadcast and by an ad-hoc unicast, left alone by a refused send, and a preset
change consumed only by a send that goes out.

The scaling above 40 online nodes is deliberately not retested here -
getConfiguredOrDefaultMsScaled() is test_default's contract, per preset and per
role. These tests pin the base and leave the multiplier alone.

NodeInfoModule gains two PIO_UNIT_TESTING accessors for the countdown:
concurrency::OSThread is a private base, so a test shim cannot reach it and only
the class itself can. They compile out of a shipping build.

The heap term in isHalfEmpty()/isFull() stays uncovered: memGet.getFreeHeap()
returns UINT32_MAX on portduino, so a native test could only pin a stub.

* chore(trunk): exempt test_nodedb_blocked from the trufflehog Lob detector

test_removeNodeByNum_presentNodeOnFullDb is exactly 35 characters after the
test_ prefix, which is the length of a Lob API key, and trufflehog's detector
matches the bare identifier. The name is years old; it surfaces now only because
this branch touches the file, and the pre-push gate reports a finding in a
changed file as new.

Added to the ignore block that already carries the same detector's hex-literal
false positives, with the reason stated alongside them. Nothing in that file is
a credential.

* fix(nodeinfo): exempt a licensed station from the floor, delay only on a real send

Two review findings on the 30-minute window.

Ham mode sets node_info_broadcast_secs to 600 s for the FCC minimum call-sign
announcement (AdminModule.cpp). The new floor refused every one of those sends
until 30 minutes had passed, so a licensed station's call sign went out three
times less often than the regulation asks - a regression the old 10-minute base
did not have. A licensed station now keeps its own interval whenever that is
shorter than the floor. The exemption is exactly the licensed case because
nothing else can get under the floor: a set-config clamps the field to an hour,
and the userprefs path clamps identically.

sendOurNodeInfo() ignored what sendToMesh() returned, so a packet the router
declined - no interface, queue full - still re-armed the routine broadcast and
still reported success, which let runOnce() consume a pending channel change
for a send that never reached the air. Only ERRNO_OK and ERRNO_SHOULD_RELEASE
now count; sendToMesh() has already released the packet in both cases.

Both are pinned by tests that fail without them, measured: the licensed case
fails at "11 min is past it, and the floor must not override it", the declined
send at "a declined send is not a send". The licensed test carries an unlicensed
control on the same configuration, so deleting the floor outright would not
satisfy it.

* test(nodeinfo): assert the deadline the scheduler reads, from an aged last_run

The countdown cases asserted Thread::interval, which is not what schedules the
next run: shouldRun() keys off _cached_next_run, and the two ways of writing it
differ. setIntervalFromNow() recomputes it from now; Thread::setInterval()
recomputes it from last_run. Swap the call in sendOurNodeInfo() for the latter
and the period still reads three hours while the deadline lands wherever the
last tick was - firing the routine copy right behind an ad-hoc send, the exact
thing the reset exists to prevent. Every test passed.

Assert the deadline instead, from a fixture where the two answers are
distinguishable: ageLastRunForTests() calls Thread::runned() with an hour-old
timestamp, the state a periodic thread is genuinely in between runs, so a
deadline off last_run lands an hour early against a five second tolerance.

Measured: with setInterval() in place of setIntervalFromNow(), the new case
fails by 3600004 ms and the eight others pass, including the one asserting the
period - which is what says the old assertion could not see this.

runned() and _cached_next_run are protected in Thread and OSThread is a private
base, so the hooks live on NodeInfoModule, with the two already there.

Raised by Copilot on #11897.

* fix(nodeinfo): a declined send must not start the throttle window either

allocReply() stamped TransmitHistory when it built the packet, before anything
had been sent. The previous commit made sendOurNodeInfo() report a router
rejection instead of swallowing it, but the stamp was already written by then,
so a packet that never reached the air still started the window - and with the
floor now at 30 minutes, that silences the node for half an hour over a send
that failed.

allocReply() has two callers and only one of them can see the outcome: the
module framework sends its own reply through currentReply, with no post-send
hook a module can reach (MeshModule::sendResponse is not virtual). So the stamp
stays there for that path, and sendOurNodeInfo() defers it across its own
allocReply() call and stamps once the router has accepted the packet.
deferHistoryStamp mirrors the shorterTimeout member alongside it - same
call-scoped signal, same lifetime.

test_sendWindow_aRejectedSendDoesNotStartTheWindow asserts both halves: no stamp
after the rejection, and the retry immediately after goes out. The existing
rejected-send case checked the first failure and the countdown only, which is
how this survived it.

248/248 across every suite that touches NodeInfoModule (admin_session_repro,
admin_radio, nodeinfo_send_window, traffic_management, fuzz_packets) plus
transmit_history, whose subject this is.

Raised by CodeRabbit on #11897.
2026-09-20 10:32:41 +00:00

381 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())); }
// Router's constructor asserts cryptLock is null before allocating it, so a per-test router can
// only be rebuilt if the previous one hands the global back.
~MockRouter()
{
delete cryptLock;
cryptLock = nullptr;
}
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() {}