From c29d3ccebc8bdc6bb1946dd587b690e8ae2067bd Mon Sep 17 00:00:00 2001 From: Jonathan Hui Date: Tue, 17 Feb 2026 23:23:59 -0600 Subject: [PATCH] [nexus] add test 6.2.2 Connectivity when Parent Joins Partition (#12479) This commit adds a new Nexus test case for 'Connectivity when Parent Joins Partition' (6.2.2) as specified in the Thread Test Specification. The test verifies that a Child (End Device or Sleepy End Device) maintains connectivity when the current Leader is removed and its parent Router joins a new partition created by another Router. Summary of changes: - tests/nexus/test_6_2_2.cpp: C++ test execution script. - Implements support for Topology A (MED) and Topology B (SED). - Sets up a network with a Leader, Router_1, Router_2, and the DUT attached to Router_1. - Configures Router_2 with a shorter NETWORK_ID_TIMEOUT and a maximum Partition ID preference. - Simulates Leader removal and verifies that Router_2 creates a new partition and Router_1 joins it. - Verifies that the DUT maintains connectivity via MLE Child Update (MED) or periodic data requests (SED). - Verifies bidirectional connectivity using ICMPv6 Echo Request. - tests/nexus/verify_6_2_2.py: Python PCAP verification script. - Validates that Router_2 creates a new partition with the expected maximum Partition ID. - Validates that Router_1 joins the new partition. - For MED, verifies the MLE Child Update Request contains correct TLVs (Source Address, Leader Data, Mode) and Partition ID. - For SED, verifies periodic connectivity to the parent. - Validates the ICMPv6 Echo Request and Reply exchange. - tests/nexus/run_nexus_tests.sh: Updated test runner. - Added 6_2_2 to the default test list. - Added expansion logic to run both A and B topologies. - tests/nexus/CMakeLists.txt: Added the new test to the build system. --- tests/nexus/CMakeLists.txt | 1 + tests/nexus/run_nexus_tests.sh | 3 +- tests/nexus/test_6_2_2.cpp | 309 +++++++++++++++++++++++++++++++++ tests/nexus/verify_6_2_2.py | 188 ++++++++++++++++++++ 4 files changed, 500 insertions(+), 1 deletion(-) create mode 100644 tests/nexus/test_6_2_2.cpp create mode 100644 tests/nexus/verify_6_2_2.py diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index a1e6a550b..6efa9f320 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -164,6 +164,7 @@ ot_nexus_test(6_1_1 "cert;nexus") ot_nexus_test(6_1_2 "cert;nexus") ot_nexus_test(6_1_3 "cert;nexus") ot_nexus_test(6_2_1 "cert;nexus") +ot_nexus_test(6_2_2 "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 d9ac2bbde..b28dac375 100755 --- a/tests/nexus/run_nexus_tests.sh +++ b/tests/nexus/run_nexus_tests.sh @@ -98,6 +98,7 @@ DEFAULT_TESTS=( "6_1_3_B" "6_2_1_A" "6_2_1_B" + "6_2_2" ) # Use provided arguments or the default test list @@ -189,7 +190,7 @@ run_test() expanded_tests=() for t in "${TESTS_TO_RUN[@]}"; do case "$t" in - 6_1_1 | 6_1_2 | 6_1_3 | 6_2_1) + 6_1_1 | 6_1_2 | 6_1_3 | 6_2_1 | 6_2_2) expanded_tests+=("${t}_A" "${t}_B") ;; *) diff --git a/tests/nexus/test_6_2_2.cpp b/tests/nexus/test_6_2_2.cpp new file mode 100644 index 000000000..d6d330ced --- /dev/null +++ b/tests/nexus/test_6_2_2.cpp @@ -0,0 +1,309 @@ +/* + * 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 { + +/** + * Time to advance for a node to form a network and become leader, in milliseconds. + */ +static constexpr uint32_t kFormNetworkTime = 13 * 1000; + +/** + * Time to advance for a node to join as a child and upgrade to a router, in milliseconds. + */ +static constexpr uint32_t kAttachToRouterTime = 200 * 1000; + +/** + * Time to advance for the network to stabilize after routers have attached. + */ +static constexpr uint32_t kStabilizationTime = 20 * 1000; + +/** + * Router 2 network ID timeout, in seconds. + */ +static constexpr uint32_t kRouter2NetworkIdTimeout = 110; + +/** + * Max Partition ID. + */ +static constexpr uint32_t kMaxPartitionId = 0xffffffff; + +/** + * End device timeout for Topology A, in seconds. + */ +static constexpr uint32_t kEndDeviceTimeout = 120; + +/** + * Poll period for SED in Topology B, in milliseconds. + */ +static constexpr uint32_t kPollPeriod = 500; + +/** + * Time to wait for ICMPv6 Echo response, in milliseconds. + */ +static constexpr uint32_t kEchoTimeout = 5000; + +enum Topology +{ + kTopologyA, + kTopologyB, +}; + +void RunTest_6_2_2(Topology aTopology, const char *aJsonFile) +{ + /** + * 6.2.2 Connectivity when Parent Joins Partition + * + * 6.2.2.1 Topology + * - Topology A: DUT as End Device (ED_1) + * - Topology B: DUT as Sleepy End Device (SED_1) + * - Leader: Configured with NETWORK_ID_TIMEOUT = 120 seconds (default). + * - Router_1: Parent of the DUT. + * - Router_2: Set NETWORK_ID_TIMEOUT = 110 seconds. Set Partition ID to max value. + * + * 6.2.2.2 Purpose & Description + * The purpose of this test case is to show that the DUT will uphold connectivity when the Leader is removed and + * Router_1 joins a new partition. + * + * Spec Reference | V1.1 Section | V1.3.0 Section + * -----------------|--------------|--------------- + * Children | 5.16.6 | 5.16.6 + */ + + Core nexus; + + Node &leader = nexus.CreateNode(); + Node &router1 = nexus.CreateNode(); + Node &router2 = nexus.CreateNode(); + Node &dut = nexus.CreateNode(); + + leader.SetName("LEADER"); + router1.SetName("ROUTER_1"); + router2.SetName("ROUTER_2"); + + if (aTopology == kTopologyA) + { + dut.SetName("ED_1"); + } + else + { + dut.SetName("SED_1"); + } + + nexus.AdvanceTime(0); + + Instance::SetLogLevel(kLogLevelNote); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 1: All"); + + /** + * Step 1: All + * - Description: Ensure topology is formed correctly. Ensure that the DUT successfully attached to Router_1. + * - Pass Criteria: N/A + */ + leader.AllowList(router1); + leader.AllowList(router2); + router1.AllowList(leader); + router1.AllowList(router2); + router1.AllowList(dut); + router2.AllowList(leader); + router2.AllowList(router1); + dut.AllowList(router1); + + leader.Form(); + nexus.AdvanceTime(kFormNetworkTime); + VerifyOrQuit(leader.Get().IsLeader()); + + router1.Join(leader); + router2.Join(leader); + nexus.AdvanceTime(kAttachToRouterTime); + VerifyOrQuit(router1.Get().IsRouter()); + VerifyOrQuit(router2.Get().IsRouter()); + + if (aTopology == kTopologyA) + { + dut.Join(router1, Node::kAsMed); + dut.Get().SetTimeout(kEndDeviceTimeout); + } + else + { + dut.Join(router1, Node::kAsSed); + SuccessOrQuit(dut.Get().SetExternalPollPeriod(kPollPeriod)); + } + nexus.AdvanceTime(kAttachToRouterTime); + VerifyOrQuit(dut.Get().IsChild()); + VerifyOrQuit(dut.Get().GetParent().GetExtAddress() == router1.Get().GetExtAddress()); + + nexus.AdvanceTime(kStabilizationTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 2: Router_2"); + + /** + * Step 2: Router_2 + * - Description: Harness configures Router_2 with NETWORK_ID_TIMEOUT = 110 seconds. + * - Pass Criteria: N/A + */ + router2.Get().SetNetworkIdTimeout(kRouter2NetworkIdTimeout); + router2.Get().SetPreferredLeaderPartitionId(kMaxPartitionId); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 3: Leader"); + + /** + * Step 3: Leader + * - Description: Harness silently removes the Leader from the network. + * - Pass Criteria: N/A + */ + leader.Get().Stop(); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 4: Router_2"); + + /** + * Step 4: Router_2 + * - Description: Automatically creates new partition and begins transmitting MLE Advertisements. + * - Pass Criteria: N/A + */ + // Router 2 will timeout after 110 seconds and become leader. + nexus.AdvanceTime(kRouter2NetworkIdTimeout * 1000 + kStabilizationTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 5: Router_1"); + + /** + * Step 5: Router_1 + * - Description: Automatically joins Router_2 partition. + * - Pass Criteria: N/A + */ + // Router 1 will timeout after 120 seconds and join Router 2's partition. + nexus.AdvanceTime(kAttachToRouterTime); + + VerifyOrQuit(router2.Get().IsLeader()); + VerifyOrQuit(router2.Get().GetLeaderData().GetPartitionId() == kMaxPartitionId); + VerifyOrQuit(router1.Get().IsRouter()); + VerifyOrQuit(router1.Get().GetLeaderData().GetPartitionId() == kMaxPartitionId); + + if (aTopology == kTopologyA) + { + Log("---------------------------------------------------------------------------------------"); + Log("Step 6: Test Harness (Topology A only)"); + + /** + * Step 6: Test Harness (Topology A only) + * - Description: Wait for 2x (double) the End Device timeout period. + * - Pass Criteria: N/A + */ + nexus.AdvanceTime(2 * kEndDeviceTimeout * 1000); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 7: MED_1 (DUT) [Topology A only]"); + + /** + * Step 7: MED_1 (DUT) [Topology A only] + * - Description: Automatically sends MLE Child Update Request to Router_1. Router_1 automatically sends a MLE + * Child Update Response to MED_1. + * - Pass Criteria: + * - The DUT MUST send a MLE Child Update Request to Router_1, including the following TLVs: + * - Source Address TLV + * - Leader Data TLV + * - Partition ID (value = max value) + * - Version (value matches its parent value) + * - Stable Version (value matches its parent value) + * - Mode TLV + * - The DUT MUST NOT transmit a MLE Announce message or an additional MLE Child ID Request. + */ + nexus.AdvanceTime(kStabilizationTime); + } + else + { + Log("---------------------------------------------------------------------------------------"); + Log("Step 8: SED_1 (DUT) [Topology B only]"); + + /** + * Step 8: SED_1 (DUT) [Topology B only] + * - Description: Automatically sends periodic 802.15.4 Data Request messages as part of the keep-alive message. + * - Pass Criteria: + * - The DUT MUST send a 802.15.4 Data Request command to the parent device and receive an ACK message in + * response. + * - The DUT MUST NOT transmit a MLE Announce message or an additional MLE Child ID Request. If it does, the + * test has failed. + */ + nexus.AdvanceTime(kStabilizationTime); + } + + Log("---------------------------------------------------------------------------------------"); + Log("Step 9: Router_1"); + + /** + * Step 9: Router_1 + * - Description: To verify connectivity, Harness instructs Router_1 to send an ICMPv6 Echo Request to the DUT link + * local address. + * - Pass Criteria: + * - The DUT MUST respond with ICMPv6 Echo Reply. + */ + nexus.SendAndVerifyEchoRequest(router1, dut.Get().GetLinkLocalAddress(), 0, 64, kEchoTimeout); + + nexus.SaveTestInfo(aJsonFile); +} + +} // namespace Nexus +} // namespace ot + +int main(int argc, char *argv[]) +{ + if (argc < 2) + { + ot::Nexus::RunTest_6_2_2(ot::Nexus::kTopologyA, "test_6_2_2_A.json"); + ot::Nexus::RunTest_6_2_2(ot::Nexus::kTopologyB, "test_6_2_2_B.json"); + } + else if (strcmp(argv[1], "A") == 0) + { + ot::Nexus::RunTest_6_2_2(ot::Nexus::kTopologyA, (argc > 2) ? argv[2] : "test_6_2_2_A.json"); + } + else if (strcmp(argv[1], "B") == 0) + { + ot::Nexus::RunTest_6_2_2(ot::Nexus::kTopologyB, (argc > 2) ? argv[2] : "test_6_2_2_B.json"); + } + else + { + fprintf(stderr, "Error: Invalid topology '%s'. Must be 'A' or 'B'.\n", argv[1]); + return 1; + } + + printf("All tests passed\n"); + return 0; +} diff --git a/tests/nexus/verify_6_2_2.py b/tests/nexus/verify_6_2_2.py new file mode 100644 index 000000000..86e324027 --- /dev/null +++ b/tests/nexus/verify_6_2_2.py @@ -0,0 +1,188 @@ +#!/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 +from pktverify.null_field import nullField + +MAX_PARTITION_ID = 0xffffffff + + +def verify(pv): + # 6.2.2 Connectivity when Parent Joins Partition + # + # 6.2.2.1 Topology + # - Topology A: DUT as End Device (ED_1) + # - Topology B: DUT as Sleepy End Device (SED_1) + # - Leader: Configured with NETWORK_ID_TIMEOUT = 120 seconds (default). + # - Router_1: Parent of the DUT. + # - Router_2: Set NETWORK_ID_TIMEOUT = 110 seconds. Set Partition ID to max value. + # + # 6.2.2.2 Purpose & Description + # The purpose of this test case is to show that the DUT will uphold connectivity when the Leader is removed and + # Router_1 joins a new partition. + # + # Spec Reference | V1.1 Section | V1.3.0 Section + # -----------------|--------------|--------------- + # Children | 5.16.6 | 5.16.6 + + pkts = pv.pkts + pv.summary.show() + + LEADER = pv.vars['LEADER'] + ROUTER_1 = pv.vars['ROUTER_1'] + ROUTER_2 = pv.vars['ROUTER_2'] + + if 'ED_1' in pv.vars: + DUT = pv.vars['ED_1'] + IS_TOPOLOGY_A = True + else: + DUT = pv.vars['SED_1'] + IS_TOPOLOGY_A = False + + # Step 1: All + # - Description: Ensure topology is formed correctly. Ensure that the DUT successfully attached to Router_1. + # - Pass Criteria: N/A + print("Step 1: All") + + # Step 2: Router_2 + # - Description: Harness configures Router_2 with NETWORK_ID_TIMEOUT = 110 seconds. + # - Pass Criteria: N/A + print("Step 2: Router_2") + + # Step 3: Leader + # - Description: Harness silently removes the Leader from the network. + # - Pass Criteria: N/A + print("Step 3: Leader") + + # Step 4: Router_2 + # - Description: Automatically creates new partition and begins transmitting MLE Advertisements. + # - Pass Criteria: N/A + print("Step 4: Router_2") + pkts.filter_wpan_src64(ROUTER_2).\ + filter_LLANMA().\ + filter_mle_cmd(consts.MLE_ADVERTISEMENT).\ + filter(lambda p: p.mle.tlv.leader_data.partition_id == MAX_PARTITION_ID).\ + must_next() + + # Step 5: Router_1 + # - Description: Automatically joins Router_2 partition. + # - Pass Criteria: N/A + print("Step 5: Router_1") + _pkt = pkts.filter_wpan_src64(ROUTER_1).\ + filter_LLANMA().\ + filter_mle_cmd(consts.MLE_ADVERTISEMENT).\ + filter(lambda p: p.mle.tlv.leader_data.partition_id == MAX_PARTITION_ID).\ + must_next() + router1_data_version = _pkt.mle.tlv.leader_data.data_version + router1_stable_data_version = _pkt.mle.tlv.leader_data.stable_data_version + + if IS_TOPOLOGY_A: + # Step 6: Test Harness (Topology A only) + # - Description: Wait for 2x (double) the End Device timeout period. + # - Pass Criteria: N/A + print("Step 6: Test Harness (Topology A only)") + + # Step 7: MED_1 (DUT) [Topology A only] + # - Description: Automatically sends MLE Child Update Request to Router_1. Router_1 automatically sends a MLE + # Child Update Response to MED_1. + # - Pass Criteria: + # - The DUT MUST send a MLE Child Update Request to Router_1, including the following TLVs: + # - Source Address TLV + # - Leader Data TLV + # - Partition ID (value = max value) + # - Version (value matches its parent value) + # - Stable Version (value matches its parent value) + # - Mode TLV + # - The DUT MUST NOT transmit a MLE Announce message or an additional MLE Child ID Request. + print("Step 7: MED_1 (DUT) [Topology A only]") + pkts.filter_wpan_src64(DUT).\ + filter_wpan_dst64(ROUTER_1).\ + filter_mle_cmd(consts.MLE_CHILD_UPDATE_REQUEST).\ + filter(lambda p: { + consts.SOURCE_ADDRESS_TLV, + consts.LEADER_DATA_TLV, + consts.MODE_TLV + } <= set(p.mle.tlv.type) and\ + p.mle.tlv.leader_data.partition_id == MAX_PARTITION_ID and\ + p.mle.tlv.leader_data.data_version == router1_data_version and\ + p.mle.tlv.leader_data.stable_data_version == router1_stable_data_version).\ + must_next() + + # Verify no MLE Announce or additional Child ID Request from DUT + pkts.filter_wpan_src64(DUT).\ + filter(lambda p: hasattr(p, 'mle') and\ + p.mle.cmd in [consts.MLE_ANNOUNCE, consts.MLE_CHILD_ID_REQUEST]).\ + must_not_next() + + else: + # Step 8: SED_1 (DUT) [Topology B only] + # - Description: Automatically sends periodic 802.15.4 Data Request messages as part of the keep-alive message. + # - Pass Criteria: + # - The DUT MUST send a 802.15.4 Data Request command to the parent device and receive an ACK message in + # response. + # - The DUT MUST NOT transmit a MLE Announce message or an additional MLE Child ID Request. If it does, the + # test has failed. + print("Step 8: SED_1 (DUT) [Topology B only]") + # We look for any packet from DUT to its parent ROUTER_1 as a sign of connectivity and keep-alive. + pkts.filter_wpan_src64(DUT).\ + filter_wpan_dst64(ROUTER_1).\ + must_next() + + # Verify no MLE Announce or additional Child ID Request from DUT + pkts.filter_wpan_src64(DUT).\ + filter(lambda p: hasattr(p, 'mle') and\ + p.mle.cmd in [consts.MLE_ANNOUNCE, consts.MLE_CHILD_ID_REQUEST]).\ + must_not_next() + + # Step 9: Router_1 + # - Description: To verify connectivity, Harness instructs Router_1 to send an ICMPv6 Echo Request to the DUT link + # local address. + # - Pass Criteria: + # - The DUT MUST respond with ICMPv6 Echo Reply. + print("Step 9: Router_1") + _pkt = pkts.filter_ping_request().\ + filter_wpan_src64(ROUTER_1).\ + filter_wpan_dst64(DUT).\ + must_next() + pkts.filter_ping_reply(identifier=_pkt.icmpv6.echo.identifier).\ + filter_wpan_src64(DUT).\ + filter_wpan_dst64(ROUTER_1).\ + must_next() + + +if __name__ == '__main__': + verify_utils.run_main(verify)