Merge pull requests from github reporter

Github reporter can be configured to merge pull reqeusts.

When there are multiple merges called at the same time, it leads to a
situation when github returns 405 MethodNotAllowed error becuase github
is checking the branch mergeability.
When we encounter this situation, we try to wait a bit (2 seconds for
now) and try to merge again.

Pre-release version of Github3.py has to be used, because the latest
released version 9.4 has a bug in merge method. Furthermore the newest
merge method supports specifying exact sha to be merged, which is
desirable to ensure that the exact commit that went through the pipeline
gets merged.

Both are already fixed in the stable branch, but not yet released on
PyPi. See:
https://github.com/sigmavirus24/github3.py/commit/90c6b7c2653d65ce686cf4346f9aea9cb9c5c836
https://github.com/sigmavirus24/github3.py/commit/6ef02cb33ff21257eeaf9cab186419ca45ef5806

Change-Id: I0c3abbcce476774a5ba8981c171382eaa4fe0abf
diff --git a/tests/base.py b/tests/base.py
index 0ad1ec1..d7bf467 100755
--- a/tests/base.py
+++ b/tests/base.py
@@ -70,6 +70,7 @@
 import zuul.merger.server
 import zuul.nodepool
 import zuul.zk
+from zuul.exceptions import MergeFailure
 
 FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
                            'fixtures')
@@ -559,6 +560,7 @@
         self.statuses = {}
         self.updated_at = None
         self.head_sha = None
+        self.is_merged = False
         self._createPRRef()
         self._addCommitToRepo()
         self._updateTimeStamp()
@@ -693,6 +695,8 @@
         self.pr_number = 0
         self.pull_requests = []
         self.upstream_root = upstream_root
+        self.merge_failure = False
+        self.merge_not_allowed_count = 0
 
     def openFakePullRequest(self, project, branch):
         self.pr_number += 1
@@ -762,6 +766,16 @@
         pull_request = self.pull_requests[pr_number - 1]
         pull_request.addComment(message)
 
+    def mergePull(self, project, pr_number, sha=None):
+        pull_request = self.pull_requests[pr_number - 1]
+        if self.merge_failure:
+            raise Exception('Pull request was not merged')
+        if self.merge_not_allowed_count > 0:
+            self.merge_not_allowed_count -= 1
+            raise MergeFailure('Merge was not successful due to mergeability'
+                               ' conflict')
+        pull_request.is_merged = True
+
     def setCommitStatus(self, project, sha, state,
                         url='', description='', context=''):
         owner, proj = project.split('/')
diff --git a/tests/fixtures/layouts/merging-github.yaml b/tests/fixtures/layouts/merging-github.yaml
new file mode 100644
index 0000000..4e13063
--- /dev/null
+++ b/tests/fixtures/layouts/merging-github.yaml
@@ -0,0 +1,19 @@
+- pipeline:
+    name: merge
+    description: Pipeline for merging the pull request
+    manager: independent
+    trigger:
+      github:
+        - event: pull_request
+          action: comment
+          comment: 'merge me'
+    success:
+      github:
+        merge: true
+        comment: false
+
+- project:
+    name: org/project
+    merge:
+      jobs:
+        - noop
diff --git a/tests/unit/test_github_driver.py b/tests/unit/test_github_driver.py
index 9017ce9..409d966 100644
--- a/tests/unit/test_github_driver.py
+++ b/tests/unit/test_github_driver.py
@@ -165,3 +165,35 @@
         self.waitUntilSettled()
         self.assertNotIn('reporting', pr.statuses)
         self.assertEqual(2, len(pr.comments))
+
+    @simple_layout('layouts/merging-github.yaml', driver='github')
+    def test_report_pull_merge(self):
+        # pipeline merges the pull request on success
+        A = self.fake_github.openFakePullRequest('org/project', 'master')
+        self.fake_github.emitEvent(A.getCommentAddedEvent('merge me'))
+        self.waitUntilSettled()
+        self.assertTrue(A.is_merged)
+
+        # pipeline merges the pull request on success after failure
+        self.fake_github.merge_failure = True
+        B = self.fake_github.openFakePullRequest('org/project', 'master')
+        self.fake_github.emitEvent(B.getCommentAddedEvent('merge me'))
+        self.waitUntilSettled()
+        self.assertFalse(B.is_merged)
+        self.fake_github.merge_failure = False
+
+        # pipeline merges the pull request on second run of merge
+        # first merge failed on 405 Method Not Allowed error
+        self.fake_github.merge_not_allowed_count = 1
+        C = self.fake_github.openFakePullRequest('org/project', 'master')
+        self.fake_github.emitEvent(C.getCommentAddedEvent('merge me'))
+        self.waitUntilSettled()
+        self.assertTrue(C.is_merged)
+
+        # pipeline does not merge the pull request
+        # merge failed on 405 Method Not Allowed error - twice
+        self.fake_github.merge_not_allowed_count = 2
+        D = self.fake_github.openFakePullRequest('org/project', 'master')
+        self.fake_github.emitEvent(D.getCommentAddedEvent('merge me'))
+        self.waitUntilSettled()
+        self.assertFalse(D.is_merged)