mirror of
https://github.com/espressif/openthread.git
synced 2026-09-08 18:20:06 +00:00
[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:
@@ -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
@@ -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)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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., 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.
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
|
||||
Executable
+90
@@ -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))
|
||||
Executable
+78
@@ -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))
|
||||
Executable
+80
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user