[CI] Add Java pairing OnNetworkLong test (#23816)
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index 23c0345..1154185 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -462,6 +462,18 @@
--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 8a7110a..fb6d9cc 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 @@
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 0000000..1417db0
--- /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 0000000..afd132c
--- /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="<timeout-second>",
+ )
+ optParser.add_option(
+ "-a",
+ "--address",
+ action="store",
+ dest="deviceAddress",
+ default='',
+ type='str',
+ help="Address of the device",
+ metavar="<device-addr>",
+ )
+ optParser.add_option(
+ "--setup-payload",
+ action="store",
+ dest="setupPayload",
+ default='',
+ type='str',
+ help="Setup Payload (manual pairing code or QR code content)",
+ metavar="<setup-payload>"
+ )
+ optParser.add_option(
+ "--nodeid",
+ action="store",
+ dest="nodeid",
+ default='1',
+ type='str',
+ help="The Node ID issued to the device",
+ metavar="<nodeid>"
+ )
+ optParser.add_option(
+ "--discriminator",
+ action="store",
+ dest="discriminator",
+ default='3840',
+ type='str',
+ help="Discriminator of the device",
+ metavar="<nodeid>"
+ )
+ 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="<paa-trust-store-path>"
+ )
+
+ (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 0000000..4ca163a
--- /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<kvs_path>[^ ]+)", 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()