blob: f7f0487b476b7f1e7698028209b2c444b5f62c81 [file] [log] [blame]
# Copyright (c) 2012 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.
"""Subclasses of various slave command classes."""
from datetime import datetime
import copy
import errno
import json
import logging
import os
import time
import traceback
from twisted.internet import defer
from twisted.python import log
from buildbot import interfaces, util
from buildbot.changes.changes import Change
from buildbot.process import buildstep
from buildbot.process.properties import WithProperties
from buildbot.status import builder
from buildbot.steps import shell
from buildbot.steps import source
import sqlalchemy as sa
from common import annotator
from master import buildbucket
from common import chromium_utils
import config
def change_to_revision(c):
"""Handle revision == None or any invalid value."""
try:
return int(str(c.revision).split('@')[-1])
except (ValueError, TypeError):
return 0
def updateText(section):
# Reflect step status in text2.
if section['status'] == builder.EXCEPTION:
result = ['exception', section['name']]
elif section['status'] == builder.FAILURE:
result = ['failed', section['name']]
else:
result = []
section['step'].setText(['<br>'.join(
# Do not exclude step, even though it gets repeated in build view,
# because waterfall and console views display only step text.
[section['name']] +
section['step_text'])])
section['step'].setText2(result + section['step_summary_text'])
# derived from addCompleteLog in process/buildstep.py
def addLogToStep(step, name, text):
"""Add a complete log to a step."""
if text and text.startswith('<!DOCTYPE html'):
step.addHTMLLog(name, text)
else:
add_log = step.addLog(name)
size = add_log.chunkSize
for start in range(0, len(text), size):
add_log.addStdout(text[start:start+size])
add_log.finish()
class GClient(source.Source):
"""Check out a source tree using gclient."""
name = 'update'
def __init__(self, svnurl=None, rm_timeout=None, gclient_spec=None, env=None,
sudo_for_remove=False, gclient_deps=None, gclient_nohooks=False,
no_gclient_branch=False, no_gclient_revision=False,
gclient_transitive=False, primary_repo=None,
gclient_jobs=None, blink_config=None, **kwargs):
# TODO: We shouldn't need to hard-code blink-specific info here. We
# should figure out how to generalize this to sub-repos somehow.
source.Source.__init__(self, **kwargs)
if env:
self.args['env'] = env.copy()
self.args['rm_timeout'] = rm_timeout
self.args['svnurl'] = svnurl
self.args['sudo_for_remove'] = sudo_for_remove
# linux doesn't handle spaces in command line args properly so remove them.
# This doesn't matter for the format of the DEPS file.
self.args['gclient_spec'] = gclient_spec.replace(' ', '')
self.args['gclient_deps'] = gclient_deps
self.args['gclient_nohooks'] = gclient_nohooks
self.args['no_gclient_branch'] = no_gclient_branch
self.args['no_gclient_revision'] = no_gclient_revision
self.args['gclient_transitive'] = gclient_transitive
self.args['primary_repo'] = primary_repo or ''
self.args['gclient_jobs'] = gclient_jobs
self.args['blink_config'] = blink_config
def computeSourceRevision(self, changes):
"""Finds the latest revision number from the changeset that have
triggered the build.
This is a hook method provided by the parent source.Source class and
default implementation in source.Source returns None. Return value of this
method is be used to set 'revsion' argument value for startVC() method."""
if not changes:
return None
# Change revision numbers can be invalid, for a try job for instance.
# TODO(maruel): Make this work for git hash.
lastChange = max([change_to_revision(c) for c in changes])
return lastChange
def startVC(self, branch, revision, patch):
warnings = []
args = copy.copy(self.args)
if args.get('gclient_spec'):
self.adjustGclientSpecForBlink(branch, revision, args)
self.adjustGclientSpecForNaCl(branch, revision, patch, args)
self.adjustGclientSpecForV8(branch, revision, patch, args)
self.adjustGclientSpecForWebRTC(branch, revision, patch, args)
try:
# parent_cr_revision might be set, but empty.
if self.getProperty('parent_cr_revision'):
revision = 'src@' + self.getProperty('parent_cr_revision')
except KeyError:
pass
self.setProperty('primary_repo', args['primary_repo'], 'Source')
args['revision'] = revision
args['branch'] = branch
if patch:
args['patch'] = patch
elif args.get('patch') is None:
del args['patch']
args['project'] = self.build.getSourceStamp().project
cmd = buildstep.LoggedRemoteCommand('gclient', args)
self.startCommand(cmd, warnings)
def adjustGclientSpecForBlink(self, branch, revision, args):
# If the bot in question is a dedicated bot for Blink changes (either
# on a waterfall, or a blink-specific trybot), we want to set a custom
# version of Blink, otherwise we leave the gclient spec alone.
if args['blink_config'] != 'blink':
return
# branch == 'trunk' means the change came from the blink poller, and the
# revision is a blink revision; otherwise, we use '', or HEAD.
wk_revision = ''
if branch == 'trunk':
wk_revision = revision
try:
# parent_wk_revision might be set, but empty.
if self.getProperty('parent_wk_revision'):
wk_revision = self.getProperty('parent_wk_revision')
except KeyError:
pass
# TODO: Make this be something less fragile.
args['gclient_spec'] = args['gclient_spec'].replace(
'"webkit_trunk"',
'"webkit_revision":"%s","webkit_trunk"' % wk_revision)
def adjustGclientSpecForNaCl(self, branch, revision, patch, args):
nacl_revision = revision
try:
# parent_nacl_revision might be set, but empty.
if self.getProperty('parent_got_nacl_revision'):
nacl_revision = self.getProperty('parent_got_nacl_revision')
except KeyError:
pass
args['gclient_spec'] = args['gclient_spec'].replace(
'$$NACL_REV$$', str(nacl_revision or ''))
def adjustGclientSpecForV8(self, branch, revision, patch, args):
v8_revision = revision
try:
# parent_v8_revision might be set, but empty.
if self.getProperty('parent_got_v8_revision'):
v8_revision = self.getProperty('parent_got_v8_revision')
except KeyError:
pass
args['gclient_spec'] = args['gclient_spec'].replace(
'$$V8_REV$$', str(v8_revision or ''))
def adjustGclientSpecForWebRTC(self, branch, revision, patch, args):
webrtc_revision = revision
try:
# parent_webrtc_revision might be set, but empty.
if self.getProperty('parent_got_webrtc_revision'):
webrtc_revision = self.getProperty('parent_got_webrtc_revision')
except KeyError:
pass
args['gclient_spec'] = args['gclient_spec'].replace(
'$$WEBRTC_REV$$', str(webrtc_revision or ''))
def describe(self, done=False):
"""Tries to append the revision number to the description."""
description = source.Source.describe(self, done)
self.appendChromeRevision(description)
self.appendWebKitRevision(description)
self.appendNaClRevision(description)
self.appendV8Revision(description)
self.appendWebRTCRevision(description)
return description
def appendChromeRevision(self, description):
"""Tries to append the Chromium revision to the given description."""
revision = None
try:
revision = self.getProperty('got_revision')
except KeyError:
# 'got_revision' doesn't exist yet, check 'revision'
try:
revision = self.getProperty('revision')
except KeyError:
pass # neither exist, go on without revision
if revision:
# TODO: Right now, 'no_gclient_branch' is a euphemism for 'git', but we
# probably ought to be explicit about this switch.
if not self.args['no_gclient_branch']:
revision = 'r%s' % revision
# Only append revision if it's not already there.
if not revision in description:
description.append(revision)
def appendWebKitRevision(self, description):
"""Tries to append the WebKit revision to the given description."""
webkit_revision = None
try:
webkit_revision = self.getProperty('got_webkit_revision')
except KeyError:
pass
if webkit_revision:
webkit_revision = 'webkit r%s' % webkit_revision
# Only append revision if it's not already there.
if not webkit_revision in description:
description.append(webkit_revision)
def appendNaClRevision(self, description):
"""Tries to append the NaCl revision to the given description."""
nacl_revision = None
try:
nacl_revision = self.getProperty('got_nacl_revision')
except KeyError:
pass
if nacl_revision:
nacl_revision = 'nacl r%s' % nacl_revision
# Only append revision if it's not already there.
if not nacl_revision in description:
description.append(nacl_revision)
def appendV8Revision(self, description):
"""Tries to append the V8 revision to the given description."""
v8_revision = None
try:
v8_revision = self.getProperty('got_v8_revision')
except KeyError:
pass
if v8_revision:
v8_revision = 'v8 r%s' % v8_revision
# Only append revision if it's not already there.
if not v8_revision in description:
description.append(v8_revision)
def appendWebRTCRevision(self, description):
"""Tries to append the WebRTC revision to the given description."""
webrtc_revision = None
try:
webrtc_revision = self.getProperty('got_webrtc_revision')
except KeyError:
pass
if webrtc_revision:
webrtc_revision = 'webrtc r%s' % webrtc_revision
# Only append revision if it's not already there.
if not webrtc_revision in description:
description.append(webrtc_revision)
def commandComplete(self, cmd):
"""Handles status updates from buildbot slave when the step is done.
Update the relevant got_XX_revision build properties if available.
"""
source.Source.commandComplete(self, cmd)
primary_repo = self.args.get('primary_repo', '')
primary_revision_key = 'got_' + primary_repo + 'revision'
properties = (
('got_revision', primary_revision_key),
('got_nacl_revision', 'got_nacl_revision'),
('got_swarming_client_revision', 'got_swarming_client_revision'),
('got_v8_revision', 'got_v8_revision'),
('got_webkit_revision', 'got_webkit_revision'),
('got_webrtc_revision', 'got_webrtc_revision'),
)
for prop_name, cmd_arg in properties:
if cmd_arg in cmd.updates:
got_revision = cmd.updates[cmd_arg][-1]
if got_revision:
self.setProperty(prop_name, str(got_revision), 'Source')
class BuilderStatus(object):
# Order in asceding severity.
BUILD_STATUS_ORDERING = [
builder.SUCCESS,
builder.WARNINGS,
builder.FAILURE,
builder.EXCEPTION,
]
@classmethod
def combine(cls, a, b):
"""Combine two status, favoring the more severe."""
if a not in cls.BUILD_STATUS_ORDERING:
return b
if b not in cls.BUILD_STATUS_ORDERING:
return a
a_rank = cls.BUILD_STATUS_ORDERING.index(a)
b_rank = cls.BUILD_STATUS_ORDERING.index(b)
pick = max(a_rank, b_rank)
return cls.BUILD_STATUS_ORDERING[pick]
class ProcessLogShellStep(shell.ShellCommand):
"""Step that can process log files.
Delegates actual processing to log_processor, which is a subclass of
process_log.PerformanceLogParser.
Sample usage:
# construct class that will have no-arg constructor.
log_processor_class = chromium_utils.PartiallyInitialize(
process_log.GraphingPageCyclerLogProcessor,
report_link='http://host:8010/report.html,
output_dir='~/www')
# We are partially constructing Step because the step final
# initialization is done by BuildBot.
step = chromium_utils.PartiallyInitialize(
chromium_step.ProcessLogShellStep,
log_processor_class)
"""
def __init__(self, log_processor_class=None, *args, **kwargs):
"""
Args:
log_processor_class: subclass of
process_log.PerformanceLogProcessor that will be initialized and
invoked once command was successfully completed.
"""
self._result_text = []
self._log_processor = None
# If log_processor_class is not None, it should be a class. Create an
# instance of it.
if log_processor_class:
self._log_processor = log_processor_class()
shell.ShellCommand.__init__(self, *args, **kwargs)
def start(self):
"""Overridden shell.ShellCommand.start method.
Adds a link for the activity that points to report ULR.
"""
self._CreateReportLinkIfNeccessary()
shell.ShellCommand.start(self)
def _GetRevision(self):
"""Returns the revision number for the build.
Result is the revision number of the latest change that went in
while doing gclient sync. Tries 'got_revision' (from log parsing)
then tries 'revision' (usually from forced build). If neither are
found, will return -1 instead.
"""
try:
repo = self.build.getProperty('primary_repository')
if not repo:
repo = ''
except KeyError:
repo = ''
revision = None
try:
revision = self.build.getProperty('got_' + repo + 'revision')
except KeyError:
pass # 'got_revision' doesn't exist (yet)
if not revision:
try:
revision = self.build.getProperty('revision')
except KeyError:
pass # neither exist
if not revision:
revision = -1
return revision
def _GetWebkitRevision(self):
"""Returns the webkit revision number for the build.
"""
try:
return self.build.getProperty('got_webkit_revision')
except KeyError:
return None
def _GetBuildProperty(self):
"""Returns a dict with the channel and version."""
build_properties = {}
try:
channel = self.build.getProperty('channel')
if channel:
build_properties.setdefault('channel', channel)
except KeyError:
pass # 'channel' doesn't exist.
try:
version = self.build.getProperty('version')
if version:
build_properties.setdefault('version', version)
except KeyError:
pass # 'version' doesn't exist.
return build_properties
def commandComplete(self, cmd):
"""Callback implementation that will use log process to parse 'stdio' data.
"""
if self._log_processor:
self._result_text = self._log_processor.Process(
self._GetRevision(), self.getLog('stdio').getText(),
self._GetBuildProperty(), webkit_revision=self._GetWebkitRevision())
def getText(self, cmd, results):
text_list = self.describe(True)
if self._result_text:
self._result_text.insert(0, '<div class="BuildResultInfo">')
self._result_text.append('</div>')
text_list += self._result_text
return text_list
def evaluateCommand(self, cmd):
shell_result = shell.ShellCommand.evaluateCommand(self, cmd)
log_result = None
if self._log_processor and 'evaluateCommand' in dir(self._log_processor):
log_result = self._log_processor.evaluateCommand(cmd)
return BuilderStatus.combine(shell_result, log_result)
def _CreateReportLinkIfNeccessary(self):
if self._log_processor and self._log_processor.ReportLink():
self.addURL('results', '%s' % self._log_processor.ReportLink())
def Prepend(filename, data, output_dir, perf_output_dir):
READABLE_FILE_PERMISSIONS = int('644', 8)
fullfn = chromium_utils.AbsoluteCanonicalPath(output_dir, filename)
dir_path = os.path.dirname(fullfn)
MakeOutputDirectory(dir_path)
# This whitelists writing to files only directly under output_dir
# or perf_expectations_dir for security reasons.
if not (dir_path.startswith(output_dir) or
dir_path.startswith(perf_output_dir)):
raise Exception('Attempted to write to log file outside of \'%s\' or '
'\'%s\': \'%s\'' % (output_dir,
perf_output_dir,
os.path.join(output_dir,
filename)))
chromium_utils.Prepend(fullfn, data)
os.chmod(fullfn, READABLE_FILE_PERMISSIONS)
def MakeOutputDirectory(output_dir):
if output_dir and not os.path.exists(output_dir):
os.makedirs(output_dir)
def getSourceStamp(build):
"""Retrieve the SourceStamp for the given build."""
source_stamp = build.getSourceStamp()
if source_stamp.revision is None:
revision = build.getProperties().getProperty('got_revision')
if revision:
# The source stamp is relative, but we have the "got_revision", so
# make it absolute.
source_stamp = source_stamp.getAbsoluteSourceStamp(revision)
else:
# The sourcestamp is absolute. DO NOT CALL getAbsoluteSourceStamp on it
# because that would wipe changes and therefore the blamelist.
pass
return source_stamp
class AnnotationObserver(buildstep.LogLineObserver):
"""This class knows how to understand annotations.
Here are a list of the currently supported annotations:
@@@BUILD_STEP <stepname>@@@
Add a new step <stepname> after the last step. End the step at the cursor,
marking with last available status. Advance the step cursor to the added step.
@@@SEED_STEP <stepname>@@@
Add a new step <stepname> after the last step. Do not end the step at the
cursor, don't advance the cursor to the seeded step.
@@@STEP_CURSOR <stepname>@@@
Set the cursor to the named step. All further commands apply to the current
cursor.
@@@STEP_LINK@<label>@<url>@@@
Add a link with label <label> linking to <url> to the current stage.
If the label value is of the form "text-->base", this is considered an alias
link, and will add an alias named "text" with the value "url" to the log or
link named "base".
@@@STEP_STARTED@@@
Start the step at the cursor location.
@@@STEP_WARNINGS@@@
Mark the current step as having warnings (orange).
@@@STEP_FAILURE@@@
Mark the current step as having failed (red).
@@@STEP_EXCEPTION@@@
Mark the current step as having exceptions (magenta).
@@@STEP_CLOSED@@@
Close the current step, finalizing its log and moving the cursor to the
preamble. The step will be marked SUCCESS unless a status message marked it
otherwise.
@@@STEP_LOG_LINE@<label>@<line>@@@
Add a log line to a log named <label>. Multiple lines can be added.
@@@STEP_LOG_END@<label>@@@
Finalizes a log added by STEP_LOG_LINE and calls addCompleteLog().
@@@STEP_LOG_END_PERF@<label>@@@
Same as STEP_LOG_END, but signifies that this is a perf log and should be
saved to the master.
@@@STEP_CLEAR@@@
Reset the text description of the current step.
@@@STEP_SUMMARY_CLEAR@@@
Reset the text summary of the current step.
@@@STEP_TEXT@<msg line>@@@
Append <msg line> to the current step text.
@@@SEED_STEP_TEXT@step@<msg line>@@@
Append <msg line> to the specified seeded step.
@@@STEP_SUMMARY_TEXT@<msg>@@@
Append <msg> to the step summary (appears on top of the waterfall).
@@@STEP_NEST_LEVEL@<level>@@@
Set the nesting level of the current step. Steps at level n are assumed to
be nested under the most recent step of level n-1.
@@@HALT_ON_FAILURE@@@
Halt if exception or failure steps are encountered (default is not).
@@@HONOR_ZERO_RETURN_CODE@@@
Honor the return code being zero (success), even if steps have other results.
@@@STEP_TRIGGER@<spec>@@@
Trigger build(s), where <spec> is a JSON-encoded dict with keys:
builderNames - A list of builder names that should be triggered.
properties - A dictionary of properties to override the default ones.
Deprecated annotations:
TODO(bradnelson): drop these when all users have been tracked down.
@@@BUILD_WARNINGS@@@
Equivalent to @@@STEP_WARNINGS@@@
@@@BUILD_FAILED@@@
Equivalent to @@@STEP_FAILURE@@@
@@@BUILD_EXCEPTION@@@
Equivalent to @@@STEP_EXCEPTION@@@
@@@link@<label>@<url>@@@
Equivalent to @@@STEP_LINK@<label>@<url>@@@
"""
# Base URL for performance test results.
PERF_BASE_URL = config.Master.perf_base_url
PERF_REPORT_URL_SUFFIX = config.Master.perf_report_url_suffix
# Directory in which to save perf output data files.
PERF_OUTPUT_DIR = config.Master.perf_output_dir
# For the GraphingLogProcessor, the file into which it will save a list
# of graph names for use by the JS doing the plotting.
GRAPH_LIST = config.Master.perf_graph_list
# Maximum number of individual log entries per step before we start
# abbreviating.
_STEP_MAX_LOGS = 50
# Label used to indicate more logs.
_STEP_MORE_LOGS_LABEL = 'More Logs'
# --------------------------------------------------------------------------
# PERF TEST SETTINGS
# In each mapping below, the first key is the target and the second is the
# perf_id. The value is the directory name in the results URL.
# Configuration of most tests.
PERF_TEST_MAPPINGS = {
'Release': {
'chrome-linux32-beta': 'linux32-beta',
'chrome-linux32-stable': 'linux32-stable',
'chrome-linux64-beta': 'linux64-beta',
'chrome-linux64-stable': 'linux64-stable',
'chrome-mac-beta': 'mac-beta',
'chrome-mac-stable': 'mac-stable',
'chrome-win-beta': 'win-beta',
'chrome-win-stable': 'win-stable',
'chromium-linux-targets': 'linux-targets',
'chromium-mac-targets': 'mac-targets',
'chromium-rel-linux': 'linux-release',
'chromium-rel-linux-64': 'linux-release-64',
'chromium-rel-linux-hardy': 'linux-release-hardy',
'chromium-rel-linux-hardy-lowmem': 'linux-release-lowmem',
'chromium-rel-linux-webkit': 'linux-release-webkit-latest',
'chromium-rel-mac': 'mac-release',
'chromium-rel-mac5': 'mac-release-10.5',
'chromium-rel-mac6': 'mac-release-10.6',
'chromium-rel-mac5-v8': 'mac-release-10.5-v8-latest',
'chromium-rel-mac6-v8': 'mac-release-10.6-v8-latest',
'chromium-rel-mac6-webkit': 'mac-release-10.6-webkit-latest',
'chromium-rel-old-mac6': 'mac-release-old-10.6',
'chromium-rel-vista-dual': 'vista-release-dual-core',
'chromium-rel-vista-dual-v8': 'vista-release-v8-latest',
'chromium-rel-vista-single': 'vista-release-single-core',
'chromium-rel-vista-webkit': 'vista-release-webkit-latest',
'chromium-rel-xp': 'xp-release',
'chromium-rel-xp-dual': 'xp-release-dual-core',
'chromium-rel-xp-single': 'xp-release-single-core',
'chromium-win-targets': 'win-targets',
'nacl-lucid64-spec-x86': 'nacl-lucid64-spec-x86',
'nacl-lucid64-spec-arm': 'nacl-lucid64-spec-arm',
'nacl-lucid64-spec-trans': 'nacl-lucid64-spec-trans',
},
'Debug': {
'chromium-dbg-linux': 'linux-debug',
'chromium-dbg-win': 'win-debug',
'chromium-dbg-mac': 'mac-debug',
'chromium-dbg-xp': 'xp-debug',
'chromium-dbg-linux-try': 'linux-try-debug',
},
}
def __init__(self, command=None, show_perf=False, perf_id=None,
perf_report_url_suffix=None, target=None, active_master=None,
*args, **kwargs):
buildstep.LogLineObserver.__init__(self, *args, **kwargs)
self.command = command
self.sections = []
self.annotate_status = builder.SUCCESS
self.halt_on_failure = False
self.honor_zero_return_code = False
self.cursor = None
self.fatal_errors = [] # a list of fatal error descriptions
self.show_perf = show_perf
self.perf_id = perf_id
self.perf_report_url_suffix = perf_report_url_suffix
self.target = target
self.active_master = active_master
self.bb_triggering_service = None
def record_fatal_error(self, description):
self.fatal_errors.append(description)
def initialSection(self):
"""Initializes the annotator's sections.
Annotator uses a list of dictionaries which hold information stuch as status
and logs for each added step. This method populates the section list with an
entry referencing the original buildbot step."""
if self.sections:
return
# Add a log section for output before the first section heading.
preamble = self.command.addLog('preamble')
self.addSection(self.command.name, self.command.step_status)
self.sections[0]['log'] = preamble
self.sections[0]['started'] = util.now()
self.cursor = self.sections[0]
def sectionIsPreamble(self, section):
return section is self.sections[0]
def cursorIsPreamble(self):
return self.sectionIsPreamble(self.cursor)
def ensureCursorIsNotPreamble(self):
if self.cursorIsPreamble():
raise ValueError('This operation is not supported when the cursor is '
'set to preamble.')
def describe(self):
"""Used for the 'original' step, when updated by buildbot's getText().
This is needed to ensure any STEP_TEXT annotations don't get overwritten
when the step is finished and buildbot calls a final getText()."""
self.initialSection()
return self.sections[0]['step_text']
def cleanupSteps(self, exclude_async_pending=False):
"""Prepares steps for build finalization.
Closes open started steps and marks any unfinished steps as failure (except
for parent step).
Args:
exclude_async_pending - if True, do not finish steps that have async ops.
"""
self.closeSections()
for section in self.sections[1:]:
if (section['step'].isStarted() and not section['step'].isFinished() and
not (exclude_async_pending and section['async_ops'])):
reason = 'step was unfinished at finalization.'
self.finishStep(section, status=builder.FAILURE, reason=reason)
def ensureStepIsStarted(self, section):
if not section['step'].isStarted():
self.startStep(section)
def addAsyncOpToCursor(self, deferred, description):
section = self.cursor
if section['closed']:
raise Exception('Can\'t add an async operation to a closed step')
section['async_ops'].append({
'deferred': deferred,
'description': description,
})
def finishStep(self, section, status=None, reason=None):
"""Mark the specified step as 'finished.'"""
# The initial section will be closed by self.finished when runCommand
# completes, so finishStep should not close it.
assert not self.sectionIsPreamble(section), ('The initial section cannot '
'be finalized at annotator '
'level.')
status_map = {
builder.SUCCESS: 'SUCCESS',
builder.WARNINGS: 'WARNINGS',
builder.FAILURE: 'FAILURE',
builder.EXCEPTION: 'EXCEPTION',
builder.RETRY: 'RETRY',
builder.SKIPPED: 'SKIPPED',
}
# Update status if set as an argument.
if status is not None:
section['status'] = status
else:
# Wasn't set as an argument, so we know it came from annotations.
if not reason:
if section['status'] == builder.SUCCESS:
reason = 'step finished normally.'
else:
reason = 'parsed annotations marked step as %s.' % status_map[
section['status']]
self.ensureStepIsStarted(section)
# If we have a 'more' log, finalize it.
if self._STEP_MORE_LOGS_LABEL in section['annotated_logs']:
self.finalizeLogLines(section, self._STEP_MORE_LOGS_LABEL)
# Final update of text.
updateText(section)
# Add timing info.
section['ended'] = section.get('ended', util.now())
started = section['started']
ended = section['ended']
msg = '\n\n' + '-' * 80 + '\n'
msg += '\n'.join([
'started: %s' % time.ctime(started),
'ended: %s' % time.ctime(ended),
'duration: %s' % util.formatInterval(ended - started),
'status: %s' % status_map[section['status']],
'status reason: %s' % reason,
'', # So we get a final \n
])
section['log'].addHeader(msg)
# Change status (unless handling the preamble).
if len(self.sections) != 1:
section['step'].stepFinished(section['status'])
# Finish log.
section['log'].finish()
def finishCursor(self, status=None, reason=None):
"""Mark the step at the current cursor as finished."""
# Potentially start initial section here, as initial section might have
# no output at all.
self.initialSection()
self.finishStep(self.cursor, status=status, reason=reason)
def closeSection(self, section):
"""Closes the step and finalizes when async_ops complete."""
if section['closed']:
raise AssertionError('Can\'t close a closed step: %r' % section['name'])
if self.sectionIsPreamble(section):
# The initial section will be closed by self.finished when runCommand
# completes.
raise AssertionError('The initial section cannot be closed')
section['closed'] = True
if section['step'].isFinished():
return
# Everything was fine on the slave side,
# so the final result depends on async operations.
async_ops = section['async_ops']
if async_ops:
op_list = '\n'.join('* %s' % o['description']
for o in async_ops)
msg = 'Will wait till async operations complete:\n%s\n\n' % (op_list,)
section['log'].addStdout(msg)
d = defer.DeferredList([o['deferred'] for o in async_ops])
def finish(results):
try:
reasons = []
status = section['status']
for succeeded, defer_result in results:
if succeeded:
# callback was called.
op_result, reason = defer_result
else:
# errback was called
op_result = builder.FAILURE
reason = defer_result
status = BuilderStatus.combine(status, op_result)
if reason is not None:
reasons.append(reason)
if not section['step'].isFinished():
reason = '\n'.join(map(str, reasons))
self.finishStep(section, status=status, reason=reason)
self.annotate_status = BuilderStatus.combine(self.annotate_status,
status)
except Exception:
trace = traceback.format_exc(limit=20)
if section['step'].isFinished():
log.msg(
'failed to process async op results of step %r: %s' % (
section['name'], trace))
else:
self.finishStep(section, status=builder.EXCEPTION, reason=trace)
d.addCallback(finish)
def closeCursor(self):
self.closeSection(self.cursor)
def closeSections(self):
"""Closes open started sections."""
for section in self.sections[1:]:
if section['step'].isStarted() and not section['closed']:
self.closeSection(section)
def errLineReceived(self, line):
self.handleOutputLine(line)
def outLineReceived(self, line):
self.handleOutputLine(line)
# Override logChunk to intercept headers and to prevent more than one line's
# worth of data from being processed in a chunk, so we can direct incomplete
# chunks to the right sub-log (so we get output promptly and completely).
def logChunk(self, build, step, logmsg, channel, text):
for line in text.splitlines(True):
if channel == interfaces.LOG_CHANNEL_STDOUT:
self.outReceived(line)
elif channel == interfaces.LOG_CHANNEL_STDERR:
self.errReceived(line)
elif channel == interfaces.LOG_CHANNEL_HEADER:
self.headerReceived(line)
def outReceived(self, data):
buildstep.LogLineObserver.outReceived(self, data)
if self.sections:
self.ensureStepIsStarted(self.cursor)
if self.cursor['log'].finished:
# This might happen if the section was finished with a premature error
# asynchronously.
pass
else:
self.cursor['log'].addStdout(data)
def errReceived(self, data):
buildstep.LogLineObserver.errReceived(self, data)
if self.sections:
self.ensureStepIsStarted(self.cursor)
self.cursor['log'].addStderr(data)
def headerReceived(self, data):
if self.sections:
preamble = self.sections[0]
self.ensureStepIsStarted(preamble)
if preamble['log'].finished:
# Silently discard message when a log is marked as finished.
# TODO(maruel): Fix race condition?
log.msg(
'Received data unexpectedly on a finished build step log: %r' %
data)
else:
preamble['log'].addHeader(data)
def updateStepStatus(self, status):
"""Update current step status and annotation status based on a new event."""
self.annotate_status = BuilderStatus.combine(self.annotate_status, status)
self.cursor['status'] = BuilderStatus.combine(self.cursor['status'], status)
if self.halt_on_failure and self.cursor['status'] in [
builder.FAILURE, builder.EXCEPTION]:
if not self.sectionIsPreamble(self.cursor):
self.finishCursor()
self.cleanupSteps()
self.command.finished(self.cursor['status'])
def lookupCursor(self, step_name):
"""Given a step name, find the latest section with that name."""
if self.sections:
for section in self.sections[::-1]: # loop backwards in case of dup steps
if section['name'] == step_name:
return section
raise IndexError('step %s doesn\'t exist!' % step_name)
def updateCursorText(self):
"""Update the text on the waterfall at the current cursor."""
updateText(self.cursor)
def startStep(self, section):
"""Marks a section as started."""
step = section['step']
if not step.isStarted():
step.stepStarted()
section['started'] = util.now()
# parent step already has its logging set up by buildbot
if section != self.sections[0]:
stdio = step.addLog('stdio')
section['log'] = stdio
def addSection(self, step_name, step=None, legacy=False):
"""Adds a new section to annotator sections, does not change cursor."""
if not step:
step = self.command.step_status.getBuild().addStepWithName(step_name)
step.setText([])
self.sections.append({
'name': step_name,
'step': step,
'closed': False,
'log': None,
'annotated_logs': {},
'finished_logs': [],
'status': builder.SUCCESS,
'links': [],
'step_summary_text': [],
# step_text is a list of lines that will printed after step name.
'step_text': [],
'started': None,
'async_ops': [],
'legacy': legacy,
})
return self.sections[-1]
def _PerfStepMappings(self, show_results, perf_id, test_name, suffix=None):
"""Looks up test IDs in PERF_TEST_MAPPINGS and returns test info."""
report_link = None
output_dir = None
perf_name = None
if show_results:
perf_name = perf_id
if (self.target in self.PERF_TEST_MAPPINGS and
perf_id in self.PERF_TEST_MAPPINGS[self.target]):
perf_name = self.PERF_TEST_MAPPINGS[self.target][perf_id]
if not suffix:
suffix = self.PERF_REPORT_URL_SUFFIX
report_link = '%s/%s/%s/%s' % (self.PERF_BASE_URL, perf_name, test_name,
suffix)
output_dir = '%s/%s/%s' % (self.PERF_OUTPUT_DIR, perf_name, test_name)
return report_link, output_dir, perf_name
def _SaveGraphInfo(self, newgraphdata, output_dir):
EXECUTABLE_FILE_PERMISSIONS = int('755', 8)
graph_filename = os.path.join(output_dir, self.GRAPH_LIST)
try:
graph_file = open(graph_filename)
except IOError, e:
if e.errno != errno.ENOENT:
raise
graph_file = None
graph_list = []
if graph_file:
try:
# We keep the original content of graphs.dat to avoid accidentally
# removing graphs when a test encounters a failure.
graph_list = json.load(graph_file)
except ValueError:
graph_file.seek(0)
logging.error('Error parsing %s: \'%s\'' % (self.GRAPH_LIST,
graph_file.read().strip()))
graph_file.close()
# We need the graph names from graph_list so we can skip graphs that already
# exist in graph_list.
graph_names = [x['name'] for x in graph_list]
newgraphs = {}
try:
newgraphs = json.loads(newgraphdata)
except ValueError:
logging.error('Error parsing incoming \'%s\'' % (self.GRAPH_LIST))
# Group all of the new graphs into their own list, ...
new_graph_list = []
for graph_name, graph in newgraphs.iteritems():
if graph_name in graph_names:
continue
new_graph_list.append(graph)
new_graph_list[-1]['name'] = graph_name
# sort them by not-'important', since True > False, and by graph_name, ...
new_graph_list.sort(lambda x, y: cmp((not x['important'], x['name']),
(not y['important'], y['name'])))
# then add the new graph list to the main graph list.
graph_list.extend(new_graph_list)
# Write the resulting graph list.
graph_file = open(graph_filename, 'w')
json.dump(graph_list, graph_file)
graph_file.close()
os.chmod(graph_filename, EXECUTABLE_FILE_PERMISSIONS)
def addLinkToCursor(self, link_label, link_url):
parts = link_label.split('-->', 1)
link_alias = None
if len(parts) == 2:
link_alias, link_label = parts
self.cursor['links'].append((link_label, link_url, link_alias))
if not link_alias:
self.cursor['step'].addURL(link_label, link_url)
else:
self.cursor['step'].addAlias(link_label, link_url, text=link_alias)
def handleOutputLine(self, line):
"""This is called once with each line of the test log."""
# Handle initial setup here, as step_status might not exist yet at init.
self.initialSection()
try:
annotator.MatchAnnotation(line.rstrip(), self)
except annotator.BadAnnotation as ex:
logging.warning('failed to match annotation: %s', ex)
def addLogLines(self, log_label, log_lines):
self.cursor['annotated_logs'].setdefault(log_label, []).extend(log_lines)
def finalizeLogLines(self, section, log_label):
section['finished_logs'].append(log_label)
logs = section['annotated_logs'].pop(log_label, ())
addLogToStep(section['step'], log_label, '\n'.join(logs))
def SET_BUILD_PROPERTY(self, name, value):
# Support: @@@SET_BUILD_PROPERTY@<name>@<json>@@@
# Sets the property and indicates that it came from an annoation on the
# current step.
self.command.build.setProperty(name, json.loads(value), 'Annotation(%s)'
% self.cursor['name'])
def STEP_LOG_LINE(self, log_label, log_line):
# Support: @@@STEP_LOG_LINE@<label>@<line>@@@ (add log to step)
# Appends a line to the log's array. When STEP_LOG_END is called,
# that will finalize the log and call addCompleteLog().
self.addLogLines(log_label, (log_line,))
def STEP_LOG_END(self, log_label):
# Support: @@@STEP_LOG_END@<label>@@@ (finalizes log to step)
if (len(self.cursor['finished_logs'])+1) >= self._STEP_MAX_LOGS:
logs = self.cursor['annotated_logs'].pop(log_label, ())
self.addLogLines(
self._STEP_MORE_LOGS_LABEL,
['%s: %s' % (log_label, log_line) for log_line in logs])
else:
self.finalizeLogLines(self.cursor, log_label)
def STEP_LOG_END_PERF(self, log_label, perf_dashboard_name):
# Support: @@@STEP_LOG_END_PERF@<label>@<line>@@@
# (finalizes log to step, marks it as being a perf step
# requiring logs to be stored on the master)
current_logs = self.cursor['annotated_logs']
log_text = '\n'.join(current_logs.get(log_label, [])) + '\n'
report_link = None
output_dir = None
if self.perf_id:
report_link, output_dir, _ = self._PerfStepMappings(
self.show_perf, self.perf_id, perf_dashboard_name,
self.perf_report_url_suffix)
PERF_EXPECTATIONS_PATH = ('../../scripts/master/log_parser/'
'perf_expectations/')
perf_output_dir = None
if output_dir:
output_dir = chromium_utils.AbsoluteCanonicalPath(output_dir)
perf_output_dir = chromium_utils.AbsoluteCanonicalPath(output_dir,
PERF_EXPECTATIONS_PATH)
if report_link and output_dir:
MakeOutputDirectory(output_dir)
if log_label == self.GRAPH_LIST:
self._SaveGraphInfo(log_text, output_dir)
else:
Prepend(log_label, log_text, output_dir, perf_output_dir)
def STEP_LINK(self, link_label, link_url):
# Support: @@@STEP_LINK@<name>@<url>@@@ (emit link)
# Also support depreceated @@@link@<name>@<url>@@@
self.addLinkToCursor(link_label, link_url)
def STEP_STARTED(self):
# Support: @@@STEP_STARTED@@@ (start a step at cursor)
self.ensureCursorIsNotPreamble()
self.startStep(self.cursor)
def STEP_CLOSED(self):
# Support: @@@STEP_CLOSED@@@
self.ensureCursorIsNotPreamble()
self.closeCursor()
self.cursor = self.sections[0]
def STEP_WARNINGS(self):
# Support: @@@STEP_WARNINGS@@@ (warn on a stage)
# Also support deprecated @@@BUILD_WARNINGS@@@
self.updateStepStatus(builder.WARNINGS)
def STEP_FAILURE(self):
# Support: @@@STEP_FAILURE@@@ (fail a stage)
# Also support deprecated @@@BUILD_FAILED@@@
if self.halt_on_failure:
self.ensureCursorIsNotPreamble()
self.updateStepStatus(builder.FAILURE)
def STEP_EXCEPTION(self):
# Support: @@@STEP_EXCEPTION@@@ (exception on a stage)
# Also support deprecated @@@BUILD_FAILED@@@
if self.halt_on_failure:
self.ensureCursorIsNotPreamble()
self.updateStepStatus(builder.EXCEPTION)
def HALT_ON_FAILURE(self):
# Support: @@@HALT_ON_FAILURE@@@ (halt if a step fails immediately)
self.halt_on_failure = True
def HONOR_ZERO_RETURN_CODE(self):
# Support: @@@HONOR_ZERO_RETURN_CODE@@@ (succeed on 0 return, even if some
# steps have failed)
self.honor_zero_return_code = True
def STEP_CLEAR(self):
# Support: @@@STEP_CLEAR@@@ (reset step description)
self.cursor['step_text'] = []
self.updateCursorText()
def STEP_SUMMARY_CLEAR(self):
# Support: @@@STEP_SUMMARY_CLEAR@@@ (reset step summary)
self.cursor['step_summary_text'] = []
self.updateCursorText()
def STEP_TEXT(self, msg):
# Support: @@@STEP_TEXT@<msg>@@@
self.cursor['step_text'].append(msg)
self.updateCursorText()
def STEP_SUMMARY_TEXT(self, msg):
# Support: @@@STEP_SUMMARY_TEXT@<msg>@@@
self.cursor['step_summary_text'].append(msg)
self.updateCursorText()
def STEP_NEST_LEVEL(self, level):
# Support: @@@STEP_NEST_LEVEL@<level>@@@
self.cursor['step'].setNestLevel(int(level))
def SEED_STEP(self, step_name):
# Support: @@@SEED_STEP <stepname>@@@ (seed a new section)
self.addSection(step_name)
def SEED_STEP_TEXT(self, step_name, step_text):
# Support: @@@SEED_STEP_TEXT@<stepname>@<step text@@@ (change step text of a
# seeded step)
target = self.lookupCursor(step_name)
target['step_text'].append(step_text)
updateText(target)
def STEP_CURSOR(self, step_name):
# Support: @@@STEP_CURSOR <stepname>@@@ (set cursor to specified section)
self.cursor = self.lookupCursor(step_name)
def BUILD_STEP(self, step_name):
# Support: @@@BUILD_STEP <step_name>@@@ (start a new section)
# Ignore duplicate consecutive step labels (for robustness).
if step_name != self.sections[-1]['name']:
# When using BUILD_STEP, close the last section, unless it is a preamble.
if not (self.cursor['step'].isFinished() or self.cursorIsPreamble()):
if self.cursor['legacy']:
self.closeCursor()
section = self.addSection(step_name, legacy=True)
self.startStep(section)
self.cursor = section
def normalizeChangeSpec(self, change):
assert isinstance(change, dict), 'Change must be a dict'
change = change.copy()
# Sort files.
files = change.get('files')
if files:
change['files'] = sorted(files)
return change
def STEP_TRIGGER(self, spec):
# Support: @@@STEP_TRIGGER <json spec>@@@ (trigger build(s)).
builder_names = ['<unknown>']
section = self.cursor
critical = True
def handle_exception(ex):
if not section['step'].isFinished():
self.finishStep(section, builder.EXCEPTION, ex)
if critical:
self.record_fatal_error(
'step %s: could not trigger builds %r: %s' %
(section['name'], builder_names, ex))
raise ex
try:
spec = json.loads(spec)
critical = spec.get('critical', True)
bucket = spec.get('bucket')
builder_names = spec.get('builderNames')
properties = spec.get('properties') or {}
changes = spec.get('changes')
tags = spec.get('tags') or []
if changes:
assert isinstance(changes, list)
changes = map(self.normalizeChangeSpec, changes)
if not builder_names:
raise ValueError('builderNames is not specified: %r' % (spec,))
build = self.command.build
build_is_from_buildbucket = bool(
build.getProperties().getProperty(buildbucket.common.INFO_PROPERTY))
trigger_via_buildbucket = bucket or build_is_from_buildbucket
properties = self.getPropertiesForTriggeredBuild(build.getProperties(),
properties)
if trigger_via_buildbucket:
d = self.triggerBuildsViaBuildBucket(
bucket, builder_names, properties, tags, changes)
else:
d = self.triggerBuildsLocally(builder_names, properties, changes)
# addAsyncOpToCursor expects a deferred to return a build result. If a
# buildset is added, then it is a success. This lambda function returns a
# tuple, which is received by addAsyncOpToCursor.
d.addCallback(lambda _: (builder.SUCCESS, None))
d.addErrback(handle_exception)
self.addAsyncOpToCursor(
d,
'Triggering build(s) on %s' % (', '.join(builder_names),))
except Exception as ex:
handle_exception(ex)
@staticmethod
def getPropertiesForTriggeredBuild(current_properties, new_properties):
props = {
'parent_buildername': current_properties.getProperty('buildername'),
'parent_buildnumber': current_properties.getProperty('buildnumber'),
}
props.update(new_properties)
return props
@defer.inlineCallbacks
def insertSourceStamp(self, master, changes_spec):
"""Inserts a new SourceStamp.
For each change in changes_spec, finds an existing or creates a new Change
object. Then creates a SourceStamp with these changes.
Args:
master: an instance of buildbot.master.BuildMaster.
changes_spec (list of dict): a list of change dicts, where each contains
keyword arguments for
buildbot.db.changes.ChangesConnectorComponent.addChange() function,
except when_timestamp is int (seconds since Unix Epoch) instead of
datetime. The first change is used to populate source stamp properties.
"""
def find_changes_by_revision(revision):
"""Searches for Changes in db by |revision| and returns change ids."""
def find(conn):
table = master.db.model.changes
q = sa.select([table.c.changeid]).where(table.c.revision == revision)
return [c.changeid for c in conn.execute(q)]
return master.db.pool.do(find)
@defer.inlineCallbacks
def get_change_by_id(change_id):
chdict = yield master.db.changes.getChange(change_id)
change = yield Change.fromChdict(master, chdict)
defer.returnValue(change)
def does_change_match(change, spec):
chdict = {
'branch': change.branch,
'category': change.category,
'author': change.getShortAuthor(),
'comments': change.comments,
'revision': change.revision,
'when_timestamp': change.when, # int, seconds since UNIX epoch.
'files': sorted(change.files),
'revlink': getattr(change, 'revlink', None),
'properties': change.properties.asDict(),
'repository': getattr(change, 'repository', None),
'project': getattr(change, 'project', None),
}
for key, value in spec.iteritems():
assert key in chdict, 'Unexpected change spec key: %s' % key
if chdict[key] != value:
return False
return True
@defer.inlineCallbacks
def getChangeForSpec(spec):
assert 'revision' in spec, 'No revision in change spec'
candidates = yield find_changes_by_revision(spec['revision'])
for change_id in candidates:
change = yield get_change_by_id(change_id)
if does_change_match(change, spec):
defer.returnValue(change)
return
add_args = spec.copy()
when_timestamp = add_args.get('when_timestamp')
if isinstance(when_timestamp, int):
add_args['when_timestamp'] = datetime.utcfromtimestamp(when_timestamp)
change_id = yield master.db.changes.addChange(**add_args)
change = yield get_change_by_id(change_id)
defer.returnValue(change)
changes = []
for spec in changes_spec:
change = yield getChangeForSpec(spec)
changes.append(change)
main_change = changes[0]
ssid = yield master.db.sourcestamps.addSourceStamp(
branch=main_change.branch,
revision=main_change.revision,
repository=main_change.repository,
project=main_change.project,
changeids=[c.number for c in changes],
)
defer.returnValue(ssid)
@defer.inlineCallbacks
def triggerBuildsViaBuildBucket(
self, bucket_name, builder_names, properties, tags, changes_spec=None):
"""Schedules builds on buildbucket."""
if self.active_master is None:
raise buildbucket.Error(
'In order to trigger builds through buildbucket, '
'ActiveMaster must be passed to AnnotatorFactory')
build = self.command.build
section = self.cursor
if not self.bb_triggering_service:
self.bb_triggering_service = yield (
buildbucket.trigger.get_triggering_service(self.active_master))
if changes_spec:
changes = map(
buildbucket.trigger.change_from_change_spec, changes_spec)
else:
def changeToBuildBucketChange(change):
d = change.asDict()
d['author'] = d.get('who')
d['files'] = change.files[:]
d['when_timestamp'] = d.get('when')
return buildbucket.trigger.change_from_change_spec(d)
source_stamp = getSourceStamp(build)
changes = map(changeToBuildBucketChange, source_stamp.changes)
for builder_name in builder_names:
result = yield self.bb_triggering_service.trigger(
build, bucket_name, builder_name, properties, tags, changes)
response = result['response']
section['log'].addStdout(
'BuildBucket.put API response: %s\n' % json.dumps(response, indent=4))
build_url = result.get('build_url')
if build_url:
section['log'].addStdout('See %s\n\n' % build_url)
@defer.inlineCallbacks
def triggerBuildsLocally(
self, builder_names, properties, changes_spec=None):
"""Creates a new buildset."""
build = self.command.build
master = build.builder.botmaster.parent
if changes_spec:
# Changes have been specified explicitly.
ssid = yield self.insertSourceStamp(master, changes_spec)
else:
# Use the same source stamp.
source_stamp = getSourceStamp(build)
ssid = yield source_stamp.getSourceStampId(master)
bsid, brids = yield master.addBuildset(
ssid=ssid,
reason='Triggered by %s' % build.builder.name,
# Specify property source.
properties={k: (v, 'ParentBuild') for k, v in properties.iteritems()},
builderNames=builder_names)
log.msg('Triggered a buildset %s with builders %s' % (bsid, builder_names))
defer.returnValue((bsid, brids))
def handleReturnCode(self, return_code):
succeeded = False
if self.fatal_errors:
# Even if return_code==0 and self.honor_zero_return_code is True
# we can't do much when there are fatal errors
preamble = self.sections[0]
fatal_errors_str = '\n'.join(self.fatal_errors)
fatal_error_log = preamble['step'].addLog('fatal_errors')
fatal_error_log.addStdout(fatal_errors_str)
fatal_error_log.finish()
self.annotate_status = builder.EXCEPTION
if not self.cursorIsPreamble():
self.finishCursor(self.annotate_status,
reason='Fatal errors: %s' % fatal_errors_str)
elif return_code == 0:
succeeded = True
# Do not close the initial section because it will be closed by
# self.finished when runCommand completes.
if not self.cursorIsPreamble():
self.closeCursor()
if self.honor_zero_return_code:
self.annotate_status = builder.SUCCESS
else:
# Treat all non-zero return codes as failure.
# We could have a special return code for warnings/exceptions, however,
# this might conflict with some existing use of a return code.
# Besides, applications can always intercept return codes and emit
# STEP_* tags.
# Only change annotate status if it'd otherwise indicate success.
# This helps propagate purple (exception) step status to to-level
# purple build status as opposed to red.
if self.annotate_status in (builder.SUCCESS, builder.WARNINGS):
self.annotate_status = builder.FAILURE
if not self.cursorIsPreamble():
self.finishCursor(self.annotate_status,
reason='return code was %d.' % return_code)
self.cleanupSteps(exclude_async_pending=succeeded)
def stepsToWait(self):
return [s for s in self.sections[1:]
if s['step'].isStarted() and not s['step'].isFinished()]
def waitForSteps(self):
sections_to_wait = self.stepsToWait()
# Assume this function is called after annotated script execution completes
# for assertion only.
for section in sections_to_wait:
assert section['async_ops'], ('The annotated script finished execution '
'but a step without async ops was not '
'closed')
step_deferreds = [s['step'].waitUntilFinished()
for s in sections_to_wait]
return defer.DeferredList(step_deferreds)
class AnnotatedCommand(ProcessLogShellStep):
"""Buildbot command that knows how to display annotations."""
def __init__(self, target=None, active_master=None, *args, **kwargs):
self.active_master = active_master
clobber = ''
perf_id = None
perf_report_url_suffix = None
show_perf = None
if 'factory_properties' in kwargs:
if kwargs['factory_properties'].get('clobber'):
clobber = '1'
perf_id = kwargs['factory_properties'].get('perf_id')
perf_report_url_suffix = kwargs['factory_properties'].get(
'perf_report_url_suffix')
show_perf = kwargs['factory_properties'].get('show_perf_results')
# kwargs is passed eventually to RemoteShellCommand(**kwargs).
# This constructor (in buildbot/process/buildstep.py) does not
# accept unknown arguments (like factory_properties)
del kwargs['factory_properties']
# Inject standard tags into the environment.
env = {
'BUILDBOT_BLAMELIST': WithProperties('%(blamelist:-[])s'),
'BUILDBOT_BRANCH': WithProperties('%(branch:-None)s'),
'BUILDBOT_BUILDBOTURL': WithProperties('%(buildbotURL:-None)s'),
'BUILDBOT_BUILDERNAME': WithProperties('%(buildername:-None)s'),
'BUILDBOT_BUILDNUMBER': WithProperties('%(buildnumber:-None)s'),
'BUILDBOT_CLOBBER': clobber or WithProperties('%(clobber:+1)s'),
'BUILDBOT_GOT_REVISION': WithProperties('%(got_revision:-None)s'),
'BUILDBOT_MASTERNAME': WithProperties('%(mastername:-None)s'),
'BUILDBOT_REVISION': WithProperties('%(revision:-None)s'),
'BUILDBOT_SCHEDULER': WithProperties('%(scheduler:-None)s'),
'BUILDBOT_SLAVENAME': WithProperties('%(slavename:-None)s'),
}
# Apply the passed in environment on top.
old_env = kwargs.get('env') or {}
env.update(old_env)
# Change passed in args (ok as a copy is made internally).
kwargs['env'] = env
ProcessLogShellStep.__init__(self, *args, **kwargs)
self.script_observer = AnnotationObserver(
self, show_perf=show_perf, perf_id=perf_id,
perf_report_url_suffix=perf_report_url_suffix, target=target,
active_master=active_master)
self.addLogObserver('stdio', self.script_observer)
def describe(self, done=False):
if self.step_status and self.step_status.isStarted():
observer_text = self.script_observer.describe()
else:
observer_text = []
if observer_text:
return observer_text
else:
return ProcessLogShellStep.describe(self, done)
def _removePreamble(self):
"""Remove preamble if there is only section.
'stdio' will be identical to 'preamble' if there is only one annotator
section, so it's redundant to show both on the waterfall.
"""
if len(self.script_observer.sections) == 1:
self.step_status.logs = [x for x in self.step_status.logs if
x.name != 'preamble']
def interrupt(self, reason):
if not self.script_observer.cursorIsPreamble():
self.script_observer.finishCursor(builder.EXCEPTION,
reason='step was interrupted.')
self.script_observer.cleanupSteps()
self._removePreamble()
return ProcessLogShellStep.interrupt(self, reason)
def evaluateCommand(self, cmd):
observer_result = self.script_observer.annotate_status
# Check if ProcessLogShellStep detected a failure or warning also.
log_processor_result = ProcessLogShellStep.evaluateCommand(self, cmd)
return BuilderStatus.combine(observer_result, log_processor_result)
def scriptComplete(self, cmd):
self.script_observer.handleReturnCode(cmd.rc)
self._removePreamble()
def runCommand(self, command):
"""Runs command and waits for emitted steps to finish."""
d = ProcessLogShellStep.runCommand(self, command)
def onCommandFinished(command_result):
"""Gets executed after remote command completes. Handles command_result
and starts to wait for remaining steps to finish. Returns command_result
as Deferred."""
self.scriptComplete(command_result)
steps_d = self.script_observer.waitForSteps()
# Ignore the waitForSteps' result and return the original result,
# so the caller of runCommand receives command_result.
steps_d.addCallback(lambda *_: command_result)
return steps_d
d.addCallback(onCommandFinished)
return d