= self.chunkSize:
- self.merge()
-
- for w in self.watchers:
- w.logChunk(self.step.build, self.step, self, channel, text)
- self.length += len(text)
-
- def addStdout(self, text):
- self.addEntry(STDOUT, text)
- def addStderr(self, text):
- self.addEntry(STDERR, text)
- def addHeader(self, text):
- self.addEntry(HEADER, text)
-
- def finish(self):
- self.merge()
- if self.openfile:
- # we don't do an explicit close, because there might be readers
- # shareing the filehandle. As soon as they stop reading, the
- # filehandle will be released and automatically closed. We will
- # do a sync, however, to make sure the log gets saved in case of
- # a crash.
- os.fsync(self.openfile.fileno())
- del self.openfile
- self.finished = True
- watchers = self.finishedWatchers
- self.finishedWatchers = []
- for w in watchers:
- w.callback(self)
- self.watchers = []
-
- # persistence stuff
- def __getstate__(self):
- d = self.__dict__.copy()
- del d['step'] # filled in upon unpickling
- del d['watchers']
- del d['finishedWatchers']
- d['entries'] = [] # let 0.6.4 tolerate the saved log. TODO: really?
- if d.has_key('finished'):
- del d['finished']
- if d.has_key('openfile'):
- del d['openfile']
- return d
-
- def __setstate__(self, d):
- self.__dict__ = d
- self.watchers = [] # probably not necessary
- self.finishedWatchers = [] # same
- # self.step must be filled in by our parent
- self.finished = True
-
- def upgrade(self, logfilename):
- """Save our .entries to a new-style offline log file (if necessary),
- and modify our in-memory representation to use it. The original
- pickled LogFile (inside the pickled Build) won't be modified."""
- self.filename = logfilename
- if not os.path.exists(self.getFilename()):
- self.openfile = open(self.getFilename(), "w")
- self.finished = False
- for channel,text in self.entries:
- self.addEntry(channel, text)
- self.finish() # releases self.openfile, which will be closed
- del self.entries
-
-class HTMLLogFile:
- implements(interfaces.IStatusLog)
-
- filename = None
-
- def __init__(self, parent, name, logfilename, html):
- self.step = parent
- self.name = name
- self.filename = logfilename
- self.html = html
-
- def getName(self):
- return self.name # set in BuildStepStatus.addLog
- def getStep(self):
- return self.step
-
- def isFinished(self):
- return True
- def waitUntilFinished(self):
- return defer.succeed(self)
-
- def hasContents(self):
- return True
- def getText(self):
- return self.html # looks kinda like text
- def getTextWithHeaders(self):
- return self.html
- def getChunks(self):
- return [(STDERR, self.html)]
-
- def subscribe(self, receiver, catchup):
- pass
- def unsubscribe(self, receiver):
- pass
-
- def finish(self):
- pass
-
- def __getstate__(self):
- d = self.__dict__.copy()
- del d['step']
- return d
-
- def upgrade(self, logfilename):
- pass
-
-
-class Event:
- implements(interfaces.IStatusEvent)
-
- started = None
- finished = None
- text = []
- color = None
-
- # IStatusEvent methods
- def getTimes(self):
- return (self.started, self.finished)
- def getText(self):
- return self.text
- def getColor(self):
- return self.color
- def getLogs(self):
- return []
-
- def finish(self):
- self.finished = util.now()
-
-class TestResult:
- implements(interfaces.ITestResult)
-
- def __init__(self, name, results, text, logs):
- assert isinstance(name, tuple)
- self.name = name
- self.results = results
- self.text = text
- self.logs = logs
-
- def getName(self):
- return self.name
-
- def getResults(self):
- return self.results
-
- def getText(self):
- return self.text
-
- def getLogs(self):
- return self.logs
-
-
-class BuildSetStatus:
- implements(interfaces.IBuildSetStatus)
-
- def __init__(self, source, reason, builderNames, bsid=None):
- self.source = source
- self.reason = reason
- self.builderNames = builderNames
- self.id = bsid
- self.successWatchers = []
- self.finishedWatchers = []
- self.stillHopeful = True
- self.finished = False
-
- def setBuildRequestStatuses(self, buildRequestStatuses):
- self.buildRequests = buildRequestStatuses
- def setResults(self, results):
- # the build set succeeds only if all its component builds succeed
- self.results = results
- def giveUpHope(self):
- self.stillHopeful = False
-
-
- def notifySuccessWatchers(self):
- for d in self.successWatchers:
- d.callback(self)
- self.successWatchers = []
-
- def notifyFinishedWatchers(self):
- self.finished = True
- for d in self.finishedWatchers:
- d.callback(self)
- self.finishedWatchers = []
-
- # methods for our clients
-
- def getSourceStamp(self):
- return self.source
- def getReason(self):
- return self.reason
- def getResults(self):
- return self.results
- def getID(self):
- return self.id
-
- def getBuilderNames(self):
- return self.builderNames
- def getBuildRequests(self):
- return self.buildRequests
- def isFinished(self):
- return self.finished
-
- def waitUntilSuccess(self):
- if self.finished or not self.stillHopeful:
- # the deferreds have already fired
- return defer.succeed(self)
- d = defer.Deferred()
- self.successWatchers.append(d)
- return d
-
- def waitUntilFinished(self):
- if self.finished:
- return defer.succeed(self)
- d = defer.Deferred()
- self.finishedWatchers.append(d)
- return d
-
-class BuildRequestStatus:
- implements(interfaces.IBuildRequestStatus)
-
- def __init__(self, source, builderName):
- self.source = source
- self.builderName = builderName
- self.builds = [] # list of BuildStatus objects
- self.observers = []
-
- def buildStarted(self, build):
- self.builds.append(build)
- for o in self.observers[:]:
- o(build)
-
- # methods called by our clients
- def getSourceStamp(self):
- return self.source
- def getBuilderName(self):
- return self.builderName
- def getBuilds(self):
- return self.builds
-
- def subscribe(self, observer):
- self.observers.append(observer)
- for b in self.builds:
- observer(b)
- def unsubscribe(self, observer):
- self.observers.remove(observer)
-
-
-class BuildStepStatus(styles.Versioned):
- """
- I represent a collection of output status for a
- L{buildbot.process.step.BuildStep}.
-
- Statistics contain any information gleaned from a step that is
- not in the form of a logfile. As an example, steps that run
- tests might gather statistics about the number of passed, failed,
- or skipped tests.
-
- @type color: string
- @cvar color: color that this step feels best represents its
- current mood. yellow,green,red,orange are the
- most likely choices, although purple indicates
- an exception
- @type progress: L{buildbot.status.progress.StepProgress}
- @cvar progress: tracks ETA for the step
- @type text: list of strings
- @cvar text: list of short texts that describe the command and its status
- @type text2: list of strings
- @cvar text2: list of short texts added to the overall build description
- @type logs: dict of string -> L{buildbot.status.builder.LogFile}
- @ivar logs: logs of steps
- @type statistics: dict
- @ivar statistics: results from running this step
- """
- # note that these are created when the Build is set up, before each
- # corresponding BuildStep has started.
- implements(interfaces.IBuildStepStatus, interfaces.IStatusEvent)
- persistenceVersion = 2
-
- started = None
- finished = None
- progress = None
- text = []
- color = None
- results = (None, [])
- text2 = []
- watchers = []
- updates = {}
- finishedWatchers = []
- statistics = {}
-
- def __init__(self, parent):
- assert interfaces.IBuildStatus(parent)
- self.build = parent
- self.logs = []
- self.urls = {}
- self.watchers = []
- self.updates = {}
- self.finishedWatchers = []
- self.statistics = {}
-
- def getName(self):
- """Returns a short string with the name of this step. This string
- may have spaces in it."""
- return self.name
-
- def getBuild(self):
- return self.build
-
- def getTimes(self):
- return (self.started, self.finished)
-
- def getExpectations(self):
- """Returns a list of tuples (name, current, target)."""
- if not self.progress:
- return []
- ret = []
- metrics = self.progress.progress.keys()
- metrics.sort()
- for m in metrics:
- t = (m, self.progress.progress[m], self.progress.expectations[m])
- ret.append(t)
- return ret
-
- def getLogs(self):
- return self.logs
-
- def getURLs(self):
- return self.urls.copy()
-
- def isFinished(self):
- return (self.finished is not None)
-
- def waitUntilFinished(self):
- if self.finished:
- d = defer.succeed(self)
- else:
- d = defer.Deferred()
- self.finishedWatchers.append(d)
- return d
-
- # while the step is running, the following methods make sense.
- # Afterwards they return None
-
- def getETA(self):
- if self.started is None:
- return None # not started yet
- if self.finished is not None:
- return None # already finished
- if not self.progress:
- return None # no way to predict
- return self.progress.remaining()
-
- # Once you know the step has finished, the following methods are legal.
- # Before this step has finished, they all return None.
-
- def getText(self):
- """Returns a list of strings which describe the step. These are
- intended to be displayed in a narrow column. If more space is
- available, the caller should join them together with spaces before
- presenting them to the user."""
- return self.text
-
- def getColor(self):
- """Returns a single string with the color that should be used to
- display this step. 'green', 'orange', 'red', 'yellow' and 'purple'
- are the most likely ones."""
- return self.color
-
- def getResults(self):
- """Return a tuple describing the results of the step.
- 'result' is one of the constants in L{buildbot.status.builder}:
- SUCCESS, WARNINGS, FAILURE, or SKIPPED.
- 'strings' is an optional list of strings that the step wants to
- append to the overall build's results. These strings are usually
- more terse than the ones returned by getText(): in particular,
- successful Steps do not usually contribute any text to the
- overall build.
-
- @rtype: tuple of int, list of strings
- @returns: (result, strings)
- """
- return (self.results, self.text2)
-
- def hasStatistic(self, name):
- """Return true if this step has a value for the given statistic.
- """
- return self.statistics.has_key(name)
-
- def getStatistic(self, name, default=None):
- """Return the given statistic, if present
- """
- return self.statistics.get(name, default)
-
- # subscription interface
-
- def subscribe(self, receiver, updateInterval=10):
- # will get logStarted, logFinished, stepETAUpdate
- assert receiver not in self.watchers
- self.watchers.append(receiver)
- self.sendETAUpdate(receiver, updateInterval)
-
- def sendETAUpdate(self, receiver, updateInterval):
- self.updates[receiver] = None
- # they might unsubscribe during stepETAUpdate
- receiver.stepETAUpdate(self.build, self,
- self.getETA(), self.getExpectations())
- if receiver in self.watchers:
- self.updates[receiver] = reactor.callLater(updateInterval,
- self.sendETAUpdate,
- receiver,
- updateInterval)
-
- def unsubscribe(self, receiver):
- if receiver in self.watchers:
- self.watchers.remove(receiver)
- if receiver in self.updates:
- if self.updates[receiver] is not None:
- self.updates[receiver].cancel()
- del self.updates[receiver]
-
-
- # methods to be invoked by the BuildStep
-
- def setName(self, stepname):
- self.name = stepname
-
- def setProgress(self, stepprogress):
- self.progress = stepprogress
-
- def stepStarted(self):
- self.started = util.now()
- if self.build:
- self.build.stepStarted(self)
-
- def addLog(self, name):
- assert self.started # addLog before stepStarted won't notify watchers
- logfilename = self.build.generateLogfileName(self.name, name)
- log = LogFile(self, name, logfilename)
- self.logs.append(log)
- for w in self.watchers:
- receiver = w.logStarted(self.build, self, log)
- if receiver:
- log.subscribe(receiver, True)
- d = log.waitUntilFinished()
- d.addCallback(lambda log: log.unsubscribe(receiver))
- d = log.waitUntilFinished()
- d.addCallback(self.logFinished)
- return log
-
- def addHTMLLog(self, name, html):
- assert self.started # addLog before stepStarted won't notify watchers
- logfilename = self.build.generateLogfileName(self.name, name)
- log = HTMLLogFile(self, name, logfilename, html)
- self.logs.append(log)
- for w in self.watchers:
- receiver = w.logStarted(self.build, self, log)
- # TODO: think about this: there isn't much point in letting
- # them subscribe
- #if receiver:
- # log.subscribe(receiver, True)
- w.logFinished(self.build, self, log)
-
- def logFinished(self, log):
- for w in self.watchers:
- w.logFinished(self.build, self, log)
-
- def addURL(self, name, url):
- self.urls[name] = url
-
- def setColor(self, color):
- self.color = color
- def setText(self, text):
- self.text = text
- def setText2(self, text):
- self.text2 = text
-
- def setStatistic(self, name, value):
- """Set the given statistic. Usually called by subclasses.
- """
- self.statistics[name] = value
-
- def stepFinished(self, results):
- self.finished = util.now()
- self.results = results
- for loog in self.logs:
- if not loog.isFinished():
- loog.finish()
-
- for r in self.updates.keys():
- if self.updates[r] is not None:
- self.updates[r].cancel()
- del self.updates[r]
-
- watchers = self.finishedWatchers
- self.finishedWatchers = []
- for w in watchers:
- w.callback(self)
-
- # persistence
-
- def __getstate__(self):
- d = styles.Versioned.__getstate__(self)
- del d['build'] # filled in when loading
- if d.has_key('progress'):
- del d['progress']
- del d['watchers']
- del d['finishedWatchers']
- del d['updates']
- return d
-
- def __setstate__(self, d):
- styles.Versioned.__setstate__(self, d)
- # self.build must be filled in by our parent
- for loog in self.logs:
- loog.step = self
-
- def upgradeToVersion1(self):
- if not hasattr(self, "urls"):
- self.urls = {}
-
- def upgradeToVersion2(self):
- if not hasattr(self, "statistics"):
- self.statistics = {}
-
-
-class BuildStatus(styles.Versioned):
- implements(interfaces.IBuildStatus, interfaces.IStatusEvent)
- persistenceVersion = 3
-
- source = None
- reason = None
- changes = []
- blamelist = []
- progress = None
- started = None
- finished = None
- currentStep = None
- text = []
- color = None
- results = None
- slavename = "???"
-
- # these lists/dicts are defined here so that unserialized instances have
- # (empty) values. They are set in __init__ to new objects to make sure
- # each instance gets its own copy.
- watchers = []
- updates = {}
- finishedWatchers = []
- testResults = {}
-
- def __init__(self, parent, number):
- """
- @type parent: L{BuilderStatus}
- @type number: int
- """
- assert interfaces.IBuilderStatus(parent)
- self.builder = parent
- self.number = number
- self.watchers = []
- self.updates = {}
- self.finishedWatchers = []
- self.steps = []
- self.testResults = {}
- self.properties = Properties()
-
- # IBuildStatus
-
- def getBuilder(self):
- """
- @rtype: L{BuilderStatus}
- """
- return self.builder
-
- def getProperty(self, propname):
- return self.properties[propname]
-
- def getProperties(self):
- return self.properties
-
- def getNumber(self):
- return self.number
-
- def getPreviousBuild(self):
- if self.number == 0:
- return None
- return self.builder.getBuild(self.number-1)
-
- def getSourceStamp(self, absolute=False):
- if not absolute or not self.properties.has_key('got_revision'):
- return self.source
- return self.source.getAbsoluteSourceStamp(self.properties['got_revision'])
-
- def getReason(self):
- return self.reason
-
- def getChanges(self):
- return self.changes
-
- def getResponsibleUsers(self):
- return self.blamelist
-
- def getInterestedUsers(self):
- # TODO: the Builder should add others: sheriffs, domain-owners
- return self.blamelist
-
- def getSteps(self):
- """Return a list of IBuildStepStatus objects. For invariant builds
- (those which always use the same set of Steps), this should be the
- complete list, however some of the steps may not have started yet
- (step.getTimes()[0] will be None). For variant builds, this may not
- be complete (asking again later may give you more of them)."""
- return self.steps
-
- def getTimes(self):
- return (self.started, self.finished)
-
- _sentinel = [] # used as a sentinel to indicate unspecified initial_value
- def getSummaryStatistic(self, name, summary_fn, initial_value=_sentinel):
- """Summarize the named statistic over all steps in which it
- exists, using combination_fn and initial_value to combine multiple
- results into a single result. This translates to a call to Python's
- X{reduce}::
- return reduce(summary_fn, step_stats_list, initial_value)
- """
- step_stats_list = [
- st.getStatistic(name)
- for st in self.steps
- if st.hasStatistic(name) ]
- if initial_value is self._sentinel:
- return reduce(summary_fn, step_stats_list)
- else:
- return reduce(summary_fn, step_stats_list, initial_value)
-
- def isFinished(self):
- return (self.finished is not None)
-
- def waitUntilFinished(self):
- if self.finished:
- d = defer.succeed(self)
- else:
- d = defer.Deferred()
- self.finishedWatchers.append(d)
- return d
-
- # while the build is running, the following methods make sense.
- # Afterwards they return None
-
- def getETA(self):
- if self.finished is not None:
- return None
- if not self.progress:
- return None
- eta = self.progress.eta()
- if eta is None:
- return None
- return eta - util.now()
-
- def getCurrentStep(self):
- return self.currentStep
-
- # Once you know the build has finished, the following methods are legal.
- # Before ths build has finished, they all return None.
-
- def getText(self):
- text = []
- text.extend(self.text)
- for s in self.steps:
- text.extend(s.text2)
- return text
-
- def getColor(self):
- return self.color
-
- def getResults(self):
- return self.results
-
- def getSlavename(self):
- return self.slavename
-
- def getTestResults(self):
- return self.testResults
-
- def getLogs(self):
- # TODO: steps should contribute significant logs instead of this
- # hack, which returns every log from every step. The logs should get
- # names like "compile" and "test" instead of "compile.output"
- logs = []
- for s in self.steps:
- for log in s.getLogs():
- logs.append(log)
- return logs
-
- # subscription interface
-
- def subscribe(self, receiver, updateInterval=None):
- # will receive stepStarted and stepFinished messages
- # and maybe buildETAUpdate
- self.watchers.append(receiver)
- if updateInterval is not None:
- self.sendETAUpdate(receiver, updateInterval)
-
- def sendETAUpdate(self, receiver, updateInterval):
- self.updates[receiver] = None
- ETA = self.getETA()
- if ETA is not None:
- receiver.buildETAUpdate(self, self.getETA())
- # they might have unsubscribed during buildETAUpdate
- if receiver in self.watchers:
- self.updates[receiver] = reactor.callLater(updateInterval,
- self.sendETAUpdate,
- receiver,
- updateInterval)
-
- def unsubscribe(self, receiver):
- if receiver in self.watchers:
- self.watchers.remove(receiver)
- if receiver in self.updates:
- if self.updates[receiver] is not None:
- self.updates[receiver].cancel()
- del self.updates[receiver]
-
- # methods for the base.Build to invoke
-
- def addStepWithName(self, name):
- """The Build is setting up, and has added a new BuildStep to its
- list. Create a BuildStepStatus object to which it can send status
- updates."""
-
- s = BuildStepStatus(self)
- s.setName(name)
- self.steps.append(s)
- return s
-
- def setProperty(self, propname, value, source):
- self.properties.setProperty(propname, value, source)
-
- def addTestResult(self, result):
- self.testResults[result.getName()] = result
-
- def setSourceStamp(self, sourceStamp):
- self.source = sourceStamp
- self.changes = self.source.changes
-
- def setReason(self, reason):
- self.reason = reason
- def setBlamelist(self, blamelist):
- self.blamelist = blamelist
- def setProgress(self, progress):
- self.progress = progress
-
- def buildStarted(self, build):
- """The Build has been set up and is about to be started. It can now
- be safely queried, so it is time to announce the new build."""
-
- self.started = util.now()
- # now that we're ready to report status, let the BuilderStatus tell
- # the world about us
- self.builder.buildStarted(self)
-
- def setSlavename(self, slavename):
- self.slavename = slavename
-
- def setText(self, text):
- assert isinstance(text, (list, tuple))
- self.text = text
- def setColor(self, color):
- self.color = color
- def setResults(self, results):
- self.results = results
-
- def buildFinished(self):
- self.currentStep = None
- self.finished = util.now()
-
- for r in self.updates.keys():
- if self.updates[r] is not None:
- self.updates[r].cancel()
- del self.updates[r]
-
- watchers = self.finishedWatchers
- self.finishedWatchers = []
- for w in watchers:
- w.callback(self)
-
- # methods called by our BuildStepStatus children
-
- def stepStarted(self, step):
- self.currentStep = step
- name = self.getBuilder().getName()
- for w in self.watchers:
- receiver = w.stepStarted(self, step)
- if receiver:
- if type(receiver) == type(()):
- step.subscribe(receiver[0], receiver[1])
- else:
- step.subscribe(receiver)
- d = step.waitUntilFinished()
- d.addCallback(lambda step: step.unsubscribe(receiver))
-
- step.waitUntilFinished().addCallback(self._stepFinished)
-
- def _stepFinished(self, step):
- results = step.getResults()
- for w in self.watchers:
- w.stepFinished(self, step, results)
-
- # methods called by our BuilderStatus parent
-
- def pruneLogs(self):
- # this build is somewhat old: remove the build logs to save space
- # TODO: delete logs visible through IBuildStatus.getLogs
- for s in self.steps:
- s.pruneLogs()
-
- def pruneSteps(self):
- # this build is very old: remove the build steps too
- self.steps = []
-
- # persistence stuff
-
- def generateLogfileName(self, stepname, logname):
- """Return a filename (relative to the Builder's base directory) where
- the logfile's contents can be stored uniquely.
-
- The base filename is made by combining our build number, the Step's
- name, and the log's name, then removing unsuitable characters. The
- filename is then made unique by appending _0, _1, etc, until it does
- not collide with any other logfile.
-
- These files are kept in the Builder's basedir (rather than a
- per-Build subdirectory) because that makes cleanup easier: cron and
- find will help get rid of the old logs, but the empty directories are
- more of a hassle to remove."""
-
- starting_filename = "%d-log-%s-%s" % (self.number, stepname, logname)
- starting_filename = re.sub(r'[^\w\.\-]', '_', starting_filename)
- # now make it unique
- unique_counter = 0
- filename = starting_filename
- while filename in [l.filename
- for step in self.steps
- for l in step.getLogs()
- if l.filename]:
- filename = "%s_%d" % (starting_filename, unique_counter)
- unique_counter += 1
- return filename
-
- def __getstate__(self):
- d = styles.Versioned.__getstate__(self)
- # for now, a serialized Build is always "finished". We will never
- # save unfinished builds.
- if not self.finished:
- d['finished'] = True
- # TODO: push an "interrupted" step so it is clear that the build
- # was interrupted. The builder will have a 'shutdown' event, but
- # someone looking at just this build will be confused as to why
- # the last log is truncated.
- del d['builder'] # filled in by our parent when loading
- del d['watchers']
- del d['updates']
- del d['finishedWatchers']
- return d
-
- def __setstate__(self, d):
- styles.Versioned.__setstate__(self, d)
- # self.builder must be filled in by our parent when loading
- for step in self.steps:
- step.build = self
- self.watchers = []
- self.updates = {}
- self.finishedWatchers = []
-
- def upgradeToVersion1(self):
- if hasattr(self, "sourceStamp"):
- # the old .sourceStamp attribute wasn't actually very useful
- maxChangeNumber, patch = self.sourceStamp
- changes = getattr(self, 'changes', [])
- source = sourcestamp.SourceStamp(branch=None,
- revision=None,
- patch=patch,
- changes=changes)
- self.source = source
- self.changes = source.changes
- del self.sourceStamp
-
- def upgradeToVersion2(self):
- self.properties = {}
-
- def upgradeToVersion3(self):
- # in version 3, self.properties became a Properties object
- propdict = self.properties
- self.properties = Properties()
- self.properties.update(propdict, "Upgrade from previous version")
-
- def upgradeLogfiles(self):
- # upgrade any LogFiles that need it. This must occur after we've been
- # attached to our Builder, and after we know about all LogFiles of
- # all Steps (to get the filenames right).
- assert self.builder
- for s in self.steps:
- for l in s.getLogs():
- if l.filename:
- pass # new-style, log contents are on disk
- else:
- logfilename = self.generateLogfileName(s.name, l.name)
- # let the logfile update its .filename pointer,
- # transferring its contents onto disk if necessary
- l.upgrade(logfilename)
-
- def saveYourself(self):
- filename = os.path.join(self.builder.basedir, "%d" % self.number)
- if os.path.isdir(filename):
- # leftover from 0.5.0, which stored builds in directories
- shutil.rmtree(filename, ignore_errors=True)
- tmpfilename = filename + ".tmp"
- try:
- dump(self, open(tmpfilename, "wb"), -1)
- if sys.platform == 'win32':
- # windows cannot rename a file on top of an existing one, so
- # fall back to delete-first. There are ways this can fail and
- # lose the builder's history, so we avoid using it in the
- # general (non-windows) case
- if os.path.exists(filename):
- os.unlink(filename)
- os.rename(tmpfilename, filename)
- except:
- log.msg("unable to save build %s-#%d" % (self.builder.name,
- self.number))
- log.err()
-
-
-
-class BuilderStatus(styles.Versioned):
- """I handle status information for a single process.base.Builder object.
- That object sends status changes to me (frequently as Events), and I
- provide them on demand to the various status recipients, like the HTML
- waterfall display and the live status clients. It also sends build
- summaries to me, which I log and provide to status clients who aren't
- interested in seeing details of the individual build steps.
-
- I am responsible for maintaining the list of historic Events and Builds,
- pruning old ones, and loading them from / saving them to disk.
-
- I live in the buildbot.process.base.Builder object, in the
- .builder_status attribute.
-
- @type category: string
- @ivar category: user-defined category this builder belongs to; can be
- used to filter on in status clients
- """
-
- implements(interfaces.IBuilderStatus, interfaces.IEventSource)
- persistenceVersion = 1
-
- # these limit the amount of memory we consume, as well as the size of the
- # main Builder pickle. The Build and LogFile pickles on disk must be
- # handled separately.
- buildCacheSize = 30
- buildHorizon = 100 # forget builds beyond this
- stepHorizon = 50 # forget steps in builds beyond this
-
- category = None
- currentBigState = "offline" # or idle/waiting/interlocked/building
- basedir = None # filled in by our parent
-
- def __init__(self, buildername, category=None):
- self.name = buildername
- self.category = category
-
- self.slavenames = []
- self.events = []
- # these three hold Events, and are used to retrieve the current
- # state of the boxes.
- self.lastBuildStatus = None
- #self.currentBig = None
- #self.currentSmall = None
- self.currentBuilds = []
- self.pendingBuilds = []
- self.nextBuild = None
- self.watchers = []
- self.buildCache = [] # TODO: age builds out of the cache
-
- # persistence
-
- def __getstate__(self):
- # when saving, don't record transient stuff like what builds are
- # currently running, because they won't be there when we start back
- # up. Nor do we save self.watchers, nor anything that gets set by our
- # parent like .basedir and .status
- d = styles.Versioned.__getstate__(self)
- d['watchers'] = []
- del d['buildCache']
- for b in self.currentBuilds:
- b.saveYourself()
- # TODO: push a 'hey, build was interrupted' event
- del d['currentBuilds']
- del d['pendingBuilds']
- del d['currentBigState']
- del d['basedir']
- del d['status']
- del d['nextBuildNumber']
- return d
-
- def __setstate__(self, d):
- # when loading, re-initialize the transient stuff. Remember that
- # upgradeToVersion1 and such will be called after this finishes.
- styles.Versioned.__setstate__(self, d)
- self.buildCache = []
- self.currentBuilds = []
- self.pendingBuilds = []
- self.watchers = []
- self.slavenames = []
- # self.basedir must be filled in by our parent
- # self.status must be filled in by our parent
-
- def upgradeToVersion1(self):
- if hasattr(self, 'slavename'):
- self.slavenames = [self.slavename]
- del self.slavename
- if hasattr(self, 'nextBuildNumber'):
- del self.nextBuildNumber # determineNextBuildNumber chooses this
-
- def determineNextBuildNumber(self):
- """Scan our directory of saved BuildStatus instances to determine
- what our self.nextBuildNumber should be. Set it one larger than the
- highest-numbered build we discover. This is called by the top-level
- Status object shortly after we are created or loaded from disk.
- """
- existing_builds = [int(f)
- for f in os.listdir(self.basedir)
- if re.match("^\d+$", f)]
- if existing_builds:
- self.nextBuildNumber = max(existing_builds) + 1
- else:
- self.nextBuildNumber = 0
-
- def saveYourself(self):
- for b in self.buildCache:
- if not b.isFinished:
- # interrupted build, need to save it anyway.
- # BuildStatus.saveYourself will mark it as interrupted.
- b.saveYourself()
- filename = os.path.join(self.basedir, "builder")
- tmpfilename = filename + ".tmp"
- try:
- dump(self, open(tmpfilename, "wb"), -1)
- if sys.platform == 'win32':
- # windows cannot rename a file on top of an existing one
- if os.path.exists(filename):
- os.unlink(filename)
- os.rename(tmpfilename, filename)
- except:
- log.msg("unable to save builder %s" % self.name)
- log.err()
-
-
- # build cache management
-
- def addBuildToCache(self, build):
- if build in self.buildCache:
- return
- self.buildCache.append(build)
- while len(self.buildCache) > self.buildCacheSize:
- self.buildCache.pop(0)
-
- def getBuildByNumber(self, number):
- for b in self.currentBuilds:
- if b.number == number:
- return b
- for build in self.buildCache:
- if build.number == number:
- return build
- filename = os.path.join(self.basedir, "%d" % number)
- try:
- build = load(open(filename, "rb"))
- styles.doUpgrade()
- build.builder = self
- # handle LogFiles from after 0.5.0 and before 0.6.5
- build.upgradeLogfiles()
- self.addBuildToCache(build)
- return build
- except IOError:
- raise IndexError("no such build %d" % number)
- except EOFError:
- raise IndexError("corrupted build pickle %d" % number)
-
- def prune(self):
- return # TODO: change this to walk through the filesystem
- # first, blow away all builds beyond our build horizon
- self.builds = self.builds[-self.buildHorizon:]
- # then prune steps in builds past the step horizon
- for b in self.builds[0:-self.stepHorizon]:
- b.pruneSteps()
-
- # IBuilderStatus methods
- def getName(self):
- return self.name
-
- def getState(self):
- return (self.currentBigState, self.currentBuilds)
-
- def getSlaves(self):
- return [self.status.getSlave(name) for name in self.slavenames]
-
- def getPendingBuilds(self):
- return self.pendingBuilds
-
- def getCurrentBuilds(self):
- return self.currentBuilds
-
- def getLastFinishedBuild(self):
- b = self.getBuild(-1)
- if not (b and b.isFinished()):
- b = self.getBuild(-2)
- return b
-
- def getBuild(self, number):
- if number < 0:
- number = self.nextBuildNumber + number
- if number < 0 or number >= self.nextBuildNumber:
- return None
-
- try:
- return self.getBuildByNumber(number)
- except IndexError:
- return None
-
- def getEvent(self, number):
- try:
- return self.events[number]
- except IndexError:
- return None
-
- def generateFinishedBuilds(self, branches=[],
- num_builds=None,
- max_buildnum=None,
- finished_before=None,
- max_search=200):
- got = 0
- for Nb in itertools.count(1):
- if Nb > self.nextBuildNumber:
- break
- if Nb > max_search:
- break
- build = self.getBuild(-Nb)
- if build is None:
- continue
- if max_buildnum is not None:
- if build.getNumber() > max_buildnum:
- continue
- if not build.isFinished():
- continue
- if finished_before is not None:
- start, end = build.getTimes()
- if end >= finished_before:
- continue
- if branches:
- if build.getSourceStamp().branch not in branches:
- continue
- got += 1
- yield build
- if num_builds is not None:
- if got >= num_builds:
- return
-
- def eventGenerator(self, branches=[]):
- """This function creates a generator which will provide all of this
- Builder's status events, starting with the most recent and
- progressing backwards in time. """
-
- # remember the oldest-to-earliest flow here. "next" means earlier.
-
- # TODO: interleave build steps and self.events by timestamp.
- # TODO: um, I think we're already doing that.
-
- # TODO: there's probably something clever we could do here to
- # interleave two event streams (one from self.getBuild and the other
- # from self.getEvent), which would be simpler than this control flow
-
- eventIndex = -1
- e = self.getEvent(eventIndex)
- for Nb in range(1, self.nextBuildNumber+1):
- b = self.getBuild(-Nb)
- if not b:
- break
- if branches and not b.getSourceStamp().branch in branches:
- continue
- steps = b.getSteps()
- for Ns in range(1, len(steps)+1):
- if steps[-Ns].started:
- step_start = steps[-Ns].getTimes()[0]
- while e is not None and e.getTimes()[0] > step_start:
- yield e
- eventIndex -= 1
- e = self.getEvent(eventIndex)
- yield steps[-Ns]
- yield b
- while e is not None:
- yield e
- eventIndex -= 1
- e = self.getEvent(eventIndex)
-
- def subscribe(self, receiver):
- # will get builderChangedState, buildStarted, and buildFinished
- self.watchers.append(receiver)
- self.publishState(receiver)
-
- def unsubscribe(self, receiver):
- self.watchers.remove(receiver)
-
- ## Builder interface (methods called by the Builder which feeds us)
-
- def setSlavenames(self, names):
- self.slavenames = names
-
- def addEvent(self, text=[], color=None):
- # this adds a duration event. When it is done, the user should call
- # e.finish(). They can also mangle it by modifying .text and .color
- e = Event()
- e.started = util.now()
- e.text = text
- e.color = color
- self.events.append(e)
- return e # they are free to mangle it further
-
- def addPointEvent(self, text=[], color=None):
- # this adds a point event, one which occurs as a single atomic
- # instant of time.
- e = Event()
- e.started = util.now()
- e.finished = 0
- e.text = text
- e.color = color
- self.events.append(e)
- return e # for consistency, but they really shouldn't touch it
-
- def setBigState(self, state):
- needToUpdate = state != self.currentBigState
- self.currentBigState = state
- if needToUpdate:
- self.publishState()
-
- def publishState(self, target=None):
- state = self.currentBigState
-
- if target is not None:
- # unicast
- target.builderChangedState(self.name, state)
- return
- for w in self.watchers:
- w.builderChangedState(self.name, state)
-
- def newBuild(self):
- """The Builder has decided to start a build, but the Build object is
- not yet ready to report status (it has not finished creating the
- Steps). Create a BuildStatus object that it can use."""
- number = self.nextBuildNumber
- self.nextBuildNumber += 1
- # TODO: self.saveYourself(), to make sure we don't forget about the
- # build number we've just allocated. This is not quite as important
- # as it was before we switch to determineNextBuildNumber, but I think
- # it may still be useful to have the new build save itself.
- s = BuildStatus(self, number)
- s.waitUntilFinished().addCallback(self._buildFinished)
- return s
-
- def addBuildRequest(self, brstatus):
- self.pendingBuilds.append(brstatus)
- def removeBuildRequest(self, brstatus):
- self.pendingBuilds.remove(brstatus)
-
- # buildStarted is called by our child BuildStatus instances
- def buildStarted(self, s):
- """Now the BuildStatus object is ready to go (it knows all of its
- Steps, its ETA, etc), so it is safe to notify our watchers."""
-
- assert s.builder is self # paranoia
- assert s.number == self.nextBuildNumber - 1
- assert s not in self.currentBuilds
- self.currentBuilds.append(s)
- self.addBuildToCache(s)
-
- # now that the BuildStatus is prepared to answer queries, we can
- # announce the new build to all our watchers
-
- for w in self.watchers: # TODO: maybe do this later? callLater(0)?
- receiver = w.buildStarted(self.getName(), s)
- if receiver:
- if type(receiver) == type(()):
- s.subscribe(receiver[0], receiver[1])
- else:
- s.subscribe(receiver)
- d = s.waitUntilFinished()
- d.addCallback(lambda s: s.unsubscribe(receiver))
-
-
- def _buildFinished(self, s):
- assert s in self.currentBuilds
- s.saveYourself()
- self.currentBuilds.remove(s)
-
- name = self.getName()
- results = s.getResults()
- for w in self.watchers:
- w.buildFinished(name, s, results)
-
- self.prune() # conserve disk
-
-
- # waterfall display (history)
-
- # I want some kind of build event that holds everything about the build:
- # why, what changes went into it, the results of the build, itemized
- # test results, etc. But, I do kind of need something to be inserted in
- # the event log first, because intermixing step events and the larger
- # build event is fraught with peril. Maybe an Event-like-thing that
- # doesn't have a file in it but does have links. Hmm, that's exactly
- # what it does now. The only difference would be that this event isn't
- # pushed to the clients.
-
- # publish to clients
- def sendLastBuildStatus(self, client):
- #client.newLastBuildStatus(self.lastBuildStatus)
- pass
- def sendCurrentActivityBigToEveryone(self):
- for s in self.subscribers:
- self.sendCurrentActivityBig(s)
- def sendCurrentActivityBig(self, client):
- state = self.currentBigState
- if state == "offline":
- client.currentlyOffline()
- elif state == "idle":
- client.currentlyIdle()
- elif state == "building":
- client.currentlyBuilding()
- else:
- log.msg("Hey, self.currentBigState is weird:", state)
-
-
- ## HTML display interface
-
- def getEventNumbered(self, num):
- # deal with dropped events, pruned events
- first = self.events[0].number
- if first + len(self.events)-1 != self.events[-1].number:
- log.msg(self,
- "lost an event somewhere: [0] is %d, [%d] is %d" % \
- (self.events[0].number,
- len(self.events) - 1,
- self.events[-1].number))
- for e in self.events:
- log.msg("e[%d]: " % e.number, e)
- return None
- offset = num - first
- log.msg(self, "offset", offset)
- try:
- return self.events[offset]
- except IndexError:
- return None
-
- ## Persistence of Status
- def loadYourOldEvents(self):
- if hasattr(self, "allEvents"):
- # first time, nothing to get from file. Note that this is only if
- # the Application gets .run() . If it gets .save()'ed, then the
- # .allEvents attribute goes away in the initial __getstate__ and
- # we try to load a non-existent file.
- return
- self.allEvents = self.loadFile("events", [])
- if self.allEvents:
- self.nextEventNumber = self.allEvents[-1].number + 1
- else:
- self.nextEventNumber = 0
- def saveYourOldEvents(self):
- self.saveFile("events", self.allEvents)
-
- ## clients
-
- def addClient(self, client):
- if client not in self.subscribers:
- self.subscribers.append(client)
- self.sendLastBuildStatus(client)
- self.sendCurrentActivityBig(client)
- client.newEvent(self.currentSmall)
- def removeClient(self, client):
- if client in self.subscribers:
- self.subscribers.remove(client)
-
-class SlaveStatus:
- implements(interfaces.ISlaveStatus)
-
- admin = None
- host = None
- connected = False
-
- def __init__(self, name):
- self.name = name
- self._lastMessageReceived = 0
- self.runningBuilds = []
-
- def getName(self):
- return self.name
- def getAdmin(self):
- return self.admin
- def getHost(self):
- return self.host
- def isConnected(self):
- return self.connected
- def lastMessageReceived(self):
- return self._lastMessageReceived
- def getRunningBuilds(self):
- return self.runningBuilds
-
- def setAdmin(self, admin):
- self.admin = admin
- def setHost(self, host):
- self.host = host
- def setConnected(self, isConnected):
- self.connected = isConnected
- def setLastMessageReceived(self, when):
- self._lastMessageReceived = when
-
- def buildStarted(self, build):
- self.runningBuilds.append(build)
- def buildFinished(self, build):
- self.runningBuilds.remove(build)
-
-class Status:
- """
- I represent the status of the buildmaster.
- """
- implements(interfaces.IStatus)
-
- def __init__(self, botmaster, basedir):
- """
- @type botmaster: L{buildbot.master.BotMaster}
- @param botmaster: the Status object uses C{.botmaster} to get at
- both the L{buildbot.master.BuildMaster} (for
- various buildbot-wide parameters) and the
- actual Builders (to get at their L{BuilderStatus}
- objects). It is not allowed to change or influence
- anything through this reference.
- @type basedir: string
- @param basedir: this provides a base directory in which saved status
- information (changes.pck, saved Build status
- pickles) can be stored
- """
- self.botmaster = botmaster
- self.basedir = basedir
- self.watchers = []
- self.activeBuildSets = []
- assert os.path.isdir(basedir)
-
-
- # methods called by our clients
-
- def getProjectName(self):
- return self.botmaster.parent.projectName
- def getProjectURL(self):
- return self.botmaster.parent.projectURL
- def getBuildbotURL(self):
- return self.botmaster.parent.buildbotURL
-
- def getURLForThing(self, thing):
- prefix = self.getBuildbotURL()
- if not prefix:
- return None
- if interfaces.IStatus.providedBy(thing):
- return prefix
- if interfaces.ISchedulerStatus.providedBy(thing):
- pass
- if interfaces.IBuilderStatus.providedBy(thing):
- builder = thing
- return prefix + "builders/%s" % (
- urllib.quote(builder.getName(), safe=''),
- )
- if interfaces.IBuildStatus.providedBy(thing):
- build = thing
- builder = build.getBuilder()
- return prefix + "builders/%s/builds/%d" % (
- urllib.quote(builder.getName(), safe=''),
- build.getNumber())
- if interfaces.IBuildStepStatus.providedBy(thing):
- step = thing
- build = step.getBuild()
- builder = build.getBuilder()
- return prefix + "builders/%s/builds/%d/steps/%s" % (
- urllib.quote(builder.getName(), safe=''),
- build.getNumber(),
- urllib.quote(step.getName(), safe=''))
- # IBuildSetStatus
- # IBuildRequestStatus
- # ISlaveStatus
-
- # IStatusEvent
- if interfaces.IStatusEvent.providedBy(thing):
- from buildbot.changes import changes
- # TODO: this is goofy, create IChange or something
- if isinstance(thing, changes.Change):
- change = thing
- return "%schanges/%d" % (prefix, change.number)
-
- if interfaces.IStatusLog.providedBy(thing):
- log = thing
- step = log.getStep()
- build = step.getBuild()
- builder = build.getBuilder()
-
- logs = step.getLogs()
- for i in range(len(logs)):
- if log is logs[i]:
- lognum = i
- break
- else:
- return None
- return prefix + "builders/%s/builds/%d/steps/%s/logs/%d" % (
- urllib.quote(builder.getName(), safe=''),
- build.getNumber(),
- urllib.quote(step.getName(), safe=''),
- lognum)
-
- def getChangeSources(self):
- return list(self.botmaster.parent.change_svc)
-
- def getChange(self, number):
- return self.botmaster.parent.change_svc.getChangeNumbered(number)
-
- def getSchedulers(self):
- return self.botmaster.parent.allSchedulers()
-
- def getBuilderNames(self, categories=None):
- if categories == None:
- return self.botmaster.builderNames[:] # don't let them break it
-
- l = []
- # respect addition order
- for name in self.botmaster.builderNames:
- builder = self.botmaster.builders[name]
- if builder.builder_status.category in categories:
- l.append(name)
- return l
-
- def getBuilder(self, name):
- """
- @rtype: L{BuilderStatus}
- """
- return self.botmaster.builders[name].builder_status
-
- def getSlaveNames(self):
- return self.botmaster.slaves.keys()
-
- def getSlave(self, slavename):
- return self.botmaster.slaves[slavename].slave_status
-
- def getBuildSets(self):
- return self.activeBuildSets[:]
-
- def generateFinishedBuilds(self, builders=[], branches=[],
- num_builds=None, finished_before=None,
- max_search=200):
-
- def want_builder(bn):
- if builders:
- return bn in builders
- return True
- builder_names = [bn
- for bn in self.getBuilderNames()
- if want_builder(bn)]
-
- # 'sources' is a list of generators, one for each Builder we're
- # using. When the generator is exhausted, it is replaced in this list
- # with None.
- sources = []
- for bn in builder_names:
- b = self.getBuilder(bn)
- g = b.generateFinishedBuilds(branches,
- finished_before=finished_before,
- max_search=max_search)
- sources.append(g)
-
- # next_build the next build from each source
- next_build = [None] * len(sources)
-
- def refill():
- for i,g in enumerate(sources):
- if next_build[i]:
- # already filled
- continue
- if not g:
- # already exhausted
- continue
- try:
- next_build[i] = g.next()
- except StopIteration:
- next_build[i] = None
- sources[i] = None
-
- got = 0
- while True:
- refill()
- # find the latest build among all the candidates
- candidates = [(i, b, b.getTimes()[1])
- for i,b in enumerate(next_build)
- if b is not None]
- candidates.sort(lambda x,y: cmp(x[2], y[2]))
- if not candidates:
- return
-
- # and remove it from the list
- i, build, finshed_time = candidates[-1]
- next_build[i] = None
- got += 1
- yield build
- if num_builds is not None:
- if got >= num_builds:
- return
-
- def subscribe(self, target):
- self.watchers.append(target)
- for name in self.botmaster.builderNames:
- self.announceNewBuilder(target, name, self.getBuilder(name))
- def unsubscribe(self, target):
- self.watchers.remove(target)
-
-
- # methods called by upstream objects
-
- def announceNewBuilder(self, target, name, builder_status):
- t = target.builderAdded(name, builder_status)
- if t:
- builder_status.subscribe(t)
-
- def builderAdded(self, name, basedir, category=None):
- """
- @rtype: L{BuilderStatus}
- """
- filename = os.path.join(self.basedir, basedir, "builder")
- log.msg("trying to load status pickle from %s" % filename)
- builder_status = None
- try:
- builder_status = load(open(filename, "rb"))
- styles.doUpgrade()
- except IOError:
- log.msg("no saved status pickle, creating a new one")
- except:
- log.msg("error while loading status pickle, creating a new one")
- log.msg("error follows:")
- log.err()
- if not builder_status:
- builder_status = BuilderStatus(name, category)
- builder_status.addPointEvent(["builder", "created"])
- log.msg("added builder %s in category %s" % (name, category))
- # an unpickled object might not have category set from before,
- # so set it here to make sure
- builder_status.category = category
- builder_status.basedir = os.path.join(self.basedir, basedir)
- builder_status.name = name # it might have been updated
- builder_status.status = self
-
- if not os.path.isdir(builder_status.basedir):
- os.mkdir(builder_status.basedir)
- builder_status.determineNextBuildNumber()
-
- builder_status.setBigState("offline")
-
- for t in self.watchers:
- self.announceNewBuilder(t, name, builder_status)
-
- return builder_status
-
- def builderRemoved(self, name):
- for t in self.watchers:
- t.builderRemoved(name)
-
- def prune(self):
- for b in self.botmaster.builders.values():
- b.builder_status.prune()
-
- def buildsetSubmitted(self, bss):
- self.activeBuildSets.append(bss)
- bss.waitUntilFinished().addCallback(self.activeBuildSets.remove)
- for t in self.watchers:
- t.buildsetSubmitted(bss)
diff --git a/tools/buildbot/pylibs/buildbot/status/client.py b/tools/buildbot/pylibs/buildbot/status/client.py
deleted file mode 100644
index c160c37..0000000
--- a/tools/buildbot/pylibs/buildbot/status/client.py
+++ /dev/null
@@ -1,568 +0,0 @@
-# -*- test-case-name: buildbot.test.test_status -*-
-
-from twisted.spread import pb
-from twisted.python import components, log as twlog
-from twisted.internet import reactor
-from twisted.application import strports
-from twisted.cred import portal, checkers
-
-from buildbot import interfaces
-from zope.interface import Interface, implements
-from buildbot.status import builder, base
-from buildbot.changes import changes
-
-class IRemote(Interface):
- pass
-
-def makeRemote(obj):
- # we want IRemote(None) to be None, but you can't really do that with
- # adapters, so we fake it
- if obj is None:
- return None
- return IRemote(obj)
-
-
-class RemoteBuildSet(pb.Referenceable):
- def __init__(self, buildset):
- self.b = buildset
-
- def remote_getSourceStamp(self):
- return self.b.getSourceStamp()
-
- def remote_getReason(self):
- return self.b.getReason()
-
- def remote_getID(self):
- return self.b.getID()
-
- def remote_getBuilderNames(self):
- return self.b.getBuilderNames()
-
- def remote_getBuildRequests(self):
- """Returns a list of (builderName, BuildRequest) tuples."""
- return [(br.getBuilderName(), IRemote(br))
- for br in self.b.getBuildRequests()]
-
- def remote_isFinished(self):
- return self.b.isFinished()
-
- def remote_waitUntilSuccess(self):
- d = self.b.waitUntilSuccess()
- d.addCallback(lambda res: self)
- return d
-
- def remote_waitUntilFinished(self):
- d = self.b.waitUntilFinished()
- d.addCallback(lambda res: self)
- return d
-
- def remote_getResults(self):
- return self.b.getResults()
-
-components.registerAdapter(RemoteBuildSet,
- interfaces.IBuildSetStatus, IRemote)
-
-
-class RemoteBuilder(pb.Referenceable):
- def __init__(self, builder):
- self.b = builder
-
- def remote_getName(self):
- return self.b.getName()
-
- def remote_getState(self):
- state, builds = self.b.getState()
- return (state,
- None, # TODO: remove leftover ETA
- [makeRemote(b) for b in builds])
-
- def remote_getSlaves(self):
- return [IRemote(s) for s in self.b.getSlaves()]
-
- def remote_getLastFinishedBuild(self):
- return makeRemote(self.b.getLastFinishedBuild())
-
- def remote_getCurrentBuilds(self):
- return [IRemote(b) for b in self.b.getCurrentBuilds()]
-
- def remote_getBuild(self, number):
- return makeRemote(self.b.getBuild(number))
-
- def remote_getEvent(self, number):
- return IRemote(self.b.getEvent(number))
-
-components.registerAdapter(RemoteBuilder,
- interfaces.IBuilderStatus, IRemote)
-
-
-class RemoteBuildRequest(pb.Referenceable):
- def __init__(self, buildreq):
- self.b = buildreq
- self.observers = []
-
- def remote_getSourceStamp(self):
- return self.b.getSourceStamp()
-
- def remote_getBuilderName(self):
- return self.b.getBuilderName()
-
- def remote_subscribe(self, observer):
- """The observer's remote_newbuild method will be called (with two
- arguments: the RemoteBuild object, and our builderName) for each new
- Build that is created to handle this BuildRequest."""
- self.observers.append(observer)
- def send(bs):
- d = observer.callRemote("newbuild",
- IRemote(bs), self.b.getBuilderName())
- d.addErrback(lambda err: None)
- reactor.callLater(0, self.b.subscribe, send)
-
- def remote_unsubscribe(self, observer):
- # PB (well, at least oldpb) doesn't re-use RemoteReference instances,
- # so sending the same object across the wire twice will result in two
- # separate objects that compare as equal ('a is not b' and 'a == b').
- # That means we can't use a simple 'self.observers.remove(observer)'
- # here.
- for o in self.observers:
- if o == observer:
- self.observers.remove(o)
-
-components.registerAdapter(RemoteBuildRequest,
- interfaces.IBuildRequestStatus, IRemote)
-
-class RemoteBuild(pb.Referenceable):
- def __init__(self, build):
- self.b = build
- self.observers = []
-
- def remote_getBuilderName(self):
- return self.b.getBuilder().getName()
-
- def remote_getNumber(self):
- return self.b.getNumber()
-
- def remote_getReason(self):
- return self.b.getReason()
-
- def remote_getChanges(self):
- return [IRemote(c) for c in self.b.getChanges()]
-
- def remote_getResponsibleUsers(self):
- return self.b.getResponsibleUsers()
-
- def remote_getSteps(self):
- return [IRemote(s) for s in self.b.getSteps()]
-
- def remote_getTimes(self):
- return self.b.getTimes()
-
- def remote_isFinished(self):
- return self.b.isFinished()
-
- def remote_waitUntilFinished(self):
- # the Deferred returned by callRemote() will fire when this build is
- # finished
- d = self.b.waitUntilFinished()
- d.addCallback(lambda res: self)
- return d
-
- def remote_getETA(self):
- return self.b.getETA()
-
- def remote_getCurrentStep(self):
- return makeRemote(self.b.getCurrentStep())
-
- def remote_getText(self):
- return self.b.getText()
-
- def remote_getColor(self):
- return self.b.getColor()
-
- def remote_getResults(self):
- return self.b.getResults()
-
- def remote_getLogs(self):
- logs = {}
- for name,log in self.b.getLogs().items():
- logs[name] = IRemote(log)
- return logs
-
- def remote_subscribe(self, observer, updateInterval=None):
- """The observer will have remote_stepStarted(buildername, build,
- stepname, step), remote_stepFinished(buildername, build, stepname,
- step, results), and maybe remote_buildETAUpdate(buildername, build,
- eta)) messages sent to it."""
- self.observers.append(observer)
- s = BuildSubscriber(observer)
- self.b.subscribe(s, updateInterval)
-
- def remote_unsubscribe(self, observer):
- # TODO: is the observer automatically unsubscribed when the build
- # finishes? Or are they responsible for unsubscribing themselves
- # anyway? How do we avoid a race condition here?
- for o in self.observers:
- if o == observer:
- self.observers.remove(o)
-
-
-components.registerAdapter(RemoteBuild,
- interfaces.IBuildStatus, IRemote)
-
-class BuildSubscriber:
- def __init__(self, observer):
- self.observer = observer
-
- def buildETAUpdate(self, build, eta):
- self.observer.callRemote("buildETAUpdate",
- build.getBuilder().getName(),
- IRemote(build),
- eta)
-
- def stepStarted(self, build, step):
- self.observer.callRemote("stepStarted",
- build.getBuilder().getName(),
- IRemote(build),
- step.getName(), IRemote(step))
- return None
-
- def stepFinished(self, build, step, results):
- self.observer.callRemote("stepFinished",
- build.getBuilder().getName(),
- IRemote(build),
- step.getName(), IRemote(step),
- results)
-
-
-class RemoteBuildStep(pb.Referenceable):
- def __init__(self, step):
- self.s = step
-
- def remote_getName(self):
- return self.s.getName()
-
- def remote_getBuild(self):
- return IRemote(self.s.getBuild())
-
- def remote_getTimes(self):
- return self.s.getTimes()
-
- def remote_getExpectations(self):
- return self.s.getExpectations()
-
- def remote_getLogs(self):
- logs = {}
- for log in self.s.getLogs():
- logs[log.getName()] = IRemote(log)
- return logs
-
- def remote_isFinished(self):
- return self.s.isFinished()
-
- def remote_waitUntilFinished(self):
- return self.s.waitUntilFinished() # returns a Deferred
-
- def remote_getETA(self):
- return self.s.getETA()
-
- def remote_getText(self):
- return self.s.getText()
-
- def remote_getColor(self):
- return self.s.getColor()
-
- def remote_getResults(self):
- return self.s.getResults()
-
-components.registerAdapter(RemoteBuildStep,
- interfaces.IBuildStepStatus, IRemote)
-
-class RemoteSlave:
- def __init__(self, slave):
- self.s = slave
-
- def remote_getName(self):
- return self.s.getName()
- def remote_getAdmin(self):
- return self.s.getAdmin()
- def remote_getHost(self):
- return self.s.getHost()
- def remote_isConnected(self):
- return self.s.isConnected()
-
-components.registerAdapter(RemoteSlave,
- interfaces.ISlaveStatus, IRemote)
-
-class RemoteEvent:
- def __init__(self, event):
- self.e = event
-
- def remote_getTimes(self):
- return self.s.getTimes()
- def remote_getText(self):
- return self.s.getText()
- def remote_getColor(self):
- return self.s.getColor()
-
-components.registerAdapter(RemoteEvent,
- interfaces.IStatusEvent, IRemote)
-
-class RemoteLog(pb.Referenceable):
- def __init__(self, log):
- self.l = log
-
- def remote_getName(self):
- return self.l.getName()
-
- def remote_isFinished(self):
- return self.l.isFinished()
- def remote_waitUntilFinished(self):
- d = self.l.waitUntilFinished()
- d.addCallback(lambda res: self)
- return d
-
- def remote_getText(self):
- return self.l.getText()
- def remote_getTextWithHeaders(self):
- return self.l.getTextWithHeaders()
- def remote_getChunks(self):
- return self.l.getChunks()
- # TODO: subscription interface
-
-components.registerAdapter(RemoteLog, builder.LogFile, IRemote)
-# TODO: something similar for builder.HTMLLogfile ?
-
-class RemoteChange:
- def __init__(self, change):
- self.c = change
-
- def getWho(self):
- return self.c.who
- def getFiles(self):
- return self.c.files
- def getComments(self):
- return self.c.comments
-
-components.registerAdapter(RemoteChange, changes.Change, IRemote)
-
-
-class StatusClientPerspective(base.StatusReceiverPerspective):
-
- subscribed = None
- client = None
-
- def __init__(self, status):
- self.status = status # the IStatus
- self.subscribed_to_builders = [] # Builders to which we're subscribed
- self.subscribed_to = [] # everything else we're subscribed to
-
- def __getstate__(self):
- d = self.__dict__.copy()
- d['client'] = None
- return d
-
- def attached(self, mind):
- #twlog.msg("StatusClientPerspective.attached")
- return self
-
- def detached(self, mind):
- twlog.msg("PB client detached")
- self.client = None
- for name in self.subscribed_to_builders:
- twlog.msg(" unsubscribing from Builder(%s)" % name)
- self.status.getBuilder(name).unsubscribe(self)
- for s in self.subscribed_to:
- twlog.msg(" unsubscribe from %s" % s)
- s.unsubscribe(self)
- self.subscribed = None
-
- def perspective_subscribe(self, mode, interval, target):
- """The remote client wishes to subscribe to some set of events.
- 'target' will be sent remote messages when these events happen.
- 'mode' indicates which events are desired: it is a string with one
- of the following values:
-
- 'builders': builderAdded, builderRemoved
- 'builds': those plus builderChangedState, buildStarted, buildFinished
- 'steps': all those plus buildETAUpdate, stepStarted, stepFinished
- 'logs': all those plus stepETAUpdate, logStarted, logFinished
- 'full': all those plus logChunk (with the log contents)
-
-
- Messages are defined by buildbot.interfaces.IStatusReceiver .
- 'interval' is used to specify how frequently ETAUpdate messages
- should be sent.
-
- Raising or lowering the subscription level will take effect starting
- with the next build or step."""
-
- assert mode in ("builders", "builds", "steps", "logs", "full")
- assert target
- twlog.msg("PB subscribe(%s)" % mode)
-
- self.client = target
- self.subscribed = mode
- self.interval = interval
- self.subscribed_to.append(self.status)
- # wait a moment before subscribing, so the new-builder messages
- # won't appear before this remote method finishes
- reactor.callLater(0, self.status.subscribe, self)
- return None
-
- def perspective_unsubscribe(self):
- twlog.msg("PB unsubscribe")
- self.status.unsubscribe(self)
- self.subscribed_to.remove(self.status)
- self.client = None
-
- def perspective_getBuildSets(self):
- """This returns tuples of (buildset, bsid), because that is much more
- convenient for tryclient."""
- return [(IRemote(s), s.getID()) for s in self.status.getBuildSets()]
-
- def perspective_getBuilderNames(self):
- return self.status.getBuilderNames()
-
- def perspective_getBuilder(self, name):
- b = self.status.getBuilder(name)
- return IRemote(b)
-
- def perspective_getSlave(self, name):
- s = self.status.getSlave(name)
- return IRemote(s)
-
- # IStatusReceiver methods, invoked if we've subscribed
-
- # mode >= builder
- def builderAdded(self, name, builder):
- self.client.callRemote("builderAdded", name, IRemote(builder))
- if self.subscribed in ("builds", "steps", "logs", "full"):
- self.subscribed_to_builders.append(name)
- return self
- return None
-
- def builderChangedState(self, name, state):
- self.client.callRemote("builderChangedState", name, state, None)
- # TODO: remove leftover ETA argument
-
- def builderRemoved(self, name):
- if name in self.subscribed_to_builders:
- self.subscribed_to_builders.remove(name)
- self.client.callRemote("builderRemoved", name)
-
- def buildsetSubmitted(self, buildset):
- # TODO: deliver to client, somehow
- pass
-
- # mode >= builds
- def buildStarted(self, name, build):
- self.client.callRemote("buildStarted", name, IRemote(build))
- if self.subscribed in ("steps", "logs", "full"):
- self.subscribed_to.append(build)
- return (self, self.interval)
- return None
-
- def buildFinished(self, name, build, results):
- if build in self.subscribed_to:
- # we might have joined during the build
- self.subscribed_to.remove(build)
- self.client.callRemote("buildFinished",
- name, IRemote(build), results)
-
- # mode >= steps
- def buildETAUpdate(self, build, eta):
- self.client.callRemote("buildETAUpdate",
- build.getBuilder().getName(), IRemote(build),
- eta)
-
- def stepStarted(self, build, step):
- # we add some information here so the client doesn't have to do an
- # extra round-trip
- self.client.callRemote("stepStarted",
- build.getBuilder().getName(), IRemote(build),
- step.getName(), IRemote(step))
- if self.subscribed in ("logs", "full"):
- self.subscribed_to.append(step)
- return (self, self.interval)
- return None
-
- def stepFinished(self, build, step, results):
- self.client.callRemote("stepFinished",
- build.getBuilder().getName(), IRemote(build),
- step.getName(), IRemote(step),
- results)
- if step in self.subscribed_to:
- # eventually (through some new subscription method) we could
- # join in the middle of the step
- self.subscribed_to.remove(step)
-
- # mode >= logs
- def stepETAUpdate(self, build, step, ETA, expectations):
- self.client.callRemote("stepETAUpdate",
- build.getBuilder().getName(), IRemote(build),
- step.getName(), IRemote(step),
- ETA, expectations)
-
- def logStarted(self, build, step, log):
- # TODO: make the HTMLLog adapter
- rlog = IRemote(log, None)
- if not rlog:
- print "hey, couldn't adapt %s to IRemote" % log
- self.client.callRemote("logStarted",
- build.getBuilder().getName(), IRemote(build),
- step.getName(), IRemote(step),
- log.getName(), IRemote(log, None))
- if self.subscribed in ("full",):
- self.subscribed_to.append(log)
- return self
- return None
-
- def logFinished(self, build, step, log):
- self.client.callRemote("logFinished",
- build.getBuilder().getName(), IRemote(build),
- step.getName(), IRemote(step),
- log.getName(), IRemote(log, None))
- if log in self.subscribed_to:
- self.subscribed_to.remove(log)
-
- # mode >= full
- def logChunk(self, build, step, log, channel, text):
- self.client.callRemote("logChunk",
- build.getBuilder().getName(), IRemote(build),
- step.getName(), IRemote(step),
- log.getName(), IRemote(log),
- channel, text)
-
-
-class PBListener(base.StatusReceiverMultiService):
- """I am a listener for PB-based status clients."""
-
- compare_attrs = ["port", "cred"]
- implements(portal.IRealm)
-
- def __init__(self, port, user="statusClient", passwd="clientpw"):
- base.StatusReceiverMultiService.__init__(self)
- if type(port) is int:
- port = "tcp:%d" % port
- self.port = port
- self.cred = (user, passwd)
- p = portal.Portal(self)
- c = checkers.InMemoryUsernamePasswordDatabaseDontUse()
- c.addUser(user, passwd)
- p.registerChecker(c)
- f = pb.PBServerFactory(p)
- s = strports.service(port, f)
- s.setServiceParent(self)
-
- def setServiceParent(self, parent):
- base.StatusReceiverMultiService.setServiceParent(self, parent)
- self.setup()
-
- def setup(self):
- self.status = self.parent.getStatus()
-
- def requestAvatar(self, avatarID, mind, interface):
- assert interface == pb.IPerspective
- p = StatusClientPerspective(self.status)
- p.attached(mind) # perhaps .callLater(0) ?
- return (pb.IPerspective, p,
- lambda p=p,mind=mind: p.detached(mind))
diff --git a/tools/buildbot/pylibs/buildbot/status/html.py b/tools/buildbot/pylibs/buildbot/status/html.py
deleted file mode 100644
index cc36a4a..0000000
--- a/tools/buildbot/pylibs/buildbot/status/html.py
+++ /dev/null
@@ -1,6 +0,0 @@
-
-# compatibility wrapper. This is currently the preferred place for master.cfg
-# to import from.
-
-from buildbot.status.web.baseweb import Waterfall, WebStatus
-_hush_pyflakes = [Waterfall, WebStatus]
diff --git a/tools/buildbot/pylibs/buildbot/status/mail.py b/tools/buildbot/pylibs/buildbot/status/mail.py
deleted file mode 100644
index a23a84c..0000000
--- a/tools/buildbot/pylibs/buildbot/status/mail.py
+++ /dev/null
@@ -1,361 +0,0 @@
-# -*- test-case-name: buildbot.test.test_status -*-
-
-# the email.MIMEMultipart module is only available in python-2.2.2 and later
-
-from email.Message import Message
-from email.Utils import formatdate
-from email.MIMEText import MIMEText
-try:
- from email.MIMEMultipart import MIMEMultipart
- canDoAttachments = True
-except ImportError:
- canDoAttachments = False
-import urllib
-
-from zope.interface import implements
-from twisted.internet import defer
-from twisted.mail.smtp import sendmail
-from twisted.python import log as twlog
-
-from buildbot import interfaces, util
-from buildbot.status import base
-from buildbot.status.builder import FAILURE, SUCCESS, WARNINGS
-
-
-class Domain(util.ComparableMixin):
- implements(interfaces.IEmailLookup)
- compare_attrs = ["domain"]
-
- def __init__(self, domain):
- assert "@" not in domain
- self.domain = domain
-
- def getAddress(self, name):
- return name + "@" + self.domain
-
-
-class MailNotifier(base.StatusReceiverMultiService):
- """This is a status notifier which sends email to a list of recipients
- upon the completion of each build. It can be configured to only send out
- mail for certain builds, and only send messages when the build fails, or
- when it transitions from success to failure. It can also be configured to
- include various build logs in each message.
-
- By default, the message will be sent to the Interested Users list, which
- includes all developers who made changes in the build. You can add
- additional recipients with the extraRecipients argument.
-
- To get a simple one-message-per-build (say, for a mailing list), use
- sendToInterestedUsers=False, extraRecipients=['listaddr@example.org']
-
- Each MailNotifier sends mail to a single set of recipients. To send
- different kinds of mail to different recipients, use multiple
- MailNotifiers.
- """
-
- implements(interfaces.IEmailSender)
-
- compare_attrs = ["extraRecipients", "lookup", "fromaddr", "mode",
- "categories", "builders", "addLogs", "relayhost",
- "subject", "sendToInterestedUsers"]
-
- def __init__(self, fromaddr, mode="all", categories=None, builders=None,
- addLogs=False, relayhost="localhost",
- subject="buildbot %(result)s in %(projectName)s on %(builder)s",
- lookup=None, extraRecipients=[],
- sendToInterestedUsers=True):
- """
- @type fromaddr: string
- @param fromaddr: the email address to be used in the 'From' header.
- @type sendToInterestedUsers: boolean
- @param sendToInterestedUsers: if True (the default), send mail to all
- of the Interested Users. If False, only
- send mail to the extraRecipients list.
-
- @type extraRecipients: tuple of string
- @param extraRecipients: a list of email addresses to which messages
- should be sent (in addition to the
- InterestedUsers list, which includes any
- developers who made Changes that went into this
- build). It is a good idea to create a small
- mailing list and deliver to that, then let
- subscribers come and go as they please.
-
- @type subject: string
- @param subject: a string to be used as the subject line of the message.
- %(builder)s will be replaced with the name of the
- builder which provoked the message.
-
- @type mode: string (defaults to all)
- @param mode: one of:
- - 'all': send mail about all builds, passing and failing
- - 'failing': only send mail about builds which fail
- - 'passing': only send mail about builds which succeed
- - 'problem': only send mail about a build which failed
- when the previous build passed
-
- @type builders: list of strings
- @param builders: a list of builder names for which mail should be
- sent. Defaults to None (send mail for all builds).
- Use either builders or categories, but not both.
-
- @type categories: list of strings
- @param categories: a list of category names to serve status
- information for. Defaults to None (all
- categories). Use either builders or categories,
- but not both.
-
- @type addLogs: boolean.
- @param addLogs: if True, include all build logs as attachments to the
- messages. These can be quite large. This can also be
- set to a list of log names, to send a subset of the
- logs. Defaults to False.
-
- @type relayhost: string
- @param relayhost: the host to which the outbound SMTP connection
- should be made. Defaults to 'localhost'
-
- @type lookup: implementor of {IEmailLookup}
- @param lookup: object which provides IEmailLookup, which is
- responsible for mapping User names (which come from
- the VC system) into valid email addresses. If not
- provided, the notifier will only be able to send mail
- to the addresses in the extraRecipients list. Most of
- the time you can use a simple Domain instance. As a
- shortcut, you can pass as string: this will be
- treated as if you had provided Domain(str). For
- example, lookup='twistedmatrix.com' will allow mail
- to be sent to all developers whose SVN usernames
- match their twistedmatrix.com account names.
- """
-
- base.StatusReceiverMultiService.__init__(self)
- assert isinstance(extraRecipients, (list, tuple))
- for r in extraRecipients:
- assert isinstance(r, str)
- assert "@" in r # require full email addresses, not User names
- self.extraRecipients = extraRecipients
- self.sendToInterestedUsers = sendToInterestedUsers
- self.fromaddr = fromaddr
- assert mode in ('all', 'failing', 'problem')
- self.mode = mode
- self.categories = categories
- self.builders = builders
- self.addLogs = addLogs
- self.relayhost = relayhost
- self.subject = subject
- if lookup is not None:
- if type(lookup) is str:
- lookup = Domain(lookup)
- assert interfaces.IEmailLookup.providedBy(lookup)
- self.lookup = lookup
- self.watched = []
- self.status = None
-
- # you should either limit on builders or categories, not both
- if self.builders != None and self.categories != None:
- twlog.err("Please specify only builders to ignore or categories to include")
- raise # FIXME: the asserts above do not raise some Exception either
-
- def setServiceParent(self, parent):
- """
- @type parent: L{buildbot.master.BuildMaster}
- """
- base.StatusReceiverMultiService.setServiceParent(self, parent)
- self.setup()
-
- def setup(self):
- self.status = self.parent.getStatus()
- self.status.subscribe(self)
-
- def disownServiceParent(self):
- self.status.unsubscribe(self)
- for w in self.watched:
- w.unsubscribe(self)
- return base.StatusReceiverMultiService.disownServiceParent(self)
-
- def builderAdded(self, name, builder):
- # only subscribe to builders we are interested in
- if self.categories != None and builder.category not in self.categories:
- return None
-
- self.watched.append(builder)
- return self # subscribe to this builder
-
- def builderRemoved(self, name):
- pass
-
- def builderChangedState(self, name, state):
- pass
- def buildStarted(self, name, build):
- pass
- def buildFinished(self, name, build, results):
- # here is where we actually do something.
- builder = build.getBuilder()
- if self.builders is not None and name not in self.builders:
- return # ignore this build
- if self.categories is not None and \
- builder.category not in self.categories:
- return # ignore this build
-
- if self.mode == "failing" and results != FAILURE:
- return
- if self.mode == "passing" and results != SUCCESS:
- return
- if self.mode == "problem":
- if results != FAILURE:
- return
- prev = build.getPreviousBuild()
- if prev and prev.getResults() == FAILURE:
- return
- # for testing purposes, buildMessage returns a Deferred that fires
- # when the mail has been sent. To help unit tests, we return that
- # Deferred here even though the normal IStatusReceiver.buildFinished
- # signature doesn't do anything with it. If that changes (if
- # .buildFinished's return value becomes significant), we need to
- # rearrange this.
- return self.buildMessage(name, build, results)
-
- def buildMessage(self, name, build, results):
- projectName = self.status.getProjectName()
- text = ""
- if self.mode == "all":
- text += "The Buildbot has finished a build"
- elif self.mode == "failing":
- text += "The Buildbot has detected a failed build"
- elif self.mode == "passing":
- text += "The Buildbot has detected a passing build"
- else:
- text += "The Buildbot has detected a new failure"
- text += " of %s on %s.\n" % (name, projectName)
- buildurl = self.status.getURLForThing(build)
- if buildurl:
- text += "Full details are available at:\n %s\n" % buildurl
- text += "\n"
-
- url = self.status.getBuildbotURL()
- if url:
- text += "Buildbot URL: %s\n\n" % urllib.quote(url, '/:')
-
- text += "Buildslave for this Build: %s\n\n" % build.getSlavename()
- text += "Build Reason: %s\n" % build.getReason()
-
- patch = None
- ss = build.getSourceStamp()
- if ss is None:
- source = "unavailable"
- else:
- source = ""
- if ss.branch:
- source += "[branch %s] " % ss.branch
- if ss.revision:
- source += ss.revision
- else:
- source += "HEAD"
- if ss.patch is not None:
- source += " (plus patch)"
- patch = ss.patch
- text += "Build Source Stamp: %s\n" % source
-
- text += "Blamelist: %s\n" % ",".join(build.getResponsibleUsers())
-
- # TODO: maybe display changes here? or in an attachment?
- text += "\n"
-
- t = build.getText()
- if t:
- t = ": " + " ".join(t)
- else:
- t = ""
-
- if results == SUCCESS:
- text += "Build succeeded!\n"
- res = "success"
- elif results == WARNINGS:
- text += "Build Had Warnings%s\n" % t
- res = "warnings"
- else:
- text += "BUILD FAILED%s\n" % t
- res = "failure"
-
- if self.addLogs and build.getLogs():
- text += "Logs are attached.\n"
-
- # TODO: it would be nice to provide a URL for the specific build
- # here. That involves some coordination with html.Waterfall .
- # Ideally we could do:
- # helper = self.parent.getServiceNamed("html")
- # if helper:
- # url = helper.getURLForBuild(build)
-
- text += "\n"
- text += "sincerely,\n"
- text += " -The Buildbot\n"
- text += "\n"
-
- haveAttachments = False
- if patch or self.addLogs:
- haveAttachments = True
- if not canDoAttachments:
- twlog.msg("warning: I want to send mail with attachments, "
- "but this python is too old to have "
- "email.MIMEMultipart . Please upgrade to python-2.3 "
- "or newer to enable addLogs=True")
-
- if haveAttachments and canDoAttachments:
- m = MIMEMultipart()
- m.attach(MIMEText(text))
- else:
- m = Message()
- m.set_payload(text)
-
- m['Date'] = formatdate(localtime=True)
- m['Subject'] = self.subject % { 'result': res,
- 'projectName': projectName,
- 'builder': name,
- }
- m['From'] = self.fromaddr
- # m['To'] is added later
-
- if patch:
- a = MIMEText(patch)
- a.add_header('Content-Disposition', "attachment",
- filename="source patch")
- m.attach(a)
- if self.addLogs:
- for log in build.getLogs():
- name = "%s.%s" % (log.getStep().getName(),
- log.getName())
- a = MIMEText(log.getText())
- a.add_header('Content-Disposition', "attachment",
- filename=name)
- m.attach(a)
-
- # now, who is this message going to?
- dl = []
- recipients = self.extraRecipients[:]
- if self.sendToInterestedUsers and self.lookup:
- for u in build.getInterestedUsers():
- d = defer.maybeDeferred(self.lookup.getAddress, u)
- d.addCallback(recipients.append)
- dl.append(d)
- d = defer.DeferredList(dl)
- d.addCallback(self._gotRecipients, recipients, m)
- return d
-
- def _gotRecipients(self, res, rlist, m):
- recipients = []
- for r in rlist:
- if r is not None and r not in recipients:
- recipients.append(r)
- recipients.sort()
- m['To'] = ", ".join(recipients)
- return self.sendMessage(m, recipients)
-
- def sendMessage(self, m, recipients):
- s = m.as_string()
- ds = []
- twlog.msg("sending mail (%d bytes) to" % len(s), recipients)
- for recip in recipients:
- ds.append(sendmail(self.relayhost, self.fromaddr, recip, s))
- return defer.DeferredList(ds)
diff --git a/tools/buildbot/pylibs/buildbot/status/progress.py b/tools/buildbot/pylibs/buildbot/status/progress.py
deleted file mode 100644
index dc4d3d5..0000000
--- a/tools/buildbot/pylibs/buildbot/status/progress.py
+++ /dev/null
@@ -1,308 +0,0 @@
-# -*- test-case-name: buildbot.test.test_status -*-
-
-from twisted.internet import reactor
-from twisted.spread import pb
-from twisted.python import log
-from buildbot import util
-
-class StepProgress:
- """I keep track of how much progress a single BuildStep has made.
-
- Progress is measured along various axes. Time consumed is one that is
- available for all steps. Amount of command output is another, and may be
- better quantified by scanning the output for markers to derive number of
- files compiled, directories walked, tests run, etc.
-
- I am created when the build begins, and given to a BuildProgress object
- so it can track the overall progress of the whole build.
-
- """
-
- startTime = None
- stopTime = None
- expectedTime = None
- buildProgress = None
- debug = False
-
- def __init__(self, name, metricNames):
- self.name = name
- self.progress = {}
- self.expectations = {}
- for m in metricNames:
- self.progress[m] = None
- self.expectations[m] = None
-
- def setBuildProgress(self, bp):
- self.buildProgress = bp
-
- def setExpectations(self, metrics):
- """The step can call this to explicitly set a target value for one
- of its metrics. E.g., ShellCommands knows how many commands it will
- execute, so it could set the 'commands' expectation."""
- for metric, value in metrics.items():
- self.expectations[metric] = value
- self.buildProgress.newExpectations()
-
- def setExpectedTime(self, seconds):
- self.expectedTime = seconds
- self.buildProgress.newExpectations()
-
- def start(self):
- if self.debug: print "StepProgress.start[%s]" % self.name
- self.startTime = util.now()
-
- def setProgress(self, metric, value):
- """The step calls this as progress is made along various axes."""
- if self.debug:
- print "setProgress[%s][%s] = %s" % (self.name, metric, value)
- self.progress[metric] = value
- if self.debug:
- r = self.remaining()
- print " step remaining:", r
- self.buildProgress.newProgress()
-
- def finish(self):
- """This stops the 'time' metric and marks the step as finished
- overall. It should be called after the last .setProgress has been
- done for each axis."""
- if self.debug: print "StepProgress.finish[%s]" % self.name
- self.stopTime = util.now()
- self.buildProgress.stepFinished(self.name)
-
- def totalTime(self):
- if self.startTime != None and self.stopTime != None:
- return self.stopTime - self.startTime
-
- def remaining(self):
- if self.startTime == None:
- return self.expectedTime
- if self.stopTime != None:
- return 0 # already finished
- # TODO: replace this with cleverness that graphs each metric vs.
- # time, then finds the inverse function. Will probably need to save
- # a timestamp with each setProgress update, when finished, go back
- # and find the 2% transition points, then save those 50 values in a
- # list. On the next build, do linear interpolation between the two
- # closest samples to come up with a percentage represented by that
- # metric.
-
- # TODO: If no other metrics are available, just go with elapsed
- # time. Given the non-time-uniformity of text output from most
- # steps, this would probably be better than the text-percentage
- # scheme currently implemented.
-
- percentages = []
- for metric, value in self.progress.items():
- expectation = self.expectations[metric]
- if value != None and expectation != None:
- p = 1.0 * value / expectation
- percentages.append(p)
- if percentages:
- avg = reduce(lambda x,y: x+y, percentages) / len(percentages)
- if avg > 1.0:
- # overdue
- avg = 1.0
- if avg < 0.0:
- avg = 0.0
- if percentages and self.expectedTime != None:
- return self.expectedTime - (avg * self.expectedTime)
- if self.expectedTime is not None:
- # fall back to pure time
- return self.expectedTime - (util.now() - self.startTime)
- return None # no idea
-
-
-class WatcherState:
- def __init__(self, interval):
- self.interval = interval
- self.timer = None
- self.needUpdate = 0
-
-class BuildProgress(pb.Referenceable):
- """I keep track of overall build progress. I hold a list of StepProgress
- objects.
- """
-
- def __init__(self, stepProgresses):
- self.steps = {}
- for s in stepProgresses:
- self.steps[s.name] = s
- s.setBuildProgress(self)
- self.finishedSteps = []
- self.watchers = {}
- self.debug = 0
-
- def setExpectationsFrom(self, exp):
- """Set our expectations from the builder's Expectations object."""
- for name, metrics in exp.steps.items():
- s = self.steps[name]
- s.setExpectedTime(exp.times[name])
- s.setExpectations(exp.steps[name])
-
- def newExpectations(self):
- """Call this when one of the steps has changed its expectations.
- This should trigger us to update our ETA value and notify any
- subscribers."""
- pass # subscribers are not implemented: they just poll
-
- def stepFinished(self, stepname):
- assert(stepname not in self.finishedSteps)
- self.finishedSteps.append(stepname)
- if len(self.finishedSteps) == len(self.steps.keys()):
- self.sendLastUpdates()
-
- def newProgress(self):
- r = self.remaining()
- if self.debug:
- print " remaining:", r
- if r != None:
- self.sendAllUpdates()
-
- def remaining(self):
- # sum eta of all steps
- sum = 0
- for name, step in self.steps.items():
- rem = step.remaining()
- if rem == None:
- return None # not sure
- sum += rem
- return sum
- def eta(self):
- left = self.remaining()
- if left == None:
- return None # not sure
- done = util.now() + left
- return done
-
-
- def remote_subscribe(self, remote, interval=5):
- # [interval, timer, needUpdate]
- # don't send an update more than once per interval
- self.watchers[remote] = WatcherState(interval)
- remote.notifyOnDisconnect(self.removeWatcher)
- self.updateWatcher(remote)
- self.startTimer(remote)
- log.msg("BuildProgress.remote_subscribe(%s)" % remote)
- def remote_unsubscribe(self, remote):
- # TODO: this doesn't work. I think 'remote' will always be different
- # than the object that appeared in _subscribe.
- log.msg("BuildProgress.remote_unsubscribe(%s)" % remote)
- self.removeWatcher(remote)
- #remote.dontNotifyOnDisconnect(self.removeWatcher)
- def removeWatcher(self, remote):
- #log.msg("removeWatcher(%s)" % remote)
- try:
- timer = self.watchers[remote].timer
- if timer:
- timer.cancel()
- del self.watchers[remote]
- except KeyError:
- log.msg("Weird, removeWatcher on non-existent subscriber:",
- remote)
- def sendAllUpdates(self):
- for r in self.watchers.keys():
- self.updateWatcher(r)
- def updateWatcher(self, remote):
- # an update wants to go to this watcher. Send it if we can, otherwise
- # queue it for later
- w = self.watchers[remote]
- if not w.timer:
- # no timer, so send update now and start the timer
- self.sendUpdate(remote)
- self.startTimer(remote)
- else:
- # timer is running, just mark as needing an update
- w.needUpdate = 1
- def startTimer(self, remote):
- w = self.watchers[remote]
- timer = reactor.callLater(w.interval, self.watcherTimeout, remote)
- w.timer = timer
- def sendUpdate(self, remote, last=0):
- self.watchers[remote].needUpdate = 0
- #text = self.asText() # TODO: not text, duh
- try:
- remote.callRemote("progress", self.remaining())
- if last:
- remote.callRemote("finished", self)
- except:
- log.deferr()
- self.removeWatcher(remote)
-
- def watcherTimeout(self, remote):
- w = self.watchers.get(remote, None)
- if not w:
- return # went away
- w.timer = None
- if w.needUpdate:
- self.sendUpdate(remote)
- self.startTimer(remote)
- def sendLastUpdates(self):
- for remote in self.watchers.keys():
- self.sendUpdate(remote, 1)
- self.removeWatcher(remote)
-
-
-class Expectations:
- debug = False
- # decay=1.0 ignores all but the last build
- # 0.9 is short time constant. 0.1 is very long time constant
- # TODO: let decay be specified per-metric
- decay = 0.5
-
- def __init__(self, buildprogress):
- """Create us from a successful build. We will expect each step to
- take as long as it did in that build."""
-
- # .steps maps stepname to dict2
- # dict2 maps metricname to final end-of-step value
- self.steps = {}
-
- # .times maps stepname to per-step elapsed time
- self.times = {}
-
- for name, step in buildprogress.steps.items():
- self.steps[name] = {}
- for metric, value in step.progress.items():
- self.steps[name][metric] = value
- self.times[name] = None
- if step.startTime is not None and step.stopTime is not None:
- self.times[name] = step.stopTime - step.startTime
-
- def wavg(self, old, current):
- if old is None:
- return current
- if current is None:
- return old
- else:
- return (current * self.decay) + (old * (1 - self.decay))
-
- def update(self, buildprogress):
- for name, stepprogress in buildprogress.steps.items():
- old = self.times[name]
- current = stepprogress.totalTime()
- if current == None:
- log.msg("Expectations.update: current[%s] was None!" % name)
- continue
- new = self.wavg(old, current)
- self.times[name] = new
- if self.debug:
- print "new expected time[%s] = %s, old %s, cur %s" % \
- (name, new, old, current)
-
- for metric, current in stepprogress.progress.items():
- old = self.steps[name][metric]
- new = self.wavg(old, current)
- if self.debug:
- print "new expectation[%s][%s] = %s, old %s, cur %s" % \
- (name, metric, new, old, current)
- self.steps[name][metric] = new
-
- def expectedBuildTime(self):
- if None in self.times.values():
- return None
- #return sum(self.times.values())
- # python-2.2 doesn't have 'sum'. TODO: drop python-2.2 support
- s = 0
- for v in self.times.values():
- s += v
- return s
diff --git a/tools/buildbot/pylibs/buildbot/status/tests.py b/tools/buildbot/pylibs/buildbot/status/tests.py
deleted file mode 100644
index 4c4c894..0000000
--- a/tools/buildbot/pylibs/buildbot/status/tests.py
+++ /dev/null
@@ -1,73 +0,0 @@
-
-from twisted.web import resource
-from twisted.web.error import NoResource
-
-# these are our test result types. Steps are responsible for mapping results
-# into these values.
-SKIP, EXPECTED_FAILURE, FAILURE, ERROR, UNEXPECTED_SUCCESS, SUCCESS = \
- "skip", "expected failure", "failure", "error", "unexpected success", \
- "success"
-UNKNOWN = "unknown" # catch-all
-
-
-class OneTest(resource.Resource):
- isLeaf = 1
- def __init__(self, parent, testName, results):
- self.parent = parent
- self.testName = testName
- self.resultType, self.results = results
-
- def render(self, request):
- request.setHeader("content-type", "text/html")
- if request.method == "HEAD":
- request.setHeader("content-length", len(self.html(request)))
- return ''
- return self.html(request)
-
- def html(self, request):
- # turn ourselves into HTML
- raise NotImplementedError
-
-class TestResults(resource.Resource):
- oneTestClass = OneTest
- def __init__(self):
- resource.Resource.__init__(self)
- self.tests = {}
- def addTest(self, testName, resultType, results=None):
- self.tests[testName] = (resultType, results)
- # TODO: .setName and .delete should be used on our Swappable
- def countTests(self):
- return len(self.tests)
- def countFailures(self):
- failures = 0
- for t in self.tests.values():
- if t[0] in (FAILURE, ERROR):
- failures += 1
- return failures
- def summary(self):
- """Return a short list of text strings as a summary, suitable for
- inclusion in an Event"""
- return ["some", "tests"]
- def describeOneTest(self, testname):
- return "%s: %s\n" % (testname, self.tests[testname][0])
- def html(self):
- data = "\nTest Results \n"
- data += "\n"
- data += "\n"
- tests = self.tests.keys()
- tests.sort()
- for testname in tests:
- data += self.describeOneTest(testname)
- data += " \n"
- data += "\n"
- return data
- def render(self, request):
- request.setHeader("content-type", "text/html")
- if request.method == "HEAD":
- request.setHeader("content-length", len(self.html()))
- return ''
- return self.html()
- def getChild(self, path, request):
- if self.tests.has_key(path):
- return self.oneTestClass(self, path, self.tests[path])
- return NoResource("No such test '%s'" % path)
diff --git a/tools/buildbot/pylibs/buildbot/status/tinderbox.py b/tools/buildbot/pylibs/buildbot/status/tinderbox.py
deleted file mode 100644
index 51d404b..0000000
--- a/tools/buildbot/pylibs/buildbot/status/tinderbox.py
+++ /dev/null
@@ -1,223 +0,0 @@
-
-from email.Message import Message
-from email.Utils import formatdate
-
-from zope.interface import implements
-from twisted.internet import defer
-
-from buildbot import interfaces
-from buildbot.status import mail
-from buildbot.status.builder import SUCCESS, WARNINGS
-from buildbot.steps.shell import WithProperties
-
-import zlib, bz2, base64
-
-# TODO: docs, maybe a test of some sort just to make sure it actually imports
-# and can format email without raising an exception.
-
-class TinderboxMailNotifier(mail.MailNotifier):
- """This is a Tinderbox status notifier. It can send e-mail to a number of
- different tinderboxes or people. E-mails are sent at the beginning and
- upon completion of each build. It can be configured to send out e-mails
- for only certain builds.
-
- The most basic usage is as follows::
- TinderboxMailNotifier(fromaddr="buildbot@localhost",
- tree="MyTinderboxTree",
- extraRecipients=["tinderboxdaemon@host.org"])
-
- The builder name (as specified in master.cfg) is used as the "build"
- tinderbox option.
-
- """
- implements(interfaces.IEmailSender)
-
- compare_attrs = ["extraRecipients", "fromaddr", "categories", "builders",
- "addLogs", "relayhost", "subject", "binaryURL", "tree",
- "logCompression", "errorparser", "columnName",
- "useChangeTime"]
-
- def __init__(self, fromaddr, tree, extraRecipients,
- categories=None, builders=None, relayhost="localhost",
- subject="buildbot %(result)s in %(builder)s", binaryURL="",
- logCompression="", errorparser="unix", columnName=None,
- useChangeTime=False):
- """
- @type fromaddr: string
- @param fromaddr: the email address to be used in the 'From' header.
-
- @type tree: string
- @param tree: The Tinderbox tree to post to.
-
- @type extraRecipients: tuple of string
- @param extraRecipients: E-mail addresses of recipients. This should at
- least include the tinderbox daemon.
-
- @type categories: list of strings
- @param categories: a list of category names to serve status
- information for. Defaults to None (all
- categories). Use either builders or categories,
- but not both.
-
- @type builders: list of strings
- @param builders: a list of builder names for which mail should be
- sent. Defaults to None (send mail for all builds).
- Use either builders or categories, but not both.
-
- @type relayhost: string
- @param relayhost: the host to which the outbound SMTP connection
- should be made. Defaults to 'localhost'
-
- @type subject: string
- @param subject: a string to be used as the subject line of the message.
- %(builder)s will be replaced with the name of the
- %builder which provoked the message.
- This parameter is not significant for the tinderbox
- daemon.
-
- @type binaryURL: string
- @param binaryURL: If specified, this should be the location where final
- binary for a build is located.
- (ie. http://www.myproject.org/nightly/08-08-2006.tgz)
- It will be posted to the Tinderbox.
-
- @type logCompression: string
- @param logCompression: The type of compression to use on the log.
- Valid options are"bzip2" and "gzip". gzip is
- only known to work on Python 2.4 and above.
-
- @type errorparser: string
- @param errorparser: The error parser that the Tinderbox server
- should use when scanning the log file.
- Default is "unix".
-
- @type columnName: string
- @param columnName: When columnName is None, use the buildername as
- the Tinderbox column name. When columnName is a
- string this exact string will be used for all
- builders that this TinderboxMailNotifier cares
- about (not recommended). When columnName is a
- WithProperties instance it will be interpolated
- as such. See WithProperties for more detail.
- @type useChangeTime: bool
- @param useChangeTime: When True, the time of the first Change for a
- build is used as the builddate. When False,
- the current time is used as the builddate.
- """
-
- mail.MailNotifier.__init__(self, fromaddr, categories=categories,
- builders=builders, relayhost=relayhost,
- subject=subject,
- extraRecipients=extraRecipients,
- sendToInterestedUsers=False)
- self.tree = tree
- self.binaryURL = binaryURL
- self.logCompression = logCompression
- self.errorparser = errorparser
- self.useChangeTime = useChangeTime
- assert columnName is None or type(columnName) is str \
- or isinstance(columnName, WithProperties), \
- "columnName must be None, a string, or a WithProperties instance"
- self.columnName = columnName
-
- def buildStarted(self, name, build):
- builder = build.getBuilder()
- if self.builders is not None and name not in self.builders:
- return # ignore this Build
- if self.categories is not None and \
- builder.category not in self.categories:
- return # ignore this build
- self.buildMessage(name, build, "building")
-
- def buildMessage(self, name, build, results):
- text = ""
- res = ""
- # shortform
- t = "tinderbox:"
-
- text += "%s tree: %s\n" % (t, self.tree)
- # the start time
- # getTimes() returns a fractioned time that tinderbox doesn't understand
- builddate = int(build.getTimes()[0])
- # attempt to pull a Change time from this Build's Changes.
- # if that doesn't work, fall back on the current time
- if self.useChangeTime:
- try:
- builddate = build.getChanges()[-1].when
- except:
- pass
- text += "%s builddate: %s\n" % (t, builddate)
- text += "%s status: " % t
-
- if results == "building":
- res = "building"
- text += res
- elif results == SUCCESS:
- res = "success"
- text += res
- elif results == WARNINGS:
- res = "testfailed"
- text += res
- else:
- res += "busted"
- text += res
-
- text += "\n";
-
- if self.columnName is None:
- # use the builder name
- text += "%s build: %s\n" % (t, name)
- elif type(self.columnName) is str:
- # use the exact string given
- text += "%s build: %s\n" % (t, self.columnName)
- elif isinstance(self.columnName, WithProperties):
- # interpolate the WithProperties instance, use that
- text += "%s build: %s\n" % (t, build.getProperties().render(self.columnName))
- else:
- raise Exception("columnName is an unhandled value")
- text += "%s errorparser: %s\n" % (t, self.errorparser)
-
- # if the build just started...
- if results == "building":
- text += "%s END\n" % t
- # if the build finished...
- else:
- text += "%s binaryurl: %s\n" % (t, self.binaryURL)
- text += "%s logcompression: %s\n" % (t, self.logCompression)
-
- # logs will always be appended
- logEncoding = ""
- tinderboxLogs = ""
- for log in build.getLogs():
- l = ""
- if self.logCompression == "bzip2":
- compressedLog = bz2.compress(log.getText())
- l = base64.encodestring(compressedLog)
- logEncoding = "base64";
- elif self.logCompression == "gzip":
- compressedLog = zlib.compress(log.getText())
- l = base64.encodestring(compressedLog)
- logEncoding = "base64";
- else:
- l = log.getText()
- tinderboxLogs += l
-
- text += "%s logencoding: %s\n" % (t, logEncoding)
- text += "%s END\n\n" % t
- text += tinderboxLogs
- text += "\n"
-
- m = Message()
- m.set_payload(text)
-
- m['Date'] = formatdate(localtime=True)
- m['Subject'] = self.subject % { 'result': res,
- 'builder': name,
- }
- m['From'] = self.fromaddr
- # m['To'] is added later
-
- d = defer.DeferredList([])
- d.addCallback(self._gotRecipients, self.extraRecipients, m)
- return d
-
diff --git a/tools/buildbot/pylibs/buildbot/status/web/__init__.py b/tools/buildbot/pylibs/buildbot/status/web/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/tools/buildbot/pylibs/buildbot/status/web/about.py b/tools/buildbot/pylibs/buildbot/status/web/about.py
deleted file mode 100644
index 09748e6..0000000
--- a/tools/buildbot/pylibs/buildbot/status/web/about.py
+++ /dev/null
@@ -1,33 +0,0 @@
-
-from twisted.web import html
-from buildbot.status.web.base import HtmlResource
-import buildbot
-import twisted
-import sys
-
-class AboutBuildbot(HtmlResource):
- title = "About this Buildbot"
-
- def body(self, request):
- data = ''
- data += 'Welcome to the Buildbot \n'
- data += 'Version Information \n'
- data += '\n'
- data += ' Buildbot: %s \n' % html.escape(buildbot.version)
- data += ' Twisted: %s \n' % html.escape(twisted.__version__)
- data += ' Python: %s \n' % html.escape(sys.version)
- data += ' Buildmaster platform: %s \n' % html.escape(sys.platform)
- data += ' \n'
-
- data += '''
-Source code
-
-Buildbot is a free software project, released under the terms of the
-GNU GPL .
-
-Please visit the Buildbot Home Page for
-more information, including documentation, bug reports, and source
-downloads.
-'''
- return data
-
diff --git a/tools/buildbot/pylibs/buildbot/status/web/base.py b/tools/buildbot/pylibs/buildbot/status/web/base.py
deleted file mode 100644
index a1e5a972..0000000
--- a/tools/buildbot/pylibs/buildbot/status/web/base.py
+++ /dev/null
@@ -1,395 +0,0 @@
-
-import urlparse, urllib, time
-from zope.interface import Interface
-from twisted.web import html, resource
-from buildbot.status import builder
-from buildbot.status.builder import SUCCESS, WARNINGS, FAILURE, EXCEPTION
-
-
-class ITopBox(Interface):
- """I represent a box in the top row of the waterfall display: the one
- which shows the status of the last build for each builder."""
- def getBox(self, request):
- """Return a Box instance, which can produce a cell.
- """
-
-class ICurrentBox(Interface):
- """I represent the 'current activity' box, just above the builder name."""
- def getBox(self, status):
- """Return a Box instance, which can produce a cell.
- """
-
-class IBox(Interface):
- """I represent a box in the waterfall display."""
- def getBox(self, request):
- """Return a Box instance, which wraps an Event and can produce a
- cell.
- """
-
-class IHTMLLog(Interface):
- pass
-
-css_classes = {SUCCESS: "success",
- WARNINGS: "warnings",
- FAILURE: "failure",
- EXCEPTION: "exception",
- }
-
-ROW_TEMPLATE = '''
-
- %(label)s
- %(field)s
-
-'''
-
-def make_row(label, field):
- """Create a name/value row for the HTML.
-
- `label` is plain text; it will be HTML-encoded.
-
- `field` is a bit of HTML structure; it will not be encoded in
- any way.
- """
- label = html.escape(label)
- return ROW_TEMPLATE % {"label": label, "field": field}
-
-def make_stop_form(stopURL, on_all=False, label="Build"):
- if on_all:
- data = """\n' % label
- return data
-
-def make_force_build_form(forceURL, on_all=False):
- if on_all:
- data = """\n')
-
-colormap = {
- 'green': '#72ff75',
- }
-def td(text="", parms={}, **props):
- data = ""
- data += " "
- #if not props.has_key("border"):
- # props["border"] = 1
- props.update(parms)
- if props.has_key("bgcolor"):
- props["bgcolor"] = colormap.get(props["bgcolor"], props["bgcolor"])
- comment = props.get("comment", None)
- if comment:
- data += "" % comment
- data += " "
- if not text:
- text = " "
- if isinstance(text, list):
- data += " ".join(text)
- else:
- data += text
- data += " \n"
- return data
-
-def build_get_class(b):
- """
- Return the class to use for a finished build or buildstep,
- based on the result.
- """
- # FIXME: this getResults duplicity might need to be fixed
- result = b.getResults()
- #print "THOMAS: result for b %r: %r" % (b, result)
- if isinstance(b, builder.BuildStatus):
- result = b.getResults()
- elif isinstance(b, builder.BuildStepStatus):
- result = b.getResults()[0]
- # after forcing a build, b.getResults() returns ((None, []), []), ugh
- if isinstance(result, tuple):
- result = result[0]
- else:
- raise TypeError, "%r is not a BuildStatus or BuildStepStatus" % b
-
- if result == None:
- # FIXME: this happens when a buildstep is running ?
- return "running"
- return builder.Results[result]
-
-def path_to_root(request):
- # /waterfall : ['waterfall'] -> ''
- # /somewhere/lower : ['somewhere', 'lower'] -> '../'
- # /somewhere/indexy/ : ['somewhere', 'indexy', ''] -> '../../'
- # / : [] -> ''
- if request.prepath:
- segs = len(request.prepath) - 1
- else:
- segs = 0
- root = "../" * segs
- return root
-
-def path_to_builder(request, builderstatus):
- return (path_to_root(request) +
- "builders/" +
- urllib.quote(builderstatus.getName(), safe=''))
-
-def path_to_build(request, buildstatus):
- return (path_to_builder(request, buildstatus.getBuilder()) +
- "/builds/%d" % buildstatus.getNumber())
-
-def path_to_step(request, stepstatus):
- return (path_to_build(request, stepstatus.getBuild()) +
- "/steps/%s" % urllib.quote(stepstatus.getName(), safe=''))
-
-class Box:
- # a Box wraps an Event. The Box has HTML parameters that Events
- # lack, and it has a base URL to which each File's name is relative.
- # Events don't know about HTML.
- spacer = False
- def __init__(self, text=[], color=None, class_=None, urlbase=None,
- **parms):
- self.text = text
- self.color = color
- self.class_ = class_
- self.urlbase = urlbase
- self.show_idle = 0
- if parms.has_key('show_idle'):
- del parms['show_idle']
- self.show_idle = 1
-
- self.parms = parms
- # parms is a dict of HTML parameters for the element that will
- # represent this Event in the waterfall display.
-
- def td(self, **props):
- props.update(self.parms)
- text = self.text
- if not text and self.show_idle:
- text = ["[idle]"]
- return td(text, props, bgcolor=self.color, class_=self.class_)
-
-
-class HtmlResource(resource.Resource):
- # this is a cheap sort of template thingy
- contentType = "text/html; charset=UTF-8"
- title = "Buildbot"
- addSlash = False # adapted from Nevow
-
- def getChild(self, path, request):
- if self.addSlash and path == "" and len(request.postpath) == 0:
- return self
- return resource.Resource.getChild(self, path, request)
-
- def render(self, request):
- # tell the WebStatus about the HTTPChannel that got opened, so they
- # can close it if we get reconfigured and the WebStatus goes away.
- # They keep a weakref to this, since chances are good that it will be
- # closed by the browser or by us before we get reconfigured. See
- # ticket #102 for details.
- if hasattr(request, "channel"):
- # web.distrib.Request has no .channel
- request.site.buildbot_service.registerChannel(request.channel)
-
- # Our pages no longer require that their URL end in a slash. Instead,
- # they all use request.childLink() or some equivalent which takes the
- # last path component into account. This clause is left here for
- # historical and educational purposes.
- if False and self.addSlash and request.prepath[-1] != '':
- # this is intended to behave like request.URLPath().child('')
- # but we need a relative URL, since we might be living behind a
- # reverse proxy
- #
- # note that the Location: header (as used in redirects) are
- # required to have absolute URIs, and my attempt to handle
- # reverse-proxies gracefully violates rfc2616. This frequently
- # works, but single-component paths sometimes break. The best
- # strategy is to avoid these redirects whenever possible by using
- # HREFs with trailing slashes, and only use the redirects for
- # manually entered URLs.
- url = request.prePathURL()
- scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
- new_url = request.prepath[-1] + "/"
- if query:
- new_url += "?" + query
- request.redirect(new_url)
- return ''
-
- data = self.content(request)
- if isinstance(data, unicode):
- data = data.encode("utf-8")
- request.setHeader("content-type", self.contentType)
- if request.method == "HEAD":
- request.setHeader("content-length", len(data))
- return ''
- return data
-
- def getStatus(self, request):
- return request.site.buildbot_service.getStatus()
- def getControl(self, request):
- return request.site.buildbot_service.getControl()
-
- def getChangemaster(self, request):
- return request.site.buildbot_service.parent.change_svc
-
- def path_to_root(self, request):
- return path_to_root(request)
-
- def getTitle(self, request):
- return self.title
-
- def fillTemplate(self, template, request):
- s = request.site.buildbot_service
- values = s.template_values.copy()
- values['root'] = self.path_to_root(request)
- # e.g. to reference the top-level 'buildbot.css' page, use
- # "%(root)sbuildbot.css"
- values['title'] = self.getTitle(request)
- return template % values
-
- def content(self, request):
- s = request.site.buildbot_service
- data = ""
- data += self.fillTemplate(s.header, request)
- data += "\n"
- for he in s.head_elements:
- data += " " + self.fillTemplate(he, request) + "\n"
- data += self.head(request)
- data += "\n\n"
-
- data += '\n' % " ".join(['%s="%s"' % (k,v)
- for (k,v) in s.body_attrs.items()])
- data += self.body(request)
- data += "\n"
- data += self.fillTemplate(s.footer, request)
- return data
-
- def head(self, request):
- return ""
-
- def body(self, request):
- return "Dummy\n"
-
-class StaticHTML(HtmlResource):
- def __init__(self, body, title):
- HtmlResource.__init__(self)
- self.bodyHTML = body
- self.title = title
- def body(self, request):
- return self.bodyHTML
-
-MINUTE = 60
-HOUR = 60*MINUTE
-DAY = 24*HOUR
-WEEK = 7*DAY
-MONTH = 30*DAY
-
-def plural(word, words, num):
- if int(num) == 1:
- return "%d %s" % (num, word)
- else:
- return "%d %s" % (num, words)
-
-def abbreviate_age(age):
- if age <= 90:
- return "%s ago" % plural("second", "seconds", age)
- if age < 90*MINUTE:
- return "about %s ago" % plural("minute", "minutes", age / MINUTE)
- if age < DAY:
- return "about %s ago" % plural("hour", "hours", age / HOUR)
- if age < 2*WEEK:
- return "about %s ago" % plural("day", "days", age / DAY)
- if age < 2*MONTH:
- return "about %s ago" % plural("week", "weeks", age / WEEK)
- return "a long time ago"
-
-
-class OneLineMixin:
- LINE_TIME_FORMAT = "%b %d %H:%M"
-
- def get_line_values(self, req, build):
- '''
- Collect the data needed for each line display
- '''
- builder_name = build.getBuilder().getName()
- results = build.getResults()
- text = build.getText()
- try:
- rev = build.getProperty("got_revision")
- if rev is None:
- rev = "??"
- except KeyError:
- rev = "??"
- rev = str(rev)
- if len(rev) > 20:
- rev = "version is too-long"
- root = self.path_to_root(req)
- css_class = css_classes.get(results, "")
- values = {'class': css_class,
- 'builder_name': builder_name,
- 'buildnum': build.getNumber(),
- 'results': css_class,
- 'text': " ".join(build.getText()),
- 'buildurl': (root +
- "builders/%s/builds/%d" % (builder_name,
- build.getNumber())),
- 'builderurl': (root + "builders/%s" % builder_name),
- 'rev': rev,
- 'time': time.strftime(self.LINE_TIME_FORMAT,
- time.localtime(build.getTimes()[0])),
- }
- return values
-
- def make_line(self, req, build, include_builder=True):
- '''
- Format and render a single line into HTML
- '''
- values = self.get_line_values(req, build)
- fmt_pieces = ['(%(time)s) ',
- 'rev=[%(rev)s]',
- '%(results)s ',
- ]
- if include_builder:
- fmt_pieces.append('%(builder_name)s ')
- fmt_pieces.append('#%(buildnum)d :')
- fmt_pieces.append('%(text)s')
- data = " ".join(fmt_pieces) % values
- return data
-
-def map_branches(branches):
- # when the query args say "trunk", present that to things like
- # IBuilderStatus.generateFinishedBuilds as None, since that's the
- # convention in use. But also include 'trunk', because some VC systems
- # refer to it that way. In the long run we should clean this up better,
- # maybe with Branch objects or something.
- if "trunk" in branches:
- return branches + [None]
- return branches
diff --git a/tools/buildbot/pylibs/buildbot/status/web/baseweb.py b/tools/buildbot/pylibs/buildbot/status/web/baseweb.py
deleted file mode 100644
index 149266cd..0000000
--- a/tools/buildbot/pylibs/buildbot/status/web/baseweb.py
+++ /dev/null
@@ -1,576 +0,0 @@
-
-import os, sys, urllib, weakref
-from itertools import count
-
-from zope.interface import implements
-from twisted.python import log
-from twisted.application import strports, service
-from twisted.web import server, distrib, static, html
-from twisted.spread import pb
-
-from buildbot.interfaces import IControl, IStatusReceiver
-
-from buildbot.status.web.base import HtmlResource, Box, \
- build_get_class, ICurrentBox, OneLineMixin, map_branches, \
- make_stop_form, make_force_build_form
-from buildbot.status.web.waterfall import WaterfallStatusResource
-from buildbot.status.web.grid import GridStatusResource
-from buildbot.status.web.changes import ChangesResource
-from buildbot.status.web.builder import BuildersResource
-from buildbot.status.web.slaves import BuildSlavesResource
-from buildbot.status.web.xmlrpc import XMLRPCServer
-from buildbot.status.web.about import AboutBuildbot
-
-# this class contains the status services (WebStatus and the older Waterfall)
-# which can be put in c['status']. It also contains some of the resources
-# that are attached to the WebStatus at various well-known URLs, which the
-# admin might wish to attach (using WebStatus.putChild) at other URLs.
-
-
-class LastBuild(HtmlResource):
- def body(self, request):
- return "missing\n"
-
-def getLastNBuilds(status, numbuilds, builders=[], branches=[]):
- """Return a list with the last few Builds, sorted by start time.
- builder_names=None means all builders
- """
-
- # TODO: this unsorts the list of builder names, ick
- builder_names = set(status.getBuilderNames())
- if builders:
- builder_names = builder_names.intersection(set(builders))
-
- # to make sure that we get everything, we must get 'numbuilds' builds
- # from *each* source, then sort by ending time, then trim to the last
- # 20. We could be more efficient, but it would require the same
- # gnarly code that the Waterfall uses to generate one event at a
- # time. TODO: factor that code out into some useful class.
- events = []
- for builder_name in builder_names:
- builder = status.getBuilder(builder_name)
- for build_number in count(1):
- if build_number > numbuilds:
- break # enough from this builder, move on to another
- build = builder.getBuild(-build_number)
- if not build:
- break # no more builds here, move on to the next builder
- #if not build.isFinished():
- # continue
- (build_start, build_end) = build.getTimes()
- event = (build_start, builder_name, build)
- events.append(event)
- def _sorter(a, b):
- return cmp( a[:2], b[:2] )
- events.sort(_sorter)
- # now only return the actual build, and only return some of them
- return [e[2] for e in events[-numbuilds:]]
-
-
-# /one_line_per_build
-# accepts builder=, branch=, numbuilds=
-class OneLinePerBuild(HtmlResource, OneLineMixin):
- """This shows one line per build, combining all builders together. Useful
- query arguments:
-
- numbuilds=: how many lines to display
- builder=: show only builds for this builder. Multiple builder= arguments
- can be used to see builds from any builder in the set.
- """
-
- title = "Recent Builds"
-
- def __init__(self, numbuilds=20):
- HtmlResource.__init__(self)
- self.numbuilds = numbuilds
-
- def getChild(self, path, req):
- status = self.getStatus(req)
- builder = status.getBuilder(path)
- return OneLinePerBuildOneBuilder(builder)
-
- def body(self, req):
- status = self.getStatus(req)
- control = self.getControl(req)
- numbuilds = int(req.args.get("numbuilds", [self.numbuilds])[0])
- builders = req.args.get("builder", [])
- branches = [b for b in req.args.get("branch", []) if b]
-
- g = status.generateFinishedBuilds(builders, map_branches(branches),
- numbuilds)
-
- data = ""
-
- # really this is "up to %d builds"
- data += "Last %d finished builds: %s \n" % \
- (numbuilds, ", ".join(branches))
- if builders:
- data += ("of builders: %s
\n" % (", ".join(builders)))
- data += "\n"
- got = 0
- building = False
- online = 0
- for build in g:
- got += 1
- data += " " + self.make_line(req, build) + " \n"
- builder_status = build.getBuilder().getState()[0]
- if builder_status == "building":
- building = True
- online += 1
- elif builder_status != "offline":
- online += 1
- if not got:
- data += " No matching builds found \n"
- data += " \n"
-
- if control is not None:
- if building:
- stopURL = "builders/_all/stop"
- data += make_stop_form(stopURL, True, "Builds")
- if online:
- forceURL = "builders/_all/force"
- data += make_force_build_form(forceURL, True)
-
- return data
-
-
-
-# /one_line_per_build/$BUILDERNAME
-# accepts branch=, numbuilds=
-
-class OneLinePerBuildOneBuilder(HtmlResource, OneLineMixin):
- def __init__(self, builder, numbuilds=20):
- HtmlResource.__init__(self)
- self.builder = builder
- self.builder_name = builder.getName()
- self.numbuilds = numbuilds
- self.title = "Recent Builds of %s" % self.builder_name
-
- def body(self, req):
- status = self.getStatus(req)
- numbuilds = int(req.args.get("numbuilds", [self.numbuilds])[0])
- branches = [b for b in req.args.get("branch", []) if b]
-
- # walk backwards through all builds of a single builder
- g = self.builder.generateFinishedBuilds(map_branches(branches),
- numbuilds)
-
- data = ""
- data += ("Last %d builds of builder %s: %s \n" %
- (numbuilds, self.builder_name, ", ".join(branches)))
- data += "\n"
- got = 0
- for build in g:
- got += 1
- data += " " + self.make_line(req, build) + " \n"
- if not got:
- data += " No matching builds found \n"
- data += " \n"
-
- return data
-
-# /one_box_per_builder
-# accepts builder=, branch=
-class OneBoxPerBuilder(HtmlResource):
- """This shows a narrow table with one row per builder. The leftmost column
- contains the builder name. The next column contains the results of the
- most recent build. The right-hand column shows the builder's current
- activity.
-
- builder=: show only builds for this builder. Multiple builder= arguments
- can be used to see builds from any builder in the set.
- """
-
- title = "Latest Build"
-
- def body(self, req):
- status = self.getStatus(req)
- control = self.getControl(req)
-
- builders = req.args.get("builder", status.getBuilderNames())
- branches = [b for b in req.args.get("branch", []) if b]
-
- data = ""
-
- data += "Latest builds: %s \n" % ", ".join(branches)
- data += "\n"
-
- building = False
- online = 0
- base_builders_url = self.path_to_root(req) + "builders/"
- for bn in builders:
- base_builder_url = base_builders_url + urllib.quote(bn, safe='')
- builder = status.getBuilder(bn)
- data += "\n"
- data += '%s \n' \
- % (base_builder_url, html.escape(bn))
- builds = list(builder.generateFinishedBuilds(map_branches(branches),
- num_builds=1))
- if builds:
- b = builds[0]
- url = (base_builder_url + "/builds/%d" % b.getNumber())
- try:
- label = b.getProperty("got_revision")
- except KeyError:
- label = None
- if not label or len(str(label)) > 20:
- label = "#%d" % b.getNumber()
- text = ['%s ' % (url, label)]
- text.extend(b.getText())
- box = Box(text, b.getColor(),
- class_="LastBuild box %s" % build_get_class(b))
- data += box.td(align="center")
- else:
- data += 'no build \n'
- current_box = ICurrentBox(builder).getBox(status)
- data += current_box.td(align="center")
-
- builder_status = builder.getState()[0]
- if builder_status == "building":
- building = True
- online += 1
- elif builder_status != "offline":
- online += 1
-
- data += "
\n"
-
- if control is not None:
- if building:
- stopURL = "builders/_all/stop"
- data += make_stop_form(stopURL, True, "Builds")
- if online:
- forceURL = "builders/_all/force"
- data += make_force_build_form(forceURL, True)
-
- return data
-
-
-
-HEADER = '''
-
-
-
-'''
-
-HEAD_ELEMENTS = [
- '%(title)s ',
- ' ',
- ]
-BODY_ATTRS = {
- 'vlink': "#800080",
- }
-
-FOOTER = '''
-
-'''
-
-
-class WebStatus(service.MultiService):
- implements(IStatusReceiver)
- # TODO: IStatusReceiver is really about things which subscribe to hear
- # about buildbot events. We need a different interface (perhaps a parent
- # of IStatusReceiver) for status targets that don't subscribe, like the
- # WebStatus class. buildbot.master.BuildMaster.loadConfig:737 asserts
- # that everything in c['status'] provides IStatusReceiver, but really it
- # should check that they provide IStatusTarget instead.
-
- """
- The webserver provided by this class has the following resources:
-
- /waterfall : the big time-oriented 'waterfall' display, with links
- to individual changes, builders, builds, steps, and logs.
- A number of query-arguments can be added to influence
- the display.
- /grid : another summary display that shows a grid of builds, with
- sourcestamps on the x axis, and builders on the y. Query
- arguments similar to those for the waterfall can be added.
- /builders/BUILDERNAME: a page summarizing the builder. This includes
- references to the Schedulers that feed it,
- any builds currently in the queue, which
- buildslaves are designated or attached, and a
- summary of the build process it uses.
- /builders/BUILDERNAME/builds/NUM: a page describing a single Build
- /builders/BUILDERNAME/builds/NUM/steps/STEPNAME: describes a single step
- /builders/BUILDERNAME/builds/NUM/steps/STEPNAME/logs/LOGNAME: a StatusLog
- /builders/BUILDERNAME/builds/NUM/tests : summarize test results
- /builders/BUILDERNAME/builds/NUM/tests/TEST.NAME: results of one test
- /builders/_all/{force,stop}: force a build/stop building on all builders.
- /changes : summarize all ChangeSources
- /changes/CHANGENUM: a page describing a single Change
- /schedulers/SCHEDULERNAME: a page describing a Scheduler, including
- a description of its behavior, a list of the
- Builders it triggers, and list of the Changes
- that are queued awaiting the tree-stable
- timer, and controls to accelerate the timer.
- /buildslaves : list all BuildSlaves
- /buildslaves/SLAVENAME : describe a single BuildSlave
- /one_line_per_build : summarize the last few builds, one line each
- /one_line_per_build/BUILDERNAME : same, but only for a single builder
- /one_box_per_builder : show the latest build and current activity
- /about : describe this buildmaster (Buildbot and support library versions)
- /xmlrpc : (not yet implemented) an XMLRPC server with build status
-
-
- All URLs for pages which are not defined here are used to look for files
- in BASEDIR/public_html/ , which means that /robots.txt or /buildbot.css
- or /favicon.ico can be placed in that directory.
-
- If an index file (index.html, index.htm, or index, in that order) is
- present in public_html/, it will be used for the root resource. If not,
- the default behavior is to put a redirection to the /waterfall page.
-
- All of the resources provided by this service use relative URLs to reach
- each other. The only absolute links are the c['projectURL'] links at the
- top and bottom of the page, and the buildbot home-page link at the
- bottom.
-
- This webserver defines class attributes on elements so they can be styled
- with CSS stylesheets. All pages pull in public_html/buildbot.css, and you
- can cause additional stylesheets to be loaded by adding a suitable
- to the WebStatus instance's .head_elements attribute.
-
- Buildbot uses some generic classes to identify the type of object, and
- some more specific classes for the various kinds of those types. It does
- this by specifying both in the class attributes where applicable,
- separated by a space. It is important that in your CSS you declare the
- more generic class styles above the more specific ones. For example,
- first define a style for .Event, and below that for .SUCCESS
-
- The following CSS class names are used:
- - Activity, Event, BuildStep, LastBuild: general classes
- - waiting, interlocked, building, offline, idle: Activity states
- - start, running, success, failure, warnings, skipped, exception:
- LastBuild and BuildStep states
- - Change: box with change
- - Builder: box for builder name (at top)
- - Project
- - Time
-
- """
-
- # we are not a ComparableMixin, and therefore the webserver will be
- # rebuilt every time we reconfig. This is because WebStatus.putChild()
- # makes it too difficult to tell whether two instances are the same or
- # not (we'd have to do a recursive traversal of all children to discover
- # all the changes).
-
- def __init__(self, http_port=None, distrib_port=None, allowForce=False):
- """Run a web server that provides Buildbot status.
-
- @type http_port: int or L{twisted.application.strports} string
- @param http_port: a strports specification describing which port the
- buildbot should use for its web server, with the
- Waterfall display as the root page. For backwards
- compatibility this can also be an int. Use
- 'tcp:8000' to listen on that port, or
- 'tcp:12345:interface=127.0.0.1' if you only want
- local processes to connect to it (perhaps because
- you are using an HTTP reverse proxy to make the
- buildbot available to the outside world, and do not
- want to make the raw port visible).
-
- @type distrib_port: int or L{twisted.application.strports} string
- @param distrib_port: Use this if you want to publish the Waterfall
- page using web.distrib instead. The most common
- case is to provide a string that is an absolute
- pathname to the unix socket on which the
- publisher should listen
- (C{os.path.expanduser(~/.twistd-web-pb)} will
- match the default settings of a standard
- twisted.web 'personal web server'). Another
- possibility is to pass an integer, which means
- the publisher should listen on a TCP socket,
- allowing the web server to be on a different
- machine entirely. Both forms are provided for
- backwards compatibility; the preferred form is a
- strports specification like
- 'unix:/home/buildbot/.twistd-web-pb'. Providing
- a non-absolute pathname will probably confuse
- the strports parser.
- @param allowForce: boolean, if True then the webserver will allow
- visitors to trigger and cancel builds
- """
-
- service.MultiService.__init__(self)
- if type(http_port) is int:
- http_port = "tcp:%d" % http_port
- self.http_port = http_port
- if distrib_port is not None:
- if type(distrib_port) is int:
- distrib_port = "tcp:%d" % distrib_port
- if distrib_port[0] in "/~.": # pathnames
- distrib_port = "unix:%s" % distrib_port
- self.distrib_port = distrib_port
- self.allowForce = allowForce
-
- # this will be replaced once we've been attached to a parent (and
- # thus have a basedir and can reference BASEDIR/public_html/)
- root = static.Data("placeholder", "text/plain")
- self.site = server.Site(root)
- self.childrenToBeAdded = {}
-
- self.setupUsualPages()
-
- # the following items are accessed by HtmlResource when it renders
- # each page.
- self.site.buildbot_service = self
- self.header = HEADER
- self.head_elements = HEAD_ELEMENTS[:]
- self.body_attrs = BODY_ATTRS.copy()
- self.footer = FOOTER
- self.template_values = {}
-
- # keep track of cached connections so we can break them when we shut
- # down. See ticket #102 for more details.
- self.channels = weakref.WeakKeyDictionary()
-
- if self.http_port is not None:
- s = strports.service(self.http_port, self.site)
- s.setServiceParent(self)
- if self.distrib_port is not None:
- f = pb.PBServerFactory(distrib.ResourcePublisher(self.site))
- s = strports.service(self.distrib_port, f)
- s.setServiceParent(self)
-
- def setupUsualPages(self):
- #self.putChild("", IndexOrWaterfallRedirection())
- self.putChild("waterfall", WaterfallStatusResource())
- self.putChild("grid", GridStatusResource())
- self.putChild("builders", BuildersResource()) # has builds/steps/logs
- self.putChild("changes", ChangesResource())
- self.putChild("buildslaves", BuildSlavesResource())
- #self.putChild("schedulers", SchedulersResource())
- self.putChild("one_line_per_build", OneLinePerBuild())
- self.putChild("one_box_per_builder", OneBoxPerBuilder())
- self.putChild("xmlrpc", XMLRPCServer())
- self.putChild("about", AboutBuildbot())
-
- def __repr__(self):
- if self.http_port is None:
- return "" % (self.distrib_port,
- hex(id(self)))
- if self.distrib_port is None:
- return "" % (self.http_port,
- hex(id(self)))
- return ("" %
- (self.http_port, self.distrib_port, hex(id(self))))
-
- def setServiceParent(self, parent):
- service.MultiService.setServiceParent(self, parent)
- self.setupSite()
-
- def setupSite(self):
- # this is responsible for creating the root resource. It isn't done
- # at __init__ time because we need to reference the parent's basedir.
- htmldir = os.path.join(self.parent.basedir, "public_html")
- if os.path.isdir(htmldir):
- log.msg("WebStatus using (%s)" % htmldir)
- else:
- log.msg("WebStatus: warning: %s is missing. Do you need to run"
- " 'buildbot upgrade-master' on this buildmaster?" % htmldir)
- # all static pages will get a 404 until upgrade-master is used to
- # populate this directory. Create the directory, though, since
- # otherwise we get internal server errors instead of 404s.
- os.mkdir(htmldir)
- root = static.File(htmldir)
-
- for name, child_resource in self.childrenToBeAdded.iteritems():
- root.putChild(name, child_resource)
-
- self.site.resource = root
-
- def putChild(self, name, child_resource):
- """This behaves a lot like root.putChild() . """
- self.childrenToBeAdded[name] = child_resource
-
- def registerChannel(self, channel):
- self.channels[channel] = 1 # weakrefs
-
- def stopService(self):
- for channel in self.channels:
- try:
- channel.transport.loseConnection()
- except:
- log.msg("WebStatus.stopService: error while disconnecting"
- " leftover clients")
- log.err()
- return service.MultiService.stopService(self)
-
- def getStatus(self):
- return self.parent.getStatus()
- def getControl(self):
- if self.allowForce:
- return IControl(self.parent)
- return None
-
- def getPortnum(self):
- # this is for the benefit of unit tests
- s = list(self)[0]
- return s._port.getHost().port
-
-# resources can get access to the IStatus by calling
-# request.site.buildbot_service.getStatus()
-
-# this is the compatibility class for the old waterfall. It is exactly like a
-# regular WebStatus except that the root resource (e.g. http://buildbot.net/)
-# always redirects to a WaterfallStatusResource, and the old arguments are
-# mapped into the new resource-tree approach. In the normal WebStatus, the
-# root resource either redirects the browser to /waterfall or serves
-# BASEDIR/public_html/index.html, and favicon/robots.txt are provided by
-# having the admin write actual files into BASEDIR/public_html/ .
-
-# note: we don't use a util.Redirect here because HTTP requires that the
-# Location: header provide an absolute URI, and it's non-trivial to figure
-# out our absolute URI from here.
-
-class Waterfall(WebStatus):
-
- if hasattr(sys, "frozen"):
- # all 'data' files are in the directory of our executable
- here = os.path.dirname(sys.executable)
- buildbot_icon = os.path.abspath(os.path.join(here, "buildbot.png"))
- buildbot_css = os.path.abspath(os.path.join(here, "classic.css"))
- else:
- # running from source
- # the icon is sibpath(__file__, "../buildbot.png") . This is for
- # portability.
- up = os.path.dirname
- buildbot_icon = os.path.abspath(os.path.join(up(up(up(__file__))),
- "buildbot.png"))
- buildbot_css = os.path.abspath(os.path.join(up(__file__),
- "classic.css"))
-
- compare_attrs = ["http_port", "distrib_port", "allowForce",
- "categories", "css", "favicon", "robots_txt"]
-
- def __init__(self, http_port=None, distrib_port=None, allowForce=True,
- categories=None, css=buildbot_css, favicon=buildbot_icon,
- robots_txt=None):
- import warnings
- m = ("buildbot.status.html.Waterfall is deprecated as of 0.7.6 "
- "and will be removed from a future release. "
- "Please use html.WebStatus instead.")
- warnings.warn(m, DeprecationWarning)
-
- WebStatus.__init__(self, http_port, distrib_port, allowForce)
- self.css = css
- if css:
- if os.path.exists(os.path.join("public_html", "buildbot.css")):
- # they've upgraded, so defer to that copy instead
- pass
- else:
- data = open(css, "rb").read()
- self.putChild("buildbot.css", static.Data(data, "text/plain"))
- self.favicon = favicon
- self.robots_txt = robots_txt
- if favicon:
- data = open(favicon, "rb").read()
- self.putChild("favicon.ico", static.Data(data, "image/x-icon"))
- if robots_txt:
- data = open(robots_txt, "rb").read()
- self.putChild("robots.txt", static.Data(data, "text/plain"))
- self.putChild("", WaterfallStatusResource(categories))
diff --git a/tools/buildbot/pylibs/buildbot/status/web/build.py b/tools/buildbot/pylibs/buildbot/status/web/build.py
deleted file mode 100644
index 905bedc..0000000
--- a/tools/buildbot/pylibs/buildbot/status/web/build.py
+++ /dev/null
@@ -1,289 +0,0 @@
-
-from twisted.web import html
-from twisted.web.util import Redirect, DeferredResource
-from twisted.internet import defer, reactor
-
-import urllib, time
-from twisted.python import log
-from buildbot.status.web.base import HtmlResource, make_row, make_stop_form, \
- css_classes, path_to_builder
-
-from buildbot.status.web.tests import TestsResource
-from buildbot.status.web.step import StepsResource
-from buildbot import version, util
-
-# /builders/$builder/builds/$buildnum
-class StatusResourceBuild(HtmlResource):
- addSlash = True
-
- def __init__(self, build_status, build_control, builder_control):
- HtmlResource.__init__(self)
- self.build_status = build_status
- self.build_control = build_control
- self.builder_control = builder_control
-
- def getTitle(self, request):
- return ("Buildbot: %s Build #%d" %
- (html.escape(self.build_status.getBuilder().getName()),
- self.build_status.getNumber()))
-
- def body(self, req):
- b = self.build_status
- status = self.getStatus(req)
- projectURL = status.getProjectURL()
- projectName = status.getProjectName()
- data = ('\n'
- % (self.path_to_root(req), projectName))
- # the color in the following line gives python-mode trouble
- builder_name = b.getBuilder().getName()
- data += ("\n"
- % (path_to_builder(req, b.getBuilder()),
- builder_name, b.getNumber()))
-
- if not b.isFinished():
- data += "Build In Progress "
- when = b.getETA()
- if when is not None:
- when_time = time.strftime("%H:%M:%S",
- time.localtime(time.time() + when))
- data += "ETA %ds (%s)
\n" % (when, when_time)
-
- if self.build_control is not None:
- stopURL = urllib.quote(req.childLink("stop"))
- data += make_stop_form(stopURL)
-
- if b.isFinished():
- results = b.getResults()
- data += "Results: \n"
- text = " ".join(b.getText())
- data += '%s \n' % (css_classes[results],
- text)
- if b.getTestResults():
- url = req.childLink("tests")
- data += "\n" % url
-
- ss = b.getSourceStamp()
- data += "SourceStamp: \n"
- data += " \n"
- if ss.branch:
- data += " Branch: %s \n" % html.escape(ss.branch)
- if ss.revision:
- data += " Revision: %s \n" % html.escape(str(ss.revision))
- if ss.patch:
- data += " Patch: YES \n" # TODO: provide link to .diff
- if ss.changes:
- data += " Changes: see below \n"
- if (ss.branch is None and ss.revision is None and ss.patch is None
- and not ss.changes):
- data += " build of most recent revision \n"
- got_revision = None
- try:
- got_revision = b.getProperty("got_revision")
- except KeyError:
- pass
- if got_revision:
- got_revision = str(got_revision)
- if len(got_revision) > 40:
- got_revision = "[revision string too long]"
- data += " Got Revision: %s \n" % got_revision
- data += " \n"
-
- # TODO: turn this into a table, or some other sort of definition-list
- # that doesn't take up quite so much vertical space
- data += "Buildslave: \n %s\n" % html.escape(b.getSlavename())
- data += "Reason: \n%s\n" % html.escape(b.getReason())
-
- data += "Steps and Logfiles: \n"
- # TODO:
-# urls = self.original.getURLs()
-# ex_url_class = "BuildStep external"
-# for name, target in urls.items():
-# text.append('[%s ]' %
-# (target, ex_url_class, html.escape(name)))
- if b.getLogs():
- data += "\n"
- for s in b.getSteps():
- name = s.getName()
- data += (" %s [%s]\n"
- % (req.childLink("steps/%s" % urllib.quote(name)),
- name,
- " ".join(s.getText())))
- if s.getLogs():
- data += " \n"
- for logfile in s.getLogs():
- logname = logfile.getName()
- logurl = req.childLink("steps/%s/logs/%s" %
- (urllib.quote(name),
- urllib.quote(logname)))
- data += (" %s \n" %
- (logurl, logfile.getName()))
- data += " \n"
- data += " \n"
- data += " \n"
-
- data += "Build Properties: \n"
- data += "Name Value Source \n"
- for name, value, source in b.getProperties().asList():
- value = str(value)
- if len(value) > 500:
- value = value[:500] + " .. [property value too long]"
- data += ""
- data += "%s " % html.escape(name)
- data += "%s " % html.escape(value)
- data += "%s " % html.escape(source)
- data += " \n"
- data += "
"
-
- data += "Blamelist: \n"
- if list(b.getResponsibleUsers()):
- data += " \n"
- for who in b.getResponsibleUsers():
- data += " %s \n" % html.escape(who)
- data += " \n"
- else:
- data += "no responsible users
\n"
-
- if ss.changes:
- data += "All Changes \n"
- data += "\n"
- for c in ss.changes:
- data += "" + c.asHTML() + " \n"
- data += " \n"
- #data += html.PRE(b.changesText()) # TODO
-
- if b.isFinished() and self.builder_control is not None:
- data += "Resubmit Build: \n"
- # can we rebuild it exactly?
- exactly = (ss.revision is not None) or b.getChanges()
- if exactly:
- data += ("This tree was built from a specific set of \n"
- "source files, and can be rebuilt exactly
\n")
- else:
- data += ("This tree was built from the most recent "
- "revision")
- if ss.branch:
- data += " (along some branch)"
- data += (" and thus it might not be possible to rebuild it \n"
- "exactly. Any changes that have been committed \n"
- "after this build was started will be \n"
- "included in a rebuild.
\n")
- rebuildURL = urllib.quote(req.childLink("rebuild"))
- data += ('\n'
-
- # TODO: this stuff should be generated by a template of some sort
- data += ' \n'
-
- return data
-
- def stop(self, req):
- b = self.build_status
- c = self.build_control
- log.msg("web stopBuild of build %s:%s" % \
- (b.getBuilder().getName(), b.getNumber()))
- name = req.args.get("username", [""])[0]
- comments = req.args.get("comments", [""])[0]
- reason = ("The web-page 'stop build' button was pressed by "
- "'%s': %s\n" % (name, comments))
- c.stopBuild(reason)
- # we're at http://localhost:8080/svn-hello/builds/5/stop?[args] and
- # we want to go to: http://localhost:8080/svn-hello
- r = Redirect("../..")
- d = defer.Deferred()
- reactor.callLater(1, d.callback, r)
- return DeferredResource(d)
-
- def rebuild(self, req):
- b = self.build_status
- bc = self.builder_control
- builder_name = b.getBuilder().getName()
- log.msg("web rebuild of build %s:%s" % (builder_name, b.getNumber()))
- name = req.args.get("username", [""])[0]
- comments = req.args.get("comments", [""])[0]
- reason = ("The web-page 'rebuild' button was pressed by "
- "'%s': %s\n" % (name, comments))
- if not bc or not b.isFinished():
- log.msg("could not rebuild: bc=%s, isFinished=%s"
- % (bc, b.isFinished()))
- # TODO: indicate an error
- else:
- bc.resubmitBuild(b, reason)
- # we're at
- # http://localhost:8080/builders/NAME/builds/5/rebuild?[args]
- # Where should we send them?
- #
- # Ideally it would be to the per-build page that they just started,
- # but we don't know the build number for it yet (besides, it might
- # have to wait for a current build to finish). The next-most
- # preferred place is somewhere that the user can see tangible
- # evidence of their build starting (or to see the reason that it
- # didn't start). This should be the Builder page.
- r = Redirect("../..") # the Builder's page
- d = defer.Deferred()
- reactor.callLater(1, d.callback, r)
- return DeferredResource(d)
-
- def getChild(self, path, req):
- if path == "stop":
- return self.stop(req)
- if path == "rebuild":
- return self.rebuild(req)
- if path == "steps":
- return StepsResource(self.build_status)
- if path == "tests":
- return TestsResource(self.build_status)
-
- return HtmlResource.getChild(self, path, req)
-
-# /builders/$builder/builds
-class BuildsResource(HtmlResource):
- addSlash = True
-
- def __init__(self, builder_status, builder_control):
- HtmlResource.__init__(self)
- self.builder_status = builder_status
- self.builder_control = builder_control
-
- def getChild(self, path, req):
- try:
- num = int(path)
- except ValueError:
- num = None
- if num is not None:
- build_status = self.builder_status.getBuild(num)
- if build_status:
- if self.builder_control:
- build_control = self.builder_control.getBuild(num)
- else:
- build_control = None
- return StatusResourceBuild(build_status, build_control,
- self.builder_control)
-
- return HtmlResource.getChild(self, path, req)
-
diff --git a/tools/buildbot/pylibs/buildbot/status/web/builder.py b/tools/buildbot/pylibs/buildbot/status/web/builder.py
deleted file mode 100644
index d625d43..0000000
--- a/tools/buildbot/pylibs/buildbot/status/web/builder.py
+++ /dev/null
@@ -1,358 +0,0 @@
-
-from twisted.web.error import NoResource
-from twisted.web import html, static
-from twisted.web.util import Redirect
-
-import re, urllib, time
-from twisted.python import log
-from buildbot import interfaces
-from buildbot.status.web.base import HtmlResource, make_row, \
- make_force_build_form, OneLineMixin
-from buildbot.process.base import BuildRequest
-from buildbot.sourcestamp import SourceStamp
-from buildbot import version, util
-
-from buildbot.status.web.build import BuildsResource, StatusResourceBuild
-
-# /builders/$builder
-class StatusResourceBuilder(HtmlResource, OneLineMixin):
- addSlash = True
-
- def __init__(self, builder_status, builder_control):
- HtmlResource.__init__(self)
- self.builder_status = builder_status
- self.builder_control = builder_control
-
- def getTitle(self, request):
- return "Buildbot: %s" % html.escape(self.builder_status.getName())
-
- def build_line(self, build, req):
- buildnum = build.getNumber()
- buildurl = req.childLink("builds/%d" % buildnum)
- data = '#%d ' % (buildurl, buildnum)
-
- when = build.getETA()
- if when is not None:
- when_time = time.strftime("%H:%M:%S",
- time.localtime(time.time() + when))
- data += "ETA %ds (%s) " % (when, when_time)
- step = build.getCurrentStep()
- if step:
- data += "[%s]" % step.getName()
- else:
- data += "[waiting for Lock]"
- # TODO: is this necessarily the case?
-
- if self.builder_control is not None:
- stopURL = urllib.quote(req.childLink("builds/%d/stop" % buildnum))
- data += '''
-''' % stopURL
- return data
-
- def body(self, req):
- b = self.builder_status
- control = self.builder_control
- status = self.getStatus(req)
-
- slaves = b.getSlaves()
- connected_slaves = [s for s in slaves if s.isConnected()]
-
- projectName = status.getProjectName()
-
- data = '%s \n' % (self.path_to_root(req), projectName)
-
- data += "Builder: %s \n" % html.escape(b.getName())
-
- # the first section shows builds which are currently running, if any.
-
- current = b.getCurrentBuilds()
- if current:
- data += "Currently Building: \n"
- data += "\n"
- for build in current:
- data += " " + self.build_line(build, req) + " \n"
- data += " \n"
- else:
- data += "no current builds \n"
-
- # Then a section with the last 5 builds, with the most recent build
- # distinguished from the rest.
-
- data += "Recent Builds: \n"
- data += "\n"
- for i,build in enumerate(b.generateFinishedBuilds(num_builds=5)):
- data += " " + self.make_line(req, build, False) + " \n"
- if i == 0:
- data += " \n" # separator
- # TODO: or empty list?
- data += " \n"
-
-
- data += "Buildslaves: \n"
- data += "\n"
- for slave in slaves:
- data += "%s : " % html.escape(slave.getName())
- if slave.isConnected():
- data += "CONNECTED\n"
- if slave.getAdmin():
- data += make_row("Admin:", html.escape(slave.getAdmin()))
- if slave.getHost():
- data += "Host info: \n"
- data += html.PRE(slave.getHost())
- else:
- data += ("NOT CONNECTED\n")
- data += " \n"
- data += " \n"
-
- if control is not None and connected_slaves:
- forceURL = urllib.quote(req.childLink("force"))
- data += make_force_build_form(forceURL)
- elif control is not None:
- data += """
- All buildslaves appear to be offline, so it's not possible
- to force this build to execute at this time.
- """
-
- if control is not None:
- pingURL = urllib.quote(req.childLink("ping"))
- data += """
-
- """ % pingURL
-
- # TODO: this stuff should be generated by a template of some sort
- projectURL = status.getProjectURL()
- projectName = status.getProjectName()
- data += ' \n'
-
- return data
-
- def force(self, req):
- """
-
- Custom properties can be passed from the web form. To do
- this, subclass this class, overriding the force() method. You
- can then determine the properties (usually from form values,
- by inspecting req.args), then pass them to this superclass
- force method.
-
- """
- name = req.args.get("username", [""])[0]
- reason = req.args.get("comments", [""])[0]
- branch = req.args.get("branch", [""])[0]
- revision = req.args.get("revision", [""])[0]
-
- r = "The web-page 'force build' button was pressed by '%s': %s\n" \
- % (name, reason)
- log.msg("web forcebuild of builder '%s', branch='%s', revision='%s'"
- % (self.builder_status.getName(), branch, revision))
-
- if not self.builder_control:
- # TODO: tell the web user that their request was denied
- log.msg("but builder control is disabled")
- return Redirect("..")
-
- # keep weird stuff out of the branch and revision strings. TODO:
- # centralize this somewhere.
- if not re.match(r'^[\w\.\-\/]*$', branch):
- log.msg("bad branch '%s'" % branch)
- return Redirect("..")
- if not re.match(r'^[\w\.\-\/]*$', revision):
- log.msg("bad revision '%s'" % revision)
- return Redirect("..")
- if not branch:
- branch = None
- if not revision:
- revision = None
-
- # TODO: if we can authenticate that a particular User pushed the
- # button, use their name instead of None, so they'll be informed of
- # the results.
- s = SourceStamp(branch=branch, revision=revision)
- req = BuildRequest(r, s, builderName=self.builder_status.getName())
- try:
- self.builder_control.requestBuildSoon(req)
- except interfaces.NoSlaveError:
- # TODO: tell the web user that their request could not be
- # honored
- pass
- # send the user back to the builder page
- return Redirect(".")
-
- def ping(self, req):
- log.msg("web ping of builder '%s'" % self.builder_status.getName())
- self.builder_control.ping() # TODO: there ought to be an ISlaveControl
- # send the user back to the builder page
- return Redirect(".")
-
- def getChild(self, path, req):
- if path == "force":
- return self.force(req)
- if path == "ping":
- return self.ping(req)
- if path == "events":
- num = req.postpath.pop(0)
- req.prepath.append(num)
- num = int(num)
- # TODO: is this dead code? .statusbag doesn't exist,right?
- log.msg("getChild['path']: %s" % req.uri)
- return NoResource("events are unavailable until code gets fixed")
- filename = req.postpath.pop(0)
- req.prepath.append(filename)
- e = self.builder_status.getEventNumbered(num)
- if not e:
- return NoResource("No such event '%d'" % num)
- file = e.files.get(filename, None)
- if file == None:
- return NoResource("No such file '%s'" % filename)
- if type(file) == type(""):
- if file[:6] in ("", ""):
- return static.Data(file, "text/html")
- return static.Data(file, "text/plain")
- return file
- if path == "builds":
- return BuildsResource(self.builder_status, self.builder_control)
-
- return HtmlResource.getChild(self, path, req)
-
-
-# /builders/_all
-class StatusResourceAllBuilders(HtmlResource, OneLineMixin):
-
- def __init__(self, status, control):
- HtmlResource.__init__(self)
- self.status = status
- self.control = control
-
- def getChild(self, path, req):
- if path == "force":
- return self.force(req)
- if path == "stop":
- return self.stop(req)
-
- return HtmlResource.getChild(self, path, req)
-
- def force(self, req):
- for bname in self.status.getBuilderNames():
- builder_status = self.status.getBuilder(bname)
- builder_control = None
- c = self.getControl(req)
- if c:
- builder_control = c.getBuilder(bname)
- build = StatusResourceBuilder(builder_status, builder_control)
- build.force(req)
- # back to the welcome page
- return Redirect("../..")
-
- def stop(self, req):
- for bname in self.status.getBuilderNames():
- builder_status = self.status.getBuilder(bname)
- builder_control = None
- c = self.getControl(req)
- if c:
- builder_control = c.getBuilder(bname)
- (state, current_builds) = builder_status.getState()
- if state != "building":
- continue
- for b in current_builds:
- build_status = builder_status.getBuild(b.number)
- if not build_status:
- continue
- if builder_control:
- build_control = builder_control.getBuild(b.number)
- else:
- build_control = None
- build = StatusResourceBuild(build_status, build_control,
- builder_control)
- build.stop(req)
- # go back to the welcome page
- return Redirect("../..")
-
-
-# /builders
-class BuildersResource(HtmlResource):
- title = "Builders"
- addSlash = True
-
- def body(self, req):
- s = self.getStatus(req)
- data = ""
- data += "Builders \n"
-
- # TODO: this is really basic. It should be expanded to include a
- # brief one-line summary of the builder (perhaps with whatever the
- # builder is currently doing)
- data += "\n"
- for bname in s.getBuilderNames():
- data += (' %s \n' %
- (req.childLink(urllib.quote(bname, safe='')),
- urllib.quote(bname, safe='')))
- data += " \n"
-
- # TODO: this stuff should be generated by a template of some sort
- projectURL = s.getProjectURL()
- projectName = s.getProjectName()
- data += ' \n'
-
- return data
-
- def getChild(self, path, req):
- s = self.getStatus(req)
- if path in s.getBuilderNames():
- builder_status = s.getBuilder(path)
- builder_control = None
- c = self.getControl(req)
- if c:
- builder_control = c.getBuilder(path)
- return StatusResourceBuilder(builder_status, builder_control)
- if path == "_all":
- return StatusResourceAllBuilders(self.getStatus(req),
- self.getControl(req))
-
- return HtmlResource.getChild(self, path, req)
-
diff --git a/tools/buildbot/pylibs/buildbot/status/web/changes.py b/tools/buildbot/pylibs/buildbot/status/web/changes.py
deleted file mode 100644
index d6b4110..0000000
--- a/tools/buildbot/pylibs/buildbot/status/web/changes.py
+++ /dev/null
@@ -1,41 +0,0 @@
-
-from zope.interface import implements
-from twisted.python import components
-from twisted.web.error import NoResource
-
-from buildbot.changes.changes import Change
-from buildbot.status.web.base import HtmlResource, StaticHTML, IBox, Box
-
-# /changes/NN
-class ChangesResource(HtmlResource):
-
- def body(self, req):
- data = ""
- data += "Change sources:\n"
- sources = self.getStatus(req).getChangeSources()
- if sources:
- data += "\n"
- for s in sources:
- data += "%s \n" % s.describe()
- data += " \n"
- else:
- data += "none (push only)\n"
- return data
-
- def getChild(self, path, req):
- num = int(path)
- c = self.getStatus(req).getChange(num)
- if not c:
- return NoResource("No change number '%d'" % num)
- return StaticHTML(c.asHTML(), "Change #%d" % num)
-
-
-class ChangeBox(components.Adapter):
- implements(IBox)
-
- def getBox(self, req):
- url = req.childLink("../changes/%d" % self.original.number)
- text = self.original.get_HTML_box(url)
- return Box([text], color="white", class_="Change")
-components.registerAdapter(ChangeBox, Change, IBox)
-
diff --git a/tools/buildbot/pylibs/buildbot/status/web/classic.css b/tools/buildbot/pylibs/buildbot/status/web/classic.css
deleted file mode 100644
index d57736e..0000000
--- a/tools/buildbot/pylibs/buildbot/status/web/classic.css
+++ /dev/null
@@ -1,78 +0,0 @@
-a:visited {
- color: #800080;
-}
-
-td.Event, td.BuildStep, td.Activity, td.Change, td.Time, td.Builder {
- border-top: 1px solid;
- border-right: 1px solid;
-}
-
-td.box {
- border: 1px solid;
-}
-
-/* Activity states */
-.offline {
- background-color: red;
-}
-.idle {
- background-color: white;
-}
-.waiting {
- background-color: yellow;
-}
-.building {
- background-color: yellow;
-}
-
-/* LastBuild, BuildStep states */
-.success {
- background-color: #72ff75;
-}
-.failure {
- background-color: red;
-}
-.warnings {
- background-color: #ff8000;
-}
-.exception {
- background-color: #c000c0;
-}
-.start,.running {
- background-color: yellow;
-}
-
-/* grid styles */
-
-table.Grid {
- border-collapse: collapse;
-}
-
-table.Grid tr td {
- padding: 0.2em;
- margin: 0px;
- text-align: center;
-}
-
-table.Grid tr td.title {
- font-size: 90%;
- border-right: 1px gray solid;
- border-bottom: 1px gray solid;
-}
-
-table.Grid tr td.sourcestamp {
- font-size: 90%;
-}
-
-table.Grid tr td.builder {
- text-align: right;
- font-size: 90%;
-}
-
-table.Grid tr td.build {
- border: 1px gray solid;
-}
-
-div.footer {
- font-size: 80%;
-}
diff --git a/tools/buildbot/pylibs/buildbot/status/web/grid.py b/tools/buildbot/pylibs/buildbot/status/web/grid.py
deleted file mode 100644
index 59a25aa..0000000
--- a/tools/buildbot/pylibs/buildbot/status/web/grid.py
+++ /dev/null
@@ -1,263 +0,0 @@
-from __future__ import generators
-
-import sys, time, os.path
-import urllib
-
-from buildbot import util
-from buildbot import version
-from buildbot.status.web.base import HtmlResource
-
-# set grid_css to the full pathname of the css file
-if hasattr(sys, "frozen"):
- # all 'data' files are in the directory of our executable
- here = os.path.dirname(sys.executable)
- grid_css = os.path.abspath(os.path.join(here, "grid.css"))
-else:
- # running from source; look for a sibling to __file__
- up = os.path.dirname
- grid_css = os.path.abspath(os.path.join(up(__file__), "grid.css"))
-
-class ANYBRANCH: pass # a flag value, used below
-
-class GridStatusResource(HtmlResource):
- # TODO: docs
- status = None
- control = None
- changemaster = None
-
- def __init__(self, allowForce=True, css=None):
- HtmlResource.__init__(self)
-
- self.allowForce = allowForce
- self.css = css or grid_css
-
- def getTitle(self, request):
- status = self.getStatus(request)
- p = status.getProjectName()
- if p:
- return "BuildBot: %s" % p
- else:
- return "BuildBot"
-
- def getChangemaster(self, request):
- # TODO: this wants to go away, access it through IStatus
- return request.site.buildbot_service.parent.change_svc
-
- # handle reloads through an http header
- # TODO: send this as a real header, rather than a tag
- def get_reload_time(self, request):
- if "reload" in request.args:
- try:
- reload_time = int(request.args["reload"][0])
- return max(reload_time, 15)
- except ValueError:
- pass
- return None
-
- def head(self, request):
- head = ''
- reload_time = self.get_reload_time(request)
- if reload_time is not None:
- head += ' \n' % reload_time
- return head
-
-# def setBuildmaster(self, buildmaster):
-# self.status = buildmaster.getStatus()
-# if self.allowForce:
-# self.control = interfaces.IControl(buildmaster)
-# else:
-# self.control = None
-# self.changemaster = buildmaster.change_svc
-#
-# # try to set the page title
-# p = self.status.getProjectName()
-# if p:
-# self.title = "BuildBot: %s" % p
-#
- def build_td(self, request, build):
- if not build:
- return ' \n'
-
- if build.isFinished():
- color = build.getColor()
- if color == 'green': color = '#72ff75' # the "Buildbot Green"
-
- # get the text and annotate the first line with a link
- text = build.getText()
- if not text: text = [ "(no information)" ]
- if text == [ "build", "successful" ]: text = [ "OK" ]
- else:
- color = 'yellow' # to match the yellow of the builder
- text = [ 'building' ]
-
- name = build.getBuilder().getName()
- number = build.getNumber()
- url = "builders/%s/builds/%d" % (name, number)
- text[0] = '%s ' % (url, text[0])
- text = ' \n'.join(text)
-
- return '%s \n' % (color, text)
-
- def builder_td(self, request, builder):
- state, builds = builder.getState()
-
- # look for upcoming builds. We say the state is "waiting" if the
- # builder is otherwise idle and there is a scheduler which tells us a
- # build will be performed some time in the near future. TODO: this
- # functionality used to be in BuilderStatus.. maybe this code should
- # be merged back into it.
- upcoming = []
- builderName = builder.getName()
- for s in self.getStatus(request).getSchedulers():
- if builderName in s.listBuilderNames():
- upcoming.extend(s.getPendingBuildTimes())
- if state == "idle" and upcoming:
- state = "waiting"
-
- if state == "building":
- color = "yellow"
- elif state == "offline":
- color = "red"
- elif state == "idle":
- color = "white"
- elif state == "waiting":
- color = "yellow"
- else:
- color = "white"
-
- # TODO: for now, this pending/upcoming stuff is in the "current
- # activity" box, but really it should go into a "next activity" row
- # instead. The only times it should show up in "current activity" is
- # when the builder is otherwise idle.
-
- # are any builds pending? (waiting for a slave to be free)
- url = 'builders/%s/' % urllib.quote(builder.getName(), safe='')
- text = '%s ' % (url, builder.getName())
- pbs = builder.getPendingBuilds()
- if state != 'idle' or pbs:
- if pbs:
- text += " (%s with %d pending)" % (state, len(pbs))
- else:
- text += " (%s)" % state
-
- return '%s \n' % \
- (color, text)
-
- def stamp_td(self, stamp):
- text = stamp.getText()
- return '%s \n' % \
- " ".join(text)
-
- def body(self, request):
- "This method builds the main waterfall display."
-
- # get url parameters
- numBuilds = int(request.args.get("width", [5])[0])
- categories = request.args.get("category", [])
- branch = request.args.get("branch", [ANYBRANCH])[0]
- if branch == 'trunk': branch = None
-
- # and the data we want to render
- status = self.getStatus(request)
- stamps = self.getRecentSourcestamps(status, numBuilds, categories, branch)
-
- projectURL = status.getProjectURL()
- projectName = status.getProjectName()
-
- data = '\n'
- data += '\n'
- data += '%s ' % (projectURL, projectName)
- if categories:
- if len(categories) > 1:
- data += '\nCategories: %s' % (' '.join(categories))
- else:
- data += '\nCategory: %s' % categories[0]
- if branch != ANYBRANCH:
- data += '\nBranch: %s' % (branch or 'trunk')
- data += ' \n'
- for stamp in stamps:
- data += self.stamp_td(stamp)
- data += ' \n'
-
- sortedBuilderNames = status.getBuilderNames()[:]
- sortedBuilderNames.sort()
- for bn in sortedBuilderNames:
- builds = [None] * len(stamps)
-
- builder = status.getBuilder(bn)
- if categories and builder.category not in categories:
- continue
-
- build = builder.getBuild(-1)
- while build and None in builds:
- ss = build.getSourceStamp(absolute=True)
- for i in range(len(stamps)):
- if ss == stamps[i] and builds[i] is None:
- builds[i] = build
- build = build.getPreviousBuild()
-
- data += '\n'
- data += self.builder_td(request, builder)
- for build in builds:
- data += self.build_td(request, build)
- data += ' \n'
-
- data += '
\n'
-
- # TODO: this stuff should be generated by a template of some sort
- data += ' \n'
- return data
-
- def getRecentSourcestamps(self, status, numBuilds, categories, branch):
- """
- get a list of the most recent NUMBUILDS SourceStamp tuples, sorted
- by the earliest start we've seen for them
- """
- # TODO: use baseweb's getLastNBuilds?
- sourcestamps = { } # { ss-tuple : earliest time }
- for bn in status.getBuilderNames():
- builder = status.getBuilder(bn)
- if categories and builder.category not in categories:
- continue
- build = builder.getBuild(-1)
- while build:
- ss = build.getSourceStamp(absolute=True)
- start = build.getTimes()[0]
- build = build.getPreviousBuild()
-
- # skip un-started builds
- if not start: continue
-
- # skip non-matching branches
- if branch != ANYBRANCH and ss.branch != branch: continue
-
- sourcestamps[ss] = min(sourcestamps.get(ss, sys.maxint), start)
-
- # now sort those and take the NUMBUILDS most recent
- sourcestamps = sourcestamps.items()
- sourcestamps.sort(lambda x, y: cmp(x[1], y[1]))
- sourcestamps = map(lambda tup : tup[0], sourcestamps)
- sourcestamps = sourcestamps[-numBuilds:]
-
- return sourcestamps
-
diff --git a/tools/buildbot/pylibs/buildbot/status/web/index.html b/tools/buildbot/pylibs/buildbot/status/web/index.html
deleted file mode 100644
index 23e6650..0000000
--- a/tools/buildbot/pylibs/buildbot/status/web/index.html
+++ /dev/null
@@ -1,32 +0,0 @@
-
-
-
-
-Welcome to the Buildbot
-
-
-
-Welcome to the Buildbot!
-
-
-
-
-
diff --git a/tools/buildbot/pylibs/buildbot/status/web/logs.py b/tools/buildbot/pylibs/buildbot/status/web/logs.py
deleted file mode 100644
index 20132ac..0000000
--- a/tools/buildbot/pylibs/buildbot/status/web/logs.py
+++ /dev/null
@@ -1,168 +0,0 @@
-
-from zope.interface import implements
-from twisted.python import components
-from twisted.spread import pb
-from twisted.web import html, server
-from twisted.web.resource import Resource
-from twisted.web.error import NoResource
-
-from buildbot import interfaces
-from buildbot.status import builder
-from buildbot.status.web.base import IHTMLLog, HtmlResource
-
-
-textlog_stylesheet = """
-
-"""
-
-class ChunkConsumer:
- implements(interfaces.IStatusLogConsumer)
-
- def __init__(self, original, textlog):
- self.original = original
- self.textlog = textlog
- def registerProducer(self, producer, streaming):
- self.producer = producer
- self.original.registerProducer(producer, streaming)
- def unregisterProducer(self):
- self.original.unregisterProducer()
- def writeChunk(self, chunk):
- formatted = self.textlog.content([chunk])
- try:
- self.original.write(formatted)
- except pb.DeadReferenceError:
- self.producing.stopProducing()
- def finish(self):
- self.textlog.finished()
-
-
-# /builders/$builder/builds/$buildnum/steps/$stepname/logs/$logname
-class TextLog(Resource):
- # a new instance of this Resource is created for each client who views
- # it, so we can afford to track the request in the Resource.
- implements(IHTMLLog)
-
- asText = False
- subscribed = False
-
- def __init__(self, original):
- Resource.__init__(self)
- self.original = original
-
- def getChild(self, path, req):
- if path == "text":
- self.asText = True
- return self
- return HtmlResource.getChild(self, path, req)
-
- def htmlHeader(self, request):
- title = "Log File contents"
- data = "\n" + title + " \n"
- data += textlog_stylesheet
- data += "\n"
- data += "\n"
- texturl = request.childLink("text")
- data += '(view as text) \n' % texturl
- data += "\n"
- return data
-
- def content(self, entries):
- spanfmt = '%s '
- data = ""
- for type, entry in entries:
- if self.asText:
- if type != builder.HEADER:
- data += entry
- else:
- data += spanfmt % (builder.ChunkTypes[type],
- html.escape(entry))
- return data
-
- def htmlFooter(self):
- data = " \n"
- data += "\n"
- return data
-
- def render_HEAD(self, request):
- if self.asText:
- request.setHeader("content-type", "text/plain")
- else:
- request.setHeader("content-type", "text/html")
-
- # vague approximation, ignores markup
- request.setHeader("content-length", self.original.length)
- return ''
-
- def render_GET(self, req):
- self.req = req
-
- if self.asText:
- req.setHeader("content-type", "text/plain")
- else:
- req.setHeader("content-type", "text/html")
-
- if not self.asText:
- req.write(self.htmlHeader(req))
-
- self.original.subscribeConsumer(ChunkConsumer(req, self))
- return server.NOT_DONE_YET
-
- def finished(self):
- if not self.req:
- return
- try:
- if not self.asText:
- self.req.write(self.htmlFooter())
- self.req.finish()
- except pb.DeadReferenceError:
- pass
- # break the cycle, the Request's .notifications list includes the
- # Deferred (from req.notifyFinish) that's pointing at us.
- self.req = None
-
-components.registerAdapter(TextLog, interfaces.IStatusLog, IHTMLLog)
-
-
-class HTMLLog(Resource):
- implements(IHTMLLog)
-
- def __init__(self, original):
- Resource.__init__(self)
- self.original = original
-
- def render(self, request):
- request.setHeader("content-type", "text/html")
- return self.original.html
-
-components.registerAdapter(HTMLLog, builder.HTMLLogFile, IHTMLLog)
-
-
-class LogsResource(HtmlResource):
- addSlash = True
-
- def __init__(self, step_status):
- HtmlResource.__init__(self)
- self.step_status = step_status
-
- def getChild(self, path, req):
- for log in self.step_status.getLogs():
- if path == log.getName():
- if log.hasContents():
- return IHTMLLog(interfaces.IStatusLog(log))
- return NoResource("Empty Log '%s'" % path)
- return HtmlResource.getChild(self, path, req)
diff --git a/tools/buildbot/pylibs/buildbot/status/web/robots.txt b/tools/buildbot/pylibs/buildbot/status/web/robots.txt
deleted file mode 100644
index 47a9d27..0000000
--- a/tools/buildbot/pylibs/buildbot/status/web/robots.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-User-agent: *
-Disallow: /waterfall
-Disallow: /builders
-Disallow: /changes
-Disallow: /buildslaves
-Disallow: /schedulers
-Disallow: /one_line_per_build
-Disallow: /one_box_per_builder
-Disallow: /xmlrpc
diff --git a/tools/buildbot/pylibs/buildbot/status/web/slaves.py b/tools/buildbot/pylibs/buildbot/status/web/slaves.py
deleted file mode 100644
index 62c4012..0000000
--- a/tools/buildbot/pylibs/buildbot/status/web/slaves.py
+++ /dev/null
@@ -1,71 +0,0 @@
-
-import time
-from buildbot.status.web.base import HtmlResource, abbreviate_age
-
-# /buildslaves/$slavename
-class OneBuildSlaveResource(HtmlResource):
- pass # TODO
-
-# /buildslaves
-class BuildSlavesResource(HtmlResource):
- title = "BuildSlaves"
- addSlash = True
-
- def body(self, req):
- s = self.getStatus(req)
- data = ""
- data += "Build Slaves \n"
-
- used_by_builder = {}
- for bname in s.getBuilderNames():
- b = s.getBuilder(bname)
- for bs in b.getSlaves():
- slavename = bs.getName()
- if slavename not in used_by_builder:
- used_by_builder[slavename] = []
- used_by_builder[slavename].append(bname)
-
- data += "\n"
- for name in s.getSlaveNames():
- slave = s.getSlave(name)
- slave_status = s.botmaster.slaves[name].slave_status
- isBusy = len(slave_status.getRunningBuilds())
- data += " %s:\n" % name
- data += " \n"
- builder_links = ['%s '
- % (req.childLink("../builders/%s" % bname),bname)
- for bname in used_by_builder.get(name, [])]
- if builder_links:
- data += (" Used by Builders: %s \n" %
- ", ".join(builder_links))
- else:
- data += " Not used by any Builders \n"
- if slave.isConnected():
- data += " Slave is currently connected \n"
- admin = slave.getAdmin()
- if admin:
- # munge it to avoid feeding the spambot harvesters
- admin = admin.replace("@", " -at- ")
- data += " Admin: %s \n" % admin
- last = slave.lastMessageReceived()
- if last:
- lt = time.strftime("%Y-%b-%d %H:%M:%S",
- time.localtime(last))
- age = abbreviate_age(time.time() - last)
- data += " Last heard from: %s " % age
- data += '(%s) ' % lt
- data += " \n"
- if isBusy:
- data += "Slave is currently building. "
- else:
- data += "Slave is idle. "
- else:
- data += " Slave is NOT currently connected \n"
-
- data += " \n"
- data += " \n"
- data += "\n"
-
- data += " \n"
-
- return data
diff --git a/tools/buildbot/pylibs/buildbot/status/web/step.py b/tools/buildbot/pylibs/buildbot/status/web/step.py
deleted file mode 100644
index c5b1aa6..0000000
--- a/tools/buildbot/pylibs/buildbot/status/web/step.py
+++ /dev/null
@@ -1,85 +0,0 @@
-
-from twisted.web import html
-
-import urllib
-from buildbot.status.web.base import HtmlResource, path_to_builder, \
- path_to_build
-from buildbot.status.web.logs import LogsResource
-
-# /builders/$builder/builds/$buildnum/steps/$stepname
-class StatusResourceBuildStep(HtmlResource):
- title = "Build Step"
- addSlash = True
-
- def __init__(self, build_status, step_status):
- HtmlResource.__init__(self)
- self.status = build_status
- self.step_status = step_status
-
- def body(self, req):
- s = self.step_status
- b = s.getBuild()
- builder_name = b.getBuilder().getName()
- build_num = b.getNumber()
- data = ""
- data += ('BuildStep %s :' %
- (path_to_builder(req, b.getBuilder()), builder_name))
- data += '#%d ' % (path_to_build(req, b), build_num)
- data += ":%s \n" % s.getName()
-
- if s.isFinished():
- data += ("Finished \n"
- "%s
\n" % html.escape("%s" % s.getText()))
- else:
- data += ("Not Finished \n"
- "ETA %s seconds
\n" % s.getETA())
-
- exp = s.getExpectations()
- if exp:
- data += ("Expectations \n"
- "\n")
- for e in exp:
- data += "%s: current=%s, target=%s \n" % \
- (html.escape(e[0]), e[1], e[2])
- data += " \n"
- logs = s.getLogs()
- if logs:
- data += ("Logs \n"
- "\n")
- for logfile in logs:
- if logfile.hasContents():
- # FIXME: If the step name has a / in it, this is broken
- # either way. If we quote it but say '/'s are safe,
- # it chops up the step name. If we quote it and '/'s
- # are not safe, it escapes the / that separates the
- # step name from the log number.
- logname = logfile.getName()
- logurl = req.childLink("logs/%s" % urllib.quote(logname))
- data += ('%s \n' %
- (logurl, html.escape(logname)))
- else:
- data += '%s \n' % html.escape(logname)
- data += " \n"
-
- return data
-
- def getChild(self, path, req):
- if path == "logs":
- return LogsResource(self.step_status)
- return HtmlResource.getChild(self, path, req)
-
-
-
-# /builders/$builder/builds/$buildnum/steps
-class StepsResource(HtmlResource):
- addSlash = True
-
- def __init__(self, build_status):
- HtmlResource.__init__(self)
- self.build_status = build_status
-
- def getChild(self, path, req):
- for s in self.build_status.getSteps():
- if s.getName() == path:
- return StatusResourceBuildStep(self.build_status, s)
- return HtmlResource.getChild(self, path, req)
diff --git a/tools/buildbot/pylibs/buildbot/status/web/tests.py b/tools/buildbot/pylibs/buildbot/status/web/tests.py
deleted file mode 100644
index b96bba2..0000000
--- a/tools/buildbot/pylibs/buildbot/status/web/tests.py
+++ /dev/null
@@ -1,64 +0,0 @@
-
-from twisted.web.error import NoResource
-from twisted.web import html
-
-from buildbot.status.web.base import HtmlResource
-
-# /builders/$builder/builds/$buildnum/tests/$testname
-class TestResult(HtmlResource):
- title = "Test Logs"
-
- def __init__(self, name, test_result):
- HtmlResource.__init__(self)
- self.name = name
- self.test_result = test_result
-
- def body(self, request):
- dotname = ".".join(self.name)
- logs = self.test_result.getLogs()
- lognames = logs.keys()
- lognames.sort()
- data = "%s \n" % html.escape(dotname)
- for name in lognames:
- data += "%s \n" % html.escape(name)
- data += "" + logs[name] + " \n\n"
-
- return data
-
-
-# /builders/$builder/builds/$buildnum/tests
-class TestsResource(HtmlResource):
- title = "Test Results"
-
- def __init__(self, build_status):
- HtmlResource.__init__(self)
- self.build_status = build_status
- self.test_results = build_status.getTestResults()
-
- def body(self, request):
- r = self.test_results
- data = "Test Results \n"
- data += "\n"
- testnames = r.keys()
- testnames.sort()
- for name in testnames:
- res = r[name]
- dotname = ".".join(name)
- data += " %s: " % dotname
- # TODO: this could break on weird test names. At the moment,
- # test names only come from Trial tests, where the name
- # components must be legal python names, but that won't always
- # be a restriction.
- url = request.childLink(dotname)
- data += "%s " % (url, " ".join(res.getText()))
- data += " \n"
- data += " \n"
- return data
-
- def getChild(self, path, request):
- try:
- name = tuple(path.split("."))
- result = self.test_results[name]
- return TestResult(name, result)
- except KeyError:
- return NoResource("No such test name '%s'" % path)
diff --git a/tools/buildbot/pylibs/buildbot/status/web/waterfall.py b/tools/buildbot/pylibs/buildbot/status/web/waterfall.py
deleted file mode 100644
index 0fc04d1..0000000
--- a/tools/buildbot/pylibs/buildbot/status/web/waterfall.py
+++ /dev/null
@@ -1,981 +0,0 @@
-# -*- test-case-name: buildbot.test.test_web -*-
-
-from zope.interface import implements
-from twisted.python import log, components
-from twisted.web import html
-import urllib
-
-import time
-import operator
-
-from buildbot import interfaces, util
-from buildbot import version
-from buildbot.status import builder
-
-from buildbot.status.web.base import Box, HtmlResource, IBox, ICurrentBox, \
- ITopBox, td, build_get_class, path_to_build, path_to_step, map_branches
-
-
-
-class CurrentBox(components.Adapter):
- # this provides the "current activity" box, just above the builder name
- implements(ICurrentBox)
-
- def formatETA(self, prefix, eta):
- if eta is None:
- return []
- if eta < 0:
- return ["Soon"]
- eta_parts = []
- eta_secs = eta
- if eta_secs > 3600:
- eta_parts.append("%d hrs" % (eta_secs / 3600))
- eta_secs %= 3600
- if eta_secs > 60:
- eta_parts.append("%d mins" % (eta_secs / 60))
- eta_secs %= 60
- eta_parts.append("%d secs" % eta_secs)
- abstime = time.strftime("%H:%M:%S", time.localtime(util.now()+eta))
- return [prefix, ", ".join(eta_parts), "at %s" % abstime]
-
- def getBox(self, status):
- # getState() returns offline, idle, or building
- state, builds = self.original.getState()
-
- # look for upcoming builds. We say the state is "waiting" if the
- # builder is otherwise idle and there is a scheduler which tells us a
- # build will be performed some time in the near future. TODO: this
- # functionality used to be in BuilderStatus.. maybe this code should
- # be merged back into it.
- upcoming = []
- builderName = self.original.getName()
- for s in status.getSchedulers():
- if builderName in s.listBuilderNames():
- upcoming.extend(s.getPendingBuildTimes())
- if state == "idle" and upcoming:
- state = "waiting"
-
- if state == "building":
- color = "yellow"
- text = ["building"]
- if builds:
- for b in builds:
- eta = b.getETA()
- text.extend(self.formatETA("ETA in", eta))
- elif state == "offline":
- color = "red"
- text = ["offline"]
- elif state == "idle":
- color = "white"
- text = ["idle"]
- elif state == "waiting":
- color = "yellow"
- text = ["waiting"]
- else:
- # just in case I add a state and forget to update this
- color = "white"
- text = [state]
-
- # TODO: for now, this pending/upcoming stuff is in the "current
- # activity" box, but really it should go into a "next activity" row
- # instead. The only times it should show up in "current activity" is
- # when the builder is otherwise idle.
-
- # are any builds pending? (waiting for a slave to be free)
- pbs = self.original.getPendingBuilds()
- if pbs:
- text.append("%d pending" % len(pbs))
- for t in upcoming:
- eta = t - util.now()
- text.extend(self.formatETA("next in", eta))
- return Box(text, color=color, class_="Activity " + state)
-
-components.registerAdapter(CurrentBox, builder.BuilderStatus, ICurrentBox)
-
-
-class BuildTopBox(components.Adapter):
- # this provides a per-builder box at the very top of the display,
- # showing the results of the most recent build
- implements(IBox)
-
- def getBox(self, req):
- assert interfaces.IBuilderStatus(self.original)
- branches = [b for b in req.args.get("branch", []) if b]
- builder = self.original
- builds = list(builder.generateFinishedBuilds(map_branches(branches),
- num_builds=1))
- if not builds:
- return Box(["none"], "white", class_="LastBuild")
- b = builds[0]
- name = b.getBuilder().getName()
- number = b.getNumber()
- url = path_to_build(req, b)
- text = b.getText()
- tests_failed = b.getSummaryStatistic('tests-failed', operator.add, 0)
- if tests_failed: text.extend(["Failed tests: %d" % tests_failed])
- # TODO: maybe add logs?
- # TODO: add link to the per-build page at 'url'
- c = b.getColor()
- class_ = build_get_class(b)
- return Box(text, c, class_="LastBuild %s" % class_)
-components.registerAdapter(BuildTopBox, builder.BuilderStatus, ITopBox)
-
-class BuildBox(components.Adapter):
- # this provides the yellow "starting line" box for each build
- implements(IBox)
-
- def getBox(self, req):
- b = self.original
- number = b.getNumber()
- url = path_to_build(req, b)
- reason = b.getReason()
- text = ('Build %d '
- % (html.escape(reason), url, number))
- color = "yellow"
- class_ = "start"
- if b.isFinished() and not b.getSteps():
- # the steps have been pruned, so there won't be any indication
- # of whether it succeeded or failed. Color the box red or green
- # to show its status
- color = b.getColor()
- class_ = build_get_class(b)
- return Box([text], color=color, class_="BuildStep " + class_)
-components.registerAdapter(BuildBox, builder.BuildStatus, IBox)
-
-class StepBox(components.Adapter):
- implements(IBox)
-
- def getBox(self, req):
- urlbase = path_to_step(req, self.original)
- text = self.original.getText()
- if text is None:
- log.msg("getText() gave None", urlbase)
- text = []
- text = text[:]
- logs = self.original.getLogs()
- for num in range(len(logs)):
- name = logs[num].getName()
- if logs[num].hasContents():
- url = urlbase + "/logs/%s" % urllib.quote(name)
- text.append("%s " % (url, html.escape(name)))
- else:
- text.append(html.escape(name))
- urls = self.original.getURLs()
- ex_url_class = "BuildStep external"
- for name, target in urls.items():
- text.append('[%s ]' %
- (target, ex_url_class, html.escape(name)))
- color = self.original.getColor()
- class_ = "BuildStep " + build_get_class(self.original)
- return Box(text, color, class_=class_)
-components.registerAdapter(StepBox, builder.BuildStepStatus, IBox)
-
-
-class EventBox(components.Adapter):
- implements(IBox)
-
- def getBox(self, req):
- text = self.original.getText()
- color = self.original.getColor()
- class_ = "Event"
- if color:
- class_ += " " + color
- return Box(text, color, class_=class_)
-components.registerAdapter(EventBox, builder.Event, IBox)
-
-
-class Spacer:
- implements(interfaces.IStatusEvent)
-
- def __init__(self, start, finish):
- self.started = start
- self.finished = finish
-
- def getTimes(self):
- return (self.started, self.finished)
- def getText(self):
- return []
- def getColor(self):
- return None
-
-class SpacerBox(components.Adapter):
- implements(IBox)
-
- def getBox(self, req):
- #b = Box(["spacer"], "white")
- b = Box([])
- b.spacer = True
- return b
-components.registerAdapter(SpacerBox, Spacer, IBox)
-
-def insertGaps(g, lastEventTime, idleGap=2):
- debug = False
-
- e = g.next()
- starts, finishes = e.getTimes()
- if debug: log.msg("E0", starts, finishes)
- if finishes == 0:
- finishes = starts
- if debug: log.msg("E1 finishes=%s, gap=%s, lET=%s" % \
- (finishes, idleGap, lastEventTime))
- if finishes is not None and finishes + idleGap < lastEventTime:
- if debug: log.msg(" spacer0")
- yield Spacer(finishes, lastEventTime)
-
- followingEventStarts = starts
- if debug: log.msg(" fES0", starts)
- yield e
-
- while 1:
- e = g.next()
- starts, finishes = e.getTimes()
- if debug: log.msg("E2", starts, finishes)
- if finishes == 0:
- finishes = starts
- if finishes is not None and finishes + idleGap < followingEventStarts:
- # there is a gap between the end of this event and the beginning
- # of the next one. Insert an idle event so the waterfall display
- # shows a gap here.
- if debug:
- log.msg(" finishes=%s, gap=%s, fES=%s" % \
- (finishes, idleGap, followingEventStarts))
- yield Spacer(finishes, followingEventStarts)
- yield e
- followingEventStarts = starts
- if debug: log.msg(" fES1", starts)
-
-HELP = '''
-
-'''
-
-class WaterfallHelp(HtmlResource):
- title = "Waterfall Help"
-
- def __init__(self, categories=None):
- HtmlResource.__init__(self)
- self.categories = categories
-
- def body(self, request):
- data = ''
- status = self.getStatus(request)
-
- showEvents_checked = 'checked="checked"'
- if request.args.get("show_events", ["true"])[0].lower() == "true":
- showEvents_checked = ''
- show_events_input = (''
- ' '
- 'Hide non-Build events'
- '
\n'
- ) % showEvents_checked
-
- branches = [b
- for b in request.args.get("branch", [])
- if b]
- branches.append('')
- show_branches_input = '\n'
-
- # this has a set of toggle-buttons to let the user choose the
- # builders
- showBuilders = request.args.get("show", [])
- showBuilders.extend(request.args.get("builder", []))
- allBuilders = status.getBuilderNames(categories=self.categories)
-
- show_builders_input = '\n'
-
- # a couple of radio-button selectors for refresh time will appear
- # just after that text
- show_reload_input = '\n'
- times = [("none", "None"),
- ("60", "60 seconds"),
- ("300", "5 minutes"),
- ("600", "10 minutes"),
- ]
- current_reload_time = request.args.get("reload", ["none"])
- if current_reload_time:
- current_reload_time = current_reload_time[0]
- if current_reload_time not in [t[0] for t in times]:
- times.insert(0, (current_reload_time, current_reload_time) )
- for value, name in times:
- checked = ""
- if value == current_reload_time:
- checked = 'checked="checked"'
- show_reload_input += (''
- ' '
- '%s \n'
- ) % (value, checked, name)
- show_reload_input += '
\n'
-
- fields = {"show_events_input": show_events_input,
- "show_branches_input": show_branches_input,
- "show_builders_input": show_builders_input,
- "show_reload_input": show_reload_input,
- }
- data += HELP % fields
- return data
-
-class WaterfallStatusResource(HtmlResource):
- """This builds the main status page, with the waterfall display, and
- all child pages."""
-
- def __init__(self, categories=None):
- HtmlResource.__init__(self)
- self.categories = categories
- self.putChild("help", WaterfallHelp(categories))
-
- def getTitle(self, request):
- status = self.getStatus(request)
- p = status.getProjectName()
- if p:
- return "BuildBot: %s" % p
- else:
- return "BuildBot"
-
- def getChangemaster(self, request):
- # TODO: this wants to go away, access it through IStatus
- return request.site.buildbot_service.parent.change_svc
-
- def get_reload_time(self, request):
- if "reload" in request.args:
- try:
- reload_time = int(request.args["reload"][0])
- return max(reload_time, 15)
- except ValueError:
- pass
- return None
-
- def head(self, request):
- head = ''
- reload_time = self.get_reload_time(request)
- if reload_time is not None:
- head += ' \n' % reload_time
- return head
-
- def body(self, request):
- "This method builds the main waterfall display."
-
- status = self.getStatus(request)
- data = ''
-
- projectName = status.getProjectName()
- projectURL = status.getProjectURL()
-
- phase = request.args.get("phase",["2"])
- phase = int(phase[0])
-
- # we start with all Builders available to this Waterfall: this is
- # limited by the config-file -time categories= argument, and defaults
- # to all defined Builders.
- allBuilderNames = status.getBuilderNames(categories=self.categories)
- builders = [status.getBuilder(name) for name in allBuilderNames]
-
- # but if the URL has one or more builder= arguments (or the old show=
- # argument, which is still accepted for backwards compatibility), we
- # use that set of builders instead. We still don't show anything
- # outside the config-file time set limited by categories=.
- showBuilders = request.args.get("show", [])
- showBuilders.extend(request.args.get("builder", []))
- if showBuilders:
- builders = [b for b in builders if b.name in showBuilders]
-
- # now, if the URL has one or category= arguments, use them as a
- # filter: only show those builders which belong to one of the given
- # categories.
- showCategories = request.args.get("category", [])
- if showCategories:
- builders = [b for b in builders if b.category in showCategories]
-
- builderNames = [b.name for b in builders]
-
- if phase == -1:
- return self.body0(request, builders)
- (changeNames, builderNames, timestamps, eventGrid, sourceEvents) = \
- self.buildGrid(request, builders)
- if phase == 0:
- return self.phase0(request, (changeNames + builderNames),
- timestamps, eventGrid)
- # start the table: top-header material
- data += '\n'
-
- if projectName and projectURL:
- # TODO: this is going to look really ugly
- topleft = '%s last build' % \
- (projectURL, projectName)
- else:
- topleft = "last build"
- data += ' \n'
- data += td(topleft, align="right", colspan=2, class_="Project")
- for b in builders:
- box = ITopBox(b).getBox(request)
- data += box.td(align="center")
- data += " \n"
-
- data += ' \n'
- data += td('current activity', align='right', colspan=2)
- for b in builders:
- box = ICurrentBox(b).getBox(status)
- data += box.td(align="center")
- data += " \n"
-
- data += " \n"
- TZ = time.tzname[time.localtime()[-1]]
- data += td("time (%s)" % TZ, align="center", class_="Time")
- data += td('changes ' % request.childLink("../changes"),
- align="center", class_="Change")
- for name in builderNames:
- safename = urllib.quote(name, safe='')
- data += td('%s ' %
- (request.childLink("../builders/%s" % safename), name),
- align="center", class_="Builder")
- data += " \n"
-
- if phase == 1:
- f = self.phase1
- else:
- f = self.phase2
- data += f(request, changeNames + builderNames, timestamps, eventGrid,
- sourceEvents)
-
- data += "
\n"
-
- data += ' \n'
- return data
-
- def body0(self, request, builders):
- # build the waterfall display
- data = ""
- data += "Basic display \n"
- data += 'See here ' % request.childLink("../waterfall")
- data += " for the waterfall display
\n"
-
- data += '\n'
- names = map(lambda builder: builder.name, builders)
-
- # the top row is two blank spaces, then the top-level status boxes
- data += " \n"
- data += td("", colspan=2)
- for b in builders:
- text = ""
- color = "#ca88f7"
- state, builds = b.getState()
- if state != "offline":
- text += "%s \n" % state #b.getCurrentBig().text[0]
- else:
- text += "OFFLINE \n"
- color = "#ffe0e0"
- data += td(text, align="center", bgcolor=color)
-
- # the next row has the column headers: time, changes, builder names
- data += " \n"
- data += td("Time", align="center")
- data += td("Changes", align="center")
- for name in names:
- data += td('%s ' %
- (request.childLink("../" + urllib.quote(name)), name),
- align="center")
- data += " \n"
-
- # all further rows involve timestamps, commit events, and build events
- data += " \n"
- data += td("04:00", align="bottom")
- data += td("fred", align="center")
- for name in names:
- data += td("stuff", align="center", bgcolor="red")
- data += " \n"
-
- data += "
\n"
- return data
-
- def buildGrid(self, request, builders):
- debug = False
- # TODO: see if we can use a cached copy
-
- showEvents = False
- if request.args.get("show_events", ["true"])[0].lower() == "true":
- showEvents = True
- filterBranches = [b for b in request.args.get("branch", []) if b]
- filterBranches = map_branches(filterBranches)
- maxTime = int(request.args.get("last_time", [util.now()])[0])
- if "show_time" in request.args:
- minTime = maxTime - int(request.args["show_time"][0])
- elif "first_time" in request.args:
- minTime = int(request.args["first_time"][0])
- else:
- minTime = None
- spanLength = 10 # ten-second chunks
- maxPageLen = int(request.args.get("num_events", [200])[0])
-
- # first step is to walk backwards in time, asking each column
- # (commit, all builders) if they have any events there. Build up the
- # array of events, and stop when we have a reasonable number.
-
- commit_source = self.getChangemaster(request)
-
- lastEventTime = util.now()
- sources = [commit_source] + builders
- changeNames = ["changes"]
- builderNames = map(lambda builder: builder.getName(), builders)
- sourceNames = changeNames + builderNames
- sourceEvents = []
- sourceGenerators = []
-
- def get_event_from(g):
- try:
- while True:
- e = g.next()
- # e might be builder.BuildStepStatus,
- # builder.BuildStatus, builder.Event,
- # waterfall.Spacer(builder.Event), or changes.Change .
- # The showEvents=False flag means we should hide
- # builder.Event .
- if not showEvents and isinstance(e, builder.Event):
- continue
- break
- event = interfaces.IStatusEvent(e)
- if debug:
- log.msg("gen %s gave1 %s" % (g, event.getText()))
- except StopIteration:
- event = None
- return event
-
- for s in sources:
- gen = insertGaps(s.eventGenerator(filterBranches), lastEventTime)
- sourceGenerators.append(gen)
- # get the first event
- sourceEvents.append(get_event_from(gen))
- eventGrid = []
- timestamps = []
-
- lastEventTime = 0
- for e in sourceEvents:
- if e and e.getTimes()[0] > lastEventTime:
- lastEventTime = e.getTimes()[0]
- if lastEventTime == 0:
- lastEventTime = util.now()
-
- spanStart = lastEventTime - spanLength
- debugGather = 0
-
- while 1:
- if debugGather: log.msg("checking (%s,]" % spanStart)
- # the tableau of potential events is in sourceEvents[]. The
- # window crawls backwards, and we examine one source at a time.
- # If the source's top-most event is in the window, is it pushed
- # onto the events[] array and the tableau is refilled. This
- # continues until the tableau event is not in the window (or is
- # missing).
-
- spanEvents = [] # for all sources, in this span. row of eventGrid
- firstTimestamp = None # timestamp of first event in the span
- lastTimestamp = None # last pre-span event, for next span
-
- for c in range(len(sourceGenerators)):
- events = [] # for this source, in this span. cell of eventGrid
- event = sourceEvents[c]
- while event and spanStart < event.getTimes()[0]:
- # to look at windows that don't end with the present,
- # condition the .append on event.time <= spanFinish
- if not IBox(event, None):
- log.msg("BAD EVENT", event, event.getText())
- assert 0
- if debug:
- log.msg("pushing", event.getText(), event)
- events.append(event)
- starts, finishes = event.getTimes()
- firstTimestamp = util.earlier(firstTimestamp, starts)
- event = get_event_from(sourceGenerators[c])
- if debug:
- log.msg("finished span")
-
- if event:
- # this is the last pre-span event for this source
- lastTimestamp = util.later(lastTimestamp,
- event.getTimes()[0])
- if debugGather:
- log.msg(" got %s from %s" % (events, sourceNames[c]))
- sourceEvents[c] = event # refill the tableau
- spanEvents.append(events)
-
- # only show events older than maxTime. This makes it possible to
- # visit a page that shows what it would be like to scroll off the
- # bottom of this one.
- if firstTimestamp is not None and firstTimestamp <= maxTime:
- eventGrid.append(spanEvents)
- timestamps.append(firstTimestamp)
-
- if lastTimestamp:
- spanStart = lastTimestamp - spanLength
- else:
- # no more events
- break
- if minTime is not None and lastTimestamp < minTime:
- break
-
- if len(timestamps) > maxPageLen:
- break
-
-
- # now loop
-
- # loop is finished. now we have eventGrid[] and timestamps[]
- if debugGather: log.msg("finished loop")
- assert(len(timestamps) == len(eventGrid))
- return (changeNames, builderNames, timestamps, eventGrid, sourceEvents)
-
- def phase0(self, request, sourceNames, timestamps, eventGrid):
- # phase0 rendering
- if not timestamps:
- return "no events"
- data = ""
- for r in range(0, len(timestamps)):
- data += "\n"
- data += "[%s] " % timestamps[r]
- row = eventGrid[r]
- assert(len(row) == len(sourceNames))
- for c in range(0, len(row)):
- if row[c]:
- data += "%s \n" % sourceNames[c]
- for e in row[c]:
- log.msg("Event", r, c, sourceNames[c], e.getText())
- lognames = [loog.getName() for loog in e.getLogs()]
- data += "%s: %s: %s %s " % (e.getText(),
- e.getTimes()[0],
- e.getColor(),
- lognames)
- else:
- data += "%s [none] \n" % sourceNames[c]
- return data
-
- def phase1(self, request, sourceNames, timestamps, eventGrid,
- sourceEvents):
- # phase1 rendering: table, but boxes do not overlap
- data = ""
- if not timestamps:
- return data
- lastDate = None
- for r in range(0, len(timestamps)):
- chunkstrip = eventGrid[r]
- # chunkstrip is a horizontal strip of event blocks. Each block
- # is a vertical list of events, all for the same source.
- assert(len(chunkstrip) == len(sourceNames))
- maxRows = reduce(lambda x,y: max(x,y),
- map(lambda x: len(x), chunkstrip))
- for i in range(maxRows):
- data += "
\n";
- if i == 0:
- stuff = []
- # add the date at the beginning, and each time it changes
- today = time.strftime("%d %b %Y ",
- time.localtime(timestamps[r]))
- todayday = time.strftime("%a ",
- time.localtime(timestamps[r]))
- if today != lastDate:
- stuff.append(todayday)
- stuff.append(today)
- lastDate = today
- stuff.append(
- time.strftime("%H:%M:%S",
- time.localtime(timestamps[r])))
- data += td(stuff, valign="bottom", align="center",
- rowspan=maxRows, class_="Time")
- for c in range(0, len(chunkstrip)):
- block = chunkstrip[c]
- assert(block != None) # should be [] instead
- # bottom-justify
- offset = maxRows - len(block)
- if i < offset:
- data += td("")
- else:
- e = block[i-offset]
- box = IBox(e).getBox(request)
- box.parms["show_idle"] = 1
- data += box.td(valign="top", align="center")
- data += " \n"
-
- return data
-
- def phase2(self, request, sourceNames, timestamps, eventGrid,
- sourceEvents):
- data = ""
- if not timestamps:
- return data
- # first pass: figure out the height of the chunks, populate grid
- grid = []
- for i in range(1+len(sourceNames)):
- grid.append([])
- # grid is a list of columns, one for the timestamps, and one per
- # event source. Each column is exactly the same height. Each element
- # of the list is a single box.
- lastDate = time.strftime("%d %b %Y ",
- time.localtime(util.now()))
- for r in range(0, len(timestamps)):
- chunkstrip = eventGrid[r]
- # chunkstrip is a horizontal strip of event blocks. Each block
- # is a vertical list of events, all for the same source.
- assert(len(chunkstrip) == len(sourceNames))
- maxRows = reduce(lambda x,y: max(x,y),
- map(lambda x: len(x), chunkstrip))
- for i in range(maxRows):
- if i != maxRows-1:
- grid[0].append(None)
- else:
- # timestamp goes at the bottom of the chunk
- stuff = []
- # add the date at the beginning (if it is not the same as
- # today's date), and each time it changes
- todayday = time.strftime("%a ",
- time.localtime(timestamps[r]))
- today = time.strftime("%d %b %Y ",
- time.localtime(timestamps[r]))
- if today != lastDate:
- stuff.append(todayday)
- stuff.append(today)
- lastDate = today
- stuff.append(
- time.strftime("%H:%M:%S",
- time.localtime(timestamps[r])))
- grid[0].append(Box(text=stuff, class_="Time",
- valign="bottom", align="center"))
-
- # at this point the timestamp column has been populated with
- # maxRows boxes, most None but the last one has the time string
- for c in range(0, len(chunkstrip)):
- block = chunkstrip[c]
- assert(block != None) # should be [] instead
- for i in range(maxRows - len(block)):
- # fill top of chunk with blank space
- grid[c+1].append(None)
- for i in range(len(block)):
- # so the events are bottom-justified
- b = IBox(block[i]).getBox(request)
- b.parms['valign'] = "top"
- b.parms['align'] = "center"
- grid[c+1].append(b)
- # now all the other columns have maxRows new boxes too
- # populate the last row, if empty
- gridlen = len(grid[0])
- for i in range(len(grid)):
- strip = grid[i]
- assert(len(strip) == gridlen)
- if strip[-1] == None:
- if sourceEvents[i-1]:
- filler = IBox(sourceEvents[i-1]).getBox(request)
- else:
- # this can happen if you delete part of the build history
- filler = Box(text=["?"], align="center")
- strip[-1] = filler
- strip[-1].parms['rowspan'] = 1
- # second pass: bubble the events upwards to un-occupied locations
- # Every square of the grid that has a None in it needs to have
- # something else take its place.
- noBubble = request.args.get("nobubble",['0'])
- noBubble = int(noBubble[0])
- if not noBubble:
- for col in range(len(grid)):
- strip = grid[col]
- if col == 1: # changes are handled differently
- for i in range(2, len(strip)+1):
- # only merge empty boxes. Don't bubble commit boxes.
- if strip[-i] == None:
- next = strip[-i+1]
- assert(next)
- if next:
- #if not next.event:
- if next.spacer:
- # bubble the empty box up
- strip[-i] = next
- strip[-i].parms['rowspan'] += 1
- strip[-i+1] = None
- else:
- # we are above a commit box. Leave it
- # be, and turn the current box into an
- # empty one
- strip[-i] = Box([], rowspan=1,
- comment="commit bubble")
- strip[-i].spacer = True
- else:
- # we are above another empty box, which
- # somehow wasn't already converted.
- # Shouldn't happen
- pass
- else:
- for i in range(2, len(strip)+1):
- # strip[-i] will go from next-to-last back to first
- if strip[-i] == None:
- # bubble previous item up
- assert(strip[-i+1] != None)
- strip[-i] = strip[-i+1]
- strip[-i].parms['rowspan'] += 1
- strip[-i+1] = None
- else:
- strip[-i].parms['rowspan'] = 1
- # third pass: render the HTML table
- for i in range(gridlen):
- data += " \n";
- for strip in grid:
- b = strip[i]
- if b:
- data += b.td()
- else:
- if noBubble:
- data += td([])
- # Nones are left empty, rowspan should make it all fit
- data += " \n"
- return data
-
diff --git a/tools/buildbot/pylibs/buildbot/status/web/xmlrpc.py b/tools/buildbot/pylibs/buildbot/status/web/xmlrpc.py
deleted file mode 100644
index 5794088..0000000
--- a/tools/buildbot/pylibs/buildbot/status/web/xmlrpc.py
+++ /dev/null
@@ -1,193 +0,0 @@
-
-from twisted.python import log
-from twisted.web import xmlrpc
-from buildbot.status.builder import Results
-from itertools import count
-
-class XMLRPCServer(xmlrpc.XMLRPC):
- def __init__(self):
- xmlrpc.XMLRPC.__init__(self)
-
- def render(self, req):
- # extract the IStatus and IControl objects for later use, since they
- # come from the request object. They'll be the same each time, but
- # they aren't available until the first request arrives.
- self.status = req.site.buildbot_service.getStatus()
- self.control = req.site.buildbot_service.getControl()
- return xmlrpc.XMLRPC.render(self, req)
-
- def xmlrpc_getAllBuilders(self):
- """Return a list of all builder names
- """
- log.msg("getAllBuilders")
- return self.status.getBuilderNames()
-
- def xmlrpc_getLastBuildResults(self, builder_name):
- """Return the result of the last build for the given builder
- """
- builder = self.status.getBuilder(builder_name)
- lastbuild = builder.getBuild(-1)
- return Results[lastbuild.getResults()]
-
- def xmlrpc_getLastBuilds(self, builder_name, num_builds):
- """Return the last N completed builds for the given builder.
- 'builder_name' is the name of the builder to query
- 'num_builds' is the number of builds to return
-
- Each build is returned in the same form as xmlrpc_getAllBuildsInInterval
- """
- log.msg("getLastBuilds: %s - %d" % (builder_name, num_builds))
- builder = self.status.getBuilder(builder_name)
- all_builds = []
- for build_number in range(1, num_builds+1):
- build = builder.getBuild(-build_number)
- if not build:
- break
- if not build.isFinished():
- continue
- (build_start, build_end) = build.getTimes()
-
- ss = build.getSourceStamp()
- branch = ss.branch
- if branch is None:
- branch = ""
- try:
- revision = build.getProperty("got_revision")
- except KeyError:
- revision = ""
- revision = str(revision)
-
- answer = (builder_name,
- build.getNumber(),
- build_end,
- branch,
- revision,
- Results[build.getResults()],
- build.getText(),
- )
- all_builds.append((build_end, answer))
-
- # now we've gotten all the builds we're interested in. Sort them by
- # end time.
- all_builds.sort(lambda a,b: cmp(a[0], b[0]))
- # and remove the timestamps
- all_builds = [t[1] for t in all_builds]
-
- log.msg("ready to go: %s" % (all_builds,))
-
- return all_builds
-
-
- def xmlrpc_getAllBuildsInInterval(self, start, stop):
- """Return a list of builds that have completed after the 'start'
- timestamp and before the 'stop' timestamp. This looks at all
- Builders.
-
- The timestamps are integers, interpreted as standard unix timestamps
- (seconds since epoch).
-
- Each Build is returned as a tuple in the form::
- (buildername, buildnumber, build_end, branchname, revision,
- results, text)
-
- The buildnumber is an integer. 'build_end' is an integer (seconds
- since epoch) specifying when the build finished.
-
- The branchname is a string, which may be an empty string to indicate
- None (i.e. the default branch). The revision is a string whose
- meaning is specific to the VC system in use, and comes from the
- 'got_revision' build property. The results are expressed as a string,
- one of ('success', 'warnings', 'failure', 'exception'). The text is a
- list of short strings that ought to be joined by spaces and include
- slightly more data about the results of the build.
- """
- #log.msg("start: %s %s %s" % (start, type(start), start.__class__))
- log.msg("getAllBuildsInInterval: %d - %d" % (start, stop))
- all_builds = []
-
- for builder_name in self.status.getBuilderNames():
- builder = self.status.getBuilder(builder_name)
- for build_number in count(1):
- build = builder.getBuild(-build_number)
- if not build:
- break
- if not build.isFinished():
- continue
- (build_start, build_end) = build.getTimes()
- # in reality, builds are mostly ordered by start time. For
- # the purposes of this method, we pretend that they are
- # strictly ordered by end time, so that we can stop searching
- # when we start seeing builds that are outside the window.
- if build_end > stop:
- continue # keep looking
- if build_end < start:
- break # stop looking
-
- ss = build.getSourceStamp()
- branch = ss.branch
- if branch is None:
- branch = ""
- try:
- revision = build.getProperty("got_revision")
- except KeyError:
- revision = ""
- revision = str(revision)
-
- answer = (builder_name,
- build.getNumber(),
- build_end,
- branch,
- revision,
- Results[build.getResults()],
- build.getText(),
- )
- all_builds.append((build_end, answer))
- # we've gotten all the builds that we care about from this
- # particular builder, so now we can continue on the next builder
-
- # now we've gotten all the builds we're interested in. Sort them by
- # end time.
- all_builds.sort(lambda a,b: cmp(a[0], b[0]))
- # and remove the timestamps
- all_builds = [t[1] for t in all_builds]
-
- log.msg("ready to go: %s" % (all_builds,))
-
- return all_builds
-
- def xmlrpc_getBuild(self, builder_name, build_number):
- """Return information about a specific build.
-
- """
- builder = self.status.getBuilder(builder_name)
- build = builder.getBuild(build_number)
- info = {}
- info['builder_name'] = builder.getName()
- info['url'] = self.status.getURLForThing(build)
- info['reason'] = build.getReason()
- info['slavename'] = build.getSlavename()
- info['results'] = build.getResults()
- info['text'] = build.getText()
- ss = build.getSourceStamp()
- info['start'], info['end'] = build.getTimes()
-
- info_steps = []
- for s in build.getSteps():
- stepinfo = {}
- stepinfo['name'] = s.getName()
- stepinfo['start'], stepinfo['end'] = s.getTimes()
- stepinfo['results'] = s.getResults()
- info_steps.append(stepinfo)
- info['steps'] = info_steps
-
- info_logs = []
- for l in build.getLogs():
- loginfo = {}
- loginfo['name'] = l.getStep().getName() + "/" + l.getName()
- #loginfo['text'] = l.getText()
- loginfo['text'] = "HUGE"
- info_logs.append(loginfo)
- info['logs'] = info_logs
-
- return info
-
diff --git a/tools/buildbot/pylibs/buildbot/status/words.py b/tools/buildbot/pylibs/buildbot/status/words.py
deleted file mode 100644
index a1b546c..0000000
--- a/tools/buildbot/pylibs/buildbot/status/words.py
+++ /dev/null
@@ -1,861 +0,0 @@
-
-# code to deliver build status through twisted.words (instant messaging
-# protocols: irc, etc)
-
-import re, shlex
-
-from zope.interface import Interface, implements
-from twisted.internet import protocol, reactor
-from twisted.words.protocols import irc
-from twisted.python import log, failure
-from twisted.application import internet
-
-from buildbot import interfaces, util
-from buildbot import version
-from buildbot.sourcestamp import SourceStamp
-from buildbot.process.base import BuildRequest
-from buildbot.status import base
-from buildbot.status.builder import SUCCESS, WARNINGS, FAILURE, EXCEPTION
-from buildbot.scripts.runner import ForceOptions
-
-class UsageError(ValueError):
- def __init__(self, string = "Invalid usage", *more):
- ValueError.__init__(self, string, *more)
-
-class IrcBuildRequest:
- hasStarted = False
- timer = None
-
- def __init__(self, parent):
- self.parent = parent
- self.timer = reactor.callLater(5, self.soon)
-
- def soon(self):
- del self.timer
- if not self.hasStarted:
- self.parent.send("The build has been queued, I'll give a shout"
- " when it starts")
-
- def started(self, c):
- self.hasStarted = True
- if self.timer:
- self.timer.cancel()
- del self.timer
- s = c.getStatus()
- eta = s.getETA()
- response = "build #%d forced" % s.getNumber()
- if eta is not None:
- response = "build forced [ETA %s]" % self.parent.convertTime(eta)
- self.parent.send(response)
- self.parent.send("I'll give a shout when the build finishes")
- d = s.waitUntilFinished()
- d.addCallback(self.parent.buildFinished)
-
-
-class Contact:
- """I hold the state for a single user's interaction with the buildbot.
-
- This base class provides all the basic behavior (the queries and
- responses). Subclasses for each channel type (IRC, different IM
- protocols) are expected to provide the lower-level send/receive methods.
-
- There will be one instance of me for each user who interacts personally
- with the buildbot. There will be an additional instance for each
- 'broadcast contact' (chat rooms, IRC channels as a whole).
- """
-
- def __init__(self, channel):
- self.channel = channel
- self.notify_events = {}
- self.subscribed = 0
-
- silly = {
- "What happen ?": "Somebody set up us the bomb.",
- "It's You !!": ["How are you gentlemen !!",
- "All your base are belong to us.",
- "You are on the way to destruction."],
- "What you say !!": ["You have no chance to survive make your time.",
- "HA HA HA HA ...."],
- }
-
- def getCommandMethod(self, command):
- meth = getattr(self, 'command_' + command.upper(), None)
- return meth
-
- def getBuilder(self, which):
- try:
- b = self.channel.status.getBuilder(which)
- except KeyError:
- raise UsageError, "no such builder '%s'" % which
- return b
-
- def getControl(self, which):
- if not self.channel.control:
- raise UsageError("builder control is not enabled")
- try:
- bc = self.channel.control.getBuilder(which)
- except KeyError:
- raise UsageError("no such builder '%s'" % which)
- return bc
-
- def getAllBuilders(self):
- """
- @rtype: list of L{buildbot.process.builder.Builder}
- """
- names = self.channel.status.getBuilderNames(categories=self.channel.categories)
- names.sort()
- builders = [self.channel.status.getBuilder(n) for n in names]
- return builders
-
- def convertTime(self, seconds):
- if seconds < 60:
- return "%d seconds" % seconds
- minutes = int(seconds / 60)
- seconds = seconds - 60*minutes
- if minutes < 60:
- return "%dm%02ds" % (minutes, seconds)
- hours = int(minutes / 60)
- minutes = minutes - 60*hours
- return "%dh%02dm%02ds" % (hours, minutes, seconds)
-
- def doSilly(self, message):
- response = self.silly[message]
- if type(response) != type([]):
- response = [response]
- when = 0.5
- for r in response:
- reactor.callLater(when, self.send, r)
- when += 2.5
-
- def command_HELLO(self, args, who):
- self.send("yes?")
-
- def command_VERSION(self, args, who):
- self.send("buildbot-%s at your service" % version)
-
- def command_LIST(self, args, who):
- args = args.split()
- if len(args) == 0:
- raise UsageError, "try 'list builders'"
- if args[0] == 'builders':
- builders = self.getAllBuilders()
- str = "Configured builders: "
- for b in builders:
- str += b.name
- state = b.getState()[0]
- if state == 'offline':
- str += "[offline]"
- str += " "
- str.rstrip()
- self.send(str)
- return
- command_LIST.usage = "list builders - List configured builders"
-
- def command_STATUS(self, args, who):
- args = args.split()
- if len(args) == 0:
- which = "all"
- elif len(args) == 1:
- which = args[0]
- else:
- raise UsageError, "try 'status '"
- if which == "all":
- builders = self.getAllBuilders()
- for b in builders:
- self.emit_status(b.name)
- return
- self.emit_status(which)
- command_STATUS.usage = "status [] - List status of a builder (or all builders)"
-
- def validate_notification_event(self, event):
- if not re.compile("^(started|finished|success|failed|exception)$").match(event):
- raise UsageError("try 'notify on|off '")
-
- def list_notified_events(self):
- self.send( "The following events are being notified: %r" % self.notify_events.keys() )
-
- def notify_for(self, *events):
- for event in events:
- if self.notify_events.has_key(event):
- return 1
- return 0
-
- def subscribe_to_build_events(self):
- self.channel.status.subscribe(self)
- self.subscribed = 1
-
- def unsubscribe_from_build_events(self):
- self.channel.status.unsubscribe(self)
- self.subscribed = 0
-
- def add_notification_events(self, events):
- for event in events:
- self.validate_notification_event(event)
- self.notify_events[event] = 1
-
- def remove_notification_events(self, events):
- for event in events:
- self.validate_notification_event(event)
- del self.notify_events[event]
-
- def remove_all_notification_events(self):
- self.notify_events = {}
-
- def command_NOTIFY(self, args, who):
- args = args.split()
-
- if not args:
- raise UsageError("try 'notify on|off|list '")
- action = args.pop(0)
- events = args
-
- if action == "on":
- if not events: events = ('started','finished')
- self.add_notification_events(events)
-
- self.list_notified_events()
-
- elif action == "off":
- if events:
- self.remove_notification_events(events)
- else:
- self.remove_all_notification_events()
-
- self.list_notified_events()
-
- elif action == "list":
- self.list_notified_events()
- return
-
- else:
- raise UsageError("try 'notify on|off '")
-
- if len(self.notify_events) > 0 and not self.subscribed:
- self.subscribe_to_build_events()
-
- elif len(self.notify_events) == 0 and self.subscribed:
- self.unsubscribe_from_build_events()
-
- command_NOTIFY.usage = "notify on|off|list [] ... - Notify me about build events. event should be one or more of: 'started', 'finished', 'failed', 'success', 'exception', 'successToFailed', 'failedToSuccess'"
-
- def command_WATCH(self, args, who):
- args = args.split()
- if len(args) != 1:
- raise UsageError("try 'watch '")
- which = args[0]
- b = self.getBuilder(which)
- builds = b.getCurrentBuilds()
- if not builds:
- self.send("there are no builds currently running")
- return
- for build in builds:
- assert not build.isFinished()
- d = build.waitUntilFinished()
- d.addCallback(self.watchedBuildFinished)
- r = "watching build %s #%d until it finishes" \
- % (which, build.getNumber())
- eta = build.getETA()
- if eta is not None:
- r += " [%s]" % self.convertTime(eta)
- r += ".."
- self.send(r)
- command_WATCH.usage = "watch - announce the completion of an active build"
-
- def buildsetSubmitted(self, buildset):
- log.msg('[Contact] Buildset %s added' % (buildset))
-
- def builderAdded(self, builderName, builder):
- log.msg('[Contact] Builder %s added' % (builder))
- builder.subscribe(self)
-
- def builderChangedState(self, builderName, state):
- log.msg('[Contact] Builder %s changed state to %s' % (builderName, state))
-
- def builderRemoved(self, builderName):
- log.msg('[Contact] Builder %s removed' % (builderName))
-
- def buildStarted(self, builderName, build):
- builder = build.getBuilder()
- log.msg('[Contact] Builder %r in category %s started' % (builder, builder.category))
-
- # only notify about builders we are interested in
-
- if (self.channel.categories != None and
- builder.category not in self.channel.categories):
- log.msg('Not notifying for a build in the wrong category')
- return
-
- if not self.notify_for('started'):
- log.msg('Not notifying for a build when started-notification disabled')
- return
-
- r = "build #%d of %s started" % \
- (build.getNumber(),
- builder.getName())
-
- r += " including [" + ", ".join(map(lambda c: repr(c.revision), build.getChanges())) + "]"
-
- self.send(r)
-
- def buildFinished(self, builderName, build, results):
- builder = build.getBuilder()
-
- results_descriptions = {
- SUCCESS: "Success",
- WARNINGS: "Warnings",
- FAILURE: "Failure",
- EXCEPTION: "Exception",
- }
-
- # only notify about builders we are interested in
- log.msg('[Contact] builder %r in category %s finished' % (builder, builder.category))
-
- if not self.notify_for('finished', 'failed', 'success', 'exception', 'failedToSuccess', 'successToFailed'):
- return
-
- if (self.channel.categories != None and
- builder.category not in self.channel.categories):
- return
-
- results = build.getResults()
-
- r = "build #%d of %s is complete: %s" % \
- (build.getNumber(),
- builder.getName(),
- results_descriptions.get(results, "??"))
- r += " [%s]" % " ".join(build.getText())
- buildurl = self.channel.status.getURLForThing(build)
- if buildurl:
- r += " Build details are at %s" % buildurl
-
- if (self.notify_for('finished')) or \
- (self.notify_for('success') and results == SUCCESS) or \
- (self.notify_for('failed') and results == FAILURE) or \
- (self.notify_for('exception') and results == EXCEPTION):
- self.send(r)
- return
-
- prevBuild = build.getPreviousBuild()
- if prevBuild:
- prevResult = prevBuild.getResult()
-
- if (self.notify_for('failureToSuccess') and prevResult == FAILURE and results == SUCCESS) or \
- (self.notify_for('successToFailure') and prevResult == SUCCESS and results == FAILURE):
- self.send(r)
-
- def watchedBuildFinished(self, b):
- results = {SUCCESS: "Success",
- WARNINGS: "Warnings",
- FAILURE: "Failure",
- EXCEPTION: "Exception",
- }
-
- # only notify about builders we are interested in
- builder = b.getBuilder()
- log.msg('builder %r in category %s finished' % (builder,
- builder.category))
- if (self.channel.categories != None and
- builder.category not in self.channel.categories):
- return
-
- r = "Hey! build %s #%d is complete: %s" % \
- (b.getBuilder().getName(),
- b.getNumber(),
- results.get(b.getResults(), "??"))
- r += " [%s]" % " ".join(b.getText())
- self.send(r)
- buildurl = self.channel.status.getURLForThing(b)
- if buildurl:
- self.send("Build details are at %s" % buildurl)
-
- def command_FORCE(self, args, who):
- args = shlex.split(args) # TODO: this requires python2.3 or newer
- if not args:
- raise UsageError("try 'force build WHICH '")
- what = args.pop(0)
- if what != "build":
- raise UsageError("try 'force build WHICH '")
- opts = ForceOptions()
- opts.parseOptions(args)
-
- which = opts['builder']
- branch = opts['branch']
- revision = opts['revision']
- reason = opts['reason']
-
- if which is None:
- raise UsageError("you must provide a Builder, "
- "try 'force build WHICH '")
-
- # keep weird stuff out of the branch and revision strings. TODO:
- # centralize this somewhere.
- if branch and not re.match(r'^[\w\.\-\/]*$', branch):
- log.msg("bad branch '%s'" % branch)
- self.send("sorry, bad branch '%s'" % branch)
- return
- if revision and not re.match(r'^[\w\.\-\/]*$', revision):
- log.msg("bad revision '%s'" % revision)
- self.send("sorry, bad revision '%s'" % revision)
- return
-
- bc = self.getControl(which)
-
- r = "forced: by %s: %s" % (self.describeUser(who), reason)
- # TODO: maybe give certain users the ability to request builds of
- # certain branches
- s = SourceStamp(branch=branch, revision=revision)
- req = BuildRequest(r, s, which)
- try:
- bc.requestBuildSoon(req)
- except interfaces.NoSlaveError:
- self.send("sorry, I can't force a build: all slaves are offline")
- return
- ireq = IrcBuildRequest(self)
- req.subscribe(ireq.started)
-
-
- command_FORCE.usage = "force build - Force a build"
-
- def command_STOP(self, args, who):
- args = args.split(None, 2)
- if len(args) < 3 or args[0] != 'build':
- raise UsageError, "try 'stop build WHICH '"
- which = args[1]
- reason = args[2]
-
- buildercontrol = self.getControl(which)
-
- r = "stopped: by %s: %s" % (self.describeUser(who), reason)
-
- # find an in-progress build
- builderstatus = self.getBuilder(which)
- builds = builderstatus.getCurrentBuilds()
- if not builds:
- self.send("sorry, no build is currently running")
- return
- for build in builds:
- num = build.getNumber()
-
- # obtain the BuildControl object
- buildcontrol = buildercontrol.getBuild(num)
-
- # make it stop
- buildcontrol.stopBuild(r)
-
- self.send("build %d interrupted" % num)
-
- command_STOP.usage = "stop build - Stop a running build"
-
- def emit_status(self, which):
- b = self.getBuilder(which)
- str = "%s: " % which
- state, builds = b.getState()
- str += state
- if state == "idle":
- last = b.getLastFinishedBuild()
- if last:
- start,finished = last.getTimes()
- str += ", last build %s secs ago: %s" % \
- (int(util.now() - finished), " ".join(last.getText()))
- if state == "building":
- t = []
- for build in builds:
- step = build.getCurrentStep()
- if step:
- s = "(%s)" % " ".join(step.getText())
- else:
- s = "(no current step)"
- ETA = build.getETA()
- if ETA is not None:
- s += " [ETA %s]" % self.convertTime(ETA)
- t.append(s)
- str += ", ".join(t)
- self.send(str)
-
- def emit_last(self, which):
- last = self.getBuilder(which).getLastFinishedBuild()
- if not last:
- str = "(no builds run since last restart)"
- else:
- start,finish = last.getTimes()
- str = "%s secs ago: " % (int(util.now() - finish))
- str += " ".join(last.getText())
- self.send("last build [%s]: %s" % (which, str))
-
- def command_LAST(self, args, who):
- args = args.split()
- if len(args) == 0:
- which = "all"
- elif len(args) == 1:
- which = args[0]
- else:
- raise UsageError, "try 'last '"
- if which == "all":
- builders = self.getAllBuilders()
- for b in builders:
- self.emit_last(b.name)
- return
- self.emit_last(which)
- command_LAST.usage = "last - list last build status for builder "
-
- def build_commands(self):
- commands = []
- for k in dir(self):
- if k.startswith('command_'):
- commands.append(k[8:].lower())
- commands.sort()
- return commands
-
- def command_HELP(self, args, who):
- args = args.split()
- if len(args) == 0:
- self.send("Get help on what? (try 'help ', or 'commands' for a command list)")
- return
- command = args[0]
- meth = self.getCommandMethod(command)
- if not meth:
- raise UsageError, "no such command '%s'" % command
- usage = getattr(meth, 'usage', None)
- if usage:
- self.send("Usage: %s" % usage)
- else:
- self.send("No usage info for '%s'" % command)
- command_HELP.usage = "help - Give help for "
-
- def command_SOURCE(self, args, who):
- banner = "My source can be found at http://buildbot.net/"
- self.send(banner)
-
- def command_COMMANDS(self, args, who):
- commands = self.build_commands()
- str = "buildbot commands: " + ", ".join(commands)
- self.send(str)
- command_COMMANDS.usage = "commands - List available commands"
-
- def command_DESTROY(self, args, who):
- self.act("readies phasers")
-
- def command_DANCE(self, args, who):
- reactor.callLater(1.0, self.send, "0-<")
- reactor.callLater(3.0, self.send, "0-/")
- reactor.callLater(3.5, self.send, "0-\\")
-
- def command_EXCITED(self, args, who):
- # like 'buildbot: destroy the sun!'
- self.send("What you say!")
-
- def handleAction(self, data, user):
- # this is sent when somebody performs an action that mentions the
- # buildbot (like '/me kicks buildbot'). 'user' is the name/nick/id of
- # the person who performed the action, so if their action provokes a
- # response, they can be named.
- if not data.endswith("s buildbot"):
- return
- words = data.split()
- verb = words[-2]
- timeout = 4
- if verb == "kicks":
- response = "%s back" % verb
- timeout = 1
- else:
- response = "%s %s too" % (verb, user)
- reactor.callLater(timeout, self.act, response)
-
-class IRCContact(Contact):
- # this is the IRC-specific subclass of Contact
-
- def __init__(self, channel, dest):
- Contact.__init__(self, channel)
- # when people send us public messages ("buildbot: command"),
- # self.dest is the name of the channel ("#twisted"). When they send
- # us private messages (/msg buildbot command), self.dest is their
- # username.
- self.dest = dest
-
- def describeUser(self, user):
- if self.dest[0] == "#":
- return "IRC user <%s> on channel %s" % (user, self.dest)
- return "IRC user <%s> (privmsg)" % user
-
- # userJoined(self, user, channel)
-
- def send(self, message):
- self.channel.msg(self.dest, message)
- def act(self, action):
- self.channel.me(self.dest, action)
-
- def command_JOIN(self, args, who):
- args = args.split()
- to_join = args[0]
- self.channel.join(to_join)
- self.send("Joined %s" % to_join)
- command_JOIN.usage = "join channel - Join another channel"
-
- def command_LEAVE(self, args, who):
- args = args.split()
- to_leave = args[0]
- self.send("Buildbot has been told to leave %s" % to_leave)
- self.channel.part(to_leave)
- command_LEAVE.usage = "leave channel - Leave a channel"
-
-
- def handleMessage(self, message, who):
- # a message has arrived from 'who'. For broadcast contacts (i.e. when
- # people do an irc 'buildbot: command'), this will be a string
- # describing the sender of the message in some useful-to-log way, and
- # a single Contact may see messages from a variety of users. For
- # unicast contacts (i.e. when people do an irc '/msg buildbot
- # command'), a single Contact will only ever see messages from a
- # single user.
- message = message.lstrip()
- if self.silly.has_key(message):
- return self.doSilly(message)
-
- parts = message.split(' ', 1)
- if len(parts) == 1:
- parts = parts + ['']
- cmd, args = parts
- log.msg("irc command", cmd)
-
- meth = self.getCommandMethod(cmd)
- if not meth and message[-1] == '!':
- meth = self.command_EXCITED
-
- error = None
- try:
- if meth:
- meth(args.strip(), who)
- except UsageError, e:
- self.send(str(e))
- except:
- f = failure.Failure()
- log.err(f)
- error = "Something bad happened (see logs): %s" % f.type
-
- if error:
- try:
- self.send(error)
- except:
- log.err()
-
- #self.say(channel, "count %d" % self.counter)
- self.channel.counter += 1
-
-class IChannel(Interface):
- """I represent the buildbot's presence in a particular IM scheme.
-
- This provides the connection to the IRC server, or represents the
- buildbot's account with an IM service. Each Channel will have zero or
- more Contacts associated with it.
- """
-
-class IrcStatusBot(irc.IRCClient):
- """I represent the buildbot to an IRC server.
- """
- implements(IChannel)
-
- def __init__(self, nickname, password, channels, status, categories):
- """
- @type nickname: string
- @param nickname: the nickname by which this bot should be known
- @type password: string
- @param password: the password to use for identifying with Nickserv
- @type channels: list of strings
- @param channels: the bot will maintain a presence in these channels
- @type status: L{buildbot.status.builder.Status}
- @param status: the build master's Status object, through which the
- bot retrieves all status information
- """
- self.nickname = nickname
- self.channels = channels
- self.password = password
- self.status = status
- self.categories = categories
- self.counter = 0
- self.hasQuit = 0
- self.contacts = {}
-
- def addContact(self, name, contact):
- self.contacts[name] = contact
-
- def getContact(self, name):
- if name in self.contacts:
- return self.contacts[name]
- new_contact = IRCContact(self, name)
- self.contacts[name] = new_contact
- return new_contact
-
- def deleteContact(self, contact):
- name = contact.getName()
- if name in self.contacts:
- assert self.contacts[name] == contact
- del self.contacts[name]
-
- def log(self, msg):
- log.msg("%s: %s" % (self, msg))
-
-
- # the following irc.IRCClient methods are called when we have input
-
- def privmsg(self, user, channel, message):
- user = user.split('!', 1)[0] # rest is ~user@hostname
- # channel is '#twisted' or 'buildbot' (for private messages)
- channel = channel.lower()
- #print "privmsg:", user, channel, message
- if channel == self.nickname:
- # private message
- contact = self.getContact(user)
- contact.handleMessage(message, user)
- return
- # else it's a broadcast message, maybe for us, maybe not. 'channel'
- # is '#twisted' or the like.
- contact = self.getContact(channel)
- if message.startswith("%s:" % self.nickname) or message.startswith("%s," % self.nickname):
- message = message[len("%s:" % self.nickname):]
- contact.handleMessage(message, user)
- # to track users comings and goings, add code here
-
- def action(self, user, channel, data):
- #log.msg("action: %s,%s,%s" % (user, channel, data))
- user = user.split('!', 1)[0] # rest is ~user@hostname
- # somebody did an action (/me actions) in the broadcast channel
- contact = self.getContact(channel)
- if "buildbot" in data:
- contact.handleAction(data, user)
-
-
-
- def signedOn(self):
- if self.password:
- self.msg("Nickserv", "IDENTIFY " + self.password)
- for c in self.channels:
- self.join(c)
-
- def joined(self, channel):
- self.log("I have joined %s" % (channel,))
- def left(self, channel):
- self.log("I have left %s" % (channel,))
- def kickedFrom(self, channel, kicker, message):
- self.log("I have been kicked from %s by %s: %s" % (channel,
- kicker,
- message))
-
- # we can using the following irc.IRCClient methods to send output. Most
- # of these are used by the IRCContact class.
- #
- # self.say(channel, message) # broadcast
- # self.msg(user, message) # unicast
- # self.me(channel, action) # send action
- # self.away(message='')
- # self.quit(message='')
-
-class ThrottledClientFactory(protocol.ClientFactory):
- lostDelay = 2
- failedDelay = 60
- def clientConnectionLost(self, connector, reason):
- reactor.callLater(self.lostDelay, connector.connect)
- def clientConnectionFailed(self, connector, reason):
- reactor.callLater(self.failedDelay, connector.connect)
-
-class IrcStatusFactory(ThrottledClientFactory):
- protocol = IrcStatusBot
-
- status = None
- control = None
- shuttingDown = False
- p = None
-
- def __init__(self, nickname, password, channels, categories):
- #ThrottledClientFactory.__init__(self) # doesn't exist
- self.status = None
- self.nickname = nickname
- self.password = password
- self.channels = channels
- self.categories = categories
-
- def __getstate__(self):
- d = self.__dict__.copy()
- del d['p']
- return d
-
- def shutdown(self):
- self.shuttingDown = True
- if self.p:
- self.p.quit("buildmaster reconfigured: bot disconnecting")
-
- def buildProtocol(self, address):
- p = self.protocol(self.nickname, self.password,
- self.channels, self.status,
- self.categories)
- p.factory = self
- p.status = self.status
- p.control = self.control
- self.p = p
- return p
-
- # TODO: I think a shutdown that occurs while the connection is being
- # established will make this explode
-
- def clientConnectionLost(self, connector, reason):
- if self.shuttingDown:
- log.msg("not scheduling reconnection attempt")
- return
- ThrottledClientFactory.clientConnectionLost(self, connector, reason)
-
- def clientConnectionFailed(self, connector, reason):
- if self.shuttingDown:
- log.msg("not scheduling reconnection attempt")
- return
- ThrottledClientFactory.clientConnectionFailed(self, connector, reason)
-
-
-class IRC(base.StatusReceiverMultiService):
- """I am an IRC bot which can be queried for status information. I
- connect to a single IRC server and am known by a single nickname on that
- server, however I can join multiple channels."""
-
- compare_attrs = ["host", "port", "nick", "password",
- "channels", "allowForce",
- "categories"]
-
- def __init__(self, host, nick, channels, port=6667, allowForce=True,
- categories=None, password=None):
- base.StatusReceiverMultiService.__init__(self)
-
- assert allowForce in (True, False) # TODO: implement others
-
- # need to stash these so we can detect changes later
- self.host = host
- self.port = port
- self.nick = nick
- self.channels = channels
- self.password = password
- self.allowForce = allowForce
- self.categories = categories
-
- # need to stash the factory so we can give it the status object
- self.f = IrcStatusFactory(self.nick, self.password,
- self.channels, self.categories)
-
- c = internet.TCPClient(host, port, self.f)
- c.setServiceParent(self)
-
- def setServiceParent(self, parent):
- base.StatusReceiverMultiService.setServiceParent(self, parent)
- self.f.status = parent.getStatus()
- if self.allowForce:
- self.f.control = interfaces.IControl(parent)
-
- def stopService(self):
- # make sure the factory will stop reconnecting
- self.f.shutdown()
- return base.StatusReceiverMultiService.stopService(self)
-
-
-## buildbot: list builders
-# buildbot: watch quick
-# print notification when current build in 'quick' finishes
-## buildbot: status
-## buildbot: status full-2.3
-## building, not, % complete, ETA
-## buildbot: force build full-2.3 "reason"
diff --git a/tools/buildbot/pylibs/buildbot/steps/__init__.py b/tools/buildbot/pylibs/buildbot/steps/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/tools/buildbot/pylibs/buildbot/steps/dummy.py b/tools/buildbot/pylibs/buildbot/steps/dummy.py
deleted file mode 100644
index 16338db..0000000
--- a/tools/buildbot/pylibs/buildbot/steps/dummy.py
+++ /dev/null
@@ -1,103 +0,0 @@
-
-from twisted.internet import reactor
-from buildbot.process.buildstep import BuildStep, LoggingBuildStep
-from buildbot.process.buildstep import LoggedRemoteCommand
-from buildbot.status.builder import SUCCESS, FAILURE
-
-# these classes are used internally by buildbot unit tests
-
-class Dummy(BuildStep):
- """I am a dummy no-op step, which runs entirely on the master, and simply
- waits 5 seconds before finishing with SUCCESS
- """
-
- haltOnFailure = True
- name = "dummy"
-
- def __init__(self, timeout=5, **kwargs):
- """
- @type timeout: int
- @param timeout: the number of seconds to delay before completing
- """
- BuildStep.__init__(self, **kwargs)
- self.addFactoryArguments(timeout=timeout)
- self.timeout = timeout
- self.timer = None
-
- def start(self):
- self.step_status.setColor("yellow")
- self.step_status.setText(["delay", "%s secs" % self.timeout])
- self.timer = reactor.callLater(self.timeout, self.done)
-
- def interrupt(self, reason):
- if self.timer:
- self.timer.cancel()
- self.timer = None
- self.step_status.setColor("red")
- self.step_status.setText(["delay", "interrupted"])
- self.finished(FAILURE)
-
- def done(self):
- self.step_status.setColor("green")
- self.finished(SUCCESS)
-
-class FailingDummy(Dummy):
- """I am a dummy no-op step that 'runs' master-side and finishes (with a
- FAILURE status) after 5 seconds."""
-
- name = "failing dummy"
-
- def start(self):
- self.step_status.setColor("yellow")
- self.step_status.setText(["boom", "%s secs" % self.timeout])
- self.timer = reactor.callLater(self.timeout, self.done)
-
- def done(self):
- self.step_status.setColor("red")
- self.finished(FAILURE)
-
-class RemoteDummy(LoggingBuildStep):
- """I am a dummy no-op step that runs on the remote side and
- simply waits 5 seconds before completing with success.
- See L{buildbot.slave.commands.DummyCommand}
- """
-
- haltOnFailure = True
- name = "remote dummy"
-
- def __init__(self, timeout=5, **kwargs):
- """
- @type timeout: int
- @param timeout: the number of seconds to delay
- """
- LoggingBuildStep.__init__(self, **kwargs)
- self.addFactoryArguments(timeout=timeout)
- self.timeout = timeout
- self.description = ["remote", "delay", "%s secs" % timeout]
-
- def describe(self, done=False):
- return self.description
-
- def start(self):
- args = {'timeout': self.timeout}
- cmd = LoggedRemoteCommand("dummy", args)
- self.startCommand(cmd)
-
-class Wait(LoggingBuildStep):
- """I start a command on the slave that waits for the unit test to
- tell it when to finish.
- """
-
- name = "wait"
- def __init__(self, handle, **kwargs):
- LoggingBuildStep.__init__(self, **kwargs)
- self.addFactoryArguments(handle=handle)
- self.handle = handle
-
- def describe(self, done=False):
- return ["wait: %s" % self.handle]
-
- def start(self):
- args = {'handle': (self.handle, self.build.reason)}
- cmd = LoggedRemoteCommand("dummy.wait", args)
- self.startCommand(cmd)
diff --git a/tools/buildbot/pylibs/buildbot/steps/maxq.py b/tools/buildbot/pylibs/buildbot/steps/maxq.py
deleted file mode 100644
index 2a29ed8..0000000
--- a/tools/buildbot/pylibs/buildbot/steps/maxq.py
+++ /dev/null
@@ -1,46 +0,0 @@
-from buildbot.steps.shell import ShellCommand
-from buildbot.status.builder import Event, SUCCESS, FAILURE
-
-class MaxQ(ShellCommand):
- flunkOnFailure = True
- name = "maxq"
-
- def __init__(self, testdir=None, **kwargs):
- if not testdir:
- raise TypeError("please pass testdir")
- kwargs['command'] = 'run_maxq.py %s' % (testdir,)
- ShellCommand.__init__(self, **kwargs)
- self.addFactoryArguments(testdir=testdir)
-
- def startStatus(self):
- evt = Event("yellow", ['running', 'maxq', 'tests'],
- files={'log': self.log})
- self.setCurrentActivity(evt)
-
-
- def finished(self, rc):
- self.failures = 0
- if rc:
- self.failures = 1
- output = self.log.getAll()
- self.failures += output.count('\nTEST FAILURE:')
-
- result = (SUCCESS, ['maxq'])
-
- if self.failures:
- result = (FAILURE, [str(self.failures), 'maxq', 'failures'])
-
- return self.stepComplete(result)
-
- def finishStatus(self, result):
- if self.failures:
- color = "red"
- text = ["maxq", "failed"]
- else:
- color = "green"
- text = ['maxq', 'tests']
- self.updateCurrentActivity(color=color, text=text)
- self.finishStatusSummary()
- self.finishCurrentActivity()
-
-
diff --git a/tools/buildbot/pylibs/buildbot/steps/python.py b/tools/buildbot/pylibs/buildbot/steps/python.py
deleted file mode 100644
index d6fa1bd..0000000
--- a/tools/buildbot/pylibs/buildbot/steps/python.py
+++ /dev/null
@@ -1,112 +0,0 @@
-
-from buildbot.status.builder import SUCCESS, FAILURE, WARNINGS
-from buildbot.steps.shell import ShellCommand
-
-try:
- import cStringIO
- StringIO = cStringIO.StringIO
-except ImportError:
- from StringIO import StringIO
-
-
-class BuildEPYDoc(ShellCommand):
- name = "epydoc"
- command = ["make", "epydocs"]
- description = ["building", "epydocs"]
- descriptionDone = ["epydoc"]
-
- def createSummary(self, log):
- import_errors = 0
- warnings = 0
- errors = 0
-
- for line in StringIO(log.getText()):
- if line.startswith("Error importing "):
- import_errors += 1
- if line.find("Warning: ") != -1:
- warnings += 1
- if line.find("Error: ") != -1:
- errors += 1
-
- self.descriptionDone = self.descriptionDone[:]
- if import_errors:
- self.descriptionDone.append("ierr=%d" % import_errors)
- if warnings:
- self.descriptionDone.append("warn=%d" % warnings)
- if errors:
- self.descriptionDone.append("err=%d" % errors)
-
- self.import_errors = import_errors
- self.warnings = warnings
- self.errors = errors
-
- def evaluateCommand(self, cmd):
- if cmd.rc != 0:
- return FAILURE
- if self.warnings or self.errors:
- return WARNINGS
- return SUCCESS
-
-
-class PyFlakes(ShellCommand):
- name = "pyflakes"
- command = ["make", "pyflakes"]
- description = ["running", "pyflakes"]
- descriptionDone = ["pyflakes"]
- flunkOnFailure = False
- flunkingIssues = ["undefined"] # any pyflakes lines like this cause FAILURE
-
- MESSAGES = ("unused", "undefined", "redefs", "import*", "misc")
-
- def createSummary(self, log):
- counts = {}
- summaries = {}
- for m in self.MESSAGES:
- counts[m] = 0
- summaries[m] = []
-
- first = True
- for line in StringIO(log.getText()).readlines():
- # the first few lines might contain echoed commands from a 'make
- # pyflakes' step, so don't count these as warnings. Stop ignoring
- # the initial lines as soon as we see one with a colon.
- if first:
- if line.find(":") != -1:
- # there's the colon, this is the first real line
- first = False
- # fall through and parse the line
- else:
- # skip this line, keep skipping non-colon lines
- continue
- if line.find("imported but unused") != -1:
- m = "unused"
- elif line.find("*' used; unable to detect undefined names") != -1:
- m = "import*"
- elif line.find("undefined name") != -1:
- m = "undefined"
- elif line.find("redefinition of unused") != -1:
- m = "redefs"
- else:
- m = "misc"
- summaries[m].append(line)
- counts[m] += 1
-
- self.descriptionDone = self.descriptionDone[:]
- for m in self.MESSAGES:
- if counts[m]:
- self.descriptionDone.append("%s=%d" % (m, counts[m]))
- self.addCompleteLog(m, "".join(summaries[m]))
- self.setProperty("pyflakes-%s" % m, counts[m], "pyflakes")
- self.setProperty("pyflakes-total", sum(counts.values()), "pyflakes")
-
-
- def evaluateCommand(self, cmd):
- if cmd.rc != 0:
- return FAILURE
- for m in self.flunkingIssues:
- if self.getProperty("pyflakes-%s" % m):
- return FAILURE
- if self.getProperty("pyflakes-total"):
- return WARNINGS
- return SUCCESS
-
diff --git a/tools/buildbot/pylibs/buildbot/steps/python_twisted.py b/tools/buildbot/pylibs/buildbot/steps/python_twisted.py
deleted file mode 100644
index 3edff9d..0000000
--- a/tools/buildbot/pylibs/buildbot/steps/python_twisted.py
+++ /dev/null
@@ -1,820 +0,0 @@
-# -*- test-case-name: buildbot.test.test_twisted -*-
-
-from twisted.python import log
-
-from buildbot.status import builder
-from buildbot.status.builder import SUCCESS, FAILURE, WARNINGS, SKIPPED
-from buildbot.process.buildstep import LogLineObserver, OutputProgressObserver
-from buildbot.process.buildstep import RemoteShellCommand
-from buildbot.steps.shell import ShellCommand
-
-try:
- import cStringIO
- StringIO = cStringIO
-except ImportError:
- import StringIO
-import re
-
-# BuildSteps that are specific to the Twisted source tree
-
-class HLint(ShellCommand):
- """I run a 'lint' checker over a set of .xhtml files. Any deviations
- from recommended style is flagged and put in the output log.
-
- This step looks at .changes in the parent Build to extract a list of
- Lore XHTML files to check."""
-
- name = "hlint"
- description = ["running", "hlint"]
- descriptionDone = ["hlint"]
- warnOnWarnings = True
- warnOnFailure = True
- # TODO: track time, but not output
- warnings = 0
-
- def __init__(self, python=None, **kwargs):
- ShellCommand.__init__(self, **kwargs)
- self.addFactoryArguments(python=python)
- self.python = python
-
- def start(self):
- # create the command
- htmlFiles = {}
- for f in self.build.allFiles():
- if f.endswith(".xhtml") and not f.startswith("sandbox/"):
- htmlFiles[f] = 1
- # remove duplicates
- hlintTargets = htmlFiles.keys()
- hlintTargets.sort()
- if not hlintTargets:
- return SKIPPED
- self.hlintFiles = hlintTargets
- c = []
- if self.python:
- c.append(self.python)
- c += ["bin/lore", "-p", "--output", "lint"] + self.hlintFiles
- self.setCommand(c)
-
- # add an extra log file to show the .html files we're checking
- self.addCompleteLog("files", "\n".join(self.hlintFiles)+"\n")
-
- ShellCommand.start(self)
-
- def commandComplete(self, cmd):
- # TODO: remove the 'files' file (a list of .xhtml files that were
- # submitted to hlint) because it is available in the logfile and
- # mostly exists to give the user an idea of how long the step will
- # take anyway).
- lines = cmd.logs['stdio'].getText().split("\n")
- warningLines = filter(lambda line:':' in line, lines)
- if warningLines:
- self.addCompleteLog("warnings", "".join(warningLines))
- warnings = len(warningLines)
- self.warnings = warnings
-
- def evaluateCommand(self, cmd):
- # warnings are in stdout, rc is always 0, unless the tools break
- if cmd.rc != 0:
- return FAILURE
- if self.warnings:
- return WARNINGS
- return SUCCESS
-
- def getText2(self, cmd, results):
- if cmd.rc != 0:
- return ["hlint"]
- return ["%d hlin%s" % (self.warnings,
- self.warnings == 1 and 't' or 'ts')]
-
-def countFailedTests(output):
- # start scanning 10kb from the end, because there might be a few kb of
- # import exception tracebacks between the total/time line and the errors
- # line
- chunk = output[-10000:]
- lines = chunk.split("\n")
- lines.pop() # blank line at end
- # lines[-3] is "Ran NN tests in 0.242s"
- # lines[-2] is blank
- # lines[-1] is 'OK' or 'FAILED (failures=1, errors=12)'
- # or 'FAILED (failures=1)'
- # or "PASSED (skips=N, successes=N)" (for Twisted-2.0)
- # there might be other lines dumped here. Scan all the lines.
- res = {'total': None,
- 'failures': 0,
- 'errors': 0,
- 'skips': 0,
- 'expectedFailures': 0,
- 'unexpectedSuccesses': 0,
- }
- for l in lines:
- out = re.search(r'Ran (\d+) tests', l)
- if out:
- res['total'] = int(out.group(1))
- if (l.startswith("OK") or
- l.startswith("FAILED ") or
- l.startswith("PASSED")):
- # the extra space on FAILED_ is to distinguish the overall
- # status from an individual test which failed. The lack of a
- # space on the OK is because it may be printed without any
- # additional text (if there are no skips,etc)
- out = re.search(r'failures=(\d+)', l)
- if out: res['failures'] = int(out.group(1))
- out = re.search(r'errors=(\d+)', l)
- if out: res['errors'] = int(out.group(1))
- out = re.search(r'skips=(\d+)', l)
- if out: res['skips'] = int(out.group(1))
- out = re.search(r'expectedFailures=(\d+)', l)
- if out: res['expectedFailures'] = int(out.group(1))
- out = re.search(r'unexpectedSuccesses=(\d+)', l)
- if out: res['unexpectedSuccesses'] = int(out.group(1))
- # successes= is a Twisted-2.0 addition, and is not currently used
- out = re.search(r'successes=(\d+)', l)
- if out: res['successes'] = int(out.group(1))
-
- return res
-
-
-class TrialTestCaseCounter(LogLineObserver):
- _line_re = re.compile(r'^(?:Doctest: )?([\w\.]+) \.\.\. \[([^\]]+)\]$')
- numTests = 0
- finished = False
-
- def outLineReceived(self, line):
- # different versions of Twisted emit different per-test lines with
- # the bwverbose reporter.
- # 2.0.0: testSlave (buildbot.test.test_runner.Create) ... [OK]
- # 2.1.0: buildbot.test.test_runner.Create.testSlave ... [OK]
- # 2.4.0: buildbot.test.test_runner.Create.testSlave ... [OK]
- # Let's just handle the most recent version, since it's the easiest.
- # Note that doctests create lines line this:
- # Doctest: viff.field.GF ... [OK]
-
- if self.finished:
- return
- if line.startswith("=" * 40):
- self.finished = True
- return
-
- m = self._line_re.search(line.strip())
- if m:
- testname, result = m.groups()
- self.numTests += 1
- self.step.setProgress('tests', self.numTests)
-
-
-UNSPECIFIED=() # since None is a valid choice
-
-class Trial(ShellCommand):
- """I run a unit test suite using 'trial', a unittest-like testing
- framework that comes with Twisted. Trial is used to implement Twisted's
- own unit tests, and is the unittest-framework of choice for many projects
- that use Twisted internally.
-
- Projects that use trial typically have all their test cases in a 'test'
- subdirectory of their top-level library directory. I.e. for my package
- 'petmail', the tests are in 'petmail/test/test_*.py'. More complicated
- packages (like Twisted itself) may have multiple test directories, like
- 'twisted/test/test_*.py' for the core functionality and
- 'twisted/mail/test/test_*.py' for the email-specific tests.
-
- To run trial tests, you run the 'trial' executable and tell it where the
- test cases are located. The most common way of doing this is with a
- module name. For petmail, I would run 'trial petmail.test' and it would
- locate all the test_*.py files under petmail/test/, running every test
- case it could find in them. Unlike the unittest.py that comes with
- Python, you do not run the test_foo.py as a script; you always let trial
- do the importing and running. The 'tests' parameter controls which tests
- trial will run: it can be a string or a list of strings.
-
- You can also use a higher-level module name and pass the --recursive flag
- to trial: this will search recursively within the named module to find
- all test cases. For large multiple-test-directory projects like Twisted,
- this means you can avoid specifying all the test directories explicitly.
- Something like 'trial --recursive twisted' will pick up everything.
-
- To find these test cases, you must set a PYTHONPATH that allows something
- like 'import petmail.test' to work. For packages that don't use a
- separate top-level 'lib' directory, PYTHONPATH=. will work, and will use
- the test cases (and the code they are testing) in-place.
- PYTHONPATH=build/lib or PYTHONPATH=build/lib.$ARCH are also useful when
- you do a'setup.py build' step first. The 'testpath' attribute of this
- class controls what PYTHONPATH= is set to.
-
- Trial has the ability (through the --testmodule flag) to run only the set
- of test cases named by special 'test-case-name' tags in source files. We
- can get the list of changed source files from our parent Build and
- provide them to trial, thus running the minimal set of test cases needed
- to cover the Changes. This is useful for quick builds, especially in
- trees with a lot of test cases. The 'testChanges' parameter controls this
- feature: if set, it will override 'tests'.
-
- The trial executable itself is typically just 'trial' (which is usually
- found on your $PATH as /usr/bin/trial), but it can be overridden with the
- 'trial' parameter. This is useful for Twisted's own unittests, which want
- to use the copy of bin/trial that comes with the sources. (when bin/trial
- discovers that it is living in a subdirectory named 'Twisted', it assumes
- it is being run from the source tree and adds that parent directory to
- PYTHONPATH. Therefore the canonical way to run Twisted's own unittest
- suite is './bin/trial twisted.test' rather than 'PYTHONPATH=.
- /usr/bin/trial twisted.test', especially handy when /usr/bin/trial has
- not yet been installed).
-
- To influence the version of python being used for the tests, or to add
- flags to the command, set the 'python' parameter. This can be a string
- (like 'python2.2') or a list (like ['python2.3', '-Wall']).
-
- Trial creates and switches into a directory named _trial_temp/ before
- running the tests, and sends the twisted log (which includes all
- exceptions) to a file named test.log . This file will be pulled up to
- the master where it can be seen as part of the status output.
-
- There are some class attributes which may be usefully overridden
- by subclasses. 'trialMode' and 'trialArgs' can influence the trial
- command line.
- """
-
- name = "trial"
- progressMetrics = ('output', 'tests', 'test.log')
- # note: the slash only works on unix buildslaves, of course, but we have
- # no way to know what the buildslave uses as a separator. TODO: figure
- # out something clever.
- logfiles = {"test.log": "_trial_temp/test.log"}
- # we use test.log to track Progress at the end of __init__()
-
- flunkOnFailure = True
- python = None
- trial = "trial"
- trialMode = ["--reporter=bwverbose"] # requires Twisted-2.1.0 or newer
- # for Twisted-2.0.0 or 1.3.0, use ["-o"] instead
- trialArgs = []
- testpath = UNSPECIFIED # required (but can be None)
- testChanges = False # TODO: needs better name
- recurse = False
- reactor = None
- randomly = False
- tests = None # required
-
- def __init__(self, reactor=UNSPECIFIED, python=None, trial=None,
- testpath=UNSPECIFIED,
- tests=None, testChanges=None,
- recurse=None, randomly=None,
- trialMode=None, trialArgs=None,
- **kwargs):
- """
- @type testpath: string
- @param testpath: use in PYTHONPATH when running the tests. If
- None, do not set PYTHONPATH. Setting this to '.' will
- cause the source files to be used in-place.
-
- @type python: string (without spaces) or list
- @param python: which python executable to use. Will form the start of
- the argv array that will launch trial. If you use this,
- you should set 'trial' to an explicit path (like
- /usr/bin/trial or ./bin/trial). Defaults to None, which
- leaves it out entirely (running 'trial args' instead of
- 'python ./bin/trial args'). Likely values are 'python',
- ['python2.2'], ['python', '-Wall'], etc.
-
- @type trial: string
- @param trial: which 'trial' executable to run.
- Defaults to 'trial', which will cause $PATH to be
- searched and probably find /usr/bin/trial . If you set
- 'python', this should be set to an explicit path (because
- 'python2.3 trial' will not work).
-
- @type trialMode: list of strings
- @param trialMode: a list of arguments to pass to trial, specifically
- to set the reporting mode. This defaults to ['-to']
- which means 'verbose colorless output' to the trial
- that comes with Twisted-2.0.x and at least -2.1.0 .
- Newer versions of Twisted may come with a trial
- that prefers ['--reporter=bwverbose'].
-
- @type trialArgs: list of strings
- @param trialArgs: a list of arguments to pass to trial, available to
- turn on any extra flags you like. Defaults to [].
-
- @type tests: list of strings
- @param tests: a list of test modules to run, like
- ['twisted.test.test_defer', 'twisted.test.test_process'].
- If this is a string, it will be converted into a one-item
- list.
-
- @type testChanges: boolean
- @param testChanges: if True, ignore the 'tests' parameter and instead
- ask the Build for all the files that make up the
- Changes going into this build. Pass these filenames
- to trial and ask it to look for test-case-name
- tags, running just the tests necessary to cover the
- changes.
-
- @type recurse: boolean
- @param recurse: If True, pass the --recurse option to trial, allowing
- test cases to be found in deeper subdirectories of the
- modules listed in 'tests'. This does not appear to be
- necessary when using testChanges.
-
- @type reactor: string
- @param reactor: which reactor to use, like 'gtk' or 'java'. If not
- provided, the Twisted's usual platform-dependent
- default is used.
-
- @type randomly: boolean
- @param randomly: if True, add the --random=0 argument, which instructs
- trial to run the unit tests in a random order each
- time. This occasionally catches problems that might be
- masked when one module always runs before another
- (like failing to make registerAdapter calls before
- lookups are done).
-
- @type kwargs: dict
- @param kwargs: parameters. The following parameters are inherited from
- L{ShellCommand} and may be useful to set: workdir,
- haltOnFailure, flunkOnWarnings, flunkOnFailure,
- warnOnWarnings, warnOnFailure, want_stdout, want_stderr,
- timeout.
- """
- ShellCommand.__init__(self, **kwargs)
- self.addFactoryArguments(reactor=reactor,
- python=python,
- trial=trial,
- testpath=testpath,
- tests=tests,
- testChanges=testChanges,
- recurse=recurse,
- randomly=randomly,
- trialMode=trialMode,
- trialArgs=trialArgs,
- )
-
- if python:
- self.python = python
- if self.python is not None:
- if type(self.python) is str:
- self.python = [self.python]
- for s in self.python:
- if " " in s:
- # this is not strictly an error, but I suspect more
- # people will accidentally try to use python="python2.3
- # -Wall" than will use embedded spaces in a python flag
- log.msg("python= component '%s' has spaces")
- log.msg("To add -Wall, use python=['python', '-Wall']")
- why = "python= value has spaces, probably an error"
- raise ValueError(why)
-
- if trial:
- self.trial = trial
- if " " in self.trial:
- raise ValueError("trial= value has spaces")
- if trialMode is not None:
- self.trialMode = trialMode
- if trialArgs is not None:
- self.trialArgs = trialArgs
-
- if testpath is not UNSPECIFIED:
- self.testpath = testpath
- if self.testpath is UNSPECIFIED:
- raise ValueError("You must specify testpath= (it can be None)")
- assert isinstance(self.testpath, str) or self.testpath is None
-
- if reactor is not UNSPECIFIED:
- self.reactor = reactor
-
- if tests is not None:
- self.tests = tests
- if type(self.tests) is str:
- self.tests = [self.tests]
- if testChanges is not None:
- self.testChanges = testChanges
- #self.recurse = True # not sure this is necessary
-
- if not self.testChanges and self.tests is None:
- raise ValueError("Must either set testChanges= or provide tests=")
-
- if recurse is not None:
- self.recurse = recurse
- if randomly is not None:
- self.randomly = randomly
-
- # build up most of the command, then stash it until start()
- command = []
- if self.python:
- command.extend(self.python)
- command.append(self.trial)
- command.extend(self.trialMode)
- if self.recurse:
- command.append("--recurse")
- if self.reactor:
- command.append("--reactor=%s" % reactor)
- if self.randomly:
- command.append("--random=0")
- command.extend(self.trialArgs)
- self.command = command
-
- if self.reactor:
- self.description = ["testing", "(%s)" % self.reactor]
- self.descriptionDone = ["tests"]
- # commandComplete adds (reactorname) to self.text
- else:
- self.description = ["testing"]
- self.descriptionDone = ["tests"]
-
- # this counter will feed Progress along the 'test cases' metric
- self.addLogObserver('stdio', TrialTestCaseCounter())
- # this one just measures bytes of output in _trial_temp/test.log
- self.addLogObserver('test.log', OutputProgressObserver('test.log'))
-
- def setupEnvironment(self, cmd):
- ShellCommand.setupEnvironment(self, cmd)
- if self.testpath != None:
- e = cmd.args['env']
- if e is None:
- cmd.args['env'] = {'PYTHONPATH': self.testpath}
- else:
- # TODO: somehow, each build causes another copy of
- # self.testpath to get prepended
- if e.get('PYTHONPATH', "") == "":
- e['PYTHONPATH'] = self.testpath
- else:
- e['PYTHONPATH'] = self.testpath + ":" + e['PYTHONPATH']
- try:
- p = cmd.args['env']['PYTHONPATH']
- if type(p) is not str:
- log.msg("hey, not a string:", p)
- assert False
- except (KeyError, TypeError):
- # KeyError if args doesn't have ['env']
- # KeyError if args['env'] doesn't have ['PYTHONPATH']
- # TypeError if args is None
- pass
-
- def start(self):
- # now that self.build.allFiles() is nailed down, finish building the
- # command
- if self.testChanges:
- for f in self.build.allFiles():
- if f.endswith(".py"):
- self.command.append("--testmodule=%s" % f)
- else:
- self.command.extend(self.tests)
- log.msg("Trial.start: command is", self.command)
-
- # if our slave is too old to understand logfiles=, fetch them
- # manually. This is a fallback for the Twisted buildbot and some old
- # buildslaves.
- self._needToPullTestDotLog = False
- if self.slaveVersionIsOlderThan("shell", "2.1"):
- log.msg("Trial: buildslave %s is too old to accept logfiles=" %
- self.getSlaveName())
- log.msg(" falling back to 'cat _trial_temp/test.log' instead")
- self.logfiles = {}
- self._needToPullTestDotLog = True
-
- ShellCommand.start(self)
-
-
- def commandComplete(self, cmd):
- if not self._needToPullTestDotLog:
- return self._gotTestDotLog(cmd)
-
- # if the buildslave was too old, pull test.log now
- catcmd = ["cat", "_trial_temp/test.log"]
- c2 = RemoteShellCommand(command=catcmd, workdir=self.workdir)
- loog = self.addLog("test.log")
- c2.useLog(loog, True, logfileName="stdio")
- self.cmd = c2 # to allow interrupts
- d = c2.run(self, self.remote)
- d.addCallback(lambda res: self._gotTestDotLog(cmd))
- return d
-
- def rtext(self, fmt='%s'):
- if self.reactor:
- rtext = fmt % self.reactor
- return rtext.replace("reactor", "")
- return ""
-
- def _gotTestDotLog(self, cmd):
- # figure out all status, then let the various hook functions return
- # different pieces of it
-
- # 'cmd' is the original trial command, so cmd.logs['stdio'] is the
- # trial output. We don't have access to test.log from here.
- output = cmd.logs['stdio'].getText()
- counts = countFailedTests(output)
-
- total = counts['total']
- failures, errors = counts['failures'], counts['errors']
- parsed = (total != None)
- text = []
- text2 = ""
-
- if cmd.rc == 0:
- if parsed:
- results = SUCCESS
- if total:
- text += ["%d %s" % \
- (total,
- total == 1 and "test" or "tests"),
- "passed"]
- else:
- text += ["no tests", "run"]
- else:
- results = FAILURE
- text += ["testlog", "unparseable"]
- text2 = "tests"
- else:
- # something failed
- results = FAILURE
- if parsed:
- text.append("tests")
- if failures:
- text.append("%d %s" % \
- (failures,
- failures == 1 and "failure" or "failures"))
- if errors:
- text.append("%d %s" % \
- (errors,
- errors == 1 and "error" or "errors"))
- count = failures + errors
- text2 = "%d tes%s" % (count, (count == 1 and 't' or 'ts'))
- else:
- text += ["tests", "failed"]
- text2 = "tests"
-
- if counts['skips']:
- text.append("%d %s" % \
- (counts['skips'],
- counts['skips'] == 1 and "skip" or "skips"))
- if counts['expectedFailures']:
- text.append("%d %s" % \
- (counts['expectedFailures'],
- counts['expectedFailures'] == 1 and "todo"
- or "todos"))
- if 0: # TODO
- results = WARNINGS
- if not text2:
- text2 = "todo"
-
- if 0:
- # ignore unexpectedSuccesses for now, but it should really mark
- # the build WARNING
- if counts['unexpectedSuccesses']:
- text.append("%d surprises" % counts['unexpectedSuccesses'])
- results = WARNINGS
- if not text2:
- text2 = "tests"
-
- if self.reactor:
- text.append(self.rtext('(%s)'))
- if text2:
- text2 = "%s %s" % (text2, self.rtext('(%s)'))
-
- self.results = results
- self.text = text
- self.text2 = [text2]
-
- def addTestResult(self, testname, results, text, tlog):
- if self.reactor is not None:
- testname = (self.reactor,) + testname
- tr = builder.TestResult(testname, results, text, logs={'log': tlog})
- #self.step_status.build.addTestResult(tr)
- self.build.build_status.addTestResult(tr)
-
- def createSummary(self, loog):
- output = loog.getText()
- problems = ""
- sio = StringIO.StringIO(output)
- warnings = {}
- while 1:
- line = sio.readline()
- if line == "":
- break
- if line.find(" exceptions.DeprecationWarning: ") != -1:
- # no source
- warning = line # TODO: consider stripping basedir prefix here
- warnings[warning] = warnings.get(warning, 0) + 1
- elif (line.find(" DeprecationWarning: ") != -1 or
- line.find(" UserWarning: ") != -1):
- # next line is the source
- warning = line + sio.readline()
- warnings[warning] = warnings.get(warning, 0) + 1
- elif line.find("Warning: ") != -1:
- warning = line
- warnings[warning] = warnings.get(warning, 0) + 1
-
- if line.find("=" * 60) == 0 or line.find("-" * 60) == 0:
- problems += line
- problems += sio.read()
- break
-
- if problems:
- self.addCompleteLog("problems", problems)
- # now parse the problems for per-test results
- pio = StringIO.StringIO(problems)
- pio.readline() # eat the first separator line
- testname = None
- done = False
- while not done:
- while 1:
- line = pio.readline()
- if line == "":
- done = True
- break
- if line.find("=" * 60) == 0:
- break
- if line.find("-" * 60) == 0:
- # the last case has --- as a separator before the
- # summary counts are printed
- done = True
- break
- if testname is None:
- # the first line after the === is like:
-# EXPECTED FAILURE: testLackOfTB (twisted.test.test_failure.FailureTestCase)
-# SKIPPED: testRETR (twisted.test.test_ftp.TestFTPServer)
-# FAILURE: testBatchFile (twisted.conch.test.test_sftp.TestOurServerBatchFile)
- r = re.search(r'^([^:]+): (\w+) \(([\w\.]+)\)', line)
- if not r:
- # TODO: cleanup, if there are no problems,
- # we hit here
- continue
- result, name, case = r.groups()
- testname = tuple(case.split(".") + [name])
- results = {'SKIPPED': SKIPPED,
- 'EXPECTED FAILURE': SUCCESS,
- 'UNEXPECTED SUCCESS': WARNINGS,
- 'FAILURE': FAILURE,
- 'ERROR': FAILURE,
- 'SUCCESS': SUCCESS, # not reported
- }.get(result, WARNINGS)
- text = result.lower().split()
- loog = line
- # the next line is all dashes
- loog += pio.readline()
- else:
- # the rest goes into the log
- loog += line
- if testname:
- self.addTestResult(testname, results, text, loog)
- testname = None
-
- if warnings:
- lines = warnings.keys()
- lines.sort()
- self.addCompleteLog("warnings", "".join(lines))
-
- def evaluateCommand(self, cmd):
- return self.results
-
- def getText(self, cmd, results):
- return self.text
- def getText2(self, cmd, results):
- return self.text2
-
-
-class ProcessDocs(ShellCommand):
- """I build all docs. This requires some LaTeX packages to be installed.
- It will result in the full documentation book (dvi, pdf, etc).
-
- """
-
- name = "process-docs"
- warnOnWarnings = 1
- command = ["admin/process-docs"]
- description = ["processing", "docs"]
- descriptionDone = ["docs"]
- # TODO: track output and time
-
- def __init__(self, **kwargs):
- """
- @type workdir: string
- @keyword workdir: the workdir to start from: must be the base of the
- Twisted tree
-
- @type results: triple of (int, int, string)
- @keyword results: [rc, warnings, output]
- - rc==0 if all files were converted successfully.
- - warnings is a count of hlint warnings.
- - output is the verbose output of the command.
- """
- ShellCommand.__init__(self, **kwargs)
-
- def createSummary(self, log):
- output = log.getText()
- # hlint warnings are of the format: 'WARNING: file:line:col: stuff
- # latex warnings start with "WARNING: LaTeX Warning: stuff", but
- # sometimes wrap around to a second line.
- lines = output.split("\n")
- warningLines = []
- wantNext = False
- for line in lines:
- wantThis = wantNext
- wantNext = False
- if line.startswith("WARNING: "):
- wantThis = True
- wantNext = True
- if wantThis:
- warningLines.append(line)
-
- if warningLines:
- self.addCompleteLog("warnings", "\n".join(warningLines) + "\n")
- self.warnings = len(warningLines)
-
- def evaluateCommand(self, cmd):
- if cmd.rc != 0:
- return FAILURE
- if self.warnings:
- return WARNINGS
- return SUCCESS
-
- def getText(self, cmd, results):
- if results == SUCCESS:
- return ["docs", "successful"]
- if results == WARNINGS:
- return ["docs",
- "%d warnin%s" % (self.warnings,
- self.warnings == 1 and 'g' or 'gs')]
- if results == FAILURE:
- return ["docs", "failed"]
-
- def getText2(self, cmd, results):
- if results == WARNINGS:
- return ["%d do%s" % (self.warnings,
- self.warnings == 1 and 'c' or 'cs')]
- return ["docs"]
-
-
-
-class BuildDebs(ShellCommand):
- """I build the .deb packages."""
-
- name = "debuild"
- flunkOnFailure = 1
- command = ["debuild", "-uc", "-us"]
- description = ["building", "debs"]
- descriptionDone = ["debs"]
-
- def __init__(self, **kwargs):
- """
- @type workdir: string
- @keyword workdir: the workdir to start from (must be the base of the
- Twisted tree)
- @type results: double of [int, string]
- @keyword results: [rc, output].
- - rc == 0 if all .debs were created successfully
- - output: string with any errors or warnings
- """
- ShellCommand.__init__(self, **kwargs)
-
- def commandComplete(self, cmd):
- errors, warnings = 0, 0
- output = cmd.logs['stdio'].getText()
- summary = ""
- sio = StringIO.StringIO(output)
- for line in sio.readlines():
- if line.find("E: ") == 0:
- summary += line
- errors += 1
- if line.find("W: ") == 0:
- summary += line
- warnings += 1
- if summary:
- self.addCompleteLog("problems", summary)
- self.errors = errors
- self.warnings = warnings
-
- def evaluateCommand(self, cmd):
- if cmd.rc != 0:
- return FAILURE
- if self.errors:
- return FAILURE
- if self.warnings:
- return WARNINGS
- return SUCCESS
-
- def getText(self, cmd, results):
- text = ["debuild"]
- if cmd.rc != 0:
- text.append("failed")
- errors, warnings = self.errors, self.warnings
- if warnings or errors:
- text.append("lintian:")
- if warnings:
- text.append("%d warnin%s" % (warnings,
- warnings == 1 and 'g' or 'gs'))
- if errors:
- text.append("%d erro%s" % (errors,
- errors == 1 and 'r' or 'rs'))
- return text
-
- def getText2(self, cmd, results):
- if cmd.rc != 0:
- return ["debuild"]
- if self.errors or self.warnings:
- return ["%d lintian" % (self.errors + self.warnings)]
- return []
-
-class RemovePYCs(ShellCommand):
- name = "remove-.pyc"
- command = 'find . -name "*.pyc" | xargs rm'
- description = ["removing", ".pyc", "files"]
- descriptionDone = ["remove", ".pycs"]
diff --git a/tools/buildbot/pylibs/buildbot/steps/shell.py b/tools/buildbot/pylibs/buildbot/steps/shell.py
deleted file mode 100644
index 2108d77..0000000
--- a/tools/buildbot/pylibs/buildbot/steps/shell.py
+++ /dev/null
@@ -1,444 +0,0 @@
-# -*- 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,
- **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.command = 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'
- 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 self.descriptionDone
- if self.description is not None:
- return 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):
- # XXX is this used? documented? replaced by properties?
- # merge in anything from Build.slaveEnvironment . Earlier steps
- # (perhaps ones which compile libraries or sub-projects that need to
- # be referenced by later steps) can add keys to
- # self.build.slaveEnvironment to affect later steps.
- properties = self.build.getProperties()
- slaveEnv = self.build.slaveEnvironment
- if slaveEnv:
- if cmd.args['env'] is None:
- cmd.args['env'] = {}
- cmd.args['env'].update(properties.render(slaveEnv))
- # 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()
-
- # create the actual RemoteShellCommand instance now
- kwargs = properties.render(self.remote_kwargs)
- kwargs['command'] = properties.render(self.command)
- kwargs['logfiles'] = self.logfiles
- cmd = RemoteShellCommand(**kwargs)
- self.setupEnvironment(cmd)
- self.checkForOldSlaveAndLogfiles()
-
- self.startCommand(cmd)
-
-
-
-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
- description = ["configuring"]
- descriptionDone = ["configure"]
- command = ["./configure"]
-
-class WarningCountingShellCommand(ShellCommand):
- warnCount = 0
- warningPattern = '.*warning[: ].*'
-
- def __init__(self, **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.
- wp = None
- if kwargs.has_key('warningPattern'):
- wp = kwargs['warningPattern']
- del kwargs['warningPattern']
- self.warningPattern = wp
-
- # And upcall to let the base class do its work
- ShellCommand.__init__(self, **kwargs)
-
- if wp:
- self.addFactoryArguments(warningPattern=wp)
-
- 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
- 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, cmd):
- # TODO: grep for the characteristic GCC error lines and
- # assemble them into a pair of buffers
- WarningCountingShellCommand.createSummary(self, cmd)
- pass
-
-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 getText(self, cmd, results):
- text = WarningCountingShellCommand.getText(self, cmd, results)
- 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:
- text.append('%d tests' % total)
- if passed:
- text.append('%d passed' % passed)
- if warnings:
- text.append('%d warnings' % warnings)
- if failed:
- text.append('%d failed' % failed)
- return text
-
-class PerlModuleTest(Test):
- command=["prove", "--lib", "lib", "-r", "t"]
- total = 0
-
- def evaluateCommand(self, cmd):
- lines = self.getLog('stdio').readlines()
-
- 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 not test_result_lines:
- return cmd.rc
-
- 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)
- passed = total - failed
-
- self.setTestResults(total=total, failed=failed, passed=passed)
-
- return rc
diff --git a/tools/buildbot/pylibs/buildbot/steps/source.py b/tools/buildbot/pylibs/buildbot/steps/source.py
deleted file mode 100644
index 62a594c..0000000
--- a/tools/buildbot/pylibs/buildbot/steps/source.py
+++ /dev/null
@@ -1,1074 +0,0 @@
-# -*- test-case-name: buildbot.test.test_vc -*-
-
-from warnings import warn
-from email.Utils import formatdate
-from twisted.python import log
-from buildbot.process.buildstep import LoggingBuildStep, LoggedRemoteCommand
-from buildbot.interfaces import BuildSlaveTooOldError
-from buildbot.status.builder import SKIPPED
-
-
-class Source(LoggingBuildStep):
- """This is a base class to generate a source tree in the buildslave.
- Each version control system has a specialized subclass, and is expected
- to override __init__ and implement computeSourceRevision() and
- startVC(). The class as a whole builds up the self.args dictionary, then
- starts a LoggedRemoteCommand with those arguments.
- """
-
- # if the checkout fails, there's no point in doing anything else
- haltOnFailure = True
- notReally = False
-
- branch = None # the default branch, should be set in __init__
-
- def __init__(self, workdir=None, mode='update', alwaysUseLatest=False,
- timeout=20*60, retry=None, **kwargs):
- """
- @type workdir: string
- @param workdir: local directory (relative to the Builder's root)
- where the tree should be placed
-
- @type mode: string
- @param mode: the kind of VC operation that is desired:
- - 'update': specifies that the checkout/update should be
- performed directly into the workdir. Each build is performed
- in the same directory, allowing for incremental builds. This
- minimizes disk space, bandwidth, and CPU time. However, it
- may encounter problems if the build process does not handle
- dependencies properly (if you must sometimes do a 'clean
- build' to make sure everything gets compiled), or if source
- files are deleted but generated files can influence test
- behavior (e.g. python's .pyc files), or when source
- directories are deleted but generated files prevent CVS from
- removing them.
-
- - 'copy': specifies that the source-controlled workspace
- should be maintained in a separate directory (called the
- 'copydir'), using checkout or update as necessary. For each
- build, a new workdir is created with a copy of the source
- tree (rm -rf workdir; cp -R -P -p copydir workdir). This
- doubles the disk space required, but keeps the bandwidth low
- (update instead of a full checkout). A full 'clean' build
- is performed each time. This avoids any generated-file
- build problems, but is still occasionally vulnerable to
- problems such as a CVS repository being manually rearranged
- (causing CVS errors on update) which are not an issue with
- a full checkout.
-
- - 'clobber': specifies that the working directory should be
- deleted each time, necessitating a full checkout for each
- build. This insures a clean build off a complete checkout,
- avoiding any of the problems described above, but is
- bandwidth intensive, as the whole source tree must be
- pulled down for each build.
-
- - 'export': is like 'clobber', except that e.g. the 'cvs
- export' command is used to create the working directory.
- This command removes all VC metadata files (the
- CVS/.svn/{arch} directories) from the tree, which is
- sometimes useful for creating source tarballs (to avoid
- including the metadata in the tar file). Not all VC systems
- support export.
-
- @type alwaysUseLatest: boolean
- @param alwaysUseLatest: whether to always update to the most
- recent available sources for this build.
-
- Normally the Source step asks its Build for a list of all
- Changes that are supposed to go into the build, then computes a
- 'source stamp' (revision number or timestamp) that will cause
- exactly that set of changes to be present in the checked out
- tree. This is turned into, e.g., 'cvs update -D timestamp', or
- 'svn update -r revnum'. If alwaysUseLatest=True, bypass this
- computation and always update to the latest available sources
- for each build.
-
- The source stamp helps avoid a race condition in which someone
- commits a change after the master has decided to start a build
- but before the slave finishes checking out the sources. At best
- this results in a build which contains more changes than the
- buildmaster thinks it has (possibly resulting in the wrong
- person taking the blame for any problems that result), at worst
- is can result in an incoherent set of sources (splitting a
- non-atomic commit) which may not build at all.
-
- @type retry: tuple of ints (delay, repeats) (or None)
- @param retry: if provided, VC update failures are re-attempted up
- to REPEATS times, with DELAY seconds between each
- attempt. Some users have slaves with poor connectivity
- to their VC repository, and they say that up to 80% of
- their build failures are due to transient network
- failures that could be handled by simply retrying a
- couple times.
-
- """
-
- LoggingBuildStep.__init__(self, **kwargs)
- self.addFactoryArguments(workdir=workdir,
- mode=mode,
- alwaysUseLatest=alwaysUseLatest,
- timeout=timeout,
- retry=retry,
- )
-
- assert mode in ("update", "copy", "clobber", "export")
- if retry:
- delay, repeats = retry
- assert isinstance(repeats, int)
- assert repeats > 0
- self.args = {'mode': mode,
- 'workdir': workdir,
- 'timeout': timeout,
- 'retry': retry,
- 'patch': None, # set during .start
- }
- self.alwaysUseLatest = alwaysUseLatest
-
- # Compute defaults for descriptions:
- description = ["updating"]
- descriptionDone = ["update"]
- if mode == "clobber":
- description = ["checkout"]
- # because checkingouting takes too much space
- descriptionDone = ["checkout"]
- elif mode == "export":
- description = ["exporting"]
- descriptionDone = ["export"]
- self.description = description
- self.descriptionDone = descriptionDone
-
- def setDefaultWorkdir(self, workdir):
- self.args['workdir'] = self.args['workdir'] or workdir
-
- def describe(self, done=False):
- if done:
- return self.descriptionDone
- return self.description
-
- def computeSourceRevision(self, changes):
- """Each subclass must implement this method to do something more
- precise than -rHEAD every time. For version control systems that use
- repository-wide change numbers (SVN, P4), this can simply take the
- maximum such number from all the changes involved in this build. For
- systems that do not (CVS), it needs to create a timestamp based upon
- the latest Change, the Build's treeStableTimer, and an optional
- self.checkoutDelay value."""
- return None
-
- def start(self):
- if self.notReally:
- log.msg("faking %s checkout/update" % self.name)
- self.step_status.setColor("green")
- self.step_status.setText(["fake", self.name, "successful"])
- self.addCompleteLog("log",
- "Faked %s checkout/update 'successful'\n" \
- % self.name)
- return SKIPPED
-
- # what source stamp would this build like to use?
- s = self.build.getSourceStamp()
- # if branch is None, then use the Step's "default" branch
- branch = s.branch or self.branch
- # if revision is None, use the latest sources (-rHEAD)
- revision = s.revision
- if not revision and not self.alwaysUseLatest:
- revision = self.computeSourceRevision(s.changes)
- # if patch is None, then do not patch the tree after checkout
-
- # 'patch' is None or a tuple of (patchlevel, diff)
- patch = s.patch
- if patch:
- self.addCompleteLog("patch", patch[1])
-
- self.startVC(branch, revision, patch)
-
- def commandComplete(self, cmd):
- got_revision = None
- if cmd.updates.has_key("got_revision"):
- got_revision = str(cmd.updates["got_revision"][-1])
- self.setProperty("got_revision", got_revision, "Source")
-
-
-
-class CVS(Source):
- """I do CVS checkout/update operations.
-
- Note: if you are doing anonymous/pserver CVS operations, you will need
- to manually do a 'cvs login' on each buildslave before the slave has any
- hope of success. XXX: fix then, take a cvs password as an argument and
- figure out how to do a 'cvs login' on each build
- """
-
- name = "cvs"
-
- #progressMetrics = ('output',)
- #
- # additional things to track: update gives one stderr line per directory
- # (starting with 'cvs server: Updating ') (and is fairly stable if files
- # is empty), export gives one line per directory (starting with 'cvs
- # export: Updating ') and another line per file (starting with U). Would
- # be nice to track these, requires grepping LogFile data for lines,
- # parsing each line. Might be handy to have a hook in LogFile that gets
- # called with each complete line.
-
- def __init__(self, cvsroot, cvsmodule,
- global_options=[], branch=None, checkoutDelay=None,
- login=None,
- **kwargs):
-
- """
- @type cvsroot: string
- @param cvsroot: CVS Repository from which the source tree should
- be obtained. '/home/warner/Repository' for local
- or NFS-reachable repositories,
- ':pserver:anon@foo.com:/cvs' for anonymous CVS,
- 'user@host.com:/cvs' for non-anonymous CVS or
- CVS over ssh. Lots of possibilities, check the
- CVS documentation for more.
-
- @type cvsmodule: string
- @param cvsmodule: subdirectory of CVS repository that should be
- retrieved
-
- @type login: string or None
- @param login: if not None, a string which will be provided as a
- password to the 'cvs login' command, used when a
- :pserver: method is used to access the repository.
- This login is only needed once, but must be run
- each time (just before the CVS operation) because
- there is no way for the buildslave to tell whether
- it was previously performed or not.
-
- @type branch: string
- @param branch: the default branch name, will be used in a '-r'
- argument to specify which branch of the source tree
- should be used for this checkout. Defaults to None,
- which means to use 'HEAD'.
-
- @type checkoutDelay: int or None
- @param checkoutDelay: if not None, the number of seconds to put
- between the last known Change and the
- timestamp given to the -D argument. This
- defaults to exactly half of the parent
- Build's .treeStableTimer, but it could be
- set to something else if your CVS change
- notification has particularly weird
- latency characteristics.
-
- @type global_options: list of strings
- @param global_options: these arguments are inserted in the cvs
- command line, before the
- 'checkout'/'update' command word. See
- 'cvs --help-options' for a list of what
- may be accepted here. ['-r'] will make
- the checked out files read only. ['-r',
- '-R'] will also assume the repository is
- read-only (I assume this means it won't
- use locks to insure atomic access to the
- ,v files)."""
-
- self.checkoutDelay = checkoutDelay
- self.branch = branch
-
- Source.__init__(self, **kwargs)
- self.addFactoryArguments(cvsroot=cvsroot,
- cvsmodule=cvsmodule,
- global_options=global_options,
- branch=branch,
- checkoutDelay=checkoutDelay,
- login=login,
- )
-
- self.args.update({'cvsroot': cvsroot,
- 'cvsmodule': cvsmodule,
- 'global_options': global_options,
- 'login': login,
- })
-
- def computeSourceRevision(self, changes):
- if not changes:
- return None
- lastChange = max([c.when for c in changes])
- if self.checkoutDelay is not None:
- when = lastChange + self.checkoutDelay
- else:
- lastSubmit = max([r.submittedAt for r in self.build.requests])
- when = (lastChange + lastSubmit) / 2
- return formatdate(when)
-
- def startVC(self, branch, revision, patch):
- if self.slaveVersionIsOlderThan("cvs", "1.39"):
- # the slave doesn't know to avoid re-using the same sourcedir
- # when the branch changes. We have no way of knowing which branch
- # the last build used, so if we're using a non-default branch and
- # either 'update' or 'copy' modes, it is safer to refuse to
- # build, and tell the user they need to upgrade the buildslave.
- if (branch != self.branch
- and self.args['mode'] in ("update", "copy")):
- m = ("This buildslave (%s) does not know about multiple "
- "branches, and using mode=%s would probably build the "
- "wrong tree. "
- "Refusing to build. Please upgrade the buildslave to "
- "buildbot-0.7.0 or newer." % (self.build.slavename,
- self.args['mode']))
- log.msg(m)
- raise BuildSlaveTooOldError(m)
-
- if branch is None:
- branch = "HEAD"
- self.args['branch'] = branch
- self.args['revision'] = revision
- self.args['patch'] = patch
-
- if self.args['branch'] == "HEAD" and self.args['revision']:
- # special case. 'cvs update -r HEAD -D today' gives no files
- # TODO: figure out why, see if it applies to -r BRANCH
- self.args['branch'] = None
-
- # deal with old slaves
- warnings = []
- slavever = self.slaveVersion("cvs", "old")
-
- if slavever == "old":
- # 0.5.0
- if self.args['mode'] == "export":
- self.args['export'] = 1
- elif self.args['mode'] == "clobber":
- self.args['clobber'] = 1
- elif self.args['mode'] == "copy":
- self.args['copydir'] = "source"
- self.args['tag'] = self.args['branch']
- assert not self.args['patch'] # 0.5.0 slave can't do patch
-
- cmd = LoggedRemoteCommand("cvs", self.args)
- self.startCommand(cmd, warnings)
-
-
-class SVN(Source):
- """I perform Subversion checkout/update operations."""
-
- name = 'svn'
-
- def __init__(self, svnurl=None, baseURL=None, defaultBranch=None,
- directory=None, **kwargs):
- """
- @type svnurl: string
- @param svnurl: the URL which points to the Subversion server,
- combining the access method (HTTP, ssh, local file),
- the repository host/port, the repository path, the
- sub-tree within the repository, and the branch to
- check out. Using C{svnurl} does not enable builds of
- alternate branches: use C{baseURL} to enable this.
- Use exactly one of C{svnurl} and C{baseURL}.
-
- @param baseURL: if branches are enabled, this is the base URL to
- which a branch name will be appended. It should
- probably end in a slash. Use exactly one of
- C{svnurl} and C{baseURL}.
-
- @param defaultBranch: if branches are enabled, this is the branch
- to use if the Build does not specify one
- explicitly. It will simply be appended
- to C{baseURL} and the result handed to
- the SVN command.
- """
-
- if not kwargs.has_key('workdir') and directory is not None:
- # deal with old configs
- warn("Please use workdir=, not directory=", DeprecationWarning)
- kwargs['workdir'] = directory
-
- self.svnurl = svnurl
- self.baseURL = baseURL
- self.branch = defaultBranch
-
- Source.__init__(self, **kwargs)
- self.addFactoryArguments(svnurl=svnurl,
- baseURL=baseURL,
- defaultBranch=defaultBranch,
- directory=directory,
- )
-
- if not svnurl and not baseURL:
- raise ValueError("you must use exactly one of svnurl and baseURL")
-
-
- def computeSourceRevision(self, changes):
- if not changes:
- return None
- lastChange = max([int(c.revision) for c in changes])
- return lastChange
-
- def startVC(self, branch, revision, patch):
-
- # handle old slaves
- warnings = []
- slavever = self.slaveVersion("svn", "old")
- if not slavever:
- m = "slave does not have the 'svn' command"
- raise BuildSlaveTooOldError(m)
-
- if self.slaveVersionIsOlderThan("svn", "1.39"):
- # the slave doesn't know to avoid re-using the same sourcedir
- # when the branch changes. We have no way of knowing which branch
- # the last build used, so if we're using a non-default branch and
- # either 'update' or 'copy' modes, it is safer to refuse to
- # build, and tell the user they need to upgrade the buildslave.
- if (branch != self.branch
- and self.args['mode'] in ("update", "copy")):
- m = ("This buildslave (%s) does not know about multiple "
- "branches, and using mode=%s would probably build the "
- "wrong tree. "
- "Refusing to build. Please upgrade the buildslave to "
- "buildbot-0.7.0 or newer." % (self.build.slavename,
- self.args['mode']))
- raise BuildSlaveTooOldError(m)
-
- if slavever == "old":
- # 0.5.0 compatibility
- if self.args['mode'] in ("clobber", "copy"):
- # TODO: use some shell commands to make up for the
- # deficiency, by blowing away the old directory first (thus
- # forcing a full checkout)
- warnings.append("WARNING: this slave can only do SVN updates"
- ", not mode=%s\n" % self.args['mode'])
- log.msg("WARNING: this slave only does mode=update")
- if self.args['mode'] == "export":
- raise BuildSlaveTooOldError("old slave does not have "
- "mode=export")
- self.args['directory'] = self.args['workdir']
- if revision is not None:
- # 0.5.0 can only do HEAD. We have no way of knowing whether
- # the requested revision is HEAD or not, and for
- # slowly-changing trees this will probably do the right
- # thing, so let it pass with a warning
- m = ("WARNING: old slave can only update to HEAD, not "
- "revision=%s" % revision)
- log.msg(m)
- warnings.append(m + "\n")
- revision = "HEAD" # interprets this key differently
- if patch:
- raise BuildSlaveTooOldError("old slave can't do patch")
-
- if self.svnurl:
- assert not branch # we need baseURL= to use branches
- self.args['svnurl'] = self.svnurl
- else:
- self.args['svnurl'] = self.baseURL + branch
- self.args['revision'] = revision
- self.args['patch'] = patch
-
- revstuff = []
- if branch is not None and branch != self.branch:
- revstuff.append("[branch]")
- if revision is not None:
- revstuff.append("r%s" % revision)
- if patch is not None:
- revstuff.append("[patch]")
- self.description.extend(revstuff)
- self.descriptionDone.extend(revstuff)
-
- cmd = LoggedRemoteCommand("svn", self.args)
- self.startCommand(cmd, warnings)
-
-
-class Darcs(Source):
- """Check out a source tree from a Darcs repository at 'repourl'.
-
- To the best of my knowledge, Darcs has no concept of file modes. This
- means the eXecute-bit will be cleared on all source files. As a result,
- you may need to invoke configuration scripts with something like:
-
- C{s(step.Configure, command=['/bin/sh', './configure'])}
- """
-
- name = "darcs"
-
- def __init__(self, repourl=None, baseURL=None, defaultBranch=None,
- **kwargs):
- """
- @type repourl: string
- @param repourl: the URL which points at the Darcs repository. This
- is used as the default branch. Using C{repourl} does
- not enable builds of alternate branches: use
- C{baseURL} to enable this. Use either C{repourl} or
- C{baseURL}, not both.
-
- @param baseURL: if branches are enabled, this is the base URL to
- which a branch name will be appended. It should
- probably end in a slash. Use exactly one of
- C{repourl} and C{baseURL}.
-
- @param defaultBranch: if branches are enabled, this is the branch
- to use if the Build does not specify one
- explicitly. It will simply be appended to
- C{baseURL} and the result handed to the
- 'darcs pull' command.
- """
- self.repourl = repourl
- self.baseURL = baseURL
- self.branch = defaultBranch
- Source.__init__(self, **kwargs)
- self.addFactoryArguments(repourl=repourl,
- baseURL=baseURL,
- defaultBranch=defaultBranch,
- )
- assert self.args['mode'] != "export", \
- "Darcs does not have an 'export' mode"
- if (not repourl and not baseURL) or (repourl and baseURL):
- raise ValueError("you must provide exactly one of repourl and"
- " baseURL")
-
- def startVC(self, branch, revision, patch):
- slavever = self.slaveVersion("darcs")
- if not slavever:
- m = "slave is too old, does not know about darcs"
- raise BuildSlaveTooOldError(m)
-
- if self.slaveVersionIsOlderThan("darcs", "1.39"):
- if revision:
- # TODO: revisit this once we implement computeSourceRevision
- m = "0.6.6 slaves can't handle args['revision']"
- raise BuildSlaveTooOldError(m)
-
- # the slave doesn't know to avoid re-using the same sourcedir
- # when the branch changes. We have no way of knowing which branch
- # the last build used, so if we're using a non-default branch and
- # either 'update' or 'copy' modes, it is safer to refuse to
- # build, and tell the user they need to upgrade the buildslave.
- if (branch != self.branch
- and self.args['mode'] in ("update", "copy")):
- m = ("This buildslave (%s) does not know about multiple "
- "branches, and using mode=%s would probably build the "
- "wrong tree. "
- "Refusing to build. Please upgrade the buildslave to "
- "buildbot-0.7.0 or newer." % (self.build.slavename,
- self.args['mode']))
- raise BuildSlaveTooOldError(m)
-
- if self.repourl:
- assert not branch # we need baseURL= to use branches
- self.args['repourl'] = self.repourl
- else:
- self.args['repourl'] = self.baseURL + branch
- self.args['revision'] = revision
- self.args['patch'] = patch
-
- revstuff = []
- if branch is not None and branch != self.branch:
- revstuff.append("[branch]")
- self.description.extend(revstuff)
- self.descriptionDone.extend(revstuff)
-
- cmd = LoggedRemoteCommand("darcs", self.args)
- self.startCommand(cmd)
-
-
-class Git(Source):
- """Check out a source tree from a git repository 'repourl'."""
-
- name = "git"
-
- def __init__(self, repourl, branch="master", **kwargs):
- """
- @type repourl: string
- @param repourl: the URL which points at the git repository
-
- @type branch: string
- @param branch: The branch or tag to check out by default. If
- a build specifies a different branch, it will
- be used instead of this.
- """
- Source.__init__(self, **kwargs)
- self.addFactoryArguments(repourl=repourl, branch=branch)
- self.args.update({'repourl': repourl,
- 'branch': branch})
-
- def computeSourceRevision(self, changes):
- if not changes:
- return None
- return changes[-1].revision
-
- def startVC(self, branch, revision, patch):
- self.args['branch'] = branch
- self.args['revision'] = revision
- self.args['patch'] = patch
- slavever = self.slaveVersion("git")
- if not slavever:
- raise BuildSlaveTooOldError("slave is too old, does not know "
- "about git")
- cmd = LoggedRemoteCommand("git", self.args)
- self.startCommand(cmd)
-
-
-class Arch(Source):
- """Check out a source tree from an Arch repository named 'archive'
- available at 'url'. 'version' specifies which version number (development
- line) will be used for the checkout: this is mostly equivalent to a
- branch name. This version uses the 'tla' tool to do the checkout, to use
- 'baz' see L{Bazaar} instead.
- """
-
- name = "arch"
- # TODO: slaves >0.6.6 will accept args['build-config'], so use it
-
- def __init__(self, url, version, archive=None, **kwargs):
- """
- @type url: string
- @param url: the Arch coordinates of the repository. This is
- typically an http:// URL, but could also be the absolute
- pathname of a local directory instead.
-
- @type version: string
- @param version: the category--branch--version to check out. This is
- the default branch. If a build specifies a different
- branch, it will be used instead of this.
-
- @type archive: string
- @param archive: The archive name. If provided, it must match the one
- that comes from the repository. If not, the
- repository's default will be used.
- """
- self.branch = version
- Source.__init__(self, **kwargs)
- self.addFactoryArguments(url=url,
- version=version,
- archive=archive,
- )
- self.args.update({'url': url,
- 'archive': archive,
- })
-
- def computeSourceRevision(self, changes):
- # in Arch, fully-qualified revision numbers look like:
- # arch@buildbot.sourceforge.net--2004/buildbot--dev--0--patch-104
- # For any given builder, all of this is fixed except the patch-104.
- # The Change might have any part of the fully-qualified string, so we
- # just look for the last part. We return the "patch-NN" string.
- if not changes:
- return None
- lastChange = None
- for c in changes:
- if not c.revision:
- continue
- if c.revision.endswith("--base-0"):
- rev = 0
- else:
- i = c.revision.rindex("patch")
- rev = int(c.revision[i+len("patch-"):])
- lastChange = max(lastChange, rev)
- if lastChange is None:
- return None
- if lastChange == 0:
- return "base-0"
- return "patch-%d" % lastChange
-
- def checkSlaveVersion(self, cmd, branch):
- warnings = []
- slavever = self.slaveVersion(cmd)
- if not slavever:
- m = "slave is too old, does not know about %s" % cmd
- raise BuildSlaveTooOldError(m)
-
- # slave 1.28 and later understand 'revision'
- if self.slaveVersionIsOlderThan(cmd, "1.28"):
- if not self.alwaysUseLatest:
- # we don't know whether our requested revision is the latest
- # or not. If the tree does not change very quickly, this will
- # probably build the right thing, so emit a warning rather
- # than refuse to build at all
- m = "WARNING, buildslave is too old to use a revision"
- log.msg(m)
- warnings.append(m + "\n")
-
- if self.slaveVersionIsOlderThan(cmd, "1.39"):
- # the slave doesn't know to avoid re-using the same sourcedir
- # when the branch changes. We have no way of knowing which branch
- # the last build used, so if we're using a non-default branch and
- # either 'update' or 'copy' modes, it is safer to refuse to
- # build, and tell the user they need to upgrade the buildslave.
- if (branch != self.branch
- and self.args['mode'] in ("update", "copy")):
- m = ("This buildslave (%s) does not know about multiple "
- "branches, and using mode=%s would probably build the "
- "wrong tree. "
- "Refusing to build. Please upgrade the buildslave to "
- "buildbot-0.7.0 or newer." % (self.build.slavename,
- self.args['mode']))
- log.msg(m)
- raise BuildSlaveTooOldError(m)
-
- return warnings
-
- def startVC(self, branch, revision, patch):
- self.args['version'] = branch
- self.args['revision'] = revision
- self.args['patch'] = patch
- warnings = self.checkSlaveVersion("arch", branch)
-
- revstuff = []
- if branch is not None and branch != self.branch:
- revstuff.append("[branch]")
- if revision is not None:
- revstuff.append("patch%s" % revision)
- self.description.extend(revstuff)
- self.descriptionDone.extend(revstuff)
-
- cmd = LoggedRemoteCommand("arch", self.args)
- self.startCommand(cmd, warnings)
-
-
-class Bazaar(Arch):
- """Bazaar is an alternative client for Arch repositories. baz is mostly
- compatible with tla, but archive registration is slightly different."""
-
- # TODO: slaves >0.6.6 will accept args['build-config'], so use it
-
- def __init__(self, url, version, archive, **kwargs):
- """
- @type url: string
- @param url: the Arch coordinates of the repository. This is
- typically an http:// URL, but could also be the absolute
- pathname of a local directory instead.
-
- @type version: string
- @param version: the category--branch--version to check out
-
- @type archive: string
- @param archive: The archive name (required). This must always match
- the one that comes from the repository, otherwise the
- buildslave will attempt to get sources from the wrong
- archive.
- """
- self.branch = version
- Source.__init__(self, **kwargs)
- self.addFactoryArguments(url=url,
- version=version,
- archive=archive,
- )
- self.args.update({'url': url,
- 'archive': archive,
- })
-
- def startVC(self, branch, revision, patch):
- self.args['version'] = branch
- self.args['revision'] = revision
- self.args['patch'] = patch
- warnings = self.checkSlaveVersion("bazaar", branch)
-
- revstuff = []
- if branch is not None and branch != self.branch:
- revstuff.append("[branch]")
- if revision is not None:
- revstuff.append("patch%s" % revision)
- self.description.extend(revstuff)
- self.descriptionDone.extend(revstuff)
-
- cmd = LoggedRemoteCommand("bazaar", self.args)
- self.startCommand(cmd, warnings)
-
-class Bzr(Source):
- """Check out a source tree from a bzr (Bazaar) repository at 'repourl'.
-
- """
-
- name = "bzr"
-
- def __init__(self, repourl=None, baseURL=None, defaultBranch=None,
- **kwargs):
- """
- @type repourl: string
- @param repourl: the URL which points at the bzr repository. This
- is used as the default branch. Using C{repourl} does
- not enable builds of alternate branches: use
- C{baseURL} to enable this. Use either C{repourl} or
- C{baseURL}, not both.
-
- @param baseURL: if branches are enabled, this is the base URL to
- which a branch name will be appended. It should
- probably end in a slash. Use exactly one of
- C{repourl} and C{baseURL}.
-
- @param defaultBranch: if branches are enabled, this is the branch
- to use if the Build does not specify one
- explicitly. It will simply be appended to
- C{baseURL} and the result handed to the
- 'bzr checkout pull' command.
- """
- self.repourl = repourl
- self.baseURL = baseURL
- self.branch = defaultBranch
- Source.__init__(self, **kwargs)
- self.addFactoryArguments(repourl=repourl,
- baseURL=baseURL,
- defaultBranch=defaultBranch,
- )
- if (not repourl and not baseURL) or (repourl and baseURL):
- raise ValueError("you must provide exactly one of repourl and"
- " baseURL")
-
- def computeSourceRevision(self, changes):
- if not changes:
- return None
- lastChange = max([int(c.revision) for c in changes])
- return lastChange
-
- def startVC(self, branch, revision, patch):
- slavever = self.slaveVersion("bzr")
- if not slavever:
- m = "slave is too old, does not know about bzr"
- raise BuildSlaveTooOldError(m)
-
- if self.repourl:
- assert not branch # we need baseURL= to use branches
- self.args['repourl'] = self.repourl
- else:
- self.args['repourl'] = self.baseURL + branch
- self.args['revision'] = revision
- self.args['patch'] = patch
-
- revstuff = []
- if branch is not None and branch != self.branch:
- revstuff.append("[branch]")
- self.description.extend(revstuff)
- self.descriptionDone.extend(revstuff)
-
- cmd = LoggedRemoteCommand("bzr", self.args)
- self.startCommand(cmd)
-
-
-class Mercurial(Source):
- """Check out a source tree from a mercurial repository 'repourl'."""
-
- name = "hg"
-
- def __init__(self, repourl=None, baseURL=None, defaultBranch=None,
- **kwargs):
- """
- @type repourl: string
- @param repourl: the URL which points at the Mercurial repository.
- This is used as the default branch. Using C{repourl}
- does not enable builds of alternate branches: use
- C{baseURL} to enable this. Use either C{repourl} or
- C{baseURL}, not both.
-
- @param baseURL: if branches are enabled, this is the base URL to
- which a branch name will be appended. It should
- probably end in a slash. Use exactly one of
- C{repourl} and C{baseURL}.
-
- @param defaultBranch: if branches are enabled, this is the branch
- to use if the Build does not specify one
- explicitly. It will simply be appended to
- C{baseURL} and the result handed to the
- 'hg clone' command.
- """
- self.repourl = repourl
- self.baseURL = baseURL
- self.branch = defaultBranch
- Source.__init__(self, **kwargs)
- self.addFactoryArguments(repourl=repourl,
- baseURL=baseURL,
- defaultBranch=defaultBranch,
- )
- if (not repourl and not baseURL) or (repourl and baseURL):
- raise ValueError("you must provide exactly one of repourl and"
- " baseURL")
-
- def startVC(self, branch, revision, patch):
- slavever = self.slaveVersion("hg")
- if not slavever:
- raise BuildSlaveTooOldError("slave is too old, does not know "
- "about hg")
-
- if self.repourl:
- assert not branch # we need baseURL= to use branches
- self.args['repourl'] = self.repourl
- else:
- self.args['repourl'] = self.baseURL + branch
- self.args['revision'] = revision
- self.args['patch'] = patch
-
- revstuff = []
- if branch is not None and branch != self.branch:
- revstuff.append("[branch]")
- self.description.extend(revstuff)
- self.descriptionDone.extend(revstuff)
-
- cmd = LoggedRemoteCommand("hg", self.args)
- self.startCommand(cmd)
-
- def computeSourceRevision(self, changes):
- if not changes:
- return None
- # without knowing the revision ancestry graph, we can't sort the
- # changes at all. So for now, assume they were given to us in sorted
- # order, and just pay attention to the last one. See ticket #103 for
- # more details.
- if len(changes) > 1:
- log.msg("Mercurial.computeSourceRevision: warning: "
- "there are %d changes here, assuming the last one is "
- "the most recent" % len(changes))
- return changes[-1].revision
-
-
-class P4(Source):
- """ P4 is a class for accessing perforce revision control"""
- name = "p4"
-
- def __init__(self, p4base, defaultBranch=None, p4port=None, p4user=None,
- p4passwd=None, p4extra_views=[],
- p4client='buildbot_%(slave)s_%(builder)s', **kwargs):
- """
- @type p4base: string
- @param p4base: A view into a perforce depot, typically
- "//depot/proj/"
-
- @type defaultBranch: string
- @param defaultBranch: Identify a branch to build by default. Perforce
- is a view based branching system. So, the branch
- is normally the name after the base. For example,
- branch=1.0 is view=//depot/proj/1.0/...
- branch=1.1 is view=//depot/proj/1.1/...
-
- @type p4port: string
- @param p4port: Specify the perforce server to connection in the format
- :. Example "perforce.example.com:1666"
-
- @type p4user: string
- @param p4user: The perforce user to run the command as.
-
- @type p4passwd: string
- @param p4passwd: The password for the perforce user.
-
- @type p4extra_views: list of tuples
- @param p4extra_views: Extra views to be added to
- the client that is being used.
-
- @type p4client: string
- @param p4client: The perforce client to use for this buildslave.
- """
-
- self.branch = defaultBranch
- Source.__init__(self, **kwargs)
- self.addFactoryArguments(p4base=p4base,
- defaultBranch=defaultBranch,
- p4port=p4port,
- p4user=p4user,
- p4passwd=p4passwd,
- p4extra_views=p4extra_views,
- p4client=p4client,
- )
- self.args['p4port'] = p4port
- self.args['p4user'] = p4user
- self.args['p4passwd'] = p4passwd
- self.args['p4base'] = p4base
- self.args['p4extra_views'] = p4extra_views
- self.p4client = p4client
-
- def setBuild(self, build):
- Source.setBuild(self, build)
- self.args['p4client'] = self.p4client % {
- 'slave': build.slavename,
- 'builder': build.builder.name,
- }
-
- def computeSourceRevision(self, changes):
- if not changes:
- return None
- lastChange = max([int(c.revision) for c in changes])
- return lastChange
-
- def startVC(self, branch, revision, patch):
- slavever = self.slaveVersion("p4")
- assert slavever, "slave is too old, does not know about p4"
- args = dict(self.args)
- args['branch'] = branch or self.branch
- args['revision'] = revision
- args['patch'] = patch
- cmd = LoggedRemoteCommand("p4", args)
- self.startCommand(cmd)
-
-class P4Sync(Source):
- """This is a partial solution for using a P4 source repository. You are
- required to manually set up each build slave with a useful P4
- environment, which means setting various per-slave environment variables,
- and creating a P4 client specification which maps the right files into
- the slave's working directory. Once you have done that, this step merely
- performs a 'p4 sync' to update that workspace with the newest files.
-
- Each slave needs the following environment:
-
- - PATH: the 'p4' binary must be on the slave's PATH
- - P4USER: each slave needs a distinct user account
- - P4CLIENT: each slave needs a distinct client specification
-
- You should use 'p4 client' (?) to set up a client view spec which maps
- the desired files into $SLAVEBASE/$BUILDERBASE/source .
- """
-
- name = "p4sync"
-
- def __init__(self, p4port, p4user, p4passwd, p4client, **kwargs):
- assert kwargs['mode'] == "copy", "P4Sync can only be used in mode=copy"
- self.branch = None
- Source.__init__(self, **kwargs)
- self.addFactoryArguments(p4port=p4port,
- p4user=p4user,
- p4passwd=p4passwd,
- p4client=p4client,
- )
- self.args['p4port'] = p4port
- self.args['p4user'] = p4user
- self.args['p4passwd'] = p4passwd
- self.args['p4client'] = p4client
-
- def computeSourceRevision(self, changes):
- if not changes:
- return None
- lastChange = max([int(c.revision) for c in changes])
- return lastChange
-
- def startVC(self, branch, revision, patch):
- slavever = self.slaveVersion("p4sync")
- assert slavever, "slave is too old, does not know about p4"
- cmd = LoggedRemoteCommand("p4sync", self.args)
- self.startCommand(cmd)
-
-class Monotone(Source):
- """Check out a revision from a monotone server at 'server_addr',
- branch 'branch'. 'revision' specifies which revision id to check
- out.
-
- This step will first create a local database, if necessary, and then pull
- the contents of the server into the database. Then it will do the
- checkout/update from this database."""
-
- name = "monotone"
-
- def __init__(self, server_addr, branch, db_path="monotone.db",
- monotone="monotone",
- **kwargs):
- Source.__init__(self, **kwargs)
- self.addFactoryArguments(server_addr=server_addr,
- branch=branch,
- db_path=db_path,
- monotone=monotone,
- )
- self.args.update({"server_addr": server_addr,
- "branch": branch,
- "db_path": db_path,
- "monotone": monotone})
-
- def computeSourceRevision(self, changes):
- if not changes:
- return None
- return changes[-1].revision
-
- def startVC(self):
- slavever = self.slaveVersion("monotone")
- assert slavever, "slave is too old, does not know about monotone"
- cmd = LoggedRemoteCommand("monotone", self.args)
- self.startCommand(cmd)
-
diff --git a/tools/buildbot/pylibs/buildbot/steps/transfer.py b/tools/buildbot/pylibs/buildbot/steps/transfer.py
deleted file mode 100644
index 67da277..0000000
--- a/tools/buildbot/pylibs/buildbot/steps/transfer.py
+++ /dev/null
@@ -1,295 +0,0 @@
-# -*- test-case-name: buildbot.test.test_transfer -*-
-
-import os.path
-from twisted.internet import reactor
-from twisted.spread import pb
-from twisted.python import log
-from buildbot.process.buildstep import RemoteCommand, BuildStep
-from buildbot.process.buildstep import SUCCESS, FAILURE
-from buildbot.interfaces import BuildSlaveTooOldError
-
-
-class _FileWriter(pb.Referenceable):
- """
- Helper class that acts as a file-object with write access
- """
-
- def __init__(self, destfile, maxsize, mode):
- self.destfile = destfile
- self.fp = open(destfile, "wb")
- if mode is not None:
- os.chmod(destfile, mode)
- self.remaining = maxsize
-
- def remote_write(self, data):
- """
- Called from remote slave to write L{data} to L{fp} within boundaries
- of L{maxsize}
-
- @type data: C{string}
- @param data: String of data to write
- """
- if self.remaining is not None:
- if len(data) > self.remaining:
- data = data[:self.remaining]
- self.fp.write(data)
- self.remaining = self.remaining - len(data)
- else:
- self.fp.write(data)
-
- def remote_close(self):
- """
- Called by remote slave to state that no more data will be transfered
- """
- self.fp.close()
- self.fp = None
-
- def __del__(self):
- # unclean shutdown, the file is probably truncated, so delete it
- # altogether rather than deliver a corrupted file
- fp = getattr(self, "fp", None)
- if fp:
- fp.close()
- os.unlink(self.destfile)
-
-
-class StatusRemoteCommand(RemoteCommand):
- def __init__(self, remote_command, args):
- RemoteCommand.__init__(self, remote_command, args)
-
- self.rc = None
- self.stderr = ''
-
- def remoteUpdate(self, update):
- #log.msg('StatusRemoteCommand: update=%r' % update)
- if 'rc' in update:
- self.rc = update['rc']
- if 'stderr' in update:
- self.stderr = self.stderr + update['stderr'] + '\n'
-
-
-class FileUpload(BuildStep):
- """
- Build step to transfer a file from the slave to the master.
-
- arguments:
-
- - ['slavesrc'] filename of source file at slave, relative to workdir
- - ['masterdest'] filename of destination file at master
- - ['workdir'] string with slave working directory relative to builder
- base dir, default 'build'
- - ['maxsize'] maximum size of the file, default None (=unlimited)
- - ['blocksize'] maximum size of each block being transfered
- - ['mode'] file access mode for the resulting master-side file.
- The default (=None) is to leave it up to the umask of
- the buildmaster process.
-
- """
-
- name = 'upload'
-
- def __init__(self, slavesrc, masterdest,
- workdir="build", maxsize=None, blocksize=16*1024, mode=None,
- **buildstep_kwargs):
- BuildStep.__init__(self, **buildstep_kwargs)
- self.addFactoryArguments(slavesrc=slavesrc,
- masterdest=masterdest,
- workdir=workdir,
- maxsize=maxsize,
- blocksize=blocksize,
- mode=mode,
- )
-
- self.slavesrc = slavesrc
- self.masterdest = masterdest
- self.workdir = workdir
- self.maxsize = maxsize
- self.blocksize = blocksize
- assert isinstance(mode, (int, type(None)))
- self.mode = mode
-
- def start(self):
- version = self.slaveVersion("uploadFile")
- properties = self.build.getProperties()
-
- if not version:
- m = "slave is too old, does not know about uploadFile"
- raise BuildSlaveTooOldError(m)
-
- source = properties.render(self.slavesrc)
- masterdest = properties.render(self.masterdest)
- # we rely upon the fact that the buildmaster runs chdir'ed into its
- # basedir to make sure that relative paths in masterdest are expanded
- # properly. TODO: maybe pass the master's basedir all the way down
- # into the BuildStep so we can do this better.
- masterdest = os.path.expanduser(masterdest)
- log.msg("FileUpload started, from slave %r to master %r"
- % (source, masterdest))
-
- self.step_status.setColor('yellow')
- self.step_status.setText(['uploading', os.path.basename(source)])
-
- # we use maxsize to limit the amount of data on both sides
- fileWriter = _FileWriter(masterdest, self.maxsize, self.mode)
-
- # default arguments
- args = {
- 'slavesrc': source,
- 'workdir': self.workdir,
- 'writer': fileWriter,
- 'maxsize': self.maxsize,
- 'blocksize': self.blocksize,
- }
-
- self.cmd = StatusRemoteCommand('uploadFile', args)
- d = self.runCommand(self.cmd)
- d.addCallback(self.finished).addErrback(self.failed)
-
- def finished(self, result):
- if self.cmd.stderr != '':
- self.addCompleteLog('stderr', self.cmd.stderr)
-
- if self.cmd.rc is None or self.cmd.rc == 0:
- self.step_status.setColor('green')
- return BuildStep.finished(self, SUCCESS)
- self.step_status.setColor('red')
- return BuildStep.finished(self, FAILURE)
-
-
-
-
-
-class _FileReader(pb.Referenceable):
- """
- Helper class that acts as a file-object with read access
- """
-
- def __init__(self, fp):
- self.fp = fp
-
- def remote_read(self, maxlength):
- """
- Called from remote slave to read at most L{maxlength} bytes of data
-
- @type maxlength: C{integer}
- @param maxlength: Maximum number of data bytes that can be returned
-
- @return: Data read from L{fp}
- @rtype: C{string} of bytes read from file
- """
- if self.fp is None:
- return ''
-
- data = self.fp.read(maxlength)
- return data
-
- def remote_close(self):
- """
- Called by remote slave to state that no more data will be transfered
- """
- if self.fp is not None:
- self.fp.close()
- self.fp = None
-
-
-class FileDownload(BuildStep):
- """
- Download the first 'maxsize' bytes of a file, from the buildmaster to the
- buildslave. Set the mode of the file
-
- Arguments::
-
- ['mastersrc'] filename of source file at master
- ['slavedest'] filename of destination file at slave
- ['workdir'] string with slave working directory relative to builder
- base dir, default 'build'
- ['maxsize'] maximum size of the file, default None (=unlimited)
- ['blocksize'] maximum size of each block being transfered
- ['mode'] use this to set the access permissions of the resulting
- buildslave-side file. This is traditionally an octal
- integer, like 0644 to be world-readable (but not
- world-writable), or 0600 to only be readable by
- the buildslave account, or 0755 to be world-executable.
- The default (=None) is to leave it up to the umask of
- the buildslave process.
-
- """
-
- name = 'download'
-
- def __init__(self, mastersrc, slavedest,
- workdir="build", maxsize=None, blocksize=16*1024, mode=None,
- **buildstep_kwargs):
- BuildStep.__init__(self, **buildstep_kwargs)
- self.addFactoryArguments(mastersrc=mastersrc,
- slavedest=slavedest,
- workdir=workdir,
- maxsize=maxsize,
- blocksize=blocksize,
- mode=mode,
- )
-
- self.mastersrc = mastersrc
- self.slavedest = slavedest
- self.workdir = workdir
- self.maxsize = maxsize
- self.blocksize = blocksize
- assert isinstance(mode, (int, type(None)))
- self.mode = mode
-
- def start(self):
- properties = self.build.getProperties()
-
- version = self.slaveVersion("downloadFile")
- if not version:
- m = "slave is too old, does not know about downloadFile"
- raise BuildSlaveTooOldError(m)
-
- # we are currently in the buildmaster's basedir, so any non-absolute
- # paths will be interpreted relative to that
- source = os.path.expanduser(properties.render(self.mastersrc))
- slavedest = properties.render(self.slavedest)
- log.msg("FileDownload started, from master %r to slave %r" %
- (source, slavedest))
-
- self.step_status.setColor('yellow')
- self.step_status.setText(['downloading', "to",
- os.path.basename(slavedest)])
-
- # setup structures for reading the file
- try:
- fp = open(source, 'rb')
- except IOError:
- # if file does not exist, bail out with an error
- self.addCompleteLog('stderr',
- 'File %r not available at master' % source)
- # TODO: once BuildStep.start() gets rewritten to use
- # maybeDeferred, just re-raise the exception here.
- reactor.callLater(0, BuildStep.finished, self, FAILURE)
- return
- fileReader = _FileReader(fp)
-
- # default arguments
- args = {
- 'slavedest': slavedest,
- 'maxsize': self.maxsize,
- 'reader': fileReader,
- 'blocksize': self.blocksize,
- 'workdir': self.workdir,
- 'mode': self.mode,
- }
-
- self.cmd = StatusRemoteCommand('downloadFile', args)
- d = self.runCommand(self.cmd)
- d.addCallback(self.finished).addErrback(self.failed)
-
- def finished(self, result):
- if self.cmd.stderr != '':
- self.addCompleteLog('stderr', self.cmd.stderr)
-
- if self.cmd.rc is None or self.cmd.rc == 0:
- self.step_status.setColor('green')
- return BuildStep.finished(self, SUCCESS)
- self.step_status.setColor('red')
- return BuildStep.finished(self, FAILURE)
-
diff --git a/tools/buildbot/pylibs/buildbot/steps/trigger.py b/tools/buildbot/pylibs/buildbot/steps/trigger.py
deleted file mode 100644
index 8bb987a..0000000
--- a/tools/buildbot/pylibs/buildbot/steps/trigger.py
+++ /dev/null
@@ -1,127 +0,0 @@
-from buildbot.process.buildstep import LoggingBuildStep, SUCCESS, FAILURE, EXCEPTION
-from buildbot.process.properties import Properties
-from buildbot.scheduler import Triggerable
-from twisted.internet import defer
-
-class Trigger(LoggingBuildStep):
- """I trigger a scheduler.Triggerable, to use one or more Builders as if
- they were a single buildstep (like a subroutine call).
- """
- name = "trigger"
-
- flunkOnFailure = True
-
- def __init__(self, schedulerNames=[], updateSourceStamp=True,
- waitForFinish=False, set_properties={}, **kwargs):
- """
- Trigger the given schedulers when this step is executed.
-
- @param schedulerNames: A list of scheduler names that should be
- triggered. Schedulers can be specified using
- WithProperties, if desired.
-
- @param updateSourceStamp: If True (the default), I will try to give
- the schedulers an absolute SourceStamp for
- their builds, so that a HEAD build will use
- the same revision even if more changes have
- occurred since my build's update step was
- run. If False, I will use the original
- SourceStamp unmodified.
-
- @param waitForFinish: If False (the default), this step will finish
- as soon as I've started the triggered
- schedulers. If True, I will wait until all of
- the triggered schedulers have finished their
- builds.
-
- @param set_properties: A dictionary of properties to set for any
- builds resulting from this trigger. To copy
- existing properties, use WithProperties. These
- properties will override properties set in the
- Triggered scheduler's constructor.
-
- """
- assert schedulerNames, "You must specify a scheduler to trigger"
- self.schedulerNames = schedulerNames
- self.updateSourceStamp = updateSourceStamp
- self.waitForFinish = waitForFinish
- self.set_properties = set_properties
- self.running = False
- LoggingBuildStep.__init__(self, **kwargs)
- self.addFactoryArguments(schedulerNames=schedulerNames,
- updateSourceStamp=updateSourceStamp,
- waitForFinish=waitForFinish)
-
- def interrupt(self, reason):
- # TODO: this doesn't actually do anything.
- if self.running:
- self.step_status.setColor("red")
- self.step_status.setText(["interrupted"])
-
- def start(self):
- properties = self.build.getProperties()
-
- # make a new properties object from a dict rendered by the old
- # properties object
- props_to_set = Properties()
- props_to_set.update(properties.render(self.set_properties), "Trigger")
-
- self.running = True
- ss = self.build.getSourceStamp()
- if self.updateSourceStamp:
- got = properties.getProperty('got_revision')
- if got:
- ss = ss.getAbsoluteSourceStamp(got)
-
- # (is there an easier way to find the BuildMaster?)
- all_schedulers = self.build.builder.botmaster.parent.allSchedulers()
- all_schedulers = dict([(sch.name, sch) for sch in all_schedulers])
- unknown_schedulers = []
- triggered_schedulers = []
-
- # TODO: don't fire any schedulers if we discover an unknown one
- dl = []
- for scheduler in self.schedulerNames:
- scheduler = properties.render(scheduler)
- if all_schedulers.has_key(scheduler):
- sch = all_schedulers[scheduler]
- if isinstance(sch, Triggerable):
- dl.append(sch.trigger(ss, set_props=props_to_set))
- triggered_schedulers.append(scheduler)
- else:
- unknown_schedulers.append(scheduler)
- else:
- unknown_schedulers.append(scheduler)
-
- if unknown_schedulers:
- self.step_status.setColor("red")
- self.step_status.setText(['no scheduler:'] + unknown_schedulers)
- rc = FAILURE
- else:
- rc = SUCCESS
- self.step_status.setText(['triggered'] + triggered_schedulers)
- if self.waitForFinish:
- self.step_status.setColor("yellow")
- else:
- self.step_status.setColor("green")
-
- if self.waitForFinish:
- d = defer.DeferredList(dl, consumeErrors=1)
- else:
- d = defer.succeed([])
-
- def cb(rclist):
- rc = SUCCESS # (this rc is not the same variable as that above)
- for was_cb, buildsetstatus in rclist:
- # TODO: make this algo more configurable
- if not was_cb:
- rc = EXCEPTION
- break
- if buildsetstatus.getResults() == FAILURE:
- rc = FAILURE
- return self.finished(rc)
-
- def eb(why):
- return self.finished(FAILURE)
-
- d.addCallbacks(cb, eb)
diff --git a/tools/buildbot/pylibs/buildbot/test/__init__.py b/tools/buildbot/pylibs/buildbot/test/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/tools/buildbot/pylibs/buildbot/test/emit.py b/tools/buildbot/pylibs/buildbot/test/emit.py
deleted file mode 100644
index 1e23e92..0000000
--- a/tools/buildbot/pylibs/buildbot/test/emit.py
+++ /dev/null
@@ -1,11 +0,0 @@
-
-import os, sys
-
-sys.stdout.write("this is stdout\n")
-sys.stderr.write("this is stderr\n")
-if os.environ.has_key("EMIT_TEST"):
- sys.stdout.write("EMIT_TEST: %s\n" % os.environ["EMIT_TEST"])
-open("log1.out","wt").write("this is log1\n")
-
-rc = int(sys.argv[1])
-sys.exit(rc)
diff --git a/tools/buildbot/pylibs/buildbot/test/emitlogs.py b/tools/buildbot/pylibs/buildbot/test/emitlogs.py
deleted file mode 100644
index 1430235..0000000
--- a/tools/buildbot/pylibs/buildbot/test/emitlogs.py
+++ /dev/null
@@ -1,42 +0,0 @@
-import sys, time, os.path, StringIO
-
-mode = 0
-if len(sys.argv) > 1:
- mode = int(sys.argv[1])
-
-if mode == 0:
- log2 = open("log2.out", "wt")
- log3 = open("log3.out", "wt")
-elif mode == 1:
- # delete the logfiles first, and wait a moment to exercise a failure path
- if os.path.exists("log2.out"):
- os.unlink("log2.out")
- if os.path.exists("log3.out"):
- os.unlink("log3.out")
- time.sleep(2)
- log2 = open("log2.out", "wt")
- log3 = open("log3.out", "wt")
-elif mode == 2:
- # don't create the logfiles at all
- log2 = StringIO.StringIO()
- log3 = StringIO.StringIO()
-
-def write(i):
- log2.write("this is log2 %d\n" % i)
- log2.flush()
- log3.write("this is log3 %d\n" % i)
- log3.flush()
- sys.stdout.write("this is stdout %d\n" % i)
- sys.stdout.flush()
-
-write(0)
-time.sleep(1)
-write(1)
-sys.stdin.read(1)
-write(2)
-
-log2.close()
-log3.close()
-
-sys.exit(0)
-
diff --git a/tools/buildbot/pylibs/buildbot/test/mail/freshcvs.1 b/tools/buildbot/pylibs/buildbot/test/mail/freshcvs.1
deleted file mode 100644
index cc8442e..0000000
--- a/tools/buildbot/pylibs/buildbot/test/mail/freshcvs.1
+++ /dev/null
@@ -1,68 +0,0 @@
-Return-Path:
-Delivered-To: warner-twistedcvs@luther.lothar.com
-Received: (qmail 11151 invoked by uid 1000); 11 Jan 2003 17:10:04 -0000
-Delivered-To: warner-twistedcvs@lothar.com
-Received: (qmail 1548 invoked by uid 13574); 11 Jan 2003 17:06:39 -0000
-Received: from unknown (HELO pyramid.twistedmatrix.com) ([64.123.27.105]) (envelope-sender )
- by 130.94.181.6 (qmail-ldap-1.03) with SMTP
- for ; 11 Jan 2003 17:06:39 -0000
-Received: from localhost ([127.0.0.1] helo=pyramid.twistedmatrix.com)
- by pyramid.twistedmatrix.com with esmtp (Exim 3.35 #1 (Debian))
- id 18XP0U-0002Mq-00; Sat, 11 Jan 2003 11:01:14 -0600
-Received: from acapnotic by pyramid.twistedmatrix.com with local (Exim 3.35 #1 (Debian))
- id 18XP02-0002MN-00
- for ; Sat, 11 Jan 2003 11:00:46 -0600
-To: twisted-commits@twistedmatrix.com
-From: moshez CVS
-Reply-To: twisted-python@twistedmatrix.com
-X-Mailer: CVSToys
-From: moshez CVS
-Reply-To: twisted-python@twistedmatrix.com
-Message-Id:
-Subject: [Twisted-commits] Instance massenger, apparently
-Sender: twisted-commits-admin@twistedmatrix.com
-Errors-To: twisted-commits-admin@twistedmatrix.com
-X-BeenThere: twisted-commits@twistedmatrix.com
-X-Mailman-Version: 2.0.11
-Precedence: bulk
-List-Help:
-List-Post:
-List-Subscribe: ,
-
-List-Id:
-List-Unsubscribe: ,
-
-List-Archive:
-Date: Sat, 11 Jan 2003 11:00:46 -0600
-Status:
-
-Modified files:
-Twisted/debian/python-twisted.menu.in 1.3 1.4
-
-Log message:
-Instance massenger, apparently
-
-
-ViewCVS links:
-http://twistedmatrix.com/users/jh.twistd/viewcvs/cgi/viewcvs.cgi/debian/python-twisted.menu.in.diff?r1=text&tr1=1.3&r2=text&tr2=1.4&cvsroot=Twisted
-
-Index: Twisted/debian/python-twisted.menu.in
-diff -u Twisted/debian/python-twisted.menu.in:1.3 Twisted/debian/python-twisted.menu.in:1.4
---- Twisted/debian/python-twisted.menu.in:1.3 Sat Dec 28 10:02:12 2002
-+++ Twisted/debian/python-twisted.menu.in Sat Jan 11 09:00:44 2003
-@@ -1,7 +1,7 @@
- ?package(python@VERSION@-twisted):\
- needs=x11\
- section="Apps/Net"\
--title="Twisted Instant Messenger (@VERSION@)"\
-+title="Twisted Instance Messenger (@VERSION@)"\
- command="/usr/bin/t-im@VERSION@"
-
- ?package(python@VERSION@-twisted):\
-
-.
-
-_______________________________________________
-Twisted-commits mailing list
-Twisted-commits@twistedmatrix.com
-http://twistedmatrix.com/cgi-bin/mailman/listinfo/twisted-commits
diff --git a/tools/buildbot/pylibs/buildbot/test/mail/freshcvs.2 b/tools/buildbot/pylibs/buildbot/test/mail/freshcvs.2
deleted file mode 100644
index ada1311..0000000
--- a/tools/buildbot/pylibs/buildbot/test/mail/freshcvs.2
+++ /dev/null
@@ -1,101 +0,0 @@
-Return-Path:
-Delivered-To: warner-twistedcvs@luther.lothar.com
-Received: (qmail 32220 invoked by uid 1000); 14 Jan 2003 21:50:04 -0000
-Delivered-To: warner-twistedcvs@lothar.com
-Received: (qmail 7923 invoked by uid 13574); 14 Jan 2003 21:49:48 -0000
-Received: from unknown (HELO pyramid.twistedmatrix.com) ([64.123.27.105]) (envelope-sender )
- by 130.94.181.6 (qmail-ldap-1.03) with SMTP
- for ; 14 Jan 2003 21:49:48 -0000
-Received: from localhost ([127.0.0.1] helo=pyramid.twistedmatrix.com)
- by pyramid.twistedmatrix.com with esmtp (Exim 3.35 #1 (Debian))
- id 18YYr0-0005en-00; Tue, 14 Jan 2003 15:44:14 -0600
-Received: from acapnotic by pyramid.twistedmatrix.com with local (Exim 3.35 #1 (Debian))
- id 18YYq7-0005eQ-00
- for ; Tue, 14 Jan 2003 15:43:19 -0600
-To: twisted-commits@twistedmatrix.com
-From: itamarst CVS
-Reply-To: twisted-python@twistedmatrix.com
-X-Mailer: CVSToys
-From: itamarst CVS
-Reply-To: twisted-python@twistedmatrix.com
-Message-Id:
-Subject: [Twisted-commits] submit formmethod now subclass of Choice
-Sender: twisted-commits-admin@twistedmatrix.com
-Errors-To: twisted-commits-admin@twistedmatrix.com
-X-BeenThere: twisted-commits@twistedmatrix.com
-X-Mailman-Version: 2.0.11
-Precedence: bulk
-List-Help:
-List-Post:
-List-Subscribe: ,
-
-List-Id:
-List-Unsubscribe: ,
-
-List-Archive:
-Date: Tue, 14 Jan 2003 15:43:19 -0600
-Status:
-
-Modified files:
-Twisted/twisted/web/woven/form.py 1.20 1.21
-Twisted/twisted/python/formmethod.py 1.12 1.13
-
-Log message:
-submit formmethod now subclass of Choice
-
-
-ViewCVS links:
-http://twistedmatrix.com/users/jh.twistd/viewcvs/cgi/viewcvs.cgi/twisted/web/woven/form.py.diff?r1=text&tr1=1.20&r2=text&tr2=1.21&cvsroot=Twisted
-http://twistedmatrix.com/users/jh.twistd/viewcvs/cgi/viewcvs.cgi/twisted/python/formmethod.py.diff?r1=text&tr1=1.12&r2=text&tr2=1.13&cvsroot=Twisted
-
-Index: Twisted/twisted/web/woven/form.py
-diff -u Twisted/twisted/web/woven/form.py:1.20 Twisted/twisted/web/woven/form.py:1.21
---- Twisted/twisted/web/woven/form.py:1.20 Tue Jan 14 12:07:29 2003
-+++ Twisted/twisted/web/woven/form.py Tue Jan 14 13:43:16 2003
-@@ -140,8 +140,8 @@
-
- def input_submit(self, request, content, arg):
- div = content.div()
-- for value in arg.buttons:
-- div.input(type="submit", name=arg.name, value=value)
-+ for tag, value, desc in arg.choices:
-+ div.input(type="submit", name=arg.name, value=tag)
- div.text(" ")
- if arg.reset:
- div.input(type="reset")
-
-Index: Twisted/twisted/python/formmethod.py
-diff -u Twisted/twisted/python/formmethod.py:1.12 Twisted/twisted/python/formmethod.py:1.13
---- Twisted/twisted/python/formmethod.py:1.12 Tue Jan 14 12:07:30 2003
-+++ Twisted/twisted/python/formmethod.py Tue Jan 14 13:43:17 2003
-@@ -180,19 +180,13 @@
- return 1
-
-
--class Submit(Argument):
-+class Submit(Choice):
- """Submit button or a reasonable facsimile thereof."""
-
-- def __init__(self, name, buttons=["Submit"], reset=0, shortDesc=None, longDesc=None):
-- Argument.__init__(self, name, shortDesc=shortDesc, longDesc=longDesc)
-- self.buttons = buttons
-+ def __init__(self, name, choices=[("Submit", "submit", "Submit form")],
-+ reset=0, shortDesc=None, longDesc=None):
-+ Choice.__init__(self, name, choices=choices, shortDesc=shortDesc, longDesc=longDesc)
- self.reset = reset
--
-- def coerce(self, val):
-- if val in self.buttons:
-- return val
-- else:
-- raise InputError, "no such action"
-
-
- class PresentationHint:
-
-.
-
-_______________________________________________
-Twisted-commits mailing list
-Twisted-commits@twistedmatrix.com
-http://twistedmatrix.com/cgi-bin/mailman/listinfo/twisted-commits
diff --git a/tools/buildbot/pylibs/buildbot/test/mail/freshcvs.3 b/tools/buildbot/pylibs/buildbot/test/mail/freshcvs.3
deleted file mode 100644
index f9ff199..0000000
--- a/tools/buildbot/pylibs/buildbot/test/mail/freshcvs.3
+++ /dev/null
@@ -1,97 +0,0 @@
-Return-Path:
-Delivered-To: warner-twistedcvs@luther.lothar.com
-Received: (qmail 32220 invoked by uid 1000); 14 Jan 2003 21:50:04 -0000
-Delivered-To: warner-twistedcvs@lothar.com
-Received: (qmail 7923 invoked by uid 13574); 14 Jan 2003 21:49:48 -0000
-Received: from unknown (HELO pyramid.twistedmatrix.com) ([64.123.27.105]) (envelope-sender )
- by 130.94.181.6 (qmail-ldap-1.03) with SMTP
- for ; 14 Jan 2003 21:49:48 -0000
-Received: from localhost ([127.0.0.1] helo=pyramid.twistedmatrix.com)
- by pyramid.twistedmatrix.com with esmtp (Exim 3.35 #1 (Debian))
- id 18YYr0-0005en-00; Tue, 14 Jan 2003 15:44:14 -0600
-Received: from acapnotic by pyramid.twistedmatrix.com with local (Exim 3.35 #1 (Debian))
- id 18YYq7-0005eQ-00
- for ; Tue, 14 Jan 2003 15:43:19 -0600
-To: twisted-commits@twistedmatrix.com
-From: itamarst CVS
-Reply-To: twisted-python@twistedmatrix.com
-X-Mailer: CVSToys
-From: itamarst CVS
-Reply-To: twisted-python@twistedmatrix.com
-Message-Id:
-Subject: [Twisted-commits] submit formmethod now subclass of Choice
-Sender: twisted-commits-admin@twistedmatrix.com
-Errors-To: twisted-commits-admin@twistedmatrix.com
-X-BeenThere: twisted-commits@twistedmatrix.com
-X-Mailman-Version: 2.0.11
-Precedence: bulk
-List-Help:
-List-Post:
-List-Subscribe: ,
-
-List-Id:
-List-Unsubscribe: ,
-
-List-Archive:
-Date: Tue, 14 Jan 2003 15:43:19 -0600
-Status:
-
-Modified files:
-Twisted/twisted/web/woven/form.py 1.20 1.21
-Twisted/twisted/python/formmethod.py 1.12 1.13
-
-Log message:
-submit formmethod now subclass of Choice
-
-
-Index: Twisted/twisted/web/woven/form.py
-diff -u Twisted/twisted/web/woven/form.py:1.20 Twisted/twisted/web/woven/form.py:1.21
---- Twisted/twisted/web/woven/form.py:1.20 Tue Jan 14 12:07:29 2003
-+++ Twisted/twisted/web/woven/form.py Tue Jan 14 13:43:16 2003
-@@ -140,8 +140,8 @@
-
- def input_submit(self, request, content, arg):
- div = content.div()
-- for value in arg.buttons:
-- div.input(type="submit", name=arg.name, value=value)
-+ for tag, value, desc in arg.choices:
-+ div.input(type="submit", name=arg.name, value=tag)
- div.text(" ")
- if arg.reset:
- div.input(type="reset")
-
-Index: Twisted/twisted/python/formmethod.py
-diff -u Twisted/twisted/python/formmethod.py:1.12 Twisted/twisted/python/formmethod.py:1.13
---- Twisted/twisted/python/formmethod.py:1.12 Tue Jan 14 12:07:30 2003
-+++ Twisted/twisted/python/formmethod.py Tue Jan 14 13:43:17 2003
-@@ -180,19 +180,13 @@
- return 1
-
-
--class Submit(Argument):
-+class Submit(Choice):
- """Submit button or a reasonable facsimile thereof."""
-
-- def __init__(self, name, buttons=["Submit"], reset=0, shortDesc=None, longDesc=None):
-- Argument.__init__(self, name, shortDesc=shortDesc, longDesc=longDesc)
-- self.buttons = buttons
-+ def __init__(self, name, choices=[("Submit", "submit", "Submit form")],
-+ reset=0, shortDesc=None, longDesc=None):
-+ Choice.__init__(self, name, choices=choices, shortDesc=shortDesc, longDesc=longDesc)
- self.reset = reset
--
-- def coerce(self, val):
-- if val in self.buttons:
-- return val
-- else:
-- raise InputError, "no such action"
-
-
- class PresentationHint:
-
-.
-
-_______________________________________________
-Twisted-commits mailing list
-Twisted-commits@twistedmatrix.com
-http://twistedmatrix.com/cgi-bin/mailman/listinfo/twisted-commits
diff --git a/tools/buildbot/pylibs/buildbot/test/mail/freshcvs.4 b/tools/buildbot/pylibs/buildbot/test/mail/freshcvs.4
deleted file mode 100644
index 9e674dc..0000000
--- a/tools/buildbot/pylibs/buildbot/test/mail/freshcvs.4
+++ /dev/null
@@ -1,45 +0,0 @@
-Return-Path:
-Delivered-To: warner-twistedcvs@luther.lothar.com
-Received: (qmail 32220 invoked by uid 1000); 14 Jan 2003 21:50:04 -0000
-Delivered-To: warner-twistedcvs@lothar.com
-Received: (qmail 7923 invoked by uid 13574); 14 Jan 2003 21:49:48 -0000
-Received: from unknown (HELO pyramid.twistedmatrix.com) ([64.123.27.105]) (envelope-sender )
- by 130.94.181.6 (qmail-ldap-1.03) with SMTP
- for ; 14 Jan 2003 21:49:48 -0000
-Received: from localhost ([127.0.0.1] helo=pyramid.twistedmatrix.com)
- by pyramid.twistedmatrix.com with esmtp (Exim 3.35 #1 (Debian))
- id 18YYr0-0005en-00; Tue, 14 Jan 2003 15:44:14 -0600
-Received: from acapnotic by pyramid.twistedmatrix.com with local (Exim 3.35 #1 (Debian))
- id 18YYq7-0005eQ-00
- for ; Tue, 14 Jan 2003 15:43:19 -0600
-To: twisted-commits@twistedmatrix.com
-From: itamarst CVS
-Reply-To: twisted-python@twistedmatrix.com
-X-Mailer: CVSToys
-From: itamarst CVS
-Reply-To: twisted-python@twistedmatrix.com
-Message-Id:
-Subject: [Twisted-commits] submit formmethod now subclass of Choice
-Sender: twisted-commits-admin@twistedmatrix.com
-Errors-To: twisted-commits-admin@twistedmatrix.com
-X-BeenThere: twisted-commits@twistedmatrix.com
-X-Mailman-Version: 2.0.11
-Precedence: bulk
-List-Help:
-List-Post:
-List-Subscribe: ,
-
-List-Id:
-List-Unsubscribe: ,
-
-List-Archive:
-Date: Tue, 14 Jan 2003 15:43:19 -0600
-Status:
-
-Modified files:
-Twisted/twisted/web/woven/form.py 1.20 1.21
-Twisted/twisted/python/formmethod.py 1.12 1.13
-
-Log message:
-submit formmethod now subclass of Choice
-
diff --git a/tools/buildbot/pylibs/buildbot/test/mail/freshcvs.5 b/tools/buildbot/pylibs/buildbot/test/mail/freshcvs.5
deleted file mode 100644
index f20a958..0000000
--- a/tools/buildbot/pylibs/buildbot/test/mail/freshcvs.5
+++ /dev/null
@@ -1,54 +0,0 @@
-Return-Path:
-Delivered-To: warner-twistedcvs@luther.lothar.com
-Received: (qmail 5865 invoked by uid 1000); 17 Jan 2003 07:00:04 -0000
-Delivered-To: warner-twistedcvs@lothar.com
-Received: (qmail 40460 invoked by uid 13574); 17 Jan 2003 06:51:55 -0000
-Received: from unknown (HELO pyramid.twistedmatrix.com) ([64.123.27.105]) (envelope-sender )
- by 130.94.181.6 (qmail-ldap-1.03) with SMTP
- for ; 17 Jan 2003 06:51:55 -0000
-Received: from localhost ([127.0.0.1] helo=pyramid.twistedmatrix.com)
- by pyramid.twistedmatrix.com with esmtp (Exim 3.35 #1 (Debian))
- id 18ZQGk-0003WL-00; Fri, 17 Jan 2003 00:46:22 -0600
-Received: from acapnotic by pyramid.twistedmatrix.com with local (Exim 3.35 #1 (Debian))
- id 18ZQFy-0003VP-00
- for ; Fri, 17 Jan 2003 00:45:34 -0600
-To: twisted-commits@twistedmatrix.com
-From: etrepum CVS
-Reply-To: twisted-python@twistedmatrix.com
-X-Mailer: CVSToys
-From: etrepum CVS
-Reply-To: twisted-python@twistedmatrix.com
-Message-Id:
-Subject: [Twisted-commits] Directory /cvs/Twisted/doc/examples/cocoaDemo added to the repository
-Sender: twisted-commits-admin@twistedmatrix.com
-Errors-To: twisted-commits-admin@twistedmatrix.com
-X-BeenThere: twisted-commits@twistedmatrix.com
-X-Mailman-Version: 2.0.11
-Precedence: bulk
-List-Help:
-List-Post:
-List-Subscribe: ,
-
-List-Id:
-List-Unsubscribe: ,
-
-List-Archive:
-Date: Fri, 17 Jan 2003 00:45:34 -0600
-Status:
-
-Modified files:
-Twisted/doc/examples/cocoaDemo 0 0
-
-Log message:
-Directory /cvs/Twisted/doc/examples/cocoaDemo added to the repository
-
-
-ViewCVS links:
-http://twistedmatrix.com/users/jh.twistd/viewcvs/cgi/viewcvs.cgi/doc/examples/cocoaDemo.diff?r1=text&tr1=NONE&r2=text&tr2=NONE&cvsroot=Twisted
-
-.
-
-_______________________________________________
-Twisted-commits mailing list
-Twisted-commits@twistedmatrix.com
-http://twistedmatrix.com/cgi-bin/mailman/listinfo/twisted-commits
diff --git a/tools/buildbot/pylibs/buildbot/test/mail/freshcvs.6 b/tools/buildbot/pylibs/buildbot/test/mail/freshcvs.6
deleted file mode 100644
index 20719f4..0000000
--- a/tools/buildbot/pylibs/buildbot/test/mail/freshcvs.6
+++ /dev/null
@@ -1,70 +0,0 @@
-Return-Path:
-Delivered-To: warner-twistedcvs@luther.lothar.com
-Received: (qmail 7252 invoked by uid 1000); 17 Jan 2003 07:10:04 -0000
-Delivered-To: warner-twistedcvs@lothar.com
-Received: (qmail 43115 invoked by uid 13574); 17 Jan 2003 07:07:57 -0000
-Received: from unknown (HELO pyramid.twistedmatrix.com) ([64.123.27.105]) (envelope-sender )
- by 130.94.181.6 (qmail-ldap-1.03) with SMTP
- for ; 17 Jan 2003 07:07:57 -0000
-Received: from localhost ([127.0.0.1] helo=pyramid.twistedmatrix.com)
- by pyramid.twistedmatrix.com with esmtp (Exim 3.35 #1 (Debian))
- id 18ZQW6-0003dA-00; Fri, 17 Jan 2003 01:02:14 -0600
-Received: from acapnotic by pyramid.twistedmatrix.com with local (Exim 3.35 #1 (Debian))
- id 18ZQV7-0003cm-00
- for ; Fri, 17 Jan 2003 01:01:13 -0600
-To: twisted-commits@twistedmatrix.com
-From: etrepum CVS
-Reply-To: twisted-python@twistedmatrix.com
-X-Mailer: CVSToys
-From: etrepum CVS
-Reply-To: twisted-python@twistedmatrix.com
-Message-Id:
-Subject: [Twisted-commits] Cocoa (OS X) clone of the QT demo, using polling reactor
-Sender: twisted-commits-admin@twistedmatrix.com
-Errors-To: twisted-commits-admin@twistedmatrix.com
-X-BeenThere: twisted-commits@twistedmatrix.com
-X-Mailman-Version: 2.0.11
-Precedence: bulk
-List-Help:
-List-Post:
-List-Subscribe: ,
-
-List-Id:
-List-Unsubscribe: ,
-
-List-Archive:
-Date: Fri, 17 Jan 2003 01:01:13 -0600
-Status:
-
-Modified files:
-Twisted/doc/examples/cocoaDemo/MyAppDelegate.py None 1.1
-Twisted/doc/examples/cocoaDemo/__main__.py None 1.1
-Twisted/doc/examples/cocoaDemo/bin-python-main.m None 1.1
-Twisted/doc/examples/cocoaDemo/English.lproj/InfoPlist.strings None 1.1
-Twisted/doc/examples/cocoaDemo/English.lproj/MainMenu.nib/classes.nib None 1.1
-Twisted/doc/examples/cocoaDemo/English.lproj/MainMenu.nib/info.nib None 1.1
-Twisted/doc/examples/cocoaDemo/English.lproj/MainMenu.nib/keyedobjects.nib None 1.1
-Twisted/doc/examples/cocoaDemo/cocoaDemo.pbproj/project.pbxproj None 1.1
-
-Log message:
-Cocoa (OS X) clone of the QT demo, using polling reactor
-
-Requires pyobjc ( http://pyobjc.sourceforge.net ), it's not much different than the template project. The reactor is iterated periodically by a repeating NSTimer.
-
-
-ViewCVS links:
-http://twistedmatrix.com/users/jh.twistd/viewcvs/cgi/viewcvs.cgi/doc/examples/cocoaDemo/MyAppDelegate.py.diff?r1=text&tr1=None&r2=text&tr2=1.1&cvsroot=Twisted
-http://twistedmatrix.com/users/jh.twistd/viewcvs/cgi/viewcvs.cgi/doc/examples/cocoaDemo/__main__.py.diff?r1=text&tr1=None&r2=text&tr2=1.1&cvsroot=Twisted
-http://twistedmatrix.com/users/jh.twistd/viewcvs/cgi/viewcvs.cgi/doc/examples/cocoaDemo/bin-python-main.m.diff?r1=text&tr1=None&r2=text&tr2=1.1&cvsroot=Twisted
-http://twistedmatrix.com/users/jh.twistd/viewcvs/cgi/viewcvs.cgi/doc/examples/cocoaDemo/English.lproj/InfoPlist.strings.diff?r1=text&tr1=None&r2=text&tr2=1.1&cvsroot=Twisted
-http://twistedmatrix.com/users/jh.twistd/viewcvs/cgi/viewcvs.cgi/doc/examples/cocoaDemo/English.lproj/MainMenu.nib/classes.nib.diff?r1=text&tr1=None&r2=text&tr2=1.1&cvsroot=Twisted
-http://twistedmatrix.com/users/jh.twistd/viewcvs/cgi/viewcvs.cgi/doc/examples/cocoaDemo/English.lproj/MainMenu.nib/info.nib.diff?r1=text&tr1=None&r2=text&tr2=1.1&cvsroot=Twisted
-http://twistedmatrix.com/users/jh.twistd/viewcvs/cgi/viewcvs.cgi/doc/examples/cocoaDemo/English.lproj/MainMenu.nib/keyedobjects.nib.diff?r1=text&tr1=None&r2=text&tr2=1.1&cvsroot=Twisted
-http://twistedmatrix.com/users/jh.twistd/viewcvs/cgi/viewcvs.cgi/doc/examples/cocoaDemo/cocoaDemo.pbproj/project.pbxproj.diff?r1=text&tr1=None&r2=text&tr2=1.1&cvsroot=Twisted
-
-.
-
-_______________________________________________
-Twisted-commits mailing list
-Twisted-commits@twistedmatrix.com
-http://twistedmatrix.com/cgi-bin/mailman/listinfo/twisted-commits
diff --git a/tools/buildbot/pylibs/buildbot/test/mail/freshcvs.7 b/tools/buildbot/pylibs/buildbot/test/mail/freshcvs.7
deleted file mode 100644
index 515be1d..0000000
--- a/tools/buildbot/pylibs/buildbot/test/mail/freshcvs.7
+++ /dev/null
@@ -1,68 +0,0 @@
-Return-Path:
-Delivered-To: warner-twistedcvs@luther.lothar.com
-Received: (qmail 8665 invoked by uid 1000); 17 Jan 2003 08:00:03 -0000
-Delivered-To: warner-twistedcvs@lothar.com
-Received: (qmail 50728 invoked by uid 13574); 17 Jan 2003 07:51:14 -0000
-Received: from unknown (HELO pyramid.twistedmatrix.com) ([64.123.27.105]) (envelope-sender )
- by 130.94.181.6 (qmail-ldap-1.03) with SMTP
- for ; 17 Jan 2003 07:51:14 -0000
-Received: from localhost ([127.0.0.1] helo=pyramid.twistedmatrix.com)
- by pyramid.twistedmatrix.com with esmtp (Exim 3.35 #1 (Debian))
- id 18ZRBm-0003pN-00; Fri, 17 Jan 2003 01:45:18 -0600
-Received: from acapnotic by pyramid.twistedmatrix.com with local (Exim 3.35 #1 (Debian))
- id 18ZRBQ-0003ou-00
- for ; Fri, 17 Jan 2003 01:44:56 -0600
-To: twisted-commits@twistedmatrix.com
-From: etrepum CVS
-Reply-To: twisted-python@twistedmatrix.com
-X-Mailer: CVSToys
-From: etrepum CVS
-Reply-To: twisted-python@twistedmatrix.com
-Message-Id:
-Subject: [Twisted-commits] Directories break debian build script, waiting for reasonable fix
-Sender: twisted-commits-admin@twistedmatrix.com
-Errors-To: twisted-commits-admin@twistedmatrix.com
-X-BeenThere: twisted-commits@twistedmatrix.com
-X-Mailman-Version: 2.0.11
-Precedence: bulk
-List-Help:
-List-Post:
-List-Subscribe: ,
-
-List-Id:
-List-Unsubscribe: ,
-
-List-Archive:
-Date: Fri, 17 Jan 2003 01:44:56 -0600
-Status:
-
-Modified files:
-Twisted/doc/examples/cocoaDemo/MyAppDelegate.py 1.1 None
-Twisted/doc/examples/cocoaDemo/__main__.py 1.1 None
-Twisted/doc/examples/cocoaDemo/bin-python-main.m 1.1 None
-Twisted/doc/examples/cocoaDemo/English.lproj/InfoPlist.strings 1.1 None
-Twisted/doc/examples/cocoaDemo/English.lproj/MainMenu.nib/classes.nib 1.1 None
-Twisted/doc/examples/cocoaDemo/English.lproj/MainMenu.nib/info.nib 1.1 None
-Twisted/doc/examples/cocoaDemo/English.lproj/MainMenu.nib/keyedobjects.nib 1.1 None
-Twisted/doc/examples/cocoaDemo/cocoaDemo.pbproj/project.pbxproj 1.1 None
-
-Log message:
-Directories break debian build script, waiting for reasonable fix
-
-
-ViewCVS links:
-http://twistedmatrix.com/users/jh.twistd/viewcvs/cgi/viewcvs.cgi/doc/examples/cocoaDemo/MyAppDelegate.py.diff?r1=text&tr1=1.1&r2=text&tr2=None&cvsroot=Twisted
-http://twistedmatrix.com/users/jh.twistd/viewcvs/cgi/viewcvs.cgi/doc/examples/cocoaDemo/__main__.py.diff?r1=text&tr1=1.1&r2=text&tr2=None&cvsroot=Twisted
-http://twistedmatrix.com/users/jh.twistd/viewcvs/cgi/viewcvs.cgi/doc/examples/cocoaDemo/bin-python-main.m.diff?r1=text&tr1=1.1&r2=text&tr2=None&cvsroot=Twisted
-http://twistedmatrix.com/users/jh.twistd/viewcvs/cgi/viewcvs.cgi/doc/examples/cocoaDemo/English.lproj/InfoPlist.strings.diff?r1=text&tr1=1.1&r2=text&tr2=None&cvsroot=Twisted
-http://twistedmatrix.com/users/jh.twistd/viewcvs/cgi/viewcvs.cgi/doc/examples/cocoaDemo/English.lproj/MainMenu.nib/classes.nib.diff?r1=text&tr1=1.1&r2=text&tr2=None&cvsroot=Twisted
-http://twistedmatrix.com/users/jh.twistd/viewcvs/cgi/viewcvs.cgi/doc/examples/cocoaDemo/English.lproj/MainMenu.nib/info.nib.diff?r1=text&tr1=1.1&r2=text&tr2=None&cvsroot=Twisted
-http://twistedmatrix.com/users/jh.twistd/viewcvs/cgi/viewcvs.cgi/doc/examples/cocoaDemo/English.lproj/MainMenu.nib/keyedobjects.nib.diff?r1=text&tr1=1.1&r2=text&tr2=None&cvsroot=Twisted
-http://twistedmatrix.com/users/jh.twistd/viewcvs/cgi/viewcvs.cgi/doc/examples/cocoaDemo/cocoaDemo.pbproj/project.pbxproj.diff?r1=text&tr1=1.1&r2=text&tr2=None&cvsroot=Twisted
-
-.
-
-_______________________________________________
-Twisted-commits mailing list
-Twisted-commits@twistedmatrix.com
-http://twistedmatrix.com/cgi-bin/mailman/listinfo/twisted-commits
diff --git a/tools/buildbot/pylibs/buildbot/test/mail/freshcvs.8 b/tools/buildbot/pylibs/buildbot/test/mail/freshcvs.8
deleted file mode 100644
index 9b1e4fd..0000000
--- a/tools/buildbot/pylibs/buildbot/test/mail/freshcvs.8
+++ /dev/null
@@ -1,61 +0,0 @@
-Return-Path:
-Delivered-To: warner-twistedcvs@luther.lothar.com
-Received: (qmail 10804 invoked by uid 1000); 19 Jan 2003 14:10:03 -0000
-Delivered-To: warner-twistedcvs@lothar.com
-Received: (qmail 6704 invoked by uid 13574); 19 Jan 2003 14:00:20 -0000
-Received: from unknown (HELO pyramid.twistedmatrix.com) ([64.123.27.105]) (envelope-sender )
- by 130.94.181.6 (qmail-ldap-1.03) with SMTP
- for ; 19 Jan 2003 14:00:20 -0000
-Received: from localhost ([127.0.0.1] helo=pyramid.twistedmatrix.com)
- by pyramid.twistedmatrix.com with esmtp (Exim 3.35 #1 (Debian))
- id 18aFtx-0002WS-00; Sun, 19 Jan 2003 07:54:17 -0600
-Received: from acapnotic by pyramid.twistedmatrix.com with local (Exim 3.35 #1 (Debian))
- id 18aFtH-0002W3-00
- for ; Sun, 19 Jan 2003 07:53:35 -0600
-To: twisted-commits@twistedmatrix.com
-From: acapnotic CVS
-X-Mailer: CVSToys
-Message-Id:
-Subject: [Twisted-commits] it doesn't work with invalid syntax
-Sender: twisted-commits-admin@twistedmatrix.com
-Errors-To: twisted-commits-admin@twistedmatrix.com
-X-BeenThere: twisted-commits@twistedmatrix.com
-X-Mailman-Version: 2.0.11
-Precedence: bulk
-List-Help:
-List-Post:
-List-Subscribe: ,
-
-List-Id:
-List-Unsubscribe: ,
-
-List-Archive:
-Date: Sun, 19 Jan 2003 07:53:35 -0600
-Status:
-
-Modified files:
-CVSROOT/freshCfg 1.16 1.17
-
-Log message:
-it doesn't work with invalid syntax
-
-
-Index: CVSROOT/freshCfg
-diff -u CVSROOT/freshCfg:1.16 CVSROOT/freshCfg:1.17
---- CVSROOT/freshCfg:1.16 Sun Jan 19 05:52:34 2003
-+++ CVSROOT/freshCfg Sun Jan 19 05:53:34 2003
-@@ -27,7 +27,7 @@
- ('/cvs', '^Reality', None, MailNotification(['reality-commits'])),
- ('/cvs', '^Twistby', None, MailNotification(['acapnotic'])),
- ('/cvs', '^CVSToys', None,
-- MailNotification(['CVSToys-list']
-+ MailNotification(['CVSToys-list'],
- "http://twistedmatrix.com/users/jh.twistd/"
- "viewcvs/cgi/viewcvs.cgi/",
- replyTo="cvstoys-list@twistedmatrix.com"),)
-
-
-_______________________________________________
-Twisted-commits mailing list
-Twisted-commits@twistedmatrix.com
-http://twistedmatrix.com/cgi-bin/mailman/listinfo/twisted-commits
diff --git a/tools/buildbot/pylibs/buildbot/test/mail/freshcvs.9 b/tools/buildbot/pylibs/buildbot/test/mail/freshcvs.9
deleted file mode 100644
index fd4f785..0000000
--- a/tools/buildbot/pylibs/buildbot/test/mail/freshcvs.9
+++ /dev/null
@@ -1,18 +0,0 @@
-From twisted-python@twistedmatrix.com Fri Dec 26 07:25:13 2003
-From: twisted-python@twistedmatrix.com (exarkun CVS)
-Date: Fri, 26 Dec 2003 00:25:13 -0700
-Subject: [Twisted-commits] Directory /cvs/Twisted/sandbox/exarkun/persist-plugin added to the repository
-Message-ID:
-
-Modified files:
-Twisted/sandbox/exarkun/persist-plugin
-
-Log message:
-Directory /cvs/Twisted/sandbox/exarkun/persist-plugin added to the repository
-
-
-ViewCVS links:
-http://cvs.twistedmatrix.com/cvs/sandbox/exarkun/persist-plugin?cvsroot=Twisted
-
-
-
diff --git a/tools/buildbot/pylibs/buildbot/test/mail/svn-commit.1 b/tools/buildbot/pylibs/buildbot/test/mail/svn-commit.1
deleted file mode 100644
index 591dfee..0000000
--- a/tools/buildbot/pylibs/buildbot/test/mail/svn-commit.1
+++ /dev/null
@@ -1,67 +0,0 @@
-X-Original-To: jm@jmason.org
-Delivered-To: jm@dogma.boxhost.net
-Received: from localhost [127.0.0.1]
- by localhost with IMAP (fetchmail-6.2.5)
- for jm@localhost (single-drop); Wed, 12 Apr 2006 01:52:04 +0100 (IST)
-Received: from mail.apache.org (hermes.apache.org [209.237.227.199])
- by dogma.boxhost.net (Postfix) with SMTP id 34F07310051
- for ; Wed, 12 Apr 2006 01:44:17 +0100 (IST)
-Received: (qmail 71414 invoked by uid 500); 12 Apr 2006 00:44:16 -0000
-Mailing-List: contact commits-help@spamassassin.apache.org; run by ezmlm
-Precedence: bulk
-list-help:
-list-unsubscribe:
-List-Post:
-Reply-To: "SpamAssassin Dev"
-List-Id:
-Delivered-To: mailing list commits@spamassassin.apache.org
-Received: (qmail 71403 invoked by uid 99); 12 Apr 2006 00:44:16 -0000
-Received: from asf.osuosl.org (HELO asf.osuosl.org) (140.211.166.49)
- by apache.org (qpsmtpd/0.29) with ESMTP; Tue, 11 Apr 2006 17:44:16 -0700
-X-ASF-Spam-Status: No, hits=-9.4 required=10.0
- tests=ALL_TRUSTED,NO_REAL_NAME
-Received: from [209.237.227.194] (HELO minotaur.apache.org) (209.237.227.194)
- by apache.org (qpsmtpd/0.29) with SMTP; Tue, 11 Apr 2006 17:44:15 -0700
-Received: (qmail 51950 invoked by uid 65534); 12 Apr 2006 00:43:55 -0000
-Message-ID: <20060412004355.51949.qmail@minotaur.apache.org>
-Content-Type: text/plain; charset="utf-8"
-MIME-Version: 1.0
-Content-Transfer-Encoding: 7bit
-Subject: svn commit: r393348 - /spamassassin/trunk/sa-update.raw
-Date: Wed, 12 Apr 2006 00:43:54 -0000
-To: commits@spamassassin.apache.org
-From: felicity@apache.org
-X-Mailer: svnmailer-1.0.7
-X-Virus-Checked: Checked by ClamAV on apache.org
-Status: O
-X-UID: 62932
-X-Keywords:
-
-Author: felicity
-Date: Tue Apr 11 17:43:54 2006
-New Revision: 393348
-
-URL: http://svn.apache.org/viewcvs?rev=393348&view=rev
-Log:
-bug 4864: remove extraneous front-slash from gpghomedir path
-
-Modified:
- spamassassin/trunk/sa-update.raw
-
-Modified: spamassassin/trunk/sa-update.raw
-URL: http://svn.apache.org/viewcvs/spamassassin/trunk/sa-update.raw?rev=393348&r1=393347&r2=393348&view=diff
-==============================================================================
---- spamassassin/trunk/sa-update.raw (original)
-+++ spamassassin/trunk/sa-update.raw Tue Apr 11 17:43:54 2006
-@@ -120,7 +120,7 @@
- @{$opt{'channel'}} = ();
- my $GPG_ENABLED = 1;
-
--$opt{'gpghomedir'} = File::Spec->catfile($LOCAL_RULES_DIR, '/sa-update-keys');
-+$opt{'gpghomedir'} = File::Spec->catfile($LOCAL_RULES_DIR, 'sa-update-keys');
-
- Getopt::Long::Configure(
- qw(bundling no_getopt_compat no_auto_abbrev no_ignore_case));
-
-
-
diff --git a/tools/buildbot/pylibs/buildbot/test/mail/svn-commit.2 b/tools/buildbot/pylibs/buildbot/test/mail/svn-commit.2
deleted file mode 100644
index eeef001..0000000
--- a/tools/buildbot/pylibs/buildbot/test/mail/svn-commit.2
+++ /dev/null
@@ -1,1218 +0,0 @@
-X-Original-To: jm@jmason.org
-Delivered-To: jm@dogma.boxhost.net
-Received: from localhost [127.0.0.1]
- by localhost with IMAP (fetchmail-6.2.5)
- for jm@localhost (single-drop); Thu, 09 Mar 2006 21:44:57 +0000 (GMT)
-Received: from minotaur.apache.org (minotaur.apache.org [209.237.227.194])
- by dogma.boxhost.net (Postfix) with SMTP id 0D3463105BF
- for ; Thu, 9 Mar 2006 19:52:50 +0000 (GMT)
-Received: (qmail 30661 invoked by uid 1833); 9 Mar 2006 19:52:44 -0000
-Delivered-To: jm@locus.apache.org
-Received: (qmail 30451 invoked from network); 9 Mar 2006 19:52:38 -0000
-Received: from hermes.apache.org (HELO mail.apache.org) (209.237.227.199)
- by minotaur.apache.org with SMTP; 9 Mar 2006 19:52:38 -0000
-Received: (qmail 97860 invoked by uid 500); 9 Mar 2006 19:52:29 -0000
-Delivered-To: apmail-jm@apache.org
-Received: (qmail 97837 invoked by uid 500); 9 Mar 2006 19:52:28 -0000
-Mailing-List: contact commits-help@spamassassin.apache.org; run by ezmlm
-Precedence: bulk
-list-help:
-list-unsubscribe:
-List-Post:
-Reply-To: "SpamAssassin Dev"
-List-Id:
-Delivered-To: mailing list commits@spamassassin.apache.org
-Received: (qmail 97826 invoked by uid 99); 9 Mar 2006 19:52:28 -0000
-Received: from asf.osuosl.org (HELO asf.osuosl.org) (140.211.166.49)
- by apache.org (qpsmtpd/0.29) with ESMTP; Thu, 09 Mar 2006 11:52:28 -0800
-X-ASF-Spam-Status: No, hits=-9.4 required=10.0
- tests=ALL_TRUSTED,NO_REAL_NAME
-Received: from [209.237.227.194] (HELO minotaur.apache.org) (209.237.227.194)
- by apache.org (qpsmtpd/0.29) with SMTP; Thu, 09 Mar 2006 11:52:26 -0800
-Received: (qmail 29644 invoked by uid 65534); 9 Mar 2006 19:52:06 -0000
-Message-ID: <20060309195206.29643.qmail@minotaur.apache.org>
-Content-Type: text/plain; charset="utf-8"
-MIME-Version: 1.0
-Content-Transfer-Encoding: 7bit
-Subject: svn commit: r384590 - in /spamassassin/branches/3.1: ./
- lib/Mail/SpamAssassin/ lib/Mail/SpamAssassin/Plugin/ spamd/
-Date: Thu, 09 Mar 2006 19:52:02 -0000
-To: commits@spamassassin.apache.org
-From: sidney@apache.org
-X-Mailer: svnmailer-1.0.7
-X-Virus-Checked: Checked by ClamAV on apache.org
-Status: O
-X-UID: 60795
-X-Keywords:
-
-Author: sidney
-Date: Thu Mar 9 11:51:59 2006
-New Revision: 384590
-
-URL: http://svn.apache.org/viewcvs?rev=384590&view=rev
-Log:
-Bug 4696: consolidated fixes for timeout bugs
-
-Added:
- spamassassin/branches/3.1/lib/Mail/SpamAssassin/Timeout.pm
-Modified:
- spamassassin/branches/3.1/MANIFEST
- spamassassin/branches/3.1/lib/Mail/SpamAssassin/Logger.pm
- spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/DCC.pm
- spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/DomainKeys.pm
- spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/Pyzor.pm
- spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/Razor2.pm
- spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/SPF.pm
- spamassassin/branches/3.1/lib/Mail/SpamAssassin/SpamdForkScaling.pm
- spamassassin/branches/3.1/spamd/spamd.raw
-
-Modified: spamassassin/branches/3.1/MANIFEST
-URL: http://svn.apache.org/viewcvs/spamassassin/branches/3.1/MANIFEST?rev=384590&r1=384589&r2=384590&view=diff
-==============================================================================
---- spamassassin/branches/3.1/MANIFEST (original)
-+++ spamassassin/branches/3.1/MANIFEST Thu Mar 9 11:51:59 2006
-@@ -89,6 +89,7 @@
- lib/Mail/SpamAssassin/SQLBasedAddrList.pm
- lib/Mail/SpamAssassin/SpamdForkScaling.pm
- lib/Mail/SpamAssassin/SubProcBackChannel.pm
-+lib/Mail/SpamAssassin/Timeout.pm
- lib/Mail/SpamAssassin/Util.pm
- lib/Mail/SpamAssassin/Util/DependencyInfo.pm
- lib/Mail/SpamAssassin/Util/Progress.pm
-
-Modified: spamassassin/branches/3.1/lib/Mail/SpamAssassin/Logger.pm
-URL: http://svn.apache.org/viewcvs/spamassassin/branches/3.1/lib/Mail/SpamAssassin/Logger.pm?rev=384590&r1=384589&r2=384590&view=diff
-==============================================================================
---- spamassassin/branches/3.1/lib/Mail/SpamAssassin/Logger.pm (original)
-+++ spamassassin/branches/3.1/lib/Mail/SpamAssassin/Logger.pm Thu Mar 9 11:51:59 2006
-@@ -142,7 +142,7 @@
-
- if ($level eq "error") {
- # don't log alarm timeouts or broken pipes of various plugins' network checks
-- return if ($message[0] =~ /__(?:alarm|brokenpipe)__ignore__/);
-+ return if ($message[0] =~ /__ignore__/);
-
- # dos: we can safely ignore any die's that we eval'd in our own modules so
- # don't log them -- this is caller 0, the use'ing package is 1, the eval is 2
-
-Modified: spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/DCC.pm
-URL: http://svn.apache.org/viewcvs/spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/DCC.pm?rev=384590&r1=384589&r2=384590&view=diff
-==============================================================================
---- spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/DCC.pm (original)
-+++ spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/DCC.pm Thu Mar 9 11:51:59 2006
-@@ -44,6 +44,7 @@
-
- use Mail::SpamAssassin::Plugin;
- use Mail::SpamAssassin::Logger;
-+use Mail::SpamAssassin::Timeout;
- use IO::Socket;
- use strict;
- use warnings;
-@@ -375,15 +376,10 @@
-
- $permsgstatus->enter_helper_run_mode();
-
-- my $oldalarm = 0;
-+ my $timer = Mail::SpamAssassin::Timeout->new({ secs => $timeout });
-+ my $err = $timer->run_and_catch(sub {
-
-- eval {
-- # safe to use $SIG{ALRM} here instead of Util::trap_sigalrm_fully(),
-- # since there are no killer regexp hang dangers here
-- local $SIG{ALRM} = sub { die "__alarm__ignore__\n" };
-- local $SIG{__DIE__}; # bug 4631
--
-- $oldalarm = alarm $timeout;
-+ local $SIG{PIPE} = sub { die "__brokenpipe__ignore__\n" };
-
- my $sock = IO::Socket::UNIX->new(Type => SOCK_STREAM,
- Peer => $sockpath) || dbg("dcc: failed to open socket") && die;
-@@ -419,28 +415,20 @@
- }
-
- dbg("dcc: dccifd got response: $response");
-+
-+ });
-
-- if (defined $oldalarm) {
-- alarm $oldalarm; $oldalarm = undef;
-- }
-- };
-+ $permsgstatus->leave_helper_run_mode();
-
-- my $err = $@;
-- if (defined $oldalarm) {
-- alarm $oldalarm; $oldalarm = undef;
-+ if ($timer->timed_out()) {
-+ dbg("dcc: dccifd check timed out after $timeout secs.");
-+ return 0;
- }
-- $permsgstatus->leave_helper_run_mode();
-
- if ($err) {
- chomp $err;
-- $response = undef;
-- if ($err eq "__alarm__ignore__") {
-- dbg("dcc: dccifd check timed out after $timeout secs.");
-- return 0;
-- } else {
-- warn("dcc: dccifd -> check skipped: $! $err");
-- return 0;
-- }
-+ warn("dcc: dccifd -> check skipped: $! $err");
-+ return 0;
- }
-
- if (!defined $response || $response !~ /^X-DCC/) {
-@@ -494,17 +482,12 @@
-
- # use a temp file here -- open2() is unreliable, buffering-wise, under spamd
- my $tmpf = $permsgstatus->create_fulltext_tmpfile($fulltext);
-- my $oldalarm = 0;
--
- my $pid;
-- eval {
-- # safe to use $SIG{ALRM} here instead of Util::trap_sigalrm_fully(),
-- # since there are no killer regexp hang dangers here
-- local $SIG{ALRM} = sub { die "__alarm__ignore__\n" };
-- local $SIG{PIPE} = sub { die "__brokenpipe__ignore__\n" };
-- local $SIG{__DIE__}; # bug 4631
-
-- $oldalarm = alarm $timeout;
-+ my $timer = Mail::SpamAssassin::Timeout->new({ secs => $timeout });
-+ my $err = $timer->run_and_catch(sub {
-+
-+ local $SIG{PIPE} = sub { die "__brokenpipe__ignore__\n" };
-
- # note: not really tainted, this came from system configuration file
- my $path = Mail::SpamAssassin::Util::untaint_file_path($self->{main}->{conf}->{dcc_path});
-@@ -542,17 +525,7 @@
-
- dbg("dcc: got response: $response");
-
-- # note: this must be called BEFORE leave_helper_run_mode()
-- # $self->cleanup_kids($pid);
-- if (defined $oldalarm) {
-- alarm $oldalarm; $oldalarm = undef;
-- }
-- };
--
-- my $err = $@;
-- if (defined $oldalarm) {
-- alarm $oldalarm; $oldalarm = undef;
-- }
-+ });
-
- if (defined(fileno(*DCC))) { # still open
- if ($pid) {
-@@ -564,11 +537,14 @@
- }
- $permsgstatus->leave_helper_run_mode();
-
-+ if ($timer->timed_out()) {
-+ dbg("dcc: check timed out after $timeout seconds");
-+ return 0;
-+ }
-+
- if ($err) {
- chomp $err;
-- if ($err eq "__alarm__ignore__") {
-- dbg("dcc: check timed out after $timeout seconds");
-- } elsif ($err eq "__brokenpipe__ignore__") {
-+ if ($err eq "__brokenpipe__ignore__") {
- dbg("dcc: check failed: broken pipe");
- } elsif ($err eq "no response") {
- dbg("dcc: check failed: no response");
-@@ -645,47 +621,37 @@
- my ($self, $options, $tmpf) = @_;
- my $timeout = $options->{report}->{conf}->{dcc_timeout};
-
-- $options->{report}->enter_helper_run_mode();
-+ # note: not really tainted, this came from system configuration file
-+ my $path = Mail::SpamAssassin::Util::untaint_file_path($options->{report}->{conf}->{dcc_path});
-
-- my $oldalarm = 0;
-+ my $opts = $options->{report}->{conf}->{dcc_options} || '';
-
-- eval {
-- local $SIG{ALRM} = sub { die "__alarm__ignore__\n" };
-- local $SIG{PIPE} = sub { die "__brokenpipe__ignore__\n" };
-- local $SIG{__DIE__}; # bug 4631
-+ my $timer = Mail::SpamAssassin::Timeout->new({ secs => $timeout });
-
-- $oldalarm = alarm $timeout;
--
-- # note: not really tainted, this came from system configuration file
-- my $path = Mail::SpamAssassin::Util::untaint_file_path($options->{report}->{conf}->{dcc_path});
-+ $options->{report}->enter_helper_run_mode();
-+ my $err = $timer->run_and_catch(sub {
-
-- my $opts = $options->{report}->{conf}->{dcc_options} || '';
-+ local $SIG{PIPE} = sub { die "__brokenpipe__ignore__\n" };
-
- my $pid = Mail::SpamAssassin::Util::helper_app_pipe_open(*DCC,
-- $tmpf, 1, $path, "-t", "many", split(' ', $opts));
-+ $tmpf, 1, $path, "-t", "many", split(' ', $opts));
- $pid or die "$!\n";
-
- my @ignored = ;
- $options->{report}->close_pipe_fh(\*DCC);
--
- waitpid ($pid, 0);
-- if (defined $oldalarm) {
-- alarm $oldalarm; $oldalarm = undef;
-- }
-- };
-+
-+ });
-+ $options->{report}->leave_helper_run_mode();
-
-- my $err = $@;
-- if (defined $oldalarm) {
-- alarm $oldalarm; $oldalarm = undef;
-+ if ($timer->timed_out()) {
-+ dbg("reporter: DCC report timed out after $timeout seconds");
-+ return 0;
- }
-
-- $options->{report}->leave_helper_run_mode();
--
- if ($err) {
- chomp $err;
-- if ($err eq "__alarm__ignore__") {
-- dbg("reporter: DCC report timed out after $timeout seconds");
-- } elsif ($err eq "__brokenpipe__ignore__") {
-+ if ($err eq "__brokenpipe__ignore__") {
- dbg("reporter: DCC report failed: broken pipe");
- } else {
- warn("reporter: DCC report failed: $err\n");
-
-Modified: spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/DomainKeys.pm
-URL: http://svn.apache.org/viewcvs/spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/DomainKeys.pm?rev=384590&r1=384589&r2=384590&view=diff
-==============================================================================
---- spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/DomainKeys.pm (original)
-+++ spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/DomainKeys.pm Thu Mar 9 11:51:59 2006
-@@ -34,6 +34,8 @@
-
- use Mail::SpamAssassin::Plugin;
- use Mail::SpamAssassin::Logger;
-+use Mail::SpamAssassin::Timeout;
-+
- use strict;
- use warnings;
- use bytes;
-@@ -165,30 +167,22 @@
- }
-
- my $timeout = $scan->{conf}->{domainkeys_timeout};
-- my $oldalarm = 0;
-
-- eval {
-- local $SIG{ALRM} = sub { die "__alarm__ignore__\n" };
-- local $SIG{__DIE__}; # bug 4631
-- $oldalarm = alarm($timeout);
-+ my $timer = Mail::SpamAssassin::Timeout->new({ secs => $timeout });
-+ my $err = $timer->run_and_catch(sub {
-+
- $self->_dk_lookup_trapped($scan, $message, $domain);
-- if (defined $oldalarm) {
-- alarm $oldalarm; $oldalarm = undef;
-- }
-- };
--
-- my $err = $@;
-- if (defined $oldalarm) {
-- alarm $oldalarm; $oldalarm = undef;
-+
-+ });
-+
-+ if ($timer->timed_out()) {
-+ dbg("dk: lookup timed out after $timeout seconds");
-+ return 0;
- }
-
- if ($err) {
- chomp $err;
-- if ($err eq "__alarm__ignore__") {
-- dbg("dk: lookup timed out after $timeout seconds");
-- } else {
-- warn("dk: lookup failed: $err\n");
-- }
-+ warn("dk: lookup failed: $err\n");
- return 0;
- }
-
-
-Modified: spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/Pyzor.pm
-URL: http://svn.apache.org/viewcvs/spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/Pyzor.pm?rev=384590&r1=384589&r2=384590&view=diff
-==============================================================================
---- spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/Pyzor.pm (original)
-+++ spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/Pyzor.pm Thu Mar 9 11:51:59 2006
-@@ -35,6 +35,7 @@
-
- use Mail::SpamAssassin::Plugin;
- use Mail::SpamAssassin::Logger;
-+use Mail::SpamAssassin::Timeout;
- use strict;
- use warnings;
- use bytes;
-@@ -229,27 +230,22 @@
-
- $pyzor_count = 0;
- $pyzor_whitelisted = 0;
--
-- $permsgstatus->enter_helper_run_mode();
-+ my $pid;
-
- # use a temp file here -- open2() is unreliable, buffering-wise, under spamd
- my $tmpf = $permsgstatus->create_fulltext_tmpfile($fulltext);
-- my $oldalarm = 0;
-
-- my $pid;
-- eval {
-- # safe to use $SIG{ALRM} here instead of Util::trap_sigalrm_fully(),
-- # since there are no killer regexp hang dangers here
-- local $SIG{ALRM} = sub { die "__alarm__ignore__\n" };
-- local $SIG{PIPE} = sub { die "__brokenpipe__ignore__\n" };
-- local $SIG{__DIE__}; # bug 4631
-+ # note: not really tainted, this came from system configuration file
-+ my $path = Mail::SpamAssassin::Util::untaint_file_path($self->{main}->{conf}->{pyzor_path});
-+
-+ my $opts = $self->{main}->{conf}->{pyzor_options} || '';
-
-- $oldalarm = alarm $timeout;
-+ $permsgstatus->enter_helper_run_mode();
-
-- # note: not really tainted, this came from system configuration file
-- my $path = Mail::SpamAssassin::Util::untaint_file_path($self->{main}->{conf}->{pyzor_path});
-+ my $timer = Mail::SpamAssassin::Timeout->new({ secs => $timeout });
-+ my $err = $timer->run_and_catch(sub {
-
-- my $opts = $self->{main}->{conf}->{pyzor_options} || '';
-+ local $SIG{PIPE} = sub { die "__brokenpipe__ignore__\n" };
-
- dbg("pyzor: opening pipe: " . join(' ', $path, $opts, "check", "< $tmpf"));
-
-@@ -273,21 +269,7 @@
- die("internal error\n");
- }
-
-- # note: this must be called BEFORE leave_helper_run_mode()
-- # $self->cleanup_kids($pid);
--
-- # attempt to call this inside the eval, as leaving this scope is
-- # a slow operation and timing *that* out is pointless
-- if (defined $oldalarm) {
-- alarm $oldalarm; $oldalarm = undef;
-- }
-- };
--
-- # clear the alarm before doing lots of time-consuming hard work
-- my $err = $@;
-- if (defined $oldalarm) {
-- alarm $oldalarm; $oldalarm = undef;
-- }
-+ });
-
- if (defined(fileno(*PYZOR))) { # still open
- if ($pid) {
-@@ -299,11 +281,14 @@
- }
- $permsgstatus->leave_helper_run_mode();
-
-+ if ($timer->timed_out()) {
-+ dbg("pyzor: check timed out after $timeout seconds");
-+ return 0;
-+ }
-+
- if ($err) {
- chomp $err;
-- if ($err eq "__alarm__ignore__") {
-- dbg("pyzor: check timed out after $timeout seconds");
-- } elsif ($err eq "__brokenpipe__ignore__") {
-+ if ($err eq "__brokenpipe__ignore__") {
- dbg("pyzor: check failed: broken pipe");
- } elsif ($err eq "no response") {
- dbg("pyzor: check failed: no response");
-@@ -364,23 +349,19 @@
-
- sub pyzor_report {
- my ($self, $options, $tmpf) = @_;
-+
-+ # note: not really tainted, this came from system configuration file
-+ my $path = Mail::SpamAssassin::Util::untaint_file_path($options->{report}->{conf}->{pyzor_path});
-+
-+ my $opts = $options->{report}->{conf}->{pyzor_options} || '';
- my $timeout = $self->{main}->{conf}->{pyzor_timeout};
-
- $options->{report}->enter_helper_run_mode();
-
-- my $oldalarm = 0;
-+ my $timer = Mail::SpamAssassin::Timeout->new({ secs => $timeout });
-+ my $err = $timer->run_and_catch(sub {
-
-- eval {
-- local $SIG{ALRM} = sub { die "__alarm__ignore__\n" };
- local $SIG{PIPE} = sub { die "__brokenpipe__ignore__\n" };
-- local $SIG{__DIE__}; # bug 4631
--
-- $oldalarm = alarm $timeout;
--
-- # note: not really tainted, this came from system configuration file
-- my $path = Mail::SpamAssassin::Util::untaint_file_path($options->{report}->{conf}->{pyzor_path});
--
-- my $opts = $options->{report}->{conf}->{pyzor_options} || '';
-
- dbg("pyzor: opening pipe: " . join(' ', $path, $opts, "report", "< $tmpf"));
-
-@@ -391,23 +372,19 @@
- my @ignored = ;
- $options->{report}->close_pipe_fh(\*PYZOR);
-
-- if (defined $oldalarm) {
-- alarm $oldalarm; $oldalarm = undef;
-- }
- waitpid ($pid, 0);
-- };
-+ });
-
-- my $err = $@;
-- if (defined $oldalarm) {
-- alarm $oldalarm; $oldalarm = undef;
-- }
- $options->{report}->leave_helper_run_mode();
-
-+ if ($timer->timed_out()) {
-+ dbg("reporter: pyzor report timed out after $timeout seconds");
-+ return 0;
-+ }
-+
- if ($err) {
- chomp $err;
-- if ($err eq '__alarm__ignore__') {
-- dbg("reporter: pyzor report timed out after $timeout seconds");
-- } elsif ($err eq '__brokenpipe__ignore__') {
-+ if ($err eq '__brokenpipe__ignore__') {
- dbg("reporter: pyzor report failed: broken pipe");
- } else {
- warn("reporter: pyzor report failed: $err\n");
-
-Modified: spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/Razor2.pm
-URL: http://svn.apache.org/viewcvs/spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/Razor2.pm?rev=384590&r1=384589&r2=384590&view=diff
-==============================================================================
---- spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/Razor2.pm (original)
-+++ spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/Razor2.pm Thu Mar 9 11:51:59 2006
-@@ -143,14 +143,11 @@
- }
-
- Mail::SpamAssassin::PerMsgStatus::enter_helper_run_mode($self);
-- my $oldalarm = 0;
-
-- eval {
-- local ($^W) = 0; # argh, warnings in Razor
-+ my $timer = Mail::SpamAssassin::Timeout->new({ secs => $timeout });
-+ my $err = $timer->run_and_catch(sub {
-
-- local $SIG{ALRM} = sub { die "__alarm__ignore__\n" };
-- local $SIG{__DIE__}; # bug 4631
-- $oldalarm = alarm $timeout;
-+ local ($^W) = 0; # argh, warnings in Razor
-
- # everything's in the module!
- my $rc = Razor2::Client::Agent->new("razor-$type");
-@@ -184,7 +181,7 @@
- # let's reset the alarm since get_server_info() calls
- # nextserver() which calls discover() which very likely will
- # reset the alarm for us ... how polite. :(
-- alarm $timeout;
-+ $timer->reset();
-
- # no facility prefix on this die
- my $sigs = $rc->compute_sigs($objects)
-@@ -219,100 +216,96 @@
- my $error = $rc->errprefix("$debug: spamassassin") || "$debug: razor2 had unknown error during disconnect";
- die $error;
- }
-+ }
-
-- # if we got here, we're done doing remote stuff, abort the alert
-- if (defined $oldalarm) {
-- alarm $oldalarm; $oldalarm = undef;
-- }
--
-- # Razor 2.14 says that if we get here, we did ok.
-- $return = 1;
-+ # Razor 2.14 says that if we get here, we did ok.
-+ $return = 1;
-
-- # figure out if we have a log file we need to close...
-- if (ref($rc->{logref}) && exists $rc->{logref}->{fd}) {
-- # the fd can be stdout or stderr, so we need to find out if it is
-- # so we don't close them by accident. Note: we can't just
-- # undef the fd here (like the IO::Handle manpage says we can)
-- # because it won't actually close, unfortunately. :(
-- my $untie = 1;
-- foreach my $log (*STDOUT{IO}, *STDERR{IO}) {
-- if ($log == $rc->{logref}->{fd}) {
-- $untie = 0;
-- last;
-- }
-- }
-- close $rc->{logref}->{fd} if ($untie);
-- }
--
-- if ($type eq 'check') {
-- # so $objects->[0] is the first (only) message, and ->{spam} is a general yes/no
-- push(@results, { result => $objects->[0]->{spam} });
-+ # figure out if we have a log file we need to close...
-+ if (ref($rc->{logref}) && exists $rc->{logref}->{fd}) {
-+ # the fd can be stdout or stderr, so we need to find out if it is
-+ # so we don't close them by accident. Note: we can't just
-+ # undef the fd here (like the IO::Handle manpage says we can)
-+ # because it won't actually close, unfortunately. :(
-+ my $untie = 1;
-+ foreach my $log (*STDOUT{IO}, *STDERR{IO}) {
-+ if ($log == $rc->{logref}->{fd}) {
-+ $untie = 0;
-+ last;
-+ }
-+ }
-+ close $rc->{logref}->{fd} if ($untie);
-+ }
-
-- # great for debugging, but leave this off!
-- #use Data::Dumper;
-- #print Dumper($objects),"\n";
--
-- # ->{p} is for each part of the message
-- # so go through each part, taking the highest cf we find
-- # of any part that isn't contested (ct). This helps avoid false
-- # positives. equals logic_method 4.
-- #
-- # razor-agents < 2.14 have a different object format, so we now support both.
-- # $objects->[0]->{resp} vs $objects->[0]->{p}->[part #]->{resp}
-- my $part = 0;
-- my $arrayref = $objects->[0]->{p} || $objects;
-- if (defined $arrayref) {
-- foreach my $cf (@{$arrayref}) {
-- if (exists $cf->{resp}) {
-- for (my $response=0; $response<@{$cf->{resp}}; $response++) {
-- my $tmp = $cf->{resp}->[$response];
-- my $tmpcf = $tmp->{cf}; # Part confidence
-- my $tmpct = $tmp->{ct}; # Part contested?
-- my $engine = $cf->{sent}->[$response]->{e};
--
-- # These should always be set, but just in case ...
-- $tmpcf = 0 unless defined $tmpcf;
-- $tmpct = 0 unless defined $tmpct;
-- $engine = 0 unless defined $engine;
--
-- push(@results,
-- { part => $part, engine => $engine, contested => $tmpct, confidence => $tmpcf });
-- }
-- }
-- else {
-- push(@results, { part => $part, noresponse => 1 });
-- }
-- $part++;
-- }
-- }
-- else {
-- # If we have some new $objects format that isn't close to
-- # the current razor-agents 2.x version, we won't FP but we
-- # should alert in debug.
-- dbg("$debug: it looks like the internal Razor object has changed format!");
-- }
-- }
-+ if ($type eq 'check') {
-+ # so $objects->[0] is the first (only) message, and ->{spam} is a general yes/no
-+ push(@results, { result => $objects->[0]->{spam} });
-+
-+ # great for debugging, but leave this off!
-+ #use Data::Dumper;
-+ #print Dumper($objects),"\n";
-+
-+ # ->{p} is for each part of the message
-+ # so go through each part, taking the highest cf we find
-+ # of any part that isn't contested (ct). This helps avoid false
-+ # positives. equals logic_method 4.
-+ #
-+ # razor-agents < 2.14 have a different object format, so we now support both.
-+ # $objects->[0]->{resp} vs $objects->[0]->{p}->[part #]->{resp}
-+ my $part = 0;
-+ my $arrayref = $objects->[0]->{p} || $objects;
-+ if (defined $arrayref) {
-+ foreach my $cf (@{$arrayref}) {
-+ if (exists $cf->{resp}) {
-+ for (my $response=0; $response<@{$cf->{resp}}; $response++) {
-+ my $tmp = $cf->{resp}->[$response];
-+ my $tmpcf = $tmp->{cf}; # Part confidence
-+ my $tmpct = $tmp->{ct}; # Part contested?
-+ my $engine = $cf->{sent}->[$response]->{e};
-+
-+ # These should always be set, but just in case ...
-+ $tmpcf = 0 unless defined $tmpcf;
-+ $tmpct = 0 unless defined $tmpct;
-+ $engine = 0 unless defined $engine;
-+
-+ push(@results,
-+ { part => $part, engine => $engine, contested => $tmpct, confidence => $tmpcf });
-+ }
-+ }
-+ else {
-+ push(@results, { part => $part, noresponse => 1 });
-+ }
-+ $part++;
-+ }
-+ }
-+ else {
-+ # If we have some new $objects format that isn't close to
-+ # the current razor-agents 2.x version, we won't FP but we
-+ # should alert in debug.
-+ dbg("$debug: it looks like the internal Razor object has changed format!");
-+ }
- }
- }
- else {
- warn "$debug: undefined Razor2::Client::Agent\n";
- }
-
-- if (defined $oldalarm) {
-- alarm $oldalarm; $oldalarm = undef;
-- }
-- };
-+ });
-+
-+ # OK, that's enough Razor stuff. now, reset all that global
-+ # state it futzes with :(
-+ # work around serious brain damage in Razor2 (constant seed)
-+ srand;
-
-- my $err = $@;
-- if (defined $oldalarm) {
-- alarm $oldalarm; $oldalarm = undef;
-+ Mail::SpamAssassin::PerMsgStatus::leave_helper_run_mode($self);
-+
-+ if ($timer->timed_out()) {
-+ dbg("$debug: razor2 $type timed out after $timeout seconds");
- }
-
- if ($err) {
- chomp $err;
-- if ($err eq "__alarm__ignore__") {
-- dbg("$debug: razor2 $type timed out after $timeout seconds");
-- } elsif ($err =~ /(?:could not connect|network is unreachable)/) {
-+ if ($err =~ /(?:could not connect|network is unreachable)/) {
- # make this a dbg(); SpamAssassin will still continue,
- # but without Razor checking. otherwise there may be
- # DSNs and errors in syslog etc., yuck
-@@ -323,11 +316,6 @@
- warn("$debug: razor2 $type failed: $! $err");
- }
- }
--
-- # work around serious brain damage in Razor2 (constant seed)
-- srand;
--
-- Mail::SpamAssassin::PerMsgStatus::leave_helper_run_mode($self);
-
- # razor also debugs to stdout. argh. fix it to stderr...
- if (would_log('dbg', $debug)) {
-
-Modified: spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/SPF.pm
-URL: http://svn.apache.org/viewcvs/spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/SPF.pm?rev=384590&r1=384589&r2=384590&view=diff
-==============================================================================
---- spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/SPF.pm (original)
-+++ spamassassin/branches/3.1/lib/Mail/SpamAssassin/Plugin/SPF.pm Thu Mar 9 11:51:59 2006
-@@ -34,6 +34,7 @@
-
- use Mail::SpamAssassin::Plugin;
- use Mail::SpamAssassin::Logger;
-+use Mail::SpamAssassin::Timeout;
- use strict;
- use warnings;
- use bytes;
-@@ -300,30 +301,17 @@
-
- my ($result, $comment);
- my $timeout = $scanner->{conf}->{spf_timeout};
-- my $oldalarm = 0;
-
-- eval {
-- local $SIG{ALRM} = sub { die "__alarm__ignore__\n" };
-- local $SIG{__DIE__}; # bug 4631
-- $oldalarm = alarm($timeout);
-+ my $timer = Mail::SpamAssassin::Timeout->new({ secs => $timeout });
-+ my $err = $timer->run_and_catch(sub {
-+
- ($result, $comment) = $query->result();
-- if (defined $oldalarm) {
-- alarm $oldalarm; $oldalarm = undef;
-- }
-- };
-
-- my $err = $@;
-- if (defined $oldalarm) {
-- alarm $oldalarm; $oldalarm = undef;
-- }
-+ });
-
- if ($err) {
- chomp $err;
-- if ($err eq "__alarm__ignore__") {
-- dbg("spf: lookup timed out after $timeout seconds");
-- } else {
-- warn("spf: lookup failed: $err\n");
-- }
-+ warn("spf: lookup failed: $err\n");
- return 0;
- }
-
-
-Modified: spamassassin/branches/3.1/lib/Mail/SpamAssassin/SpamdForkScaling.pm
-URL: http://svn.apache.org/viewcvs/spamassassin/branches/3.1/lib/Mail/SpamAssassin/SpamdForkScaling.pm?rev=384590&r1=384589&r2=384590&view=diff
-==============================================================================
---- spamassassin/branches/3.1/lib/Mail/SpamAssassin/SpamdForkScaling.pm (original)
-+++ spamassassin/branches/3.1/lib/Mail/SpamAssassin/SpamdForkScaling.pm Thu Mar 9 11:51:59 2006
-@@ -25,6 +25,7 @@
-
- use Mail::SpamAssassin::Util;
- use Mail::SpamAssassin::Logger;
-+use Mail::SpamAssassin::Timeout;
-
- use vars qw {
- @PFSTATE_VARS %EXPORT_TAGS @EXPORT_OK
-@@ -109,6 +110,9 @@
-
- delete $self->{kids}->{$pid};
-
-+ # note this for the select()-caller's benefit
-+ $self->{child_just_exited} = 1;
-+
- # remove the child from the backchannel list, too
- $self->{backchannel}->delete_socket_for_child($pid);
-
-@@ -188,24 +192,63 @@
- vec($rin, $self->{server_fileno}, 1) = 0;
- }
-
-- my ($rout, $eout, $nfound, $timeleft);
-+ my ($rout, $eout, $nfound, $timeleft, $selerr);
-+
-+ # use alarm to back up select()'s built-in alarm, to debug Theo's bug.
-+ # not that I can remember what Theo's bug was, but hey ;) A good
-+ # 60 seconds extra on the alarm() should make that quite rare...
-+
-+ my $timer = Mail::SpamAssassin::Timeout->new({ secs => ($tout*2) + 60 });
-
-- # use alarm to back up select()'s built-in alarm, to debug theo's bug
-- eval {
-- Mail::SpamAssassin::Util::trap_sigalrm_fully(sub { die "tcp timeout"; });
-- alarm ($tout*2) if ($tout);
-+ $timer->run(sub {
-+
-+ $self->{child_just_exited} = 0;
- ($nfound, $timeleft) = select($rout=$rin, undef, $eout=$rin, $tout);
-- };
-- alarm 0;
-+ $selerr = $!;
-
-- if ($@) {
-- warn "prefork: select timeout failed! recovering\n";
-- sleep 1; # avoid overload
-- return;
-- }
-+ });
-+
-+ # bug 4696: under load, the process can go for such a long time without
-+ # being context-switched in, that when it does return the alarm() fires
-+ # before the select() timeout does. Treat this as a select() timeout
-+ if ($timer->timed_out) {
-+ dbg("prefork: select timed out (via alarm)");
-+ $nfound = 0;
-+ $timeleft = 0;
-+ }
-+
-+ # errors; handle undef *or* -1 returned. do this before "errors on
-+ # the handle" below, since an error condition is signalled both via
-+ # a -1 return and a $eout bit.
-+ if (!defined $nfound || $nfound < 0)
-+ {
-+ if (exists &Errno::EINTR && $selerr == &Errno::EINTR)
-+ {
-+ # this happens if the process is signalled during the select(),
-+ # for example if someone sends SIGHUP to reload the configuration.
-+ # just return inmmediately
-+ dbg("prefork: select returned err $selerr, probably signalled");
-+ return;
-+ }
-+
-+ # if a child exits during that select() call, it generates a spurious
-+ # error, like this:
-+ #
-+ # Jan 29 12:53:17 dogma spamd[18518]: prefork: child states: BI
-+ # Jan 29 12:53:17 dogma spamd[18518]: spamd: handled cleanup of child pid 13101 due to SIGCHLD
-+ # Jan 29 12:53:17 dogma spamd[18518]: prefork: select returned -1! recovering:
-+ #
-+ # avoid by setting a boolean in the child_exited() callback and checking
-+ # it here. log $! just in case, though.
-+ if ($self->{child_just_exited} && $nfound == -1) {
-+ dbg("prefork: select returned -1 due to child exiting, ignored ($selerr)");
-+ return;
-+ }
-+
-+ warn "prefork: select returned ".
-+ (defined $nfound ? $nfound : "undef").
-+ "! recovering: $selerr\n";
-
-- if (!defined $nfound) {
-- warn "prefork: select returned undef! recovering\n";
- sleep 1; # avoid overload
- return;
- }
-@@ -213,7 +256,7 @@
- # errors on the handle?
- # return them immediately, they may be from a SIGHUP restart signal
- if (vec ($eout, $self->{server_fileno}, 1)) {
-- warn "prefork: select returned error on server filehandle: $!\n";
-+ warn "prefork: select returned error on server filehandle: $selerr $!\n";
- return;
- }
-
-@@ -282,7 +325,7 @@
-
- my ($sock, $kid);
- while (($kid, $sock) = each %{$self->{backchannel}->{kids}}) {
-- $self->syswrite_with_retry($sock, PF_PING_ORDER) and next;
-+ $self->syswrite_with_retry($sock, PF_PING_ORDER, $kid, 3) and next;
-
- warn "prefork: write of ping failed to $kid fd=".$sock->fileno.": ".$!;
-
-@@ -353,7 +396,7 @@
- return $self->order_idle_child_to_accept();
- }
-
-- if (!$self->syswrite_with_retry($sock, PF_ACCEPT_ORDER))
-+ if (!$self->syswrite_with_retry($sock, PF_ACCEPT_ORDER, $kid))
- {
- # failure to write to the child; bad news. call it dead
- warn "prefork: killing rogue child $kid, failed to write on fd ".$sock->fileno.": $!\n";
-@@ -396,7 +439,7 @@
- my ($self, $kid) = @_;
- if ($self->{waiting_for_idle_child}) {
- my $sock = $self->{backchannel}->get_socket_for_child($kid);
-- $self->syswrite_with_retry($sock, PF_ACCEPT_ORDER)
-+ $self->syswrite_with_retry($sock, PF_ACCEPT_ORDER, $kid)
- or die "prefork: $kid claimed it was ready, but write failed on fd ".
- $sock->fileno.": ".$!;
- $self->{waiting_for_idle_child} = 0;
-@@ -426,7 +469,7 @@
- sub report_backchannel_socket {
- my ($self, $str) = @_;
- my $sock = $self->{backchannel}->get_parent_socket();
-- $self->syswrite_with_retry($sock, $str)
-+ $self->syswrite_with_retry($sock, $str, 'parent')
- or write "syswrite() to parent failed: $!";
- }
-
-@@ -537,12 +580,31 @@
- }
-
- sub syswrite_with_retry {
-- my ($self, $sock, $buf) = @_;
-+ my ($self, $sock, $buf, $targetname, $numretries) = @_;
-+ $numretries ||= 10; # default 10 retries
-
- my $written = 0;
-+ my $try = 0;
-
- retry_write:
-+
-+ $try++;
-+ if ($try > 1) {
-+ warn "prefork: syswrite(".$sock->fileno.") to $targetname failed on try $try";
-+ if ($try > $numretries) {
-+ warn "prefork: giving up";
-+ return undef;
-+ }
-+ else {
-+ # give it 1 second to recover. we retry indefinitely.
-+ my $rout = '';
-+ vec($rout, $sock->fileno, 1) = 1;
-+ select(undef, $rout, undef, 1);
-+ }
-+ }
-+
- my $nbytes = $sock->syswrite($buf);
-+
- if (!defined $nbytes) {
- unless ((exists &Errno::EAGAIN && $! == &Errno::EAGAIN)
- || (exists &Errno::EWOULDBLOCK && $! == &Errno::EWOULDBLOCK))
-@@ -551,13 +613,7 @@
- return undef;
- }
-
-- warn "prefork: syswrite(".$sock->fileno.") failed, retrying...";
--
-- # give it 5 seconds to recover. we retry indefinitely.
-- my $rout = '';
-- vec($rout, $sock->fileno, 1) = 1;
-- select(undef, $rout, undef, 5);
--
-+ warn "prefork: retrying syswrite(): $!";
- goto retry_write;
- }
- else {
-@@ -568,7 +624,8 @@
- return $written; # it's complete, we can return
- }
- else {
-- warn "prefork: partial write of $nbytes, towrite=".length($buf).
-+ warn "prefork: partial write of $nbytes to ".
-+ $targetname.", towrite=".length($buf).
- " sofar=".$written." fd=".$sock->fileno.", recovering";
- goto retry_write;
- }
-
-Added: spamassassin/branches/3.1/lib/Mail/SpamAssassin/Timeout.pm
-URL: http://svn.apache.org/viewcvs/spamassassin/branches/3.1/lib/Mail/SpamAssassin/Timeout.pm?rev=384590&view=auto
-==============================================================================
---- spamassassin/branches/3.1/lib/Mail/SpamAssassin/Timeout.pm (added)
-+++ spamassassin/branches/3.1/lib/Mail/SpamAssassin/Timeout.pm Thu Mar 9 11:51:59 2006
-@@ -0,0 +1,215 @@
-+# <@LICENSE>
-+# Copyright 2004 Apache Software Foundation
-+#
-+# Licensed under the Apache License, Version 2.0 (the "License");
-+# you may not use this file except in compliance with the License.
-+# You may obtain a copy of the License at
-+#
-+# http://www.apache.org/licenses/LICENSE-2.0
-+#
-+# Unless required by applicable law or agreed to in writing, software
-+# distributed under the License is distributed on an "AS IS" BASIS,
-+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-+# See the License for the specific language governing permissions and
-+# limitations under the License.
-+# @LICENSE>
-+
-+=head1 NAME
-+
-+Mail::SpamAssassin::Timeout - safe, reliable timeouts in perl
-+
-+=head1 SYNOPSIS
-+
-+ # non-timeout code...
-+
-+ my $t = Mail::SpamAssassin::Timeout->new({ secs => 5 });
-+
-+ $t->run(sub {
-+ # code to run with a 5-second timeout...
-+ });
-+
-+ if ($t->timed_out()) {
-+ # do something...
-+ }
-+
-+ # more non-timeout code...
-+
-+=head1 DESCRIPTION
-+
-+This module provides a safe, reliable and clean API to provide
-+C-based timeouts for perl code.
-+
-+Note that C<$SIG{ALRM}> is used to provide the timeout, so this will not
-+interrupt out-of-control regular expression matches.
-+
-+Nested timeouts are supported.
-+
-+=head1 PUBLIC METHODS
-+
-+=over 4
-+
-+=cut
-+
-+package Mail::SpamAssassin::Timeout;
-+
-+use strict;
-+use warnings;
-+use bytes;
-+
-+use vars qw{
-+ @ISA
-+};
-+
-+@ISA = qw();
-+
-+###########################################################################
-+
-+=item my $t = Mail::SpamAssassin::Timeout->new({ ... options ... });
-+
-+Constructor. Options include:
-+
-+=over 4
-+
-+=item secs => $seconds
-+
-+timeout, in seconds. Optional; if not specified, no timeouts will be applied.
-+
-+=back
-+
-+=cut
-+
-+sub new {
-+ my ($class, $opts) = @_;
-+ $class = ref($class) || $class;
-+ my %selfval = $opts ? %{$opts} : ();
-+ my $self = \%selfval;
-+
-+ bless ($self, $class);
-+ $self;
-+}
-+
-+###########################################################################
-+
-+=item $t->run($coderef)
-+
-+Run a code reference within the currently-defined timeout.
-+
-+The timeout is as defined by the B parameter to the constructor.
-+
-+Returns whatever the subroutine returns, or C on timeout.
-+If the timer times out, C<$t-timed_out()> will return C<1>.
-+
-+Time elapsed is not cumulative; multiple runs of C will restart the
-+timeout from scratch.
-+
-+=item $t->run_and_catch($coderef)
-+
-+Run a code reference, as per C<$t-run()>, but also catching any
-+C calls within the code reference.
-+
-+Returns C if no C call was executed and C<$@> was unset, or the
-+value of C<$@> if it was set. (The timeout event doesn't count as a C.)
-+
-+=cut
-+
-+sub run { $_[0]->_run($_[1], 0); }
-+
-+sub run_and_catch { $_[0]->_run($_[1], 1); }
-+
-+sub _run { # private
-+ my ($self, $sub, $and_catch) = @_;
-+
-+ delete $self->{timed_out};
-+
-+ if (!$self->{secs}) { # no timeout! just call the sub and return.
-+ return &$sub;
-+ }
-+
-+ # assertion
-+ if ($self->{secs} < 0) {
-+ die "Mail::SpamAssassin::Timeout: oops? neg value for 'secs': $self->{secs}";
-+ }
-+
-+ my $oldalarm = 0;
-+ my $ret;
-+
-+ eval {
-+ # note use of local to ensure closed scope here
-+ local $SIG{ALRM} = sub { die "__alarm__ignore__\n" };
-+ local $SIG{__DIE__}; # bug 4631
-+
-+ $oldalarm = alarm($self->{secs});
-+
-+ $ret = &$sub;
-+
-+ # Unset the alarm() before we leave eval{ } scope, as that stack-pop
-+ # operation can take a second or two under load. Note: previous versions
-+ # restored $oldalarm here; however, that is NOT what we want to do, since
-+ # it creates a new race condition, namely that an old alarm could then fire
-+ # while the stack-pop was underway, thereby appearing to be *this* timeout
-+ # timing out. In terms of how we might possibly have nested timeouts in
-+ # SpamAssassin, this is an academic issue with little impact, but it's
-+ # still worth avoiding anyway.
-+
-+ alarm 0;
-+ };
-+
-+ my $err = $@;
-+
-+ if (defined $oldalarm) {
-+ # now, we could have died from a SIGALRM == timed out. if so,
-+ # restore the previously-active one, or zero all timeouts if none
-+ # were previously active.
-+ alarm $oldalarm;
-+ }
-+
-+ if ($err) {
-+ if ($err =~ /__alarm__ignore__/) {
-+ $self->{timed_out} = 1;
-+ } else {
-+ if ($and_catch) {
-+ return $@;
-+ } else {
-+ die $@; # propagate any "real" errors
-+ }
-+ }
-+ }
-+
-+ if ($and_catch) {
-+ return; # undef
-+ } else {
-+ return $ret;
-+ }
-+}
-+
-+###########################################################################
-+
-+=item $t->timed_out()
-+
-+Returns C<1> if the most recent code executed in C timed out, or
-+C if it did not.
-+
-+=cut
-+
-+sub timed_out {
-+ my ($self) = @_;
-+ return $self->{timed_out};
-+}
-+
-+###########################################################################
-+
-+=item $t->reset()
-+
-+If called within a C code reference, causes the current alarm timer to
-+be reset to its starting value.
-+
-+=cut
-+
-+sub reset {
-+ my ($self) = @_;
-+ alarm($self->{secs});
-+}
-+
-+###########################################################################
-+
-+1;
-
-Modified: spamassassin/branches/3.1/spamd/spamd.raw
-URL: http://svn.apache.org/viewcvs/spamassassin/branches/3.1/spamd/spamd.raw?rev=384590&r1=384589&r2=384590&view=diff
-==============================================================================
---- spamassassin/branches/3.1/spamd/spamd.raw (original)
-+++ spamassassin/branches/3.1/spamd/spamd.raw Thu Mar 9 11:51:59 2006
-@@ -2049,6 +2049,9 @@
- foreach (keys %children) {
- kill 'INT' => $_;
- my $pid = waitpid($_, 0);
-+ if ($scaling) {
-+ $scaling->child_exited($pid);
-+ }
- info("spamd: child $pid killed successfully");
- }
- %children = ();
-
-
-
-
-
\ No newline at end of file
diff --git a/tools/buildbot/pylibs/buildbot/test/mail/syncmail.1 b/tools/buildbot/pylibs/buildbot/test/mail/syncmail.1
deleted file mode 100644
index eb35e25..0000000
--- a/tools/buildbot/pylibs/buildbot/test/mail/syncmail.1
+++ /dev/null
@@ -1,152 +0,0 @@
-Return-Path:
-Delivered-To: warner-sourceforge@luther.lothar.com
-Received: (qmail 23758 invoked by uid 1000); 28 Jul 2003 07:22:14 -0000
-Delivered-To: warner-sourceforge@lothar.com
-Received: (qmail 62715 invoked by uid 13574); 28 Jul 2003 07:22:03 -0000
-Received: from unknown (HELO sc8-sf-list1.sourceforge.net) ([66.35.250.206]) (envelope-sender )
- by 130.94.181.6 (qmail-ldap-1.03) with SMTP
- for ; 28 Jul 2003 07:22:03 -0000
-Received: from sc8-sf-sshgate.sourceforge.net ([66.35.250.220] helo=sc8-sf-netmisc.sourceforge.net)
- by sc8-sf-list1.sourceforge.net with esmtp
- (Cipher TLSv1:DES-CBC3-SHA:168) (Exim 3.31-VA-mm2 #1 (Debian))
- id 19h2KY-0004Nr-00
- for ; Mon, 28 Jul 2003 00:22:02 -0700
-Received: from sc8-pr-cvs1-b.sourceforge.net ([10.5.1.7] helo=sc8-pr-cvs1.sourceforge.net)
- by sc8-sf-netmisc.sourceforge.net with esmtp (Exim 3.36 #1 (Debian))
- id 19h2KY-0001rv-00
- for ; Mon, 28 Jul 2003 00:22:02 -0700
-Received: from localhost ([127.0.0.1] helo=sc8-pr-cvs1.sourceforge.net)
- by sc8-pr-cvs1.sourceforge.net with esmtp (Exim 3.22 #1 (Debian))
- id 19h2KY-0003r4-00
- for ; Mon, 28 Jul 2003 00:22:02 -0700
-From: warner@users.sourceforge.net
-To: warner@users.sourceforge.net
-Subject: buildbot/buildbot/changes freshcvsmail.py,1.2,1.3
-Message-Id: