diff --git a/.github/workflows/toranj.yml b/.github/workflows/toranj.yml index 698c8ec66..8ee673222 100644 --- a/.github/workflows/toranj.yml +++ b/.github/workflows/toranj.yml @@ -51,7 +51,7 @@ jobs: strategy: fail-fast: false matrix: - TORANJ_RADIO: ['15.4', 'trel', 'multi'] + TORANJ_RADIO: ['15.4'] env: COVERAGE: 1 TORANJ_RADIO : ${{ matrix.TORANJ_RADIO }} @@ -104,7 +104,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - TORANJ_RADIO: ['15.4'] + TORANJ_RADIO: ['15.4', 'trel', 'multi'] env: COVERAGE: 1 TORANJ_RADIO : ${{ matrix.TORANJ_RADIO }} diff --git a/src/cli/cli.cpp b/src/cli/cli.cpp index cbebd903b..218e86f9b 100644 --- a/src/cli/cli.cpp +++ b/src/cli/cli.cpp @@ -145,6 +145,10 @@ Interpreter::Interpreter(Instance *aInstance, otCliOutputCallback aCallback, voi #endif #endif // OPENTHREAD_FTD || OPENTHREAD_MTD { +#if (OPENTHREAD_FTD || OPENTHREAD_MTD) && OPENTHREAD_CONFIG_CLI_REGISTER_IP6_RECV_CALLBACK + otIp6SetReceiveCallback(GetInstancePtr(), &Interpreter::HandleIp6Receive, this); +#endif + OutputPrompt(); } @@ -1957,7 +1961,7 @@ template <> otError Interpreter::Process(Arg aArgs[]) */ else if (aArgs[1] == "delay") { - error = ProcessSet(aArgs + 2, otChannelManagerSetDelay); + error = ProcessGetSet(aArgs + 2, otChannelManagerGetDelay, otChannelManagerSetDelay); } /** * @cli channel manager interval @@ -7195,6 +7199,15 @@ void Interpreter::HandleDiscoveryRequest(const otThreadDiscoveryRequestInfo &aIn } #endif +#if OPENTHREAD_CONFIG_CLI_REGISTER_IP6_RECV_CALLBACK +void Interpreter::HandleIp6Receive(otMessage *aMessage, void *aContext) +{ + OT_UNUSED_VARIABLE(aContext); + + otMessageFree(aMessage); +} +#endif + #endif // OPENTHREAD_FTD || OPENTHREAD_MTD void Interpreter::Initialize(otInstance *aInstance, otCliOutputCallback aCallback, void *aContext) diff --git a/src/cli/cli.hpp b/src/cli/cli.hpp index 478c3fa68..a3aaea2e1 100644 --- a/src/cli/cli.hpp +++ b/src/cli/cli.hpp @@ -508,6 +508,10 @@ private: void HandleDiscoveryRequest(const otThreadDiscoveryRequestInfo &aInfo); #endif +#if OPENTHREAD_CONFIG_CLI_REGISTER_IP6_RECV_CALLBACK + static void HandleIp6Receive(otMessage *aMessage, void *aContext); +#endif + #endif // OPENTHREAD_FTD || OPENTHREAD_MTD void SetCommandTimeout(uint32_t aTimeoutMilli); diff --git a/src/cli/cli_config.h b/src/cli/cli_config.h index c263bfb82..15b444bb2 100644 --- a/src/cli/cli_config.h +++ b/src/cli/cli_config.h @@ -151,4 +151,17 @@ #define OPENTHREAD_CONFIG_CLI_TXT_RECORD_MAX_SIZE 512 #endif +/** + * @def OPENTHREAD_CONFIG_CLI_REGISTER_IP6_RECV_CALLBACK + * + * Define as 1 to have CLI register an IPv6 receive callback using `otIp6SetReceiveCallback()`. + * + * This is intended for testing only. Receive callback should be registered for the `otIp6GetBorderRoutingCounters()` + * to count the messages being passed to the callback. + * + */ +#ifndef OPENTHREAD_CONFIG_CLI_REGISTER_IP6_RECV_CALLBACK +#define OPENTHREAD_CONFIG_CLI_REGISTER_IP6_RECV_CALLBACK 0 +#endif + #endif // CONFIG_CLI_H_ diff --git a/tests/toranj/cli/cli.py b/tests/toranj/cli/cli.py index 85b4e3e88..c005f2dbd 100644 --- a/tests/toranj/cli/cli.py +++ b/tests/toranj/cli/cli.py @@ -46,6 +46,12 @@ import weakref JOIN_TYPE_ROUTER = 'router' JOIN_TYPE_END_DEVICE = 'ed' JOIN_TYPE_SLEEPY_END_DEVICE = 'sed' +JOIN_TYPE_REED = 'reed' + +# for use as `radios` parameter in `Node.__init__()` +RADIO_15_4 = "-15.4" +RADIO_TREL = "-trel" +RADIO_15_4_TREL = "-15.4-trel" # ---------------------------------------------------------------------------------------------------------------------- @@ -106,16 +112,17 @@ class Node(object): _all_nodes = weakref.WeakSet() - def __init__(self, verbose=_VERBOSE): + def __init__(self, radios='', index=None, verbose=_VERBOSE): """Creates a new `Node` instance""" - index = Node._cur_index - Node._cur_index += 1 + if index is None: + index = Node._cur_index + Node._cur_index += 1 self._index = index self._verbose = verbose - cmd = f'{self._OT_CLI_FTD} --time-speed={self._SPEED_UP_FACTOR} ' + cmd = f'{self._OT_CLI_FTD}{radios} --time-speed={self._SPEED_UP_FACTOR} ' if Node._SAVE_LOGS: log_file_name = self._LOG_FNAME + str(index) + '.log' @@ -261,6 +268,12 @@ class Node(object): def set_router_selection_jitter(self, jitter): self._cli_no_output('routerselectionjitter', jitter) + def get_router_eligible(self): + return self._cli_single_output('routereligible') + + def set_router_eligible(self, enable): + self._cli_no_output('routereligible', enable) + def interface_up(self): self._cli_no_output('ifconfig up') @@ -276,9 +289,18 @@ class Node(object): def thread_stop(self): self._cli_no_output('thread stop') + def get_rloc16(self): + return self._cli_single_output('rloc16') + def get_ip_addrs(self): return self.cli('ipaddr') + def add_ip_addr(self, address): + self._cli_no_output('ipaddr add', address) + + def remove_ip_addr(self, address): + self._cli_no_output('ipaddr del', address) + def get_mleid_ip_addr(self): return self._cli_single_output('ipaddr mleid') @@ -288,6 +310,132 @@ class Node(object): def get_rloc_ip_addr(self): return self._cli_single_output('ipaddr rloc') + def get_mesh_local_prefix(self): + return self._cli_single_output('prefix meshlocal') + + def get_ip_maddrs(self): + return self.cli('ipmaddr') + + def add_ip_maddr(self, maddr): + return self._cli_no_output('ipmaddr add', maddr) + + def get_pollperiod(self): + return self._cli_single_output('pollperiod') + + def set_pollperiod(self, period): + self._cli_no_output('pollperiod', period) + + def get_partition_id(self): + return self._cli_single_output('partitionid') + + def get_parent_info(self): + outputs = self.cli('parent') + result = {} + for line in outputs: + fields = line.split(':') + result[fields[0].strip()] = fields[1].strip() + return result + + def get_child_table(self): + return Node.parse_table(self.cli('child table')) + + def get_neighbor_table(self): + return Node.parse_table(self.cli('neighbor table')) + + def get_router_table(self): + return Node.parse_table(self.cli('router table')) + + def get_eidcache(self): + return self.cli('eidcache') + + #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + # netdata + + def get_netdata(self): + outputs = self.cli('netdata show') + outputs = [line.strip() for line in outputs] + routes_index = outputs.index('Routes:') + services_index = outputs.index('Services:') + result = {} + result['prefixes'] = outputs[1:routes_index] + result['routes'] = outputs[routes_index + 1:services_index] + result['services'] = outputs[services_index + 1:] + return result + + def get_netdata_prefixes(self): + return self.get_netdata()['prefixes'] + + def get_netdata_routes(self): + return self.get_netdata()['routes'] + + def get_netdata_services(self): + return self.get_netdata()['services'] + + def get_netdata_versions(self): + leaderdata = Node.parse_list(self.cli('leaderdata')) + return (int(leaderdata['Data Version']), int(leaderdata['Stable Data Version'])) + + def add_prefix(self, prefix, flags=None, prf=None): + return self._cli_no_output('prefix add', prefix, flags, prf) + + def add_route(self, prefix, flags=None, prf=None): + return self._cli_no_output('route add', prefix, flags, prf) + + def remove_prefix(self, prefix): + return self._cli_no_output('prefix remove', prefix) + + def register_netdata(self): + self._cli_no_output('netdata register') + + #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + # ping and counters + + def ping(self, address, size=0, count=1, verify_success=True): + outputs = self.cli('ping', address, size, count) + m = re.match(r'(\d+) packets transmitted, (\d+) packets received.', outputs[-1].strip()) + verify(m is not None) + verify(int(m.group(1)) == count) + if verify_success: + verify(int(m.group(2)) == count) + + def get_mle_counter(self): + return self.cli('counters mle') + + def get_br_counter_unicast_outbound_packets(self): + outputs = self.cli('counters br') + for line in outputs: + m = re.match(r'Outbound Unicast: Packets (\d+) Bytes (\d+)', line.strip()) + if m is not None: + counter = int(m.group(1)) + break + else: + verify(False) + return counter + + #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + # UDP + + def udp_open(self): + self._cli_no_output('udp open') + + def udp_close(self): + self._cli_no_output('udp close') + + def udp_bind(self, address, port): + self._cli_no_output('udp bind', address, port) + + def udp_send(self, address, port, text): + self._cli_no_output('udp send', address, port, '-t', text) + + #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + # multiradio + + def multiradio_get_radios(self): + return self._cli_single_output('multiradio') + + def multiradio_get_neighbor_list(self): + return self.cli('multiradio neighbor list') + #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # SRP client @@ -532,6 +680,7 @@ class Node(object): self.set_channel(channel) if xpanid is not None: self.set_ext_panid(xpanid) + self.set_mode('rdn') self.set_panid(panid) self.interface_up() self.thread_start() @@ -546,6 +695,9 @@ class Node(object): self.set_mode('rn') elif type == JOIN_TYPE_SLEEPY_END_DEVICE: self.set_mode('-') + elif type == JOIN_TYPE_REED: + self.set_mode('rdn') + self.set_router_eligible('disable') else: self.set_mode('rdn') self.set_router_selection_jitter(1) @@ -565,6 +717,49 @@ class Node(object): """Removes a given node (of node `Node) from the allowlist""" self._cli_no_output('macfilter addr remove', node.get_ext_addr()) + # ------------------------------------------------------------------------------------------------------------------ + # Parsing helpers + + @classmethod + def parse_table(cls, table_lines): + verify(len(table_lines) >= 2) + headers = cls.split_table_row(table_lines[0]) + info = [] + for row in table_lines[2:]: + if row.strip() == '': + continue + fields = cls.split_table_row(row) + verify(len(fields) == len(headers)) + info.append({headers[i]: fields[i] for i in range(len(fields))}) + return info + + @classmethod + def split_table_row(cls, row): + return [field.strip() for field in row.strip().split('|')[1:-1]] + + @classmethod + def parse_list(cls, list_lines): + result = {} + for line in list_lines: + fields = line.split(':', 1) + result[fields[0].strip()] = fields[1].strip() + return result + + @classmethod + def parse_multiradio_neighbor_entry(cls, line): + # Example: "ExtAddr:42aa94ad67229f14, RLOC16:0x9400, Radios:[15.4(245), TREL(255)]" + result = {} + for field in line.split(', ', 2): + key_value = field.split(':') + result[key_value[0]] = key_value[1] + radios = {} + for item in result['Radios'][1:-1].split(','): + name, prf = item.strip().split('(') + verify(prf.endswith(')')) + radios[name] = int(prf[:-1]) + result['Radios'] = radios + return result + # ------------------------------------------------------------------------------------------------------------------ # class methods diff --git a/tests/toranj/cli/test-002-form.py b/tests/toranj/cli/test-002-form.py index eea06d5f0..3bd3c1600 100755 --- a/tests/toranj/cli/test-002-form.py +++ b/tests/toranj/cli/test-002-form.py @@ -40,7 +40,7 @@ print('Starting \'{}\''.format(test_name)) # ----------------------------------------------------------------------------------------------------------------------- # Creating `Nodes` instances -speedup = 4 +speedup = 10 cli.Node.set_time_speedup_factor(speedup) node = cli.Node() @@ -48,8 +48,6 @@ node = cli.Node() # ----------------------------------------------------------------------------------------------------------------------- # Test implementation -WAIT_TIME = 5 - verify(node.get_state() == 'disabled') node.form('test') diff --git a/tests/toranj/cli/test-003-join.py b/tests/toranj/cli/test-003-join.py index f1cc05a4c..d24602a94 100755 --- a/tests/toranj/cli/test-003-join.py +++ b/tests/toranj/cli/test-003-join.py @@ -40,14 +40,12 @@ print('Starting \'{}\''.format(test_name)) # ----------------------------------------------------------------------------------------------------------------------- # Creating `cli.Nodes` instances -speedup = 4 +speedup = 10 cli.Node.set_time_speedup_factor(speedup) node1 = cli.Node() node2 = cli.Node() -WAIT_TIME = 5 - # ----------------------------------------------------------------------------------------------------------------------- # Test implementation @@ -72,6 +70,20 @@ node2.join(node1, cli.JOIN_TYPE_SLEEPY_END_DEVICE) verify(node2.get_state() == 'child') verify(node2.get_mode() == '-') +node2.interface_down() + +# Create a poor link between child and parent using MAC fixed RSSI +# filter and make sure child can still attach. + +node1.cli('macfilter rss add * -99') +node2.cli('macfilter rss add * -99') + +node2.join(node1, cli.JOIN_TYPE_END_DEVICE) +verify(node2.get_state() == 'child') +verify(node2.get_mode() == 'rn') + +verify(len(node1.get_child_table()) == 1) + # ----------------------------------------------------------------------------------------------------------------------- # Test finished diff --git a/tests/toranj/cli/test-004-scan.py b/tests/toranj/cli/test-004-scan.py new file mode 100755 index 000000000..35590db7f --- /dev/null +++ b/tests/toranj/cli/test-004-scan.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2022, 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. + +from cli import verify +from cli import verify_within +import cli + +# ----------------------------------------------------------------------------------------------------------------------- +# Test description: MAC scan operation + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print('-' * 120) +print('Starting \'{}\''.format(test_name)) + + +# ----------------------------------------------------------------------------------------------------------------------- +def verify_scan_result_conatins_nodes(scan_result, nodes): + table = cli.Node.parse_table(scan_result) + verify(len(table) >= len(nodes)) + for node in nodes: + ext_addr = node.get_ext_addr() + for entry in table: + if entry['MAC Address'] == ext_addr: + verify(int(entry['PAN'], 16) == int(node.get_panid(), 16)) + verify(int(entry['Ch']) == int(node.get_channel())) + break + else: + verify(False) + + +# ----------------------------------------------------------------------------------------------------------------------- +# Creating `cli.Nodes` instances + +speedup = 10 +cli.Node.set_time_speedup_factor(speedup) + +node1 = cli.Node() +node2 = cli.Node() +node3 = cli.Node() +node4 = cli.Node() +node5 = cli.Node() +scanner = cli.Node() + +nodes = [node1, node2, node3, node4, node5] + +# ----------------------------------------------------------------------------------------------------------------------- +# Test implementation + +node1.form('net1', panid=0x1111, channel=12) +node2.form('net2', panid=0x2222, channel=13) +node3.form('net3', panid=0x3333, channel=14) +node4.form('net4', panid=0x4444, channel=15) +node5.form('net5', panid=0x5555, channel=16) + +# MAC scan + +# Scan on all channels, should see all nodes +verify_scan_result_conatins_nodes(scanner.cli('scan'), nodes) + +# Scan on channel 12 only, should only see node1 +verify_scan_result_conatins_nodes(scanner.cli('scan 12'), [node1]) + +# Scan on channel 20 only, should see no result +verify_scan_result_conatins_nodes(scanner.cli('scan 20'), []) + +# MLE Discover scan + +scanner.interface_up() + +verify_scan_result_conatins_nodes(scanner.cli('discover'), nodes) +verify_scan_result_conatins_nodes(scanner.cli('scan 12'), [node1]) +verify_scan_result_conatins_nodes(scanner.cli('scan 20'), []) + +scanner.form('scanner') +verify_scan_result_conatins_nodes(scanner.cli('discover'), nodes) + +# ----------------------------------------------------------------------------------------------------------------------- +# Test finished + +cli.Node.finalize_all_nodes() + +print('\'{}\' passed.'.format(test_name)) diff --git a/tests/toranj/cli/test-005-traffic-router-to-child.py b/tests/toranj/cli/test-005-traffic-router-to-child.py new file mode 100755 index 000000000..ac0044479 --- /dev/null +++ b/tests/toranj/cli/test-005-traffic-router-to-child.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2022, 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. + +from cli import verify +from cli import verify_within +import cli + +# ----------------------------------------------------------------------------------------------------------------------- +# Test description: +# +# Traffic between router to end-device (link-local and mesh-local IPv6 addresses). + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print('-' * 120) +print('Starting \'{}\''.format(test_name)) + +# ----------------------------------------------------------------------------------------------------------------------- +# Creating `cli.Nodes` instances + +speedup = 10 +cli.Node.set_time_speedup_factor(speedup) + +node1 = cli.Node() +node2 = cli.Node() +node3 = cli.Node() + +# ----------------------------------------------------------------------------------------------------------------------- +# Test implementation + +node1.form('net') +node2.join(node1, cli.JOIN_TYPE_END_DEVICE) +node3.join(node1, cli.JOIN_TYPE_SLEEPY_END_DEVICE) + +verify(node1.get_state() == 'leader') +verify(node2.get_state() == 'child') +verify(node3.get_state() == 'child') + +ll1 = node1.get_linklocal_ip_addr() +ll2 = node2.get_linklocal_ip_addr() +ll3 = node2.get_linklocal_ip_addr() + +ml1 = node1.get_mleid_ip_addr() +ml2 = node2.get_mleid_ip_addr() +ml3 = node2.get_mleid_ip_addr() + +sizes = [0, 80, 500, 1000] +count = 2 + +for size in sizes: + node1.ping(ll2, size=size, count=count) + node2.ping(ll1, size=size, count=count) + node1.ping(ml2, size=size, count=count) + node2.ping(ml1, size=size, count=count) + +poll_periods = [10, 100, 300] + +for period in poll_periods: + node3.set_pollperiod(period) + verify(int(node3.get_pollperiod()) == period) + + for size in sizes: + node1.ping(ll3, size=size, count=count) + node3.ping(ll1, size=size, count=count) + node1.ping(ml3, size=size, count=count) + node3.ping(ml1, size=size, count=count) + +# ----------------------------------------------------------------------------------------------------------------------- +# Test finished + +cli.Node.finalize_all_nodes() + +print('\'{}\' passed.'.format(test_name)) diff --git a/tests/toranj/cli/test-006-traffic-multi-hop.py b/tests/toranj/cli/test-006-traffic-multi-hop.py new file mode 100755 index 000000000..0f6f48c71 --- /dev/null +++ b/tests/toranj/cli/test-006-traffic-multi-hop.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2022, 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. + +from cli import verify +from cli import verify_within +import cli +import time + +# ----------------------------------------------------------------------------------------------------------------------- +# Test description: +# +# Traffic over multi-hop in a network with chain topology +# +# r1 ----- r2 ---- r3 ----- r4 +# /\ /\ +# / \ / \ +# fed1 sed1 sed4 fed4 +# +# +# Traffic flow: +# - From first router to last router +# - From SED child of last router to SED child of first router +# - From FED child of first router to FED child of last router +# +# The test verifies the following: +# - Verifies Address Query process to routers and FEDs. +# - Verifies Mesh Header frame forwarding over multiple routers. +# - Verifies forwarding of large IPv6 messages (1000 bytes) requiring lowpan fragmentation. + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print('-' * 120) +print('Starting \'{}\''.format(test_name)) + +# ----------------------------------------------------------------------------------------------------------------------- +# Creating `cli.Nodes` instances + +speedup = 10 +cli.Node.set_time_speedup_factor(speedup) + +r1 = cli.Node() +r2 = cli.Node() +r3 = cli.Node() +r4 = cli.Node() +fed1 = cli.Node() +sed1 = cli.Node() +fed4 = cli.Node() +sed4 = cli.Node() + +# ----------------------------------------------------------------------------------------------------------------------- +# Form topology + +r1.allowlist_node(r2) +r1.allowlist_node(fed1) +r1.allowlist_node(sed1) + +r2.allowlist_node(r1) +r2.allowlist_node(r3) + +r3.allowlist_node(r2) +r3.allowlist_node(r4) + +r4.allowlist_node(r3) +r4.allowlist_node(sed4) +r4.allowlist_node(fed4) + +fed1.allowlist_node(r1) +sed1.allowlist_node(r1) + +fed4.allowlist_node(r4) +sed4.allowlist_node(r4) + +r1.form("pan") +r2.join(r1) +r3.join(r2) +r4.join(r3) +fed1.join(r1, cli.JOIN_TYPE_REED) +sed1.join(r1, cli.JOIN_TYPE_SLEEPY_END_DEVICE) +fed4.join(r4, cli.JOIN_TYPE_REED) +sed4.join(r4, cli.JOIN_TYPE_SLEEPY_END_DEVICE) + +verify(r1.get_state() == 'leader') +verify(r2.get_state() == 'router') +verify(r3.get_state() == 'router') +verify(r4.get_state() == 'router') +verify(fed1.get_state() == 'child') +verify(fed4.get_state() == 'child') +verify(sed1.get_state() == 'child') +verify(sed4.get_state() == 'child') + +sed1.set_pollperiod(200) +sed4.set_pollperiod(200) +verify(int(sed1.get_pollperiod()) == 200) +verify(int(sed4.get_pollperiod()) == 200) + +# ----------------------------------------------------------------------------------------------------------------------- +# Test Implementation + +# Wait till first router has either established a link or +# has a valid "next hop" towards all other routers. + +r1_rloc16 = int(r1.get_rloc16(), 16) + + +def check_r1_router_table(): + table = r1.get_router_table() + verify(len(table) == 4) + for entry in table: + verify(int(entry['RLOC16'], 0) == r1_rloc16 or int(entry['Link']) == 1 or int(entry['Next Hop']) != 63) + + +verify_within(check_r1_router_table, 160) + +sizes = [0, 80, 500, 1000] + +# Traffic from the first router in the chain to the last one. + +r4_mleid = r4.get_mleid_ip_addr() + +for size in sizes: + r1.ping(r4_mleid, size=size) + +# Send from the SED child of the last router to the SED child of the first +# router. + +sed1_mleid = sed1.get_mleid_ip_addr() + +for size in sizes: + sed4.ping(sed1_mleid, size=size) + +# Send from the FED child of the first router to the FED child of the last +# router. + +fed4_mleid = fed4.get_mleid_ip_addr() + +for size in sizes: + fed1.ping(fed4_mleid, size=size) + +# ----------------------------------------------------------------------------------------------------------------------- +# Test finished + +cli.Node.finalize_all_nodes() + +print('\'{}\' passed.'.format(test_name)) diff --git a/tests/toranj/cli/test-007-off-mesh-route-traffic.py b/tests/toranj/cli/test-007-off-mesh-route-traffic.py new file mode 100755 index 000000000..bd7238785 --- /dev/null +++ b/tests/toranj/cli/test-007-off-mesh-route-traffic.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2022, 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. + +from cli import verify +from cli import verify_within +import cli +import time + +# ----------------------------------------------------------------------------------------------------------------------- +# Test description: Adding off-mesh routes (on routers and FEDs) and traffic flow to off-mesh addresses. +# +# Test topology: +# +# r1---- r2 ---- r3 ---- r4 +# | | +# | | +# fed1 med3 +# +# The off-mesh-routes are added as follows: +# - `r1` adds fd00:1:1::/48 high +# - `r2` adds fd00:1:1:2::64 med +# - `r3` adds fd00:3::/64 med & fed00:4::/32 med +# - 'r4' adds fd00:1:1::/48 low +# - `fed1` adds fd00:4::/32 med +# + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print('-' * 120) +print('Starting \'{}\''.format(test_name)) + +# ----------------------------------------------------------------------------------------------------------------------- +# Creating `cli.Nodes` instances + +speedup = 10 +cli.Node.set_time_speedup_factor(speedup) + +r1 = cli.Node() +r2 = cli.Node() +r3 = cli.Node() +r4 = cli.Node() +fed1 = cli.Node() +med3 = cli.Node() + +nodes = [r1, r2, r3, r4, fed1, med3] + +# ----------------------------------------------------------------------------------------------------------------------- +# Form topology + +r1.allowlist_node(r2) +r1.allowlist_node(fed1) + +r2.allowlist_node(r1) +r2.allowlist_node(r3) + +r3.allowlist_node(r2) +r3.allowlist_node(r4) +r3.allowlist_node(med3) + +r4.allowlist_node(r3) + +fed1.allowlist_node(r1) +med3.allowlist_node(r3) + +r1.form("off-mesh") +r2.join(r1) +r3.join(r2) +r4.join(r3) +fed1.join(r1, cli.JOIN_TYPE_REED) +med3.join(r3, cli.JOIN_TYPE_END_DEVICE) + +verify(r1.get_state() == 'leader') +verify(r2.get_state() == 'router') +verify(r3.get_state() == 'router') +verify(r4.get_state() == 'router') +verify(fed1.get_state() == 'child') +verify(med3.get_state() == 'child') + +# ----------------------------------------------------------------------------------------------------------------------- +# Test Implementation + +# Wait till first router has either established a link or +# has a valid "next hop" towards all other routers. + +r1_rloc16 = int(r1.get_rloc16(), 16) + + +def check_r1_router_table(): + table = r1.get_router_table() + verify(len(table) == 4) + for entry in table: + verify(int(entry['RLOC16'], 0) == r1_rloc16 or int(entry['Link']) == 1 or int(entry['Next Hop']) != 63) + + +verify_within(check_r1_router_table, 120) + +# Add an on-mesh prefix on r1 and different off-mesh routes +# on different nodes and make sure they are seen on all nodes + +r1.add_prefix('fd00:abba::/64', 'paros', 'med') +r1.add_route('fd00:1:1::/48', 's', 'high') +r1.register_netdata() + +r2.add_route('fd00:1:1:2::/64', 's', 'med') +r2.register_netdata() + +r3.add_route('fd00:3::/64', 's', 'med') +r3.add_route('fd00:4::/32', 'med') +r3.register_netdata() + +r4.add_route('fd00:1:1::/48', 's', 'med') + +fed1.add_route('fd00:4::/32', 'med') +fed1.register_netdata() + + +def check_netdata_on_all_nodes(): + for node in nodes: + netdata = node.get_netdata() + verify(len(netdata['prefixes']) == 1) + verify(len(netdata['routes']) == 6) + + +verify_within(check_netdata_on_all_nodes, 5) + +# Send from `med3` to an address matching `fd00:1:1:2::/64` added +# by`r2` and verify that it is received on `r2` and not `r1`, since +# it is better prefix match with `fd00:1:1:2/64` on r2. + +r1_counter = r1.get_br_counter_unicast_outbound_packets() +r2_counter = r2.get_br_counter_unicast_outbound_packets() + +med3.ping('fd00:1:1:2::1', verify_success=False) + +verify(r1_counter == r1.get_br_counter_unicast_outbound_packets()) +verify(r2_counter + 1 == r2.get_br_counter_unicast_outbound_packets()) + +# Send from`r3` to an address matching `fd00:1:1::/48` which is +# added by both `r1` and `r4` and verify that it is received on +# `r1` since it adds it with higher preference. + +r1_counter = r1.get_br_counter_unicast_outbound_packets() +r4_counter = r4.get_br_counter_unicast_outbound_packets() + +r3.ping('fd00:1:1::2', count=3, verify_success=False) + +verify(r1_counter + 3 == r1.get_br_counter_unicast_outbound_packets()) +verify(r4_counter == r4.get_br_counter_unicast_outbound_packets()) + +# TRy the same address from `r4` itself, it should again end up +# going to `r1` due to it adding it at high preference. + +r1_counter = r1.get_br_counter_unicast_outbound_packets() +r4_counter = r4.get_br_counter_unicast_outbound_packets() + +r4.ping('fd00:1:1::2', count=2, verify_success=False) + +verify(r1_counter + 2 == r1.get_br_counter_unicast_outbound_packets()) +verify(r4_counter == r4.get_br_counter_unicast_outbound_packets()) + +# Send from `fed1` to an address matching `fd00::3::/64` (from `r3`) +# and verify that it is received on `r3`. + +r3_counter = r3.get_br_counter_unicast_outbound_packets() + +fed1.ping('fd00:3::3', count=2, verify_success=False) + +verify(r3_counter + 2 == r3.get_br_counter_unicast_outbound_packets()) + +# Send from `r1` to an address matching `fd00::4::/32` which is added +# by both `fed1` and `r3` with same preference. Verify that it is +# received on `fed1` since it is closer to `r1` and we would have a +# smaller path cost from `r1` to `fed1`. + +fed1_counter = fed1.get_br_counter_unicast_outbound_packets() +r3_counter = r3.get_br_counter_unicast_outbound_packets() + +r1.ping('fd00:4::4', count=1, verify_success=False) + +verify(fed1_counter + 1 == fed1.get_br_counter_unicast_outbound_packets()) +verify(r3_counter == r3.get_br_counter_unicast_outbound_packets()) + +# Try the same, but now send from `fed1` and make sure it selects +# itself as destination. + +fed1_counter = fed1.get_br_counter_unicast_outbound_packets() +r3_counter = r3.get_br_counter_unicast_outbound_packets() + +fed1.ping('fd00:4::5', count=2, verify_success=False) + +verify(fed1_counter + 2 == fed1.get_br_counter_unicast_outbound_packets()) +verify(r3_counter == r3.get_br_counter_unicast_outbound_packets()) + +# Try the same but now send from `r2`. Now the `r3` would be closer +# and should be selected as destination. + +fed1_counter = fed1.get_br_counter_unicast_outbound_packets() +r3_counter = r3.get_br_counter_unicast_outbound_packets() + +r2.ping('fd00:4::6', count=3, verify_success=False) + +verify(fed1_counter == fed1.get_br_counter_unicast_outbound_packets()) +verify(r3_counter + 3 == r3.get_br_counter_unicast_outbound_packets()) + +# Again try same but send from `med1` and make sure its parent +# `r3` receives. + +fed1_counter = fed1.get_br_counter_unicast_outbound_packets() +r3_counter = r3.get_br_counter_unicast_outbound_packets() + +med3.ping('fd00:4::7', count=1, verify_success=False) + +verify(fed1_counter == fed1.get_br_counter_unicast_outbound_packets()) +verify(r3_counter + 1 == r3.get_br_counter_unicast_outbound_packets()) + +# ----------------------------------------------------------------------------------------------------------------------- +# Test finished + +cli.Node.finalize_all_nodes() + +print('\'{}\' passed.'.format(test_name)) diff --git a/tests/toranj/cli/test-008-multicast-traffic.py b/tests/toranj/cli/test-008-multicast-traffic.py new file mode 100755 index 000000000..01c8cd2d6 --- /dev/null +++ b/tests/toranj/cli/test-008-multicast-traffic.py @@ -0,0 +1,247 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2022, 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. + +from cli import verify +from cli import verify_within +import cli +import time + +# ----------------------------------------------------------------------------------------------------------------------- +# Test description: Multicast traffic +# +# Network topology +# +# r1 ---- r2 ---- r3 ---- r4 +# | | +# | | +# fed sed +# +# Test covers the following multicast traffic: +# +# - r2 =>> link-local all-nodes. Expected response from [r1, r3, fed]. +# - r3 =>> mesh-local all-nodes. Expected response from [r1, r2, r4, fed]. +# - r3 =>> link-local all-routers. Expected response from [r2, r4]. +# - r3 =>> mesh-local all-routers. Expected response from all routers. +# - r1 =>> link-local all-thread. Expected response from [r1, r2]. +# - fed =>> mesh-local all-thread. Expected response from all nodes. +# - r1 =>> mesh-local all-thread (one hop). Expected response from [r2]. +# - r1 =>> mesh-local all-thread (two hops). Expected response from [r2, r3, fed]. +# - r1 =>> mesh-local all-thread (three hops). Expected response from [r2, r3, r4, fed]. +# - r1 =>> mesh-local all-thread (four hops). Expected response from [r2, r3, r4, fed, sed]. +# - r1 =>> specific address (on r2 and sed). Expected to receive on [r2, sed]. +# + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print('-' * 120) +print('Starting \'{}\''.format(test_name)) + +# ----------------------------------------------------------------------------------------------------------------------- +# Creating `cli.Nodes` instances + +speedup = 10 +cli.Node.set_time_speedup_factor(speedup) + +r1 = cli.Node() +r2 = cli.Node() +r3 = cli.Node() +r4 = cli.Node() +fed = cli.Node() +sed = cli.Node() + +# ----------------------------------------------------------------------------------------------------------------------- +# Form topology + +r1.allowlist_node(r2) + +r2.allowlist_node(r1) +r2.allowlist_node(fed) +r2.allowlist_node(r3) + +fed.allowlist_node(r2) + +r3.allowlist_node(r2) +r3.allowlist_node(r4) + +r4.allowlist_node(r3) +r4.allowlist_node(sed) + +sed.allowlist_node(r4) + +r1.form("multicast") +r2.join(r1) +r3.join(r2) +r4.join(r3) +fed.join(r1, cli.JOIN_TYPE_REED) +sed.join(r3, cli.JOIN_TYPE_SLEEPY_END_DEVICE) + +sed.set_pollperiod(600) + +verify(r1.get_state() == 'leader') +verify(r2.get_state() == 'router') +verify(r3.get_state() == 'router') +verify(r4.get_state() == 'router') +verify(fed.get_state() == 'child') +verify(sed.get_state() == 'child') + +# ----------------------------------------------------------------------------------------------------------------------- +# Test Implementation + +# Wait till first router has either established a link or +# has a valid "next hop" towards all other routers. + +r1_rloc16 = int(r1.get_rloc16(), 16) + + +def check_r1_router_table(): + table = r1.get_router_table() + verify(len(table) == 4) + for entry in table: + verify(int(entry['RLOC16'], 0) == r1_rloc16 or int(entry['Link']) == 1 or int(entry['Next Hop']) != 63) + + +verify_within(check_r1_router_table, 160) + +# r2 =>> link-local all-nodes. Expected response from [r1, r3, fed]. + +outputs = r2.cli('ping ff02::1') +verify(len(outputs) == 4) +for node in [r1, r3, fed]: + ll_addr = node.get_linklocal_ip_addr() + verify(any(ll_addr in line for line in outputs)) + +# r3 =>> mesh-local all-nodes. Expected response from [r1, r2, r4, fed]. + +outputs = r3.cli('ping ff03::1') +verify(len(outputs) == 5) +for node in [r1, r2, r4, fed]: + ml_addr = node.get_mleid_ip_addr() + verify(any(ml_addr in line for line in outputs)) + +# r3 =>> link-local all-routers. Expected response from [r2, r4]. + +outputs = r3.cli('ping ff02::2') +verify(len(outputs) == 3) +for node in [r2, r4]: + ll_addr = node.get_linklocal_ip_addr() + verify(any(ll_addr in line for line in outputs)) + +# r3 =>> mesh-local all-routers. Expected response from all routers. + +outputs = r3.cli('ping ff03::2') +verify(len(outputs) == 5) +for node in [r1, r2, r4, fed]: + ml_addr = node.get_mleid_ip_addr() + verify(any(ml_addr in line for line in outputs)) + +# r1 =>> link-local all-thread. Expected response from [r2]. + +ml_prefix = r1.get_mesh_local_prefix().strip().split('/')[0] +ll_all_thread_nodes_addr = 'ff32:40:' + ml_prefix + '1' +outputs = r1.cli('ping', ll_all_thread_nodes_addr) +verify(len(outputs) == 2) +for node in [r2]: + ll_addr = node.get_linklocal_ip_addr() + verify(any(ll_addr in line for line in outputs)) + +# fed =>> mesh-local all-thread. Expected response from all nodes. + +ml_all_thread_nodes_addr = 'ff33:40:' + ml_prefix + '1' +outputs = fed.cli('ping', ml_all_thread_nodes_addr) +verify(len(outputs) == 6) +for node in [r1, r2, r3, r4, sed]: + ml_addr = node.get_mleid_ip_addr() + verify(any(ml_addr in line for line in outputs)) + +# Repeat the same with larger ping msg requiring fragmentation + +outputs = fed.cli('ping', ml_all_thread_nodes_addr, 200) +verify(len(outputs) == 6) +for node in [r1, r2, r3, r4, sed]: + ml_addr = node.get_mleid_ip_addr() + verify(any(ml_addr in line for line in outputs)) + +# r1 =>> mesh-local all-thread (one hop). Expected response from [r2]. + +hop_limit = 1 +outputs = r1.cli('ping', ml_all_thread_nodes_addr, 10, 1, 0, hop_limit) +verify(len(outputs) == 2) +for node in [r2]: + ml_addr = node.get_mleid_ip_addr() + verify(any(ml_addr in line for line in outputs)) + +# r1 =>> mesh-local all-thread (two hops). Expected response from [r2, r3, fed]. + +hop_limit = 2 +outputs = r1.cli('ping', ml_all_thread_nodes_addr, 10, 1, 0, hop_limit) +verify(len(outputs) == 4) +for node in [r2, r3, fed]: + ml_addr = node.get_mleid_ip_addr() + verify(any(ml_addr in line for line in outputs)) + +# r1 =>> mesh-local all-thread (three hops). Expected response from [r2, r3, r4, fed]. + +hop_limit = 3 +outputs = r1.cli('ping', ml_all_thread_nodes_addr, 10, 1, 0, hop_limit) +verify(len(outputs) == 5) +for node in [r2, r3, r4, fed]: + ml_addr = node.get_mleid_ip_addr() + verify(any(ml_addr in line for line in outputs)) + +# r1 =>> mesh-local all-thread (four hops). Expected response from [r2, r3, r4, fed, sed]. + +hop_limit = 4 +outputs = r1.cli('ping', ml_all_thread_nodes_addr, 10, 1, 0, hop_limit) +verify(len(outputs) == 6) +for node in [r2, r3, r4, fed, sed]: + ml_addr = node.get_mleid_ip_addr() + verify(any(ml_addr in line for line in outputs)) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Subscribe to a specific multicast address on r2 and sed + +mcast_addr = 'ff03:0:0:0:0:0:0:114' +r2.add_ip_maddr(mcast_addr) +sed.add_ip_maddr(mcast_addr) +time.sleep(1) +maddrs = r2.get_ip_maddrs() +verify(any(mcast_addr in maddr for maddr in maddrs)) +maddrs = sed.get_ip_maddrs() +verify(any(mcast_addr in maddr for maddr in maddrs)) + +outputs = r1.cli('ping', mcast_addr) +verify(len(outputs) == 3) +for node in [r2, sed]: + ml_addr = node.get_mleid_ip_addr() + verify(any(ml_addr in line for line in outputs)) + +# ----------------------------------------------------------------------------------------------------------------------- +# Test finished + +cli.Node.finalize_all_nodes() + +print('\'{}\' passed.'.format(test_name)) diff --git a/tests/toranj/cli/test-009-router-table.py b/tests/toranj/cli/test-009-router-table.py new file mode 100755 index 000000000..67159f87e --- /dev/null +++ b/tests/toranj/cli/test-009-router-table.py @@ -0,0 +1,251 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2022, 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. + +from cli import verify +from cli import verify_within +import cli +import time + +# ----------------------------------------------------------------------------------------------------------------------- +# Test description: +# +# Verify router table entries. +# +# r1 ------ r2 ---- r6 +# \ / +# \ / +# \ / +# r3 ------ r4 ----- r5 +# +# + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print('-' * 120) +print('Starting \'{}\''.format(test_name)) + +# ----------------------------------------------------------------------------------------------------------------------- +# Creating `cli.Nodes` instances + +speedup = 10 +cli.Node.set_time_speedup_factor(speedup) + +r1 = cli.Node() +r2 = cli.Node() +r3 = cli.Node() +r4 = cli.Node() +r5 = cli.Node() +r6 = cli.Node() + +# ----------------------------------------------------------------------------------------------------------------------- +# Form topology + +r1.allowlist_node(r2) +r1.allowlist_node(r3) + +r2.allowlist_node(r1) +r2.allowlist_node(r3) +r2.allowlist_node(r6) + +r3.allowlist_node(r1) +r3.allowlist_node(r2) +r3.allowlist_node(r4) + +r4.allowlist_node(r3) +r4.allowlist_node(r5) + +r5.allowlist_node(r4) + +r6.allowlist_node(r2) + +r1.form("topo") +for node in [r2, r3, r4, r5, r6]: + node.join(r1) + +verify(r1.get_state() == 'leader') +for node in [r2, r3, r4, r5, r6]: + verify(node.get_state() == 'router') + +# ----------------------------------------------------------------------------------------------------------------------- +# Test Implementation + +r1_rloc16 = int(r1.get_rloc16(), 16) +r2_rloc16 = int(r2.get_rloc16(), 16) +r3_rloc16 = int(r3.get_rloc16(), 16) +r4_rloc16 = int(r4.get_rloc16(), 16) +r5_rloc16 = int(r5.get_rloc16(), 16) +r6_rloc16 = int(r6.get_rloc16(), 16) + +r1_rid = r1_rloc16 / 1024 +r2_rid = r2_rloc16 / 1024 +r3_rid = r3_rloc16 / 1024 +r4_rid = r4_rloc16 / 1024 +r5_rid = r5_rloc16 / 1024 +r6_rid = r6_rloc16 / 1024 + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + +def check_r1_router_table(): + table = r1.get_router_table() + verify(len(table) == 6) + for entry in table: + rloc16 = int(entry['RLOC16'], 0) + link = int(entry['Link']) + nexthop = int(entry['Next Hop']) + cost = int(entry['Path Cost']) + if rloc16 == r1_rloc16: + verify(link == 0) + elif rloc16 == r2_rloc16: + verify(link == 1) + verify(nexthop == r3_rid) + elif rloc16 == r3_rloc16: + verify(link == 1) + verify(nexthop == r2_rid) + elif rloc16 == r4_rloc16: + verify(link == 0) + verify(nexthop == r3_rid) + verify(cost == 1) + elif rloc16 == r5_rloc16: + verify(link == 0) + verify(nexthop == r3_rid) + verify(cost == 2) + elif rloc16 == r6_rloc16: + verify(link == 0) + verify(nexthop == r2_rid) + verify(cost == 1) + + +verify_within(check_r1_router_table, 160) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + +def check_r2_router_table(): + table = r2.get_router_table() + verify(len(table) == 6) + for entry in table: + rloc16 = int(entry['RLOC16'], 0) + link = int(entry['Link']) + nexthop = int(entry['Next Hop']) + cost = int(entry['Path Cost']) + if rloc16 == r1_rloc16: + verify(link == 1) + verify(nexthop == r3_rid) + elif rloc16 == r2_rloc16: + verify(link == 0) + elif rloc16 == r3_rloc16: + verify(link == 1) + verify(nexthop == r1_rid) + elif rloc16 == r4_rloc16: + verify(link == 0) + verify(nexthop == r3_rid) + verify(cost == 1) + elif rloc16 == r5_rloc16: + verify(link == 0) + verify(nexthop == r3_rid) + verify(cost == 2) + elif rloc16 == r6_rloc16: + verify(link == 1) + + +verify_within(check_r2_router_table, 160) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + +def check_r4_router_table(): + table = r4.get_router_table() + verify(len(table) == 6) + for entry in table: + rloc16 = int(entry['RLOC16'], 0) + link = int(entry['Link']) + nexthop = int(entry['Next Hop']) + cost = int(entry['Path Cost']) + if rloc16 == r1_rloc16: + verify(link == 0) + verify(nexthop == r3_rid) + verify(cost == 1) + elif rloc16 == r2_rloc16: + verify(link == 0) + verify(nexthop == r3_rid) + verify(cost == 1) + elif rloc16 == r3_rloc16: + verify(link == 1) + elif rloc16 == r4_rloc16: + verify(link == 0) + elif rloc16 == r5_rloc16: + verify(link == 1) + elif rloc16 == r6_rloc16: + verify(link == 0) + verify(nexthop == r3_rid) + verify(cost == 2) + + +verify_within(check_r4_router_table, 160) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + +def check_r5_router_table(): + table = r5.get_router_table() + verify(len(table) == 6) + for entry in table: + rloc16 = int(entry['RLOC16'], 0) + link = int(entry['Link']) + nexthop = int(entry['Next Hop']) + cost = int(entry['Path Cost']) + if rloc16 == r1_rloc16: + verify(link == 0) + verify(nexthop == r4_rid) + verify(cost == 2) + elif rloc16 == r2_rloc16: + verify(link == 0) + verify(nexthop == r4_rid) + verify(cost == 2) + elif rloc16 == r3_rloc16: + verify(link == 0) + verify(nexthop == r4_rid) + verify(cost == 1) + elif rloc16 == r4_rloc16: + verify(link == 1) + elif rloc16 == r5_rloc16: + verify(link == 0) + elif rloc16 == r6_rloc16: + verify(link == 0) + verify(nexthop == r4_rid) + verify(cost == 3) + + +verify_within(check_r5_router_table, 160) + +# ----------------------------------------------------------------------------------------------------------------------- +# Test finished + +cli.Node.finalize_all_nodes() + +print('\'{}\' passed.'.format(test_name)) diff --git a/tests/toranj/cli/test-010-partition-merge.py b/tests/toranj/cli/test-010-partition-merge.py new file mode 100755 index 000000000..415373701 --- /dev/null +++ b/tests/toranj/cli/test-010-partition-merge.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2022, 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. + +from cli import verify +from cli import verify_within +import cli +import time + +# ----------------------------------------------------------------------------------------------------------------------- +# Test description: Partition formation and merge +# +# Network Topology: +# +# r1 ---- / ---- r2 +# | \ | +# | / | +# c1 \ c2 +# +# +# Test covers the following situations: +# +# - r2 forming its own partition when it can no longer hear r1 +# - Partitions merging into one once r1 and r2 can talk again +# - Adding on-mesh prefixes on each partition and ensuring after +# merge the info in combined. +# + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print('-' * 120) +print('Starting \'{}\''.format(test_name)) + +# ----------------------------------------------------------------------------------------------------------------------- +# Creating `cli.Nodes` instances + +speedup = 25 +cli.Node.set_time_speedup_factor(speedup) + +r1 = cli.Node() +r2 = cli.Node() +c1 = cli.Node() +c2 = cli.Node() + +# ----------------------------------------------------------------------------------------------------------------------- +# Form topology + +r1.allowlist_node(r2) +r1.allowlist_node(c1) + +r2.allowlist_node(r1) +r2.allowlist_node(c2) + +r1.form("partmrge") +r2.join(r1) +c1.join(r1, cli.JOIN_TYPE_END_DEVICE) +c2.join(r2, cli.JOIN_TYPE_END_DEVICE) + +verify(r1.get_state() == 'leader') +verify(r2.get_state() == 'router') +verify(c1.get_state() == 'child') +verify(c2.get_state() == 'child') + +nodes = [r1, r2, c1, c2] + +# ----------------------------------------------------------------------------------------------------------------------- +# Test Implementation + +# Force the two routers to form their own partition +# by removing them from each other's allowlist table + +r1.un_allowlist_node(r2) +r2.un_allowlist_node(r1) + +# Add a prefix before r2 realizes it can no longer talk +# to leader (r1). + +r2.add_prefix('fd00:abba::/64', 'paros', 'med') +r2.register_netdata() + +# Check that r2 forms its own partition + + +def check_r2_become_leader(): + verify(r2.get_state() == 'leader') + + +verify_within(check_r2_become_leader, 20) + +# While we have two partition, add a prefix on r1 +r1.add_prefix('fd00:1234::/64', 'paros', 'med') +r1.register_netdata() + +# Update allowlist and wait for partitions to merge. +r1.allowlist_node(r2) +r2.allowlist_node(r1) + + +def check_partition_id_match(): + verify(r1.get_partition_id() == r2.get_partition_id()) + + +verify_within(check_partition_id_match, 20) + +# Check that partitions merged successfully + + +def check_r1_r2_roles(): + verify(r1.get_state() in ['leader', 'router']) + verify(r2.get_state() in ['leader', 'router']) + + +verify_within(check_r1_r2_roles, 10) + +# Verify all nodes see both prefixes + + +def check_netdata_on_all_nodes(): + for node in nodes: + netdata = node.get_netdata() + verify(len(netdata['prefixes']) == 2) + + +verify_within(check_netdata_on_all_nodes, 10) + +# ----------------------------------------------------------------------------------------------------------------------- +# Test finished + +cli.Node.finalize_all_nodes() + +print('\'{}\' passed.'.format(test_name)) diff --git a/tests/toranj/cli/test-011-network-data-timeout.py b/tests/toranj/cli/test-011-network-data-timeout.py new file mode 100755 index 000000000..a3a4086a7 --- /dev/null +++ b/tests/toranj/cli/test-011-network-data-timeout.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2022, 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. + +from cli import verify +from cli import verify_within +import cli +import time + +# ----------------------------------------------------------------------------------------------------------------------- +# Test description: Network Data (on-mesh prefix) timeout and entry removal +# +# Network topology +# +# r1 ----- r2 +# | +# | +# c2 (sleepy) +# +# +# Test covers the following steps: +# +# - Every node adds a unique on-mesh prefix. +# - Every node also adds a common on-mesh prefix (with different flags). +# - Verify that all the unique and common prefixes are present on all nodes are associated with correct RLOC16. +# - Remove `r2` from network (which removes `c2` as well) from Thread partition created by `r1`. +# - Verify that all on-mesh prefixes added by `r2` or `c2` (unique and common) are removed on `r1`. +# + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print('-' * 120) +print('Starting \'{}\''.format(test_name)) + +# ----------------------------------------------------------------------------------------------------------------------- +# Creating `cli.Nodes` instances + +speedup = 25 +cli.Node.set_time_speedup_factor(speedup) + +r1 = cli.Node() +r2 = cli.Node() +c2 = cli.Node() + +# ----------------------------------------------------------------------------------------------------------------------- +# Form topology + +r1.allowlist_node(r2) + +r2.allowlist_node(r1) +r2.allowlist_node(c2) + +c2.allowlist_node(r2) + +r1.form("netdatatmout") +r2.join(r1) +c2.join(r1, cli.JOIN_TYPE_SLEEPY_END_DEVICE) +c2.set_pollperiod(500) + +verify(r1.get_state() == 'leader') +verify(r2.get_state() == 'router') +verify(c2.get_state() == 'child') + +nodes = [r1, r2, c2] + +# ----------------------------------------------------------------------------------------------------------------------- +# Test Implementation + +common_prefix = 'fd00:cafe::' +prefix1 = 'fd00:1::' +prefix2 = 'fd00:2::' +prefix3 = 'fd00:3::' + +# Each node adds its own prefix. +r1.add_prefix('fd00:1::/64', 'paros', 'med') +r2.add_prefix('fd00:2::/64', 'paros', 'med') +c2.add_prefix('fd00:3::/64', 'paros', 'med') + +# a common prefix is added by all three nodes (with different preference) +r1.add_prefix('fd00:abba::/64', 'paros', 'high') +r2.add_prefix('fd00:abba::/64', 'paros', 'med') +c2.add_prefix('fd00:abba::/64', 'paros', 'low') + +r1.register_netdata() +r2.register_netdata() +c2.register_netdata() + + +def check_netdata_on_all_nodes(): + for node in nodes: + netdata = node.get_netdata() + prefixes = netdata['prefixes'] + verify(len(prefixes) == 6) + + +verify_within(check_netdata_on_all_nodes, 10) + +# Remove `r2`. This should trigger all the prefixes added by it or its +# child to timeout and be removed. + +r2.thread_stop() +r2.interface_down() + + +def check_netdata_on_r1(): + netdata = r1.get_netdata() + prefixes = netdata['prefixes'] + verify(len(prefixes) == 2) + + +verify_within(check_netdata_on_r1, 120) + +# ----------------------------------------------------------------------------------------------------------------------- +# Test finished + +cli.Node.finalize_all_nodes() + +print('\'{}\' passed.'.format(test_name)) diff --git a/tests/toranj/cli/test-012-reset-recovery.py b/tests/toranj/cli/test-012-reset-recovery.py new file mode 100755 index 000000000..8af851441 --- /dev/null +++ b/tests/toranj/cli/test-012-reset-recovery.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2022, 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. + +from cli import verify +from cli import verify_within +import cli +import time + +# ----------------------------------------------------------------------------------------------------------------------- +# Test description: +# This test covers reset of parent, router, leader and restoring children after reset +# + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print('-' * 120) +print('Starting \'{}\''.format(test_name)) + +# ----------------------------------------------------------------------------------------------------------------------- +# Creating `cli.Nodes` instances + +speedup = 25 +cli.Node.set_time_speedup_factor(speedup) + +leader = cli.Node() +router = cli.Node() +child1 = cli.Node() +child2 = cli.Node() + +# ----------------------------------------------------------------------------------------------------------------------- +# Form topology + +leader.form('reset') +child1.join(leader, cli.JOIN_TYPE_END_DEVICE) +child2.join(leader, cli.JOIN_TYPE_END_DEVICE) + +verify(leader.get_state() == 'leader') +verify(child1.get_state() == 'child') +verify(child2.get_state() == 'child') + +# ----------------------------------------------------------------------------------------------------------------------- +# Test Implementation + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Reset the parent and verify that both children are restored on +# parent through "Child Update" exchange process and none of them got +# detached and needed to attach back. + +del leader +leader = cli.Node(index=1) +leader.interface_up() +leader.thread_start() + + +def check_leader_state(): + verify(leader.get_state() == 'leader') + + +verify_within(check_leader_state, 10) + +# Check that `child1` and `child2` did not detach + +verify(child1.get_state() == 'child') +verify(child2.get_state() == 'child') + +verify(int(cli.Node.parse_list(child1.get_mle_counter())['Role Detached']) == 1) +verify(int(cli.Node.parse_list(child2.get_mle_counter())['Role Detached']) == 1) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Reset `router` and make sure it recovers as router with same router ID. + +router.join(leader) + +verify(router.get_state() == 'router') +router_rloc16 = int(router.get_rloc16(), 16) + +time.sleep(0.75) + +del router +router = cli.Node(index=2) +router.interface_up() +router.thread_start() + + +def check_router_state(): + verify(router.get_state() == 'router') + + +verify_within(check_router_state, 10) +verify(router_rloc16 == int(router.get_rloc16(), 16)) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Reset `leader` and make sure `router` is its neighbor again + +del leader +leader = cli.Node(index=1) +leader.interface_up() +leader.thread_start() + + +def check_leader_state(): + verify(leader.get_state() == 'leader') + + +verify_within(check_leader_state, 10) + + +def check_leader_neighbor_table(): + verify(len(leader.get_neighbor_table()) == 3) + + +verify_within(check_leader_neighbor_table, 10) + +# ----------------------------------------------------------------------------------------------------------------------- +# Test finished + +cli.Node.finalize_all_nodes() + +print('\'{}\' passed.'.format(test_name)) diff --git a/tests/toranj/cli/test-013-address-cache-parent-switch.py b/tests/toranj/cli/test-013-address-cache-parent-switch.py new file mode 100755 index 000000000..eee1befd9 --- /dev/null +++ b/tests/toranj/cli/test-013-address-cache-parent-switch.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2022, 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. + +from cli import verify +from cli import verify_within +import cli +import time + +# ----------------------------------------------------------------------------------------------------------------------- +# Test description: Address Cache Table +# +# This test verifies the behavior of `AddressResolver` module and entries in +# address cache table. It also tests the behavior of nodes when there are +# topology changes in the network (e.g., a child switches parent). In +# particular, the test covers the address cache update through snooping, i.e., +# the logic which inspects forwarded frames to update address cache table if +# source RLOC16 on a received frame differs from an existing entry in the +# address cache table. +# +# Network topology: +# +# r1 ---- r2 ---- r3 +# | | | +# | | | +# c1 c2(s) c3 +# +# c1 and c3 are FED children, c2 is an SED which is first attached to r2 and +# then forced to switch to r3. + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print('-' * 120) +print('Starting \'{}\''.format(test_name)) + +# ----------------------------------------------------------------------------------------------------------------------- +# Creating `cli.Nodes` instances + +speedup = 25 +cli.Node.set_time_speedup_factor(speedup) + +r1 = cli.Node() +r2 = cli.Node() +r3 = cli.Node() +c1 = cli.Node() +c2 = cli.Node() +c3 = cli.Node() + +# ----------------------------------------------------------------------------------------------------------------------- +# Form topology + +r1.allowlist_node(r2) +r1.allowlist_node(c1) + +r2.allowlist_node(r1) +r2.allowlist_node(r3) +r2.allowlist_node(c2) + +r3.allowlist_node(r2) +r3.allowlist_node(c3) + +c1.allowlist_node(r1) +c2.allowlist_node(r2) +c3.allowlist_node(r3) + +r1.form('addrrslvr') +r2.join(r1) +r3.join(r1) +c1.join(r1, cli.JOIN_TYPE_REED) +c2.join(r1, cli.JOIN_TYPE_SLEEPY_END_DEVICE) +c3.join(r1, cli.JOIN_TYPE_REED) +c2.set_pollperiod(400) + +verify(r1.get_state() == 'leader') +verify(r2.get_state() == 'router') +verify(r3.get_state() == 'router') +verify(c1.get_state() == 'child') +verify(c2.get_state() == 'child') +verify(c3.get_state() == 'child') + +# ----------------------------------------------------------------------------------------------------------------------- +# Test Implementation + +# Wait till first router has either established a link or +# has a valid "next hop" towards all other routers. + +r1_rloc16 = int(r1.get_rloc16(), 16) + + +def check_r1_router_table(): + table = r1.get_router_table() + verify(len(table) == 3) + for entry in table: + verify(int(entry['RLOC16'], 0) == r1_rloc16 or int(entry['Link']) == 1 or int(entry['Next Hop']) != 63) + + +verify_within(check_r1_router_table, 120) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +r1_address = r1.get_mleid_ip_addr() +c1_address = c1.get_mleid_ip_addr() +c2_address = c2.get_mleid_ip_addr() +c3_address = c3.get_mleid_ip_addr() + +r1_rloc16 = int(r1.get_rloc16(), 16) +r2_rloc16 = int(r2.get_rloc16(), 16) +r3_rloc16 = int(r3.get_rloc16(), 16) +c1_rloc16 = int(c1.get_rloc16(), 16) +c3_rloc16 = int(c3.get_rloc16(), 16) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# From r1 ping c2 and c3 + +r1.ping(c2_address) +r1.ping(c3_address) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Verify that address cache table contains both c2 and c3 addresses c2 +# address should match its parent r2 (since c2 is sleepy), and c1 +# address should match to itself (since c3 is fed). + +cache_table = r1.get_eidcache() +verify(len(cache_table) >= 2) + +for entry in cache_table: + fields = entry.strip().split(' ') + verify(fields[2] == 'cache') + if fields[0] == c2_address: + verify(int(fields[1], 16) == r2_rloc16) + elif fields[0] == c3_address: + verify(int(fields[1], 16) == c3_rloc16) + else: + verify(False) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Force c2 to switch its parent from r2 to r3 +# +# New network topology +# +# r1 ---- r2 ---- r3 +# | /\ +# | / \ +# c1 c2(s) c3 + +r2.un_allowlist_node(c2) +r3.allowlist_node(c2) +c2.allowlist_node(r3) + +c2.thread_stop() +c2.thread_start() + + +def check_c2_attaches_to_r3(): + verify(c2.get_state() == 'child') + verify(int(c2.get_parent_info()['Rloc'], 16) == r3_rloc16) + + +verify_within(check_c2_attaches_to_r3, 10) + + +def check_r2_child_table_is_empty(): + verify(len(r2.get_child_table()) == 0) + + +verify_within(check_r2_child_table_is_empty, 10) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Note that r1 still has r2 as the destination for c2's address in its +# address cache table. But since r2 is aware that c2 is no longer its +# child, when it receives the IPv6 message with c2's address, r2 +# itself would do an address query for the address and forward the +# IPv6 message. + +cache_table = r1.get_eidcache() +for entry in cache_table: + fields = entry.strip().split(' ') + if fields[0] == c2_address: + verify(int(fields[1], 16) == r2_rloc16) + break +else: + verify(False) + +r1.ping(c2_address) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Ping c1 from c2. This will go through c1's parent r1. r1 upon +# receiving and forwarding the message should update its address +# cache table for c2 (address cache update through snooping). + +c2.ping(c1_address) + +cache_table = r1.get_eidcache() + +for entry in cache_table: + fields = entry.strip().split(' ') + if fields[0] == c2_address: + verify(int(fields[1], 16) == r3_rloc16) + verify(fields[2] == 'snoop') + break +else: + verify(False) + +# ----------------------------------------------------------------------------------------------------------------------- +# Test finished + +cli.Node.finalize_all_nodes() + +print('\'{}\' passed.'.format(test_name)) diff --git a/tests/toranj/cli/test-014-address-resolver.py b/tests/toranj/cli/test-014-address-resolver.py new file mode 100755 index 000000000..4203129fa --- /dev/null +++ b/tests/toranj/cli/test-014-address-resolver.py @@ -0,0 +1,415 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2022, 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. + +from cli import verify +from cli import verify_within +import cli +import time + +# ----------------------------------------------------------------------------------------------------------------------- +# Test description: Address Cache Table +# +# This test verifies the behavior of `AddressResolver` and how the cache +# table is managed. In particular it verifies behavior query timeout and +# query retry and snoop optimization. +# +# Build network topology +# +# r3 ---- r1 ---- r2 +# | | +# | | +# c3 c2 +# + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print('-' * 120) +print('Starting \'{}\''.format(test_name)) + +# ----------------------------------------------------------------------------------------------------------------------- +# Creating `cli.Node` instances + +speedup = 10 +cli.Node.set_time_speedup_factor(speedup) + +r1 = cli.Node() +r2 = cli.Node() +r3 = cli.Node() +c2 = cli.Node() +c3 = cli.Node() + +# ----------------------------------------------------------------------------------------------------------------------- +# Form topology + +r1.allowlist_node(r2) +r1.allowlist_node(r3) + +r2.allowlist_node(r1) +r2.allowlist_node(c2) + +r3.allowlist_node(r1) +r3.allowlist_node(c3) + +c2.allowlist_node(r2) +c3.allowlist_node(r3) + +r1.form('addrrslvr') + +prefix = 'fd00:abba::' +r1.add_prefix(prefix + '/64', 'pos', 'med') +r1.register_netdata() + +r2.join(r1) +r3.join(r1) +c2.join(r1, cli.JOIN_TYPE_END_DEVICE) +c3.join(r1, cli.JOIN_TYPE_SLEEPY_END_DEVICE) +c3.set_pollperiod(400) + +verify(r1.get_state() == 'leader') +verify(r2.get_state() == 'router') +verify(r3.get_state() == 'router') +verify(c2.get_state() == 'child') +verify(c3.get_state() == 'child') + +# ----------------------------------------------------------------------------------------------------------------------- +# Test Implementation + +# Wait till first router has either established a link or +# has a valid "next hop" towards all other routers. + +r1_rloc16 = int(r1.get_rloc16(), 16) + + +def check_r1_router_table(): + table = r1.get_router_table() + verify(len(table) == 3) + for entry in table: + verify(int(entry['RLOC16'], 0) == r1_rloc16 or int(entry['Link']) == 1 or int(entry['Next Hop']) != 63) + + +verify_within(check_r1_router_table, 120) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +r1_rloc = int(r1.get_rloc16(), 16) +r2_rloc = int(r2.get_rloc16(), 16) +r3_rloc = int(r3.get_rloc16(), 16) +c2_rloc = int(c2.get_rloc16(), 16) +c3_rloc = int(c3.get_rloc16(), 16) + +# AddressResolver constants: + +max_cache_entries = 16 +max_snooped_non_evictable = 2 + +# Add IPv6 addresses matching the on-mesh prefix on all nodes + +r1.add_ip_addr(prefix + '1') + +num_addresses = 4 # Number of addresses to add on r2, r3, c2, and c3 + +for num in range(num_addresses): + r2.add_ip_addr(prefix + "2:" + str(num)) + r3.add_ip_addr(prefix + "3:" + str(num)) + c2.add_ip_addr(prefix + "c2:" + str(num)) + c3.add_ip_addr(prefix + "c3:" + str(num)) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +# From r1 send msg to a group of addresses that are not provided by +# any nodes in network. + +num_queries = 5 +stagger_interval = 1.2 +port = 1234 +initial_retry_delay = 8 + +r1.udp_open() + +for num in range(num_queries): + r1.udp_send(prefix + '800:' + str(num), port, 'hi_nobody') + # Wait before next tx to stagger the address queries + # request ensuring different timeouts + time.sleep(stagger_interval / (num_queries * speedup)) + +# Verify that we do see entries in cache table for all the addresses +# and all are in "query" state + +cache_table = r1.get_eidcache() +verify(len(cache_table) == num_queries) +for entry in cache_table: + fields = entry.strip().split(' ') + verify(fields[2] == 'query') + verify(fields[3] == 'canEvict=0') + verify(fields[4].startswith('timeout=')) + verify(int(fields[4].split('=')[1]) > 0) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Check the retry-query behavior +# +# Wait till all the address queries time out and verify they +# enter "retry-query" state. + + +def check_cache_entry_switch_to_retry_state(): + cache_table = r1.get_eidcache() + for entry in cache_table: + fields = entry.strip().split(' ') + verify(fields[2] == 'retry') + verify(fields[3] == 'canEvict=1') + verify(fields[4].startswith('timeout=')) + verify(int(fields[4].split('=')[1]) >= 0) + verify(fields[5].startswith('retryDelay=')) + verify(int(fields[5].split('=')[1]) == initial_retry_delay) + + +verify_within(check_cache_entry_switch_to_retry_state, 20) + +# Try sending again to same addresses which are all in "retry" state. + +for num in range(num_queries): + r1.udp_send(prefix + '800:' + str(num), port, 'hi_nobody') + +# Make sure the entries stayed in retry-query state as before. + +verify_within(check_cache_entry_switch_to_retry_state, 20) + +# Now wait for all entries to reach zero timeout. + + +def check_cache_entry_in_retry_state_to_get_to_zero_timeout(): + cache_table = r1.get_eidcache() + for entry in cache_table: + fields = entry.strip().split(' ') + verify(fields[2] == 'retry') + verify(fields[3] == 'canEvict=1') + verify(fields[4].startswith('timeout=')) + verify(int(fields[4].split('=')[1]) == 0) + + +verify_within(check_cache_entry_in_retry_state_to_get_to_zero_timeout, 20) + +# Now send again to the same addresses. + +for num in range(num_queries): + r1.udp_send(prefix + '800:' + str(num), port, 'hi_nobody') + +# We expect now after the delay to see retries for same addresses. + + +def check_cache_entry_switch_to_query_state(): + cache_table = r1.get_eidcache() + for entry in cache_table: + fields = entry.strip().split(' ') + verify(fields[2] == 'query') + verify(fields[3] == 'canEvict=1') + + +verify_within(check_cache_entry_switch_to_query_state, 20) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Verify snoop optimization behavior. + +# Send to r1 from all addresses on r2. + +r2.udp_open() +for num in range(num_addresses): + r2.udp_bind(prefix + '2:' + str(num), port) + r2.udp_send(prefix + '1', port, 'hi_r1_from_r2_snoop_me') + +# Verify that we see all addresses from r2 as snooped in cache table. +# At most two of them should be marked as non-evictable. + + +def check_cache_entry_contains_snooped_entries(): + cache_table = r1.get_eidcache() + verify(len(cache_table) >= num_addresses) + snooped_count = 0 + snooped_non_evictable = 0 + for entry in cache_table: + fields = entry.strip().split(' ') + if fields[2] == 'snoop': + verify(fields[0].startswith('fd00:abba:0:0:0:0:2:')) + verify(int(fields[1], 16) == r2_rloc) + snooped_count = snooped_count + 1 + if fields[3] == 'canEvict=0': + snooped_non_evictable = snooped_non_evictable + 1 + verify(snooped_count == num_addresses) + verify(snooped_non_evictable == max_snooped_non_evictable) + + +verify_within(check_cache_entry_contains_snooped_entries, 20) + +# Now we use the snooped entries by sending from r1 to r2 using +# all its addresses. + +for num in range(num_addresses): + r1.udp_send(prefix + '2:' + str(num), port, 'hi_back_r2_from_r1') + +time.sleep(0.1) + +# We expect to see the entries to be in "cached" state now. + +cache_table = r1.get_eidcache() +verify(len(cache_table) >= num_addresses) +match_count = 0 +for entry in cache_table: + fields = entry.strip().split(' ') + if fields[0].startswith('fd00:abba:0:0:0:0:2:'): + verify(fields[2] == 'cache') + verify(fields[3] == 'canEvict=1') + match_count = match_count + 1 +verify(match_count == num_addresses) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Check query requests and last transaction time + +# Send from r1 to all addresses on r3. Check entries +# for r3 are at the top of cache table list. + +for num in range(num_addresses): + r1.udp_send(prefix + '3:' + str(num), port, 'hi_r3_from_r1') + + +def check_cache_entry_contains_r3_entries(): + cache_table = r1.get_eidcache() + for num in range(num_addresses): + entry = cache_table[num] + fields = entry.strip().split(' ') + verify(fields[0].startswith('fd00:abba:0:0:0:0:3:')) + verify(int(fields[1], 16) == r3_rloc) + verify(fields[2] == 'cache') + verify(fields[3] == 'canEvict=1') + verify(fields[4] == 'transTime=0') + + +verify_within(check_cache_entry_contains_r3_entries, 20) + +# Send from r1 to all addresses of c3 (sleepy child of r3) + +for num in range(num_addresses): + r1.udp_send(prefix + 'c3:' + str(num), port, 'hi_c3_from_r1') + + +def check_cache_entry_contains_c3_entries(): + cache_table = r1.get_eidcache() + for num in range(num_addresses): + entry = cache_table[num] + fields = entry.strip().split(' ') + verify(fields[0].startswith('fd00:abba:0:0:0:0:c3:')) + verify(int(fields[1], 16) == r3_rloc) + verify(fields[2] == 'cache') + verify(fields[3] == 'canEvict=1') + verify(fields[4] == 'transTime=0') + + +verify_within(check_cache_entry_contains_c3_entries, 20) + +# Send again to r2. This should cause the related cache entries to +# be moved to top of the list. + +for num in range(num_addresses): + r1.udp_send(prefix + '2:' + str(num), port, 'hi_again_r2_from_r1') + + +def check_cache_entry_contains_r2_entries(): + cache_table = r1.get_eidcache() + for num in range(num_addresses): + entry = cache_table[num] + fields = entry.strip().split(' ') + verify(fields[0].startswith('fd00:abba:0:0:0:0:2:')) + verify(int(fields[1], 16) == r2_rloc) + verify(fields[2] == 'cache') + verify(fields[3] == 'canEvict=1') + + +verify_within(check_cache_entry_contains_r2_entries, 20) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Check behavior when address cache table is full. + +cache_table = r1.get_eidcache() +verify(len(cache_table) == max_cache_entries) + +# From r1 send to non-existing addresses. + +for num in range(num_queries): + r1.udp_send(prefix + '900:' + str(num), port, 'hi_nobody!') + +cache_table = r1.get_eidcache() +verify(len(cache_table) == max_cache_entries) + +# Send from c2 to r1 and verify that snoop optimization uses at most +# `max_snooped_non_evictable` entries + +c2.udp_open() + +for num in range(num_addresses): + c2.udp_bind(prefix + 'c2:' + str(num), port) + c2.udp_send(prefix + '1', port, 'hi_r1_from_c2_snoop_me') + + +def check_cache_entry_contains_max_allowed_snopped(): + cache_table = r1.get_eidcache() + snooped_non_evictable = 0 + for entry in cache_table: + fields = entry.strip().split(' ') + if fields[2] == 'snoop': + verify(fields[0].startswith('fd00:abba:0:0:0:0:c2:')) + verify(fields[3] == 'canEvict=0') + snooped_non_evictable = snooped_non_evictable + 1 + verify(snooped_non_evictable == max_snooped_non_evictable) + + +verify_within(check_cache_entry_contains_max_allowed_snopped, 20) + +# Now send from r1 to c2, the snooped entries would be used +# some other addresses will go through full address query. + +for num in range(num_addresses): + r1.udp_send(prefix + 'c2:' + str(num), port, 'hi_c2_from_r1') + + +def check_cache_entry_contains_c2_entries(): + cache_table = r1.get_eidcache() + for num in range(num_addresses): + entry = cache_table[num] + fields = entry.strip().split(' ') + verify(fields[0].startswith('fd00:abba:0:0:0:0:c2:')) + verify(int(fields[1], 16) == r2_rloc) + verify(fields[2] == 'cache') + verify(fields[3] == 'canEvict=1') + + +verify_within(check_cache_entry_contains_c2_entries, 20) + +# ----------------------------------------------------------------------------------------------------------------------- +# Test finished + +cli.Node.finalize_all_nodes() + +print('\'{}\' passed.'.format(test_name)) diff --git a/tests/toranj/cli/test-015-clear-addresss-cache-for-sed.py b/tests/toranj/cli/test-015-clear-addresss-cache-for-sed.py new file mode 100755 index 000000000..31f75cc14 --- /dev/null +++ b/tests/toranj/cli/test-015-clear-addresss-cache-for-sed.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2022, 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. + +from cli import verify +from cli import verify_within +import cli +import time + +# ----------------------------------------------------------------------------------------------------------------------- +# Test description: Address Cache Table +# +# This test verifies that address cache entries associated with SED child +# addresses are removed from new parent node ensuring we would not have a +# routing loop. +# +# +# r1 ---- r2 ---- r3 +# | +# | +# c +# +# c is initially attached to r3 but it switches parent during test to r2 and then r1. + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print('-' * 120) +print('Starting \'{}\''.format(test_name)) + +# ----------------------------------------------------------------------------------------------------------------------- +# Creating `cli.Node` instances + +speedup = 40 +cli.Node.set_time_speedup_factor(speedup) + +r1 = cli.Node() +r2 = cli.Node() +r3 = cli.Node() +c = cli.Node() + +# ----------------------------------------------------------------------------------------------------------------------- +# Form topology + +r1.allowlist_node(r2) + +r2.allowlist_node(r1) +r2.allowlist_node(r3) + +r3.allowlist_node(r2) +r3.allowlist_node(c) + +c.allowlist_node(r3) + +r1.form('sed-cache') + +prefix = 'fd00:abba::' +r1.add_prefix(prefix + '/64', 'paos', 'med') +r1.register_netdata() + +r2.join(r1) +r3.join(r1) +c.join(r1, cli.JOIN_TYPE_SLEEPY_END_DEVICE) +c.set_pollperiod(400) + +verify(r1.get_state() == 'leader') +verify(r2.get_state() == 'router') +verify(r3.get_state() == 'router') +verify(c.get_state() == 'child') + +# ----------------------------------------------------------------------------------------------------------------------- +# Test Implementation + +# Wait till first router has either established a link or +# has a valid "next hop" towards all other routers. + +r1_rloc16 = int(r1.get_rloc16(), 16) + + +def check_r1_router_table(): + table = r1.get_router_table() + verify(len(table) == 3) + for entry in table: + verify(int(entry['RLOC16'], 0) == r1_rloc16 or int(entry['Link']) == 1 or int(entry['Next Hop']) != 63) + + +verify_within(check_r1_router_table, 120) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +r1_rloc = int(r1.get_rloc16(), 16) +r2_rloc = int(r2.get_rloc16(), 16) +r3_rloc = int(r3.get_rloc16(), 16) +c_rloc = int(c.get_rloc16(), 16) + +r1_address = next(addr for addr in r1.get_ip_addrs() if addr.startswith('fd00:abba:')) +r2_address = next(addr for addr in r2.get_ip_addrs() if addr.startswith('fd00:abba:')) +r3_address = next(addr for addr in r3.get_ip_addrs() if addr.startswith('fd00:abba:')) +c_address = next(addr for addr in c.get_ip_addrs() if addr.startswith('fd00:abba:')) + +port = 4321 + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Send a single UDP message from r2 to c. This adds an address cache +# entry on r2 for c pointing to r3 (the current parent of c). + +r2.udp_open() +r2.udp_send(c_address, port, 'hi_from_r2_to_c') + + +def check_r2_cache_table(): + cache_table = r2.get_eidcache() + for entry in cache_table: + fields = entry.strip().split(' ') + if (fields[0] == c_address): + verify(int(fields[1], 16) == r3_rloc) + verify(fields[2] == 'cache') + break + else: + verify(False) + + +verify_within(check_r2_cache_table, 20) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Force c to switch its parent from r3 to r2 +# +# r1 ---- r2 ---- r3 +# | +# | +# c + +r3.un_allowlist_node(c) +r2.allowlist_node(c) +c.allowlist_node(r2) +c.un_allowlist_node(r3) + +c.thread_stop() +c.thread_start() + + +def check_c_attaches_to_r2(): + verify(c.get_state() == 'child') + verify(int(c.get_parent_info()['Rloc'], 16) == r2_rloc) + + +verify_within(check_c_attaches_to_r2, 10) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Send a single UDP message from r3 to c. This adds an address cache +# entry on r3 for c pointing to r2 (the current parent of c). + +r3.udp_open() +r3.udp_send(c_address, port, 'hi_from_r3_to_c') + + +def check_r3_cache_table(): + cache_table = r3.get_eidcache() + for entry in cache_table: + fields = entry.strip().split(' ') + if (fields[0] == c_address): + verify(int(fields[1], 16) == r2_rloc) + verify(fields[2] == 'cache') + break + else: + verify(False) + + +verify_within(check_r3_cache_table, 20) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Force c to switch its parent again now to r1 +# +# r1 ---- r2 ---- r3 +# | +# | +# c + +r2.un_allowlist_node(c) +r1.allowlist_node(c) +c.allowlist_node(r1) +c.un_allowlist_node(r2) + +c.thread_stop() +c.thread_start() + + +def check_c_attaches_to_r1(): + verify(c.get_state() == 'child') + verify(int(c.get_parent_info()['Rloc'], 16) == r1_rloc) + + +verify_within(check_c_attaches_to_r1, 10) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Now ping c from r2. +# +# If r2 address cache entry is not cleared when c attached to r1, r2 +# will still have an entry pointing to r3, and r3 will have an entry +# pointing to r2, thus creating a loop (the msg will not be delivered +# to r3) + +r2.ping(c_address) + +# ----------------------------------------------------------------------------------------------------------------------- +# Test finished + +cli.Node.finalize_all_nodes() + +print('\'{}\' passed.'.format(test_name)) diff --git a/tests/toranj/cli/test-016-child-mode-change.py b/tests/toranj/cli/test-016-child-mode-change.py new file mode 100755 index 000000000..b8cfa2ec4 --- /dev/null +++ b/tests/toranj/cli/test-016-child-mode-change.py @@ -0,0 +1,214 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2022, 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. + +from cli import verify +from cli import verify_within +import cli +import time + +# ----------------------------------------------------------------------------------------------------------------------- +# Test description: +# +# Verify device mode change on children. +# + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print('-' * 120) +print('Starting \'{}\''.format(test_name)) + +# ----------------------------------------------------------------------------------------------------------------------- +# Creating `cli.Node` instances + +speedup = 10 +cli.Node.set_time_speedup_factor(speedup) + +parent = cli.Node() +child1 = cli.Node() +child2 = cli.Node() + +# ----------------------------------------------------------------------------------------------------------------------- +# Form topology + +parent.form('modechange') +child1.join(parent, cli.JOIN_TYPE_END_DEVICE) +child2.join(parent, cli.JOIN_TYPE_SLEEPY_END_DEVICE) + +verify(parent.get_state() == 'leader') +verify(child1.get_state() == 'child') +verify(child2.get_state() == 'child') + +# ----------------------------------------------------------------------------------------------------------------------- +# Test Implementation + +child1_rloc = int(child1.get_rloc16(), 16) +child2_rloc = int(child2.get_rloc16(), 16) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Check the mode on children and also on parent child table + +verify(parent.get_mode() == 'rdn') +verify(child1.get_mode() == 'rn') +verify(child2.get_mode() == '-') + +child_table = parent.get_child_table() +verify(len(child_table) == 2) +for entry in child_table: + if int(entry['RLOC16'], 16) == child1_rloc: + verify(entry['R'] == '1') + verify(entry['D'] == '0') + verify(entry['N'] == '1') + elif int(entry['RLOC16'], 16) == child2_rloc: + verify(entry['R'] == '0') + verify(entry['D'] == '0') + verify(entry['N'] == '0') + else: + verify(False) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Change the network data flag on child 2 (sleepy) and verify that it +# gets changed on parent's child table. + +child2.set_mode('n') + + +def check_child2_n_flag_change(): + verify(child2.get_mode() == 'n') + child_table = parent.get_child_table() + verify(len(child_table) == 2) + for entry in child_table: + if int(entry['RLOC16'], 16) == child1_rloc: + verify(entry['R'] == '1') + verify(entry['D'] == '0') + verify(entry['N'] == '1') + elif int(entry['RLOC16'], 16) == child2_rloc: + verify(entry['R'] == '0') + verify(entry['D'] == '0') + verify(entry['N'] == '1') + else: + verify(False) + + +verify_within(check_child2_n_flag_change, 5) + +# Verify that mode change did not cause child2 to detach and re-attach + +verify(int(cli.Node.parse_list(child2.get_mle_counter())['Role Detached']) == 1) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Change child1 from rx-on to sleepy and verify the change on +# parent's child table. This mode change should require child +# to detach and attach again. + +child1.set_mode('n') + + +def check_child1_become_sleepy(): + verify(child1.get_mode() == 'n') + child_table = parent.get_child_table() + verify(len(child_table) == 2) + for entry in child_table: + if int(entry['RLOC16'], 16) == child1_rloc: + verify(entry['R'] == '0') + verify(entry['D'] == '0') + verify(entry['N'] == '1') + elif int(entry['RLOC16'], 16) == child2_rloc: + verify(entry['R'] == '0') + verify(entry['D'] == '0') + verify(entry['N'] == '1') + else: + verify(False) + + +verify_within(check_child1_become_sleepy, 5) + +verify(int(cli.Node.parse_list(child1.get_mle_counter())['Role Detached']) == 2) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Change child2 from sleepy to rx-on and verify the change on +# parent's child table. Verify that child2 did not detach and +# used MLE "Child Update" exchange. + +child2.set_mode('rn') + + +def check_child2_become_rx_on(): + verify(child2.get_mode() == 'rn') + child_table = parent.get_child_table() + verify(len(child_table) == 2) + for entry in child_table: + if int(entry['RLOC16'], 16) == child1_rloc: + verify(entry['R'] == '0') + verify(entry['D'] == '0') + verify(entry['N'] == '1') + elif int(entry['RLOC16'], 16) == child2_rloc: + verify(entry['R'] == '1') + verify(entry['D'] == '0') + verify(entry['N'] == '1') + else: + verify(False) + + +verify_within(check_child2_become_rx_on, 5) + +verify(int(cli.Node.parse_list(child2.get_mle_counter())['Role Detached']) == 1) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Now change child2 to become sleepy again. Since it was originally +# attached as sleepy it should not detach and attach again and +# can do this using MlE "Child Update" exchange with parent. + +child2.set_mode('n') + + +def check_child2_become_sleepy_again(): + verify(child2.get_mode() == 'n') + child_table = parent.get_child_table() + verify(len(child_table) == 2) + for entry in child_table: + if int(entry['RLOC16'], 16) == child1_rloc: + verify(entry['R'] == '0') + verify(entry['D'] == '0') + verify(entry['N'] == '1') + elif int(entry['RLOC16'], 16) == child2_rloc: + verify(entry['R'] == '0') + verify(entry['D'] == '0') + verify(entry['N'] == '1') + else: + verify(False) + + +verify_within(check_child2_become_sleepy_again, 5) + +verify(int(cli.Node.parse_list(child2.get_mle_counter())['Role Detached']) == 1) + +# ----------------------------------------------------------------------------------------------------------------------- +# Test finished + +cli.Node.finalize_all_nodes() + +print('\'{}\' passed.'.format(test_name)) diff --git a/tests/toranj/cli/test-017-network-data-versions.py b/tests/toranj/cli/test-017-network-data-versions.py new file mode 100755 index 000000000..d90bb13f0 --- /dev/null +++ b/tests/toranj/cli/test-017-network-data-versions.py @@ -0,0 +1,346 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2022, 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. + +from cli import verify +from cli import verify_within +import cli +import time + +# ----------------------------------------------------------------------------------------------------------------------- +# Test description: Network Data update and version changes (stable only vs. full version). +# +# Network topology +# +# r1 ---- r2 ---- r3 +# | +# | +# sed +# +# +# sed is sleepy-end node and also configured to request stable Network Data only +# +# Test covers the following steps: +# - Adding/removing prefixes (stable or temporary) on r1 +# - Verifying that Network Data is updated on all nodes +# - Ensuring correct update to version and stable version +# +# The above steps are repeated over many different situations: +# - Where the same prefixes are also added by other nodes +# - Or the same prefixes are added as off-mesh routes by other nodes + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print('-' * 120) +print('Starting \'{}\''.format(test_name)) + +# ----------------------------------------------------------------------------------------------------------------------- +# Creating `cli.Node` instances + +speedup = 10 +cli.Node.set_time_speedup_factor(speedup) + +r1 = cli.Node() +r2 = cli.Node() +r3 = cli.Node() +sed = cli.Node() + +# ----------------------------------------------------------------------------------------------------------------------- +# Form topology + +r1.allowlist_node(r2) + +r2.allowlist_node(r1) +r2.allowlist_node(r3) +r2.allowlist_node(sed) + +r2.allowlist_node(r2) + +sed.allowlist_node(r2) + +r1.form('netdata') +r2.join(r1) +r3.join(r1) +sed.join(r1, cli.JOIN_TYPE_SLEEPY_END_DEVICE) + +sed.set_mode('-') +sed.set_pollperiod(400) + +# ----------------------------------------------------------------------------------------------------------------------- +# Test Implementation + +r1_rloc = r1.get_rloc16() +r2_rloc = r2.get_rloc16() +r3_rloc = r3.get_rloc16() + +versions = r1.get_netdata_versions() + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + +def verify_versions_incremented(): + global versions + new_versions = r1.get_netdata_versions() + verify(new_versions[0] == ((versions[0] + 1) % 256)) + verify(new_versions[1] == ((versions[1] + 1) % 256)) + versions = new_versions + + +def verify_stabe_version_incremented(): + global versions + new_versions = r1.get_netdata_versions() + verify(new_versions[0] == ((versions[0] + 1) % 256)) + verify(new_versions[1] == versions[1]) + versions = new_versions + + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +# Add prefix `fd00:1::/64` on r1 as stable and validate +# that entries are updated on all nodes and versions are changed. + +r1.add_prefix('fd00:1::/64', 'os') +r1.register_netdata() + + +def check_r1_prefix_added_on_all_nodes(): + for node in [r1, r2, r3]: + verify('fd00:1:0:0::/64 os med ' + r1_rloc in node.get_netdata_prefixes()) + verify('fd00:1:0:0::/64 os med fffe' in sed.get_netdata_prefixes()) + + +verify_within(check_r1_prefix_added_on_all_nodes, 2) +verify_versions_incremented() + +# Add prefix `fd00:2::/64` on r2 as temporary and ensure it is seen on +# all nodes and not seen on sed. + +r2.add_prefix('fd00:2::/64', 'po', 'high') +r2.register_netdata() + + +def check_r2_prefix_added_on_all_nodes(): + for node in [r1, r2, r3]: + verify('fd00:2:0:0::/64 po high ' + r2_rloc in node.get_netdata_prefixes()) + verify(not 'fd00:2:0:0::/64 po high fffe' in sed.get_netdata_prefixes()) + + +verify_within(check_r2_prefix_added_on_all_nodes, 2) +verify_stabe_version_incremented() + +# Remove prefix `fd00:1::/64` from r1. + +r1.remove_prefix('fd00:1::/64') +r1.register_netdata() + + +def check_r1_prefix_removed_on_all_nodes(): + for node in [r1, r2, r3]: + verify(not 'fd00:1:0:0::/64 os med ' + r1_rloc in node.get_netdata_prefixes()) + verify(not 'fd00:1:0:0::/64 os med fffe' in sed.get_netdata_prefixes()) + + +verify_within(check_r1_prefix_removed_on_all_nodes, 2) +verify_versions_incremented() + +# Remove prefix `fd00:2::/64` from r2. + +r2.remove_prefix('fd00:2::/64') +r2.register_netdata() + + +def check_r2_prefix_removed_on_all_nodes(): + for node in [r1, r2, r3]: + verify(not 'fd00:2:0:0::/64 po high ' + r2_rloc in node.get_netdata_prefixes()) + verify(not 'fd00:2:0:0::/64 po high fffe' in sed.get_netdata_prefixes()) + + +verify_within(check_r2_prefix_removed_on_all_nodes, 2) +verify_stabe_version_incremented() + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Repeat the same checks but now r3 also adds ``fd00:1::/64` with different +# flags. + +r3.add_prefix('fd00:1::/64', 'paos') +r3.register_netdata() + + +def check_r3_prefix_added_on_all_nodes(): + for node in [r1, r2, r3]: + verify('fd00:1:0:0::/64 paos med ' + r3_rloc in node.get_netdata_prefixes()) + verify('fd00:1:0:0::/64 paos med fffe' in sed.get_netdata_prefixes()) + + +verify_within(check_r3_prefix_added_on_all_nodes, 2) +verify_versions_incremented() + +r1.add_prefix('fd00:1::/64', 'os') +r1.register_netdata() +verify_within(check_r1_prefix_added_on_all_nodes, 2) +verify_versions_incremented() + +r2.add_prefix('fd00:2::/64', 'po', 'high') +r2.register_netdata() +verify_within(check_r2_prefix_added_on_all_nodes, 2) +verify_stabe_version_incremented() + +r1.remove_prefix('fd00:1::/64') +r1.register_netdata() +verify_within(check_r1_prefix_removed_on_all_nodes, 2) +verify_versions_incremented() + +r2.remove_prefix('fd00:2::/64') +r2.register_netdata() +verify_within(check_r2_prefix_removed_on_all_nodes, 2) +verify_stabe_version_incremented() + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Repeat the same checks with r3 also adding ``fd00:2::/64` + +r3.add_prefix('fd00:2::/64', 'paos') +r3.register_netdata() + + +def check_new_r3_prefix_added_on_all_nodes(): + for node in [r1, r2, r3]: + verify('fd00:2:0:0::/64 paos med ' + r3_rloc in node.get_netdata_prefixes()) + verify('fd00:2:0:0::/64 paos med fffe' in sed.get_netdata_prefixes()) + + +verify_within(check_new_r3_prefix_added_on_all_nodes, 2) +verify_versions_incremented() + +r1.add_prefix('fd00:1::/64', 'os') +r1.register_netdata() +verify_within(check_r1_prefix_added_on_all_nodes, 2) +verify_versions_incremented() + +r2.add_prefix('fd00:2::/64', 'po', 'high') +r2.register_netdata() +verify_within(check_r2_prefix_added_on_all_nodes, 2) +verify_stabe_version_incremented() + +r1.remove_prefix('fd00:1::/64') +r1.register_netdata() +verify_within(check_r1_prefix_removed_on_all_nodes, 2) +verify_versions_incremented() + +r2.remove_prefix('fd00:2::/64') +r2.register_netdata() +verify_within(check_r2_prefix_removed_on_all_nodes, 2) +verify_stabe_version_incremented() + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Repeat the same checks with r3 adding the two prefixes as temporary. + +r3.remove_prefix('fd00:1::/64') +r3.remove_prefix('fd00:2::/64') +r3.add_prefix('fd00:1::/64', 'pao') +r3.add_prefix('fd00:2::/64', 'pao') +r3.register_netdata() + + +def check_r3_prefixes_as_temp_added_on_all_nodes(): + for node in [r1, r2, r3]: + prefixes = node.get_netdata_prefixes() + verify('fd00:1:0:0::/64 pao med ' + r3_rloc in prefixes) + verify('fd00:1:0:0::/64 pao med ' + r3_rloc in prefixes) + verify(len(sed.get_netdata_prefixes()) == 0) + + +verify_within(check_r3_prefixes_as_temp_added_on_all_nodes, 2) +verify_versions_incremented() + +r1.add_prefix('fd00:1::/64', 'os') +r1.register_netdata() +verify_within(check_r1_prefix_added_on_all_nodes, 2) +verify_versions_incremented() + +r2.add_prefix('fd00:2::/64', 'po', 'high') +r2.register_netdata() +verify_within(check_r2_prefix_added_on_all_nodes, 2) +verify_stabe_version_incremented() + +r1.remove_prefix('fd00:1::/64') +r1.register_netdata() +verify_within(check_r1_prefix_removed_on_all_nodes, 2) +verify_versions_incremented() + +r2.remove_prefix('fd00:2::/64') +r2.register_netdata() +verify_within(check_r2_prefix_removed_on_all_nodes, 2) +verify_stabe_version_incremented() + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Finally repeat the same checks with r3 adding the two prefixes +# as off-mesh route prefix. + +r3.remove_prefix('fd00:1::/64') +r3.remove_prefix('fd00:2::/64') +r3.add_route('fd00:1::/64', 's') +r3.add_route('fd00:2::/64', '-') +r3.register_netdata() + + +def check_r3_routes_added_on_all_nodes(): + for node in [r1, r2, r3]: + routes = node.get_netdata_routes() + verify('fd00:1:0:0::/64 s med ' + r3_rloc in routes) + verify('fd00:2:0:0::/64 med ' + r3_rloc in routes) + verify('fd00:1:0:0::/64 s med fffe' in sed.get_netdata_routes()) + verify(not 'fd00:1:0:0::/64 med fffe' in sed.get_netdata_routes()) + + +verify_within(check_r3_routes_added_on_all_nodes, 2) +verify_versions_incremented() + +r1.add_prefix('fd00:1::/64', 'os') +r1.register_netdata() +verify_within(check_r1_prefix_added_on_all_nodes, 2) +verify_versions_incremented() + +r2.add_prefix('fd00:2::/64', 'po', 'high') +r2.register_netdata() +verify_within(check_r2_prefix_added_on_all_nodes, 2) +verify_stabe_version_incremented() + +r1.remove_prefix('fd00:1::/64') +r1.register_netdata() +verify_within(check_r1_prefix_removed_on_all_nodes, 2) +verify_versions_incremented() + +r2.remove_prefix('fd00:2::/64') +r2.register_netdata() +verify_within(check_r2_prefix_removed_on_all_nodes, 2) +verify_stabe_version_incremented() + +# ----------------------------------------------------------------------------------------------------------------------- +# Test finished + +cli.Node.finalize_all_nodes() + +print('\'{}\' passed.'.format(test_name)) diff --git a/tests/toranj/cli/test-601-channel-manager-channel-change.py b/tests/toranj/cli/test-601-channel-manager-channel-change.py new file mode 100755 index 000000000..cb67d64f6 --- /dev/null +++ b/tests/toranj/cli/test-601-channel-manager-channel-change.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2022, 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. + +from cli import verify +from cli import verify_within +import cli +import time + +# ----------------------------------------------------------------------------------------------------------------------- +# Test description: +# +# Verifies `ChannelManager` channel change process. +# +# Network Topology: +# +# r1 --- r2 --- r3 +# /\ | | +# / \ | | +# sc1 ec1 sc2 sc3 +# + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print('-' * 120) +print('Starting \'{}\''.format(test_name)) + +# ----------------------------------------------------------------------------------------------------------------------- +# Creating `cli.Node` instances + +speedup = 20 +cli.Node.set_time_speedup_factor(speedup) + +r1 = cli.Node() +r2 = cli.Node() +r3 = cli.Node() +sc1 = cli.Node() +ec1 = cli.Node() +sc2 = cli.Node() +sc3 = cli.Node() + +nodes = [r1, r2, r3, sc1, ec1, sc2, sc3] + +# ----------------------------------------------------------------------------------------------------------------------- +# Form topology + +r1.allowlist_node(r2) +r1.allowlist_node(sc1) +r1.allowlist_node(ec1) + +r2.allowlist_node(r1) +r2.allowlist_node(r3) +r2.allowlist_node(sc2) + +r3.allowlist_node(r2) +r3.allowlist_node(sc3) + +sc1.allowlist_node(r1) +ec1.allowlist_node(r1) +sc2.allowlist_node(r2) +sc3.allowlist_node(r3) + +r1.form('chan-man', channel=11) +r2.join(r1) +r3.join(r1) +sc1.join(r1, cli.JOIN_TYPE_SLEEPY_END_DEVICE) +ec1.join(r1, cli.JOIN_TYPE_END_DEVICE) +sc2.join(r1, cli.JOIN_TYPE_SLEEPY_END_DEVICE) +sc3.join(r1, cli.JOIN_TYPE_SLEEPY_END_DEVICE) + +sc1.set_pollperiod(500) +sc2.set_pollperiod(500) +sc3.set_pollperiod(500) + +verify(r1.get_state() == 'leader') +verify(r2.get_state() == 'router') +verify(r3.get_state() == 'router') +verify(ec1.get_state() == 'child') +verify(sc1.get_state() == 'child') +verify(sc2.get_state() == 'child') +verify(sc3.get_state() == 'child') + +# ----------------------------------------------------------------------------------------------------------------------- +# Test Implementation + +result = cli.Node.parse_list(r1.cli('channel manager')) +verify(result['channel'] == '0') +verify(result['auto'] == '0') + +r1.cli('channel manager delay 4') +verify(int(r1.cli('channel manager delay')[0]) == 4) + +channel = 11 + + +def check_channel_on_all_nodes(): + verify(all(int(node.get_channel()) == channel for node in nodes)) + + +verify_within(check_channel_on_all_nodes, 10) + +# Request a channel change to channel 13 from router r1. Verify +# that all nodes switch to the new channel. + +channel = 13 +r1.cli('channel manager change', channel) +verify_within(check_channel_on_all_nodes, 10) + +# Request same channel change on multiple routers at the same time. + +channel = 14 +r1.cli('channel manager change', channel) +r2.cli('channel manager change', channel) +r3.cli('channel manager change', channel) +verify_within(check_channel_on_all_nodes, 10) + +# Request different channel changes from same router back-to-back. + +channel = 15 +r1.cli('channel manager change', channel) +time.sleep(1 / speedup) +channel = 16 +r1.cli('channel manager change', channel) +verify_within(check_channel_on_all_nodes, 10) + +# Request different channels from two routers (r1 and r2). +# We increase delay on r1 to make sure r1 is in middle of +# channel when new change is requested from r2. + +r1.cli('channel manager delay 20') +r1.cli('channel manager change 17') +time.sleep(5 / speedup) +verify_within(check_channel_on_all_nodes, 10) +channael = 18 +r2.cli('channel manager change', channel) +verify_within(check_channel_on_all_nodes, 10) + +# ----------------------------------------------------------------------------------------------------------------------- +# Test finished + +cli.Node.finalize_all_nodes() + +print('\'{}\' passed.'.format(test_name)) diff --git a/tests/toranj/cli/test-602-channel-manager-channel-select.py b/tests/toranj/cli/test-602-channel-manager-channel-select.py new file mode 100755 index 000000000..0ddf3584b --- /dev/null +++ b/tests/toranj/cli/test-602-channel-manager-channel-select.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2022, 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. + +from cli import verify +from cli import verify_within +import cli +import time + +# ----------------------------------------------------------------------------------------------------------------------- +# Test description: +# +# Verifies `ChannelManager` channel selection procedure + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print('-' * 120) +print('Starting \'{}\''.format(test_name)) + +# ----------------------------------------------------------------------------------------------------------------------- +# Creating `cli.Node` instances + +# Run the test with 10,000 time speedup factor +speedup = 10000 +cli.Node.set_time_speedup_factor(speedup) + +node = cli.Node() + +# ----------------------------------------------------------------------------------------------------------------------- +# Form topology + +node.form('chan-sel', channel=24) + +verify(node.get_state() == 'leader') + +# ----------------------------------------------------------------------------------------------------------------------- +# Test Implementation + +channel = 24 + + +def check_channel(): + verify(int(node.get_channel()) == channel) + + +check_channel() + +all_channels_mask = int('0x7fff800', 0) +chan_12_to_15_mask = int('0x000f000', 0) +chan_15_to_17_mask = int('0x0038000', 0) + +# Set supported channel mask to be all channels +node.cli('channel manager supported', all_channels_mask) + +# Sleep for 4.5 second with speedup factor of 10,000 this is more than 12 +# hours. We sleep instead of immediately checking the sample counter in +# order to not add more actions/events into simulation (specially since +# we are running at very high speedup). +time.sleep(4.5) + +result = cli.Node.parse_list(node.cli('channel monitor')[:5]) +verify(result['enabled'] == '1') +verify(int(result['count']) > 970) + +# Issue a channel-select with quality check enabled, and verify that no +# action is taken. + +node.cli('channel manager select 0') +result = cli.Node.parse_list(node.cli('channel manager')) +verify(result['channel'] == '0') + +# Issue a channel-select with quality check disabled, verify that channel +# is switched to channel 11. + +node.cli('channel manager select 1') +result = cli.Node.parse_list(node.cli('channel manager')) +verify(result['channel'] == '11') +channel = 11 +verify_within(check_channel, 2) + +# Set channels 12-15 as favorable and request a channel select, verify +# that channel is switched to 12. +# +# Even though 11 would be best, quality difference between 11 and 12 +# is not high enough for selection algorithm to pick an unfavored +# channel. + +node.cli('channel manager favored', chan_12_to_15_mask) + +channel = 25 +node.cli('channel manager change', channel) +verify_within(check_channel, 2) + +node.cli('channel manager select 1') +result = cli.Node.parse_list(node.cli('channel manager')) +verify(result['channel'] == '12') +channel = 12 +verify_within(check_channel, 2) + +# Set channels 15-17 as favorables and request a channel select, +# verify that channel is switched to 11. +# +# This time the quality difference between 11 and 15 should be high +# enough for selection algorithm to pick the best though unfavored +# channel (i.e., channel 11). + +channel = 25 +node.cli('channel manager change', channel) +verify_within(check_channel, 2) + +node.cli('channel manager favored', chan_15_to_17_mask) + +node.cli('channel manager select 1') +result = cli.Node.parse_list(node.cli('channel manager')) +verify(result['channel'] == '11') +channel = 11 +verify_within(check_channel, 2) + +# Set channels 12-15 as favorable and request a channel select, verify +# that channel is not switched. + +node.cli('channel manager favored', chan_12_to_15_mask) + +node.cli('channel manager select 1') +result = cli.Node.parse_list(node.cli('channel manager')) +verify(result['channel'] == '11') +channel = 11 +verify_within(check_channel, 2) + +# Starting from channel 12 and issuing a channel select (which would +# pick 11 as best channel). However, since quality difference between +# current channel 12 and new best channel 11 is not large enough, no +# action should be taken. + +channel = 12 +node.cli('channel manager change', channel) +verify_within(check_channel, 2) + +node.cli('channel manager favored', all_channels_mask) + +node.cli('channel manager select 1') +result = cli.Node.parse_list(node.cli('channel manager')) +verify(result['channel'] == '12') +verify_within(check_channel, 2) + +# ----------------------------------------------------------------------------------------------------------------------- +# Test finished + +cli.Node.finalize_all_nodes() + +print('\'{}\' passed.'.format(test_name)) diff --git a/tests/toranj/cli/test-603-channel-announce-recovery.py b/tests/toranj/cli/test-603-channel-announce-recovery.py new file mode 100755 index 000000000..6482aa989 --- /dev/null +++ b/tests/toranj/cli/test-603-channel-announce-recovery.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2022, 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. + +from cli import verify +from cli import verify_within +import cli +import time + +# ----------------------------------------------------------------------------------------------------------------------- +# Test description: +# +# Orphaned node attach through MLE Announcement after channel change. + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print('-' * 120) +print('Starting \'{}\''.format(test_name)) + +# ----------------------------------------------------------------------------------------------------------------------- +# Creating `cli.Node` instances + +speedup = 40 +cli.Node.set_time_speedup_factor(speedup) + +router = cli.Node() +c1 = cli.Node() +c2 = cli.Node() + +# ----------------------------------------------------------------------------------------------------------------------- +# Form topology + +router.form('announce-tst', channel=11) + +c1.join(router, cli.JOIN_TYPE_SLEEPY_END_DEVICE) +c2.join(router, cli.JOIN_TYPE_SLEEPY_END_DEVICE) +c1.set_pollperiod(500) +c2.set_pollperiod(500) + +verify(router.get_state() == 'leader') +verify(c1.get_state() == 'child') +verify(c2.get_state() == 'child') + +# ----------------------------------------------------------------------------------------------------------------------- +# Test Implementation + +# Stop c2 + +c2.thread_stop() + +# Switch the rest of network to channel 26 +router.cli('channel manager change 26') + + +def check_channel_changed_to_26_on_r1_c1(): + for node in [router, c1]: + verify(int(node.get_channel()) == 26) + + +verify_within(check_channel_changed_to_26_on_r1_c1, 10) + +# Now re-enable c2 and verify that it does attach to router and is on +# channel 26. c2 would go through the ML Announce recovery. + +c2.thread_start() +verify(int(c2.get_channel()) == 11) + +# wait for 20s for c2 to be attached/associated + + +def check_c2_is_attched(): + verify(c2.get_state() == 'child') + + +verify_within(check_c2_is_attched, 20) + +# Check that c2 is now on channel 26. +verify(int(c2.get_channel()) == 26) + +# ----------------------------------------------------------------------------------------------------------------------- +# Test finished + +cli.Node.finalize_all_nodes() + +print('\'{}\' passed.'.format(test_name)) diff --git a/tests/toranj/cli/test-700-multi-radio-join.py b/tests/toranj/cli/test-700-multi-radio-join.py new file mode 100755 index 000000000..236a0f09d --- /dev/null +++ b/tests/toranj/cli/test-700-multi-radio-join.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2022, 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. + +from cli import verify +from cli import verify_within +import cli +import time + +# ----------------------------------------------------------------------------------------------------------------------- +# Test description: This test covers joining of nodes with different radio links +# +# Parent node with trel and 15.4 with 3 children. +# +# parent +# (trel + 15.4) +# / | \ +# / | \ +# / | \ +# c1 c2 c3 +# (15.4) (trel) (trel+15.4) + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print('-' * 120) +print('Starting \'{}\''.format(test_name)) + +# ----------------------------------------------------------------------------------------------------------------------- +# Creating `cli.Node` instances + +speedup = 10 +cli.Node.set_time_speedup_factor(speedup) + +parent = cli.Node(cli.RADIO_15_4_TREL) +c1 = cli.Node(cli.RADIO_15_4) +c2 = cli.Node(cli.RADIO_TREL) +c3 = cli.Node(cli.RADIO_15_4_TREL) + +# ----------------------------------------------------------------------------------------------------------------------- +# Test Implementation + +# Verify that each node supports the correct radio links. + +verify(parent.multiradio_get_radios() == '[15.4, TREL]') +verify(c1.multiradio_get_radios() == '[15.4]') +verify(c2.multiradio_get_radios() == '[TREL]') +verify(c3.multiradio_get_radios() == '[15.4, TREL]') + +parent.form("multi-radio") + +c1.join(parent, cli.JOIN_TYPE_END_DEVICE) +c2.join(parent, cli.JOIN_TYPE_END_DEVICE) +c3.join(parent, cli.JOIN_TYPE_END_DEVICE) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Verify that parent correctly learns about all the children and their +# supported radio links. + +c1_ext_addr = c1.get_ext_addr() +c2_ext_addr = c2.get_ext_addr() +c3_ext_addr = c3.get_ext_addr() + +neighbor_radios = parent.multiradio_get_neighbor_list() +verify(len(neighbor_radios) == 3) +for entry in neighbor_radios: + info = cli.Node.parse_multiradio_neighbor_entry(entry) + radios = info['Radios'] + if info['ExtAddr'] == c1_ext_addr: + verify(int(info['RLOC16'], 16) == int(c1.get_rloc16(), 16)) + verify(len(radios) == 1) + verify('15.4' in radios) + elif info['ExtAddr'] == c2_ext_addr: + verify(int(info['RLOC16'], 16) == int(c2.get_rloc16(), 16)) + verify(len(radios) == 1) + verify('TREL' in radios) + elif info['ExtAddr'] == c3_ext_addr: + verify(int(info['RLOC16'], 16) == int(c3.get_rloc16(), 16)) + verify(len(radios) == 2) + verify('15.4' in radios) + verify('TREL' in radios) + else: + verify(False) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Reset the parent and check that all children are attached back successfully + +del parent +parent = cli.Node(cli.RADIO_15_4_TREL, index=1) +parent.interface_up() +parent.thread_start() + + +def check_all_children_are_attached(): + verify(len(parent.get_child_table()) == 3) + + +verify_within(check_all_children_are_attached, 10) + +# ----------------------------------------------------------------------------------------------------------------------- +# Test finished + +cli.Node.finalize_all_nodes() + +print('\'{}\' passed.'.format(test_name)) diff --git a/tests/toranj/cli/test-701-multi-radio-probe.py b/tests/toranj/cli/test-701-multi-radio-probe.py new file mode 100755 index 000000000..bc0f128c9 --- /dev/null +++ b/tests/toranj/cli/test-701-multi-radio-probe.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2022, 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. + +from cli import verify +from cli import verify_within +import cli +import time + +# ----------------------------------------------------------------------------------------------------------------------- +# Test description: This test covers behavior of device after trel network is temporarily disabled +# and rediscovery of trel radio using probe mechanism. +# +# r1 ---------- r2 +# (15.4+trel) (15.4+trel) +# +# On r2 we disable trel temporarily. +# + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print('-' * 120) +print('Starting \'{}\''.format(test_name)) + +# ----------------------------------------------------------------------------------------------------------------------- +# Creating `cli.Node` instances + +speedup = 10 +cli.Node.set_time_speedup_factor(speedup) + +r1 = cli.Node(cli.RADIO_15_4_TREL) +r2 = cli.Node(cli.RADIO_15_4_TREL) + +# ----------------------------------------------------------------------------------------------------------------------- +# Build network topology + +r1.form("prove-discover") +r2.join(r1) + +verify(r1.get_state() == 'leader') +verify(r2.get_state() == 'router') + +# ----------------------------------------------------------------------------------------------------------------------- +# Test Implementation + +high_preference_threshold = 220 +min_preference_threshold = 0 + +verify(r1.multiradio_get_radios() == '[15.4, TREL]') +verify(r2.multiradio_get_radios() == '[15.4, TREL]') + +r1_rloc = int(r1.get_rloc16(), 16) +r2_rloc = int(r2.get_rloc16(), 16) + +r1_ml_addr = r1.get_mleid_ip_addr() +r2_ml_addr = r2.get_mleid_ip_addr() + +# Wait till routes are discovered. + + +def check_r1_router_table(): + table = r1.get_router_table() + verify(len(table) == 2) + for entry in table: + verify(int(entry['RLOC16'], 0) == r1_rloc or int(entry['Link']) == 1) + + +verify_within(check_r1_router_table, 120) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Verify that r1 detected both TREL & 15.4 as supported radios by r2 + + +def check_r1_sees_r2_has_two_radio_links(): + neighbor_radios = r1.multiradio_get_neighbor_list() + verify(len(neighbor_radios) == 1) + info = cli.Node.parse_multiradio_neighbor_entry(neighbor_radios[0]) + verify(int(info['RLOC16'], 16) == r2_rloc) + radios = info['Radios'] + verify(len(radios) == 2) + verify('15.4' in radios) + verify('TREL' in radios) + + +cli.verify_within(check_r1_sees_r2_has_two_radio_links, 10) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Ping r2 from r1 and verify that r1 prefers trel radio link for +# sending to r2. + +r1.ping(r2_ml_addr, count=5) + +neighbor_radios = r1.multiradio_get_neighbor_list() +verify(len(neighbor_radios) == 1) +info = cli.Node.parse_multiradio_neighbor_entry(neighbor_radios[0]) +radios = info['Radios'] +verify(radios['TREL'] >= high_preference_threshold) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Now temporary filter trel link on r2 and ping again. We expect that +# r1 to quickly detect that trel is no longer supported by r2 and +# prefer 15.4 for tx to r2. + +r2.cli('trel filter enable') +verify(r2.cli('trel filter')[0] == 'Enabled') + +r1.udp_open() +for count in range(5): + r1.udp_send(r2_ml_addr, 12345, 'hi_r2_from_r1') + + +def check_r1_does_not_prefer_trel_for_r2(): + neighbor_radios = r1.multiradio_get_neighbor_list() + verify(len(neighbor_radios) == 1) + info = cli.Node.parse_multiradio_neighbor_entry(neighbor_radios[0]) + radios = info['Radios'] + verify(radios['TREL'] <= min_preference_threshold) + + +verify_within(check_r1_does_not_prefer_trel_for_r2, 10) + +# Check that we can send between r1 and r2 (now all tx should use 15.4) + +r1.ping(r2_ml_addr, count=5, verify_success=False) +r1.ping(r2_ml_addr, count=5) + +neighbor_radios = r1.multiradio_get_neighbor_list() +verify(len(neighbor_radios) == 1) +info = cli.Node.parse_multiradio_neighbor_entry(neighbor_radios[0]) +radios = info['Radios'] +verify(radios['TREL'] <= min_preference_threshold) +verify(radios['15.4'] >= high_preference_threshold) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Enable trel back on r2, start sending traffic from r1 to r2 +# The probe mechanism should kick and detect that r2 has trel again. + +r2.cli('trel filter disable') +verify(r2.cli('trel filter')[0] == 'Disabled') + +r2.udp_open() +for count in range(80): + r2.udp_send(r1_ml_addr, 12345, 'hi_r1_from_r2') + + +def check_r1_again_prefers_trel_for_r2(): + neighbor_radios = r1.multiradio_get_neighbor_list() + verify(len(neighbor_radios) == 1) + info = cli.Node.parse_multiradio_neighbor_entry(neighbor_radios[0]) + radios = info['Radios'] + verify(radios['TREL'] >= high_preference_threshold) + + +verify_within(check_r1_again_prefers_trel_for_r2, 10) + +# ----------------------------------------------------------------------------------------------------------------------- +# Test finished + +cli.Node.finalize_all_nodes() + +print('\'{}\' passed.'.format(test_name)) diff --git a/tests/toranj/cli/test-702-multi-radio-discover-by-rx.py b/tests/toranj/cli/test-702-multi-radio-discover-by-rx.py new file mode 100755 index 000000000..8e4a396c5 --- /dev/null +++ b/tests/toranj/cli/test-702-multi-radio-discover-by-rx.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2022, 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. + +from cli import verify +from cli import verify_within +import cli +import time + +# ----------------------------------------------------------------------------------------------------------------------- +# Test description: This test covers behavior of device after TREL network is temporarily disabled +# and rediscovery of TREL by receiving message over that radio from the neighbor. +# +# r1 ---------- r2 +# (15.4+trel) (15.4+trel) +# +# On r2 we disable trel temporarily. +# + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print('-' * 120) +print('Starting \'{}\''.format(test_name)) + +# ----------------------------------------------------------------------------------------------------------------------- +# Creating `cli.Node` instances + +speedup = 10 +cli.Node.set_time_speedup_factor(speedup) + +r1 = cli.Node(cli.RADIO_15_4_TREL) +r2 = cli.Node(cli.RADIO_15_4_TREL) + +# ----------------------------------------------------------------------------------------------------------------------- +# Build network topology + +r1.form("discover-by-rx") +r2.join(r1) + +verify(r1.get_state() == 'leader') +verify(r2.get_state() == 'router') + +# ----------------------------------------------------------------------------------------------------------------------- +# Test Implementation + +high_preference_threshold = 220 +min_preference_threshold = 0 + +verify(r1.multiradio_get_radios() == '[15.4, TREL]') +verify(r2.multiradio_get_radios() == '[15.4, TREL]') + +r1_rloc = int(r1.get_rloc16(), 16) +r2_rloc = int(r2.get_rloc16(), 16) + +r1_ml_addr = r1.get_mleid_ip_addr() +r2_ml_addr = r2.get_mleid_ip_addr() + +# Wait till routes are discovered. + + +def check_r1_router_table(): + table = r1.get_router_table() + verify(len(table) == 2) + for entry in table: + verify(int(entry['RLOC16'], 0) == r1_rloc or int(entry['Link']) == 1) + + +verify_within(check_r1_router_table, 120) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Verify that r1 detected both TREL & 15.4 as supported radios by r2 + + +def check_r1_sees_r2_has_two_radio_links(): + neighbor_radios = r1.multiradio_get_neighbor_list() + verify(len(neighbor_radios) == 1) + info = cli.Node.parse_multiradio_neighbor_entry(neighbor_radios[0]) + verify(int(info['RLOC16'], 16) == r2_rloc) + radios = info['Radios'] + verify(len(radios) == 2) + verify('15.4' in radios) + verify('TREL' in radios) + + +cli.verify_within(check_r1_sees_r2_has_two_radio_links, 10) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Ping r2 from r1 and verify that r1 prefers trel radio link for +# sending to r2. + +r1.ping(r2_ml_addr, count=5) + +neighbor_radios = r1.multiradio_get_neighbor_list() +verify(len(neighbor_radios) == 1) +info = cli.Node.parse_multiradio_neighbor_entry(neighbor_radios[0]) +radios = info['Radios'] +verify(radios['TREL'] >= high_preference_threshold) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Now temporary filter trel link on r2 and ping again. We expect that +# r1 to quickly detect that trel is no longer supported by r2 and +# prefer 15.4 for tx to r2. + +r2.cli('trel filter enable') +verify(r2.cli('trel filter')[0] == 'Enabled') + +r1.udp_open() +for count in range(5): + r1.udp_send(r2.get_mleid_ip_addr(), 12345, 'hi_r2_from_r1') + + +def check_r1_does_not_prefer_trel_for_r2(): + neighbor_radios = r1.multiradio_get_neighbor_list() + verify(len(neighbor_radios) == 1) + info = cli.Node.parse_multiradio_neighbor_entry(neighbor_radios[0]) + radios = info['Radios'] + verify(radios['TREL'] <= min_preference_threshold) + + +verify_within(check_r1_does_not_prefer_trel_for_r2, 10) + +# Check that we can send between r1 and r2 (now all tx should use 15.4) + +r1.ping(r2.get_mleid_ip_addr(), count=5, verify_success=False) +r1.ping(r2.get_mleid_ip_addr(), count=5) + +neighbor_radios = r1.multiradio_get_neighbor_list() +verify(len(neighbor_radios) == 1) +info = cli.Node.parse_multiradio_neighbor_entry(neighbor_radios[0]) +radios = info['Radios'] +verify(radios['TREL'] <= min_preference_threshold) +verify(radios['15.4'] >= high_preference_threshold) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Enable trel back on r2, start sending traffic from r2 to r1. +# r2 would use probe mechanism to discover that trel is enabled again +# r1 should notice new rx on trel and update trel preference for r1 + +r2.cli('trel filter disable') +verify(r2.cli('trel filter')[0] == 'Disabled') + +for count in range(80): + r1.udp_send(r2.get_mleid_ip_addr(), 12345, 'hi_r2_from_r1_probe') + + +def check_r1_again_prefers_trel_for_r2(): + neighbor_radios = r1.multiradio_get_neighbor_list() + verify(len(neighbor_radios) == 1) + info = cli.Node.parse_multiradio_neighbor_entry(neighbor_radios[0]) + radios = info['Radios'] + verify(radios['TREL'] >= high_preference_threshold) + + +verify_within(check_r1_again_prefers_trel_for_r2, 10) + +# ----------------------------------------------------------------------------------------------------------------------- +# Test finished + +cli.Node.finalize_all_nodes() + +print('\'{}\' passed.'.format(test_name)) diff --git a/tests/toranj/cli/test-703-multi-radio-mesh-header-msg.py b/tests/toranj/cli/test-703-multi-radio-mesh-header-msg.py new file mode 100755 index 000000000..d20286734 --- /dev/null +++ b/tests/toranj/cli/test-703-multi-radio-mesh-header-msg.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2022, 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. + +from cli import verify +from cli import verify_within +import cli +import time + +# ----------------------------------------------------------------------------------------------------------------------- +# Test description: This test covers behavior of MeshHeader messages (message sent over multi-hop) when the +# underlying devices (in the path) support different radio types with different frame MTU length. +# +# r1 --------- r2 ---------- r3 +# (trel) (15.4+trel) (15.4) +# | +# | +# c2 (15.4) +# + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print('-' * 120) +print('Starting \'{}\''.format(test_name)) + +# ----------------------------------------------------------------------------------------------------------------------- +# Creating `cli.Node` instances + +speedup = 10 +cli.Node.set_time_speedup_factor(speedup) + +r1 = cli.Node(cli.RADIO_TREL) +r2 = cli.Node(cli.RADIO_15_4_TREL) +r3 = cli.Node(cli.RADIO_15_4) +c2 = cli.Node(cli.RADIO_15_4) + +# ----------------------------------------------------------------------------------------------------------------------- +# Build network topology + +r1.allowlist_node(r2) + +r2.allowlist_node(r1) +r2.allowlist_node(r3) +r2.allowlist_node(c2) + +r3.allowlist_node(r2) + +c2.allowlist_node(r2) + +r1.form("mesh-header") + +r2.join(r1) +r3.join(r2) +c2.join(r2, cli.JOIN_TYPE_END_DEVICE) + +verify(r1.get_state() == 'leader') +verify(r2.get_state() == 'router') +verify(r3.get_state() == 'router') +verify(c2.get_state() == 'child') + +# ----------------------------------------------------------------------------------------------------------------------- +# Test Implementation + +verify(r1.multiradio_get_radios() == '[TREL]') +verify(r2.multiradio_get_radios() == '[15.4, TREL]') +verify(r3.multiradio_get_radios() == '[15.4]') + +r1_rloc = int(r1.get_rloc16(), 16) + +r1_ml_addr = r1.get_mleid_ip_addr() +r3_ml_addr = r3.get_mleid_ip_addr() + +# Wait till routes are discovered. + + +def check_r1_router_table(): + table = r1.get_router_table() + verify(len(table) == 3) + for entry in table: + verify(int(entry['RLOC16'], 0) == r1_rloc or int(entry['Link']) == 1 or int(entry['Next Hop']) != 63) + + +verify_within(check_r1_router_table, 120) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Ping r3 from r2 using large message size. Such a message would be sent as a +# Mesh Header message. Note that the origin r1 is a trel-only device +# whereas the final destination r3 is a 15.4-only device. The originator +# should use smaller MTU size (which then fragment the message into +# multiple frames) otherwise the 15.4 link won't be able to handle it. + +r1.ping(r3_ml_addr, size=1000, count=2) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Ping r1 from c2 using large message size. + +c2.ping(r1_ml_addr, size=1000, count=2) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Ping r1 from r3 + +r3.ping(r1_ml_addr, size=1000, count=2) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Finally ping r1 and r3 from r2. + +r2.ping(r1_ml_addr, size=1000, count=2) +r2.ping(r3_ml_addr, size=1000, count=2) + +# ----------------------------------------------------------------------------------------------------------------------- +# Test finished + +cli.Node.finalize_all_nodes() + +print('\'{}\' passed.'.format(test_name)) diff --git a/tests/toranj/cli/test-704-multi-radio-scan.py b/tests/toranj/cli/test-704-multi-radio-scan.py new file mode 100755 index 000000000..697f08dd8 --- /dev/null +++ b/tests/toranj/cli/test-704-multi-radio-scan.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2022, 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. + +from cli import verify +from cli import verify_within +import cli +import time + +# ----------------------------------------------------------------------------------------------------------------------- +# Test description: Test active scan with nodes supporting different radios +# + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print('-' * 120) +print('Starting \'{}\''.format(test_name)) + +# ----------------------------------------------------------------------------------------------------------------------- +# Creating `cli.Node` instances + +speedup = 10 +cli.Node.set_time_speedup_factor(speedup) + +n1 = cli.Node(cli.RADIO_15_4) +n2 = cli.Node(cli.RADIO_TREL) +n3 = cli.Node(cli.RADIO_15_4_TREL) +s1 = cli.Node(cli.RADIO_15_4) +s2 = cli.Node(cli.RADIO_TREL) +s3 = cli.Node(cli.RADIO_15_4_TREL) + +# ----------------------------------------------------------------------------------------------------------------------- +# Build network topology + +n1.form("n1", channel='20') +n2.form("n2", channel='21') +n3.form("n3", channel='22') + +# ----------------------------------------------------------------------------------------------------------------------- +# Test Implementation + +verify(n1.multiradio_get_radios() == '[15.4]') +verify(n2.multiradio_get_radios() == '[TREL]') +verify(n3.multiradio_get_radios() == '[15.4, TREL]') +verify(s1.multiradio_get_radios() == '[15.4]') +verify(s2.multiradio_get_radios() == '[TREL]') +verify(s3.multiradio_get_radios() == '[15.4, TREL]') + + +def verify_scan_result_conatins_nodes(scan_result, nodes): + table = cli.Node.parse_table(scan_result) + verify(len(table) >= len(nodes)) + for node in nodes: + ext_addr = node.get_ext_addr() + for entry in table: + if entry['MAC Address'] == ext_addr: + verify(int(entry['PAN'], 16) == int(node.get_panid(), 16)) + verify(int(entry['Ch']) == int(node.get_channel())) + break + else: + verify(False) + + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Scan by scanner nodes (no network) + +# Scan by s1 (15.4 only), expect to see n1(15.4) and n3(15.4+trel) + +verify_scan_result_conatins_nodes(s1.cli('scan'), [n1, n3]) + +# Scan by s2 (trel only), expect to see n2(trel) and n3(15.4+trel) +verify_scan_result_conatins_nodes(s2.cli('scan'), [n2, n3]) + +# Scan by s3 (trel+15.4), expect to see all nodes +verify_scan_result_conatins_nodes(s3.cli('scan'), [n1, n2, n3]) + +# ----------------------------------------------------------------------------------------------------------------------- +# Test finished + +cli.Node.finalize_all_nodes() + +print('\'{}\' passed.'.format(test_name)) diff --git a/tests/toranj/cli/test-705-multi-radio-discover-scan.py b/tests/toranj/cli/test-705-multi-radio-discover-scan.py new file mode 100755 index 000000000..4206280c8 --- /dev/null +++ b/tests/toranj/cli/test-705-multi-radio-discover-scan.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2022, 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. + +from cli import verify +from cli import verify_within +import cli +import time + +# ----------------------------------------------------------------------------------------------------------------------- +# Test description: Test MLE discover scan with nodes supporting different radios +# + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print('-' * 120) +print('Starting \'{}\''.format(test_name)) + +# ----------------------------------------------------------------------------------------------------------------------- +# Creating `cli.Node` instances + +speedup = 10 +cli.Node.set_time_speedup_factor(speedup) + +n1 = cli.Node(cli.RADIO_15_4) +n2 = cli.Node(cli.RADIO_TREL) +n3 = cli.Node(cli.RADIO_15_4_TREL) +s1 = cli.Node(cli.RADIO_15_4) +s2 = cli.Node(cli.RADIO_TREL) +s3 = cli.Node(cli.RADIO_15_4_TREL) + +# ----------------------------------------------------------------------------------------------------------------------- +# Build network topology + +n1.form("n1", channel='20') +n2.form("n2", channel='21') +n3.form("n3", channel='22') + +# ----------------------------------------------------------------------------------------------------------------------- +# Test Implementation + +verify(n1.multiradio_get_radios() == '[15.4]') +verify(n2.multiradio_get_radios() == '[TREL]') +verify(n3.multiradio_get_radios() == '[15.4, TREL]') +verify(s1.multiradio_get_radios() == '[15.4]') +verify(s2.multiradio_get_radios() == '[TREL]') +verify(s3.multiradio_get_radios() == '[15.4, TREL]') + + +def verify_scan_result_conatins_nodes(scan_result, nodes): + table = cli.Node.parse_table(scan_result) + verify(len(table) >= len(nodes)) + for node in nodes: + ext_addr = node.get_ext_addr() + for entry in table: + if entry['MAC Address'] == ext_addr: + verify(int(entry['PAN'], 16) == int(node.get_panid(), 16)) + verify(int(entry['Ch']) == int(node.get_channel())) + break + else: + verify(False) + + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Scan by scanner nodes (no network) + +s1.interface_up() +s2.interface_up() +s3.interface_up() + +# Scan by s1 (15.4 only), expect to see n1(15.4) and n3(15.4+trel) + +verify_scan_result_conatins_nodes(s1.cli('discover'), [n1, n3]) + +# Scan by s2 (trel only), expect to see n2(trel) and n3(15.4+trel) +verify_scan_result_conatins_nodes(s2.cli('discover'), [n2, n3]) + +# Scan by s3 (trel+15.4), expect to see all nodes +verify_scan_result_conatins_nodes(s3.cli('discover'), [n1, n2, n3]) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Scan by the nodes + +# Scan by n1 (15.4 only), expect to see only n3(15.4+trel) +verify_scan_result_conatins_nodes(n1.cli('discover'), [n3]) + +# Scan by n2 (trel only), expect to see only n3(15.4+trel) +verify_scan_result_conatins_nodes(n2.cli('discover'), [n3]) + +# Scan by n3 (15.4+trel), expect to see n1(15.4) and n2(trel) +verify_scan_result_conatins_nodes(n3.cli('discover'), [n1, n2]) + +# ----------------------------------------------------------------------------------------------------------------------- +# Test finished + +cli.Node.finalize_all_nodes() + +print('\'{}\' passed.'.format(test_name)) diff --git a/tests/toranj/openthread-core-toranj-config.h b/tests/toranj/openthread-core-toranj-config.h index 54a12d29d..7ee4ce53c 100644 --- a/tests/toranj/openthread-core-toranj-config.h +++ b/tests/toranj/openthread-core-toranj-config.h @@ -75,6 +75,14 @@ */ #define OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE 1 +/** + * @def OPENTHREAD_CONFIG_IP6_BR_COUNTERS_ENABLE + * + * Define as 1 to enable IPv6 Border Routing counters. + * + */ +#define OPENTHREAD_CONFIG_IP6_BR_COUNTERS_ENABLE 1 + /** * @def OPENTHREAD_CONFIG_COMMISSIONER_ENABLE * @@ -546,6 +554,17 @@ */ #define OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE 1 +/** + * @def OPENTHREAD_CONFIG_CLI_REGISTER_IP6_RECV_CALLBACK + * + * Define as 1 to have CLI register an IPv6 receive callback using `otIp6SetReceiveCallback()`. + * + * This is intended for testing only. Receive callback should be registered for the `otIp6GetBorderRoutingCounters()` + * to count the messages being passed to the callback. + * + */ +#define OPENTHREAD_CONFIG_CLI_REGISTER_IP6_RECV_CALLBACK 1 + #if OPENTHREAD_RADIO /** * @def OPENTHREAD_CONFIG_MAC_SOFTWARE_ACK_TIMEOUT_ENABLE diff --git a/tests/toranj/start.sh b/tests/toranj/start.sh index a7a7ebfc9..6c24d336b 100755 --- a/tests/toranj/start.sh +++ b/tests/toranj/start.sh @@ -39,32 +39,18 @@ cleanup() sudo rm tmp/*.flash tmp/*.data tmp/*.swap >/dev/null 2>&1 sudo rm ./*.log >/dev/null 2>&1 - # Clear any wpantund instances - sudo killall wpantund >/dev/null 2>&1 + if [ "$TORANJ_CLI" != 1 ]; then + # Clear any wpantund instances + sudo killall wpantund >/dev/null 2>&1 - while read -r interface; do - sudo ip link delete "$interface" >/dev/null 2>&1 - done < <(ifconfig 2>/dev/null | grep -o "wpan[0-9]*") - - sudo ip address flush trel + while read -r interface; do + sudo ip link delete "$interface" >/dev/null 2>&1 + done < <(ifconfig 2>/dev/null | grep -o "wpan[0-9]*") + fi sleep 0.3 } -prepare_trel_link() -{ - # Prepares a netif "trel" as virtual eth to use for - # testing "TREL" radio link in POSIX platform. - - echo "Preparing trel netif" - sudo ip link delete trel - sudo ip link add trel type veth peer name trel-peer || die - sudo ip link set trel multicast on || die - sudo ip link set trel up || die - sudo ip link set trel-peer multicast on || die - sudo ip link set trel-peer up || die -} - run() { counter=0 @@ -114,11 +100,8 @@ fi if [ "$TORANJ_RADIO" = "multi" ]; then # Build all combinations ./build.sh "${coverage_option}" "${app_name}"-15.4 || die "${app_name}-15.4 build failed" - (cd ${top_builddir} && make clean) || die "cd and clean failed" ./build.sh "${coverage_option}" "${app_name}"-trel || die "${app_name}-trel build failed" - (cd ${top_builddir} && make clean) || die "cd and clean failed" ./build.sh "${coverage_option}" "${app_name}"-15.4+trel || die "${app_name}-15.4+trel build failed" - (cd ${top_builddir} && make clean) || die "cd and clean failed" else ./build.sh "${coverage_option}" "${app_name}"-"${TORANJ_RADIO}" || die "build failed" fi @@ -126,10 +109,43 @@ fi cleanup if [ "$TORANJ_CLI" = 1 ]; then + + if [ "$TORANJ_RADIO" = "multi" ]; then + run cli/test-700-multi-radio-join.py + run cli/test-701-multi-radio-probe.py + run cli/test-702-multi-radio-discover-by-rx.py + run cli/test-703-multi-radio-mesh-header-msg.py + run cli/test-704-multi-radio-scan.py + run cli/test-705-multi-radio-discover-scan.py + + exit 0 + fi + run cli/test-001-get-set.py run cli/test-002-form.py run cli/test-003-join.py + run cli/test-004-scan.py + run cli/test-005-traffic-router-to-child.py + run cli/test-006-traffic-multi-hop.py + run cli/test-007-off-mesh-route-traffic.py + run cli/test-008-multicast-traffic.py + run cli/test-009-router-table.py + run cli/test-010-partition-merge.py + run cli/test-011-network-data-timeout.py + run cli/test-012-reset-recovery.py + run cli/test-013-address-cache-parent-switch.py + run cli/test-014-address-resolver.py + run cli/test-015-clear-addresss-cache-for-sed.py + run cli/test-016-child-mode-change.py + run cli/test-017-network-data-versions.py run cli/test-400-srp-client-server.py + run cli/test-601-channel-manager-channel-change.py + # Skip the "channel-select" test on a TREL only radio link, since it + # requires energy scan which is not supported in this case. + if [ "$TORANJ_RADIO" != "trel" ]; then + run cli/test-602-channel-manager-channel-select.py + fi + run cli/test-603-channel-announce-recovery.py exit 0 fi