The first draft of ported AnnotatedCommand.
diff --git a/zorg/buildbot/commands/AnnotatedCommand.py b/zorg/buildbot/commands/AnnotatedCommand.py
index 8ec5780..ec62bf0 100644
--- a/zorg/buildbot/commands/AnnotatedCommand.py
+++ b/zorg/buildbot/commands/AnnotatedCommand.py
@@ -1,45 +1,112 @@
-#!/usr/bin/python
-# Copyright (c) 2011 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.chromium.TXT file.
+#NOTE: Updated to use with Buildbot 2.8.
+# pylint: disable=C0103
-import copy
+#TODO: Handle errors in op defers.
+#TODO: Redesign of waiting ops completition on the command complete event.
+
import re
-import time
+from twisted.internet import defer
+from twisted.python import failure
+from twisted.python import log as logging
-from buildbot import util
+from buildbot.plugins import util
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
+
+# 1: Create step object.
+# 2: yield step.addStep()
+# ...
+# N+1: yield step.finishStep()
+
+class VirtBuildStep(buildstep.BuildStep):
+ def __init__(self, parent_step, *args, **kwargs):
+ self.parent_step = parent_step
+ buildstep.BuildStep.__init__(self, *args, **kwargs)
+
+ logging.msg("VirtBuildStep:__init__: %s" % self.name)
+
+ # Step is really unpredictable
+ self.useProgress = False
+
+ self.build = self.parent_step.build
+ self.master = self.parent_step.build.master
+ self.worker = self.parent_step.worker
+
+ self.setText = lambda text: self.realUpdateSummary()
+ self.setText2 = lambda text: self.realUpdateSummary()
+
+ self.finished_step = False
+
+ self.stdio_log = None
+ self.step_text = ""
+ self.step_summary_text = []
+
+ @defer.inlineCallbacks
+ def addStep(self):
+ logging.msg("VirtBuildStep: adding step with name '%s'" % self.name)
+
+ yield buildstep.BuildStep.addStep(self)
+ # Put ourselves into a list of processed steps of the build object.
+ self.build.executedSteps.append(self)
+ # Init result to SUCCESS by default.
+ self.results = builder.SUCCESS
+ self.setText([self.name])
+ self.stdio_log = yield self.addLog('stdio')
+
+ logging.msg("VirtBuildStep: added step '%s': stepid=%s, buildid=%s" % (
+ self.name, self.stepid, self.build.buildid))
-class BuilderStatus(object):
- # Order in asceding severity.
- BUILD_STATUS_ORDERING = [
- builder.SUCCESS,
- builder.WARNINGS,
- builder.FAILURE,
- builder.EXCEPTION,
- ]
+ @defer.inlineCallbacks
+ def finishStep(self, results):
+ if self.finished_step:
+ return
+ self.finished_step = True
+ if results is not None:
+ self.results = results
+ hidden = False
+ logging.msg("VirtBuildStep: finish step '%s': stepid=%s, buildid=%s, results=%s" % (
+ self.name, self.stepid, self.build.buildid, self.results))
+ yield self.master.data.updates.finishStep(self.stepid, self.results,
+ hidden)
+ # finish unfinished logs
+ all_finished = yield self.finishUnfinishedLogs()
+ logging.msg("VirtBuildStep: finish step '%s': stepid=%s, buildid=%s" % (
+ self.name, self.stepid, self.build.buildid))
+ if not all_finished:
+ self.results = 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]
+ #NOTE: we should not run this step in regular way.
+ def run(self):
+ self.results = builder.EXCEPTION
+ return self.results
+
+class BuilderStatus:
+ # 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.
+ """ Step that can process log files.
Delegates actual processing to log_processor, which is a subclass of
process_log.PerformanceLogParser.
@@ -56,366 +123,867 @@
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 __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.
+ def start(self):
+ """Overridden shell.ShellCommand.start method.
- Adds a link for the activity that points to report ULR.
+ Adds a link for the activity that points to report ULR.
+ """
+
+ # got_revision could be poisoned by checking out the script itself.
+ # So, let's assume that we will get the exactly the same revision
+ # this build has been triggered for, and let the script report
+ # the revision it checked out.
+ self.setProperty('got_revision', self.getProperty('revision'), 'Source')
+
+ 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.
+ """
+ revision = None
+ try:
+ revision = self.build.getProperty('got_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 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())
+
+ 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 = 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())
+
+
+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>. End the current step, marking with last available
+ status.
+
+ @@@STEP_LINK@<label>@<url>@@@
+ Add a link with label <label> linking to <url> to the current stage.
+
+ @@@STEP_WARNINGS@@@
+ Mark the current step as having warnings (oragnge).
+
+ @@@STEP_FAILURE@@@
+ Mark the current step as having failed (red).
+
+ @@@STEP_EXCEPTION@@@
+ Mark the current step as having exceptions (magenta).
+
+ @@@STEP_CLEAR@@@
+ Reset the text description of the current step.
+
+ @@@STEP_SUMMARY_CLEAR@@@
+ Reset the text summary of the current step.
+
+ @@@STEP_TEXT@<msg>@@@
+ Append <msg> to the current step text.
+
+ @@@STEP_SUMMARY_TEXT@<msg>@@@
+ Append <msg> to the step summary (appears on top of the waterfall).
+
+ @@@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.
+
+ 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>@@@
"""
- # got_revision could be poisoned by checking out the script itself.
- # So, let's assume that we will get the exactly the same revision
- # this build has been triggered for, and let the script report
- # the revision it checked out.
- self.setProperty('got_revision', self.getProperty('revision'), 'Source')
+ _re_step_link = re.compile(r'^@@@(STEP_LINK|link)@(?P<link_label>.*)@(?P<link_url>.*)@@@', re.M)
+ _re_step_warnings = re.compile(r'^@@@(STEP_WARNINGS|BUILD_WARNINGS)@@@', re.M)
+ _re_step_failure = re.compile(r'^@@@(STEP_FAILURE|BUILD_FAILED)@@@', re.M)
+ _re_step_exception = re.compile(r'^@@@(STEP_EXCEPTION|BUILD_EXCEPTION)@@@', re.M)
+ _re_halt_on_failure = re.compile(r'^@@@HALT_ON_FAILURE@@@', re.M)
+ _re_honor_zero_rc = re.compile(r'^@@@HONOR_ZERO_RETURN_CODE@@@', re.M)
+ _re_step_clear = re.compile(r'^@@@STEP_CLEAR@@@', re.M)
+ _re_step_summary_clear = re.compile(r'^@@@STEP_SUMMARY_CLEAR@@@', re.M)
+ _re_step_text = re.compile(r'^@@@STEP_TEXT@(?P<text>.*)@@@', re.M)
+ _re_step_summary_step = re.compile(r'^@@@STEP_SUMMARY_TEXT@(?P<text>.*)@@@', re.M)
+ _re_build_step = re.compile(r'^@@@BUILD_STEP (?P<name>.*)@@@', re.M)
- self._CreateReportLinkIfNeccessary()
- shell.ShellCommand.start(self)
+ def __init__(self, command=None, *args, **kwargs):
+ buildstep.LogLineObserver.__init__(self, *args, **kwargs)
+ self.command = command
+ self.annotate_status = builder.SUCCESS
+ self.halt_on_failure = False
+ self.honor_zero_return_code = False
- def _GetRevision(self):
- """Returns the revision number for the build.
+ # A sequence of synchronius operations.
+ self._delayed_op_queue = []
+ self._proc_op_in_queue = None
+ # Current active virtual step, generated by annotation.
+ # We cannot process more than one step at the time, so
+ # having a single object for that should be enough.
+ self._active_vstep = None
- 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.
- """
- revision = None
- try:
- revision = self.build.getProperty('got_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
+ self._dlock = defer.DeferredLock()
- 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._re_set = [
+ # Support: @@@STEP_LINK@<name>@<url>@@@ (emit link)
+ # Also support depreceated @@@link@<name>@<url>@@@
+ {
+ 're' : AnnotationObserver_._re_step_link,
+ 'op' : lambda args: (
+ logging.msg("+++ op: STEP_LINK"),
+ self.queue_op(op=lambda: self.op_addStepLink(args['link_label'],
+ args['link_url']),
+ name="op_addStepLink")
+ )
+ },
+ # Support: @@@STEP_WARNINGS@@@ (warn on a stage)
+ # Also support deprecated @@@BUILD_WARNINGS@@@
+ {
+ 're' : AnnotationObserver_._re_step_warnings,
+ 'op' : lambda args: (
+ logging.msg("+++ op: STEP_WARNINGS"),
+ self.queue_op(op=lambda: self.op_updateStepStatus(builder.WARNINGS),
+ name="op_updateStepStatus")
+ )
+ },
+ # Support: @@@STEP_FAILURE@@@ (fail a stage)
+ # Also support deprecated @@@BUILD_FAILED@@@
+ {
+ 're' : AnnotationObserver_._re_step_failure,
+ 'op' : lambda args: (
+ logging.msg("+++ op: STEP_FAILURE"),
+ self.queue_op(op=lambda: self.op_updateStepStatus(builder.FAILURE),
+ name="op_updateStepStatus")
+ )
+ },
+ # Support: @@@STEP_EXCEPTION@@@ (exception on a stage)
+ # Also support deprecated @@@BUILD_FAILED@@@
+ {
+ 're' : AnnotationObserver_._re_step_exception,
+ 'op' : lambda args: (
+ logging.msg("+++ op: STEP_EXCEPTION"),
+ self.queue_op(op=lambda: self.op_updateStepStatus(builder.EXCEPTION),
+ name="op_updateStepStatus")
+ )
+ },
+ # Support: @@@HALT_ON_FAILURE@@@ (halt if a step fails immediately)
+ {
+ 're' : AnnotationObserver_._re_halt_on_failure,
+ 'op' : lambda args: (
+ logging.msg("+++ op: HALT_ON_FAILURE"),
+ self.setHaltOnFailure(True)
+ )
+ },
+ # Support: @@@HONOR_ZERO_RETURN_CODE@@@ (succeed on 0 return, even if some
+ # steps have failed)
+ {
+ 're' : AnnotationObserver_._re_honor_zero_rc,
+ 'op' : lambda args: (
+ logging.msg("+++ op: HONOR_ZERO_RC"),
+ self.setHonorZeroReturnCode(True)
+ )
+ },
+ # Support: @@@STEP_CLEAR@@@ (reset step description)
+ {
+ 're' : AnnotationObserver_._re_step_clear,
+ 'op' : lambda args: (
+ logging.msg("+++ op: STEP_CLEAR"),
+ self.queue_op(op=lambda: self.op_updateStepText(text=None),
+ name="op_updateStepText")
+ )
+ },
+ # Support: @@@STEP_SUMMARY_CLEAR@@@ (reset step summary)
+ {
+ 're' : AnnotationObserver_._re_step_summary_clear,
+ 'op' : lambda args: (
+ logging.msg("+++ op: STEP_SUMMARY_CLEAR"),
+ self.queue_op(op=lambda: self.op_updateStepSummaryInfo(text=None),
+ name="op_updateStepSummaryInfo")
+ )
+ },
+ # Support: @@@STEP_TEXT@<msg>@@@
+ {
+ 're' : AnnotationObserver_._re_step_text,
+ 'op' : lambda args: (
+ logging.msg("+++ op: STEP_TEXT"),
+ self.queue_op(op=lambda: self.op_updateStepText(text=args['text']),
+ name="op_updateStepText")
+ )
+ },
+ # Support: @@@STEP_SUMMARY_TEXT@<msg>@@@
+ {
+ 're' : AnnotationObserver_._re_step_summary_step,
+ 'op' : lambda args: (
+ logging.msg("+++ op: STEP_SUMMARY_TEXT"),
+ self.queue_op(op=lambda: self.op_updateStepSummaryInfo(text=args['text']),
+ name="op_updateStepSummaryInfo")
+ )
+ },
+ # Support: @@@BUILD_STEP <step_name>@@@ (start a new section)
+ {
+ 're' : AnnotationObserver_._re_build_step,
+ 'op' : lambda args: (
+ logging.msg("+++ op: BUILD_STEP"),
+ self.queue_op(op=lambda: self.op_fixupActiveStep(),
+ name="op_fixupActiveStep"),
+ self.queue_op(op=lambda: self.op_startNewStep(name=args['name'].strip(),
+ logline=args['logline']),
+ name="op_startNewStep")
+ )
+ },
+ ]
- 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 = text_list + self._result_text
- return text_list
+ def queue_op(self, op, name="unspecified"):
+ # pylint: disable=unused-argument
+ #logging.msg(">>> queue_op: buildid={0}, opname='{1}', op={2}".format(
+ # self.command.build.buildid, name, op))
- 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)
+ if op is not None:
+ self._delayed_op_queue.append(op)
+ if self._proc_op_in_queue is None and len(self._delayed_op_queue) > 0:
+ #self._queue_catchup_all()
+ self._queue_catchup_locked()
- def _CreateReportLinkIfNeccessary(self):
- if self._log_processor and self._log_processor.ReportLink():
- self.addURL('results', "%s" % self._log_processor.ReportLink())
+ @defer.inlineCallbacks
+ def _queue_catchup_locked(self):
+ yield self._dlock.run(self._queue_catchup_all)
+
+ @defer.inlineCallbacks
+ def _queue_catchup_all(self):
+ #logging.msg(">>> _queue_catchup_all: buildid={0}".format(
+ # self.command.build.buildid))
+
+ while self._delayed_op_queue:
+ op = self._delayed_op_queue.pop(0)
+ if op is not None:
+ try:
+ d = defer.maybeDeferred(op)
+ except Exception:
+ d = defer.fail(failure.Failure())
+
+ # Currently processing deferred operation.
+ self._proc_op_in_queue = d
+
+ #TODO: Do we really need this callback here?
+ def processed(status):
+ return status
+ d.addCallback(processed)
+ #TODO: ???:d.addErrback
+
+ yield d
+
+ self._proc_op_in_queue = None
+
+ @defer.inlineCallbacks
+ def queue_wait(self):
+ logging.msg(">>> queue_wait: buildid={0}".format(
+ self.command.build.buildid))
+
+ yield self._queue_catchup_locked()
+ """
+ if self._proc_op_in_queue:
+ logging.msg(">>> queue_wait: buildid={0}, processing defer".format(
+ self.command.build.buildid))
+ if len(self._delayed_op_queue) == 0:
+ logging.msg(">>> queue_wait: buildid={0}, empty op queue".format(
+ self.command.build.buildid))
+ if self._proc_op_in_queue is None and len(self._delayed_op_queue) > 0:
+ yield self._queue_catchup_all()
+ """
+ def queue_clean(self):
+ self._delayed_op_queue = []
+
+ # Synchronius Operations.
+ #
+
+ # Adding stdout log for the currently active step.
+ def op_addStepStdout(self, text):
+ step = self._active_vstep
+
+ # Adding to preamble log in case we still didn't get any vsteps.
+ if step is None:
+ if self.command.preamble_log is not None:
+ self.command.preamble_log.addStdout(text)
+ return
+
+ if step.stdio_log is not None:
+ step.stdio_log.addStdout(text)
+
+ # Updating a status for the currently active step.
+ @defer.inlineCallbacks
+ def op_updateStepStatus(self, status):
+ """Update current step status and annotation status based on a new event."""
+
+ logging.msg(">>> op_updateStepStatus: buildid=%s, status=%s" % (
+ self.command.build.buildid, status))
+
+ self.annotate_status = BuilderStatus.combine(self.annotate_status, status)
+
+ step = self._active_vstep
+ if step is None:
+ logging.msg("FATAL ERROR: op_updateStepStatus: no active vstep.")
+ #TODO: return defer.fail(...)?
+ return
+
+ step.results = BuilderStatus.combine(step.results, status)
+
+ if self.halt_on_failure and step.results in [builder.FAILURE, builder.EXCEPTION]:
+ # We got fatal error, which breaks the build. Clean up all scheduled operations
+ # and finalize the build.
+ self.queue_clean()
+ yield self.op_fixupActiveStep()
+ self.command.finished(step.results)
+
+ # Updating a step text. None to clean up.
+ def op_updateStepText(self, text = None):
+ logging.msg(">>> op_updateStepText: buildid=%s, text='%s'" % (
+ self.command.build.buildid, text))
+
+ step = self._active_vstep
+ if step is None:
+ logging.msg("FATAL ERROR: op_updateStepText: no active vstep.")
+ return
+
+ step.step_text = text if text else ""
+ step.setText([step.name] + [step.step_text])
+
+ # Updating a step summary info. None to clean up.
+ def op_updateStepSummaryInfo(self, text = None):
+ logging.msg(">>> op_updateStepSummaryInfo: buildid=%s, text='%s'" % (
+ self.command.build.buildid, text))
+
+ step = self._active_vstep
+ if step is None:
+ logging.msg("FATAL ERROR: op_updateStepSummaryInfo: no active vstep.")
+ return
+
+ if text is not None:
+ step.step_summary_text.append(text)
+ else:
+ step.step_summary_text = []
+ # Reflect step status in text2.
+ if step.results == builder.EXCEPTION:
+ result = ['exception', step.name]
+ elif step.results == builder.FAILURE:
+ result = ['failed', step.name]
+ else:
+ result = []
+
+ step.setText2(result + step.step_summary_text)
+
+ def op_addStepLink(self, link_label, link_url):
+ logging.msg(">>> op_addStepLink: buildid=%s, link_label='%s', link_url='%s'" % (
+ self.command.build.buildid, link_label, link_url))
+
+ step = self._active_vstep
+ if step is None:
+ logging.msg("FATAL ERROR: op_addStepLink: no active vstep.")
+ return
+
+ step.addURL(link_label, link_url)
+
+ @defer.inlineCallbacks
+ def op_startNewStep(self, name, logline = None):
+ logging.msg(">>> op_startNewStep: buildid=%s, name='%s'" % (
+ self.command.build.buildid, name))
+
+ if self._active_vstep is not None:
+ logging.msg("FATAL ERROR: op_startNewStep: previous vstep is not finished: "
+ "stepid=%s, buildid=%s, name='%s'" % (
+ self._active_vstep.stepid, self._active_vstep.build.buildid,
+ self._active_vstep.name))
+ #TODO: return defer.fail(...)?
+ return
+
+ step = VirtBuildStep(parent_step=self.command, name=name)
+ yield step.addStep()
+
+ # Doing this last so that @@@BUILD_STEP... occurs in the log of the new
+ # step.
+ if logline is not None:
+ step.stdio_log.addStdout(logline)
+
+ self._active_vstep = step
+ logging.msg("<<< op_startNewStep: stepid=%s, buildid=%s, name='%s'" % (
+ step.stepid, step.build.buildid, step.name))
+
+ @defer.inlineCallbacks
+ def op_fixupActiveStep(self, status = None):
+ logging.msg(">>> op_fixupActiveStep: buildid=%s, status='%s'" % (
+ self.command.build.buildid, status))
+
+ step = self._active_vstep
+ if step is None:
+ #TODO: return defer.fail(...)?
+ return
+
+ self._active_vstep = None
+ yield step.finishStep(status)
+ logging.msg("<<< op_fixupActiveStep: stepid=%s, buildid=%s, name='%s'" % (
+ step.stepid, step.build.buildid, step.name))
+
+ ##
+
+ def errLineReceived(self, line):
+ self.outLineReceived(line)
+
+ def setHaltOnFailure(self, enable = True):
+ self.halt_on_failure = enable
+
+ def setHonorZeroReturnCode(self, enable = True):
+ self.honor_zero_return_code = enable
+
+ def outLineReceived(self, line):
+ """This is called once with each line of the test log."""
+
+ # returns: opname, op, args
+ def parse_annotate_cmd(ln):
+ for k in self._re_set:
+ ro = k['re'].search(ln)
+ if ro is not None:
+ # Get the regex named group values.
+ # We can get the empty set, but it is ok.
+ args = ro.groupdict()
+ # No need actually, but just in case.
+ if args is None:
+ args = {}
+ # Store the current log line within the arguments.
+ # We will need to save it in some cases.
+ args['logline'] = ln
+ return k['op'], args
+ return None, None
+
+ # Add \n if not there, which seems to be the case for log lines from
+ # windows agents, but not others.
+ if not line.endswith('\n'):
+ line += '\n'
+
+ op, args = parse_annotate_cmd(line)
+ if op is not None:
+ try:
+ op(args)
+ except Exception:
+ logging.msg("Exception occurs while processing annotated command: "
+ "op=%r, args=%s." % (op, args))
+ logging.err()
+ else:
+ # Add to the current secondary log.
+ self.queue_op(op=lambda: self.op_addStepStdout(line), name="op_addStepStdout")
+
+ def handleReturnCode(self, return_code):
+ # 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.
+ if return_code == 0:
+ self.queue_op(op=lambda: self.op_fixupActiveStep(),
+ name="handleReturnCode.fixupActiveStep")
+ if self.honor_zero_return_code:
+ self.annotate_status = builder.SUCCESS
+ else:
+ self.annotate_status = builder.FAILURE
+ self.queue_op(op=lambda: self.op_fixupActiveStep(status=builder.FAILURE),
+ name="handleReturnCode.fixupActiveStep(FAILURE)")
+
+
+class AnnotatedCommand_(ProcessLogShellStep):
+ """Buildbot command that knows how to display annotations."""
+
+ # pylint: disable=too-many-ancestors
+ def __init__(self, *args, **kwargs):
+ # Inject standard tags into the environment.
+ env = {
+ 'BUILDBOT_BLAMELIST': util.Interpolate('%(prop:blamelist:-[])s'),
+ 'BUILDBOT_BRANCH': util.Interpolate('%(prop:branch:-None)s'),
+ 'BUILDBOT_BUILDERNAME': util.Interpolate('%(prop:buildername:-None)s'),
+ 'BUILDBOT_BUILDNUMBER': util.Interpolate('%(prop:buildnumber:-None)s'),
+ 'BUILDBOT_CLOBBER': util.Interpolate('%(prop:clobber:+1)s'),
+ 'BUILDBOT_GOT_REVISION': util.Interpolate('%(prop:got_revision:-None)s'),
+ 'BUILDBOT_REVISION': util.Interpolate('%(prop:revision:-None)s'),
+ 'BUILDBOT_SCHEDULER': util.Interpolate('%(prop:scheduler:-None)s'),
+ 'BUILDBOT_SLAVENAME': util.Interpolate('%(prop:slavename:-None)s'),
+ 'BUILDBOT_MSAN_ORIGINS': util.Interpolate('%(prop:msan_origins:-)s'),
+ }
+ # Apply the passed in environment on top.
+ old_env = kwargs.get('env')
+ if not old_env:
+ old_env = {}
+ 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)
+ self.addLogObserver('stdio', self.script_observer)
+ self.preamble_log = None
+
+ @defer.inlineCallbacks
+ def start(self):
+ # Create a preamble log for primary annotate step.
+ self.preamble_log = yield self.addLog('preamble')
+ r = ProcessLogShellStep.start(self)
+ #TODO:VV:
+ yield self.script_observer.queue_wait()
+ return r
+
+ def interrupt(self, reason):
+ logging.msg(">>> AnnotatedCommand::interrupt: buildid=%s" % self.build.buildid)
+ self.script_observer.op_fixupActiveStep(status=builder.EXCEPTION)
+ #TODO:VV:self.script_observer.queue_wait()
+ return ProcessLogShellStep.interrupt(self, reason)
+
+ def evaluateCommand(self, cmd):
+ logging.msg(">>> AnnotatedCommand::evaluateCommand: buildid=%s" % self.build.buildid)
+ 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 commandComplete(self, cmd):
+ logging.msg(">>> AnnotatedCommand::commandComplete: buildid=%s" % self.build.buildid)
+ self.script_observer.handleReturnCode(cmd.rc)
+ #TODO:VV:self.script_observer.queue_wait()
+ return ProcessLogShellStep.commandComplete(self, cmd)
+
+
+####
class AnnotationObserver(buildstep.LogLineObserver):
- """This class knows how to understand annotations.
+ """This class knows how to understand annotations.
- Here are a list of the currently supported annotations:
+ Here are a list of the currently supported annotations:
- @@@BUILD_STEP <stepname>@@@
- Add a new step <stepname>. End the current step, marking with last available
- status.
+ @@@BUILD_STEP <stepname>@@@
+ Add a new step <stepname>. End the current step, marking with last available
+ status.
- @@@STEP_LINK@<label>@<url>@@@
- Add a link with label <label> linking to <url> to the current stage.
+ @@@STEP_LINK@<label>@<url>@@@
+ Add a link with label <label> linking to <url> to the current stage.
- @@@STEP_WARNINGS@@@
- Mark the current step as having warnings (oragnge).
+ @@@STEP_WARNINGS@@@
+ Mark the current step as having warnings (oragnge).
- @@@STEP_FAILURE@@@
- Mark the current step as having failed (red).
+ @@@STEP_FAILURE@@@
+ Mark the current step as having failed (red).
- @@@STEP_EXCEPTION@@@
- Mark the current step as having exceptions (magenta).
+ @@@STEP_EXCEPTION@@@
+ Mark the current step as having exceptions (magenta).
- @@@STEP_CLEAR@@@
- Reset the text description of the current step.
+ @@@STEP_CLEAR@@@
+ Reset the text description of the current step.
- @@@STEP_SUMMARY_CLEAR@@@
- Reset the text summary of the current step.
+ @@@STEP_SUMMARY_CLEAR@@@
+ Reset the text summary of the current step.
- @@@STEP_TEXT@<msg>@@@
- Append <msg> to the current step text.
+ @@@STEP_TEXT@<msg>@@@
+ Append <msg> to the current step text.
- @@@STEP_SUMMARY_TEXT@<msg>@@@
- Append <msg> to the step summary (appears on top of the waterfall).
+ @@@STEP_SUMMARY_TEXT@<msg>@@@
+ Append <msg> to the step summary (appears on top of the waterfall).
- @@@HALT_ON_FAILURE@@@
- Halt if exception or failure steps are encountered (default is not).
+ @@@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.
+ @@@HONOR_ZERO_RETURN_CODE@@@
+ Honor the return code being zero (success), even if steps have other results.
- Deprecated annotations:
- TODO(bradnelson): drop these when all users have been tracked down.
+ Deprecated annotations:
+ TODO(bradnelson): drop these when all users have been tracked down.
- @@@BUILD_WARNINGS@@@
- Equivalent to @@@STEP_WARNINGS@@@
+ @@@BUILD_WARNINGS@@@
+ Equivalent to @@@STEP_WARNINGS@@@
- @@@BUILD_FAILED@@@
- Equivalent to @@@STEP_FAILURE@@@
+ @@@BUILD_FAILED@@@
+ Equivalent to @@@STEP_FAILURE@@@
- @@@BUILD_EXCEPTION@@@
- Equivalent to @@@STEP_EXCEPTION@@@
+ @@@BUILD_EXCEPTION@@@
+ Equivalent to @@@STEP_EXCEPTION@@@
- @@@link@<label>@<url>@@@
- Equivalent to @@@STEP_LINK@<label>@<url>@@@
- """
+ @@@link@<label>@<url>@@@
+ Equivalent to @@@STEP_LINK@<label>@<url>@@@
+ """
- def __init__(self, command=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
+ _re_step_link = re.compile(r'^@@@(STEP_LINK|link)@(?P<link_label>.*)@(?P<link_url>.*)@@@', re.M)
+ _re_step_warnings = re.compile(r'^@@@(STEP_WARNINGS|BUILD_WARNINGS)@@@', re.M)
+ _re_step_failure = re.compile(r'^@@@(STEP_FAILURE|BUILD_FAILED)@@@', re.M)
+ _re_step_exception = re.compile(r'^@@@(STEP_EXCEPTION|BUILD_EXCEPTION)@@@', re.M)
+ _re_halt_on_failure = re.compile(r'^@@@HALT_ON_FAILURE@@@', re.M)
+ _re_honor_zero_rc = re.compile(r'^@@@HONOR_ZERO_RETURN_CODE@@@', re.M)
+ _re_step_clear = re.compile(r'^@@@STEP_CLEAR@@@', re.M)
+ _re_step_summary_clear = re.compile(r'^@@@STEP_SUMMARY_CLEAR@@@', re.M)
+ _re_step_text = re.compile(r'^@@@STEP_TEXT@(?P<text>.*)@@@', re.M)
+ _re_step_summary_step = re.compile(r'^@@@STEP_SUMMARY_TEXT@(?P<text>.*)@@@', re.M)
+ _re_build_step = re.compile(r'^@@@BUILD_STEP (?P<name>.*)@@@', re.M)
- def initialSection(self):
- if self.sections:
- return
- # Add a log section for output before the first section heading.
- log = self.command.addLog('preamble')
- self.sections.append({
- 'name': 'preamble',
- 'step': self.command.step_status.getBuild().steps[-1],
- 'log': log,
- 'status': builder.SUCCESS,
- 'links': [],
- 'step_summary_text': [],
- 'step_text': [],
- })
-
- def fixupLast(self, status=None):
- # Potentially start initial section here, as initial section might have
- # no output at all.
- self.initialSection()
-
- last = self.sections[-1]
- # Update status if set as an argument.
- if status is not None:
- last['status'] = status
- # Final update of text.
- self.updateText()
- # Add timing info.
- (start, end) = self.command.step_status.getTimes()
- msg = '\n\n' + '-' * 80 + '\n'
- if start is None:
- msg += 'Not Started\n'
- else:
- if end is None:
- end = util.now()
- msg += '\n'.join([
- 'started: %s' % time.ctime(start),
- 'ended: %s' % time.ctime(end),
- 'duration: %s' % util.formatInterval(end - start),
- '', # So we get a final \n
- ])
- last['log'].addStdout(msg)
- # Change status (unless handling the preamble).
- if len(self.sections) != 1:
- last['step'].stepFinished(last['status'])
- # Finish log.
- last['log'].finish()
-
- def errLineReceived(self, line):
- self.outLineReceived(line)
-
- 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)
- last = self.sections[-1]
- last['status'] = BuilderStatus.combine(last['status'], status)
- if self.halt_on_failure and last['status'] in [
- builder.FAILURE, builder.EXCEPTION]:
- self.fixupLast()
- self.command.finished(last['status'])
-
- def updateText(self):
- # Don't update the main phase's text.
- if len(self.sections) == 1:
- return
-
- last = self.sections[-1]
-
- # Reflect step status in text2.
- if last['status'] == builder.EXCEPTION:
- result = ['exception', last['name']]
- elif last['status'] == builder.FAILURE:
- result = ['failed', last['name']]
- else:
- result = []
-
- last['step'].setText([last['name']] + last['step_text'])
- last['step'].setText2(result + last['step_summary_text'])
-
- def outLineReceived(self, line):
- """This is called once with each line of the test log."""
- # Add \n if not there, which seems to be the case for log lines from
- # windows agents, but not others.
- if not line.endswith('\n'):
- line += '\n'
- # Handle initial setup here, as step_status might not exist yet at init.
- self.initialSection()
- # Support: @@@STEP_LINK@<name>@<url>@@@ (emit link)
- # Also support depreceated @@@link@<name>@<url>@@@
- m = re.match('^@@@STEP_LINK@(.*)@(.*)@@@', line)
- if not m:
- m = re.match('^@@@link@(.*)@(.*)@@@', line)
- if m:
- link_label = m.group(1)
- link_url = m.group(2)
- self.sections[-1]['links'].append((link_label, link_url))
- self.sections[-1]['step'].addURL(link_label, link_url)
- # Support: @@@STEP_WARNINGS@@@ (warn on a stage)
- # Also support deprecated @@@BUILD_WARNINGS@@@
- if (line.startswith('@@@STEP_WARNINGS@@@') or
- line.startswith('@@@BUILD_WARNINGS@@@')):
- self.updateStepStatus(builder.WARNINGS)
- # Support: @@@STEP_FAILURE@@@ (fail a stage)
- # Also support deprecated @@@BUILD_FAILED@@@
- if (line.startswith('@@@STEP_FAILURE@@@') or
- line.startswith('@@@BUILD_FAILED@@@')):
- self.updateStepStatus(builder.FAILURE)
- # Support: @@@STEP_EXCEPTION@@@ (exception on a stage)
- # Also support deprecated @@@BUILD_FAILED@@@
- if (line.startswith('@@@STEP_EXCEPTION@@@') or
- line.startswith('@@@BUILD_EXCEPTION@@@')):
- self.updateStepStatus(builder.EXCEPTION)
- # Support: @@@HALT_ON_FAILURE@@@ (halt if a step fails immediately)
- if line.startswith('@@@HALT_ON_FAILURE@@@'):
- self.halt_on_failure = True
- # Support: @@@HONOR_ZERO_RETURN_CODE@@@ (succeed on 0 return, even if some
- # steps have failed)
- if line.startswith('@@@HONOR_ZERO_RETURN_CODE@@@'):
- self.honor_zero_return_code = True
- # Support: @@@STEP_CLEAR@@@ (reset step description)
- if line.startswith('@@@STEP_CLEAR@@@'):
- self.sections[-1]['step_text'] = []
- self.updateText()
- # Support: @@@STEP_SUMMARY_CLEAR@@@ (reset step summary)
- if line.startswith('@@@STEP_SUMMARY_CLEAR@@@'):
- self.sections[-1]['step_summary_text'] = []
- self.updateText()
- # Support: @@@STEP_TEXT@<msg>@@@
- m = re.match('^@@@STEP_TEXT@(.*)@@@', line)
- if m:
- self.sections[-1]['step_text'].append(m.group(1))
- self.updateText()
- # Support: @@@STEP_SUMMARY_TEXT@<msg>@@@
- m = re.match('^@@@STEP_SUMMARY_TEXT@(.*)@@@', line)
- if m:
- self.sections[-1]['step_summary_text'].append(m.group(1))
- self.updateText()
- # Support: @@@BUILD_STEP <step_name>@@@ (start a new section)
- m = re.match('^@@@BUILD_STEP (.*)@@@', line)
- if m:
- step_name = m.group(1)
- # Ignore duplicate consecutive step labels (for robustness).
- if step_name != self.sections[-1]['name']:
- # Finish up last section.
- self.fixupLast()
- # Add new one.
- step = self.command.step_status.getBuild().addStepWithName(step_name)
- step.stepStarted()
- step.setText([step_name])
- log = step.addLog('stdio')
- self.sections.append({
- 'name': step_name,
- 'step': step,
- 'log': log,
- 'status': builder.SUCCESS,
- 'links': [],
- 'step_summary_text': [],
- 'step_text': [],
- })
- # Add to the current secondary log.
- # Doing this last so that @@@BUILD_STEP... occurs in the log of the new
- # step.
- self.sections[-1]['log'].addStdout(line)
-
- def handleReturnCode(self, return_code):
- # 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.
- if return_code == 0:
- self.fixupLast()
- if self.honor_zero_return_code:
+ def __init__(self, command=None, *args, **kwargs):
+ buildstep.LogLineObserver.__init__(self, *args, **kwargs)
+ self.command = command
self.annotate_status = builder.SUCCESS
- else:
- self.annotate_status = builder.FAILURE
- self.fixupLast(builder.FAILURE)
+ self.halt_on_failure = False
+ self.honor_zero_return_code = False
+ self.active_log = None
+ self._re_set = [
+ # Support: @@@STEP_LINK@<name>@<url>@@@ (emit link)
+ # Also support depreceated @@@link@<name>@<url>@@@
+ {
+ 're' : AnnotationObserver._re_step_link,
+ 'op' : lambda args: self.command.addURL(args['link_label'], args['link_url'])
+ },
+ # Support: @@@STEP_WARNINGS@@@ (warn on a stage)
+ # Also support deprecated @@@BUILD_WARNINGS@@@
+ {
+ 're' : AnnotationObserver._re_step_warnings,
+ 'op' : lambda args: self.update_status(builder.WARNINGS)
+ },
+ # Support: @@@STEP_FAILURE@@@ (fail a stage)
+ # Also support deprecated @@@BUILD_FAILED@@@
+ {
+ 're' : AnnotationObserver._re_step_failure,
+ 'op' : lambda args: self.update_status(builder.FAILURE)
+ },
+ # Support: @@@STEP_EXCEPTION@@@ (exception on a stage)
+ # Also support deprecated @@@BUILD_FAILED@@@
+ {
+ 're' : AnnotationObserver._re_step_exception,
+ 'op' : lambda args: self.update_status(builder.EXCEPTION)
+ },
+ # Support: @@@HALT_ON_FAILURE@@@ (halt if a step fails immediately)
+ {
+ 're' : AnnotationObserver._re_halt_on_failure,
+ 'op' : lambda args: self.set_halt_on_failure(True)
+ },
+ # Support: @@@HONOR_ZERO_RETURN_CODE@@@ (succeed on 0 return, even if some
+ # steps have failed)
+ {
+ 're' : AnnotationObserver._re_honor_zero_rc,
+ 'op' : lambda args: self.set_honor_zero_return_code(True)
+ },
+ # Support: @@@STEP_CLEAR@@@ (reset step description)
+ {
+ 're' : AnnotationObserver._re_step_clear,
+ 'op' : lambda args: args
+ },
+ # Support: @@@STEP_SUMMARY_CLEAR@@@ (reset step summary)
+ {
+ 're' : AnnotationObserver._re_step_summary_clear,
+ 'op' : lambda args: args
+ },
+ # Support: @@@STEP_TEXT@<msg>@@@
+ {
+ 're' : AnnotationObserver._re_step_text,
+ 'op' : lambda args: args # args['text']
+ },
+ # Support: @@@STEP_SUMMARY_TEXT@<msg>@@@
+ {
+ 're' : AnnotationObserver._re_step_summary_step,
+ 'op' : lambda args: args # args['text']
+ },
+ # Support: @@@BUILD_STEP <step_name>@@@ (start a new section)
+ {
+ 're' : AnnotationObserver._re_build_step,
+ 'op' : lambda args: self.start_new_section(args['name'], args['logline'])
+ },
+ ]
-class AnnotatedCommand(ProcessLogShellStep):
- """Buildbot command that knows how to display annotations."""
+ def errLineReceived(self, line):
+ self.outLineReceived(line)
- def __init__(self, *args, **kwargs):
- # Inject standard tags into the environment.
- env = {
- 'BUILDBOT_BLAMELIST': WithProperties('%(blamelist:-[])s'),
- 'BUILDBOT_BRANCH': WithProperties('%(branch:-None)s'),
- 'BUILDBOT_BUILDERNAME': WithProperties('%(buildername:-None)s'),
- 'BUILDBOT_BUILDNUMBER': WithProperties('%(buildnumber:-None)s'),
- 'BUILDBOT_CLOBBER': WithProperties('%(clobber:+1)s'),
- 'BUILDBOT_GOT_REVISION': WithProperties('%(got_revision:-None)s'),
- 'BUILDBOT_REVISION': WithProperties('%(revision:-None)s'),
- 'BUILDBOT_SCHEDULER': WithProperties('%(scheduler:-None)s'),
- 'BUILDBOT_SLAVENAME': WithProperties('%(slavename:-None)s'),
- 'BUILDBOT_MSAN_ORIGINS': WithProperties('%(msan_origins:-)s'),
- }
- # Apply the passed in environment on top.
- old_env = kwargs.get('env')
- if not old_env:
- old_env = {}
- env.update(old_env)
- # Change passed in args (ok as a copy is made internally).
- kwargs['env'] = env
+ def outLineReceived(self, line):
+ """This is called once with each line of the test log."""
- ProcessLogShellStep.__init__(self, *args, **kwargs)
- self.script_observer = AnnotationObserver(self)
- self.addLogObserver('stdio', self.script_observer)
+ # returns: opname, op, args
+ def parse_annotate_cmd(ln):
+ for k in self._re_set:
+ ro = k['re'].search(ln)
+ if ro is not None:
+ # Get the regex named group values.
+ # We can get the empty set, but it is ok.
+ args = ro.groupdict()
+ # No need actually, but just in case.
+ if args is None:
+ args = {}
+ # Store the current log line within the arguments.
+ # We will need to save it in some cases.
+ args['logline'] = ln
+ return k['op'], args
+ return None, None
- def interrupt(self, reason):
- self.script_observer.fixupLast(builder.EXCEPTION)
- return ProcessLogShellStep.interrupt(self, reason)
+ # Add \n if not there, which seems to be the case for log lines from
+ # windows agents, but not others.
+ if not line.endswith('\n'):
+ line += '\n'
- 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)
+ op, args = parse_annotate_cmd(line)
+ if op is not None:
+ try:
+ op(args)
+ except Exception:
+ logging.msg("Exception occurs while processing annotated command: "
+ "op=%r, args=%s." % (op, args))
+ logging.err()
+ else:
+ # Add to the current log.
+ if self.active_log is not None:
+ self.active_log.addStdout(line)
+ elif self.command.preamble_log is not None:
+ self.command.preamble_log.addStdout(line)
- def commandComplete(self, cmd):
- self.script_observer.handleReturnCode(cmd.rc)
- return ProcessLogShellStep.commandComplete(self, cmd)
+ def handleReturnCode(self, return_code):
+ # 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.
+ if return_code == 0:
+ self.finalize_annotation()
+ if self.honor_zero_return_code:
+ self.annotate_status = builder.SUCCESS
+ else:
+ self.annotate_status = builder.FAILURE
+ self.finalize_annotation(status=builder.FAILURE)
+
+ def set_halt_on_failure(self, v):
+ self.halt_on_failure = v
+
+ def set_honor_zero_return_code(self, v):
+ self.honor_zero_return_code = v
+
+ def start_new_section(self, name, line):
+ if self.active_log is not None:
+ self.active_log.finish()
+ self.active_log = self.command.addLog(name.strip())
+ self.active_log.addStdout(line)
+
+ # Updating a status for the currently active step.
+ def update_status(self, status):
+ self.annotate_status = BuilderStatus.combine(self.annotate_status, status)
+
+ if self.halt_on_failure and status in [builder.FAILURE, builder.EXCEPTION]:
+ self.finalize_annotation(status)
+ self.command.finished(status)
+
+ def finalize_annotation(self, status=None):
+ if status is not None:
+ self.annotate_status = BuilderStatus.combine(self.annotate_status, status)
+ if self.active_log is not None:
+ self.active_log.finish()
+ self.active_log = None
+
+class AnnotatedCommand(shell.ShellCommand):
+ """Buildbot command that knows how to display annotations."""
+
+ # pylint: disable=too-many-ancestors
+ def __init__(self, *args, **kwargs):
+ # Inject standard tags into the environment.
+ env = {
+ 'BUILDBOT_BLAMELIST': util.Interpolate('%(prop:blamelist:-[])s'),
+ 'BUILDBOT_BRANCH': util.Interpolate('%(prop:branch:-None)s'),
+ 'BUILDBOT_BUILDERNAME': util.Interpolate('%(prop:buildername:-None)s'),
+ 'BUILDBOT_BUILDNUMBER': util.Interpolate('%(prop:buildnumber:-None)s'),
+ 'BUILDBOT_CLOBBER': util.Interpolate('%(prop:clobber:+1)s'),
+ 'BUILDBOT_GOT_REVISION': util.Interpolate('%(prop:got_revision:-None)s'),
+ 'BUILDBOT_REVISION': util.Interpolate('%(prop:revision:-None)s'),
+ 'BUILDBOT_SCHEDULER': util.Interpolate('%(prop:scheduler:-None)s'),
+ 'BUILDBOT_SLAVENAME': util.Interpolate('%(prop:slavename:-None)s'),
+ 'BUILDBOT_MSAN_ORIGINS': util.Interpolate('%(prop:msan_origins:-)s'),
+ }
+ # Apply the passed in environment on top.
+ old_env = kwargs.get('env')
+ if not old_env:
+ old_env = {}
+ env.update(old_env)
+ # Change passed in args (ok as a copy is made internally).
+ kwargs['env'] = env
+
+ shell.ShellCommand.__init__(self, *args, **kwargs)
+ self.script_observer = AnnotationObserver(self)
+ self.addLogObserver('stdio', self.script_observer)
+ self.preamble_log = None
+
+ def start(self):
+ # Create a preamble log for primary annotate step.
+ self.preamble_log = self.addLog('preamble')
+ shell.ShellCommand.start(self)
+
+ def interrupt(self, reason):
+ logging.msg(">>> AnnotatedCommand::interrupt: buildid=%s" % self.build.buildid)
+ self.script_observer.finalize_annotation(status=builder.EXCEPTION)
+ return shell.ShellCommand.interrupt(self, reason)
+
+ def evaluateCommand(self, cmd):
+ logging.msg(">>> AnnotatedCommand::evaluateCommand: buildid=%s" % self.build.buildid)
+ observer_result = self.script_observer.annotate_status
+ # Check if shell.ShellCommand detected a failure or warning also.
+ log_processor_result = shell.ShellCommand.evaluateCommand(self, cmd)
+ return BuilderStatus.combine(observer_result, log_processor_result)
+
+ def commandComplete(self, cmd):
+ logging.msg(">>> AnnotatedCommand::commandComplete: buildid=%s" % self.build.buildid)
+ self.script_observer.handleReturnCode(cmd.rc)
+ return shell.ShellCommand.commandComplete(self, cmd)