/* * Copyright (c) 2026, The OpenThread Authors. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. Neither the name of the copyright holder nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #include "instance/instance.hpp" #include "test_platform.h" #include "test_util.hpp" namespace ot { #if OPENTHREAD_CONFIG_SEEKER_ENABLE || OPENTHREAD_CONFIG_JOINER_ENABLE class UnitTester { public: using Seeker = MeshCoP::Seeker; using CandidateEntry = MeshCoP::Seeker::CandidateEntry; static void CreateScanResult(ScanResult &aResult, uint64_t aExtPanId, uint64_t aExtAddr, int8_t aRssi) { ClearAllBytes(aResult); LittleEndian::WriteUint64(aExtPanId, aResult.mExtendedPanId.m8); LittleEndian::WriteUint64(aExtAddr, aResult.mExtAddress.m8); aResult.mRssi = aRssi; aResult.mPanId = static_cast(aExtPanId & 0xffff); aResult.mChannel = 11; aResult.mJoinerUdpPort = 1000; } static void LogCandidate(const CandidateEntry &aEntry) { if (aEntry.IsEmpty()) { printf(" empty\n"); } else { printf(" ext-addr:%2.2s, ext-panid:%4.4s, rssi:%d, prf:%u, conn-attempted:%u\n", aEntry.mExtAddr.ToString().AsCString(), aEntry.mExtPanId.ToString().AsCString(), aEntry.mRssi, aEntry.mPreferred, aEntry.mConnAttempted); } } static void LogCandidates(const Seeker &aSeeker) { CandidateEntry entry; printf("\nCandidates:\n"); for (entry.InitForIteration(); aSeeker.mCandidates.ReadNext(entry) == kErrorNone;) { LogCandidate(entry); } printf("\n"); } static void SaveCandidate(Seeker &aSeeker, uint64_t aExtPanId, uint64_t aExtAddr, int8_t aRssi, bool aPreferred) { ScanResult result; CreateScanResult(result, aExtPanId, aExtAddr, aRssi); aSeeker.SaveCandidate(result, aPreferred); } static bool Contains(const Seeker &aSeeker, uint64_t aExtPanId, uint64_t aExtAddr) { MeshCoP::ExtendedPanId extPanId; Mac::ExtAddress extAddr; CandidateEntry entry; LittleEndian::WriteUint64(aExtPanId, extPanId.m8); LittleEndian::WriteUint64(aExtAddr, extAddr.m8); return (aSeeker.mCandidates.FindMatching(entry, extPanId, extAddr) == kErrorNone); } static void StartCandidateSelection(Seeker &aSeeker) { // Manually set the state so we can call and validate the // `SelectNextCandidate()`. aSeeker.SetState(Seeker::kStateConnectingNetworks); } static void SelectNextCandidate(Seeker &aSeeker, CandidateEntry &aEntry) { Error error = aSeeker.SelectNextCandidate(aEntry); if (error == kErrorNone) { aEntry.mConnAttempted = true; SuccessOrQuit(aSeeker.mCandidates.Write(aEntry)); } else { aEntry.MarkAsEmpty(); } } static constexpr uint16_t kSelectionArraySize = 32; using ExtAddrArray = Array; static void CheckSelection(Seeker &aSeeker, const ExtAddrArray &aExtAddrs) { CandidateEntry entry; Mac::ExtAddress extAddr; printf("\nSelection order:\n"); StartCandidateSelection(aSeeker); for (uint64_t addr : aExtAddrs) { SelectNextCandidate(aSeeker, entry); LogCandidate(entry); VerifyOrQuit(!entry.IsEmpty()); LittleEndian::WriteUint64(addr, extAddr.m8); VerifyOrQuit(entry.mExtAddr == extAddr); VerifyOrQuit(entry.mConnAttempted); } SelectNextCandidate(aSeeker, entry); VerifyOrQuit(entry.IsEmpty()); } static void TestSeekerCandidates(void) { Instance *instance; printf("TestSeekerCandidates()\n"); instance = static_cast(testInitInstance()); VerifyOrQuit(instance != nullptr); Seeker &seeker = instance->Get(); CandidateEntry entry; ExtAddrArray selectionOrder; printf("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n"); printf("Basic addition & replacement\n\n"); seeker.Stop(); printf("Save a single candidate"); SaveCandidate(seeker, 0xaaaa, 0xa1, -50, false); LogCandidates(seeker); VerifyOrQuit(seeker.mCandidates.GetLength() == 1); VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa1)); printf("Save same candidate with better RSSI"); SaveCandidate(seeker, 0xaaaa, 0xa1, -40, false); LogCandidates(seeker); VerifyOrQuit(seeker.mCandidates.GetLength() == 1); VerifyOrQuit(seeker.mCandidates.ReadAt(0, entry) == kErrorNone); VerifyOrQuit(entry.mRssi == -40); printf("Save same candidate with worse RSSI, still should replace as it is same extAddr\n"); SaveCandidate(seeker, 0xaaaa, 0xa1, -60, false); LogCandidates(seeker); VerifyOrQuit(seeker.mCandidates.GetLength() == 1); VerifyOrQuit(seeker.mCandidates.ReadAt(0, entry) == kErrorNone); VerifyOrQuit(entry.mRssi == -60); printf("Validate candidate selection with single entry in array\n\n"); selectionOrder.Clear(); SuccessOrQuit(selectionOrder.PushBack(0xa1)); CheckSelection(seeker, selectionOrder); printf("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n"); printf("Max candidates per network (limit = 3)\n\n"); seeker.Stop(); printf("Save 3 candidates for network 0xaaaa along with some extra entries\n"); SaveCandidate(seeker, 0xaaaa, 0xa1, -50, false); SaveCandidate(seeker, 0xbbbb, 0xb1, -70, true); SaveCandidate(seeker, 0xaaaa, 0xa2, -52, false); SaveCandidate(seeker, 0xcccc, 0xc1, -80, true); SaveCandidate(seeker, 0xaaaa, 0xa3, -51, false); SaveCandidate(seeker, 0xdddd, 0xd1, -40, false); LogCandidates(seeker); VerifyOrQuit(seeker.mCandidates.GetLength() == 6); VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa1)); VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa2)); VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa3)); printf("Try adding 4th for 0xaaaa (worse RSSI) -> should be dropped\n"); SaveCandidate(seeker, 0xaaaa, 0xa4, -90, false); LogCandidates(seeker); VerifyOrQuit(seeker.mCandidates.GetLength() == 6); VerifyOrQuit(!Contains(seeker, 0xaaaa, 0xa4)); VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa1)); VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa2)); VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa3)); printf("Try adding 4th for 0xaaaa (better RSSI) -> should replace a2 (lowest RSSI)\n"); SaveCandidate(seeker, 0xaaaa, 0xa5, -40, false); LogCandidates(seeker); VerifyOrQuit(seeker.mCandidates.GetLength() == 6); VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa5)); VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa1)); VerifyOrQuit(!Contains(seeker, 0xaaaa, 0xa2)); VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa3)); printf("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n"); printf("Behavior under full candidates array and eviction\n\n"); SaveCandidate(seeker, 0xbbbb, 0xb2, -75, true); SaveCandidate(seeker, 0xeeee, 0xe1, -30, false); LogCandidates(seeker); VerifyOrQuit(seeker.mCandidates.GetLength() == 8); VerifyOrQuit(seeker.mCandidates.IsFull()); VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa1)); VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa5)); VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa3)); VerifyOrQuit(Contains(seeker, 0xbbbb, 0xb1)); VerifyOrQuit(Contains(seeker, 0xbbbb, 0xb2)); VerifyOrQuit(Contains(seeker, 0xcccc, 0xc1)); VerifyOrQuit(Contains(seeker, 0xdddd, 0xd1)); VerifyOrQuit(Contains(seeker, 0xeeee, 0xe1)); printf("Try adding new entry 0xb3 for 0xbbbb with better RSSI -> should replace 0xb2\n"); SaveCandidate(seeker, 0xbbbb, 0xb3, -65, true); LogCandidates(seeker); VerifyOrQuit(seeker.mCandidates.GetLength() == 8); VerifyOrQuit(seeker.mCandidates.IsFull()); VerifyOrQuit(Contains(seeker, 0xbbbb, 0xb1)); VerifyOrQuit(Contains(seeker, 0xbbbb, 0xb3)); VerifyOrQuit(!Contains(seeker, 0xbbbb, 0xb2)); VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa1)); VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa5)); VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa3)); VerifyOrQuit(Contains(seeker, 0xcccc, 0xc1)); VerifyOrQuit(Contains(seeker, 0xdddd, 0xd1)); VerifyOrQuit(Contains(seeker, 0xeeee, 0xe1)); printf("Try adding new entry 0xb4 for 0xbbbb with worst RSSI -> should be dropped\n"); SaveCandidate(seeker, 0xbbbb, 0xb4, -95, true); LogCandidates(seeker); VerifyOrQuit(seeker.mCandidates.GetLength() == 8); VerifyOrQuit(seeker.mCandidates.IsFull()); VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa1)); VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa5)); VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa3)); VerifyOrQuit(Contains(seeker, 0xbbbb, 0xb1)); VerifyOrQuit(Contains(seeker, 0xbbbb, 0xb3)); VerifyOrQuit(Contains(seeker, 0xcccc, 0xc1)); VerifyOrQuit(Contains(seeker, 0xdddd, 0xd1)); VerifyOrQuit(Contains(seeker, 0xeeee, 0xe1)); printf("Try adding new entry 0xc2 for 0xcccc with better RSSI but not preferred -> should be ignored\n"); SaveCandidate(seeker, 0xcccc, 0xc2, -40, false); LogCandidates(seeker); VerifyOrQuit(seeker.mCandidates.GetLength() == 8); VerifyOrQuit(seeker.mCandidates.IsFull()); VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa1)); VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa5)); VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa3)); VerifyOrQuit(Contains(seeker, 0xbbbb, 0xb1)); VerifyOrQuit(Contains(seeker, 0xbbbb, 0xb3)); VerifyOrQuit(Contains(seeker, 0xcccc, 0xc1)); VerifyOrQuit(Contains(seeker, 0xdddd, 0xd1)); VerifyOrQuit(Contains(seeker, 0xeeee, 0xe1)); printf("Try adding new entry 0xc3 for 0xcccc with better RSSI and preferred -> should replace 0xc1\n"); SaveCandidate(seeker, 0xcccc, 0xc3, -40, true); LogCandidates(seeker); VerifyOrQuit(seeker.mCandidates.GetLength() == 8); VerifyOrQuit(seeker.mCandidates.IsFull()); VerifyOrQuit(Contains(seeker, 0xcccc, 0xc3)); VerifyOrQuit(!Contains(seeker, 0xcccc, 0xc1)); VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa1)); VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa5)); VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa3)); VerifyOrQuit(Contains(seeker, 0xbbbb, 0xb1)); VerifyOrQuit(Contains(seeker, 0xbbbb, 0xb3)); VerifyOrQuit(Contains(seeker, 0xdddd, 0xd1)); VerifyOrQuit(Contains(seeker, 0xeeee, 0xe1)); printf("Try adding new entry 0xe2 for 0xeeee with worse RSSI but preferred -> should replace 0xe1\n"); SaveCandidate(seeker, 0xeeee, 0xe2, -99, true); LogCandidates(seeker); VerifyOrQuit(seeker.mCandidates.GetLength() == 8); VerifyOrQuit(seeker.mCandidates.IsFull()); VerifyOrQuit(!Contains(seeker, 0xeeee, 0xe1)); VerifyOrQuit(Contains(seeker, 0xeeee, 0xe2)); VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa1)); VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa5)); VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa3)); VerifyOrQuit(Contains(seeker, 0xbbbb, 0xb1)); VerifyOrQuit(Contains(seeker, 0xbbbb, 0xb3)); VerifyOrQuit(Contains(seeker, 0xcccc, 0xc3)); VerifyOrQuit(Contains(seeker, 0xdddd, 0xd1)); printf("Try adding new network, 0xf1 for 0xffff -> should evict 0xa3\n"); SaveCandidate(seeker, 0xffff, 0xf1, -65, false); LogCandidates(seeker); VerifyOrQuit(seeker.mCandidates.GetLength() == 8); VerifyOrQuit(seeker.mCandidates.IsFull()); VerifyOrQuit(!Contains(seeker, 0xaaaa, 0xa3)); VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa1)); VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa5)); VerifyOrQuit(Contains(seeker, 0xbbbb, 0xb1)); VerifyOrQuit(Contains(seeker, 0xbbbb, 0xb3)); VerifyOrQuit(Contains(seeker, 0xcccc, 0xc3)); VerifyOrQuit(Contains(seeker, 0xdddd, 0xd1)); VerifyOrQuit(Contains(seeker, 0xeeee, 0xe2)); VerifyOrQuit(Contains(seeker, 0xffff, 0xf1)); printf("Adding two new entries for new network -> should evict 0xa1 and 0xb1\n"); SaveCandidate(seeker, 0x1234, 0x01, -80, false); SaveCandidate(seeker, 0x5678, 0x02, -70, false); LogCandidates(seeker); VerifyOrQuit(seeker.mCandidates.GetLength() == 8); VerifyOrQuit(seeker.mCandidates.IsFull()); VerifyOrQuit(!Contains(seeker, 0xaaaa, 0xa1)); VerifyOrQuit(!Contains(seeker, 0xbbbb, 0xb1)); VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa5)); VerifyOrQuit(Contains(seeker, 0xbbbb, 0xb3)); VerifyOrQuit(Contains(seeker, 0xcccc, 0xc3)); VerifyOrQuit(Contains(seeker, 0xdddd, 0xd1)); VerifyOrQuit(Contains(seeker, 0xeeee, 0xe2)); VerifyOrQuit(Contains(seeker, 0xffff, 0xf1)); VerifyOrQuit(Contains(seeker, 0x1234, 0x01)); VerifyOrQuit(Contains(seeker, 0x5678, 0x02)); printf("The candidates array is full and consists of distinct networks\n"); printf("Try adding a new entry for yet another network -> should be dropped\n"); SaveCandidate(seeker, 0xabcd, 0x03, -80, true); LogCandidates(seeker); VerifyOrQuit(seeker.mCandidates.GetLength() == 8); VerifyOrQuit(seeker.mCandidates.IsFull()); VerifyOrQuit(Contains(seeker, 0xaaaa, 0xa5)); VerifyOrQuit(Contains(seeker, 0xbbbb, 0xb3)); VerifyOrQuit(Contains(seeker, 0xcccc, 0xc3)); VerifyOrQuit(Contains(seeker, 0xdddd, 0xd1)); VerifyOrQuit(Contains(seeker, 0xeeee, 0xe2)); VerifyOrQuit(Contains(seeker, 0xffff, 0xf1)); VerifyOrQuit(Contains(seeker, 0x1234, 0x01)); VerifyOrQuit(Contains(seeker, 0x5678, 0x02)); selectionOrder.Clear(); SuccessOrQuit(selectionOrder.PushBack(0xc3)); SuccessOrQuit(selectionOrder.PushBack(0xb3)); SuccessOrQuit(selectionOrder.PushBack(0xe2)); SuccessOrQuit(selectionOrder.PushBack(0xa5)); SuccessOrQuit(selectionOrder.PushBack(0xd1)); SuccessOrQuit(selectionOrder.PushBack(0xf1)); SuccessOrQuit(selectionOrder.PushBack(0x02)); SuccessOrQuit(selectionOrder.PushBack(0x01)); CheckSelection(seeker, selectionOrder); printf("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n"); printf("Selection strategy\n\n"); seeker.Stop(); SaveCandidate(seeker, 0xdddd, 0xd1, -30, false); SaveCandidate(seeker, 0xaaaa, 0xa1, -60, false); SaveCandidate(seeker, 0xeeee, 0xe1, -30, true); SaveCandidate(seeker, 0xbbbb, 0xb1, -65, true); SaveCandidate(seeker, 0xaaaa, 0xa2, -40, false); SaveCandidate(seeker, 0xcccc, 0xc1, -90, true); SaveCandidate(seeker, 0xaaaa, 0xa3, -70, false); SaveCandidate(seeker, 0xcccc, 0xc2, -40, false); LogCandidates(seeker); VerifyOrQuit(seeker.mCandidates.GetLength() == 8); // First we should go through all distinct networks, starting // with most favored over all. Then go through the extra // backup candidates. // // For `0xaaaa`, we have 3 candidates: // ext-addr:a2, ext-panid:aaaa, rssi:-40, prf:0, conn-attempted:0 // ext-addr:a1, ext-panid:aaaa, rssi:-60, prf:0, conn-attempted:0 // ext-addr:a3, ext-panid:aaaa, rssi:-70, prf:0, conn-attempted:0 // // For `0xbbbb`, only one candidate: // ext-addr:b1, ext-panid:bbbb, rssi:-65, prf:1, conn-attempted:0 // // For `0xcccc`, we have two: // ext-addr:c1, ext-panid:cccc, rssi:-90, prf:1, conn-attempted:0 // ext-addr:c2, ext-panid:cccc, rssi:-40, prf:0, conn-attempted:0 // // For `0xdddd`, we have one: // ext-addr:d1, ext-panid:dddd, rssi:-30, prf:0, conn-attempted:0 // // For `0xeeee`, we have one: // ext-addr:e1, ext-panid:eeee, rssi:-30, prf:1, conn-attempted:0 // // We go through networks first // - e1 has highest RSSI and also preferred // - b1 is preferred with high RSSI // - c1 is also preferred even though it has low RSSI // - d1 has best RSSI among non-preferred // - a2 would be next among all `0xaaaa` candidates // // Next we go through remaining candidates // - c2, a1 and a3 selectionOrder.Clear(); SuccessOrQuit(selectionOrder.PushBack(0xe1)); SuccessOrQuit(selectionOrder.PushBack(0xb1)); SuccessOrQuit(selectionOrder.PushBack(0xc1)); SuccessOrQuit(selectionOrder.PushBack(0xd1)); SuccessOrQuit(selectionOrder.PushBack(0xa2)); SuccessOrQuit(selectionOrder.PushBack(0xc2)); SuccessOrQuit(selectionOrder.PushBack(0xa1)); SuccessOrQuit(selectionOrder.PushBack(0xa3)); CheckSelection(seeker, selectionOrder); seeker.Stop(); // Adding two candidates for 3 networks (total 6) SaveCandidate(seeker, 0xcccc, 0xc2, -92, true); SaveCandidate(seeker, 0xaaaa, 0xa2, -76, true); SaveCandidate(seeker, 0xbbbb, 0xb2, -56, false); SaveCandidate(seeker, 0xbbbb, 0xb1, -55, false); SaveCandidate(seeker, 0xcccc, 0xc1, -90, true); SaveCandidate(seeker, 0xaaaa, 0xa1, -75, true); LogCandidates(seeker); VerifyOrQuit(seeker.mCandidates.GetLength() == 6); selectionOrder.Clear(); SuccessOrQuit(selectionOrder.PushBack(0xa1)); SuccessOrQuit(selectionOrder.PushBack(0xc1)); SuccessOrQuit(selectionOrder.PushBack(0xb1)); SuccessOrQuit(selectionOrder.PushBack(0xa2)); SuccessOrQuit(selectionOrder.PushBack(0xc2)); SuccessOrQuit(selectionOrder.PushBack(0xb2)); CheckSelection(seeker, selectionOrder); printf("\nTestSeekerCandidates() passed\n\n"); testFreeInstance(instance); } }; #endif // #if OPENTHREAD_CONFIG_SEEKER_ENABLE || OPENTHREAD_CONFIG_JOINER_ENABLE } // namespace ot int main(void) { #if OPENTHREAD_CONFIG_SEEKER_ENABLE || OPENTHREAD_CONFIG_JOINER_ENABLE #if (OPENTHREAD_CONFIG_JOINER_MAX_CANDIDATES == 8) ot::UnitTester::TestSeekerCandidates(); printf("All tests passed\n"); #else printf("Skipping tests as the test expects `OPENTHREAD_CONFIG_JOINER_MAX_CANDIDATES` to be `8`\n"); printf("This config is specifically set to 8 in `toranj-config` for this test\n"); #endif #else printf("Seeker feature is disabled, skipping the test\n"); #endif return 0; }