blob: e9d8a0a72df15ebcfde3065916cfe6cb111ac1ef [file] [log] [blame]
# -*- coding: utf-8 -*-
# Copyright 2018 The Chromium OS Authors. All rights reserved.
# 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."""
from __future__ import print_function
import datetime
import json
import os
import sys
from chromite.cli import command
from chromite.lib import commandline
from chromite.lib import cros_build_lib
from chromite.lib import cros_logging as logging
from chromite.lib.buildstore import BuildStore
assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
_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.CommandDecorator('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 samus-pre-cq
cros buildresult --build-config samus-pre-cq --date 2018-1-2
cros buildresult --build-config samus-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 master-*-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):
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."""
self.options.Freeze()
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)