Skip to content

Commit

Permalink
Merge pull request #2179 from ryansydnor/hipchat
Browse files Browse the repository at this point in the history
Add Hipchat reporter
  • Loading branch information
tardyp committed May 11, 2016
2 parents cc398b2 + 8009bc4 commit be16103
Show file tree
Hide file tree
Showing 7 changed files with 399 additions and 7 deletions.
95 changes: 95 additions & 0 deletions master/buildbot/reporters/hipchat.py
@@ -0,0 +1,95 @@
from twisted.internet import defer
from twisted.python import log

from buildbot import config
from buildbot.process.results import statusToString
from buildbot.reporters import utils
from buildbot.reporters.http import HttpStatusPushBase


class HipChatStatusPush(HttpStatusPushBase):
name = "HipChatStatusPush"

def checkConfig(self, auth_token, endpoint="https://api.hipchat.com",
builder_room_map=None, builder_user_map=None,
event_messages=None, **kwargs):
if not isinstance(auth_token, basestring):
config.error('auth_token must be a string')
if not isinstance(endpoint, basestring):
config.error('endpoint must be a string')
if builder_room_map and not isinstance(builder_room_map, dict):
config.error('builder_room_map must be a dict')
if builder_user_map and not isinstance(builder_user_map, dict):
config.error('builder_user_map must be a dict')

@defer.inlineCallbacks
def reconfigService(self, auth_token, endpoint="https://api.hipchat.com",
builder_room_map=None, builder_user_map=None,
event_messages=None, **kwargs):
yield HttpStatusPushBase.reconfigService(self, **kwargs)
self.auth_token = auth_token
self.endpoint = endpoint
self.builder_room_map = builder_room_map
self.builder_user_map = builder_user_map
self.user_notify = '%sv2/user/%s/message?auth_token=%s'
self.room_notify = '%sv2/room/%s/notification?auth_token=%s'

@defer.inlineCallbacks
def buildStarted(self, key, build):
yield self.send(build, key[2])

@defer.inlineCallbacks
def buildFinished(self, key, build):
yield self.send(build, key[2])

@defer.inlineCallbacks
def getBuildDetailsAndSendMessage(self, build, key):
yield utils.getDetailsForBuild(self.master, build, **self.neededDetails)
postData = yield self.getRecipientList(build, key)
postData['message'] = yield self.getMessage(build, key)
extra_params = yield self.getExtraParams(build, key)
postData.update(extra_params)
defer.returnValue(postData)

def getRecipientList(self, build, event_name):
result = {}
builder_name = build['builder']['name']
if self.builder_user_map and builder_name in self.builder_user_map:
result['id_or_email'] = self.builder_user_map[builder_name]
if self.builder_room_map and builder_name in self.builder_room_map:
result['room_id_or_name'] = self.builder_room_map[builder_name]
return result

def getMessage(self, build, event_name):
event_messages = {
'new': 'Buildbot started build %s here: %s' % (build['builder']['name'], build['url']),
'finished': 'Buildbot finished build %s with result %s here: %s'
% (build['builder']['name'], statusToString(build['results']), build['url'])
}
return event_messages.get(event_name, '')

# use this as an extension point to inject extra parameters into your postData
def getExtraParams(self, build, event_name):
return {}

@defer.inlineCallbacks
def send(self, build, key):
postData = yield self.getBuildDetailsAndSendMessage(build, key)
if not postData or 'message' not in postData or not postData['message']:
return

if not self.endpoint.endswith('/'):
self.endpoint += '/'

urls = []
if 'id_or_email' in postData:
urls.append(self.user_notify % (self.endpoint, postData.pop('id_or_email'), self.auth_token))
if 'room_id_or_name' in postData:
urls.append(self.room_notify % (self.endpoint, postData.pop('room_id_or_name'), self.auth_token))

if urls:
for url in urls:
response = yield self.session.post(url, postData)
if response.status_code != 200:
log.msg("%s: unable to upload status: %s" %
(response.status_code, response.content))
7 changes: 2 additions & 5 deletions master/buildbot/reporters/http.py
Expand Up @@ -57,15 +57,15 @@ def sessionFactory(self):
def startService(self): def startService(self):
self.session = self.sessionFactory() self.session = self.sessionFactory()
yield service.BuildbotService.startService(self) yield service.BuildbotService.startService(self)
startConsuming = self.master.mq.startConsuming


startConsuming = self.master.mq.startConsuming
self._buildCompleteConsumer = yield startConsuming( self._buildCompleteConsumer = yield startConsuming(
self.buildFinished, self.buildFinished,
('builds', None, 'finished')) ('builds', None, 'finished'))


self._buildStartedConsumer = yield startConsuming( self._buildStartedConsumer = yield startConsuming(
self.buildStarted, self.buildStarted,
('builds', None, 'started')) ('builds', None, 'new'))


def stopService(self): def stopService(self):
self._buildCompleteConsumer.stopConsuming() self._buildCompleteConsumer.stopConsuming()
Expand Down Expand Up @@ -99,9 +99,6 @@ class HttpStatusPush(HttpStatusPushBase):


def checkConfig(self, serverUrl, user, password, **kwargs): def checkConfig(self, serverUrl, user, password, **kwargs):
HttpStatusPushBase.checkConfig(self, **kwargs) HttpStatusPushBase.checkConfig(self, **kwargs)
if txrequests is None:
config.error("Please install txrequests and requests to use %s (pip install txrequest)" %
(self.__class__,))


def reconfigService(self, serverUrl, user, password, **kwargs): def reconfigService(self, serverUrl, user, password, **kwargs):
HttpStatusPushBase.reconfigService(self, **kwargs) HttpStatusPushBase.reconfigService(self, **kwargs)
Expand Down
2 changes: 1 addition & 1 deletion master/buildbot/test/unit/test_reporter_http.py
Expand Up @@ -76,7 +76,7 @@ def test_basic(self):
yield self.createReporter() yield self.createReporter()
build = yield self.setupBuildResults(SUCCESS) build = yield self.setupBuildResults(SUCCESS)
build['complete'] = False build['complete'] = False
self.sp.buildStarted(("build", 20, "started"), build) self.sp.buildStarted(("build", 20, "new"), build)
build['complete'] = True build['complete'] = True
self.sp.buildFinished(("build", 20, "finished"), build) self.sp.buildFinished(("build", 20, "finished"), build)
# we make sure proper calls to txrequests have been made # we make sure proper calls to txrequests have been made
Expand Down
174 changes: 174 additions & 0 deletions master/buildbot/test/unit/test_reporters_hipchat.py
@@ -0,0 +1,174 @@
# This file is part of Buildbot. Buildbot is free software: you can
# redistribute it and/or modify it under the terms of the GNU General Public
# License as published by the Free Software Foundation, version 2.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc., 51
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Copyright Buildbot Team Members
from mock import Mock
from mock import call
from twisted.internet import defer
from twisted.trial import unittest
from buildbot import config
from buildbot.process.results import SUCCESS
from buildbot.reporters.hipchat import HipChatStatusPush
from buildbot.test.fake import fakemaster
from buildbot.test.util.reporter import ReporterTestMixin


class TestHipchatStatusPush(unittest.TestCase, ReporterTestMixin):

def setUp(self):
# ignore config error if txrequests is not installed
config._errors = Mock()
self.master = fakemaster.make_master(testcase=self, wantData=True, wantDb=True, wantMq=True)

@defer.inlineCallbacks
def tearDown(self):
yield self.sp.stopService()
self.assertEqual(self.sp.session.close.call_count, 1)
config._errors = None

@defer.inlineCallbacks
def createReporter(self, **kwargs):
kwargs['auth_token'] = kwargs.get('auth_token', 'abc')
self.sp = HipChatStatusPush(**kwargs)
self.sp.sessionFactory = Mock(return_value=Mock())
yield self.sp.setServiceParent(self.master)
yield self.sp.startService()

@defer.inlineCallbacks
def setupBuildResults(self):
self.insertTestData([SUCCESS], SUCCESS)
build = yield self.master.data.get(("builds", 20))
defer.returnValue(build)

@defer.inlineCallbacks
def test_authtokenTypeCheck(self):
yield self.createReporter(auth_token=2)
config._errors.addError.assert_any_call('auth_token must be a string')

@defer.inlineCallbacks
def test_endpointTypeCheck(self):
yield self.createReporter(endpoint=2)
config._errors.addError.assert_any_call('endpoint must be a string')

@defer.inlineCallbacks
def test_builderRoomMapTypeCheck(self):
yield self.createReporter(builder_room_map=2)
config._errors.addError.assert_any_call('builder_room_map must be a dict')

@defer.inlineCallbacks
def test_builderUserMapTypeCheck(self):
yield self.createReporter(builder_user_map=2)
config._errors.addError.assert_any_call('builder_user_map must be a dict')

@defer.inlineCallbacks
def test_build_started(self):
yield self.createReporter(builder_user_map={'Builder0': '123'})
build = yield self.setupBuildResults()
self.sp.buildStarted(('build', 20, 'new'), build)
expected = [call('https://api.hipchat.com/v2/user/123/message?auth_token=abc',
{'message': 'Buildbot started build Builder0 here: http://localhost:8080/#builders/79/builds/0'})]
self.assertEqual(self.sp.session.post.mock_calls, expected)

@defer.inlineCallbacks
def test_build_finished(self):
yield self.createReporter(builder_room_map={'Builder0': '123'})
build = yield self.setupBuildResults()
self.sp.buildFinished(('build', 20, 'finished'), build)
expected = [call('https://api.hipchat.com/v2/room/123/notification?auth_token=abc',
{'message': 'Buildbot finished build Builder0 with result success '
'here: http://localhost:8080/#builders/79/builds/0'})]
self.assertEqual(self.sp.session.post.mock_calls, expected)

@defer.inlineCallbacks
def test_inject_extra_params(self):
yield self.createReporter(builder_room_map={'Builder0': '123'})
self.sp.getExtraParams = Mock()
self.sp.getExtraParams.return_value = {'format': 'html'}
build = yield self.setupBuildResults()
self.sp.buildFinished(('build', 20, 'finished'), build)
expected = [call('https://api.hipchat.com/v2/room/123/notification?auth_token=abc',
{'message': 'Buildbot finished build Builder0 with result success '
'here: http://localhost:8080/#builders/79/builds/0',
'format': 'html'})]
self.assertEqual(self.sp.session.post.mock_calls, expected)

@defer.inlineCallbacks
def test_no_message_sent_empty_message(self):
yield self.createReporter()
build = yield self.setupBuildResults()
self.sp.send(build, 'unknown')
assert not self.sp.session.post.called

@defer.inlineCallbacks
def test_no_message_sent_without_id(self):
yield self.createReporter()
build = yield self.setupBuildResults()
self.sp.send(build, 'new')
assert not self.sp.session.post.called

@defer.inlineCallbacks
def test_private_message_sent_with_user_id(self):
token = 'tok'
endpoint = 'example.com'
yield self.createReporter(auth_token=token, endpoint=endpoint)
self.sp.getBuildDetailsAndSendMessage = Mock()
message = {'message': 'hi'}
postData = dict(message)
postData.update({'id_or_email': '123'})
self.sp.getBuildDetailsAndSendMessage.return_value = postData
self.sp.send({}, 'test')
expected = [call('%s/v2/user/123/message?auth_token=%s' % (endpoint, token), message)]
self.assertEqual(self.sp.session.post.mock_calls, expected)

@defer.inlineCallbacks
def test_room_message_sent_with_room_id(self):
token = 'tok'
endpoint = 'example.com'
yield self.createReporter(auth_token=token, endpoint=endpoint)
self.sp.getBuildDetailsAndSendMessage = Mock()
message = {'message': 'hi'}
postData = dict(message)
postData.update({'room_id_or_name': '123'})
self.sp.getBuildDetailsAndSendMessage.return_value = postData
self.sp.send({}, 'test')
expected = [call('%s/v2/room/123/notification?auth_token=%s' % (endpoint, token), message)]
self.assertEqual(self.sp.session.post.mock_calls, expected)

@defer.inlineCallbacks
def test_private_and_room_message_sent_with_both_ids(self):
token = 'tok'
endpoint = 'example.com'
yield self.createReporter(auth_token=token, endpoint=endpoint)
self.sp.getBuildDetailsAndSendMessage = Mock()
message = {'message': 'hi'}
postData = dict(message)
postData.update({'room_id_or_name': '123', 'id_or_email': '456'})
self.sp.getBuildDetailsAndSendMessage.return_value = postData
self.sp.send({}, 'test')
expected = [call('%s/v2/user/456/message?auth_token=%s' % (endpoint, token), message),
call('%s/v2/room/123/notification?auth_token=%s' % (endpoint, token), message)]
self.assertEqual(self.sp.session.post.mock_calls, expected)

@defer.inlineCallbacks
def test_postData_values_passed_through(self):
token = 'tok'
endpoint = 'example.com'
yield self.createReporter(auth_token=token, endpoint=endpoint)
self.sp.getBuildDetailsAndSendMessage = Mock()
message = {'message': 'hi', 'notify': True, 'message_format': 'html'}
postData = dict(message)
postData.update({'id_or_email': '123'})
self.sp.getBuildDetailsAndSendMessage.return_value = postData
self.sp.send({}, 'test')
expected = [call('%s/v2/user/123/message?auth_token=%s' % (endpoint, token), message)]
self.assertEqual(self.sp.session.post.mock_calls, expected)

0 comments on commit be16103

Please sign in to comment.