blob: 93e6b54c9e569d35eecf1997cf1ae034b9be63d4 [file] [log] [blame] [edit]
# Copyright 2013 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""A StatusReceiver module to send mail under certain conditions.
A very simple subclass of MailNotifier, with the ability to decide whether or
not to email based on the failure state of individual steps.
Similar to ChromiumNotifier, but sane.
"""
from buildbot import interfaces
from buildbot.status.builder import SUCCESS, FAILURE
from buildbot.status.mail import MailNotifier
# Singleton objects used as values for |watch_mode|.
BUILDERS = object()
CATEGORIES = object()
class StepNotifier(MailNotifier):
"""This is a status notifier which can selectively mail for certain steps."""
@staticmethod
def log(msg):
print '[StepNotifier] %s' % msg
def __init__(self, watch_mode, interesting_steps, boring_steps=None,
**kwargs):
"""
@type watch_mode: either BUILDERS or CATEGORIES
@param watch_mode: A singleton object denoting whether this class is
watching steps keyed by builder name or by category.
@type interesting_steps: mapping of strings to lists of strings.
@param interesting_steps: Keys are either builder names or categories;
values are lists of steps which we care about.
The key '*' (wildcard) means 'all builders'.
The value ['*'] (wildcard) means 'all steps'.
All keys (other than wildcard) must be present in
either |builders| or |categories| (in kwargs).
Required since otherwise this class has no point.
@type boring_steps: mapping of strings to lists of strings.
@param boring_steps: Keys are either builder names or categories;
values are lists of steps which we do not care about.
The key '*' (wildcard) means 'all builders'.
The value ['*'] (wildcard) means 'all steps'.
All keys (other than wildcard) must be present in
either |builders| or |categories| (in kwargs).
Boring steps override interesting steps.
"""
self.watch_mode = watch_mode
self.interesting_steps = interesting_steps
self.boring_steps = boring_steps or {}
# Pass through the keys in interesting/boring steps to the appropriate list
# in the parent class.
if any((arg in ('builders', 'categores') for arg in kwargs)):
raise interfaces.ParameterError(
'Please do not specify |builders| or |categories| in StepNotifier.')
keys = set(self.interesting_steps) | set(self.boring_steps)
keys.discard('*')
if self.watch_mode is BUILDERS:
kwargs['builders'] = list(keys)
elif self.watch_mode is CATEGORIES:
kwargs['categories'] = list(keys)
else:
raise interfaces.ParameterError(
'Please specify either BUILDERS or CATEGORIES for |watch_mode|.')
self.log('Interesting steps: %s' % self.interesting_steps)
self.log('Boring steps: %s' % self.boring_steps)
self.log('Instantiating MailNotifier with: %s' % kwargs)
# Don't use super because MailNotifier is an old-style class.
MailNotifier.__init__(self, **kwargs)
def _isInterestingBuilder(self, builder):
"""Determines if we care about a builder.
We care about a builder if:
* we care about steps on all (wildcard) builders
* we care about steps on the builder with this name
* we care about steps on any of this builder's categories
"""
if '*' in self.interesting_steps or {}:
self.watched.append(builder)
self.log('Interested in %s (wildcard).' % builder.name)
return True
if builder.name in self.builders or []:
self.watched.append(builder)
self.log('Interested in %s (in builders).' % builder.name)
return True
if any([cat in (self.categories or [])
for cat in builder.category.split('|')]):
self.log('Interested in %s (%s in categories).' % (
builder.name, builder.category))
self.watched.append(builder)
return True
self.log('Not interested in %s.' % builder.name)
return False
def builderAdded(self, name, builder):
"""Called as a hook when a builder attaches to the master.
We subscribe to events from the builder if we find it interesting.
"""
if self._isInterestingBuilder(builder):
return self # subscribe to this builder
return None
def isMailNeeded(self, build, _results):
"""Called by buildFinished to determine if we should send mail."""
builder = build.getBuilder()
if not self._isInterestingBuilder(builder):
# We should never be here (we shouldn't be subscribed) but just in case.
return False
watched_steps = set(self.interesting_steps.get('*', []))
if self.watch_mode is BUILDERS:
watched_steps |= (set(self.interesting_steps.get(builder.name, [])) -
set(self.boring_steps.get(builder.name, [])))
else:
for category in builder.category.split('|'):
watched_steps |= (set(self.interesting_steps.get(category, [])) -
set(self.boring_steps.get(category, [])))
watched_steps -= set(self.boring_steps.get('*', []))
self.log('Watched steps: %s' % watched_steps)
self.log('Mail mode: %s' % self.mode)
def log_mail_reason(mode, step, result):
self.log('Mode is %s and step %s was a %s; sending mail.' % (
mode, step, result))
for step in build.getSteps():
name = step.getName()
if name not in watched_steps:
continue
result = step.getResults()[0]
if self.mode == "passing" and result == SUCCESS:
log_mail_reason('passing', name, 'success')
return True
if self.mode == "warnings" and result != SUCCESS:
log_mail_reason('warnings', name, 'warning')
return True
if self.mode == "failing" and result == FAILURE:
log_mail_reason('failing', name, 'failure')
return True
# TODO(agable): Handle 'problem' and 'change' on a step-by-step level.
if self.mode == "problem":
prev = build.getPreviousBuild()
if result == FAILURE and (prev and prev.getResults() != FAILURE):
log_mail_reason('problem', name, 'failure')
return True
if self.mode == "change":
prev = build.getPreviousBuild()
if not prev or prev.getResults() != result:
log_mail_reason('change', name, result)
return True
self.log('Not mailing')
return False