Skip to content

Commit be16103

Browse files
committed
Merge pull request #2179 from ryansydnor/hipchat
Add Hipchat reporter
2 parents cc398b2 + 8009bc4 commit be16103

File tree

7 files changed

+399
-7
lines changed

7 files changed

+399
-7
lines changed

master/buildbot/reporters/hipchat.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
from twisted.internet import defer
2+
from twisted.python import log
3+
4+
from buildbot import config
5+
from buildbot.process.results import statusToString
6+
from buildbot.reporters import utils
7+
from buildbot.reporters.http import HttpStatusPushBase
8+
9+
10+
class HipChatStatusPush(HttpStatusPushBase):
11+
name = "HipChatStatusPush"
12+
13+
def checkConfig(self, auth_token, endpoint="https://api.hipchat.com",
14+
builder_room_map=None, builder_user_map=None,
15+
event_messages=None, **kwargs):
16+
if not isinstance(auth_token, basestring):
17+
config.error('auth_token must be a string')
18+
if not isinstance(endpoint, basestring):
19+
config.error('endpoint must be a string')
20+
if builder_room_map and not isinstance(builder_room_map, dict):
21+
config.error('builder_room_map must be a dict')
22+
if builder_user_map and not isinstance(builder_user_map, dict):
23+
config.error('builder_user_map must be a dict')
24+
25+
@defer.inlineCallbacks
26+
def reconfigService(self, auth_token, endpoint="https://api.hipchat.com",
27+
builder_room_map=None, builder_user_map=None,
28+
event_messages=None, **kwargs):
29+
yield HttpStatusPushBase.reconfigService(self, **kwargs)
30+
self.auth_token = auth_token
31+
self.endpoint = endpoint
32+
self.builder_room_map = builder_room_map
33+
self.builder_user_map = builder_user_map
34+
self.user_notify = '%sv2/user/%s/message?auth_token=%s'
35+
self.room_notify = '%sv2/room/%s/notification?auth_token=%s'
36+
37+
@defer.inlineCallbacks
38+
def buildStarted(self, key, build):
39+
yield self.send(build, key[2])
40+
41+
@defer.inlineCallbacks
42+
def buildFinished(self, key, build):
43+
yield self.send(build, key[2])
44+
45+
@defer.inlineCallbacks
46+
def getBuildDetailsAndSendMessage(self, build, key):
47+
yield utils.getDetailsForBuild(self.master, build, **self.neededDetails)
48+
postData = yield self.getRecipientList(build, key)
49+
postData['message'] = yield self.getMessage(build, key)
50+
extra_params = yield self.getExtraParams(build, key)
51+
postData.update(extra_params)
52+
defer.returnValue(postData)
53+
54+
def getRecipientList(self, build, event_name):
55+
result = {}
56+
builder_name = build['builder']['name']
57+
if self.builder_user_map and builder_name in self.builder_user_map:
58+
result['id_or_email'] = self.builder_user_map[builder_name]
59+
if self.builder_room_map and builder_name in self.builder_room_map:
60+
result['room_id_or_name'] = self.builder_room_map[builder_name]
61+
return result
62+
63+
def getMessage(self, build, event_name):
64+
event_messages = {
65+
'new': 'Buildbot started build %s here: %s' % (build['builder']['name'], build['url']),
66+
'finished': 'Buildbot finished build %s with result %s here: %s'
67+
% (build['builder']['name'], statusToString(build['results']), build['url'])
68+
}
69+
return event_messages.get(event_name, '')
70+
71+
# use this as an extension point to inject extra parameters into your postData
72+
def getExtraParams(self, build, event_name):
73+
return {}
74+
75+
@defer.inlineCallbacks
76+
def send(self, build, key):
77+
postData = yield self.getBuildDetailsAndSendMessage(build, key)
78+
if not postData or 'message' not in postData or not postData['message']:
79+
return
80+
81+
if not self.endpoint.endswith('/'):
82+
self.endpoint += '/'
83+
84+
urls = []
85+
if 'id_or_email' in postData:
86+
urls.append(self.user_notify % (self.endpoint, postData.pop('id_or_email'), self.auth_token))
87+
if 'room_id_or_name' in postData:
88+
urls.append(self.room_notify % (self.endpoint, postData.pop('room_id_or_name'), self.auth_token))
89+
90+
if urls:
91+
for url in urls:
92+
response = yield self.session.post(url, postData)
93+
if response.status_code != 200:
94+
log.msg("%s: unable to upload status: %s" %
95+
(response.status_code, response.content))

master/buildbot/reporters/http.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,15 @@ def sessionFactory(self):
5757
def startService(self):
5858
self.session = self.sessionFactory()
5959
yield service.BuildbotService.startService(self)
60-
startConsuming = self.master.mq.startConsuming
6160

61+
startConsuming = self.master.mq.startConsuming
6262
self._buildCompleteConsumer = yield startConsuming(
6363
self.buildFinished,
6464
('builds', None, 'finished'))
6565

6666
self._buildStartedConsumer = yield startConsuming(
6767
self.buildStarted,
68-
('builds', None, 'started'))
68+
('builds', None, 'new'))
6969

7070
def stopService(self):
7171
self._buildCompleteConsumer.stopConsuming()
@@ -99,9 +99,6 @@ class HttpStatusPush(HttpStatusPushBase):
9999

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

106103
def reconfigService(self, serverUrl, user, password, **kwargs):
107104
HttpStatusPushBase.reconfigService(self, **kwargs)

master/buildbot/test/unit/test_reporter_http.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def test_basic(self):
7676
yield self.createReporter()
7777
build = yield self.setupBuildResults(SUCCESS)
7878
build['complete'] = False
79-
self.sp.buildStarted(("build", 20, "started"), build)
79+
self.sp.buildStarted(("build", 20, "new"), build)
8080
build['complete'] = True
8181
self.sp.buildFinished(("build", 20, "finished"), build)
8282
# we make sure proper calls to txrequests have been made
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
# This file is part of Buildbot. Buildbot is free software: you can
2+
# redistribute it and/or modify it under the terms of the GNU General Public
3+
# License as published by the Free Software Foundation, version 2.
4+
#
5+
# This program is distributed in the hope that it will be useful, but WITHOUT
6+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
7+
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
8+
# details.
9+
#
10+
# You should have received a copy of the GNU General Public License along with
11+
# this program; if not, write to the Free Software Foundation, Inc., 51
12+
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
13+
#
14+
# Copyright Buildbot Team Members
15+
from mock import Mock
16+
from mock import call
17+
from twisted.internet import defer
18+
from twisted.trial import unittest
19+
from buildbot import config
20+
from buildbot.process.results import SUCCESS
21+
from buildbot.reporters.hipchat import HipChatStatusPush
22+
from buildbot.test.fake import fakemaster
23+
from buildbot.test.util.reporter import ReporterTestMixin
24+
25+
26+
class TestHipchatStatusPush(unittest.TestCase, ReporterTestMixin):
27+
28+
def setUp(self):
29+
# ignore config error if txrequests is not installed
30+
config._errors = Mock()
31+
self.master = fakemaster.make_master(testcase=self, wantData=True, wantDb=True, wantMq=True)
32+
33+
@defer.inlineCallbacks
34+
def tearDown(self):
35+
yield self.sp.stopService()
36+
self.assertEqual(self.sp.session.close.call_count, 1)
37+
config._errors = None
38+
39+
@defer.inlineCallbacks
40+
def createReporter(self, **kwargs):
41+
kwargs['auth_token'] = kwargs.get('auth_token', 'abc')
42+
self.sp = HipChatStatusPush(**kwargs)
43+
self.sp.sessionFactory = Mock(return_value=Mock())
44+
yield self.sp.setServiceParent(self.master)
45+
yield self.sp.startService()
46+
47+
@defer.inlineCallbacks
48+
def setupBuildResults(self):
49+
self.insertTestData([SUCCESS], SUCCESS)
50+
build = yield self.master.data.get(("builds", 20))
51+
defer.returnValue(build)
52+
53+
@defer.inlineCallbacks
54+
def test_authtokenTypeCheck(self):
55+
yield self.createReporter(auth_token=2)
56+
config._errors.addError.assert_any_call('auth_token must be a string')
57+
58+
@defer.inlineCallbacks
59+
def test_endpointTypeCheck(self):
60+
yield self.createReporter(endpoint=2)
61+
config._errors.addError.assert_any_call('endpoint must be a string')
62+
63+
@defer.inlineCallbacks
64+
def test_builderRoomMapTypeCheck(self):
65+
yield self.createReporter(builder_room_map=2)
66+
config._errors.addError.assert_any_call('builder_room_map must be a dict')
67+
68+
@defer.inlineCallbacks
69+
def test_builderUserMapTypeCheck(self):
70+
yield self.createReporter(builder_user_map=2)
71+
config._errors.addError.assert_any_call('builder_user_map must be a dict')
72+
73+
@defer.inlineCallbacks
74+
def test_build_started(self):
75+
yield self.createReporter(builder_user_map={'Builder0': '123'})
76+
build = yield self.setupBuildResults()
77+
self.sp.buildStarted(('build', 20, 'new'), build)
78+
expected = [call('https://api.hipchat.com/v2/user/123/message?auth_token=abc',
79+
{'message': 'Buildbot started build Builder0 here: http://localhost:8080/#builders/79/builds/0'})]
80+
self.assertEqual(self.sp.session.post.mock_calls, expected)
81+
82+
@defer.inlineCallbacks
83+
def test_build_finished(self):
84+
yield self.createReporter(builder_room_map={'Builder0': '123'})
85+
build = yield self.setupBuildResults()
86+
self.sp.buildFinished(('build', 20, 'finished'), build)
87+
expected = [call('https://api.hipchat.com/v2/room/123/notification?auth_token=abc',
88+
{'message': 'Buildbot finished build Builder0 with result success '
89+
'here: http://localhost:8080/#builders/79/builds/0'})]
90+
self.assertEqual(self.sp.session.post.mock_calls, expected)
91+
92+
@defer.inlineCallbacks
93+
def test_inject_extra_params(self):
94+
yield self.createReporter(builder_room_map={'Builder0': '123'})
95+
self.sp.getExtraParams = Mock()
96+
self.sp.getExtraParams.return_value = {'format': 'html'}
97+
build = yield self.setupBuildResults()
98+
self.sp.buildFinished(('build', 20, 'finished'), build)
99+
expected = [call('https://api.hipchat.com/v2/room/123/notification?auth_token=abc',
100+
{'message': 'Buildbot finished build Builder0 with result success '
101+
'here: http://localhost:8080/#builders/79/builds/0',
102+
'format': 'html'})]
103+
self.assertEqual(self.sp.session.post.mock_calls, expected)
104+
105+
@defer.inlineCallbacks
106+
def test_no_message_sent_empty_message(self):
107+
yield self.createReporter()
108+
build = yield self.setupBuildResults()
109+
self.sp.send(build, 'unknown')
110+
assert not self.sp.session.post.called
111+
112+
@defer.inlineCallbacks
113+
def test_no_message_sent_without_id(self):
114+
yield self.createReporter()
115+
build = yield self.setupBuildResults()
116+
self.sp.send(build, 'new')
117+
assert not self.sp.session.post.called
118+
119+
@defer.inlineCallbacks
120+
def test_private_message_sent_with_user_id(self):
121+
token = 'tok'
122+
endpoint = 'example.com'
123+
yield self.createReporter(auth_token=token, endpoint=endpoint)
124+
self.sp.getBuildDetailsAndSendMessage = Mock()
125+
message = {'message': 'hi'}
126+
postData = dict(message)
127+
postData.update({'id_or_email': '123'})
128+
self.sp.getBuildDetailsAndSendMessage.return_value = postData
129+
self.sp.send({}, 'test')
130+
expected = [call('%s/v2/user/123/message?auth_token=%s' % (endpoint, token), message)]
131+
self.assertEqual(self.sp.session.post.mock_calls, expected)
132+
133+
@defer.inlineCallbacks
134+
def test_room_message_sent_with_room_id(self):
135+
token = 'tok'
136+
endpoint = 'example.com'
137+
yield self.createReporter(auth_token=token, endpoint=endpoint)
138+
self.sp.getBuildDetailsAndSendMessage = Mock()
139+
message = {'message': 'hi'}
140+
postData = dict(message)
141+
postData.update({'room_id_or_name': '123'})
142+
self.sp.getBuildDetailsAndSendMessage.return_value = postData
143+
self.sp.send({}, 'test')
144+
expected = [call('%s/v2/room/123/notification?auth_token=%s' % (endpoint, token), message)]
145+
self.assertEqual(self.sp.session.post.mock_calls, expected)
146+
147+
@defer.inlineCallbacks
148+
def test_private_and_room_message_sent_with_both_ids(self):
149+
token = 'tok'
150+
endpoint = 'example.com'
151+
yield self.createReporter(auth_token=token, endpoint=endpoint)
152+
self.sp.getBuildDetailsAndSendMessage = Mock()
153+
message = {'message': 'hi'}
154+
postData = dict(message)
155+
postData.update({'room_id_or_name': '123', 'id_or_email': '456'})
156+
self.sp.getBuildDetailsAndSendMessage.return_value = postData
157+
self.sp.send({}, 'test')
158+
expected = [call('%s/v2/user/456/message?auth_token=%s' % (endpoint, token), message),
159+
call('%s/v2/room/123/notification?auth_token=%s' % (endpoint, token), message)]
160+
self.assertEqual(self.sp.session.post.mock_calls, expected)
161+
162+
@defer.inlineCallbacks
163+
def test_postData_values_passed_through(self):
164+
token = 'tok'
165+
endpoint = 'example.com'
166+
yield self.createReporter(auth_token=token, endpoint=endpoint)
167+
self.sp.getBuildDetailsAndSendMessage = Mock()
168+
message = {'message': 'hi', 'notify': True, 'message_format': 'html'}
169+
postData = dict(message)
170+
postData.update({'id_or_email': '123'})
171+
self.sp.getBuildDetailsAndSendMessage.return_value = postData
172+
self.sp.send({}, 'test')
173+
expected = [call('%s/v2/user/123/message?auth_token=%s' % (endpoint, token), message)]
174+
self.assertEqual(self.sp.session.post.mock_calls, expected)

0 commit comments

Comments
 (0)