Support for dependent pipelines with github

Change-Id: I5e7937d0f524107572f8aec7945f94b620d77b61
Co-Authored-By: Jesse Keating <omgjlk@us.ibm.com>
diff --git a/tests/unit/test_github_driver.py b/tests/unit/test_github_driver.py
index 97a0066..967a226 100644
--- a/tests/unit/test_github_driver.py
+++ b/tests/unit/test_github_driver.py
@@ -31,14 +31,14 @@
     def test_pull_event(self):
         self.executor_server.hold_jobs_in_build = True
 
-        pr = self.fake_github.openFakePullRequest('org/project', 'master')
-        self.fake_github.emitEvent(pr.getPullRequestOpenedEvent())
+        A = self.fake_github.openFakePullRequest('org/project', 'master', 'A')
+        self.fake_github.emitEvent(A.getPullRequestOpenedEvent())
         self.waitUntilSettled()
 
         build_params = self.builds[0].parameters
         self.assertEqual('master', build_params['ZUUL_BRANCH'])
-        self.assertEqual(str(pr.number), build_params['ZUUL_CHANGE'])
-        self.assertEqual(pr.head_sha, build_params['ZUUL_PATCHSET'])
+        self.assertEqual(str(A.number), build_params['ZUUL_CHANGE'])
+        self.assertEqual(A.head_sha, build_params['ZUUL_PATCHSET'])
 
         self.executor_server.hold_jobs_in_build = False
         self.executor_server.release()
@@ -51,20 +51,20 @@
 
         job = self.getJobFromHistory('project-test2')
         zuulvars = job.parameters['vars']['zuul']
-        self.assertEqual(pr.number, zuulvars['change'])
-        self.assertEqual(pr.head_sha, zuulvars['patchset'])
-        self.assertEqual(1, len(pr.comments))
+        self.assertEqual(A.number, zuulvars['change'])
+        self.assertEqual(A.head_sha, zuulvars['patchset'])
+        self.assertEqual(1, len(A.comments))
 
     @simple_layout('layouts/basic-github.yaml', driver='github')
     def test_comment_event(self):
-        pr = self.fake_github.openFakePullRequest('org/project', 'master')
-        self.fake_github.emitEvent(pr.getCommentAddedEvent('test me'))
+        A = self.fake_github.openFakePullRequest('org/project', 'master', 'A')
+        self.fake_github.emitEvent(A.getCommentAddedEvent('test me'))
         self.waitUntilSettled()
         self.assertEqual(2, len(self.history))
 
         # Test an unmatched comment, history should remain the same
-        pr = self.fake_github.openFakePullRequest('org/project', 'master')
-        self.fake_github.emitEvent(pr.getCommentAddedEvent('casual comment'))
+        B = self.fake_github.openFakePullRequest('org/project', 'master', 'B')
+        self.fake_github.emitEvent(B.getCommentAddedEvent('casual comment'))
         self.waitUntilSettled()
         self.assertEqual(2, len(self.history))
 
@@ -116,7 +116,7 @@
 
     @simple_layout('layouts/labeling-github.yaml', driver='github')
     def test_labels(self):
-        A = self.fake_github.openFakePullRequest('org/project', 'master')
+        A = self.fake_github.openFakePullRequest('org/project', 'master', 'A')
         self.fake_github.emitEvent(A.addLabel('test'))
         self.waitUntilSettled()
         self.assertEqual(1, len(self.history))
@@ -124,7 +124,7 @@
         self.assertEqual(['tests passed'], A.labels)
 
         # test label removed
-        B = self.fake_github.openFakePullRequest('org/project', 'master')
+        B = self.fake_github.openFakePullRequest('org/project', 'master', 'B')
         B.addLabel('do not test')
         self.fake_github.emitEvent(B.removeLabel('do not test'))
         self.waitUntilSettled()
@@ -133,7 +133,7 @@
         self.assertEqual(['tests passed'], B.labels)
 
         # test unmatched label
-        C = self.fake_github.openFakePullRequest('org/project', 'master')
+        C = self.fake_github.openFakePullRequest('org/project', 'master', 'C')
         self.fake_github.emitEvent(C.addLabel('other label'))
         self.waitUntilSettled()
         self.assertEqual(2, len(self.history))
@@ -143,16 +143,16 @@
     def test_dequeue_pull_synchronized(self):
         self.executor_server.hold_jobs_in_build = True
 
-        pr = self.fake_github.openFakePullRequest(
-            'org/one-job-project', 'master')
-        self.fake_github.emitEvent(pr.getPullRequestOpenedEvent())
+        A = self.fake_github.openFakePullRequest(
+            'org/one-job-project', 'master', 'A')
+        self.fake_github.emitEvent(A.getPullRequestOpenedEvent())
         self.waitUntilSettled()
 
         # event update stamp has resolution one second, wait so the latter
         # one has newer timestamp
         time.sleep(1)
-        pr.addCommit()
-        self.fake_github.emitEvent(pr.getPullRequestSynchronizeEvent())
+        A.addCommit()
+        self.fake_github.emitEvent(A.getPullRequestSynchronizeEvent())
         self.waitUntilSettled()
 
         self.executor_server.hold_jobs_in_build = False
@@ -166,11 +166,11 @@
     def test_dequeue_pull_abandoned(self):
         self.executor_server.hold_jobs_in_build = True
 
-        pr = self.fake_github.openFakePullRequest(
-            'org/one-job-project', 'master')
-        self.fake_github.emitEvent(pr.getPullRequestOpenedEvent())
+        A = self.fake_github.openFakePullRequest(
+            'org/one-job-project', 'master', 'A')
+        self.fake_github.emitEvent(A.getPullRequestOpenedEvent())
         self.waitUntilSettled()
-        self.fake_github.emitEvent(pr.getPullRequestClosedEvent())
+        self.fake_github.emitEvent(A.getPullRequestClosedEvent())
         self.waitUntilSettled()
 
         self.executor_server.hold_jobs_in_build = False
@@ -196,54 +196,54 @@
     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())
+        A = self.fake_github.openFakePullRequest('org/project', 'master', 'A')
+        self.fake_github.emitEvent(A.getPullRequestOpenedEvent())
         self.waitUntilSettled()
-        self.assertIn('check', pr.statuses)
-        check_status = pr.statuses['check']
+        self.assertIn('check', A.statuses)
+        check_status = A.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.assertEqual(0, len(A.comments))
 
         self.executor_server.hold_jobs_in_build = False
         self.executor_server.release()
         self.waitUntilSettled()
-        check_status = pr.statuses['check']
+        check_status = A.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],
+        self.assertEqual(1, len(A.comments))
+        self.assertThat(A.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'))
+            A.getCommentAddedEvent('reporting check'))
         self.waitUntilSettled()
-        self.assertNotIn('reporting', pr.statuses)
+        self.assertNotIn('reporting', A.statuses)
         # comments increased by one for the start message
-        self.assertEqual(2, len(pr.comments))
-        self.assertThat(pr.comments[1],
+        self.assertEqual(2, len(A.comments))
+        self.assertThat(A.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))
+        self.assertNotIn('reporting', A.statuses)
+        self.assertEqual(2, len(A.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')
+        A = self.fake_github.openFakePullRequest('org/project', 'master', 'A')
         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')
+        B = self.fake_github.openFakePullRequest('org/project', 'master', 'B')
         self.fake_github.emitEvent(B.getCommentAddedEvent('merge me'))
         self.waitUntilSettled()
         self.assertFalse(B.is_merged)
@@ -252,7 +252,7 @@
         # 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')
+        C = self.fake_github.openFakePullRequest('org/project', 'master', 'C')
         self.fake_github.emitEvent(C.getCommentAddedEvent('merge me'))
         self.waitUntilSettled()
         self.assertTrue(C.is_merged)
@@ -260,7 +260,180 @@
         # 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')
+        D = self.fake_github.openFakePullRequest('org/project', 'master', 'D')
         self.fake_github.emitEvent(D.getCommentAddedEvent('merge me'))
         self.waitUntilSettled()
         self.assertFalse(D.is_merged)
+
+    @simple_layout('layouts/dependent-github.yaml', driver='github')
+    def test_parallel_changes(self):
+        "Test that changes are tested in parallel and merged in series"
+
+        self.executor_server.hold_jobs_in_build = True
+        A = self.fake_github.openFakePullRequest('org/project', 'master', 'A')
+        B = self.fake_github.openFakePullRequest('org/project', 'master', 'B')
+        C = self.fake_github.openFakePullRequest('org/project', 'master', 'C')
+
+        self.fake_github.emitEvent(A.addLabel('merge'))
+        self.fake_github.emitEvent(B.addLabel('merge'))
+        self.fake_github.emitEvent(C.addLabel('merge'))
+
+        self.waitUntilSettled()
+        self.assertEqual(len(self.builds), 1)
+        self.assertEqual(self.builds[0].name, 'project-merge')
+        self.assertTrue(self.builds[0].hasChanges(A))
+
+        self.executor_server.release('.*-merge')
+        self.waitUntilSettled()
+        self.assertEqual(len(self.builds), 3)
+        self.assertEqual(self.builds[0].name, 'project-test1')
+        self.assertTrue(self.builds[0].hasChanges(A))
+        self.assertEqual(self.builds[1].name, 'project-test2')
+        self.assertTrue(self.builds[1].hasChanges(A))
+        self.assertEqual(self.builds[2].name, 'project-merge')
+        self.assertTrue(self.builds[2].hasChanges(A, B))
+
+        self.executor_server.release('.*-merge')
+        self.waitUntilSettled()
+        self.assertEqual(len(self.builds), 5)
+        self.assertEqual(self.builds[0].name, 'project-test1')
+        self.assertTrue(self.builds[0].hasChanges(A))
+        self.assertEqual(self.builds[1].name, 'project-test2')
+        self.assertTrue(self.builds[1].hasChanges(A))
+
+        self.assertEqual(self.builds[2].name, 'project-test1')
+        self.assertTrue(self.builds[2].hasChanges(A))
+        self.assertEqual(self.builds[3].name, 'project-test2')
+        self.assertTrue(self.builds[3].hasChanges(A, B))
+
+        self.assertEqual(self.builds[4].name, 'project-merge')
+        self.assertTrue(self.builds[4].hasChanges(A, B, C))
+
+        self.executor_server.release('.*-merge')
+        self.waitUntilSettled()
+        self.assertEqual(len(self.builds), 6)
+        self.assertEqual(self.builds[0].name, 'project-test1')
+        self.assertTrue(self.builds[0].hasChanges(A))
+        self.assertEqual(self.builds[1].name, 'project-test2')
+        self.assertTrue(self.builds[1].hasChanges(A))
+
+        self.assertEqual(self.builds[2].name, 'project-test1')
+        self.assertTrue(self.builds[2].hasChanges(A, B))
+        self.assertEqual(self.builds[3].name, 'project-test2')
+        self.assertTrue(self.builds[3].hasChanges(A, B))
+
+        self.assertEqual(self.builds[4].name, 'project-test1')
+        self.assertTrue(self.builds[4].hasChanges(A, B, C))
+        self.assertEqual(self.builds[5].name, 'project-test2')
+        self.assertTrue(self.builds[5].hasChanges(A, B, C))
+
+        all_builds = self.builds[:]
+        self.release(all_builds[2])
+        self.release(all_builds[3])
+        self.waitUntilSettled()
+        self.assertFalse(A.is_merged)
+        self.assertFalse(B.is_merged)
+        self.assertFalse(C.is_merged)
+
+        self.release(all_builds[0])
+        self.release(all_builds[1])
+        self.waitUntilSettled()
+        self.assertTrue(A.is_merged)
+        self.assertTrue(B.is_merged)
+        self.assertFalse(C.is_merged)
+
+        self.executor_server.hold_jobs_in_build = False
+        self.executor_server.release()
+        self.waitUntilSettled()
+        self.assertEqual(len(self.builds), 0)
+        self.assertEqual(len(self.history), 9)
+        self.assertTrue(C.is_merged)
+
+        self.assertNotIn('merge', A.labels)
+        self.assertNotIn('merge', B.labels)
+        self.assertNotIn('merge', C.labels)
+
+    @simple_layout('layouts/dependent-github.yaml', driver='github')
+    def test_failed_changes(self):
+        "Test that a change behind a failed change is retested"
+        self.executor_server.hold_jobs_in_build = True
+
+        A = self.fake_github.openFakePullRequest('org/project', 'master', 'A')
+        B = self.fake_github.openFakePullRequest('org/project', 'master', 'B')
+
+        self.executor_server.failJob('project-test1', A)
+
+        self.fake_github.emitEvent(A.addLabel('merge'))
+        self.fake_github.emitEvent(B.addLabel('merge'))
+        self.waitUntilSettled()
+
+        self.executor_server.release('.*-merge')
+        self.waitUntilSettled()
+
+        self.executor_server.hold_jobs_in_build = False
+        self.executor_server.release()
+
+        self.waitUntilSettled()
+        # It's certain that the merge job for change 2 will run, but
+        # the test1 and test2 jobs may or may not run.
+        self.assertTrue(len(self.history) > 6)
+        self.assertFalse(A.is_merged)
+        self.assertTrue(B.is_merged)
+        self.assertNotIn('merge', A.labels)
+        self.assertNotIn('merge', B.labels)
+
+    @simple_layout('layouts/dependent-github.yaml', driver='github')
+    def test_failed_change_at_head(self):
+        "Test that if a change at the head fails, jobs behind it are canceled"
+
+        self.executor_server.hold_jobs_in_build = True
+        A = self.fake_github.openFakePullRequest('org/project', 'master', 'A')
+        B = self.fake_github.openFakePullRequest('org/project', 'master', 'B')
+        C = self.fake_github.openFakePullRequest('org/project', 'master', 'C')
+
+        self.executor_server.failJob('project-test1', A)
+
+        self.fake_github.emitEvent(A.addLabel('merge'))
+        self.fake_github.emitEvent(B.addLabel('merge'))
+        self.fake_github.emitEvent(C.addLabel('merge'))
+
+        self.waitUntilSettled()
+
+        self.assertEqual(len(self.builds), 1)
+        self.assertEqual(self.builds[0].name, 'project-merge')
+        self.assertTrue(self.builds[0].hasChanges(A))
+
+        self.executor_server.release('.*-merge')
+        self.waitUntilSettled()
+        self.executor_server.release('.*-merge')
+        self.waitUntilSettled()
+        self.executor_server.release('.*-merge')
+        self.waitUntilSettled()
+
+        self.assertEqual(len(self.builds), 6)
+        self.assertEqual(self.builds[0].name, 'project-test1')
+        self.assertEqual(self.builds[1].name, 'project-test2')
+        self.assertEqual(self.builds[2].name, 'project-test1')
+        self.assertEqual(self.builds[3].name, 'project-test2')
+        self.assertEqual(self.builds[4].name, 'project-test1')
+        self.assertEqual(self.builds[5].name, 'project-test2')
+
+        self.release(self.builds[0])
+        self.waitUntilSettled()
+
+        # project-test2, project-merge for B
+        self.assertEqual(len(self.builds), 2)
+        self.assertEqual(self.countJobResults(self.history, 'ABORTED'), 4)
+
+        self.executor_server.hold_jobs_in_build = False
+        self.executor_server.release()
+        self.waitUntilSettled()
+
+        self.assertEqual(len(self.builds), 0)
+        self.assertEqual(len(self.history), 15)
+        self.assertFalse(A.is_merged)
+        self.assertTrue(B.is_merged)
+        self.assertTrue(C.is_merged)
+        self.assertNotIn('merge', A.labels)
+        self.assertNotIn('merge', B.labels)
+        self.assertNotIn('merge', C.labels)