From 297fb501a0d595f7667c5554c894023ceed6ab3e Mon Sep 17 00:00:00 2001 From: Jonathan Hui Date: Wed, 22 Apr 2026 23:02:33 -0700 Subject: [PATCH] [nexus] migrate anycast locator test to Nexus (#12962) This commit migrates the anycast locator test from the thread-cert functional tests to the Nexus simulation framework. The new Nexus test 'test_anycast_locator.cpp' covers: - Anycast Locator (ALOC) resolution for the Leader from all nodes. - Custom service ALOC resolution when only one node provides it. - Closest-node ALOC resolution when multiple nodes provide the same service in a line topology (LEADER-R1-R2-R3-R4). - Verification that nodes resolve to the nearest service instance. The original Python test 'test_anycast_locator.py' is removed as its functionality is now fully covered by the Nexus test. --- tests/nexus/CMakeLists.txt | 1 + tests/nexus/test_anycast_locator.cpp | 238 ++++++++++++++++++ .../thread-cert/test_anycast_locator.py | 167 ------------ 3 files changed, 239 insertions(+), 167 deletions(-) create mode 100644 tests/nexus/test_anycast_locator.cpp delete mode 100755 tests/scripts/thread-cert/test_anycast_locator.py diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index 14c4e03d4..e8bf2be35 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -384,6 +384,7 @@ ot_nexus_test(1_4_CS_TC_3 "cert;nexus") ot_nexus_test(inform_previous_parent_on_reattach "cert;nexus") # Misc tests +ot_nexus_test(anycast_locator "core;nexus") ot_nexus_test(br_upgrade_router_role "core;nexus") ot_nexus_test(border_admitter "core;nexus") ot_nexus_test(border_agent "core;nexus") diff --git a/tests/nexus/test_anycast_locator.cpp b/tests/nexus/test_anycast_locator.cpp new file mode 100644 index 000000000..9ad0ba369 --- /dev/null +++ b/tests/nexus/test_anycast_locator.cpp @@ -0,0 +1,238 @@ +/* + * 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 +#include +#include + +#include "platform/nexus_core.hpp" +#include "platform/nexus_node.hpp" + +namespace ot { +namespace Nexus { + +struct LocateResult +{ + Error mError; + Ip6::Address mMeshLocalAddress; + uint16_t mRloc16; + bool mCallbackInvoked; + + void Clear(void) + { + mError = kErrorNone; + mMeshLocalAddress.Clear(); + mRloc16 = 0; + mCallbackInvoked = false; + } +}; + +static void HandleLocateResult(void *aContext, otError aError, const otIp6Address *aAddress, uint16_t aRloc16) +{ + LocateResult *result = static_cast(aContext); + + result->mError = aError; + result->mCallbackInvoked = true; + result->mRloc16 = aRloc16; + + if (aAddress != nullptr) + { + result->mMeshLocalAddress = AsCoreType(aAddress); + } +} + +static void Locate(Core &aNexus, Node &aNode, const Ip6::Address &aAnycastAddress, LocateResult &aResult) +{ + aResult.Clear(); + SuccessOrQuit(aNode.Get().Locate(aAnycastAddress, HandleLocateResult, &aResult)); + + for (uint32_t i = 0; i < 10000; i++) + { + aNexus.AdvanceTime(10); + if (aResult.mCallbackInvoked) + { + break; + } + } + + VerifyOrQuit(aResult.mCallbackInvoked); + SuccessOrQuit(aResult.mError); +} + +void TestAnycastLocator(void) +{ + // Topology: + // LEADER -- ROUTER1 -- ROUTER2 -- ROUTER3 -- ROUTER4 + + Core nexus; + + Node &leader = nexus.CreateNode(); + Node &router1 = nexus.CreateNode(); + Node &router2 = nexus.CreateNode(); + Node &router3 = nexus.CreateNode(); + Node &router4 = nexus.CreateNode(); + + leader.SetName("LEADER"); + router1.SetName("ROUTER1"); + router2.SetName("ROUTER2"); + router3.SetName("ROUTER3"); + router4.SetName("ROUTER4"); + + Node *nodes[] = {&leader, &router1, &router2, &router3, &router4}; + + nexus.AdvanceTime(0); + + Log("---------------------------------------------------------------------------------------"); + Log("Form network with line topology"); + + AllowLinkBetween(leader, router1); + AllowLinkBetween(router1, router2); + AllowLinkBetween(router2, router3); + AllowLinkBetween(router3, router4); + + leader.Form(); + nexus.AdvanceTime(20 * 1000); + + for (uint8_t i = 1; i < GetArrayLength(nodes); i++) + { + nodes[i]->Join(*nodes[i - 1]); + nexus.AdvanceTime(20 * 1000); + } + + // Wait for all to become routers and network to stabilize + nexus.AdvanceTime(300 * 1000); + + for (Node *node : nodes) + { + VerifyOrQuit(node->Get().IsRouter() || node->Get().IsLeader()); + } + + Log("---------------------------------------------------------------------------------------"); + Log("1. Locate leader ALOC from all nodes"); + + Ip6::Address leaderAloc; + leader.Get().GetLeaderAloc(leaderAloc); + + LocateResult result; + + for (Node *node : nodes) + { + Log("Node %s locating leader ALOC", node->GetName()); + Locate(nexus, *node, leaderAloc, result); + VerifyOrQuit(result.mMeshLocalAddress == leader.Get().GetMeshLocalEid()); + VerifyOrQuit(result.mRloc16 == leader.Get().GetRloc16()); + } + + Log("---------------------------------------------------------------------------------------"); + Log("2. Add a service on router4 and locate its ALOC"); + + uint32_t enterpriseNumber = 44970; + NetworkData::ServiceData serviceData; + NetworkData::ServerData serverData; + uint8_t sData[] = {0x57, 0x12, 0x34}; + uint8_t rData[] = {0x00}; + + serviceData.Init(sData, sizeof(sData)); + serverData.Init(rData, sizeof(rData)); + + SuccessOrQuit(router4.Get().AddService(enterpriseNumber, serviceData, true, serverData)); + router4.Get().HandleServerDataUpdated(); + nexus.AdvanceTime(20 * 1000); + + // Get the service ALOC from router4 + Ip6::Address serviceAloc; + bool found = false; + + for (const Ip6::Netif::UnicastAddress &addr : router4.Get().GetUnicastAddresses()) + { + if (router4.Get().IsAnycastLocator(addr.GetAddress()) && + addr.GetAddress().GetIid().IsAnycastLocator()) + { + // The leader ALOC is also an ALOC, so we need to make sure it's not the leader ALOC. + if (addr.GetAddress() != leaderAloc) + { + serviceAloc = addr.GetAddress(); + found = true; + break; + } + } + } + VerifyOrQuit(found); + + for (Node *node : nodes) + { + Log("Node %s locating service ALOC", node->GetName()); + Locate(nexus, *node, serviceAloc, result); + VerifyOrQuit(result.mMeshLocalAddress == router4.Get().GetMeshLocalEid()); + VerifyOrQuit(result.mRloc16 == router4.Get().GetRloc16()); + } + + Log("---------------------------------------------------------------------------------------"); + Log("3. Add same service on leader and ensure we locate the closest ALOC destination"); + + SuccessOrQuit(leader.Get().AddService(enterpriseNumber, serviceData, true, serverData)); + leader.Get().HandleServerDataUpdated(); + nexus.AdvanceTime(20 * 1000); + + // leader and router1 should locate leader as closest service ALOC + for (Node *node : {&leader, &router1}) + { + Log("Node %s locating service ALOC (expected leader)", node->GetName()); + Locate(nexus, *node, serviceAloc, result); + VerifyOrQuit(result.mMeshLocalAddress == leader.Get().GetMeshLocalEid()); + VerifyOrQuit(result.mRloc16 == leader.Get().GetRloc16()); + } + + // router3 and router4 should locate router4 + for (Node *node : {&router3, &router4}) + { + Log("Node %s locating service ALOC (expected router4)", node->GetName()); + Locate(nexus, *node, serviceAloc, result); + VerifyOrQuit(result.mMeshLocalAddress == router4.Get().GetMeshLocalEid()); + VerifyOrQuit(result.mRloc16 == router4.Get().GetRloc16()); + } + + // router2 is in middle and can locate either one + Log("Node %s locating service ALOC (expected either leader or router4)", router2.GetName()); + Locate(nexus, router2, serviceAloc, result); + VerifyOrQuit((result.mMeshLocalAddress == leader.Get().GetMeshLocalEid() && + result.mRloc16 == leader.Get().GetRloc16()) || + (result.mMeshLocalAddress == router4.Get().GetMeshLocalEid() && + result.mRloc16 == router4.Get().GetRloc16())); + + Log("All tests passed"); +} + +} // namespace Nexus +} // namespace ot + +int main(void) +{ + ot::Nexus::TestAnycastLocator(); + return 0; +} diff --git a/tests/scripts/thread-cert/test_anycast_locator.py b/tests/scripts/thread-cert/test_anycast_locator.py deleted file mode 100755 index 7d0883f6b..000000000 --- a/tests/scripts/thread-cert/test_anycast_locator.py +++ /dev/null @@ -1,167 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (c) 2021, 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. -# - -import config -import ipaddress -import unittest - -import command -import config -import thread_cert - -# Test description: -# This test verifies Anycast Locator functionality -# -# Topology: -# -# LEADER -- ROUTER1 -- ROUTER2 -- ROUTER3 -- ROUTER4 -# - -LEADER = 1 -ROUTER1 = 2 -ROUTER2 = 3 -ROUTER3 = 4 -ROUTER4 = 5 - -SRV_ENT_NUMBER = '44970' -SRV_SERVICE_DATA = '571234' -SRV_SERVER_DATA = '00' - - -class AnycastLocator(thread_cert.TestCase): - USE_MESSAGE_FACTORY = False - SUPPORT_NCP = False - - TOPOLOGY = { - LEADER: { - 'name': 'LEADER', - 'mode': 'rdn', - 'allowlist': [ROUTER1] - }, - ROUTER1: { - 'name': 'ROUTER1', - 'mode': 'rdn', - 'allowlist': [LEADER, ROUTER2] - }, - ROUTER2: { - 'name': 'ROUTER2', - 'mode': 'rdn', - 'allowlist': [ROUTER1, ROUTER3] - }, - ROUTER3: { - 'name': 'ROUTER3', - 'mode': 'rdn', - 'allowlist': [ROUTER2, ROUTER4] - }, - ROUTER4: { - 'name': 'ROUTER4', - 'mode': 'rdn', - 'allowlist': [ROUTER3] - }, - } - - def test(self): - leader = self.nodes[LEADER] - router1 = self.nodes[ROUTER1] - router2 = self.nodes[ROUTER2] - router3 = self.nodes[ROUTER3] - router4 = self.nodes[ROUTER4] - nodes = [leader, router1, router2, router3, router4] - - # - # 0. Start the leader and routers - # - - leader.start() - self.simulator.go(config.LEADER_STARTUP_DELAY) - self.assertEqual(leader.get_state(), 'leader') - - for node in nodes[1:]: - node.start() - self.simulator.go(config.ROUTER_STARTUP_DELAY) - self.assertEqual(node.get_state(), 'router') - - # - # 1. Locate leader ALOC from all nodes - # - - leader_aloc = leader.get_addr_leader_aloc() - - for node in nodes: - result = node.locate(leader_aloc) - self.assertEqual(result[0], leader.get_mleid()) - self.assertEqual(int(result[1], 0), leader.get_addr16()) - - # - # 2. Add a service on router4 and locate its ALOC - # - - router4.add_service(SRV_ENT_NUMBER, SRV_SERVICE_DATA, SRV_SERVER_DATA) - router4.register_netdata() - self.simulator.go(5) - services = leader.get_services() - self.assertEqual(len(services), 1) - - service_aloc = router4.get_ip6_address(config.ADDRESS_TYPE.ALOC)[0] - - for node in nodes: - result = node.locate(service_aloc) - self.assertEqual(result[0], router4.get_mleid()) - self.assertEqual(int(result[1], 0), router4.get_addr16()) - - # - # 3. Add same service on leader and ensure we locate the closest ALOC destination - # - - leader.add_service(SRV_ENT_NUMBER, SRV_SERVICE_DATA, SRV_SERVER_DATA) - leader.register_netdata() - self.simulator.go(5) - services = leader.get_services() - self.assertEqual(len(services), 2) - - # leader and router1 should locate leader as closest service ALOC, - # router3 and router4 should locate router4. router2 is in middle - # and can locate either one. - for node in [leader, router1]: - result = node.locate(service_aloc) - self.assertEqual(result[0], leader.get_mleid()) - self.assertEqual(int(result[1], 0), leader.get_addr16()) - - for node in [router3, router4]: - result = node.locate(service_aloc) - self.assertEqual(result[0], router4.get_mleid()) - self.assertEqual(int(result[1], 0), router4.get_addr16()) - - result = router2.locate(service_aloc) - self.assertTrue(result[0] in [leader.get_mleid(), router4.get_mleid()]) - self.assertTrue(int(result[1], 0) in [leader.get_addr16(), router4.get_addr16()]) - - -if __name__ == '__main__': - unittest.main()