1# Copyright 2020 The Chromium Authors 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4"""Methods related to querying builder information from Buildbucket.""" 5 6from __future__ import print_function 7 8import concurrent.futures 9import json 10import logging 11import os 12import subprocess 13from typing import Any, Dict, Iterable, List, Optional, Set, Tuple 14 15import six 16 17from unexpected_passes_common import constants 18from unexpected_passes_common import data_types 19 20TESTING_BUILDBOT_DIR = os.path.realpath( 21 os.path.join(constants.CHROMIUM_SRC_DIR, 'testing', 'buildbot')) 22INTERNAL_TESTING_BUILDBOT_DIR = os.path.realpath( 23 os.path.join(constants.SRC_INTERNAL_DIR, 'testing', 'buildbot')) 24INFRA_CONFIG_BUILDERS_DIR = os.path.realpath( 25 os.path.join(constants.CHROMIUM_SRC_DIR, 'infra', 'config', 'generated', 26 'builders')) 27INTERNAL_INFRA_CONFIG_BUILDERS_DIR = os.path.realpath( 28 os.path.join(constants.SRC_INTERNAL_DIR, 'infra', 'config', 'generated', 29 'builders')) 30 31# Public JSON files for internal builders, which should be treated as internal. 32PUBLIC_INTERNAL_JSON_FILES = { 33 'chrome.json', 34 'chrome.gpu.fyi.json', 35 'chromeos.preuprev.json', 36 'internal.chrome.fyi.json', 37 'internal.chromeos.fyi.json', 38} 39 40AUTOGENERATED_JSON_KEY = 'AAAAA1 AUTOGENERATED FILE DO NOT EDIT' 41 42FakeBuildersDict = Dict[data_types.BuilderEntry, Set[data_types.BuilderEntry]] 43 44# TODO(crbug.com/358591565): Refactor this to remove the need for global 45# statements. 46_registered_instance = None 47 48 49def GetInstance() -> 'Builders': 50 return _registered_instance 51 52 53def RegisterInstance(instance: 'Builders') -> None: 54 global _registered_instance # pylint: disable=global-statement 55 assert _registered_instance is None 56 assert isinstance(instance, Builders) 57 _registered_instance = instance 58 59 60def ClearInstance() -> None: 61 global _registered_instance # pylint: disable=global-statement 62 _registered_instance = None 63 64 65class Builders(): 66 def __init__(self, suite: Optional[str], include_internal_builders: bool): 67 """ 68 Args: 69 suite: A string containing particular suite of interest if applicable, 70 such as for Telemetry-based tests. Can be None if not applicable. 71 include_internal_builders: A boolean indicating whether data from 72 internal builders should be used in addition to external ones. 73 """ 74 self._authenticated = False 75 self._suite = suite 76 self._include_internal_builders = include_internal_builders 77 78 def _ProcessTestingBuildbotJsonFiles( 79 self, files: List[str], are_internal_files: bool, 80 builder_type: str) -> Set[data_types.BuilderEntry]: 81 builders = set() 82 for filepath in files: 83 if not filepath.endswith('.json'): 84 continue 85 if builder_type == constants.BuilderTypes.CI: 86 if 'tryserver' in filepath: 87 continue 88 elif builder_type == constants.BuilderTypes.TRY: 89 if 'tryserver' not in filepath: 90 continue 91 with open(filepath, encoding='utf-8') as f: 92 buildbot_json = json.load(f) 93 # Skip any JSON files that don't contain builder information. 94 if AUTOGENERATED_JSON_KEY not in buildbot_json: 95 continue 96 97 for builder, test_map in buildbot_json.items(): 98 # Remove the auto-generated comments. 99 if 'AAAA' in builder: 100 continue 101 # Filter out any builders that don't run the suite in question. 102 if not self._BuilderRunsTestOfInterest(test_map): 103 continue 104 builders.add( 105 data_types.BuilderEntry(builder, builder_type, are_internal_files)) 106 return builders 107 108 def _ProcessInfraConfigJsonFiles( 109 self, files: List[Tuple[str, str]], are_internal_files: bool, 110 builder_type: str) -> Set[data_types.BuilderEntry]: 111 builders = set() 112 for builder_name, filepath in files: 113 if not filepath.endswith('.json'): 114 raise RuntimeError(f'Given path {filepath} was not a JSON file') 115 with open(filepath, encoding='utf-8') as f: 116 targets_json = json.load(f) 117 118 # For CI builders, we can directly use the builder name from the JSON 119 # file, as this will always be a valid CI builder name. Additionally, this 120 # properly handles cases of a parent builder triggering a child tester - 121 # the parent builder's JSON contains the names of the child testers. 122 # For trybots, we want to instead use the builder name from the filepath. 123 # This is because trybots that mirror CI builders contain the CI builder 124 # names in the JSON, but we want the trybot name. 125 for ci_builder_name, test_map in targets_json.items(): 126 if not self._BuilderRunsTestOfInterest(test_map): 127 continue 128 if builder_type == constants.BuilderTypes.CI: 129 builders.add( 130 data_types.BuilderEntry(ci_builder_name, builder_type, 131 are_internal_files)) 132 else: 133 builders.add( 134 data_types.BuilderEntry(builder_name, builder_type, 135 are_internal_files)) 136 return builders 137 138 def GetCiBuilders(self) -> Set[data_types.BuilderEntry]: 139 """Gets the set of CI builders to query. 140 141 Returns: 142 A set of data_types.BuilderEntry, each element corresponding to either a 143 public or internal CI builder to query results from. 144 """ 145 ci_builders = set() 146 147 logging.info('Getting CI builders') 148 ci_builders = self._ProcessTestingBuildbotJsonFiles( 149 _GetPublicTestingBuildbotJsonFiles(), False, constants.BuilderTypes.CI) 150 ci_builders |= self._ProcessInfraConfigJsonFiles( 151 _GetPublicInfraConfigCiJsonFiles(), False, constants.BuilderTypes.CI) 152 if self._include_internal_builders: 153 ci_builders |= self._ProcessTestingBuildbotJsonFiles( 154 _GetInternalTestingBuildbotJsonFiles(), True, 155 constants.BuilderTypes.CI) 156 ci_builders |= self._ProcessInfraConfigJsonFiles( 157 _GetInternalInfraConfigCiJsonFiles(), True, constants.BuilderTypes.CI) 158 159 logging.debug('Got %d CI builders after trimming: %s', len(ci_builders), 160 ', '.join([b.name for b in ci_builders])) 161 return ci_builders 162 163 def _BuilderRunsTestOfInterest(self, test_map: Dict[str, Any]) -> bool: 164 """Determines if a builder runs a test of interest. 165 166 Args: 167 test_map: A dict, corresponding to a builder's test spec from a 168 //testing/buildbot JSON file. 169 suite: A string containing particular suite of interest if applicable, 170 such as for Telemetry-based tests. Can be None if not applicable. 171 172 Returns: 173 True if |test_map| contains a test of interest, else False. 174 """ 175 raise NotImplementedError() 176 177 def GetTryBuilders(self, ci_builders: Iterable[data_types.BuilderEntry] 178 ) -> Set[data_types.BuilderEntry]: 179 """Gets the set of try builders to query. 180 181 A try builder is of interest if it mirrors a builder in |ci_builders| or is 182 a dedicated try builder. 183 184 Args: 185 ci_builders: An iterable of data_types.BuilderEntry, each element being a 186 public or internal CI builder that results will be/were queried from. 187 188 Returns: 189 A set of data_types.BuilderEntry, each element being the name of a 190 Chromium try builder to query results from. 191 """ 192 logging.info('Getting try builders') 193 dedicated_try_builders = self._ProcessTestingBuildbotJsonFiles([ 194 os.path.join(TESTING_BUILDBOT_DIR, f) 195 for f in os.listdir(TESTING_BUILDBOT_DIR) 196 ], False, constants.BuilderTypes.TRY) 197 dedicated_try_builders |= self._ProcessInfraConfigJsonFiles( 198 _GetPublicInfraConfigTryJsonFiles(), False, constants.BuilderTypes.TRY) 199 if self._include_internal_builders: 200 dedicated_try_builders |= self._ProcessTestingBuildbotJsonFiles([ 201 os.path.join(INTERNAL_TESTING_BUILDBOT_DIR, f) 202 for f in os.listdir(INTERNAL_TESTING_BUILDBOT_DIR) 203 ], True, constants.BuilderTypes.TRY) 204 dedicated_try_builders |= self._ProcessInfraConfigJsonFiles( 205 _GetInternalInfraConfigTryJsonFiles(), True, 206 constants.BuilderTypes.TRY) 207 mirrored_builders = set() 208 no_output_builders = set() 209 210 with concurrent.futures.ThreadPoolExecutor( 211 max_workers=os.cpu_count()) as pool: 212 results_iter = pool.map(self._GetMirroredBuildersForCiBuilder, 213 ci_builders) 214 for (builders, found_mirror) in results_iter: 215 if found_mirror: 216 mirrored_builders |= builders 217 else: 218 no_output_builders |= builders 219 220 if no_output_builders: 221 raise RuntimeError( 222 'Did not get Buildbucket output for the following builders. They may ' 223 'need to be added to the GetFakeCiBuilders or ' 224 'GetNonChromiumBuilders .\n%s' % 225 '\n'.join([b.name for b in no_output_builders])) 226 logging.debug('Got %d try builders: %s', len(mirrored_builders), 227 mirrored_builders) 228 return dedicated_try_builders | mirrored_builders 229 230 def _GetMirroredBuildersForCiBuilder( 231 self, ci_builder: data_types.BuilderEntry 232 ) -> Tuple[Set[data_types.BuilderEntry], bool]: 233 """Gets the set of try builders that mirror a CI builder. 234 235 Args: 236 ci_builder: A data_types.BuilderEntry for a public or internal CI builder. 237 238 Returns: 239 A tuple (builders, found_mirror). |builders| is a set of 240 data_types.BuilderEntry, either the set of try builders that mirror 241 |ci_builder| or |ci_builder|, depending on the value of |found_mirror|. 242 |found_mirror| is True if mirrors were actually found, in which case 243 |builders| contains the try builders. Otherwise, |found_mirror| is False 244 and |builders| contains |ci_builder|. 245 """ 246 mirrored_builders = set() 247 if ci_builder in self.GetNonChromiumBuilders(): 248 logging.debug('%s is a non-Chromium CI builder', ci_builder.name) 249 return mirrored_builders, True 250 251 fake_builders = self.GetFakeCiBuilders() 252 if ci_builder in fake_builders: 253 mirrored_builders |= fake_builders[ci_builder] 254 logging.debug('%s is a fake CI builder mirrored by %s', ci_builder.name, 255 ', '.join(b.name for b in fake_builders[ci_builder])) 256 return mirrored_builders, True 257 258 bb_output = self._GetBuildbucketOutputForCiBuilder(ci_builder) 259 if not bb_output: 260 mirrored_builders.add(ci_builder) 261 logging.debug('Did not get Buildbucket output for builder %s', 262 ci_builder.name) 263 return mirrored_builders, False 264 265 bb_json = json.loads(bb_output) 266 mirrored = bb_json.get('output', {}).get('properties', 267 {}).get('mirrored_builders', []) 268 # The mirror names from Buildbucket include the group separated by :, e.g. 269 # tryserver.chromium.android:gpu-fyi-try-android-m-nexus-5x-64, so only grab 270 # the builder name. 271 for mirror in mirrored: 272 split = mirror.split(':') 273 assert len(split) == 2 274 logging.debug('Got mirrored builder for %s: %s', ci_builder.name, 275 split[1]) 276 mirrored_builders.add( 277 data_types.BuilderEntry(split[1], constants.BuilderTypes.TRY, 278 ci_builder.is_internal_builder)) 279 return mirrored_builders, True 280 281 def _GetBuildbucketOutputForCiBuilder(self, 282 ci_builder: data_types.BuilderEntry 283 ) -> str: 284 # Ensure the user is logged in to bb. 285 if not self._authenticated: 286 try: 287 with open(os.devnull, 'w', newline='', encoding='utf-8') as devnull: 288 subprocess.check_call(['bb', 'auth-info'], 289 stdout=devnull, 290 stderr=devnull) 291 except subprocess.CalledProcessError as e: 292 six.raise_from( 293 RuntimeError('You are not logged into bb - run `bb auth-login`.'), 294 e) 295 self._authenticated = True 296 # Split out for ease of testing. 297 # Get the Buildbucket ID for the most recent completed build for a builder. 298 p = subprocess.Popen([ 299 'bb', 300 'ls', 301 '-id', 302 '-1', 303 '-status', 304 'ended', 305 '%s/ci/%s' % (ci_builder.project, ci_builder.name), 306 ], 307 stdout=subprocess.PIPE) 308 # Use the ID to get the most recent build. 309 bb_output = subprocess.check_output([ 310 'bb', 311 'get', 312 '-A', 313 '-json', 314 ], 315 stdin=p.stdout, 316 text=True) 317 return bb_output 318 319 def GetIsolateNames(self) -> Set[str]: 320 """Gets the isolate names that are relevant to this implementation. 321 322 Returns: 323 A set of strings, each element being the name of an isolate of interest. 324 """ 325 raise NotImplementedError() 326 327 def GetFakeCiBuilders(self) -> FakeBuildersDict: 328 """Gets a mapping of fake CI builders to their mirrored trybots. 329 330 Returns: 331 A dict of data_types.BuilderEntry -> set(data_types.BuilderEntry). Each 332 key is a CI builder that doesn't actually exist and each value is a set of 333 try builders that mirror the CI builder but do exist. 334 """ 335 raise NotImplementedError() 336 337 def GetNonChromiumBuilders(self) -> Set[data_types.BuilderEntry]: 338 """Gets the builders that are not actual Chromium builders. 339 340 These are listed in the Chromium //testing/buildbot files, but aren't under 341 the Chromium Buildbucket project. These don't use the same recipes as 342 Chromium builders, and thus don't have the list of trybot mirrors. 343 344 Returns: 345 A set of data_types.BuilderEntry, each element being a non-Chromium 346 builder. 347 """ 348 raise NotImplementedError() 349 350 351def _GetPublicTestingBuildbotJsonFiles() -> List[str]: 352 return [ 353 os.path.join(TESTING_BUILDBOT_DIR, f) 354 for f in os.listdir(TESTING_BUILDBOT_DIR) 355 if f not in PUBLIC_INTERNAL_JSON_FILES 356 ] 357 358 359def _GetInternalTestingBuildbotJsonFiles() -> List[str]: 360 internal_files = [ 361 os.path.join(INTERNAL_TESTING_BUILDBOT_DIR, f) 362 for f in os.listdir(INTERNAL_TESTING_BUILDBOT_DIR) 363 ] 364 public_internal_files = [ 365 os.path.join(TESTING_BUILDBOT_DIR, f) 366 for f in os.listdir(TESTING_BUILDBOT_DIR) 367 if f in PUBLIC_INTERNAL_JSON_FILES 368 ] 369 return internal_files + public_internal_files 370 371 372def _GetPublicInfraConfigCiJsonFiles() -> List[Tuple[str, str]]: 373 return _GetInfraConfigJsonFiles(INFRA_CONFIG_BUILDERS_DIR, 'ci') 374 375 376def _GetInternalInfraConfigCiJsonFiles() -> List[Tuple[str, str]]: 377 return _GetInfraConfigJsonFiles(INTERNAL_INFRA_CONFIG_BUILDERS_DIR, 'ci') 378 379 380def _GetPublicInfraConfigTryJsonFiles() -> List[Tuple[str, str]]: 381 return _GetInfraConfigJsonFiles(INFRA_CONFIG_BUILDERS_DIR, 'try') 382 383 384def _GetInternalInfraConfigTryJsonFiles() -> List[Tuple[str, str]]: 385 return _GetInfraConfigJsonFiles(INTERNAL_INFRA_CONFIG_BUILDERS_DIR, 'try') 386 387 388def _GetInfraConfigJsonFiles(builders_dir: str, 389 subdirectory: str) -> List[Tuple[str, str]]: 390 """Gets the relevant //infra/config JSON files. 391 392 Args: 393 builders_dir: The generated builders directory to look in, mainly for 394 specifying whether to look for public or internal files. 395 subdirectory: The subdirectory in |builders_dir| to look in, mainly for 396 specifying whether to look for CI or try builders. 397 398 Returns: 399 A list of tuples (builder_name, filepath). |builder_name| is the name of the 400 builder that was found, while |filepath| is the path to a generated JSON 401 file. 402 """ 403 json_files = [] 404 group_path = os.path.join(builders_dir, subdirectory) 405 for builder_name in os.listdir(group_path): 406 target_dir = os.path.join(group_path, builder_name, 'targets') 407 if not os.path.exists(target_dir): 408 continue 409 for target_file in os.listdir(target_dir): 410 if not target_file.endswith('.json'): 411 continue 412 json_files.append((builder_name, os.path.join(target_dir, target_file))) 413 414 return json_files 415