• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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