blob: 9fbb7f7d56af84e0823d7ecc3861ad15cab973fc [file] [log] [blame]
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
from recipe_engine import recipe_api
from PB.recipe_engine import result as result_pb2
from import common as common_pb2
class PresubmitApi(recipe_api.RecipeApi):
def __init__(self, properties, **kwargs):
super(PresubmitApi, self).__init__(**kwargs)
self._runhooks = properties.runhooks
# 8 minutes seems like a reasonable upper bound on presubmit timings.
# According to event mon data we have, it seems like anything longer than
# this is a bug, and should just instant fail.
self._timeout_s = properties.timeout_s
self._vpython_spec_path = properties.vpython_spec_path
def presubmit_support_path(self):
return self.repo_resource('')
def __call__(self, *args, **kwargs):
"""Return a presubmit step."""
name = kwargs.pop('name', 'presubmit')
with self.m.depot_tools.on_path():
presubmit_args = list(args) + [
'--json_output', self.m.json.output(),
step_data = self.m.python(
name, self.presubmit_support_path, presubmit_args, **kwargs)
return step_data.json.output
def prepare(self):
"""Set up a presubmit run.
This includes:
- setting up the checkout w/ bot_update
- locally committing the applied patch
- running hooks, if requested
This expects the gclient configuration to already have been set.
the StepResult from the bot_update step.
# Expect callers to have already set up their gclient configuration.
bot_update_step = self.m.bot_update.ensure_checkout()
relative_root = self.m.gclient.get_gerrit_patch_root().rstrip('/')
abs_root = self.m.context.cwd.join(relative_root)
with self.m.context(cwd=abs_root):
# TODO(unowned): Consider either:
# - extracting user name & email address from the issue, or
# - using a dedicated and clearly nonexistent name/email address
self.m.git('-c', '',
'-c', ' Commit Bot',
'commit', '-a', '-m', 'Committed patch',
name='commit-git-patch', infra_step=False)
if self._runhooks:
with self.m.context(cwd=self.m.path['checkout']):
return bot_update_step
def execute(self, bot_update_step):
"""Runs presubmit and sets summary markdown if applicable.
bot_update_step: the StepResult from a previously executed bot_update step.
a RawResult object, suitable for being returned from RunSteps.
relative_root = self.m.gclient.get_gerrit_patch_root().rstrip('/')
abs_root = self.m.context.cwd.join(relative_root)
got_revision_properties = self.m.bot_update.get_project_revision_properties(
# Replace path.sep with '/', since most recipes are written assuming '/'
# as the delimiter. This breaks on windows otherwise.
relative_root.replace(self.m.path.sep, '/'), self.m.gclient.c)
upstream = bot_update_step.json.output['properties'].get(
presubmit_args = [
'--issue', self.m.tryserver.gerrit_change.change,
'--patchset', self.m.tryserver.gerrit_change.patchset,
'--gerrit_url', 'https://%s' %,
if self.m.cq.state == self.m.cq.DRY:
'--root', abs_root,
'--verbose', '--verbose',
'--skip_canned', 'CheckTreeIsOpen',
'--skip_canned', 'CheckBuildbotPendingBuilds',
'--upstream', upstream, # '' if not in bot_update mode.
venv = None
# TODO(iannucci): verify that correctly finds and
# uses .vpython files, then remove this configuration.
if self._vpython_spec_path:
venv = abs_root.join(self._vpython_spec_path)
raw_result = result_pb2.RawResult()
step_json = self(
venv=venv, timeout=self._timeout_s,
# ok_ret='any' causes all exceptions to be ignored in this step
# Set recipe result values
if step_json:
raw_result.summary_markdown = _createSummaryMarkdown(step_json)
retcode = self.m.step.active_result.retcode
if retcode == 0:
raw_result.status = common_pb2.SUCCESS
return raw_result
self.m.step.active_result.presentation.status = 'FAILURE'
if self.m.step.active_result.exc_result.had_timeout:
raw_result.status = common_pb2.FAILURE
raw_result.summary_markdown += (
'\n\nTimeout occurred during presubmit step.')
elif retcode == 1:
raw_result.status = common_pb2.FAILURE
raw_result.status = common_pb2.INFRA_FAILURE
# Handle unexpected errors not caught by json output
if raw_result.summary_markdown == '':
raw_result.status = common_pb2.INFRA_FAILURE
raw_result.summary_markdown = (
'Something unexpected occurred'
' while running presubmit checks.'
' Please [file a bug]('
return raw_result
def _limitSize(message_list, char_limit=450):
"""Returns a list of strings within a certain character length.
* message_list (List[str]) - The message to truncate as a list
of lines (without line endings).
hint = ('**The complete output can be'
' found at the bottom of the presubmit stdout.**')
char_count = 0
for index, message in enumerate(message_list):
char_count += len(message)
if char_count > char_limit:
if index == 0:
# Show at minimum part of the first error message
first_message = message_list[index].splitlines()
return ['\n'.join(
total_errors = len(message_list)
# If code is being cropped, the closing code tag will
# get removed, so add it back before the hint.
code_tag = '```'
message_list[index - 1] = '\n'.join((message_list[index - 1], code_tag))
oversized_msg = ('\n**Error size > %d chars, '
'there are %d more error(s) (%d total)**') % (
char_limit, total_errors - index, total_errors
return message_list[:index] + [oversized_msg, hint]
return message_list
def _createSummaryMarkdown(step_json):
"""Returns a string with data on errors, warnings, and notifications.
Extracts the number of errors, warnings and notifications
from the dictionary(step_json).
Then it lists all the errors line by line.
* step_json = {
'errors': [
'message': string,
'long_text': string,
'items: [string],
'fatal': boolean
'notifications': [
'message': string,
'long_text': string,
'items: [string],
'fatal': boolean
'warnings': [
'message': string,
'long_text': string,
'items: [string],
'fatal': boolean
errors = step_json['errors']
warning_count = len(step_json['warnings'])
notif_count = len(step_json['notifications'])
description = (
'#### There are %d error(s), %d warning(s),'
' and %d notifications(s). Here are the errors:') % (
len(errors), warning_count, notif_count
error_messages = []
for error in errors:
'**ERROR**\n```\n%s\n%s\n```' % (
error['message'], error['long_text'])
error_messages = _limitSize(error_messages)
# Description is not counted in the total message size.
# It is inserted afterward to ensure it is the first message seen.
error_messages.insert(0, description)
if warning_count or notif_count:
('#### To see notifications and warnings,'
' look at the stdout of the presubmit step.')
return '\n\n'.join(error_messages)