Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/buildbot/buildbot/steps/shell.py
diff options
context:
space:
mode:
Diffstat (limited to 'buildbot/buildbot/steps/shell.py')
-rw-r--r--buildbot/buildbot/steps/shell.py487
1 files changed, 487 insertions, 0 deletions
diff --git a/buildbot/buildbot/steps/shell.py b/buildbot/buildbot/steps/shell.py
new file mode 100644
index 0000000..e979f04
--- /dev/null
+++ b/buildbot/buildbot/steps/shell.py
@@ -0,0 +1,487 @@
+# -*- test-case-name: buildbot.test.test_steps,buildbot.test.test_properties -*-
+
+import re
+from twisted.python import log
+from buildbot.process.buildstep import LoggingBuildStep, RemoteShellCommand
+from buildbot.status.builder import SUCCESS, WARNINGS, FAILURE, STDOUT, STDERR
+
+# for existing configurations that import WithProperties from here. We like
+# to move this class around just to keep our readers guessing.
+from buildbot.process.properties import WithProperties
+_hush_pyflakes = [WithProperties]
+del _hush_pyflakes
+
+class ShellCommand(LoggingBuildStep):
+ """I run a single shell command on the buildslave. I return FAILURE if
+ the exit code of that command is non-zero, SUCCESS otherwise. To change
+ this behavior, override my .evaluateCommand method.
+
+ By default, a failure of this step will mark the whole build as FAILURE.
+ To override this, give me an argument of flunkOnFailure=False .
+
+ I create a single Log named 'log' which contains the output of the
+ command. To create additional summary Logs, override my .createSummary
+ method.
+
+ The shell command I run (a list of argv strings) can be provided in
+ several ways:
+ - a class-level .command attribute
+ - a command= parameter to my constructor (overrides .command)
+ - set explicitly with my .setCommand() method (overrides both)
+
+ @ivar command: a list of renderable objects (typically strings or
+ WithProperties instances). This will be used by start()
+ to create a RemoteShellCommand instance.
+
+ @ivar logfiles: a dict mapping log NAMEs to workdir-relative FILENAMEs
+ of their corresponding logfiles. The contents of the file
+ named FILENAME will be put into a LogFile named NAME, ina
+ something approximating real-time. (note that logfiles=
+ is actually handled by our parent class LoggingBuildStep)
+
+ """
+
+ name = "shell"
+ description = None # set this to a list of short strings to override
+ descriptionDone = None # alternate description when the step is complete
+ command = None # set this to a command, or set in kwargs
+ # logfiles={} # you can also set 'logfiles' to a dictionary, and it
+ # will be merged with any logfiles= argument passed in
+ # to __init__
+
+ # override this on a specific ShellCommand if you want to let it fail
+ # without dooming the entire build to a status of FAILURE
+ flunkOnFailure = True
+
+ def __init__(self, workdir=None,
+ description=None, descriptionDone=None,
+ command=None,
+ usePTY="slave-config",
+ **kwargs):
+ # most of our arguments get passed through to the RemoteShellCommand
+ # that we create, but first strip out the ones that we pass to
+ # BuildStep (like haltOnFailure and friends), and a couple that we
+ # consume ourselves.
+
+ if description:
+ self.description = description
+ if isinstance(self.description, str):
+ self.description = [self.description]
+ if descriptionDone:
+ self.descriptionDone = descriptionDone
+ if isinstance(self.descriptionDone, str):
+ self.descriptionDone = [self.descriptionDone]
+ if command:
+ self.setCommand(command)
+
+ # pull out the ones that LoggingBuildStep wants, then upcall
+ buildstep_kwargs = {}
+ for k in kwargs.keys()[:]:
+ if k in self.__class__.parms:
+ buildstep_kwargs[k] = kwargs[k]
+ del kwargs[k]
+ LoggingBuildStep.__init__(self, **buildstep_kwargs)
+ self.addFactoryArguments(workdir=workdir,
+ description=description,
+ descriptionDone=descriptionDone,
+ command=command)
+
+ # everything left over goes to the RemoteShellCommand
+ kwargs['workdir'] = workdir # including a copy of 'workdir'
+ kwargs['usePTY'] = usePTY
+ self.remote_kwargs = kwargs
+ # we need to stash the RemoteShellCommand's args too
+ self.addFactoryArguments(**kwargs)
+
+ def setDefaultWorkdir(self, workdir):
+ rkw = self.remote_kwargs
+ rkw['workdir'] = rkw['workdir'] or workdir
+
+ def setCommand(self, command):
+ self.command = command
+
+ def describe(self, done=False):
+ """Return a list of short strings to describe this step, for the
+ status display. This uses the first few words of the shell command.
+ You can replace this by setting .description in your subclass, or by
+ overriding this method to describe the step better.
+
+ @type done: boolean
+ @param done: whether the command is complete or not, to improve the
+ way the command is described. C{done=False} is used
+ while the command is still running, so a single
+ imperfect-tense verb is appropriate ('compiling',
+ 'testing', ...) C{done=True} is used when the command
+ has finished, and the default getText() method adds some
+ text, so a simple noun is appropriate ('compile',
+ 'tests' ...)
+ """
+
+ if done and self.descriptionDone is not None:
+ return list(self.descriptionDone)
+ if self.description is not None:
+ return list(self.description)
+
+ properties = self.build.getProperties()
+ words = self.command
+ if isinstance(words, (str, unicode)):
+ words = words.split()
+ # render() each word to handle WithProperties objects
+ words = properties.render(words)
+ if len(words) < 1:
+ return ["???"]
+ if len(words) == 1:
+ return ["'%s'" % words[0]]
+ if len(words) == 2:
+ return ["'%s" % words[0], "%s'" % words[1]]
+ return ["'%s" % words[0], "%s" % words[1], "...'"]
+
+ def setupEnvironment(self, cmd):
+ # merge in anything from Build.slaveEnvironment
+ # This can be set from a Builder-level environment, or from earlier
+ # BuildSteps. The latter method is deprecated and superceded by
+ # BuildProperties.
+ # Environment variables passed in by a BuildStep override
+ # those passed in at the Builder level.
+ properties = self.build.getProperties()
+ slaveEnv = self.build.slaveEnvironment
+ if slaveEnv:
+ if cmd.args['env'] is None:
+ cmd.args['env'] = {}
+ fullSlaveEnv = slaveEnv.copy()
+ fullSlaveEnv.update(cmd.args['env'])
+ cmd.args['env'] = properties.render(fullSlaveEnv)
+ # note that each RemoteShellCommand gets its own copy of the
+ # dictionary, so we shouldn't be affecting anyone but ourselves.
+
+ def checkForOldSlaveAndLogfiles(self):
+ if not self.logfiles:
+ return # doesn't matter
+ if not self.slaveVersionIsOlderThan("shell", "2.1"):
+ return # slave is new enough
+ # this buildslave is too old and will ignore the 'logfiles'
+ # argument. You'll either have to pull the logfiles manually
+ # (say, by using 'cat' in a separate RemoteShellCommand) or
+ # upgrade the buildslave.
+ msg1 = ("Warning: buildslave %s is too old "
+ "to understand logfiles=, ignoring it."
+ % self.getSlaveName())
+ msg2 = "You will have to pull this logfile (%s) manually."
+ log.msg(msg1)
+ for logname,remotefilename in self.logfiles.items():
+ newlog = self.addLog(logname)
+ newlog.addHeader(msg1 + "\n")
+ newlog.addHeader(msg2 % remotefilename + "\n")
+ newlog.finish()
+ # now prevent setupLogfiles() from adding them
+ self.logfiles = {}
+
+ def start(self):
+ # this block is specific to ShellCommands. subclasses that don't need
+ # to set up an argv array, an environment, or extra logfiles= (like
+ # the Source subclasses) can just skip straight to startCommand()
+ properties = self.build.getProperties()
+
+ warnings = []
+
+ # create the actual RemoteShellCommand instance now
+ kwargs = properties.render(self.remote_kwargs)
+ kwargs['command'] = properties.render(self.command)
+ kwargs['logfiles'] = self.logfiles
+
+ # check for the usePTY flag
+ if kwargs.has_key('usePTY') and kwargs['usePTY'] != 'slave-config':
+ slavever = self.slaveVersion("shell", "old")
+ if self.slaveVersionIsOlderThan("svn", "2.7"):
+ warnings.append("NOTE: slave does not allow master to override usePTY\n")
+
+ cmd = RemoteShellCommand(**kwargs)
+ self.setupEnvironment(cmd)
+ self.checkForOldSlaveAndLogfiles()
+
+ self.startCommand(cmd, warnings)
+
+
+
+class TreeSize(ShellCommand):
+ name = "treesize"
+ command = ["du", "-s", "-k", "."]
+ kib = None
+
+ def commandComplete(self, cmd):
+ out = cmd.logs['stdio'].getText()
+ m = re.search(r'^(\d+)', out)
+ if m:
+ self.kib = int(m.group(1))
+ self.setProperty("tree-size-KiB", self.kib, "treesize")
+
+ def evaluateCommand(self, cmd):
+ if cmd.rc != 0:
+ return FAILURE
+ if self.kib is None:
+ return WARNINGS # not sure how 'du' could fail, but whatever
+ return SUCCESS
+
+ def getText(self, cmd, results):
+ if self.kib is not None:
+ return ["treesize", "%d KiB" % self.kib]
+ return ["treesize", "unknown"]
+
+class SetProperty(ShellCommand):
+ name = "setproperty"
+
+ def __init__(self, **kwargs):
+ self.property = None
+ self.extract_fn = None
+ self.strip = True
+
+ if kwargs.has_key('property'):
+ self.property = kwargs['property']
+ del kwargs['property']
+ if kwargs.has_key('extract_fn'):
+ self.extract_fn = kwargs['extract_fn']
+ del kwargs['extract_fn']
+ if kwargs.has_key('strip'):
+ self.strip = kwargs['strip']
+ del kwargs['strip']
+
+ ShellCommand.__init__(self, **kwargs)
+
+ self.addFactoryArguments(property=self.property)
+ self.addFactoryArguments(extract_fn=self.extract_fn)
+ self.addFactoryArguments(strip=self.strip)
+
+ assert self.property or self.extract_fn, \
+ "SetProperty step needs either property= or extract_fn="
+
+ self.property_changes = {}
+
+ def commandComplete(self, cmd):
+ if self.property:
+ result = cmd.logs['stdio'].getText()
+ if self.strip: result = result.strip()
+ propname = self.build.getProperties().render(self.property)
+ self.setProperty(propname, result, "SetProperty Step")
+ self.property_changes[propname] = result
+ else:
+ log = cmd.logs['stdio']
+ new_props = self.extract_fn(cmd.rc,
+ ''.join(log.getChunks([STDOUT], onlyText=True)),
+ ''.join(log.getChunks([STDERR], onlyText=True)))
+ for k,v in new_props.items():
+ self.setProperty(k, v, "SetProperty Step")
+ self.property_changes = new_props
+
+ def createSummary(self, log):
+ props_set = [ "%s: %r" % (k,v) for k,v in self.property_changes.items() ]
+ self.addCompleteLog('property changes', "\n".join(props_set))
+
+ def getText(self, cmd, results):
+ if self.property_changes:
+ return [ "set props:" ] + self.property_changes.keys()
+ else:
+ return [ "no change" ]
+
+class Configure(ShellCommand):
+
+ name = "configure"
+ haltOnFailure = 1
+ flunkOnFailure = 1
+ description = ["configuring"]
+ descriptionDone = ["configure"]
+ command = ["./configure"]
+
+class WarningCountingShellCommand(ShellCommand):
+ warnCount = 0
+ warningPattern = '.*warning[: ].*'
+
+ def __init__(self, warningPattern=None, **kwargs):
+ # See if we've been given a regular expression to use to match
+ # warnings. If not, use a default that assumes any line with "warning"
+ # present is a warning. This may lead to false positives in some cases.
+ if warningPattern:
+ self.warningPattern = warningPattern
+
+ # And upcall to let the base class do its work
+ ShellCommand.__init__(self, **kwargs)
+
+ self.addFactoryArguments(warningPattern=warningPattern)
+
+ def createSummary(self, log):
+ self.warnCount = 0
+
+ # Now compile a regular expression from whichever warning pattern we're
+ # using
+ if not self.warningPattern:
+ return
+
+ wre = self.warningPattern
+ if isinstance(wre, str):
+ wre = re.compile(wre)
+
+ # Check if each line in the output from this command matched our
+ # warnings regular expressions. If did, bump the warnings count and
+ # add the line to the collection of lines with warnings
+ warnings = []
+ # TODO: use log.readlines(), except we need to decide about stdout vs
+ # stderr
+ for line in log.getText().split("\n"):
+ if wre.match(line):
+ warnings.append(line)
+ self.warnCount += 1
+
+ # If there were any warnings, make the log if lines with warnings
+ # available
+ if self.warnCount:
+ self.addCompleteLog("warnings", "\n".join(warnings) + "\n")
+
+ warnings_stat = self.step_status.getStatistic('warnings', 0)
+ self.step_status.setStatistic('warnings', warnings_stat + self.warnCount)
+
+ try:
+ old_count = self.getProperty("warnings-count")
+ except KeyError:
+ old_count = 0
+ self.setProperty("warnings-count", old_count + self.warnCount, "WarningCountingShellCommand")
+
+
+ def evaluateCommand(self, cmd):
+ if cmd.rc != 0:
+ return FAILURE
+ if self.warnCount:
+ return WARNINGS
+ return SUCCESS
+
+
+class Compile(WarningCountingShellCommand):
+
+ name = "compile"
+ haltOnFailure = 1
+ flunkOnFailure = 1
+ description = ["compiling"]
+ descriptionDone = ["compile"]
+ command = ["make", "all"]
+
+ OFFprogressMetrics = ('output',)
+ # things to track: number of files compiled, number of directories
+ # traversed (assuming 'make' is being used)
+
+ def createSummary(self, log):
+ # TODO: grep for the characteristic GCC error lines and
+ # assemble them into a pair of buffers
+ WarningCountingShellCommand.createSummary(self, log)
+
+class Test(WarningCountingShellCommand):
+
+ name = "test"
+ warnOnFailure = 1
+ description = ["testing"]
+ descriptionDone = ["test"]
+ command = ["make", "test"]
+
+ def setTestResults(self, total=0, failed=0, passed=0, warnings=0):
+ """
+ Called by subclasses to set the relevant statistics; this actually
+ adds to any statistics already present
+ """
+ total += self.step_status.getStatistic('tests-total', 0)
+ self.step_status.setStatistic('tests-total', total)
+ failed += self.step_status.getStatistic('tests-failed', 0)
+ self.step_status.setStatistic('tests-failed', failed)
+ warnings += self.step_status.getStatistic('tests-warnings', 0)
+ self.step_status.setStatistic('tests-warnings', warnings)
+ passed += self.step_status.getStatistic('tests-passed', 0)
+ self.step_status.setStatistic('tests-passed', passed)
+
+ def describe(self, done=False):
+ description = WarningCountingShellCommand.describe(self, done)
+ if done:
+ if self.step_status.hasStatistic('tests-total'):
+ total = self.step_status.getStatistic("tests-total", 0)
+ failed = self.step_status.getStatistic("tests-failed", 0)
+ passed = self.step_status.getStatistic("tests-passed", 0)
+ warnings = self.step_status.getStatistic("tests-warnings", 0)
+ if not total:
+ total = failed + passed + warnings
+
+ if total:
+ description.append('%d tests' % total)
+ if passed:
+ description.append('%d passed' % passed)
+ if warnings:
+ description.append('%d warnings' % warnings)
+ if failed:
+ description.append('%d failed' % failed)
+ return description
+
+class PerlModuleTest(Test):
+ command=["prove", "--lib", "lib", "-r", "t"]
+ total = 0
+
+ def evaluateCommand(self, cmd):
+ # Get stdio, stripping pesky newlines etc.
+ lines = map(
+ lambda line : line.replace('\r\n','').replace('\r','').replace('\n',''),
+ self.getLog('stdio').readlines()
+ )
+
+ total = 0
+ passed = 0
+ failed = 0
+ rc = cmd.rc
+
+ # New version of Test::Harness?
+ try:
+ test_summary_report_index = lines.index("Test Summary Report")
+
+ del lines[0:test_summary_report_index + 2]
+
+ re_test_result = re.compile("^Result: (PASS|FAIL)$|Tests: \d+ Failed: (\d+)\)|Files=\d+, Tests=(\d+)")
+
+ mos = map(lambda line: re_test_result.search(line), lines)
+ test_result_lines = [mo.groups() for mo in mos if mo]
+
+ for line in test_result_lines:
+ if line[0] == 'PASS':
+ rc = SUCCESS
+ elif line[0] == 'FAIL':
+ rc = FAILURE
+ elif line[1]:
+ failed += int(line[1])
+ elif line[2]:
+ total = int(line[2])
+
+ except ValueError: # Nope, it's the old version
+ re_test_result = re.compile("^(All tests successful)|(\d+)/(\d+) subtests failed|Files=\d+, Tests=(\d+),")
+
+ mos = map(lambda line: re_test_result.search(line), lines)
+ test_result_lines = [mo.groups() for mo in mos if mo]
+
+ if test_result_lines:
+ test_result_line = test_result_lines[0]
+
+ success = test_result_line[0]
+
+ if success:
+ failed = 0
+
+ test_totals_line = test_result_lines[1]
+ total_str = test_totals_line[3]
+
+ rc = SUCCESS
+ else:
+ failed_str = test_result_line[1]
+ failed = int(failed_str)
+
+ total_str = test_result_line[2]
+
+ rc = FAILURE
+
+ total = int(total_str)
+
+ if total:
+ passed = total - failed
+
+ self.setTestResults(total=total, failed=failed, passed=passed)
+
+ return rc