| # Copyright 2018 The ChromiumOS Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """cros buildresult: Look up results for a single build.""" |
| |
| import datetime |
| import json |
| import logging |
| import os |
| |
| from chromite.cli import command |
| from chromite.lib import commandline |
| from chromite.lib import cros_build_lib |
| from chromite.lib.buildstore import BuildStore |
| |
| |
| _FINISHED_STATUSES = ( |
| "fail", |
| "pass", |
| "missing", |
| "aborted", |
| "skipped", |
| "forgiven", |
| ) |
| |
| |
| def FetchBuildStatuses(buildstore, options): |
| """Fetch the requested build statuses. |
| |
| The results are NOT filtered or fixed up. |
| |
| Args: |
| buildstore: BuildStore instance to make db calls. |
| options: Parsed command line options set. |
| |
| Returns: |
| List of build_status dicts from Buildbucket, or None. |
| """ |
| if options.buildbucket_id: |
| build_status = buildstore.GetBuildStatuses([options.buildbucket_id])[0] |
| if build_status: |
| return [build_status] |
| elif options.build_config: |
| start_date = options.start_date or options.date |
| end_date = options.end_date or options.date |
| return buildstore.GetBuildHistory( |
| options.build_config, |
| buildstore.NUM_RESULTS_NO_LIMIT, |
| start_date=start_date, |
| end_date=end_date, |
| ) |
| else: |
| cros_build_lib.Die("You must specify which builds.") |
| |
| |
| def IsBuildStatusFinished(build_status): |
| """Populates the 'artifacts_url' and 'stages' build_status fields. |
| |
| Args: |
| build_status: Single build_status dict returned by any Fetch method. |
| |
| Returns: |
| build_status dict with additional fields populated. |
| """ |
| return build_status["status"] in _FINISHED_STATUSES |
| |
| |
| def FixUpBuildStatus(buildstore, build_status): |
| """Add 'extra' build_status values we need. |
| |
| Populates the 'artifacts_url' and 'stages' build_status fields. |
| |
| Args: |
| buildstore: BuildStore instance to make DB calls. |
| build_status: Single build_status dict returned by any Fetch method. |
| |
| Returns: |
| build_status dict with additional fields populated. |
| """ |
| # We don't actually store the artifacts_url, but we store a URL for a |
| # specific artifact we can use to derive it. |
| build_status["artifacts_url"] = None |
| if build_status["metadata_url"]: |
| build_status["artifacts_url"] = os.path.dirname( |
| build_status["metadata_url"] |
| ) |
| |
| # Find stage information. |
| build_status["stages"] = buildstore.GetBuildsStages( |
| buildbucket_ids=[build_status["buildbucket_id"]] |
| ) |
| |
| return build_status |
| |
| |
| def Report(build_statuses): |
| """Generate the stdout description of a given build. |
| |
| Args: |
| build_statuses: List of build_status dict's from FetchBuildStatus. |
| |
| Returns: |
| str to display as the final report. |
| """ |
| result = "" |
| |
| for build_status in build_statuses: |
| result += "\n".join( |
| [ |
| "buildbucket_id: %s" % build_status["buildbucket_id"], |
| "status: %s" % build_status["status"], |
| "artifacts_url: %s" % build_status["artifacts_url"], |
| "toolchain_url: %s" % build_status["toolchain_url"], |
| "stages:\n", |
| ] |
| ) |
| for stage in build_status["stages"]: |
| result += " %s: %s\n" % (stage["name"], stage["status"]) |
| result += "\n" # Blank line between builds. |
| |
| return result |
| |
| |
| def ReportJson(build_statuses): |
| """Generate the json description of a given build. |
| |
| Args: |
| build_statuses: List of build_status dict's from FetchBuildStatus. |
| |
| Returns: |
| str to display as the final report. |
| """ |
| report = {} |
| |
| for build_status in build_statuses: |
| report[build_status["buildbucket_id"]] = { |
| "buildbucket_id": build_status["buildbucket_id"], |
| "status": build_status["status"], |
| "stages": {s["name"]: s["status"] for s in build_status["stages"]}, |
| "artifacts_url": build_status["artifacts_url"], |
| "toolchain_url": build_status["toolchain_url"], |
| } |
| |
| return json.dumps(report) |
| |
| |
| @command.command_decorator("buildresult") |
| class BuildResultCommand(command.CliCommand): |
| """Script that looks up results of finished builds.""" |
| |
| EPILOG = """ |
| Look up a single build result: |
| cros buildresult --buildbucket-id 1234567890123 |
| |
| Look up results by build config name: |
| cros buildresult --build-config hatch-pre-cq |
| cros buildresult --build-config hatch-pre-cq --date 2018-1-2 |
| cros buildresult --build-config hatch-pre-cq \ |
| --start-date 2018-1-2 --end-date 2018-1-7 |
| |
| Output can be json formatted with: |
| cros buildresult --buildbucket-id 1234567890123 --report json |
| |
| Note: |
| This tool does NOT work for main-*-tryjob, precq-launcher-try, or |
| builds on branches older than CL:942097. |
| |
| Note: |
| Exit code 1: A script error or bad options combination. |
| Exit code 2: No matching finished builds were found. |
| """ |
| |
| @classmethod |
| def AddParser(cls, parser) -> None: |
| super(cls, BuildResultCommand).AddParser(parser) |
| |
| # What build do we report on? |
| request_group = parser.add_mutually_exclusive_group() |
| |
| request_group.add_argument( |
| "--buildbucket-id", |
| help="Buildbucket ID of build to look up. It is a 19-digit long ID " |
| "which can be found in Milo or GoldenEye URL.", |
| ) |
| request_group.add_argument("--build-config", help="") |
| |
| # |
| date_group = parser.add_argument_group() |
| |
| date_group.add_argument( |
| "--date", |
| action="store", |
| type="date", |
| default=datetime.date.today(), |
| help="Request all finished builds on a given day. Default today.", |
| ) |
| |
| date_group.add_argument( |
| "--start-date", |
| action="store", |
| type="date", |
| default=None, |
| help="Request all builds between (inclusive) start and end dates.", |
| ) |
| |
| date_group.add_argument( |
| "--end-date", |
| action="store", |
| type="date", |
| default=None, |
| help="End of date range (inclusive) specified by --start-date.", |
| ) |
| |
| # What kind of report do we generate? |
| parser.add_argument( |
| "--report", |
| default="standard", |
| choices=["standard", "json"], |
| help="What format is the output in?", |
| ) |
| |
| def Run(self): |
| """Run cros buildresult.""" |
| commandline.RunInsideChroot(self) |
| |
| buildstore = BuildStore(_write_to_cidb=False) |
| build_statuses = FetchBuildStatuses(buildstore, self.options) |
| |
| if build_statuses: |
| # Filter out builds that don't exist in Buildbucket, |
| # or which aren't finished. |
| build_statuses = [ |
| b for b in build_statuses if IsBuildStatusFinished(b) |
| ] |
| |
| # If we found no builds at all, return a different exit code to help |
| # automated scripts know they should try waiting longer. |
| if not build_statuses: |
| logging.error("No build found. Perhaps not started?") |
| return 2 |
| |
| # Fixup all of the builds we have. |
| build_statuses = [ |
| FixUpBuildStatus(buildstore, b) for b in build_statuses |
| ] |
| |
| # Produce our final result. |
| if self.options.report == "json": |
| report = ReportJson(build_statuses) |
| else: |
| report = Report(build_statuses) |
| |
| print(report) |