Skip to content

Commit

Permalink
used MessageFormatter in order to render the comment text
Browse files Browse the repository at this point in the history
  • Loading branch information
EmilioPeJu committed Jun 23, 2017
1 parent 8e55dfa commit cce5f7e
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 93 deletions.
86 changes: 53 additions & 33 deletions master/buildbot/reporters/bitbucketserver.py
Expand Up @@ -23,9 +23,11 @@
from buildbot.process.properties import Properties
from buildbot.process.results import SUCCESS
from buildbot.reporters import http
from buildbot.reporters import notifier
from buildbot.util import httpclientservice
from buildbot.util.logger import Logger
from buildbot.util import unicode2bytes
from buildbot.util import bytes2NativeString

log = Logger()

Expand Down Expand Up @@ -105,45 +107,63 @@ def send(self, build):
code=response.code, content=content)


class BitbucketServerPRCommentPush(http.HttpStatusPushBase):
class BitbucketServerPRCommentPush(notifier.NotifierBase):
name = "BitbucketServerPRCommentPush"

@defer.inlineCallbacks
def reconfigService(self, base_url, user, password, text=None,
verbose=False, **kwargs):
yield http.HttpStatusPushBase.reconfigService(
self, wantProperties=True, **kwargs)
self.text = text or Interpolate('Builder: %(prop:buildername)s '
'Status: %(prop:statustext)s')
def reconfigService(self, base_url, user, password, messageFormatter=None,
verbose=False, debug=None, verify=None, **kwargs):
yield notifier.NotifierBase.reconfigService(
self, messageFormatter=messageFormatter, watchedWorkers=None,
messageFormatterMissingWorker=None, subject='', addLogs=False,
addPatch=False, **kwargs)
self.verbose = verbose
self._http = yield httpclientservice.HTTPClientService.getService(
self.master, base_url, auth=(user, password),
debug=self.debug, verify=self.verify)
debug=debug, verify=verify)

@defer.inlineCallbacks
def send(self, build):
if build['complete'] and "pullrequesturl" in build['properties']:
yield self.sendPullRequestComment(build)
def checkConfig(self, base_url, user, password, messageFormatter=None,
verbose=False, debug=None, verify=None, **kwargs):

notifier.NotifierBase.checkConfig(self,
messageFormatter=messageFormatter,
watchedWorkers=None,
messageFormatterMissingWorker=None,
subject='',
addLogs=False,
addPatch=False,
**kwargs)

def isMessageNeeded(self, build):
if 'pullrequesturl' in build['properties']:
return notifier.NotifierBase.isMessageNeeded(self, build)
return False

def workerMissing(self, key, worker):
# a comment is always associated to a change
pass

@defer.inlineCallbacks
def sendPullRequestComment(self, build):
props = Properties.fromDict(build['properties'])
pr_url = props.getProperty("pullrequesturl")
# we assume that the PR URL is well-formed as it comes from a PR event
path = urlparse(unicode2bytes(pr_url)).path
status = "SUCCESS" if build['results'] == SUCCESS else "FAILED"
props.setProperty('statustext', status, self.name)
props.setProperty('url', build['url'], self.name)
comment_text = yield props.render(self.text)
payload = {'text': comment_text}
response = yield self._http.post(
COMMENT_API_URL.format(path=path), json=payload)

if response.code == HTTP_CREATED:
if self.verbose:
log.info('{comment} sent to {url}',
comment=comment_text, url=pr_url)
else:
content = yield response.content()
log.error("{code}: Unable to send a comment: {content}",
code=response.code, content=content)
def sendMessage(self, body, subject=None, type=None, builderName=None,
results=None, builds=None, users=None, patches=None,
logs=None, worker=None):
pr_urls = set()
for build in builds:
props = Properties.fromDict(build['properties'])
pr_urls.add(props.getProperty("pullrequesturl"))
for pr_url in pr_urls:
# we assume that the PR URL is well-formed as it comes from a PR event
path = urlparse(unicode2bytes(pr_url)).path
payload = {'text': body}
response = yield self._http.post(
COMMENT_API_URL.format(
path=bytes2NativeString(path)), json=payload)

if response.code == HTTP_CREATED:
if self.verbose:
log.info('{comment} sent to {url}',
comment=body, url=pr_url)
else:
content = yield response.content()
log.error("{code}: Unable to send a comment: {content}",
code=response.code, content=content)
120 changes: 69 additions & 51 deletions master/buildbot/test/unit/test_reporter_bitbucketserver.py
Expand Up @@ -15,21 +15,22 @@

from __future__ import absolute_import
from __future__ import print_function
from future.utils import PY3

from mock import Mock

from twisted.internet import defer
from twisted.trial import unittest

from buildbot import config
from buildbot.process.properties import Interpolate
from buildbot.process.results import FAILURE
from buildbot.process.results import SUCCESS
from buildbot.reporters.bitbucketserver import BitbucketServerPRCommentPush
from buildbot.reporters.bitbucketserver import BitbucketServerStatusPush
from buildbot.test.fake import httpclientservice as fakehttpclientservice
from buildbot.test.fake import fakemaster
from buildbot.test.util.logging import LoggingMixin
from buildbot.test.util.notifier import NotifierTestMixin
from buildbot.test.util.reporter import ReporterTestMixin


Expand All @@ -39,8 +40,8 @@ class TestBitbucketServerStatusPush(unittest.TestCase, ReporterTestMixin, Loggin
def setupReporter(self, **kwargs):
# ignore config error if txrequests is not installed
self.patch(config, '_errors', Mock())
self.master = fakemaster.make_master(testcase=self,
wantData=True, wantDb=True, wantMq=True)
self.master = fakemaster.make_master(
testcase=self, wantData=True, wantDb=True, wantMq=True)

self._http = yield fakehttpclientservice.HTTPClientService.getFakeService(
self.master, self,
Expand Down Expand Up @@ -142,91 +143,108 @@ def test_error(self):
self.assertLogged('404: Unable to send Bitbucket Server status')


class TestBitbucketServerPRCommentPush(unittest.TestCase, ReporterTestMixin, LoggingMixin):
UNICODE_BODY = u"body: \u00E5\u00E4\u00F6 text"
EXPECTED_API = u'/rest/api/1.0/projects/PRO/repos/myrepo/pull-requests/20/comments'
PR_URL = "http://example.com/projects/PRO/repos/myrepo/pull-requests/20"


class TestBitbucketServerPRCommentPush(unittest.TestCase, NotifierTestMixin, LoggingMixin):

@defer.inlineCallbacks
def setupReporter(self, **kwargs):
# ignore config error if txrequests is not installed
self.patch(config, '_errors', Mock())
self.master = fakemaster.make_master(testcase=self,
wantData=True, wantDb=True, wantMq=True)
self.master = fakemaster.make_master(
testcase=self, wantData=True, wantDb=True, wantMq=True)

self._http = yield fakehttpclientservice.HTTPClientService.getFakeService(
self.master, self, 'serv', auth=('username', 'passwd'), debug=None,
verify=None)
self.cp = cp = BitbucketServerPRCommentPush("serv", "username", "passwd", **kwargs)
yield cp.setServiceParent(self.master)
self.cp = BitbucketServerPRCommentPush("serv", "username", "passwd", **kwargs)
yield self.cp.setServiceParent(self.master)
yield self.master.startService()
self.cp.messageFormatter = Mock(spec=self.cp.messageFormatter)
self.cp.messageFormatter.formatMessageForBuildResults.return_value = \
{"body": UNICODE_BODY,
"type": "text"}

@defer.inlineCallbacks
def tearDown(self):
yield self.master.stopService()

@defer.inlineCallbacks
def setupBuildResults(self, buildResults):
self.insertTestData([buildResults], buildResults)
self.master.db.builds.setBuildProperty(
20, "pullrequesturl",
"http://example.com/projects/PRO/repos/myrepo/pull-requests/20", "test")
build = yield self.master.data.get(("builds", 20))
defer.returnValue(build)
def setupBuildResults(self, buildResults, set_pr=True):
buildset, builds = yield NotifierTestMixin.setupBuildResults(self, buildResults)
if set_pr:
self.master.db.builds.setBuildProperty(
20, "pullrequesturl", PR_URL, "test")
defer.returnValue((buildset, builds))

@defer.inlineCallbacks
def test_basic(self):
self.setupReporter()
build = yield self.setupBuildResults(SUCCESS)
def test_reporter_basic(self):
yield self.setupReporter()
_, builds = yield self.setupBuildResults(SUCCESS)
build = builds[0]
self._http.expect(
"post",
u'/rest/api/1.0/projects/PRO/repos/myrepo/pull-requests/20/comments',
json={"text": "Builder: Builder0 Status: SUCCESS"},
EXPECTED_API,
json={"text": UNICODE_BODY},
code=201)
build["complete"] = False
# this shouldn't send anything
self.cp.buildStarted(("build", 20, "started"), build)
self.assertEqual(len(self._http._expected), 1)
build["complete"] = True
self.cp.buildFinished(("build", 20, "finished"), build)
self._http.expect(
"post",
u'/rest/api/1.0/projects/PRO/repos/myrepo/pull-requests/20/comments',
json={"text": "Builder: Builder0 Status: FAILED"},
code=201)
build["results"] = FAILURE
self.cp.buildFinished(("build", 20, "finished"), build)
self.cp.buildComplete(("build", 20, "finished"), build)

@defer.inlineCallbacks
def test_setting_options(self):
self.setupReporter(text=Interpolate('url: %(prop:url)s status: %(prop:statustext)s'))
build = yield self.setupBuildResults(SUCCESS)
def test_reporter_non_unicode(self):
if PY3:
raise unittest.SkipTest("not supported in Python3")
yield self.setupReporter()

self.cp.messageFormatter.formatMessageForBuildResults.return_value = \
{"body": "body text",
"type": "text"}

_, builds = yield self.setupBuildResults(SUCCESS)
build = builds[0]
self._http.expect(
"post",
u'/rest/api/1.0/projects/PRO/repos/myrepo/pull-requests/20/comments',
json={'text': 'url: http://localhost:8080/#builders/79/builds/0 status: SUCCESS'},
EXPECTED_API,
json={"text": "body text"},
code=201)
build["complete"] = False
self.cp.buildStarted(("build", 20, "started"), build)
self.assertEqual(len(self._http._expected), 1)
build["complete"] = True
self.cp.buildFinished(("build", 20, "finished"), build)
self.cp.buildComplete(("build", 20, "finished"), build)

@defer.inlineCallbacks
def test_reporter_without_pullrequest(self):
yield self.setupReporter()
_, builds = yield self.setupBuildResults(SUCCESS, set_pr=False)
build = builds[0]
build["complete"] = True
# we don't expect any request
self.cp.buildComplete(("builds", 20, "finished"), build)

@defer.inlineCallbacks
def test_reporter_with_buildset(self):
yield self.setupReporter(buildSetSummary=True)
buildset, _ = yield self.setupBuildResults(SUCCESS)
self._http.expect(
"post",
u'/rest/api/1.0/projects/PRO/repos/myrepo/pull-requests/20/comments',
json={'text': 'url: http://localhost:8080/#builders/79/builds/0 status: FAILED'},
EXPECTED_API,
json={"text": UNICODE_BODY},
code=201)
build["results"] = FAILURE
self.cp.buildFinished(("build", 20, "finished"), build)
self.cp.buildsetComplete(("buildsets", 20, "complete"), buildset)

@defer.inlineCallbacks
def test_error(self):
self.setupReporter()
build = yield self.setupBuildResults(SUCCESS)
def test_reporter_on_invalid_return_code(self):
yield self.setupReporter()
_, builds = yield self.setupBuildResults(SUCCESS)
build = builds[0]
self._http.expect(
"post",
u'/rest/api/1.0/projects/PRO/repos/myrepo/pull-requests/20/comments',
json={"text": "Builder: Builder0 Status: SUCCESS"},
EXPECTED_API,
json={"text": UNICODE_BODY},
code=404,
content_json=None)
self.setUpLogging()
build['complete'] = True
self.cp.buildFinished(("build", 20, "finished"), build)
self.cp.buildComplete(("builds", 20, "finished"), build)
self.assertLogged('404: Unable to send a comment: None')
31 changes: 22 additions & 9 deletions master/docs/manual/cfg-reporters.rst
Expand Up @@ -1033,23 +1033,36 @@ BitbucketServerPRCommentPush
:class:`BitbucketServerPRCommentPush` publishes a comment on a PR using `Bitbucket Server REST API <https://developer.atlassian.com/static/rest/bitbucket-server/5.0.1/bitbucket-rest.html#idm45993793481168>`_.


.. py:class:: BitbucketServerPRCommentPush(base_url, user, password, text=None, verbose=False, builders=None)
.. py:class:: BitBucketServerPRCommentPush(base_url, user, password, messageFormatter=None, verbose=False, debug=None, verify=None, mode=('failing', 'passing', 'warnings'), tags=None, builders=None, schedulers=None, branches=None, buildSetSummary=False):
:param string base_url: The base url of the Bitbucket server host
:param string user: The Bitbucket server user to post as.
:param string password: The Bitbucket server user's password.
:param renderable string text: Used to render the comment text.
A static string can be passed or :class:`Interpolate` for dynamic substitution (default: `Interpolate('Builder: %(prop:buildername)s Status: %(prop:statustext)s')`).
:param messageFormatter: This is an optional instance of :class:`MessageFormatter` that can be used to generate a custom comment.
:param boolean verbose: If True, logs a message for each successful status push.
:param boolean debug: logs every requests and their response
:param boolean verify: disable ssl verification for the case you use temporary self signed certificates
:param list mode: A list of strings which will determine the build status that will be reported.
The values could be ``change``, ``failing``, ``passing``, ``problem``, ``warnings`` or ``exception``.
There are two shortcuts:

Besides the build properties, you have the following available:
``all``
Equivalent to (``change``, ``failing``, ``passing``, ``problem``, ``warnings``, ``exception``)

:url: link to the build page
:statustext: ``SUCCESS`` or ``FAILED`` according to the build status
``warnings``
Equivalent to (``warnings``, ``failing``).

:param boolean verbose: If True, logs a message for each successful status push.
:param list tags: A list of tag names to serve status information for.
Defaults to ``None`` (all tags).
Use either builders or tags, but not both.
:param list builders: Only send update for specified builders.
:param boolean verify: disable ssl verification for the case you use temporary self signed certificates
:param boolean debug: logs every requests and their response
Defaults to ``None`` (all builders).
Use either builders or tags, but not both
:param list schedulers: A list of scheduler names to serve status information for.
Defaults to ``None`` (all schedulers).
:param list branches: A list of branch names to serve status information for.
Defaults to ``None`` (all branches).
:param boolean buildSetSummary: If true, post a comment when a build set is finished with all build completion messages in it, instead of doing it for each separate build.

.. Note::
This reporter depends on the Bitbucket server hook to get the pull request url.
Expand Down

0 comments on commit cce5f7e

Please sign in to comment.