From fc4ebb1aaa08b202304c20c9d63e6ac05053259d Mon Sep 17 00:00:00 2001 From: Jonathan Hui Date: Mon, 27 Apr 2026 18:57:07 -0700 Subject: [PATCH] [nexus] migrate test_service.py to nexus (#12974) This commit migrates the test_service.py script from thread-cert to the Nexus test framework. The new test_service.cpp implements the same test logic: - Forms a network with a Leader and two Routers. - Adds and removes services on different nodes. - Verifies that Service Anycast Locators (ALOCs) are correctly added to and removed from the nodes' unicast addresses. - Confirms reachability of the ALOCs using ICMPv6 Echo Requests from all nodes in the network. - Ensures ALOCs become unreachable after the service is removed from the network data. Changes: - Added tests/nexus/test_service.cpp - Updated tests/nexus/CMakeLists.txt to include the new test. - Removed tests/scripts/thread-cert/test_service.py. --- tests/nexus/CMakeLists.txt | 1 + tests/nexus/test_service.cpp | 272 ++++++++++++++++++++++ tests/scripts/thread-cert/test_service.py | 196 ---------------- 3 files changed, 273 insertions(+), 196 deletions(-) create mode 100644 tests/nexus/test_service.cpp delete mode 100755 tests/scripts/thread-cert/test_service.py diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index 73d226313..4d88effc9 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -413,6 +413,7 @@ ot_nexus_test(router_downgrade_on_sec_policy_change "core;nexus") ot_nexus_test(router_multicast_link_request "core;nexus") ot_nexus_test(router_reattach "core;nexus") ot_nexus_test(router_reboot_multiple_link_request "core;nexus") +ot_nexus_test(service "core;nexus") ot_nexus_test(srp_auto_start "core;nexus") ot_nexus_test(srp_client_change_lease "core;nexus") ot_nexus_test(srp_client_remove_host "core;nexus") diff --git a/tests/nexus/test_service.cpp b/tests/nexus/test_service.cpp new file mode 100644 index 000000000..362d9a8b6 --- /dev/null +++ b/tests/nexus/test_service.cpp @@ -0,0 +1,272 @@ +/* + * 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 "platform/nexus_core.hpp" +#include "platform/nexus_node.hpp" + +namespace ot { +namespace Nexus { + +namespace { + +struct NoEchoReplyContext +{ + bool mReceived; + uint16_t mId; +}; + +void HandleIcmpNoEchoReply(void *aContext, otMessage *, const otMessageInfo *, const otIcmp6Header *aIcmpHeader) +{ + NoEchoReplyContext *ctx = static_cast(aContext); + const Ip6::Icmp::Header *header = AsCoreTypePtr(aIcmpHeader); + + if (header->GetType() == Ip6::Icmp::Header::kTypeEchoReply && header->GetId() == ctx->mId) + { + ctx->mReceived = true; + } +} + +void SendAndVerifyNoEchoReply(Core &aNexus, Node &aSender, const Ip6::Address &aDestination, uint32_t aTimeout = 1000) +{ + static constexpr uint16_t kIdentifier = 0xabcd; + + NoEchoReplyContext context; + context.mReceived = false; + context.mId = kIdentifier; + + Ip6::Icmp::Handler icmpHandler(HandleIcmpNoEchoReply, &context); + + SuccessOrQuit(aSender.Get().RegisterHandler(icmpHandler)); + + aSender.SendEchoRequest(aDestination, kIdentifier); + aNexus.AdvanceTime(aTimeout); + + SuccessOrQuit(aSender.Get().UnregisterHandler(icmpHandler)); + + VerifyOrQuit(!context.mReceived, "Echo reply received when none was expected"); +} + +} // namespace + +void TestService(void) +{ + Core nexus; + + Node &leader = nexus.CreateNode(); + Node &router1 = nexus.CreateNode(); + Node &router2 = nexus.CreateNode(); + + leader.SetName("LEADER"); + router1.SetName("ROUTER1"); + router2.SetName("ROUTER2"); + + Log("---------------------------------------------------------------------------------------"); + Log("Forming network..."); + AllowLinkBetween(leader, router1); + AllowLinkBetween(leader, router2); + AllowLinkBetween(router1, router2); + + leader.Form(); + nexus.AdvanceTime(10000); + + router1.Join(leader); + router2.Join(leader); + + // Wait for all to become routers + nexus.AdvanceTime(300000); + + VerifyOrQuit(leader.Get().IsLeader()); + VerifyOrQuit(router1.Get().IsRouter()); + VerifyOrQuit(router2.Get().IsRouter()); + + Ip6::Address aloc0; + Ip6::Address aloc1; + + aloc0.SetToAnycastLocator(leader.Get().GetMeshLocalPrefix(), Mle::Aloc16::FromServiceId(0)); + aloc1.SetToAnycastLocator(leader.Get().GetMeshLocalPrefix(), Mle::Aloc16::FromServiceId(1)); + + // Initial check: no ALOCs + for (Node &node : nexus.GetNodes()) + { + VerifyOrQuit(!node.Get().HasUnicastAddress(aloc0)); + VerifyOrQuit(!node.Get().HasUnicastAddress(aloc1)); + } + + Log("---------------------------------------------------------------------------------------"); + Log("Step 1: Add Service 0 on ROUTER1"); + + uint32_t enterpriseNumber0 = 123; + uint8_t serviceData0[] = {0x31, 0x32, 0x33}; // "123" + uint8_t serverData0[] = {0x61, 0x62, 0x63}; // "abc" + + NetworkData::ServiceData sData0; + NetworkData::ServerData rData0; + + sData0.Init(serviceData0, sizeof(serviceData0)); + rData0.Init(serverData0, sizeof(serverData0)); + + SuccessOrQuit(router1.Get().AddService(enterpriseNumber0, sData0, true, rData0)); + router1.Get().HandleServerDataUpdated(); + + nexus.AdvanceTime(5000); + + // Verify Service 0 ALOC on ROUTER1 + // Note: Leader assigns Service IDs. First service should get ID 0. + VerifyOrQuit(!leader.Get().HasUnicastAddress(aloc0)); + VerifyOrQuit(router1.Get().HasUnicastAddress(aloc0)); + VerifyOrQuit(!router2.Get().HasUnicastAddress(aloc0)); + + Log("Pinging Service 0 ALOC %s from all nodes", aloc0.ToString().AsCString()); + for (Node &node : nexus.GetNodes()) + { + nexus.SendAndVerifyEchoRequest(node, aloc0); + } + + Log("---------------------------------------------------------------------------------------"); + Log("Step 2: Add same Service 0 on LEADER"); + SuccessOrQuit(leader.Get().AddService(enterpriseNumber0, sData0, true, rData0)); + leader.Get().HandleServerDataUpdated(); + + nexus.AdvanceTime(5000); + + VerifyOrQuit(leader.Get().HasUnicastAddress(aloc0)); + VerifyOrQuit(router1.Get().HasUnicastAddress(aloc0)); + VerifyOrQuit(!router2.Get().HasUnicastAddress(aloc0)); + + Log("Pinging Service 0 ALOC from all nodes again"); + for (Node &node : nexus.GetNodes()) + { + nexus.SendAndVerifyEchoRequest(node, aloc0); + } + + Log("---------------------------------------------------------------------------------------"); + Log("Step 3: Add Service 1 on ROUTER2"); + + uint32_t enterpriseNumber1 = 234; + uint8_t serviceData1[] = {0x34, 0x35, 0x36}; // "456" + uint8_t serverData1[] = {0x64, 0x65, 0x66}; // "def" + + NetworkData::ServiceData sData1; + NetworkData::ServerData rData1; + + sData1.Init(serviceData1, sizeof(serviceData1)); + rData1.Init(serverData1, sizeof(serverData1)); + + SuccessOrQuit(router2.Get().AddService(enterpriseNumber1, sData1, true, rData1)); + router2.Get().HandleServerDataUpdated(); + + nexus.AdvanceTime(5000); + + VerifyOrQuit(leader.Get().HasUnicastAddress(aloc0)); + VerifyOrQuit(router1.Get().HasUnicastAddress(aloc0)); + VerifyOrQuit(!router2.Get().HasUnicastAddress(aloc0)); + VerifyOrQuit(router2.Get().HasUnicastAddress(aloc1)); + VerifyOrQuit(!leader.Get().HasUnicastAddress(aloc1)); + VerifyOrQuit(!router1.Get().HasUnicastAddress(aloc1)); + + Log("Pinging both ALOCs from all nodes"); + for (Node &node : nexus.GetNodes()) + { + nexus.SendAndVerifyEchoRequest(node, aloc0); + nexus.SendAndVerifyEchoRequest(node, aloc1); + } + + Log("---------------------------------------------------------------------------------------"); + Log("Step 4: Remove Service 0 from ROUTER1"); + SuccessOrQuit(router1.Get().RemoveService(enterpriseNumber0, sData0)); + router1.Get().HandleServerDataUpdated(); + + nexus.AdvanceTime(5000); + + VerifyOrQuit(leader.Get().HasUnicastAddress(aloc0)); + VerifyOrQuit(!router1.Get().HasUnicastAddress(aloc0)); + VerifyOrQuit(router2.Get().HasUnicastAddress(aloc1)); + + Log("Pinging both ALOCs from all nodes again"); + for (Node &node : nexus.GetNodes()) + { + nexus.SendAndVerifyEchoRequest(node, aloc0); + nexus.SendAndVerifyEchoRequest(node, aloc1); + } + + Log("---------------------------------------------------------------------------------------"); + Log("Step 5: Remove Service 0 from LEADER"); + SuccessOrQuit(leader.Get().RemoveService(enterpriseNumber0, sData0)); + leader.Get().HandleServerDataUpdated(); + + nexus.AdvanceTime(5000); + + VerifyOrQuit(!leader.Get().HasUnicastAddress(aloc0)); + VerifyOrQuit(!router1.Get().HasUnicastAddress(aloc0)); + VerifyOrQuit(!router2.Get().HasUnicastAddress(aloc0)); + VerifyOrQuit(router2.Get().HasUnicastAddress(aloc1)); + + Log("Service 0 ALOC should now be unreachable"); + for (Node &node : nexus.GetNodes()) + { + SendAndVerifyNoEchoReply(nexus, node, aloc0); + } + Log("Service 1 ALOC should still be reachable"); + for (Node &node : nexus.GetNodes()) + { + nexus.SendAndVerifyEchoRequest(node, aloc1); + } + + Log("---------------------------------------------------------------------------------------"); + Log("Step 6: Remove Service 1 from ROUTER2"); + SuccessOrQuit(router2.Get().RemoveService(enterpriseNumber1, sData1)); + router2.Get().HandleServerDataUpdated(); + + nexus.AdvanceTime(5000); + + for (Node &node : nexus.GetNodes()) + { + VerifyOrQuit(!node.Get().HasUnicastAddress(aloc0)); + VerifyOrQuit(!node.Get().HasUnicastAddress(aloc1)); + } + + Log("Service 1 ALOC should now be unreachable"); + for (Node &node : nexus.GetNodes()) + { + SendAndVerifyNoEchoReply(nexus, node, aloc1); + } + + Log("All tests passed!"); +} + +} // namespace Nexus +} // namespace ot + +int main(void) +{ + ot::Nexus::TestService(); + return 0; +} diff --git a/tests/scripts/thread-cert/test_service.py b/tests/scripts/thread-cert/test_service.py deleted file mode 100755 index 6032f6c3b..000000000 --- a/tests/scripts/thread-cert/test_service.py +++ /dev/null @@ -1,196 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (c) 2017, 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 re -import unittest - -import config -import thread_cert - -LEADER = 1 -ROUTER1 = 2 -ROUTER2 = 3 - -SRV_0_ID = 0 -SRV_0_ENT_NUMBER = '123' -SRV_0_SERVICE_DATA = '123' -SRV_0_SERVER_DATA = 'abc' - -SRV_1_ID = 1 -SRV_1_ENT_NUMBER = '234' -SRV_1_SERVICE_DATA = '456' -SRV_1_SERVER_DATA = 'def' - - -class Test_Service(thread_cert.TestCase): - SUPPORT_NCP = False - - TOPOLOGY = { - LEADER: { - 'channel': 12, - 'mode': 'rdn', - 'network_name': 'OpenThread', - 'allowlist': [ROUTER1, ROUTER2] - }, - ROUTER1: { - 'channel': 12, - 'mode': 'rdn', - 'network_name': 'OpenThread', - 'allowlist': [LEADER, ROUTER2] - }, - ROUTER2: { - 'channel': 12, - 'mode': 'rdn', - 'network_name': 'OpenThread', - 'allowlist': [LEADER, ROUTER1] - }, - } - - def hasAloc(self, node_id, service_id): - for addr in self.nodes[node_id].get_ip6_address(config.ADDRESS_TYPE.ALOC): - m = re.match('.*:fc(..)$', addr, re.I) - if m is not None: - if m.group(1) == str(service_id + 10): # for service_id=3 look for '...:fc13' - return True - - return False - - def pingFromAll(self, addr): - for n in list(self.nodes.values()): - self.assertTrue(n.ping(addr)) - - def failToPingFromAll(self, addr): - for n in list(self.nodes.values()): - self.assertFalse(n.ping(addr, timeout=3)) - - def test(self): - self.nodes[LEADER].start() - self.simulator.go(config.LEADER_STARTUP_DELAY) - self.assertEqual(self.nodes[LEADER].get_state(), 'leader') - - self.nodes[ROUTER1].start() - self.nodes[ROUTER2].start() - self.simulator.go(config.ROUTER_STARTUP_DELAY) - self.assertEqual(self.nodes[ROUTER1].get_state(), 'router') - self.assertEqual(self.nodes[ROUTER2].get_state(), 'router') - - self.assertEqual(self.hasAloc(LEADER, SRV_0_ID), False) - self.assertEqual(self.hasAloc(LEADER, SRV_1_ID), False) - self.assertEqual(self.hasAloc(ROUTER1, SRV_0_ID), False) - self.assertEqual(self.hasAloc(ROUTER1, SRV_1_ID), False) - self.assertEqual(self.hasAloc(ROUTER2, SRV_0_ID), False) - self.assertEqual(self.hasAloc(ROUTER2, SRV_1_ID), False) - - self.nodes[ROUTER1].add_service(SRV_0_ENT_NUMBER, SRV_0_SERVICE_DATA, SRV_0_SERVER_DATA) - self.nodes[ROUTER1].register_netdata() - self.simulator.go(2) - - self.assertEqual(self.hasAloc(LEADER, SRV_0_ID), False) - self.assertEqual(self.hasAloc(LEADER, SRV_1_ID), False) - self.assertEqual(self.hasAloc(ROUTER1, SRV_0_ID), True) - self.assertEqual(self.hasAloc(ROUTER1, SRV_1_ID), False) - self.assertEqual(self.hasAloc(ROUTER2, SRV_0_ID), False) - self.assertEqual(self.hasAloc(ROUTER2, SRV_1_ID), False) - - aloc0 = self.nodes[ROUTER1].get_ip6_address(config.ADDRESS_TYPE.ALOC)[0] - self.pingFromAll(aloc0) - - self.nodes[LEADER].add_service(SRV_0_ENT_NUMBER, SRV_0_SERVICE_DATA, SRV_0_SERVER_DATA) - self.nodes[LEADER].register_netdata() - self.simulator.go(2) - - self.assertEqual(self.hasAloc(LEADER, SRV_0_ID), True) - self.assertEqual(self.hasAloc(LEADER, SRV_1_ID), False) - self.assertEqual(self.hasAloc(ROUTER1, SRV_0_ID), True) - self.assertEqual(self.hasAloc(ROUTER1, SRV_1_ID), False) - self.assertEqual(self.hasAloc(ROUTER2, SRV_0_ID), False) - self.assertEqual(self.hasAloc(ROUTER2, SRV_1_ID), False) - - self.pingFromAll(aloc0) - - self.nodes[ROUTER2].add_service(SRV_1_ENT_NUMBER, SRV_1_SERVICE_DATA, SRV_1_SERVER_DATA) - self.nodes[ROUTER2].register_netdata() - self.simulator.go(2) - - self.assertEqual(self.hasAloc(LEADER, SRV_0_ID), True) - self.assertEqual(self.hasAloc(LEADER, SRV_1_ID), False) - self.assertEqual(self.hasAloc(ROUTER1, SRV_0_ID), True) - self.assertEqual(self.hasAloc(ROUTER1, SRV_1_ID), False) - self.assertEqual(self.hasAloc(ROUTER2, SRV_0_ID), False) - self.assertEqual(self.hasAloc(ROUTER2, SRV_1_ID), True) - - aloc1 = self.nodes[ROUTER2].get_ip6_address(config.ADDRESS_TYPE.ALOC)[0] - self.pingFromAll(aloc0) - self.pingFromAll(aloc1) - - self.nodes[ROUTER1].remove_service(SRV_0_ENT_NUMBER, SRV_0_SERVICE_DATA) - self.nodes[ROUTER1].register_netdata() - self.simulator.go(2) - - self.assertEqual(self.hasAloc(LEADER, SRV_0_ID), True) - self.assertEqual(self.hasAloc(LEADER, SRV_1_ID), False) - self.assertEqual(self.hasAloc(ROUTER1, SRV_0_ID), False) - self.assertEqual(self.hasAloc(ROUTER1, SRV_1_ID), False) - self.assertEqual(self.hasAloc(ROUTER2, SRV_0_ID), False) - self.assertEqual(self.hasAloc(ROUTER2, SRV_1_ID), True) - - self.pingFromAll(aloc0) - self.pingFromAll(aloc1) - - self.nodes[LEADER].remove_service(SRV_0_ENT_NUMBER, SRV_0_SERVICE_DATA) - self.nodes[LEADER].register_netdata() - self.simulator.go(2) - - self.assertEqual(self.hasAloc(LEADER, SRV_0_ID), False) - self.assertEqual(self.hasAloc(LEADER, SRV_1_ID), False) - self.assertEqual(self.hasAloc(ROUTER1, SRV_0_ID), False) - self.assertEqual(self.hasAloc(ROUTER1, SRV_1_ID), False) - self.assertEqual(self.hasAloc(ROUTER2, SRV_0_ID), False) - self.assertEqual(self.hasAloc(ROUTER2, SRV_1_ID), True) - - self.failToPingFromAll(aloc0) - self.pingFromAll(aloc1) - - self.nodes[ROUTER2].remove_service(SRV_1_ENT_NUMBER, SRV_1_SERVICE_DATA) - self.nodes[ROUTER2].register_netdata() - self.simulator.go(2) - - self.assertEqual(self.hasAloc(LEADER, SRV_0_ID), False) - self.assertEqual(self.hasAloc(LEADER, SRV_1_ID), False) - self.assertEqual(self.hasAloc(ROUTER1, SRV_0_ID), False) - self.assertEqual(self.hasAloc(ROUTER1, SRV_1_ID), False) - self.assertEqual(self.hasAloc(ROUTER2, SRV_0_ID), False) - self.assertEqual(self.hasAloc(ROUTER2, SRV_1_ID), False) - - self.failToPingFromAll(aloc0) - self.failToPingFromAll(aloc1) - - -if __name__ == '__main__': - unittest.main()