From 6144736cdc09a9691a594a5f43926e6458541bed Mon Sep 17 00:00:00 2001 From: feasel <120589145+feasel0@users.noreply.github.com> Date: Wed, 4 Sep 2024 09:59:27 -0400 Subject: [PATCH] Generating separate binaries for each unit test for EFR32 (#35028) * silabs full changes, flashable just if * silabs full changes, flashable just if (corrected) * silabs changes, flashable changes * yes executables.gni, cts.gni, args.gni, test_driver build.gn * yes executables.gni, args.gni, test_driver build.gn * Modified chip_test_suite to handle logic for both efr32 test_driver and chip_link_test * Doc update * Added final newline * Comment updates * Remove deprecated `tests` variable for per-test custom mains. * switched to shutil.copy instead of subprocess copy * Added chip_link_tests to test_driver/efr32/args.gni and removed special logic from chip_test_suite * Restyled by gn * Restyled by autopep8 * Punctuation change * Added special exception for darwin to always include test_sources in common lib. * Added comment re darwin exception * Restyled by gn * Revisions to builder scripts - removing map() usage and propagating OSError to make_wrapper. * Restyled by autopep8 --------- Co-authored-by: Restyled.io --- .github/.wordlist.txt | 2 - build/chip/chip_test.gni | 71 --------- build/chip/chip_test_suite.gni | 101 ++++++------- build/toolchain/flashable_executable.gni | 18 ++- scripts/build/builders/efr32.py | 55 ++++++- scripts/build/builders/host.py | 2 +- scripts/flashing/firmware_utils.py | 11 +- scripts/flashing/silabs_firmware_utils.py | 38 +++++ src/test_driver/efr32/BUILD.gn | 26 +--- src/test_driver/efr32/README.md | 16 +- src/test_driver/efr32/args.gni | 19 ++- src/test_driver/efr32/py/BUILD.gn | 26 ---- .../efr32/py/nl_test_runner/nl_test_runner.py | 141 ------------------ .../__init__.py | 0 .../efr32/py/pw_test_runner/pw_test_runner.py | 42 ++++-- src/test_driver/efr32/py/setup.cfg | 2 +- third_party/silabs/silabs_executable.gni | 73 ++++++--- 17 files changed, 265 insertions(+), 378 deletions(-) delete mode 100644 build/chip/chip_test.gni delete mode 100644 src/test_driver/efr32/py/nl_test_runner/nl_test_runner.py rename src/test_driver/efr32/py/{nl_test_runner => pw_test_runner}/__init__.py (100%) diff --git a/.github/.wordlist.txt b/.github/.wordlist.txt index 4444d8299d26cb..5637540f7f9b25 100644 --- a/.github/.wordlist.txt +++ b/.github/.wordlist.txt @@ -950,8 +950,6 @@ NitricOxideConcentrationMeasurement NitrogenDioxideConcentrationMeasurement nl nltest -NLUnitTest -NLUnitTests nmcli nmtui noc diff --git a/build/chip/chip_test.gni b/build/chip/chip_test.gni deleted file mode 100644 index b5b32f24d0b0b7..00000000000000 --- a/build/chip/chip_test.gni +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright (c) 2020 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("//build_overrides/build.gni") -import("//build_overrides/chip.gni") -import("//build_overrides/pigweed.gni") - -import("$dir_pw_build/python_action.gni") - -import("${chip_root}/build/chip/tests.gni") -import("${chip_root}/src/platform/device.gni") -import("${dir_pw_unit_test}/test.gni") - -assert(chip_build_tests) - -if (chip_link_tests) { - template("chip_test") { - _test_name = target_name - - _test_output_dir = "${root_out_dir}/tests" - if (defined(invoker.output_dir)) { - _test_output_dir = invoker.output_dir - } - - executable(_test_name) { - forward_variables_from(invoker, "*", [ "output_dir" ]) - output_dir = _test_output_dir - } - - group(_test_name + ".lib") { - } - - if (chip_pw_run_tests) { - pw_python_action(_test_name + ".run") { - deps = [ ":${_test_name}" ] - inputs = [ pw_unit_test_AUTOMATIC_RUNNER ] - module = "pw_unit_test.test_runner" - python_deps = [ - "$dir_pw_cli/py", - "$dir_pw_unit_test/py", - ] - args = [ - "--runner", - rebase_path(pw_unit_test_AUTOMATIC_RUNNER, root_build_dir), - "--test", - rebase_path("$_test_output_dir/$_test_name", root_build_dir), - ] - stamp = true - } - } - } -} else { - template("chip_test") { - group(target_name) { - } - group(target_name + ".lib") { - } - not_needed(invoker, "*") - } -} diff --git a/build/chip/chip_test_suite.gni b/build/chip/chip_test_suite.gni index 60f29346a48fa6..de6b7c16848eb5 100644 --- a/build/chip/chip_test_suite.gni +++ b/build/chip/chip_test_suite.gni @@ -14,13 +14,20 @@ import("//build_overrides/build.gni") import("//build_overrides/chip.gni") +import("//build_overrides/pigweed.gni") -import("${chip_root}/build/chip/chip_test.gni") import("${chip_root}/build/chip/tests.gni") import("${dir_pw_unit_test}/test.gni") assert(chip_build_tests) +declare_args() { + # These may be overridden in args.gni to build platform-specific test binaries. + test_executable_output_name = "" + test_executable_output_name_suffix = "" + test_executable_ldflags = [] +} + # Define CHIP unit tests # # Simple usage @@ -41,50 +48,34 @@ assert(chip_build_tests) # "${chip_root}/src/lib/foo", # add dependencies here # ] # } -# -# -# Deprecated usage (writing own driver files): -# -# chip_test_suite("tests") { -# output_name = "libFooTests" -# -# sources = [ -# "TestDeclarations.h", -# "TestFoo.cpp", -# "TestBar.cpp", -# ] -# -# public_deps = [ -# "${chip_root}/src/lib/foo", # add dependencies here -# ] -# -# tests = [ -# "TestFoo", # Assumes TestFooDriver.cpp exists -# "TestBar", # Assumes TestBarDriver.cpp exists -# ] -# } # template("chip_test_suite") { _suite_name = target_name - # Ensures that the common library has sources containing both common - # and individual unit tests. - if (!defined(invoker.sources)) { - invoker.sources = [] - } - - if (defined(invoker.test_sources)) { - invoker.sources += invoker.test_sources + exclude_variables = [ "tests" ] + if (chip_link_tests && chip_device_platform != "darwin") { + # Common library shouldn't have all the individual unit tests, only the common sources. + exclude_variables += [ "test_sources" ] + # NOTE: For `Build on Darwin (clang, python_lib, simulated)` the test_sources must be in common lib. + } else { + # Common library should have all the individual unit tests, in addition to the common sources. + if (!defined(invoker.sources)) { + invoker.sources = [] + } + if (defined(invoker.test_sources)) { + invoker.sources += invoker.test_sources + } } + # Target for the common library. Contains all the common sources, and sometimes all the individual test sources. if (chip_build_test_static_libraries) { _target_type = "static_library" } else { _target_type = "source_set" } target(_target_type, "${_suite_name}.lib") { - forward_variables_from(invoker, "*", [ "tests" ]) + forward_variables_from(invoker, "*", exclude_variables) output_dir = "${root_out_dir}/lib" @@ -102,6 +93,8 @@ template("chip_test_suite") { public_deps += [ "${chip_root}/src/platform/logging:default" ] } } + + # Build a source_set or a flashable executable for each individual unit test source, which also includes the common files. if (chip_link_tests) { tests = [] @@ -115,6 +108,7 @@ template("chip_test_suite") { } pw_test(_test_name) { + # Forward certain variables from the invoker. forward_variables_from(invoker, [ "deps", @@ -122,43 +116,30 @@ template("chip_test_suite") { "cflags", "configs", ]) + + # Link to the common lib for this suite so we get its `sources`. public_deps += [ ":${_suite_name}.lib" ] - sources = [ _test ] - output_dir = _test_output_dir - } - tests += [ _test_name ] - } - } - if (defined(invoker.tests)) { - foreach(_test, invoker.tests) { - _test_output_dir = "${root_out_dir}/tests" - if (defined(invoker.output_dir)) { - _test_output_dir = invoker.output_dir - } + # Set variables that the platform executable may need. + if (test_executable_output_name != "") { + output_name = test_executable_output_name + _test_name + + test_executable_output_name_suffix + } + ldflags = test_executable_ldflags + + # Add the individual test source file (e.g. "TestSomething.cpp"). + sources = [ _test ] - pw_test(_test) { - forward_variables_from(invoker, - [ - "deps", - "public_deps", - "cflags", - "configs", - ]) - public_deps += [ ":${_suite_name}.lib" ] - test_main = "" - sources = [ - "${_test}.cpp", - "${_test}Driver.cpp", - ] output_dir = _test_output_dir } - tests += [ _test ] + tests += [ _test_name ] } } group(_suite_name) { deps = [] + + # Add each individual unit test. foreach(_test, tests) { deps += [ ":${_test}" ] } @@ -167,6 +148,8 @@ template("chip_test_suite") { if (chip_pw_run_tests) { group("${_suite_name}_run") { deps = [] + + # Add the .run targets created by pw_test. foreach(_test, tests) { deps += [ ":${_test}.run" ] } diff --git a/build/toolchain/flashable_executable.gni b/build/toolchain/flashable_executable.gni index 6233d58382b43d..b7f96b95f46f08 100644 --- a/build/toolchain/flashable_executable.gni +++ b/build/toolchain/flashable_executable.gni @@ -86,6 +86,10 @@ template("gen_flashing_script") { template("flashable_executable") { executable_target = "$target_name.executable" + if (!defined(invoker.output_dir)) { + invoker.output_dir = root_out_dir + } + if (defined(invoker.flashing_script_name)) { # Generating the flashing script is the final target. final_target = "$target_name.flashing" @@ -110,7 +114,10 @@ template("flashable_executable") { data_deps += invoker.data_deps } - write_runtime_deps = "${root_out_dir}/${flashbundle_name}" + # Invoker can stop this template from creating the flashbundle.txt by setting flashbundle_name to empty string. + if (flashbundle_name != "") { + write_runtime_deps = "${invoker.output_dir}/${flashbundle_name}" + } } if (defined(invoker.objcopy_image_name)) { @@ -124,8 +131,8 @@ template("flashable_executable") { objcopy = invoker.objcopy objcopy_convert(image_target) { - conversion_input = "${root_out_dir}/${invoker.output_name}" - conversion_output = "${root_out_dir}/${image_name}" + conversion_input = "${invoker.output_dir}/${invoker.output_name}" + conversion_output = "${invoker.output_dir}/${image_name}" conversion_target_format = image_format deps = [ ":$executable_target" ] } @@ -141,7 +148,8 @@ template("flashable_executable") { gen_flashing_script("$target_name.flashing") { flashing_script_generator = invoker.flashing_script_generator flashing_script_inputs = invoker.flashing_script_inputs - flashing_script_name = "$root_out_dir/${invoker.flashing_script_name}" + flashing_script_name = + "${invoker.output_dir}/${invoker.flashing_script_name}" if (defined(invoker.flashing_options)) { flashing_options = invoker.flashing_options } else { @@ -155,7 +163,7 @@ template("flashable_executable") { flashing_options += [ "--application", - rebase_path(image_name, root_out_dir, root_out_dir), + rebase_path(image_name, invoker.output_dir, invoker.output_dir), ] data_deps = [ ":$image_target" ] } diff --git a/scripts/build/builders/efr32.py b/scripts/build/builders/efr32.py index 3972dc7bb48eff..6545f75b55da6b 100644 --- a/scripts/build/builders/efr32.py +++ b/scripts/build/builders/efr32.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import glob +import logging import os import shlex import subprocess @@ -78,7 +80,7 @@ def FlashBundleName(self): elif self == Efr32App.PUMP: return 'pump_app.flashbundle.txt' elif self == Efr32App.UNIT_TEST: - return 'efr32_device_tests.flashbundle.txt' + return os.path.join('tests', 'efr32_device_tests.flashbundle.txt') else: raise Exception('Unknown app type: %r' % self) @@ -259,27 +261,64 @@ def __init__(self, def GnBuildArgs(self): return self.extra_gn_options + def _bundle(self): + # Only unit-test needs to generate the flashbundle here. All other examples will generate a flashbundle via the silabs_executable template. + if self.app == Efr32App.UNIT_TEST: + flash_bundle_path = os.path.join(self.output_dir, self.app.FlashBundleName()) + logging.info(f'Generating flashbundle {flash_bundle_path}') + + patterns = [ + os.path.join(self.output_dir, "tests", "*.flash.py"), + os.path.join(self.output_dir, "tests", "*.s37"), + os.path.join(self.output_dir, "tests", "silabs_firmware_utils.py"), + os.path.join(self.output_dir, "tests", "firmware_utils.py"), + ] + + # Generate the list of files by globbing each pattern. + files = [] + for pattern in patterns: + files.extend([os.path.basename(x) for x in glob.glob(pattern)]) + + # Create the bundle file. + with open(flash_bundle_path, 'w') as bundle_file: + bundle_file.write("\n".join(files)) + def build_outputs(self): extensions = ["out", "hex"] if self.options.enable_link_map_file: extensions.append("out.map") - for ext in extensions: - name = f"{self.app.AppNamePrefix()}.{ext}" - yield BuilderOutput(os.path.join(self.output_dir, name), name) + + if self.app == Efr32App.UNIT_TEST: + # Efr32 unit-test generates the "tests" subdir with a set of files for each individual unit test source. + for ext in extensions: + pattern = os.path.join(self.output_dir, "tests", f"*.{ext}") + for name in [os.path.basename(x) for x in glob.glob(pattern)]: + yield BuilderOutput(os.path.join(self.output_dir, "tests", name), name) + else: + # All other examples have just one set of files. + for ext in extensions: + name = f"{self.app.AppNamePrefix()}.{ext}" + yield BuilderOutput(os.path.join(self.output_dir, name), name) if self.app == Efr32App.UNIT_TEST: # Include test runner python wheels - for root, dirs, files in os.walk(os.path.join(self.output_dir, 'chip_nl_test_runner_wheels')): + for root, dirs, files in os.walk(os.path.join(self.output_dir, 'chip_pw_test_runner_wheels')): for file in files: yield BuilderOutput( os.path.join(root, file), - os.path.join("chip_nl_test_runner_wheels", file)) + os.path.join("chip_pw_test_runner_wheels", file)) - # Figure out flash bundle files and build accordingly + def bundle_outputs(self): + # If flashbundle creation is enabled, the outputs will include the s37 and flash.py files, plus the two firmware utils scripts that support flash.py. + # For the unit-test example, there will be a s37 and flash.py file for each unit test source. with open(os.path.join(self.output_dir, self.app.FlashBundleName())) as f: for name in filter(None, [x.strip() for x in f.readlines()]): + if self.app == Efr32App.UNIT_TEST: + sourcepath = os.path.join(self.output_dir, "tests", name) # Unit tests are in the "tests" subdir. + else: + sourcepath = os.path.join(self.output_dir, name) yield BuilderOutput( - os.path.join(self.output_dir, name), + sourcepath, os.path.join("flashbundle", name)) def generate(self): diff --git a/scripts/build/builders/host.py b/scripts/build/builders/host.py index b69421a782e948..01b35c7a01185f 100644 --- a/scripts/build/builders/host.py +++ b/scripts/build/builders/host.py @@ -220,7 +220,7 @@ def OutputNames(self): elif self == HostApp.PYTHON_BINDINGS: yield 'controller/python' # Directory containing WHL files elif self == HostApp.EFR32_TEST_RUNNER: - yield 'chip_nl_test_runner_wheels' + yield 'chip_pw_test_runner_wheels' elif self == HostApp.TV_CASTING: yield 'chip-tv-casting-app' yield 'chip-tv-casting-app.map' diff --git a/scripts/flashing/firmware_utils.py b/scripts/flashing/firmware_utils.py index 6087360c0614ac..2b844e8e3eb550 100644 --- a/scripts/flashing/firmware_utils.py +++ b/scripts/flashing/firmware_utils.py @@ -23,6 +23,7 @@ import subprocess import sys import textwrap +import traceback # Here are the options that can be use to configure a `Flasher` # object (as dictionary keys) and/or passed as command line options. @@ -409,7 +410,11 @@ def make_wrapper(self, argv): # Give platform-specific code a chance to manipulate the arguments # for the wrapper script. - self._platform_wrapper_args(args) + try: + self._platform_wrapper_args(args) + except OSError: + traceback.print_last() + return 1 # Find any option values that differ from the class defaults. # These will be inserted into the wrapper script. @@ -445,7 +450,7 @@ def make_wrapper(self, argv): os.chmod(args.output, (stat.S_IXUSR | stat.S_IRUSR | stat.S_IWUSR | stat.S_IXGRP | stat.S_IRGRP | stat.S_IXOTH | stat.S_IROTH)) - except OSError as exception: - print(exception, sys.stderr) + except OSError: + traceback.print_last() return 1 return 0 diff --git a/scripts/flashing/silabs_firmware_utils.py b/scripts/flashing/silabs_firmware_utils.py index 5e0a689f5a62f4..bd7b64cf021bcd 100755 --- a/scripts/flashing/silabs_firmware_utils.py +++ b/scripts/flashing/silabs_firmware_utils.py @@ -49,6 +49,8 @@ Do not reset device after flashing """ +import os +import shutil import sys import firmware_utils @@ -169,6 +171,42 @@ def actions(self): return self + def _platform_wrapper_args(self, args): + """Called from make_wrapper() to optionally manipulate arguments.""" + # Generate the flashbundle.txt file and copy the firmware utils. + if args.flashbundle_file is not None: + # Generate the flashbundle contents. + # Copy the platform-specific and general firmware utils to the same directory as the wrapper. + flashbundle_contents = os.path.basename(args.output) + if args.application is not None: + flashbundle_contents += "\n" + os.path.basename(args.application) + output_dir = os.path.dirname(args.output) or "." + if args.platform_firmware_utils is not None: + flashbundle_contents += "\n" + os.path.basename(args.platform_firmware_utils) + shutil.copy(args.platform_firmware_utils, output_dir) + if args.firmware_utils is not None: + flashbundle_contents += "\n" + os.path.basename(args.firmware_utils) + shutil.copy(args.firmware_utils, output_dir) + + # Create the flashbundle file. + with open(args.flashbundle_file, 'w') as flashbundle_file: + flashbundle_file.write(flashbundle_contents.strip()) + + def make_wrapper(self, argv): + self.parser.add_argument( + '--flashbundle-file', + metavar='FILENAME', + help='path and name of the flashbundle text file to create') + self.parser.add_argument( + '--platform-firmware-utils', + metavar='FILENAME', + help='path and file of the platform-specific firmware utils script') + self.parser.add_argument( + '--firmware-utils', + metavar='FILENAME', + help='path and file of the general firmware utils script') + super().make_wrapper(argv) + if __name__ == '__main__': sys.exit(Flasher().flash_command(sys.argv)) diff --git a/src/test_driver/efr32/BUILD.gn b/src/test_driver/efr32/BUILD.gn index b15b16864548c9..0d10cb8d76a475 100644 --- a/src/test_driver/efr32/BUILD.gn +++ b/src/test_driver/efr32/BUILD.gn @@ -19,7 +19,6 @@ import("//build_overrides/pigweed.gni") import("${build_root}/config/defaults.gni") import("${efr32_sdk_build_root}/efr32_sdk.gni") -import("${efr32_sdk_build_root}/silabs_executable.gni") import("${chip_root}/examples/common/pigweed/pigweed_rpcs.gni") import("${chip_root}/src/platform/device.gni") @@ -62,9 +61,8 @@ efr32_sdk("sdk") { ] } -silabs_executable("efr32_device_tests") { - output_name = "matter-silabs-device_tests.out" - +# This is the test runner. `pw_test` will dep this for each `silabs_executable` target. +source_set("efr32_test_main") { defines = [ "PW_RPC_ENABLED" ] sources = [ "${chip_root}/examples/common/pigweed/RpcService.cpp", @@ -83,7 +81,6 @@ silabs_executable("efr32_device_tests") { "$dir_pw_unit_test:rpc_service", "${chip_root}/config/efr32/lib/pw_rpc:pw_rpc", "${chip_root}/examples/common/pigweed:system_rpc_server", - "${chip_root}/src:tests", "${chip_root}/src/lib", "${chip_root}/src/lib/support:pw_tests_wrapper", "${chip_root}/src/platform/silabs/provision:provision-headers", @@ -106,27 +103,18 @@ silabs_executable("efr32_device_tests") { ] include_dirs = [ "${chip_root}/examples/common/pigweed/efr32" ] - - ldscript = "${examples_common_plat_dir}/ldscripts/${silabs_family}.ld" - - inputs = [ ldscript ] - - ldflags = [ - "-T" + rebase_path(ldscript, root_build_dir), - "-Wl,--no-warn-rwx-segment", - ] - - output_dir = root_out_dir } +# This target is referred to by BuildRoot in scripts/build/builders/efr32.py, as well as the example in README.md. +# It builds the root target "src:tests", which builds the chip_test_suite target in each test directory, which builds a pw_test target for each test source file, which builds a silabs_executable, which includes the "efr32_test_main" target defined above. group("efr32") { - deps = [ ":efr32_device_tests" ] + deps = [ "${chip_root}/src:tests" ] } group("runner") { deps = [ - "${efr32_project_dir}/py:nl_test_runner.install", - "${efr32_project_dir}/py:nl_test_runner_wheel", + "${efr32_project_dir}/py:pw_test_runner.install", + "${efr32_project_dir}/py:pw_test_runner_wheel", ] } diff --git a/src/test_driver/efr32/README.md b/src/test_driver/efr32/README.md index c36812e484fdee..c846426890abaa 100644 --- a/src/test_driver/efr32/README.md +++ b/src/test_driver/efr32/README.md @@ -1,6 +1,6 @@ #CHIP EFR32 Test Driver -This builds and runs the NLUnitTest on the efr32 device +This builds and runs the unit tests on the efr32 device.
@@ -14,9 +14,9 @@ This builds and runs the NLUnitTest on the efr32 device ## Introduction -This builds a test binary which contains the NLUnitTests and can be flashed onto -a device. The device is controlled using the included RPCs, through the python -test runner. +This builds a set of test binaries which contain the unit tests and can be +flashed onto a device. The device is controlled using the included RPCs, through +the python test runner. @@ -83,7 +83,7 @@ Or build using build script from the root ``` cd - ./scripts/build/build_examples.py --target linux-x64-nl-test-runner build + ./scripts/build/build_examples.py --target linux-x64-pw-test-runner build ``` The runner will be installed into the venv and python wheels will be packaged in @@ -92,7 +92,7 @@ the output folder for deploying. Then the python wheels need to installed using pip3. ``` - pip3 install out/debug/chip_nl_test_runner_wheels/*.whl + pip3 install out/debug/chip_pw_test_runner_wheels/*.whl ``` Other python libraries may need to be installed such as @@ -101,8 +101,8 @@ Other python libraries may need to be installed such as pip3 install pyserial ``` -- To run the tests: +- To run all tests: ``` - python -m nl_test_runner.nl_test_runner -d /dev/ttyACM1 -f out/debug/matter-silabs-device_tests.s37 -o out.log + python -m pw_test_runner.pw_test_runner -d /dev/ttyACM1 -f out/debug/matter-silabs-device_tests.s37 -o out.log ``` diff --git a/src/test_driver/efr32/args.gni b/src/test_driver/efr32/args.gni index 71bf093f3e3a16..f7f52398a5889e 100644 --- a/src/test_driver/efr32/args.gni +++ b/src/test_driver/efr32/args.gni @@ -17,14 +17,15 @@ import("//build_overrides/pigweed.gni") import("${chip_root}/config/efr32/lib/pw_rpc/pw_rpc.gni") import("${chip_root}/examples/platform/silabs/args.gni") import("${chip_root}/src/platform/silabs/efr32/args.gni") +import("${chip_root}/third_party/silabs/silabs_board.gni") # silabs_family silabs_sdk_target = get_label_info(":sdk", "label_no_toolchain") chip_enable_pw_rpc = true chip_build_tests = true +chip_link_tests = true chip_enable_openthread = true chip_openthread_ftd = false # use mtd as it is smaller. -chip_monolithic_tests = true openthread_external_platform = "${chip_root}/third_party/openthread/platforms/efr32:libopenthread-efr32" @@ -35,3 +36,19 @@ pw_assert_BACKEND = "$dir_pw_assert_log" pw_log_BACKEND = "$dir_pw_log_basic" pw_unit_test_BACKEND = "$dir_pw_unit_test:light" + +# Override the executable type and the test main's target. +pw_unit_test_EXECUTABLE_TARGET_TYPE = "silabs_executable" +pw_unit_test_EXECUTABLE_TARGET_TYPE_FILE = + "${efr32_sdk_build_root}/silabs_executable.gni" +pw_unit_test_MAIN = "//:efr32_test_main" + +# Additional variables needed by silabs_executable that must be passed in to pw_test. +test_executable_output_name = "matter-silabs-device_tests-" +test_executable_output_name_suffix = ".out" +_ldscript = + "${chip_root}/examples/platform/silabs/ldscripts/${silabs_family}.ld" +test_executable_ldflags = [ + "-T" + rebase_path(_ldscript, root_build_dir), + "-Wl,--no-warn-rwx-segment", +] diff --git a/src/test_driver/efr32/py/BUILD.gn b/src/test_driver/efr32/py/BUILD.gn index 615fe5603d00c9..6f36e8f3e4a839 100644 --- a/src/test_driver/efr32/py/BUILD.gn +++ b/src/test_driver/efr32/py/BUILD.gn @@ -19,32 +19,6 @@ import("$dir_pw_build/python.gni") import("$dir_pw_build/python_dist.gni") import("${chip_root}/examples/common/pigweed/pigweed_rpcs.gni") -# TODO [PW_MIGRATION]: remove nl test runner script once transition away from nlunit-test is completed -pw_python_package("nl_test_runner") { - setup = [ - "pyproject.toml", - "setup.cfg", - "setup.py", - ] - - sources = [ - "nl_test_runner/__init__.py", - "nl_test_runner/nl_test_runner.py", - ] - - python_deps = [ - "$dir_pw_hdlc/py", - "$dir_pw_protobuf_compiler/py", - "$dir_pw_rpc/py", - "${chip_root}/src/test_driver/efr32:nl_test_service.python", - ] -} - -pw_python_wheels("nl_test_runner_wheel") { - packages = [ ":nl_test_runner" ] - directory = "$root_out_dir/chip_nl_test_runner_wheels" -} - pw_python_package("pw_test_runner") { setup = [ "pw_test_runner/pyproject.toml", diff --git a/src/test_driver/efr32/py/nl_test_runner/nl_test_runner.py b/src/test_driver/efr32/py/nl_test_runner/nl_test_runner.py deleted file mode 100644 index 0fdb2e7aea7e3d..00000000000000 --- a/src/test_driver/efr32/py/nl_test_runner/nl_test_runner.py +++ /dev/null @@ -1,141 +0,0 @@ -# -# Copyright (c) 2021 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 argparse -import logging -import subprocess -import sys -import time -from typing import Any - -import serial # type: ignore -from pw_hdlc import rpc - -# RPC Protos -from nl_test_service import nl_test_pb2 # isort:skip - -PW_LOG = logging.getLogger(__name__) - -PROTOS = [nl_test_pb2] - - -class colors: - HEADER = '\033[95m' - OKBLUE = '\033[94m' - OKCYAN = '\033[96m' - OKGREEN = '\033[92m' - WARNING = '\033[93m' - FAIL = '\033[91m' - ENDC = '\033[0m' - BOLD = '\033[1m' - - -PASS_STRING = colors.OKGREEN + u'\N{check mark}' + colors.ENDC -FAIL_STRING = colors.FAIL + 'FAILED' + colors.ENDC - - -def _parse_args(): - """Parses and returns the command line arguments.""" - parser = argparse.ArgumentParser( - description="CHIP on device unit test runner.") - parser.add_argument('-d', '--device', help='the serial port to use') - parser.add_argument('-b', - '--baudrate', - type=int, - default=115200, - help='the baud rate to use') - parser.add_argument('-f', '--flash_image', - help='a firmware image which will be flashed berfore runnning the test') - parser.add_argument( - '-o', - '--output', - type=argparse.FileType('wb'), - default=sys.stdout.buffer, - help=('The file to which to write device output (HDLC channel 1); ' - 'provide - or omit for stdout.')) - return parser.parse_args() - - -def flash_device(device: str, flash_image: str, **kwargs): - """flashes the EFR32 device using commander""" - err = subprocess.call( - ['commander', 'flash', '--device', 'EFR32', flash_image]) - if err: - raise Exception("flash failed") - - -def get_hdlc_rpc_client(device: str, baudrate: int, output: Any, **kwargs): - """Get the HdlcRpcClient based on arguments.""" - serial_device = serial.Serial(device, baudrate, timeout=1) - reader = rpc.SerialReader(serial_device, 8192) - write = serial_device.write - return rpc.HdlcRpcClient(reader, PROTOS, rpc.default_channels(write), - lambda data: rpc.write_to_file(data, output)) - - -def runner(client) -> int: - """ Run the tests""" - def on_error_callback(call_object, error): - raise Exception("Error running test RPC: {}".format(error)) - - rpc = client.client.channel(1).rpcs.chip.rpc.NlTest.Run - invoke = rpc.invoke(rpc.request(), on_error=on_error_callback) - - total_failed = 0 - total_run = 0 - for streamed_data in invoke.get_responses(): - if streamed_data.HasField("test_suite_start"): - print("\n{}".format( - colors.HEADER + streamed_data.test_suite_start.suite_name) + colors.ENDC) - if streamed_data.HasField("test_case_run"): - print("\t{}: {}".format(streamed_data.test_case_run.test_case_name, - FAIL_STRING if streamed_data.test_case_run.failed else PASS_STRING)) - if streamed_data.HasField("test_suite_tests_run_summary"): - total_run += streamed_data.test_suite_tests_run_summary.total_count - total_failed += streamed_data.test_suite_tests_run_summary.failed_count - print("{}Total tests failed: {} of {}".format( - colors.OKGREEN if streamed_data.test_suite_tests_run_summary.failed_count == 0 else colors.FAIL, - streamed_data.test_suite_tests_run_summary.failed_count, - streamed_data.test_suite_tests_run_summary.total_count) + colors.ENDC) - if streamed_data.HasField("test_suite_asserts_summary"): - print("{}Total asserts failed: {} of {}".format( - colors.OKGREEN if streamed_data.test_suite_asserts_summary.failed_count == 0 else colors.FAIL, - streamed_data.test_suite_asserts_summary.failed_count, - streamed_data.test_suite_asserts_summary.total_count) + colors.ENDC) - for step in ["test_suite_setup", "test_suite_teardown", "test_case_initialize", "test_case_terminate"]: - if streamed_data.HasField(step): - print(colors.OKCYAN + "\t{}: {}".format(step, - FAIL_STRING if getattr(streamed_data, step).failed else PASS_STRING)) - print(colors.OKBLUE + colors.BOLD + - "\n\nAll tests completed" + colors.ENDC) - print("{}Total of all tests failed: {} of {}".format( - colors.OKGREEN if total_failed == 0 else colors.FAIL, - total_failed, total_run) + colors.ENDC) - return total_failed - - -def main() -> int: - args = _parse_args() - if args.flash_image: - flash_device(**vars(args)) - time.sleep(1) # Give time for device to boot - with get_hdlc_rpc_client(**vars(args)) as client: - return runner(client) - - -if __name__ == '__main__': - sys.exit(main()) diff --git a/src/test_driver/efr32/py/nl_test_runner/__init__.py b/src/test_driver/efr32/py/pw_test_runner/__init__.py similarity index 100% rename from src/test_driver/efr32/py/nl_test_runner/__init__.py rename to src/test_driver/efr32/py/pw_test_runner/__init__.py diff --git a/src/test_driver/efr32/py/pw_test_runner/pw_test_runner.py b/src/test_driver/efr32/py/pw_test_runner/pw_test_runner.py index c8fca307f015ff..8e10903920cc77 100644 --- a/src/test_driver/efr32/py/pw_test_runner/pw_test_runner.py +++ b/src/test_driver/efr32/py/pw_test_runner/pw_test_runner.py @@ -16,6 +16,7 @@ # import argparse +import glob import logging import os import subprocess @@ -60,7 +61,7 @@ def _parse_args(): parser.add_argument( "-f", "--flash_image", - help="a firmware image which will be flashed berfore runnning the test", + help="A firmware image which will be flashed berfore runnning the test. Or a directory containing firmware images, each of which will be flashed and then run.", ) parser.add_argument( "-o", @@ -75,7 +76,7 @@ def _parse_args(): return parser.parse_args() -def flash_device(device: str, flash_image: str, **kwargs): +def flash_device(device: str, flash_image: str): """flashes the EFR32 device using commander""" err = subprocess.call( ["commander", "flash", "--device", "EFR32", flash_image]) @@ -96,22 +97,41 @@ def get_hdlc_rpc_client(device: str, baudrate: int, output: Any, **kwargs): ) -def runner(client: rpc.HdlcRpcClient) -> int: - """Run the tests""" +def run(args) -> int: + """Run the tests. Return the number of failed tests.""" + with get_hdlc_rpc_client(**vars(args)) as client: + test_records = run_tests(client.rpcs()) + return len(test_records.failing_tests) - test_records = run_tests(client.rpcs()) - return len(test_records.failing_tests) +def list_images(flash_directory: str) -> list[str]: + filenames: list[str] = glob.glob(os.path.join(flash_directory, "*.s37")) + return list(map(lambda x: os.path.join(flash_directory, x), filenames)) def main() -> int: args = _parse_args() - if args.flash_image: - flash_device(**vars(args)) - time.sleep(1) # Give time for device to boot - with get_hdlc_rpc_client(**vars(args)) as client: - return runner(client) + failures = 0 + if args.flash_image: + if os.path.isdir(args.flash_image): + images = list_images(args.flash_image) + if not images: + raise Exception(f"No images found in `{args.flash_image}`") + elif os.path.isfile(args.flash_image): + images = [args.flash_image] + else: + raise Exception(f"File or directory not found `{args.flash_image}`") + + for image in images: + flash_device(args.device, image) + time.sleep(1) # Give time for device to boot + + failures += run(args) + else: # No image provided. Just run what's on the device. + failures += run(args) + + return failures if __name__ == "__main__": diff --git a/src/test_driver/efr32/py/setup.cfg b/src/test_driver/efr32/py/setup.cfg index 77108ab8288747..cb949a38092b07 100644 --- a/src/test_driver/efr32/py/setup.cfg +++ b/src/test_driver/efr32/py/setup.cfg @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. [metadata] -name = nl_test_runner +name = pw_test_runner version = 0.0.1 [options] diff --git a/third_party/silabs/silabs_executable.gni b/third_party/silabs/silabs_executable.gni index a639a4d19ae62d..37b853c5df84b8 100644 --- a/third_party/silabs/silabs_executable.gni +++ b/third_party/silabs/silabs_executable.gni @@ -47,64 +47,93 @@ template("generate_rps_file") { } template("silabs_executable") { + # output_dir is optional and will default to root_out_dir + if (!defined(invoker.output_dir)) { + invoker.output_dir = root_out_dir + } + + # output_name is optional and will default to "$target_name.bin" + if (!defined(invoker.output_name)) { + invoker.output_name = target_name + ".bin" + } + output_base_name = get_path_info(invoker.output_name, "name") objcopy_image_name = output_base_name + ".s37" objcopy_image_format = "srec" objcopy = "arm-none-eabi-objcopy" - # Copy flashing dependencies to the output directory so that the output - # is collectively self-contained; this allows flashing to work reliably - # even if the build and flashing steps take place on different machines - # or in different containers. - if (use_rps_extension) { flashing_image_name = output_base_name + ".rps" } - flashing_runtime_target = target_name + ".flashing_runtime" - flashing_script_inputs = [ - "${chip_root}/scripts/flashing/silabs_firmware_utils.py", - "${chip_root}/scripts/flashing/firmware_utils.py", - ] - copy(flashing_runtime_target) { - sources = flashing_script_inputs - outputs = [ "${root_out_dir}/{{source_file_part}}" ] - } + # flashable_executable calls a generator script to do the following: + # Create a flash.py script with the name of the binary hardcoded in it. + # Copy flashing dependencies to the output directory so that the output + # is collectively self-contained; this allows flashing to work reliably + # even if the build and flashing steps take place on different machines + # or in different containers. + # Create *.flashbundle.txt with a list of all files needed for flashing flashing_script_generator = "${chip_root}/scripts/flashing/gen_flashing_script.py" flashing_script_name = output_base_name + ".flash.py" - flashing_options = [ "silabs" ] + _flashbundle_file = "${invoker.output_dir}/${target_name}.flashbundle.txt" + _platform_firmware_utils = + "${chip_root}/scripts/flashing/silabs_firmware_utils.py" + _firmware_utils = "${chip_root}/scripts/flashing/firmware_utils.py" + flashing_options = [ + # Use module "{platform}_firmware_utils.py" + "silabs", + + # flashbundle.txt file to create. + "--flashbundle-file", + rebase_path(_flashbundle_file, root_build_dir), + + # Platform-specific firmware module to copy. + "--platform-firmware-utils", + rebase_path(_platform_firmware_utils, root_build_dir), + # General firmware module to copy. + "--firmware-utils", + rebase_path(_firmware_utils, root_build_dir), + ] + flashing_script_inputs = [ + _platform_firmware_utils, + _firmware_utils, + ] + flashbundle_name = "" # Stop flashable_executable from making flashbundle. + + # Target to generate the s37 file, flashing script, and flashbundle. flash_target_name = target_name + ".flash_executable" - flashbundle_name = "${target_name}.flashbundle.txt" flashable_executable(flash_target_name) { forward_variables_from(invoker, "*") - data_deps = [ ":${flashing_runtime_target}" ] } - # Add a target which generates the hex file in addition to s37. + # Target to generate the hex file. executable_target = "$flash_target_name.executable" hex_image_name = output_base_name + ".hex" hex_target_name = target_name + ".hex" objcopy_convert(hex_target_name) { - conversion_input = "${root_out_dir}/${invoker.output_name}" - conversion_output = "${root_out_dir}/${hex_image_name}" + conversion_input = "${invoker.output_dir}/${invoker.output_name}" + conversion_output = "${invoker.output_dir}/${hex_image_name}" conversion_target_format = "ihex" deps = [ ":$executable_target" ] } + # Target to generate the rps file. if (use_rps_extension) { rps_target_name = target_name + ".rps" generate_rps_file(rps_target_name) { - conversion_input = "${root_out_dir}/${objcopy_image_name}" - conversion_output = "${root_out_dir}/${flashing_image_name}" + conversion_input = "${invoker.output_dir}/${objcopy_image_name}" + conversion_output = "${invoker.output_dir}/${flashing_image_name}" deps = [ ":$executable_target", ":$flash_target_name.image", ] } } + + # Main target that deps the targets defined above. group(target_name) { deps = [ ":$flash_target_name",