[nexus] add test 5.1.2 and refactor nexus test infrastructure (#12363)

Adds a new Nexus test case for 'Child Address Timeout' (5.1.2) and
performs several refactorings to improve the nexus test framework.

Summary of changes:
- Implemented Nexus test 5.1.2 (Child Address Timeout):
    - Added test_5_1_2.cpp: Sets up a Leader, Router (DUT), MED, and SED
      topology with restricted connectivity using AllowList. Verifies
      that the parent stops responding to Address Queries for children
      after their timeout interval expires.
    - Added verify_5_1_2.py: PCAP verification script for test 5.1.2.
- Refactored Nexus verification infrastructure:
    - Created verify_utils.py: Shared module for common verification
      logic, including monkey-patches for CoapTlvParser and
      which_tshark, and a generic 'run_main' test runner function.
    - Updated verify_5_1_1.py and verify_5_1_2.py to use verify_utils.py,
      significantly reducing boilerplate code.
    - Cleaned up imports in verify_utils.py by removing unused logging.
- Updated build and execution scripts:
    - Modified CMakeLists.txt to build the new 5.1.2 test executable.
    - Updated run_nexus_tests.sh to include 5.1.2 in the default test list.
This commit is contained in:
Jonathan Hui
2026-02-03 21:44:05 -08:00
committed by GitHub
parent 1a0023d119
commit 7577311aa4
6 changed files with 443 additions and 73 deletions
+1
View File
@@ -114,6 +114,7 @@ endmacro()
#----------------------------------------------------------------------------------------------------------------------
ot_nexus_test(5_1_1)
ot_nexus_test(5_1_2)
ot_nexus_test(border_agent)
ot_nexus_test(border_agent_tracker)
ot_nexus_test(discover_scan)
+1
View File
@@ -49,6 +49,7 @@ NEXUS_BIN_DIR="${BUILD_DIR}/tests/nexus"
# Default test list if no arguments are provided
DEFAULT_TESTS=(
"5_1_1"
"5_1_2"
)
# Use provided arguments or the default test list
+214
View File
@@ -0,0 +1,214 @@
/*
* 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 <stdio.h>
#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.
*/
static constexpr uint32_t kFormNetworkTime = 13 * 1000;
/**
* Time to advance for a node to join as a child and upgrade to a router.
*/
static constexpr uint32_t kAttachToRouterTime = 200 * 1000;
/**
* Time to advance for a node to join as a child.
*/
static constexpr uint32_t kAttachAsChildTime = 5 * 1000;
/**
* Child timeout value in seconds.
*/
static constexpr uint32_t kChildTimeout = 10;
/**
* Time to wait for child timeout to expire.
*/
static constexpr uint32_t kChildTimeoutWaitTime = (kChildTimeout + 2) * 1000;
/**
* Time to wait for ICMPv6 Echo response (Address Query).
*/
static constexpr uint32_t kEchoRequestWaitTime = 5 * 1000;
static void SendEchoRequest(Node &aSender, const Ip6::Address &aPeerAddr, uint16_t aIdentifier)
{
Message *message = aSender.Get<Ip6::Icmp>().NewMessage();
Ip6::MessageInfo messageInfo;
VerifyOrQuit(message != nullptr);
messageInfo.SetPeerAddr(aPeerAddr);
messageInfo.SetHopLimit(64);
SuccessOrQuit(aSender.Get<Ip6::Icmp>().SendEchoRequest(*message, messageInfo, aIdentifier));
}
void Test5_1_2(void)
{
/**
* 5.1.2 Child Address Timeout
*
* 5.1.2.1 Topology
* - Leader
* - Router_1 (DUT)
* - MED_1
* - SED_1
*
* 5.1.2.2 Purpose & Description
* The purpose of the test case is to verify that when the timer reaches the value of the Timeout TLV sent by the
* Child, the Parent stops responding to Address Query on the Child's behalf.
*
* Spec Reference: Timing Out Children
* V1.1 Section: 4.7.5
* V1.3.0 Section: 4.6.3
*/
Core nexus;
Node &leader = nexus.CreateNode();
Node &router = nexus.CreateNode();
Node &med = nexus.CreateNode();
Node &sed = nexus.CreateNode();
leader.SetName("LEADER");
router.SetName("ROUTER_1");
med.SetName("MED_1");
sed.SetName("SED_1");
nexus.AdvanceTime(0);
// Use AllowList feature to restrict the topology.
leader.AllowList(router);
router.AllowList(leader);
router.AllowList(med);
med.AllowList(router);
router.AllowList(sed);
sed.AllowList(router);
Log("---------------------------------------------------------------------------------------");
Log("Step 1: All");
/**
* Step 1: All
* - Description: Verify topology is formed correctly
* - Pass Criteria: N/A
*/
leader.Form();
nexus.AdvanceTime(kFormNetworkTime);
VerifyOrQuit(leader.Get<Mle::Mle>().IsLeader());
router.Join(leader);
nexus.AdvanceTime(kAttachToRouterTime);
VerifyOrQuit(router.Get<Mle::Mle>().IsRouter());
med.Get<Mle::Mle>().SetTimeout(kChildTimeout);
med.Join(router, Node::kAsMed);
nexus.AdvanceTime(kAttachAsChildTime);
VerifyOrQuit(med.Get<Mle::Mle>().IsChild());
sed.Get<Mle::Mle>().SetTimeout(kChildTimeout);
sed.Join(router, Node::kAsSed);
nexus.AdvanceTime(kAttachAsChildTime);
VerifyOrQuit(sed.Get<Mle::Mle>().IsChild());
Log("---------------------------------------------------------------------------------------");
Log("Step 2: MED_1, SED_1");
/**
* Step 2: MED_1, SED_1
* - Description: Harness silently powers-off both devices and waits for the keep-alive timeout to expire
* - Pass Criteria: N/A
*/
med.Get<Mle::Mle>().Stop();
sed.Get<Mle::Mle>().Stop();
nexus.AdvanceTime(kChildTimeoutWaitTime);
Log("---------------------------------------------------------------------------------------");
Log("Step 3: Leader");
/**
* Step 3: Leader
* - Description: Harness instructs the Leader to send an ICMPv6 Echo Request to MED_1. As part of the process, the
* Leader automatically attempts to perform address resolution by sending an Address Query Request
* - Pass Criteria: N/A
*/
SendEchoRequest(leader, med.Get<Mle::Mle>().GetMeshLocalEid(), 0x1234);
Log("---------------------------------------------------------------------------------------");
Log("Step 4: Router_1 (DUT)");
/**
* Step 4: Router_1 (DUT)
* - Description: Does not respond to Address Query Request
* - Pass Criteria: The DUT MUST NOT respond with an Address Notification Message
*/
nexus.AdvanceTime(kEchoRequestWaitTime);
Log("---------------------------------------------------------------------------------------");
Log("Step 6: Leader");
/**
* Step 6: Leader
* - Description: Harness instructs the Leader to send an ICMPv6 Echo Request to SED_1. As part of the process, the
* Leader automatically attempts to perform address resolution by sending an Address Query Request
* - Pass Criteria: N/A
*/
SendEchoRequest(leader, sed.Get<Mle::Mle>().GetMeshLocalEid(), 0x5678);
Log("---------------------------------------------------------------------------------------");
Log("Step 7: Router_1 (DUT)");
/**
* Step 7: Router_1 (DUT)
* - Description: Does not to Address Query Request
* - Pass Criteria: The DUT MUST NOT respond with an Address Notification Message
*/
nexus.AdvanceTime(kEchoRequestWaitTime);
nexus.SaveTestInfo("test_5_1_2.json");
}
} // namespace Nexus
} // namespace ot
int main(void)
{
ot::Nexus::Test5_1_2();
printf("All tests passed\n");
return 0;
}
+4 -73
View File
@@ -29,51 +29,14 @@
import sys
import os
import json
import logging
import traceback
# Add the thread-cert directory to sys.path to find pktverify
# Add the current directory to sys.path to find verify_utils
CUR_DIR = os.path.dirname(os.path.abspath(__file__))
OT_ROOT = os.path.abspath(os.path.join(CUR_DIR, '../..'))
THREAD_CERT_DIR = os.path.join(OT_ROOT, 'tests/scripts/thread-cert')
sys.path.append(THREAD_CERT_DIR)
sys.path.append(CUR_DIR)
import verify_utils
from pktverify import consts
from pktverify.packet_verifier import PacketVerifier
from pktverify.null_field import nullField
from pktverify import utils as pvutils
from pktverify.coap import CoapTlvParser
import struct
# Monkey-patch CoapTlvParser to parse Thread TLVs in CoAP payload
def thread_coap_tlv_parse(t, v):
kvs = []
if t == consts.NL_MAC_EXTENDED_ADDRESS_TLV:
kvs.append(('mac_addr', v.hex()))
elif t == consts.NL_RLOC16_TLV:
kvs.append(('rloc16', hex(struct.unpack('>H', v)[0])))
elif t == consts.NL_STATUS_TLV:
kvs.append(('status', str(v[0])))
elif t == consts.NL_ROUTER_MASK_TLV:
kvs.append(('router_mask', v.hex()))
return kvs
CoapTlvParser.parse = staticmethod(thread_coap_tlv_parse)
# Monkey-patch which_tshark to use system tshark if /tmp/thread-wireshark/tshark is not found
def which_tshark_patch():
default_path = '/tmp/thread-wireshark/tshark'
if os.path.exists(default_path):
return default_path
import shutil
return shutil.which('tshark') or '/usr/local/bin/tshark'
pvutils.which_tshark = which_tshark_patch
def verify(pv):
@@ -365,37 +328,5 @@ def verify(pv):
must_next()
def main():
if len(sys.argv) < 2:
print("Usage: python3 verify_5_1_1.py <json_file>")
sys.exit(1)
json_file = os.path.abspath(sys.argv[1])
with open(json_file, 'rt') as f:
data = json.load(f)
try:
wireshark_prefs = consts.WIRESHARK_OVERRIDE_PREFS.copy()
network_key = data.get('network_key')
if network_key:
wireshark_prefs['uat:ieee802154_keys'] = f'"{network_key}","1","Thread hash"'
mesh_local_prefix = data.get('extra_vars', {}).get('mesh_local_prefix')
if mesh_local_prefix:
prefix_addr = mesh_local_prefix.split('/')[0]
wireshark_prefs['6lowpan.context0'] = f'{prefix_addr}/64'
pv = PacketVerifier(json_file, wireshark_prefs=wireshark_prefs)
pv.add_common_vars()
verify(pv)
print("Verification PASSED")
except Exception as e:
print(f"Verification FAILED: {e}")
traceback.print_exc()
sys.exit(1)
if __name__ == '__main__':
main()
verify_utils.run_main(verify)
+113
View File
@@ -0,0 +1,113 @@
#!/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):
"""
5.1.2 Child Address Timeout
5.1.2.1 Topology
- Leader
- Router_1 (DUT)
- MED_1
- SED_1
5.1.2.2 Purpose & Description
The purpose of the test case is to verify that when the timer reaches the value of the Timeout TLV sent by the Child, the Parent stops responding to Address Query on the Child's behalf.
Spec Reference: Timing Out Children
V1.1 Section: 4.7.5
V1.3.0 Section: 4.6.3
"""
pkts = pv.pkts
pv.summary.show()
LEADER = pv.vars['LEADER']
ROUTER_1 = pv.vars['ROUTER_1']
# Step 1: All
# - Description: Verify topology is formed correctly
# - Pass Criteria: N/A
print("Step 1: Verify topology is formed correctly")
pkts.filter_wpan_src64(LEADER).filter_mle_cmd(consts.MLE_ADVERTISEMENT).must_next()
pkts.filter_wpan_src64(ROUTER_1).filter_mle_cmd(consts.MLE_ADVERTISEMENT).must_next()
# Step 2: MED_1, SED_1
# - Description: Harness silently powers-off both devices and waits for the keep-alive timeout to expire
# - Pass Criteria: N/A
print("Step 2: MED_1 and SED_1 power off")
# Step 3: Leader
# - Description: Harness instructs the Leader to send an ICMPv6 Echo Request to MED_1. As part of the process, the Leader automatically attempts to perform address resolution by sending an Address Query Request
# - Pass Criteria: N/A
print("Step 3: Leader sends Address Query for MED_1")
pkts.filter_wpan_src64(LEADER).\
filter_coap_request(consts.ADDR_QRY_URI).\
filter(lambda p: p.coap.tlv.target_eid == pv.vars['MED_1_MLEID']).\
must_next()
# Step 4: Router_1 (DUT)
# - Description: Does not respond to Address Query Request
# - Pass Criteria: The DUT MUST NOT respond with an Address Notification Message
print("Step 4: Router_1 (DUT) MUST NOT respond with an Address Notification Message")
pkts.filter_wpan_src64(ROUTER_1).\
filter_coap_request(consts.ADDR_NTF_URI).\
must_not_next()
# Step 6: Leader
# - Description: Harness instructs the Leader to send an ICMPv6 Echo Request to SED_1. As part of the process, the Leader automatically attempts to perform address resolution by sending an Address Query Request
# - Pass Criteria: N/A
print("Step 6: Leader sends Address Query for SED_1")
pkts.filter_wpan_src64(LEADER).\
filter_coap_request(consts.ADDR_QRY_URI).\
filter(lambda p: p.coap.tlv.target_eid == pv.vars['SED_1_MLEID']).\
must_next()
# Step 7: Router_1 (DUT)
# - Description: Does not to Address Query Request
# - Pass Criteria: The DUT MUST NOT respond with an Address Notification Message
print("Step 7: Router_1 (DUT) MUST NOT respond with an Address Notification Message")
pkts.filter_wpan_src64(ROUTER_1).\
filter_coap_request(consts.ADDR_NTF_URI).\
must_not_next()
if __name__ == '__main__':
verify_utils.run_main(verify)
+110
View File
@@ -0,0 +1,110 @@
#!/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
import json
import traceback
import struct
# Add the thread-cert directory to sys.path to find pktverify
CUR_DIR = os.path.dirname(os.path.abspath(__file__))
OT_ROOT = os.path.abspath(os.path.join(CUR_DIR, '../..'))
THREAD_CERT_DIR = os.path.join(OT_ROOT, 'tests/scripts/thread-cert')
if THREAD_CERT_DIR not in sys.path:
sys.path.append(THREAD_CERT_DIR)
from pktverify import consts
from pktverify.packet_verifier import PacketVerifier
from pktverify import utils as pvutils
from pktverify.coap import CoapTlvParser
from pktverify.addrs import Ipv6Addr
# Monkey-patch CoapTlvParser to parse Thread TLVs in CoAP payload
def thread_coap_tlv_parse(t, v):
kvs = []
if t == consts.NL_TARGET_EID_TLV:
kvs.append(('target_eid', str(Ipv6Addr(v))))
elif t == consts.NL_MAC_EXTENDED_ADDRESS_TLV:
kvs.append(('mac_addr', v.hex()))
elif t == consts.NL_RLOC16_TLV:
kvs.append(('rloc16', hex(struct.unpack('>H', v)[0])))
elif t == consts.NL_STATUS_TLV:
kvs.append(('status', str(v[0])))
elif t == consts.NL_ROUTER_MASK_TLV:
kvs.append(('router_mask', v.hex()))
return kvs
def apply_patches():
CoapTlvParser.parse = staticmethod(thread_coap_tlv_parse)
def which_tshark_patch():
default_path = '/tmp/thread-wireshark/tshark'
if os.path.exists(default_path):
return default_path
import shutil
return shutil.which('tshark') or '/usr/local/bin/tshark'
pvutils.which_tshark = which_tshark_patch
def run_main(verify_func):
if len(sys.argv) < 2:
print(f"Usage: python3 {sys.argv[0]} <json_file>")
sys.exit(1)
apply_patches()
json_file = os.path.abspath(sys.argv[1])
with open(json_file, 'rt') as f:
data = json.load(f)
try:
wireshark_prefs = consts.WIRESHARK_OVERRIDE_PREFS.copy()
network_key = data.get('network_key')
if network_key:
wireshark_prefs['uat:ieee802154_keys'] = f'"{network_key}","1","Thread hash"'
mesh_local_prefix = data.get('extra_vars', {}).get('mesh_local_prefix')
if mesh_local_prefix:
prefix_addr = mesh_local_prefix.split('/')[0]
wireshark_prefs['6lowpan.context0'] = f'{prefix_addr}/64'
pv = PacketVerifier(json_file, wireshark_prefs=wireshark_prefs)
pv.add_common_vars()
verify_func(pv)
print("Verification PASSED")
except Exception as e:
print(f"Verification FAILED: {e}")
traceback.print_exc()
sys.exit(1)