emdashes begone (#10847)

This commit is contained in:
Tom
2026-07-01 19:01:27 -05:00
committed by GitHub
parent dee94e0758
commit 3becaf2d95
276 changed files with 1795 additions and 1793 deletions
+20 -20
View File
@@ -1,10 +1,10 @@
# Native Unit Tests — Authoring Guide
# Native Unit Tests - Authoring Guide
This directory contains C++ unit tests that run on the host machine via PlatformIO's native environment. Tests use the [Unity](http://www.throwtheswitch.org/unity) framework.
## Running Tests
**Preferred: use `bin/run-tests.sh`** — it runs the `coverage` env (ASan/LSan sanitizers), cross-checks the number of suites that actually ran, and emits an unambiguous RED/AMBER/GREEN verdict:
**Preferred: use `bin/run-tests.sh`** - it runs the `coverage` env (ASan/LSan sanitizers), cross-checks the number of suites that actually ran, and emits an unambiguous RED/AMBER/GREEN verdict:
```bash
./bin/run-tests.sh # all suites
@@ -16,7 +16,7 @@ Exit codes: 0 = GREEN, 1 = RED, 2 = AMBER.
> **Copilot interface note:** When running tests via the Copilot chat interface, edits made through the chat may not be reflected in the on-disk files that the test binary reads. If tests pass in chat but fail locally (or vice versa), verify the files on disk match what you expect before trusting the result. Always confirm with a local terminal run.
**Raw `pio test` (no sanitizers, no verdict logic)** — use when you need to override the env or inspect verbose Unity output:
**Raw `pio test` (no sanitizers, no verdict logic)** - use when you need to override the env or inspect verbose Unity output:
```bash
# All test suites
@@ -31,7 +31,7 @@ pio test -e native -f test_your_module -vvv
**Never pipe through `| tail -N` to shorten output.** PlatformIO prints build errors at the top of output and test results at the bottom; `tail` will show stale cached results from a prior successful build while hiding the compile error that caused the current run to fail.
**Preferred pattern for raw pio — redirect to file, then grep:**
**Preferred pattern for raw pio - redirect to file, then grep:**
```bash
# Redirect all output to a file; grep for errors and results after it exits
@@ -113,7 +113,7 @@ See `.github/actions/setup-native/action.yml` for the canonical list.
test/test_your_module/test_main.cpp
```
One file per suite. No per-test `platformio.ini` is needed — tests build under the `[env:native]` environment defined in the root `platformio.ini`.
One file per suite. No per-test `platformio.ini` is needed - tests build under the `[env:native]` environment defined in the root `platformio.ini`.
### 2. File Skeleton
@@ -128,14 +128,14 @@ One file per suite. No per-test `platformio.ini` is needed — tests build under
#include "gps/RTC.h"
#include "mesh/NodeDB.h"
#include "modules/YourModule.h"
#include <cstdio> // required for printf() — used for blank-line group separators
#include <cstdio> // required for printf() - used for blank-line group separators
#include <cstring>
#include <memory>
// --- Test output helpers ---
// printf() writes directly to stdout and appears in -vv output as a plain line (no prefix).
// Use it for blank-line group separators: printf("\n");
// TEST_MESSAGE() emits a "file:line:INFO: <text>" line — visible at -vv and above.
// TEST_MESSAGE() emits a "file:line:INFO: <text>" line - visible at -vv and above.
// Use TEST_MSG_FMT for formatted diagnostic lines inside tests.
#define MSG_BUF_LEN 200
#define TEST_MSG_FMT(fmt, ...) do { \
@@ -159,13 +159,13 @@ void tearDown(void) { /* runs after every test */ }
void setup()
{
initializeTestEnvironment(); // MUST call — sets up RTC, OSThread, console
initializeTestEnvironment(); // MUST call - sets up RTC, OSThread, console
UNITY_BEGIN();
printf("\n=== Example group ===\n"); // header line to help find tests
RUN_TEST(test_example);
exit(UNITY_END()); // exit() required — Unity runner expects it
exit(UNITY_END()); // exit() required - Unity runner expects it
}
void loop() {}
@@ -238,7 +238,7 @@ class YourModuleTestShim : public YourModule
{
public:
// Pull protected methods into public scope via using.
// IMPORTANT: using requires the method to be protected (or public) in the base —
// IMPORTANT: using requires the method to be protected (or public) in the base -
// friend alone does NOT satisfy this. See pitfall #6.
using YourModule::runOnce;
using YourModule::someProtectedMethod;
@@ -248,7 +248,7 @@ class YourModuleTestShim : public YourModule
};
```
For methods you want to expose via `using`, use the conditional access-specifier pattern in the header — **not** plain `friend`:
For methods you want to expose via `using`, use the conditional access-specifier pattern in the header - **not** plain `friend`:
```cpp
// In YourModule.h, inside the class body:
@@ -309,7 +309,7 @@ void setUp(void) {
### 2. File-Scope Mutable Globals Persist Across Tests
Variables like `static uint8_t someDenominator = 8;` in the module `.cpp` file retain mutations from previous tests. This is distinct from member variables — it affects all instances.
Variables like `static uint8_t someDenominator = 8;` in the module `.cpp` file retain mutations from previous tests. This is distinct from member variables - it affects all instances.
**Fix:** Add a `static void resetGlobal()` method to the module and call it in `setUp()`.
@@ -352,7 +352,7 @@ Fixed-size data structures (hash sets, ring buffers) overflow when tests inject
### 6. Granting test access to private/protected members
PlatformIO defines `PIO_UNIT_TESTING` during `pio test` builds. Several production headers (`TransmitHistory.h`, `CryptoEngine.h`, `MQTT.h`, `RTC.h`) use this to gate test-only visibility changes. PlatformIO also defines `UNIT_TEST` in the same builds for backward compatibility, but that spelling is deprecated — always use `PIO_UNIT_TESTING` in new code. The established pattern for exposing a private method to a test shim **without widening production visibility**:
PlatformIO defines `PIO_UNIT_TESTING` during `pio test` builds. Several production headers (`TransmitHistory.h`, `CryptoEngine.h`, `MQTT.h`, `RTC.h`) use this to gate test-only visibility changes. PlatformIO also defines `UNIT_TEST` in the same builds for backward compatibility, but that spelling is deprecated - always use `PIO_UNIT_TESTING` in new code. The established pattern for exposing a private method to a test shim **without widening production visibility**:
```cpp
#ifdef PIO_UNIT_TESTING
@@ -363,7 +363,7 @@ PlatformIO defines `PIO_UNIT_TESTING` during `pio test` builds. Several producti
bool myMethod();
```
**Critical C++ rule:** a `using` declaration in a derived class (e.g. `using Base::myMethod`) requires `myMethod` to be `protected` or `public` in the base — `friend` alone does **not** satisfy this. Adding `friend class TestShim` while leaving the method `private` will still fail to compile. Use the conditional access-specifier pattern above, not `friend`.
**Critical C++ rule:** a `using` declaration in a derived class (e.g. `using Base::myMethod`) requires `myMethod` to be `protected` or `public` in the base - `friend` alone does **not** satisfy this. Adding `friend class TestShim` while leaving the method `private` will still fail to compile. Use the conditional access-specifier pattern above, not `friend`.
## setUp/tearDown Checklist
@@ -372,7 +372,7 @@ PlatformIO defines `PIO_UNIT_TESTING` during `pio test` builds. Several producti
- [ ] Set `nodeDB = mockNodeDB`
- [ ] Delete persisted state files (`FSCom.remove(...)`)
- [ ] Reset file-scope mutable globals
- [ ] Reset mock clock to a safe base value (e.g. `mockTime = ONE_HOUR_MS`) — prevents unsigned subtraction underflow in time-dependent logic
- [ ] Reset mock clock to a safe base value (e.g. `mockTime = ONE_HOUR_MS`) - prevents unsigned subtraction underflow in time-dependent logic
- [ ] Disable randomness/jitter flags
- [ ] In `tearDown`: null the global singleton pointer, restore flags
@@ -380,11 +380,11 @@ PlatformIO defines `PIO_UNIT_TESTING` during `pio test` builds. Several producti
A well-structured test suite follows this pattern:
1. **Topology/scenario builders** — static helper functions that set up specific test conditions
2. **Injection helpers** — simulate realistic traffic, time, or event patterns
3. **Scenario tests** — each builds a scenario, runs the module, asserts on outcomes
4. **Lifecycle tests** — state persistence, startup from blank, restart recovery
5. **Summary test** (optional) — emits a scenario table into the log for quick CI review
1. **Topology/scenario builders** - static helper functions that set up specific test conditions
2. **Injection helpers** - simulate realistic traffic, time, or event patterns
3. **Scenario tests** - each builds a scenario, runs the module, asserts on outcomes
4. **Lifecycle tests** - state persistence, startup from blank, restart recovery
5. **Summary test** (optional) - emits a scenario table into the log for quick CI review
## Existing Test Suites
+2 -2
View File
@@ -5,7 +5,7 @@ plus tooling that compiles them to binary `.proto` files and pushes them
onto a device for testing.
Centered on Truth or Consequences, NM (33.1284°N, 107.2528°W) with a 60 km
spread — change via `--centroid` and `--spread-km` if you want a different
spread - change via `--centroid` and `--spread-km` if you want a different
geography.
## Pipeline
@@ -52,7 +52,7 @@ the compile step to pin it for byte-identical CI artifacts.
1. Variants with `custom_meshtastic_support_level = 1` in `variants/*/*/platformio.ini`
2. Values present in the `HardwareModel` enum in `mesh.proto`
This excludes legacy/deprecated boards (Heltec V1–V2, TLORA V1–V2, classic
This excludes legacy/deprecated boards (Heltec V1-V2, TLORA V1-V2, classic
TBEAM (4) and TBEAM_V0P7 (6), Nano G1, Station G1/G2, etc.) and fuzzer-only
sentinels (PORTDUINO, ANDROID_SIM, DIY_V1, LORA_RELAY_V1, etc.).
+1 -1
View File
@@ -235,7 +235,7 @@ static const RegionInfo testRegions[] = {
{meshtastic_Config_LoRaConfig_RegionCode_LORA_24, 2400.0f, 2483.5f, 100, 10, false, true, &TEST_PROFILE_TURBO,
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, 0, "TEST_LORA24_TURBO"},
// Sentinel — must be last
// Sentinel - must be last
{meshtastic_Config_LoRaConfig_RegionCode_UNSET, 902.0f, 928.0f, 100, 30, false, false, &TEST_PROFILE_SPACED,
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, 0, "TEST_UNSET"},
};
+1 -1
View File
@@ -266,7 +266,7 @@ void test_XEdDSA_max_payload(void)
// Signing the same message twice yields signatures that both verify. This XEdDSA implementation is
// deterministic in practice (the two signatures are typically byte-identical, even though
// HardwareRNG::fill provides real entropy on this platform), so we assert only the security-relevant
// property — every produced signature verifies — rather than asserting (non-)determinism.
// property - every produced signature verifies - rather than asserting (non-)determinism.
void test_XEdDSA_repeated_sign_verifies(void)
{
uint8_t pub[32], priv[32], sig1[64], sig2[64];
+1 -1
View File
@@ -139,7 +139,7 @@ void test_ms_below_threshold()
void test_ms_at_threshold()
{
// INT32_MAX / 1000 = 2,147,483 — largest secs that does not clamp.
// INT32_MAX / 1000 = 2,147,483 - largest secs that does not clamp.
TEST_ASSERT_EQUAL_UINT32(2147483000U, Default::getConfiguredOrDefaultMs(2147483U, 0));
}
+15 -15
View File
@@ -23,12 +23,12 @@
static constexpr NodeNum kLocalNode = 0x11111111;
// Shared mock clock — drives HopScalingModule::nowMs()
// Shared mock clock - drives HopScalingModule::nowMs()
static uint32_t &mockTime = HopScalingModule::s_testNowMs;
static constexpr uint32_t ONE_HOUR_MS = 3600UL * 1000UL;
// ---------------------------------------------------------------------------
// MockNodeDB — not used for hop decisions any more, kept for completeness
// MockNodeDB - not used for hop decisions any more, kept for completeness
// ---------------------------------------------------------------------------
class MockNodeDB : public NodeDB
{
@@ -56,7 +56,7 @@ class MockNodeDB : public NodeDB
};
// ---------------------------------------------------------------------------
// Test shim — expose protected/private members for direct invocation
// Test shim - expose protected/private members for direct invocation
// ---------------------------------------------------------------------------
class HopScalingTestShim : public HopScalingModule
{
@@ -100,14 +100,14 @@ static MockNodeDB *mockNodeDB = nullptr;
// Create deterministic IDs that produce a broad spread of 16-bit hashes.
// HopScalingModule admission uses passesFilter(hashNodeId(nodeId), denom), NOT a raw nodeId
// modulo check — do not assume (nodeId & (denom-1)) == 0 determines whether a node is admitted.
// modulo check - do not assume (nodeId & (denom-1)) == 0 determines whether a node is admitted.
static uint32_t makeDistributedNodeId(uint32_t baseId, uint32_t ordinal, uint32_t salt = 0)
{
return baseId + salt + (ordinal * 33u);
}
// ---------------------------------------------------------------------------
// Helpers — mesh topology builders
// Helpers - mesh topology builders
// ---------------------------------------------------------------------------
// Helper: add N nodes at a given hop with ages spread across a time range.
@@ -151,7 +151,7 @@ static void assertCompactHistogramActive(HopScalingTestShim &shim)
// Topology builders
// ---------------------------------------------------------------------------
// Scenario A: Dense local mesh — 110 nodes, heavy at hops 0–2.
// Scenario A: Dense local mesh - 110 nodes, heavy at hops 0-2.
static void buildDenseLocalMesh()
{
mockNodeDB->clearTestNodes();
@@ -164,7 +164,7 @@ static void buildDenseLocalMesh()
addNodesAtHop(0x7000, 6, 10, 3000);
}
// Scenario B: Spread sparse mesh — 76 nodes across hops 0–7.
// Scenario B: Spread sparse mesh - 76 nodes across hops 0-7.
static void buildSpreadSparseMesh()
{
mockNodeDB->clearTestNodes();
@@ -178,7 +178,7 @@ static void buildSpreadSparseMesh()
addNodesAtHop(0x8000, 7, 10, 3600);
}
// Scenario C: Deep linear chain — 22 thin nodes, never reaches 40.
// Scenario C: Deep linear chain - 22 thin nodes, never reaches 40.
static void buildDeepLinearChain()
{
mockNodeDB->clearTestNodes();
@@ -192,7 +192,7 @@ static void buildDeepLinearChain()
addNodesAtHop(0x8000, 7, 3, 3600);
}
// Scenario D: Router cluster — 71 nodes, 45 at hop 2.
// Scenario D: Router cluster - 71 nodes, 45 at hop 2.
static void buildRouterCluster()
{
mockNodeDB->clearTestNodes();
@@ -206,7 +206,7 @@ static void buildRouterCluster()
addNodesAtHop(0x8000, 7, 3, 3600);
}
// Scenario E: Megamesh — 199 nodes (DB near capacity).
// Scenario E: Megamesh - 199 nodes (DB near capacity).
static void buildMegamesh()
{
mockNodeDB->clearTestNodes();
@@ -221,7 +221,7 @@ static void buildMegamesh()
}
// ---------------------------------------------------------------------------
// Tests — Topology-driven hop reduction scenarios
// Tests - Topology-driven hop reduction scenarios
// ---------------------------------------------------------------------------
void test_dense_local_telemetry()
@@ -491,7 +491,7 @@ void test_startup_blank_state()
}
// ---------------------------------------------------------------------------
// Tests — Denominator state machine
// Tests - Denominator state machine
// ---------------------------------------------------------------------------
void test_denominator_rises_on_overflow()
@@ -549,7 +549,7 @@ void test_filtering_denom_hold_counts_down()
TEST_ASSERT_EQUAL_UINT8(4u, shim->getFilteringDenominator());
TEST_ASSERT_EQUAL_UINT8(1u, shim->getFilteringDenomHoldRollsRemaining());
// Roll 3: hold 1→0, step fires — filteringDenominator halves to max(2, samp=1) = 2.
// Roll 3: hold 1→0, step fires - filteringDenominator halves to max(2, samp=1) = 2.
shim->rollHourTest();
TEST_MSG_FMT("After hold expires: filt=1/%u samp=1/%u holdRolls=%u", shim->getFilteringDenominator(),
shim->getSamplingDenominator(), shim->getFilteringDenomHoldRollsRemaining());
@@ -575,10 +575,10 @@ void test_filtering_denom_steps_down_gradually()
shim->rollHourTest(); // hold=0 (no decrement), step: 4/2=2 > 1, filt=2
TEST_ASSERT_EQUAL_UINT8(2u, shim->getFilteringDenominator());
shim->rollHourTest(); // step: 2/2=1, not > samp=1, filt=samp=1 — converged
shim->rollHourTest(); // step: 2/2=1, not > samp=1, filt=samp=1 - converged
TEST_ASSERT_EQUAL_UINT8(1u, shim->getFilteringDenominator());
shim->rollHourTest(); // filt==samp, outer if is false — no further change
shim->rollHourTest(); // filt==samp, outer if is false - no further change
TEST_ASSERT_EQUAL_UINT8(1u, shim->getFilteringDenominator());
TEST_ASSERT_EQUAL_UINT8(HopScalingModule::DENOM_MIN, shim->getSamplingDenominator());
+3 -3
View File
@@ -49,7 +49,7 @@ void test_colon_separated_lowercase()
void test_no_colons_packed_hex()
{
// The CLI form produced by some tools — 12 hex chars, no separators.
// The CLI form produced by some tools - 12 hex chars, no separators.
uint8_t dmac[6] = {0};
TEST_ASSERT_TRUE(MAC_from_string("AABBCCDDEEFF", dmac));
TEST_ASSERT_EQUAL_HEX8(0xAA, dmac[0]);
@@ -75,7 +75,7 @@ void test_does_not_read_external_state()
{
// The function must derive every byte from its parameter, not from any
// global. Provide a unique MAC and verify all six bytes match the input
// exactly — leaves no room for the function to be smuggling bytes from
// exactly - leaves no room for the function to be smuggling bytes from
// elsewhere.
uint8_t dmac[6] = {0};
TEST_ASSERT_TRUE(MAC_from_string("12:34:56:78:9A:BC", dmac));
@@ -149,7 +149,7 @@ void test_partial_hex_failure_preserves_dmac()
{
// First five bytes are valid hex; the sixth ("ZZ") is not. Without the
// temp-buffer staging, dmac would be partially overwritten with the five
// good bytes plus stale data in slot 5 — silently producing a wrong MAC
// good bytes plus stale data in slot 5 - silently producing a wrong MAC
// since the only caller that uses this in getMacAddr() ignores the bool
// return value.
uint8_t dmac[6] = {0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x11};
+18 -18
View File
@@ -51,7 +51,7 @@ constexpr NodeNum kLocalNode = 0xAAAA0001;
constexpr NodeNum kRemoteNode = 0xBBBB0002;
// ---------------------------------------------------------------------------
// Minimal MockMeshService — stubs out side-effecting virtuals.
// Minimal MockMeshService - stubs out side-effecting virtuals.
// handleToRadio is non-virtual so it runs the real implementation; we guard
// against the router->sendLocal path by setting a MockRouter below.
// ---------------------------------------------------------------------------
@@ -94,7 +94,7 @@ class MockRouter : public Router
};
// ---------------------------------------------------------------------------
// AdminModuleTestShim — exposes protected handleSetModuleConfig.
// AdminModuleTestShim - exposes protected handleSetModuleConfig.
// ---------------------------------------------------------------------------
class AdminModuleTestShim : public AdminModule
{
@@ -103,7 +103,7 @@ class AdminModuleTestShim : public AdminModule
};
// ---------------------------------------------------------------------------
// MeshBeaconBroadcastModuleTestShim — exposes private internals for testing.
// MeshBeaconBroadcastModuleTestShim - exposes private internals for testing.
// ---------------------------------------------------------------------------
class MeshBeaconBroadcastModuleTestShim : public MeshBeaconBroadcastModule
{
@@ -117,7 +117,7 @@ class MeshBeaconBroadcastModuleTestShim : public MeshBeaconBroadcastModule
};
// ---------------------------------------------------------------------------
// MeshBeaconListenerModuleTestShim — exposes handleReceivedProtobuf.
// MeshBeaconListenerModuleTestShim - exposes handleReceivedProtobuf.
// ---------------------------------------------------------------------------
class MeshBeaconListenerModuleTestShim : public MeshBeaconListenerModule
{
@@ -154,7 +154,7 @@ static void resetConfig()
moduleConfig = meshtastic_LocalModuleConfig_init_zero;
config = meshtastic_LocalConfig_init_zero;
// Device is an EU_868 node with LONG_FAST — the starting point.
// Device is an EU_868 node with LONG_FAST - the starting point.
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
config.lora.use_preset = true;
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
@@ -204,7 +204,7 @@ static void installTestSecondaryChannel(uint8_t index, const char *name, const u
}
// ===========================================================================
// Group 1: AdminModule config validation — bad inputs must be sanitised
// Group 1: AdminModule config validation - bad inputs must be sanitised
// ===========================================================================
/**
@@ -610,7 +610,7 @@ static void test_broadcaster_rebuildCache_idempotent(void)
MeshBeaconBroadcastModuleTestShim bcast;
bcast.rebuildCache();
pb_size_t firstSize = bcast.payloadCacheSize;
bcast.rebuildCache(); // second call — should be identical
bcast.rebuildCache(); // second call - should be identical
pb_size_t secondSize = bcast.payloadCacheSize;
TEST_ASSERT_FALSE(bcast.payloadCacheDirty);
@@ -618,7 +618,7 @@ static void test_broadcaster_rebuildCache_idempotent(void)
}
// ===========================================================================
// Group 3: Broadcaster sendBeacon — packet structure
// Group 3: Broadcaster sendBeacon - packet structure
// ===========================================================================
/**
@@ -708,7 +708,7 @@ static void test_broadcaster_sendBeacon_fallsBackToTextMessagePortnum(void)
moduleConfig.has_mesh_beacon = true;
const char *msg = "plain-text-beacon";
strncpy(moduleConfig.mesh_beacon.broadcast_message, msg, sizeof(moduleConfig.mesh_beacon.broadcast_message) - 1);
// broadcast_on_preset set, but no offer — should still be TEXT_MESSAGE_APP
// broadcast_on_preset set, but no offer - should still be TEXT_MESSAGE_APP
moduleConfig.mesh_beacon.has_broadcast_on_preset = true;
moduleConfig.mesh_beacon.broadcast_on_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW;
@@ -805,7 +805,7 @@ static void test_broadcaster_runOnce_silentWhenDisabled(void)
}
// ===========================================================================
// Group 4: Listener — offer caching and guards
// Group 4: Listener - offer caching and guards
// ===========================================================================
// Helper: build a decoded MESH_BEACON_APP packet carrying the given MeshBeacon.
@@ -902,7 +902,7 @@ static void test_listener_emptyMessageWithoutOffer_isDropped(void)
/**
* Verify a LONG_FAST offer (preset enum value 0) with no message still populates the offer cache.
* Important to guard the has_offer_preset fix — LONG_FAST must not be treated as 'no offer present'.
* Important to guard the has_offer_preset fix - LONG_FAST must not be treated as 'no offer present'.
*/
static void test_listener_offerOnly_isCached(void)
{
@@ -970,7 +970,7 @@ static void test_listener_receiveWithNoOffer_cacheStaysInvalid(void)
/**
* Verify the listener does NOT unwrap a combined beacon's text into a synthesized TEXT_MESSAGE_APP.
* The original MESH_BEACON_APP packet already reaches the client (the handler returns CONTINUE), so
* a beacon-aware client reads `message` directly from it — injecting a copy would only duplicate it,
* a beacon-aware client reads `message` directly from it - injecting a copy would only duplicate it,
* and re-injecting onto the mesh would amplify/re-attribute. So: nothing onto the mesh, nothing
* synthesized to the phone, and the handler must not consume the packet.
*/
@@ -990,7 +990,7 @@ static void test_listener_textMessage_notUnwrapped(void)
bool consumed = listener.handleReceivedProtobuf(mp, &b);
// CONTINUE (not STOP): the original MESH_BEACON_APP keeps flowing to the client, which reads
// `message` from it — the simple path for a beacon-aware client.
// `message` from it - the simple path for a beacon-aware client.
TEST_ASSERT_FALSE_MESSAGE(consumed, "Listener must not consume the beacon; the original must reach the client");
// Nothing re-injected onto the mesh.
TEST_ASSERT_EQUAL_UINT32_MESSAGE(0, mockRouter->sentPackets.size(),
@@ -1176,7 +1176,7 @@ static void test_broadcaster_channelPskOverride_swapsBeaconChannelAndRestores(vo
/**
* Without a broadcast_on_channel override, the beacon must transmit on the primary channel unchanged
* (no swap). Guards against the swap firing — and churning the channel table — when it isn't needed.
* (no swap). Guards against the swap firing - and churning the channel table - when it isn't needed.
*/
static void test_broadcaster_noChannelOverride_doesNotSwapPrimary(void)
{
@@ -1253,7 +1253,7 @@ static void test_broadcaster_targetChannelIndex_blankSlotFallsBackToPreset(void)
MeshBeaconBroadcastModuleTestShim bcast;
bcast.sendBeacon();
// Exactly one beacon goes out, and a blank slot does NOT trigger a crypto swap — the primary
// Exactly one beacon goes out, and a blank slot does NOT trigger a crypto swap - the primary
// stays "Home" (no garbage / no clobber), matching the no-channel-override default-for-preset path.
TEST_ASSERT_EQUAL_UINT32(1, mockRouter->sentPackets.size());
TEST_ASSERT_TRUE_MESSAGE(mockRouter->primaryAtSend.size() >= 1, "expected at least one send");
@@ -1263,7 +1263,7 @@ static void test_broadcaster_targetChannelIndex_blankSlotFallsBackToPreset(void)
/**
* Two broadcast_targets that resolve to the same effective radio config (same preset/region/channel)
* must produce only ONE beacon — the payload is identical, so re-broadcasting wastes airtime.
* must produce only ONE beacon - the payload is identical, so re-broadcasting wastes airtime.
*/
static void test_broadcaster_duplicateTargets_dedupedToOnePacket(void)
{
@@ -1289,7 +1289,7 @@ static void test_broadcaster_duplicateTargets_dedupedToOnePacket(void)
}
/**
* Two distinct broadcast_targets (different presets) must BOTH be sent — dedup must not over-collapse.
* Two distinct broadcast_targets (different presets) must BOTH be sent - dedup must not over-collapse.
*/
static void test_broadcaster_distinctTargets_bothSent(void)
{
@@ -1341,7 +1341,7 @@ void tearDown(void)
testAdmin = nullptr;
// Drain any packets the listener delivered via sendToPhone() (toPhoneQueue takes ownership and
// nothing else dequeues them in tests) so they are returned to packetPool — otherwise they leak
// nothing else dequeues them in tests) so they are returned to packetPool - otherwise they leak
// and LeakSanitizer aborts the process at exit.
if (mockSvc) {
meshtastic_MeshPacket *p;
+1 -1
View File
@@ -856,7 +856,7 @@ void test_configCustomHostAndPort(void)
TEST_ASSERT_TRUE(MQTT::isValidConfig(config));
}
// An unreachable server is still a valid config — settings always save.
// An unreachable server is still a valid config - settings always save.
// A warning notification is sent in non-test builds, but isValidConfig returns true.
void test_configWithUnreachableServerIsStillValid(void)
{
+7 -7
View File
@@ -30,7 +30,7 @@
static constexpr NodeNum kLocalNode = 0x11111111; // last byte 0x11
// ---------------------------------------------------------------------------
// MockNodeDB — inject nodes with controlled last byte, hop distance, age, role, favorite flag.
// MockNodeDB - inject nodes with controlled last byte, hop distance, age, role, favorite flag.
// ---------------------------------------------------------------------------
class MockNodeDB : public NodeDB
{
@@ -67,7 +67,7 @@ class MockNodeDB : public NodeDB
};
// ---------------------------------------------------------------------------
// Test shim — expose getNextHop and the route-health helpers; reset health between tests.
// Test shim - expose getNextHop and the route-health helpers; reset health between tests.
// Nulls cryptLock so the Router base can be (re)constructed (same pattern as test_mqtt MockRouter).
// ---------------------------------------------------------------------------
class NextHopRouterTestShim : public NextHopRouter
@@ -125,7 +125,7 @@ void setUp(void)
void tearDown(void) {}
// ===========================================================================
// Group 1 — resolveLastByte (M1)
// Group 1 - resolveLastByte (M1)
// ===========================================================================
void test_resolve_none_when_empty(void)
@@ -136,7 +136,7 @@ void test_resolve_none_when_empty(void)
void test_resolve_zero_byte_is_none(void)
{
// 0 is the NO_RELAY_NODE / NO_NEXT_HOP_PREFERENCE sentinel — never resolves.
// 0 is the NO_RELAY_NODE / NO_NEXT_HOP_PREFERENCE sentinel - never resolves.
mockNodeDB->addNode(0x22222200, 0, true, 60); // last byte maps to 0xFF, not 0
TEST_ASSERT_EQUAL(LastByteResolution::None, mockNodeDB->resolveLastByte(0x00, true).status);
TEST_ASSERT_EQUAL(LastByteResolution::None, mockNodeDB->resolveLastByte(0x00, false).status);
@@ -227,7 +227,7 @@ void test_resolve_unique_helper(void)
}
// ===========================================================================
// Group 2 — getNextHop (M2 send-path gate + M3 decay)
// Group 2 - getNextHop (M2 send-path gate + M3 decay)
// ===========================================================================
static constexpr NodeNum DEST = 0x000000B0; // DM destination (last byte 0xB0, distinct from 0xAB)
@@ -283,7 +283,7 @@ void test_getnexthop_decays_stale_route(void)
}
// ===========================================================================
// Group 3 — route-health helpers (M3)
// Group 3 - route-health helpers (M3)
// ===========================================================================
void test_health_fresh_not_stale(void)
@@ -379,7 +379,7 @@ void test_health_lru_eviction_bounds_table(void)
}
// ===========================================================================
// Group 4 — shouldDecrementHopLimit favorite-router resolution (M2, site 4)
// Group 4 - shouldDecrementHopLimit favorite-router resolution (M2, site 4)
// ===========================================================================
void test_hoplimit_preserve_unique_favorite_router(void)
+5 -5
View File
@@ -1,6 +1,6 @@
// Tests for the NodeDB hot-store migration and favourite/ignored (blocked)
// retention paths — src/mesh/NodeDB.cpp.
#include "MeshTypes.h" // BEFORE TestUtil.h — provides WARM_NODE_COUNT / MAX_NUM_NODES via mesh-pb-constants.h
// retention paths - src/mesh/NodeDB.cpp.
#include "MeshTypes.h" // BEFORE TestUtil.h - provides WARM_NODE_COUNT / MAX_NUM_NODES via mesh-pb-constants.h
#include "TestUtil.h"
#include <unity.h>
@@ -19,7 +19,7 @@
// Subclass shim: exposes the private maintenance paths (via the friend
// declaration in NodeDB.h) and lets a test own the hot store directly
// (meshNodes/numMeshNodes are public). Declared at global scope so it matches
// `friend class NodeDBTestShim` — an anonymous-namespace class would not.
// `friend class NodeDBTestShim` - an anonymous-namespace class would not.
class NodeDBTestShim : public NodeDB
{
public:
@@ -165,7 +165,7 @@ static void test_ignored_survivesEvictionAndCleanup(void)
TEST_ASSERT_NOT_NULL(db->getMeshNode(4000 + 1)); // blocked node survived
TEST_ASSERT_NULL(db->getMeshNode(4000 + 2)); // oldest non-blocked evicted
// (b) cleanup protection — ignored kept without user info, plain no-user purged
// (b) cleanup protection - ignored kept without user info, plain no-user purged
db->clearHot();
db->seedSelf();
db->push(5000, 100, false, /*ignored=*/true, /*withUser=*/false, false);
@@ -213,7 +213,7 @@ NDB_TEST_ENTRY void setup()
}
NDB_TEST_ENTRY void loop() {}
#else // WARM_NODE_COUNT == 0 — nothing to exercise here
#else // WARM_NODE_COUNT == 0 - nothing to exercise here
void setUp(void) {}
void tearDown(void) {}
+33 -33
View File
@@ -1,5 +1,5 @@
/*
* Unit tests for PacketHistory — the packet deduplication engine
* Unit tests for PacketHistory - the packet deduplication engine
* used by the mesh routing stack.
*
* PacketHistory maintains a fixed-size array of PacketRecords with an
@@ -41,7 +41,7 @@ static meshtastic_MeshPacket makePacket(uint32_t from, uint32_t id, uint8_t hop_
}
// ---------------------------------------------------------------------------
// setUp / tearDown — called before and after every test
// setUp / tearDown - called before and after every test
// ---------------------------------------------------------------------------
void setUp(void)
{
@@ -56,7 +56,7 @@ void tearDown(void)
}
// ===========================================================================
// Group 1 — Initialization
// Group 1 - Initialization
// ===========================================================================
void test_init_valid_size(void)
@@ -79,7 +79,7 @@ void test_init_too_small_falls_back(void)
}
// ===========================================================================
// Group 2 — Basic Deduplication
// Group 2 - Basic Deduplication
// ===========================================================================
void test_first_packet_not_seen(void)
@@ -128,7 +128,7 @@ void test_withUpdate_true_inserts(void)
}
// ===========================================================================
// Group 3 — LRU Eviction
// Group 3 - LRU Eviction
// ===========================================================================
void test_fill_capacity_all_found(void)
@@ -157,7 +157,7 @@ void test_eviction_oldest_replaced(void)
// entries with identical timestamps all have age 0 and none gets selected.
delay(1);
// Insert a 9th packet — should evict the oldest
// Insert a 9th packet - should evict the oldest
auto p9 = makePacket(0xAAAA, 9);
ph->wasSeenRecently(&p9);
@@ -176,7 +176,7 @@ void test_eviction_oldest_replaced(void)
void test_matching_slot_reused(void)
{
// Insert packet, then re-insert same (sender, id) — should reuse slot, not evict others
// Insert packet, then re-insert same (sender, id) - should reuse slot, not evict others
auto p1 = makePacket(0xAAAA, 1);
auto p2 = makePacket(0xBBBB, 2);
ph->wasSeenRecently(&p1);
@@ -192,7 +192,7 @@ void test_matching_slot_reused(void)
void test_free_slot_preferred(void)
{
// Insert 4 packets into capacity-8 history — next insert should use a free slot, not evict
// Insert 4 packets into capacity-8 history - next insert should use a free slot, not evict
for (uint32_t i = 1; i <= 4; i++) {
auto p = makePacket(0xAAAA, i);
ph->wasSeenRecently(&p);
@@ -236,7 +236,7 @@ void test_evict_all_old_packets(void)
}
// ===========================================================================
// Group 4 — Relayer Tracking
// Group 4 - Relayer Tracking
// ===========================================================================
void test_wasRelayer_true(void)
@@ -326,7 +326,7 @@ void test_wasRelayer_all_six_slots(void)
}
// ===========================================================================
// Group 5 — removeRelayer
// Group 5 - removeRelayer
// ===========================================================================
void test_removeRelayer_removes(void)
@@ -366,12 +366,12 @@ void test_removeRelayer_nonexistent_safe(void)
void test_removeRelayer_packet_not_found_safe(void)
{
// Packet not in history — should not crash
// Packet not in history - should not crash
ph->removeRelayer(0xAA, 999, 0x9999);
}
// ===========================================================================
// Group 6 — checkRelayers
// Group 6 - checkRelayers
// ===========================================================================
void test_checkRelayers_both_found(void)
@@ -414,7 +414,7 @@ void test_checkRelayers_r2WasSole(void)
}
// ===========================================================================
// Group 7 — wasSeenRecently Merge Logic
// Group 7 - wasSeenRecently Merge Logic
// ===========================================================================
void test_merge_preserves_original_next_hop(void)
@@ -461,11 +461,11 @@ void test_merge_no_duplicate_relayers(void)
auto p1 = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID);
ph->wasSeenRecently(&p1);
// Re-observe with same relay_node=ourRelayID — should not create duplicates
// Re-observe with same relay_node=ourRelayID - should not create duplicates
auto p2 = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID);
ph->wasSeenRecently(&p2);
// ourRelayID should appear exactly once — wasSole should still be true
// ourRelayID should appear exactly once - wasSole should still be true
bool wasSole = false;
TEST_ASSERT_TRUE(ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111, &wasSole));
TEST_ASSERT_TRUE(wasSole);
@@ -508,7 +508,7 @@ void test_merge_heard_back_stores_relay_node(void)
}
// ===========================================================================
// Group 8 — Fallback-to-Flooding Detection
// Group 8 - Fallback-to-Flooding Detection
// ===========================================================================
void test_fallback_detected(void)
@@ -531,7 +531,7 @@ void test_fallback_detected(void)
// Step 3: Router removes us from the relayer list
ph->removeRelayer(OUR_RELAY_ID, 100, 0x1111);
// Step 4: Sender falls back to flooding — same packet, NO_NEXT_HOP_PREFERENCE, from 0xAA
// Step 4: Sender falls back to flooding - same packet, NO_NEXT_HOP_PREFERENCE, from 0xAA
auto p3 = makePacket(0x1111, 100, 1, NO_NEXT_HOP_PREFERENCE, 0xAA);
bool wasFallback = false;
ph->wasSeenRecently(&p3, true, &wasFallback);
@@ -554,7 +554,7 @@ void test_fallback_not_when_we_relayed(void)
void test_fallback_not_on_first_observation(void)
{
// First time seen — can't be a fallback
// First time seen - can't be a fallback
auto p = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, 0xAA);
bool wasFallback = false;
ph->wasSeenRecently(&p, true, &wasFallback);
@@ -562,7 +562,7 @@ void test_fallback_not_on_first_observation(void)
}
// ===========================================================================
// Group 9 — Next-Hop and Upgrade Detection
// Group 9 - Next-Hop and Upgrade Detection
// ===========================================================================
void test_weWereNextHop_true(void)
@@ -620,7 +620,7 @@ void test_wasUpgraded_false(void)
}
// ===========================================================================
// Group 10 — Edge Cases
// Group 10 - Edge Cases
// ===========================================================================
void test_packet_id_zero_not_stored(void)
@@ -632,7 +632,7 @@ void test_packet_id_zero_not_stored(void)
void test_sender_zero_substituted(void)
{
// from=0 means "from us" — getFrom() substitutes nodeDB->getNodeNum()
// from=0 means "from us" - getFrom() substitutes nodeDB->getNodeNum()
auto p = makePacket(0, 100);
ph->wasSeenRecently(&p);
@@ -663,7 +663,7 @@ void test_multiple_instances_independent(void)
}
// ===========================================================================
// Group 11 — Hash Table Stress
// Group 11 - Hash Table Stress
// ===========================================================================
void test_many_packets_no_false_negatives(void)
@@ -739,12 +739,12 @@ void setup()
initializeTestEnvironment();
UNITY_BEGIN();
// Group 1 — Initialization
// Group 1 - Initialization
RUN_TEST(test_init_valid_size);
RUN_TEST(test_init_minimum_size);
RUN_TEST(test_init_too_small_falls_back);
// Group 2 — Basic Deduplication
// Group 2 - Basic Deduplication
RUN_TEST(test_first_packet_not_seen);
RUN_TEST(test_same_packet_seen_twice);
RUN_TEST(test_different_id_not_confused);
@@ -752,14 +752,14 @@ void setup()
RUN_TEST(test_withUpdate_false_no_insert);
RUN_TEST(test_withUpdate_true_inserts);
// Group 3 — LRU Eviction
// Group 3 - LRU Eviction
RUN_TEST(test_fill_capacity_all_found);
RUN_TEST(test_eviction_oldest_replaced);
RUN_TEST(test_matching_slot_reused);
RUN_TEST(test_free_slot_preferred);
RUN_TEST(test_evict_all_old_packets);
// Group 4 — Relayer Tracking
// Group 4 - Relayer Tracking
RUN_TEST(test_wasRelayer_true);
RUN_TEST(test_wasRelayer_false);
RUN_TEST(test_wasRelayer_zero_returns_false);
@@ -768,18 +768,18 @@ void setup()
RUN_TEST(test_wasRelayer_wasSole_false);
RUN_TEST(test_wasRelayer_all_six_slots);
// Group 5 — removeRelayer
// Group 5 - removeRelayer
RUN_TEST(test_removeRelayer_removes);
RUN_TEST(test_removeRelayer_compacts);
RUN_TEST(test_removeRelayer_nonexistent_safe);
RUN_TEST(test_removeRelayer_packet_not_found_safe);
// Group 6 — checkRelayers
// Group 6 - checkRelayers
RUN_TEST(test_checkRelayers_both_found);
RUN_TEST(test_checkRelayers_one_found);
RUN_TEST(test_checkRelayers_r2WasSole);
// Group 7 — Merge Logic
// Group 7 - Merge Logic
RUN_TEST(test_merge_preserves_original_next_hop);
RUN_TEST(test_merge_preserves_highest_hop_limit);
RUN_TEST(test_merge_no_duplicate_relayers);
@@ -787,23 +787,23 @@ void setup()
RUN_TEST(test_merge_we_relay_sets_slot_zero);
RUN_TEST(test_merge_heard_back_stores_relay_node);
// Group 8 — Fallback-to-Flooding Detection
// Group 8 - Fallback-to-Flooding Detection
RUN_TEST(test_fallback_detected);
RUN_TEST(test_fallback_not_when_we_relayed);
RUN_TEST(test_fallback_not_on_first_observation);
// Group 9 — Next-Hop and Upgrade Detection
// Group 9 - Next-Hop and Upgrade Detection
RUN_TEST(test_weWereNextHop_true);
RUN_TEST(test_weWereNextHop_false);
RUN_TEST(test_wasUpgraded_true);
RUN_TEST(test_wasUpgraded_false);
// Group 10 — Edge Cases
// Group 10 - Edge Cases
RUN_TEST(test_packet_id_zero_not_stored);
RUN_TEST(test_sender_zero_substituted);
RUN_TEST(test_multiple_instances_independent);
// Group 11 — Hash Table Stress
// Group 11 - Hash Table Stress
RUN_TEST(test_many_packets_no_false_negatives);
RUN_TEST(test_many_packets_no_false_positives);
RUN_TEST(test_churn_correctness);
+9 -9
View File
@@ -1,5 +1,5 @@
// Tests for XEdDSA packet-signing *policy* — the receive-path accept/reject behavior and the
// send-path signing policy — as opposed to the raw sign/verify primitive (covered in test_crypto).
// Tests for XEdDSA packet-signing *policy* - the receive-path accept/reject behavior and the
// send-path signing policy - as opposed to the raw sign/verify primitive (covered in test_crypto).
//
// The decision logic under test lives inside perhapsDecode()/perhapsEncode() (free functions in
// Router.cpp). It only runs after a packet is decrypted, so every case drives a real
@@ -37,7 +37,7 @@ static constexpr size_t SMALL_PAYLOAD = 16;
static constexpr size_t OVERSIZED_PAYLOAD = 180;
// ---------------------------------------------------------------------------
// MockNodeDB — inject nodes with controlled public keys / signer bits.
// MockNodeDB - inject nodes with controlled public keys / signer bits.
// Mirrors the pattern in test/test_hop_scaling. meshNodes/numMeshNodes are public on NodeDB.
// ---------------------------------------------------------------------------
class MockNodeDB : public NodeDB
@@ -101,7 +101,7 @@ static meshtastic_MeshPacket makeDecoded(NodeNum from, NodeNum to, meshtastic_Po
return p;
}
// Sign a decoded packet with the CryptoEngine's current key — used to simulate a *remote* signer,
// Sign a decoded packet with the CryptoEngine's current key - used to simulate a *remote* signer,
// because perhapsEncode only auto-signs packets that originate from us.
static void signWithCurrentKey(meshtastic_MeshPacket *p)
{
@@ -154,7 +154,7 @@ void tearDown(void)
}
// ===========================================================================
// Group A — receive-side accept/reject matrix
// Group A - receive-side accept/reject matrix
// ===========================================================================
// A1: valid signature from a node whose key we know -> accepted, marked signed, signer bit learned.
@@ -205,7 +205,7 @@ void test_A3_signed_no_pubkey_accepted_unverified(void)
TEST_ASSERT_FALSE_MESSAGE(remoteSignerBit(), "must not learn signer without verifying");
}
// A4: downgrade protection — unsigned small broadcast from a known signer -> dropped.
// A4: downgrade protection - unsigned small broadcast from a known signer -> dropped.
void test_A4_downgrade_unsigned_broadcast_from_signer_dropped(void)
{
mockNodeDB->addNode(REMOTE_NODE);
@@ -217,7 +217,7 @@ void test_A4_downgrade_unsigned_broadcast_from_signer_dropped(void)
TEST_ASSERT_EQUAL(DECODE_FAILURE, roundTrip(&p));
}
// A5: no prior knowledge — unsigned small broadcast from a non-signer -> accepted.
// A5: no prior knowledge - unsigned small broadcast from a non-signer -> accepted.
void test_A5_unsigned_broadcast_from_nonsigner_accepted(void)
{
mockNodeDB->addNode(REMOTE_NODE); // signer bit clear
@@ -252,7 +252,7 @@ void test_A7_unsigned_oversized_broadcast_from_signer_accepted(void)
}
// ===========================================================================
// Group B — send-side signing policy (perhapsEncode)
// Group B - send-side signing policy (perhapsEncode)
// ===========================================================================
// B1: our own small broadcast is auto-signed (and verifies on the way back in).
@@ -291,7 +291,7 @@ void test_B3_local_oversized_broadcast_not_signed(void)
}
// ===========================================================================
// Group C — NodeInfoModule downgrade drop (stricter: any unsigned NodeInfo from a known signer)
// Group C - NodeInfoModule downgrade drop (stricter: any unsigned NodeInfo from a known signer)
// ===========================================================================
class NodeInfoTestShim : public NodeInfoModule
{
+1 -1
View File
@@ -22,7 +22,7 @@ static void test_withinPrecisionCell_movingLonLeavesCell()
TEST_ASSERT_FALSE(PositionModule::positionWithinPrecisionCell(0x12340000, 0x22340000, 0x12340000, 0x22350000, 16));
}
// precision 0 means position sharing is off — never treat as stationary/suppressible.
// precision 0 means position sharing is off - never treat as stationary/suppressible.
static void test_withinPrecisionCell_zeroPrecisionNeverSuppresses()
{
TEST_ASSERT_FALSE(PositionModule::positionWithinPrecisionCell(0x12340000, 0x22340000, 0x12340000, 0x22340000, 0));
+1 -1
View File
@@ -201,7 +201,7 @@ static void test_applyModemConfig_customCodingRateLowerThanPreset()
}
// -----------------------------------------------------------------------
// getRegionPresetMap() — region->valid-preset map sent to clients during want_config
// getRegionPresetMap() - region->valid-preset map sent to clients during want_config
// -----------------------------------------------------------------------
static size_t countKnownRegions()
+34 -34
View File
@@ -1,4 +1,4 @@
#include "MeshTypes.h" // Include BEFORE TestUtil.h — provides HAS_TRAFFIC_MANAGEMENT (via mesh-pb-constants.h)
#include "MeshTypes.h" // Include BEFORE TestUtil.h - provides HAS_TRAFFIC_MANAGEMENT (via mesh-pb-constants.h)
#include "TestUtil.h"
#include <cstdlib>
#include <unity.h>
@@ -88,7 +88,7 @@ class MockNodeDB : public NodeDB
// Seed a node into the hot-store buffer at index 1 (index 0 is reserved for
// "self"). Respects the fixed-buffer invariant: `meshNodes` is a buffer of
// MAX_NUM_NODES slots with `numMeshNodes` as the logical count — we grow the
// MAX_NUM_NODES slots with `numMeshNodes` as the logical count - we grow the
// buffer if needed and bump the count, never clear()/push_back() (which would
// shrink it and break NodeDB::resetNodes()'s begin()+1..end() fill).
void setHotNode(NodeNum n, uint8_t nextHop)
@@ -101,7 +101,7 @@ class MockNodeDB : public NodeDB
numMeshNodes = 2;
}
// Evict everything but "self" — simulates the hot DB rolling over. Logical
// Evict everything but "self" - simulates the hot DB rolling over. Logical
// count only; the buffer is left intact so the invariant holds.
void rollHotStore()
{
@@ -735,7 +735,7 @@ static void test_tm_nodeinfo_directResponse_psramMissDoesNotFallbackToNodeDb(voi
/**
* Verify relayed telemetry broadcasts are NOT hop-exhausted.
* exhaust_hop_telemetry / exhaust_hop_position have been removed from the config
* as "not suitable right now" — alterReceived must leave hop_limit unchanged.
* as "not suitable right now" - alterReceived must leave hop_limit unchanged.
*/
static void test_tm_alterReceived_telemetryBroadcast_hopLimitUnchanged(void)
{
@@ -757,7 +757,7 @@ static void test_tm_alterReceived_telemetryBroadcast_hopLimitUnchanged(void)
/**
* Verify alterReceived does not modify unicast or local-origin packets.
* The precision clamp (the only active alterReceived path) only fires for
* broadcast position packets from remote nodes — these should be untouched.
* broadcast position packets from remote nodes - these should be untouched.
*/
static void test_tm_alterReceived_skipsLocalAndUnicast(void)
{
@@ -858,7 +858,7 @@ static void test_tm_positionDedup_precisionAbove32_usesDefaultPrecision(void)
* Verify the dedup fingerprint does not collapse positions that are distinct at the
* channel's *effective* precision. Dedup only runs on well-known (public) channels,
* where precision is capped at MAX_POSITION_PRECISION_PUBLIC_KEY (15) regardless of the
* channel's configured value — so the requested 32 is clamped to 15. Positions must
* channel's configured value - so the requested 32 is clamped to 15. Positions must
* therefore differ in the top 15 bits (>= 2^17 raw units) to read as distinct; here
* they differ by 2^18, well clear of the precision-15 grid, so neither is dropped.
*/
@@ -1026,7 +1026,7 @@ static void test_tm_unknownPackets_thresholdAbove255_clamps(void)
/**
* Verify relayed position broadcasts are NOT hop-exhausted.
* exhaust_hop_position has been removed — alterReceived must leave hop_limit unchanged.
* exhaust_hop_position has been removed - alterReceived must leave hop_limit unchanged.
*/
static void test_tm_alterReceived_positionBroadcast_hopLimitUnchanged(void)
{
@@ -1064,7 +1064,7 @@ static void test_tm_alterReceived_skipsUndecodedPackets(void)
}
/**
* Verify shouldExhaustHops() always returns false — exhaust_hop_* features are
* Verify shouldExhaustHops() always returns false - exhaust_hop_* features are
* removed, so the exhaustRequested flag is never set.
* Guards against accidental re-enablement without updating the flag logic.
*/
@@ -1089,7 +1089,7 @@ static void test_tm_alterReceived_exhaustFlagAlwaysFalse(void)
/**
* Verify shouldExhaustHops() returns false for any packet regardless of from/id.
* Since exhaust is removed, the from+id scope check is moot — this guards that
* Since exhaust is removed, the from+id scope check is moot - this guards that
* the always-false invariant holds across multiple distinct packets.
*/
static void test_tm_alterReceived_exhaustFlag_isPacketScoped(void)
@@ -1113,7 +1113,7 @@ static void test_tm_alterReceived_exhaustFlag_isPacketScoped(void)
/**
* Verify runOnce() returns sleep-forever interval when has_traffic_management is false.
* TMM has no runtime enable flag — the presence bit is the only runtime gate.
* TMM has no runtime enable flag - the presence bit is the only runtime gate.
*/
static void test_tm_runOnce_disabledReturnsMaxInterval(void)
{
@@ -1169,7 +1169,7 @@ static void test_tm_nextHop_setAndGetRoundTrip(void)
/**
* The headline scenario: a node carrying a next hop in the hot NodeInfoLite DB
* is warm-loaded into the TMM cache, then the hot DB is "rolled" (the node ages
* out entirely). The hint must still be served — now exclusively from TMM.
* out entirely). The hint must still be served - now exclusively from TMM.
*/
static void test_tm_nextHop_servedAfterNodeDbRoll(void)
{
@@ -1186,7 +1186,7 @@ static void test_tm_nextHop_servedAfterNodeDbRoll(void)
mockNodeDB->rollHotStore();
TEST_ASSERT_NULL(nodeDB->getMeshNode(kTargetNode)); // gone from the hot store
// Hit is still served — proving it now comes from the TMM overflow cache.
// Hit is still served - proving it now comes from the TMM overflow cache.
TEST_ASSERT_EQUAL_UINT8(0x42, module.getNextHopHint(kTargetNode));
}
@@ -1212,7 +1212,7 @@ static void test_tm_nextHop_preloadDoesNotClobberLearned(void)
/**
* A pure routing hint (no dedup/rate/unknown state) must survive the maintenance
* sweep — next_hop != 0 keeps the slot alive even though it has no TTL.
* sweep - next_hop != 0 keeps the slot alive even though it has no TTL.
*/
static void test_tm_nextHop_keptAliveAcrossMaintenanceSweep(void)
{
@@ -1237,7 +1237,7 @@ static void test_tm_nextHop_keptAliveAcrossMaintenanceSweep(void)
*/
static void test_tm_trackerRole_capsDedupWindowAtOneHour(void)
{
// Operator interval is 11 h — longer than the tracker cap.
// Operator interval is 11 h - longer than the tracker cap.
moduleConfig.traffic_management.position_min_interval_secs = default_traffic_mgmt_position_min_interval_secs;
installWellKnownPrimaryChannelWithPrecision(16);
@@ -1295,14 +1295,14 @@ static void test_tm_takTrackerRole_capsDedupWindowAtOneHour(void)
/**
* Verify the tracker role exception survives the node being evicted from BOTH the
* hot and warm NodeDB stores — the TMM unified cache is the third fallback. The
* hot and warm NodeDB stores - the TMM unified cache is the third fallback. The
* role is cached on the entry while NodeDB still knows the node; once NodeDB
* forgets it (getNodeRole → CLIENT), the cached role must keep the 1-hour cap
* applied instead of reverting to the 11-hour default interval.
*/
static void test_tm_trackerRole_survivesNodeDbEvictionViaCachedRole(void)
{
// Operator interval is 11 h — longer than the tracker cap.
// Operator interval is 11 h - longer than the tracker cap.
moduleConfig.traffic_management.position_min_interval_secs = default_traffic_mgmt_position_min_interval_secs;
installWellKnownPrimaryChannelWithPrecision(16);
@@ -1321,7 +1321,7 @@ static void test_tm_trackerRole_survivesNodeDbEvictionViaCachedRole(void)
// Node ages out of NodeDB entirely (hot + warm). getNodeRole now returns CLIENT.
mockNodeDB->clearCachedNode();
ProcessMessage r2 = module.handleReceived(dup); // within 1-hour cap — still drop
ProcessMessage r2 = module.handleReceived(dup); // within 1-hour cap - still drop
// Advance past the tracker cap (3600 s) but stay well under the 11-hour default.
// Without the cached-role fallback this would still be inside the 11-hour window
// (CLIENT → no exception) and wrongly drop; with it, the 1-hour cap lets it pass.
@@ -1338,7 +1338,7 @@ static void test_tm_trackerRole_survivesNodeDbEvictionViaCachedRole(void)
/**
* Verify a role change observed via NodeInfo updates the cached role (write-time update),
* so an exception is dropped when a node is demoted from TRACKER back to CLIENT. The role
* is read from the NodeInfo's User payload — the same event that updates NodeDB — not by
* is read from the NodeInfo's User payload - the same event that updates NodeDB - not by
* re-scanning NodeDB on the position path.
*/
static void test_tm_roleChange_viaNodeInfo_dropsTrackerException(void)
@@ -1356,7 +1356,7 @@ static void test_tm_roleChange_viaNodeInfo_dropsTrackerException(void)
ProcessMessage r1 = module.handleReceived(first);
// The node demotes to CLIENT and announces it. The NodeInfo refresh must overwrite
// the cached TRACKER role with CLIENT — even though the packet payload, not NodeDB,
// the cached TRACKER role with CLIENT - even though the packet payload, not NodeDB,
// is the source of truth here (NodeDB role left stale on purpose to prove the path).
meshtastic_MeshPacket info = makeNodeInfoPacketWithRole(kRemoteNode, meshtastic_Config_DeviceConfig_Role_CLIENT);
module.handleReceived(info);
@@ -1375,8 +1375,8 @@ static void test_tm_roleChange_viaNodeInfo_dropsTrackerException(void)
/**
* Verify a cached special (non-CLIENT) role pins the TMM entry through the maintenance
* sweep: once the node's position state has expired and been cleared, the entry — and its
* role — must still survive (role has no TTL), the same way a confirmed next-hop hint does.
* sweep: once the node's position state has expired and been cleared, the entry - and its
* role - must still survive (role has no TTL), the same way a confirmed next-hop hint does.
*/
static void test_tm_specialRole_pinsEntryThroughSweep(void)
{
@@ -1424,7 +1424,7 @@ static void test_tm_specialRole_evictedLastUnderPressure(void)
// Fill past capacity with newer CLIENT nodes, forcing many evictions. The tracker is
// the stalest entry but is "preferred", so an unprotected client must always be the
// victim instead — the tracker must never be evicted.
// victim instead - the tracker must never be evicted.
for (uint32_t i = 0; i < static_cast<uint32_t>(TRAFFIC_MANAGEMENT_CACHE_SIZE) + 50; i++) {
const NodeNum filler = 0x01000000u + i;
module.handleReceived(makePositionPacket(filler, 300 + static_cast<int>(i), 400 + static_cast<int>(i)));
@@ -1439,7 +1439,7 @@ static void test_tm_specialRole_evictedLastUnderPressure(void)
*/
static void test_tm_trackerRole_doesNotLengthenShorterOperatorInterval(void)
{
// Operator set 5-minute interval — shorter than the 1-hour tracker cap.
// Operator set 5-minute interval - shorter than the 1-hour tracker cap.
moduleConfig.traffic_management.position_min_interval_secs = 300;
installWellKnownPrimaryChannelWithPrecision(16);
@@ -1454,7 +1454,7 @@ static void test_tm_trackerRole_doesNotLengthenShorterOperatorInterval(void)
ProcessMessage r1 = module.handleReceived(first);
ProcessMessage r2 = module.handleReceived(dup);
// The 300 s operator interval rounds to 1 pos-tick (360 s) — dedup is tick-granular.
// The 300 s operator interval rounds to 1 pos-tick (360 s) - dedup is tick-granular.
// Advance past one full tick to verify the window is 1 tick (not the 10-tick tracker cap).
TrafficManagementModule::s_testNowMs += 360'000UL + 1;
ProcessMessage r3 = module.handleReceived(afterShortInterval);
@@ -1488,7 +1488,7 @@ static void test_tm_lostAndFoundRole_capsDedupAtFifteenMinutes(void)
meshtastic_MeshPacket afterCap = makePositionPacket(kRemoteNode, 374221234, -1220845678);
ProcessMessage r1 = module.handleReceived(first);
ProcessMessage r2 = module.handleReceived(dup); // same tick — drop
ProcessMessage r2 = module.handleReceived(dup); // same tick - drop
// One pos-tick later: STILL within the 15-min (~2-tick) cap, unlike the old 1-tick exception.
// (360 s = kPosTimeTickMs, kept as a literal because the constant is private to the module.)
TrafficManagementModule::s_testNowMs += 360'000UL + 1;
@@ -1515,7 +1515,7 @@ static void test_tm_unknownRole_noDbEntry_appliesFullInterval(void)
moduleConfig.traffic_management.position_min_interval_secs = 300;
installWellKnownPrimaryChannelWithPrecision(16);
// No cached node — getMeshNode returns nullptr for kRemoteNode.
// No cached node - getMeshNode returns nullptr for kRemoteNode.
mockNodeDB->clearCachedNode();
TrafficManagementModuleTestShim module;
@@ -1525,8 +1525,8 @@ static void test_tm_unknownRole_noDbEntry_appliesFullInterval(void)
meshtastic_MeshPacket afterShort = makePositionPacket(kRemoteNode, 374221234, -1220845678);
ProcessMessage r1 = module.handleReceived(first);
ProcessMessage r2 = module.handleReceived(dup); // within 300-s window — must drop
// The 300 s operator interval rounds to 1 pos-tick (360 s) — dedup is tick-granular.
ProcessMessage r2 = module.handleReceived(dup); // within 300-s window - must drop
// The 300 s operator interval rounds to 1 pos-tick (360 s) - dedup is tick-granular.
// Advance past one full tick to confirm the packet passes without any tracker exception.
TrafficManagementModule::s_testNowMs += 360'000UL + 1;
ProcessMessage r3 = module.handleReceived(afterShort);
@@ -1548,7 +1548,7 @@ static void test_tm_unknownRole_noUserBit_appliesFullInterval(void)
moduleConfig.traffic_management.position_min_interval_secs = 300;
installWellKnownPrimaryChannelWithPrecision(16);
// Node is in NodeDB but the HAS_USER bit is NOT set — role must be ignored.
// Node is in NodeDB but the HAS_USER bit is NOT set - role must be ignored.
mockNodeDB->setCachedNode(kRemoteNode);
// Clear the HAS_USER bit that setCachedNode sets, leaving role at CLIENT default.
mockNodeDB->cachedNodeForTest().bitfield &= ~NODEINFO_BITFIELD_HAS_USER_MASK;
@@ -1560,7 +1560,7 @@ static void test_tm_unknownRole_noUserBit_appliesFullInterval(void)
meshtastic_MeshPacket dup = makePositionPacket(kRemoteNode, 374221234, -1220845678);
ProcessMessage r1 = module.handleReceived(first);
ProcessMessage r2 = module.handleReceived(dup); // within 300-s window — must drop
ProcessMessage r2 = module.handleReceived(dup); // within 300-s window - must drop
meshtastic_TrafficManagementStats stats = module.getStats();
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(r1));
@@ -1569,14 +1569,14 @@ static void test_tm_unknownRole_noUserBit_appliesFullInterval(void)
}
/**
* Verify a LOST_AND_FOUND origin now GETS the relayed precision clamp — the
* Verify a LOST_AND_FOUND origin now GETS the relayed precision clamp - the
* anti-dox exemption was removed, so a relayed position more precise than the
* channel setting is clamped down to the channel ceiling like any other node's.
*/
static void test_tm_lostAndFoundRole_getsAlterReceivedPrecisionClamp(void)
{
// Set channel precision ceiling to 13 bits. Must be <= MAX_POSITION_PRECISION_PUBLIC_KEY
// (15) — well-known channels have a public PSK (size==1), so getPositionPrecisionForChannel
// (15) - well-known channels have a public PSK (size==1), so getPositionPrecisionForChannel
// clamps any value above 15 via usesPublicKey().
installWellKnownPrimaryChannelWithPrecision(13);
@@ -1585,7 +1585,7 @@ static void test_tm_lostAndFoundRole_getsAlterReceivedPrecisionClamp(void)
TrafficManagementModuleTestShim module;
// Full-precision packet — 32 bits — exceeds the channel cap.
// Full-precision packet - 32 bits - exceeds the channel cap.
const uint32_t fullPrecision = 32;
meshtastic_MeshPacket packet = makePositionPacketWithPrecision(kRemoteNode, 374221234, -1220845678, fullPrecision);
packet.hop_start = 3;
@@ -1595,7 +1595,7 @@ static void test_tm_lostAndFoundRole_getsAlterReceivedPrecisionClamp(void)
meshtastic_Position out;
TEST_ASSERT_TRUE(decodePositionPayload(packet, out));
// Clamped to channel ceiling (13 bits) — lost-and-found is no longer exempt.
// Clamped to channel ceiling (13 bits) - lost-and-found is no longer exempt.
// Note: precision must be <= MAX_POSITION_PRECISION_PUBLIC_KEY (15); well-known
// channels always have a public PSK so getPositionPrecisionForChannel caps at 15.
TEST_ASSERT_EQUAL_UINT32(13, out.precision_bits);
+4 -4
View File
@@ -61,7 +61,7 @@ static void test_throttle_blocks_within_interval()
static void test_throttle_allows_after_interval()
{
// Unknown key returns 0 — throttle should NOT block
// Unknown key returns 0 - throttle should NOT block
uint32_t lastMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
TEST_ASSERT_EQUAL_UINT32(0, lastMs);
@@ -72,13 +72,13 @@ static void test_throttle_allows_after_interval()
static void test_throttle_blocks_after_set_then_zero_does_not()
{
// Set it — now throttle should block
// Set it - now throttle should block
transmitHistory->setLastSentToMesh(meshtastic_PortNum_TELEMETRY_APP);
uint32_t lastMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_TELEMETRY_APP);
bool shouldSend = (lastMs == 0) || !Throttle::isWithinTimespanMs(lastMs, 60 * 60 * 1000);
TEST_ASSERT_FALSE(shouldSend); // Should be blocked (within 1hr interval)
// Different key — should allow
// Different key - should allow
uint32_t otherMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_POSITION_APP);
bool otherShouldSend = (otherMs == 0) || !Throttle::isWithinTimespanMs(otherMs, 60 * 60 * 1000);
TEST_ASSERT_TRUE(otherShouldSend);
@@ -200,7 +200,7 @@ static void test_boot_after_recent_send_still_throttles()
// Regression test for issue #9901:
// A device powered off for longer than the throttle window must broadcast NodeInfo
// on its next boot — it must not be silenced because loadFromDisk() once treated
// on its next boot - it must not be silenced because loadFromDisk() once treated
// every loaded entry as "just sent" by seeding lastMillis to millis() at boot.
static void test_boot_after_long_gap_allows_nodeinfo()
{
+4 -4
View File
@@ -16,7 +16,7 @@ void test_ascii_unchanged()
void test_valid_2byte_unchanged()
{
// "café" — é is C3 A9
// "café" - é is C3 A9
char buf[16] = "caf\xC3\xA9";
TEST_ASSERT_FALSE(sanitizeUtf8(buf, sizeof(buf)));
TEST_ASSERT_EQUAL_STRING("caf\xC3\xA9", buf);
@@ -40,7 +40,7 @@ void test_valid_4byte_emoji_unchanged()
void test_valid_mixed_unchanged()
{
// "Hi 🌙!" — mix of ASCII and 4-byte
// "Hi 🌙!" - mix of ASCII and 4-byte
char buf[16] = "Hi \xF0\x9F\x8C\x99!";
TEST_ASSERT_FALSE(sanitizeUtf8(buf, sizeof(buf)));
TEST_ASSERT_EQUAL_STRING("Hi \xF0\x9F\x8C\x99!", buf);
@@ -98,7 +98,7 @@ void test_overlong_2byte()
void test_surrogate_half()
{
// ED A0 80 encodes U+D800 (surrogate half — invalid in UTF-8)
// ED A0 80 encodes U+D800 (surrogate half - invalid in UTF-8)
char buf[8] = "\xED\xA0\x80";
TEST_ASSERT_TRUE(sanitizeUtf8(buf, sizeof(buf)));
TEST_ASSERT_EQUAL_STRING("???", buf);
@@ -106,7 +106,7 @@ void test_surrogate_half()
void test_5byte_sequence_rejected()
{
// F8 80 80 80 80 — 5-byte sequence, not valid UTF-8
// F8 80 80 80 80 - 5-byte sequence, not valid UTF-8
char buf[8] = "\xF8\x80\x80\x80\x80";
TEST_ASSERT_TRUE(sanitizeUtf8(buf, sizeof(buf)));
// F8 is invalid lead (>= 0xF8), each 0x80 is bare continuation
+2 -2
View File
@@ -1,7 +1,7 @@
// Unit tests for the warm ("long-tail") node tier — src/mesh/WarmNodeStore.cpp.
// Unit tests for the warm ("long-tail") node tier - src/mesh/WarmNodeStore.cpp.
// Covers admission/eviction policy (keyed entries outrank keyless), take()
// rehydration semantics, and a tolerant persistence round trip.
#include "MeshTypes.h" // BEFORE TestUtil.h — provides WARM_NODE_COUNT via mesh-pb-constants.h
#include "MeshTypes.h" // BEFORE TestUtil.h - provides WARM_NODE_COUNT via mesh-pb-constants.h
#include "TestUtil.h"
#include <unity.h>