blob: a15cc63271729dc02005a664aad991c4ebe15515 [file] [log] [blame]
# Copyright 2020 The Chromium Authors. All rights reserved.
# 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 unexpected_passes_common import multiprocessing_utils
TESTING_BUILDBOT_DIR = os.path.realpath(
os.path.join(os.path.dirname(__file__), '..', 'buildbot'))
AUTOGENERATED_JSON_KEY = 'AAAAA1 AUTOGENERATED FILE DO NOT EDIT'
_registered_instance = None
def GetInstance():
return _registered_instance
def RegisterInstance(instance):
global _registered_instance
assert _registered_instance is None
assert isinstance(instance, Builders)
_registered_instance = instance
def ClearInstance():
global _registered_instance
_registered_instance = None
class Builders(object):
def __init__(self):
self._authenticated = False
def GetCiBuilders(self, suite):
"""Gets the set of CI builders to query.
Args:
suite: A string containing particular suite of interest if applicable,
such as for Telemetry-based tests. Can be None if not applicable.
Returns:
A set of strings, each element being the name of a Chromium CI builder to
query results from.
"""
logging.info('Getting CI builders')
ci_builders = set()
for buildbot_file in os.listdir(TESTING_BUILDBOT_DIR):
if not buildbot_file.endswith('.json'):
continue
filepath = os.path.join(TESTING_BUILDBOT_DIR, buildbot_file)
with open(filepath) 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 compile-only builders and the auto-generated comments.
if 'Builder' in builder or 'AAAA' in builder:
continue
# Filter out any builders that don't run the suite in question.
if not self._BuilderRunsTestOfInterest(test_map, suite):
continue
ci_builders.add(builder)
logging.debug('Got %d CI builders after trimming: %s', len(ci_builders),
ci_builders)
return ci_builders
def _BuilderRunsTestOfInterest(self, test_map, suite):
"""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):
"""Gets the set of try builders to query.
A try builder is of interest if it mirrors a builder in |ci_builders|.
Args:
ci_builders: An iterable of strings, each element being the name of a
Chromium CI builder that results will be/were queried from.
Returns:
A set of strings, each element being the name of a Chromium try builder to
query results from.
"""
logging.info('Getting try builders')
mirrored_builders = set()
no_output_builders = set()
pool = multiprocessing_utils.GetProcessPool()
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(no_output_builders))
logging.debug('Got %d try builders: %s', len(mirrored_builders),
mirrored_builders)
return mirrored_builders
def _GetMirroredBuildersForCiBuilder(self, ci_builder):
"""Gets the set of try builders that mirror a CI builder.
Args:
ci_builder: A string containing the name of a Chromium CI builder.
Returns:
A tuple (builders, found_mirror). |builders| is a set of strings, 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)
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,
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)
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, split[1])
mirrored_builders.add(split[1])
return mirrored_builders, True
def _GetBuildbucketOutputForCiBuilder(self, ci_builder):
# Ensure the user is logged in to bb.
if not self._authenticated:
try:
with open(os.devnull, 'w') as devnull:
subprocess.check_call(['bb', 'auth-info'],
stdout=devnull,
stderr=devnull)
except:
raise RuntimeError('You are not logged into bb - run `bb auth-login`.')
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',
'chromium/ci/%s' % ci_builder,
],
stdout=subprocess.PIPE)
# Use the ID to get the most recent build.
bb_output = subprocess.check_output([
'bb',
'get',
'-A',
'-json',
],
stdin=p.stdout)
return bb_output
def GetIsolateNames(self):
"""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):
"""Gets a mapping of fake CI builders to their mirrored trybots.
Returns:
A dict of string -> set(string). 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):
"""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 strings, each element being the name of a non-Chromium builder.
"""
raise NotImplementedError()