blob: a18e9309f24453ea4a3884868ad4c17db67f877b [file] [log] [blame]
# Copyright (C) 2022 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.
"""The tradefest test ruleset.
This file contains the definition and implementation of:
- tradefed_test_suite, which expands to
- tradefed_deviceless_test
- tradefed_host_driven_device_test
- tradefed_device_driven_test
These rules provide Tradefed harness support around test executables and
runfiles. They are language independent, and thus work with cc_test, java_test,
and other test types.
The execution mode (host, device, deviceless) is automatically determined by the
target_compatible_with attribute of the test dependency. Whether a test runs is
handled by Bazel's incompatible target skipping, i.e. a test dep that's
compatible only with android would cause the tradefed_deviceless_test to be
SKIPPED automatically.
"""
load("@bazel_skylib//lib:dicts.bzl", "dicts")
load("@bazel_skylib//lib:paths.bzl", "paths")
# Apply this suffix to the name of the test dep target (e.g. the cc_test target)
TEST_DEP_SUFFIX = "__tf_internal"
LANGUAGE_CC = "cc"
LANGUAGE_JAVA = "java"
# A transition to force the target device platforms configuration. This is
# used in the tradefed -> cc_test edge (for example).
#
# TODO(b/290716628): Handle multilib. For example, cc_test sets `multilib:
# "both"` by default, so this may drop the secondary arch of the test, depending
# on the TARGET_PRODUCT.
def _tradefed_always_device_transition_impl(settings, _):
old_platform = str(settings["//command_line_option:platforms"][0])
# TODO(b/290716626): This is brittle handling for distinguishing between
# device / not-device of the current target platform. Could use better
# helpers.
new_platform = old_platform.removesuffix("_linux_x86_64")
return {
"//command_line_option:platforms": new_platform,
}
_tradefed_always_device_transition = transition(
implementation = _tradefed_always_device_transition_impl,
inputs = ["//command_line_option:platforms"],
outputs = ["//command_line_option:platforms"],
)
_TRADEFED_TEST_ATTRIBUTES = {
"_tradefed_test_sh_template": attr.label(
default = ":tradefed.sh.tpl",
allow_single_file = True,
doc = "Template script to launch tradefed.",
),
"_atest_tradefed_launcher": attr.label(
default = "//tools/asuite/atest:atest_tradefed.sh",
allow_single_file = True,
cfg = "exec",
),
"_atest_helper": attr.label(
default = "//tools/asuite/atest:atest_script_help.sh",
allow_single_file = True,
cfg = "exec",
),
# TODO(b/285949958): Use source-built adb for device tests.
"_adb": attr.label(
default = "//prebuilts/runtime:prebuilt-runtime-adb",
allow_single_file = True,
cfg = "exec",
),
"_tradefed_dependencies": attr.label_list(
default = [
"//tools/tradefederation/prebuilts/filegroups/tradefed:tradefed-prebuilt",
"//tools/tradefederation/prebuilts/filegroups/suite:compatibility-host-util-prebuilt",
"//tools/tradefederation/prebuilts/filegroups/suite:compatibility-tradefed-prebuilt",
"//tools/asuite/atest:atest-tradefed",
"//tools/asuite/atest/bazel/reporter:bazel-result-reporter",
],
doc = "Files needed on the classpath to run tradefed",
cfg = "exec",
),
"test_config": attr.label(
allow_single_file = True,
doc = "Test/Tradefed config.",
),
"template_test_config": attr.label(
allow_single_file = True,
doc = "Template to generate test config.",
),
"template_configs": attr.string_list(
doc = "Extra tradefed config options to extend into generated test config.",
),
"template_install_base": attr.string(
default = "/data/local/tmp",
doc = "Directory to install tests onto the device for generated config",
),
"test_language": attr.string(
default = "",
values = ["", LANGUAGE_CC, LANGUAGE_JAVA],
doc = "the programming language the test uses",
),
}
# The normalized name of test under tradefed harness. This is without any of the
#
# "__tf" suffixes, e.g. adbd_test or hello_world_test.
#
#The normalized module name is used as the stem for the test executable or
# config files, which are referenced in AndroidTest.xml, like in PushFilePreparer elements.
def _normalize_test_name(s):
return s.replace(TEST_DEP_SUFFIX, "")
# Get test config if specified or generate test config from template.
def _get_or_generate_test_config(ctx, tf_test_dir, test_executable, test_language):
# Validate input
total = 0
if ctx.file.test_config:
total += 1
if ctx.file.template_test_config:
total += 1
if total != 1:
fail("Exactly one of test_config or test_config_template should be provided, but got: " +
"%s %s" % (ctx.file.test_config, ctx.file.template_test_config))
basename = _normalize_test_name(test_executable.basename)
# If existing tradefed config is specified, symlink to it and return early.
#
# The config needs to be a sibling file to the test executable, and both
# files must be in tf_test_dir. Given that ctx.file.test_config could be
# from another package, like //build/make/core, this symlink handles that.
#
# $ tree bazel-bin/packages/modules/adb/adb_test__tf_deviceless_test/testcases/
# bazel-bin/packages/modules/adb/adb_test__tf_deviceless_test/testcases/
# ├── adb_test -> /out/bazel/output_user_root/7f5b7a0603f97e18eafe9111781920be/execroot/__main__/bazel-out/aosp_cf_x86_64_phone-userdebug_linux_x86_64-opt-ST-0f3d6a823fdb/bin/packages/modules/adb/bin/adb_test/adb_test
# └── adb_test.config
out = ctx.actions.declare_file(paths.join(tf_test_dir, basename + ".config"))
if ctx.file.test_config:
ctx.actions.symlink(
output = out,
target_file = ctx.file.test_config,
)
return out
# No test config specified, generate config from template. Join extra
# configs together and add xml spacing indent.
if test_language == LANGUAGE_JAVA:
# rm ".jar" extension since it's "{MODULE}.jar" in Java config template
basename = basename.removesuffix(".jar")
ctx.actions.expand_template(
template = ctx.file.template_test_config,
output = out,
substitutions = {
"{MODULE}": basename,
"{EXTRA_CONFIGS}": "\n ".join(ctx.attr.template_configs),
"{TEST_INSTALL_BASE}": ctx.attr.template_install_base,
},
)
return out
# Generate tradefed result reporter config.
def _create_result_reporter_config(ctx):
result_reporters_config_file = ctx.actions.declare_file("result-reporters.xml")
config_lines = [
"<?xml version=\"1.0\" encoding=\"utf-8\"?>",
"<configuration>",
]
result_reporters = [
"com.android.tradefed.result.BazelExitCodeResultReporter",
"com.android.tradefed.result.BazelXmlResultReporter",
"com.android.tradefed.result.proto.FileProtoResultReporter",
]
for result_reporter in result_reporters:
config_lines.append(" <result_reporter class=\"%s\" />" % result_reporter)
config_lines.append("</configuration>")
ctx.actions.write(result_reporters_config_file, "\n".join(config_lines))
return result_reporters_config_file
# Get the test Target object.
#
# ctx.attr.test could be a list, depending on the rule configuration. Host
# driven device test transitions ctx.attr.test to device config, which turns the
# test attr into a label list.
def _get_test_target(ctx):
if type(ctx.attr.test) == "list":
return ctx.attr.test[0]
return ctx.attr.test
# Generate and run tradefed bash script entry point and associated runfiles.
def _tradefed_test_impl(ctx, tradefed_options = []):
tf_test_dir = ctx.label.name + "/testcases"
test_target = _get_test_target(ctx)
test_language = ctx.attr.test_language
# For Java, a library may make more sense here than the executable. When
# expanding tradefed_test_impl to accept more rule types, this could be
# turned into a provider, whether set by the rule or an aspect visiting the
# rule.
test_executable = test_target.files_to_run.executable
test_basename = _normalize_test_name(test_executable.basename)
# Get or generate test config.
test_config = _get_or_generate_test_config(ctx, tf_test_dir, test_executable, test_language)
# Generate result reporter config file.
report_config = _create_result_reporter_config(ctx)
test_runfiles = []
out = ctx.actions.declare_file(test_basename, sibling = test_config)
# Symlink the test executable to the test cases directory
ctx.actions.symlink(output = out, target_file = test_executable)
root_relative_tests_dir = paths.dirname(out.short_path)
test_runfiles.append(out)
# Prepare test-provided runfiles
for f in test_target.files.to_list():
if f == test_executable:
continue
test_runfiles.append(f)
# Add harness dependencies into runfiles.
test_runfiles.extend(ctx.files._tradefed_dependencies)
test_runfiles.append(ctx.file._adb)
path_additions = [_abspath(ctx.file._adb.dirname)]
# Gather runfiles.
runfiles = ctx.runfiles(
files = test_runfiles + [
test_config,
report_config,
ctx.file._atest_tradefed_launcher,
ctx.file._atest_helper,
],
)
runfiles = runfiles.merge(test_target.default_runfiles)
# Generate script to run tradefed.
script = ctx.actions.declare_file("%s.sh" % ctx.label.name)
ctx.actions.expand_template(
template = ctx.file._tradefed_test_sh_template,
output = script,
is_executable = True,
substitutions = {
"{MODULE}": test_basename,
"{atest_tradefed_launcher}": _abspath(ctx.file._atest_tradefed_launcher.short_path),
"{atest_helper}": _abspath(ctx.file._atest_helper.short_path),
"{tradefed_classpath}": _classpath(ctx.files._tradefed_dependencies),
"{path_additions}": ":".join(path_additions),
"{root_relative_tests_dir}": root_relative_tests_dir,
"{additional_tradefed_options}": " ".join(tradefed_options),
},
)
return [DefaultInfo(
executable = script,
runfiles = runfiles,
)]
def _tradefed_deviceless_test_impl(ctx):
return _tradefed_test_impl(
ctx,
tradefed_options = [
"--null-device", # don't allocate a device
],
)
tradefed_deviceless_test = rule(
attrs = dicts.add(_TRADEFED_TEST_ATTRIBUTES, {
"test": attr.label(
# Deviceless test executables should always build for host.
doc = "Test target to run in tradefed.",
),
}),
test = True,
implementation = _tradefed_deviceless_test_impl,
doc = """A rule used to run host deviceless tests using Tradefed.
Generally tests that use one of the following runners (not exhaustive):
- com.android.compatibility.common.tradefed.testtype.JarHostTest (java_test_host).
- com.android.tradefed.testtype.HostTest
- com.android.tradefed.testtype.HostGTest (cc_test_host)
- com.android.tradefed.testtype.python.PythonBinaryHostTest
These are tests built and executed on the host, and do NOT need a
connected device to run.
""",
)
tradefed_device_driven_test = rule(
attrs = dicts.add(_TRADEFED_TEST_ATTRIBUTES, {
"test": attr.label(
# Device driven tests should always build for device.
cfg = _tradefed_always_device_transition,
doc = "Test target to run in tradefed.",
),
"_allowlist_function_transition": attr.label(
default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
),
}),
test = True,
implementation = _tradefed_test_impl,
doc = """A rule used to run device-driven tests using Tradefed
Generally tests that use one of the following runners (not exhaustive):
- com.android.tradefed.testtype.AndroidJUnitTest (android instrumentation test).
- com.android.tradefed.testtype.GTest (native device tests)
- com.android.compatibility.testtype.LibcoreTest
- com.android.compatibility.testtype.DalvikTest
- com.android.compatibility.tradefed.CtsRootTradefedTest
This results in a single tradefed invocation for a device driven test that uses
plugins like SuiteApkInstaller. Tradefed is still executed on the host,
but the test is driven entirely on the device.""",
)
tradefed_host_driven_device_test = rule(
attrs = dicts.add(_TRADEFED_TEST_ATTRIBUTES, {
"test": attr.label(
# Host driven device tests should always build for the host by
# default (since they're host driven!). There may be dependencies
# from the test to other runtime deps which the host executable will
# push onto the device during test runtime, but we'll let the test
# itself handle the configuration transition to those dependencies.
doc = "Test target to run in tradefed.",
),
}),
test = True,
implementation = _tradefed_test_impl,
doc = """A rule used to run host-driven device tests using Tradefed
Generally tests that use one of the following runners (not exhaustive):
- com.android.compatibility.common.tradefed.testtype.JarHostTest (java_test_host).
- com.android.tradefed.testtype.HostTest
- com.android.tradefed.testtype.HostGTest (cc_test_host)
- com.android.tradefed.testtype.mobly.MoblyBinaryHostTest
- com.android.tradefed.testtype.python.PythonBinaryHostTest
These are tests built and executed on the host, but may rely on Tradefed
plugins to install data onto the device during the test, like
PushFilePreparer or SuiteApkInstaller.
""",
)
def tradefed_test_suite(
name,
test_dep,
test_config,
template_configs,
template_install_base,
tags,
visibility,
test_language = "",
deviceless_test_config = None,
device_driven_test_config = None,
host_driven_device_test_config = None):
"""The tradefed_test_suite macro groups all three test types under a single test_suite.o
This enables users or tools to simply run 'b test //path/to:foo_test_suite' and bazel
can automatically determine which of the device or deviceless variants to run, using
target_compatible_with information from the test_dep target.
Args:
name: name of the test suite. This is the canonical name of the test, e.g. "hello_world_test".
test_dep: label of the language-specific test dependency.
test_config: label of a custom Tradefed XML config. if specified, skip auto generation with default configs.
template_configs: additional lines to be added to the test config.
template_install_base: the default install location on device for files.
tags: additional tags for the top level test_suite target. This can be used for filtering tests.
visibility: Bazel visibility declarations for this target.
test_language: language used for the test dependency. One of [LANGUAGE_CC, LANGUAGE_JAVA].
deviceless_test_config: default Tradefed test config for the deviceless execution mode.
device_driven_test_config: default Tradefed test config for the device driven execution mode.
host_driven_device_test_config: default Tradefed test config for the host driven execution mode.
"""
# Validate names.
if not test_dep.endswith(TEST_DEP_SUFFIX) or test_dep.removesuffix(TEST_DEP_SUFFIX) != name:
fail("tradefed_test_suite.test_dep must be named %s%s, " % (name, TEST_DEP_SUFFIX) +
"but got %s" % test_dep)
# Shared attributes between all three test types. The only difference between them
# are the default template_test_config at this level.
common_tradefed_attrs = dict(
[
("test", test_dep),
# User-specified test config should take precedence over auto-generated ones.
("test_config", test_config),
# Extra lines to go into the test config.
("template_configs", template_configs),
# Path to install the test executable on device.
("template_install_base", template_install_base),
# Test language helps to determine test_executable and fit test into config templates.
("test_language", test_language),
# There shouldn't be package-external dependencies on the internal tests.
("visibility", ["//visibility:private"]),
# The internal tests shouldn't run with ... or :all target patterns
("tags", ["manual"]),
# Tradefed harness always builds for host.
("target_compatible_with", ["//build/bazel/platforms/os:linux"]),
],
)
# Tradefed deviceless test. Device NOT necessary. Tradefed will be invoked with --null-device.
tradefed_deviceless_test_name = name + "__tf_deviceless_test"
tests = [tradefed_deviceless_test_name]
tradefed_deviceless_test(
name = tradefed_deviceless_test_name,
template_test_config = None if test_config else deviceless_test_config,
**common_tradefed_attrs
)
# | type | deviceless / unit | device-driven | host-driven device |
# |------------------+-----------------------+---------------+--------------------|
# | java_test_host | YES | | YES |
# | java_test | YES if host_supported | YES | |
# | cc_test_host | YES | | YES |
# | cc_test | YES if host_supported | YES | |
# | python_test_host | YES | | YES |
# | python_test | YES if host_supported | YES | |
# | android_test | | YES | |
if device_driven_test_config and host_driven_device_test_config:
fail("%s: device tests cannot be both device driven and host driven at the same time." % name)
# Tradefed host or device driven device test. Device necessary.
if device_driven_test_config or host_driven_device_test_config:
tradefed_device_test_name = name + "__tf_device_test"
tests.append(tradefed_device_test_name)
if device_driven_test_config:
tradefed_device_driven_test(
name = tradefed_device_test_name,
template_test_config = None if test_config else device_driven_test_config,
**common_tradefed_attrs
)
else:
tradefed_host_driven_device_test(
name = tradefed_device_test_name,
template_test_config = None if test_config else host_driven_device_test_config,
**common_tradefed_attrs
)
native.test_suite(
name = name,
tests = tests,
visibility = visibility,
# Warning: be careful when specifying tags here, as tags have special
# meaning in test suites for filtering tests.
tags = tags,
target_compatible_with = ["//build/bazel/platforms/os:linux"],
)
def _abspath(relative):
return "${TEST_SRCDIR}/${TEST_WORKSPACE}/" + relative
def _classpath(jars):
return ":".join([_abspath(f.short_path) for f in depset(jars).to_list()])