From 255a326b10972097916e1bdc56e98851d625b271 Mon Sep 17 00:00:00 2001 From: Abtin Keshavarzian Date: Tue, 22 Mar 2022 14:28:08 -0700 Subject: [PATCH] [toranj] add support for CLI based test-cases (#7258) This commit updates `toranj` test framework to add support for writing CLI based test-cases (in addition to existing model which uses OT NCP build along with `wpantund`/`wpanctl`). --- .github/workflows/toranj.yml | 43 +- tests/toranj/README.md | 377 +---------- tests/toranj/README_CLI.md | 188 ++++++ tests/toranj/README_NCP.md | 370 ++++++++++ tests/toranj/build.sh | 87 ++- tests/toranj/cli/cli.py | 630 ++++++++++++++++++ tests/toranj/cli/test-001-get-set.py | 90 +++ tests/toranj/cli/test-002-form.py | 78 +++ tests/toranj/cli/test-003-join.py | 80 +++ .../toranj/cli/test-400-srp-client-server.py | 130 ++++ tests/toranj/{ => ncp}/test-001-get-set.py | 0 tests/toranj/{ => ncp}/test-002-form.py | 0 tests/toranj/{ => ncp}/test-003-join.py | 0 tests/toranj/{ => ncp}/test-004-scan.py | 0 .../{ => ncp}/test-005-discover-scan.py | 0 .../test-006-traffic-router-end-device.py | 0 .../test-007-traffic-router-sleepy.py | 0 .../toranj/{ => ncp}/test-008-permit-join.py | 0 .../test-009-insecure-traffic-join.py | 0 .../test-010-on-mesh-prefix-config-gateway.py | 0 .../toranj/{ => ncp}/test-011-child-table.py | 0 .../{ => ncp}/test-012-multi-hop-traffic.py | 0 .../test-013-off-mesh-route-traffic.py | 0 .../{ => ncp}/test-014-ip6-address-add.py | 0 .../test-015-same-prefix-on-multiple-nodes.py | 0 .../{ => ncp}/test-016-neighbor-table.py | 0 .../test-017-parent-reset-child-recovery.py | 0 .../{ => ncp}/test-018-child-supervision.py | 0 .../test-019-inform-previous-parent.py | 0 .../toranj/{ => ncp}/test-020-router-table.py | 0 .../{ => ncp}/test-021-address-cache-table.py | 0 .../test-022-multicast-ip6-address.py | 0 .../{ => ncp}/test-023-multicast-traffic.py | 0 .../{ => ncp}/test-024-partition-merge.py | 0 .../test-025-network-data-timeout.py | 0 .../test-026-slaac-address-wpantund.py | 0 .../{ => ncp}/test-027-child-mode-change.py | 0 .../test-028-router-leader-reset-recovery.py | 0 .../{ => ncp}/test-029-data-poll-interval.py | 0 .../{ => ncp}/test-030-slaac-address-ncp.py | 0 .../test-031-meshcop-joiner-commissioner.py | 0 ...child-attach-with-multiple-ip-addresses.py | 0 .../test-033-mesh-local-prefix-change.py | 0 .../test-034-poor-link-parent-child-attach.py | 0 .../test-035-child-timeout-large-data-poll.py | 0 ...test-036-wpantund-host-route-management.py | 0 ...ntund-auto-add-route-for-on-mesh-prefix.py | 0 .../test-038-clear-address-cache-for-sed.py | 0 .../test-039-address-cache-table-snoop.py | 0 .../test-040-network-data-stable-full.py | 0 .../test-041-lowpan-fragmentation.py | 0 .../test-042-meshcop-joiner-discerner.py | 0 .../test-043-meshcop-joiner-router.py | 0 .../{ => ncp}/test-100-mcu-power-state.py | 0 .../test-600-channel-manager-properties.py | 0 ...test-601-channel-manager-channel-change.py | 0 ...test-602-channel-manager-channel-select.py | 0 ...t-603-channel-manager-announce-recovery.py | 0 .../{ => ncp}/test-700-multi-radio-join.py | 0 .../{ => ncp}/test-701-multi-radio-probe.py | 0 .../test-702-multi-radio-discovery-by-rx.py | 0 .../test-703-multi-radio-mesh-header-msg.py | 0 .../{ => ncp}/test-704-multi-radio-scan.py | 0 .../test-705-multi-radio-discover-scan.py | 0 tests/toranj/{ => ncp}/test-nnn-template.py | 0 tests/toranj/{ => ncp}/wpan.py | 2 +- tests/toranj/start.sh | 142 ++-- 67 files changed, 1783 insertions(+), 434 deletions(-) create mode 100644 tests/toranj/README_CLI.md create mode 100644 tests/toranj/README_NCP.md create mode 100644 tests/toranj/cli/cli.py create mode 100755 tests/toranj/cli/test-001-get-set.py create mode 100755 tests/toranj/cli/test-002-form.py create mode 100755 tests/toranj/cli/test-003-join.py create mode 100755 tests/toranj/cli/test-400-srp-client-server.py rename tests/toranj/{ => ncp}/test-001-get-set.py (100%) rename tests/toranj/{ => ncp}/test-002-form.py (100%) rename tests/toranj/{ => ncp}/test-003-join.py (100%) rename tests/toranj/{ => ncp}/test-004-scan.py (100%) rename tests/toranj/{ => ncp}/test-005-discover-scan.py (100%) rename tests/toranj/{ => ncp}/test-006-traffic-router-end-device.py (100%) rename tests/toranj/{ => ncp}/test-007-traffic-router-sleepy.py (100%) rename tests/toranj/{ => ncp}/test-008-permit-join.py (100%) rename tests/toranj/{ => ncp}/test-009-insecure-traffic-join.py (100%) rename tests/toranj/{ => ncp}/test-010-on-mesh-prefix-config-gateway.py (100%) rename tests/toranj/{ => ncp}/test-011-child-table.py (100%) rename tests/toranj/{ => ncp}/test-012-multi-hop-traffic.py (100%) rename tests/toranj/{ => ncp}/test-013-off-mesh-route-traffic.py (100%) rename tests/toranj/{ => ncp}/test-014-ip6-address-add.py (100%) rename tests/toranj/{ => ncp}/test-015-same-prefix-on-multiple-nodes.py (100%) rename tests/toranj/{ => ncp}/test-016-neighbor-table.py (100%) rename tests/toranj/{ => ncp}/test-017-parent-reset-child-recovery.py (100%) rename tests/toranj/{ => ncp}/test-018-child-supervision.py (100%) rename tests/toranj/{ => ncp}/test-019-inform-previous-parent.py (100%) rename tests/toranj/{ => ncp}/test-020-router-table.py (100%) rename tests/toranj/{ => ncp}/test-021-address-cache-table.py (100%) rename tests/toranj/{ => ncp}/test-022-multicast-ip6-address.py (100%) rename tests/toranj/{ => ncp}/test-023-multicast-traffic.py (100%) rename tests/toranj/{ => ncp}/test-024-partition-merge.py (100%) rename tests/toranj/{ => ncp}/test-025-network-data-timeout.py (100%) rename tests/toranj/{ => ncp}/test-026-slaac-address-wpantund.py (100%) rename tests/toranj/{ => ncp}/test-027-child-mode-change.py (100%) rename tests/toranj/{ => ncp}/test-028-router-leader-reset-recovery.py (100%) rename tests/toranj/{ => ncp}/test-029-data-poll-interval.py (100%) rename tests/toranj/{ => ncp}/test-030-slaac-address-ncp.py (100%) rename tests/toranj/{ => ncp}/test-031-meshcop-joiner-commissioner.py (100%) rename tests/toranj/{ => ncp}/test-032-child-attach-with-multiple-ip-addresses.py (100%) rename tests/toranj/{ => ncp}/test-033-mesh-local-prefix-change.py (100%) rename tests/toranj/{ => ncp}/test-034-poor-link-parent-child-attach.py (100%) rename tests/toranj/{ => ncp}/test-035-child-timeout-large-data-poll.py (100%) rename tests/toranj/{ => ncp}/test-036-wpantund-host-route-management.py (100%) rename tests/toranj/{ => ncp}/test-037-wpantund-auto-add-route-for-on-mesh-prefix.py (100%) rename tests/toranj/{ => ncp}/test-038-clear-address-cache-for-sed.py (100%) rename tests/toranj/{ => ncp}/test-039-address-cache-table-snoop.py (100%) rename tests/toranj/{ => ncp}/test-040-network-data-stable-full.py (100%) rename tests/toranj/{ => ncp}/test-041-lowpan-fragmentation.py (100%) rename tests/toranj/{ => ncp}/test-042-meshcop-joiner-discerner.py (100%) rename tests/toranj/{ => ncp}/test-043-meshcop-joiner-router.py (100%) rename tests/toranj/{ => ncp}/test-100-mcu-power-state.py (100%) rename tests/toranj/{ => ncp}/test-600-channel-manager-properties.py (100%) rename tests/toranj/{ => ncp}/test-601-channel-manager-channel-change.py (100%) rename tests/toranj/{ => ncp}/test-602-channel-manager-channel-select.py (100%) rename tests/toranj/{ => ncp}/test-603-channel-manager-announce-recovery.py (100%) rename tests/toranj/{ => ncp}/test-700-multi-radio-join.py (100%) rename tests/toranj/{ => ncp}/test-701-multi-radio-probe.py (100%) rename tests/toranj/{ => ncp}/test-702-multi-radio-discovery-by-rx.py (100%) rename tests/toranj/{ => ncp}/test-703-multi-radio-mesh-header-msg.py (100%) rename tests/toranj/{ => ncp}/test-704-multi-radio-scan.py (100%) rename tests/toranj/{ => ncp}/test-705-multi-radio-discover-scan.py (100%) rename tests/toranj/{ => ncp}/test-nnn-template.py (100%) rename tests/toranj/{ => ncp}/wpan.py (99%) diff --git a/.github/workflows/toranj.yml b/.github/workflows/toranj.yml index 04606a4a2..e8e083ce9 100644 --- a/.github/workflows/toranj.yml +++ b/.github/workflows/toranj.yml @@ -40,8 +40,8 @@ jobs: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" if: "github.ref != 'refs/heads/main'" - toranj: - name: toranj-${{ matrix.TORANJ_RADIO }} + toranj-ncp: + name: toranj-ncp-${{ matrix.TORANJ_RADIO }} runs-on: ubuntu-18.04 strategy: fail-fast: false @@ -83,12 +83,47 @@ jobs: - uses: actions/upload-artifact@v2 if: "matrix.TORANJ_RADIO != 'multi'" with: - name: cov-toranj-${{ matrix.TORANJ_RADIO }} + name: cov-toranj-ncp-${{ matrix.TORANJ_RADIO }} + path: tmp/coverage.info + + toranj-cli: + name: toranj-cli-${{ matrix.TORANJ_RADIO }} + runs-on: ubuntu-18.04 + strategy: + matrix: + TORANJ_RADIO: ['15.4'] + env: + COVERAGE: 1 + TORANJ_RADIO : ${{ matrix.TORANJ_RADIO }} + TORANJ_CLI: 1 + steps: + - uses: actions/checkout@v2 + with: + submodules: true + - name: Bootstrap + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + run: | + sudo rm /etc/apt/sources.list.d/* && sudo apt-get update + sudo apt-get --no-install-recommends install -y lcov + python3 -m pip install -r tests/scripts/thread-cert/requirements.txt + - name: Build & Run + run: | + top_builddir=$(pwd)/build/toranj ./tests/toranj/start.sh + - name: Generate Coverage + if: "matrix.TORANJ_RADIO != 'multi'" + run: | + ./script/test generate_coverage gcc + - uses: actions/upload-artifact@v2 + if: "matrix.TORANJ_RADIO != 'multi'" + with: + name: cov-toranj-cli-${{ matrix.TORANJ_RADIO }} path: tmp/coverage.info upload-coverage: needs: - - toranj + - toranj-ncp + - toranj-cli runs-on: ubuntu-18.04 steps: - uses: actions/checkout@v2 diff --git a/tests/toranj/README.md b/tests/toranj/README.md index 265cc7f69..d7024f85f 100644 --- a/tests/toranj/README.md +++ b/tests/toranj/README.md @@ -1,372 +1,25 @@ # `toranj` test framework -`toranj` is a test framework for OpenThread and `wpantund`. +`toranj` is a test framework for OpenThread. -- It enables testing of combined behavior of OpenThread (in NCP mode), spinel interface, and `wpantund` driver on linux. +It provides two modes: + +- `toranj-cli` which enables testing of OpenThread using its CLI interface. +- `toranj-ncp` which enables testing of the combined behavior of OpenThread (in NCP mode), spinel interface, and `wpantund` driver on linux. + +`toranj` features: + +- It is developed in Python. - It can be used to simulate multiple nodes forming complex network topologies. - It allows testing of network interactions between many nodes (IPv6 traffic exchanges). +- `toranj` in NCP mode runs `wpantund` natively with OpenThread in NCP mode on simulation platform (real-time). +- `toranj` in CLI mode runs `ot-cli-ftd` on simulation platform (real-time). +- `toranj` tests run as part of GitHub Actions pull request validation in OpenThread and `wpantund` GitHub projects. -`toranj` is developed in Python. `toranj` runs wpantund natively with OpenThread in NCP mode on POSIX simulation platform. `toranj` tests will run as part of GitHub Actions pull request validation in OpenThread and/or `wpantund` GitHub projects. +## `toranj` modes -## Setup - -`toranj` requires `wpantund` to be installed. - -- Please follow [`wpantund` installation guide](https://github.com/openthread/wpantund/blob/master/INSTALL.md#wpantund-installation-guide). Note that `toranj` expects `wpantund` installed from latest master branch. -- Alternative way to install `wpantund` is to use the same commands from git workflow [Simulation](https://github.com/openthread/openthread/blob/4b55284bd20f99a88e8e2c617ba358a0a5547f5d/.github/workflows/simulation.yml#L336-L341) for build target `toranj-test-framework`. - -To run all tests, `start` script can be used. This script will build OpenThread with proper configuration options and starts running all test. - -```bash - cd tests/toranj/ # from OpenThread repo root - ./start.sh -``` - -Each test-case has its own script following naming model `test-nnn-name.py` (e.g., `test-001-get-set.py`). - -To run a specific test - -```bash - sudo python test-001-get-set.py -``` - -## `toranj` Components - -`wpan` python module defines the `toranj` test components. - -### `wpan.Node()` Class - -`wpan.Node()` class creates a Thread node instance. It creates a sub-process to run `wpantund` and OpenThread, and provides methods to control the node. - -```python ->>> import wpan ->>> node1 = wpan.Node() ->>> node1 -Node (index=1, interface_name=wpan1) ->>> node2 = wpan.Node() ->>> node2 -Node (index=2, interface_name=wpan2) -``` - -Note: You may need to run as `sudo` to allow `wpantund` to create tunnel interface (i.e., use `sudo python`). - -### `wpan.Node` methods providing `wpanctl` commands - -`wpan.Node()` provides methods matching all `wpanctl` commands. - -- Get the value of a `wpantund` property, set the value, or add/remove value to/from a list based property: - -```python - node.get(prop_name) - node.set(prop_name, value, binary_data=False) - node.add(prop_name, value, binary_data=False) - node.remove(prop_name, value, binary_data=False) -``` - -Example: - -```python ->>> node.get(wpan.WPAN_NAME) -'"test-network"' ->>> node.set(wpan.WPAN_NAME, 'my-network') ->>> node.get(wpan.WPAN_NAME) -'"my-network"' ->>> node.set(wpan.WPAN_KEY, '65F2C35C7B543BAC1F3E26BB9F866C1D', binary_data=True) ->>> node.get(wpan.WPAN_KEY) -'[65F2C35C7B543BAC1F3E26BB9F866C1D]' -``` - -- Common network operations: - -```python - node.reset() # Reset the NCP - node.status() # Get current status - node.leave() # Leave the current network, clear all persistent data - - # Form a network in given channel (if none given use a random one) - node.form(name, channel=None) - - # Join a network with given info. - # node_type can be JOIN_TYPE_ROUTER, JOIN_TYPE_END_DEVICE, JOIN_TYPE_SLEEPY_END_DEVICE - node.join(name, channel=None, node_type=None, panid=None, xpanid=None) -``` - -Example: - -```python ->>> result = node.status() ->>> print result -wpan1 => [ - "NCP:State" => "offline" - "Daemon:Enabled" => true - "NCP:Version" => "OPENTHREAD/20170716-00460-ga438cef0c-dirty; NONE; Feb 12 2018 11:47:01" - "Daemon:Version" => "0.08.00d (0.07.01-191-g63265f7; Feb 2 2018 18:05:47)" - "Config:NCP:DriverName" => "spinel" - "NCP:HardwareAddress" => [18B4300000000001] -] ->>> ->>> node.form("test-network", channel=12) -'Forming WPAN "test-network" as node type "router"\nSuccessfully formed!' ->>> ->>> print node.status() -wpan1 => [ - "NCP:State" => "associated" - "Daemon:Enabled" => true - "NCP:Version" => "OPENTHREAD/20170716-00460-ga438cef0c-dirty; NONE; Feb 12 2018 11:47:01" - "Daemon:Version" => "0.08.00d (0.07.01-191-g63265f7; Feb 2 2018 18:05:47)" - "Config:NCP:DriverName" => "spinel" - "NCP:HardwareAddress" => [18B4300000000001] - "NCP:Channel" => 12 - "Network:NodeType" => "leader" - "Network:Name" => "test-network" - "Network:XPANID" => 0xA438CF5973FD86B2 - "Network:PANID" => 0x9D81 - "IPv6:MeshLocalAddress" => "fda4:38cf:5973:0:b899:3436:15c6:941d" - "IPv6:MeshLocalPrefix" => "fda4:38cf:5973::/64" - "com.nestlabs.internal:Network:AllowingJoin" => false -] -``` - -- Scan: - -```python - node.active_scan(channel=None) - node.energy_scan(channel=None) - node.discover_scan(channel=None, joiner_only=False, enable_filtering=False, panid_filter=None) - node.permit_join(duration_sec=None, port=None, udp=True, tcp=True) -``` - -- On-mesh prefixes and off-mesh routes: - -```python - node.config_gateway(prefix, default_route=False) - node.add_route(route_prefix, prefix_len_in_bytes=None, priority=None) - node.remove_route(route_prefix, prefix_len_in_bytes=None, priority=None) -``` - -A direct `wpanctl` command can be issued using `node.wpanctl(command)` with a given `command` string. - -`wpan` module provides variables for different `wpantund` properties. Some commonly used are: - -- Network/NCP properties: WPAN_STATE, WPAN_NAME, WPAN_PANID, WPAN_XPANID, WPAN_KEY, WPAN_CHANNEL, WPAN_HW_ADDRESS, WPAN_EXT_ADDRESS, WPAN_POLL_INTERVAL, WPAN_NODE_TYPE, WPAN_ROLE, WPAN_PARTITION_ID - -- IPv6 Addresses: WPAN_IP6_LINK_LOCAL_ADDRESS, WPAN_IP6_MESH_LOCAL_ADDRESS, WPAN_IP6_MESH_LOCAL_PREFIX, WPAN_IP6_ALL_ADDRESSES, WPAN_IP6_MULTICAST_ADDRESSES - -- Thread Properties: WPAN_THREAD_RLOC16, WPAN_THREAD_ROUTER_ID, WPAN_THREAD_LEADER_ADDRESS, WPAN_THREAD_LEADER_ROUTER_ID, WPAN_THREAD_LEADER_WEIGHT, WPAN_THREAD_LEADER_NETWORK_DATA, - - WPAN_THREAD_CHILD_TABLE, WPAN_THREAD_CHILD_TABLE_ADDRESSES, WPAN_THREAD_NEIGHBOR_TABLE, - WPAN_THREAD_ROUTER_TABLE - -Method `join_node()` can be used by a node to join another node: - -```python - # `node1` joining `node2`'s network as a router - node1.join_node(node2, node_type=JOIN_TYPE_ROUTER) -``` - -Method `allowlist_node()` can be used to add a given node to the allowlist of the device and enables allowlisting: - -```python - # `node2` is added to the allowlist of `node1` and allowlisting is enabled on `node1` - node1.allowlist_node(node2) -``` - -#### Example (simple 3-node topology) - -Script below shows how to create a 3-node network topology with `node1` and `node2` being routers, and `node3` an end-device connected to `node2`: - -```python ->>> import wpan ->>> node1 = wpan.Node() ->>> node2 = wpan.Node() ->>> node3 = wpan.Node() - ->>> wpan.Node.init_all_nodes() - ->>> node1.form("test-PAN") -'Forming WPAN "test-PAN" as node type "router"\nSuccessfully formed!' - ->>> node1.allowlist_node(node2) ->>> node2.allowlist_node(node1) - ->>> node2.join_node(node1, wpan.JOIN_TYPE_ROUTER) -'Joining "test-PAN" C474513CB487778D as node type "router"\nSuccessfully Joined!' - ->>> node3.allowlist_node(node2) ->>> node2.allowlist_node(node3) - ->>> node3.join_node(node2, wpan.JOIN_TYPE_END_DEVICE) -'Joining "test-PAN" C474513CB487778D as node type "end-device"\nSuccessfully Joined!' - ->>> print node2.get(wpan.WPAN_THREAD_NEIGHBOR_TABLE) -[ - "EAC1672C3EAB30A4, RLOC16:9401, LQIn:3, AveRssi:-20, LastRssi:-20, Age:30, LinkFC:6, MleFC:0, IsChild:yes, RxOnIdle:yes, FTD:yes, SecDataReq:yes, FullNetData:yes" - "A2042C8762576FD5, RLOC16:dc00, LQIn:3, AveRssi:-20, LastRssi:-20, Age:5, LinkFC:21, MleFC:18, IsChild:no, RxOnIdle:yes, FTD:yes, SecDataReq:no, FullNetData:yes" -] ->>> print node1.get(wpan.WPAN_THREAD_NEIGHBOR_TABLE) -[ - "960947C53415DAA1, RLOC16:9400, LQIn:3, AveRssi:-20, LastRssi:-20, Age:18, LinkFC:15, MleFC:11, IsChild:no, RxOnIdle:yes, FTD:yes, SecDataReq:no, FullNetData:yes" -] - -``` - -### IPv6 Message Exchange - -`toranj` allows a test-case to define traffic patterns (IPv6 message exchange) between different nodes. Message exchanges (tx/rx) are prepared and then an async rx/tx operation starts. The success and failure of tx/rx operations can then be verified by the test case. - -`wpan.Node` method `prepare_tx()` prepares a UDP6 transmission from a node. - -```python - node1.prepare_tx(src, dst, data, count) -``` - -- `src` and `dst` can be - - - either a string containing an IPv6 address - - or a tuple (ipv6 address as string, port). if no port is given, a random port number is used. - -- `data` can be - - - either a string containing the message to be sent, - - or an int indicating size of the message (a random message with the given length will be generated). - -- `count` gives number of times the message will be sent (default is 1). - -`prepare_tx` returns a `wpan.AsyncSender` object. The sender object can be used to check success/failure of tx operation. - -`wpan.Node` method `prepare_rx()` prepares a node to listen for UDP messages from a sender. - -```python - node2.prepare_rx(sender) -``` - -- `sender` should be an `wpan.AsyncSender` object returned from previous `prepare_tx`. -- `prepare_rx()` returns a `wpan.AsyncReceiver` object to help test to check success/failure of rx operation. - -After all exchanges are prepared, static method `perform_async_tx_rx()` should be used to start all previously prepared rx and tx operations. - -```python - wpan.Node.perform_async_tx_rx(timeout) -``` - -- `timeout` gives amount of time (in seconds) to wait for all operations to finish. (default is 20 seconds) - -After `perform_async_tx_rx()` is done, the `AsyncSender` and `AsyncReceiver` objects can check if operations were successful (using property `was_successful`) - -#### Example - -Sending 10 messages containing `"Hello there!"` from `node1` to `node2` using their mesh-local addresses: - -```python -# `node1` and `node2` are already joined and are part of the same Thread network. - -# Get the mesh local addresses ->>> mladdr1 = node1.get(wpan.WPAN_IP6_MESH_LOCAL_ADDRESS)[1:-1] # remove `"` from start/end of string ->>> mladdr2 = node2.get(wpan.WPAN_IP6_MESH_LOCAL_ADDRESS)[1:-1] - ->>> print (mladdr1, mladdr2) -('fda4:38cf:5973:0:b899:3436:15c6:941d', 'fda4:38cf:5973:0:5836:fa55:7394:6d4b') - -# prepare a `sender` and corresponding `recver` ->>> sender = node1.prepare_tx((mladdr1, 1234), (mladdr2, 2345), "Hello there!", 10) ->>> recver = node2.prepare_rx(sender) - -# perform async message transfer ->>> wpan.Node.perform_async_tx_rx() - -# check status of `sender` and `recver` ->>> sender.was_successful -True ->>> recver.was_successful -True - -# `sender` or `recver` can provide info about the exchange - ->>> sender.src_addr -'fda4:38cf:5973:0:b899:3436:15c6:941d' ->>> sender.src_port -1234 ->>> sender.dst_addr -'fda4:38cf:5973:0:5836:fa55:7394:6d4b' ->>> sender.dst_port -2345 ->>> sender.msg -'Hello there!' ->>> sender.count -10 - -# get all received msg by `recver` as list of tuples `(msg, (src_address, src_port))` ->>> recver.all_rx_msg -[('Hello there!', ('fda4:38cf:5973:0:b899:3436:15c6:941d', 1234)), ... ] -``` - -### Logs and Verbose mode - -Every `wpan.Node()` instance will save its corresponding `wpantund` logs. By default the logs are saved in a file `wpantun-log.log`. By setting `wpan.Node__TUND_LOG_TO_FILE` to `False` the logs are written to `stdout` as the test-cases are executed. - -When `start.sh` script is used to run all test-cases, if any test fails, to help with debugging of the issue, the last 30 lines of `wpantund` logs of every node involved in the test-case is dumped to `stdout`. - -A `wpan.Node()` instance can also provide additional logs and info as the test-cases are run (verbose mode). It can be enabled for a node instance when it is created: - -```python - node = wpan.Node(verbose=True) # `node` instance will provide extra logs. -``` - -Alternatively, `wpan.Node._VERBOSE` settings can be changed to enable verbose logging for all nodes. The default value of `wpan.Node._VERBOSE` is determined from environment variable `TORANJ_VERBOSE` (verbose mode is enabled when env variable is set to any of `1`, `True`, `Yes`, `Y`, `On` (case-insensitive)), otherwise it is disabled. When `TORANJ_VERBOSE` is enabled, the OpenThread logging is also enabled (and collected in `wpantund-log.log`files) on all nodes. - -Here is example of small test script and its corresponding log output with `verbose` mode enabled: - -```python -node1 = wpan.Node(verbose=True) -node2 = wpan.Node(verbose=True) - -wpan.Node.init_all_nodes() - -node1.form("toranj-net") -node2.active_scan() - -node2.join_node(node1) -verify(node2.get(wpan.WPAN_STATE) == wpan.STATE_ASSOCIATED) - -lladdr1 = node1.get(wpan.WPAN_IP6_LINK_LOCAL_ADDRESS)[1:-1] -lladdr2 = node2.get(wpan.WPAN_IP6_LINK_LOCAL_ADDRESS)[1:-1] - -sender = node1.prepare_tx(lladdr1, lladdr2, 20) -recver = node2.prepare_rx(sender) - -wpan.Node.perform_async_tx_rx() - -``` - -``` -$ Node1.__init__() cmd: /usr/local/sbin/wpantund -o Config:NCP:SocketPath "system:../../examples/apps/ncp/ot-ncp-ftd 1" -o Config:TUN:InterfaceName wpan1 -o Config:NCP:DriverName spinel -o Daemon:SyslogMask "all -debug" -$ Node2.__init__() cmd: /usr/local/sbin/wpantund -o Config:NCP:SocketPath "system:../../examples/apps/ncp/ot-ncp-ftd 2" -o Config:TUN:InterfaceName wpan2 -o Config:NCP:DriverName spinel -o Daemon:SyslogMask "all -debug" -$ Node1.wpanctl('leave') -> 'Leaving current WPAN. . .' -$ Node2.wpanctl('leave') -> 'Leaving current WPAN. . .' -$ Node1.wpanctl('form "toranj-net"'): - Forming WPAN "toranj-net" as node type "router" - Successfully formed! -$ Node2.wpanctl('scan'): - | Joinable | NetworkName | PAN ID | Ch | XPanID | HWAddr | RSSI - ---+----------+--------------------+--------+----+------------------+------------------+------ - 1 | NO | "toranj-net" | 0x9DEB | 16 | 8CC6CFC810F23E1B | BEECDAF3439DC931 | -20 -$ Node1.wpanctl('get -v NCP:State') -> '"associated"' -$ Node1.wpanctl('get -v Network:Name') -> '"toranj-net"' -$ Node1.wpanctl('get -v Network:PANID') -> '0x9DEB' -$ Node1.wpanctl('get -v Network:XPANID') -> '0x8CC6CFC810F23E1B' -$ Node1.wpanctl('get -v Network:Key') -> '[BA2733A5D81EAB8FFB3C9A7383CB6045]' -$ Node1.wpanctl('get -v NCP:Channel') -> '16' -$ Node2.wpanctl('set Network:Key -d -v BA2733A5D81EAB8FFB3C9A7383CB6045') -> '' -$ Node2.wpanctl('join "toranj-net" -c 16 -T r -p 0x9DEB -x 0x8CC6CFC810F23E1B'): - Joining "toranj-net" 8CC6CFC810F23E1B as node type "router" - Successfully Joined! -$ Node2.wpanctl('get -v NCP:State') -> '"associated"' -$ Node1.wpanctl('get -v IPv6:LinkLocalAddress') -> '"fe80::bcec:daf3:439d:c931"' -$ Node2.wpanctl('get -v IPv6:LinkLocalAddress') -> '"fe80::ec08:f348:646f:d37d"' -- Node1 sent 20 bytes (":YeQuNKjuOtd%H#ipM7P") to [fe80::ec08:f348:646f:d37d]:404 from [fe80::bcec:daf3:439d:c931]:12557 -- Node2 received 20 bytes (":YeQuNKjuOtd%H#ipM7P") on port 404 from [fe80::bcec:daf3:439d:c931]:12557 - -``` +- [`toranj-cli` guide](README_CLI.md) +- [`toranj-ncp` guide](README_NCP.md) --- diff --git a/tests/toranj/README_CLI.md b/tests/toranj/README_CLI.md new file mode 100644 index 000000000..024a3219d --- /dev/null +++ b/tests/toranj/README_CLI.md @@ -0,0 +1,188 @@ +# `toranj-cli` + +`toranj-cli` is a test framework for OpenThread using its CLI interface. + +`toranj` features: + +- It is developed in Python. +- It can be used to simulate multiple nodes forming complex network topologies. +- It allows testing of network interactions between many nodes. +- `toranj` in CLI mode runs `ot-cli-ftd` on simulation platform (real-time). + +## Setup + +To build OpenThread with `toranj` configuration, the `test/toranj/build.sh` script can be used: + +```bash +$ ./tests/toranj/build.sh cmake +==================================================================================================== +Building OpenThread (NCP/CLI for FTD/MTD/RCP mode) with simulation platform using cmake +==================================================================================================== +-- OpenThread Source Directory: /Users/abtink/GitHub/openthread +-- OpenThread CMake build type: Debug +-- Package Name: OPENTHREAD +... + +``` + +Or to build using autoconf/make we can use: + +```bash +$ ./tests/toranj/build.sh cli +==================================================================================================== +Building OpenThread (NCP/CLI for FTD/MTD/RCP mode) with simulation platform using cmake +==================================================================================================== +-- OpenThread Source Directory: /Users/abtink/GitHub/openthread +-- OpenThread CMake build type: Debug +-- Package Name: OPENTHREAD +... + +``` + +The `toranj-cli` tests are included in `tests/toranj/cli` folder. Each test-case has its own script following naming model `test-nnn-name.py` (e.g., `test-001-get-set.py`). + +To run a specific test: + +```bash +$ cd tests/toranj/cli +$ python3 test-001-get-set.py +``` + +To run all CLI tests, `start` script can be used. This script will build OpenThread with proper configuration options and starts running all tests. + +```bash +# From OpenThread repo root folder +$ top_builddir=($pwd) TORANJ_CLI=1 ./tests/toranj/start.sh +``` + +## `toranj-cli` Components + +`cli` python module defines the `toranj-cli` test components. + +### `cli.Node()` Class + +`cli.Node()` class creates a Thread node instance. It creates a sub-process to run `ot-cli-ftd` and provides methods to control the node and issue CLI commands. + +```python +>>> import cli +>>> node1 = cli.Node() +>>> node1 +Node (index=1) +>>> node2 = cli.Node() +>>> node2 +Node (index=2) +``` + +Note: You may need to run as `sudo` to allow log file to be written (i.e., use `sudo python` or `sudo python3`). + +### `cli.Node` methods + +`cli.Node()` provides methods matching different CLI commands, in addition to some helper methods for common operations. + +Example: + +```python +>>> node.get_state() +'disabled' +>>> node.get_channel() +'11' +>>> node.set_channel(12) +>>> node.get_channel() +'12' +>>> node.set_network_key('11223344556677889900aabbccddeeff') +>>> node.get_network_key() +'11223344556677889900aabbccddeeff' +``` + +Common network operations: + +```python + # Form a Thread network with all the given parameters. + node.form(network_name=None, network_key=None, channel=None, panid=0x1234, xpanid=None): + + # Try to join an existing network as specified by `another_node`. + # `type` can be `JOIN_TYPE_ROUTER`, `JOIN_TYPE_END_DEVICE, or `JOIN_TYPE_SLEEPY_END_DEVICE` + node.join(another_node, type=JOIN_TYPE_ROUTER): +``` + +A direct CLI command can be issued using `node.cli(command)` with a given `command` string. + +```python +>>> node.cli('uptime') +['00:36:18.778'] +``` + +Method `allowlist_node()` can be used to add a given node to the allowlist of the device and enables allowlisting: + +```python + # `node2` is added to the allowlist of `node1` and allowlisting is enabled on `node1` + node1.allowlist_node(node2) +``` + +#### Example (simple 3-node topology) + +Script below shows how to create a 3-node network topology with `node1` and `node2` being routers, and `node3` an end-device connected to `node2`: + +```python +>>> import cli +>>> node1 = cli.Node() +>>> node2 = cli.Node() +>>> node3 = cli.Node() + +>>> node1.form('test') +>>> node1.get_state() +'leader' + +>>> node1.allowlist_node(node2) +>>> node1.allowlist_node(node3) + +>>> node2.join(node1, cli.JOIN_TYPE_ROUTER) +>>> node2.get_state() +'router' + +>>> node3.join(node1, cli.JOIN_TYPE_END_DEVICE) +>>> node3.get_state() +'child' + +>>> node1.cli('neighbor list') +['0x1c01 0x0400 '] +``` + +### Logs and Verbose mode + +Every `cli.Node()` instance will save its corresponding logs. By default the logs are saved in a file `ot-logs.log`. + +When `start.sh` script is used to run all test-cases, if any test fails, to help with debugging of the issue, the last 30 lines of logs of every node involved in the test-case are dumped to `stdout`. + +A `cli.Node()` instance can also provide additional logs and info as the test-cases are run (verbose mode). It can be enabled for a node instance when it is created: + +```python +>>> import cli +>>> node = cli.Node(verbose=True) +$ Node1.__init__() cmd: `../../../examples/apps/cli/ot-cli-ftd --time-speed=1 1` + +>>> node.get_state() +$ Node1.cli('state') -> disabled +'disabled' + +>>> node.form('test') +$ Node1.cli('networkname test') +$ Node1.cli('panid 4660') +$ Node1.cli('ifconfig up') +$ Node1.cli('thread start') +$ Node1.cli('state') -> detached +$ Node1.cli('state') -> detached +... +$ Node1.cli('state') -> leader +``` + +Alternatively, `cli.Node._VERBOSE` settings can be changed to enable verbose logging for all nodes. The default value of `cli.Node._VERBOSE` is determined from environment variable `TORANJ_VERBOSE` (verbose mode is enabled when env variable is set to any of `1`, `True`, `Yes`, `Y`, `On` (case-insensitive)), otherwise it is disabled. + +## `toranj-cli` and `thread-cert` test framework + +`toranj-cli` uses CLI commands to test the behavior of OpenThread with simulation platform. `thread-cert` scripts (in `tests/scripts/thread-cert`) also use CLI commands. However, these two test frameworks have certain differences and are intended for different situations. The `toranj` test cases run in real-time (though it is possible to run with a time speed-up factor) while the `thread-cert` scripts use virtual-time and event-based simulation model. + +- `toranj` test cases are useful to validate the real-time (non event-based) simulation platform implementation itself. +- `toranj` test cases can be used in situations where the platform layer may not support event-based model. +- `toranj` frameworks allows for more interactive testing (e.g., read–eval–print loop (REPL) model in python) and do not need a separate process to run to handle/dispatch events (which is required for the virtual-time simulation model). +- `thread-cert` test cases can run quickly (due to virtual time emulation), but the test script itself needs to manage the flow and advancement of time. diff --git a/tests/toranj/README_NCP.md b/tests/toranj/README_NCP.md new file mode 100644 index 000000000..1c1e09f0d --- /dev/null +++ b/tests/toranj/README_NCP.md @@ -0,0 +1,370 @@ +# `toranj-ncp` test framework + +`toranj-ncp` is a test framework for OpenThread enabling testing of the combined behavior of OpenThread (in NCP mode), spinel interface, and `wpantund` driver on linux. + +`toranj` features: + +- It is developed in Python. +- It can be used to simulate multiple nodes forming complex network topologies. +- It allows testing of network interactions between many nodes (IPv6 traffic exchanges). +- `toranj` in NCP mode runs `wpantund` natively with OpenThread in NCP mode on simulation platform (real-time). + +## Setup + +`toranj-ncp` requires `wpantund` to be installed. + +- Please follow [`wpantund` installation guide](https://github.com/openthread/wpantund/blob/master/INSTALL.md#wpantund-installation-guide). Note that `toranj` expects `wpantund` installed from latest master branch. +- Alternative way to install `wpantund` is to use the same commands from git workflow [Simulation](https://github.com/openthread/openthread/blob/4b55284bd20f99a88e8e2c617ba358a0a5547f5d/.github/workflows/simulation.yml#L336-L341) for build target `toranj-test-framework`. + +To run all tests, `start` script can be used. This script will build OpenThread with proper configuration options and starts running all test. + +```bash + cd tests/toranj/ # from OpenThread repo root + TORANJ_CLI=0 ./start.sh +``` + +The `toranj-ncp` tests are included in `tests/toranj/ncp` folder. Each test-case has its own script following naming model `test-nnn-name.py` (e.g., `test-001-get-set.py`). + +To run a specific test + +```bash + sudo python ncp/test-001-get-set.py +``` + +## `toranj` Components + +`wpan` python module defines the `toranj` test components. + +### `wpan.Node()` Class + +`wpan.Node()` class creates a Thread node instance. It creates a sub-process to run `wpantund` and OpenThread, and provides methods to control the node. + +```python +>>> import wpan +>>> node1 = wpan.Node() +>>> node1 +Node (index=1, interface_name=wpan1) +>>> node2 = wpan.Node() +>>> node2 +Node (index=2, interface_name=wpan2) +``` + +Note: You may need to run as `sudo` to allow `wpantund` to create tunnel interface (i.e., use `sudo python`). + +### `wpan.Node` methods providing `wpanctl` commands + +`wpan.Node()` provides methods matching all `wpanctl` commands. + +- Get the value of a `wpantund` property, set the value, or add/remove value to/from a list based property: + +```python + node.get(prop_name) + node.set(prop_name, value, binary_data=False) + node.add(prop_name, value, binary_data=False) + node.remove(prop_name, value, binary_data=False) +``` + +Example: + +```python +>>> node.get(wpan.WPAN_NAME) +'"test-network"' +>>> node.set(wpan.WPAN_NAME, 'my-network') +>>> node.get(wpan.WPAN_NAME) +'"my-network"' +>>> node.set(wpan.WPAN_KEY, '65F2C35C7B543BAC1F3E26BB9F866C1D', binary_data=True) +>>> node.get(wpan.WPAN_KEY) +'[65F2C35C7B543BAC1F3E26BB9F866C1D]' +``` + +- Common network operations: + +```python + node.reset() # Reset the NCP + node.status() # Get current status + node.leave() # Leave the current network, clear all persistent data + + # Form a network in given channel (if none given use a random one) + node.form(name, channel=None) + + # Join a network with given info. + # node_type can be JOIN_TYPE_ROUTER, JOIN_TYPE_END_DEVICE, JOIN_TYPE_SLEEPY_END_DEVICE + node.join(name, channel=None, node_type=None, panid=None, xpanid=None) +``` + +Example: + +```python +>>> result = node.status() +>>> print result +wpan1 => [ + "NCP:State" => "offline" + "Daemon:Enabled" => true + "NCP:Version" => "OPENTHREAD/20170716-00460-ga438cef0c-dirty; NONE; Feb 12 2018 11:47:01" + "Daemon:Version" => "0.08.00d (0.07.01-191-g63265f7; Feb 2 2018 18:05:47)" + "Config:NCP:DriverName" => "spinel" + "NCP:HardwareAddress" => [18B4300000000001] +] +>>> +>>> node.form("test-network", channel=12) +'Forming WPAN "test-network" as node type "router"\nSuccessfully formed!' +>>> +>>> print node.status() +wpan1 => [ + "NCP:State" => "associated" + "Daemon:Enabled" => true + "NCP:Version" => "OPENTHREAD/20170716-00460-ga438cef0c-dirty; NONE; Feb 12 2018 11:47:01" + "Daemon:Version" => "0.08.00d (0.07.01-191-g63265f7; Feb 2 2018 18:05:47)" + "Config:NCP:DriverName" => "spinel" + "NCP:HardwareAddress" => [18B4300000000001] + "NCP:Channel" => 12 + "Network:NodeType" => "leader" + "Network:Name" => "test-network" + "Network:XPANID" => 0xA438CF5973FD86B2 + "Network:PANID" => 0x9D81 + "IPv6:MeshLocalAddress" => "fda4:38cf:5973:0:b899:3436:15c6:941d" + "IPv6:MeshLocalPrefix" => "fda4:38cf:5973::/64" + "com.nestlabs.internal:Network:AllowingJoin" => false +] +``` + +- Scan: + +```python + node.active_scan(channel=None) + node.energy_scan(channel=None) + node.discover_scan(channel=None, joiner_only=False, enable_filtering=False, panid_filter=None) + node.permit_join(duration_sec=None, port=None, udp=True, tcp=True) +``` + +- On-mesh prefixes and off-mesh routes: + +```python + node.config_gateway(prefix, default_route=False) + node.add_route(route_prefix, prefix_len_in_bytes=None, priority=None) + node.remove_route(route_prefix, prefix_len_in_bytes=None, priority=None) +``` + +A direct `wpanctl` command can be issued using `node.wpanctl(command)` with a given `command` string. + +`wpan` module provides variables for different `wpantund` properties. Some commonly used are: + +- Network/NCP properties: WPAN_STATE, WPAN_NAME, WPAN_PANID, WPAN_XPANID, WPAN_KEY, WPAN_CHANNEL, WPAN_HW_ADDRESS, WPAN_EXT_ADDRESS, WPAN_POLL_INTERVAL, WPAN_NODE_TYPE, WPAN_ROLE, WPAN_PARTITION_ID + +- IPv6 Addresses: WPAN_IP6_LINK_LOCAL_ADDRESS, WPAN_IP6_MESH_LOCAL_ADDRESS, WPAN_IP6_MESH_LOCAL_PREFIX, WPAN_IP6_ALL_ADDRESSES, WPAN_IP6_MULTICAST_ADDRESSES + +- Thread Properties: WPAN_THREAD_RLOC16, WPAN_THREAD_ROUTER_ID, WPAN_THREAD_LEADER_ADDRESS, WPAN_THREAD_LEADER_ROUTER_ID, WPAN_THREAD_LEADER_WEIGHT, WPAN_THREAD_LEADER_NETWORK_DATA, + + WPAN_THREAD_CHILD_TABLE, WPAN_THREAD_CHILD_TABLE_ADDRESSES, WPAN_THREAD_NEIGHBOR_TABLE, + WPAN_THREAD_ROUTER_TABLE + +Method `join_node()` can be used by a node to join another node: + +```python + # `node1` joining `node2`'s network as a router + node1.join_node(node2, node_type=JOIN_TYPE_ROUTER) +``` + +Method `allowlist_node()` can be used to add a given node to the allowlist of the device and enables allowlisting: + +```python + # `node2` is added to the allowlist of `node1` and allowlisting is enabled on `node1` + node1.allowlist_node(node2) +``` + +#### Example (simple 3-node topology) + +Script below shows how to create a 3-node network topology with `node1` and `node2` being routers, and `node3` an end-device connected to `node2`: + +```python +>>> import wpan +>>> node1 = wpan.Node() +>>> node2 = wpan.Node() +>>> node3 = wpan.Node() + +>>> wpan.Node.init_all_nodes() + +>>> node1.form("test-PAN") +'Forming WPAN "test-PAN" as node type "router"\nSuccessfully formed!' + +>>> node1.allowlist_node(node2) +>>> node2.allowlist_node(node1) + +>>> node2.join_node(node1, wpan.JOIN_TYPE_ROUTER) +'Joining "test-PAN" C474513CB487778D as node type "router"\nSuccessfully Joined!' + +>>> node3.allowlist_node(node2) +>>> node2.allowlist_node(node3) + +>>> node3.join_node(node2, wpan.JOIN_TYPE_END_DEVICE) +'Joining "test-PAN" C474513CB487778D as node type "end-device"\nSuccessfully Joined!' + +>>> print node2.get(wpan.WPAN_THREAD_NEIGHBOR_TABLE) +[ + "EAC1672C3EAB30A4, RLOC16:9401, LQIn:3, AveRssi:-20, LastRssi:-20, Age:30, LinkFC:6, MleFC:0, IsChild:yes, RxOnIdle:yes, FTD:yes, SecDataReq:yes, FullNetData:yes" + "A2042C8762576FD5, RLOC16:dc00, LQIn:3, AveRssi:-20, LastRssi:-20, Age:5, LinkFC:21, MleFC:18, IsChild:no, RxOnIdle:yes, FTD:yes, SecDataReq:no, FullNetData:yes" +] +>>> print node1.get(wpan.WPAN_THREAD_NEIGHBOR_TABLE) +[ + "960947C53415DAA1, RLOC16:9400, LQIn:3, AveRssi:-20, LastRssi:-20, Age:18, LinkFC:15, MleFC:11, IsChild:no, RxOnIdle:yes, FTD:yes, SecDataReq:no, FullNetData:yes" +] + +``` + +### IPv6 Message Exchange + +`toranj` allows a test-case to define traffic patterns (IPv6 message exchange) between different nodes. Message exchanges (tx/rx) are prepared and then an async rx/tx operation starts. The success and failure of tx/rx operations can then be verified by the test case. + +`wpan.Node` method `prepare_tx()` prepares a UDP6 transmission from a node. + +```python + node1.prepare_tx(src, dst, data, count) +``` + +- `src` and `dst` can be + + - either a string containing an IPv6 address + - or a tuple (ipv6 address as string, port). if no port is given, a random port number is used. + +- `data` can be + + - either a string containing the message to be sent, + - or an int indicating size of the message (a random message with the given length will be generated). + +- `count` gives number of times the message will be sent (default is 1). + +`prepare_tx` returns a `wpan.AsyncSender` object. The sender object can be used to check success/failure of tx operation. + +`wpan.Node` method `prepare_rx()` prepares a node to listen for UDP messages from a sender. + +```python + node2.prepare_rx(sender) +``` + +- `sender` should be an `wpan.AsyncSender` object returned from previous `prepare_tx`. +- `prepare_rx()` returns a `wpan.AsyncReceiver` object to help test to check success/failure of rx operation. + +After all exchanges are prepared, static method `perform_async_tx_rx()` should be used to start all previously prepared rx and tx operations. + +```python + wpan.Node.perform_async_tx_rx(timeout) +``` + +- `timeout` gives amount of time (in seconds) to wait for all operations to finish. (default is 20 seconds) + +After `perform_async_tx_rx()` is done, the `AsyncSender` and `AsyncReceiver` objects can check if operations were successful (using property `was_successful`) + +#### Example + +Sending 10 messages containing `"Hello there!"` from `node1` to `node2` using their mesh-local addresses: + +```python +# `node1` and `node2` are already joined and are part of the same Thread network. + +# Get the mesh local addresses +>>> mladdr1 = node1.get(wpan.WPAN_IP6_MESH_LOCAL_ADDRESS)[1:-1] # remove `"` from start/end of string +>>> mladdr2 = node2.get(wpan.WPAN_IP6_MESH_LOCAL_ADDRESS)[1:-1] + +>>> print (mladdr1, mladdr2) +('fda4:38cf:5973:0:b899:3436:15c6:941d', 'fda4:38cf:5973:0:5836:fa55:7394:6d4b') + +# prepare a `sender` and corresponding `recver` +>>> sender = node1.prepare_tx((mladdr1, 1234), (mladdr2, 2345), "Hello there!", 10) +>>> recver = node2.prepare_rx(sender) + +# perform async message transfer +>>> wpan.Node.perform_async_tx_rx() + +# check status of `sender` and `recver` +>>> sender.was_successful +True +>>> recver.was_successful +True + +# `sender` or `recver` can provide info about the exchange + +>>> sender.src_addr +'fda4:38cf:5973:0:b899:3436:15c6:941d' +>>> sender.src_port +1234 +>>> sender.dst_addr +'fda4:38cf:5973:0:5836:fa55:7394:6d4b' +>>> sender.dst_port +2345 +>>> sender.msg +'Hello there!' +>>> sender.count +10 + +# get all received msg by `recver` as list of tuples `(msg, (src_address, src_port))` +>>> recver.all_rx_msg +[('Hello there!', ('fda4:38cf:5973:0:b899:3436:15c6:941d', 1234)), ... ] +``` + +### Logs and Verbose mode + +Every `wpan.Node()` instance will save its corresponding `wpantund` logs. By default the logs are saved in a file `wpantun-log.log`. By setting `wpan.Node__TUND_LOG_TO_FILE` to `False` the logs are written to `stdout` as the test-cases are executed. + +When `start.sh` script is used to run all test-cases, if any test fails, to help with debugging of the issue, the last 30 lines of `wpantund` logs of every node involved in the test-case is dumped to `stdout`. + +A `wpan.Node()` instance can also provide additional logs and info as the test-cases are run (verbose mode). It can be enabled for a node instance when it is created: + +```python + node = wpan.Node(verbose=True) # `node` instance will provide extra logs. +``` + +Alternatively, `wpan.Node._VERBOSE` settings can be changed to enable verbose logging for all nodes. The default value of `wpan.Node._VERBOSE` is determined from environment variable `TORANJ_VERBOSE` (verbose mode is enabled when env variable is set to any of `1`, `True`, `Yes`, `Y`, `On` (case-insensitive)), otherwise it is disabled. When `TORANJ_VERBOSE` is enabled, the OpenThread logging is also enabled (and collected in `wpantund-log.log`files) on all nodes. + +Here is example of small test script and its corresponding log output with `verbose` mode enabled: + +```python +node1 = wpan.Node(verbose=True) +node2 = wpan.Node(verbose=True) + +wpan.Node.init_all_nodes() + +node1.form("toranj-net") +node2.active_scan() + +node2.join_node(node1) +verify(node2.get(wpan.WPAN_STATE) == wpan.STATE_ASSOCIATED) + +lladdr1 = node1.get(wpan.WPAN_IP6_LINK_LOCAL_ADDRESS)[1:-1] +lladdr2 = node2.get(wpan.WPAN_IP6_LINK_LOCAL_ADDRESS)[1:-1] + +sender = node1.prepare_tx(lladdr1, lladdr2, 20) +recver = node2.prepare_rx(sender) + +wpan.Node.perform_async_tx_rx() + +``` + +``` +$ Node1.__init__() cmd: /usr/local/sbin/wpantund -o Config:NCP:SocketPath "system:../../examples/apps/ncp/ot-ncp-ftd 1" -o Config:TUN:InterfaceName wpan1 -o Config:NCP:DriverName spinel -o Daemon:SyslogMask "all -debug" +$ Node2.__init__() cmd: /usr/local/sbin/wpantund -o Config:NCP:SocketPath "system:../../examples/apps/ncp/ot-ncp-ftd 2" -o Config:TUN:InterfaceName wpan2 -o Config:NCP:DriverName spinel -o Daemon:SyslogMask "all -debug" +$ Node1.wpanctl('leave') -> 'Leaving current WPAN. . .' +$ Node2.wpanctl('leave') -> 'Leaving current WPAN. . .' +$ Node1.wpanctl('form "toranj-net"'): + Forming WPAN "toranj-net" as node type "router" + Successfully formed! +$ Node2.wpanctl('scan'): + | Joinable | NetworkName | PAN ID | Ch | XPanID | HWAddr | RSSI + ---+----------+--------------------+--------+----+------------------+------------------+------ + 1 | NO | "toranj-net" | 0x9DEB | 16 | 8CC6CFC810F23E1B | BEECDAF3439DC931 | -20 +$ Node1.wpanctl('get -v NCP:State') -> '"associated"' +$ Node1.wpanctl('get -v Network:Name') -> '"toranj-net"' +$ Node1.wpanctl('get -v Network:PANID') -> '0x9DEB' +$ Node1.wpanctl('get -v Network:XPANID') -> '0x8CC6CFC810F23E1B' +$ Node1.wpanctl('get -v Network:Key') -> '[BA2733A5D81EAB8FFB3C9A7383CB6045]' +$ Node1.wpanctl('get -v NCP:Channel') -> '16' +$ Node2.wpanctl('set Network:Key -d -v BA2733A5D81EAB8FFB3C9A7383CB6045') -> '' +$ Node2.wpanctl('join "toranj-net" -c 16 -T r -p 0x9DEB -x 0x8CC6CFC810F23E1B'): + Joining "toranj-net" 8CC6CFC810F23E1B as node type "router" + Successfully Joined! +$ Node2.wpanctl('get -v NCP:State') -> '"associated"' +$ Node1.wpanctl('get -v IPv6:LinkLocalAddress') -> '"fe80::bcec:daf3:439d:c931"' +$ Node2.wpanctl('get -v IPv6:LinkLocalAddress') -> '"fe80::ec08:f348:646f:d37d"' +- Node1 sent 20 bytes (":YeQuNKjuOtd%H#ipM7P") to [fe80::ec08:f348:646f:d37d]:404 from [fe80::bcec:daf3:439d:c931]:12557 +- Node2 received 20 bytes (":YeQuNKjuOtd%H#ipM7P") on port 404 from [fe80::bcec:daf3:439d:c931]:12557 + +``` diff --git a/tests/toranj/build.sh b/tests/toranj/build.sh index adcb71c32..4c25b1a2a 100755 --- a/tests/toranj/build.sh +++ b/tests/toranj/build.sh @@ -38,6 +38,10 @@ display_usage() echo " ncp-15.4 : Build OpenThread NCP FTD mode with simulation platform - 15.4 radio" echo " ncp-trel : Build OpenThread NCP FTD mode with simulation platform - TREL radio " echo " ncp-15.4+trel : Build OpenThread NCP FTD mode with simulation platform - multi radio (15.4+TREL)" + echo " cli : Build OpenThread CLI FTD mode with simulation platform" + echo " cli-15.4 : Build OpenThread CLI FTD mode with simulation platform - 15.4 radio" + echo " cli-trel : Build OpenThread CLI FTD mode with simulation platform - TREL radio " + echo " cli-15.4+trel : Build OpenThread CLI FTD mode with simulation platform - multi radio (15.4+TREL)" echo " rcp : Build OpenThread RCP (NCP in radio mode) with simulation platform" echo " posix : Build OpenThread POSIX NCP" echo " posix-15.4 : Build OpenThread POSIX NCP - 15.4 radio" @@ -92,7 +96,7 @@ fi build_config=$1 -configure_options=( +ncp_configure_options=( "--disable-docs" "--enable-tests=$tests" "--enable-coverage=$coverage" @@ -100,6 +104,14 @@ configure_options=( "--enable-ncp" ) +cli_configure_options=( + "--disable-docs" + "--enable-tests=$tests" + "--enable-coverage=$coverage" + "--enable-ftd" + "--enable-cli" +) + posix_configure_options=( "--disable-docs" "--enable-tests=$tests" @@ -127,7 +139,7 @@ case ${build_config} in ${top_srcdir}/configure \ CPPFLAGS="$cppflags_config" \ --with-examples=simulation \ - "${configure_options[@]}" || die + "${ncp_configure_options[@]}" || die make -j 8 || die ;; @@ -143,7 +155,7 @@ case ${build_config} in ${top_srcdir}/configure \ CPPFLAGS="$cppflags_config" \ --with-examples=simulation \ - "${configure_options[@]}" || die + "${ncp_configure_options[@]}" || die make -j 8 || die cp -p ${top_builddir}/examples/apps/ncp/ot-ncp-ftd ${top_builddir}/examples/apps/ncp/ot-ncp-ftd-15.4 ;; @@ -160,7 +172,7 @@ case ${build_config} in ${top_srcdir}/configure \ CPPFLAGS="$cppflags_config" \ --with-examples=simulation \ - "${configure_options[@]}" || die + "${ncp_configure_options[@]}" || die make -j 8 || die cp -p ${top_builddir}/examples/apps/ncp/ot-ncp-ftd ${top_builddir}/examples/apps/ncp/ot-ncp-ftd-trel ;; @@ -177,11 +189,76 @@ case ${build_config} in ${top_srcdir}/configure \ CPPFLAGS="$cppflags_config" \ --with-examples=simulation \ - "${configure_options[@]}" || die + "${ncp_configure_options[@]}" || die make -j 8 || die cp -p ${top_builddir}/examples/apps/ncp/ot-ncp-ftd ${top_builddir}/examples/apps/ncp/ot-ncp-ftd-15.4-trel ;; + cli | cli-) + echo "===================================================================================================" + echo "Building OpenThread CLI FTD mode with simulation platform (radios determined by config)" + echo "===================================================================================================" + ./bootstrap || die "bootstrap failed" + cd "${top_builddir}" || die "cd failed" + cppflags_config='-DOPENTHREAD_PROJECT_CORE_CONFIG_FILE=\"../tests/toranj/openthread-core-toranj-config-simulation.h\"' + ${top_srcdir}/configure \ + CPPFLAGS="$cppflags_config" \ + --with-examples=simulation \ + "${cli_configure_options[@]}" || die + make -j 8 || die + ;; + + cli-15.4) + echo "===================================================================================================" + echo "Building OpenThread CLI FTD mode with simulation platform - 15.4 radio" + echo "===================================================================================================" + cppflags_config='-DOPENTHREAD_PROJECT_CORE_CONFIG_FILE=\"../tests/toranj/openthread-core-toranj-config-simulation.h\"' + cppflags_config="${cppflags_config} -DOPENTHREAD_CONFIG_RADIO_LINK_IEEE_802_15_4_ENABLE=1" + cppflags_config="${cppflags_config} -DOPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE=0" + ./bootstrap || die "bootstrap failed" + cd "${top_builddir}" || die "cd failed" + ${top_srcdir}/configure \ + CPPFLAGS="$cppflags_config" \ + --with-examples=simulation \ + "${cli_configure_options[@]}" || die + make -j 8 || die + cp -p ${top_builddir}/examples/apps/cli/ot-cli-ftd ${top_builddir}/examples/apps/cli/ot-cli-ftd-15.4 + ;; + + cli-trel) + echo "===================================================================================================" + echo "Building OpenThread CLI FTD mode with simulation platform - TREL radio" + echo "===================================================================================================" + cppflags_config='-DOPENTHREAD_PROJECT_CORE_CONFIG_FILE=\"../tests/toranj/openthread-core-toranj-config-simulation.h\"' + cppflags_config="${cppflags_config} -DOPENTHREAD_CONFIG_RADIO_LINK_IEEE_802_15_4_ENABLE=0" + cppflags_config="${cppflags_config} -DOPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE=1" + ./bootstrap || die "bootstrap failed" + cd "${top_builddir}" || die "cd failed" + ${top_srcdir}/configure \ + CPPFLAGS="$cppflags_config" \ + --with-examples=simulation \ + "${cli_configure_options[@]}" || die + make -j 8 || die + cp -p ${top_builddir}/examples/apps/cli/ot-cli-ftd ${top_builddir}/examples/apps/cli/ot-cli-ftd-trel + ;; + + cli-15.4+trel | cli-trel+15.4) + echo "===================================================================================================" + echo "Building OpenThread NCP FTD mode with simulation platform - multi radio (15.4 + TREL)" + echo "===================================================================================================" + cppflags_config='-DOPENTHREAD_PROJECT_CORE_CONFIG_FILE=\"../tests/toranj/openthread-core-toranj-config-simulation.h\"' + cppflags_config="${cppflags_config} -DOPENTHREAD_CONFIG_RADIO_LINK_IEEE_802_15_4_ENABLE=1" + cppflags_config="${cppflags_config} -DOPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE=1" + ./bootstrap || die "bootstrap failed" + cd "${top_builddir}" || die "cd failed" + ${top_srcdir}/configure \ + CPPFLAGS="$cppflags_config" \ + --with-examples=simulation \ + "${cli_configure_options[@]}" || die + make -j 8 || die + cp -p ${top_builddir}/examples/apps/cli/ot-cli-ftd ${top_builddir}/examples/apps/cli/ot-cli-ftd-15.4-trel + ;; + rcp) echo "====================================================================================================" echo "Building OpenThread RCP (NCP in radio mode) with simulation platform" diff --git a/tests/toranj/cli/cli.py b/tests/toranj/cli/cli.py new file mode 100644 index 000000000..184271b09 --- /dev/null +++ b/tests/toranj/cli/cli.py @@ -0,0 +1,630 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2021, The OpenThread Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + +import sys +import os +import time +import re +import random +import string +import subprocess +import pexpect +import pexpect.popen_spawn +import signal +import inspect +import weakref + +# ---------------------------------------------------------------------------------------------------------------------- +# Constants + +JOIN_TYPE_ROUTER = 'router' +JOIN_TYPE_END_DEVICE = 'ed' +JOIN_TYPE_SLEEPY_END_DEVICE = 'sed' + +# ---------------------------------------------------------------------------------------------------------------------- + + +def _log(text, new_line=True, flush=True): + sys.stdout.write(text) + if new_line: + sys.stdout.write('\n') + if flush: + sys.stdout.flush() + + +# ---------------------------------------------------------------------------------------------------------------------- +# CliError class + + +class CliError(Exception): + + def __init__(self, error_code, message): + self._error_code = error_code + self._message = message + + @property + def error_code(self): + return self._error_code + + @property + def message(self): + return self._message + + +# ---------------------------------------------------------------------------------------------------------------------- +# Node class + + +class Node(object): + """ An OT CLI instance """ + + # defines the default verbosity setting (can be changed per `Node`) + _VERBOSE = os.getenv('TORANJ_VERBOSE', 'no').lower() in ['true', '1', 't', 'y', 'yes', 'on'] + + _SPEED_UP_FACTOR = 1 # defines the default time speed up factor + + # Determine whether to save logs in a file. + _SAVE_LOGS = True + + # name of log file (if _SAVE_LOGS is `True`) + _LOG_FNAME = 'ot-logs' + + _OT_BUILDDIR = os.getenv('top_builddir', '../../..') + + _OT_CLI_FTD = '%s/examples/apps/cli/ot-cli-ftd' % _OT_BUILDDIR + + _WAIT_TIME = 10 + + _START_INDEX = 1 + _cur_index = _START_INDEX + + _all_nodes = weakref.WeakSet() + + def __init__(self, verbose=_VERBOSE): + """Creates a new `Node` instance""" + + index = Node._cur_index + Node._cur_index += 1 + + self._index = index + self._verbose = verbose + + if Node._SAVE_LOGS: + self._log_file = open(self._LOG_FNAME + str(index) + '.log', 'wb') + else: + self._log_file = None + + cmd = f'{self._OT_CLI_FTD} --time-speed={self._SPEED_UP_FACTOR} {self._index}' + + if self._verbose: + _log(f'$ Node{index}.__init__() cmd: `{cmd}`') + + self._cli_process = pexpect.popen_spawn.PopenSpawn(cmd, logfile=self._log_file) + Node._all_nodes.add(self) + + def __del__(self): + self._finalize() + + def __repr__(self): + return f'Node(index={self._index})' + + @property + def index(self): + return self._index + + # ------------------------------------------------------------------------------------------------------------------ + # Executing a `cli` command + + def cli(self, *args): + """ Issues a CLI command on the given node and returns the resulting output. + + The returned result is a list of strings (with `\r\n` removed) as outputted by the CLI. + If executing the command fails, `CliError` is raised with error code and error message. + """ + + cmd = ' '.join([f'{arg}' for arg in args if arg is not None]).strip() + + if self._verbose: + _log(f'$ Node{self._index}.cli(\'{cmd}\')', new_line=False) + + self._cli_process.send(cmd + '\n') + index = self._cli_process.expect(['(.*)Done\r\n', '.*Error (\d+):(.*)\r\n']) + + if index == 0: + result = [ + line for line in self._cli_process.match.group(1).decode().splitlines() + if not self._is_ot_logg_line(line) if not line.strip().endswith(cmd) + ] + + if self._verbose: + if len(result) > 1: + _log(':') + for line in result: + _log(' ' + line) + elif len(result) == 1: + _log(f' -> {result[0]}') + else: + _log('') + + return result + else: + match = self._cli_process.match + e = CliError(int(match.group(1).decode()), match.group(2).decode().strip()) + if self._verbose: + _log(f': Error {e.message} ({e.error_code})') + raise e + + def _is_ot_logg_line(self, line): + return any(level in line for level in [' [D] ', ' [I] ', ' [N] ', ' [W] ', ' [C] ', ' [-] ']) + + def _cli_no_output(self, cmd, *args): + outputs = self.cli(cmd, *args) + verify(len(outputs) == 0) + + def _cli_single_output(self, cmd, expected_outputs=None): + outputs = self.cli(cmd) + verify(len(outputs) == 1) + verify((expected_outputs is None) or (outputs[0] in expected_outputs)) + return outputs[0] + + def _finalize(self): + if self._cli_process.proc.poll() is None: + if self._verbose: + _log(f'$ Node{self.index} terminating') + self._cli_process.send('exit\n') + self._cli_process.wait() + + # ------------------------------------------------------------------------------------------------------------------ + # cli commands + + def get_state(self): + return self._cli_single_output('state', ['detached', 'child', 'router', 'leader', 'disabled']) + + def get_channel(self): + return self._cli_single_output('channel') + + def set_channel(self, channel): + self._cli_no_output('channel', channel) + + def get_ext_addr(self): + return self._cli_single_output('extaddr') + + def set_ext_addr(self, ext_addr): + self._cli_no_output('extaddr', ext_addr) + + def get_ext_panid(self): + return self._cli_single_output('extpanid') + + def set_ext_panid(self, ext_panid): + self._cli_no_output('extpanid', ext_panid) + + def get_mode(self): + return self._cli_single_output('mode') + + def set_mode(self, mode): + self._cli_no_output('mode', mode) + + def get_network_key(self): + return self._cli_single_output('networkkey') + + def set_network_key(self, networkkey): + self._cli_no_output('networkkey', networkkey) + + def get_network_name(self): + return self._cli_single_output('networkname') + + def set_network_name(self, network_name): + self._cli_no_output('networkname', network_name) + + def get_panid(self): + return self._cli_single_output('panid') + + def set_panid(self, panid): + self._cli_no_output('panid', panid) + + def get_router_upgrade_threshold(self): + return self._cli_single_output('routerupgradethreshold') + + def set_router_upgrade_threshold(self, threshold): + self._cli_no_output('routerupgradethreshold', threshold) + + def get_router_selection_jitter(self): + return self._cli_single_output('routerselectionjitter') + + def set_router_selection_jitter(self, jitter): + self._cli_no_output('routerselectionjitter', jitter) + + def interface_up(self): + self._cli_no_output('ifconfig up') + + def interface_down(self): + self._cli_no_output('ifconfig down') + + def get_interface_state(self): + return self._cli_single_output('ifconfig') + + def thread_start(self): + self._cli_no_output('thread start') + + def thread_stop(self): + self._cli_no_output('thread stop') + + def get_ip_addrs(self): + return self.cli('ipaddr') + + def get_mleid_ip_addr(self): + return self._cli_single_output('ipaddr mleid') + + def get_linklocal_ip_addr(self): + return self._cli_single_output('ipaddr linklocal') + + def get_rloc_ip_addr(self): + return self._cli_single_output('ipaddr rloc') + + #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + # SRP client + + def srp_client_start(self, server_address, server_port): + self._cli_no_output('srp client start', server_address, server_port) + + def srp_client_stop(self): + self._cli_no_output('srp client stop') + + def srp_client_get_state(self): + return self._cli_single_output('srp client state', ['Enabled', 'Disabled']) + + def srp_client_get_auto_start_mode(self): + return self._cli_single_output('srp client autostart', ['Enabled', 'Disabled']) + + def srp_client_enable_auto_start_mode(self): + self._cli_no_output('srp client autostart enable') + + def srp_client_disable_auto_start_mode(self): + self._cli_no_output('srp client autostart disable') + + def srp_client_get_server_address(self): + return self._cli_single_output('srp client server address') + + def srp_client_get_server_port(self): + return self._cli_single_output('srp client server port') + + def srp_client_get_host_state(self): + return self._cli_single_output('srp client host state') + + def srp_client_set_host_name(self, name): + self._cli_no_output('srp client host name', name) + + def srp_client_get_host_name(self): + return self._cli_single_output('srp client host name') + + def srp_client_remove_host(self, remove_key=False, send_unreg_to_server=False): + self._cli_no_output('srp client host remove', int(remove_key), int(send_unreg_to_server)) + + def srp_client_clear_host(self): + self._cli_no_output('srp client host clear') + + def srp_client_set_host_address(self, *addrs): + self._cli_no_output('srp client host address', *addrs) + + def srp_client_get_host_address(self): + return self.cli('srp client host address') + + def srp_client_add_service(self, instance_name, service_name, port, priority=0, weight=0, txt_entries=[]): + txt_record = "".join(self._encode_txt_entry(entry) for entry in txt_entries) + self._cli_no_output('srp client service add', instance_name, service_name, port, priority, weight, txt_record) + + def srp_client_remove_service(self, instance_name, service_name): + self._cli_no_output('srp client service remove', instance_name, service_name) + + def srp_client_clear_service(self, instance_name, service_name): + self._cli_no_output('srp client service clear', instance_name, service_name) + + def srp_client_get_services(self): + outputs = self.cli('srp client service') + return [self._parse_srp_client_service(line) for line in outputs] + + def _encode_txt_entry(self, entry): + """Encodes the TXT entry to the DNS-SD TXT record format as a HEX string. + + Example usage: + self._encode_txt_entries(['abc']) -> '03616263' + self._encode_txt_entries(['def=']) -> '046465663d' + self._encode_txt_entries(['xyz=XYZ']) -> '0778797a3d58595a' + """ + return '{:02x}'.format(len(entry)) + "".join("{:02x}".format(ord(c)) for c in entry) + + def _parse_srp_client_service(self, line): + """Parse one line of srp service list into a dictionary which + maps string keys to string values. + + Example output for input + 'instance:\"%s\", name:\"%s\", state:%s, port:%d, priority:%d, weight:%d"' + { + 'instance': 'my-service', + 'name': '_ipps._udp', + 'state': 'ToAdd', + 'port': '12345', + 'priority': '0', + 'weight': '0' + } + + Note that value of 'port', 'priority' and 'weight' are represented + as strings but not integers. + """ + key_values = [word.strip().split(':') for word in line.split(', ')] + return {key_value[0].strip(): key_value[1].strip('"') for key_value in key_values} + + #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + # SRP server + + def srp_server_get_state(self): + return self._cli_single_output('srp server state', ['disabled', 'running', 'stopped']) + + def srp_server_get_addr_mode(self): + return self._cli_single_output('srp server addrmode', ['unicast', 'anycast']) + + def srp_server_set_addr_mode(self, mode): + self._cli_no_output('srp server addrmode', mode) + + def srp_server_get_anycast_seq_num(self): + return self._cli_single_output('srp server seqnum') + + def srp_server_set_anycast_seq_num(self, seqnum): + self._cli_no_output('srp server seqnum', seqnum) + + def srp_server_enable(self): + self._cli_no_output('srp server enable') + + def srp_server_disable(self): + self._cli_no_output('srp server disable') + + def srp_server_set_lease(self, min_lease, max_lease, min_key_lease, max_key_lease): + self._cli_no_output('srp server lease', min_lease, max_lease, min_key_lease, max_key_lease) + + def srp_server_get_hosts(self): + """Returns the host list on the SRP server as a list of property + dictionary. + + Example output: + [{ + 'fullname': 'my-host.default.service.arpa.', + 'name': 'my-host', + 'deleted': 'false', + 'addresses': ['2001::1', '2001::2'] + }] + """ + outputs = self.cli('srp server host') + host_list = [] + while outputs: + host = {} + host['fullname'] = outputs.pop(0).strip() + host['name'] = host['fullname'].split('.')[0] + host['deleted'] = outputs.pop(0).strip().split(':')[1].strip() + if host['deleted'] == 'true': + host_list.append(host) + continue + addresses = outputs.pop(0).strip().split('[')[1].strip(' ]').split(',') + map(str.strip, addresses) + host['addresses'] = [addr for addr in addresses if addr] + host_list.append(host) + return host_list + + def srp_server_get_host(self, host_name): + """Returns host on the SRP server that matches given host name. + + Example usage: + self.srp_server_get_host("my-host") + """ + for host in self.srp_server_get_hosts(): + if host_name == host['name']: + return host + + def srp_server_get_services(self): + """Returns the service list on the SRP server as a list of property + dictionary. + + Example output: + [{ + 'fullname': 'my-service._ipps._tcp.default.service.arpa.', + 'instance': 'my-service', + 'name': '_ipps._tcp', + 'deleted': 'false', + 'port': '12345', + 'priority': '0', + 'weight': '0', + 'TXT': ['abc=010203'], + 'host_fullname': 'my-host.default.service.arpa.', + 'host': 'my-host', + 'addresses': ['2001::1', '2001::2'] + }] + + Note that the TXT data is output as a HEX string. + """ + outputs = self.cli('srp server service') + service_list = [] + while outputs: + service = {} + service['fullname'] = outputs.pop(0).strip() + name_labels = service['fullname'].split('.') + service['instance'] = name_labels[0] + service['name'] = '.'.join(name_labels[1:3]) + service['deleted'] = outputs.pop(0).strip().split(':')[1].strip() + if service['deleted'] == 'true': + service_list.append(service) + continue + # 'subtypes', port', 'priority', 'weight' + for i in range(0, 4): + key_value = outputs.pop(0).strip().split(':') + service[key_value[0].strip()] = key_value[1].strip() + txt_entries = outputs.pop(0).strip().split('[')[1].strip(' ]').split(',') + txt_entries = map(str.strip, txt_entries) + service['TXT'] = [txt for txt in txt_entries if txt] + service['host_fullname'] = outputs.pop(0).strip().split(':')[1].strip() + service['host'] = service['host_fullname'].split('.')[0] + addresses = outputs.pop(0).strip().split('[')[1].strip(' ]').split(',') + addresses = map(str.strip, addresses) + service['addresses'] = [addr for addr in addresses if addr] + service_list.append(service) + return service_list + + def srp_server_get_service(self, instance_name, service_name): + """Returns service on the SRP server that matches given instance + name and service name. + + Example usage: + self.srp_server_get_service("my-service", "_ipps._tcp") + """ + for service in self.srp_server_get_services(): + if (instance_name == service['instance'] and service_name == service['name']): + return service + + # ------------------------------------------------------------------------------------------------------------------ + # Helper methods + + def form(self, network_name=None, network_key=None, channel=None, panid=0x1234, xpanid=None): + if network_name is not None: + self.set_network_name(network_name) + if network_key is not None: + self.set_network_key(network_key) + if channel is not None: + self.set_channel(channel) + if xpanid is not None: + self.set_ext_panid(xpanid) + self.set_panid(panid) + self.interface_up() + self.thread_start() + verify_within(_check_node_is_leader, self._WAIT_TIME, arg=self) + + def join(self, node, type=JOIN_TYPE_ROUTER): + self.set_network_name(node.get_network_name()) + self.set_network_key(node.get_network_key()) + self.set_channel(node.get_channel()) + self.set_panid(node.get_panid()) + if type == JOIN_TYPE_END_DEVICE: + self.set_mode('rn') + elif type == JOIN_TYPE_SLEEPY_END_DEVICE: + self.set_mode('-') + else: + self.set_mode('rdn') + self.set_router_selection_jitter(1) + self.interface_up() + self.thread_start() + if type == JOIN_TYPE_ROUTER: + verify_within(_check_node_is_router, self._WAIT_TIME, arg=self) + else: + verify_within(_check_node_is_child, self._WAIT_TIME, arg=self) + + def allowlist_node(self, node): + """Adds a given node to the allowlist of `self` and enables allowlisting on `self`""" + self._cli_no_output('macfilter addr add', node.get_ext_addr()) + self._cli_no_output('macfilter addr allowlist') + + def un_allowlist_node(self, node): + """Removes a given node (of node `Node) from the allowlist""" + self._cli_no_output('macfilter addr remove', node.get_ext_addr()) + + # ------------------------------------------------------------------------------------------------------------------ + # class methods + + @classmethod + def finalize_all_nodes(cls): + """Finalizes all previously created `Node` instances (stops the CLI process)""" + for node in Node._all_nodes: + node._finalize() + + @classmethod + def set_time_speedup_factor(cls, factor): + """Sets up the time speed up factor - should be set before creating any `Node` objects""" + if len(Node._all_nodes) != 0: + raise Node._NodeError('set_time_speedup_factor() cannot be called after creating a `Node`') + Node._SPEED_UP_FACTOR = factor + + +def _check_node_is_leader(node): + verify(node.get_state() == 'leader') + + +def _check_node_is_router(node): + verify(node.get_state() == 'router') + + +def _check_node_is_child(node): + verify(node.get_state() == 'child') + + +# ---------------------------------------------------------------------------------------------------------------------- + + +class VerifyError(Exception): + pass + + +_is_in_verify_within = False + + +def verify(condition): + """Verifies that a `condition` is true, otherwise raises a VerifyError""" + global _is_in_verify_within + if not condition: + calling_frame = inspect.currentframe().f_back + error_message = 'verify() failed at line {} in "{}"'.format(calling_frame.f_lineno, + calling_frame.f_code.co_filename) + if not _is_in_verify_within: + print(error_message) + raise VerifyError(error_message) + + +def verify_within(condition_checker_func, wait_time, arg=None, delay_time=0.1): + """Verifies that a given function `condition_checker_func` passes successfully within a given wait timeout. + `wait_time` is maximum time waiting for condition_checker to pass (in seconds). + `arg` is optional parameter and if it s not None, will be passed to `condition_checker_func()` + `delay_time` specifies a delay interval added between failed attempts (in seconds). + """ + global _is_in_verify_within + start_time = time.time() + old_is_in_verify_within = _is_in_verify_within + _is_in_verify_within = True + while True: + try: + if arg is None: + condition_checker_func() + else: + condition_checker_func(arg) + except VerifyError as e: + if time.time() - start_time > wait_time: + print('Took too long to pass the condition ({}>{} sec)'.format(time.time() - start_time, wait_time)) + print(e.message) + raise e + except BaseException: + raise + else: + break + if delay_time != 0: + time.sleep(delay_time) + _is_in_verify_within = old_is_in_verify_within diff --git a/tests/toranj/cli/test-001-get-set.py b/tests/toranj/cli/test-001-get-set.py new file mode 100755 index 000000000..f64b05ae6 --- /dev/null +++ b/tests/toranj/cli/test-001-get-set.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2021, The OpenThread Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from cli import verify +import cli + +import sys + +print(sys.version) + +# ----------------------------------------------------------------------------------------------------------------------- +# Test description: simple CLI get and set commands + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print('-' * 120) +print('Starting \'{}\''.format(test_name)) + +# ----------------------------------------------------------------------------------------------------------------------- +# Creating `Nodes` instances + +node = cli.Node() + +# ----------------------------------------------------------------------------------------------------------------------- +# Test implementation + +node.set_channel(21) +verify(node.get_channel() == '21') + +ext_addr = '1122334455667788' +node.set_ext_addr(ext_addr) +verify(node.get_ext_addr() == ext_addr) + +ext_panid = '1020031510006016' +node.set_ext_panid(ext_panid) +verify(node.get_ext_panid() == ext_panid) + +key = '0123456789abcdeffecdba9876543210' +node.set_network_key(key) +verify(node.get_network_key() == key) + +panid = '0xabba' +node.set_panid(panid) +verify(node.get_panid() == panid) + +mode = 'rd' +node.set_mode(mode) +verify(node.get_mode() == mode) + +threshold = '1' +node.set_router_upgrade_threshold(threshold) +verify(node.get_router_upgrade_threshold() == threshold) + +jitter = '100' +node.set_router_selection_jitter(jitter) +verify(node.get_router_selection_jitter() == jitter) + +verify(node.get_interface_state() == 'down') +verify(node.get_state() == 'disabled') + +# ----------------------------------------------------------------------------------------------------------------------- +# Test finished + +cli.Node.finalize_all_nodes() + +print('\'{}\' passed.'.format(test_name)) diff --git a/tests/toranj/cli/test-002-form.py b/tests/toranj/cli/test-002-form.py new file mode 100755 index 000000000..eea06d5f0 --- /dev/null +++ b/tests/toranj/cli/test-002-form.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2021, The OpenThread Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from cli import verify +from cli import verify_within +import cli + +# ----------------------------------------------------------------------------------------------------------------------- +# Test description: forming a Thread network + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print('-' * 120) +print('Starting \'{}\''.format(test_name)) + +# ----------------------------------------------------------------------------------------------------------------------- +# Creating `Nodes` instances + +speedup = 4 +cli.Node.set_time_speedup_factor(speedup) + +node = cli.Node() + +# ----------------------------------------------------------------------------------------------------------------------- +# Test implementation + +WAIT_TIME = 5 + +verify(node.get_state() == 'disabled') +node.form('test') + +verify(node.get_network_name() == 'test') + +node.interface_down() +verify(node.get_state() == 'disabled') + +node.form('form-test', + channel=21, + panid=0x5678, + xpanid='1020031510006016', + network_key='0123456789abcdeffecdba9876543210') + +verify(node.get_network_name() == 'form-test') +verify(node.get_channel() == '21') +verify(node.get_panid() == '0x5678') +verify(node.get_ext_panid() == '1020031510006016') +verify(node.get_network_key() == '0123456789abcdeffecdba9876543210') + +# ----------------------------------------------------------------------------------------------------------------------- +# Test finished + +cli.Node.finalize_all_nodes() + +print('\'{}\' passed.'.format(test_name)) diff --git a/tests/toranj/cli/test-003-join.py b/tests/toranj/cli/test-003-join.py new file mode 100755 index 000000000..f1cc05a4c --- /dev/null +++ b/tests/toranj/cli/test-003-join.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2021, The OpenThread Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from cli import verify +from cli import verify_within +import cli + +# ----------------------------------------------------------------------------------------------------------------------- +# Test description: joining (as router, end-device, sleepy) - two node network + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print('-' * 120) +print('Starting \'{}\''.format(test_name)) + +# ----------------------------------------------------------------------------------------------------------------------- +# Creating `cli.Nodes` instances + +speedup = 4 +cli.Node.set_time_speedup_factor(speedup) + +node1 = cli.Node() +node2 = cli.Node() + +WAIT_TIME = 5 + +# ----------------------------------------------------------------------------------------------------------------------- +# Test implementation + +node1.allowlist_node(node2) +node2.allowlist_node(node1) + +node1.form('join-net') +verify(node1.get_state() == 'leader') + +node2.join(node1) +verify(node2.get_state() == 'router') + +node2.interface_down() + +node2.join(node1, cli.JOIN_TYPE_END_DEVICE) +verify(node2.get_state() == 'child') +verify(node2.get_mode() == 'rn') + +node2.interface_down() + +node2.join(node1, cli.JOIN_TYPE_SLEEPY_END_DEVICE) +verify(node2.get_state() == 'child') +verify(node2.get_mode() == '-') + +# ----------------------------------------------------------------------------------------------------------------------- +# Test finished + +cli.Node.finalize_all_nodes() + +print('\'{}\' passed.'.format(test_name)) diff --git a/tests/toranj/cli/test-400-srp-client-server.py b/tests/toranj/cli/test-400-srp-client-server.py new file mode 100755 index 000000000..a74d26153 --- /dev/null +++ b/tests/toranj/cli/test-400-srp-client-server.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2021, The OpenThread Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from cli import verify +from cli import verify_within +import cli + +# ----------------------------------------------------------------------------------------------------------------------- +# Test description: joining (as router, end-device, sleepy) - two node network + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print('-' * 120) +print('Starting \'{}\''.format(test_name)) + +# ----------------------------------------------------------------------------------------------------------------------- +# Creating `cli.Nodes` instances + +speedup = 4 +cli.Node.set_time_speedup_factor(speedup) + +server = cli.Node() +client = cli.Node() + +WAIT_TIME = 5 + +# ----------------------------------------------------------------------------------------------------------------------- +# Test implementation + +server.form('srp-test') +verify(server.get_state() == 'leader') + +client.join(server) +verify(client.get_state() == 'router') + +# Check initial state of SRP client and server +verify(server.srp_server_get_state() == 'disabled') +verify(server.srp_server_get_addr_mode() == 'unicast') +verify(client.srp_client_get_state() == 'Disabled') +verify(client.srp_client_get_auto_start_mode() == 'Disabled') + +# Start server and client and register single service +server.srp_server_enable() +client.srp_client_enable_auto_start_mode() + +client.srp_client_set_host_name('host') +client.srp_client_set_host_address('fd00::cafe') +client.srp_client_add_service('ins', '_test._udp', 777, 2, 1) + + +def check_server_has_service(): + verify(len(server.srp_server_get_hosts()) > 0) + + +verify_within(check_server_has_service, WAIT_TIME) + +# Check state and service/host info on client and server + +verify(client.srp_client_get_auto_start_mode() == 'Enabled') +verify(client.srp_client_get_state() == 'Enabled') +verify(client.srp_client_get_server_address() == server.get_mleid_ip_addr()) + +verify(client.srp_client_get_host_state() == 'Registered') +verify(client.srp_client_get_host_name() == 'host') +addresses = client.srp_client_get_host_address() +verify(len(addresses) == 1) +verify(addresses[0] == 'fd00:0:0:0:0:0:0:cafe') + +services = client.srp_client_get_services() +verify(len(services) == 1) +service = services[0] +verify(service['instance'] == 'ins') +verify(service['name'] == '_test._udp') +verify(service['state'] == 'Registered') +verify(service['port'] == '777') +verify(service['priority'] == '2') +verify(service['weight'] == '1') + +verify(server.srp_server_get_state() == 'running') + +hosts = server.srp_server_get_hosts() +verify(len(hosts) == 1) +host = hosts[0] +verify(host['name'] == 'host') +verify(host['deleted'] == 'false') +verify(host['addresses'] == ['fd00:0:0:0:0:0:0:cafe']) + +services = server.srp_server_get_services() +verify(len(services) == 1) +service = services[0] +verify(service['instance'] == 'ins') +verify(service['name'] == '_test._udp') +verify(service['deleted'] == 'false') +verify(service['subtypes'] == '(null)') +verify(service['port'] == '777') +verify(service['priority'] == '2') +verify(service['weight'] == '1') +verify(service['host'] == 'host') +verify(service['addresses'] == ['fd00:0:0:0:0:0:0:cafe']) + +# ----------------------------------------------------------------------------------------------------------------------- +# Test finished + +cli.Node.finalize_all_nodes() + +print('\'{}\' passed.'.format(test_name)) diff --git a/tests/toranj/test-001-get-set.py b/tests/toranj/ncp/test-001-get-set.py similarity index 100% rename from tests/toranj/test-001-get-set.py rename to tests/toranj/ncp/test-001-get-set.py diff --git a/tests/toranj/test-002-form.py b/tests/toranj/ncp/test-002-form.py similarity index 100% rename from tests/toranj/test-002-form.py rename to tests/toranj/ncp/test-002-form.py diff --git a/tests/toranj/test-003-join.py b/tests/toranj/ncp/test-003-join.py similarity index 100% rename from tests/toranj/test-003-join.py rename to tests/toranj/ncp/test-003-join.py diff --git a/tests/toranj/test-004-scan.py b/tests/toranj/ncp/test-004-scan.py similarity index 100% rename from tests/toranj/test-004-scan.py rename to tests/toranj/ncp/test-004-scan.py diff --git a/tests/toranj/test-005-discover-scan.py b/tests/toranj/ncp/test-005-discover-scan.py similarity index 100% rename from tests/toranj/test-005-discover-scan.py rename to tests/toranj/ncp/test-005-discover-scan.py diff --git a/tests/toranj/test-006-traffic-router-end-device.py b/tests/toranj/ncp/test-006-traffic-router-end-device.py similarity index 100% rename from tests/toranj/test-006-traffic-router-end-device.py rename to tests/toranj/ncp/test-006-traffic-router-end-device.py diff --git a/tests/toranj/test-007-traffic-router-sleepy.py b/tests/toranj/ncp/test-007-traffic-router-sleepy.py similarity index 100% rename from tests/toranj/test-007-traffic-router-sleepy.py rename to tests/toranj/ncp/test-007-traffic-router-sleepy.py diff --git a/tests/toranj/test-008-permit-join.py b/tests/toranj/ncp/test-008-permit-join.py similarity index 100% rename from tests/toranj/test-008-permit-join.py rename to tests/toranj/ncp/test-008-permit-join.py diff --git a/tests/toranj/test-009-insecure-traffic-join.py b/tests/toranj/ncp/test-009-insecure-traffic-join.py similarity index 100% rename from tests/toranj/test-009-insecure-traffic-join.py rename to tests/toranj/ncp/test-009-insecure-traffic-join.py diff --git a/tests/toranj/test-010-on-mesh-prefix-config-gateway.py b/tests/toranj/ncp/test-010-on-mesh-prefix-config-gateway.py similarity index 100% rename from tests/toranj/test-010-on-mesh-prefix-config-gateway.py rename to tests/toranj/ncp/test-010-on-mesh-prefix-config-gateway.py diff --git a/tests/toranj/test-011-child-table.py b/tests/toranj/ncp/test-011-child-table.py similarity index 100% rename from tests/toranj/test-011-child-table.py rename to tests/toranj/ncp/test-011-child-table.py diff --git a/tests/toranj/test-012-multi-hop-traffic.py b/tests/toranj/ncp/test-012-multi-hop-traffic.py similarity index 100% rename from tests/toranj/test-012-multi-hop-traffic.py rename to tests/toranj/ncp/test-012-multi-hop-traffic.py diff --git a/tests/toranj/test-013-off-mesh-route-traffic.py b/tests/toranj/ncp/test-013-off-mesh-route-traffic.py similarity index 100% rename from tests/toranj/test-013-off-mesh-route-traffic.py rename to tests/toranj/ncp/test-013-off-mesh-route-traffic.py diff --git a/tests/toranj/test-014-ip6-address-add.py b/tests/toranj/ncp/test-014-ip6-address-add.py similarity index 100% rename from tests/toranj/test-014-ip6-address-add.py rename to tests/toranj/ncp/test-014-ip6-address-add.py diff --git a/tests/toranj/test-015-same-prefix-on-multiple-nodes.py b/tests/toranj/ncp/test-015-same-prefix-on-multiple-nodes.py similarity index 100% rename from tests/toranj/test-015-same-prefix-on-multiple-nodes.py rename to tests/toranj/ncp/test-015-same-prefix-on-multiple-nodes.py diff --git a/tests/toranj/test-016-neighbor-table.py b/tests/toranj/ncp/test-016-neighbor-table.py similarity index 100% rename from tests/toranj/test-016-neighbor-table.py rename to tests/toranj/ncp/test-016-neighbor-table.py diff --git a/tests/toranj/test-017-parent-reset-child-recovery.py b/tests/toranj/ncp/test-017-parent-reset-child-recovery.py similarity index 100% rename from tests/toranj/test-017-parent-reset-child-recovery.py rename to tests/toranj/ncp/test-017-parent-reset-child-recovery.py diff --git a/tests/toranj/test-018-child-supervision.py b/tests/toranj/ncp/test-018-child-supervision.py similarity index 100% rename from tests/toranj/test-018-child-supervision.py rename to tests/toranj/ncp/test-018-child-supervision.py diff --git a/tests/toranj/test-019-inform-previous-parent.py b/tests/toranj/ncp/test-019-inform-previous-parent.py similarity index 100% rename from tests/toranj/test-019-inform-previous-parent.py rename to tests/toranj/ncp/test-019-inform-previous-parent.py diff --git a/tests/toranj/test-020-router-table.py b/tests/toranj/ncp/test-020-router-table.py similarity index 100% rename from tests/toranj/test-020-router-table.py rename to tests/toranj/ncp/test-020-router-table.py diff --git a/tests/toranj/test-021-address-cache-table.py b/tests/toranj/ncp/test-021-address-cache-table.py similarity index 100% rename from tests/toranj/test-021-address-cache-table.py rename to tests/toranj/ncp/test-021-address-cache-table.py diff --git a/tests/toranj/test-022-multicast-ip6-address.py b/tests/toranj/ncp/test-022-multicast-ip6-address.py similarity index 100% rename from tests/toranj/test-022-multicast-ip6-address.py rename to tests/toranj/ncp/test-022-multicast-ip6-address.py diff --git a/tests/toranj/test-023-multicast-traffic.py b/tests/toranj/ncp/test-023-multicast-traffic.py similarity index 100% rename from tests/toranj/test-023-multicast-traffic.py rename to tests/toranj/ncp/test-023-multicast-traffic.py diff --git a/tests/toranj/test-024-partition-merge.py b/tests/toranj/ncp/test-024-partition-merge.py similarity index 100% rename from tests/toranj/test-024-partition-merge.py rename to tests/toranj/ncp/test-024-partition-merge.py diff --git a/tests/toranj/test-025-network-data-timeout.py b/tests/toranj/ncp/test-025-network-data-timeout.py similarity index 100% rename from tests/toranj/test-025-network-data-timeout.py rename to tests/toranj/ncp/test-025-network-data-timeout.py diff --git a/tests/toranj/test-026-slaac-address-wpantund.py b/tests/toranj/ncp/test-026-slaac-address-wpantund.py similarity index 100% rename from tests/toranj/test-026-slaac-address-wpantund.py rename to tests/toranj/ncp/test-026-slaac-address-wpantund.py diff --git a/tests/toranj/test-027-child-mode-change.py b/tests/toranj/ncp/test-027-child-mode-change.py similarity index 100% rename from tests/toranj/test-027-child-mode-change.py rename to tests/toranj/ncp/test-027-child-mode-change.py diff --git a/tests/toranj/test-028-router-leader-reset-recovery.py b/tests/toranj/ncp/test-028-router-leader-reset-recovery.py similarity index 100% rename from tests/toranj/test-028-router-leader-reset-recovery.py rename to tests/toranj/ncp/test-028-router-leader-reset-recovery.py diff --git a/tests/toranj/test-029-data-poll-interval.py b/tests/toranj/ncp/test-029-data-poll-interval.py similarity index 100% rename from tests/toranj/test-029-data-poll-interval.py rename to tests/toranj/ncp/test-029-data-poll-interval.py diff --git a/tests/toranj/test-030-slaac-address-ncp.py b/tests/toranj/ncp/test-030-slaac-address-ncp.py similarity index 100% rename from tests/toranj/test-030-slaac-address-ncp.py rename to tests/toranj/ncp/test-030-slaac-address-ncp.py diff --git a/tests/toranj/test-031-meshcop-joiner-commissioner.py b/tests/toranj/ncp/test-031-meshcop-joiner-commissioner.py similarity index 100% rename from tests/toranj/test-031-meshcop-joiner-commissioner.py rename to tests/toranj/ncp/test-031-meshcop-joiner-commissioner.py diff --git a/tests/toranj/test-032-child-attach-with-multiple-ip-addresses.py b/tests/toranj/ncp/test-032-child-attach-with-multiple-ip-addresses.py similarity index 100% rename from tests/toranj/test-032-child-attach-with-multiple-ip-addresses.py rename to tests/toranj/ncp/test-032-child-attach-with-multiple-ip-addresses.py diff --git a/tests/toranj/test-033-mesh-local-prefix-change.py b/tests/toranj/ncp/test-033-mesh-local-prefix-change.py similarity index 100% rename from tests/toranj/test-033-mesh-local-prefix-change.py rename to tests/toranj/ncp/test-033-mesh-local-prefix-change.py diff --git a/tests/toranj/test-034-poor-link-parent-child-attach.py b/tests/toranj/ncp/test-034-poor-link-parent-child-attach.py similarity index 100% rename from tests/toranj/test-034-poor-link-parent-child-attach.py rename to tests/toranj/ncp/test-034-poor-link-parent-child-attach.py diff --git a/tests/toranj/test-035-child-timeout-large-data-poll.py b/tests/toranj/ncp/test-035-child-timeout-large-data-poll.py similarity index 100% rename from tests/toranj/test-035-child-timeout-large-data-poll.py rename to tests/toranj/ncp/test-035-child-timeout-large-data-poll.py diff --git a/tests/toranj/test-036-wpantund-host-route-management.py b/tests/toranj/ncp/test-036-wpantund-host-route-management.py similarity index 100% rename from tests/toranj/test-036-wpantund-host-route-management.py rename to tests/toranj/ncp/test-036-wpantund-host-route-management.py diff --git a/tests/toranj/test-037-wpantund-auto-add-route-for-on-mesh-prefix.py b/tests/toranj/ncp/test-037-wpantund-auto-add-route-for-on-mesh-prefix.py similarity index 100% rename from tests/toranj/test-037-wpantund-auto-add-route-for-on-mesh-prefix.py rename to tests/toranj/ncp/test-037-wpantund-auto-add-route-for-on-mesh-prefix.py diff --git a/tests/toranj/test-038-clear-address-cache-for-sed.py b/tests/toranj/ncp/test-038-clear-address-cache-for-sed.py similarity index 100% rename from tests/toranj/test-038-clear-address-cache-for-sed.py rename to tests/toranj/ncp/test-038-clear-address-cache-for-sed.py diff --git a/tests/toranj/test-039-address-cache-table-snoop.py b/tests/toranj/ncp/test-039-address-cache-table-snoop.py similarity index 100% rename from tests/toranj/test-039-address-cache-table-snoop.py rename to tests/toranj/ncp/test-039-address-cache-table-snoop.py diff --git a/tests/toranj/test-040-network-data-stable-full.py b/tests/toranj/ncp/test-040-network-data-stable-full.py similarity index 100% rename from tests/toranj/test-040-network-data-stable-full.py rename to tests/toranj/ncp/test-040-network-data-stable-full.py diff --git a/tests/toranj/test-041-lowpan-fragmentation.py b/tests/toranj/ncp/test-041-lowpan-fragmentation.py similarity index 100% rename from tests/toranj/test-041-lowpan-fragmentation.py rename to tests/toranj/ncp/test-041-lowpan-fragmentation.py diff --git a/tests/toranj/test-042-meshcop-joiner-discerner.py b/tests/toranj/ncp/test-042-meshcop-joiner-discerner.py similarity index 100% rename from tests/toranj/test-042-meshcop-joiner-discerner.py rename to tests/toranj/ncp/test-042-meshcop-joiner-discerner.py diff --git a/tests/toranj/test-043-meshcop-joiner-router.py b/tests/toranj/ncp/test-043-meshcop-joiner-router.py similarity index 100% rename from tests/toranj/test-043-meshcop-joiner-router.py rename to tests/toranj/ncp/test-043-meshcop-joiner-router.py diff --git a/tests/toranj/test-100-mcu-power-state.py b/tests/toranj/ncp/test-100-mcu-power-state.py similarity index 100% rename from tests/toranj/test-100-mcu-power-state.py rename to tests/toranj/ncp/test-100-mcu-power-state.py diff --git a/tests/toranj/test-600-channel-manager-properties.py b/tests/toranj/ncp/test-600-channel-manager-properties.py similarity index 100% rename from tests/toranj/test-600-channel-manager-properties.py rename to tests/toranj/ncp/test-600-channel-manager-properties.py diff --git a/tests/toranj/test-601-channel-manager-channel-change.py b/tests/toranj/ncp/test-601-channel-manager-channel-change.py similarity index 100% rename from tests/toranj/test-601-channel-manager-channel-change.py rename to tests/toranj/ncp/test-601-channel-manager-channel-change.py diff --git a/tests/toranj/test-602-channel-manager-channel-select.py b/tests/toranj/ncp/test-602-channel-manager-channel-select.py similarity index 100% rename from tests/toranj/test-602-channel-manager-channel-select.py rename to tests/toranj/ncp/test-602-channel-manager-channel-select.py diff --git a/tests/toranj/test-603-channel-manager-announce-recovery.py b/tests/toranj/ncp/test-603-channel-manager-announce-recovery.py similarity index 100% rename from tests/toranj/test-603-channel-manager-announce-recovery.py rename to tests/toranj/ncp/test-603-channel-manager-announce-recovery.py diff --git a/tests/toranj/test-700-multi-radio-join.py b/tests/toranj/ncp/test-700-multi-radio-join.py similarity index 100% rename from tests/toranj/test-700-multi-radio-join.py rename to tests/toranj/ncp/test-700-multi-radio-join.py diff --git a/tests/toranj/test-701-multi-radio-probe.py b/tests/toranj/ncp/test-701-multi-radio-probe.py similarity index 100% rename from tests/toranj/test-701-multi-radio-probe.py rename to tests/toranj/ncp/test-701-multi-radio-probe.py diff --git a/tests/toranj/test-702-multi-radio-discovery-by-rx.py b/tests/toranj/ncp/test-702-multi-radio-discovery-by-rx.py similarity index 100% rename from tests/toranj/test-702-multi-radio-discovery-by-rx.py rename to tests/toranj/ncp/test-702-multi-radio-discovery-by-rx.py diff --git a/tests/toranj/test-703-multi-radio-mesh-header-msg.py b/tests/toranj/ncp/test-703-multi-radio-mesh-header-msg.py similarity index 100% rename from tests/toranj/test-703-multi-radio-mesh-header-msg.py rename to tests/toranj/ncp/test-703-multi-radio-mesh-header-msg.py diff --git a/tests/toranj/test-704-multi-radio-scan.py b/tests/toranj/ncp/test-704-multi-radio-scan.py similarity index 100% rename from tests/toranj/test-704-multi-radio-scan.py rename to tests/toranj/ncp/test-704-multi-radio-scan.py diff --git a/tests/toranj/test-705-multi-radio-discover-scan.py b/tests/toranj/ncp/test-705-multi-radio-discover-scan.py similarity index 100% rename from tests/toranj/test-705-multi-radio-discover-scan.py rename to tests/toranj/ncp/test-705-multi-radio-discover-scan.py diff --git a/tests/toranj/test-nnn-template.py b/tests/toranj/ncp/test-nnn-template.py similarity index 100% rename from tests/toranj/test-nnn-template.py rename to tests/toranj/ncp/test-nnn-template.py diff --git a/tests/toranj/wpan.py b/tests/toranj/ncp/wpan.py similarity index 99% rename from tests/toranj/wpan.py rename to tests/toranj/ncp/wpan.py index 283df0a52..c2c7a5157 100644 --- a/tests/toranj/wpan.py +++ b/tests/toranj/ncp/wpan.py @@ -278,7 +278,7 @@ RADIO_LINK_IEEE_802_15_4 = "IEEE_802_15_4" RADIO_LINK_TREL_UDP6 = "TREL_UDP6" RADIO_LINK_TOBLE = "TOBLE" -_OT_BUILDDIR = os.getenv('top_builddir', '../..') +_OT_BUILDDIR = os.getenv('top_builddir', '../../..') _WPANTUND_PREFIX = os.getenv('WPANTUND_PREFIX', '/usr/local') # ----------------------------------------------------------------------------------------------------------------------- diff --git a/tests/toranj/start.sh b/tests/toranj/start.sh index be232bc25..889844172 100755 --- a/tests/toranj/start.sh +++ b/tests/toranj/start.sh @@ -69,8 +69,7 @@ run() { counter=0 while true; do - - if sudo -E python "$1"; then + if sudo -E "${python_app}" "$1"; then cleanup return fi @@ -85,7 +84,7 @@ run() fi echo " *** TEST FAILED" - tail -n 40 wpantund-logs*.log + tail -n 40 "${log_file_name}"*.log exit 1 done } @@ -102,85 +101,104 @@ else coverage_option="" fi +if [ "$TORANJ_CLI" = 1 ]; then + app_name="cli" + python_app="python3" + log_file_name="ot-logs" +else + app_name="ncp" + python_app="python" + log_file_name="wpantund-logs" +fi + if [ "$TORANJ_RADIO" = "multi" ]; then # Build all combinations - ./build.sh "${coverage_option}" ncp-15.4 || die "ncp-15.4 build failed" + ./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}" ncp-trel || die "ncp-trel build 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}" ncp-15.4+trel || die "ncp-15.4+trel build 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}" ncp-"${TORANJ_RADIO}" || die "ncp build failed" + ./build.sh "${coverage_option}" "${app_name}"-"${TORANJ_RADIO}" || die "build failed" fi cleanup -if [ "$TORANJ_RADIO" = "multi" ]; then - run test-700-multi-radio-join.py - run test-701-multi-radio-probe.py - run test-702-multi-radio-discovery-by-rx.py - run test-703-multi-radio-mesh-header-msg.py - run test-704-multi-radio-scan.py - run test-705-multi-radio-discover-scan.py +if [ "$TORANJ_CLI" = 1 ]; then + run cli/test-001-get-set.py + run cli/test-002-form.py + run cli/test-003-join.py + run cli/test-400-srp-client-server.py exit 0 fi -run test-001-get-set.py -run test-002-form.py -run test-003-join.py -run test-004-scan.py -run test-005-discover-scan.py -run test-006-traffic-router-end-device.py -run test-007-traffic-router-sleepy.py -run test-008-permit-join.py -run test-009-insecure-traffic-join.py -run test-010-on-mesh-prefix-config-gateway.py -run test-011-child-table.py -run test-012-multi-hop-traffic.py -run test-013-off-mesh-route-traffic.py -run test-014-ip6-address-add.py -run test-015-same-prefix-on-multiple-nodes.py -run test-016-neighbor-table.py -run test-017-parent-reset-child-recovery.py -run test-018-child-supervision.py -run test-019-inform-previous-parent.py -run test-020-router-table.py -run test-021-address-cache-table.py -run test-022-multicast-ip6-address.py -run test-023-multicast-traffic.py -run test-024-partition-merge.py -run test-025-network-data-timeout.py -run test-026-slaac-address-wpantund.py -run test-027-child-mode-change.py -run test-028-router-leader-reset-recovery.py -run test-029-data-poll-interval.py -run test-030-slaac-address-ncp.py -run test-031-meshcop-joiner-commissioner.py -run test-032-child-attach-with-multiple-ip-addresses.py -run test-033-mesh-local-prefix-change.py -run test-034-poor-link-parent-child-attach.py -run test-035-child-timeout-large-data-poll.py -run test-036-wpantund-host-route-management.py -run test-037-wpantund-auto-add-route-for-on-mesh-prefix.py -run test-038-clear-address-cache-for-sed.py -run test-039-address-cache-table-snoop.py -run test-040-network-data-stable-full.py -run test-041-lowpan-fragmentation.py -run test-042-meshcop-joiner-discerner.py -run test-043-meshcop-joiner-router.py -run test-100-mcu-power-state.py -run test-600-channel-manager-properties.py -run test-601-channel-manager-channel-change.py +if [ "$TORANJ_RADIO" = "multi" ]; then + run ncp/test-700-multi-radio-join.py + run ncp/test-701-multi-radio-probe.py + run ncp/test-702-multi-radio-discovery-by-rx.py + run ncp/test-703-multi-radio-mesh-header-msg.py + run ncp/test-704-multi-radio-scan.py + run ncp/test-705-multi-radio-discover-scan.py + + exit 0 +fi + +run ncp/test-001-get-set.py +run ncp/test-002-form.py +run ncp/test-003-join.py +run ncp/test-004-scan.py +run ncp/test-005-discover-scan.py +run ncp/test-006-traffic-router-end-device.py +run ncp/test-007-traffic-router-sleepy.py +run ncp/test-008-permit-join.py +run ncp/test-009-insecure-traffic-join.py +run ncp/test-010-on-mesh-prefix-config-gateway.py +run ncp/test-011-child-table.py +run ncp/test-012-multi-hop-traffic.py +run ncp/test-013-off-mesh-route-traffic.py +run ncp/test-014-ip6-address-add.py +run ncp/test-015-same-prefix-on-multiple-nodes.py +run ncp/test-016-neighbor-table.py +run ncp/test-017-parent-reset-child-recovery.py +run ncp/test-018-child-supervision.py +run ncp/test-019-inform-previous-parent.py +run ncp/test-020-router-table.py +run ncp/test-021-address-cache-table.py +run ncp/test-022-multicast-ip6-address.py +run ncp/test-023-multicast-traffic.py +run ncp/test-024-partition-merge.py +run ncp/test-025-network-data-timeout.py +run ncp/test-026-slaac-address-wpantund.py +run ncp/test-027-child-mode-change.py +run ncp/test-028-router-leader-reset-recovery.py +run ncp/test-029-data-poll-interval.py +run ncp/test-030-slaac-address-ncp.py +run ncp/test-031-meshcop-joiner-commissioner.py +run ncp/test-032-child-attach-with-multiple-ip-addresses.py +run ncp/test-033-mesh-local-prefix-change.py +run ncp/test-034-poor-link-parent-child-attach.py +run ncp/test-035-child-timeout-large-data-poll.py +run ncp/test-036-wpantund-host-route-management.py +run ncp/test-037-wpantund-auto-add-route-for-on-mesh-prefix.py +run ncp/test-038-clear-address-cache-for-sed.py +run ncp/test-039-address-cache-table-snoop.py +run ncp/test-040-network-data-stable-full.py +run ncp/test-041-lowpan-fragmentation.py +run ncp/test-042-meshcop-joiner-discerner.py +run ncp/test-043-meshcop-joiner-router.py +run ncp/test-100-mcu-power-state.py +run ncp/test-600-channel-manager-properties.py +run ncp/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 test-602-channel-manager-channel-select.py + run ncp/test-602-channel-manager-channel-select.py fi -run test-603-channel-manager-announce-recovery.py +run ncp/test-603-channel-manager-announce-recovery.py exit 0