[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`).
This commit is contained in:
Abtin Keshavarzian
2022-03-22 14:28:08 -07:00
committed by GitHub
parent bc6ce12f03
commit 255a326b10
67 changed files with 1783 additions and 434 deletions
+39 -4
View File
@@ -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
+15 -362
View File
@@ -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<node_index>.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<node_index>.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)
---
+188
View File
@@ -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<node_index>.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., readevalprint 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.
+370
View File
@@ -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<node_index>.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<node_index>.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
```
+82 -5
View File
@@ -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"
+630
View File
@@ -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
+90
View File
@@ -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))
+78
View File
@@ -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))
+80
View File
@@ -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))
+130
View File
@@ -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))
@@ -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')
# -----------------------------------------------------------------------------------------------------------------------
+80 -62
View File
@@ -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