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