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