
Given two PRs with B depending on A which are enqueued in gate, A is closed and then immediately reopened. This sequence of events will currently dequeue A and then immediately enqueue it behind B. Since the check for whether a dependency is already in the queue doesn't care if it's ahead or behind the current change, we'll not dequeue B and the content of builds executed by B will not include A. This change updates the check to determine if a change is already in the queue to only check for changes ahead of it. This causes B to be correctly dequeued in the next pipeline pass. This behavior is correct, but isn't always intuitive or consistent. If the time between closing and reopening a change is long enough for a pipeline process, then both changes will be enqueued by the reopening (because we check for changes needing enqueued changes and enqueue them behind). But if both events are processed in a single pipeline run, then the removal of B happens after the re-enqueue of A which means that it won't be re-added. To correct this, whenever we remove abandoned changes, we will also remove changes behind them that depend on the removed abandoned changes at the same time. This means that in our scenario above, the re-enqueue happens under the same conditions as the original enqueue, and both A and B are re-enqueued. Co-Authored-By: James E. Blair <jim@acmegating.com> Change-Id: Ia1d79bccb9ea39e486483283611601aa23903000
260 lines
10 KiB
Python
260 lines
10 KiB
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'
|
|
scheduler_count = 1
|
|
|
|
@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.scheds.first.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)
|
|
|
|
@simple_layout('layouts/github-message-update.yaml', driver='github')
|
|
def test_crd_message_update(self):
|
|
"Test a change is dequeued when the PR in its depends-on is updated"
|
|
|
|
# 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)
|
|
|
|
# Create a commit in C that sets the dependency on B
|
|
msg = "Depends-On: https://github.com/org/project2/pull/%s" % B.number
|
|
C = self.fake_github.openFakePullRequest('org/project3', 'master', 'C',
|
|
body=msg)
|
|
|
|
# A change we'll use later to replace A
|
|
A1 = self.fake_github.openFakePullRequest(
|
|
'org/project1', 'master', 'A1')
|
|
|
|
self.executor_server.hold_jobs_in_build = True
|
|
|
|
# Enqueue A,B,C
|
|
self.fake_github.emitEvent(C.getReviewAddedEvent('approved'))
|
|
self.waitUntilSettled()
|
|
|
|
self.assertEqual(len(self.builds), 1)
|
|
tenant = self.scheds.first.sched.abide.tenants.get('tenant-one')
|
|
items = tenant.layout.pipelines['check'].getAllItems()
|
|
self.assertEqual(len(items), 3)
|
|
|
|
# Update B to point at A1 instead of A
|
|
msg = "Depends-On: https://github.com/org/project1/pull/%s" % A1.number
|
|
self.fake_github.emitEvent(B.editBody(msg))
|
|
self.waitUntilSettled()
|
|
|
|
# Release
|
|
self.executor_server.hold_jobs_in_build = False
|
|
self.executor_server.release()
|
|
self.waitUntilSettled()
|
|
|
|
# The job should be aborted since B was updated while enqueued.
|
|
self.assertHistory([dict(name='project3-test', result='ABORTED')])
|
|
|
|
@simple_layout('layouts/crd-github.yaml', driver='github')
|
|
def test_crd_dependent_close_reopen(self):
|
|
"""
|
|
Test that closing and reopening PR correctly re-enqueues the
|
|
dependent change in the correct order.
|
|
"""
|
|
self.executor_server.hold_jobs_in_build = True
|
|
|
|
A = self.fake_github.openFakePullRequest('org/project3', 'master', 'A')
|
|
B = self.fake_github.openFakePullRequest(
|
|
'org/project4', 'master', 'B',
|
|
body=f"Depends-On: https://github.com/org/project3/pull/{A.number}"
|
|
)
|
|
|
|
self.fake_github.emitEvent(B.getPullRequestEditedEvent())
|
|
self.waitUntilSettled()
|
|
|
|
# Close and reopen PR A
|
|
self.fake_github.emitEvent(A.getPullRequestClosedEvent())
|
|
self.fake_github.emitEvent(A.getPullRequestReopenedEvent())
|
|
self.waitUntilSettled()
|
|
|
|
self.executor_server.hold_jobs_in_build = False
|
|
self.executor_server.release()
|
|
self.waitUntilSettled()
|
|
|
|
# The changes for the job from project4 should include the project3
|
|
# PR content
|
|
self.assertHistory([
|
|
dict(name='project3-test', result='ABORTED',
|
|
changes=f"{A.number},{A.head_sha}"),
|
|
dict(name='project4-test', result='ABORTED',
|
|
changes=f"{A.number},{A.head_sha} {B.number},{B.head_sha}"),
|
|
dict(name='project3-test', result='SUCCESS',
|
|
changes=f"{A.number},{A.head_sha}"),
|
|
dict(name='project4-test', result='SUCCESS',
|
|
changes=f"{A.number},{A.head_sha} {B.number},{B.head_sha}"),
|
|
], ordered=False)
|
|
|
|
self.assertTrue(A.is_merged)
|
|
self.assertTrue(B.is_merged)
|