summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/base.py49
-rw-r--r--tests/fakegithub.py2
-rw-r--r--tests/fixtures/layouts/gerrit-trigger-requirements.yaml162
-rw-r--r--tests/fixtures/layouts/github-trigger-requirements.yaml112
-rw-r--r--tests/unit/test_circular_dependencies.py64
-rw-r--r--tests/unit/test_client.py136
-rw-r--r--tests/unit/test_gerrit.py99
-rw-r--r--tests/unit/test_github_driver.py7
-rw-r--r--tests/unit/test_github_requirements.py178
-rw-r--r--tests/unit/test_requirements.py221
-rw-r--r--tests/unit/test_scheduler.py79
11 files changed, 1069 insertions, 40 deletions
diff --git a/tests/base.py b/tests/base.py
index fd927a92c..af10ebe96 100644
--- a/tests/base.py
+++ b/tests/base.py
@@ -384,7 +384,7 @@ class FakeGerritChange(object):
def __init__(self, gerrit, number, project, branch, subject,
status='NEW', upstream_root=None, files={},
parent=None, merge_parents=None, merge_files=None,
- topic=None):
+ topic=None, empty=False):
self.gerrit = gerrit
self.source = gerrit
self.reported = 0
@@ -403,6 +403,7 @@ class FakeGerritChange(object):
self.comments = []
self.checks = {}
self.checks_history = []
+ self.submit_requirements = []
self.data = {
'branch': branch,
'comments': self.comments,
@@ -429,7 +430,7 @@ class FakeGerritChange(object):
self.addMergePatchset(parents=merge_parents,
merge_files=merge_files)
else:
- self.addPatchset(files=files, parent=parent)
+ self.addPatchset(files=files, parent=parent, empty=empty)
if merge_parents:
self.data['parents'] = merge_parents
elif parent:
@@ -503,9 +504,11 @@ class FakeGerritChange(object):
repo.heads['master'].checkout()
return r
- def addPatchset(self, files=None, large=False, parent=None):
+ def addPatchset(self, files=None, large=False, parent=None, empty=False):
self.latest_patchset += 1
- if not files:
+ if empty:
+ files = {}
+ elif not files:
fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
data = ("test %s %s %s\n" %
(self.branch, self.number, self.latest_patchset))
@@ -786,6 +789,12 @@ class FakeGerritChange(object):
return [{'status': 'NOT_READY',
'labels': labels}]
+ def getSubmitRequirements(self):
+ return self.submit_requirements
+
+ def setSubmitRequirements(self, reqs):
+ self.submit_requirements = reqs
+
def setDependsOn(self, other, patchset):
self.depends_on_change = other
self.depends_on_patchset = patchset
@@ -892,6 +901,7 @@ class FakeGerritChange(object):
data['parents'] = self.data['parents']
if 'topic' in self.data:
data['topic'] = self.data['topic']
+ data['submit_requirements'] = self.getSubmitRequirements()
return json.loads(json.dumps(data))
def queryRevisionHTTP(self, revision):
@@ -940,6 +950,7 @@ class FakeGerritChange(object):
if self.fail_merge:
return
self.data['status'] = 'MERGED'
+ self.data['open'] = False
self.open = False
path = os.path.join(self.upstream_root, self.project)
@@ -1330,7 +1341,7 @@ class FakeGerritConnection(gerritconnection.GerritConnection):
def addFakeChange(self, project, branch, subject, status='NEW',
files=None, parent=None, merge_parents=None,
- merge_files=None, topic=None):
+ merge_files=None, topic=None, empty=False):
"""Add a change to the fake Gerrit."""
self.change_number += 1
c = FakeGerritChange(self, self.change_number, project, branch,
@@ -1338,7 +1349,7 @@ class FakeGerritConnection(gerritconnection.GerritConnection):
status=status, files=files, parent=parent,
merge_parents=merge_parents,
merge_files=merge_files,
- topic=topic)
+ topic=topic, empty=empty)
self.changes[self.change_number] = c
return c
@@ -1494,8 +1505,9 @@ class FakeGerritConnection(gerritconnection.GerritConnection):
msg = msg[1:-1]
l = [queryMethod(change) for change in self.changes.values()
if msg in change.data['commitMessage']]
- elif query.startswith("status:"):
+ else:
cut_off_time = 0
+ l = list(self.changes.values())
parts = query.split(" ")
for part in parts:
if part.startswith("-age"):
@@ -1503,17 +1515,18 @@ class FakeGerritConnection(gerritconnection.GerritConnection):
cut_off_time = (
datetime.datetime.now().timestamp() - float(age[:-1])
)
- l = [
- queryMethod(change) for change in self.changes.values()
- if change.data["lastUpdated"] >= cut_off_time
- ]
- elif query.startswith('topic:'):
- topic = query[len('topic:'):].strip()
- l = [queryMethod(change) for change in self.changes.values()
- if topic in change.data.get('topic', '')]
- else:
- # Query all open changes
- l = [queryMethod(change) for change in self.changes.values()]
+ l = [
+ change for change in l
+ if change.data["lastUpdated"] >= cut_off_time
+ ]
+ if part.startswith('topic:'):
+ topic = part[len('topic:'):].strip()
+ l = [
+ change for change in l
+ if 'topic' in change.data
+ and topic in change.data['topic']
+ ]
+ l = [queryMethod(change) for change in l]
return l
def simpleQuerySSH(self, query, event=None):
diff --git a/tests/fakegithub.py b/tests/fakegithub.py
index 725c083e2..25dcb15da 100644
--- a/tests/fakegithub.py
+++ b/tests/fakegithub.py
@@ -730,7 +730,7 @@ class FakeGithubSession(object):
'message': 'Merge not allowed because of fake reason',
}
return FakeResponse(data, 405, 'Method not allowed')
- pr.setMerged(json["commit_message"])
+ pr.setMerged(json.get("commit_message", ""))
return FakeResponse({"merged": True}, 200)
return FakeResponse(None, 404)
diff --git a/tests/fixtures/layouts/gerrit-trigger-requirements.yaml b/tests/fixtures/layouts/gerrit-trigger-requirements.yaml
new file mode 100644
index 000000000..72ad6b41e
--- /dev/null
+++ b/tests/fixtures/layouts/gerrit-trigger-requirements.yaml
@@ -0,0 +1,162 @@
+- pipeline:
+ name: require-open
+ manager: independent
+ trigger:
+ gerrit:
+ - event: comment-added
+ comment: test require-open
+ require:
+ open: true
+ success:
+ gerrit:
+ Verified: 1
+
+- pipeline:
+ name: reject-open
+ manager: independent
+ trigger:
+ gerrit:
+ - event: comment-added
+ comment: test reject-open
+ reject:
+ open: true
+ success:
+ gerrit:
+ Verified: 1
+
+- pipeline:
+ name: require-wip
+ manager: independent
+ trigger:
+ gerrit:
+ - event: comment-added
+ comment: test require-wip
+ require:
+ wip: true
+ success:
+ gerrit:
+ Verified: 1
+
+- pipeline:
+ name: reject-wip
+ manager: independent
+ trigger:
+ gerrit:
+ - event: comment-added
+ comment: test reject-wip
+ reject:
+ wip: true
+ success:
+ gerrit:
+ Verified: 1
+
+- pipeline:
+ name: require-current-patchset
+ manager: independent
+ trigger:
+ gerrit:
+ - event: comment-added
+ comment: test require-current-patchset
+ require:
+ current-patchset: true
+ success:
+ gerrit:
+ Verified: 1
+
+- pipeline:
+ name: reject-current-patchset
+ manager: independent
+ trigger:
+ gerrit:
+ - event: comment-added
+ comment: test reject-current-patchset
+ reject:
+ current-patchset: true
+ success:
+ gerrit:
+ Verified: 1
+
+- pipeline:
+ name: require-status
+ manager: independent
+ trigger:
+ gerrit:
+ - event: comment-added
+ comment: test require-status
+ require:
+ status: MERGED
+ success:
+ gerrit:
+ Verified: 1
+
+- pipeline:
+ name: reject-status
+ manager: independent
+ trigger:
+ gerrit:
+ - event: comment-added
+ comment: test reject-status
+ reject:
+ status: MERGED
+ success:
+ gerrit:
+ Verified: 1
+
+- pipeline:
+ name: require-approval
+ manager: independent
+ trigger:
+ gerrit:
+ - event: comment-added
+ comment: test require-approval
+ require:
+ approval:
+ username: zuul
+ Verified: 1
+ success:
+ gerrit:
+ Verified: 1
+
+- pipeline:
+ name: reject-approval
+ manager: independent
+ trigger:
+ gerrit:
+ - event: comment-added
+ comment: test reject-approval
+ reject:
+ approval:
+ username: zuul
+ Verified: 1
+ success:
+ gerrit:
+ Verified: 1
+
+- job:
+ name: base
+ parent: null
+ run: playbooks/base.yaml
+
+- job: {name: require-open}
+- job: {name: reject-open}
+- job: {name: require-wip}
+- job: {name: reject-wip}
+- job: {name: require-current-patchset}
+- job: {name: reject-current-patchset}
+- job: {name: require-status}
+- job: {name: reject-status}
+- job: {name: require-approval}
+- job: {name: reject-approval}
+
+- project:
+ name: org/project
+ require-open: {jobs: [require-open]}
+ reject-open: {jobs: [reject-open]}
+ require-wip: {jobs: [require-wip]}
+ reject-wip: {jobs: [reject-wip]}
+ require-current-patchset: {jobs: [require-current-patchset]}
+ reject-current-patchset: {jobs: [reject-current-patchset]}
+ require-status: {jobs: [require-status]}
+ reject-status: {jobs: [reject-status]}
+ require-approval: {jobs: [require-approval]}
+ reject-approval: {jobs: [reject-approval]}
diff --git a/tests/fixtures/layouts/github-trigger-requirements.yaml b/tests/fixtures/layouts/github-trigger-requirements.yaml
new file mode 100644
index 000000000..5014df3bb
--- /dev/null
+++ b/tests/fixtures/layouts/github-trigger-requirements.yaml
@@ -0,0 +1,112 @@
+- pipeline:
+ name: require-status
+ manager: independent
+ trigger:
+ github:
+ - event: pull_request
+ action: comment
+ comment: test require-status
+ require:
+ status:
+ - zuul:tenant-one/check:success
+ success:
+ github:
+ comment: true
+
+- pipeline:
+ name: reject-status
+ manager: independent
+ trigger:
+ github:
+ - event: pull_request
+ action: comment
+ comment: test reject-status
+ reject:
+ status:
+ - zuul:tenant-one/check:failure
+ success:
+ github:
+ comment: true
+
+- pipeline:
+ name: require-review
+ manager: independent
+ trigger:
+ github:
+ - event: pull_request
+ action: comment
+ comment: test require-review
+ require:
+ review:
+ - type: approved
+ permission: write
+ success:
+ github:
+ comment: true
+
+- pipeline:
+ name: reject-review
+ manager: independent
+ trigger:
+ github:
+ - event: pull_request
+ action: comment
+ comment: test reject-review
+ reject:
+ review:
+ - type: changes_requested
+ permission: write
+ success:
+ github:
+ comment: true
+
+- pipeline:
+ name: require-label
+ manager: independent
+ trigger:
+ github:
+ - event: pull_request
+ action: comment
+ comment: test require-label
+ require:
+ label:
+ - approved
+ success:
+ github:
+ comment: true
+
+- pipeline:
+ name: reject-label
+ manager: independent
+ trigger:
+ github:
+ - event: pull_request
+ action: comment
+ comment: test reject-label
+ reject:
+ label:
+ - rejected
+ success:
+ github:
+ comment: true
+
+- job:
+ name: base
+ parent: null
+ run: playbooks/base.yaml
+
+- job: {name: require-status}
+- job: {name: reject-status}
+- job: {name: require-review}
+- job: {name: reject-review}
+- job: {name: require-label}
+- job: {name: reject-label}
+
+- project:
+ name: org/project
+ require-status: {jobs: [require-status]}
+ reject-status: {jobs: [reject-status]}
+ require-review: {jobs: [require-review]}
+ reject-review: {jobs: [reject-review]}
+ require-label: {jobs: [require-label]}
+ reject-label: {jobs: [reject-label]}
diff --git a/tests/unit/test_circular_dependencies.py b/tests/unit/test_circular_dependencies.py
index a3f9dda33..28ca528b5 100644
--- a/tests/unit/test_circular_dependencies.py
+++ b/tests/unit/test_circular_dependencies.py
@@ -2246,6 +2246,70 @@ class TestGerritCircularDependencies(ZuulTestCase):
self.assertEqual(B.data["status"], "MERGED")
@simple_layout('layouts/deps-by-topic.yaml')
+ def test_deps_by_topic_git_needs(self):
+ A = self.fake_gerrit.addFakeChange('org/project1', "master", "A",
+ topic='test-topic')
+ B = self.fake_gerrit.addFakeChange('org/project2', "master", "B",
+ topic='test-topic')
+ C = self.fake_gerrit.addFakeChange('org/project2', "master", "C",
+ topic='other-topic')
+ D = self.fake_gerrit.addFakeChange('org/project1', "master", "D",
+ topic='other-topic')
+
+ # Git level dependency between B and C
+ B.setDependsOn(C, 1)
+
+ self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+ self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1))
+ self.fake_gerrit.addEvent(C.getPatchsetCreatedEvent(1))
+ self.fake_gerrit.addEvent(D.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+
+ self.assertEqual(len(A.patchsets[-1]["approvals"]), 1)
+ self.assertEqual(A.patchsets[-1]["approvals"][0]["type"], "Verified")
+ self.assertEqual(A.patchsets[-1]["approvals"][0]["value"], "1")
+
+ self.assertEqual(len(B.patchsets[-1]["approvals"]), 1)
+ self.assertEqual(B.patchsets[-1]["approvals"][0]["type"], "Verified")
+ self.assertEqual(B.patchsets[-1]["approvals"][0]["value"], "1")
+
+ self.assertEqual(len(C.patchsets[-1]["approvals"]), 1)
+ self.assertEqual(C.patchsets[-1]["approvals"][0]["type"], "Verified")
+ self.assertEqual(C.patchsets[-1]["approvals"][0]["value"], "1")
+
+ self.assertEqual(len(D.patchsets[-1]["approvals"]), 1)
+ self.assertEqual(D.patchsets[-1]["approvals"][0]["type"], "Verified")
+ self.assertEqual(D.patchsets[-1]["approvals"][0]["value"], "1")
+
+ # We're about to add approvals to changes without adding the
+ # triggering events to Zuul, so that we can be sure that it is
+ # enqueing the changes based on dependencies, not because of
+ # triggering events. Since it will have the changes cached
+ # already (without approvals), we need to clear the cache
+ # first.
+ for connection in self.scheds.first.connections.connections.values():
+ connection.maintainCache([], max_age=0)
+
+ A.addApproval("Code-Review", 2)
+ B.addApproval("Code-Review", 2)
+ C.addApproval("Code-Review", 2)
+ D.addApproval("Code-Review", 2)
+ A.addApproval("Approved", 1)
+ C.addApproval("Approved", 1)
+ D.addApproval("Approved", 1)
+ self.fake_gerrit.addEvent(B.addApproval("Approved", 1))
+ self.waitUntilSettled()
+
+ self.assertEqual(A.reported, 3)
+ self.assertEqual(B.reported, 3)
+ self.assertEqual(C.reported, 3)
+ self.assertEqual(D.reported, 3)
+ self.assertEqual(A.data["status"], "MERGED")
+ self.assertEqual(B.data["status"], "MERGED")
+ self.assertEqual(C.data["status"], "MERGED")
+ self.assertEqual(D.data["status"], "MERGED")
+
+ @simple_layout('layouts/deps-by-topic.yaml')
def test_deps_by_topic_new_patchset(self):
# Make sure that we correctly update the change cache on new
# patchsets.
diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py
index f241147eb..2e90d3fb4 100644
--- a/tests/unit/test_client.py
+++ b/tests/unit/test_client.py
@@ -21,10 +21,12 @@ import time
import configparser
import datetime
import dateutil.tz
+import uuid
import fixtures
import jwt
import testtools
+import sqlalchemy
from zuul.zk import ZooKeeperClient
from zuul.zk.locks import SessionAwareLock
@@ -499,27 +501,107 @@ class TestDBPruneParse(BaseTestCase):
class DBPruneTestCase(ZuulTestCase):
tenant_config_file = 'config/single-tenant/main.yaml'
+ # This should be larger than the limit size in sqlconnection
+ num_buildsets = 55
+
+ def _createBuildset(self, update_time):
+ connection = self.scheds.first.sched.sql.connection
+ buildset_uuid = uuid.uuid4().hex
+ event_id = uuid.uuid4().hex
+ with connection.getSession() as db:
+ start_time = update_time - datetime.timedelta(seconds=1)
+ end_time = update_time
+ db_buildset = db.createBuildSet(
+ uuid=buildset_uuid,
+ tenant='tenant-one',
+ pipeline='check',
+ project='org/project',
+ change='1',
+ patchset='1',
+ ref='refs/changes/1',
+ oldrev='',
+ newrev='',
+ branch='master',
+ zuul_ref='Zref',
+ ref_url='http://gerrit.example.com/1',
+ event_id=event_id,
+ event_timestamp=update_time,
+ updated=update_time,
+ first_build_start_time=start_time,
+ last_build_end_time=end_time,
+ result='SUCCESS',
+ )
+ for build_num in range(2):
+ build_uuid = uuid.uuid4().hex
+ db_build = db_buildset.createBuild(
+ uuid=build_uuid,
+ job_name=f'job{build_num}',
+ start_time=start_time,
+ end_time=end_time,
+ result='SUCCESS',
+ voting=True,
+ )
+ for art_num in range(2):
+ db_build.createArtifact(
+ name=f'artifact{art_num}',
+ url='http://example.com',
+ )
+ for provides_num in range(2):
+ db_build.createProvides(
+ name=f'item{provides_num}',
+ )
+ for event_num in range(2):
+ db_build.createBuildEvent(
+ event_type=f'event{event_num}',
+ event_time=start_time,
+ )
+
+ def _query(self, db, model):
+ table = model.__table__
+ q = db.session().query(model).order_by(table.c.id.desc())
+ try:
+ return q.all()
+ except sqlalchemy.orm.exc.NoResultFound:
+ return []
+
+ def _getBuildsets(self, db):
+ return self._query(db, db.connection.buildSetModel)
+
+ def _getBuilds(self, db):
+ return self._query(db, db.connection.buildModel)
+
+ def _getProvides(self, db):
+ return self._query(db, db.connection.providesModel)
+
+ def _getArtifacts(self, db):
+ return self._query(db, db.connection.artifactModel)
+
+ def _getBuildEvents(self, db):
+ return self._query(db, db.connection.buildEventModel)
def _setup(self):
config_file = os.path.join(self.test_root, 'zuul.conf')
with open(config_file, 'w') as f:
self.config.write(f)
- A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
- self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
- self.waitUntilSettled()
-
- time.sleep(1)
-
- B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
- self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1))
- self.waitUntilSettled()
+ update_time = (datetime.datetime.utcnow() -
+ datetime.timedelta(minutes=self.num_buildsets))
+ for x in range(self.num_buildsets):
+ update_time = update_time + datetime.timedelta(minutes=1)
+ self._createBuildset(update_time)
connection = self.scheds.first.sched.sql.connection
- buildsets = connection.getBuildsets()
- builds = connection.getBuilds()
- self.assertEqual(len(buildsets), 2)
- self.assertEqual(len(builds), 6)
+ with connection.getSession() as db:
+ buildsets = self._getBuildsets(db)
+ builds = self._getBuilds(db)
+ artifacts = self._getArtifacts(db)
+ provides = self._getProvides(db)
+ events = self._getBuildEvents(db)
+ self.assertEqual(len(buildsets), self.num_buildsets)
+ self.assertEqual(len(builds), 2 * self.num_buildsets)
+ self.assertEqual(len(artifacts), 4 * self.num_buildsets)
+ self.assertEqual(len(provides), 4 * self.num_buildsets)
+ self.assertEqual(len(events), 4 * self.num_buildsets)
for build in builds:
self.log.debug("Build %s %s %s",
build, build.start_time, build.end_time)
@@ -535,6 +617,7 @@ class DBPruneTestCase(ZuulTestCase):
start_time = buildsets[0].first_build_start_time
self.log.debug("Cutoff %s", start_time)
+ # Use the default batch size (omit --batch-size arg)
p = subprocess.Popen(
[os.path.join(sys.prefix, 'bin/zuul-admin'),
'-c', config_file,
@@ -545,13 +628,20 @@ class DBPruneTestCase(ZuulTestCase):
out, _ = p.communicate()
self.log.debug(out.decode('utf8'))
- buildsets = connection.getBuildsets()
- builds = connection.getBuilds()
- self.assertEqual(len(buildsets), 1)
- self.assertEqual(len(builds), 3)
+ with connection.getSession() as db:
+ buildsets = self._getBuildsets(db)
+ builds = self._getBuilds(db)
+ artifacts = self._getArtifacts(db)
+ provides = self._getProvides(db)
+ events = self._getBuildEvents(db)
for build in builds:
self.log.debug("Build %s %s %s",
build, build.start_time, build.end_time)
+ self.assertEqual(len(buildsets), 1)
+ self.assertEqual(len(builds), 2)
+ self.assertEqual(len(artifacts), 4)
+ self.assertEqual(len(provides), 4)
+ self.assertEqual(len(events), 4)
def test_db_prune_older_than(self):
# Test pruning buildsets older than a relative time
@@ -567,15 +657,23 @@ class DBPruneTestCase(ZuulTestCase):
'-c', config_file,
'prune-database',
'--older-than', '0d',
+ '--batch-size', '5',
],
stdout=subprocess.PIPE)
out, _ = p.communicate()
self.log.debug(out.decode('utf8'))
- buildsets = connection.getBuildsets()
- builds = connection.getBuilds()
+ with connection.getSession() as db:
+ buildsets = self._getBuildsets(db)
+ builds = self._getBuilds(db)
+ artifacts = self._getArtifacts(db)
+ provides = self._getProvides(db)
+ events = self._getBuildEvents(db)
self.assertEqual(len(buildsets), 0)
self.assertEqual(len(builds), 0)
+ self.assertEqual(len(artifacts), 0)
+ self.assertEqual(len(provides), 0)
+ self.assertEqual(len(events), 0)
class TestDBPruneMysql(DBPruneTestCase):
diff --git a/tests/unit/test_gerrit.py b/tests/unit/test_gerrit.py
index 2a63d5ef8..4085e8b1b 100644
--- a/tests/unit/test_gerrit.py
+++ b/tests/unit/test_gerrit.py
@@ -827,6 +827,14 @@ class TestGerritFake(ZuulTestCase):
config_file = "zuul-gerrit-github.conf"
tenant_config_file = "config/circular-dependencies/main.yaml"
+ def _make_tuple(self, data):
+ ret = []
+ for c in data:
+ dep_change = c['number']
+ dep_ps = c['currentPatchSet']['number']
+ ret.append((int(dep_change), int(dep_ps)))
+ return sorted(ret)
+
def _get_tuple(self, change_number):
ret = []
data = self.fake_gerrit.get(
@@ -903,6 +911,11 @@ class TestGerritFake(ZuulTestCase):
ret = self.fake_gerrit._getSubmittedTogether(C1, None)
self.assertEqual(ret, [(4, 1)])
+ # Test also the query used by the GerritConnection:
+ ret = self.fake_gerrit._simpleQuery('status:open topic:test-topic')
+ ret = self._make_tuple(ret)
+ self.assertEqual(ret, [(3, 1), (4, 1)])
+
class TestGerritConnection(ZuulTestCase):
config_file = 'zuul-gerrit-web.conf'
@@ -958,6 +971,92 @@ class TestGerritConnection(ZuulTestCase):
self.assertEqual(A.data['status'], 'MERGED')
self.assertEqual(B.data['status'], 'MERGED')
+ def test_submit_requirements(self):
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ A.addApproval('Code-Review', 2)
+ # Set an unsatisfied submit requirement
+ A.setSubmitRequirements([
+ {
+ "name": "Code-Review",
+ "description": "Disallow self-review",
+ "status": "UNSATISFIED",
+ "is_legacy": False,
+ "submittability_expression_result": {
+ "expression": "label:Code-Review=MAX,user=non_uploader "
+ "AND -label:Code-Review=MIN",
+ "fulfilled": False,
+ "passing_atoms": [],
+ "failing_atoms": [
+ "label:Code-Review=MAX,user=non_uploader",
+ "label:Code-Review=MIN"
+ ]
+ }
+ },
+ {
+ "name": "Verified",
+ "status": "UNSATISFIED",
+ "is_legacy": True,
+ "submittability_expression_result": {
+ "expression": "label:Verified=MAX -label:Verified=MIN",
+ "fulfilled": False,
+ "passing_atoms": [],
+ "failing_atoms": [
+ "label:Verified=MAX",
+ "-label:Verified=MIN"
+ ]
+ }
+ },
+ ])
+ self.fake_gerrit.addEvent(A.addApproval('Approved', 1))
+ self.waitUntilSettled()
+ self.assertHistory([])
+ self.assertEqual(A.queried, 1)
+ self.assertEqual(A.data['status'], 'NEW')
+
+ # Mark the requirement satisfied
+ A.setSubmitRequirements([
+ {
+ "name": "Code-Review",
+ "description": "Disallow self-review",
+ "status": "SATISFIED",
+ "is_legacy": False,
+ "submittability_expression_result": {
+ "expression": "label:Code-Review=MAX,user=non_uploader "
+ "AND -label:Code-Review=MIN",
+ "fulfilled": False,
+ "passing_atoms": [
+ "label:Code-Review=MAX,user=non_uploader",
+ ],
+ "failing_atoms": [
+ "label:Code-Review=MIN"
+ ]
+ }
+ },
+ {
+ "name": "Verified",
+ "status": "UNSATISFIED",
+ "is_legacy": True,
+ "submittability_expression_result": {
+ "expression": "label:Verified=MAX -label:Verified=MIN",
+ "fulfilled": False,
+ "passing_atoms": [],
+ "failing_atoms": [
+ "label:Verified=MAX",
+ "-label:Verified=MIN"
+ ]
+ }
+ },
+ ])
+ self.fake_gerrit.addEvent(A.addApproval('Approved', 1))
+ self.waitUntilSettled()
+ self.assertHistory([
+ dict(name="project-merge", result="SUCCESS", changes="1,1"),
+ dict(name="project-test1", result="SUCCESS", changes="1,1"),
+ dict(name="project-test2", result="SUCCESS", changes="1,1"),
+ ], ordered=False)
+ self.assertEqual(A.queried, 3)
+ self.assertEqual(A.data['status'], 'MERGED')
+
class TestGerritUnicodeRefs(ZuulTestCase):
config_file = 'zuul-gerrit-web.conf'
diff --git a/tests/unit/test_github_driver.py b/tests/unit/test_github_driver.py
index 47e84ca7f..3060f5673 100644
--- a/tests/unit/test_github_driver.py
+++ b/tests/unit/test_github_driver.py
@@ -1430,7 +1430,9 @@ class TestGithubDriver(ZuulTestCase):
repo._set_branch_protection(
'master', contexts=['tenant-one/check', 'tenant-one/gate'])
- A = self.fake_github.openFakePullRequest('org/project', 'master', 'A')
+ pr_description = "PR description"
+ A = self.fake_github.openFakePullRequest('org/project', 'master', 'A',
+ body_text=pr_description)
self.fake_github.emitEvent(A.getPullRequestOpenedEvent())
self.waitUntilSettled()
@@ -1448,6 +1450,9 @@ class TestGithubDriver(ZuulTestCase):
merges = [report for report in self.fake_github.github_data.reports
if report[2] == 'merge']
assert (len(merges) == 1 and merges[0][3] == 'squash')
+ # Assert that we won't duplicate the PR title in the merge
+ # message description.
+ self.assertEqual(A.merge_message, pr_description)
@simple_layout('layouts/basic-github.yaml', driver='github')
def test_invalid_event(self):
diff --git a/tests/unit/test_github_requirements.py b/tests/unit/test_github_requirements.py
index ef1f75944..f3021d41d 100644
--- a/tests/unit/test_github_requirements.py
+++ b/tests/unit/test_github_requirements.py
@@ -678,3 +678,181 @@ class TestGithubAppRequirements(ZuulGithubAppTestCase):
self.fake_github.emitEvent(comment)
self.waitUntilSettled()
self.assertEqual(len(self.history), 1)
+
+
+class TestGithubTriggerRequirements(ZuulTestCase):
+ """Test pipeline and trigger requirements"""
+ config_file = 'zuul-github-driver.conf'
+ scheduler_count = 1
+
+ @simple_layout('layouts/github-trigger-requirements.yaml', driver='github')
+ def test_require_status(self):
+ # Test trigger require-status
+ jobname = 'require-status'
+ project = 'org/project'
+ A = self.fake_github.openFakePullRequest(project, 'master', 'A')
+ # A comment event that we will keep submitting to trigger
+ comment = A.getCommentAddedEvent(f'test {jobname}')
+
+ # No status from zuul so should not be enqueued
+ self.fake_github.emitEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 0)
+
+ # An error status should not cause it to be enqueued
+ self.fake_github.setCommitStatus(project, A.head_sha, 'error',
+ context='tenant-one/check')
+ self.fake_github.emitEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 0)
+
+ # A success status goes in
+ self.fake_github.setCommitStatus(project, A.head_sha, 'success',
+ context='tenant-one/check')
+ self.fake_github.emitEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+ self.assertEqual(self.history[0].name, jobname)
+
+ @simple_layout('layouts/github-trigger-requirements.yaml', driver='github')
+ def test_reject_status(self):
+ # Test trigger reject-status
+ jobname = 'reject-status'
+ project = 'org/project'
+ A = self.fake_github.openFakePullRequest(project, 'master', 'A')
+ # A comment event that we will keep submitting to trigger
+ comment = A.getCommentAddedEvent(f'test {jobname}')
+
+ # No status from zuul so should be enqueued
+ self.fake_github.emitEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+ self.assertEqual(self.history[0].name, jobname)
+
+ # A failure status should not cause it to be enqueued
+ self.fake_github.setCommitStatus(project, A.head_sha, 'failure',
+ context='tenant-one/check')
+ self.fake_github.emitEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+
+ # A success status goes in
+ self.fake_github.setCommitStatus(project, A.head_sha, 'success',
+ context='tenant-one/check')
+ self.fake_github.emitEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 2)
+ self.assertEqual(self.history[1].name, jobname)
+
+ @simple_layout('layouts/github-trigger-requirements.yaml', driver='github')
+ def test_require_review(self):
+ # Test trigger require-review
+ jobname = 'require-review'
+ project = 'org/project'
+ A = self.fake_github.openFakePullRequest(project, 'master', 'A')
+ A.writers.extend(('maintainer',))
+ # A comment event that we will keep submitting to trigger
+ comment = A.getCommentAddedEvent(f'test {jobname}')
+
+ # No review so should not be enqueued
+ self.fake_github.emitEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 0)
+
+ # An changes requested review should not cause it to be enqueued
+ A.addReview('maintainer', 'CHANGES_REQUESTED')
+ self.fake_github.emitEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 0)
+
+ # A positive review goes in
+ A.addReview('maintainer', 'APPROVED')
+ self.fake_github.emitEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+ self.assertEqual(self.history[0].name, jobname)
+
+ @simple_layout('layouts/github-trigger-requirements.yaml', driver='github')
+ def test_reject_review(self):
+ # Test trigger reject-review
+ jobname = 'reject-review'
+ project = 'org/project'
+ A = self.fake_github.openFakePullRequest(project, 'master', 'A')
+ A.writers.extend(('maintainer',))
+ # A comment event that we will keep submitting to trigger
+ comment = A.getCommentAddedEvent(f'test {jobname}')
+
+ # No review so should be enqueued
+ self.fake_github.emitEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+ self.assertEqual(self.history[0].name, jobname)
+
+ # An changes requested review should not cause it to be enqueued
+ A.addReview('maintainer', 'CHANGES_REQUESTED')
+ self.fake_github.emitEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+
+ # A positive review goes in
+ A.addReview('maintainer', 'APPROVED')
+ self.fake_github.emitEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 2)
+ self.assertEqual(self.history[1].name, jobname)
+
+ @simple_layout('layouts/github-trigger-requirements.yaml', driver='github')
+ def test_require_label(self):
+ # Test trigger require-label
+ jobname = 'require-label'
+ project = 'org/project'
+ A = self.fake_github.openFakePullRequest(project, 'master', 'A')
+ # A comment event that we will keep submitting to trigger
+ comment = A.getCommentAddedEvent(f'test {jobname}')
+
+ # No label so should not be enqueued
+ self.fake_github.emitEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 0)
+
+ # A random should not cause it to be enqueued
+ A.addLabel('foobar')
+ self.fake_github.emitEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 0)
+
+ # An approved label goes in
+ A.addLabel('approved')
+ self.fake_github.emitEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+ self.assertEqual(self.history[0].name, jobname)
+
+ @simple_layout('layouts/github-trigger-requirements.yaml', driver='github')
+ def test_reject_label(self):
+ # Test trigger reject-label
+ jobname = 'reject-label'
+ project = 'org/project'
+ A = self.fake_github.openFakePullRequest(project, 'master', 'A')
+ # A comment event that we will keep submitting to trigger
+ comment = A.getCommentAddedEvent(f'test {jobname}')
+
+ # No label so should be enqueued
+ self.fake_github.emitEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+ self.assertEqual(self.history[0].name, jobname)
+
+ # A rejected label should not cause it to be enqueued
+ A.addLabel('rejected')
+ self.fake_github.emitEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+
+ # Any other label, it goes in
+ A.removeLabel('rejected')
+ A.addLabel('okay')
+ self.fake_github.emitEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 2)
+ self.assertEqual(self.history[1].name, jobname)
diff --git a/tests/unit/test_requirements.py b/tests/unit/test_requirements.py
index 9f3b87187..c5dca56cd 100644
--- a/tests/unit/test_requirements.py
+++ b/tests/unit/test_requirements.py
@@ -14,7 +14,7 @@
import time
-from tests.base import ZuulTestCase
+from tests.base import ZuulTestCase, simple_layout
class TestRequirementsApprovalNewerThan(ZuulTestCase):
@@ -490,3 +490,222 @@ class TestRequirementsTrustedCheck(ZuulTestCase):
self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1))
self.waitUntilSettled()
self.assertHistory([])
+
+
+class TestGerritTriggerRequirements(ZuulTestCase):
+ scheduler_count = 1
+
+ @simple_layout('layouts/gerrit-trigger-requirements.yaml')
+ def test_require_open(self):
+ # Test trigger require-open
+ jobname = 'require-open'
+ project = 'org/project'
+ A = self.fake_gerrit.addFakeChange(project, 'master', 'A')
+ # A comment event that we will keep submitting to trigger
+ comment = A.getChangeCommentEvent(1, f'test {jobname}')
+
+ # It's open, so it should be enqueued
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+ self.assertEqual(self.history[0].name, jobname)
+
+ # Not open, so should be ignored
+ A.setMerged()
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+ self.assertEqual(self.history[0].name, jobname)
+
+ @simple_layout('layouts/gerrit-trigger-requirements.yaml')
+ def test_reject_open(self):
+ # Test trigger reject-open
+ jobname = 'reject-open'
+ project = 'org/project'
+ A = self.fake_gerrit.addFakeChange(project, 'master', 'A')
+ # A comment event that we will keep submitting to trigger
+ comment = A.getChangeCommentEvent(1, f'test {jobname}')
+
+ # It's open, so it should not be enqueued
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 0)
+
+ # Not open, so should be enqueued
+ A.setMerged()
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+ self.assertEqual(self.history[0].name, jobname)
+
+ @simple_layout('layouts/gerrit-trigger-requirements.yaml')
+ def test_require_wip(self):
+ # Test trigger require-wip
+ jobname = 'require-wip'
+ project = 'org/project'
+ A = self.fake_gerrit.addFakeChange(project, 'master', 'A')
+ # A comment event that we will keep submitting to trigger
+ comment = A.getChangeCommentEvent(1, f'test {jobname}')
+
+ # It's not WIP, so it should be ignored
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 0)
+
+ # WIP, so should be enqueued
+ A.setWorkInProgress(True)
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+ self.assertEqual(self.history[0].name, jobname)
+
+ @simple_layout('layouts/gerrit-trigger-requirements.yaml')
+ def test_reject_wip(self):
+ # Test trigger reject-wip
+ jobname = 'reject-wip'
+ project = 'org/project'
+ A = self.fake_gerrit.addFakeChange(project, 'master', 'A')
+ # A comment event that we will keep submitting to trigger
+ comment = A.getChangeCommentEvent(1, f'test {jobname}')
+
+ # It's not WIP, so it should be enqueued
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+ self.assertEqual(self.history[0].name, jobname)
+
+ # WIP, so should be ignored
+ A.setWorkInProgress(True)
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+ self.assertEqual(self.history[0].name, jobname)
+
+ @simple_layout('layouts/gerrit-trigger-requirements.yaml')
+ def test_require_current_patchset(self):
+ # Test trigger require-current_patchset
+ jobname = 'require-current-patchset'
+ project = 'org/project'
+ A = self.fake_gerrit.addFakeChange(project, 'master', 'A')
+ # A comment event that we will keep submitting to trigger
+ comment = A.getChangeCommentEvent(1, f'test {jobname}')
+
+ # It's current, so it should be enqueued
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+ self.assertEqual(self.history[0].name, jobname)
+
+ # Not current, so should be ignored
+ A.addPatchset()
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+ self.assertEqual(self.history[0].name, jobname)
+
+ @simple_layout('layouts/gerrit-trigger-requirements.yaml')
+ def test_reject_current_patchset(self):
+ # Test trigger reject-current_patchset
+ jobname = 'reject-current-patchset'
+ project = 'org/project'
+ A = self.fake_gerrit.addFakeChange(project, 'master', 'A')
+ # A comment event that we will keep submitting to trigger
+ comment = A.getChangeCommentEvent(1, f'test {jobname}')
+
+ # It's current, so it should be ignored
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 0)
+
+ # Not current, so should be enqueued
+ A.addPatchset()
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+ self.assertEqual(self.history[0].name, jobname)
+
+ @simple_layout('layouts/gerrit-trigger-requirements.yaml')
+ def test_require_status(self):
+ # Test trigger require-status
+ jobname = 'require-status'
+ project = 'org/project'
+ A = self.fake_gerrit.addFakeChange(project, 'master', 'A')
+ # A comment event that we will keep submitting to trigger
+ comment = A.getChangeCommentEvent(1, f'test {jobname}')
+
+ # It's not merged, so it should be ignored
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 0)
+
+ # Merged, so should be enqueued
+ A.setMerged()
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+ self.assertEqual(self.history[0].name, jobname)
+
+ @simple_layout('layouts/gerrit-trigger-requirements.yaml')
+ def test_reject_status(self):
+ # Test trigger reject-status
+ jobname = 'reject-status'
+ project = 'org/project'
+ A = self.fake_gerrit.addFakeChange(project, 'master', 'A')
+ # A comment event that we will keep submitting to trigger
+ comment = A.getChangeCommentEvent(1, f'test {jobname}')
+
+ # It's not merged, so it should be enqueued
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+ self.assertEqual(self.history[0].name, jobname)
+
+ # Merged, so should be ignored
+ A.setMerged()
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+ self.assertEqual(self.history[0].name, jobname)
+
+ @simple_layout('layouts/gerrit-trigger-requirements.yaml')
+ def test_require_approval(self):
+ # Test trigger require-approval
+ jobname = 'require-approval'
+ project = 'org/project'
+ A = self.fake_gerrit.addFakeChange(project, 'master', 'A')
+ # A comment event that we will keep submitting to trigger
+ comment = A.getChangeCommentEvent(1, f'test {jobname}')
+
+ # Missing approval, so it should be ignored
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 0)
+
+ # Has approval, so it should be enqueued
+ A.addApproval('Verified', 1, username='zuul')
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+ self.assertEqual(self.history[0].name, jobname)
+
+ @simple_layout('layouts/gerrit-trigger-requirements.yaml')
+ def test_reject_approval(self):
+ # Test trigger reject-approval
+ jobname = 'reject-approval'
+ project = 'org/project'
+ A = self.fake_gerrit.addFakeChange(project, 'master', 'A')
+ # A comment event that we will keep submitting to trigger
+ comment = A.getChangeCommentEvent(1, f'test {jobname}')
+
+ # Missing approval, so it should be enqueued
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+ self.assertEqual(self.history[0].name, jobname)
+
+ # Has approval, so it should be ignored
+ A.addApproval('Verified', 1, username='zuul')
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+ self.assertEqual(self.history[0].name, jobname)
diff --git a/tests/unit/test_scheduler.py b/tests/unit/test_scheduler.py
index 172ed34dc..131034f17 100644
--- a/tests/unit/test_scheduler.py
+++ b/tests/unit/test_scheduler.py
@@ -7440,6 +7440,85 @@ class TestSchedulerMerges(ZuulTestCase):
result = self._test_project_merge_mode('cherry-pick')
self.assertEqual(result, expected_messages)
+ def test_project_merge_mode_cherrypick_redundant(self):
+ # A redundant commit (that is, one that has already been applied to the
+ # working tree) should be skipped
+ self.executor_server.keep_jobdir = False
+ project = 'org/project-cherry-pick'
+ files = {
+ "foo.txt": "ABC",
+ }
+ A = self.fake_gerrit.addFakeChange(project, 'master', 'A', files=files)
+ A.addApproval('Code-Review', 2)
+ self.fake_gerrit.addEvent(A.addApproval('Approved', 1))
+ self.waitUntilSettled()
+
+ self.executor_server.hold_jobs_in_build = True
+ B = self.fake_gerrit.addFakeChange(project, 'master', 'B', files=files)
+ B.addApproval('Code-Review', 2)
+ self.fake_gerrit.addEvent(B.addApproval('Approved', 1))
+ self.waitUntilSettled()
+
+ build = self.builds[-1]
+ path = os.path.join(build.jobdir.src_root, 'review.example.com',
+ project)
+ repo = git.Repo(path)
+ repo_messages = [c.message.strip() for c in repo.iter_commits()]
+ repo_messages.reverse()
+
+ self.executor_server.hold_jobs_in_build = False
+ self.executor_server.release()
+ self.waitUntilSettled()
+
+ expected_messages = [
+ 'initial commit',
+ 'add content from fixture',
+ 'A-1',
+ ]
+ self.assertHistory([
+ dict(name='project-test1', result='SUCCESS', changes='1,1'),
+ dict(name='project-test1', result='SUCCESS', changes='2,1'),
+ ])
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(B.data['status'], 'MERGED')
+ self.assertEqual(repo_messages, expected_messages)
+
+ def test_project_merge_mode_cherrypick_empty(self):
+ # An empty commit (that is, one that doesn't modify any files) should
+ # be preserved
+ self.executor_server.keep_jobdir = False
+ project = 'org/project-cherry-pick'
+ self.executor_server.hold_jobs_in_build = True
+ A = self.fake_gerrit.addFakeChange(project, 'master', 'A', empty=True)
+ A.addApproval('Code-Review', 2)
+ self.fake_gerrit.addEvent(A.addApproval('Approved', 1))
+ self.waitUntilSettled()
+
+ build = self.builds[-1]
+ path = os.path.join(build.jobdir.src_root, 'review.example.com',
+ project)
+ repo = git.Repo(path)
+ repo_messages = [c.message.strip() for c in repo.iter_commits()]
+ repo_messages.reverse()
+
+ changed_files = list(repo.commit("HEAD").diff(repo.commit("HEAD~1")))
+ self.assertEqual(changed_files, [])
+
+ self.executor_server.hold_jobs_in_build = False
+ self.executor_server.release()
+ self.waitUntilSettled()
+
+ expected_messages = [
+ 'initial commit',
+ 'add content from fixture',
+ 'A-1',
+ ]
+ self.assertHistory([
+ dict(name='project-test1', result='SUCCESS', changes='1,1'),
+ ])
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(repo_messages, expected_messages)
+
def test_project_merge_mode_cherrypick_branch_merge(self):
"Test that branches can be merged together in cherry-pick mode"
self.create_branch('org/project-merge-branches', 'mp')