/* * Unit tests for PacketHistory — the packet deduplication engine * used by the mesh routing stack. * * PacketHistory maintains a fixed-size array of PacketRecords with an * optional hash table for O(1) lookup. It tracks which nodes relayed * each packet, supports LRU-style eviction, and detects fallback-to- * flooding and hop-limit upgrades. */ #include "PacketHistory.h" #include "TestUtil.h" #include // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- static constexpr uint32_t OUR_NODE_NUM = 0xDEAD1234; static constexpr uint8_t OUR_RELAY_ID = 0x34; // getLastByteOfNodeNum(OUR_NODE_NUM) static constexpr uint32_t SMALL_CAPACITY = 8; // --------------------------------------------------------------------------- // Per-test state // --------------------------------------------------------------------------- static PacketHistory *ph = nullptr; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- static meshtastic_MeshPacket makePacket(uint32_t from, uint32_t id, uint8_t hop_limit = 3, uint8_t next_hop = NO_NEXT_HOP_PREFERENCE, uint8_t relay_node = 0) { meshtastic_MeshPacket p = meshtastic_MeshPacket_init_zero; p.from = from; p.id = id; p.hop_limit = hop_limit; p.next_hop = next_hop; p.relay_node = relay_node; return p; } // --------------------------------------------------------------------------- // setUp / tearDown — called before and after every test // --------------------------------------------------------------------------- void setUp(void) { myNodeInfo.my_node_num = OUR_NODE_NUM; ph = new PacketHistory(SMALL_CAPACITY); } void tearDown(void) { delete ph; ph = nullptr; } // =========================================================================== // Group 1 — Initialization // =========================================================================== void test_init_valid_size(void) { PacketHistory h(8); TEST_ASSERT_TRUE(h.initOk()); } void test_init_minimum_size(void) { PacketHistory h(4); TEST_ASSERT_TRUE(h.initOk()); } void test_init_too_small_falls_back(void) { // Sizes < 4 or > PACKETHISTORY_MAX are clamped to PACKETHISTORY_MAX inside the constructor PacketHistory h(2); TEST_ASSERT_TRUE(h.initOk()); } // =========================================================================== // Group 2 — Basic Deduplication // =========================================================================== void test_first_packet_not_seen(void) { auto p = makePacket(0x1111, 100); TEST_ASSERT_FALSE(ph->wasSeenRecently(&p)); } void test_same_packet_seen_twice(void) { auto p = makePacket(0x1111, 100); TEST_ASSERT_FALSE(ph->wasSeenRecently(&p)); // first time TEST_ASSERT_TRUE(ph->wasSeenRecently(&p)); // duplicate } void test_different_id_not_confused(void) { auto p1 = makePacket(0x1111, 100); auto p2 = makePacket(0x1111, 200); ph->wasSeenRecently(&p1); TEST_ASSERT_FALSE(ph->wasSeenRecently(&p2)); } void test_different_sender_not_confused(void) { auto p1 = makePacket(0x1111, 100); auto p2 = makePacket(0x2222, 100); ph->wasSeenRecently(&p1); TEST_ASSERT_FALSE(ph->wasSeenRecently(&p2)); } void test_withUpdate_false_no_insert(void) { auto p = makePacket(0x1111, 100); // First call with withUpdate=false: should not store TEST_ASSERT_FALSE(ph->wasSeenRecently(&p, /*withUpdate=*/false)); // Second call with withUpdate=true: still not found because first didn't store TEST_ASSERT_FALSE(ph->wasSeenRecently(&p, /*withUpdate=*/true)); } void test_withUpdate_true_inserts(void) { auto p = makePacket(0x1111, 100); TEST_ASSERT_FALSE(ph->wasSeenRecently(&p, /*withUpdate=*/true)); TEST_ASSERT_TRUE(ph->wasSeenRecently(&p, /*withUpdate=*/false)); // found without inserting again } // =========================================================================== // Group 3 — LRU Eviction // =========================================================================== void test_fill_capacity_all_found(void) { for (uint32_t i = 1; i <= SMALL_CAPACITY; i++) { auto p = makePacket(0xAAAA, i); ph->wasSeenRecently(&p); } // All 8 should be found for (uint32_t i = 1; i <= SMALL_CAPACITY; i++) { auto p = makePacket(0xAAAA, i); TEST_ASSERT_TRUE(ph->wasSeenRecently(&p, false)); } } void test_eviction_oldest_replaced(void) { // Fill all 8 slots for (uint32_t i = 1; i <= SMALL_CAPACITY; i++) { auto p = makePacket(0xAAAA, i); ph->wasSeenRecently(&p); } // Advance time so the eviction logic can distinguish "oldest" from "newest". // insert() uses (now_millis - rxTimeMsec) > OldtrxTimeMsec with strict >, so // entries with identical timestamps all have age 0 and none gets selected. delay(1); // Insert a 9th packet — should evict the oldest auto p9 = makePacket(0xAAAA, 9); ph->wasSeenRecently(&p9); // The 9th should be found TEST_ASSERT_TRUE(ph->wasSeenRecently(&p9, false)); // At least one of the originals should have been evicted int evicted = 0; for (uint32_t i = 1; i <= SMALL_CAPACITY; i++) { auto p = makePacket(0xAAAA, i); if (!ph->wasSeenRecently(&p, false)) evicted++; } TEST_ASSERT_TRUE(evicted > 0); } void test_matching_slot_reused(void) { // 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); ph->wasSeenRecently(&p2); // Re-observe p1 (triggers merge path) ph->wasSeenRecently(&p1); // Both should still be present TEST_ASSERT_TRUE(ph->wasSeenRecently(&p1, false)); TEST_ASSERT_TRUE(ph->wasSeenRecently(&p2, false)); } void test_free_slot_preferred(void) { // 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); } auto p5 = makePacket(0xAAAA, 5); ph->wasSeenRecently(&p5); // All 5 should be present (no eviction needed) for (uint32_t i = 1; i <= 5; i++) { auto p = makePacket(0xAAAA, i); TEST_ASSERT_TRUE(ph->wasSeenRecently(&p, false)); } } void test_evict_all_old_packets(void) { // Fill with packets 1..8 for (uint32_t i = 1; i <= SMALL_CAPACITY; i++) { auto p = makePacket(0xAAAA, i); ph->wasSeenRecently(&p); } // Advance time so the replacement batch can evict the originals delay(1); // Replace all with packets 101..108 for (uint32_t i = 101; i <= 100 + SMALL_CAPACITY; i++) { auto p = makePacket(0xBBBB, i); ph->wasSeenRecently(&p); } // None of the originals should be found for (uint32_t i = 1; i <= SMALL_CAPACITY; i++) { auto p = makePacket(0xAAAA, i); TEST_ASSERT_FALSE(ph->wasSeenRecently(&p, false)); } // All new ones should be found for (uint32_t i = 101; i <= 100 + SMALL_CAPACITY; i++) { auto p = makePacket(0xBBBB, i); TEST_ASSERT_TRUE(ph->wasSeenRecently(&p, false)); } } // =========================================================================== // Group 4 — Relayer Tracking // =========================================================================== void test_wasRelayer_true(void) { // Non-us relay_nodes only enter relayed_by[] through the "heard-back" merge path: // we must have relayed first, then observe the packet return at hop_limit-1. auto p1 = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID); ph->wasSeenRecently(&p1); // Heard-back from 0xCC at hop_limit=2 (ourTxHopLimit-1) triggers the merge auto p2 = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, 0xCC); ph->wasSeenRecently(&p2); TEST_ASSERT_TRUE(ph->wasRelayer(0xCC, 100, 0x1111)); } void test_wasRelayer_false(void) { auto p = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, 0xAA); ph->wasSeenRecently(&p); // 0xCC was never a relayer TEST_ASSERT_FALSE(ph->wasRelayer(0xCC, 100, 0x1111)); } void test_wasRelayer_zero_returns_false(void) { auto p = makePacket(0x1111, 100); ph->wasSeenRecently(&p); TEST_ASSERT_FALSE(ph->wasRelayer(0, 100, 0x1111)); } void test_wasRelayer_not_found(void) { // Packet not in history at all TEST_ASSERT_FALSE(ph->wasRelayer(0xAA, 999, 0x9999)); } void test_wasRelayer_wasSole_true(void) { // relay_node = ourRelayID → relayed_by[0] = ourRelayID auto p = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID); ph->wasSeenRecently(&p); bool wasSole = false; bool result = ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111, &wasSole); TEST_ASSERT_TRUE(result); TEST_ASSERT_TRUE(wasSole); } void test_wasRelayer_wasSole_false(void) { // First observation: we relay auto p1 = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID); ph->wasSeenRecently(&p1); // Second observation: different relayer adds to record auto p2 = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, 0xBB); ph->wasSeenRecently(&p2); bool wasSole = true; bool result = ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111, &wasSole); TEST_ASSERT_TRUE(result); TEST_ASSERT_FALSE(wasSole); } void test_wasRelayer_all_six_slots(void) { // First observation: we relay with hop_limit=3 (fills slot 0, ourTxHopLimit=3) auto p = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID); ph->wasSeenRecently(&p); // Each heard-back must satisfy: hop_limit == ourTxHopLimit OR ourTxHopLimit-1. // Using hop_limit=2 (ourTxHopLimit-1) for all, which triggers the heard-back // merge path each time. Each new relay_node pushes to slot 0 and shifts existing // relayers right, eventually filling all NUM_RELAYERS(6) slots. uint8_t relayers[] = {0x11, 0x22, 0x33, 0x44, 0x55}; for (int i = 0; i < 5; i++) { auto pn = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, relayers[i]); ph->wasSeenRecently(&pn); } // All 6 should be detected TEST_ASSERT_TRUE(ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111)); for (int i = 0; i < 5; i++) { TEST_ASSERT_TRUE(ph->wasRelayer(relayers[i], 100, 0x1111)); } } // =========================================================================== // Group 5 — removeRelayer // =========================================================================== void test_removeRelayer_removes(void) { auto p1 = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID); ph->wasSeenRecently(&p1); TEST_ASSERT_TRUE(ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111)); ph->removeRelayer(OUR_RELAY_ID, 100, 0x1111); TEST_ASSERT_FALSE(ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111)); } void test_removeRelayer_compacts(void) { // We relay first auto p1 = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID); ph->wasSeenRecently(&p1); // Second relayer auto p2 = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, 0xBB); ph->wasSeenRecently(&p2); // Remove us, 0xBB should still be found ph->removeRelayer(OUR_RELAY_ID, 100, 0x1111); TEST_ASSERT_FALSE(ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111)); TEST_ASSERT_TRUE(ph->wasRelayer(0xBB, 100, 0x1111)); } void test_removeRelayer_nonexistent_safe(void) { auto p = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID); ph->wasSeenRecently(&p); // Removing a relayer that doesn't exist should not crash ph->removeRelayer(0xFF, 100, 0x1111); // Original should still be there TEST_ASSERT_TRUE(ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111)); } void test_removeRelayer_packet_not_found_safe(void) { // Packet not in history — should not crash ph->removeRelayer(0xAA, 999, 0x9999); } // =========================================================================== // Group 6 — checkRelayers // =========================================================================== void test_checkRelayers_both_found(void) { // We relay first auto p1 = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID); ph->wasSeenRecently(&p1); // Second relayer auto p2 = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, 0xBB); ph->wasSeenRecently(&p2); bool r1 = false, r2 = false; ph->checkRelayers(OUR_RELAY_ID, 0xBB, 100, 0x1111, &r1, &r2); TEST_ASSERT_TRUE(r1); TEST_ASSERT_TRUE(r2); } void test_checkRelayers_one_found(void) { auto p = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID); ph->wasSeenRecently(&p); bool r1 = false, r2 = false; ph->checkRelayers(OUR_RELAY_ID, 0xCC, 100, 0x1111, &r1, &r2); TEST_ASSERT_TRUE(r1); TEST_ASSERT_FALSE(r2); } void test_checkRelayers_r2WasSole(void) { auto p = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID); ph->wasSeenRecently(&p); bool r1 = false, r2 = false, r2Sole = false; // relayer1=0xCC (not found), relayer2=OUR_RELAY_ID (sole relayer) ph->checkRelayers(0xCC, OUR_RELAY_ID, 100, 0x1111, &r1, &r2, &r2Sole); TEST_ASSERT_FALSE(r1); TEST_ASSERT_TRUE(r2); TEST_ASSERT_TRUE(r2Sole); } // =========================================================================== // Group 7 — wasSeenRecently Merge Logic // =========================================================================== void test_merge_preserves_original_next_hop(void) { // First observation with next_hop=0x55 auto p1 = makePacket(0x1111, 100, 3, 0x55, 0xAA); ph->wasSeenRecently(&p1); // Re-observation with different next_hop auto p2 = makePacket(0x1111, 100, 2, 0x77, 0xBB); ph->wasSeenRecently(&p2); // The stored next_hop should still be 0x55 (the original) // We verify via weWereNextHop: if we set original next_hop to ourRelayID, it should detect it auto p3 = makePacket(0x1111, 200, 3, OUR_RELAY_ID, 0xAA); ph->wasSeenRecently(&p3); auto p4 = makePacket(0x1111, 200, 2, 0x99, 0xBB); bool weWereNextHop = false; ph->wasSeenRecently(&p4, true, nullptr, &weWereNextHop); TEST_ASSERT_TRUE(weWereNextHop); } void test_merge_preserves_highest_hop_limit(void) { // First observation with hop_limit=5 auto p1 = makePacket(0x1111, 100, 5); ph->wasSeenRecently(&p1); // Re-observation with hop_limit=2 (lower) auto p2 = makePacket(0x1111, 100, 2); ph->wasSeenRecently(&p2); // Third observation with hop_limit=3 should not trigger upgrade (highest was 5) bool wasUpgraded = true; auto p3 = makePacket(0x1111, 100, 3); ph->wasSeenRecently(&p3, true, nullptr, nullptr, &wasUpgraded); TEST_ASSERT_FALSE(wasUpgraded); } void test_merge_no_duplicate_relayers(void) { // Observe with relayer 0xAA (stored via relay_node, but only slot 0 for ourRelayID) // We need to use ourRelayID for the first observation to get it into slot 0 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 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 bool wasSole = false; TEST_ASSERT_TRUE(ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111, &wasSole)); TEST_ASSERT_TRUE(wasSole); } void test_merge_adds_new_relayer(void) { auto p1 = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID); ph->wasSeenRecently(&p1); auto p2 = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, 0xBB); ph->wasSeenRecently(&p2); TEST_ASSERT_TRUE(ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111)); TEST_ASSERT_TRUE(ph->wasRelayer(0xBB, 100, 0x1111)); } void test_merge_we_relay_sets_slot_zero(void) { // When relay_node == ourRelayID, relayed_by[0] should be set to ourRelayID auto p = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID); ph->wasSeenRecently(&p); TEST_ASSERT_TRUE(ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111)); } void test_merge_heard_back_stores_relay_node(void) { // First: we relay (hop_limit=3) auto p1 = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID); ph->wasSeenRecently(&p1); // Second: we hear the packet back with hop_limit=2 (one less), from relay_node=0xCC // This triggers the "heard back" logic: weWereRelayer && hop_limit == ourTxHopLimit-1 auto p2 = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, 0xCC); ph->wasSeenRecently(&p2); TEST_ASSERT_TRUE(ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111)); TEST_ASSERT_TRUE(ph->wasRelayer(0xCC, 100, 0x1111)); } // =========================================================================== // Group 8 — Fallback-to-Flooding Detection // =========================================================================== void test_fallback_detected(void) { // The fallback condition requires wasRelayer(relay_node) && !wasRelayer(ourRelayID). // Non-us relayers only enter relayed_by[] via the heard-back merge path, which // also stores ourRelayID. So we must removeRelayer(ourRelayID) to satisfy both. // // Scenario: we relay a directed packet, hear it back from 0xAA, then the router // removes us from the relayer list. Later the sender falls back to flooding. // Step 1: We relay (directed to next_hop=0x55) auto p1 = makePacket(0x1111, 100, 3, 0x55, OUR_RELAY_ID); ph->wasSeenRecently(&p1); // Step 2: Heard-back from 0xAA at hop_limit-1 → stores 0xAA in relayed_by auto p2 = makePacket(0x1111, 100, 2, 0x55, 0xAA); ph->wasSeenRecently(&p2); // 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 auto p3 = makePacket(0x1111, 100, 1, NO_NEXT_HOP_PREFERENCE, 0xAA); bool wasFallback = false; ph->wasSeenRecently(&p3, true, &wasFallback); TEST_ASSERT_TRUE(wasFallback); } void test_fallback_not_when_we_relayed(void) { // First observation: directed, we relayed it auto p1 = makePacket(0x1111, 100, 3, 0x55, OUR_RELAY_ID); ph->wasSeenRecently(&p1); // Second observation: fallback to flooding from same relayer (us) // But since we already relayed, wasFallback should be false auto p2 = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID); bool wasFallback = false; ph->wasSeenRecently(&p2, true, &wasFallback); TEST_ASSERT_FALSE(wasFallback); } void test_fallback_not_on_first_observation(void) { // 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); TEST_ASSERT_FALSE(wasFallback); } // =========================================================================== // Group 9 — Next-Hop and Upgrade Detection // =========================================================================== void test_weWereNextHop_true(void) { // Packet directed to us (next_hop = ourRelayID) auto p1 = makePacket(0x1111, 100, 3, OUR_RELAY_ID, 0xAA); ph->wasSeenRecently(&p1); // Re-observe: check if we were the original next_hop auto p2 = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, 0xBB); bool weWereNextHop = false; ph->wasSeenRecently(&p2, true, nullptr, &weWereNextHop); TEST_ASSERT_TRUE(weWereNextHop); } void test_weWereNextHop_false(void) { // Packet directed to someone else auto p1 = makePacket(0x1111, 100, 3, 0x99, 0xAA); ph->wasSeenRecently(&p1); auto p2 = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, 0xBB); bool weWereNextHop = false; ph->wasSeenRecently(&p2, true, nullptr, &weWereNextHop); TEST_ASSERT_FALSE(weWereNextHop); } void test_wasUpgraded_true(void) { // First observation with hop_limit=3 → stored as highestHopLimit bits 0-2 = 3 auto p1 = makePacket(0x1111, 100, 3); ph->wasSeenRecently(&p1); // Re-observation with hop_limit=5 // The upgrade check on line 122 compares the raw packed byte found->hop_limit against p->hop_limit. // found->hop_limit has highestHopLimit=3 in bits 0-2 (and possibly ourTxHopLimit in bits 3-5). // So the packed byte value is 3 (or more if ourTxHopLimit was set), and p->hop_limit is 5. // Since 3 < 5 (with no ourTxHopLimit set), this should detect an upgrade. auto p2 = makePacket(0x1111, 100, 5); bool wasUpgraded = false; ph->wasSeenRecently(&p2, true, nullptr, nullptr, &wasUpgraded); TEST_ASSERT_TRUE(wasUpgraded); } void test_wasUpgraded_false(void) { auto p1 = makePacket(0x1111, 100, 5); ph->wasSeenRecently(&p1); // Same or lower hop_limit auto p2 = makePacket(0x1111, 100, 3); bool wasUpgraded = false; ph->wasSeenRecently(&p2, true, nullptr, nullptr, &wasUpgraded); TEST_ASSERT_FALSE(wasUpgraded); } // =========================================================================== // Group 10 — Edge Cases // =========================================================================== void test_packet_id_zero_not_stored(void) { auto p = makePacket(0x1111, 0); TEST_ASSERT_FALSE(ph->wasSeenRecently(&p)); TEST_ASSERT_FALSE(ph->wasSeenRecently(&p)); // still not found } void test_sender_zero_substituted(void) { // from=0 means "from us" — getFrom() substitutes nodeDB->getNodeNum() auto p = makePacket(0, 100); ph->wasSeenRecently(&p); // Should be stored under our node num, not 0 auto p2 = makePacket(OUR_NODE_NUM, 100); TEST_ASSERT_TRUE(ph->wasSeenRecently(&p2, false)); } // NOTE: Tests for the !initOk() short-circuit in wasSeenRecently / wasRelayer // were removed: the only way to drive that branch from a test is to destruct // the object and then call methods on it, which is UB and trips ASAN under // `pio test -e coverage`. In practice the guard can only fire if `new[]` throws // (in which case the constructor doesn't return), so the lost coverage is // purely defensive. void test_multiple_instances_independent(void) { PacketHistory h2(SMALL_CAPACITY); auto p = makePacket(0x1111, 100); ph->wasSeenRecently(&p); // h2 should NOT find it TEST_ASSERT_FALSE(h2.wasSeenRecently(&p, false)); // ph should still find it TEST_ASSERT_TRUE(ph->wasSeenRecently(&p, false)); } // =========================================================================== // Group 11 — Hash Table Stress // =========================================================================== void test_many_packets_no_false_negatives(void) { PacketHistory big(64); for (uint32_t i = 1; i <= 64; i++) { auto p = makePacket(0xAAAA, i); big.wasSeenRecently(&p); } for (uint32_t i = 1; i <= 64; i++) { auto p = makePacket(0xAAAA, i); TEST_ASSERT_TRUE_MESSAGE(big.wasSeenRecently(&p, false), "False negative in hash table"); } } void test_many_packets_no_false_positives(void) { PacketHistory big(64); for (uint32_t i = 1; i <= 64; i++) { auto p = makePacket(0xAAAA, i); big.wasSeenRecently(&p); } // IDs 65..128 were never inserted for (uint32_t i = 65; i <= 128; i++) { auto p = makePacket(0xAAAA, i); TEST_ASSERT_FALSE_MESSAGE(big.wasSeenRecently(&p, false), "False positive in hash table"); } } void test_churn_correctness(void) { // Insert 3x capacity to force heavy eviction. // Advance time between each generation so eviction can distinguish old from new. PacketHistory big(32); uint32_t capacity = 32; uint32_t generations = 3; for (uint32_t gen = 0; gen < generations; gen++) { if (gen > 0) delay(1); // Ensure new generation has a newer timestamp than the old for (uint32_t i = 1; i <= capacity; i++) { auto p = makePacket(0xAAAA, gen * capacity + i); big.wasSeenRecently(&p); } } uint32_t total = capacity * generations; // Only the most recent 32 should be present (due to LRU eviction) for (uint32_t i = total - 31; i <= total; i++) { auto p = makePacket(0xAAAA, i); TEST_ASSERT_TRUE_MESSAGE(big.wasSeenRecently(&p, false), "Recent packet lost after churn"); } // Older packets should be gone int found = 0; for (uint32_t i = 1; i <= total - capacity; i++) { auto p = makePacket(0xAAAA, i); if (big.wasSeenRecently(&p, false)) found++; } TEST_ASSERT_EQUAL_INT_MESSAGE(0, found, "Evicted packets should not be found"); } // =========================================================================== // Test runner // =========================================================================== void setup() { delay(10); delay(2000); initializeTestEnvironment(); UNITY_BEGIN(); // 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 RUN_TEST(test_first_packet_not_seen); RUN_TEST(test_same_packet_seen_twice); RUN_TEST(test_different_id_not_confused); RUN_TEST(test_different_sender_not_confused); RUN_TEST(test_withUpdate_false_no_insert); RUN_TEST(test_withUpdate_true_inserts); // 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 RUN_TEST(test_wasRelayer_true); RUN_TEST(test_wasRelayer_false); RUN_TEST(test_wasRelayer_zero_returns_false); RUN_TEST(test_wasRelayer_not_found); RUN_TEST(test_wasRelayer_wasSole_true); RUN_TEST(test_wasRelayer_wasSole_false); RUN_TEST(test_wasRelayer_all_six_slots); // 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 RUN_TEST(test_checkRelayers_both_found); RUN_TEST(test_checkRelayers_one_found); RUN_TEST(test_checkRelayers_r2WasSole); // 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); RUN_TEST(test_merge_adds_new_relayer); RUN_TEST(test_merge_we_relay_sets_slot_zero); RUN_TEST(test_merge_heard_back_stores_relay_node); // 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 RUN_TEST(test_weWereNextHop_true); RUN_TEST(test_weWereNextHop_false); RUN_TEST(test_wasUpgraded_true); RUN_TEST(test_wasUpgraded_false); // 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 RUN_TEST(test_many_packets_no_false_negatives); RUN_TEST(test_many_packets_no_false_positives); RUN_TEST(test_churn_correctness); exit(UNITY_END()); } void loop() {}