# 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") load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") load("@env//:env.bzl", "env") load("//build/bazel/flags:common.bzl", "is_env_true") load("//build/bazel/platforms:platform_utils.bzl", "platforms") load("//build/bazel_common_rules/rules/remote_device/device:device_environment.bzl", "DeviceEnvironment") load(":cc_aspects.bzl", "CcTestSharedLibsInfo", "collect_cc_libs_aspect") # Apply this suffix to the name of the test dep target (e.g. the cc_test target) TEST_DEP_SUFFIX = "__tf_internal" # Apply this suffix to the name of the test filter generator target. FILTER_GENERATOR_SUFFIX = "__filter_generator" LANGUAGE_CC = "cc" LANGUAGE_JAVA = "java" LANGUAGE_ANDROID = "android" LANGUAGE_SHELL = "shell" # 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, _): device_platform = str(settings["//build/bazel/product_config:device_platform"]) return { "//command_line_option:platforms": device_platform, } _tradefed_always_device_transition = transition( implementation = _tradefed_always_device_transition_impl, inputs = ["//build/bazel/product_config:device_platform"], 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", executable = True, allow_single_file = True, cfg = "exec", ), "_aapt": attr.label( default = "//frameworks/base/tools/aapt:aapt", executable = True, cfg = "exec", doc = "aapt (v1). Used by Tradefed.", ), "_aapt2": attr.label( default = "//frameworks/base/tools/aapt2:aapt2", executable = True, cfg = "exec", doc = "aapt (v2). Used by Tradefed.", ), "_auto_gen_test_config": attr.label( default = "//build/make/tools:auto_gen_test_config", executable = True, cfg = "exec", doc = "Python script for automatically generating the Tradefed test config for android tests.", ), "_empty_test_config": attr.label( default = "//build/make/core:empty_test_config.xml", allow_single_file = True, ), "_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", ), "data_bins": attr.label_list( doc = "Executables that need to be installed alongside the test entry point.", cfg = "exec", ), "_platform_utils": attr.label( default = Label("//build/bazel/platforms:platform_utils"), ), "test_config": attr.label( allow_single_file = True, doc = "Test/Tradefed config.", ), "dynamic_config": attr.label( allow_single_file = True, doc = "Dynamic test 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_filter_generator": attr.label( allow_single_file = True, doc = "test filter to specify test class and method to run", ), "test_language": attr.string( default = "", values = ["", LANGUAGE_CC, LANGUAGE_JAVA, LANGUAGE_ANDROID, LANGUAGE_SHELL], doc = "the programming language the test uses", ), "suffix": attr.string( default = "", values = ["", "32", "64"], doc = "the suffix of the test binary", ), "_java_runtime": attr.label( default = Label("@bazel_tools//tools/jdk:current_java_runtime"), cfg = "exec", providers = [java_common.JavaRuntimeInfo], ), } # 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, "") def _copy_file(ctx, input, output): ctx.actions.run_shell( inputs = depset(direct = [input]), outputs = [output], command = "cp -f %s %s" % (input.path, output.path), mnemonic = "CopyFile", use_default_shell_env = True, ) # Get test config if specified or generate test config from template. def _get_or_generate_test_config(ctx, module_name, tf_test_dir, test_entry_point, 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)) # If dynamic_config is specified copy it with a new name. dynamic_config = None if ctx.file.dynamic_config: # Dynamic config file is specified in test config file and doesn't have the 32/64 suffix. dynamic_config = ctx.actions.declare_file(paths.join(tf_test_dir, module_name + ".dynamic")) _copy_file(ctx, ctx.file.dynamic_config, dynamic_config) # If existing tradefed config is specified, copy 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 copy 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 # └── adb_test.config test_config = ctx.actions.declare_file(paths.join(tf_test_dir, module_name + ".config")) if ctx.file.test_config: _copy_file(ctx, ctx.file.test_config, test_config) return test_config, dynamic_config # No test config specified, generate config from template. if test_language == LANGUAGE_ANDROID: # android tests require a tool to parse the final AndroidManifest.xml # for label, package and runner class. # # First, dump the xmltree with aapt2. android_binary doesn't have a # provider to access the AndroidManifest.xml directly, and we can't use # the compiled XML from the APK directly. xmltree = ctx.actions.declare_file(module_name + ".xmltree", sibling = test_config) extra_configs = "" if ctx.attr.template_configs: extra_configs = "--extra-configs %s" % ("\\n ".join(ctx.attr.template_configs)) ctx.actions.run_shell( inputs = [test_entry_point, ctx.executable._aapt2], outputs = [xmltree], command = "%s dump xmltree %s --file AndroidManifest.xml %s > %s" % ( ctx.executable._aapt2.path, test_entry_point.path, extra_configs, xmltree.path, ), mnemonic = "DumpManifestXmlTree", progress_message = "Extracting test information from AndroidManifest.xml for %s" % module_name, ) # Then, run auto_gen_test_config.py which has a small xmltree parser. args = ctx.actions.args() args.add_all([test_config, xmltree, ctx.file._empty_test_config, ctx.file.template_test_config]) ctx.actions.run( executable = ctx.executable._auto_gen_test_config, arguments = [args], inputs = [ xmltree, ctx.file._empty_test_config, ctx.file.template_test_config, ], outputs = [test_config], mnemonic = "AutoGenTestConfig", progress_message = "Generating Tradefed test config for %s" % module_name, ) return test_config, dynamic_config # Non-android tests. expand_template_substitutions = { "{MODULE}": module_name, "{EXTRA_CONFIGS}": "\n ".join(ctx.attr.template_configs), "{TEST_INSTALL_BASE}": ctx.attr.template_install_base, } if test_language == LANGUAGE_SHELL: expand_template_substitutions["{OUTPUT_FILENAME}"] = module_name + ".sh" ctx.actions.expand_template( template = ctx.file.template_test_config, output = test_config, substitutions = expand_template_substitutions, ) return test_config, dynamic_config # Generate tradefed result reporter config. def _create_result_reporter_config(ctx): result_reporters_config_file = ctx.actions.declare_file("result-reporters.xml") config_lines = [ "", "", ] 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) config_lines.append("") 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 = []): device_script = "" if _is_remote_device_test(ctx): device_script = _abspath(ctx.attr._run_with[DeviceEnvironment].runner.to_list()[0].short_path) tf_test_dir = paths.join(ctx.label.name, "testcases") test_target = _get_test_target(ctx) test_language = ctx.attr.test_language if test_language == LANGUAGE_ANDROID: test_entry_point = test_target[ApkInfo].signed_apk else: # cc, java, py, sh test_entry_point = test_target.files_to_run.executable # 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_basename_with_ext = _normalize_test_name(test_entry_point.basename) module_name = paths.replace_extension(test_basename_with_ext, "") # clean module name test_config_files = [] # Get or generate test config. test_config, dynamic_config = _get_or_generate_test_config( ctx, module_name, tf_test_dir, test_entry_point, test_language, ) test_config_files.append(test_config) if dynamic_config != None: test_config_files.append(dynamic_config) # Generate result reporter config file. report_config = _create_result_reporter_config(ctx) test_runfiles = [] test_filter_output = None if ctx.attr.test_filter_generator: test_filter_output = ctx.file.test_filter_generator test_runfiles.append(test_filter_output) # This may contain a 32/64 suffix for multilib native test, or .jar/.apk # extension for others. out = ctx.actions.declare_file(test_basename_with_ext, sibling = test_config) # Copy the test executable to the test cases directory _copy_file(ctx, test_entry_point, out) root_relative_tests_dir = paths.dirname(out.short_path) test_runfiles.append(out) if ctx.attr.suffix and test_basename_with_ext.endswith(ctx.attr.suffix): # Create a compat entry point symlink without the 32/64 suffix so # Tradefed can find it with its local file target preparers, like # PushFilePreparer. # # This is also so that the test will pass regardless of # whether `