blob: 717eeaa33dfc05d231b63f3aacb745bc76d67196 [file] [log] [blame]
# 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)