• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2023, 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"""
16Test runner for Roboleaf mode.
17
18This runner is used to run the tests that have been fully converted to Bazel.
19"""
20
21import enum
22import shlex
23import os
24import logging
25import json
26import subprocess
27
28from typing import Any, Dict, List, Set
29
30from atest import atest_utils
31from atest import constants
32from atest import bazel_mode
33from atest import result_reporter
34
35from atest.atest_enum import ExitCode
36from atest.test_finders.test_info import TestInfo
37from atest.test_runners import test_runner_base
38from atest.tools.singleton import Singleton
39
40# Roboleaf maintains allow lists that identify which modules have been
41# fully converted to bazel.  Users of atest can use
42# --roboleaf-mode=[PROD/STAGING/DEV] to filter by these allow lists.
43# PROD (default) is the only mode expected to be fully converted and passing.
44_ALLOW_LIST_PROD_PATH = ('/soong/soong_injection/allowlists/'
45                    'mixed_build_prod_allowlist.txt')
46_ALLOW_LIST_STAGING_PATH = ('/soong/soong_injection/allowlists/'
47                       'mixed_build_staging_allowlist.txt')
48_ROBOLEAF_MODULE_MAP_PATH = ('/soong/soong_injection/metrics/'
49                             'converted_modules_path_map.json')
50_ROBOLEAF_BUILD_CMD = 'build/soong/soong_ui.bash'
51
52
53@enum.unique
54class BazelBuildMode(enum.Enum):
55    "Represents different bp2build allow lists to use whening running bazel (b)"
56    OFF = 'off'
57    DEV = 'dev'
58    STAGING = 'staging'
59    PROD = 'prod'
60
61
62class RoboleafModuleMap(metaclass=Singleton):
63    """Roboleaf Module Map Singleton class."""
64
65    def __init__(self,
66                 module_map_location: str = ''):
67        self._module_map = _generate_map(module_map_location)
68        self.modules_prod = _read_allow_list(_ALLOW_LIST_PROD_PATH)
69        self.modules_staging = _read_allow_list(_ALLOW_LIST_STAGING_PATH)
70
71    def get_map(self) -> Dict[str, str]:
72        """Return converted module map.
73
74        Returns:
75            A dictionary of test names that bazel paths for eligible tests,
76            for example { "test_a": "//platform/test_a" }.
77        """
78        return self._module_map
79
80def _generate_map(module_map_location: str = '') -> Dict[str, str]:
81    """Generate converted module map.
82
83    Args:
84        module_map_location: Path of the module_map_location to check.
85
86    Returns:
87        A dictionary of test names that bazel paths for eligible tests,
88        for example { "test_a": "//platform/test_a" }.
89    """
90    if not module_map_location:
91        module_map_location = (
92            atest_utils.get_build_out_dir() + _ROBOLEAF_MODULE_MAP_PATH)
93
94    # TODO(b/274161649): It is possible it could be stale on first run.
95    # Invoking m or b test will check/recreate this file.  Bug here is
96    # to determine if we can check staleness without a large time penalty.
97    if not os.path.exists(module_map_location):
98        logging.warning('The roboleaf converted modules file: %s was not '
99                        'found.', module_map_location)
100        # Attempt to generate converted modules file.
101        try:
102            cmd = _generate_bp2build_command()
103            env_vars = os.environ.copy()
104            logging.info(
105                'Running `bp2build` to generate converted modules file.'
106                '\n%s', ' '.join(cmd))
107            subprocess.check_call(cmd,  env=env_vars)
108        except subprocess.CalledProcessError as e:
109            logging.error(e)
110            return {}
111
112    with open(module_map_location, 'r', encoding='utf8') as robo_map:
113        return json.load(robo_map)
114
115def _read_allow_list(allow_list_location: str = '') -> List[str]:
116    """Generate a list of modules based on an allow list file.
117    The expected file format is a text file that has a module name on each line.
118    Lines that start with '#' or '//' are considered comments and skipped.
119
120    Args:
121        location: Path of the allow_list file to parse.
122
123    Returns:
124        A list of module names.
125    """
126
127    allow_list_location = (
128            atest_utils.get_build_out_dir() + allow_list_location)
129
130    if not os.path.exists(allow_list_location):
131        logging.error('The roboleaf allow list file: %s was not '
132                        'found.', allow_list_location)
133        return []
134    with open(allow_list_location,  encoding='utf-8') as f:
135        allowed = []
136        for module_name in f.read().splitlines():
137            if module_name.startswith('#') or module_name.startswith('//'):
138                continue
139            allowed.append(module_name)
140        return allowed
141
142def _generate_bp2build_command() -> List[str]:
143    """Build command to run bp2build.
144
145    Returns:
146        A list of commands to run bp2build.
147    """
148    soong_ui = (
149        f'{os.environ.get(constants.ANDROID_BUILD_TOP, os.getcwd())}/'
150        f'{_ROBOLEAF_BUILD_CMD}')
151    return [soong_ui, '--make-mode', 'bp2build']
152
153
154class AbortRunException(Exception):
155    """Roboleaf Abort Run Exception Class."""
156
157
158class RoboleafTestRunner(test_runner_base.TestRunnerBase):
159    """Roboleaf Test Runner class."""
160    NAME = 'RoboleafTestRunner'
161    EXECUTABLE = 'b'
162
163    # pylint: disable=unused-argument
164    def generate_run_commands(self,
165                              test_infos: Set[Any],
166                              extra_args: Dict[str, Any],
167                              port: int = None) -> List[str]:
168        """Generate a list of run commands from TestInfos.
169
170        Args:
171            test_infos: A set of TestInfo instances.
172            extra_args: A Dict of extra args to append.
173            port: Optional. An int of the port number to send events to.
174
175        Returns:
176            A list of run commands to run the tests.
177        """
178        target_patterns = ' '.join(
179            self.test_info_target_label(i) for i in test_infos)
180        bazel_args = bazel_mode.parse_args(test_infos, extra_args, None)
181        bazel_args.append('--config=android')
182        bazel_args.append(
183            '--//build/bazel/rules/tradefed:runmode=host_driven_test'
184        )
185        bazel_args_str = ' '.join(shlex.quote(arg) for arg in bazel_args)
186        command = f'{self.EXECUTABLE} test {target_patterns} {bazel_args_str}'
187        results = [command]
188        logging.info("Roboleaf test runner command:\n"
189                     "\n".join(results))
190        return results
191
192    def test_info_target_label(self, test: TestInfo) -> str:
193        """ Get bazel path of test
194
195        Args:
196            test: An object of TestInfo.
197
198        Returns:
199            The bazel path of the test.
200        """
201        module_map = RoboleafModuleMap().get_map()
202        return f'{module_map[test.test_name]}:{test.test_name}'
203
204    def run_tests(self,
205                  test_infos: List[TestInfo],
206                  extra_args: Dict[str, Any],
207                  reporter: result_reporter.ResultReporter) -> int:
208        """Run the list of test_infos.
209
210        Args:
211            test_infos: List of TestInfo.
212            extra_args: Dict of extra args to add to test run.
213            reporter: An instance of result_reporter.ResultReporter.
214        """
215        reporter.register_unsupported_runner(self.NAME)
216        ret_code = ExitCode.SUCCESS
217        try:
218            run_cmds = self.generate_run_commands(test_infos, extra_args)
219        except AbortRunException as e:
220            atest_utils.colorful_print(f'Stop running test(s): {e}',
221                                       constants.RED)
222            return ExitCode.ERROR
223        for run_cmd in run_cmds:
224            subproc = self.run(run_cmd, output_to_stdout=True)
225            ret_code |= self.wait_for_subprocess(subproc)
226        return ret_code
227
228    def get_test_runner_build_reqs(
229        self,
230        test_infos: List[TestInfo]) -> Set[str]:
231        return set()
232
233    def host_env_check(self) -> None:
234        """Check that host env has everything we need.
235
236        We actually can assume the host env is fine because we have the same
237        requirements that atest has. Update this to check for android env vars
238        if that changes.
239        """
240
241    def roboleaf_eligible_tests(
242        self,
243        mode: BazelBuildMode,
244        module_names: List[str]) -> Dict[str, TestInfo]:
245        """Filter the given module_names to only ones that are currently
246        fully converted with roboleaf (b test) and then filter further by the
247        given allow list specified in BazelBuildMode.
248
249        Args:
250            mode: A BazelBuildMode value to filter by allow list.
251            module_names: A list of module names to check for roboleaf support.
252
253        Returns:
254            A dictionary keyed by test name and value of Roboleaf TestInfo.
255        """
256        if not module_names:
257            return {}
258
259        mod_map = RoboleafModuleMap()
260        supported_modules = set(filter(
261            lambda m: m in mod_map.get_map(), module_names))
262
263
264        if mode == BazelBuildMode.PROD:
265            supported_modules = set(filter(
266            lambda m: m in supported_modules, mod_map.modules_prod))
267        elif mode == BazelBuildMode.STAGING:
268            supported_modules = set(filter(
269            lambda m: m in supported_modules, mod_map.modules_staging))
270
271        return {
272            module: TestInfo(module, RoboleafTestRunner.NAME, set())
273            for module in supported_modules
274        }
275