• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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