From 57539b0725472c62fd4917666c5a998202eae357 Mon Sep 17 00:00:00 2001 From: Jonathan Hui Date: Wed, 1 Apr 2026 08:17:01 -0700 Subject: [PATCH] [nexus] add 1_4_TREL_TC_5 for multi-radio discovery scan (#12808) This commit adds Nexus test case 1_4_TREL_TC_5, which verifies MLE discovery scan behavior when nodes support different radio links, per the Thread 1.4 specification. The implementation includes: - tests/nexus/test_1_4_TREL_TC_5.cpp: Sets up a topology with three nodes: Node_1 (DUT) and Node_2 support both 15.4 and TREL, while Node_3 supports 15.4 only. Each node forms its own network. The test performs Discovery Scans from Node_2 and Node_3 and verifies that all expected peers are seen. - tests/nexus/verify_1_4_TREL_TC_5.py: Verifies the exchange of MLE Discovery Request and Response packets in the pcap output, ensuring that nodes with different radio capabilities can discover each other correctly. - Updated tests/nexus/CMakeLists.txt and tests/nexus/run_nexus_tests.sh to include the new test in the build and default test list. --- tests/nexus/CMakeLists.txt | 1 + tests/nexus/run_nexus_tests.sh | 1 + tests/nexus/test_1_4_TREL_TC_5.cpp | 227 ++++++++++++++++++++++++++++ tests/nexus/verify_1_4_TREL_TC_5.py | 139 +++++++++++++++++ 4 files changed, 368 insertions(+) create mode 100644 tests/nexus/test_1_4_TREL_TC_5.cpp create mode 100644 tests/nexus/verify_1_4_TREL_TC_5.py diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index 4df4b7abb..b74ed8395 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -288,6 +288,7 @@ ot_nexus_test(1_4_TREL_TC_1 "cert;nexus") ot_nexus_test(1_4_TREL_TC_2 "cert;nexus") ot_nexus_test(1_4_TREL_TC_3 "cert;nexus") ot_nexus_test(1_4_TREL_TC_4 "cert;nexus") +ot_nexus_test(1_4_TREL_TC_5 "cert;nexus") # Misc tests ot_nexus_test(border_admitter "core;nexus") diff --git a/tests/nexus/run_nexus_tests.sh b/tests/nexus/run_nexus_tests.sh index 9c85d4860..306a249d7 100755 --- a/tests/nexus/run_nexus_tests.sh +++ b/tests/nexus/run_nexus_tests.sh @@ -223,6 +223,7 @@ DEFAULT_TESTS=( "1_4_TREL_TC_2" "1_4_TREL_TC_3" "1_4_TREL_TC_4" + "1_4_TREL_TC_5" ) # Use provided arguments or the default test list diff --git a/tests/nexus/test_1_4_TREL_TC_5.cpp b/tests/nexus/test_1_4_TREL_TC_5.cpp new file mode 100644 index 000000000..52495ddc5 --- /dev/null +++ b/tests/nexus/test_1_4_TREL_TC_5.cpp @@ -0,0 +1,227 @@ +/* + * 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 "platform/nexus_core.hpp" +#include "platform/nexus_node.hpp" + +namespace ot { +namespace Nexus { + +#if OPENTHREAD_CONFIG_MULTI_RADIO + +static constexpr uint32_t kFormNetworkTime = 35 * 1000; +static constexpr uint32_t kScanTime = 10 * 1000; +static constexpr uint32_t kInfraIfIndex = 1; +static constexpr uint16_t kDiscoveryMaxResults = 16; +static constexpr uint16_t kPanIdAny = 0xffff; + +struct DiscoverContext +{ + void Clear(void) + { + mDiscoverDone = false; + mScanResults.Clear(); + } + + bool mDiscoverDone; + Array mScanResults; +}; + +void HandleDiscoverResult(otActiveScanResult *aResult, void *aContext) +{ + DiscoverContext *context = static_cast(aContext); + ScanResult *result = AsCoreTypePtr(aResult); + + VerifyOrQuit(aContext != nullptr); + + if (result == nullptr) + { + context->mDiscoverDone = true; + } + else + { + VerifyOrQuit(!context->mDiscoverDone); + SuccessOrQuit(context->mScanResults.PushBack(*result)); + } +} + +void Test1_4_Trel_Tc_5(void) +{ + /** + * 8.5. [1.4] [CERT] Discover Scan over multi-radio + * + * 8.5.1. Purpose + * This test covers MLE discovery scan when nodes support different radio links. + * + * 8.5.2. Topology + * - 1. Node_1 BR (DUT) - Support multi-radio (15.4 and TREL). + * - 2. Node_2 - Reference device that supports multi-radio (15.4 and TREL). + * - 3. Node_3 - Reference device that supports 15.4 radio only. + * Devices using TREL MUST be connected to the same infrastructure link. + * Note: the infrastructure link is not shown in the figure below. + * + * Spec Reference | V1.1 Section | V1.3.0 Section + * -----------------|--------------|--------------- + * Discovery Scan | 4.7.2.1 | 4.5.2.1 + */ + + Core nexus; + Node &node1 = nexus.CreateNode(); + Node &node2 = nexus.CreateNode(); + Node &node3 = nexus.CreateNode(); + + DiscoverContext resultContext; + + node1.SetName("NODE_1_DUT"); + node2.SetName("NODE_2"); + node3.SetName("NODE_3"); + + nexus.AdvanceTime(0); + + SuccessOrQuit(Instance::SetGlobalLogLevel(kLogLevelNote)); + + /** + * Step 1 + * - Device: Node_1 (DUT), Node_2, Node_3 + * - Description (TREL-8.5): Form a network on Node_1. Form a different network on Node_2 and different one on + * Node_3 (each node has its own network). + * - Pass Criteria: + * - N/A + */ + Log("Step 1: Form separate networks on Node_1, Node_2, and Node_3"); + + SuccessOrQuit(node1.Get().SetEnabled(true, kInfraIfIndex)); + SuccessOrQuit(node2.Get().SetEnabled(true, kInfraIfIndex)); + + /** Node_3 supports 15.4 radio only. Disable TREL interface on Node_3. */ + node3.Get().SetEnabled(false, ot::Trel::Interface::kRequesterUser); + + node1.Form(); + node2.Form(); + node3.Form(); + + nexus.AdvanceTime(kFormNetworkTime); + + VerifyOrQuit(node1.Get().IsLeader()); + VerifyOrQuit(node2.Get().IsLeader()); + VerifyOrQuit(node3.Get().IsLeader()); + + /** + * Step 2 + * - Device: Node_2 + * - Description (TREL-8.5): Harness instructs device to perform MLE Discovery Scan. + * - Pass Criteria: + * - Node_2 MUST see both the DUT and Node_3 in its scan result. + */ + Log("Step 2: Node_2 performs MLE Discovery Scan"); + + resultContext.Clear(); + SuccessOrQuit(node2.Get().Discover(Mac::ChannelMask(), kPanIdAny, /* aJoiner */ false, + /* aFilter */ false, /* aFilterIndexes */ nullptr, + HandleDiscoverResult, &resultContext)); + + nexus.AdvanceTime(kScanTime); + + VerifyOrQuit(resultContext.mDiscoverDone); + { + bool foundNode1 = false; + bool foundNode3 = false; + + for (const ScanResult &result : resultContext.mScanResults) + { + if (result.GetExtAddress() == node1.Get().GetExtAddress()) + { + foundNode1 = true; + } + if (result.GetExtAddress() == node3.Get().GetExtAddress()) + { + foundNode3 = true; + } + } + + VerifyOrQuit(foundNode1, "Node_2 did not see Node_1 (DUT) in scan result"); + VerifyOrQuit(foundNode3, "Node_2 did not see Node_3 in scan result"); + } + + /** + * Step 3 + * - Device: Node_3 + * - Description (TREL-8.5): Harness instructs device to perform MLE Discovery Scan. + * - Pass Criteria: + * - Node_3 MUST see both the DUT and Node_2 in its scan result. + */ + Log("Step 3: Node_3 performs MLE Discovery Scan"); + + resultContext.Clear(); + SuccessOrQuit(node3.Get().Discover(Mac::ChannelMask(), kPanIdAny, /* aJoiner */ false, + /* aFilter */ false, /* aFilterIndexes */ nullptr, + HandleDiscoverResult, &resultContext)); + + nexus.AdvanceTime(kScanTime); + + VerifyOrQuit(resultContext.mDiscoverDone); + { + bool foundNode1 = false; + bool foundNode2 = false; + + for (const ScanResult &result : resultContext.mScanResults) + { + if (result.GetExtAddress() == node1.Get().GetExtAddress()) + { + foundNode1 = true; + } + if (result.GetExtAddress() == node2.Get().GetExtAddress()) + { + foundNode2 = true; + } + } + + VerifyOrQuit(foundNode1, "Node_3 did not see Node_1 (DUT) in scan result"); + VerifyOrQuit(foundNode2, "Node_3 did not see Node_2 in scan result"); + } + + nexus.SaveTestInfo("test_1_4_TREL_TC_5.json"); +} + +#endif // OPENTHREAD_CONFIG_MULTI_RADIO + +} // namespace Nexus +} // namespace ot + +int main(void) +{ +#if OPENTHREAD_CONFIG_MULTI_RADIO + ot::Nexus::Test1_4_Trel_Tc_5(); + printf("All tests passed\n"); +#else + printf("Multi-radio is not enabled - test skipped\n"); +#endif + return 0; +} diff --git a/tests/nexus/verify_1_4_TREL_TC_5.py b/tests/nexus/verify_1_4_TREL_TC_5.py new file mode 100644 index 000000000..d222aa880 --- /dev/null +++ b/tests/nexus/verify_1_4_TREL_TC_5.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +# +# 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. +# + +import sys +import os + +# Add the current directory to sys.path to find verify_utils +CUR_DIR = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(CUR_DIR) + +import verify_utils +from pktverify import consts + + +def verify(pv): + # 8.5. [1.4] [CERT] Discover Scan over multi-radio + # + # 8.5.1. Purpose + # This test covers MLE discovery scan when nodes support different radio links. + # + # 8.5.2. Topology + # - 1. Node_1 BR (DUT) - Support multi-radio (15.4 and TREL). + # - 2. Node_2 - Reference device that supports multi-radio (15.4 and TREL). + # - 3. Node_3 - Reference device that supports 15.4 radio only. + # Devices using TREL MUST be connected to the same infrastructure link. + # Note: the infrastructure link is not shown in the figure below. + # + # Spec Reference | V1.1 Section | V1.3.0 Section + # -----------------|--------------|--------------- + # Discovery Scan | 4.7.2.1 | 4.5.2.1 + + pkts = pv.pkts + pv.summary.show() + + NODE_1 = pv.vars['NODE_1_DUT'] + NODE_2 = pv.vars['NODE_2'] + NODE_3 = pv.vars['NODE_3'] + + # Step 1 + # - Device: Node_1 (DUT), Node_2, Node_3 + # - Description (TREL-8.5): Form a network on Node_1. Form a different network on Node_2 and different one on + # Node_3 (each node has its own network). + # - Pass Criteria: + # - N/A + print("Step 1: Form separate networks on Node_1, Node_2, and Node_3") + # Verified by C++ code internally + + # Step 2 + # - Device: Node_2 + # - Description (TREL-8.5): Harness instructs device to perform MLE Discovery Scan. + # - Pass Criteria: + # - Node_2 MUST see both the DUT and Node_3 in its scan result. + print("Step 2: Node_2 performs MLE Discovery Scan") + # Node_2 MUST send MLE Discovery Request on both 15.4 and TREL + # We verify at least one of them, but specifically check for TREL response from DUT + # and 15.4 response from Node_3. + + # 1. Node_2 MUST send MLE Discovery Request on 15.4 + pkts.copy().\ + filter_wpan_src64(NODE_2).\ + filter_mle_cmd(consts.MLE_DISCOVERY_REQUEST).\ + filter(lambda p: p.wpan).\ + must_next() + + # 2. Node_2 MUST send MLE Discovery Request on TREL + pkts.copy().\ + filter_eth_src(pv.vars['NODE_2_ETH']).\ + filter(lambda p: p.udp.dstport == pv.vars['NODE_1_DUT_TREL_PORT'] or p.udp.dstport == pv.vars['NODE_2_TREL_PORT']).\ + filter(lambda p: p.trel).\ + must_next() + + # Node_2 MUST see Node_1 (DUT) in its scan result (verify TREL response) + pkts.copy().\ + filter_eth_src(pv.vars['NODE_1_DUT_ETH']).\ + filter(lambda p: p.eth.dst == pv.vars['NODE_2_ETH']).\ + filter(lambda p: p.udp.dstport == pv.vars['NODE_2_TREL_PORT']).\ + filter(lambda p: p.trel).\ + must_next() + + # Node_2 MUST see Node_3 in its scan result (verify 15.4 response) + pkts.copy().\ + filter_wpan_src64(NODE_3).\ + filter_wpan_dst64(NODE_2).\ + filter_mle_cmd(consts.MLE_DISCOVERY_RESPONSE).\ + must_next() + + # Step 3 + # - Device: Node_3 + # - Description (TREL-8.5): Harness instructs device to perform MLE Discovery Scan. + # - Pass Criteria: + # - Node_3 MUST see both the DUT and Node_2 in its scan result. + print("Step 3: Node_3 performs MLE Discovery Scan") + # Node_3 MUST send MLE Discovery Request (15.4 only) + pkts.filter_wpan_src64(NODE_3).\ + filter_mle_cmd(consts.MLE_DISCOVERY_REQUEST).\ + must_next() + + # Node_3 MUST see Node_1 (DUT) and Node_2 in its scan result (both on 15.4) + pkts.copy().\ + filter_wpan_src64(NODE_1).\ + filter_wpan_dst64(NODE_3).\ + filter_mle_cmd(consts.MLE_DISCOVERY_RESPONSE).\ + must_next() + + pkts.copy().\ + filter_wpan_src64(NODE_2).\ + filter_wpan_dst64(NODE_3).\ + filter_mle_cmd(consts.MLE_DISCOVERY_RESPONSE).\ + must_next() + + +if __name__ == '__main__': + verify_utils.run_main(verify)