Build and run unit tests through GN
This change adds a GN template for defining unit test executables.
The template, called pw_test, defines the executable and outputs a JSON
metadata file for the test.
A new build argument is added. This argument determines whether unit
test run targets are supported by the current build target. If this is
set, the pw_test template additionally creates a run target for its test
executable which invokes the executable through a script.
A basic test runner script is added to the pw_unit_test module. This
script currently only runs a single test executable directly.
The unit tests in the pw_preprocessor module are updated to use the
pw_test template.
Change-Id: I3cbde9c19440276dbab80dd2bab5fec87abe6d7e
diff --git a/BUILD.gn b/BUILD.gn
index 8d69e9b..d696ae4 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -20,3 +20,10 @@
"$dir_pw_unit_test",
]
}
+
+# Targets for all module unit test groups.
+group("pw_module_tests") {
+ deps = [
+ "$dir_pw_preprocessor:unit_tests",
+ ]
+}
diff --git a/pw_preprocessor/BUILD.gn b/pw_preprocessor/BUILD.gn
index ccc89ad..230d65b 100644
--- a/pw_preprocessor/BUILD.gn
+++ b/pw_preprocessor/BUILD.gn
@@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations under
# the License.
+import("$dir_pw_unit_test/test.gni")
+
config("default_config") {
include_dirs = [ "public" ]
}
@@ -31,7 +33,8 @@
sources = public
}
-group("pw_preprocessor_tests") {
+# All pw_preprocessor test binaries.
+group("unit_tests") {
deps = [
":boolean_test",
":concat_test",
@@ -40,49 +43,24 @@
]
}
-group("pw_preprocessor_tests_linux") {
- deps = [
- ":pw_preprocessor_tests($dir_pw_toolchain:x86_linux_o2)",
- ]
+template("preprocessor_test") {
+ not_needed([ "invoker" ])
+ pw_test(target_name) {
+ deps = [
+ ":pw_preprocessor",
+ "$dir_pw_unit_test:main",
+ ]
+ sources = [
+ "$target_name.cc",
+ ]
+ }
}
-# TODO(frolv): Change these to special unit test executables.
-executable("boolean_test") {
- deps = [
- ":pw_preprocessor",
- "$dir_pw_unit_test:main",
- ]
- sources = [
- "boolean_test.cc",
- ]
+preprocessor_test("boolean_test") {
}
-
-executable("concat_test") {
- deps = [
- ":pw_preprocessor",
- "$dir_pw_unit_test:main",
- ]
- sources = [
- "concat_test.cc",
- ]
+preprocessor_test("concat_test") {
}
-
-executable("macro_arg_count_test") {
- deps = [
- ":pw_preprocessor",
- "$dir_pw_unit_test:main",
- ]
- sources = [
- "macro_arg_count_test.cc",
- ]
+preprocessor_test("macro_arg_count_test") {
}
-
-executable("util_test") {
- deps = [
- ":pw_preprocessor",
- "$dir_pw_unit_test:main",
- ]
- sources = [
- "util_test.cc",
- ]
+preprocessor_test("util_test") {
}
diff --git a/pw_unit_test/py/test_runner.py b/pw_unit_test/py/test_runner.py
new file mode 100644
index 0000000..f7f4e46
--- /dev/null
+++ b/pw_unit_test/py/test_runner.py
@@ -0,0 +1,90 @@
+# Copyright 2019 The Pigweed 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
+#
+# https://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.
+
+"""Script which runs Pigweed unit tests built using GN.
+
+Currently, only a single test can be run at a time. The build path and GN target
+name of the test are given to the script.
+"""
+
+import argparse
+import pathlib
+import os
+import subprocess
+import sys
+
+
+def parse_args() -> argparse.Namespace:
+ """Parses command-line arguments."""
+
+ parser = argparse.ArgumentParser('Run Pigweed unit tests')
+ parser.add_argument('--touch', type=str,
+ help='File to touch after test run')
+ parser.add_argument('test', type=str, help='Path to unit test binary')
+ return parser.parse_args()
+
+
+# TODO(frolv): This should be extracted into a script-running script which
+# performs path resolution before calling another script.
+def find_binary(target: str) -> str:
+ """Tries to find a binary for a gn build target.
+
+ Args:
+ target: Relative path to the target's output directory and target name,
+ separated by a colon.
+
+ Returns:
+ Full path to the target's binary.
+
+ Raises:
+ RuntimeError: No binary found for target.
+ """
+
+ target_path, target_name = target.split(':')
+
+ extensions = ['', '.elf']
+ for extension in extensions:
+ potential_filename = f'{target_path}/{target_name}{extension}'
+ if os.path.isfile(potential_filename):
+ return potential_filename
+
+ raise FileNotFoundError(
+ f'could not find output binary for build target {target}')
+
+
+def main() -> int:
+ """Runs some unit tests."""
+
+ args = parse_args()
+
+ try:
+ test_binary = find_binary(args.test)
+ except FileNotFoundError as err:
+ print(f'{sys.argv[0]}: {err}', file=sys.stderr)
+ return 1
+
+ exit_status = subprocess.call([test_binary])
+
+ # GN expects "action" targets to output a file, and uses that to determine
+ # whether the target should be run again. Touching an empty file allows GN
+ # to only run unit tests which have been affected by code changes since the
+ # previous run, taking advantage of its dependency resolution.
+ if args.touch is not None:
+ pathlib.Path(args.touch).touch()
+
+ return exit_status
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/pw_unit_test/test.gni b/pw_unit_test/test.gni
new file mode 100644
index 0000000..bb1577e
--- /dev/null
+++ b/pw_unit_test/test.gni
@@ -0,0 +1,108 @@
+# Copyright 2019 The Pigweed 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
+#
+# https://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("$dir_pw_build/pw_executable.gni")
+
+declare_args() {
+ # Whether GN unit test runner targets should be created.
+ #
+ # If set to true, the pw_test_run() template will create an action that
+ # invokes the test runner script on a test executable.
+ # If false, the pw_test_run() template becomes a no-op.
+ #
+ # This should be enabled for targets which support parallelized running
+ # of unit tests, such as desktops with multiple cores.
+ pw_unit_test_create_run_targets = false
+}
+
+# Creates an executable target for a unit test.
+# Additionally, outputs a file containing unit test metadata in JSON format for
+# the test runner script.
+#
+# This template accepts all of the regular "executable" target args.
+template("pw_test") {
+ # This is required in order to reference the pw_test template's target name
+ # within the test_metadata of the metadata group below. The group() definition
+ # creates a new scope where the "target_name" variable is set to its target,
+ # shadowing the one in this scope.
+ _target_name = target_name
+
+ # Metadata for the test runner script. This is a dummy target which doesn't
+ # require or produce anything; it simply exists to define the unit test
+ # metadata.
+ _metadata_group_target = _target_name + "_pw_metadata_group"
+ group(_metadata_group_target) {
+ metadata = {
+ test_metadata = [
+ {
+ test_name = _target_name
+ },
+ ]
+ }
+ }
+
+ # Output file for the unit test metadata. Reads metadata from the dummy group
+ # target and outputs to a JSON file.
+ _metadata_file_target = _target_name + "_pw_test_metadata"
+ generated_file(_metadata_file_target) {
+ outputs = [
+ "$target_out_dir/$_target_name.meta.json",
+ ]
+ data_keys = [ "test_metadata" ]
+ output_conversion = "json"
+
+ deps = [
+ ":$_metadata_group_target",
+ ]
+ }
+
+ # Actual executable file for the unit test. Depends on the metadata output
+ # file in order to generate it as well.
+ pw_executable(_target_name) {
+ forward_variables_from(invoker, "*")
+
+ if (!defined(deps)) {
+ deps = []
+ }
+ deps += [ ":$_metadata_file_target" ]
+ }
+
+ if (pw_unit_test_create_run_targets) {
+ # When the run targets arg is set, create an action which runs the unit test
+ # executable using the test runner script.
+ _run_action_name = _target_name + "_run"
+
+ # Resolve the GN path of a dependency to a filesystem path relative to the
+ # build directory. Keep the target name.
+ _binary_path = rebase_path(target_out_dir, root_build_dir) + ":$_target_name"
+
+ # The runner script touches this file to indicate that the test has run.
+ _stamp_file = "$target_gen_dir/${_target_name}.test_completion.stamp"
+
+ action(_run_action_name) {
+ deps = [
+ ":$_target_name",
+ ]
+ script = "$dir_pw_unit_test/py/test_runner.py"
+ args = [
+ "--touch",
+ rebase_path(_stamp_file, root_build_dir),
+ _binary_path,
+ ]
+ outputs = [
+ _stamp_file,
+ ]
+ }
+ }
+}