• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# Copyright 2019 The Chromium OS Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7"""Runs a tryjob/tryjobs after updating the packages."""
8
9from __future__ import print_function
10
11import argparse
12import datetime
13import json
14import os
15import subprocess
16
17import chroot
18import failure_modes
19import get_llvm_hash
20import update_chromeos_llvm_hash
21
22VALID_CQ_TRYBOTS = ['llvm', 'llvm-next', 'llvm-tot']
23
24
25def GetCommandLineArgs():
26  """Parses the command line for the command line arguments.
27
28  Returns:
29    The log level to use when retrieving the LLVM hash or google3 LLVM version,
30    the chroot path to use for executing chroot commands,
31    a list of a package or packages to update their LLVM next hash,
32    and the LLVM version to use when retrieving the LLVM hash.
33  """
34
35  # Default path to the chroot if a path is not specified.
36  cros_root = os.path.expanduser('~')
37  cros_root = os.path.join(cros_root, 'chromiumos')
38
39  # Create parser and add optional command-line arguments.
40  parser = argparse.ArgumentParser(
41      description='Update an LLVM hash of packages and run tests.')
42
43  # Add argument for other change lists that want to run alongside the tryjob
44  # which has a change list of updating a package's git hash.
45  parser.add_argument(
46      '--extra_change_lists',
47      type=int,
48      nargs='+',
49      default=[],
50      help='change lists that would like to be run alongside the change list '
51      'of updating the packages')
52
53  # Add argument for a specific chroot path.
54  parser.add_argument('--chroot_path',
55                      default=cros_root,
56                      help='the path to the chroot (default: %(default)s)')
57
58  # Add argument to choose between llvm and llvm-next.
59  parser.add_argument(
60      '--is_llvm_next',
61      action='store_true',
62      help='which llvm hash to update. Update LLVM_NEXT_HASH if specified. '
63      'Otherwise, update LLVM_HASH')
64
65  # Add argument for the absolute path to the file that contains information on
66  # the previous tested svn version.
67  parser.add_argument(
68      '--last_tested',
69      help='the absolute path to the file that contains the last tested '
70      'arguments.')
71
72  # Add argument for the LLVM version to use.
73  parser.add_argument('--llvm_version',
74                      type=get_llvm_hash.IsSvnOption,
75                      required=True,
76                      help='which git hash of LLVM to find '
77                      '{google3, ToT, <svn_version>} '
78                      '(default: finds the git hash of the google3 LLVM '
79                      'version)')
80
81  # Add argument to add reviewers for the created CL.
82  parser.add_argument('--reviewers',
83                      nargs='+',
84                      default=[],
85                      help='The reviewers for the package update changelist')
86
87  # Add argument for whether to display command contents to `stdout`.
88  parser.add_argument('--verbose',
89                      action='store_true',
90                      help='display contents of a command to the terminal '
91                      '(default: %(default)s)')
92
93  subparsers = parser.add_subparsers(dest='subparser_name')
94  subparser_names = []
95  # Testing with the tryjobs.
96  tryjob_subparser = subparsers.add_parser('tryjobs')
97  subparser_names.append('tryjobs')
98  tryjob_subparser.add_argument('--builders',
99                                required=True,
100                                nargs='+',
101                                default=[],
102                                help='builders to use for the tryjob testing')
103
104  # Add argument for custom options for the tryjob.
105  tryjob_subparser.add_argument('--options',
106                                required=False,
107                                nargs='+',
108                                default=[],
109                                help='options to use for the tryjob testing')
110
111  # Testing with the recipe builders
112  recipe_subparser = subparsers.add_parser('recipe')
113  subparser_names.append('recipe')
114  recipe_subparser.add_argument('--options',
115                                required=False,
116                                nargs='+',
117                                default=[],
118                                help='options passed to the recipe builders')
119
120  recipe_subparser.add_argument('--builders',
121                                required=True,
122                                nargs='+',
123                                default=[],
124                                help='recipe builders to launch')
125
126  # Testing with CQ.
127  cq_subparser = subparsers.add_parser('cq')
128  subparser_names.append('cq')
129
130  # Add argument for specify a cq trybot to test along with other cq builders
131  # e.g. llvm, llvm-next or llvm-tot
132  cq_subparser.add_argument(
133      '--cq_trybot',
134      choices=VALID_CQ_TRYBOTS,
135      help='include the trybot to test together with other cq builders '
136      'available: %(choices)s')
137
138  args_output = parser.parse_args()
139
140  if args_output.subparser_name not in subparser_names:
141    parser.error('one of %s must be specified' % subparser_names)
142
143  return args_output
144
145
146def UnchangedSinceLastRun(last_tested_file, arg_dict):
147  """Gets the arguments used for last run
148
149  Args:
150    last_tested_file: The absolute path to the file that contains the
151    arguments for the last run.
152    arg_dict: The arguments used for this run.
153
154  Returns:
155    Return true if the arguments used for last run exist and are the
156    same as the arguments used for this run. Otherwise return false.
157  """
158
159  if not last_tested_file:
160    return False
161
162  # Get the last tested svn version if the file exists.
163  last_arg_dict = None
164  try:
165    with open(last_tested_file) as f:
166      last_arg_dict = json.load(f)
167
168  except (IOError, ValueError):
169    return False
170
171  return arg_dict == last_arg_dict
172
173
174def AddReviewers(cl, reviewers, chroot_path):
175  """Add reviewers for the created CL."""
176
177  gerrit_abs_path = os.path.join(chroot_path, 'chromite/bin/gerrit')
178  for reviewer in reviewers:
179    cmd = [gerrit_abs_path, 'reviewers', str(cl), reviewer]
180
181    subprocess.check_output(cmd)
182
183
184def AddLinksToCL(tests, cl, chroot_path):
185  """Adds the test link(s) to the CL as a comment."""
186
187  # NOTE: Invoking `cros_sdk` does not make each tryjob link appear on its own
188  # line, so invoking the `gerrit` command directly instead of using `cros_sdk`
189  # to do it for us.
190  #
191  # FIXME: Need to figure out why `cros_sdk` does not add each tryjob link as a
192  # newline.
193  gerrit_abs_path = os.path.join(chroot_path, 'chromite/bin/gerrit')
194
195  links = ['Started the following tests:']
196  links.extend(test['link'] for test in tests)
197
198  add_message_cmd = [gerrit_abs_path, 'message', str(cl), '\n'.join(links)]
199
200  subprocess.check_output(add_message_cmd)
201
202
203# Testing with tryjobs
204def GetCurrentTimeInUTC():
205  """Returns the current time via `datetime.datetime.utcnow()`."""
206  return datetime.datetime.utcnow()
207
208
209def GetTryJobCommand(change_list, extra_change_lists, options, builder):
210  """Constructs the 'tryjob' command.
211
212  Args:
213    change_list: The CL obtained from updating the packages.
214    extra_change_lists: Extra change lists that would like to be run alongside
215    the change list of updating the packages.
216    options: Options to be passed into the tryjob command.
217    builder: The builder to be passed into the tryjob command.
218
219  Returns:
220    The 'tryjob' command with the change list of updating the packages and
221    any extra information that was passed into the command line.
222  """
223
224  tryjob_cmd = ['cros', 'tryjob', '--yes', '--json', '-g', '%d' % change_list]
225
226  if extra_change_lists:
227    for extra_cl in extra_change_lists:
228      tryjob_cmd.extend(['-g', '%d' % extra_cl])
229
230  if options:
231    tryjob_cmd.extend('--%s' % option for option in options)
232
233  tryjob_cmd.append(builder)
234
235  return tryjob_cmd
236
237
238def RunTryJobs(cl_number, extra_change_lists, options, builders, chroot_path):
239  """Runs a tryjob/tryjobs.
240
241  Args:
242    cl_number: The CL created by updating the packages.
243    extra_change_lists: Any extra change lists that would run alongside the CL
244    that was created by updating the packages ('cl_number').
245    options: Any options to be passed into the 'tryjob' command.
246    builders: All the builders to run the 'tryjob' with.
247    chroot_path: The absolute path to the chroot.
248
249  Returns:
250    A list that contains stdout contents of each tryjob, where stdout is
251    information (a hashmap) about the tryjob. The hashmap also contains stderr
252    if there was an error when running a tryjob.
253
254  Raises:
255    ValueError: Failed to submit a tryjob.
256  """
257
258  # Contains the results of each builder.
259  tests = []
260
261  # Run tryjobs with the change list number obtained from updating the
262  # packages and append additional changes lists and options obtained from the
263  # command line.
264  for builder in builders:
265    cmd = GetTryJobCommand(cl_number, extra_change_lists, options, builder)
266
267    out = subprocess.check_output(cmd, cwd=chroot_path, encoding='utf-8')
268
269    test_output = json.loads(out)
270
271    buildbucket_id = int(test_output[0]['id'])
272
273    tests.append({
274        'launch_time': str(GetCurrentTimeInUTC()),
275        'link': 'http://ci.chromium.org/b/%s' % buildbucket_id,
276        'buildbucket_id': buildbucket_id,
277        'extra_cls': extra_change_lists,
278        'options': options,
279        'builder': [builder]
280    })
281
282  AddLinksToCL(tests, cl_number, chroot_path)
283
284  return tests
285
286
287def StartRecipeBuilders(cl_number, extra_change_lists, options, builders,
288                        chroot_path):
289  """Launch recipe builders.
290
291  Args:
292    cl_number: The CL created by updating the packages.
293    extra_change_lists: Any extra change lists that would run alongside the CL
294    that was created by updating the packages ('cl_number').
295    options: Any options to be passed into the 'tryjob' command.
296    builders: All the builders to run the 'tryjob' with.
297    chroot_path: The absolute path to the chroot.
298
299  Returns:
300    A list that contains stdout contents of each builder, where stdout is
301    information (a hashmap) about the tryjob. The hashmap also contains stderr
302    if there was an error when running a tryjob.
303
304  Raises:
305    ValueError: Failed to start a builder.
306  """
307
308  # Contains the results of each builder.
309  tests = []
310
311  # Launch a builders with the change list number obtained from updating the
312  # packages and append additional changes lists and options obtained from the
313  # command line.
314  for builder in builders:
315    cmd = ['bb', 'add', '-json']
316
317    if cl_number:
318      cmd.extend(['-cl', 'crrev.com/c/%d' % cl_number])
319
320    if extra_change_lists:
321      for cl in extra_change_lists:
322        cmd.extend(['-cl', 'crrev.com/c/%d' % cl])
323
324    if options:
325      cmd.extend(options)
326
327    cmd.append(builder)
328
329    out = subprocess.check_output(cmd, cwd=chroot_path, encoding='utf-8')
330
331    test_output = json.loads(out)
332
333    tests.append({
334        'launch_time': test_output['createTime'],
335        'link': 'http://ci.chromium.org/b/%s' % test_output['id'],
336        'buildbucket_id': test_output['id'],
337        'extra_cls': extra_change_lists,
338        'options': options,
339        'builder': [builder]
340    })
341
342  AddLinksToCL(tests, cl_number, chroot_path)
343
344  return tests
345
346
347# Testing with CQ
348def GetCQDependString(dependent_cls):
349  """Get CQ dependency string e.g. `Cq-Depend: chromium:MM, chromium:NN`."""
350
351  if not dependent_cls:
352    return None
353
354  # Cq-Depend must start a new paragraph prefixed with "Cq-Depend".
355  return '\nCq-Depend: ' + ', '.join(
356      ('chromium:%s' % i) for i in dependent_cls)
357
358
359def GetCQIncludeTrybotsString(trybot):
360  """Get Cq-Include-Trybots string, for more llvm testings"""
361
362  if not trybot:
363    return None
364
365  if trybot not in VALID_CQ_TRYBOTS:
366    raise ValueError('%s is not a valid llvm trybot' % trybot)
367
368  # Cq-Include-Trybots must start a new paragraph prefixed
369  # with "Cq-Include-Trybots".
370  return '\nCq-Include-Trybots:chromeos/cq:cq-%s-orchestrator' % trybot
371
372
373def StartCQDryRun(cl, dependent_cls, chroot_path):
374  """Start CQ dry run for the changelist and dependencies."""
375
376  gerrit_abs_path = os.path.join(chroot_path, 'chromite/bin/gerrit')
377
378  cl_list = [cl]
379  cl_list.extend(dependent_cls)
380
381  for changes in cl_list:
382    cq_dry_run_cmd = [gerrit_abs_path, 'label-cq', str(changes), '1']
383
384    subprocess.check_output(cq_dry_run_cmd)
385
386
387def main():
388  """Updates the packages' LLVM hash and run tests.
389
390  Raises:
391    AssertionError: The script was run inside the chroot.
392  """
393
394  chroot.VerifyOutsideChroot()
395
396  args_output = GetCommandLineArgs()
397
398  patch_metadata_file = 'PATCHES.json'
399
400  svn_option = args_output.llvm_version
401
402  git_hash, svn_version = get_llvm_hash.GetLLVMHashAndVersionFromSVNOption(
403      svn_option)
404
405  # There is no need to run tryjobs when all the key parameters remain unchanged
406  # from last time.
407
408  # If --last_tested is specified, check if the current run has the same
409  # arguments last time --last_tested is used.
410  if args_output.last_tested:
411    chroot_file_paths = chroot.GetChrootEbuildPaths(
412        args_output.chroot_path, update_chromeos_llvm_hash.DEFAULT_PACKAGES)
413    arg_dict = {
414        'svn_version': svn_version,
415        'ebuilds': chroot_file_paths,
416        'extra_cls': args_output.extra_change_lists,
417    }
418    if args_output.subparser_name in ('tryjobs', 'recipe'):
419      arg_dict['builders'] = args_output.builders
420      arg_dict['tryjob_options'] = args_output.options
421    if UnchangedSinceLastRun(args_output.last_tested, arg_dict):
422      print('svn version (%d) matches the last tested svn version in %s' %
423            (svn_version, args_output.last_tested))
424      return
425
426  llvm_variant = update_chromeos_llvm_hash.LLVMVariant.current
427  if args_output.is_llvm_next:
428    llvm_variant = update_chromeos_llvm_hash.LLVMVariant.next
429  update_chromeos_llvm_hash.verbose = args_output.verbose
430  extra_commit_msg = None
431  if args_output.subparser_name == 'cq':
432    cq_depend_msg = GetCQDependString(args_output.extra_change_lists)
433    if cq_depend_msg:
434      extra_commit_msg = cq_depend_msg
435    cq_trybot_msg = GetCQIncludeTrybotsString(args_output.cq_trybot)
436    if cq_trybot_msg:
437      extra_commit_msg += cq_trybot_msg
438
439  change_list = update_chromeos_llvm_hash.UpdatePackages(
440      update_chromeos_llvm_hash.DEFAULT_PACKAGES,
441      llvm_variant,
442      git_hash,
443      svn_version,
444      args_output.chroot_path,
445      patch_metadata_file,
446      failure_modes.FailureModes.DISABLE_PATCHES,
447      svn_option,
448      extra_commit_msg=extra_commit_msg)
449
450  AddReviewers(change_list.cl_number, args_output.reviewers,
451               args_output.chroot_path)
452
453  print('Successfully updated packages to %d' % svn_version)
454  print('Gerrit URL: %s' % change_list.url)
455  print('Change list number: %d' % change_list.cl_number)
456
457  if args_output.subparser_name == 'tryjobs':
458    tests = RunTryJobs(change_list.cl_number, args_output.extra_change_lists,
459                       args_output.options, args_output.builders,
460                       args_output.chroot_path)
461    print('Tests:')
462    for test in tests:
463      print(test)
464  elif args_output.subparser_name == 'recipe':
465    tests = StartRecipeBuilders(change_list.cl_number,
466                                args_output.extra_change_lists,
467                                args_output.options, args_output.builders,
468                                args_output.chroot_path)
469    print('Tests:')
470    for test in tests:
471      print(test)
472
473  else:
474    StartCQDryRun(change_list.cl_number, args_output.extra_change_lists,
475                  args_output.chroot_path)
476
477  # If --last_tested is specified, record the arguments used
478  if args_output.last_tested:
479    with open(args_output.last_tested, 'w') as f:
480      json.dump(arg_dict, f, indent=2)
481
482
483if __name__ == '__main__':
484  main()
485