xref: /aosp_15_r20/external/bazelbuild-rules_android/rules/android_local_test/impl.bzl (revision 9e965d6fece27a77de5377433c2f7e6999b8cc0b)
1# Copyright 2018 The Bazel Authors. All rights reserved.
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"""Bazel rule for Android local test."""
16
17load("//rules:acls.bzl", "acls")
18load("//rules:attrs.bzl", "attrs")
19load("//rules:common.bzl", "common")
20load("//rules:java.bzl", "java")
21load(
22    "//rules:processing_pipeline.bzl",
23    "ProviderInfo",
24    "processing_pipeline",
25)
26load("//rules:providers.bzl", "AndroidFilteredJdepsInfo")
27load("//rules:resources.bzl", "resources")
28load(
29    "//rules:utils.bzl",
30    "ANDROID_TOOLCHAIN_TYPE",
31    "compilation_mode",
32    "get_android_sdk",
33    "get_android_toolchain",
34    "log",
35    "utils",
36)
37load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
38
39JACOCOCO_CLASS = "com.google.testing.coverage.JacocoCoverageRunner"
40TEST_RUNNER_CLASS = "com.google.testing.junit.runner.BazelTestRunner"
41
42# JVM processes for android_local_test targets are typically short lived. By
43# using TieredStopAtLevel=1, aggressive JIT compilations are avoided, which is
44# more optimal for android_local_test workloads.
45DEFAULT_JIT_FLAGS = ["-XX:+TieredCompilation", "-XX:TieredStopAtLevel=1"]
46
47# Many P99 and above android_local_test targets use a lot of memory so the default 1 GiB
48# JVM max heap size is not sufficient. Bump the max heap size to from 1 GiB -> 8 GiB. This performs
49# the best across all P% layers from profiling.
50DEFAULT_GC_FLAGS = ["-Xmx8g"]
51
52# disable class loading by default for faster classloading and consistent enviroment across
53# local and remote execution
54DEFAULT_VERIFY_FLAGS = ["-Xverify:none"]
55
56def _validations_processor(ctx, **_unused_sub_ctxs):
57    _check_src_pkg(ctx, True)
58
59def _process_manifest(ctx, java_package, **_unused_sub_ctxs):
60    manifest_ctx = None
61    manifest_values = resources.process_manifest_values(
62        ctx,
63        ctx.attr.manifest_values,
64        acls.get_min_sdk_floor(str(ctx.label)),
65    )
66    if ctx.file.manifest == None:
67        # No manifest provided, generate one
68        manifest = ctx.actions.declare_file("_generated/" + ctx.label.name + "/AndroidManifest.xml")
69        resources.generate_dummy_manifest(
70            ctx,
71            out_manifest = manifest,
72            java_package = java_package,
73            min_sdk_version = int(manifest_values.get("minSdkVersion", 16)),  # minsdk supported by robolectric framework
74        )
75        manifest_ctx = struct(processed_manifest = manifest, processed_manifest_values = manifest_values)
76    else:
77        manifest_ctx = resources.bump_min_sdk(
78            ctx,
79            manifest = ctx.file.manifest,
80            manifest_values = ctx.attr.manifest_values,
81            floor = acls.get_min_sdk_floor(str(ctx.label)),
82            enforce_min_sdk_floor_tool = get_android_toolchain(ctx).enforce_min_sdk_floor_tool.files_to_run,
83        )
84
85    return ProviderInfo(
86        name = "manifest_ctx",
87        value = manifest_ctx,
88    )
89
90def _process_resources(ctx, java_package, manifest_ctx, **_unused_sub_ctxs):
91    resources_ctx = resources.package(
92        ctx,
93        deps = ctx.attr.deps,
94        manifest = manifest_ctx.processed_manifest,
95        manifest_values = manifest_ctx.processed_manifest_values,
96        resource_files = ctx.files.resource_files,
97        assets = ctx.files.assets,
98        assets_dir = ctx.attr.assets_dir,
99        resource_configs = ctx.attr.resource_configuration_filters,
100        densities = ctx.attr.densities,
101        nocompress_extensions = ctx.attr.nocompress_extensions,
102        compilation_mode = compilation_mode.get(ctx),
103        java_package = java_package,
104        shrink_resources = attrs.tristate.no,
105        aapt = get_android_toolchain(ctx).aapt2.files_to_run,
106        android_jar = get_android_sdk(ctx).android_jar,
107        busybox = get_android_toolchain(ctx).android_resources_busybox.files_to_run,
108        host_javabase = ctx.attr._host_javabase,
109        # TODO(b/140582167): Throwing on resource conflict need to be rolled
110        # out to android_local_test.
111        should_throw_on_conflict = False,
112    )
113
114    return ProviderInfo(
115        name = "resources_ctx",
116        value = resources_ctx,
117    )
118
119def _process_jvm(ctx, resources_ctx, **_unused_sub_ctxs):
120    deps = (
121        ctx.attr._implicit_classpath +
122        ctx.attr.deps +
123        [get_android_toolchain(ctx).testsupport]
124    )
125
126    if ctx.configuration.coverage_enabled:
127        deps.append(get_android_toolchain(ctx).jacocorunner)
128        java_start_class = JACOCOCO_CLASS
129        coverage_start_class = TEST_RUNNER_CLASS
130    else:
131        java_start_class = TEST_RUNNER_CLASS
132        coverage_start_class = None
133
134    java_info = java_common.add_constraints(
135        java.compile_android(
136            ctx,
137            ctx.outputs.jar,
138            ctx.actions.declare_file(ctx.label.name + "-src.jar"),
139            srcs = ctx.files.srcs,
140            resources = ctx.files.resources,
141            javac_opts = ctx.attr.javacopts,
142            r_java = resources_ctx.r_java,
143            deps = (
144                utils.collect_providers(JavaInfo, deps) +
145                [
146                    JavaInfo(
147                        output_jar = get_android_sdk(ctx).android_jar,
148                        compile_jar = get_android_sdk(ctx).android_jar,
149                        # The android_jar must not be compiled into the test, it
150                        # will bloat the Jar with no benefit.
151                        neverlink = True,
152                    ),
153                ]
154            ),
155            plugins = utils.collect_providers(JavaPluginInfo, ctx.attr.plugins),
156            java_toolchain = common.get_java_toolchain(ctx),
157        ),
158        constraints = ["android"],
159    )
160
161    # TODO(timpeut): some conformance tests require a filtered JavaInfo
162    # with no transitive_ deps.
163    providers = [java_info]
164    runfiles = []
165
166    # Create a filtered jdeps with no resources jar. See b/129011477 for more context.
167    if java_info.outputs.jdeps != None:
168        filtered_jdeps = ctx.actions.declare_file(ctx.label.name + ".filtered.jdeps")
169        filter_jdeps(ctx, java_info.outputs.jdeps, filtered_jdeps, utils.only(resources_ctx.r_java.compile_jars.to_list()))
170        providers.append(AndroidFilteredJdepsInfo(jdeps = filtered_jdeps))
171        runfiles.append(filtered_jdeps)
172
173    return ProviderInfo(
174        name = "jvm_ctx",
175        value = struct(
176            java_info = java_info,
177            providers = providers,
178            deps = deps,
179            java_start_class = java_start_class,
180            coverage_start_class = coverage_start_class,
181            android_properties_file = ctx.attr.robolectric_properties_file,
182            additional_jvm_flags = [],
183        ),
184        runfiles = ctx.runfiles(files = runfiles),
185    )
186
187def _process_proto(_ctx, **_unused_sub_ctxs):
188    return ProviderInfo(
189        name = "proto_ctx",
190        value = struct(
191            proto_extension_registry_dep = depset(),
192        ),
193    )
194
195def _process_deploy_jar(ctx, java_package, jvm_ctx, proto_ctx, resources_ctx, **_unused_sub_ctxs):
196    res_file_path = resources_ctx.validation_result.short_path
197    subs = {
198        "%android_merged_manifest%": resources_ctx.processed_manifest.short_path,
199        "%android_merged_resources%": "jar:file:" + res_file_path + "!/res",
200        "%android_merged_assets%": "jar:file:" + res_file_path + "!/assets",
201        # The native resources_ctx has the package field, whereas the starlark resources_ctx uses the java_package
202        "%android_custom_package%": getattr(resources_ctx, "package", java_package or ""),
203        "%android_resource_apk%": resources_ctx.resources_apk.short_path,
204    }
205    res_runfiles = [
206        resources_ctx.resources_apk,
207        resources_ctx.validation_result,
208        resources_ctx.processed_manifest,
209    ]
210
211    properties_file = _genfiles_artifact(ctx, "test_config.properties")
212    properties_jar = _genfiles_artifact(ctx, "properties.jar")
213    ctx.actions.expand_template(
214        template = utils.only(get_android_toolchain(ctx).robolectric_template.files.to_list()),
215        output = properties_file,
216        substitutions = subs,
217    )
218    _zip_file(ctx, properties_file, "com/android/tools", properties_jar)
219    properties_jar_dep = depset([properties_jar])
220
221    runtime_deps = depset(transitive = [
222        x.transitive_runtime_jars
223        for x in utils.collect_providers(JavaInfo, ctx.attr.runtime_deps)
224    ])
225    android_jar_dep = depset([get_android_sdk(ctx).android_jar])
226    out_jar_dep = depset([ctx.outputs.jar])
227    classpath = depset(
228        transitive = [
229            proto_ctx.proto_extension_registry_dep,
230            out_jar_dep,
231            resources_ctx.r_java.compile_jars,
232            properties_jar_dep,
233            runtime_deps,
234            android_jar_dep,
235            jvm_ctx.java_info.transitive_runtime_jars,
236        ],
237    )
238
239    java.singlejar(
240        ctx,
241        # TODO(timpeut): investigate whether we need to filter the stub classpath as well
242        [f for f in classpath.to_list() if f.short_path.endswith(".jar")],
243        ctx.outputs.deploy_jar,
244        mnemonic = "JavaDeployJar",
245        include_build_data = True,
246        java_toolchain = common.get_java_toolchain(ctx),
247    )
248    return ProviderInfo(
249        name = "deploy_jar_ctx",
250        value = struct(
251            classpath = classpath,
252        ),
253        runfiles = ctx.runfiles(files = res_runfiles, transitive_files = classpath),
254    )
255
256def _preprocess_stub(ctx, **_unused_sub_ctxs):
257    javabase = ctx.attr._current_java_runtime[java_common.JavaRuntimeInfo]
258    java_executable = str(javabase.java_executable_runfiles_path)
259    java_executable_files = javabase.files
260
261    # Absolute java_executable does not require any munging
262    if java_executable.startswith("/"):
263        java_executable = "JAVABIN=" + java_executable
264
265    prefix = ctx.attr._runfiles_root_prefix[BuildSettingInfo].value
266    if not java_executable.startswith(prefix):
267        java_executable = prefix + java_executable
268
269    java_executable = "JAVABIN=${JAVABIN:-${JAVA_RUNFILES}/" + java_executable + "}"
270
271    substitutes = {
272        "%javabin%": java_executable,
273        "%load_lib%": "",
274        "%set_ASAN_OPTIONS%": "",
275    }
276    runfiles = [java_executable_files]
277
278    return ProviderInfo(
279        name = "stub_preprocess_ctx",
280        value = struct(
281            substitutes = substitutes,
282            runfiles = runfiles,
283        ),
284    )
285
286def _process_stub(ctx, deploy_jar_ctx, jvm_ctx, stub_preprocess_ctx, **_unused_sub_ctxs):
287    runfiles = []
288
289    merged_instr = None
290    if ctx.configuration.coverage_enabled:
291        merged_instr = ctx.actions.declare_file(ctx.label.name + "_merged_instr.jar")
292        java.singlejar(
293            ctx,
294            [f for f in deploy_jar_ctx.classpath.to_list() if f.short_path.endswith(".jar")],
295            merged_instr,
296            mnemonic = "JavaDeployJar",
297            include_build_data = True,
298            java_toolchain = common.get_java_toolchain(ctx),
299        )
300        runfiles.append(merged_instr)
301
302    stub = ctx.actions.declare_file(ctx.label.name)
303    classpath_file = ctx.actions.declare_file(ctx.label.name + "_classpath")
304    runfiles.append(classpath_file)
305    test_class = _get_test_class(ctx)
306    if not test_class:
307        # fatal error
308        log.error("test_class could not be derived for " + str(ctx.label) +
309                  ". Explicitly set test_class or move this source file to " +
310                  "a java source root.")
311
312    _create_stub(
313        ctx,
314        stub_preprocess_ctx.substitutes,
315        stub,
316        classpath_file,
317        deploy_jar_ctx.classpath,
318        _get_jvm_flags(ctx, test_class, jvm_ctx.android_properties_file, jvm_ctx.additional_jvm_flags),
319        jvm_ctx.java_start_class,
320        jvm_ctx.coverage_start_class,
321        merged_instr,
322    )
323    return ProviderInfo(
324        name = "stub_ctx",
325        value = struct(
326            stub = stub,
327        ),
328        runfiles = ctx.runfiles(
329            files = runfiles,
330            transitive_files = depset(
331                transitive = stub_preprocess_ctx.runfiles,
332            ),
333        ),
334    )
335
336PROCESSORS = dict(
337    ValidationsProcessor = _validations_processor,
338    ManifestProcessor = _process_manifest,
339    ResourceProcessor = _process_resources,
340    JvmProcessor = _process_jvm,
341    ProtoProcessor = _process_proto,
342    DeployJarProcessor = _process_deploy_jar,
343    StubPreProcessor = _preprocess_stub,
344    StubProcessor = _process_stub,
345)
346
347def finalize(
348        ctx,
349        jvm_ctx,
350        proto_ctx,
351        providers,
352        runfiles,
353        stub_ctx,
354        validation_outputs,
355        **_unused_sub_ctxs):
356    """Creates the final providers for the rule.
357
358    Args:
359      ctx: The context.
360      jvm_ctx: ProviderInfo. The jvm ctx.
361      proto_ctx: ProviderInfo. The proto ctx.
362      providers: sequence of providers. The providers to propagate.
363      runfiles: Runfiles. The runfiles collected during processing.
364      stub_ctx: ProviderInfo. The stub ctx.
365      validation_outputs: sequence of Files. The validation outputs.
366      **_unused_sub_ctxs: Unused ProviderInfo.
367
368    Returns:
369      A struct with Android and Java legacy providers and a list of providers.
370    """
371    runfiles = runfiles.merge(ctx.runfiles(collect_data = True))
372    runfiles = runfiles.merge(utils.get_runfiles(ctx, jvm_ctx.deps + ctx.attr.data + ctx.attr.runtime_deps))
373
374    providers.extend([
375        DefaultInfo(
376            files = depset(
377                [ctx.outputs.jar, stub_ctx.stub],
378                transitive = [proto_ctx.proto_extension_registry_dep],
379                order = "preorder",
380            ),
381            executable = stub_ctx.stub,
382            runfiles = runfiles,
383        ),
384        OutputGroupInfo(
385            _validation = depset(validation_outputs),
386        ),
387        coverage_common.instrumented_files_info(
388            ctx = ctx,
389            source_attributes = ["srcs"],
390            dependency_attributes = ["deps", "runtime_deps", "data"],
391        ),
392    ])
393    return providers
394
395_PROCESSING_PIPELINE = processing_pipeline.make_processing_pipeline(
396    processors = PROCESSORS,
397    finalize = finalize,
398)
399
400def impl(ctx):
401    java_package = java.resolve_package_from_label(ctx.label, ctx.attr.custom_package)
402    return processing_pipeline.run(ctx, java_package, _PROCESSING_PIPELINE)
403
404def _check_src_pkg(ctx, warn = True):
405    pkg = ctx.label.package
406    for attr in ctx.attr.srcs:
407        if attr.label.package != pkg:
408            msg = "Do not import %s directly. Either move the file to this package or depend on an appropriate rule there." % attr.label
409            if warn:
410                log.warn(msg)
411            else:
412                log.error(msg)
413
414def _genfiles_artifact(ctx, name):
415    return ctx.actions.declare_file(
416        "/".join([ctx.genfiles_dir.path, ctx.label.name, name]),
417    )
418
419def _get_test_class(ctx):
420    # Use the specified test_class if set
421    if ctx.attr.test_class != "":
422        return ctx.attr.test_class
423
424    # Use a heuristic based on the rule name and the "srcs" list
425    # to determine the primary Java class.
426    expected = "/" + ctx.label.name + ".java"
427    for f in ctx.attr.srcs:
428        path = f.label.package + "/" + f.label.name
429        if path.endswith(expected):
430            return java.resolve_package(path[:-5])
431
432    # Last resort: Use the name and package name of the target.
433    return java.resolve_package(ctx.label.package + "/" + ctx.label.name)
434
435def _create_stub(
436        ctx,
437        substitutes,
438        stub_file,
439        classpath_file,
440        runfiles,
441        jvm_flags,
442        java_start_class,
443        coverage_start_class,
444        merged_instr):
445    subs = {
446        "%needs_runfiles%": "1",
447        "%runfiles_manifest_only%": "",
448        # To avoid cracking open the depset, classpath is read from a separate
449        # file created in its own action. Needed as expand_template does not
450        # support ctx.actions.args().
451        "%classpath%": "$(eval echo $(<%s))" % (classpath_file.short_path),
452        "%java_start_class%": java_start_class,
453        "%jvm_flags%": " ".join(jvm_flags),
454        "%workspace_prefix%": ctx.workspace_name + "/",
455    }
456
457    if coverage_start_class:
458        prefix = ctx.attr._runfiles_root_prefix[BuildSettingInfo].value
459        subs["%set_jacoco_metadata%"] = (
460            "export JACOCO_METADATA_JAR=${JAVA_RUNFILES}/" + prefix +
461            merged_instr.short_path
462        )
463        subs["%set_jacoco_main_class%"] = (
464            "export JACOCO_MAIN_CLASS=" + coverage_start_class
465        )
466        subs["%set_jacoco_java_runfiles_root%"] = (
467            "export JACOCO_JAVA_RUNFILES_ROOT=${JAVA_RUNFILES}/" + prefix
468        )
469    else:
470        subs["%set_jacoco_metadata%"] = ""
471        subs["%set_jacoco_main_class%"] = ""
472        subs["%set_jacoco_java_runfiles_root%"] = ""
473
474    subs.update(substitutes)
475
476    ctx.actions.expand_template(
477        template = utils.only(get_android_toolchain(ctx).java_stub.files.to_list()),
478        output = stub_file,
479        substitutions = subs,
480        is_executable = True,
481    )
482
483    args = ctx.actions.args()
484    args.add_joined(
485        runfiles,
486        join_with = ":",
487        map_each = _get_classpath,
488    )
489    args.set_param_file_format("multiline")
490    ctx.actions.write(
491        output = classpath_file,
492        content = args,
493    )
494    return stub_file
495
496def _get_classpath(s):
497    return "${J3}" + s.short_path
498
499def _get_jvm_flags(ctx, main_class, robolectric_properties_path, additional_jvm_flags):
500    return [
501        "-ea",
502        "-Dbazel.test_suite=" + main_class,
503        "-Drobolectric.offline=true",
504        "-Drobolectric-deps.properties=" + robolectric_properties_path,
505        "-Duse_framework_manifest_parser=true",
506        "-Drobolectric.logging=stdout",
507        "-Drobolectric.logging.enabled=true",
508        "-Dorg.robolectric.packagesToNotAcquire=com.google.testing.junit.runner.util",
509    ] + DEFAULT_JIT_FLAGS + DEFAULT_GC_FLAGS + DEFAULT_VERIFY_FLAGS + additional_jvm_flags + [
510        ctx.expand_make_variables(
511            "jvm_flags",
512            ctx.expand_location(flag, ctx.attr.data),
513            {},
514        )
515        for flag in ctx.attr.jvm_flags
516    ]
517
518def _zip_file(ctx, f, dir_name, out_zip):
519    cmd = """
520base=$(pwd)
521tmp_dir=$(mktemp -d)
522
523cd $tmp_dir
524mkdir -p {dir_name}
525cp $base/{f} {dir_name}
526$base/{zip_tool} -jt -X -q $base/{out_zip} {dir_name}/$(basename {f})
527""".format(
528        zip_tool = get_android_toolchain(ctx).zip_tool.files_to_run.executable.path,
529        f = f.path,
530        dir_name = dir_name,
531        out_zip = out_zip.path,
532    )
533    ctx.actions.run_shell(
534        command = cmd,
535        inputs = [f],
536        tools = get_android_toolchain(ctx).zip_tool.files,
537        outputs = [out_zip],
538        mnemonic = "AddToZip",
539        toolchain = ANDROID_TOOLCHAIN_TYPE,
540    )
541
542def filter_jdeps(ctx, in_jdeps, out_jdeps, filter_suffix):
543    """Runs the JdepsFilter tool.
544
545    Args:
546      ctx: The context.
547      in_jdeps: File. The input jdeps file.
548      out_jdeps: File. The filtered jdeps output.
549      filter_suffix: File. The jdeps suffix to filter.
550    """
551    args = ctx.actions.args()
552    args.add("--in")
553    args.add(in_jdeps.path)
554    args.add("--target")
555    args.add(filter_suffix)
556    args.add("--out")
557    args.add(out_jdeps.path)
558    ctx.actions.run(
559        inputs = [in_jdeps],
560        outputs = [out_jdeps],
561        executable = get_android_toolchain(ctx).jdeps_tool.files_to_run,
562        arguments = [args],
563        mnemonic = "JdepsFilter",
564        progress_message = "Filtering jdeps",
565        toolchain = ANDROID_TOOLCHAIN_TYPE,
566    )
567