Support for github commit status

Github reporter sets commit status of pull request based on the result
of the pipeline.

Change-Id: Id95bf0dbaa710c555e3a1838d3430e18ac9501aa
Co-Authored-By: Jesse Keating <omgjlk@us.ibm.com>
diff --git a/tests/base.py b/tests/base.py
index 56b1269..0ad1ec1 100755
--- a/tests/base.py
+++ b/tests/base.py
@@ -556,6 +556,7 @@
         self.branch = branch
         self.upstream_root = upstream_root
         self.comments = []
+        self.statuses = {}
         self.updated_at = None
         self.head_sha = None
         self._createPRRef()
@@ -566,11 +567,13 @@
         """Adds a commit on top of the actual PR head."""
         self._addCommitToRepo()
         self._updateTimeStamp()
+        self._clearStatuses()
 
     def forcePush(self):
         """Clears actual commits and add a commit on top of the base."""
         self._addCommitToRepo(reset=True)
         self._updateTimeStamp()
+        self._clearStatuses()
 
     def getPullRequestOpenedEvent(self):
         return self._getPullRequestEvent('opened')
@@ -644,6 +647,16 @@
         repo = self._getRepo()
         return repo.references[self._getPRReference()].commit.hexsha
 
+    def setStatus(self, state, url, description, context):
+        self.statuses[context] = {
+            'state': state,
+            'url': url,
+            'description': description
+        }
+
+    def _clearStatuses(self):
+        self.statuses = {}
+
     def _getPRReference(self):
         return '%s/head' % self.number
 
@@ -745,10 +758,19 @@
         # just returns master for now
         return ['master']
 
-    def report(self, project, pr_number, message, params=None):
+    def commentPull(self, project, pr_number, message):
         pull_request = self.pull_requests[pr_number - 1]
         pull_request.addComment(message)
 
+    def setCommitStatus(self, project, sha, state,
+                        url='', description='', context=''):
+        owner, proj = project.split('/')
+        for pr in self.pull_requests:
+            pr_owner, pr_project = pr.project.split('/')
+            if (pr_owner == owner and pr_project == proj and
+                pr.head_sha == sha):
+                pr.setStatus(state, url, description, context)
+
 
 class BuildHistory(object):
     def __init__(self, **kw):
diff --git a/tests/fixtures/layouts/reporting-github.yaml b/tests/fixtures/layouts/reporting-github.yaml
new file mode 100644
index 0000000..bcbac1b
--- /dev/null
+++ b/tests/fixtures/layouts/reporting-github.yaml
@@ -0,0 +1,45 @@
+- pipeline:
+    name: check
+    description: Standard check
+    manager: independent
+    trigger:
+      github:
+        - event: pull_request
+          action: opened
+    start:
+      github:
+        status: 'pending'
+        comment: false
+    success:
+      github:
+        status: 'success'
+
+- pipeline:
+    name: reporting
+    description: Uncommon reporting
+    manager: independent
+    trigger:
+      github:
+        - event: pull_request
+          action: comment
+          comment: 'reporting check'
+    start:
+      github: {}
+    success:
+      github:
+        comment: false
+    failure:
+      github:
+        comment: false
+
+- job:
+    name: project-test1
+
+- project:
+    name: org/project
+    check:
+      jobs:
+        - project-test1
+    reporting:
+      jobs:
+        - project-test1
diff --git a/tests/fixtures/zuul-github-driver.conf b/tests/fixtures/zuul-github-driver.conf
index b4a85f7..58c7cd5 100644
--- a/tests/fixtures/zuul-github-driver.conf
+++ b/tests/fixtures/zuul-github-driver.conf
@@ -3,6 +3,7 @@
 
 [zuul]
 job_name_in_report=true
+status_url=http://zuul.example.com/status
 
 [merger]
 git_dir=/tmp/zuul-test/git
diff --git a/tests/unit/test_github_driver.py b/tests/unit/test_github_driver.py
index 1e5f6a6..9017ce9 100644
--- a/tests/unit/test_github_driver.py
+++ b/tests/unit/test_github_driver.py
@@ -13,6 +13,8 @@
 # under the License.
 
 import logging
+import re
+from testtools.matchers import MatchesRegex
 
 from tests.base import ZuulTestCase, simple_layout, random_sha1
 
@@ -122,3 +124,44 @@
         """Test that git_ssh option gives git url with ssh"""
         url = self.fake_github_ssh.real_getGitUrl('org/project')
         self.assertEqual('ssh://git@github.com/org/project.git', url)
+
+    @simple_layout('layouts/reporting-github.yaml', driver='github')
+    def test_reporting(self):
+        # pipeline reports pull status both on start and success
+        self.executor_server.hold_jobs_in_build = True
+        pr = self.fake_github.openFakePullRequest('org/project', 'master')
+        self.fake_github.emitEvent(pr.getPullRequestOpenedEvent())
+        self.waitUntilSettled()
+        self.assertIn('check', pr.statuses)
+        check_status = pr.statuses['check']
+        self.assertEqual('Standard check', check_status['description'])
+        self.assertEqual('pending', check_status['state'])
+        self.assertEqual('http://zuul.example.com/status', check_status['url'])
+        self.assertEqual(0, len(pr.comments))
+
+        self.executor_server.hold_jobs_in_build = False
+        self.executor_server.release()
+        self.waitUntilSettled()
+        check_status = pr.statuses['check']
+        self.assertEqual('Standard check', check_status['description'])
+        self.assertEqual('success', check_status['state'])
+        self.assertEqual('http://zuul.example.com/status', check_status['url'])
+        self.assertEqual(1, len(pr.comments))
+        self.assertThat(pr.comments[0],
+                        MatchesRegex('.*Build succeeded.*', re.DOTALL))
+
+        # pipeline does not report any status but does comment
+        self.executor_server.hold_jobs_in_build = True
+        self.fake_github.emitEvent(
+            pr.getCommentAddedEvent('reporting check'))
+        self.waitUntilSettled()
+        self.assertNotIn('reporting', pr.statuses)
+        # comments increased by one for the start message
+        self.assertEqual(2, len(pr.comments))
+        self.assertThat(pr.comments[1],
+                        MatchesRegex('.*Starting reporting jobs.*', re.DOTALL))
+        self.executor_server.hold_jobs_in_build = False
+        self.executor_server.release()
+        self.waitUntilSettled()
+        self.assertNotIn('reporting', pr.statuses)
+        self.assertEqual(2, len(pr.comments))