From a51677de9461a2387008ee95612a5d16d1315b58 Mon Sep 17 00:00:00 2001 From: Jonathan Hui Date: Tue, 21 Apr 2026 05:42:46 -0700 Subject: [PATCH] [nexus] migrate srp scale test to nexus (#12949) This commit migrates the test_srp_register_500_services.py test from the thread-cert test suite to the Nexus platform. The new C++ test (tests/nexus/test_srp_scale.cpp) implements the same functionality: it verifies that 25 SRP clients (13 routers and 12 FEDs) can successfully register a total of 500 services (20 services per client) with a single SRP server (the leader). The commit includes: - Removal of the original Python test file. - Addition of the new Nexus C++ test file. - Integration of the new test into CMakeLists.txt and run_nexus_tests.sh. --- tests/nexus/CMakeLists.txt | 1 + tests/nexus/test_srp_scale.cpp | 243 ++++++++++++++++++ .../test_srp_register_500_services_br.py | 81 ------ .../test_srp_register_500_services.py | 191 -------------- 4 files changed, 244 insertions(+), 272 deletions(-) create mode 100644 tests/nexus/test_srp_scale.cpp delete mode 100755 tests/scripts/thread-cert/border_router/test_srp_register_500_services_br.py delete mode 100755 tests/scripts/thread-cert/test_srp_register_500_services.py diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index b036c7aac..3e4b614b5 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -410,6 +410,7 @@ ot_nexus_test(srp_client_change_lease "core;nexus") ot_nexus_test(srp_client_remove_host "core;nexus") ot_nexus_test(srp_lease "core;nexus") ot_nexus_test(srp_many_services_mtu_check "core;nexus") +ot_nexus_test(srp_scale "core;nexus") ot_nexus_test(zero_len_external_route "core;nexus") # Trel diff --git a/tests/nexus/test_srp_scale.cpp b/tests/nexus/test_srp_scale.cpp new file mode 100644 index 000000000..812fdeccb --- /dev/null +++ b/tests/nexus/test_srp_scale.cpp @@ -0,0 +1,243 @@ +/* + * 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 { + +const uint16_t kNumSrpClients = 25; +const uint16_t kInstanceNumPerClient = 20; +const uint16_t kServicePort = 12345; +const char *kServiceName = "_srpclient._tcp"; + +struct ClientInfo +{ + Srp::Client::Service mServices[kInstanceNumPerClient]; + String<32> mInstanceNames[kInstanceNumPerClient]; +}; + +static ClientInfo sClientInfo[kNumSrpClients + 1]; + +/** + * This function registers SRP services from a given node. + * + * @param[in] aNode The node from which to register services. + * @param[in] aClientId The client ID to use for host and instance names. + * + */ +void RegisterServices(Node &aNode, uint16_t aClientId) +{ + String<32> hostName; + + hostName.Append("client%u", aClientId); + + aNode.Get().EnableAutoStartMode(nullptr, nullptr); + SuccessOrQuit(aNode.Get().SetHostName(hostName.AsCString())); + SuccessOrQuit(aNode.Get().EnableAutoHostAddress()); + + for (uint16_t j = 1; j <= kInstanceNumPerClient; j++) + { + String<32> &instanceName = sClientInfo[aClientId].mInstanceNames[j - 1]; + + instanceName.Clear().Append("client%u_%u", aClientId, j); + + Srp::Client::Service &service = sClientInfo[aClientId].mServices[j - 1]; + + // We use `memset` to zero-initialize the service object. This is + // safer than `ClearAllBytes` as it correctly handles the + // `LinkedListEntry` base class part. + memset(&service, 0, sizeof(service)); + + service.mName = kServiceName; + service.mInstanceName = instanceName.AsCString(); + service.mPort = kServicePort; + + SuccessOrQuit(service.Init()); + + Error error = aNode.Get().AddService(service); + if (error != kErrorNone) + { + Log("AddService failed with error %s (%d) for client %u, instance %u", ErrorToString(error), error, + aClientId, j); + } + SuccessOrQuit(error); + + if (j % 10 == 0 || j == kInstanceNumPerClient) + { + Core::Get().AdvanceTime(5 * 1000); + } + } +} + +/** + * This function verifies registered services on a given SRP client node. + * + * @param[in] aNode The node to verify. + * + */ +void VerifyClient(Node &aNode) +{ + const Srp::Client::Service *service = aNode.Get().GetServices().GetHead(); + uint16_t count = 0; + + while (service != nullptr) + { + VerifyOrQuit(service->GetState() == Srp::Client::kRegistered); + count++; + service = service->GetNext(); + } + + VerifyOrQuit(count == kInstanceNumPerClient); +} + +} // namespace + +/** + * This test verifies SRP function that SRP clients can register up to 500 services to one SRP server. + * + * Topology: + * LEADER (SRP server) + * | + * | + * ROUTER1 ... ROUTER13 (SRP clients) + * | | + * FED14 ... FED25 (SRP clients) + * + */ +void TestSrpScale(void) +{ + const uint16_t kNumRouters = (kNumSrpClients / 2) + (kNumSrpClients % 2); + const uint16_t kNumFeds = kNumSrpClients - kNumRouters; + + Core nexus; + Node &leader = nexus.CreateNode(); + Node *routers[kNumRouters]; + Node *feds[kNumFeds]; + + Log("Test SRP Scale: 500 services"); + + leader.Form(); + nexus.AdvanceTime(15 * 1000); + VerifyOrQuit(leader.Get().IsLeader()); + + leader.Get().SetEnabled(true); + nexus.AdvanceTime(1000); + + for (uint16_t i = 0; i < kNumRouters; i++) + { + routers[i] = &nexus.CreateNode(); + routers[i]->SetName("router", i + 1); + routers[i]->Get().SetRouterSelectionJitter(1); + routers[i]->Join(leader); + } + + // Wait for routers to become routers + nexus.AdvanceTime(20 * 1000); + + for (uint16_t i = 0; i < kNumRouters; i++) + { + VerifyOrQuit(routers[i]->Get().IsRouter()); + } + + for (uint16_t i = 0; i < kNumFeds; i++) + { + feds[i] = &nexus.CreateNode(); + feds[i]->SetName("fed", i + kNumRouters + 1); + feds[i]->Join(*routers[i], Node::kAsFed); + } + + nexus.AdvanceTime(10 * 1000); + + for (uint16_t i = 0; i < kNumFeds; i++) + { + VerifyOrQuit(feds[i]->Get().IsChild()); + } + + Log("Registering services from routers..."); + for (uint16_t i = 0; i < kNumRouters; i++) + { + RegisterServices(*routers[i], i + 1); + } + + Log("Registering services from FEDs..."); + for (uint16_t i = 0; i < kNumFeds; i++) + { + RegisterServices(*feds[i], i + kNumRouters + 1); + } + + // Give it some more time to settle + nexus.AdvanceTime(20 * 1000); + + Log("Verifying registrations..."); + + for (uint16_t i = 0; i < kNumRouters; i++) + { + VerifyClient(*routers[i]); + } + + for (uint16_t i = 0; i < kNumFeds; i++) + { + VerifyClient(*feds[i]); + } + + uint32_t totalServices = 0; + const Srp::Server::Host *host = nullptr; + while ((host = leader.Get().GetNextHost(host)) != nullptr) + { + const Srp::Server::Service *service = nullptr; + while ((service = host->GetNextService(service)) != nullptr) + { + if (!service->IsDeleted()) + { + totalServices++; + } + } + } + + Log("Total services registered on server: %u", totalServices); + VerifyOrQuit(totalServices == kNumSrpClients * kInstanceNumPerClient); + + Log("Test passed!"); +} + +} // namespace Nexus +} // namespace ot + +int main(void) +{ + ot::Nexus::TestSrpScale(); + printf("All tests passed\n"); + return 0; +} diff --git a/tests/scripts/thread-cert/border_router/test_srp_register_500_services_br.py b/tests/scripts/thread-cert/border_router/test_srp_register_500_services_br.py deleted file mode 100755 index 5576cc876..000000000 --- a/tests/scripts/thread-cert/border_router/test_srp_register_500_services_br.py +++ /dev/null @@ -1,81 +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 unittest - -from test_srp_register_500_services import CLIENT_IDS, INSTANCE_IDS, SrpRegister500Services, SERVER, SERVICE_NAME - -# Test description: -# This test verifies SRP function that SRP clients can register up to 500 services to one SRP server. -# -# This test is same as test_srp_register_500_services.py, with the only difference that the SRP server -# is running on an OTBR (Docker). -# - -HOST = SERVER + 1 - - -class SrpRegister500ServicesBR(SrpRegister500Services): - TOPOLOGY = SrpRegister500Services.TOPOLOGY - TOPOLOGY[SERVER]['is_otbr'] = True - TOPOLOGY[HOST] = { - 'name': 'Host', - 'is_host': True, - } - - def test(self): - self.nodes[HOST].start(start_radvd=False) - - self._test_impl() - - # Verify all SRP services are resolvable via mDNS - host = self.nodes[HOST] - - browse_retry = 3 - for i in range(browse_retry): - try: - service_instances = set(host.browse_mdns_services(SERVICE_NAME, timeout=10)) - print(service_instances) - - for clientid in CLIENT_IDS: - for instanceid in INSTANCE_IDS: - self.assertIn(f'client{clientid}_{instanceid}', service_instances) - - break - - except Exception: - if i < browse_retry - 1: - self.simulator.go(3) - continue - - -del SrpRegister500Services - -if __name__ == '__main__': - unittest.main() diff --git a/tests/scripts/thread-cert/test_srp_register_500_services.py b/tests/scripts/thread-cert/test_srp_register_500_services.py deleted file mode 100755 index a350f7b05..000000000 --- a/tests/scripts/thread-cert/test_srp_register_500_services.py +++ /dev/null @@ -1,191 +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 unittest - -import config -import thread_cert - -# Test description: -# This test verifies SRP function that SRP clients can register up to 500 services to one SRP server. -# -# Topology: -# LEADER (SRP server) -# | -# | -# ROUTER1 ... ROUTER12 ROUTER13 (SRP clients) -# | | -# FED14 ... FED25 (SRP clients) -# - -NUM_SRP_CLIENTS = 25 -INSTANCE_NUM_PER_CLIENT = 20 -NUM_ROUTERS = (NUM_SRP_CLIENTS // 2) + (NUM_SRP_CLIENTS % 2) -NUM_FEDS = NUM_SRP_CLIENTS - NUM_ROUTERS -assert (NUM_FEDS <= NUM_ROUTERS) - -SERVER = NUM_SRP_CLIENTS + 1 -CLIENT1 = 1 - -ROUTER_IDS = tuple(range(1, NUM_ROUTERS + 1)) -FED_IDS = tuple(range(NUM_ROUTERS + 1, NUM_SRP_CLIENTS + 1)) -assert len(FED_IDS) == NUM_FEDS -CLIENT_IDS = tuple(range(1, NUM_SRP_CLIENTS + 1)) -INSTANCE_IDS = tuple(range(1, INSTANCE_NUM_PER_CLIENT + 1)) -SERVICE_NAME = '_srpclient._tcp' -SERVICE_PORT = 12345 - - -class SrpRegister500Services(thread_cert.TestCase): - USE_MESSAGE_FACTORY = False - SUPPORT_NCP = False - - TOPOLOGY = { - SERVER: { - 'name': 'SRP_SERVER', - 'mode': 'rdn', - 'allowlist': [], - }, - } - - for nodeid in ROUTER_IDS: - TOPOLOGY[nodeid] = { - 'name': f'SRP_CLIENT_{nodeid}', - 'mode': 'rdn', - 'allowlist': [SERVER], - } - TOPOLOGY[SERVER]['allowlist'].append(nodeid) - - for nodeid in FED_IDS: - parentid = nodeid - NUM_ROUTERS - TOPOLOGY[nodeid] = { - 'name': f'SRP_CLIENT_{nodeid}', - 'mode': 'rdn', - 'router_eligible': False, - 'allowlist': [parentid], - } - TOPOLOGY[parentid]['allowlist'].append(nodeid) - - def test(self): - self._test_impl() - - def _test_impl(self): - server = self.nodes[SERVER] - - def routers(): - for nodeid in ROUTER_IDS: - yield self.nodes[nodeid] - - def feds(): - for nodeid in FED_IDS: - yield self.nodes[nodeid] - - def clients(): - for nodeid in CLIENT_IDS: - yield self.nodes[nodeid] - - # Start the server & clients. - server.srp_server_set_enabled(True) - server.start() - self.simulator.go(config.LEADER_STARTUP_DELAY) - self.assertEqual(server.get_state(), 'leader') - - for router in routers(): - router.srp_server_set_enabled(False) - router.start() - self.simulator.go(config.ROUTER_STARTUP_DELAY) - - for router in routers(): - self.assertEqual(router.get_state(), 'router') - - for fed in feds(): - fed.srp_server_set_enabled(False) - fed.start() - self.simulator.go(3) - - for fed in feds(): - self.assertEqual(fed.get_state(), 'child') - - for clientid in CLIENT_IDS: - client = self.nodes[clientid] - self.assertEqual(client.srp_client_get_auto_start_mode(), 'Enabled') - client.srp_client_set_host_name(f'client{clientid}') - client.srp_client_set_host_address(f'2001::{clientid}') - - for instanceid in INSTANCE_IDS: - client.srp_client_add_service(f'client{clientid}_{instanceid}', SERVICE_NAME, SERVICE_PORT) - - if instanceid % 10 == 0 or instanceid == INSTANCE_IDS[-1]: - self._wait_client_all_services_registered(client) - - self._check_client_host_and_services(client, clientid) - - # Make sure all service are registered successfully - self._check_server_hosts_and_services(server) - - def _wait_client_all_services_registered(self, client, timeout=60): - all_registered = False - while timeout > 0: - self.simulator.go(3) - timeout -= 3 - - client_services = client.srp_client_get_services() - all_registered = all(service['state'] == 'Registered' for service in client_services) - if all_registered: - break - - self.assertTrue(all_registered, "Some services are not registered successfully") - - def _check_client_host_and_services(self, client, clientid): - client_services = client.srp_client_get_services() - self.assertEqual(len(client_services), INSTANCE_NUM_PER_CLIENT) - service_instance_names = {f'client{clientid}_{instanceid}' for instanceid in INSTANCE_IDS} - for service in client_services: - self.assertEqual(service['name'], SERVICE_NAME) - self.assertEqual(service['state'], 'Registered') - self.assertEqual(int(service['port']), SERVICE_PORT) - self.assertIn(service['instance'], service_instance_names) - service_instance_names.discard(service['instance']) - - def _check_server_hosts_and_services(self, server): - server_services = server.srp_server_get_services() - self.assertEqual(len(server_services), NUM_SRP_CLIENTS * INSTANCE_NUM_PER_CLIENT) - - service_instance_names = { - f'client{clientid}_{instanceid}' for clientid in CLIENT_IDS for instanceid in INSTANCE_IDS - } - - for service in server_services: - self.assertEqual(service['name'], SERVICE_NAME) - self.assertIn(service['instance'], service_instance_names) - service_instance_names.discard(service['instance']) - - -if __name__ == '__main__': - unittest.main()