diff --git a/.github/workflows/examples-openiotsdk.yaml b/.github/workflows/examples-openiotsdk.yaml index 0d058e2ff1bdf2..f7a574877ad49f 100644 --- a/.github/workflows/examples-openiotsdk.yaml +++ b/.github/workflows/examples-openiotsdk.yaml @@ -28,6 +28,9 @@ jobs: name: Open IoT SDK examples building timeout-minutes: 90 + env: + TEST_NETWORK_NAME: OIStest + runs-on: ubuntu-latest if: github.actor != 'restyled-io[bot]' @@ -35,6 +38,7 @@ jobs: image: connectedhomeip/chip-build-openiotsdk:0.6.06 volumes: - "/tmp/bloat_reports:/tmp/bloat_reports" + options: --privileged steps: - uses: Wandalen/wretry.action@v1.0.36 @@ -58,6 +62,11 @@ jobs: timeout-minutes: 10 run: scripts/build/gn_bootstrap.sh + - name: Build and install Python controller + timeout-minutes: 10 + run: | + scripts/run_in_build_env.sh './scripts/build_python.sh --install_wheel build-env' + - name: Build shell example id: build_shell timeout-minutes: 10 @@ -77,3 +86,17 @@ jobs: openiotsdk release lock-app \ examples/lock-app/openiotsdk/build/chip-openiotsdk-lock-app-example.elf \ /tmp/bloat_reports/ + + - name: Test shell example + if: steps.build_shell.outcome == 'success' + timeout-minutes: 5 + run: | + scripts/examples/openiotsdk_example.sh -C test shell + + - name: Test lock-app example + if: steps.build_lock_app.outcome == 'success' + timeout-minutes: 5 + run: | + scripts/setup/openiotsdk/network_setup.sh -n $TEST_NETWORK_NAME up + scripts/run_in_ns.sh ${TEST_NETWORK_NAME}ns scripts/examples/openiotsdk_example.sh -C test -n ${TEST_NETWORK_NAME}tap lock-app + scripts/setup/openiotsdk/network_setup.sh -n $TEST_NETWORK_NAME down diff --git a/.vscode/launch.json b/.vscode/launch.json index f9bdb959233f7b..26d362a17794fc 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -434,7 +434,7 @@ "executable": "./build/chip-openiotsdk-${input:openiotsdkApp}-example.elf", "armToolchainPath": "${env:ARM_GCC_TOOLCHAIN_PATH}/bin", "servertype": "external", - "gdbTarget": ":31627", //GDBserver port on FVP + "gdbTarget": "${input:openiotsdkRemoteHost}:31627", //GDBserver port on FVP "overrideLaunchCommands": ["-enable-pretty-printing"], "runToEntryPoint": "main", "preLaunchTask": "Debug Open IoT SDK example", @@ -501,6 +501,12 @@ "description": "What Open IoT SDK example do you want to use?", "options": ["shell", "lock-app"], "default": "shell" + }, + { + "type": "promptString", + "id": "openiotsdkRemoteHost", + "description": "Type the hostname/IP address of external GDB target that you want to connect to. Leave blank for internal GDB server", + "default": "" } ] } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index f1e205623409f2..b6b5c5c5fa95ae 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -265,9 +265,33 @@ { "label": "Run Open IoT SDK example", "type": "shell", - "command": "scripts/examples/openiotsdk_example.sh", + "command": "scripts/run_in_ns.sh", "args": [ + "${input:openiotsdkNetworkNamespace}", + "scripts/examples/openiotsdk_example.sh", "-Crun", + "-n${input:openiotsdkNetworkInterface}", + "${input:openiotsdkExample}" + ], + "group": "test", + "problemMatcher": { + "pattern": { + "regexp": "^(.*):(\\d+):(\\d+):\\s+(warning|error):\\s+(.*)$", + "file": 1, + "line": 2, + "message": 5 + } + } + }, + { + "label": "Test Open IoT SDK example", + "type": "shell", + "command": "scripts/run_in_ns.sh", + "args": [ + "${input:openiotsdkNetworkNamespace}", + "scripts/examples/openiotsdk_example.sh", + "-Ctest", + "-n${input:openiotsdkNetworkInterface}", "${input:openiotsdkExample}" ], "group": "test", @@ -283,9 +307,12 @@ { "label": "Debug Open IoT SDK example", "type": "shell", - "command": "scripts/examples/openiotsdk_example.sh", + "command": "scripts/run_in_ns.sh", "args": [ + "${input:openiotsdkNetworkNamespace}", + "scripts/examples/openiotsdk_example.sh", "-Crun", + "-n${input:openiotsdkNetworkInterface}", "-dtrue", "${input:openiotsdkExample}" ], @@ -363,6 +390,18 @@ "options": ["shell", "lock-app"], "default": "shell" }, + { + "type": "promptString", + "id": "openiotsdkNetworkNamespace", + "description": "Type the network namespace that you want to use. \"default\" means host default network namespace", + "default": "default" + }, + { + "type": "promptString", + "id": "openiotsdkNetworkInterface", + "description": "Type the network interface name that you want to use. \"user\" means user network mode", + "default": "user" + }, { "type": "promptString", "id": "exampleGlob", diff --git a/scripts/examples/openiotsdk_example.sh b/scripts/examples/openiotsdk_example.sh index cfc52536820c62..faf1ae80349a7e 100755 --- a/scripts/examples/openiotsdk_example.sh +++ b/scripts/examples/openiotsdk_example.sh @@ -18,6 +18,7 @@ # Build and/or run Open IoT SDK examples. +IS_TEST=0 NAME="$(basename "$0")" HERE="$(dirname "$0")" CHIP_ROOT="$(realpath "$HERE"/../..)" @@ -34,21 +35,25 @@ FVP_BIN=FVP_Corstone_SSE-300_Ethos-U55 GDB_PLUGIN="$FAST_MODEL_PLUGINS_PATH/GDBRemoteConnection.so" OIS_CONFIG="$CHIP_ROOT/config/openiotsdk" FVP_CONFIG_FILE="$OIS_CONFIG/fvp/cs300.conf" +EXAMPLE_TEST_PATH="$CHIP_ROOT/src/test_driver/openiotsdk/integration-tests" TELNET_TERMINAL_PORT=5000 +FAILED_TESTS=0 +FVP_NETWORK="user" function show_usage() { cat < Action to execute + -C,--command Action to execute -d,--debug Build in debug mode -p,--path Build path + -n,--network FVP network interface name Examples: shell @@ -122,6 +127,12 @@ function run_fvp() { RUN_OPTIONS+=(--allow-debug-plugin --plugin "$GDB_PLUGIN") fi + if [[ $FVP_NETWORK == "user" ]]; then + RUN_OPTIONS+=(-C mps3_board.hostbridge.userNetworking=1) + else + RUN_OPTIONS+=(-C mps3_board.hostbridge.interfaceName="$FVP_NETWORK") + fi + echo "Running $EXAMPLE_EXE_PATH with options: ${RUN_OPTIONS[@]}" "$FVP_BIN" "${RUN_OPTIONS[@]}" -f "$FVP_CONFIG_FILE" --application "$EXAMPLE_EXE_PATH" >/dev/null 2>&1 & @@ -134,6 +145,53 @@ function run_fvp() { sleep 1 } +function run_test() { + + EXAMPLE_EXE_PATH="$BUILD_PATH/chip-openiotsdk-$EXAMPLE-example.elf" + # Check if executable file exists + if ! [ -f "$EXAMPLE_EXE_PATH" ]; then + echo "Error: $EXAMPLE_EXE_PATH does not exist." >&2 + exit 1 + fi + + # Check if FVP exists + if ! [ -x "$(command -v "$FVP_BIN")" ]; then + echo "Error: $FVP_BIN not installed." >&2 + exit 1 + fi + + # Activate Matter environment with pytest + source "$CHIP_ROOT"/scripts/activate.sh + + # Check if pytest exists + if ! [ -x "$(command -v pytest)" ]; then + echo "Error: pytest not installed." >&2 + exit 1 + fi + + TEST_OPTIONS=() + + if [[ $FVP_NETWORK ]]; then + TEST_OPTIONS+=(--networkInterface="$FVP_NETWORK") + fi + + if [[ -f $EXAMPLE_TEST_PATH/$EXAMPLE/test_report.json ]]; then + rm -rf "$EXAMPLE_TEST_PATH/$EXAMPLE"/test_report.json + fi + + set +e + pytest --json-report --json-report-summary --json-report-file="$EXAMPLE_TEST_PATH/$EXAMPLE"/test_report.json --binaryPath="$EXAMPLE_EXE_PATH" --fvp="$FVP_BIN" --fvpConfig="$FVP_CONFIG_FILE" "${TEST_OPTIONS[@]}" "$EXAMPLE_TEST_PATH/$EXAMPLE"/test_app.py + set -e + + if [[ ! -f $EXAMPLE_TEST_PATH/$EXAMPLE/test_report.json ]]; then + exit 1 + else + if [[ $(jq '.summary | has("failed")' $EXAMPLE_TEST_PATH/$EXAMPLE/test_report.json) == true ]]; then + FAILED_TESTS=$(jq '.summary.failed' "$EXAMPLE_TEST_PATH/$EXAMPLE"/test_report.json) + fi + fi +} + SHORT=C:,p:,d:.n:,c,s,h LONG=command:,path:,debug:.network:,clean,scratch,help OPTS=$(getopt -n build --options "$SHORT" --longoptions "$LONG" -- "$@") @@ -166,6 +224,10 @@ while :; do BUILD_PATH=$CHIP_ROOT/$2 shift 2 ;; + -n | --network) + FVP_NETWORK=$2 + shift 2 + ;; -* | --*) shift break @@ -174,7 +236,7 @@ while :; do echo "Unexpected option: $1" show_usage exit 2 - ;; + ;; esac done @@ -195,7 +257,7 @@ case "$1" in esac case "$COMMAND" in - build | run | build-run) ;; + build | run | test | build-run) ;; *) echo "Wrong command definition" show_usage @@ -217,3 +279,12 @@ fi if [[ "$COMMAND" == *"run"* ]]; then run_fvp fi + +if [[ "$COMMAND" == *"test"* ]]; then + IS_TEST=1 + run_test +fi + +if [[ $IS_TEST -eq 1 ]]; then + exit "$FAILED_TESTS" +fi diff --git a/scripts/run_in_ns.sh b/scripts/run_in_ns.sh new file mode 100755 index 00000000000000..33d4f6661f432d --- /dev/null +++ b/scripts/run_in_ns.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +# +# Copyright (c) 2022 Project CHIP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# This script executes the command given as an argument within +# CHIP_ROOT in a specific network namespace. + +NETWORK_NAMESPACE="default" + +function show_usage() { + cat <&2 + exit 1 +fi + +NETWORK_NAMESPACE=$1 +shift + +if [[ $NETWORK_NAMESPACE == "default" ]]; then + "$@" +else + if [ ! -f /var/run/netns/"$NETWORK_NAMESPACE" ]; then + echo "$NETWORK_NAMESPACE network namespace does not exist" + show_usage >&2 + exit 1 + fi + echo "Run command: $@ in $NETWORK_NAMESPACE namespace" + if [ "$EUID" -ne 0 ]; then + sudo env PATH="$PATH" ip netns exec "$NETWORK_NAMESPACE" "$@" + else + ip netns exec "$NETWORK_NAMESPACE" "$@" + fi +fi diff --git a/scripts/setup/openiotsdk/connect_if.sh b/scripts/setup/openiotsdk/connect_if.sh new file mode 100755 index 00000000000000..21c7460a01fb79 --- /dev/null +++ b/scripts/setup/openiotsdk/connect_if.sh @@ -0,0 +1,72 @@ +#!/bin/bash + +# +# Copyright (c) 2022 Project CHIP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Enable/disable/restart TAP/TUN Open IoT SDK networking environment. + +HOST_BRIDGE="ARMhbr" +DEFAULT_ROUTE_IF="" +USER="$(id -u -n)" +INTERFACES=() + +declare -A default_if_info + +if [ "$EUID" -ne 0 ]; then + echo "Run a script with root permissions" + exit 1 +fi + +function show_usage() { + cat < ... + +Connect specific network interfaces with the default route interface. Create a bridge and link all interfaces to it. +Keep the default route of network traffic. + +EOF +} + +function get_default_if_info() { + default_if_info[ip]="$(ifconfig "$DEFAULT_ROUTE_IF" | grep -w inet | awk '{print $2}' | cut -d ":" -f 2)" + default_if_info[netmask]="$(ifconfig "$DEFAULT_ROUTE_IF" | grep -w inet | awk '{print $4}' | cut -d ":" -f 2)" + default_if_info[broadcast]="$(ifconfig "$DEFAULT_ROUTE_IF" | grep -w inet | awk '{print $6}' | cut -d ":" -f 2)" + default_if_info[gateway]="$(ip route show 0.0.0.0/0 dev "$DEFAULT_ROUTE_IF" | cut -d\ -f3)" +} + +function connect_with_host() { + ip link add name "$HOST_BRIDGE" type bridge + ip link set "$DEFAULT_ROUTE_IF" master "$HOST_BRIDGE" + ip addr flush dev "$DEFAULT_ROUTE_IF" + for interface in "${INTERFACES[@]}"; do + ip link set "$interface" master "$HOST_BRIDGE" + ip addr flush dev "$interface" + done + ifconfig "$HOST_BRIDGE" "${default_if_info[ip]}" netmask "${default_if_info[netmask]}" broadcast "${default_if_info[broadcast]}" + route add default gw "${default_if_info[gateway]}" "$HOST_BRIDGE" +} + +if [[ $# -lt 1 ]]; then + show_usage >&2 + exit 1 +fi + +INTERFACES=("$*") +DEFAULT_ROUTE_IF=$(route | grep '^default' | grep -o '[^ ]*$') +echo "Default route interface $DEFAULT_ROUTE_IF" +get_default_if_info +echo "Connect $INTERFACES to $DEFAULT_ROUTE_IF via bridge" +connect_with_host diff --git a/scripts/setup/openiotsdk/network_setup.sh b/scripts/setup/openiotsdk/network_setup.sh new file mode 100755 index 00000000000000..e5d5739ac129e1 --- /dev/null +++ b/scripts/setup/openiotsdk/network_setup.sh @@ -0,0 +1,189 @@ +#!/bin/bash + +# +# Copyright (c) 2022 Project CHIP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Enable/disable/restart Open IoT SDK networking environment. + +NAMESPACE_NAME="ns" +HOST_SIDE_IF_NAME="hveth" +NAMESPACE_SIDE_IF_NAME="nveth" +TAP_TUN_INTERFACE_NAME="tap" +BRIDGE_INTERFACE_NAME="br" +HOST_IPV6_ADDR="fe00::1" +NAMESPACE_IPV6_ADDR="fe00::2" +HOST_IPV4_ADDR="10.200.1.1" +NAMESPACE_IPV4_ADDR="10.200.1.2" +NAME="ARM" +INTERNET_ENABLE=false +USER="$(id -u -n)" + +if [ "$EUID" -ne 0 ]; then + echo "Run a script with root permissions" + exit 1 +fi + +function show_usage() { + cat < Open IoT SDK network base name + -u,--user Network user + -I,--Internet Add Internet connection support to network namespace + +command: + up + down + restart + +EOF +} + +function net_ns_up() { + # Enable IPv6 and IP-forwarding + sysctl net.ipv6.conf.all.disable_ipv6=0 net.ipv4.conf.all.forwarding=1 net.ipv6.conf.all.forwarding=1 + + echo "Create $NAMESPACE_NAME network namespace" + # Create namespace. + ip netns add "$NAMESPACE_NAME" + + # Enable lo interface in namespace + ip netns exec "$NAMESPACE_NAME" ip link set dev lo up + + echo "Adding $HOST_SIDE_IF_NAME veth with peer $NAMESPACE_SIDE_IF_NAME" + # Create two virtual interfaces and link them - one on host side, one on namespace side. + ip link add "$HOST_SIDE_IF_NAME" type veth peer name "$NAMESPACE_SIDE_IF_NAME" + + # Give the host a known IPv6 addr and set the host side up + echo "Set IP addresses $HOST_IPV4_ADDR/24 $HOST_IPV6_ADDR/64 to $HOST_SIDE_IF_NAME interface" + ip addr add "$HOST_IPV4_ADDR"/24 dev "$HOST_SIDE_IF_NAME" + ip -6 addr add "$HOST_IPV6_ADDR"/64 dev "$HOST_SIDE_IF_NAME" + ip link set "$HOST_SIDE_IF_NAME" up + + echo "Adding $NAMESPACE_SIDE_IF_NAME veth to namespace $NAMESPACE_NAME" + # Associate namespace IF with the namespace + ip link set "$NAMESPACE_SIDE_IF_NAME" netns "$NAMESPACE_NAME" + ip netns exec "$NAMESPACE_NAME" ip link set dev "$NAMESPACE_SIDE_IF_NAME" up + + echo "Create $TAP_TUN_INTERFACE_NAME TAP device" + ip netns exec "$NAMESPACE_NAME" ip tuntap add dev "$TAP_TUN_INTERFACE_NAME" mode tap user "$USER" + ip netns exec "$NAMESPACE_NAME" ifconfig "$TAP_TUN_INTERFACE_NAME" 0.0.0.0 promisc + + echo "Create $BRIDGE_INTERFACE_NAME bridge interface between $NAMESPACE_SIDE_IF_NAME and $TAP_TUN_INTERFACE_NAME" + ip netns exec "$NAMESPACE_NAME" ip link add "$BRIDGE_INTERFACE_NAME" type bridge + echo "Set IP addresses $NAMESPACE_IPV4_ADDR/24 $NAMESPACE_IPV6_ADDR/64 to $BRIDGE_INTERFACE_NAME bridge interface" + ip netns exec "$NAMESPACE_NAME" ip -6 addr add "$NAMESPACE_IPV6_ADDR"/64 dev "$BRIDGE_INTERFACE_NAME" + ip netns exec "$NAMESPACE_NAME" ip addr add "$NAMESPACE_IPV4_ADDR"/24 dev "$BRIDGE_INTERFACE_NAME" + ip netns exec "$NAMESPACE_NAME" ip addr flush dev "$NAMESPACE_SIDE_IF_NAME" + ip netns exec "$NAMESPACE_NAME" ip link set "$TAP_TUN_INTERFACE_NAME" master "$BRIDGE_INTERFACE_NAME" + ip netns exec "$NAMESPACE_NAME" ip link set "$NAMESPACE_SIDE_IF_NAME" master "$BRIDGE_INTERFACE_NAME" + ip netns exec "$NAMESPACE_NAME" ip link set dev "$BRIDGE_INTERFACE_NAME" up + + ip netns exec "$NAMESPACE_NAME" ip route add default via "$HOST_IPV4_ADDR" + + if "$INTERNET_ENABLE"; then + echo "Set Internet connection to $NAMESPACE_NAME namespace" + DEFAULT_ROUTE=$(route | grep '^default' | grep -o '[^ ]*$') + echo "Default route interface $DEFAULT_ROUTE" + # Enable masquerading of namespace IP address + iptables -t nat -A POSTROUTING -s "$NAMESPACE_IPV4_ADDR"/24 -o "$DEFAULT_ROUTE" -j MASQUERADE + + iptables -A FORWARD -i "$DEFAULT_ROUTE" -o "$HOST_SIDE_IF_NAME" -j ACCEPT + iptables -A FORWARD -o "$DEFAULT_ROUTE" -i "$HOST_SIDE_IF_NAME" -j ACCEPT + fi + + echo "$NAMESPACE_NAME namespace configuration" + ip netns exec "$NAMESPACE_NAME" ifconfig + echo "Host configuration" + ifconfig +} + +function net_ns_down() { + ip netns delete "$NAMESPACE_NAME" + ip link delete dev "$HOST_SIDE_IF_NAME" + echo "Host configuration" + ifconfig +} + +SHORT=n:,u:,I,h, +LONG=name:,user:,Internet,help +OPTS=$(getopt -n build --options "$SHORT" --longoptions "$LONG" -- "$@") + +eval set -- "$OPTS" + +while :; do + case "$1" in + -h | --help) + show_usage + exit 0 + ;; + -n | --name) + NAME=$2 + shift 2 + ;; + -u | --user) + USER=$2 + shift 2 + ;; + -I | --Internet) + INTERNET_ENABLE=true + shift + ;; + -* | --*) + shift + break + ;; + *) + echo "Unexpected option: $1" + show_usage + exit 2 + ;; + esac +done + +if [[ $# -lt 1 ]]; then + show_usage >&2 + exit 1 +fi + +case "$1" in + up | down | restart) + COMMAND=$1 + ;; + *) + echo "ERROR: Command $COMMAND not supported" + show_usage + exit 1 + ;; +esac + +NAMESPACE_NAME="$NAME$NAMESPACE_NAME" +HOST_SIDE_IF_NAME="$NAME$HOST_SIDE_IF_NAME" +NAMESPACE_SIDE_IF_NAME="$NAME$NAMESPACE_SIDE_IF_NAME" +TAP_TUN_INTERFACE_NAME="$NAME$TAP_TUN_INTERFACE_NAME" +BRIDGE_INTERFACE_NAME="$NAME$BRIDGE_INTERFACE_NAME" + +if [[ "$COMMAND" == *"down"* || "$COMMAND" == *"restart"* ]]; then + net_ns_down +fi + +if [[ "$COMMAND" == *"up"* || "$COMMAND" == *"restart"* ]]; then + net_ns_up +fi diff --git a/src/test_driver/openiotsdk/integration-tests/.gitignore b/src/test_driver/openiotsdk/integration-tests/.gitignore new file mode 100644 index 00000000000000..9e76edca71065a --- /dev/null +++ b/src/test_driver/openiotsdk/integration-tests/.gitignore @@ -0,0 +1 @@ +/**/test_report.json diff --git a/src/test_driver/openiotsdk/integration-tests/common/__init__.py b/src/test_driver/openiotsdk/integration-tests/common/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/src/test_driver/openiotsdk/integration-tests/common/device.py b/src/test_driver/openiotsdk/integration-tests/common/device.py new file mode 100644 index 00000000000000..eff76e1c5ad413 --- /dev/null +++ b/src/test_driver/openiotsdk/integration-tests/common/device.py @@ -0,0 +1,120 @@ +# +# Copyright (c) 2022 Project CHIP Authors +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import logging +import queue +from time import sleep, time +from typing import Optional + +log = logging.getLogger(__name__) + + +class Device: + + def __init__(self, name: Optional[str] = None): + """ + Base Device runner class containing device handling functions and logging + :param name: Logging name for the client + """ + self.iq = queue.Queue() + self.oq = queue.Queue() + if name is None: + self.name = str(hex(id(self))) + else: + self.name = name + + def send(self, command, expected_output=None, wait_before_read=None, wait_for_response=10, assert_output=True): + """ + Send command for client + :param command: Command + :param expected_output: Reply to wait from the client + :param wait_before_read: Timeout after write + :param wait_for_response: Timeout waiting the response + :param assert_output: Assert the fail situations to end the test run + :return: If there's expected output then the response line is returned + """ + log.debug('{}: Sending command to client: "{}"'.format( + self.name, command)) + self.flush(0) + self._write(command) + if expected_output is not None: + if wait_before_read is not None: + sleep(wait_before_read) + return self.wait_for_output(expected_output, wait_for_response, assert_output) + + def flush(self, timeout: float = 0) -> [str]: + """ + Flush the lines in the input queue + :param timeout: The timeout before flushing starts + :type timeout: float + :return: The lines removed from the input queue + :rtype: list of str + """ + sleep(timeout) + lines = [] + while True: + try: + lines.append(self._read_line(0)) + except queue.Empty: + return lines + + def wait_for_output(self, search: str, timeout: float = 10, assert_timeout: bool = True) -> [str]: + """ + Wait for expected output response + :param search: Expected response string + :type search: str + :param timeout: Response waiting time + :type timeout: float + :param assert_timeout: Assert on timeout situations + :type assert_timeout: bool + :return: Line received before a match + :rtype: list of str + """ + lines = [] + start = time() + now = 0 + timeout_error_msg = '{}: Didn\'t find {} in {} s'.format( + self.name, search, timeout) + + while time() - start <= timeout: + try: + line = self._read_line(1) + if line: + lines.append(line) + if search in line: + end = time() + return lines + + except queue.Empty: + last = now + now = time() + if now - start >= timeout: + if assert_timeout: + log.error(timeout_error_msg) + assert False, timeout_error_msg + else: + log.warning(timeout_error_msg) + return [] + if now - last > 1: + log.info('{}: Waiting for "{}" string... Timeout in {:.0f} s'.format(self.name, search, + abs(now - start - timeout))) + + def _write(self, data): + self.oq.put(data) + + def _read_line(self, timeout): + return self.iq.get(timeout=timeout) diff --git a/src/test_driver/openiotsdk/integration-tests/common/fixtures.py b/src/test_driver/openiotsdk/integration-tests/common/fixtures.py new file mode 100644 index 00000000000000..2613b196567c00 --- /dev/null +++ b/src/test_driver/openiotsdk/integration-tests/common/fixtures.py @@ -0,0 +1,122 @@ +# +# Copyright (c) 2022 Project CHIP Authors +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import pytest +import os +import pathlib +from time import sleep +import shutil + +from .telnet_connection import TelnetConnection +from .fvp_device import FvpDevice + +from chip import exceptions + +from chip import ChipDeviceCtrl +from chip.ChipStack import * +import chip.native +import chip.CertificateAuthority + +import logging +log = logging.getLogger(__name__) + + +@pytest.fixture(scope="session") +def rootDir(): + return pathlib.Path(__file__).parents[5].absolute() + + +@pytest.fixture(scope="session") +def fvp(request): + if request.config.getoption('fvp'): + return request.config.getoption('fvp') + else: + return shutil.which('FVP_Corstone_SSE-300_Ethos-U55') + + +@pytest.fixture(scope="session") +def fvpConfig(request, rootDir): + if request.config.getoption('fvpConfig'): + return request.config.getoption('fvpConfig') + else: + return os.path.join(rootDir, 'config/openiotsdk/fvp/cs300.conf') + + +@pytest.fixture(scope="session") +def telnetPort(request): + return request.config.getoption('telnetPort') + + +@pytest.fixture(scope="session") +def networkInterface(request): + if request.config.getoption('networkInterface'): + return request.config.getoption('networkInterface') + else: + return None + + +@pytest.fixture(scope="function") +def device(fvp, fvpConfig, binaryPath, telnetPort, networkInterface): + connection = TelnetConnection('localhost', telnetPort) + device = FvpDevice(fvp, fvpConfig, binaryPath, connection, networkInterface, "FVPdev") + device.start() + yield device + device.stop() + + +@pytest.fixture(scope="session") +def vendor_id(): + return 0xFFF1 + + +@pytest.fixture(scope="session") +def fabric_id(): + return 1 + + +@pytest.fixture(scope="session") +def node_id(): + return 1 + + +@pytest.fixture(scope="function") +def controller(vendor_id, fabric_id, node_id): + try: + chip.native.Init() + chipStack = chip.ChipStack.ChipStack( + persistentStoragePath='/tmp/openiotsdk-test-storage.json', enableServerInteractions=False) + certificateAuthorityManager = chip.CertificateAuthority.CertificateAuthorityManager( + chipStack, chipStack.GetStorageManager()) + certificateAuthorityManager.LoadAuthoritiesFromStorage() + if (len(certificateAuthorityManager.activeCaList) == 0): + ca = certificateAuthorityManager.NewCertificateAuthority() + ca.NewFabricAdmin(vendorId=vendor_id, fabricId=fabric_id) + elif (len(certificateAuthorityManager.activeCaList[0].adminList) == 0): + certificateAuthorityManager.activeCaList[0].NewFabricAdmin(vendorId=vendor_id, fabricId=fabric_id) + + caList = certificateAuthorityManager.activeCaList + + devCtrl = caList[0].adminList[0].NewController() + + except exceptions.ChipStackException as ex: + log.error("Controller initialization failed {}".format(ex)) + return None + except: + log.error("Controller initialization failed") + return None + + yield devCtrl diff --git a/src/test_driver/openiotsdk/integration-tests/common/fvp_device.py b/src/test_driver/openiotsdk/integration-tests/common/fvp_device.py new file mode 100644 index 00000000000000..3255e6a42eaf48 --- /dev/null +++ b/src/test_driver/openiotsdk/integration-tests/common/fvp_device.py @@ -0,0 +1,102 @@ +# +# Copyright (c) 2022 Project CHIP Authors +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import logging +import threading +import os +import subprocess +from time import sleep + +from .device import Device + +log = logging.getLogger(__name__) + + +class FvpDevice(Device): + + def __init__(self, fvp, fvp_config, binary_file, connection_channel, network_interface, name=None): + + self.run = False + self.connection_channel = connection_channel + super(FvpDevice, self).__init__(name) + + input_thread_name = '<-- {}'.format(self.name) + output_thread_name = '--> {}'.format(self.name) + + self.fvp_cmd = [ + fvp, + '-f', f'{fvp_config}', + '--application', f'{binary_file}', + '--quantum=25', + '-C', 'mps3_board.telnetterminal0.start_port={}'.format(self.connection_channel.get_port()) + ] + + if network_interface != None: + self.fvp_cmd.extend(['-C', 'mps3_board.hostbridge.interfaceName={}'.format(network_interface), ]) + else: + self.fvp_cmd.extend(['-C', 'mps3_board.hostbridge.userNetworking=1']) + + self.it = threading.Thread( + target=self._input_thread, name=input_thread_name) + self.ot = threading.Thread( + target=self._output_thread, name=output_thread_name) + + def start(self): + """ + Start the FVP and connection channel with device + """ + log.info('Starting "{}" runner...'.format(self.name)) + + self.proc = subprocess.Popen(self.fvp_cmd) + sleep(3) + self.connection_channel.open() + self.run = True + self.it.start() + self.ot.start() + log.info('"{}" runner started'.format(self.name)) + + def stop(self): + """ + Stop the FVP and connection channel + """ + log.info('Stopping "{}" runner...'.format(self.name)) + self.run = False + self.connection_channel.close() + self.oq.put(None) + self.it.join() + self.ot.join() + self.proc.terminate() + self.proc.wait() + log.info('"{}" runner stoped'.format(self.name)) + + def _input_thread(self): + while self.run: + line = self.connection_channel.readline() + if line: + log.info('<--|{}| {}'.format(self.name, line.strip())) + self.iq.put(line) + else: + pass + + def _output_thread(self): + while self.run: + line = self.oq.get() + if line: + log.info('-->|{}| {}'.format(self.name, line.strip())) + self.connection_channel.write(line) + else: + log.debug('Nothing sent') diff --git a/src/test_driver/openiotsdk/integration-tests/common/telnet_connection.py b/src/test_driver/openiotsdk/integration-tests/common/telnet_connection.py new file mode 100644 index 00000000000000..b5367ed5747387 --- /dev/null +++ b/src/test_driver/openiotsdk/integration-tests/common/telnet_connection.py @@ -0,0 +1,123 @@ +# Copyright (c) 2009-2021 Arm Limited +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +from time import sleep +import re + +from telnetlib import Telnet + +log = logging.getLogger(__name__) + + +class TelnetConnection: + """ + Telnet Connection class containing telnet connection handling functions + :param host: host name + :param port: prot number + """ + + def __init__(self, host=None, port=0): + self.telnet = Telnet() + self.host = host + self.port = port + self.is_open = False + + def open(self): + """ + Open telnet connection + """ + try: + self.telnet.open(self.host, self.port) + except Exception as e: + log.error('Open telnet connection to {}:{} failed {}'.format(self.host, self.port, e)) + return None + + self.is_open = True + + def __strip_escape(self, string_to_escape) -> str: + """ + Strip escape characters from string. + :param string_to_escape: string to work on + :return: stripped string + """ + raw_ansi_pattern = r'\033\[((?:\d|;)*)([a-zA-Z])' + ansi_pattern = raw_ansi_pattern.encode() + ansi_eng = re.compile(ansi_pattern) + matches = [] + for match in ansi_eng.finditer(string_to_escape): + matches.append(match) + matches.reverse() + for match in matches: + start = match.start() + end = match.end() + string_to_escape = string_to_escape[0:start] + string_to_escape[end:] + return string_to_escape + + def __formatline(self, line) -> str: + output_line = self.__strip_escape(line) + # Testapp uses \r to print characters to the same line, strip those and return only the last part + # If there is only one \r, don't remove anything. + if b'\r' in line and line.count(b'\r') > 1: + output_line = output_line.split(b'\r')[-2] + # Debug traces use tabulator characters, change those to spaces for readability + output_line = output_line.replace(b'\t', b' ') + output_line = output_line.decode('utf-8', 'ignore') + output_line.rstrip() + return output_line + + def readline(self): + """ + Read line from telnet session + :return: One line from telnet stream + """ + if not self.is_open: + return None + try: + output = self.telnet.read_until(b"\n", 1) + return self.__formatline(output) + except Exception as e: + log.error('Telnet read failed {}'.format(e)) + return None + + def write(self, data): + """ + Write data to telnet input + :param data: Data to send [bytes array] + """ + if not self.is_open: + return None + try: + data = data + '\n' + for item in data: + self.telnet.write(item.encode('utf-8')) + sleep(0.03) + except Exception as e: + log.error('Telnet write failed {}'.format(e)) + return None + + def close(self): + """ + Close telnet connection + """ + self.telnet.close() + self.is_open = False + + def get_port(self): + """ + Get port number of telnet connection + :return: Port Nnumber + """ + return self.port diff --git a/src/test_driver/openiotsdk/integration-tests/common/utils.py b/src/test_driver/openiotsdk/integration-tests/common/utils.py new file mode 100644 index 00000000000000..04d463d8558d38 --- /dev/null +++ b/src/test_driver/openiotsdk/integration-tests/common/utils.py @@ -0,0 +1,249 @@ +# +# Copyright (c) 2022 Project CHIP Authors +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import os +import random +import shlex +import re +import ctypes +import asyncio +from time import sleep + +from chip.setup_payload import SetupPayload +from chip import exceptions + +from chip.clusters import Objects as GeneratedObjects +from chip import discovery + +import logging +log = logging.getLogger(__name__) + +IP_ADDRESS_BUFFER_LEN = 100 + + +def get_setup_payload(device): + """ + Get device setup payload from logs + :param device: serial device instance + :return: setup payload or None + """ + ret = device.wait_for_output("SetupQRCode") + if ret == None or len(ret) < 2: + return None + + qr_code = re.sub( + r"[\[\]]", "", ret[-1].partition("SetupQRCode:")[2]).strip() + try: + setup_payload = SetupPayload().ParseQrCode( + "VP:vendorpayload%{}".format(qr_code)) + except exceptions.ChipStackError as ex: + log.error("SetupPayload failed {}".format(str(ex))) + return None + + return setup_payload + + +def discover_device(devCtrl, setupPayload): + """ + Discover specific device in network. + Search by device discriminator from setup payload + :param devCtrl: device controller instance + :param setupPayload: device setup payload + :return: CommissionableNode object if node device discovered or None if failed + """ + log.info("Attempting to find device on network") + longDiscriminator = int(setupPayload.attributes['Long discriminator']) + try: + res = devCtrl.DiscoverCommissionableNodes( + discovery.FilterType.LONG_DISCRIMINATOR, longDiscriminator, stopOnFirst=True, timeoutSecond=5) + except exceptions.ChipStackError as ex: + log.error("DiscoverCommissionableNodes failed {}".format(str(ex))) + return None + if not res: + log.info("Device not found") + return None + return res[0] + + +def connect_device(setupPayload, commissionableDevice, nodeId=None): + """ + Connect to Matter discovered device on network + :param setupPayload: device setup payload + :param commissionableDevice: CommissionableNode object with discovered device + :param nodeId: device node ID + :return: node ID if connection successful or None if failed + """ + if nodeId == None: + nodeId = random.randint(1, 1000000) + + pincode = int(setupPayload.attributes['SetUpPINCode']) + try: + commissionableDevice.Commission(nodeId, pincode) + except exceptions.ChipStackError as ex: + log.error("Commission discovered device failed {}".format(str(ex))) + return None + return nodeId + + +def disconnect_device(devCtrl, nodeId): + """ + Disconnect Matter device + :param devCtrl: device controller instance + :param nodeId: device node ID + :return: node ID if connection successful or None if failed + """ + try: + devCtrl.CloseSession(nodeId) + except exceptions.ChipStackException as ex: + log.error("CloseSession failed {}".format(str(ex))) + return False + return True + + +class ParsingError(exceptions.ChipStackException): + def __init__(self, msg=None): + self.msg = "Parsing Error: " + msg + + def __str__(self): + return self.msg + + +def ParseEncodedString(value): + if value.find(":") < 0: + raise ParsingError( + "Value should be encoded in encoding:encodedvalue format") + enc, encValue = value.split(":", 1) + if enc == "str": + return encValue.encode("utf-8") + b'\x00' + elif enc == "hex": + return bytes.fromhex(encValue) + raise ParsingError("Only str and hex encoding is supported") + + +def ParseValueWithType(value, type): + if type == 'int': + return int(value) + elif type == 'str': + return value + elif type == 'bytes': + return ParseEncodedString(value) + elif type == 'bool': + return (value.upper() not in ['F', 'FALSE', '0']) + else: + raise ParsingError('Cannot recognize type: {}'.format(type)) + + +def ParseValueWithStruct(value, cluster): + return eval(f"GeneratedObjects.{cluster}.Structs.{value}") + + +def ParseValue(value, valueType, cluster): + if valueType: + return ParseValueWithType(value, valueType) + elif value.find(":") > 0 and value.split(":", 1)[0] == "struct": + return ParseValueWithStruct(value.split(":", 1)[1], cluster) + else: + raise ParsingError('Cannot parse value: {}'.format(value)) + + +def FormatZCLArguments(cluster, args, cmdArgsWithType): + cmdArgsDict = {} + for kvPair in args: + if kvPair.find("=") < 0: + raise ParsingError("Argument should in key=value format") + key, value = kvPair.split("=", 1) + valueType = cmdArgsWithType.get(key, None) + cmdArgsDict[key] = ParseValue(value, valueType, cluster) + return cmdArgsDict + + +def send_zcl_command(devCtrl, line, requestTimeoutMs: int = None): + """ + Format and send ZCL message to device. + :param devCtrl: device controller instance + :param line: command line + :param requestTimeoutMs: command request timeout in ms + :return: error code and command response + """ + res = None + err = 0 + try: + args = shlex.split(line) + if len(args) < 4: + raise exceptions.InvalidArgumentCount(4, len(args)) + + cluster, command, nodeId, endpoint = args[0:4] + cmdArgsLine = args[4:] + allCommands = devCtrl.ZCLCommandList() + if cluster not in allCommands: + raise exceptions.UnknownCluster(cluster) + cmdArgsWithType = allCommands.get(cluster).get(command, None) + # When command takes no arguments, (not command) is True + if command == None: + raise exceptions.UnknownCommand(cluster, command) + + args = FormatZCLArguments(cluster, cmdArgsLine, cmdArgsWithType) + clusterObj = getattr(GeneratedObjects, cluster) + commandObj = getattr(clusterObj.Commands, command) + req = commandObj(**args) + + res = asyncio.run(devCtrl.SendCommand(int(nodeId), int(endpoint), req, timedRequestTimeoutMs=requestTimeoutMs)) + + except exceptions.ChipStackException as ex: + log.error("An exception occurred during processing ZCL command: {}".format(str(ex))) + err = -1 + except Exception as ex: + log.error("An exception occurred during processing input: {}".format(str(ex))) + err = -1 + + return (err, res) + + +def read_zcl_attribute(devCtrl, line): + """ + Read ZCL attribute from device: + + :param devCtrl: device controller instance + :param line: command line + :return: error code and attribute response + """ + res = None + err = 0 + try: + args = shlex.split(line) + if len(args) < 4: + raise exceptions.InvalidArgumentCount(4, len(args)) + + cluster, attribute, nodeId, endpoint = args[0:4] + allAttrs = devCtrl.ZCLAttributeList() + if cluster not in allAttrs: + raise exceptions.UnknownCluster(cluster) + + attrDetails = allAttrs.get(cluster).get(attribute, None) + if attrDetails == None: + raise exceptions.UnknownAttribute(cluster, attribute) + + res = devCtrl.ZCLReadAttribute(cluster, attribute, int( + nodeId), int(endpoint), 0) + except exceptions.ChipStackException as ex: + log.error("An exception occurred during processing ZCL attribute: {}".format(str(ex))) + err = -1 + except Exception as ex: + log.error("An exception occurred during processing input: {}".format(str(ex))) + err = -1 + + return (err, res) diff --git a/src/test_driver/openiotsdk/integration-tests/conftest.py b/src/test_driver/openiotsdk/integration-tests/conftest.py new file mode 100644 index 00000000000000..90116956e55dec --- /dev/null +++ b/src/test_driver/openiotsdk/integration-tests/conftest.py @@ -0,0 +1,38 @@ +# +# Copyright (c) 2022 Project CHIP Authors +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import pytest + +pytest_plugins = ['common.fixtures'] + + +def pytest_addoption(parser): + """ + Function for pytest to enable own custom commandline arguments + :param parser: argparser + :return: + """ + parser.addoption('--binaryPath', action='store', + help='Application binary path') + parser.addoption('--fvp', action='store', + help='FVP instance path') + parser.addoption('--fvpConfig', action='store', + help='FVP configuration file path') + parser.addoption('--telnetPort', action='store', + help='Telnet terminal port number.', default="5000") + parser.addoption('--networkInterface', action='store', + help='FVP network interface name') diff --git a/src/test_driver/openiotsdk/integration-tests/lock-app/__init__.py b/src/test_driver/openiotsdk/integration-tests/lock-app/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/src/test_driver/openiotsdk/integration-tests/lock-app/test_app.py b/src/test_driver/openiotsdk/integration-tests/lock-app/test_app.py new file mode 100644 index 00000000000000..d89d26dc32d698 --- /dev/null +++ b/src/test_driver/openiotsdk/integration-tests/lock-app/test_app.py @@ -0,0 +1,176 @@ +# +# Copyright (c) 2022 Project CHIP Authors +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import pytest +from time import sleep + +from common.utils import * + +from chip.clusters.Objects import DoorLock + +import logging +log = logging.getLogger(__name__) + + +@pytest.fixture(scope="session") +def binaryPath(request, rootDir): + if request.config.getoption('binaryPath'): + return request.config.getoption('binaryPath') + else: + return os.path.join(rootDir, 'examples/lock-app/openiotsdk/build/chip-openiotsdk-lock-app-example.elf') + + +@pytest.mark.smoketest +def test_smoke_test(device): + ret = device.wait_for_output("Open IoT SDK lock-app example application start") + assert ret != None and len(ret) > 0 + ret = device.wait_for_output("Open IoT SDK lock-app example application run") + assert ret != None and len(ret) > 0 + + +@pytest.mark.commissioningtest +def test_commissioning(device, controller): + assert controller != None + devCtrl = controller + + setupPayload = get_setup_payload(device) + assert setupPayload != None + + commissionable_device = discover_device(devCtrl, setupPayload) + assert commissionable_device != None + + assert commissionable_device.vendorId == int(setupPayload.attributes['VendorID']) + assert commissionable_device.productId == int(setupPayload.attributes['ProductID']) + assert commissionable_device.addresses[0] != None + + nodeId = connect_device(setupPayload, commissionable_device) + assert nodeId != None + log.info("Device {} connected".format(commissionable_device.addresses[0])) + + ret = device.wait_for_output("Commissioning completed successfully", timeout=30) + assert ret != None and len(ret) > 0 + + assert disconnect_device(devCtrl, nodeId) + + +LOCK_CTRL_TEST_PIN_CODE = 12345 +LOCK_CTRL_TEST_USER_INDEX = 1 +LOCK_CTRL_TEST_ENDPOINT_ID = 1 +LOCK_CTRL_TEST_USER_NAME = 'testUser' +LOCK_CTRL_TEST_CREDENTIAL_INDEX = 1 + + +@pytest.mark.ctrltest +def test_lock_ctrl(device, controller): + assert controller != None + devCtrl = controller + + setupPayload = get_setup_payload(device) + assert setupPayload != None + + commissionable_device = discover_device(devCtrl, setupPayload) + assert commissionable_device != None + + nodeId = connect_device(setupPayload, commissionable_device) + assert nodeId != None + + ret = device.wait_for_output("Commissioning completed successfully", timeout=30) + assert ret != None and len(ret) > 0 + + err, res = send_zcl_command( + devCtrl, "DoorLock SetUser {} {} operationType={} userIndex={} userName={} userUniqueId={} " + "userStatus={} userType={} credentialRule={}".format(nodeId, LOCK_CTRL_TEST_ENDPOINT_ID, + DoorLock.Enums.DlDataOperationType.kAdd, + LOCK_CTRL_TEST_USER_INDEX, + LOCK_CTRL_TEST_USER_NAME, + LOCK_CTRL_TEST_USER_INDEX, + DoorLock.Enums.DlUserStatus.kOccupiedEnabled, + DoorLock.Enums.DlUserType.kUnrestrictedUser, + DoorLock.Enums.DlCredentialRule.kSingle), requestTimeoutMs=1000) + assert err == 0 + + ret = device.wait_for_output("Successfully set the user [mEndpointId={},index={},adjustedIndex=0]".format( + LOCK_CTRL_TEST_ENDPOINT_ID, + LOCK_CTRL_TEST_USER_INDEX)) + assert ret != None and len(ret) > 0 + + err, res = send_zcl_command( + devCtrl, "DoorLock GetUser {} {} userIndex={}".format(nodeId, LOCK_CTRL_TEST_ENDPOINT_ID, + LOCK_CTRL_TEST_USER_INDEX)) + assert err == 0 + assert res.userIndex == LOCK_CTRL_TEST_USER_INDEX + assert res.userName == LOCK_CTRL_TEST_USER_NAME + assert res.userUniqueId == LOCK_CTRL_TEST_USER_INDEX + assert res.userStatus == DoorLock.Enums.DlUserStatus.kOccupiedEnabled + assert res.userType == DoorLock.Enums.DlUserType.kUnrestrictedUser + assert res.credentialRule == DoorLock.Enums.DlCredentialRule.kSingle + + err, res = send_zcl_command( + devCtrl, "DoorLock SetCredential {} {} operationType={} " + "credential=struct:DlCredential(credentialType={},credentialIndex={}) credentialData=str:{} " + "userIndex={} userStatus={} userType={}".format(nodeId, LOCK_CTRL_TEST_ENDPOINT_ID, + DoorLock.Enums.DlDataOperationType.kAdd, + DoorLock.Enums.DlCredentialType.kPin, + LOCK_CTRL_TEST_CREDENTIAL_INDEX, + LOCK_CTRL_TEST_PIN_CODE, + LOCK_CTRL_TEST_USER_INDEX, + DoorLock.Enums.DlUserStatus.kOccupiedEnabled, + DoorLock.Enums.DlUserType.kUnrestrictedUser), requestTimeoutMs=1000) + assert err == 0 + assert res.status == DoorLock.Enums.DlStatus.kSuccess + + ret = device.wait_for_output("Successfully set the credential [mEndpointId={},index={}," + "credentialType={},creator=1,modifier=1]".format(LOCK_CTRL_TEST_ENDPOINT_ID, + LOCK_CTRL_TEST_USER_INDEX, DoorLock.Enums.DlCredentialType.kPin)) + assert ret != None and len(ret) > 0 + + err, res = send_zcl_command( + devCtrl, "DoorLock GetCredentialStatus {} {} credential=struct:DlCredential(credentialType={}," + "credentialIndex={})".format(nodeId, LOCK_CTRL_TEST_ENDPOINT_ID, + DoorLock.Enums.DlCredentialType.kPin, + LOCK_CTRL_TEST_CREDENTIAL_INDEX), requestTimeoutMs=1000) + assert err == 0 + assert res.credentialExists + assert res.userIndex == LOCK_CTRL_TEST_USER_INDEX + + err, res = send_zcl_command( + devCtrl, "DoorLock LockDoor {} {} pinCode=str:{}".format(nodeId, LOCK_CTRL_TEST_ENDPOINT_ID, + LOCK_CTRL_TEST_PIN_CODE), requestTimeoutMs=1000) + assert err == 0 + + ret = device.wait_for_output("Setting door lock state to \"Locked\" [endpointId={}]".format(LOCK_CTRL_TEST_ENDPOINT_ID)) + assert ret != None and len(ret) > 0 + + err, res = read_zcl_attribute( + devCtrl, "DoorLock LockState {} {}".format(nodeId, LOCK_CTRL_TEST_ENDPOINT_ID)) + assert err == 0 + assert res.value == DoorLock.Enums.DlLockState.kLocked + + err, res = send_zcl_command( + devCtrl, "DoorLock UnlockDoor {} {} pinCode=str:{}".format(nodeId, LOCK_CTRL_TEST_ENDPOINT_ID, + LOCK_CTRL_TEST_PIN_CODE), requestTimeoutMs=1000) + assert err == 0 + + ret = device.wait_for_output("Setting door lock state to \"Unlocked\" [endpointId={}]".format(LOCK_CTRL_TEST_ENDPOINT_ID)) + assert ret != None and len(ret) > 0 + + err, res = read_zcl_attribute( + devCtrl, "DoorLock LockState {} {}".format(nodeId, LOCK_CTRL_TEST_ENDPOINT_ID)) + assert err == 0 + assert res.value == DoorLock.Enums.DlLockState.kUnlocked + + assert disconnect_device(devCtrl, nodeId) diff --git a/src/test_driver/openiotsdk/integration-tests/pytest.ini b/src/test_driver/openiotsdk/integration-tests/pytest.ini new file mode 100644 index 00000000000000..78c8ab6382ffa5 --- /dev/null +++ b/src/test_driver/openiotsdk/integration-tests/pytest.ini @@ -0,0 +1,9 @@ +[pytest] +log_cli = true +log_level = INFO +log_format = %(asctime)s.%(msecs)03d %(levelname)s %(message)s +log_cli_format = %(asctime)s.%(msecs)03d %(levelname)s %(message)s +markers = + smoketest: A simple test to verify that the application is properly launched + commissioningtest: Test to validate the commissionign process + ctrltest: Test checking the operation of the application through integration with it diff --git a/src/test_driver/openiotsdk/integration-tests/shell/__init__.py b/src/test_driver/openiotsdk/integration-tests/shell/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/src/test_driver/openiotsdk/integration-tests/shell/test_app.py b/src/test_driver/openiotsdk/integration-tests/shell/test_app.py new file mode 100644 index 00000000000000..72cd8d0166c50f --- /dev/null +++ b/src/test_driver/openiotsdk/integration-tests/shell/test_app.py @@ -0,0 +1,191 @@ +# Copyright (c) 2009-2021 Arm Limited +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest +import re +from packaging import version +from time import sleep + +from chip.setup_payload import SetupPayload +from chip import exceptions +import chip.native + +from common.utils import * + +import logging +log = logging.getLogger(__name__) + + +@pytest.fixture(scope="session") +def binaryPath(request, rootDir): + if request.config.getoption('binaryPath'): + return request.config.getoption('binaryPath') + else: + return os.path.join(rootDir, 'examples/shell/openiotsdk/build/chip-openiotsdk-shell-example.elf') + + +SHELL_COMMAND_NAME = ["base64", "exit", "help", "version", + "config", "device", "onboardingcodes", "dns", + "echo", "log", "rand"] + + +def get_shell_command(response): + return [line.split()[0].strip() for line in response] + + +def parse_config_response(response): + config = {} + for param in response: + param_name = param.split(":")[0].lower() + if "discriminator" in param_name: + value = int(param.split(":")[1].strip(), 16) + elif "pincode" in param_name: + value = int(param.split(":")[1].strip()) + else: + value = int(param.split(":")[1].split()[0].strip()) + + if "hardwareversion" in param_name: + param_name = "hardwarever" + + config[param_name] = value + return config + + +def parse_boarding_codes_response(response): + codes = {} + for param in response: + codes[param.split(":")[0].lower()] = param.split()[1].strip() + return codes + + +@pytest.mark.smoketest +def test_smoke_test(device): + ret = device.wait_for_output("Open IoT SDK shell example application start") + assert ret != None and len(ret) > 0 + ret = device.wait_for_output("Open IoT SDK shell example application run") + assert ret != None and len(ret) > 0 + + +@pytest.mark.ctrltest +def test_command_check(device): + try: + chip.native.Init() + except exceptions.ChipStackException as ex: + log.error("CHIP initialization failed {}".format(ex)) + assert False + except: + log.error("CHIP initialization failed") + assert False + + ret = device.wait_for_output("Open IoT SDK shell example application start") + assert ret != None and len(ret) > 0 + ret = device.wait_for_output("Open IoT SDK shell example application run") + assert ret != None and len(ret) > 0 + + # Help + ret = device.send(command="help", expected_output="Done") + assert ret != None and len(ret) > 1 + shell_commands = get_shell_command(ret[1:-1]) + assert set(SHELL_COMMAND_NAME) == set(shell_commands) + + # Echo + ret = device.send(command="echo Hello", expected_output="Done") + assert ret != None and len(ret) > 1 + assert "Hello" in ret[-2] + + # Log + ret = device.send(command="log Hello", expected_output="Done") + assert ret != None and len(ret) > 1 + assert "[INF] [TOO] Hello" in ret[-2] + + # Rand + ret = device.send(command="rand", expected_output="Done") + assert ret != None and len(ret) > 1 + assert ret[-2].rstrip().isdigit() + + # Base64 + hex_string = "1234" + ret = device.send(command="base64 encode {}".format( + hex_string), expected_output="Done") + assert ret != None and len(ret) > 1 + base64code = ret[-2] + ret = device.send(command="base64 decode {}".format( + base64code), expected_output="Done") + assert ret != None and len(ret) > 1 + assert ret[-2].rstrip() == hex_string + + # Version + ret = device.send(command="version", expected_output="Done") + assert ret != None and len(ret) > 1 + assert "CHIP" in ret[-2].split()[0] + app_version = ret[-2].split()[1] + assert isinstance(version.parse(app_version), version.Version) + + # Config + ret = device.send(command="config", expected_output="Done") + assert ret != None and len(ret) > 2 + + config = parse_config_response(ret[1:-1]) + + for param_name, value in config.items(): + ret = device.send(command="config {}".format( + param_name), expected_output="Done") + assert ret != None and len(ret) > 1 + if "discriminator" in param_name: + assert int(ret[-2].split()[0], 16) == value + else: + assert int(ret[-2].split()[0]) == value + + new_value = int(config['discriminator']) + 1 + ret = device.send(command="config discriminator {}".format( + new_value), expected_output="Done") + assert ret != None and len(ret) > 1 + assert "Setup discriminator set to: {}".format(new_value) in ret[-2] + + ret = device.send(command="config discriminator", expected_output="Done") + assert ret != None and len(ret) > 1 + assert int(ret[-2].split()[0], 16) == new_value + + # Onboardingcodes + ret = device.send(command="onboardingcodes none", expected_output="Done") + assert ret != None and len(ret) > 2 + + boarding_codes = parse_boarding_codes_response(ret[1:-1]) + + for param, value in boarding_codes.items(): + ret = device.send(command="onboardingcodes none {}".format( + param), expected_output="Done") + assert ret != None and len(ret) > 1 + assert value == ret[-2].strip() + + try: + device_details = dict(SetupPayload().ParseQrCode( + "VP:vendorpayload%{}".format(boarding_codes['qrcode'])).attributes) + except exceptions.ChipStackError as ex: + log.error(ex.msg) + assert False + assert device_details != None and len(device_details) != 0 + + try: + device_details = dict(SetupPayload().ParseManualPairingCode( + boarding_codes['manualpairingcode']).attributes) + except exceptions.ChipStackError as ex: + log.error(ex.msg) + assert False + assert device_details != None and len(device_details) != 0 + + # Exit - should be the last check + ret = device.send(command="exit", expected_output="Goodbye") + assert ret != None and len(ret) > 0