diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 23c03459c369c5..11541859a24299 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -462,6 +462,18 @@ jobs: --target linux-x64-java-matter-controller \ build \ " + - name: Run Tests + timeout-minutes: 65 + run: | + scripts/run_in_build_env.sh \ + './scripts/tests/run_java_test.py \ + --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app \ + --app-args "--discriminator 3840 --interface-id -1" \ + --tool-path out/linux-x64-java-matter-controller \ + --tool-cluster "pairing" \ + --tool-args "--nodeid 1 --setup-payload 20202021 --discriminator 3840 -t 1000" \ + --factoryreset \ + ' - name: Uploading core files uses: actions/upload-artifact@v3 if: ${{ failure() && !env.ACT }} diff --git a/examples/java-matter-controller/java/src/com/matter/controller/commands/common/CommandManager.java b/examples/java-matter-controller/java/src/com/matter/controller/commands/common/CommandManager.java index 8a7110a664b372..fb6d9ccf3bf52f 100644 --- a/examples/java-matter-controller/java/src/com/matter/controller/commands/common/CommandManager.java +++ b/examples/java-matter-controller/java/src/com/matter/controller/commands/common/CommandManager.java @@ -97,6 +97,7 @@ public final void run(String[] args) { command.initArguments(temp.length, temp); command.run(); } catch (IllegalArgumentException e) { + System.out.println("Run command failed with exception: " + e.getMessage()); showCommand(args[0], command); } catch (Exception e) { System.out.println("Run command failed with exception: " + e.getMessage()); diff --git a/scripts/tests/java/base.py b/scripts/tests/java/base.py new file mode 100755 index 00000000000000..1417db0a7e981a --- /dev/null +++ b/scripts/tests/java/base.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 + +# +# 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. +# + +# Commissioning test. +import logging +import os +import sys +import queue +import datetime +import asyncio +import threading +import typing +import time +import subprocess +from colorama import Fore, Style + + +def EnqueueLogOutput(fp, tag, q): + for line in iter(fp.readline, b''): + timestamp = time.time() + if len(line) > len('[1646290606.901990]') and line[0:1] == b'[': + try: + timestamp = float(line[1:18].decode()) + line = line[19:] + except Exception as ex: + pass + sys.stdout.buffer.write( + (f"[{datetime.datetime.fromtimestamp(timestamp).isoformat(sep=' ')}]").encode() + tag + line) + sys.stdout.flush() + fp.close() + + +def RedirectQueueThread(fp, tag, queue) -> threading.Thread: + log_queue_thread = threading.Thread(target=EnqueueLogOutput, args=( + fp, tag, queue)) + log_queue_thread.start() + return log_queue_thread + + +def DumpProgramOutputToQueue(thread_list: typing.List[threading.Thread], tag: str, process: subprocess.Popen, queue: queue.Queue): + thread_list.append(RedirectQueueThread(process.stdout, + (f"[{tag}][{Fore.YELLOW}STDOUT{Style.RESET_ALL}]").encode(), queue)) + thread_list.append(RedirectQueueThread(process.stderr, + (f"[{tag}][{Fore.RED}STDERR{Style.RESET_ALL}]").encode(), queue)) diff --git a/scripts/tests/java/commissioning_test.py b/scripts/tests/java/commissioning_test.py new file mode 100755 index 00000000000000..afd132c575b4e7 --- /dev/null +++ b/scripts/tests/java/commissioning_test.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 + +# +# 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. +# + +# Commissioning test. +import logging +import os +import sys +import asyncio +import queue +import subprocess +import threading +import typing +from optparse import OptionParser +from colorama import Fore, Style +from java.base import DumpProgramOutputToQueue + + +class CommissioningTest: + def __init__(self, thread_list: typing.List[threading.Thread], queue: queue.Queue, cmd: [], args: str): + self.thread_list = thread_list + self.queue = queue + self.command = cmd + + optParser = OptionParser() + optParser.add_option( + "-t", + "--timeout", + action="store", + dest="testTimeout", + default='200', + type='str', + help="The program will return with timeout after specified seconds.", + metavar="", + ) + optParser.add_option( + "-a", + "--address", + action="store", + dest="deviceAddress", + default='', + type='str', + help="Address of the device", + metavar="", + ) + optParser.add_option( + "--setup-payload", + action="store", + dest="setupPayload", + default='', + type='str', + help="Setup Payload (manual pairing code or QR code content)", + metavar="" + ) + optParser.add_option( + "--nodeid", + action="store", + dest="nodeid", + default='1', + type='str', + help="The Node ID issued to the device", + metavar="" + ) + optParser.add_option( + "--discriminator", + action="store", + dest="discriminator", + default='3840', + type='str', + help="Discriminator of the device", + metavar="" + ) + optParser.add_option( + "-p", + "--paa-trust-store-path", + action="store", + dest="paaTrustStorePath", + default='', + type='str', + help="Path that contains valid and trusted PAA Root Certificates.", + metavar="" + ) + + (options, remainingArgs) = optParser.parse_args(args.split()) + + self.nodeid = options.nodeid + self.setupPayload = options.setupPayload + self.discriminator = options.discriminator + self.testTimeout = options.testTimeout + + logging.basicConfig(level=logging.INFO) + + def TestOnnetworkLong(self, nodeid, setuppin, discriminator, timeout): + java_command = self.command + ['pairing', 'onnetwork-long', nodeid, setuppin, discriminator, timeout] + print(java_command) + logging.info(f"Execute: {java_command}") + java_process = subprocess.Popen( + java_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + DumpProgramOutputToQueue(self.thread_list, Fore.GREEN + "JAVA " + Style.RESET_ALL, java_process, self.queue) + return java_process.wait() + + def RunTest(self): + logging.info("Testing onnetwork-long pairing") + java_exit_code = self.TestOnnetworkLong(self.nodeid, self.setupPayload, self.discriminator, self.testTimeout) + if java_exit_code != 0: + logging.error("Testing onnetwork-long pairing failed with error %r" % java_exit_code) + return java_exit_code + + # Testing complete without errors + return 0 diff --git a/scripts/tests/run_java_test.py b/scripts/tests/run_java_test.py new file mode 100755 index 00000000000000..4ca163a7eb4790 --- /dev/null +++ b/scripts/tests/run_java_test.py @@ -0,0 +1,114 @@ +#!/usr/bin/env -S python3 -B + +# 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. + +import click +import coloredlogs +import logging +import os +import pathlib +import pty +import queue +import re +import shlex +import signal +import subprocess +import sys +from java.base import DumpProgramOutputToQueue +from java.commissioning_test import CommissioningTest +from colorama import Fore, Style + + +@click.command() +@click.option("--app", type=click.Path(exists=True), default=None, help='Path to local application to use, omit to use external apps.') +@click.option("--app-args", type=str, default='', help='The extra arguments passed to the device.') +@click.option("--tool-path", type=click.Path(exists=True), default=None, help='Path to java-matter-controller.') +@click.option("--tool-cluster", type=str, default='pairing', help='The cluster name passed to the java-matter-controller.') +@click.option("--tool-args", type=str, default='', help='The arguments passed to the java-matter-controller.') +@click.option("--factoryreset", is_flag=True, help='Remove app configs (/tmp/chip*) before running the tests.') +def main(app: str, app_args: str, tool_path: str, tool_cluster: str, tool_args: str, factoryreset: bool): + logging.info("Execute: {script_command}") + + if factoryreset: + # Remove native app config + retcode = subprocess.call("rm -rf /tmp/chip*", shell=True) + if retcode != 0: + raise Exception("Failed to remove /tmp/chip* for factory reset.") + + print("Contents of test directory: %s" % os.getcwd()) + print(subprocess.check_output(["ls -l"], shell=True).decode('us-ascii')) + + # Remove native app KVS if that was used + kvs_match = re.search(r"--KVS (?P[^ ]+)", app_args) + if kvs_match: + kvs_path_to_remove = kvs_match.group("kvs_path") + retcode = subprocess.call("rm -f %s" % kvs_path_to_remove, shell=True) + print("Trying to remove KVS path %s" % kvs_path_to_remove) + if retcode != 0: + raise Exception("Failed to remove %s for factory reset." % kvs_path_to_remove) + + coloredlogs.install(level='INFO') + + log_queue = queue.Queue() + log_cooking_threads = [] + + if tool_path: + if not os.path.exists(tool_path): + if tool_path is None: + raise FileNotFoundError(f"{tool_path} not found") + + app_process = None + if app: + if not os.path.exists(app): + if app is None: + raise FileNotFoundError(f"{app} not found") + app_args = [app] + shlex.split(app_args) + logging.info(f"Execute: {app_args}") + app_process = subprocess.Popen( + app_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0) + DumpProgramOutputToQueue( + log_cooking_threads, Fore.GREEN + "APP " + Style.RESET_ALL, app_process, log_queue) + + command = ['java', '-Djava.library.path=' + tool_path + '/lib/jni', '-jar', tool_path + '/bin/java-matter-controller'] + + if tool_cluster == 'pairing': + logging.info("Testing pairing cluster") + + test = CommissioningTest(log_cooking_threads, log_queue, command, tool_args) + controller_exit_code = test.RunTest() + + if controller_exit_code != 0: + logging.error("Test script exited with error %r" % test_script_exit_code) + + app_exit_code = 0 + if app_process: + logging.warning("Stopping app with SIGINT") + app_process.send_signal(signal.SIGINT.value) + app_exit_code = app_process.wait() + + # There are some logs not cooked, so we wait until we have processed all logs. + # This procedure should be very fast since the related processes are finished. + for thread in log_cooking_threads: + thread.join() + + if controller_exit_code != 0: + sys.exit(controller_exit_code) + else: + # We expect both app and controller should exit with 0 + sys.exit(app_exit_code) + + +if __name__ == '__main__': + main()