Implement Depends-On for github
This adds the capability for one github project pr to depend on another
project's PR, by marking it so in the pull request body. This means we
need to be able to react to editing of the pull request body to see if a
dependency was created or removed.
Reverse searching for a change that needs the current change is
implemented via a github search of (open) pull request bodies.
Various bits of how change objects are created and cached needed to be
fixed to support a change being added while adding another change.
Also put a try around the webhook event handling, so that a traceback,
such as when a dependency loop is detected, can be gracefully logged
instead of the whole thing shutting down.
Change-Id: Ia32e6cc70b163f7e32e74d88f0a1f9b4e3255320
Story: 2000774
Task: 4686
diff --git a/tests/unit/test_github_crd.py b/tests/unit/test_github_crd.py
new file mode 100644
index 0000000..bc7d499
--- /dev/null
+++ b/tests/unit/test_github_crd.py
@@ -0,0 +1,172 @@
+#!/usr/bin/env python
+# Copyright (c) 2017 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from tests.base import ZuulTestCase, simple_layout
+
+
+class TestGithubCrossRepoDeps(ZuulTestCase):
+ """Test Github cross-repo dependencies"""
+ config_file = 'zuul-github-driver.conf'
+
+ @simple_layout('layouts/crd-github.yaml', driver='github')
+ def test_crd_independent(self):
+ "Test cross-repo dependences on an independent pipeline"
+
+ # Create a change in project1 that a project2 change will depend on
+ A = self.fake_github.openFakePullRequest('org/project1', 'master', 'A')
+
+ # Create a commit in B that sets the dependency on A
+ msg = "Depends-On: https://github.com/org/project1/pull/%s" % A.number
+ B = self.fake_github.openFakePullRequest('org/project2', 'master', 'B',
+ body=msg)
+
+ # Make an event to re-use
+ event = B.getPullRequestEditedEvent()
+
+ self.fake_github.emitEvent(event)
+ self.waitUntilSettled()
+
+ # The changes for the job from project2 should include the project1
+ # PR contet
+ changes = self.getJobFromHistory(
+ 'project2-test', 'org/project2').changes
+
+ self.assertEqual(changes, "%s,%s %s,%s" % (A.number,
+ A.head_sha,
+ B.number,
+ B.head_sha))
+
+ # There should be no more changes in the queue
+ tenant = self.sched.abide.tenants.get('tenant-one')
+ self.assertEqual(len(tenant.layout.pipelines['check'].queues), 0)
+
+ @simple_layout('layouts/crd-github.yaml', driver='github')
+ def test_crd_dependent(self):
+ "Test cross-repo dependences on a dependent pipeline"
+
+ # Create a change in project3 that a project4 change will depend on
+ A = self.fake_github.openFakePullRequest('org/project3', 'master', 'A')
+
+ # Create a commit in B that sets the dependency on A
+ msg = "Depends-On: https://github.com/org/project3/pull/%s" % A.number
+ B = self.fake_github.openFakePullRequest('org/project4', 'master', 'B',
+ body=msg)
+
+ # Make an event to re-use
+ event = B.getPullRequestEditedEvent()
+
+ self.fake_github.emitEvent(event)
+ self.waitUntilSettled()
+
+ # The changes for the job from project4 should include the project3
+ # PR contet
+ changes = self.getJobFromHistory(
+ 'project4-test', 'org/project4').changes
+
+ self.assertEqual(changes, "%s,%s %s,%s" % (A.number,
+ A.head_sha,
+ B.number,
+ B.head_sha))
+
+ self.assertTrue(A.is_merged)
+ self.assertTrue(B.is_merged)
+
+ @simple_layout('layouts/crd-github.yaml', driver='github')
+ def test_crd_unshared_dependent(self):
+ "Test cross-repo dependences on unshared dependent pipeline"
+
+ # Create a change in project1 that a project2 change will depend on
+ A = self.fake_github.openFakePullRequest('org/project5', 'master', 'A')
+
+ # Create a commit in B that sets the dependency on A
+ msg = "Depends-On: https://github.com/org/project5/pull/%s" % A.number
+ B = self.fake_github.openFakePullRequest('org/project6', 'master', 'B',
+ body=msg)
+
+ # Make an event for B
+ event = B.getPullRequestEditedEvent()
+
+ # Emit for B, which should not enqueue A because they do not share
+ # A queue. Since B depends on A, and A isn't enqueue, B will not run
+ self.fake_github.emitEvent(event)
+ self.waitUntilSettled()
+
+ self.assertEqual(0, len(self.history))
+
+ # Enqueue A alone, let it finish
+ self.fake_github.emitEvent(A.getPullRequestEditedEvent())
+ self.waitUntilSettled()
+
+ self.assertTrue(A.is_merged)
+ self.assertFalse(B.is_merged)
+ self.assertEqual(1, len(self.history))
+
+ # With A merged, B should go through
+ self.fake_github.emitEvent(event)
+ self.waitUntilSettled()
+
+ self.assertTrue(B.is_merged)
+ self.assertEqual(2, len(self.history))
+
+ @simple_layout('layouts/crd-github.yaml', driver='github')
+ def test_crd_cycle(self):
+ "Test cross-repo dependency cycles"
+
+ # A -> B -> A
+ msg = "Depends-On: https://github.com/org/project6/pull/2"
+ A = self.fake_github.openFakePullRequest('org/project5', 'master', 'A',
+ body=msg)
+ msg = "Depends-On: https://github.com/org/project5/pull/1"
+ B = self.fake_github.openFakePullRequest('org/project6', 'master', 'B',
+ body=msg)
+
+ self.fake_github.emitEvent(A.getPullRequestEditedEvent())
+ self.waitUntilSettled()
+
+ self.assertFalse(A.is_merged)
+ self.assertFalse(B.is_merged)
+ self.assertEqual(0, len(self.history))
+
+ @simple_layout('layouts/crd-github.yaml', driver='github')
+ def test_crd_needed_changes(self):
+ "Test cross-repo needed changes discovery"
+
+ # Given change A and B, where B depends on A, when A
+ # completes B should be enqueued (using a shared queue)
+
+ # Create a change in project3 that a project4 change will depend on
+ A = self.fake_github.openFakePullRequest('org/project3', 'master', 'A')
+
+ # Set B to depend on A
+ msg = "Depends-On: https://github.com/org/project3/pull/%s" % A.number
+ B = self.fake_github.openFakePullRequest('org/project4', 'master', 'B',
+ body=msg)
+
+ # Enqueue A, which when finished should enqueue B
+ self.fake_github.emitEvent(A.getPullRequestEditedEvent())
+ self.waitUntilSettled()
+
+ # The changes for the job from project4 should include the project3
+ # PR contet
+ changes = self.getJobFromHistory(
+ 'project4-test', 'org/project4').changes
+
+ self.assertEqual(changes, "%s,%s %s,%s" % (A.number,
+ A.head_sha,
+ B.number,
+ B.head_sha))
+
+ self.assertTrue(A.is_merged)
+ self.assertTrue(B.is_merged)