blob: 5bca68b343f819b32b49959b1b0059d6d1fa4e57 [file] [log] [blame]
# Copyright 2020 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Methods related to querying builder information from Buildbucket."""
from __future__ import print_function
import json
import logging
import os
import subprocess
from typing import Any, Dict, Iterable, List, Optional, Set, Tuple
import six
from unexpected_passes_common import constants
from unexpected_passes_common import data_types
from unexpected_passes_common import multiprocessing_utils
TESTING_BUILDBOT_DIR = os.path.realpath(
os.path.join(os.path.dirname(__file__), '..', 'buildbot'))
INTERNAL_TESTING_BUILDBOT_DIR = os.path.realpath(
os.path.join(constants.SRC_INTERNAL_DIR, 'testing', 'buildbot'))
# Public JSON files for internal builders, which should be treated as internal.
PUBLIC_INTERNAL_JSON_FILES = {
'chrome.json',
'chrome.gpu.fyi.json',
'internal.chrome.fyi.json',
'internal.chromeos.fyi.json',
}
AUTOGENERATED_JSON_KEY = 'AAAAA1 AUTOGENERATED FILE DO NOT EDIT'
FakeBuildersDict = Dict[data_types.BuilderEntry, Set[data_types.BuilderEntry]]
_registered_instance = None
def GetInstance() -> 'Builders':
return _registered_instance
def RegisterInstance(instance: 'Builders') -> None:
global _registered_instance
assert _registered_instance is None
assert isinstance(instance, Builders)
_registered_instance = instance
def ClearInstance() -> None:
global _registered_instance
_registered_instance = None
class Builders():
def __init__(self, suite: Optional[str], include_internal_builders: bool):
"""
Args:
suite: A string containing particular suite of interest if applicable,
such as for Telemetry-based tests. Can be None if not applicable.
include_internal_builders: A boolean indicating whether data from
internal builders should be used in addition to external ones.
"""
self._authenticated = False
self._suite = suite
self._include_internal_builders = include_internal_builders
def _ProcessJsonFiles(self, files: List[str], are_internal_files: bool,
builder_type: str) -> Set[data_types.BuilderEntry]:
builders = set()
for filepath in files:
if not filepath.endswith('.json'):
continue
if builder_type == constants.BuilderTypes.CI:
if 'tryserver' in filepath:
continue
elif builder_type == constants.BuilderTypes.TRY:
if 'tryserver' not in filepath:
continue
with open(filepath, encoding='utf-8') as f:
buildbot_json = json.load(f)
# Skip any JSON files that don't contain builder information.
if AUTOGENERATED_JSON_KEY not in buildbot_json:
continue
for builder, test_map in buildbot_json.items():
# Remove the auto-generated comments.
if 'AAAA' in builder:
continue
# Filter out any builders that don't run the suite in question.
if not self._BuilderRunsTestOfInterest(test_map):
continue
builders.add(
data_types.BuilderEntry(builder, builder_type, are_internal_files))
return builders
def GetCiBuilders(self) -> Set[data_types.BuilderEntry]:
"""Gets the set of CI builders to query.
Returns:
A set of data_types.BuilderEntry, each element corresponding to either a
public or internal CI builder to query results from.
"""
ci_builders = set()
logging.info('Getting CI builders')
ci_builders = self._ProcessJsonFiles(_GetPublicJsonFiles(), False,
constants.BuilderTypes.CI)
if self._include_internal_builders:
ci_builders |= self._ProcessJsonFiles(_GetInternalJsonFiles(), True,
constants.BuilderTypes.CI)
logging.debug('Got %d CI builders after trimming: %s', len(ci_builders),
', '.join([b.name for b in ci_builders]))
return ci_builders
def _BuilderRunsTestOfInterest(self, test_map: Dict[str, Any]) -> bool:
"""Determines if a builder runs a test of interest.
Args:
test_map: A dict, corresponding to a builder's test spec from a
//testing/buildbot JSON file.
suite: A string containing particular suite of interest if applicable,
such as for Telemetry-based tests. Can be None if not applicable.
Returns:
True if |test_map| contains a test of interest, else False.
"""
raise NotImplementedError()
def GetTryBuilders(self, ci_builders: Iterable[data_types.BuilderEntry]
) -> Set[data_types.BuilderEntry]:
"""Gets the set of try builders to query.
A try builder is of interest if it mirrors a builder in |ci_builders| or is
a dedicated try builder.
Args:
ci_builders: An iterable of data_types.BuilderEntry, each element being a
public or internal CI builder that results will be/were queried from.
Returns:
A set of data_types.BuilderEntry, each element being the name of a
Chromium try builder to query results from.
"""
logging.info('Getting try builders')
dedicated_try_builders = self._ProcessJsonFiles([
os.path.join(TESTING_BUILDBOT_DIR, f)
for f in os.listdir(TESTING_BUILDBOT_DIR)
], False, constants.BuilderTypes.TRY)
if self._include_internal_builders:
dedicated_try_builders |= self._ProcessJsonFiles([
os.path.join(INTERNAL_TESTING_BUILDBOT_DIR, f)
for f in os.listdir(INTERNAL_TESTING_BUILDBOT_DIR)
], True, constants.BuilderTypes.TRY)
mirrored_builders = set()
no_output_builders = set()
with multiprocessing_utils.GetProcessPoolContext() as pool:
results = pool.map(self._GetMirroredBuildersForCiBuilder, ci_builders)
for (builders, found_mirror) in results:
if found_mirror:
mirrored_builders |= builders
else:
no_output_builders |= builders
if no_output_builders:
raise RuntimeError(
'Did not get Buildbucket output for the following builders. They may '
'need to be added to the GetFakeCiBuilders or '
'GetNonChromiumBuilders .\n%s' %
'\n'.join([b.name for b in no_output_builders]))
logging.debug('Got %d try builders: %s', len(mirrored_builders),
mirrored_builders)
return dedicated_try_builders | mirrored_builders
def _GetMirroredBuildersForCiBuilder(
self, ci_builder: data_types.BuilderEntry
) -> Tuple[Set[data_types.BuilderEntry], bool]:
"""Gets the set of try builders that mirror a CI builder.
Args:
ci_builder: A data_types.BuilderEntry for a public or internal CI builder.
Returns:
A tuple (builders, found_mirror). |builders| is a set of
data_types.BuilderEntry, either the set of try builders that mirror
|ci_builder| or |ci_builder|, depending on the value of |found_mirror|.
|found_mirror| is True if mirrors were actually found, in which case
|builders| contains the try builders. Otherwise, |found_mirror| is False
and |builders| contains |ci_builder|.
"""
mirrored_builders = set()
if ci_builder in self.GetNonChromiumBuilders():
logging.debug('%s is a non-Chromium CI builder', ci_builder.name)
return mirrored_builders, True
fake_builders = self.GetFakeCiBuilders()
if ci_builder in fake_builders:
mirrored_builders |= fake_builders[ci_builder]
logging.debug('%s is a fake CI builder mirrored by %s', ci_builder.name,
', '.join(b.name for b in fake_builders[ci_builder]))
return mirrored_builders, True
bb_output = self._GetBuildbucketOutputForCiBuilder(ci_builder)
if not bb_output:
mirrored_builders.add(ci_builder)
logging.debug('Did not get Buildbucket output for builder %s',
ci_builder.name)
return mirrored_builders, False
bb_json = json.loads(bb_output)
mirrored = bb_json.get('output', {}).get('properties',
{}).get('mirrored_builders', [])
# The mirror names from Buildbucket include the group separated by :, e.g.
# tryserver.chromium.android:gpu-fyi-try-android-m-nexus-5x-64, so only grab
# the builder name.
for mirror in mirrored:
split = mirror.split(':')
assert len(split) == 2
logging.debug('Got mirrored builder for %s: %s', ci_builder.name,
split[1])
mirrored_builders.add(
data_types.BuilderEntry(split[1], constants.BuilderTypes.TRY,
ci_builder.is_internal_builder))
return mirrored_builders, True
def _GetBuildbucketOutputForCiBuilder(self,
ci_builder: data_types.BuilderEntry
) -> str:
# Ensure the user is logged in to bb.
if not self._authenticated:
try:
with open(os.devnull, 'w', newline='', encoding='utf-8') as devnull:
subprocess.check_call(['bb', 'auth-info'],
stdout=devnull,
stderr=devnull)
except subprocess.CalledProcessError as e:
six.raise_from(
RuntimeError('You are not logged into bb - run `bb auth-login`.'),
e)
self._authenticated = True
# Split out for ease of testing.
# Get the Buildbucket ID for the most recent completed build for a builder.
p = subprocess.Popen([
'bb',
'ls',
'-id',
'-1',
'-status',
'ended',
'%s/ci/%s' % (ci_builder.project, ci_builder.name),
],
stdout=subprocess.PIPE)
# Use the ID to get the most recent build.
bb_output = subprocess.check_output([
'bb',
'get',
'-A',
'-json',
],
stdin=p.stdout,
text=True)
return bb_output
def GetIsolateNames(self) -> Set[str]:
"""Gets the isolate names that are relevant to this implementation.
Returns:
A set of strings, each element being the name of an isolate of interest.
"""
raise NotImplementedError()
def GetFakeCiBuilders(self) -> FakeBuildersDict:
"""Gets a mapping of fake CI builders to their mirrored trybots.
Returns:
A dict of data_types.BuilderEntry -> set(data_types.BuilderEntry). Each
key is a CI builder that doesn't actually exist and each value is a set of
try builders that mirror the CI builder but do exist.
"""
raise NotImplementedError()
def GetNonChromiumBuilders(self) -> Set[data_types.BuilderEntry]:
"""Gets the builders that are not actual Chromium builders.
These are listed in the Chromium //testing/buildbot files, but aren't under
the Chromium Buildbucket project. These don't use the same recipes as
Chromium builders, and thus don't have the list of trybot mirrors.
Returns:
A set of data_types.BuilderEntry, each element being a non-Chromium
builder.
"""
raise NotImplementedError()
def _GetPublicJsonFiles() -> List[str]:
return [
os.path.join(TESTING_BUILDBOT_DIR, f)
for f in os.listdir(TESTING_BUILDBOT_DIR)
if f not in PUBLIC_INTERNAL_JSON_FILES
]
def _GetInternalJsonFiles() -> List[str]:
internal_files = [
os.path.join(INTERNAL_TESTING_BUILDBOT_DIR, f)
for f in os.listdir(INTERNAL_TESTING_BUILDBOT_DIR)
]
public_internal_files = [
os.path.join(TESTING_BUILDBOT_DIR, f)
for f in os.listdir(TESTING_BUILDBOT_DIR)
if f in PUBLIC_INTERNAL_JSON_FILES
]
return internal_files + public_internal_files