diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index 74066031c..04e373d88 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -402,6 +402,7 @@ ot_nexus_test(dnssd_name_with_special_chars "core;nexus") ot_nexus_test(dtls "core;nexus") ot_nexus_test(fed_rx_only_link_establishment "core;nexus") ot_nexus_test(form_join "core;nexus") +ot_nexus_test(history_tracker "core;nexus") ot_nexus_test(ipv6_fragmentation "core;nexus") ot_nexus_test(ipv6_source_selection "core;nexus") ot_nexus_test(key_rotation_guard_time "core;nexus") diff --git a/tests/nexus/test_history_tracker.cpp b/tests/nexus/test_history_tracker.cpp new file mode 100644 index 000000000..ec4cb7159 --- /dev/null +++ b/tests/nexus/test_history_tracker.cpp @@ -0,0 +1,235 @@ +/* + * 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 "platform/nexus_core.hpp" +#include "platform/nexus_node.hpp" + +namespace ot { +namespace Nexus { + +void TestHistoryTracker(void) +{ + static constexpr uint32_t kFormLeaderTimeMsec = 13 * 1000; + static constexpr uint32_t kStopLeaderTimeMsec = 5 * 1000; + static constexpr uint32_t kRestartLeaderTimeMsec = 25 * 1000; + static constexpr uint32_t kJoinChildTimeMsec = 10 * 1000; + static constexpr uint32_t kAgeVerificationWindowMsec = 10000u; + + Core nexus; + + Node &leader = nexus.CreateNode(); + Node &child = nexus.CreateNode(); + + nexus.AdvanceTime(0); + + SuccessOrQuit(Instance::SetGlobalLogLevel(kLogLevelInfo)); + + Log("---------------------------------------------------------------------------------------"); + Log("Start leader and verify its netinfo history"); + + leader.Form(); + nexus.AdvanceTime(kFormLeaderTimeMsec); + VerifyOrQuit(leader.Get().IsLeader()); + + HistoryTracker::Iterator iter; + uint32_t age; + const HistoryTracker::NetworkInfo *netInfo; + + iter.Init(); + netInfo = leader.Get().IterateNetInfoHistory(iter, age); + VerifyOrQuit(netInfo != nullptr); + VerifyOrQuit(MapEnum(netInfo->mRole) == Mle::kRoleLeader); + VerifyOrQuit(netInfo->mMode.mRxOnWhenIdle); + VerifyOrQuit(netInfo->mMode.mDeviceType); + VerifyOrQuit(netInfo->mRloc16 == leader.Get().GetRloc16()); + VerifyOrQuit(netInfo->mPartitionId == leader.Get().GetLeaderData().GetPartitionId()); + + netInfo = leader.Get().IterateNetInfoHistory(iter, age); + VerifyOrQuit(netInfo != nullptr); + VerifyOrQuit(MapEnum(netInfo->mRole) == Mle::kRoleDetached); + + Log("---------------------------------------------------------------------------------------"); + Log("Stop leader and verify its netinfo history contains 'disabled'"); + + leader.Get().Down(); + leader.Get().Stop(); + + nexus.AdvanceTime(kStopLeaderTimeMsec); + + iter.Init(); + netInfo = leader.Get().IterateNetInfoHistory(iter, age); + VerifyOrQuit(netInfo != nullptr); + VerifyOrQuit(MapEnum(netInfo->mRole) == Mle::kRoleDisabled); + + netInfo = leader.Get().IterateNetInfoHistory(iter, age); + VerifyOrQuit(netInfo != nullptr); + VerifyOrQuit(MapEnum(netInfo->mRole) == Mle::kRoleLeader); + + Log("---------------------------------------------------------------------------------------"); + Log("Wait for 49 days and verify age calculations"); + + nexus.AdvanceTime(Time::kOneDayInMsec); + + iter.Init(); + netInfo = leader.Get().IterateNetInfoHistory(iter, age); + VerifyOrQuit(netInfo != nullptr); + VerifyOrQuit(age >= Time::kOneDayInMsec && age < Time::kOneDayInMsec + kAgeVerificationWindowMsec); + + nexus.AdvanceTime(Time::kOneDayInMsec); + iter.Init(); + netInfo = leader.Get().IterateNetInfoHistory(iter, age); + VerifyOrQuit(netInfo != nullptr); + VerifyOrQuit(age >= 2 * Time::kOneDayInMsec && age < 2 * Time::kOneDayInMsec + kAgeVerificationWindowMsec); + + nexus.AdvanceTime(47 * Time::kOneDayInMsec); + iter.Init(); + netInfo = leader.Get().IterateNetInfoHistory(iter, age); + VerifyOrQuit(netInfo != nullptr); + VerifyOrQuit(age == HistoryTracker::kMaxAge); + + Log("---------------------------------------------------------------------------------------"); + Log("Restart Leader, join Child"); + + leader.Form(); + nexus.AdvanceTime(kRestartLeaderTimeMsec); + VerifyOrQuit(leader.Get().IsLeader()); + + child.Join(leader, Node::kAsMed); + nexus.AdvanceTime(kJoinChildTimeMsec); + VerifyOrQuit(child.Get().IsChild()); + + Log("---------------------------------------------------------------------------------------"); + Log("Verify child netinfo"); + + iter.Init(); + netInfo = child.Get().IterateNetInfoHistory(iter, age); + VerifyOrQuit(netInfo != nullptr); + VerifyOrQuit(MapEnum(netInfo->mRole) == Mle::kRoleChild); + VerifyOrQuit(netInfo->mMode.mRxOnWhenIdle); + VerifyOrQuit(!netInfo->mMode.mDeviceType); + VerifyOrQuit(netInfo->mRloc16 == child.Get().GetRloc16()); + VerifyOrQuit(netInfo->mPartitionId == leader.Get().GetLeaderData().GetPartitionId()); + + netInfo = child.Get().IterateNetInfoHistory(iter, age); + VerifyOrQuit(netInfo != nullptr); + VerifyOrQuit(MapEnum(netInfo->mRole) == Mle::kRoleDetached); + + Log("---------------------------------------------------------------------------------------"); + Log("Change child mode and verify netinfo"); + + Mle::DeviceMode mode(Mle::DeviceMode::kModeRxOnWhenIdle | Mle::DeviceMode::kModeFullThreadDevice | + Mle::DeviceMode::kModeFullNetworkData); + + SuccessOrQuit(child.Get().SetDeviceMode(mode)); + nexus.AdvanceTime(kJoinChildTimeMsec); + + iter.Init(); + netInfo = child.Get().IterateNetInfoHistory(iter, age); + VerifyOrQuit(netInfo != nullptr); + VerifyOrQuit(netInfo->mMode.mDeviceType); + + Log("---------------------------------------------------------------------------------------"); + Log("Ping between leader and child and verify TX and RX message histories"); + + static constexpr uint16_t kPingSizes[] = {10, 100, 1000}; + for (uint16_t size : kPingSizes) + { + nexus.SendAndVerifyEchoRequest(leader, child.Get().GetMeshLocalEid(), size); + } + + // Check the TX history of the leader for the 3 Echo Requests + const HistoryTracker::MessageInfo *msgInfo; + + iter.Init(); + for (int i = 2; i >= 0; --i) + { + uint16_t size = kPingSizes[i]; + msgInfo = leader.Get().IterateTxHistory(iter, age); + VerifyOrQuit(msgInfo != nullptr); + VerifyOrQuit(msgInfo->mIcmp6Type == OT_ICMP6_TYPE_ECHO_REQUEST); + VerifyOrQuit(msgInfo->mIpProto == OT_IP6_PROTO_ICMP6); + VerifyOrQuit(msgInfo->mPayloadLength == size + sizeof(Ip6::Icmp::Header)); + VerifyOrQuit(AsCoreType(&msgInfo->mSource.mAddress) == leader.Get().GetMeshLocalEid()); + VerifyOrQuit(AsCoreType(&msgInfo->mDestination.mAddress) == child.Get().GetMeshLocalEid()); + VerifyOrQuit(msgInfo->mLinkSecurity); + VerifyOrQuit(msgInfo->mRadioIeee802154); + VerifyOrQuit(msgInfo->mPriority == OT_HISTORY_TRACKER_MSG_PRIORITY_NORMAL); + VerifyOrQuit(msgInfo->mNeighborRloc16 == child.Get().GetRloc16()); + VerifyOrQuit(msgInfo->mChecksum != 0); + VerifyOrQuit(msgInfo->mTxSuccess); + } + + // Now check the RX history of the child (should have received the 3 Echo Requests) + iter.Init(); + for (int i = 2; i >= 0; --i) + { + uint16_t size = kPingSizes[i]; + msgInfo = child.Get().IterateRxHistory(iter, age); + VerifyOrQuit(msgInfo != nullptr); + VerifyOrQuit(msgInfo->mIcmp6Type == OT_ICMP6_TYPE_ECHO_REQUEST); + VerifyOrQuit(msgInfo->mIpProto == OT_IP6_PROTO_ICMP6); + VerifyOrQuit(msgInfo->mPayloadLength == size + sizeof(Ip6::Icmp::Header)); + VerifyOrQuit(AsCoreType(&msgInfo->mSource.mAddress) == leader.Get().GetMeshLocalEid()); + VerifyOrQuit(AsCoreType(&msgInfo->mDestination.mAddress) == child.Get().GetMeshLocalEid()); + VerifyOrQuit(msgInfo->mLinkSecurity); + VerifyOrQuit(msgInfo->mRadioIeee802154); + VerifyOrQuit(msgInfo->mPriority == OT_HISTORY_TRACKER_MSG_PRIORITY_NORMAL); + VerifyOrQuit(msgInfo->mNeighborRloc16 == leader.Get().GetRloc16()); + VerifyOrQuit(msgInfo->mChecksum != 0); + } + + // The child then replied, so let's check the RX history of the leader for the 3 Echo Replies + iter.Init(); + uint8_t repliesFound = 0; + while ((msgInfo = leader.Get().IterateRxHistory(iter, age)) != nullptr) + { + if (msgInfo->mIcmp6Type == OT_ICMP6_TYPE_ECHO_REPLY) + { + VerifyOrQuit(msgInfo->mIpProto == OT_IP6_PROTO_ICMP6); + VerifyOrQuit(AsCoreType(&msgInfo->mSource.mAddress) == child.Get().GetMeshLocalEid()); + VerifyOrQuit(AsCoreType(&msgInfo->mDestination.mAddress) == leader.Get().GetMeshLocalEid()); + VerifyOrQuit(msgInfo->mLinkSecurity); + VerifyOrQuit(msgInfo->mRadioIeee802154); + VerifyOrQuit(msgInfo->mPriority == OT_HISTORY_TRACKER_MSG_PRIORITY_NORMAL); + VerifyOrQuit(msgInfo->mNeighborRloc16 == child.Get().GetRloc16()); + VerifyOrQuit(msgInfo->mChecksum != 0); + repliesFound++; + } + } + VerifyOrQuit(repliesFound == 3); +} + +} // namespace Nexus +} // namespace ot + +int main(void) +{ + ot::Nexus::TestHistoryTracker(); + printf("All tests passed\n"); + return 0; +} diff --git a/tests/scripts/thread-cert/test_history_tracker.py b/tests/scripts/thread-cert/test_history_tracker.py deleted file mode 100755 index 2424f2a0f..000000000 --- a/tests/scripts/thread-cert/test_history_tracker.py +++ /dev/null @@ -1,212 +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 os -import unittest -import sys - -import config -import thread_cert - -# Test description: -# This test verifies History Tracker behavior. -# -# Topology: -# -# LEADER -# | -# | -# CHILD -# - -LEADER = 1 -CHILD = 2 - -SHORT_WAIT = 5 -ONE_DAY = 24 * 60 * 60 -MAX_AGE_IN_DAYS = 49 - - -class TestHistoryTracker(thread_cert.TestCase): - USE_MESSAGE_FACTORY = False - SUPPORT_NCP = False - - TOPOLOGY = { - LEADER: { - 'name': 'Leader', - 'mode': 'rdn', - }, - CHILD: { - 'name': 'Child', - 'mode': 'n', - }, - } - - def test(self): - leader = self.nodes[LEADER] - child = self.nodes[CHILD] - - # Start the leader and verify that 'netinfo' history - # is updated correctly. - - leader.start() - self.simulator.go(SHORT_WAIT * 2) - self.assertEqual(leader.get_state(), 'leader') - - netinfo = leader.history_netinfo() - self.assertEqual(len(netinfo), 2) - self.assertEqual(netinfo[0]['role'], 'leader') - self.assertEqual(netinfo[0]['mode'], 'rdn') - self.assertEqual(int(netinfo[0]['rloc16'], 16), leader.get_addr16()) - self.assertEqual(netinfo[0]['partition-id'], leader.get_partition_id()) - self.assertEqual(netinfo[1]['role'], 'detached') - - # Stop the leader - - leader.thread_stop() - leader.interface_down() - self.simulator.go(SHORT_WAIT) - netinfo = leader.history_netinfo(2) - self.assertEqual(len(netinfo), 2) - self.assertEqual(netinfo[0]['role'], 'disabled') - self.assertEqual(netinfo[1]['role'], 'leader') - - # Wait for one day, two days, then up to max age and verify that - # `netinfo` entry age is updated correctly. - # - # Since we want to wait for long duration (49 days), to speed up - # the simulation time, we disable leader to avoid the need to - # to simulate all the message/events (e.g. MLE adv) while thread - # is operational. - - self.simulator.go(ONE_DAY) - netinfo = leader.history_netinfo(1) - self.assertTrue(netinfo[0]['age'].startswith('1 day')) - - self.simulator.go(ONE_DAY) - netinfo = leader.history_netinfo(1) - self.assertTrue(netinfo[0]['age'].startswith('2 days')) - - self.simulator.go((MAX_AGE_IN_DAYS - 3) * ONE_DAY) - netinfo = leader.history_netinfo(1) - self.assertTrue(netinfo[0]['age'].startswith('{} days'.format(MAX_AGE_IN_DAYS - 1))) - - self.simulator.go(ONE_DAY) - netinfo = leader.history_netinfo(1) - self.assertTrue(netinfo[0]['age'].startswith('more than {} days'.format(MAX_AGE_IN_DAYS))) - - self.simulator.go(2 * ONE_DAY) - netinfo = leader.history_netinfo(1) - self.assertTrue(netinfo[0]['age'].startswith('more than {} days'.format(MAX_AGE_IN_DAYS))) - - # Start leader and child - - leader.start() - self.simulator.go(config.LEADER_RESET_DELAY) - self.assertEqual(leader.get_state(), 'leader') - - child.start() - self.simulator.go(SHORT_WAIT) - self.assertEqual(child.get_state(), 'child') - - child_rloc16 = child.get_addr16() - leader_rloc16 = leader.get_addr16() - - # Verify the `netinfo` history on child - - netinfo = child.history_netinfo(2) - self.assertEqual(len(netinfo), 2) - self.assertEqual(netinfo[0]['role'], 'child') - self.assertEqual(netinfo[0]['mode'], 'n') - self.assertEqual(int(netinfo[0]['rloc16'], 16), child_rloc16) - self.assertEqual(netinfo[0]['partition-id'], leader.get_partition_id()) - self.assertEqual(netinfo[1]['role'], 'detached') - - # Change the child mode and verify that `netinfo` history - # records this change. - - child.set_mode('rn') - self.simulator.go(SHORT_WAIT) - netinfo = child.history_netinfo(1) - self.assertEqual(len(netinfo), 1) - self.assertEqual(netinfo[0]['mode'], 'rn') - - # Ping from leader to child and check the RX and TX history - # on child and leader. - - child_mleid = child.get_mleid() - leader_mleid = leader.get_mleid() - - ping_sizes = [10, 100, 1000] - num_msgs = len(ping_sizes) - - for size in ping_sizes: - leader.ping(child_mleid, size=size) - - leader_tx = leader.history_tx(num_msgs) - leader_rx = leader.history_rx(num_msgs) - child_tx = child.history_tx(num_msgs) - child_rx = child.history_rx(num_msgs) - - for index in range(num_msgs): - self.assertEqual(leader_tx[index]['type'], 'ICMP6(EchoReqst)') - self.assertEqual(leader_tx[index]['sec'], 'yes') - self.assertEqual(leader_tx[index]['prio'], 'norm') - self.assertEqual(leader_tx[index]['tx-success'], 'yes') - self.assertEqual(leader_tx[index]['radio'], '15.4') - self.assertEqual(int(leader_tx[index]['to'], 16), child_rloc16) - self.assertEqual(leader_tx[index]['src'][1:-3], leader_mleid) - self.assertEqual(leader_tx[index]['dst'][1:-3], child_mleid) - - self.assertEqual(child_rx[index]['type'], 'ICMP6(EchoReqst)') - self.assertEqual(child_rx[index]['sec'], 'yes') - self.assertEqual(child_rx[index]['prio'], 'norm') - self.assertEqual(child_rx[index]['radio'], '15.4') - self.assertEqual(int(child_rx[index]['from'], 16), leader_rloc16) - self.assertEqual(child_rx[index]['src'][1:-3], leader_mleid) - self.assertEqual(child_rx[index]['dst'][1:-3], child_mleid) - - self.assertEqual(leader_rx[index]['type'], 'ICMP6(EchoReply)') - self.assertEqual(child_tx[index]['type'], 'ICMP6(EchoReply)') - - self.assertEqual(leader_tx[index]['len'], child_rx[index]['len']) - self.assertEqual(leader_rx[index]['len'], child_tx[index]['len']) - - -if __name__ == '__main__': - # FIXME: We skip the test under distcheck build (the simulation - # under this build for some reason cannot seem to handle longer - # wait times - days up to 50 days in this test). We return error - # code 77 which indicates that this test case was skipped (in - # automake). - - if os.getenv('DISTCHECK_BUILD') == '1': - sys.exit(77) - - unittest.main()