• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 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"""
16Implementation of Atest's Bazel mode.
17
18Bazel mode runs tests using Bazel by generating a synthetic workspace that
19contains test targets. Using Bazel allows Atest to leverage features such as
20sandboxing, caching, and remote execution.
21"""
22# pylint: disable=missing-function-docstring
23# pylint: disable=missing-class-docstring
24# pylint: disable=too-many-lines
25
26from __future__ import annotations
27
28import argparse
29import contextlib
30import dataclasses
31import enum
32import functools
33import os
34import re
35import shutil
36import subprocess
37import warnings
38
39from abc import ABC, abstractmethod
40from collections import defaultdict, deque, OrderedDict
41from pathlib import Path
42from types import MappingProxyType
43from typing import Any, Callable, Dict, IO, List, Set
44
45import atest_utils
46import constants
47import module_info
48
49from atest_enum import ExitCode
50from test_finders import test_finder_base
51from test_finders import test_info
52from test_runners import test_runner_base as trb
53from test_runners import atest_tf_test_runner as tfr
54
55
56_BAZEL_WORKSPACE_DIR = 'atest_bazel_workspace'
57_SUPPORTED_BAZEL_ARGS = MappingProxyType({
58    # https://docs.bazel.build/versions/main/command-line-reference.html#flag--runs_per_test
59    constants.ITERATIONS:
60        lambda arg_value: [f'--runs_per_test={str(arg_value)}'],
61    # https://docs.bazel.build/versions/main/command-line-reference.html#flag--test_keep_going
62    constants.RERUN_UNTIL_FAILURE:
63        lambda arg_value:
64        ['--notest_keep_going', f'--runs_per_test={str(arg_value)}'],
65    # https://docs.bazel.build/versions/main/command-line-reference.html#flag--flaky_test_attempts
66    constants.RETRY_ANY_FAILURE:
67        lambda arg_value: [f'--flaky_test_attempts={str(arg_value)}'],
68    constants.BAZEL_ARG:
69        lambda arg_value: [item for sublist in arg_value for item in sublist]
70})
71
72
73@enum.unique
74class Features(enum.Enum):
75    NULL_FEATURE = ('--null-feature', 'Enables a no-action feature.', True)
76    EXPERIMENTAL_DEVICE_DRIVEN_TEST = (
77        '--experimental-device-driven-test',
78        'Enables running device-driven tests in Bazel mode.', True)
79    EXPERIMENTAL_BES_PUBLISH = ('--experimental-bes-publish',
80                                'Upload test results via BES in Bazel mode.',
81                                False)
82
83    def __init__(self, arg_flag, description, affects_workspace):
84        self.arg_flag = arg_flag
85        self.description = description
86        self.affects_workspace = affects_workspace
87
88
89def add_parser_arguments(parser: argparse.ArgumentParser, dest: str):
90    for _, member in Features.__members__.items():
91        parser.add_argument(member.arg_flag,
92                            action='append_const',
93                            const=member,
94                            dest=dest,
95                            help=member.description)
96
97
98def get_bazel_workspace_dir() -> Path:
99    return Path(atest_utils.get_build_out_dir()).joinpath(_BAZEL_WORKSPACE_DIR)
100
101
102def generate_bazel_workspace(mod_info: module_info.ModuleInfo,
103                             enabled_features: Set[Features] = None):
104    """Generate or update the Bazel workspace used for running tests."""
105
106    src_root_path = Path(os.environ.get(constants.ANDROID_BUILD_TOP))
107    workspace_path = get_bazel_workspace_dir()
108    workspace_generator = WorkspaceGenerator(
109        src_root_path,
110        workspace_path,
111        Path(os.environ.get(constants.ANDROID_PRODUCT_OUT)),
112        Path(os.environ.get(constants.ANDROID_HOST_OUT)),
113        Path(atest_utils.get_build_out_dir()),
114        mod_info,
115        enabled_features,
116    )
117    workspace_generator.generate()
118
119
120def get_default_build_metadata():
121    return BuildMetadata(atest_utils.get_manifest_branch(),
122                         atest_utils.get_build_target())
123
124
125class WorkspaceGenerator:
126    """Class for generating a Bazel workspace."""
127
128    # pylint: disable=too-many-arguments
129    def __init__(self, src_root_path: Path, workspace_out_path: Path,
130                 product_out_path: Path, host_out_path: Path,
131                 build_out_dir: Path, mod_info: module_info.ModuleInfo,
132                 enabled_features: Set[Features] = None):
133        """Initializes the generator.
134
135        Args:
136            src_root_path: Path of the ANDROID_BUILD_TOP.
137            workspace_out_path: Path where the workspace will be output.
138            product_out_path: Path of the ANDROID_PRODUCT_OUT.
139            host_out_path: Path of the ANDROID_HOST_OUT.
140            build_out_dir: Path of OUT_DIR
141            mod_info: ModuleInfo object.
142            enabled_features: Set of enabled features.
143        """
144        self.enabled_features = enabled_features or set()
145        self.src_root_path = src_root_path
146        self.workspace_out_path = workspace_out_path
147        self.product_out_path = product_out_path
148        self.host_out_path = host_out_path
149        self.build_out_dir = build_out_dir
150        self.mod_info = mod_info
151        self.path_to_package = {}
152
153    def generate(self):
154        """Generate a Bazel workspace.
155
156        If the workspace md5 checksum file doesn't exist or is stale, a new
157        workspace will be generated. Otherwise, the existing workspace will be
158        reused.
159        """
160        workspace_md5_checksum_file = self.workspace_out_path.joinpath(
161            'workspace_md5_checksum')
162        enabled_features_file = self.workspace_out_path.joinpath(
163            'atest_bazel_mode_enabled_features')
164        enabled_features_file_contents = '\n'.join(sorted(
165            f.name for f in self.enabled_features if f.affects_workspace))
166
167        if self.workspace_out_path.exists():
168            # Update the file with the set of the currently enabled features to
169            # make sure that changes are detected in the workspace checksum.
170            enabled_features_file.write_text(enabled_features_file_contents)
171            if atest_utils.check_md5(workspace_md5_checksum_file):
172                return
173
174            # We raise an exception if rmtree fails to avoid leaving stale
175            # files in the workspace that could interfere with execution.
176            shutil.rmtree(self.workspace_out_path)
177
178        atest_utils.colorful_print("Generating Bazel workspace.\n",
179                                   constants.RED)
180
181        self._add_test_module_targets()
182
183        self.workspace_out_path.mkdir(parents=True)
184        self._generate_artifacts()
185
186        # Note that we write the set of enabled features despite having written
187        # it above since the workspace no longer exists at this point.
188        enabled_features_file.write_text(enabled_features_file_contents)
189        atest_utils.save_md5(
190            [
191                self.mod_info.mod_info_file_path,
192                enabled_features_file,
193            ],
194            workspace_md5_checksum_file
195        )
196
197    def _add_test_module_targets(self):
198        seen = set()
199
200        for name, info in self.mod_info.name_to_module_info.items():
201            # Ignore modules that have a 'host_cross_' prefix since they are
202            # duplicates of existing modules. For example,
203            # 'host_cross_aapt2_tests' is a duplicate of 'aapt2_tests'. We also
204            # ignore modules with a '_32' suffix since these also are redundant
205            # given that modules have both 32 and 64-bit variants built by
206            # default. See b/77288544#comment6 and b/23566667 for more context.
207            if name.endswith("_32") or name.startswith("host_cross_"):
208                continue
209
210            if (Features.EXPERIMENTAL_DEVICE_DRIVEN_TEST in
211                    self.enabled_features and
212                    self.mod_info.is_device_driven_test(info)):
213                self._resolve_dependencies(
214                    self._add_test_target(
215                        info, 'device',
216                        TestTarget.create_device_test_target), seen)
217
218            if self.is_host_unit_test(info):
219                self._resolve_dependencies(
220                    self._add_test_target(
221                        info, 'host',
222                        TestTarget.create_deviceless_test_target), seen)
223
224    def _resolve_dependencies(
225        self, top_level_target: Target, seen: Set[Target]):
226
227        stack = [deque([top_level_target])]
228
229        while stack:
230            top = stack[-1]
231
232            if not top:
233                stack.pop()
234                continue
235
236            target = top.popleft()
237
238            # Note that we're relying on Python's default identity-based hash
239            # and equality methods. This is fine since we actually DO want
240            # reference-equality semantics for Target objects in this context.
241            if target in seen:
242                continue
243
244            seen.add(target)
245
246            next_top = deque()
247
248            for ref in target.dependencies():
249                info = ref.info or self._get_module_info(ref.name)
250                ref.set(self._add_prebuilt_target(info))
251                next_top.append(ref.target())
252
253            stack.append(next_top)
254
255    def _add_test_target(self, info: Dict[str, Any], name_suffix: str,
256                         create_fn: Callable) -> Target:
257        package_name = self._get_module_path(info)
258        name = f'{info["module_name"]}_{name_suffix}'
259
260        def create():
261            return create_fn(
262                name,
263                package_name,
264                info,
265            )
266
267        return self._add_target(package_name, name, create)
268
269    def _add_prebuilt_target(self, info: Dict[str, Any]) -> Target:
270        package_name = self._get_module_path(info)
271        name = info['module_name']
272
273        def create():
274            return SoongPrebuiltTarget.create(
275                self,
276                info,
277                package_name,
278            )
279
280        return self._add_target(package_name, name, create)
281
282    def _add_target(self, package_path: str, target_name: str,
283                    create_fn: Callable) -> Target:
284
285        package = self.path_to_package.get(package_path)
286
287        if not package:
288            package = Package(package_path)
289            self.path_to_package[package_path] = package
290
291        target = package.get_target(target_name)
292
293        if target:
294            return target
295
296        target = create_fn()
297        package.add_target(target)
298
299        return target
300
301    def _get_module_info(self, module_name: str) -> Dict[str, Any]:
302        info = self.mod_info.get_module_info(module_name)
303
304        if not info:
305            raise Exception(f'Could not find module `{module_name}` in'
306                            f' module_info file')
307
308        return info
309
310    def _get_module_path(self, info: Dict[str, Any]) -> str:
311        mod_path = info.get(constants.MODULE_PATH)
312
313        if len(mod_path) < 1:
314            module_name = info['module_name']
315            raise Exception(f'Module `{module_name}` does not have any path')
316
317        if len(mod_path) > 1:
318            module_name = info['module_name']
319            # We usually have a single path but there are a few exceptions for
320            # modules like libLLVM_android and libclang_android.
321            # TODO(yangbill): Raise an exception for multiple paths once
322            # b/233581382 is resolved.
323            warnings.formatwarning = lambda msg, *args, **kwargs: f'{msg}\n'
324            warnings.warn(
325                f'Module `{module_name}` has more than one path: `{mod_path}`')
326
327        return mod_path[0]
328
329    def is_host_unit_test(self, info: Dict[str, Any]) -> bool:
330        return self.mod_info.is_testable_module(
331            info) and self.mod_info.is_host_unit_test(info)
332
333    def _generate_artifacts(self):
334        """Generate workspace files on disk."""
335
336        self._create_base_files()
337        self._symlink(src='tools/asuite/atest/bazel/rules',
338                      target='bazel/rules')
339        self._symlink(src='tools/asuite/atest/bazel/configs',
340                      target='bazel/configs')
341        # Symlink to package with toolchain definitions.
342        self._symlink(src='prebuilts/build-tools',
343                      target='prebuilts/build-tools')
344        self._create_constants_file()
345
346        for package in self.path_to_package.values():
347            package.generate(self.workspace_out_path)
348
349
350
351    def _symlink(self, *, src, target):
352        """Create a symbolic link in workspace pointing to source file/dir.
353
354        Args:
355            src: A string of a relative path to root of Android source tree.
356                This is the source file/dir path for which the symbolic link
357                will be created.
358            target: A string of a relative path to workspace root. This is the
359                target file/dir path where the symbolic link will be created.
360        """
361        symlink = self.workspace_out_path.joinpath(target)
362        symlink.parent.mkdir(parents=True, exist_ok=True)
363        symlink.symlink_to(self.src_root_path.joinpath(src))
364
365    def _create_base_files(self):
366        self._symlink(src='tools/asuite/atest/bazel/WORKSPACE',
367                      target='WORKSPACE')
368        self._symlink(src='tools/asuite/atest/bazel/bazelrc',
369                      target='.bazelrc')
370        self.workspace_out_path.joinpath('BUILD.bazel').touch()
371
372    def _create_constants_file(self):
373
374        def variable_name(target_name):
375            return re.sub(r'[.-]', '_', target_name) + '_label'
376
377        targets = []
378        seen = set()
379
380        for module_name in TestTarget.DEVICELESS_TEST_PREREQUISITES.union(
381                TestTarget.DEVICE_TEST_PREREQUISITES):
382            info = self.mod_info.get_module_info(module_name)
383            target = self._add_prebuilt_target(info)
384            self._resolve_dependencies(target, seen)
385            targets.append(target)
386
387        with self.workspace_out_path.joinpath(
388                'constants.bzl').open('w') as f:
389            writer = IndentWriter(f)
390            for target in targets:
391                writer.write_line(
392                    '%s = "%s"' %
393                    (variable_name(target.name()), target.qualified_name())
394                )
395
396
397class Package:
398    """Class for generating an entire Package on disk."""
399
400    def __init__(self, path: str):
401        self.path = path
402        self.imports = defaultdict(set)
403        self.name_to_target = OrderedDict()
404
405    def add_target(self, target):
406        target_name = target.name()
407
408        if target_name in self.name_to_target:
409            raise Exception(f'Cannot add target `{target_name}` which already'
410                            f' exists in package `{self.path}`')
411
412        self.name_to_target[target_name] = target
413
414        for i in target.required_imports():
415            self.imports[i.bzl_package].add(i.symbol)
416
417    def generate(self, workspace_out_path: Path):
418        package_dir = workspace_out_path.joinpath(self.path)
419        package_dir.mkdir(parents=True, exist_ok=True)
420
421        self._create_filesystem_layout(package_dir)
422        self._write_build_file(package_dir)
423
424    def _create_filesystem_layout(self, package_dir: Path):
425        for target in self.name_to_target.values():
426            target.create_filesystem_layout(package_dir)
427
428    def _write_build_file(self, package_dir: Path):
429        with package_dir.joinpath('BUILD.bazel').open('w') as f:
430            f.write('package(default_visibility = ["//visibility:public"])\n')
431            f.write('\n')
432
433            for bzl_package, symbols in sorted(self.imports.items()):
434                symbols_text = ', '.join('"%s"' % s for s in sorted(symbols))
435                f.write(f'load("{bzl_package}", {symbols_text})\n')
436
437            for target in self.name_to_target.values():
438                f.write('\n')
439                target.write_to_build_file(f)
440
441    def get_target(self, target_name: str) -> Target:
442        return self.name_to_target.get(target_name, None)
443
444
445@dataclasses.dataclass(frozen=True)
446class Import:
447    bzl_package: str
448    symbol: str
449
450
451@dataclasses.dataclass(frozen=True)
452class Config:
453    name: str
454    out_path: Path
455
456
457class ModuleRef:
458
459    @staticmethod
460    def for_info(info) -> ModuleRef:
461        return ModuleRef(info=info)
462
463    @staticmethod
464    def for_name(name) -> ModuleRef:
465        return ModuleRef(name=name)
466
467    def __init__(self, info=None, name=None):
468        self.info = info
469        self.name = name
470        self._target = None
471
472    def target(self) -> Target:
473        if not self._target:
474            module_name = self.info['module_name']
475            raise Exception(f'Target not set for ref `{module_name}`')
476
477        return self._target
478
479    def set(self, target):
480        self._target = target
481
482
483class Target(ABC):
484    """Abstract class for a Bazel target."""
485
486    @abstractmethod
487    def name(self) -> str:
488        pass
489
490    def package_name(self) -> str:
491        pass
492
493    def qualified_name(self) -> str:
494        return f'//{self.package_name()}:{self.name()}'
495
496    def required_imports(self) -> Set[Import]:
497        return set()
498
499    def supported_configs(self) -> Set[Config]:
500        return set()
501
502    def dependencies(self) -> List[ModuleRef]:
503        return []
504
505    def write_to_build_file(self, f: IO):
506        pass
507
508    def create_filesystem_layout(self, package_dir: Path):
509        pass
510
511
512class TestTarget(Target):
513    """Class for generating a test target."""
514
515    DEVICELESS_TEST_PREREQUISITES = frozenset({
516        'adb',
517        'atest-tradefed',
518        'atest_script_help.sh',
519        'atest_tradefed.sh',
520        'tradefed',
521        'tradefed-test-framework',
522        'bazel-result-reporter'
523    })
524
525    DEVICE_TEST_PREREQUISITES = frozenset(
526        {'aapt'}).union(DEVICELESS_TEST_PREREQUISITES)
527
528    @staticmethod
529    def create_deviceless_test_target(name: str, package_name: str,
530                                      info: Dict[str, Any]):
531        return TestTarget(name, package_name, info, 'tradefed_deviceless_test',
532                          TestTarget.DEVICELESS_TEST_PREREQUISITES)
533
534    @staticmethod
535    def create_device_test_target(name: str, package_name: str,
536                                  info: Dict[str, Any]):
537        return TestTarget(name, package_name, info, 'tradefed_device_test',
538                          TestTarget.DEVICE_TEST_PREREQUISITES)
539
540    def __init__(self, name: str, package_name: str, info: Dict[str, Any],
541                 rule_name: str, prerequisites=frozenset()):
542        self._name = name
543        self._package_name = package_name
544        self._test_module_ref = ModuleRef.for_info(info)
545        self._rule_name = rule_name
546        self._prerequisites = prerequisites
547
548    def name(self) -> str:
549        return self._name
550
551    def package_name(self) -> str:
552        return self._package_name
553
554    def required_imports(self) -> Set[Import]:
555        return { Import('//bazel/rules:tradefed_test.bzl', self._rule_name) }
556
557    def dependencies(self) -> List[ModuleRef]:
558        prerequisite_refs = map(ModuleRef.for_name, self._prerequisites)
559        return [self._test_module_ref] + list(prerequisite_refs)
560
561    def write_to_build_file(self, f: IO):
562        prebuilt_target_name = self._test_module_ref.target().qualified_name()
563        writer = IndentWriter(f)
564
565        writer.write_line(f'{self._rule_name}(')
566
567        with writer.indent():
568            writer.write_line(f'name = "{self._name}",')
569            writer.write_line(f'test = "{prebuilt_target_name}",')
570
571        writer.write_line(')')
572
573
574class SoongPrebuiltTarget(Target):
575    """Class for generating a Soong prebuilt target on disk."""
576
577    @staticmethod
578    def create(gen: WorkspaceGenerator,
579               info: Dict[str, Any],
580               package_name: str=''):
581        module_name = info['module_name']
582
583        configs = [
584            Config('host', gen.host_out_path),
585            Config('device', gen.product_out_path),
586        ]
587
588        installed_paths = get_module_installed_paths(info, gen.src_root_path)
589        config_files = group_paths_by_config(configs, installed_paths)
590
591        # For test modules, we only create symbolic link to the 'testcases'
592        # directory since the information in module-info is not accurate.
593        #
594        # Note that we use is_tf_testable_module here instead of ModuleInfo
595        # class's is_testable_module method to avoid misadding a shared library
596        # as a test module.
597        # e.g.
598        # 1. test_module A has a shared_lib (or RLIB, DYLIB) of B
599        # 2. We create target B as a result of method _resolve_dependencies for
600        #    target A
601        # 3. B matches the conditions of is_testable_module:
602        #     a. B has installed path.
603        #     b. has_config return True
604        #     Note that has_config method also looks for AndroidTest.xml in the
605        #     dir of B. If there is a test module in the same dir, B could be
606        #     added as a test module.
607        # 4. We create symbolic link to the 'testcases' for non test target B
608        #    and cause errors.
609        if is_tf_testable_module(gen.mod_info, info):
610            config_files = {c: [c.out_path.joinpath(f'testcases/{module_name}')]
611                            for c in config_files.keys()}
612
613        return SoongPrebuiltTarget(
614            module_name,
615            package_name,
616            config_files,
617            find_runtime_dep_refs(gen.mod_info, info, configs,
618                                  gen.src_root_path),
619            find_data_dep_refs(gen.mod_info, info, configs,
620                                  gen.src_root_path)
621        )
622
623    def __init__(self, name: str, package_name: str,
624                 config_files: Dict[Config, List[Path]],
625                 runtime_dep_refs: List[ModuleRef],
626                 data_dep_refs: List[ModuleRef]):
627        self._name = name
628        self._package_name = package_name
629        self.config_files = config_files
630        self.runtime_dep_refs = runtime_dep_refs
631        self.data_dep_refs = data_dep_refs
632
633    def name(self) -> str:
634        return self._name
635
636    def package_name(self) -> str:
637        return self._package_name
638
639    def required_imports(self) -> Set[Import]:
640        return {
641            Import('//bazel/rules:soong_prebuilt.bzl', self._rule_name()),
642        }
643
644    @functools.lru_cache(maxsize=None)
645    def supported_configs(self) -> Set[Config]:
646        supported_configs = set(self.config_files.keys())
647
648        if supported_configs:
649            return supported_configs
650
651        # If a target has no installed files, then it supports the same
652        # configurations as its dependencies. This is required because some
653        # build modules are just intermediate targets that don't produce any
654        # output but that still have transitive dependencies.
655        for ref in self.runtime_dep_refs:
656            supported_configs.update(ref.target().supported_configs())
657
658        return supported_configs
659
660    def dependencies(self) -> List[ModuleRef]:
661        all_deps = set(self.runtime_dep_refs)
662        all_deps.update(self.data_dep_refs)
663        return list(all_deps)
664
665    def write_to_build_file(self, f: IO):
666        writer = IndentWriter(f)
667
668        writer.write_line(f'{self._rule_name()}(')
669
670        with writer.indent():
671            writer.write_line(f'name = "{self._name}",')
672            writer.write_line(f'module_name = "{self._name}",')
673            self._write_files_attribute(writer)
674            self._write_deps_attribute(writer, 'runtime_deps',
675                                       self.runtime_dep_refs)
676            self._write_deps_attribute(writer, 'data', self.data_dep_refs)
677
678        writer.write_line(')')
679
680    def create_filesystem_layout(self, package_dir: Path):
681        prebuilts_dir = package_dir.joinpath(self._name)
682        prebuilts_dir.mkdir()
683
684        for config, files in self.config_files.items():
685            config_prebuilts_dir = prebuilts_dir.joinpath(config.name)
686            config_prebuilts_dir.mkdir()
687
688            for f in files:
689                rel_path = f.relative_to(config.out_path)
690                symlink = config_prebuilts_dir.joinpath(rel_path)
691                symlink.parent.mkdir(parents=True, exist_ok=True)
692                symlink.symlink_to(f)
693
694    def _rule_name(self):
695        return ('soong_prebuilt' if self.config_files
696                else 'soong_uninstalled_prebuilt')
697
698    def _write_files_attribute(self, writer: IndentWriter):
699        if not self.config_files:
700            return
701
702        name = self._name
703
704        writer.write('files = ')
705        write_config_select(
706            writer,
707            self.config_files,
708            lambda c, _: writer.write(f'glob(["{name}/{c.name}/**/*"])'),
709        )
710        writer.write_line(',')
711
712    def _write_deps_attribute(self, writer, attribute_name, module_refs):
713        config_deps = filter_configs(
714            group_targets_by_config(r.target() for r in module_refs),
715            self.supported_configs()
716        )
717
718        if not config_deps:
719            return
720
721        for config in self.supported_configs():
722            config_deps.setdefault(config, [])
723
724        writer.write(f'{attribute_name} = ')
725        write_config_select(
726            writer,
727            config_deps,
728            lambda _, targets: write_target_list(writer, targets),
729        )
730        writer.write_line(',')
731
732
733def group_paths_by_config(
734    configs: List[Config], paths: List[Path]) -> Dict[Config, List[Path]]:
735
736    config_files = defaultdict(list)
737
738    for f in paths:
739        matching_configs = [
740            c for c in configs if _is_relative_to(f, c.out_path)]
741
742        if not matching_configs:
743            continue
744
745        # The path can only appear in ANDROID_HOST_OUT for host target or
746        # ANDROID_PRODUCT_OUT, but cannot appear in both.
747        if len(matching_configs) > 1:
748            raise Exception(f'Installed path `{f}` is not in'
749                            f' ANDROID_HOST_OUT or ANDROID_PRODUCT_OUT')
750
751        config_files[matching_configs[0]].append(f)
752
753    return config_files
754
755
756def group_targets_by_config(
757    targets: List[Target]) -> Dict[Config, List[Target]]:
758
759    config_to_targets = defaultdict(list)
760
761    for target in targets:
762        for config in target.supported_configs():
763            config_to_targets[config].append(target)
764
765    return config_to_targets
766
767
768def filter_configs(
769    config_dict: Dict[Config, Any], configs: Set[Config],) -> Dict[Config, Any]:
770    return { k: v for (k, v) in config_dict.items() if k in configs }
771
772
773def _is_relative_to(path1: Path, path2: Path) -> bool:
774    """Return True if the path is relative to another path or False."""
775    # Note that this implementation is required because Path.is_relative_to only
776    # exists starting with Python 3.9.
777    try:
778        path1.relative_to(path2)
779        return True
780    except ValueError:
781        return False
782
783
784def get_module_installed_paths(
785    info: Dict[str, Any], src_root_path: Path) -> List[Path]:
786
787    # Install paths in module-info are usually relative to the Android
788    # source root ${ANDROID_BUILD_TOP}. When the output directory is
789    # customized by the user however, the install paths are absolute.
790    def resolve(install_path_string):
791        install_path = Path(install_path_string)
792        if not install_path.expanduser().is_absolute():
793            return src_root_path.joinpath(install_path)
794        return install_path
795
796    return map(resolve, info.get(constants.MODULE_INSTALLED))
797
798
799def find_runtime_dep_refs(
800    mod_info: module_info.ModuleInfo,
801    info: module_info.Module,
802    configs: List[Config],
803    src_root_path: Path,
804) -> List[ModuleRef]:
805    """Return module references for runtime dependencies."""
806
807    # We don't use the `dependencies` module-info field for shared libraries
808    # since it's ambiguous and could generate more targets and pull in more
809    # dependencies than necessary. In particular, libraries that support both
810    # static and dynamic linking could end up becoming runtime dependencies
811    # even though the build specifies static linking. For example, if a target
812    # 'T' is statically linked to 'U' which supports both variants, the latter
813    # still appears as a dependency. Since we can't tell, this would result in
814    # the shared library variant of 'U' being added on the library path.
815    libs = set()
816    libs.update(info.get(constants.MODULE_SHARED_LIBS, []))
817    libs.update(info.get(constants.MODULE_RUNTIME_DEPS, []))
818    runtime_dep_refs = _find_module_refs(mod_info, configs, src_root_path, libs)
819
820    runtime_library_class = {'RLIB_LIBRARIES', 'DYLIB_LIBRARIES'}
821    # We collect rlibs even though they are technically static libraries since
822    # they could refer to dylibs which are required at runtime. Generating
823    # Bazel targets for these intermediate modules keeps the generator simple
824    # and preserves the shape (isomorphic) of the Soong structure making the
825    # workspace easier to debug.
826    for dep_name in info.get(constants.MODULE_DEPENDENCIES, []):
827        dep_info = mod_info.get_module_info(dep_name)
828        if not dep_info:
829            continue
830        if not runtime_library_class.intersection(
831            dep_info.get(constants.MODULE_CLASS, [])):
832            continue
833        runtime_dep_refs.append(ModuleRef.for_info(dep_info))
834
835    return runtime_dep_refs
836
837
838def find_data_dep_refs(
839    mod_info: module_info.ModuleInfo,
840    info: module_info.Module,
841    configs: List[Config],
842    src_root_path: Path,
843) -> List[ModuleRef]:
844    """Return module references for data dependencies."""
845
846    return _find_module_refs(mod_info,
847                             configs,
848                             src_root_path,
849                             info.get(constants.MODULE_DATA_DEPS, []))
850
851
852def _find_module_refs(
853    mod_info: module_info.ModuleInfo,
854    configs: List[Config],
855    src_root_path: Path,
856    module_names: List[str],
857) -> List[ModuleRef]:
858    """Return module references for modules."""
859
860    module_refs = []
861
862    for name in module_names:
863        info = mod_info.get_module_info(name)
864        if not info:
865            continue
866
867        installed_paths = get_module_installed_paths(info, src_root_path)
868        config_files = group_paths_by_config(configs, installed_paths)
869        if not config_files:
870            continue
871
872        module_refs.append(ModuleRef.for_info(info))
873
874    return module_refs
875
876
877class IndentWriter:
878
879    def __init__(self, f: IO):
880        self._file = f
881        self._indent_level = 0
882        self._indent_string = 4 * ' '
883        self._indent_next = True
884
885    def write_line(self, text: str=''):
886        if text:
887            self.write(text)
888
889        self._file.write('\n')
890        self._indent_next = True
891
892    def write(self, text):
893        if self._indent_next:
894            self._file.write(self._indent_string * self._indent_level)
895            self._indent_next = False
896
897        self._file.write(text)
898
899    @contextlib.contextmanager
900    def indent(self):
901        self._indent_level += 1
902        yield
903        self._indent_level -= 1
904
905
906def write_config_select(
907    writer: IndentWriter,
908    config_dict: Dict[Config, Any],
909    write_value_fn: Callable,
910):
911    writer.write_line('select({')
912
913    with writer.indent():
914        for config, value in sorted(
915            config_dict.items(), key=lambda c: c[0].name):
916
917            writer.write(f'"//bazel/rules:{config.name}": ')
918            write_value_fn(config, value)
919            writer.write_line(',')
920
921    writer.write('})')
922
923
924def write_target_list(writer: IndentWriter, targets: List[Target]):
925    writer.write_line('[')
926
927    with writer.indent():
928        for label in sorted(set(t.qualified_name() for t in targets)):
929            writer.write_line(f'"{label}",')
930
931    writer.write(']')
932
933
934def is_tf_testable_module(mod_info: module_info.ModuleInfo,
935                          info: Dict[str, Any]):
936    """Check if the module is a Tradefed runnable test module.
937
938    ModuleInfo.is_testable_module() is from ATest's point of view. It only
939    checks if a module has installed path and has local config files. This
940    way is not reliable since some libraries might match these two conditions
941    and be included mistakenly. Robolectric_utils is an example that matched
942    these two conditions but not testable. This function make sure the module
943    is a TF runnable test module.
944    """
945    return (mod_info.is_testable_module(info)
946            and info.get(constants.MODULE_COMPATIBILITY_SUITES))
947
948
949def _decorate_find_method(mod_info, finder_method_func, host, enabled_features):
950    """A finder_method decorator to override TestInfo properties."""
951
952    def use_bazel_runner(finder_obj, test_id):
953        test_infos = finder_method_func(finder_obj, test_id)
954        if not test_infos:
955            return test_infos
956        for tinfo in test_infos:
957            m_info = mod_info.get_module_info(tinfo.test_name)
958
959            # Only run device-driven tests in Bazel mode when '--host' is not
960            # specified and the feature is enabled.
961            if not host and mod_info.is_device_driven_test(m_info):
962                if Features.EXPERIMENTAL_DEVICE_DRIVEN_TEST in enabled_features:
963                    tinfo.test_runner = BazelTestRunner.NAME
964                continue
965
966            if mod_info.is_suite_in_compatibility_suites(
967                'host-unit-tests', m_info):
968                tinfo.test_runner = BazelTestRunner.NAME
969        return test_infos
970    return use_bazel_runner
971
972
973def create_new_finder(mod_info: module_info.ModuleInfo,
974                      finder: test_finder_base.TestFinderBase,
975                      host: bool,
976                      enabled_features: List[Features]=None):
977    """Create new test_finder_base.Finder with decorated find_method.
978
979    Args:
980      mod_info: ModuleInfo object.
981      finder: Test Finder class.
982      host: Whether to run the host variant.
983      enabled_features: List of enabled features.
984
985    Returns:
986        List of ordered find methods.
987    """
988    return test_finder_base.Finder(finder.test_finder_instance,
989                                   _decorate_find_method(
990                                       mod_info,
991                                       finder.find_method,
992                                       host,
993                                       enabled_features or []),
994                                   finder.finder_info)
995
996
997def default_run_command(args: List[str], cwd: Path) -> str:
998    return subprocess.check_output(
999        args=args,
1000        cwd=cwd,
1001        text=True,
1002        stderr=subprocess.DEVNULL,
1003    )
1004
1005
1006@dataclasses.dataclass
1007class BuildMetadata:
1008    build_branch: str
1009    build_target: str
1010
1011
1012class BazelTestRunner(trb.TestRunnerBase):
1013    """Bazel Test Runner class."""
1014
1015    NAME = 'BazelTestRunner'
1016    EXECUTABLE = 'none'
1017
1018    # pylint: disable=redefined-outer-name
1019    # pylint: disable=too-many-arguments
1020    def __init__(self,
1021                 results_dir,
1022                 mod_info: module_info.ModuleInfo,
1023                 extra_args: Dict[str, Any]=None,
1024                 test_infos: List[test_info.TestInfo]=None,
1025                 src_top: Path=None,
1026                 workspace_path: Path=None,
1027                 run_command: Callable=default_run_command,
1028                 build_metadata: BuildMetadata=None,
1029                 env: Dict[str, str]=None,
1030                 **kwargs):
1031        super().__init__(results_dir, **kwargs)
1032        self.mod_info = mod_info
1033        self.test_infos = test_infos
1034        self.src_top = src_top or Path(os.environ.get(
1035            constants.ANDROID_BUILD_TOP))
1036        self.starlark_file = self.src_top.joinpath(
1037            'tools/asuite/atest/bazel/format_as_soong_module_name.cquery')
1038
1039        self.bazel_binary = self.src_top.joinpath(
1040            'prebuilts/bazel/linux-x86_64/bazel')
1041        self.bazel_workspace = workspace_path or get_bazel_workspace_dir()
1042        self.run_command = run_command
1043        self._extra_args = extra_args or {}
1044        self.build_metadata = build_metadata or get_default_build_metadata()
1045        self.env = env or os.environ
1046
1047    # pylint: disable=unused-argument
1048    def run_tests(self, test_infos, extra_args, reporter):
1049        """Run the list of test_infos.
1050
1051        Args:
1052            test_infos: List of TestInfo.
1053            extra_args: Dict of extra args to add to test run.
1054            reporter: An instance of result_report.ResultReporter.
1055        """
1056        reporter.register_unsupported_runner(self.NAME)
1057        ret_code = ExitCode.SUCCESS
1058
1059        run_cmds = self.generate_run_commands(test_infos, extra_args)
1060        for run_cmd in run_cmds:
1061            subproc = self.run(run_cmd, output_to_stdout=True)
1062            ret_code |= self.wait_for_subprocess(subproc)
1063        return ret_code
1064
1065    def _get_bes_publish_args(self):
1066        args = []
1067
1068        if not self.env.get("ATEST_BAZEL_BES_PUBLISH_CONFIG"):
1069            return args
1070
1071        config = self.env["ATEST_BAZEL_BES_PUBLISH_CONFIG"]
1072        branch = self.build_metadata.build_branch
1073        target = self.build_metadata.build_target
1074
1075        args.append(f'--config={config}')
1076        args.append(f'--build_metadata=ab_branch={branch}')
1077        args.append(f'--build_metadata=ab_target={target}')
1078
1079        return args
1080
1081    def host_env_check(self):
1082        """Check that host env has everything we need.
1083
1084        We actually can assume the host env is fine because we have the same
1085        requirements that atest has. Update this to check for android env vars
1086        if that changes.
1087        """
1088
1089    def get_test_runner_build_reqs(self) -> Set[str]:
1090        if not self.test_infos:
1091            return set()
1092
1093        deps_expression = ' + '.join(
1094            sorted(self.test_info_target_label(i) for i in self.test_infos)
1095        )
1096
1097        query_args = [
1098            self.bazel_binary,
1099            'cquery',
1100            f'deps(tests({deps_expression}))',
1101            '--output=starlark',
1102            f'--starlark:file={self.starlark_file}',
1103        ]
1104
1105        output = self.run_command(query_args, self.bazel_workspace)
1106
1107        return set(filter(bool, map(str.strip, output.splitlines())))
1108
1109    def test_info_target_label(self, test: test_info.TestInfo) -> str:
1110        module_name = test.test_name
1111        info = self.mod_info.get_module_info(module_name)
1112        package_name = info.get(constants.MODULE_PATH)[0]
1113        target_suffix = 'host'
1114
1115        if not self._extra_args.get(
1116                constants.HOST,
1117                False) and self.mod_info.is_device_driven_test(info):
1118            target_suffix = 'device'
1119
1120        return f'//{package_name}:{module_name}_{target_suffix}'
1121
1122    # pylint: disable=unused-argument
1123    def generate_run_commands(self, test_infos, extra_args, port=None):
1124        """Generate a list of run commands from TestInfos.
1125
1126        Args:
1127            test_infos: A set of TestInfo instances.
1128            extra_args: A Dict of extra args to append.
1129            port: Optional. An int of the port number to send events to.
1130
1131        Returns:
1132            A list of run commands to run the tests.
1133        """
1134        startup_options = ''
1135        bazelrc = self.env.get('ATEST_BAZELRC')
1136
1137        if bazelrc:
1138            startup_options = f'--bazelrc={bazelrc}'
1139
1140        target_patterns = ' '.join(
1141            self.test_info_target_label(i) for i in test_infos)
1142
1143        bazel_args = self._parse_extra_args(test_infos, extra_args)
1144
1145        if Features.EXPERIMENTAL_BES_PUBLISH in extra_args.get(
1146                'BAZEL_MODE_FEATURES', []):
1147            bazel_args.extend(self._get_bes_publish_args())
1148
1149        bazel_args_str = ' '.join(bazel_args)
1150
1151        # Use 'cd' instead of setting the working directory in the subprocess
1152        # call for a working --dry-run command that users can run.
1153        return [
1154            f'cd {self.bazel_workspace} &&'
1155            f'{self.bazel_binary} {startup_options} '
1156            f'test {target_patterns} {bazel_args_str}'
1157        ]
1158
1159    def _parse_extra_args(self, test_infos: List[test_info.TestInfo],
1160                          extra_args: trb.ARGS) -> trb.ARGS:
1161        args_to_append = []
1162        # Make a copy of the `extra_args` dict to avoid modifying it for other
1163        # Atest runners.
1164        extra_args_copy = extra_args.copy()
1165
1166        # Map args to their native Bazel counterparts.
1167        for arg in _SUPPORTED_BAZEL_ARGS:
1168            if arg not in extra_args_copy:
1169                continue
1170            args_to_append.extend(
1171                self.map_to_bazel_args(arg, extra_args_copy[arg]))
1172            # Remove the argument since we already mapped it to a Bazel option
1173            # and no longer need it mapped to a Tradefed argument below.
1174            del extra_args_copy[arg]
1175
1176        # TODO(b/215461642): Store the extra_args in the top-level object so
1177        # that we don't have to re-parse the extra args to get BAZEL_ARG again.
1178        tf_args, _ = tfr.extra_args_to_tf_args(
1179            self.mod_info, test_infos, extra_args_copy)
1180
1181        # Add ATest include filter argument to allow testcase filtering.
1182        tf_args.extend(tfr.get_include_filter(test_infos))
1183
1184        args_to_append.extend([f'--test_arg={i}' for i in tf_args])
1185
1186        return args_to_append
1187
1188    @staticmethod
1189    def map_to_bazel_args(arg: str, arg_value: Any) -> List[str]:
1190        return _SUPPORTED_BAZEL_ARGS[arg](
1191            arg_value) if arg in _SUPPORTED_BAZEL_ARGS else []
1192