# Copyright (C) 2021 The Android Open Source Project # # 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. """Rules used to run tests using Tradefed.""" load("//bazel/rules:platform_transitions.bzl", "device_transition", "host_transition") load("//bazel/rules:tradefed_test_aspects.bzl", "soong_prebuilt_tradefed_test_aspect") load("//bazel/rules:tradefed_test_dependency_info.bzl", "TradefedTestDependencyInfo") load("//bazel/rules:common_settings.bzl", "BuildSettingInfo") load( "//:constants.bzl", "aapt2_label", "aapt_label", "adb_label", "atest_script_help_sh_label", "atest_tradefed_label", "atest_tradefed_sh_label", "bazel_result_reporter_label", "compatibility_tradefed_label", "tradefed_label", "tradefed_test_framework_label", "vts_core_tradefed_harness_label", ) load("//bazel/rules:device_test.bzl", "device_test") TradefedTestInfo = provider( doc = "Info about a Tradefed test module", fields = { "module_name": "Name of the original Tradefed test module", }, ) _BAZEL_WORK_DIR = "${TEST_SRCDIR}/${TEST_WORKSPACE}/" _PY_TOOLCHAIN = "@bazel_tools//tools/python:toolchain_type" _JAVA_TOOLCHAIN = "@bazel_tools//tools/jdk:runtime_toolchain_type" _TOOLCHAINS = [_PY_TOOLCHAIN, _JAVA_TOOLCHAIN] _TRADEFED_TEST_ATTRIBUTES = { "module_name": attr.string(), "_tradefed_test_template": attr.label( default = "//bazel/rules:tradefed_test.sh.template", allow_single_file = True, ), "_tradefed_classpath_jars": attr.label_list( default = [ atest_tradefed_label, tradefed_label, tradefed_test_framework_label, bazel_result_reporter_label, ], cfg = host_transition, aspects = [soong_prebuilt_tradefed_test_aspect], ), "_atest_tradefed_launcher": attr.label( default = atest_tradefed_sh_label, allow_single_file = True, cfg = host_transition, aspects = [soong_prebuilt_tradefed_test_aspect], ), "_atest_helper": attr.label( default = atest_script_help_sh_label, allow_single_file = True, cfg = host_transition, aspects = [soong_prebuilt_tradefed_test_aspect], ), "_adb": attr.label( default = adb_label, allow_single_file = True, cfg = host_transition, aspects = [soong_prebuilt_tradefed_test_aspect], ), "_extra_tradefed_result_reporters": attr.label( default = "//bazel/rules:extra_tradefed_result_reporters", ), # This attribute is required to use Starlark transitions. It allows # allowlisting usage of this rule. For more information, see # https://docs.bazel.build/versions/master/skylark/config.html#user-defined-transitions "_allowlist_function_transition": attr.label( default = "@bazel_tools//tools/allowlists/function_transition_allowlist", ), } def _add_dicts(*dictionaries): """Creates a new `dict` that has all the entries of the given dictionaries. This function serves as a replacement for the `+` operator which does not work with dictionaries. The implementation is inspired by Skylib's `dict.add` and duplicated to avoid the dependency. See https://github.com/bazelbuild/bazel/issues/6461 for more details. Note, if the same key is present in more than one of the input dictionaries, the last of them in the argument list overrides any earlier ones. Args: *dictionaries: Dictionaries to be added. Returns: A new `dict` that has all the entries of the given dictionaries. """ result = {} for d in dictionaries: result.update(d) return result def _tradefed_deviceless_test_impl(ctx): return _tradefed_test_impl( ctx, tradefed_options = [ "-n", "--prioritize-host-config", "--skip-host-arch-check", ], test_host_deps = ctx.attr.test, ) tradefed_deviceless_test = rule( attrs = _add_dicts( _TRADEFED_TEST_ATTRIBUTES, { "test": attr.label( mandatory = True, cfg = host_transition, aspects = [soong_prebuilt_tradefed_test_aspect], ), }, ), test = True, implementation = _tradefed_deviceless_test_impl, toolchains = _TOOLCHAINS, doc = "A rule used to run host-side deviceless tests using Tradefed", ) def _tradefed_robolectric_test_impl(ctx): def add_android_all_files(ctx, tradefed_test_dir): android_all_files = [] for target in ctx.attr._android_all: for f in target.files.to_list(): # Tradefed expects a flat `android-all` directory structure for # Robolectric tests. symlink = _symlink(ctx, f, "%s/android-all/%s" % (tradefed_test_dir, f.basename)) android_all_files.append(symlink) return android_all_files return _tradefed_test_impl( ctx, data = [ctx.attr.jdk], tradefed_options = [ "-n", "--prioritize-host-config", "--skip-host-arch-check", "--test-arg", "com.android.tradefed.testtype.IsolatedHostTest:java-folder:%s" % ctx.attr.jdk.label.package, ], test_host_deps = ctx.attr.test, add_extra_tradefed_test_files = add_android_all_files, ) tradefed_robolectric_test = rule( attrs = _add_dicts( _TRADEFED_TEST_ATTRIBUTES, { "test": attr.label( mandatory = True, cfg = host_transition, aspects = [soong_prebuilt_tradefed_test_aspect], ), "jdk": attr.label( mandatory = True, ), "_android_all": attr.label_list( default = ["//android-all:android-all"], ), }, ), test = True, implementation = _tradefed_robolectric_test_impl, toolchains = _TOOLCHAINS, doc = "A rule used to run Robolectric tests using Tradefed", ) def _tradefed_device_test_impl(ctx): tradefed_deps = [] tradefed_deps.extend(ctx.attr._aapt) tradefed_deps.extend(ctx.attr._aapt2) tradefed_deps.extend(ctx.attr.tradefed_deps) test_device_deps = [] test_host_deps = [] if ctx.attr.host_test: test_host_deps.extend(ctx.attr.host_test) if ctx.attr.device_test: test_device_deps.extend(ctx.attr.device_test) return _tradefed_test_impl( ctx, tradefed_deps = tradefed_deps, test_device_deps = test_device_deps, test_host_deps = test_host_deps, path_additions = [ _BAZEL_WORK_DIR + ctx.file._aapt.dirname, _BAZEL_WORK_DIR + ctx.file._aapt2.dirname, ], ) _tradefed_device_test = rule( attrs = _add_dicts( _TRADEFED_TEST_ATTRIBUTES, { "device_test": attr.label( cfg = device_transition, aspects = [soong_prebuilt_tradefed_test_aspect], ), "host_test": attr.label( cfg = host_transition, aspects = [soong_prebuilt_tradefed_test_aspect], ), "tradefed_deps": attr.label_list( cfg = host_transition, aspects = [soong_prebuilt_tradefed_test_aspect], ), "_aapt": attr.label( default = aapt_label, allow_single_file = True, cfg = host_transition, aspects = [soong_prebuilt_tradefed_test_aspect], ), "_aapt2": attr.label( default = aapt2_label, allow_single_file = True, cfg = host_transition, aspects = [soong_prebuilt_tradefed_test_aspect], ), }, ), test = True, implementation = _tradefed_device_test_impl, toolchains = _TOOLCHAINS, doc = "A rule used to run device tests using Tradefed", ) def tradefed_device_driven_test( name, test, tradefed_deps = [], suites = [], **attrs): tradefed_test_name = "tradefed_test_%s" % name _tradefed_device_test( name = tradefed_test_name, device_test = test, tradefed_deps = _get_tradefed_deps(suites, tradefed_deps), **attrs ) device_test( name = name, test = tradefed_test_name, ) def tradefed_host_driven_device_test(test, tradefed_deps = [], suites = [], **attrs): _tradefed_device_test( host_test = test, tradefed_deps = _get_tradefed_deps(suites, tradefed_deps), **attrs ) def _tradefed_test_impl( ctx, tradefed_options = [], tradefed_deps = [], test_host_deps = [], test_device_deps = [], path_additions = [], add_extra_tradefed_test_files = lambda ctx, tradefed_test_dir: [], data = []): path_additions = path_additions + [_BAZEL_WORK_DIR + ctx.file._adb.dirname] # Files required to run the host-side test. test_host_runfiles = _collect_runfiles(ctx, test_host_deps) test_host_runtime_jars = _collect_runtime_jars(test_host_deps) test_host_runtime_shared_libs = _collect_runtime_shared_libs(test_host_deps) # Files required to run the device-side test. test_device_runfiles = _collect_runfiles(ctx, test_device_deps) # Files required to run Tradefed. all_tradefed_deps = [] all_tradefed_deps.extend(ctx.attr._tradefed_classpath_jars) all_tradefed_deps.extend(ctx.attr._atest_tradefed_launcher) all_tradefed_deps.extend(ctx.attr._atest_helper) all_tradefed_deps.extend(ctx.attr._adb) all_tradefed_deps.extend(tradefed_deps) tradefed_runfiles = _collect_runfiles(ctx, all_tradefed_deps) tradefed_runtime_jars = _collect_runtime_jars(all_tradefed_deps) tradefed_runtime_shared_libs = _collect_runtime_shared_libs(all_tradefed_deps) result_reporters_config_file = _generate_reporter_config(ctx) tradefed_runfiles = tradefed_runfiles.merge( ctx.runfiles(files = [result_reporters_config_file]), ) py_paths, py_runfiles = _configure_python_toolchain(ctx) java_paths, java_runfiles, java_home = _configure_java_toolchain(ctx) path_additions = path_additions + java_paths + py_paths tradefed_runfiles = tradefed_runfiles.merge_all([py_runfiles, java_runfiles]) tradefed_test_dir = "%s_tradefed_test_dir" % ctx.label.name tradefed_test_files = [] for dep in tradefed_deps + test_host_deps + test_device_deps: for f in dep[TradefedTestDependencyInfo].transitive_test_files.to_list(): symlink = _symlink(ctx, f, "%s/%s" % (tradefed_test_dir, f.short_path)) tradefed_test_files.append(symlink) tradefed_test_files.extend(add_extra_tradefed_test_files(ctx, tradefed_test_dir)) script = ctx.actions.declare_file("tradefed_test_%s.sh" % ctx.label.name) ctx.actions.expand_template( template = ctx.file._tradefed_test_template, output = script, is_executable = True, substitutions = { "{module_name}": ctx.attr.module_name, "{atest_tradefed_launcher}": _abspath(ctx.file._atest_tradefed_launcher), "{atest_helper}": _abspath(ctx.file._atest_helper), "{tradefed_test_dir}": _BAZEL_WORK_DIR + "%s/%s" % ( ctx.label.package, tradefed_test_dir, ), "{tradefed_classpath}": _classpath([tradefed_runtime_jars, test_host_runtime_jars]), "{shared_lib_dirs}": _ld_library_path([tradefed_runtime_shared_libs, test_host_runtime_shared_libs]), "{path_additions}": ":".join(path_additions), "{additional_tradefed_options}": " ".join(tradefed_options), "{result_reporters_config_file}": _abspath(result_reporters_config_file), "{java_home}": java_home, }, ) return [ DefaultInfo( executable = script, runfiles = tradefed_runfiles.merge_all([ test_host_runfiles, test_device_runfiles, ctx.runfiles(tradefed_test_files), ] + [ctx.runfiles(d.files.to_list()) for d in data]), ), TradefedTestInfo( module_name = ctx.attr.module_name, ), ] def _get_tradefed_deps(suites, tradefed_deps = []): suite_to_deps = { "host-unit-tests": [], "null-suite": [], "device-tests": [], "general-tests": [], "vts": [vts_core_tradefed_harness_label], } all_tradefed_deps = {d: None for d in tradefed_deps} for s in suites: all_tradefed_deps.update({ d: None for d in suite_to_deps.get(s, [compatibility_tradefed_label]) }) # Since `vts-core-tradefed-harness` includes `compatibility-tradefed`, we # will exclude `compatibility-tradefed` if `vts-core-tradefed-harness` exists. if vts_core_tradefed_harness_label in all_tradefed_deps: all_tradefed_deps.pop(compatibility_tradefed_label, default = None) return all_tradefed_deps.keys() def _generate_reporter_config(ctx): result_reporters = [ "com.android.tradefed.result.BazelExitCodeResultReporter", "com.android.tradefed.result.BazelXmlResultReporter", ] result_reporters.extend(ctx.attr._extra_tradefed_result_reporters[BuildSettingInfo].value) result_reporters_config_file = ctx.actions.declare_file("result-reporters-%s.xml" % ctx.label.name) _write_reporters_config_file( ctx, result_reporters_config_file, result_reporters, ) return result_reporters_config_file def _write_reporters_config_file(ctx, config_file, result_reporters): config_lines = [ "", "", ] for result_reporter in result_reporters: config_lines.append(" " % result_reporter) config_lines.append("") ctx.actions.write(config_file, "\n".join(config_lines)) def _configure_java_toolchain(ctx): java_runtime = ctx.toolchains[_JAVA_TOOLCHAIN].java_runtime java_home_path = _BAZEL_WORK_DIR + java_runtime.java_home java_runfiles = ctx.runfiles(transitive_files = java_runtime.files) return ([java_home_path + "/bin"], java_runfiles, java_home_path) def _configure_python_toolchain(ctx): py_toolchain_info = ctx.toolchains[_PY_TOOLCHAIN] py2_interpreter = py_toolchain_info.py2_runtime.interpreter py3_interpreter = py_toolchain_info.py3_runtime.interpreter # Create `python` and `python3` symlinks in the runfiles tree and add them # to the executable path. This is required because scripts reference these # commands in their shebang line. py_runfiles = ctx.runfiles(symlinks = { "/".join([py2_interpreter.dirname, "python"]): py2_interpreter, "/".join([py3_interpreter.dirname, "python3"]): py3_interpreter, }) py_paths = [ _BAZEL_WORK_DIR + py2_interpreter.dirname, _BAZEL_WORK_DIR + py3_interpreter.dirname, ] return (py_paths, py_runfiles) def _symlink(ctx, target_file, output_path): symlink = ctx.actions.declare_file(output_path) ctx.actions.symlink(output = symlink, target_file = target_file) return symlink def _collect_runfiles(ctx, targets): return ctx.runfiles().merge_all([ target[DefaultInfo].default_runfiles for target in targets ]) def _collect_runtime_jars(deps): return depset( transitive = [ d[TradefedTestDependencyInfo].runtime_jars for d in deps ], ) def _collect_runtime_shared_libs(deps): return depset( transitive = [ d[TradefedTestDependencyInfo].runtime_shared_libraries for d in deps ], ) def _classpath(deps): runtime_jars = depset(transitive = deps) return ":".join([_abspath(f) for f in runtime_jars.to_list()]) def _ld_library_path(deps): runtime_shared_libs = depset(transitive = deps) return ":".join( [_BAZEL_WORK_DIR + f.dirname for f in runtime_shared_libs.to_list()], ) def _abspath(file): return _BAZEL_WORK_DIR + file.short_path