1# Copyright (C) 2021 The Android Open Source Project 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15"""Rules used to run tests using Tradefed.""" 16 17load("//bazel/rules:platform_transitions.bzl", "host_transition", "device_transition") 18load("//bazel/rules:tradefed_test_aspects.bzl", "soong_prebuilt_tradefed_test_aspect") 19load("//bazel/rules:tradefed_test_info.bzl", "TradefedTestInfo") 20load("//bazel/rules:common_settings.bzl", "BuildSettingInfo") 21load("//:constants.bzl", 22 "aapt_label", 23 "adb_label", 24 "atest_script_help_sh_label", 25 "atest_tradefed_label", 26 "atest_tradefed_sh_label", 27 "bazel_result_reporter_label", 28 "tradefed_label", 29 "tradefed_test_framework_label" 30) 31 32_BAZEL_WORK_DIR = "${TEST_SRCDIR}/${TEST_WORKSPACE}/" 33_PY_TOOLCHAIN = "@bazel_tools//tools/python:toolchain_type" 34_TOOLCHAINS = [_PY_TOOLCHAIN] 35 36_TRADEFED_TEST_ATTRIBUTES = { 37 "_tradefed_test_template": attr.label( 38 default = "//bazel/rules:tradefed_test.sh.template", 39 allow_single_file = True, 40 ), 41 "_tradefed_classpath_jars": attr.label_list( 42 default = [ 43 atest_tradefed_label, 44 tradefed_label, 45 tradefed_test_framework_label, 46 bazel_result_reporter_label, 47 ], 48 cfg = host_transition 49 ), 50 "_atest_tradefed_launcher": attr.label( 51 default = atest_tradefed_sh_label, 52 allow_single_file = True, 53 cfg = host_transition, 54 ), 55 "_atest_helper": attr.label( 56 default = atest_script_help_sh_label, 57 allow_single_file = True, 58 cfg = host_transition, 59 ), 60 "_adb": attr.label( 61 default = adb_label, 62 allow_single_file = True, 63 cfg = host_transition, 64 ), 65 "_extra_tradefed_result_reporters": attr.label( 66 default = "//bazel/rules:extra_tradefed_result_reporters", 67 ), 68 # This attribute is required to use Starlark transitions. It allows 69 # allowlisting usage of this rule. For more information, see 70 # https://docs.bazel.build/versions/master/skylark/config.html#user-defined-transitions 71 "_allowlist_function_transition": attr.label( 72 default = "@bazel_tools//tools/allowlists/function_transition_allowlist", 73 ), 74} 75 76def _add_dicts(*dictionaries): 77 """Creates a new `dict` that has all the entries of the given dictionaries. 78 79 This function serves as a replacement for the `+` operator which does not 80 work with dictionaries. The implementation is inspired by Skylib's 81 `dict.add` and duplicated to avoid the dependency. See 82 https://github.com/bazelbuild/bazel/issues/6461 for more details. 83 84 Note, if the same key is present in more than one of the input dictionaries, 85 the last of them in the argument list overrides any earlier ones. 86 87 Args: 88 *dictionaries: Dictionaries to be added. 89 90 Returns: 91 A new `dict` that has all the entries of the given dictionaries. 92 """ 93 result = {} 94 for d in dictionaries: 95 result.update(d) 96 return result 97 98def _tradefed_deviceless_test_impl(ctx): 99 return _tradefed_test_impl( 100 ctx, 101 tradefed_options = [ 102 "-n", 103 "--prioritize-host-config", 104 "--skip-host-arch-check", 105 ], 106 host_deps = ctx.attr.test 107 ) 108 109tradefed_deviceless_test = rule( 110 attrs = _add_dicts( 111 _TRADEFED_TEST_ATTRIBUTES, 112 { 113 "test": attr.label( 114 mandatory = True, 115 cfg = host_transition, 116 aspects = [soong_prebuilt_tradefed_test_aspect], 117 ), 118 }, 119 ), 120 test = True, 121 implementation = _tradefed_deviceless_test_impl, 122 toolchains = _TOOLCHAINS, 123 doc = "A rule used to run host-side deviceless tests using Tradefed", 124) 125 126def _tradefed_device_test_impl(ctx): 127 return _tradefed_test_impl( 128 ctx, 129 host_deps = ctx.attr._aapt, 130 device_deps = ctx.attr.test, 131 path_additions = [ 132 _BAZEL_WORK_DIR + ctx.file._aapt.dirname, 133 ] 134 ) 135 136tradefed_device_test = rule( 137 attrs = _add_dicts( 138 _TRADEFED_TEST_ATTRIBUTES, 139 { 140 "test": attr.label( 141 mandatory = True, 142 cfg = device_transition, 143 aspects = [soong_prebuilt_tradefed_test_aspect], 144 ), 145 "_aapt": attr.label( 146 default = aapt_label, 147 allow_single_file = True, 148 cfg = host_transition, 149 ), 150 }, 151 ), 152 test = True, 153 implementation = _tradefed_device_test_impl, 154 toolchains = _TOOLCHAINS, 155 doc = "A rule used to run device tests using Tradefed", 156) 157 158def _tradefed_test_impl( 159 ctx, 160 tradefed_options=[], 161 host_deps=[], 162 device_deps=[], 163 path_additions=[], 164 ): 165 166 path_additions = path_additions + [_BAZEL_WORK_DIR + ctx.file._adb.dirname] 167 168 tradefed_classpath = [] 169 for tradefed_classpath_jar in ctx.attr._tradefed_classpath_jars: 170 for f in tradefed_classpath_jar.files.to_list(): 171 tradefed_classpath.append(_BAZEL_WORK_DIR + f.short_path) 172 tradefed_classpath = ":".join(tradefed_classpath) 173 174 tradefed_host_deps = [] 175 tradefed_host_deps.extend(ctx.attr._tradefed_classpath_jars) 176 tradefed_host_deps.extend(ctx.attr._atest_tradefed_launcher) 177 tradefed_host_deps.extend(ctx.attr._atest_helper) 178 tradefed_host_deps.extend(ctx.attr._adb) 179 host_runfiles = _get_runfiles_from_targets( 180 ctx, 181 tradefed_host_deps + host_deps, 182 ) 183 184 shared_lib_dirs = [] 185 for f in host_runfiles.files.to_list(): 186 if f.extension == "so": 187 shared_lib_dirs.append(_BAZEL_WORK_DIR + f.dirname) 188 shared_lib_dirs = ":".join(shared_lib_dirs) 189 190 # Configure the Python toolchain. 191 py_toolchain_info = ctx.toolchains[_PY_TOOLCHAIN] 192 py2_interpreter = py_toolchain_info.py2_runtime.interpreter 193 py3_interpreter = py_toolchain_info.py3_runtime.interpreter 194 195 # Create `python` and `python3` symlinks in the runfiles tree and add them to the executable 196 # path. This is required because scripts reference these commands in their shebang line. 197 host_runfiles = host_runfiles.merge(ctx.runfiles(symlinks = { 198 "/".join([py2_interpreter.dirname, "python"]): py2_interpreter, 199 "/".join([py3_interpreter.dirname, "python3"]): py3_interpreter, 200 })) 201 path_additions = path_additions + [ 202 _BAZEL_WORK_DIR + py2_interpreter.dirname, 203 _BAZEL_WORK_DIR + py3_interpreter.dirname, 204 ] 205 206 result_reporters = [ 207 "com.android.tradefed.result.BazelExitCodeResultReporter", 208 "com.android.tradefed.result.BazelXmlResultReporter", 209 ] 210 211 result_reporters.extend(ctx.attr._extra_tradefed_result_reporters[BuildSettingInfo].value) 212 213 result_reporters_config_file = ctx.actions.declare_file("result-reporters-%s.xml" % ctx.label.name) 214 _write_reporters_config_file( 215 ctx, result_reporters_config_file, result_reporters) 216 reporter_runfiles = ctx.runfiles(files = [result_reporters_config_file]) 217 218 script = ctx.actions.declare_file("tradefed_test_%s.sh" % ctx.label.name) 219 ctx.actions.expand_template( 220 template = ctx.file._tradefed_test_template, 221 output = script, 222 is_executable = True, 223 substitutions = { 224 "{module_name}": ctx.attr.test[0][TradefedTestInfo].module_name, 225 "{atest_tradefed_launcher}": _BAZEL_WORK_DIR + ctx.file._atest_tradefed_launcher.short_path, 226 "{atest_helper}": _BAZEL_WORK_DIR + ctx.file._atest_helper.short_path, 227 "{tradefed_tests_dir}": _BAZEL_WORK_DIR + ctx.attr.test[0].label.package, 228 "{tradefed_classpath}": tradefed_classpath, 229 "{shared_lib_dirs}": shared_lib_dirs, 230 "{path_additions}": ":".join(path_additions), 231 "{additional_tradefed_options}": " ".join(tradefed_options), 232 "{result_reporters_config_file}": _BAZEL_WORK_DIR + result_reporters_config_file.short_path, 233 }, 234 ) 235 236 device_runfiles = _get_runfiles_from_targets(ctx, device_deps) 237 return [DefaultInfo(executable = script, 238 runfiles = host_runfiles.merge_all([device_runfiles, reporter_runfiles]))] 239 240def _write_reporters_config_file(ctx, config_file, result_reporters): 241 config_lines = [ 242 "<?xml version=\"1.0\" encoding=\"utf-8\"?>", 243 "<configuration>" 244 ] 245 246 for result_reporter in result_reporters: 247 config_lines.append(" <result_reporter class=\"%s\" />" % result_reporter) 248 249 config_lines.append("</configuration>") 250 251 ctx.actions.write(config_file, "\n".join(config_lines)) 252 253def _get_runfiles_from_targets(ctx, targets): 254 return ctx.runfiles().merge_all([ 255 target[DefaultInfo].default_runfiles for target in targets 256 ]) 257