Move tests into test/unit
This makes room for a sibling directory for nodepool functional tests.
Change-Id: Iace94d313edb04192ac23a533ed967f076410980
diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/unit/__init__.py
diff --git a/tests/unit/test_change_matcher.py b/tests/unit/test_change_matcher.py
new file mode 100644
index 0000000..0585322
--- /dev/null
+++ b/tests/unit/test_change_matcher.py
@@ -0,0 +1,154 @@
+# Copyright 2015 Red Hat, Inc.
+#
+# 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 zuul import change_matcher as cm
+from zuul import model
+
+from tests.base import BaseTestCase
+
+
+class BaseTestMatcher(BaseTestCase):
+
+ project = 'project'
+
+ def setUp(self):
+ super(BaseTestMatcher, self).setUp()
+ self.change = model.Change(self.project)
+
+
+class TestAbstractChangeMatcher(BaseTestMatcher):
+
+ def test_str(self):
+ matcher = cm.ProjectMatcher(self.project)
+ self.assertEqual(str(matcher), '{ProjectMatcher:project}')
+
+ def test_repr(self):
+ matcher = cm.ProjectMatcher(self.project)
+ self.assertEqual(repr(matcher), '<ProjectMatcher project>')
+
+
+class TestProjectMatcher(BaseTestMatcher):
+
+ def test_matches_returns_true(self):
+ matcher = cm.ProjectMatcher(self.project)
+ self.assertTrue(matcher.matches(self.change))
+
+ def test_matches_returns_false(self):
+ matcher = cm.ProjectMatcher('not_project')
+ self.assertFalse(matcher.matches(self.change))
+
+
+class TestBranchMatcher(BaseTestMatcher):
+
+ def setUp(self):
+ super(TestBranchMatcher, self).setUp()
+ self.matcher = cm.BranchMatcher('foo')
+
+ def test_matches_returns_true_on_matching_branch(self):
+ self.change.branch = 'foo'
+ self.assertTrue(self.matcher.matches(self.change))
+
+ def test_matches_returns_true_on_matching_ref(self):
+ self.change.branch = 'bar'
+ self.change.ref = 'foo'
+ self.assertTrue(self.matcher.matches(self.change))
+
+ def test_matches_returns_false_for_no_match(self):
+ self.change.branch = 'bar'
+ self.change.ref = 'baz'
+ self.assertFalse(self.matcher.matches(self.change))
+
+ def test_matches_returns_false_for_missing_attrs(self):
+ delattr(self.change, 'branch')
+ # ref is by default not an attribute
+ self.assertFalse(self.matcher.matches(self.change))
+
+
+class TestFileMatcher(BaseTestMatcher):
+
+ def setUp(self):
+ super(TestFileMatcher, self).setUp()
+ self.matcher = cm.FileMatcher('filename')
+
+ def test_matches_returns_true(self):
+ self.change.files = ['filename']
+ self.assertTrue(self.matcher.matches(self.change))
+
+ def test_matches_returns_false_when_no_files(self):
+ self.assertFalse(self.matcher.matches(self.change))
+
+ def test_matches_returns_false_when_files_attr_missing(self):
+ delattr(self.change, 'files')
+ self.assertFalse(self.matcher.matches(self.change))
+
+
+class TestAbstractMatcherCollection(BaseTestMatcher):
+
+ def test_str(self):
+ matcher = cm.MatchAll([cm.FileMatcher('foo')])
+ self.assertEqual(str(matcher), '{MatchAll:{FileMatcher:foo}}')
+
+ def test_repr(self):
+ matcher = cm.MatchAll([])
+ self.assertEqual(repr(matcher), '<MatchAll>')
+
+
+class TestMatchAllFiles(BaseTestMatcher):
+
+ def setUp(self):
+ super(TestMatchAllFiles, self).setUp()
+ self.matcher = cm.MatchAllFiles([cm.FileMatcher('^docs/.*$')])
+
+ def _test_matches(self, expected, files=None):
+ if files is not None:
+ self.change.files = files
+ self.assertEqual(expected, self.matcher.matches(self.change))
+
+ def test_matches_returns_false_when_files_attr_missing(self):
+ delattr(self.change, 'files')
+ self._test_matches(False)
+
+ def test_matches_returns_false_when_no_files(self):
+ self._test_matches(False)
+
+ def test_matches_returns_false_when_not_all_files_match(self):
+ self._test_matches(False, files=['/COMMIT_MSG', 'docs/foo', 'foo/bar'])
+
+ def test_matches_returns_false_when_commit_message_matches(self):
+ self._test_matches(False, files=['/COMMIT_MSG'])
+
+ def test_matches_returns_true_when_all_files_match(self):
+ self._test_matches(True, files=['/COMMIT_MSG', 'docs/foo'])
+
+
+class TestMatchAll(BaseTestMatcher):
+
+ def test_matches_returns_true(self):
+ matcher = cm.MatchAll([cm.ProjectMatcher(self.project)])
+ self.assertTrue(matcher.matches(self.change))
+
+ def test_matches_returns_false_for_missing_matcher(self):
+ matcher = cm.MatchAll([cm.ProjectMatcher('not_project')])
+ self.assertFalse(matcher.matches(self.change))
+
+
+class TestMatchAny(BaseTestMatcher):
+
+ def test_matches_returns_true(self):
+ matcher = cm.MatchAny([cm.ProjectMatcher(self.project)])
+ self.assertTrue(matcher.matches(self.change))
+
+ def test_matches_returns_false(self):
+ matcher = cm.MatchAny([cm.ProjectMatcher('not_project')])
+ self.assertFalse(matcher.matches(self.change))
diff --git a/tests/unit/test_clonemapper.py b/tests/unit/test_clonemapper.py
new file mode 100644
index 0000000..b7814f8
--- /dev/null
+++ b/tests/unit/test_clonemapper.py
@@ -0,0 +1,84 @@
+# Copyright 2014 Antoine "hashar" Musso
+# Copyright 2014 Wikimedia Foundation Inc.
+#
+# 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.
+
+import logging
+import testtools
+from zuul.lib.clonemapper import CloneMapper
+
+logging.basicConfig(level=logging.DEBUG,
+ format='%(asctime)s %(name)-17s '
+ '%(levelname)-8s %(message)s')
+
+
+class TestCloneMapper(testtools.TestCase):
+
+ def test_empty_mapper(self):
+ """Given an empty map, the slashes in project names are directory
+ separators"""
+ cmap = CloneMapper(
+ {},
+ [
+ 'project1',
+ 'plugins/plugin1'
+ ])
+
+ self.assertEqual(
+ {'project1': '/basepath/project1',
+ 'plugins/plugin1': '/basepath/plugins/plugin1'},
+ cmap.expand('/basepath')
+ )
+
+ def test_map_to_a_dot_dir(self):
+ """Verify we normalize path, hence '.' refers to the basepath"""
+ cmap = CloneMapper(
+ [{'name': 'mediawiki/core', 'dest': '.'}],
+ ['mediawiki/core'])
+ self.assertEqual(
+ {'mediawiki/core': '/basepath'},
+ cmap.expand('/basepath'))
+
+ def test_map_using_regex(self):
+ """One can use regex in maps and use \\1 to forge the directory"""
+ cmap = CloneMapper(
+ [{'name': 'plugins/(.*)', 'dest': 'project/plugins/\\1'}],
+ ['plugins/PluginFirst'])
+ self.assertEqual(
+ {'plugins/PluginFirst': '/basepath/project/plugins/PluginFirst'},
+ cmap.expand('/basepath'))
+
+ def test_map_discarding_regex_group(self):
+ cmap = CloneMapper(
+ [{'name': 'plugins/(.*)', 'dest': 'project/'}],
+ ['plugins/Plugin_1'])
+ self.assertEqual(
+ {'plugins/Plugin_1': '/basepath/project'},
+ cmap.expand('/basepath'))
+
+ def test_cant_dupe_destinations(self):
+ """We cant clone multiple projects in the same directory"""
+ cmap = CloneMapper(
+ [{'name': 'plugins/(.*)', 'dest': 'catchall/'}],
+ ['plugins/plugin1', 'plugins/plugin2']
+ )
+ self.assertRaises(Exception, cmap.expand, '/basepath')
+
+ def test_map_with_dot_and_regex(self):
+ """Combining relative path and regex"""
+ cmap = CloneMapper(
+ [{'name': 'plugins/(.*)', 'dest': './\\1'}],
+ ['plugins/PluginInBasePath'])
+ self.assertEqual(
+ {'plugins/PluginInBasePath': '/basepath/PluginInBasePath'},
+ cmap.expand('/basepath'))
diff --git a/tests/unit/test_cloner.py b/tests/unit/test_cloner.py
new file mode 100644
index 0000000..67b5303
--- /dev/null
+++ b/tests/unit/test_cloner.py
@@ -0,0 +1,624 @@
+#!/usr/bin/env python
+
+# Copyright 2012 Hewlett-Packard Development Company, L.P.
+# Copyright 2014 Wikimedia Foundation Inc.
+#
+# 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.
+
+import logging
+import os
+import shutil
+import time
+
+import git
+
+import zuul.lib.cloner
+
+from tests.base import ZuulTestCase
+
+logging.basicConfig(level=logging.DEBUG,
+ format='%(asctime)s %(name)-32s '
+ '%(levelname)-8s %(message)s')
+
+
+class TestCloner(ZuulTestCase):
+
+ log = logging.getLogger("zuul.test.cloner")
+ workspace_root = None
+
+ def setUp(self):
+ self.skip("Disabled for early v3 development")
+
+ super(TestCloner, self).setUp()
+ self.workspace_root = os.path.join(self.test_root, 'workspace')
+
+ self.updateConfigLayout(
+ 'tests/fixtures/layout-cloner.yaml')
+ self.sched.reconfigure(self.config)
+ self.registerJobs()
+
+ def getWorkspaceRepos(self, projects):
+ repos = {}
+ for project in projects:
+ repos[project] = git.Repo(
+ os.path.join(self.workspace_root, project))
+ return repos
+
+ def getUpstreamRepos(self, projects):
+ repos = {}
+ for project in projects:
+ repos[project] = git.Repo(
+ os.path.join(self.upstream_root, project))
+ return repos
+
+ def test_cache_dir(self):
+ projects = ['org/project1', 'org/project2']
+ cache_root = os.path.join(self.test_root, "cache")
+ for project in projects:
+ upstream_repo_path = os.path.join(self.upstream_root, project)
+ cache_repo_path = os.path.join(cache_root, project)
+ git.Repo.clone_from(upstream_repo_path, cache_repo_path)
+
+ self.worker.hold_jobs_in_build = True
+ A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A')
+ A.addApproval('CRVW', 2)
+ self.fake_gerrit.addEvent(A.addApproval('APRV', 1))
+
+ self.waitUntilSettled()
+
+ self.assertEquals(1, len(self.builds), "One build is running")
+
+ B = self.fake_gerrit.addFakeChange('org/project2', 'master', 'B')
+ B.setMerged()
+
+ upstream = self.getUpstreamRepos(projects)
+ states = [{
+ 'org/project1': self.builds[0].parameters['ZUUL_COMMIT'],
+ 'org/project2': str(upstream['org/project2'].commit('master')),
+ }]
+
+ for number, build in enumerate(self.builds):
+ self.log.debug("Build parameters: %s", build.parameters)
+ cloner = zuul.lib.cloner.Cloner(
+ git_base_url=self.upstream_root,
+ projects=projects,
+ workspace=self.workspace_root,
+ zuul_branch=build.parameters['ZUUL_BRANCH'],
+ zuul_ref=build.parameters['ZUUL_REF'],
+ zuul_url=self.git_root,
+ cache_dir=cache_root,
+ )
+ cloner.execute()
+ work = self.getWorkspaceRepos(projects)
+ state = states[number]
+
+ for project in projects:
+ self.assertEquals(state[project],
+ str(work[project].commit('HEAD')),
+ 'Project %s commit for build %s should '
+ 'be correct' % (project, number))
+
+ work = self.getWorkspaceRepos(projects)
+ upstream_repo_path = os.path.join(self.upstream_root, 'org/project1')
+ self.assertEquals(
+ work['org/project1'].remotes.origin.url,
+ upstream_repo_path,
+ 'workspace repo origin should be upstream, not cache'
+ )
+
+ self.worker.hold_jobs_in_build = False
+ self.worker.release()
+ self.waitUntilSettled()
+
+ def test_one_branch(self):
+ self.worker.hold_jobs_in_build = True
+
+ projects = ['org/project1', 'org/project2']
+ A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project2', 'master', 'B')
+ A.addApproval('CRVW', 2)
+ B.addApproval('CRVW', 2)
+ self.fake_gerrit.addEvent(A.addApproval('APRV', 1))
+ self.fake_gerrit.addEvent(B.addApproval('APRV', 1))
+
+ self.waitUntilSettled()
+
+ self.assertEquals(2, len(self.builds), "Two builds are running")
+
+ upstream = self.getUpstreamRepos(projects)
+ states = [
+ {'org/project1': self.builds[0].parameters['ZUUL_COMMIT'],
+ 'org/project2': str(upstream['org/project2'].commit('master')),
+ },
+ {'org/project1': self.builds[0].parameters['ZUUL_COMMIT'],
+ 'org/project2': self.builds[1].parameters['ZUUL_COMMIT'],
+ },
+ ]
+
+ for number, build in enumerate(self.builds):
+ self.log.debug("Build parameters: %s", build.parameters)
+ cloner = zuul.lib.cloner.Cloner(
+ git_base_url=self.upstream_root,
+ projects=projects,
+ workspace=self.workspace_root,
+ zuul_branch=build.parameters['ZUUL_BRANCH'],
+ zuul_ref=build.parameters['ZUUL_REF'],
+ zuul_url=self.git_root,
+ )
+ cloner.execute()
+ work = self.getWorkspaceRepos(projects)
+ state = states[number]
+
+ for project in projects:
+ self.assertEquals(state[project],
+ str(work[project].commit('HEAD')),
+ 'Project %s commit for build %s should '
+ 'be correct' % (project, number))
+
+ shutil.rmtree(self.workspace_root)
+
+ self.worker.hold_jobs_in_build = False
+ self.worker.release()
+ self.waitUntilSettled()
+
+ def test_multi_branch(self):
+ self.worker.hold_jobs_in_build = True
+ projects = ['org/project1', 'org/project2',
+ 'org/project3', 'org/project4']
+
+ self.create_branch('org/project2', 'stable/havana')
+ self.create_branch('org/project4', 'stable/havana')
+ A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project2', 'stable/havana',
+ 'B')
+ C = self.fake_gerrit.addFakeChange('org/project3', 'master', 'C')
+ A.addApproval('CRVW', 2)
+ B.addApproval('CRVW', 2)
+ C.addApproval('CRVW', 2)
+ self.fake_gerrit.addEvent(A.addApproval('APRV', 1))
+ self.fake_gerrit.addEvent(B.addApproval('APRV', 1))
+ self.fake_gerrit.addEvent(C.addApproval('APRV', 1))
+
+ self.waitUntilSettled()
+
+ self.assertEquals(3, len(self.builds), "Three builds are running")
+
+ upstream = self.getUpstreamRepos(projects)
+ states = [
+ {'org/project1': self.builds[0].parameters['ZUUL_COMMIT'],
+ 'org/project2': str(upstream['org/project2'].commit('master')),
+ 'org/project3': str(upstream['org/project3'].commit('master')),
+ 'org/project4': str(upstream['org/project4'].
+ commit('master')),
+ },
+ {'org/project1': self.builds[0].parameters['ZUUL_COMMIT'],
+ 'org/project2': self.builds[1].parameters['ZUUL_COMMIT'],
+ 'org/project3': str(upstream['org/project3'].commit('master')),
+ 'org/project4': str(upstream['org/project4'].
+ commit('stable/havana')),
+ },
+ {'org/project1': self.builds[0].parameters['ZUUL_COMMIT'],
+ 'org/project2': str(upstream['org/project2'].commit('master')),
+ 'org/project3': self.builds[2].parameters['ZUUL_COMMIT'],
+ 'org/project4': str(upstream['org/project4'].
+ commit('master')),
+ },
+ ]
+
+ for number, build in enumerate(self.builds):
+ self.log.debug("Build parameters: %s", build.parameters)
+ cloner = zuul.lib.cloner.Cloner(
+ git_base_url=self.upstream_root,
+ projects=projects,
+ workspace=self.workspace_root,
+ zuul_branch=build.parameters['ZUUL_BRANCH'],
+ zuul_ref=build.parameters['ZUUL_REF'],
+ zuul_url=self.git_root,
+ )
+ cloner.execute()
+ work = self.getWorkspaceRepos(projects)
+ state = states[number]
+
+ for project in projects:
+ self.assertEquals(state[project],
+ str(work[project].commit('HEAD')),
+ 'Project %s commit for build %s should '
+ 'be correct' % (project, number))
+ shutil.rmtree(self.workspace_root)
+
+ self.worker.hold_jobs_in_build = False
+ self.worker.release()
+ self.waitUntilSettled()
+
+ def test_upgrade(self):
+ # Simulates an upgrade test
+ self.worker.hold_jobs_in_build = True
+ projects = ['org/project1', 'org/project2', 'org/project3',
+ 'org/project4', 'org/project5', 'org/project6']
+
+ self.create_branch('org/project2', 'stable/havana')
+ self.create_branch('org/project3', 'stable/havana')
+ self.create_branch('org/project4', 'stable/havana')
+ self.create_branch('org/project5', 'stable/havana')
+ A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project2', 'master', 'B')
+ C = self.fake_gerrit.addFakeChange('org/project3', 'stable/havana',
+ 'C')
+ D = self.fake_gerrit.addFakeChange('org/project3', 'master', 'D')
+ E = self.fake_gerrit.addFakeChange('org/project4', 'stable/havana',
+ 'E')
+ A.addApproval('CRVW', 2)
+ B.addApproval('CRVW', 2)
+ C.addApproval('CRVW', 2)
+ D.addApproval('CRVW', 2)
+ E.addApproval('CRVW', 2)
+ self.fake_gerrit.addEvent(A.addApproval('APRV', 1))
+ self.fake_gerrit.addEvent(B.addApproval('APRV', 1))
+ self.fake_gerrit.addEvent(C.addApproval('APRV', 1))
+ self.fake_gerrit.addEvent(D.addApproval('APRV', 1))
+ self.fake_gerrit.addEvent(E.addApproval('APRV', 1))
+
+ self.waitUntilSettled()
+
+ self.assertEquals(5, len(self.builds), "Five builds are running")
+
+ # Check the old side of the upgrade first
+ upstream = self.getUpstreamRepos(projects)
+ states = [
+ {'org/project1': self.builds[0].parameters['ZUUL_COMMIT'],
+ 'org/project2': str(upstream['org/project2'].commit(
+ 'stable/havana')),
+ 'org/project3': str(upstream['org/project3'].commit(
+ 'stable/havana')),
+ 'org/project4': str(upstream['org/project4'].commit(
+ 'stable/havana')),
+ 'org/project5': str(upstream['org/project5'].commit(
+ 'stable/havana')),
+ 'org/project6': str(upstream['org/project6'].commit('master')),
+ },
+ {'org/project1': self.builds[0].parameters['ZUUL_COMMIT'],
+ 'org/project2': str(upstream['org/project2'].commit(
+ 'stable/havana')),
+ 'org/project3': str(upstream['org/project3'].commit(
+ 'stable/havana')),
+ 'org/project4': str(upstream['org/project4'].commit(
+ 'stable/havana')),
+ 'org/project5': str(upstream['org/project5'].commit(
+ 'stable/havana')),
+ 'org/project6': str(upstream['org/project6'].commit('master')),
+ },
+ {'org/project1': self.builds[0].parameters['ZUUL_COMMIT'],
+ 'org/project2': str(upstream['org/project2'].commit(
+ 'stable/havana')),
+ 'org/project3': self.builds[2].parameters['ZUUL_COMMIT'],
+ 'org/project4': str(upstream['org/project4'].commit(
+ 'stable/havana')),
+
+ 'org/project5': str(upstream['org/project5'].commit(
+ 'stable/havana')),
+ 'org/project6': str(upstream['org/project6'].commit('master')),
+ },
+ {'org/project1': self.builds[0].parameters['ZUUL_COMMIT'],
+ 'org/project2': str(upstream['org/project2'].commit(
+ 'stable/havana')),
+ 'org/project3': self.builds[2].parameters['ZUUL_COMMIT'],
+ 'org/project4': str(upstream['org/project4'].commit(
+ 'stable/havana')),
+ 'org/project5': str(upstream['org/project5'].commit(
+ 'stable/havana')),
+ 'org/project6': str(upstream['org/project6'].commit('master')),
+ },
+ {'org/project1': self.builds[0].parameters['ZUUL_COMMIT'],
+ 'org/project2': str(upstream['org/project2'].commit(
+ 'stable/havana')),
+ 'org/project3': self.builds[2].parameters['ZUUL_COMMIT'],
+ 'org/project4': self.builds[4].parameters['ZUUL_COMMIT'],
+ 'org/project5': str(upstream['org/project5'].commit(
+ 'stable/havana')),
+ 'org/project6': str(upstream['org/project6'].commit('master')),
+ },
+ ]
+
+ for number, build in enumerate(self.builds):
+ self.log.debug("Build parameters: %s", build.parameters)
+ cloner = zuul.lib.cloner.Cloner(
+ git_base_url=self.upstream_root,
+ projects=projects,
+ workspace=self.workspace_root,
+ zuul_branch=build.parameters['ZUUL_BRANCH'],
+ zuul_ref=build.parameters['ZUUL_REF'],
+ zuul_url=self.git_root,
+ branch='stable/havana', # Old branch for upgrade
+ )
+ cloner.execute()
+ work = self.getWorkspaceRepos(projects)
+ state = states[number]
+
+ for project in projects:
+ self.assertEquals(state[project],
+ str(work[project].commit('HEAD')),
+ 'Project %s commit for build %s should '
+ 'be correct on old side of upgrade' %
+ (project, number))
+ shutil.rmtree(self.workspace_root)
+
+ # Check the new side of the upgrade
+ states = [
+ {'org/project1': self.builds[0].parameters['ZUUL_COMMIT'],
+ 'org/project2': str(upstream['org/project2'].commit('master')),
+ 'org/project3': str(upstream['org/project3'].commit('master')),
+ 'org/project4': str(upstream['org/project4'].commit('master')),
+ 'org/project5': str(upstream['org/project5'].commit('master')),
+ 'org/project6': str(upstream['org/project6'].commit('master')),
+ },
+ {'org/project1': self.builds[0].parameters['ZUUL_COMMIT'],
+ 'org/project2': self.builds[1].parameters['ZUUL_COMMIT'],
+ 'org/project3': str(upstream['org/project3'].commit('master')),
+ 'org/project4': str(upstream['org/project4'].commit('master')),
+ 'org/project5': str(upstream['org/project5'].commit('master')),
+ 'org/project6': str(upstream['org/project6'].commit('master')),
+ },
+ {'org/project1': self.builds[0].parameters['ZUUL_COMMIT'],
+ 'org/project2': self.builds[1].parameters['ZUUL_COMMIT'],
+ 'org/project3': str(upstream['org/project3'].commit('master')),
+ 'org/project4': str(upstream['org/project4'].commit('master')),
+ 'org/project5': str(upstream['org/project5'].commit('master')),
+ 'org/project6': str(upstream['org/project6'].commit('master')),
+ },
+ {'org/project1': self.builds[0].parameters['ZUUL_COMMIT'],
+ 'org/project2': self.builds[1].parameters['ZUUL_COMMIT'],
+ 'org/project3': self.builds[3].parameters['ZUUL_COMMIT'],
+ 'org/project4': str(upstream['org/project4'].commit('master')),
+ 'org/project5': str(upstream['org/project5'].commit('master')),
+ 'org/project6': str(upstream['org/project6'].commit('master')),
+ },
+ {'org/project1': self.builds[0].parameters['ZUUL_COMMIT'],
+ 'org/project2': self.builds[1].parameters['ZUUL_COMMIT'],
+ 'org/project3': self.builds[3].parameters['ZUUL_COMMIT'],
+ 'org/project4': str(upstream['org/project4'].commit('master')),
+ 'org/project5': str(upstream['org/project5'].commit('master')),
+ 'org/project6': str(upstream['org/project6'].commit('master')),
+ },
+ ]
+
+ for number, build in enumerate(self.builds):
+ self.log.debug("Build parameters: %s", build.parameters)
+ cloner = zuul.lib.cloner.Cloner(
+ git_base_url=self.upstream_root,
+ projects=projects,
+ workspace=self.workspace_root,
+ zuul_branch=build.parameters['ZUUL_BRANCH'],
+ zuul_ref=build.parameters['ZUUL_REF'],
+ zuul_url=self.git_root,
+ branch='master', # New branch for upgrade
+ )
+ cloner.execute()
+ work = self.getWorkspaceRepos(projects)
+ state = states[number]
+
+ for project in projects:
+ self.assertEquals(state[project],
+ str(work[project].commit('HEAD')),
+ 'Project %s commit for build %s should '
+ 'be correct on old side of upgrade' %
+ (project, number))
+ shutil.rmtree(self.workspace_root)
+
+ self.worker.hold_jobs_in_build = False
+ self.worker.release()
+ self.waitUntilSettled()
+
+ def test_project_override(self):
+ self.worker.hold_jobs_in_build = True
+ projects = ['org/project1', 'org/project2', 'org/project3',
+ 'org/project4', 'org/project5', 'org/project6']
+
+ self.create_branch('org/project3', 'stable/havana')
+ self.create_branch('org/project4', 'stable/havana')
+ self.create_branch('org/project6', 'stable/havana')
+ A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project1', 'master', 'B')
+ C = self.fake_gerrit.addFakeChange('org/project2', 'master', 'C')
+ D = self.fake_gerrit.addFakeChange('org/project3', 'stable/havana',
+ 'D')
+ A.addApproval('CRVW', 2)
+ B.addApproval('CRVW', 2)
+ C.addApproval('CRVW', 2)
+ D.addApproval('CRVW', 2)
+ self.fake_gerrit.addEvent(A.addApproval('APRV', 1))
+ self.fake_gerrit.addEvent(B.addApproval('APRV', 1))
+ self.fake_gerrit.addEvent(C.addApproval('APRV', 1))
+ self.fake_gerrit.addEvent(D.addApproval('APRV', 1))
+
+ self.waitUntilSettled()
+
+ self.assertEquals(4, len(self.builds), "Four builds are running")
+
+ upstream = self.getUpstreamRepos(projects)
+ states = [
+ {'org/project1': self.builds[0].parameters['ZUUL_COMMIT'],
+ 'org/project2': str(upstream['org/project2'].commit('master')),
+ 'org/project3': str(upstream['org/project3'].commit('master')),
+ 'org/project4': str(upstream['org/project4'].commit('master')),
+ 'org/project5': str(upstream['org/project5'].commit('master')),
+ 'org/project6': str(upstream['org/project6'].commit('master')),
+ },
+ {'org/project1': self.builds[1].parameters['ZUUL_COMMIT'],
+ 'org/project2': str(upstream['org/project2'].commit('master')),
+ 'org/project3': str(upstream['org/project3'].commit('master')),
+ 'org/project4': str(upstream['org/project4'].commit('master')),
+ 'org/project5': str(upstream['org/project5'].commit('master')),
+ 'org/project6': str(upstream['org/project6'].commit('master')),
+ },
+ {'org/project1': self.builds[1].parameters['ZUUL_COMMIT'],
+ 'org/project2': self.builds[2].parameters['ZUUL_COMMIT'],
+ 'org/project3': str(upstream['org/project3'].commit('master')),
+ 'org/project4': str(upstream['org/project4'].commit('master')),
+ 'org/project5': str(upstream['org/project5'].commit('master')),
+ 'org/project6': str(upstream['org/project6'].commit('master')),
+ },
+ {'org/project1': self.builds[1].parameters['ZUUL_COMMIT'],
+ 'org/project2': self.builds[2].parameters['ZUUL_COMMIT'],
+ 'org/project3': self.builds[3].parameters['ZUUL_COMMIT'],
+ 'org/project4': str(upstream['org/project4'].commit('master')),
+ 'org/project5': str(upstream['org/project5'].commit('master')),
+ 'org/project6': str(upstream['org/project6'].commit(
+ 'stable/havana')),
+ },
+ ]
+
+ for number, build in enumerate(self.builds):
+ self.log.debug("Build parameters: %s", build.parameters)
+ cloner = zuul.lib.cloner.Cloner(
+ git_base_url=self.upstream_root,
+ projects=projects,
+ workspace=self.workspace_root,
+ zuul_branch=build.parameters['ZUUL_BRANCH'],
+ zuul_ref=build.parameters['ZUUL_REF'],
+ zuul_url=self.git_root,
+ project_branches={'org/project4': 'master'},
+ )
+ cloner.execute()
+ work = self.getWorkspaceRepos(projects)
+ state = states[number]
+
+ for project in projects:
+ self.assertEquals(state[project],
+ str(work[project].commit('HEAD')),
+ 'Project %s commit for build %s should '
+ 'be correct' % (project, number))
+ shutil.rmtree(self.workspace_root)
+
+ self.worker.hold_jobs_in_build = False
+ self.worker.release()
+ self.waitUntilSettled()
+
+ def test_periodic(self):
+ self.worker.hold_jobs_in_build = True
+ self.create_branch('org/project', 'stable/havana')
+ self.updateConfigLayout(
+ 'tests/fixtures/layout-timer.yaml')
+ self.sched.reconfigure(self.config)
+ self.registerJobs()
+
+ # The pipeline triggers every second, so we should have seen
+ # several by now.
+ time.sleep(5)
+ self.waitUntilSettled()
+
+ builds = self.builds[:]
+
+ self.worker.hold_jobs_in_build = False
+ # Stop queuing timer triggered jobs so that the assertions
+ # below don't race against more jobs being queued.
+ self.updateConfigLayout(
+ 'tests/fixtures/layout-no-timer.yaml')
+ self.sched.reconfigure(self.config)
+ self.registerJobs()
+ self.worker.release()
+ self.waitUntilSettled()
+
+ projects = ['org/project']
+
+ self.assertEquals(2, len(builds), "Two builds are running")
+
+ upstream = self.getUpstreamRepos(projects)
+ states = [
+ {'org/project':
+ str(upstream['org/project'].commit('stable/havana')),
+ },
+ {'org/project':
+ str(upstream['org/project'].commit('stable/havana')),
+ },
+ ]
+
+ for number, build in enumerate(builds):
+ self.log.debug("Build parameters: %s", build.parameters)
+ cloner = zuul.lib.cloner.Cloner(
+ git_base_url=self.upstream_root,
+ projects=projects,
+ workspace=self.workspace_root,
+ zuul_branch=build.parameters.get('ZUUL_BRANCH', None),
+ zuul_ref=build.parameters.get('ZUUL_REF', None),
+ zuul_url=self.git_root,
+ branch='stable/havana',
+ )
+ cloner.execute()
+ work = self.getWorkspaceRepos(projects)
+ state = states[number]
+
+ for project in projects:
+ self.assertEquals(state[project],
+ str(work[project].commit('HEAD')),
+ 'Project %s commit for build %s should '
+ 'be correct' % (project, number))
+
+ shutil.rmtree(self.workspace_root)
+
+ self.worker.hold_jobs_in_build = False
+ self.worker.release()
+ self.waitUntilSettled()
+
+ def test_post_checkout(self):
+ project = "org/project"
+ path = os.path.join(self.upstream_root, project)
+ repo = git.Repo(path)
+ repo.head.reference = repo.heads['master']
+ commits = []
+ for i in range(0, 3):
+ commits.append(self.create_commit(project))
+ newRev = commits[1]
+
+ cloner = zuul.lib.cloner.Cloner(
+ git_base_url=self.upstream_root,
+ projects=[project],
+ workspace=self.workspace_root,
+ zuul_branch=None,
+ zuul_ref='master',
+ zuul_url=self.git_root,
+ zuul_project=project,
+ zuul_newrev=newRev,
+ )
+ cloner.execute()
+ repos = self.getWorkspaceRepos([project])
+ cloned_sha = repos[project].rev_parse('HEAD').hexsha
+ self.assertEqual(newRev, cloned_sha)
+
+ def test_post_and_master_checkout(self):
+ project = "org/project1"
+ master_project = "org/project2"
+ path = os.path.join(self.upstream_root, project)
+ repo = git.Repo(path)
+ repo.head.reference = repo.heads['master']
+ commits = []
+ for i in range(0, 3):
+ commits.append(self.create_commit(project))
+ newRev = commits[1]
+
+ cloner = zuul.lib.cloner.Cloner(
+ git_base_url=self.upstream_root,
+ projects=[project, master_project],
+ workspace=self.workspace_root,
+ zuul_branch=None,
+ zuul_ref='master',
+ zuul_url=self.git_root,
+ zuul_project=project,
+ zuul_newrev=newRev
+ )
+ cloner.execute()
+ repos = self.getWorkspaceRepos([project, master_project])
+ cloned_sha = repos[project].rev_parse('HEAD').hexsha
+ self.assertEqual(newRev, cloned_sha)
+ self.assertEqual(
+ repos[master_project].rev_parse('HEAD').hexsha,
+ repos[master_project].rev_parse('master').hexsha)
diff --git a/tests/unit/test_cloner_cmd.py b/tests/unit/test_cloner_cmd.py
new file mode 100644
index 0000000..9cbb5b8
--- /dev/null
+++ b/tests/unit/test_cloner_cmd.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env python
+
+# 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.
+
+import logging
+import os
+
+import testtools
+import zuul.cmd.cloner
+
+logging.basicConfig(level=logging.DEBUG,
+ format='%(asctime)s %(name)-32s '
+ '%(levelname)-8s %(message)s')
+
+
+class TestClonerCmdArguments(testtools.TestCase):
+
+ def setUp(self):
+ super(TestClonerCmdArguments, self).setUp()
+ self.app = zuul.cmd.cloner.Cloner()
+
+ def test_default_cache_dir_empty(self):
+ self.app.parse_arguments(['base', 'repo'])
+ self.assertEqual(None, self.app.args.cache_dir)
+
+ def test_default_cache_dir_environ(self):
+ try:
+ os.environ['ZUUL_CACHE_DIR'] = 'fromenviron'
+ self.app.parse_arguments(['base', 'repo'])
+ self.assertEqual('fromenviron', self.app.args.cache_dir)
+ finally:
+ del os.environ['ZUUL_CACHE_DIR']
+
+ def test_default_cache_dir_override_environ(self):
+ try:
+ os.environ['ZUUL_CACHE_DIR'] = 'fromenviron'
+ self.app.parse_arguments(['--cache-dir', 'argument',
+ 'base', 'repo'])
+ self.assertEqual('argument', self.app.args.cache_dir)
+ finally:
+ del os.environ['ZUUL_CACHE_DIR']
+
+ def test_default_cache_dir_argument(self):
+ self.app.parse_arguments(['--cache-dir', 'argument',
+ 'base', 'repo'])
+ self.assertEqual('argument', self.app.args.cache_dir)
diff --git a/tests/unit/test_connection.py b/tests/unit/test_connection.py
new file mode 100644
index 0000000..f8d1bf5
--- /dev/null
+++ b/tests/unit/test_connection.py
@@ -0,0 +1,74 @@
+# Copyright 2014 Rackspace Australia
+#
+# 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
+
+
+class TestConnections(ZuulTestCase):
+ config_file = 'zuul-connections-same-gerrit.conf'
+ tenant_config_file = 'config/zuul-connections-same-gerrit/main.yaml'
+
+ def test_multiple_connections(self):
+ "Test multiple connections to the one gerrit"
+
+ A = self.fake_review_gerrit.addFakeChange('org/project', 'master', 'A')
+ self.addEvent('review_gerrit', A.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(A.patchsets[-1]['approvals'][0]['by']['username'],
+ 'jenkins')
+
+ B = self.fake_review_gerrit.addFakeChange('org/project', 'master', 'B')
+ self.launch_server.failJob('project-test2', B)
+ self.addEvent('review_gerrit', B.getPatchsetCreatedEvent(1))
+
+ self.waitUntilSettled()
+
+ 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(B.patchsets[-1]['approvals'][0]['by']['username'],
+ 'civoter')
+
+
+class TestMultipleGerrits(ZuulTestCase):
+ def setUp(self):
+ self.skip("Disabled for early v3 development")
+
+ def setup_config(self,
+ config_file='zuul-connections-multiple-gerrits.conf'):
+ super(TestMultipleGerrits, self).setup_config(config_file)
+ self.self.updateConfigLayout(
+ 'layout-connections-multiple-gerrits.yaml')
+
+ def test_multiple_project_separate_gerrits(self):
+ self.worker.hold_jobs_in_build = True
+
+ A = self.fake_another_gerrit.addFakeChange(
+ 'org/project', 'master', 'A')
+ self.fake_another_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+
+ self.waitUntilSettled()
+
+ self.assertEqual(1, len(self.builds))
+ self.assertEqual('project-another-gerrit', self.builds[0].name)
+ self.assertTrue(self.job_has_changes(self.builds[0], A))
+
+ self.worker.hold_jobs_in_build = False
+ self.worker.release()
+ self.waitUntilSettled()
diff --git a/tests/unit/test_daemon.py b/tests/unit/test_daemon.py
new file mode 100644
index 0000000..689d4f7
--- /dev/null
+++ b/tests/unit/test_daemon.py
@@ -0,0 +1,63 @@
+#!/usr/bin/env python
+
+# Copyright 2014 Hewlett-Packard Development Company, L.P.
+#
+# 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.
+
+import daemon
+import logging
+import os
+import sys
+
+import extras
+import fixtures
+import testtools
+
+from tests.base import iterate_timeout
+
+# as of python-daemon 1.6 it doesn't bundle pidlockfile anymore
+# instead it depends on lockfile-0.9.1 which uses pidfile.
+pid_file_module = extras.try_imports(['daemon.pidlockfile', 'daemon.pidfile'])
+
+
+def daemon_test(pidfile, flagfile):
+ pid = pid_file_module.TimeoutPIDLockFile(pidfile, 10)
+ with daemon.DaemonContext(pidfile=pid):
+ for x in iterate_timeout(30, "flagfile to be removed"):
+ if not os.path.exists(flagfile):
+ break
+ sys.exit(0)
+
+
+class TestDaemon(testtools.TestCase):
+ log = logging.getLogger("zuul.test.daemon")
+
+ def setUp(self):
+ super(TestDaemon, self).setUp()
+ self.test_root = self.useFixture(fixtures.TempDir(
+ rootdir=os.environ.get("ZUUL_TEST_ROOT"))).path
+
+ def test_daemon(self):
+ pidfile = os.path.join(self.test_root, "daemon.pid")
+ flagfile = os.path.join(self.test_root, "daemon.flag")
+ open(flagfile, 'w').close()
+ if not os.fork():
+ self._cleanups = []
+ daemon_test(pidfile, flagfile)
+ for x in iterate_timeout(30, "daemon to start"):
+ if os.path.exists(pidfile):
+ break
+ os.unlink(flagfile)
+ for x in iterate_timeout(30, "daemon to stop"):
+ if not os.path.exists(pidfile):
+ break
diff --git a/tests/unit/test_gerrit.py b/tests/unit/test_gerrit.py
new file mode 100644
index 0000000..999e55d
--- /dev/null
+++ b/tests/unit/test_gerrit.py
@@ -0,0 +1,81 @@
+#!/usr/bin/env python
+
+# Copyright 2015 BMW Car IT GmbH
+#
+# 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.
+import os
+
+try:
+ from unittest import mock
+except ImportError:
+ import mock
+
+import tests.base
+from tests.base import BaseTestCase
+from zuul.driver.gerrit.gerritconnection import GerritConnection
+
+FIXTURE_DIR = os.path.join(tests.base.FIXTURE_DIR, 'gerrit')
+
+
+def read_fixture(file):
+ with open('%s/%s' % (FIXTURE_DIR, file), 'r') as fixturefile:
+ lines = fixturefile.readlines()
+ command = lines[0].replace('\n', '')
+ value = ''.join(lines[1:])
+ return command, value
+
+
+def read_fixtures(files):
+ calls = []
+ values = []
+ for fixture_file in files:
+ command, value = read_fixture(fixture_file)
+ calls.append(mock.call(command))
+ values.append([value, ''])
+ return calls, values
+
+
+class TestGerrit(BaseTestCase):
+
+ @mock.patch('zuul.driver.gerrit.gerritconnection.GerritConnection._ssh')
+ def run_query(self, files, expected_patches, _ssh_mock):
+ gerrit_config = {
+ 'user': 'gerrit',
+ 'server': 'localhost',
+ }
+ gerrit = GerritConnection(None, 'review_gerrit', gerrit_config)
+
+ calls, values = read_fixtures(files)
+ _ssh_mock.side_effect = values
+
+ result = gerrit.simpleQuery('project:openstack-infra/zuul')
+
+ _ssh_mock.assert_has_calls(calls)
+ self.assertEquals(len(calls), _ssh_mock.call_count,
+ '_ssh should be called %d times' % len(calls))
+ self.assertIsNotNone(result, 'Result is not none')
+ self.assertEquals(len(result), expected_patches,
+ 'There must be %d patches.' % expected_patches)
+
+ def test_simple_query_pagination_new(self):
+ files = ['simple_query_pagination_new_1',
+ 'simple_query_pagination_new_2']
+ expected_patches = 5
+ self.run_query(files, expected_patches)
+
+ def test_simple_query_pagination_old(self):
+ files = ['simple_query_pagination_old_1',
+ 'simple_query_pagination_old_2',
+ 'simple_query_pagination_old_3']
+ expected_patches = 5
+ self.run_query(files, expected_patches)
diff --git a/tests/unit/test_layoutvalidator.py b/tests/unit/test_layoutvalidator.py
new file mode 100644
index 0000000..38c8e29
--- /dev/null
+++ b/tests/unit/test_layoutvalidator.py
@@ -0,0 +1,81 @@
+#!/usr/bin/env python
+
+# Copyright 2013 OpenStack Foundation
+#
+# 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 six.moves import configparser as ConfigParser
+import os
+import re
+
+import testtools
+import voluptuous
+import yaml
+
+import zuul.layoutvalidator
+import zuul.lib.connections
+
+FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
+ 'fixtures')
+LAYOUT_RE = re.compile(r'^(good|bad)_.*\.yaml$')
+
+
+class TestLayoutValidator(testtools.TestCase):
+ def setUp(self):
+ self.skip("Disabled for early v3 development")
+
+ def test_layouts(self):
+ """Test layout file validation"""
+ print()
+ errors = []
+ for fn in os.listdir(os.path.join(FIXTURE_DIR, 'layouts')):
+ m = LAYOUT_RE.match(fn)
+ if not m:
+ continue
+ print(fn)
+
+ # Load any .conf file by the same name but .conf extension.
+ config_file = ("%s.conf" %
+ os.path.join(FIXTURE_DIR, 'layouts',
+ fn.split('.yaml')[0]))
+ if not os.path.isfile(config_file):
+ config_file = os.path.join(FIXTURE_DIR, 'layouts',
+ 'zuul_default.conf')
+ config = ConfigParser.ConfigParser()
+ config.read(config_file)
+ connections = zuul.lib.connections.configure_connections(config)
+
+ layout = os.path.join(FIXTURE_DIR, 'layouts', fn)
+ data = yaml.load(open(layout))
+ validator = zuul.layoutvalidator.LayoutValidator()
+ if m.group(1) == 'good':
+ try:
+ validator.validate(data, connections)
+ except voluptuous.Invalid as e:
+ raise Exception(
+ 'Unexpected YAML syntax error in %s:\n %s' %
+ (fn, str(e)))
+ else:
+ try:
+ validator.validate(data, connections)
+ raise Exception("Expected a YAML syntax error in %s." %
+ fn)
+ except voluptuous.Invalid as e:
+ error = str(e)
+ print(' ', error)
+ if error in errors:
+ raise Exception("Error has already been tested: %s" %
+ error)
+ else:
+ errors.append(error)
+ pass
diff --git a/tests/unit/test_merger_repo.py b/tests/unit/test_merger_repo.py
new file mode 100644
index 0000000..5062c14
--- /dev/null
+++ b/tests/unit/test_merger_repo.py
@@ -0,0 +1,80 @@
+#!/usr/bin/env python
+
+# Copyright 2012 Hewlett-Packard Development Company, L.P.
+# Copyright 2014 Wikimedia Foundation Inc.
+#
+# 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.
+
+import logging
+import os
+
+import git
+
+from zuul.merger.merger import Repo
+from tests.base import ZuulTestCase
+
+logging.basicConfig(level=logging.DEBUG,
+ format='%(asctime)s %(name)-32s '
+ '%(levelname)-8s %(message)s')
+
+
+class TestMergerRepo(ZuulTestCase):
+
+ log = logging.getLogger("zuul.test.merger.repo")
+ tenant_config_file = 'config/single-tenant/main.yaml'
+ workspace_root = None
+
+ def setUp(self):
+ super(TestMergerRepo, self).setUp()
+ self.workspace_root = os.path.join(self.test_root, 'workspace')
+
+ def test_ensure_cloned(self):
+ parent_path = os.path.join(self.upstream_root, 'org/project1')
+
+ # Forge a repo having a submodule
+ parent_repo = git.Repo(parent_path)
+ parent_repo.git.submodule('add', os.path.join(
+ self.upstream_root, 'org/project2'), 'subdir')
+ parent_repo.index.commit('Adding project2 as a submodule in subdir')
+ # git 1.7.8 changed .git from being a directory to a file pointing
+ # to the parent repository /.git/modules/*
+ self.assertTrue(os.path.exists(
+ os.path.join(parent_path, 'subdir', '.git')),
+ msg='.git file in submodule should be a file')
+
+ work_repo = Repo(parent_path, self.workspace_root,
+ 'none@example.org', 'User Name')
+ self.assertTrue(
+ os.path.isdir(os.path.join(self.workspace_root, 'subdir')),
+ msg='Cloned repository has a submodule placeholder directory')
+ self.assertFalse(os.path.exists(
+ os.path.join(self.workspace_root, 'subdir', '.git')),
+ msg='Submodule is not initialized')
+
+ sub_repo = Repo(
+ os.path.join(self.upstream_root, 'org/project2'),
+ os.path.join(self.workspace_root, 'subdir'),
+ 'none@example.org', 'User Name')
+ self.assertTrue(os.path.exists(
+ os.path.join(self.workspace_root, 'subdir', '.git')),
+ msg='Cloned over the submodule placeholder')
+
+ self.assertEquals(
+ os.path.join(self.upstream_root, 'org/project1'),
+ work_repo.createRepoObject().remotes[0].url,
+ message="Parent clone still point to upstream project1")
+
+ self.assertEquals(
+ os.path.join(self.upstream_root, 'org/project2'),
+ sub_repo.createRepoObject().remotes[0].url,
+ message="Sub repository points to upstream project2")
diff --git a/tests/unit/test_model.py b/tests/unit/test_model.py
new file mode 100644
index 0000000..0189340
--- /dev/null
+++ b/tests/unit/test_model.py
@@ -0,0 +1,404 @@
+# Copyright 2015 Red Hat, Inc.
+#
+# 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.
+
+
+import os
+import random
+
+import fixtures
+import testtools
+
+from zuul import model
+from zuul import configloader
+
+from tests.base import BaseTestCase
+
+
+class TestJob(BaseTestCase):
+
+ @property
+ def job(self):
+ layout = model.Layout()
+ job = configloader.JobParser.fromYaml(layout, {
+ 'name': 'job',
+ 'irrelevant-files': [
+ '^docs/.*$'
+ ]})
+ return job
+
+ def test_change_matches_returns_false_for_matched_skip_if(self):
+ change = model.Change('project')
+ change.files = ['/COMMIT_MSG', 'docs/foo']
+ self.assertFalse(self.job.changeMatches(change))
+
+ def test_change_matches_returns_true_for_unmatched_skip_if(self):
+ change = model.Change('project')
+ change.files = ['/COMMIT_MSG', 'foo']
+ self.assertTrue(self.job.changeMatches(change))
+
+ def test_job_sets_defaults_for_boolean_attributes(self):
+ self.assertIsNotNone(self.job.voting)
+
+ def test_job_inheritance(self):
+ layout = model.Layout()
+
+ pipeline = model.Pipeline('gate', layout)
+ layout.addPipeline(pipeline)
+ queue = model.ChangeQueue(pipeline)
+ project = model.Project('project')
+
+ base = configloader.JobParser.fromYaml(layout, {
+ '_source_project': project,
+ 'name': 'base',
+ 'timeout': 30,
+ })
+ layout.addJob(base)
+ python27 = configloader.JobParser.fromYaml(layout, {
+ '_source_project': project,
+ 'name': 'python27',
+ 'parent': 'base',
+ 'timeout': 40,
+ })
+ layout.addJob(python27)
+ python27diablo = configloader.JobParser.fromYaml(layout, {
+ '_source_project': project,
+ 'name': 'python27',
+ 'branches': [
+ 'stable/diablo'
+ ],
+ 'timeout': 50,
+ })
+ layout.addJob(python27diablo)
+
+ project_config = configloader.ProjectParser.fromYaml(layout, {
+ 'name': 'project',
+ 'gate': {
+ 'jobs': [
+ 'python27'
+ ]
+ }
+ })
+ layout.addProjectConfig(project_config, update_pipeline=False)
+
+ change = model.Change(project)
+ change.branch = 'master'
+ item = queue.enqueueChange(change)
+ item.current_build_set.layout = layout
+
+ self.assertTrue(base.changeMatches(change))
+ self.assertTrue(python27.changeMatches(change))
+ self.assertFalse(python27diablo.changeMatches(change))
+
+ item.freezeJobTree()
+ self.assertEqual(len(item.getJobs()), 1)
+ job = item.getJobs()[0]
+ self.assertEqual(job.name, 'python27')
+ self.assertEqual(job.timeout, 40)
+
+ change.branch = 'stable/diablo'
+ item = queue.enqueueChange(change)
+ item.current_build_set.layout = layout
+
+ self.assertTrue(base.changeMatches(change))
+ self.assertTrue(python27.changeMatches(change))
+ self.assertTrue(python27diablo.changeMatches(change))
+
+ item.freezeJobTree()
+ self.assertEqual(len(item.getJobs()), 1)
+ job = item.getJobs()[0]
+ self.assertEqual(job.name, 'python27')
+ self.assertEqual(job.timeout, 50)
+
+ def test_job_auth_inheritance(self):
+ layout = model.Layout()
+ project = model.Project('project')
+
+ base = configloader.JobParser.fromYaml(layout, {
+ '_source_project': project,
+ 'name': 'base',
+ 'timeout': 30,
+ })
+ layout.addJob(base)
+ pypi_upload_without_inherit = configloader.JobParser.fromYaml(layout, {
+ '_source_project': project,
+ 'name': 'pypi-upload-without-inherit',
+ 'parent': 'base',
+ 'timeout': 40,
+ 'auth': {
+ 'secrets': [
+ 'pypi-credentials',
+ ]
+ }
+ })
+ layout.addJob(pypi_upload_without_inherit)
+ pypi_upload_with_inherit = configloader.JobParser.fromYaml(layout, {
+ '_source_project': project,
+ 'name': 'pypi-upload-with-inherit',
+ 'parent': 'base',
+ 'timeout': 40,
+ 'auth': {
+ 'inherit': True,
+ 'secrets': [
+ 'pypi-credentials',
+ ]
+ }
+ })
+ layout.addJob(pypi_upload_with_inherit)
+ pypi_upload_with_inherit_false = configloader.JobParser.fromYaml(
+ layout, {
+ '_source_project': project,
+ 'name': 'pypi-upload-with-inherit-false',
+ 'parent': 'base',
+ 'timeout': 40,
+ 'auth': {
+ 'inherit': False,
+ 'secrets': [
+ 'pypi-credentials',
+ ]
+ }
+ })
+ layout.addJob(pypi_upload_with_inherit_false)
+ in_repo_job_without_inherit = configloader.JobParser.fromYaml(layout, {
+ '_source_project': project,
+ 'name': 'in-repo-job-without-inherit',
+ 'parent': 'pypi-upload-without-inherit',
+ })
+ layout.addJob(in_repo_job_without_inherit)
+ in_repo_job_with_inherit = configloader.JobParser.fromYaml(layout, {
+ '_source_project': project,
+ 'name': 'in-repo-job-with-inherit',
+ 'parent': 'pypi-upload-with-inherit',
+ })
+ layout.addJob(in_repo_job_with_inherit)
+ in_repo_job_with_inherit_false = configloader.JobParser.fromYaml(
+ layout, {
+ '_source_project': project,
+ 'name': 'in-repo-job-with-inherit-false',
+ 'parent': 'pypi-upload-with-inherit-false',
+ })
+ layout.addJob(in_repo_job_with_inherit_false)
+
+ self.assertNotIn('auth', in_repo_job_without_inherit.auth)
+ self.assertIn('secrets', in_repo_job_with_inherit.auth)
+ self.assertEquals(in_repo_job_with_inherit.auth['secrets'],
+ ['pypi-credentials'])
+ self.assertNotIn('auth', in_repo_job_with_inherit_false.auth)
+
+ def test_job_inheritance_job_tree(self):
+ layout = model.Layout()
+
+ pipeline = model.Pipeline('gate', layout)
+ layout.addPipeline(pipeline)
+ queue = model.ChangeQueue(pipeline)
+ project = model.Project('project')
+
+ base = configloader.JobParser.fromYaml(layout, {
+ '_source_project': project,
+ 'name': 'base',
+ 'timeout': 30,
+ })
+ layout.addJob(base)
+ python27 = configloader.JobParser.fromYaml(layout, {
+ '_source_project': project,
+ 'name': 'python27',
+ 'parent': 'base',
+ 'timeout': 40,
+ })
+ layout.addJob(python27)
+ python27diablo = configloader.JobParser.fromYaml(layout, {
+ '_source_project': project,
+ 'name': 'python27',
+ 'branches': [
+ 'stable/diablo'
+ ],
+ 'timeout': 50,
+ })
+ layout.addJob(python27diablo)
+
+ project_config = configloader.ProjectParser.fromYaml(layout, {
+ 'name': 'project',
+ 'gate': {
+ 'jobs': [
+ {'python27': {'timeout': 70}}
+ ]
+ }
+ })
+ layout.addProjectConfig(project_config, update_pipeline=False)
+
+ change = model.Change(project)
+ change.branch = 'master'
+ item = queue.enqueueChange(change)
+ item.current_build_set.layout = layout
+
+ self.assertTrue(base.changeMatches(change))
+ self.assertTrue(python27.changeMatches(change))
+ self.assertFalse(python27diablo.changeMatches(change))
+
+ item.freezeJobTree()
+ self.assertEqual(len(item.getJobs()), 1)
+ job = item.getJobs()[0]
+ self.assertEqual(job.name, 'python27')
+ self.assertEqual(job.timeout, 70)
+
+ change.branch = 'stable/diablo'
+ item = queue.enqueueChange(change)
+ item.current_build_set.layout = layout
+
+ self.assertTrue(base.changeMatches(change))
+ self.assertTrue(python27.changeMatches(change))
+ self.assertTrue(python27diablo.changeMatches(change))
+
+ item.freezeJobTree()
+ self.assertEqual(len(item.getJobs()), 1)
+ job = item.getJobs()[0]
+ self.assertEqual(job.name, 'python27')
+ self.assertEqual(job.timeout, 70)
+
+ def test_inheritance_keeps_matchers(self):
+ layout = model.Layout()
+
+ pipeline = model.Pipeline('gate', layout)
+ layout.addPipeline(pipeline)
+ queue = model.ChangeQueue(pipeline)
+ project = model.Project('project')
+
+ base = configloader.JobParser.fromYaml(layout, {
+ '_source_project': project,
+ 'name': 'base',
+ 'timeout': 30,
+ })
+ layout.addJob(base)
+ python27 = configloader.JobParser.fromYaml(layout, {
+ '_source_project': project,
+ 'name': 'python27',
+ 'parent': 'base',
+ 'timeout': 40,
+ 'irrelevant-files': ['^ignored-file$'],
+ })
+ layout.addJob(python27)
+
+ project_config = configloader.ProjectParser.fromYaml(layout, {
+ 'name': 'project',
+ 'gate': {
+ 'jobs': [
+ 'python27',
+ ]
+ }
+ })
+ layout.addProjectConfig(project_config, update_pipeline=False)
+
+ change = model.Change(project)
+ change.branch = 'master'
+ change.files = ['/COMMIT_MSG', 'ignored-file']
+ item = queue.enqueueChange(change)
+ item.current_build_set.layout = layout
+
+ self.assertTrue(base.changeMatches(change))
+ self.assertFalse(python27.changeMatches(change))
+
+ item.freezeJobTree()
+ self.assertEqual([], item.getJobs())
+
+ def test_job_source_project(self):
+ layout = model.Layout()
+ base_project = model.Project('base_project')
+ base = configloader.JobParser.fromYaml(layout, {
+ '_source_project': base_project,
+ 'name': 'base',
+ })
+ layout.addJob(base)
+
+ other_project = model.Project('other_project')
+ base2 = configloader.JobParser.fromYaml(layout, {
+ '_source_project': other_project,
+ 'name': 'base',
+ })
+ with testtools.ExpectedException(
+ Exception,
+ "Job base in other_project is not permitted "
+ "to shadow job base in base_project"):
+ layout.addJob(base2)
+
+
+class TestJobTimeData(BaseTestCase):
+ def setUp(self):
+ super(TestJobTimeData, self).setUp()
+ self.tmp_root = self.useFixture(fixtures.TempDir(
+ rootdir=os.environ.get("ZUUL_TEST_ROOT"))
+ ).path
+
+ def test_empty_timedata(self):
+ path = os.path.join(self.tmp_root, 'job-name')
+ self.assertFalse(os.path.exists(path))
+ self.assertFalse(os.path.exists(path + '.tmp'))
+ td = model.JobTimeData(path)
+ self.assertEqual(td.success_times, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
+ self.assertEqual(td.failure_times, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
+ self.assertEqual(td.results, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
+
+ def test_save_reload(self):
+ path = os.path.join(self.tmp_root, 'job-name')
+ self.assertFalse(os.path.exists(path))
+ self.assertFalse(os.path.exists(path + '.tmp'))
+ td = model.JobTimeData(path)
+ self.assertEqual(td.success_times, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
+ self.assertEqual(td.failure_times, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
+ self.assertEqual(td.results, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
+ success_times = []
+ failure_times = []
+ results = []
+ for x in range(10):
+ success_times.append(int(random.random() * 1000))
+ failure_times.append(int(random.random() * 1000))
+ results.append(0)
+ results.append(1)
+ random.shuffle(results)
+ s = f = 0
+ for result in results:
+ if result:
+ td.add(failure_times[f], 'FAILURE')
+ f += 1
+ else:
+ td.add(success_times[s], 'SUCCESS')
+ s += 1
+ self.assertEqual(td.success_times, success_times)
+ self.assertEqual(td.failure_times, failure_times)
+ self.assertEqual(td.results, results[10:])
+ td.save()
+ self.assertTrue(os.path.exists(path))
+ self.assertFalse(os.path.exists(path + '.tmp'))
+ td = model.JobTimeData(path)
+ td.load()
+ self.assertEqual(td.success_times, success_times)
+ self.assertEqual(td.failure_times, failure_times)
+ self.assertEqual(td.results, results[10:])
+
+
+class TestTimeDataBase(BaseTestCase):
+ def setUp(self):
+ super(TestTimeDataBase, self).setUp()
+ self.tmp_root = self.useFixture(fixtures.TempDir(
+ rootdir=os.environ.get("ZUUL_TEST_ROOT"))
+ ).path
+ self.db = model.TimeDataBase(self.tmp_root)
+
+ def test_timedatabase(self):
+ self.assertEqual(self.db.getEstimatedTime('job-name'), 0)
+ self.db.update('job-name', 50, 'SUCCESS')
+ self.assertEqual(self.db.getEstimatedTime('job-name'), 50)
+ self.db.update('job-name', 100, 'SUCCESS')
+ self.assertEqual(self.db.getEstimatedTime('job-name'), 75)
+ for x in range(10):
+ self.db.update('job-name', 100, 'SUCCESS')
+ self.assertEqual(self.db.getEstimatedTime('job-name'), 100)
diff --git a/tests/unit/test_nodepool.py b/tests/unit/test_nodepool.py
new file mode 100644
index 0000000..6462f9a
--- /dev/null
+++ b/tests/unit/test_nodepool.py
@@ -0,0 +1,122 @@
+# Copyright 2017 Red Hat, Inc.
+#
+# 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.
+
+
+import time
+
+import zuul.zk
+import zuul.nodepool
+from zuul import model
+
+from tests.base import BaseTestCase, ChrootedKazooFixture, FakeNodepool
+
+
+class TestNodepool(BaseTestCase):
+ # Tests the Nodepool interface class using a fake nodepool and
+ # scheduler.
+
+ def setUp(self):
+ super(BaseTestCase, self).setUp()
+
+ self.zk_chroot_fixture = self.useFixture(ChrootedKazooFixture())
+ self.zk_config = zuul.zk.ZooKeeperConnectionConfig(
+ self.zk_chroot_fixture.zookeeper_host,
+ self.zk_chroot_fixture.zookeeper_port,
+ self.zk_chroot_fixture.zookeeper_chroot)
+
+ self.zk = zuul.zk.ZooKeeper()
+ self.zk.connect([self.zk_config])
+
+ self.provisioned_requests = []
+ # This class implements the scheduler methods zuul.nodepool
+ # needs, so we pass 'self' as the scheduler.
+ self.nodepool = zuul.nodepool.Nodepool(self)
+
+ self.fake_nodepool = FakeNodepool(self.zk_config.host,
+ self.zk_config.port,
+ self.zk_config.chroot)
+
+ def waitForRequests(self):
+ # Wait until all requests are complete.
+ while self.nodepool.requests:
+ time.sleep(0.1)
+
+ def onNodesProvisioned(self, request):
+ # This is a scheduler method that the nodepool class calls
+ # back when a request is provisioned.
+ self.provisioned_requests.append(request)
+
+ def test_node_request(self):
+ # Test a simple node request
+
+ nodeset = model.NodeSet()
+ nodeset.addNode(model.Node('controller', 'ubuntu-xenial'))
+ nodeset.addNode(model.Node('compute', 'ubuntu-xenial'))
+ job = model.Job('testjob')
+ job.nodeset = nodeset
+ request = self.nodepool.requestNodes(None, job)
+ self.waitForRequests()
+ self.assertEqual(len(self.provisioned_requests), 1)
+ self.assertEqual(request.state, 'fulfilled')
+
+ # Accept the nodes
+ self.nodepool.acceptNodes(request)
+ nodeset = request.nodeset
+
+ for node in nodeset.getNodes():
+ self.assertIsNotNone(node.lock)
+ self.assertEqual(node.state, 'ready')
+
+ # Mark the nodes in use
+ self.nodepool.useNodeSet(nodeset)
+ for node in nodeset.getNodes():
+ self.assertEqual(node.state, 'in-use')
+
+ # Return the nodes
+ self.nodepool.returnNodeSet(nodeset)
+ for node in nodeset.getNodes():
+ self.assertIsNone(node.lock)
+ self.assertEqual(node.state, 'used')
+
+ def test_node_request_disconnect(self):
+ # Test that node requests are re-submitted after disconnect
+
+ nodeset = model.NodeSet()
+ nodeset.addNode(model.Node('controller', 'ubuntu-xenial'))
+ nodeset.addNode(model.Node('compute', 'ubuntu-xenial'))
+ job = model.Job('testjob')
+ job.nodeset = nodeset
+ self.fake_nodepool.paused = True
+ request = self.nodepool.requestNodes(None, job)
+ self.zk.client.stop()
+ self.zk.client.start()
+ self.fake_nodepool.paused = False
+ self.waitForRequests()
+ self.assertEqual(len(self.provisioned_requests), 1)
+ self.assertEqual(request.state, 'fulfilled')
+
+ def test_node_request_canceled(self):
+ # Test that node requests can be canceled
+
+ nodeset = model.NodeSet()
+ nodeset.addNode(model.Node('controller', 'ubuntu-xenial'))
+ nodeset.addNode(model.Node('compute', 'ubuntu-xenial'))
+ job = model.Job('testjob')
+ job.nodeset = nodeset
+ self.fake_nodepool.paused = True
+ request = self.nodepool.requestNodes(None, job)
+ self.nodepool.cancelRequest(request)
+
+ self.waitForRequests()
+ self.assertEqual(len(self.provisioned_requests), 0)
diff --git a/tests/unit/test_openstack.py b/tests/unit/test_openstack.py
new file mode 100644
index 0000000..175b4bd
--- /dev/null
+++ b/tests/unit/test_openstack.py
@@ -0,0 +1,62 @@
+#!/usr/bin/env python
+
+# Copyright 2012 Hewlett-Packard Development Company, L.P.
+#
+# 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.
+
+import logging
+
+from tests.base import AnsibleZuulTestCase
+
+logging.basicConfig(level=logging.DEBUG,
+ format='%(asctime)s %(name)-32s '
+ '%(levelname)-8s %(message)s')
+
+
+class TestOpenStack(AnsibleZuulTestCase):
+ # A temporary class to experiment with how openstack can use
+ # Zuulv3
+
+ tenant_config_file = 'config/openstack/main.yaml'
+
+ def test_nova_master(self):
+ A = self.fake_gerrit.addFakeChange('openstack/nova', 'master', 'A')
+ A.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+ self.assertEqual(self.getJobFromHistory('python27').result,
+ 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('python35').result,
+ 'SUCCESS')
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2,
+ "A should report start and success")
+ self.assertEqual(self.getJobFromHistory('python27').node,
+ 'ubuntu-xenial')
+
+ def test_nova_mitaka(self):
+ self.create_branch('openstack/nova', 'stable/mitaka')
+ A = self.fake_gerrit.addFakeChange('openstack/nova',
+ 'stable/mitaka', 'A')
+ A.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+ self.assertEqual(self.getJobFromHistory('python27').result,
+ 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('python35').result,
+ 'SUCCESS')
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2,
+ "A should report start and success")
+ self.assertEqual(self.getJobFromHistory('python27').node,
+ 'ubuntu-trusty')
diff --git a/tests/unit/test_requirements.py b/tests/unit/test_requirements.py
new file mode 100644
index 0000000..1ea0b2e
--- /dev/null
+++ b/tests/unit/test_requirements.py
@@ -0,0 +1,420 @@
+#!/usr/bin/env python
+
+# Copyright 2012-2014 Hewlett-Packard Development Company, L.P.
+#
+# 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.
+
+import logging
+import time
+
+from tests.base import ZuulTestCase
+
+logging.basicConfig(level=logging.DEBUG,
+ format='%(asctime)s %(name)-32s '
+ '%(levelname)-8s %(message)s')
+
+
+class TestRequirementsApprovalNewerThan(ZuulTestCase):
+ """Requirements with a newer-than comment requirement"""
+
+ tenant_config_file = 'config/requirements/newer-than/main.yaml'
+
+ def test_pipeline_require_approval_newer_than(self):
+ "Test pipeline requirement: approval newer than"
+ return self._test_require_approval_newer_than('org/project1',
+ 'project1-job')
+
+ def test_trigger_require_approval_newer_than(self):
+ "Test trigger requirement: approval newer than"
+ return self._test_require_approval_newer_than('org/project2',
+ 'project2-job')
+
+ def _test_require_approval_newer_than(self, project, job):
+ A = self.fake_gerrit.addFakeChange(project, 'master', 'A')
+ # A comment event that we will keep submitting to trigger
+ comment = A.addApproval('code-review', 2, username='nobody')
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ # No +1 from Jenkins so should not be enqueued
+ self.assertEqual(len(self.history), 0)
+
+ # Add a too-old +1, should not be enqueued
+ A.addApproval('verified', 1, username='jenkins',
+ granted_on=time.time() - 72 * 60 * 60)
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 0)
+
+ # Add a recent +1
+ self.fake_gerrit.addEvent(A.addApproval('verified', 1,
+ username='jenkins'))
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+ self.assertEqual(self.history[0].name, job)
+
+
+class TestRequirementsApprovalOlderThan(ZuulTestCase):
+ """Requirements with a older-than comment requirement"""
+
+ tenant_config_file = 'config/requirements/older-than/main.yaml'
+
+ def test_pipeline_require_approval_older_than(self):
+ "Test pipeline requirement: approval older than"
+ return self._test_require_approval_older_than('org/project1',
+ 'project1-job')
+
+ def test_trigger_require_approval_older_than(self):
+ "Test trigger requirement: approval older than"
+ return self._test_require_approval_older_than('org/project2',
+ 'project2-job')
+
+ def _test_require_approval_older_than(self, project, job):
+ A = self.fake_gerrit.addFakeChange(project, 'master', 'A')
+ # A comment event that we will keep submitting to trigger
+ comment = A.addApproval('code-review', 2, username='nobody')
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ # No +1 from Jenkins so should not be enqueued
+ self.assertEqual(len(self.history), 0)
+
+ # Add a recent +1 which should not be enqueued
+ A.addApproval('verified', 1)
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 0)
+
+ # Add an old +1 which should be enqueued
+ A.addApproval('verified', 1, username='jenkins',
+ granted_on=time.time() - 72 * 60 * 60)
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+ self.assertEqual(self.history[0].name, job)
+
+
+class TestRequirementsUserName(ZuulTestCase):
+ """Requirements with a username requirement"""
+
+ tenant_config_file = 'config/requirements/username/main.yaml'
+
+ def test_pipeline_require_approval_username(self):
+ "Test pipeline requirement: approval username"
+ return self._test_require_approval_username('org/project1',
+ 'project1-job')
+
+ def test_trigger_require_approval_username(self):
+ "Test trigger requirement: approval username"
+ return self._test_require_approval_username('org/project2',
+ 'project2-job')
+
+ def _test_require_approval_username(self, project, job):
+ A = self.fake_gerrit.addFakeChange(project, 'master', 'A')
+ # A comment event that we will keep submitting to trigger
+ comment = A.addApproval('code-review', 2, username='nobody')
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ # No approval from Jenkins so should not be enqueued
+ self.assertEqual(len(self.history), 0)
+
+ # Add an approval from Jenkins
+ A.addApproval('verified', 1, username='jenkins')
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+ self.assertEqual(self.history[0].name, job)
+
+
+class TestRequirementsEmail(ZuulTestCase):
+ """Requirements with a email requirement"""
+
+ tenant_config_file = 'config/requirements/email/main.yaml'
+
+ def test_pipeline_require_approval_email(self):
+ "Test pipeline requirement: approval email"
+ return self._test_require_approval_email('org/project1',
+ 'project1-job')
+
+ def test_trigger_require_approval_email(self):
+ "Test trigger requirement: approval email"
+ return self._test_require_approval_email('org/project2',
+ 'project2-job')
+
+ def _test_require_approval_email(self, project, job):
+ A = self.fake_gerrit.addFakeChange(project, 'master', 'A')
+ # A comment event that we will keep submitting to trigger
+ comment = A.addApproval('code-review', 2, username='nobody')
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ # No approval from Jenkins so should not be enqueued
+ self.assertEqual(len(self.history), 0)
+
+ # Add an approval from Jenkins
+ A.addApproval('verified', 1, username='jenkins')
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+ self.assertEqual(self.history[0].name, job)
+
+
+class TestRequirementsVote1(ZuulTestCase):
+ """Requirements with a voting requirement"""
+
+ tenant_config_file = 'config/requirements/vote1/main.yaml'
+
+ def test_pipeline_require_approval_vote1(self):
+ "Test pipeline requirement: approval vote with one value"
+ return self._test_require_approval_vote1('org/project1',
+ 'project1-job')
+
+ def test_trigger_require_approval_vote1(self):
+ "Test trigger requirement: approval vote with one value"
+ return self._test_require_approval_vote1('org/project2',
+ 'project2-job')
+
+ def _test_require_approval_vote1(self, project, job):
+ A = self.fake_gerrit.addFakeChange(project, 'master', 'A')
+ # A comment event that we will keep submitting to trigger
+ comment = A.addApproval('code-review', 2, username='nobody')
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ # No approval from Jenkins so should not be enqueued
+ self.assertEqual(len(self.history), 0)
+
+ # A -1 from jenkins should not cause it to be enqueued
+ A.addApproval('verified', -1, username='jenkins')
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 0)
+
+ # A +1 should allow it to be enqueued
+ A.addApproval('verified', 1, username='jenkins')
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+ self.assertEqual(self.history[0].name, job)
+
+
+class TestRequirementsVote2(ZuulTestCase):
+ """Requirements with a voting requirement"""
+
+ tenant_config_file = 'config/requirements/vote2/main.yaml'
+
+ def test_pipeline_require_approval_vote2(self):
+ "Test pipeline requirement: approval vote with two values"
+ return self._test_require_approval_vote2('org/project1',
+ 'project1-job')
+
+ def test_trigger_require_approval_vote2(self):
+ "Test trigger requirement: approval vote with two values"
+ return self._test_require_approval_vote2('org/project2',
+ 'project2-job')
+
+ def _test_require_approval_vote2(self, project, job):
+ A = self.fake_gerrit.addFakeChange(project, 'master', 'A')
+ # A comment event that we will keep submitting to trigger
+ comment = A.addApproval('code-review', 2, username='nobody')
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ # No approval from Jenkins so should not be enqueued
+ self.assertEqual(len(self.history), 0)
+
+ # A -1 from jenkins should not cause it to be enqueued
+ A.addApproval('verified', -1, username='jenkins')
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 0)
+
+ # A -2 from jenkins should not cause it to be enqueued
+ A.addApproval('verified', -2, username='jenkins')
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 0)
+
+ # A +1 from jenkins should allow it to be enqueued
+ A.addApproval('verified', 1, username='jenkins')
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+ self.assertEqual(self.history[0].name, job)
+
+ # A +2 from nobody should not cause it to be enqueued
+ B = self.fake_gerrit.addFakeChange(project, 'master', 'B')
+ # A comment event that we will keep submitting to trigger
+ comment = B.addApproval('code-review', 2, username='nobody')
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+
+ # A +2 from jenkins should allow it to be enqueued
+ B.addApproval('verified', 2, username='jenkins')
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 2)
+ self.assertEqual(self.history[1].name, job)
+
+
+class TestRequirementsState(ZuulTestCase):
+ """Requirements with simple state requirement"""
+
+ tenant_config_file = 'config/requirements/state/main.yaml'
+
+ def test_pipeline_require_current_patchset(self):
+ # Create two patchsets and let their tests settle out. Then
+ # comment on first patchset and check that no additional
+ # jobs are run.
+ A = self.fake_gerrit.addFakeChange('current-project', 'master', 'A')
+ self.fake_gerrit.addEvent(A.addApproval('code-review', 1))
+ self.waitUntilSettled()
+ A.addPatchset()
+ self.fake_gerrit.addEvent(A.addApproval('code-review', 1))
+ self.waitUntilSettled()
+
+ self.assertEqual(len(self.history), 2) # one job for each ps
+ self.fake_gerrit.addEvent(A.getChangeCommentEvent(1))
+ self.waitUntilSettled()
+
+ # Assert no new jobs ran after event for old patchset.
+ self.assertEqual(len(self.history), 2)
+
+ # Make sure the same event on a new PS will trigger
+ self.fake_gerrit.addEvent(A.getChangeCommentEvent(2))
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 3)
+
+ def test_pipeline_require_open(self):
+ A = self.fake_gerrit.addFakeChange('open-project', 'master', 'A',
+ status='MERGED')
+ self.fake_gerrit.addEvent(A.addApproval('code-review', 2))
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 0)
+
+ B = self.fake_gerrit.addFakeChange('open-project', 'master', 'B')
+ self.fake_gerrit.addEvent(B.addApproval('code-review', 2))
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+
+ def test_pipeline_require_status(self):
+ A = self.fake_gerrit.addFakeChange('status-project', 'master', 'A',
+ status='MERGED')
+ self.fake_gerrit.addEvent(A.addApproval('code-review', 2))
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 0)
+
+ B = self.fake_gerrit.addFakeChange('status-project', 'master', 'B')
+ self.fake_gerrit.addEvent(B.addApproval('code-review', 2))
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+
+
+class TestRequirementsRejectUsername(ZuulTestCase):
+ """Requirements with reject username requirement"""
+
+ tenant_config_file = 'config/requirements/reject-username/main.yaml'
+
+ def _test_require_reject_username(self, project, job):
+ "Test negative username's match"
+ # Should only trigger if Jenkins hasn't voted.
+ # add in a change with no comments
+ A = self.fake_gerrit.addFakeChange(project, 'master', 'A')
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 0)
+
+ # add in a comment that will trigger
+ self.fake_gerrit.addEvent(A.addApproval('code-review', 1,
+ username='reviewer'))
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+ self.assertEqual(self.history[0].name, job)
+
+ # add in a comment from jenkins user which shouldn't trigger
+ self.fake_gerrit.addEvent(A.addApproval('verified', 1,
+ username='jenkins'))
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+
+ # Check future reviews also won't trigger as a 'jenkins' user has
+ # commented previously
+ self.fake_gerrit.addEvent(A.addApproval('code-review', 1,
+ username='reviewer'))
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+
+ def test_pipeline_reject_username(self):
+ "Test negative pipeline requirement: no comment from jenkins"
+ return self._test_require_reject_username('org/project1',
+ 'project1-job')
+
+ def test_trigger_reject_username(self):
+ "Test negative trigger requirement: no comment from jenkins"
+ return self._test_require_reject_username('org/project2',
+ 'project2-job')
+
+
+class TestRequirementsReject(ZuulTestCase):
+ """Requirements with reject requirement"""
+
+ tenant_config_file = 'config/requirements/reject/main.yaml'
+
+ def _test_require_reject(self, project, job):
+ "Test no approval matches a reject param"
+ A = self.fake_gerrit.addFakeChange(project, 'master', 'A')
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 0)
+
+ # First positive vote should not queue until jenkins has +1'd
+ comment = A.addApproval('verified', 1, username='reviewer_a')
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 0)
+
+ # Jenkins should put in a +1 which will also queue
+ comment = A.addApproval('verified', 1, username='jenkins')
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+ self.assertEqual(self.history[0].name, job)
+
+ # Negative vote should not queue
+ comment = A.addApproval('verified', -1, username='reviewer_b')
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+
+ # Future approvals should do nothing
+ comment = A.addApproval('verified', 1, username='reviewer_c')
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+
+ # Change/update negative vote should queue
+ comment = A.addApproval('verified', 1, username='reviewer_b')
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 2)
+ self.assertEqual(self.history[1].name, job)
+
+ # Future approvals should also queue
+ comment = A.addApproval('verified', 1, username='reviewer_d')
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 3)
+ self.assertEqual(self.history[2].name, job)
+
+ def test_pipeline_require_reject(self):
+ "Test pipeline requirement: rejections absent"
+ return self._test_require_reject('org/project1', 'project1-job')
+
+ def test_trigger_require_reject(self):
+ "Test trigger requirement: rejections absent"
+ return self._test_require_reject('org/project2', 'project2-job')
diff --git a/tests/unit/test_scheduler.py b/tests/unit/test_scheduler.py
new file mode 100755
index 0000000..821629c
--- /dev/null
+++ b/tests/unit/test_scheduler.py
@@ -0,0 +1,4760 @@
+#!/usr/bin/env python
+
+# Copyright 2012 Hewlett-Packard Development Company, L.P.
+#
+# 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.
+
+import json
+import logging
+import os
+import re
+import shutil
+import time
+from unittest import skip
+
+import git
+from six.moves import urllib
+import testtools
+
+import zuul.change_matcher
+import zuul.scheduler
+import zuul.rpcclient
+import zuul.model
+
+from tests.base import (
+ ZuulTestCase,
+ repack_repo,
+)
+
+logging.basicConfig(level=logging.DEBUG,
+ format='%(asctime)s %(name)-32s '
+ '%(levelname)-8s %(message)s')
+
+
+class TestScheduler(ZuulTestCase):
+ tenant_config_file = 'config/single-tenant/main.yaml'
+
+ def test_jobs_launched(self):
+ "Test that jobs are launched and a change is merged"
+
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ A.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+ self.assertEqual(self.getJobFromHistory('project-merge').result,
+ 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('project-test1').result,
+ 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('project-test2').result,
+ 'SUCCESS')
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2)
+ self.assertEqual(self.getJobFromHistory('project-test1').node,
+ 'image1')
+
+ # TODOv3(jeblair): we may want to report stats by tenant (also?).
+ self.assertReportedStat('gerrit.event.comment-added', value='1|c')
+ self.assertReportedStat('zuul.pipeline.gate.current_changes',
+ value='1|g')
+ self.assertReportedStat('zuul.pipeline.gate.job.project-merge.SUCCESS',
+ kind='ms')
+ self.assertReportedStat('zuul.pipeline.gate.job.project-merge.SUCCESS',
+ value='1|c')
+ self.assertReportedStat('zuul.pipeline.gate.resident_time', kind='ms')
+ self.assertReportedStat('zuul.pipeline.gate.total_changes',
+ value='1|c')
+ self.assertReportedStat(
+ 'zuul.pipeline.gate.org.project.resident_time', kind='ms')
+ self.assertReportedStat(
+ 'zuul.pipeline.gate.org.project.total_changes', value='1|c')
+
+ for build in self.builds:
+ self.assertEqual(build.parameters['ZUUL_VOTING'], '1')
+
+ def test_initial_pipeline_gauges(self):
+ "Test that each pipeline reported its length on start"
+ self.assertReportedStat('zuul.pipeline.gate.current_changes',
+ value='0|g')
+ self.assertReportedStat('zuul.pipeline.check.current_changes',
+ value='0|g')
+
+ def test_job_branch(self):
+ "Test the correct variant of a job runs on a branch"
+ self.create_branch('org/project', 'stable')
+ A = self.fake_gerrit.addFakeChange('org/project', 'stable', 'A')
+ A.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+ self.assertEqual(self.getJobFromHistory('project-test1').result,
+ 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('project-test2').result,
+ 'SUCCESS')
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2,
+ "A should report start and success")
+ self.assertIn('gate', A.messages[1],
+ "A should transit gate")
+ self.assertEqual(self.getJobFromHistory('project-test1').node,
+ 'image2')
+
+ def test_parallel_changes(self):
+ "Test that changes are tested in parallel and merged in series"
+
+ self.launch_server.hold_jobs_in_build = True
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+ C = self.fake_gerrit.addFakeChange('org/project', 'master', 'C')
+ A.addApproval('code-review', 2)
+ B.addApproval('code-review', 2)
+ C.addApproval('code-review', 2)
+
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(B.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(C.addApproval('approved', 1))
+
+ self.waitUntilSettled()
+ self.assertEqual(len(self.builds), 1)
+ self.assertEqual(self.builds[0].name, 'project-merge')
+ self.assertTrue(self.builds[0].hasChanges(A))
+
+ self.launch_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.launch_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, B))
+ 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.launch_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))
+
+ self.launch_server.hold_jobs_in_build = False
+ self.launch_server.release()
+ self.waitUntilSettled()
+ self.assertEqual(len(self.builds), 0)
+
+ self.assertEqual(len(self.history), 9)
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(B.data['status'], 'MERGED')
+ self.assertEqual(C.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2)
+ self.assertEqual(B.reported, 2)
+ self.assertEqual(C.reported, 2)
+
+ def test_failed_changes(self):
+ "Test that a change behind a failed change is retested"
+ self.launch_server.hold_jobs_in_build = True
+
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+ A.addApproval('code-review', 2)
+ B.addApproval('code-review', 2)
+
+ self.launch_server.failJob('project-test1', A)
+
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(B.addApproval('approved', 1))
+ self.waitUntilSettled()
+ self.assertBuilds([dict(name='project-merge', changes='1,1')])
+
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ # A/project-merge is complete
+ self.assertBuilds([
+ dict(name='project-test1', changes='1,1'),
+ dict(name='project-test2', changes='1,1'),
+ dict(name='project-merge', changes='1,1 2,1'),
+ ])
+
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ # A/project-merge is complete
+ # B/project-merge is complete
+ self.assertBuilds([
+ dict(name='project-test1', changes='1,1'),
+ dict(name='project-test2', changes='1,1'),
+ dict(name='project-test1', changes='1,1 2,1'),
+ dict(name='project-test2', changes='1,1 2,1'),
+ ])
+
+ # Release project-test1 for A which will fail. This will
+ # abort both running B jobs and relaunch project-merge for B.
+ self.builds[0].release()
+ self.waitUntilSettled()
+
+ self.orderedRelease()
+ self.assertHistory([
+ dict(name='project-merge', result='SUCCESS', changes='1,1'),
+ dict(name='project-merge', result='SUCCESS', changes='1,1 2,1'),
+ dict(name='project-test1', result='FAILURE', changes='1,1'),
+ dict(name='project-test1', result='ABORTED', changes='1,1 2,1'),
+ dict(name='project-test2', result='ABORTED', changes='1,1 2,1'),
+ dict(name='project-test2', result='SUCCESS', changes='1,1'),
+ dict(name='project-merge', result='SUCCESS', changes='2,1'),
+ dict(name='project-test1', result='SUCCESS', changes='2,1'),
+ dict(name='project-test2', result='SUCCESS', changes='2,1'),
+ ], ordered=False)
+
+ self.assertEqual(A.data['status'], 'NEW')
+ self.assertEqual(B.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2)
+ self.assertEqual(B.reported, 2)
+
+ def test_independent_queues(self):
+ "Test that changes end up in the right queues"
+
+ self.launch_server.hold_jobs_in_build = True
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project1', 'master', 'B')
+ C = self.fake_gerrit.addFakeChange('org/project2', 'master', 'C')
+ A.addApproval('code-review', 2)
+ B.addApproval('code-review', 2)
+ C.addApproval('code-review', 2)
+
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(B.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(C.addApproval('approved', 1))
+
+ self.waitUntilSettled()
+
+ # There should be one merge job at the head of each queue running
+ self.assertBuilds([
+ dict(name='project-merge', changes='1,1'),
+ dict(name='project-merge', changes='2,1'),
+ ])
+
+ # Release the current merge builds
+ self.builds[0].release()
+ self.waitUntilSettled()
+ self.builds[0].release()
+ self.waitUntilSettled()
+ # Release the merge job for project2 which is behind project1
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+
+ # All the test builds should be running:
+ self.assertBuilds([
+ dict(name='project-test1', changes='1,1'),
+ dict(name='project-test2', changes='1,1'),
+ dict(name='project-test1', changes='2,1'),
+ dict(name='project-test2', changes='2,1'),
+ dict(name='project1-project2-integration', changes='2,1'),
+ dict(name='project-test1', changes='2,1 3,1'),
+ dict(name='project-test2', changes='2,1 3,1'),
+ ])
+
+ self.orderedRelease()
+ self.assertHistory([
+ dict(name='project-merge', result='SUCCESS', changes='1,1'),
+ dict(name='project-merge', result='SUCCESS', changes='2,1'),
+ dict(name='project-merge', result='SUCCESS', changes='2,1 3,1'),
+ dict(name='project-test1', result='SUCCESS', changes='1,1'),
+ dict(name='project-test2', result='SUCCESS', changes='1,1'),
+ dict(name='project-test1', result='SUCCESS', changes='2,1'),
+ dict(name='project-test2', result='SUCCESS', changes='2,1'),
+ dict(
+ name='project1-project2-integration',
+ result='SUCCESS',
+ changes='2,1'),
+ dict(name='project-test1', result='SUCCESS', changes='2,1 3,1'),
+ dict(name='project-test2', result='SUCCESS', changes='2,1 3,1'),
+ ])
+
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(B.data['status'], 'MERGED')
+ self.assertEqual(C.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2)
+ self.assertEqual(B.reported, 2)
+ self.assertEqual(C.reported, 2)
+
+ def test_failed_change_at_head(self):
+ "Test that if a change at the head fails, jobs behind it are canceled"
+
+ self.launch_server.hold_jobs_in_build = True
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+ C = self.fake_gerrit.addFakeChange('org/project', 'master', 'C')
+ A.addApproval('code-review', 2)
+ B.addApproval('code-review', 2)
+ C.addApproval('code-review', 2)
+
+ self.launch_server.failJob('project-test1', A)
+
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(B.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(C.addApproval('approved', 1))
+
+ self.waitUntilSettled()
+
+ self.assertBuilds([
+ dict(name='project-merge', changes='1,1'),
+ ])
+
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+
+ self.assertBuilds([
+ dict(name='project-test1', changes='1,1'),
+ dict(name='project-test2', changes='1,1'),
+ dict(name='project-test1', changes='1,1 2,1'),
+ dict(name='project-test2', changes='1,1 2,1'),
+ dict(name='project-test1', changes='1,1 2,1 3,1'),
+ dict(name='project-test2', changes='1,1 2,1 3,1'),
+ ])
+
+ self.release(self.builds[0])
+ self.waitUntilSettled()
+
+ # project-test2, project-merge for B
+ self.assertBuilds([
+ dict(name='project-test2', changes='1,1'),
+ dict(name='project-merge', changes='2,1'),
+ ])
+ # Unordered history comparison because the aborts can finish
+ # in any order.
+ self.assertHistory([
+ dict(name='project-merge', result='SUCCESS',
+ changes='1,1'),
+ dict(name='project-merge', result='SUCCESS',
+ changes='1,1 2,1'),
+ dict(name='project-merge', result='SUCCESS',
+ changes='1,1 2,1 3,1'),
+ dict(name='project-test1', result='FAILURE',
+ changes='1,1'),
+ dict(name='project-test1', result='ABORTED',
+ changes='1,1 2,1'),
+ dict(name='project-test2', result='ABORTED',
+ changes='1,1 2,1'),
+ dict(name='project-test1', result='ABORTED',
+ changes='1,1 2,1 3,1'),
+ dict(name='project-test2', result='ABORTED',
+ changes='1,1 2,1 3,1'),
+ ], ordered=False)
+
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.orderedRelease()
+
+ self.assertBuilds([])
+ self.assertHistory([
+ dict(name='project-merge', result='SUCCESS',
+ changes='1,1'),
+ dict(name='project-merge', result='SUCCESS',
+ changes='1,1 2,1'),
+ dict(name='project-merge', result='SUCCESS',
+ changes='1,1 2,1 3,1'),
+ dict(name='project-test1', result='FAILURE',
+ changes='1,1'),
+ dict(name='project-test1', result='ABORTED',
+ changes='1,1 2,1'),
+ dict(name='project-test2', result='ABORTED',
+ changes='1,1 2,1'),
+ dict(name='project-test1', result='ABORTED',
+ changes='1,1 2,1 3,1'),
+ dict(name='project-test2', result='ABORTED',
+ changes='1,1 2,1 3,1'),
+ dict(name='project-merge', result='SUCCESS',
+ changes='2,1'),
+ dict(name='project-merge', result='SUCCESS',
+ changes='2,1 3,1'),
+ dict(name='project-test2', result='SUCCESS',
+ changes='1,1'),
+ dict(name='project-test1', result='SUCCESS',
+ changes='2,1'),
+ dict(name='project-test2', result='SUCCESS',
+ changes='2,1'),
+ dict(name='project-test1', result='SUCCESS',
+ changes='2,1 3,1'),
+ dict(name='project-test2', result='SUCCESS',
+ changes='2,1 3,1'),
+ ], ordered=False)
+
+ self.assertEqual(A.data['status'], 'NEW')
+ self.assertEqual(B.data['status'], 'MERGED')
+ self.assertEqual(C.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2)
+ self.assertEqual(B.reported, 2)
+ self.assertEqual(C.reported, 2)
+
+ def test_failed_change_in_middle(self):
+ "Test a failed change in the middle of the queue"
+
+ self.launch_server.hold_jobs_in_build = True
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+ C = self.fake_gerrit.addFakeChange('org/project', 'master', 'C')
+ A.addApproval('code-review', 2)
+ B.addApproval('code-review', 2)
+ C.addApproval('code-review', 2)
+
+ self.launch_server.failJob('project-test1', B)
+
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(B.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(C.addApproval('approved', 1))
+
+ self.waitUntilSettled()
+
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.launch_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[2])
+ self.waitUntilSettled()
+
+ # project-test1 and project-test2 for A
+ # project-test2 for B
+ # project-merge for C (without B)
+ self.assertEqual(len(self.builds), 4)
+ self.assertEqual(self.countJobResults(self.history, 'ABORTED'), 2)
+
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+
+ # project-test1 and project-test2 for A
+ # project-test2 for B
+ # project-test1 and project-test2 for C
+ self.assertEqual(len(self.builds), 5)
+
+ tenant = self.sched.abide.tenants.get('tenant-one')
+ items = tenant.layout.pipelines['gate'].getAllItems()
+ builds = items[0].current_build_set.getBuilds()
+ self.assertEqual(self.countJobResults(builds, 'SUCCESS'), 1)
+ self.assertEqual(self.countJobResults(builds, None), 2)
+ builds = items[1].current_build_set.getBuilds()
+ self.assertEqual(self.countJobResults(builds, 'SUCCESS'), 1)
+ self.assertEqual(self.countJobResults(builds, 'FAILURE'), 1)
+ self.assertEqual(self.countJobResults(builds, None), 1)
+ builds = items[2].current_build_set.getBuilds()
+ self.assertEqual(self.countJobResults(builds, 'SUCCESS'), 1)
+ self.assertEqual(self.countJobResults(builds, None), 2)
+
+ self.launch_server.hold_jobs_in_build = False
+ self.launch_server.release()
+ self.waitUntilSettled()
+
+ self.assertEqual(len(self.builds), 0)
+ self.assertEqual(len(self.history), 12)
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(B.data['status'], 'NEW')
+ self.assertEqual(C.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2)
+ self.assertEqual(B.reported, 2)
+ self.assertEqual(C.reported, 2)
+
+ def test_failed_change_at_head_with_queue(self):
+ "Test that if a change at the head fails, queued jobs are canceled"
+
+ self.gearman_server.hold_jobs_in_queue = True
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+ C = self.fake_gerrit.addFakeChange('org/project', 'master', 'C')
+ A.addApproval('code-review', 2)
+ B.addApproval('code-review', 2)
+ C.addApproval('code-review', 2)
+
+ self.launch_server.failJob('project-test1', A)
+
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(B.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(C.addApproval('approved', 1))
+
+ self.waitUntilSettled()
+ queue = self.gearman_server.getQueue()
+ self.assertEqual(len(self.builds), 0)
+ self.assertEqual(len(queue), 1)
+ self.assertEqual(queue[0].name, 'launcher:launch')
+ job_args = json.loads(queue[0].arguments)
+ self.assertEqual(job_args['job'], 'project-merge')
+ self.assertEqual(job_args['items'][0]['number'], '%d' % A.number)
+
+ self.gearman_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.gearman_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.gearman_server.release('.*-merge')
+ self.waitUntilSettled()
+ queue = self.gearman_server.getQueue()
+
+ self.assertEqual(len(self.builds), 0)
+ self.assertEqual(len(queue), 6)
+
+ self.assertEqual(
+ json.loads(queue[0].arguments)['job'], 'project-test1')
+ self.assertEqual(
+ json.loads(queue[1].arguments)['job'], 'project-test2')
+ self.assertEqual(
+ json.loads(queue[2].arguments)['job'], 'project-test1')
+ self.assertEqual(
+ json.loads(queue[3].arguments)['job'], 'project-test2')
+ self.assertEqual(
+ json.loads(queue[4].arguments)['job'], 'project-test1')
+ self.assertEqual(
+ json.loads(queue[5].arguments)['job'], 'project-test2')
+
+ self.release(queue[0])
+ self.waitUntilSettled()
+
+ self.assertEqual(len(self.builds), 0)
+ queue = self.gearman_server.getQueue()
+ self.assertEqual(len(queue), 2) # project-test2, project-merge for B
+ self.assertEqual(self.countJobResults(self.history, 'ABORTED'), 0)
+
+ self.gearman_server.hold_jobs_in_queue = False
+ self.gearman_server.release()
+ self.waitUntilSettled()
+
+ self.assertEqual(len(self.builds), 0)
+ self.assertEqual(len(self.history), 11)
+ self.assertEqual(A.data['status'], 'NEW')
+ self.assertEqual(B.data['status'], 'MERGED')
+ self.assertEqual(C.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2)
+ self.assertEqual(B.reported, 2)
+ self.assertEqual(C.reported, 2)
+
+ @skip("Disabled for early v3 development")
+ def _test_time_database(self, iteration):
+ self.launch_server.hold_jobs_in_build = True
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ A.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+ time.sleep(2)
+
+ data = json.loads(self.sched.formatStatusJSON())
+ found_job = None
+ for pipeline in data['pipelines']:
+ if pipeline['name'] != 'gate':
+ continue
+ for queue in pipeline['change_queues']:
+ for head in queue['heads']:
+ for item in head:
+ for job in item['jobs']:
+ if job['name'] == 'project-merge':
+ found_job = job
+ break
+
+ self.assertIsNotNone(found_job)
+ if iteration == 1:
+ self.assertIsNotNone(found_job['estimated_time'])
+ self.assertIsNone(found_job['remaining_time'])
+ else:
+ self.assertIsNotNone(found_job['estimated_time'])
+ self.assertTrue(found_job['estimated_time'] >= 2)
+ self.assertIsNotNone(found_job['remaining_time'])
+
+ self.launch_server.hold_jobs_in_build = False
+ self.launch_server.release()
+ self.waitUntilSettled()
+
+ @skip("Disabled for early v3 development")
+ def test_time_database(self):
+ "Test the time database"
+
+ self._test_time_database(1)
+ self._test_time_database(2)
+
+ def test_two_failed_changes_at_head(self):
+ "Test that changes are reparented correctly if 2 fail at head"
+
+ self.launch_server.hold_jobs_in_build = True
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+ C = self.fake_gerrit.addFakeChange('org/project', 'master', 'C')
+ A.addApproval('code-review', 2)
+ B.addApproval('code-review', 2)
+ C.addApproval('code-review', 2)
+
+ self.launch_server.failJob('project-test1', A)
+ self.launch_server.failJob('project-test1', B)
+
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(B.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(C.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.launch_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.assertTrue(self.builds[0].hasChanges(A))
+ self.assertTrue(self.builds[2].hasChanges(A))
+ self.assertTrue(self.builds[2].hasChanges(B))
+ self.assertTrue(self.builds[4].hasChanges(A))
+ self.assertTrue(self.builds[4].hasChanges(B))
+ self.assertTrue(self.builds[4].hasChanges(C))
+
+ # Fail change B first
+ self.release(self.builds[2])
+ self.waitUntilSettled()
+
+ # restart of C after B failure
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+
+ self.assertEqual(len(self.builds), 5)
+ self.assertEqual(self.builds[0].name, 'project-test1')
+ self.assertEqual(self.builds[1].name, 'project-test2')
+ self.assertEqual(self.builds[2].name, 'project-test2')
+ self.assertEqual(self.builds[3].name, 'project-test1')
+ self.assertEqual(self.builds[4].name, 'project-test2')
+
+ self.assertTrue(self.builds[1].hasChanges(A))
+ self.assertTrue(self.builds[2].hasChanges(A))
+ self.assertTrue(self.builds[2].hasChanges(B))
+ self.assertTrue(self.builds[4].hasChanges(A))
+ self.assertFalse(self.builds[4].hasChanges(B))
+ self.assertTrue(self.builds[4].hasChanges(C))
+
+ # Finish running all passing jobs for change A
+ self.release(self.builds[1])
+ self.waitUntilSettled()
+ # Fail and report change A
+ self.release(self.builds[0])
+ self.waitUntilSettled()
+
+ # restart of B,C after A failure
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+
+ self.assertEqual(len(self.builds), 4)
+ self.assertEqual(self.builds[0].name, 'project-test1') # B
+ self.assertEqual(self.builds[1].name, 'project-test2') # B
+ self.assertEqual(self.builds[2].name, 'project-test1') # C
+ self.assertEqual(self.builds[3].name, 'project-test2') # C
+
+ self.assertFalse(self.builds[1].hasChanges(A))
+ self.assertTrue(self.builds[1].hasChanges(B))
+ self.assertFalse(self.builds[1].hasChanges(C))
+
+ self.assertFalse(self.builds[2].hasChanges(A))
+ # After A failed and B and C restarted, B should be back in
+ # C's tests because it has not failed yet.
+ self.assertTrue(self.builds[2].hasChanges(B))
+ self.assertTrue(self.builds[2].hasChanges(C))
+
+ self.launch_server.hold_jobs_in_build = False
+ self.launch_server.release()
+ self.waitUntilSettled()
+
+ self.assertEqual(len(self.builds), 0)
+ self.assertEqual(len(self.history), 21)
+ self.assertEqual(A.data['status'], 'NEW')
+ self.assertEqual(B.data['status'], 'NEW')
+ self.assertEqual(C.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2)
+ self.assertEqual(B.reported, 2)
+ self.assertEqual(C.reported, 2)
+
+ def test_patch_order(self):
+ "Test that dependent patches are tested in the right order"
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+ C = self.fake_gerrit.addFakeChange('org/project', 'master', 'C')
+ A.addApproval('code-review', 2)
+ B.addApproval('code-review', 2)
+ C.addApproval('code-review', 2)
+
+ M2 = self.fake_gerrit.addFakeChange('org/project', 'master', 'M2')
+ M1 = self.fake_gerrit.addFakeChange('org/project', 'master', 'M1')
+ M2.setMerged()
+ M1.setMerged()
+
+ # C -> B -> A -> M1 -> M2
+ # M2 is here to make sure it is never queried. If it is, it
+ # means zuul is walking down the entire history of merged
+ # changes.
+
+ C.setDependsOn(B, 1)
+ B.setDependsOn(A, 1)
+ A.setDependsOn(M1, 1)
+ M1.setDependsOn(M2, 1)
+
+ self.fake_gerrit.addEvent(C.addApproval('approved', 1))
+
+ self.waitUntilSettled()
+
+ self.assertEqual(A.data['status'], 'NEW')
+ self.assertEqual(B.data['status'], 'NEW')
+ self.assertEqual(C.data['status'], 'NEW')
+
+ self.fake_gerrit.addEvent(B.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+
+ self.waitUntilSettled()
+ self.assertEqual(M2.queried, 0)
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(B.data['status'], 'MERGED')
+ self.assertEqual(C.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2)
+ self.assertEqual(B.reported, 2)
+ self.assertEqual(C.reported, 2)
+
+ def test_needed_changes_enqueue(self):
+ "Test that a needed change is enqueued ahead"
+ # A Given a git tree like this, if we enqueue
+ # / \ change C, we should walk up and down the tree
+ # B G and enqueue changes in the order ABCDEFG.
+ # /|\ This is also the order that you would get if
+ # *C E F you enqueued changes in the order ABCDEFG, so
+ # / the ordering is stable across re-enqueue events.
+ # D
+
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+ C = self.fake_gerrit.addFakeChange('org/project', 'master', 'C')
+ D = self.fake_gerrit.addFakeChange('org/project', 'master', 'D')
+ E = self.fake_gerrit.addFakeChange('org/project', 'master', 'E')
+ F = self.fake_gerrit.addFakeChange('org/project', 'master', 'F')
+ G = self.fake_gerrit.addFakeChange('org/project', 'master', 'G')
+ B.setDependsOn(A, 1)
+ C.setDependsOn(B, 1)
+ D.setDependsOn(C, 1)
+ E.setDependsOn(B, 1)
+ F.setDependsOn(B, 1)
+ G.setDependsOn(A, 1)
+
+ A.addApproval('code-review', 2)
+ B.addApproval('code-review', 2)
+ C.addApproval('code-review', 2)
+ D.addApproval('code-review', 2)
+ E.addApproval('code-review', 2)
+ F.addApproval('code-review', 2)
+ G.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(C.addApproval('approved', 1))
+
+ self.waitUntilSettled()
+
+ self.assertEqual(A.data['status'], 'NEW')
+ self.assertEqual(B.data['status'], 'NEW')
+ self.assertEqual(C.data['status'], 'NEW')
+ self.assertEqual(D.data['status'], 'NEW')
+ self.assertEqual(E.data['status'], 'NEW')
+ self.assertEqual(F.data['status'], 'NEW')
+ self.assertEqual(G.data['status'], 'NEW')
+
+ # 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.connections.connections.values():
+ connection.maintainCache([])
+
+ self.launch_server.hold_jobs_in_build = True
+ A.addApproval('approved', 1)
+ B.addApproval('approved', 1)
+ D.addApproval('approved', 1)
+ E.addApproval('approved', 1)
+ F.addApproval('approved', 1)
+ G.addApproval('approved', 1)
+ self.fake_gerrit.addEvent(C.addApproval('approved', 1))
+
+ for x in range(8):
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.launch_server.hold_jobs_in_build = False
+ self.launch_server.release()
+ self.waitUntilSettled()
+
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(B.data['status'], 'MERGED')
+ self.assertEqual(C.data['status'], 'MERGED')
+ self.assertEqual(D.data['status'], 'MERGED')
+ self.assertEqual(E.data['status'], 'MERGED')
+ self.assertEqual(F.data['status'], 'MERGED')
+ self.assertEqual(G.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2)
+ self.assertEqual(B.reported, 2)
+ self.assertEqual(C.reported, 2)
+ self.assertEqual(D.reported, 2)
+ self.assertEqual(E.reported, 2)
+ self.assertEqual(F.reported, 2)
+ self.assertEqual(G.reported, 2)
+ self.assertEqual(self.history[6].changes,
+ '1,1 2,1 3,1 4,1 5,1 6,1 7,1')
+
+ def test_source_cache(self):
+ "Test that the source cache operates correctly"
+ self.launch_server.hold_jobs_in_build = True
+
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+ X = self.fake_gerrit.addFakeChange('org/project', 'master', 'X')
+ A.addApproval('code-review', 2)
+ B.addApproval('code-review', 2)
+
+ M1 = self.fake_gerrit.addFakeChange('org/project', 'master', 'M1')
+ M1.setMerged()
+
+ B.setDependsOn(A, 1)
+ A.setDependsOn(M1, 1)
+
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(X.getPatchsetCreatedEvent(1))
+
+ self.waitUntilSettled()
+
+ for build in self.builds:
+ if build.parameters['ZUUL_PIPELINE'] == 'check':
+ build.release()
+ self.waitUntilSettled()
+ for build in self.builds:
+ if build.parameters['ZUUL_PIPELINE'] == 'check':
+ build.release()
+ self.waitUntilSettled()
+
+ self.fake_gerrit.addEvent(B.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ self.log.debug("len %s" % self.fake_gerrit._change_cache.keys())
+ # there should still be changes in the cache
+ self.assertNotEqual(len(self.fake_gerrit._change_cache.keys()), 0)
+
+ self.launch_server.hold_jobs_in_build = False
+ self.launch_server.release()
+ self.waitUntilSettled()
+
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(B.data['status'], 'MERGED')
+ self.assertEqual(A.queried, 2) # Initial and isMerged
+ self.assertEqual(B.queried, 3) # Initial A, refresh from B, isMerged
+
+ def test_can_merge(self):
+ "Test whether a change is ready to merge"
+ # TODO: move to test_gerrit (this is a unit test!)
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ tenant = self.sched.abide.tenants.get('tenant-one')
+ source = tenant.layout.pipelines['gate'].source
+
+ # TODO(pabelanger): As we add more source / trigger APIs we should make
+ # it easier for users to create events for testing.
+ event = zuul.model.TriggerEvent()
+ event.trigger_name = 'gerrit'
+ event.change_number = '1'
+ event.patch_number = '2'
+
+ a = source.getChange(event)
+ mgr = tenant.layout.pipelines['gate'].manager
+ self.assertFalse(source.canMerge(a, mgr.getSubmitAllowNeeds()))
+
+ A.addApproval('code-review', 2)
+ a = source.getChange(event, refresh=True)
+ self.assertFalse(source.canMerge(a, mgr.getSubmitAllowNeeds()))
+
+ A.addApproval('approved', 1)
+ a = source.getChange(event, refresh=True)
+ self.assertTrue(source.canMerge(a, mgr.getSubmitAllowNeeds()))
+
+ @skip("Disabled for early v3 development")
+ def test_build_configuration_conflict(self):
+ "Test that merge conflicts are handled"
+
+ self.gearman_server.hold_jobs_in_queue = True
+ A = self.fake_gerrit.addFakeChange('org/conflict-project',
+ 'master', 'A')
+ A.addPatchset(['conflict'])
+ B = self.fake_gerrit.addFakeChange('org/conflict-project',
+ 'master', 'B')
+ B.addPatchset(['conflict'])
+ C = self.fake_gerrit.addFakeChange('org/conflict-project',
+ 'master', 'C')
+ A.addApproval('code-review', 2)
+ B.addApproval('code-review', 2)
+ C.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(B.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(C.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ self.assertEqual(A.reported, 1)
+ self.assertEqual(B.reported, 1)
+ self.assertEqual(C.reported, 1)
+
+ self.gearman_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.gearman_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.gearman_server.release('.*-merge')
+ self.waitUntilSettled()
+
+ self.assertEqual(len(self.history), 2) # A and C merge jobs
+
+ self.gearman_server.hold_jobs_in_queue = False
+ self.gearman_server.release()
+ self.waitUntilSettled()
+
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(B.data['status'], 'NEW')
+ self.assertEqual(C.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2)
+ self.assertEqual(B.reported, 2)
+ self.assertEqual(C.reported, 2)
+ self.assertEqual(len(self.history), 6)
+
+ def test_post(self):
+ "Test that post jobs run"
+
+ e = {
+ "type": "ref-updated",
+ "submitter": {
+ "name": "User Name",
+ },
+ "refUpdate": {
+ "oldRev": "90f173846e3af9154517b88543ffbd1691f31366",
+ "newRev": "d479a0bfcb34da57a31adb2a595c0cf687812543",
+ "refName": "master",
+ "project": "org/project",
+ }
+ }
+ self.fake_gerrit.addEvent(e)
+ self.waitUntilSettled()
+
+ job_names = [x.name for x in self.history]
+ self.assertEqual(len(self.history), 1)
+ self.assertIn('project-post', job_names)
+
+ def test_post_ignore_deletes(self):
+ "Test that deleting refs does not trigger post jobs"
+
+ e = {
+ "type": "ref-updated",
+ "submitter": {
+ "name": "User Name",
+ },
+ "refUpdate": {
+ "oldRev": "90f173846e3af9154517b88543ffbd1691f31366",
+ "newRev": "0000000000000000000000000000000000000000",
+ "refName": "master",
+ "project": "org/project",
+ }
+ }
+ self.fake_gerrit.addEvent(e)
+ self.waitUntilSettled()
+
+ job_names = [x.name for x in self.history]
+ self.assertEqual(len(self.history), 0)
+ self.assertNotIn('project-post', job_names)
+
+ def test_post_ignore_deletes_negative(self):
+ "Test that deleting refs does trigger post jobs"
+
+ self.updateConfigLayout('layout-dont-ignore-ref-deletes')
+ self.sched.reconfigure(self.config)
+
+ e = {
+ "type": "ref-updated",
+ "submitter": {
+ "name": "User Name",
+ },
+ "refUpdate": {
+ "oldRev": "90f173846e3af9154517b88543ffbd1691f31366",
+ "newRev": "0000000000000000000000000000000000000000",
+ "refName": "master",
+ "project": "org/project",
+ }
+ }
+ self.fake_gerrit.addEvent(e)
+ self.waitUntilSettled()
+
+ job_names = [x.name for x in self.history]
+ self.assertEqual(len(self.history), 1)
+ self.assertIn('project-post', job_names)
+
+ @skip("Disabled for early v3 development")
+ def test_build_configuration_branch(self):
+ "Test that the right commits are on alternate branches"
+
+ self.gearman_server.hold_jobs_in_queue = True
+ A = self.fake_gerrit.addFakeChange('org/project', 'mp', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project', 'mp', 'B')
+ C = self.fake_gerrit.addFakeChange('org/project', 'mp', 'C')
+ A.addApproval('code-review', 2)
+ B.addApproval('code-review', 2)
+ C.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(B.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(C.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ self.gearman_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.gearman_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.gearman_server.release('.*-merge')
+ self.waitUntilSettled()
+ queue = self.gearman_server.getQueue()
+ ref = self.getParameter(queue[-1], 'ZUUL_REF')
+ self.gearman_server.hold_jobs_in_queue = False
+ self.gearman_server.release()
+ self.waitUntilSettled()
+
+ path = os.path.join(self.git_root, "org/project")
+ repo = git.Repo(path)
+ repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
+ repo_messages.reverse()
+ correct_messages = ['initial commit', 'mp commit', 'A-1', 'B-1', 'C-1']
+ self.assertEqual(repo_messages, correct_messages)
+
+ @skip("Disabled for early v3 development")
+ def test_build_configuration_branch_interaction(self):
+ "Test that switching between branches works"
+ self.test_build_configuration()
+ self.test_build_configuration_branch()
+ # C has been merged, undo that
+ path = os.path.join(self.upstream_root, "org/project")
+ repo = git.Repo(path)
+ repo.heads.master.commit = repo.commit('init')
+ self.test_build_configuration()
+
+ @skip("Disabled for early v3 development")
+ def test_build_configuration_multi_branch(self):
+ "Test that dependent changes on multiple branches are merged"
+
+ self.gearman_server.hold_jobs_in_queue = True
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project', 'mp', 'B')
+ C = self.fake_gerrit.addFakeChange('org/project', 'master', 'C')
+ A.addApproval('code-review', 2)
+ B.addApproval('code-review', 2)
+ C.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(B.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(C.addApproval('approved', 1))
+ self.waitUntilSettled()
+ queue = self.gearman_server.getQueue()
+ job_A = None
+ for job in queue:
+ if 'project-merge' in job.name:
+ job_A = job
+ ref_A = self.getParameter(job_A, 'ZUUL_REF')
+ commit_A = self.getParameter(job_A, 'ZUUL_COMMIT')
+ self.log.debug("Got Zuul ref for change A: %s" % ref_A)
+ self.log.debug("Got Zuul commit for change A: %s" % commit_A)
+
+ self.gearman_server.release('.*-merge')
+ self.waitUntilSettled()
+ queue = self.gearman_server.getQueue()
+ job_B = None
+ for job in queue:
+ if 'project-merge' in job.name:
+ job_B = job
+ ref_B = self.getParameter(job_B, 'ZUUL_REF')
+ commit_B = self.getParameter(job_B, 'ZUUL_COMMIT')
+ self.log.debug("Got Zuul ref for change B: %s" % ref_B)
+ self.log.debug("Got Zuul commit for change B: %s" % commit_B)
+
+ self.gearman_server.release('.*-merge')
+ self.waitUntilSettled()
+ queue = self.gearman_server.getQueue()
+ for job in queue:
+ if 'project-merge' in job.name:
+ job_C = job
+ ref_C = self.getParameter(job_C, 'ZUUL_REF')
+ commit_C = self.getParameter(job_C, 'ZUUL_COMMIT')
+ self.log.debug("Got Zuul ref for change C: %s" % ref_C)
+ self.log.debug("Got Zuul commit for change C: %s" % commit_C)
+ self.gearman_server.hold_jobs_in_queue = False
+ self.gearman_server.release()
+ self.waitUntilSettled()
+
+ path = os.path.join(self.git_root, "org/project")
+ repo = git.Repo(path)
+
+ repo_messages = [c.message.strip()
+ for c in repo.iter_commits(ref_C)]
+ repo_shas = [c.hexsha for c in repo.iter_commits(ref_C)]
+ repo_messages.reverse()
+ correct_messages = ['initial commit', 'A-1', 'C-1']
+ # Ensure the right commits are in the history for this ref
+ self.assertEqual(repo_messages, correct_messages)
+ # Ensure ZUUL_REF -> ZUUL_COMMIT
+ self.assertEqual(repo_shas[0], commit_C)
+
+ repo_messages = [c.message.strip()
+ for c in repo.iter_commits(ref_B)]
+ repo_shas = [c.hexsha for c in repo.iter_commits(ref_B)]
+ repo_messages.reverse()
+ correct_messages = ['initial commit', 'mp commit', 'B-1']
+ self.assertEqual(repo_messages, correct_messages)
+ self.assertEqual(repo_shas[0], commit_B)
+
+ repo_messages = [c.message.strip()
+ for c in repo.iter_commits(ref_A)]
+ repo_shas = [c.hexsha for c in repo.iter_commits(ref_A)]
+ repo_messages.reverse()
+ correct_messages = ['initial commit', 'A-1']
+ self.assertEqual(repo_messages, correct_messages)
+ self.assertEqual(repo_shas[0], commit_A)
+
+ self.assertNotEqual(ref_A, ref_B, ref_C)
+ self.assertNotEqual(commit_A, commit_B, commit_C)
+
+ def test_dependent_changes_dequeue(self):
+ "Test that dependent patches are not needlessly tested"
+
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+ C = self.fake_gerrit.addFakeChange('org/project', 'master', 'C')
+ A.addApproval('code-review', 2)
+ B.addApproval('code-review', 2)
+ C.addApproval('code-review', 2)
+
+ M1 = self.fake_gerrit.addFakeChange('org/project', 'master', 'M1')
+ M1.setMerged()
+
+ # C -> B -> A -> M1
+
+ C.setDependsOn(B, 1)
+ B.setDependsOn(A, 1)
+ A.setDependsOn(M1, 1)
+
+ self.launch_server.failJob('project-merge', A)
+
+ self.fake_gerrit.addEvent(C.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(B.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+
+ self.waitUntilSettled()
+
+ self.assertEqual(A.data['status'], 'NEW')
+ self.assertEqual(A.reported, 2)
+ self.assertEqual(B.data['status'], 'NEW')
+ self.assertEqual(B.reported, 2)
+ self.assertEqual(C.data['status'], 'NEW')
+ self.assertEqual(C.reported, 2)
+ self.assertEqual(len(self.history), 1)
+
+ def test_failing_dependent_changes(self):
+ "Test that failing dependent patches are taken out of stream"
+ self.launch_server.hold_jobs_in_build = True
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+ C = self.fake_gerrit.addFakeChange('org/project', 'master', 'C')
+ D = self.fake_gerrit.addFakeChange('org/project', 'master', 'D')
+ E = self.fake_gerrit.addFakeChange('org/project', 'master', 'E')
+ A.addApproval('code-review', 2)
+ B.addApproval('code-review', 2)
+ C.addApproval('code-review', 2)
+ D.addApproval('code-review', 2)
+ E.addApproval('code-review', 2)
+
+ # E, D -> C -> B, A
+
+ D.setDependsOn(C, 1)
+ C.setDependsOn(B, 1)
+
+ self.launch_server.failJob('project-test1', B)
+
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(D.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(C.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(B.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(E.addApproval('approved', 1))
+
+ self.waitUntilSettled()
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+
+ self.launch_server.hold_jobs_in_build = False
+ for build in self.builds:
+ if build.parameters['ZUUL_CHANGE'] != '1':
+ build.release()
+ self.waitUntilSettled()
+
+ self.launch_server.release()
+ self.waitUntilSettled()
+
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2)
+ self.assertIn('Build succeeded', A.messages[1])
+ self.assertEqual(B.data['status'], 'NEW')
+ self.assertEqual(B.reported, 2)
+ self.assertIn('Build failed', B.messages[1])
+ self.assertEqual(C.data['status'], 'NEW')
+ self.assertEqual(C.reported, 2)
+ self.assertIn('depends on a change', C.messages[1])
+ self.assertEqual(D.data['status'], 'NEW')
+ self.assertEqual(D.reported, 2)
+ self.assertIn('depends on a change', D.messages[1])
+ self.assertEqual(E.data['status'], 'MERGED')
+ self.assertEqual(E.reported, 2)
+ self.assertIn('Build succeeded', E.messages[1])
+ self.assertEqual(len(self.history), 18)
+
+ def test_head_is_dequeued_once(self):
+ "Test that if a change at the head fails it is dequeued only once"
+ # If it's dequeued more than once, we should see extra
+ # aborted jobs.
+
+ self.launch_server.hold_jobs_in_build = True
+ A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project1', 'master', 'B')
+ C = self.fake_gerrit.addFakeChange('org/project1', 'master', 'C')
+ A.addApproval('code-review', 2)
+ B.addApproval('code-review', 2)
+ C.addApproval('code-review', 2)
+
+ self.launch_server.failJob('project-test1', A)
+ self.launch_server.failJob('project-test2', A)
+ self.launch_server.failJob('project1-project2-integration', A)
+
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(B.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(C.addApproval('approved', 1))
+
+ self.waitUntilSettled()
+
+ self.assertEqual(len(self.builds), 1)
+ self.assertEqual(self.builds[0].name, 'project-merge')
+ self.assertTrue(self.builds[0].hasChanges(A))
+
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+
+ self.assertEqual(len(self.builds), 9)
+ self.assertEqual(self.builds[0].name, 'project-test1')
+ self.assertEqual(self.builds[1].name, 'project-test2')
+ self.assertEqual(self.builds[2].name, 'project1-project2-integration')
+ self.assertEqual(self.builds[3].name, 'project-test1')
+ self.assertEqual(self.builds[4].name, 'project-test2')
+ self.assertEqual(self.builds[5].name, 'project1-project2-integration')
+ self.assertEqual(self.builds[6].name, 'project-test1')
+ self.assertEqual(self.builds[7].name, 'project-test2')
+ self.assertEqual(self.builds[8].name, 'project1-project2-integration')
+
+ self.release(self.builds[0])
+ self.waitUntilSettled()
+
+ self.assertEqual(len(self.builds), 3) # test2,integration, merge for B
+ self.assertEqual(self.countJobResults(self.history, 'ABORTED'), 6)
+
+ self.launch_server.hold_jobs_in_build = False
+ self.launch_server.release()
+ self.waitUntilSettled()
+
+ self.assertEqual(len(self.builds), 0)
+ self.assertEqual(len(self.history), 20)
+
+ self.assertEqual(A.data['status'], 'NEW')
+ self.assertEqual(B.data['status'], 'MERGED')
+ self.assertEqual(C.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2)
+ self.assertEqual(B.reported, 2)
+ self.assertEqual(C.reported, 2)
+
+ def test_nonvoting_job(self):
+ "Test that non-voting jobs don't vote."
+
+ A = self.fake_gerrit.addFakeChange('org/nonvoting-project',
+ 'master', 'A')
+ A.addApproval('code-review', 2)
+ self.launch_server.failJob('nonvoting-project-test2', A)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+
+ self.waitUntilSettled()
+
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2)
+ self.assertEqual(
+ self.getJobFromHistory('nonvoting-project-merge').result,
+ 'SUCCESS')
+ self.assertEqual(
+ self.getJobFromHistory('nonvoting-project-test1').result,
+ 'SUCCESS')
+ self.assertEqual(
+ self.getJobFromHistory('nonvoting-project-test2').result,
+ 'FAILURE')
+
+ for build in self.builds:
+ self.assertEqual(build.parameters['ZUUL_VOTING'], '0')
+
+ def test_check_queue_success(self):
+ "Test successful check queue jobs."
+
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+
+ self.waitUntilSettled()
+
+ self.assertEqual(A.data['status'], 'NEW')
+ self.assertEqual(A.reported, 1)
+ self.assertEqual(self.getJobFromHistory('project-merge').result,
+ 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('project-test1').result,
+ 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('project-test2').result,
+ 'SUCCESS')
+
+ def test_check_queue_failure(self):
+ "Test failed check queue jobs."
+
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ self.launch_server.failJob('project-test2', A)
+ self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+
+ self.waitUntilSettled()
+
+ self.assertEqual(A.data['status'], 'NEW')
+ self.assertEqual(A.reported, 1)
+ self.assertEqual(self.getJobFromHistory('project-merge').result,
+ 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('project-test1').result,
+ 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('project-test2').result,
+ 'FAILURE')
+
+ @skip("Disabled for early v3 development")
+ def test_dependent_behind_dequeue(self):
+ "test that dependent changes behind dequeued changes work"
+ # This complicated test is a reproduction of a real life bug
+ self.sched.reconfigure(self.config)
+
+ self.launch_server.hold_jobs_in_build = True
+ A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project1', 'master', 'B')
+ C = self.fake_gerrit.addFakeChange('org/project2', 'master', 'C')
+ D = self.fake_gerrit.addFakeChange('org/project2', 'master', 'D')
+ E = self.fake_gerrit.addFakeChange('org/project2', 'master', 'E')
+ F = self.fake_gerrit.addFakeChange('org/project3', 'master', 'F')
+ D.setDependsOn(C, 1)
+ E.setDependsOn(D, 1)
+ A.addApproval('code-review', 2)
+ B.addApproval('code-review', 2)
+ C.addApproval('code-review', 2)
+ D.addApproval('code-review', 2)
+ E.addApproval('code-review', 2)
+ F.addApproval('code-review', 2)
+
+ A.fail_merge = True
+
+ # Change object re-use in the gerrit trigger is hidden if
+ # changes are added in quick succession; waiting makes it more
+ # like real life.
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+ self.fake_gerrit.addEvent(B.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+
+ self.fake_gerrit.addEvent(C.addApproval('approved', 1))
+ self.waitUntilSettled()
+ self.fake_gerrit.addEvent(D.addApproval('approved', 1))
+ self.waitUntilSettled()
+ self.fake_gerrit.addEvent(E.addApproval('approved', 1))
+ self.waitUntilSettled()
+ self.fake_gerrit.addEvent(F.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+
+ # all jobs running
+
+ # Grab pointers to the jobs we want to release before
+ # releasing any, because list indexes may change as
+ # the jobs complete.
+ a, b, c = self.builds[:3]
+ a.release()
+ b.release()
+ c.release()
+ self.waitUntilSettled()
+
+ self.launch_server.hold_jobs_in_build = False
+ self.launch_server.release()
+ self.waitUntilSettled()
+
+ self.assertEqual(A.data['status'], 'NEW')
+ self.assertEqual(B.data['status'], 'MERGED')
+ self.assertEqual(C.data['status'], 'MERGED')
+ self.assertEqual(D.data['status'], 'MERGED')
+ self.assertEqual(E.data['status'], 'MERGED')
+ self.assertEqual(F.data['status'], 'MERGED')
+
+ self.assertEqual(A.reported, 2)
+ self.assertEqual(B.reported, 2)
+ self.assertEqual(C.reported, 2)
+ self.assertEqual(D.reported, 2)
+ self.assertEqual(E.reported, 2)
+ self.assertEqual(F.reported, 2)
+
+ self.assertEqual(self.countJobResults(self.history, 'ABORTED'), 15)
+ self.assertEqual(len(self.history), 44)
+
+ def test_merger_repack(self):
+ "Test that the merger works after a repack"
+
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ A.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+ self.assertEqual(self.getJobFromHistory('project-merge').result,
+ 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('project-test1').result,
+ 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('project-test2').result,
+ 'SUCCESS')
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2)
+ self.assertEmptyQueues()
+ self.build_history = []
+
+ path = os.path.join(self.git_root, "org/project")
+ print(repack_repo(path))
+
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ A.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+ self.assertEqual(self.getJobFromHistory('project-merge').result,
+ 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('project-test1').result,
+ 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('project-test2').result,
+ 'SUCCESS')
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2)
+
+ def test_merger_repack_large_change(self):
+ "Test that the merger works with large changes after a repack"
+ # https://bugs.launchpad.net/zuul/+bug/1078946
+ # This test assumes the repo is already cloned; make sure it is
+ tenant = self.sched.abide.tenants.get('tenant-one')
+ url = self.fake_gerrit.getGitUrl(
+ tenant.layout.project_configs.get('org/project1'))
+ self.merge_server.merger.addProject('org/project1', url)
+ A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A')
+ A.addPatchset(large=True)
+ path = os.path.join(self.upstream_root, "org/project1")
+ print(repack_repo(path))
+ path = os.path.join(self.git_root, "org/project1")
+ print(repack_repo(path))
+
+ A.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+ self.assertEqual(self.getJobFromHistory('project-merge').result,
+ 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('project-test1').result,
+ 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('project-test2').result,
+ 'SUCCESS')
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2)
+
+ def test_new_patchset_dequeues_old(self):
+ "Test that a new patchset causes the old to be dequeued"
+ # D -> C (depends on B) -> B (depends on A) -> A -> M
+ self.launch_server.hold_jobs_in_build = True
+ M = self.fake_gerrit.addFakeChange('org/project', 'master', 'M')
+ M.setMerged()
+
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+ C = self.fake_gerrit.addFakeChange('org/project', 'master', 'C')
+ D = self.fake_gerrit.addFakeChange('org/project', 'master', 'D')
+ A.addApproval('code-review', 2)
+ B.addApproval('code-review', 2)
+ C.addApproval('code-review', 2)
+ D.addApproval('code-review', 2)
+
+ C.setDependsOn(B, 1)
+ B.setDependsOn(A, 1)
+ A.setDependsOn(M, 1)
+
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(B.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(C.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(D.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ B.addPatchset()
+ self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(2))
+ self.waitUntilSettled()
+
+ self.launch_server.hold_jobs_in_build = False
+ self.launch_server.release()
+ self.waitUntilSettled()
+
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2)
+ self.assertEqual(B.data['status'], 'NEW')
+ self.assertEqual(B.reported, 2)
+ self.assertEqual(C.data['status'], 'NEW')
+ self.assertEqual(C.reported, 2)
+ self.assertEqual(D.data['status'], 'MERGED')
+ self.assertEqual(D.reported, 2)
+ self.assertEqual(len(self.history), 9) # 3 each for A, B, D.
+
+ def test_new_patchset_check(self):
+ "Test a new patchset in check"
+
+ self.launch_server.hold_jobs_in_build = True
+
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+ tenant = self.sched.abide.tenants.get('tenant-one')
+ check_pipeline = tenant.layout.pipelines['check']
+
+ # Add two git-dependent changes
+ B.setDependsOn(A, 1)
+ self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+ self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+
+ # A live item, and a non-live/live pair
+ items = check_pipeline.getAllItems()
+ self.assertEqual(len(items), 3)
+
+ self.assertEqual(items[0].change.number, '1')
+ self.assertEqual(items[0].change.patchset, '1')
+ self.assertFalse(items[0].live)
+
+ self.assertEqual(items[1].change.number, '2')
+ self.assertEqual(items[1].change.patchset, '1')
+ self.assertTrue(items[1].live)
+
+ self.assertEqual(items[2].change.number, '1')
+ self.assertEqual(items[2].change.patchset, '1')
+ self.assertTrue(items[2].live)
+
+ # Add a new patchset to A
+ A.addPatchset()
+ self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(2))
+ self.waitUntilSettled()
+
+ # The live copy of A,1 should be gone, but the non-live and B
+ # should continue, and we should have a new A,2
+ items = check_pipeline.getAllItems()
+ self.assertEqual(len(items), 3)
+
+ self.assertEqual(items[0].change.number, '1')
+ self.assertEqual(items[0].change.patchset, '1')
+ self.assertFalse(items[0].live)
+
+ self.assertEqual(items[1].change.number, '2')
+ self.assertEqual(items[1].change.patchset, '1')
+ self.assertTrue(items[1].live)
+
+ self.assertEqual(items[2].change.number, '1')
+ self.assertEqual(items[2].change.patchset, '2')
+ self.assertTrue(items[2].live)
+
+ # Add a new patchset to B
+ B.addPatchset()
+ self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(2))
+ self.waitUntilSettled()
+
+ # The live copy of B,1 should be gone, and it's non-live copy of A,1
+ # but we should have a new B,2 (still based on A,1)
+ items = check_pipeline.getAllItems()
+ self.assertEqual(len(items), 3)
+
+ self.assertEqual(items[0].change.number, '1')
+ self.assertEqual(items[0].change.patchset, '2')
+ self.assertTrue(items[0].live)
+
+ self.assertEqual(items[1].change.number, '1')
+ self.assertEqual(items[1].change.patchset, '1')
+ self.assertFalse(items[1].live)
+
+ self.assertEqual(items[2].change.number, '2')
+ self.assertEqual(items[2].change.patchset, '2')
+ self.assertTrue(items[2].live)
+
+ self.builds[0].release()
+ self.waitUntilSettled()
+ self.builds[0].release()
+ self.waitUntilSettled()
+ self.launch_server.hold_jobs_in_build = False
+ self.launch_server.release()
+ self.waitUntilSettled()
+
+ self.assertEqual(A.reported, 1)
+ self.assertEqual(B.reported, 1)
+ self.assertEqual(self.history[0].result, 'ABORTED')
+ self.assertEqual(self.history[0].changes, '1,1')
+ self.assertEqual(self.history[1].result, 'ABORTED')
+ self.assertEqual(self.history[1].changes, '1,1 2,1')
+ self.assertEqual(self.history[2].result, 'SUCCESS')
+ self.assertEqual(self.history[2].changes, '1,2')
+ self.assertEqual(self.history[3].result, 'SUCCESS')
+ self.assertEqual(self.history[3].changes, '1,1 2,2')
+
+ def test_abandoned_gate(self):
+ "Test that an abandoned change is dequeued from gate"
+
+ self.launch_server.hold_jobs_in_build = True
+
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ A.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+ self.assertEqual(len(self.builds), 1, "One job being built (on hold)")
+ self.assertEqual(self.builds[0].name, 'project-merge')
+
+ self.fake_gerrit.addEvent(A.getChangeAbandonedEvent())
+ self.waitUntilSettled()
+
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+
+ self.assertBuilds([])
+ self.assertHistory([
+ dict(name='project-merge', result='ABORTED', changes='1,1')],
+ ordered=False)
+ self.assertEqual(A.reported, 1,
+ "Abandoned gate change should report only start")
+
+ def test_abandoned_check(self):
+ "Test that an abandoned change is dequeued from check"
+
+ self.launch_server.hold_jobs_in_build = True
+
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+ tenant = self.sched.abide.tenants.get('tenant-one')
+ check_pipeline = tenant.layout.pipelines['check']
+
+ # Add two git-dependent changes
+ B.setDependsOn(A, 1)
+ self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+ self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+ # A live item, and a non-live/live pair
+ items = check_pipeline.getAllItems()
+ self.assertEqual(len(items), 3)
+
+ self.assertEqual(items[0].change.number, '1')
+ self.assertFalse(items[0].live)
+
+ self.assertEqual(items[1].change.number, '2')
+ self.assertTrue(items[1].live)
+
+ self.assertEqual(items[2].change.number, '1')
+ self.assertTrue(items[2].live)
+
+ # Abandon A
+ self.fake_gerrit.addEvent(A.getChangeAbandonedEvent())
+ self.waitUntilSettled()
+
+ # The live copy of A should be gone, but the non-live and B
+ # should continue
+ items = check_pipeline.getAllItems()
+ self.assertEqual(len(items), 2)
+
+ self.assertEqual(items[0].change.number, '1')
+ self.assertFalse(items[0].live)
+
+ self.assertEqual(items[1].change.number, '2')
+ self.assertTrue(items[1].live)
+
+ self.launch_server.hold_jobs_in_build = False
+ self.launch_server.release()
+ self.waitUntilSettled()
+
+ self.assertEqual(len(self.history), 4)
+ self.assertEqual(self.history[0].result, 'ABORTED',
+ 'Build should have been aborted')
+ self.assertEqual(A.reported, 0, "Abandoned change should not report")
+ self.assertEqual(B.reported, 1, "Change should report")
+
+ @skip("Disabled for early v3 development")
+ def test_abandoned_not_timer(self):
+ "Test that an abandoned change does not cancel timer jobs"
+
+ self.launch_server.hold_jobs_in_build = True
+
+ # Start timer trigger - also org/project
+ self.updateConfigLayout(
+ 'tests/fixtures/layout-idle.yaml')
+ self.sched.reconfigure(self.config)
+ self.registerJobs()
+ # The pipeline triggers every second, so we should have seen
+ # several by now.
+ time.sleep(5)
+ self.waitUntilSettled()
+ # Stop queuing timer triggered jobs so that the assertions
+ # below don't race against more jobs being queued.
+ self.updateConfigLayout(
+ 'tests/fixtures/layout-no-timer.yaml')
+ self.sched.reconfigure(self.config)
+ self.registerJobs()
+ self.assertEqual(len(self.builds), 2, "Two timer jobs")
+
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+ self.assertEqual(len(self.builds), 3, "One change plus two timer jobs")
+
+ self.fake_gerrit.addEvent(A.getChangeAbandonedEvent())
+ self.waitUntilSettled()
+
+ self.assertEqual(len(self.builds), 2, "Two timer jobs remain")
+
+ self.launch_server.release()
+ self.waitUntilSettled()
+
+ def test_zuul_url_return(self):
+ "Test if ZUUL_URL is returning when zuul_url is set in zuul.conf"
+ self.assertTrue(self.sched.config.has_option('merger', 'zuul_url'))
+ self.launch_server.hold_jobs_in_build = True
+
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ A.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ self.assertEqual(len(self.builds), 1)
+ for build in self.builds:
+ self.assertTrue('ZUUL_URL' in build.parameters)
+
+ self.launch_server.hold_jobs_in_build = False
+ self.launch_server.release()
+ self.waitUntilSettled()
+
+ def test_new_patchset_dequeues_old_on_head(self):
+ "Test that a new patchset causes the old to be dequeued (at head)"
+ # D -> C (depends on B) -> B (depends on A) -> A -> M
+ self.launch_server.hold_jobs_in_build = True
+ M = self.fake_gerrit.addFakeChange('org/project', 'master', 'M')
+ M.setMerged()
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+ C = self.fake_gerrit.addFakeChange('org/project', 'master', 'C')
+ D = self.fake_gerrit.addFakeChange('org/project', 'master', 'D')
+ A.addApproval('code-review', 2)
+ B.addApproval('code-review', 2)
+ C.addApproval('code-review', 2)
+ D.addApproval('code-review', 2)
+
+ C.setDependsOn(B, 1)
+ B.setDependsOn(A, 1)
+ A.setDependsOn(M, 1)
+
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(B.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(C.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(D.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ A.addPatchset()
+ self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(2))
+ self.waitUntilSettled()
+
+ self.launch_server.hold_jobs_in_build = False
+ self.launch_server.release()
+ self.waitUntilSettled()
+
+ self.assertEqual(A.data['status'], 'NEW')
+ self.assertEqual(A.reported, 2)
+ self.assertEqual(B.data['status'], 'NEW')
+ self.assertEqual(B.reported, 2)
+ self.assertEqual(C.data['status'], 'NEW')
+ self.assertEqual(C.reported, 2)
+ self.assertEqual(D.data['status'], 'MERGED')
+ self.assertEqual(D.reported, 2)
+ self.assertEqual(len(self.history), 7)
+
+ def test_new_patchset_dequeues_old_without_dependents(self):
+ "Test that a new patchset causes only the old to be dequeued"
+ self.launch_server.hold_jobs_in_build = True
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+ C = self.fake_gerrit.addFakeChange('org/project', 'master', 'C')
+ A.addApproval('code-review', 2)
+ B.addApproval('code-review', 2)
+ C.addApproval('code-review', 2)
+
+ self.fake_gerrit.addEvent(C.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(B.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ B.addPatchset()
+ self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(2))
+ self.waitUntilSettled()
+
+ self.launch_server.hold_jobs_in_build = False
+ self.launch_server.release()
+ self.waitUntilSettled()
+
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2)
+ self.assertEqual(B.data['status'], 'NEW')
+ self.assertEqual(B.reported, 2)
+ self.assertEqual(C.data['status'], 'MERGED')
+ self.assertEqual(C.reported, 2)
+ self.assertEqual(len(self.history), 9)
+
+ def test_new_patchset_dequeues_old_independent_queue(self):
+ "Test that a new patchset causes the old to be dequeued (independent)"
+ self.launch_server.hold_jobs_in_build = True
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+ C = self.fake_gerrit.addFakeChange('org/project', 'master', 'C')
+ self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+ self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1))
+ self.fake_gerrit.addEvent(C.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+
+ B.addPatchset()
+ self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(2))
+ self.waitUntilSettled()
+
+ self.launch_server.hold_jobs_in_build = False
+ self.launch_server.release()
+ self.waitUntilSettled()
+
+ self.assertEqual(A.data['status'], 'NEW')
+ self.assertEqual(A.reported, 1)
+ self.assertEqual(B.data['status'], 'NEW')
+ self.assertEqual(B.reported, 1)
+ self.assertEqual(C.data['status'], 'NEW')
+ self.assertEqual(C.reported, 1)
+ self.assertEqual(len(self.history), 10)
+ self.assertEqual(self.countJobResults(self.history, 'ABORTED'), 1)
+
+ def test_noop_job(self):
+ "Test that the internal noop job works"
+ A = self.fake_gerrit.addFakeChange('org/noop-project', 'master', 'A')
+ A.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ self.assertEqual(len(self.gearman_server.getQueue()), 0)
+ self.assertTrue(self.sched._areAllBuildsComplete())
+ self.assertEqual(len(self.history), 0)
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2)
+
+ def test_no_job_project(self):
+ "Test that reports with no jobs don't get sent"
+ A = self.fake_gerrit.addFakeChange('org/no-jobs-project',
+ 'master', 'A')
+ self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+
+ # Change wasn't reported to
+ self.assertEqual(A.reported, False)
+
+ # Check queue is empty afterwards
+ tenant = self.sched.abide.tenants.get('tenant-one')
+ check_pipeline = tenant.layout.pipelines['check']
+ items = check_pipeline.getAllItems()
+ self.assertEqual(len(items), 0)
+
+ self.assertEqual(len(self.history), 0)
+
+ def test_zuul_refs(self):
+ "Test that zuul refs exist and have the right changes"
+ self.launch_server.hold_jobs_in_build = True
+ M1 = self.fake_gerrit.addFakeChange('org/project1', 'master', 'M1')
+ M1.setMerged()
+ M2 = self.fake_gerrit.addFakeChange('org/project2', 'master', 'M2')
+ M2.setMerged()
+
+ A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project1', 'master', 'B')
+ C = self.fake_gerrit.addFakeChange('org/project2', 'master', 'C')
+ D = self.fake_gerrit.addFakeChange('org/project2', 'master', 'D')
+ A.addApproval('code-review', 2)
+ B.addApproval('code-review', 2)
+ C.addApproval('code-review', 2)
+ D.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(B.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(C.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(D.addApproval('approved', 1))
+
+ self.waitUntilSettled()
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+
+ a_zref = b_zref = c_zref = d_zref = None
+ a_build = b_build = c_build = d_build = None
+ for x in self.builds:
+ if x.parameters['ZUUL_CHANGE'] == '3':
+ a_zref = x.parameters['ZUUL_REF']
+ a_build = x
+ elif x.parameters['ZUUL_CHANGE'] == '4':
+ b_zref = x.parameters['ZUUL_REF']
+ b_build = x
+ elif x.parameters['ZUUL_CHANGE'] == '5':
+ c_zref = x.parameters['ZUUL_REF']
+ c_build = x
+ elif x.parameters['ZUUL_CHANGE'] == '6':
+ d_zref = x.parameters['ZUUL_REF']
+ d_build = x
+ if a_build and b_build and c_build and d_build:
+ break
+
+ # There are... four... refs.
+ self.assertIsNotNone(a_zref)
+ self.assertIsNotNone(b_zref)
+ self.assertIsNotNone(c_zref)
+ self.assertIsNotNone(d_zref)
+
+ # And they should all be different
+ refs = set([a_zref, b_zref, c_zref, d_zref])
+ self.assertEqual(len(refs), 4)
+
+ # should have a, not b, and should not be in project2
+ self.assertTrue(a_build.hasChanges(A))
+ self.assertFalse(a_build.hasChanges(B, M2))
+
+ # should have a and b, and should not be in project2
+ self.assertTrue(b_build.hasChanges(A, B))
+ self.assertFalse(b_build.hasChanges(M2))
+
+ # should have a and b in 1, c in 2
+ self.assertTrue(c_build.hasChanges(A, B, C))
+ self.assertFalse(c_build.hasChanges(D))
+
+ # should have a and b in 1, c and d in 2
+ self.assertTrue(d_build.hasChanges(A, B, C, D))
+
+ self.launch_server.hold_jobs_in_build = False
+ self.launch_server.release()
+ self.waitUntilSettled()
+
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2)
+ self.assertEqual(B.data['status'], 'MERGED')
+ self.assertEqual(B.reported, 2)
+ self.assertEqual(C.data['status'], 'MERGED')
+ self.assertEqual(C.reported, 2)
+ self.assertEqual(D.data['status'], 'MERGED')
+ self.assertEqual(D.reported, 2)
+
+ def test_rerun_on_error(self):
+ "Test that if a worker fails to run a job, it is run again"
+ self.launch_server.hold_jobs_in_build = True
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ A.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ self.builds[0].run_error = True
+ self.launch_server.hold_jobs_in_build = False
+ self.launch_server.release()
+ self.waitUntilSettled()
+ self.assertEqual(self.countJobResults(self.history, 'RUN_ERROR'), 1)
+ self.assertEqual(self.countJobResults(self.history, 'SUCCESS'), 3)
+
+ def test_statsd(self):
+ "Test each of the statsd methods used in the scheduler"
+ import extras
+ statsd = extras.try_import('statsd.statsd')
+ statsd.incr('test-incr')
+ statsd.timing('test-timing', 3)
+ statsd.gauge('test-gauge', 12)
+ self.assertReportedStat('test-incr', '1|c')
+ self.assertReportedStat('test-timing', '3|ms')
+ self.assertReportedStat('test-gauge', '12|g')
+
+ @skip("Disabled for early v3 development")
+ def test_stuck_job_cleanup(self):
+ "Test that pending jobs are cleaned up if removed from layout"
+ # This job won't be registered at startup because it is not in
+ # the standard layout, but we need it to already be registerd
+ # for when we reconfigure, as that is when Zuul will attempt
+ # to run the new job.
+ self.worker.registerFunction('build:gate-noop')
+ self.gearman_server.hold_jobs_in_queue = True
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ A.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+ self.assertEqual(len(self.gearman_server.getQueue()), 1)
+
+ self.updateConfigLayout(
+ 'tests/fixtures/layout-no-jobs.yaml')
+ self.sched.reconfigure(self.config)
+ self.waitUntilSettled()
+
+ self.gearman_server.release('gate-noop')
+ self.waitUntilSettled()
+ self.assertEqual(len(self.gearman_server.getQueue()), 0)
+ self.assertTrue(self.sched._areAllBuildsComplete())
+
+ self.assertEqual(len(self.history), 1)
+ self.assertEqual(self.history[0].name, 'gate-noop')
+ self.assertEqual(self.history[0].result, 'SUCCESS')
+
+ def test_file_head(self):
+ # This is a regression test for an observed bug. A change
+ # with a file named "HEAD" in the root directory of the repo
+ # was processed by a merger. It then was unable to reset the
+ # repo because of:
+ # GitCommandError: 'git reset --hard HEAD' returned
+ # with exit code 128
+ # stderr: 'fatal: ambiguous argument 'HEAD': both revision
+ # and filename
+ # Use '--' to separate filenames from revisions'
+
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ A.addPatchset({'HEAD': ''})
+ B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+
+ self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(2))
+ self.waitUntilSettled()
+
+ self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+
+ self.assertIn('Build succeeded', A.messages[0])
+ self.assertIn('Build succeeded', B.messages[0])
+
+ @skip("Disabled for early v3 development")
+ def test_file_jobs(self):
+ "Test that file jobs run only when appropriate"
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ A.addPatchset(['pip-requires'])
+ B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+ A.addApproval('code-review', 2)
+ B.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(B.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ testfile_jobs = [x for x in self.history
+ if x.name == 'project-testfile']
+
+ self.assertEqual(len(testfile_jobs), 1)
+ self.assertEqual(testfile_jobs[0].changes, '1,2')
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2)
+ self.assertEqual(B.data['status'], 'MERGED')
+ self.assertEqual(B.reported, 2)
+
+ def _test_irrelevant_files_jobs(self, should_skip):
+ "Test that jobs with irrelevant-files filter run only when appropriate"
+ self.updateConfigLayout('layout-irrelevant-files')
+ self.sched.reconfigure(self.config)
+
+ if should_skip:
+ files = {'ignoreme': 'ignored\n'}
+ else:
+ files = {'respectme': 'please!\n'}
+
+ change = self.fake_gerrit.addFakeChange('org/project',
+ 'master',
+ 'test irrelevant-files',
+ files=files)
+ self.fake_gerrit.addEvent(change.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+
+ tested_change_ids = [x.changes[0] for x in self.history
+ if x.name == 'project-test-irrelevant-files']
+
+ if should_skip:
+ self.assertEqual([], tested_change_ids)
+ else:
+ self.assertIn(change.data['number'], tested_change_ids)
+
+ def test_irrelevant_files_match_skips_job(self):
+ self._test_irrelevant_files_jobs(should_skip=True)
+
+ def test_irrelevant_files_no_match_runs_job(self):
+ self._test_irrelevant_files_jobs(should_skip=False)
+
+ def test_inherited_jobs_keep_matchers(self):
+ self.updateConfigLayout('layout-inheritance')
+ self.sched.reconfigure(self.config)
+
+ files = {'ignoreme': 'ignored\n'}
+
+ change = self.fake_gerrit.addFakeChange('org/project',
+ 'master',
+ 'test irrelevant-files',
+ files=files)
+ self.fake_gerrit.addEvent(change.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+
+ run_jobs = set([build.name for build in self.history])
+
+ self.assertEqual(set(['project-test-nomatch-starts-empty',
+ 'project-test-nomatch-starts-full']), run_jobs)
+
+ @skip("Disabled for early v3 development")
+ def test_test_config(self):
+ "Test that we can test the config"
+ self.sched.testConfig(self.config.get('zuul', 'tenant_config'),
+ self.connections)
+
+ @skip("Disabled for early v3 development")
+ def test_queue_names(self):
+ "Test shared change queue names"
+ project1 = self.sched.layout.projects['org/project1']
+ project2 = self.sched.layout.projects['org/project2']
+ q1 = self.sched.layout.pipelines['gate'].getQueue(project1)
+ q2 = self.sched.layout.pipelines['gate'].getQueue(project2)
+ self.assertEqual(q1.name, 'integration')
+ self.assertEqual(q2.name, 'integration')
+
+ self.updateConfigLayout(
+ 'tests/fixtures/layout-bad-queue.yaml')
+ with testtools.ExpectedException(
+ Exception, "More than one name assigned to change queue"):
+ self.sched.reconfigure(self.config)
+
+ def test_queue_precedence(self):
+ "Test that queue precedence works"
+
+ self.gearman_server.hold_jobs_in_queue = True
+ self.launch_server.hold_jobs_in_build = True
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+ A.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+
+ self.waitUntilSettled()
+ self.gearman_server.hold_jobs_in_queue = False
+ self.gearman_server.release()
+ self.waitUntilSettled()
+
+ # Run one build at a time to ensure non-race order:
+ self.orderedRelease()
+ self.launch_server.hold_jobs_in_build = False
+ self.waitUntilSettled()
+
+ self.log.debug(self.history)
+ self.assertEqual(self.history[0].pipeline, 'gate')
+ self.assertEqual(self.history[1].pipeline, 'check')
+ self.assertEqual(self.history[2].pipeline, 'gate')
+ self.assertEqual(self.history[3].pipeline, 'gate')
+ self.assertEqual(self.history[4].pipeline, 'check')
+ self.assertEqual(self.history[5].pipeline, 'check')
+
+ @skip("Disabled for early v3 development")
+ def test_json_status(self):
+ "Test that we can retrieve JSON status info"
+ self.launch_server.hold_jobs_in_build = True
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ A.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ self.launch_server.release('project-merge')
+ self.waitUntilSettled()
+
+ port = self.webapp.server.socket.getsockname()[1]
+
+ req = urllib.request.Request("http://localhost:%s/status.json" % port)
+ f = urllib.request.urlopen(req)
+ headers = f.info()
+ self.assertIn('Content-Length', headers)
+ self.assertIn('Content-Type', headers)
+ self.assertIsNotNone(re.match('^application/json(; charset=UTF-8)?$',
+ headers['Content-Type']))
+ self.assertIn('Access-Control-Allow-Origin', headers)
+ self.assertIn('Cache-Control', headers)
+ self.assertIn('Last-Modified', headers)
+ self.assertIn('Expires', headers)
+ data = f.read()
+
+ self.launch_server.hold_jobs_in_build = False
+ self.launch_server.release()
+ self.waitUntilSettled()
+
+ data = json.loads(data)
+ status_jobs = []
+ for p in data['pipelines']:
+ for q in p['change_queues']:
+ if p['name'] in ['gate', 'conflict']:
+ self.assertEqual(q['window'], 20)
+ else:
+ self.assertEqual(q['window'], 0)
+ for head in q['heads']:
+ for change in head:
+ self.assertTrue(change['active'])
+ self.assertEqual(change['id'], '1,1')
+ for job in change['jobs']:
+ status_jobs.append(job)
+ self.assertEqual('project-merge', status_jobs[0]['name'])
+ self.assertEqual('https://server/job/project-merge/0/',
+ status_jobs[0]['url'])
+ self.assertEqual('http://logs.example.com/1/1/gate/project-merge/0',
+ status_jobs[0]['report_url'])
+
+ self.assertEqual('project-test1', status_jobs[1]['name'])
+ self.assertEqual('https://server/job/project-test1/1/',
+ status_jobs[1]['url'])
+ self.assertEqual('http://logs.example.com/1/1/gate/project-test1/1',
+ status_jobs[1]['report_url'])
+
+ self.assertEqual('project-test2', status_jobs[2]['name'])
+ self.assertEqual('https://server/job/project-test2/2/',
+ status_jobs[2]['url'])
+ self.assertEqual('http://logs.example.com/1/1/gate/project-test2/2',
+ status_jobs[2]['report_url'])
+
+ @skip("Disabled for early v3 development")
+ def test_merging_queues(self):
+ "Test that transitively-connected change queues are merged"
+ self.updateConfigLayout(
+ 'tests/fixtures/layout-merge-queues.yaml')
+ self.sched.reconfigure(self.config)
+ self.assertEqual(len(self.sched.layout.pipelines['gate'].queues), 1)
+
+ @skip("Disabled for early v3 development")
+ def test_mutex(self):
+ "Test job mutexes"
+ self.config.set('zuul', 'layout_config',
+ 'tests/fixtures/layout-mutex.yaml')
+ self.sched.reconfigure(self.config)
+
+ self.launch_server.hold_jobs_in_build = True
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+ self.assertFalse('test-mutex' in self.sched.mutex.mutexes)
+
+ self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+ self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+ self.assertEqual(len(self.builds), 3)
+ self.assertEqual(self.builds[0].name, 'project-test1')
+ self.assertEqual(self.builds[1].name, 'mutex-one')
+ self.assertEqual(self.builds[2].name, 'project-test1')
+
+ self.launch_server.release('mutex-one')
+ self.waitUntilSettled()
+
+ self.assertEqual(len(self.builds), 3)
+ self.assertEqual(self.builds[0].name, 'project-test1')
+ self.assertEqual(self.builds[1].name, 'project-test1')
+ self.assertEqual(self.builds[2].name, 'mutex-two')
+ self.assertTrue('test-mutex' in self.sched.mutex.mutexes)
+
+ self.launch_server.release('mutex-two')
+ self.waitUntilSettled()
+
+ self.assertEqual(len(self.builds), 3)
+ self.assertEqual(self.builds[0].name, 'project-test1')
+ self.assertEqual(self.builds[1].name, 'project-test1')
+ self.assertEqual(self.builds[2].name, 'mutex-one')
+ self.assertTrue('test-mutex' in self.sched.mutex.mutexes)
+
+ self.launch_server.release('mutex-one')
+ self.waitUntilSettled()
+
+ self.assertEqual(len(self.builds), 3)
+ self.assertEqual(self.builds[0].name, 'project-test1')
+ self.assertEqual(self.builds[1].name, 'project-test1')
+ self.assertEqual(self.builds[2].name, 'mutex-two')
+ self.assertTrue('test-mutex' in self.sched.mutex.mutexes)
+
+ self.launch_server.release('mutex-two')
+ self.waitUntilSettled()
+
+ self.assertEqual(len(self.builds), 2)
+ self.assertEqual(self.builds[0].name, 'project-test1')
+ self.assertEqual(self.builds[1].name, 'project-test1')
+ self.assertFalse('test-mutex' in self.sched.mutex.mutexes)
+
+ self.launch_server.hold_jobs_in_build = False
+ self.launch_server.release()
+
+ self.waitUntilSettled()
+ self.assertEqual(len(self.builds), 0)
+
+ self.assertEqual(A.reported, 1)
+ self.assertEqual(B.reported, 1)
+ self.assertFalse('test-mutex' in self.sched.mutex.mutexes)
+
+ @skip("Disabled for early v3 development")
+ def test_node_label(self):
+ "Test that a job runs on a specific node label"
+ self.worker.registerFunction('build:node-project-test1:debian')
+
+ A = self.fake_gerrit.addFakeChange('org/node-project', 'master', 'A')
+ A.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ self.assertIsNone(self.getJobFromHistory('node-project-merge').node)
+ self.assertEqual(self.getJobFromHistory('node-project-test1').node,
+ 'debian')
+ self.assertIsNone(self.getJobFromHistory('node-project-test2').node)
+
+ def test_live_reconfiguration(self):
+ "Test that live reconfiguration works"
+ self.launch_server.hold_jobs_in_build = True
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ A.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ self.sched.reconfigure(self.config)
+
+ self.launch_server.hold_jobs_in_build = False
+ self.launch_server.release()
+ self.waitUntilSettled()
+ self.assertEqual(self.getJobFromHistory('project-merge').result,
+ 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('project-test1').result,
+ 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('project-test2').result,
+ 'SUCCESS')
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2)
+
+ @skip("Disabled for early v3 development")
+ def test_live_reconfiguration_merge_conflict(self):
+ # A real-world bug: a change in a gate queue has a merge
+ # conflict and a job is added to its project while it's
+ # sitting in the queue. The job gets added to the change and
+ # enqueued and the change gets stuck.
+ self.worker.registerFunction('build:project-test3')
+ self.launch_server.hold_jobs_in_build = True
+
+ # This change is fine. It's here to stop the queue long
+ # enough for the next change to be subject to the
+ # reconfiguration, as well as to provide a conflict for the
+ # next change. This change will succeed and merge.
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ A.addPatchset(['conflict'])
+ A.addApproval('code-review', 2)
+
+ # This change will be in merge conflict. During the
+ # reconfiguration, we will add a job. We want to make sure
+ # that doesn't cause it to get stuck.
+ B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+ B.addPatchset(['conflict'])
+ B.addApproval('code-review', 2)
+
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(B.addApproval('approved', 1))
+
+ self.waitUntilSettled()
+
+ # No jobs have run yet
+ self.assertEqual(A.data['status'], 'NEW')
+ self.assertEqual(A.reported, 1)
+ self.assertEqual(B.data['status'], 'NEW')
+ self.assertEqual(B.reported, 1)
+ self.assertEqual(len(self.history), 0)
+
+ # Add the "project-test3" job.
+ self.updateConfigLayout(
+ 'tests/fixtures/layout-live-reconfiguration-add-job.yaml')
+ self.sched.reconfigure(self.config)
+ self.waitUntilSettled()
+
+ self.launch_server.hold_jobs_in_build = False
+ self.launch_server.release()
+ self.waitUntilSettled()
+
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2)
+ self.assertEqual(B.data['status'], 'NEW')
+ self.assertEqual(B.reported, 2)
+ self.assertEqual(self.getJobFromHistory('project-merge').result,
+ 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('project-test1').result,
+ 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('project-test2').result,
+ 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('project-test3').result,
+ 'SUCCESS')
+ self.assertEqual(len(self.history), 4)
+
+ @skip("Disabled for early v3 development")
+ def test_live_reconfiguration_failed_root(self):
+ # An extrapolation of test_live_reconfiguration_merge_conflict
+ # that tests a job added to a job tree with a failed root does
+ # not run.
+ self.worker.registerFunction('build:project-test3')
+ self.launch_server.hold_jobs_in_build = True
+
+ # This change is fine. It's here to stop the queue long
+ # enough for the next change to be subject to the
+ # reconfiguration. This change will succeed and merge.
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ A.addPatchset(['conflict'])
+ A.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+
+ B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+ self.launch_server.failJob('project-merge', B)
+ B.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(B.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+
+ # Both -merge jobs have run, but no others.
+ self.assertEqual(A.data['status'], 'NEW')
+ self.assertEqual(A.reported, 1)
+ self.assertEqual(B.data['status'], 'NEW')
+ self.assertEqual(B.reported, 1)
+ self.assertEqual(self.history[0].result, 'SUCCESS')
+ self.assertEqual(self.history[0].name, 'project-merge')
+ self.assertEqual(self.history[1].result, 'FAILURE')
+ self.assertEqual(self.history[1].name, 'project-merge')
+ self.assertEqual(len(self.history), 2)
+
+ # Add the "project-test3" job.
+ self.updateConfigLayout(
+ 'tests/fixtures/layout-live-reconfiguration-add-job.yaml')
+ self.sched.reconfigure(self.config)
+ self.waitUntilSettled()
+
+ self.launch_server.hold_jobs_in_build = False
+ self.launch_server.release()
+ self.waitUntilSettled()
+
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2)
+ self.assertEqual(B.data['status'], 'NEW')
+ self.assertEqual(B.reported, 2)
+ self.assertEqual(self.history[0].result, 'SUCCESS')
+ self.assertEqual(self.history[0].name, 'project-merge')
+ self.assertEqual(self.history[1].result, 'FAILURE')
+ self.assertEqual(self.history[1].name, 'project-merge')
+ self.assertEqual(self.history[2].result, 'SUCCESS')
+ self.assertEqual(self.history[3].result, 'SUCCESS')
+ self.assertEqual(self.history[4].result, 'SUCCESS')
+ self.assertEqual(len(self.history), 5)
+
+ @skip("Disabled for early v3 development")
+ def test_live_reconfiguration_failed_job(self):
+ # Test that a change with a removed failing job does not
+ # disrupt reconfiguration. If a change has a failed job and
+ # that job is removed during a reconfiguration, we observed a
+ # bug where the code to re-set build statuses would run on
+ # that build and raise an exception because the job no longer
+ # existed.
+ self.launch_server.hold_jobs_in_build = True
+
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+
+ # This change will fail and later be removed by the reconfiguration.
+ self.launch_server.failJob('project-test1', A)
+
+ self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.launch_server.release('project-test1')
+ self.waitUntilSettled()
+
+ self.assertEqual(A.data['status'], 'NEW')
+ self.assertEqual(A.reported, 0)
+
+ self.assertEqual(self.getJobFromHistory('project-merge').result,
+ 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('project-test1').result,
+ 'FAILURE')
+ self.assertEqual(len(self.history), 2)
+
+ # Remove the test1 job.
+ self.updateConfigLayout(
+ 'tests/fixtures/layout-live-reconfiguration-failed-job.yaml')
+ self.sched.reconfigure(self.config)
+ self.waitUntilSettled()
+
+ self.launch_server.hold_jobs_in_build = False
+ self.launch_server.release()
+ self.waitUntilSettled()
+
+ self.assertEqual(self.getJobFromHistory('project-test2').result,
+ 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('project-testfile').result,
+ 'SUCCESS')
+ self.assertEqual(len(self.history), 4)
+
+ self.assertEqual(A.data['status'], 'NEW')
+ self.assertEqual(A.reported, 1)
+ self.assertIn('Build succeeded', A.messages[0])
+ # Ensure the removed job was not included in the report.
+ self.assertNotIn('project-test1', A.messages[0])
+
+ @skip("Disabled for early v3 development")
+ def test_live_reconfiguration_shared_queue(self):
+ # Test that a change with a failing job which was removed from
+ # this project but otherwise still exists in the system does
+ # not disrupt reconfiguration.
+
+ self.launch_server.hold_jobs_in_build = True
+
+ A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A')
+
+ self.launch_server.failJob('project1-project2-integration', A)
+
+ self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.launch_server.release('project1-project2-integration')
+ self.waitUntilSettled()
+
+ self.assertEqual(A.data['status'], 'NEW')
+ self.assertEqual(A.reported, 0)
+
+ self.assertEqual(self.getJobFromHistory('project1-merge').result,
+ 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory(
+ 'project1-project2-integration').result, 'FAILURE')
+ self.assertEqual(len(self.history), 2)
+
+ # Remove the integration job.
+ self.updateConfigLayout(
+ 'tests/fixtures/layout-live-reconfiguration-shared-queue.yaml')
+ self.sched.reconfigure(self.config)
+ self.waitUntilSettled()
+
+ self.launch_server.hold_jobs_in_build = False
+ self.launch_server.release()
+ self.waitUntilSettled()
+
+ self.assertEqual(self.getJobFromHistory('project1-merge').result,
+ 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('project1-test1').result,
+ 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('project1-test2').result,
+ 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory(
+ 'project1-project2-integration').result, 'FAILURE')
+ self.assertEqual(len(self.history), 4)
+
+ self.assertEqual(A.data['status'], 'NEW')
+ self.assertEqual(A.reported, 1)
+ self.assertIn('Build succeeded', A.messages[0])
+ # Ensure the removed job was not included in the report.
+ self.assertNotIn('project1-project2-integration', A.messages[0])
+
+ @skip("Disabled for early v3 development")
+ def test_double_live_reconfiguration_shared_queue(self):
+ # This was a real-world regression. A change is added to
+ # gate; a reconfigure happens, a second change which depends
+ # on the first is added, and a second reconfiguration happens.
+ # Ensure that both changes merge.
+
+ # A failure may indicate incorrect caching or cleaning up of
+ # references during a reconfiguration.
+ self.launch_server.hold_jobs_in_build = True
+
+ A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project1', 'master', 'B')
+ B.setDependsOn(A, 1)
+ A.addApproval('code-review', 2)
+ B.addApproval('code-review', 2)
+
+ # Add the parent change.
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+
+ # Reconfigure (with only one change in the pipeline).
+ self.sched.reconfigure(self.config)
+ self.waitUntilSettled()
+
+ # Add the child change.
+ self.fake_gerrit.addEvent(B.addApproval('approved', 1))
+ self.waitUntilSettled()
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+
+ # Reconfigure (with both in the pipeline).
+ self.sched.reconfigure(self.config)
+ self.waitUntilSettled()
+
+ self.launch_server.hold_jobs_in_build = False
+ self.launch_server.release()
+ self.waitUntilSettled()
+
+ self.assertEqual(len(self.history), 8)
+
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2)
+ self.assertEqual(B.data['status'], 'MERGED')
+ self.assertEqual(B.reported, 2)
+
+ @skip("Disabled for early v3 development")
+ def test_live_reconfiguration_del_project(self):
+ # Test project deletion from layout
+ # while changes are enqueued
+
+ self.launch_server.hold_jobs_in_build = True
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project1', 'master', 'B')
+ C = self.fake_gerrit.addFakeChange('org/project1', 'master', 'C')
+
+ # A Depends-On: B
+ A.data['commitMessage'] = '%s\n\nDepends-On: %s\n' % (
+ A.subject, B.data['id'])
+ self.fake_gerrit.addEvent(B.addApproval('approved', 1))
+
+ self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+ self.fake_gerrit.addEvent(C.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.assertEqual(len(self.builds), 5)
+
+ # This layout defines only org/project, not org/project1
+ self.updateConfigLayout(
+ 'tests/fixtures/layout-live-reconfiguration-del-project.yaml')
+ self.sched.reconfigure(self.config)
+ self.waitUntilSettled()
+
+ # Builds for C aborted, builds for A succeed,
+ # and have change B applied ahead
+ job_c = self.getJobFromHistory('project1-test1')
+ self.assertEqual(job_c.changes, '3,1')
+ self.assertEqual(job_c.result, 'ABORTED')
+
+ self.launch_server.hold_jobs_in_build = False
+ self.launch_server.release()
+ self.waitUntilSettled()
+
+ self.assertEqual(self.getJobFromHistory('project-test1').changes,
+ '2,1 1,1')
+
+ self.assertEqual(A.data['status'], 'NEW')
+ self.assertEqual(B.data['status'], 'NEW')
+ self.assertEqual(C.data['status'], 'NEW')
+ self.assertEqual(A.reported, 1)
+ self.assertEqual(B.reported, 0)
+ self.assertEqual(C.reported, 0)
+
+ self.assertEqual(len(self.sched.layout.pipelines['check'].queues), 0)
+ self.assertIn('Build succeeded', A.messages[0])
+
+ @skip("Disabled for early v3 development")
+ def test_live_reconfiguration_functions(self):
+ "Test live reconfiguration with a custom function"
+ self.worker.registerFunction('build:node-project-test1:debian')
+ self.worker.registerFunction('build:node-project-test1:wheezy')
+ A = self.fake_gerrit.addFakeChange('org/node-project', 'master', 'A')
+ A.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ self.assertIsNone(self.getJobFromHistory('node-project-merge').node)
+ self.assertEqual(self.getJobFromHistory('node-project-test1').node,
+ 'debian')
+ self.assertIsNone(self.getJobFromHistory('node-project-test2').node)
+
+ self.updateConfigLayout(
+ 'tests/fixtures/layout-live-reconfiguration-functions.yaml')
+ self.sched.reconfigure(self.config)
+ self.worker.build_history = []
+
+ B = self.fake_gerrit.addFakeChange('org/node-project', 'master', 'B')
+ B.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(B.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ self.assertIsNone(self.getJobFromHistory('node-project-merge').node)
+ self.assertEqual(self.getJobFromHistory('node-project-test1').node,
+ 'wheezy')
+ self.assertIsNone(self.getJobFromHistory('node-project-test2').node)
+
+ @skip("Disabled for early v3 development")
+ def test_delayed_repo_init(self):
+ self.updateConfigLayout(
+ 'tests/fixtures/layout-delayed-repo-init.yaml')
+ self.sched.reconfigure(self.config)
+
+ self.init_repo("org/new-project")
+ A = self.fake_gerrit.addFakeChange('org/new-project', 'master', 'A')
+
+ A.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+ self.assertEqual(self.getJobFromHistory('project-merge').result,
+ 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('project-test1').result,
+ 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('project-test2').result,
+ 'SUCCESS')
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2)
+
+ def test_repo_deleted(self):
+ self.updateConfigLayout('layout-repo-deleted')
+ self.sched.reconfigure(self.config)
+
+ self.init_repo("org/delete-project")
+ A = self.fake_gerrit.addFakeChange('org/delete-project', 'master', 'A')
+
+ A.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+ self.assertEqual(self.getJobFromHistory('project-merge').result,
+ 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('project-test1').result,
+ 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('project-test2').result,
+ 'SUCCESS')
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2)
+
+ # Delete org/new-project zuul repo. Should be recloned.
+ shutil.rmtree(os.path.join(self.git_root, "org/delete-project"))
+
+ B = self.fake_gerrit.addFakeChange('org/delete-project', 'master', 'B')
+
+ B.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(B.addApproval('approved', 1))
+ self.waitUntilSettled()
+ self.assertEqual(self.getJobFromHistory('project-merge').result,
+ 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('project-test1').result,
+ 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('project-test2').result,
+ 'SUCCESS')
+ self.assertEqual(B.data['status'], 'MERGED')
+ self.assertEqual(B.reported, 2)
+
+ @skip("Disabled for early v3 development")
+ def test_tags(self):
+ "Test job tags"
+ self.config.set('zuul', 'layout_config',
+ 'tests/fixtures/layout-tags.yaml')
+ self.sched.reconfigure(self.config)
+
+ A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project2', 'master', 'B')
+ self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+ self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+
+ results = {'project1-merge': 'extratag merge project1',
+ 'project2-merge': 'merge'}
+
+ for build in self.history:
+ self.assertEqual(results.get(build.name, ''),
+ build.parameters.get('BUILD_TAGS'))
+
+ @skip("Disabled for early v3 development")
+ def test_timer(self):
+ "Test that a periodic job is triggered"
+ self.launch_server.hold_jobs_in_build = True
+ self.updateConfigLayout(
+ 'tests/fixtures/layout-timer.yaml')
+ self.sched.reconfigure(self.config)
+ self.registerJobs()
+
+ # The pipeline triggers every second, so we should have seen
+ # several by now.
+ time.sleep(5)
+ self.waitUntilSettled()
+
+ self.assertEqual(len(self.builds), 2)
+
+ port = self.webapp.server.socket.getsockname()[1]
+
+ req = urllib.request.Request("http://localhost:%s/status.json" % port)
+ f = urllib.request.urlopen(req)
+ data = f.read()
+
+ self.launch_server.hold_jobs_in_build = False
+ # Stop queuing timer triggered jobs so that the assertions
+ # below don't race against more jobs being queued.
+ self.updateConfigLayout(
+ 'tests/fixtures/layout-no-timer.yaml')
+ self.sched.reconfigure(self.config)
+ self.registerJobs()
+ self.launch_server.release()
+ self.waitUntilSettled()
+
+ self.assertEqual(self.getJobFromHistory(
+ 'project-bitrot-stable-old').result, 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory(
+ 'project-bitrot-stable-older').result, 'SUCCESS')
+
+ data = json.loads(data)
+ status_jobs = set()
+ for p in data['pipelines']:
+ for q in p['change_queues']:
+ for head in q['heads']:
+ for change in head:
+ self.assertEqual(change['id'], None)
+ for job in change['jobs']:
+ status_jobs.add(job['name'])
+ self.assertIn('project-bitrot-stable-old', status_jobs)
+ self.assertIn('project-bitrot-stable-older', status_jobs)
+
+ @skip("Disabled for early v3 development")
+ def test_idle(self):
+ "Test that frequent periodic jobs work"
+ self.launch_server.hold_jobs_in_build = True
+
+ for x in range(1, 3):
+ # Test that timer triggers periodic jobs even across
+ # layout config reloads.
+ # Start timer trigger
+ self.updateConfigLayout(
+ 'tests/fixtures/layout-idle.yaml')
+ self.sched.reconfigure(self.config)
+ self.registerJobs()
+ self.waitUntilSettled()
+
+ # The pipeline triggers every second, so we should have seen
+ # several by now.
+ time.sleep(5)
+
+ # Stop queuing timer triggered jobs so that the assertions
+ # below don't race against more jobs being queued.
+ self.updateConfigLayout(
+ 'tests/fixtures/layout-no-timer.yaml')
+ self.sched.reconfigure(self.config)
+ self.registerJobs()
+ self.waitUntilSettled()
+
+ self.assertEqual(len(self.builds), 2)
+ self.launch_server.release('.*')
+ self.waitUntilSettled()
+ self.assertEqual(len(self.builds), 0)
+ self.assertEqual(len(self.history), x * 2)
+
+ def test_check_smtp_pool(self):
+ self.updateConfigLayout('layout-smtp')
+ self.sched.reconfigure(self.config)
+
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ self.waitUntilSettled()
+
+ self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+
+ self.assertEqual(len(self.smtp_messages), 2)
+
+ # A.messages only holds what FakeGerrit places in it. Thus we
+ # work on the knowledge of what the first message should be as
+ # it is only configured to go to SMTP.
+
+ self.assertEqual('zuul@example.com',
+ self.smtp_messages[0]['from_email'])
+ self.assertEqual(['you@example.com'],
+ self.smtp_messages[0]['to_email'])
+ self.assertEqual('Starting check jobs.',
+ self.smtp_messages[0]['body'])
+
+ self.assertEqual('zuul_from@example.com',
+ self.smtp_messages[1]['from_email'])
+ self.assertEqual(['alternative_me@example.com'],
+ self.smtp_messages[1]['to_email'])
+ self.assertEqual(A.messages[0],
+ self.smtp_messages[1]['body'])
+
+ @skip("Disabled for early v3 development")
+ def test_timer_smtp(self):
+ "Test that a periodic job is triggered"
+ self.launch_server.hold_jobs_in_build = True
+ self.updateConfigLayout(
+ 'tests/fixtures/layout-timer-smtp.yaml')
+ self.sched.reconfigure(self.config)
+ self.registerJobs()
+
+ # The pipeline triggers every second, so we should have seen
+ # several by now.
+ time.sleep(5)
+ self.waitUntilSettled()
+
+ self.assertEqual(len(self.builds), 2)
+ self.launch_server.release('.*')
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 2)
+
+ self.assertEqual(self.getJobFromHistory(
+ 'project-bitrot-stable-old').result, 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory(
+ 'project-bitrot-stable-older').result, 'SUCCESS')
+
+ self.assertEqual(len(self.smtp_messages), 1)
+
+ # A.messages only holds what FakeGerrit places in it. Thus we
+ # work on the knowledge of what the first message should be as
+ # it is only configured to go to SMTP.
+
+ self.assertEqual('zuul_from@example.com',
+ self.smtp_messages[0]['from_email'])
+ self.assertEqual(['alternative_me@example.com'],
+ self.smtp_messages[0]['to_email'])
+ self.assertIn('Subject: Periodic check for org/project succeeded',
+ self.smtp_messages[0]['headers'])
+
+ # Stop queuing timer triggered jobs and let any that may have
+ # queued through so that end of test assertions pass.
+ self.updateConfigLayout(
+ 'tests/fixtures/layout-no-timer.yaml')
+ self.sched.reconfigure(self.config)
+ self.registerJobs()
+ self.waitUntilSettled()
+ self.launch_server.release('.*')
+ self.waitUntilSettled()
+
+ def test_client_enqueue_change(self):
+ "Test that the RPC client can enqueue a change"
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ A.addApproval('code-review', 2)
+ A.addApproval('approved', 1)
+
+ client = zuul.rpcclient.RPCClient('127.0.0.1',
+ self.gearman_server.port)
+ r = client.enqueue(tenant='tenant-one',
+ pipeline='gate',
+ project='org/project',
+ trigger='gerrit',
+ change='1,1')
+ self.waitUntilSettled()
+ self.assertEqual(self.getJobFromHistory('project-merge').result,
+ 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('project-test1').result,
+ 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('project-test2').result,
+ 'SUCCESS')
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2)
+ self.assertEqual(r, True)
+
+ @skip("Disabled for early v3 development")
+ def test_client_enqueue_ref(self):
+ "Test that the RPC client can enqueue a ref"
+
+ client = zuul.rpcclient.RPCClient('127.0.0.1',
+ self.gearman_server.port)
+ r = client.enqueue_ref(
+ tenant='tenant-one',
+ pipeline='post',
+ project='org/project',
+ trigger='gerrit',
+ ref='master',
+ oldrev='90f173846e3af9154517b88543ffbd1691f31366',
+ newrev='d479a0bfcb34da57a31adb2a595c0cf687812543')
+ self.waitUntilSettled()
+ job_names = [x.name for x in self.history]
+ self.assertEqual(len(self.history), 1)
+ self.assertIn('project-post', job_names)
+ self.assertEqual(r, True)
+
+ def test_client_enqueue_negative(self):
+ "Test that the RPC client returns errors"
+ client = zuul.rpcclient.RPCClient('127.0.0.1',
+ self.gearman_server.port)
+ with testtools.ExpectedException(zuul.rpcclient.RPCFailure,
+ "Invalid tenant"):
+ r = client.enqueue(tenant='tenant-foo',
+ pipeline='gate',
+ project='org/project',
+ trigger='gerrit',
+ change='1,1')
+ client.shutdown()
+ self.assertEqual(r, False)
+
+ with testtools.ExpectedException(zuul.rpcclient.RPCFailure,
+ "Invalid project"):
+ r = client.enqueue(tenant='tenant-one',
+ pipeline='gate',
+ project='project-does-not-exist',
+ trigger='gerrit',
+ change='1,1')
+ client.shutdown()
+ self.assertEqual(r, False)
+
+ with testtools.ExpectedException(zuul.rpcclient.RPCFailure,
+ "Invalid pipeline"):
+ r = client.enqueue(tenant='tenant-one',
+ pipeline='pipeline-does-not-exist',
+ project='org/project',
+ trigger='gerrit',
+ change='1,1')
+ client.shutdown()
+ self.assertEqual(r, False)
+
+ with testtools.ExpectedException(zuul.rpcclient.RPCFailure,
+ "Invalid trigger"):
+ r = client.enqueue(tenant='tenant-one',
+ pipeline='gate',
+ project='org/project',
+ trigger='trigger-does-not-exist',
+ change='1,1')
+ client.shutdown()
+ self.assertEqual(r, False)
+
+ with testtools.ExpectedException(zuul.rpcclient.RPCFailure,
+ "Invalid change"):
+ r = client.enqueue(tenant='tenant-one',
+ pipeline='gate',
+ project='org/project',
+ trigger='gerrit',
+ change='1,1')
+ client.shutdown()
+ self.assertEqual(r, False)
+
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 0)
+ self.assertEqual(len(self.builds), 0)
+
+ def test_client_promote(self):
+ "Test that the RPC client can promote a change"
+ self.launch_server.hold_jobs_in_build = True
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+ C = self.fake_gerrit.addFakeChange('org/project', 'master', 'C')
+ A.addApproval('code-review', 2)
+ B.addApproval('code-review', 2)
+ C.addApproval('code-review', 2)
+
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(B.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(C.addApproval('approved', 1))
+
+ self.waitUntilSettled()
+
+ tenant = self.sched.abide.tenants.get('tenant-one')
+ items = tenant.layout.pipelines['gate'].getAllItems()
+ enqueue_times = {}
+ for item in items:
+ enqueue_times[str(item.change)] = item.enqueue_time
+
+ client = zuul.rpcclient.RPCClient('127.0.0.1',
+ self.gearman_server.port)
+ r = client.promote(tenant='tenant-one',
+ pipeline='gate',
+ change_ids=['2,1', '3,1'])
+
+ # ensure that enqueue times are durable
+ items = tenant.layout.pipelines['gate'].getAllItems()
+ for item in items:
+ self.assertEqual(
+ enqueue_times[str(item.change)], item.enqueue_time)
+
+ self.waitUntilSettled()
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.launch_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.assertTrue(self.builds[0].hasChanges(B))
+ self.assertFalse(self.builds[0].hasChanges(A))
+ self.assertFalse(self.builds[0].hasChanges(C))
+
+ self.assertTrue(self.builds[2].hasChanges(B))
+ self.assertTrue(self.builds[2].hasChanges(C))
+ self.assertFalse(self.builds[2].hasChanges(A))
+
+ self.assertTrue(self.builds[4].hasChanges(B))
+ self.assertTrue(self.builds[4].hasChanges(C))
+ self.assertTrue(self.builds[4].hasChanges(A))
+
+ self.launch_server.release()
+ self.waitUntilSettled()
+
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2)
+ self.assertEqual(B.data['status'], 'MERGED')
+ self.assertEqual(B.reported, 2)
+ self.assertEqual(C.data['status'], 'MERGED')
+ self.assertEqual(C.reported, 2)
+
+ client.shutdown()
+ self.assertEqual(r, True)
+
+ def test_client_promote_dependent(self):
+ "Test that the RPC client can promote a dependent change"
+ # C (depends on B) -> B -> A ; then promote C to get:
+ # A -> C (depends on B) -> B
+ self.launch_server.hold_jobs_in_build = True
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+ C = self.fake_gerrit.addFakeChange('org/project', 'master', 'C')
+
+ C.setDependsOn(B, 1)
+
+ A.addApproval('code-review', 2)
+ B.addApproval('code-review', 2)
+ C.addApproval('code-review', 2)
+
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(B.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(C.addApproval('approved', 1))
+
+ self.waitUntilSettled()
+
+ client = zuul.rpcclient.RPCClient('127.0.0.1',
+ self.gearman_server.port)
+ r = client.promote(tenant='tenant-one',
+ pipeline='gate',
+ change_ids=['3,1'])
+
+ self.waitUntilSettled()
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.launch_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.assertTrue(self.builds[0].hasChanges(B))
+ self.assertFalse(self.builds[0].hasChanges(A))
+ self.assertFalse(self.builds[0].hasChanges(C))
+
+ self.assertTrue(self.builds[2].hasChanges(B))
+ self.assertTrue(self.builds[2].hasChanges(C))
+ self.assertFalse(self.builds[2].hasChanges(A))
+
+ self.assertTrue(self.builds[4].hasChanges(B))
+ self.assertTrue(self.builds[4].hasChanges(C))
+ self.assertTrue(self.builds[4].hasChanges(A))
+
+ self.launch_server.release()
+ self.waitUntilSettled()
+
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2)
+ self.assertEqual(B.data['status'], 'MERGED')
+ self.assertEqual(B.reported, 2)
+ self.assertEqual(C.data['status'], 'MERGED')
+ self.assertEqual(C.reported, 2)
+
+ client.shutdown()
+ self.assertEqual(r, True)
+
+ def test_client_promote_negative(self):
+ "Test that the RPC client returns errors for promotion"
+ self.launch_server.hold_jobs_in_build = True
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ A.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ client = zuul.rpcclient.RPCClient('127.0.0.1',
+ self.gearman_server.port)
+
+ with testtools.ExpectedException(zuul.rpcclient.RPCFailure):
+ r = client.promote(tenant='tenant-one',
+ pipeline='nonexistent',
+ change_ids=['2,1', '3,1'])
+ client.shutdown()
+ self.assertEqual(r, False)
+
+ with testtools.ExpectedException(zuul.rpcclient.RPCFailure):
+ r = client.promote(tenant='tenant-one',
+ pipeline='gate',
+ change_ids=['4,1'])
+ client.shutdown()
+ self.assertEqual(r, False)
+
+ self.launch_server.hold_jobs_in_build = False
+ self.launch_server.release()
+ self.waitUntilSettled()
+
+ @skip("Disabled for early v3 development")
+ def test_queue_rate_limiting(self):
+ "Test that DependentPipelines are rate limited with dep across window"
+ self.updateConfigLayout(
+ 'tests/fixtures/layout-rate-limit.yaml')
+ self.sched.reconfigure(self.config)
+ self.launch_server.hold_jobs_in_build = True
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+ C = self.fake_gerrit.addFakeChange('org/project', 'master', 'C')
+
+ C.setDependsOn(B, 1)
+ self.launch_server.failJob('project-test1', A)
+
+ A.addApproval('code-review', 2)
+ B.addApproval('code-review', 2)
+ C.addApproval('code-review', 2)
+
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(B.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(C.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ # Only A and B will have their merge jobs queued because
+ # window is 2.
+ self.assertEqual(len(self.builds), 2)
+ self.assertEqual(self.builds[0].name, 'project-merge')
+ self.assertEqual(self.builds[1].name, 'project-merge')
+
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+
+ # Only A and B will have their test jobs queued because
+ # window is 2.
+ self.assertEqual(len(self.builds), 4)
+ 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.launch_server.release('project-.*')
+ self.waitUntilSettled()
+
+ queue = self.sched.layout.pipelines['gate'].queues[0]
+ # A failed so window is reduced by 1 to 1.
+ self.assertEqual(queue.window, 1)
+ self.assertEqual(queue.window_floor, 1)
+ self.assertEqual(A.data['status'], 'NEW')
+
+ # Gate is reset and only B's merge job is queued because
+ # window shrunk to 1.
+ self.assertEqual(len(self.builds), 1)
+ self.assertEqual(self.builds[0].name, 'project-merge')
+
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+
+ # Only B's test jobs are queued because window is still 1.
+ self.assertEqual(len(self.builds), 2)
+ self.assertEqual(self.builds[0].name, 'project-test1')
+ self.assertEqual(self.builds[1].name, 'project-test2')
+
+ self.launch_server.release('project-.*')
+ self.waitUntilSettled()
+
+ # B was successfully merged so window is increased to 2.
+ self.assertEqual(queue.window, 2)
+ self.assertEqual(queue.window_floor, 1)
+ self.assertEqual(B.data['status'], 'MERGED')
+
+ # Only C is left and its merge job is queued.
+ self.assertEqual(len(self.builds), 1)
+ self.assertEqual(self.builds[0].name, 'project-merge')
+
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+
+ # After successful merge job the test jobs for C are queued.
+ self.assertEqual(len(self.builds), 2)
+ self.assertEqual(self.builds[0].name, 'project-test1')
+ self.assertEqual(self.builds[1].name, 'project-test2')
+
+ self.launch_server.release('project-.*')
+ self.waitUntilSettled()
+
+ # C successfully merged so window is bumped to 3.
+ self.assertEqual(queue.window, 3)
+ self.assertEqual(queue.window_floor, 1)
+ self.assertEqual(C.data['status'], 'MERGED')
+
+ @skip("Disabled for early v3 development")
+ def test_queue_rate_limiting_dependent(self):
+ "Test that DependentPipelines are rate limited with dep in window"
+ self.updateConfigLayout(
+ 'tests/fixtures/layout-rate-limit.yaml')
+ self.sched.reconfigure(self.config)
+ self.launch_server.hold_jobs_in_build = True
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+ C = self.fake_gerrit.addFakeChange('org/project', 'master', 'C')
+
+ B.setDependsOn(A, 1)
+
+ self.launch_server.failJob('project-test1', A)
+
+ A.addApproval('code-review', 2)
+ B.addApproval('code-review', 2)
+ C.addApproval('code-review', 2)
+
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(B.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(C.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ # Only A and B will have their merge jobs queued because
+ # window is 2.
+ self.assertEqual(len(self.builds), 2)
+ self.assertEqual(self.builds[0].name, 'project-merge')
+ self.assertEqual(self.builds[1].name, 'project-merge')
+
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+
+ # Only A and B will have their test jobs queued because
+ # window is 2.
+ self.assertEqual(len(self.builds), 4)
+ 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.launch_server.release('project-.*')
+ self.waitUntilSettled()
+
+ queue = self.sched.layout.pipelines['gate'].queues[0]
+ # A failed so window is reduced by 1 to 1.
+ self.assertEqual(queue.window, 1)
+ self.assertEqual(queue.window_floor, 1)
+ self.assertEqual(A.data['status'], 'NEW')
+ self.assertEqual(B.data['status'], 'NEW')
+
+ # Gate is reset and only C's merge job is queued because
+ # window shrunk to 1 and A and B were dequeued.
+ self.assertEqual(len(self.builds), 1)
+ self.assertEqual(self.builds[0].name, 'project-merge')
+
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+
+ # Only C's test jobs are queued because window is still 1.
+ self.assertEqual(len(self.builds), 2)
+ self.assertEqual(self.builds[0].name, 'project-test1')
+ self.assertEqual(self.builds[1].name, 'project-test2')
+
+ self.launch_server.release('project-.*')
+ self.waitUntilSettled()
+
+ # C was successfully merged so window is increased to 2.
+ self.assertEqual(queue.window, 2)
+ self.assertEqual(queue.window_floor, 1)
+ self.assertEqual(C.data['status'], 'MERGED')
+
+ @skip("Disabled for early v3 development")
+ def test_worker_update_metadata(self):
+ "Test if a worker can send back metadata about itself"
+ self.launch_server.hold_jobs_in_build = True
+
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ A.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ self.assertEqual(len(self.launcher.builds), 1)
+
+ self.log.debug('Current builds:')
+ self.log.debug(self.launcher.builds)
+
+ start = time.time()
+ while True:
+ if time.time() - start > 10:
+ raise Exception("Timeout waiting for gearman server to report "
+ + "back to the client")
+ build = self.launcher.builds.values()[0]
+ if build.worker.name == "My Worker":
+ break
+ else:
+ time.sleep(0)
+
+ self.log.debug(build)
+ self.assertEqual("My Worker", build.worker.name)
+ self.assertEqual("localhost", build.worker.hostname)
+ self.assertEqual(['127.0.0.1', '192.168.1.1'], build.worker.ips)
+ self.assertEqual("zuul.example.org", build.worker.fqdn)
+ self.assertEqual("FakeBuilder", build.worker.program)
+ self.assertEqual("v1.1", build.worker.version)
+ self.assertEqual({'something': 'else'}, build.worker.extra)
+
+ self.launch_server.hold_jobs_in_build = False
+ self.launch_server.release()
+ self.waitUntilSettled()
+
+ @skip("Disabled for early v3 development")
+ def test_footer_message(self):
+ "Test a pipeline's footer message is correctly added to the report."
+ self.updateConfigLayout(
+ 'tests/fixtures/layout-footer-message.yaml')
+ self.sched.reconfigure(self.config)
+ self.registerJobs()
+
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ A.addApproval('code-review', 2)
+ self.launch_server.failJob('test1', A)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+ B.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(B.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ self.assertEqual(2, len(self.smtp_messages))
+
+ failure_body = """\
+Build failed. For information on how to proceed, see \
+http://wiki.example.org/Test_Failures
+
+- test1 http://logs.example.com/1/1/gate/test1/0 : FAILURE in 0s
+- test2 http://logs.example.com/1/1/gate/test2/1 : SUCCESS in 0s
+
+For CI problems and help debugging, contact ci@example.org"""
+
+ success_body = """\
+Build succeeded.
+
+- test1 http://logs.example.com/2/1/gate/test1/2 : SUCCESS in 0s
+- test2 http://logs.example.com/2/1/gate/test2/3 : SUCCESS in 0s
+
+For CI problems and help debugging, contact ci@example.org"""
+
+ self.assertEqual(failure_body, self.smtp_messages[0]['body'])
+ self.assertEqual(success_body, self.smtp_messages[1]['body'])
+
+ @skip("Disabled for early v3 development")
+ def test_merge_failure_reporters(self):
+ """Check that the config is set up correctly"""
+
+ self.updateConfigLayout(
+ 'tests/fixtures/layout-merge-failure.yaml')
+ self.sched.reconfigure(self.config)
+ self.registerJobs()
+
+ self.assertEqual(
+ "Merge Failed.\n\nThis change or one of its cross-repo "
+ "dependencies was unable to be automatically merged with the "
+ "current state of its repository. Please rebase the change and "
+ "upload a new patchset.",
+ self.sched.layout.pipelines['check'].merge_failure_message)
+ self.assertEqual(
+ "The merge failed! For more information...",
+ self.sched.layout.pipelines['gate'].merge_failure_message)
+
+ self.assertEqual(
+ len(self.sched.layout.pipelines['check'].merge_failure_actions), 1)
+ self.assertEqual(
+ len(self.sched.layout.pipelines['gate'].merge_failure_actions), 2)
+
+ self.assertTrue(isinstance(
+ self.sched.layout.pipelines['check'].merge_failure_actions[0],
+ zuul.reporter.gerrit.GerritReporter))
+
+ self.assertTrue(
+ (
+ isinstance(self.sched.layout.pipelines['gate'].
+ merge_failure_actions[0],
+ zuul.reporter.smtp.SMTPReporter) and
+ isinstance(self.sched.layout.pipelines['gate'].
+ merge_failure_actions[1],
+ zuul.reporter.gerrit.GerritReporter)
+ ) or (
+ isinstance(self.sched.layout.pipelines['gate'].
+ merge_failure_actions[0],
+ zuul.reporter.gerrit.GerritReporter) and
+ isinstance(self.sched.layout.pipelines['gate'].
+ merge_failure_actions[1],
+ zuul.reporter.smtp.SMTPReporter)
+ )
+ )
+
+ @skip("Disabled for early v3 development")
+ def test_merge_failure_reports(self):
+ """Check that when a change fails to merge the correct message is sent
+ to the correct reporter"""
+ self.updateConfigLayout(
+ 'tests/fixtures/layout-merge-failure.yaml')
+ self.sched.reconfigure(self.config)
+ self.registerJobs()
+
+ # Check a test failure isn't reported to SMTP
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ A.addApproval('code-review', 2)
+ self.launch_server.failJob('project-test1', A)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ self.assertEqual(3, len(self.history)) # 3 jobs
+ self.assertEqual(0, len(self.smtp_messages))
+
+ # Check a merge failure is reported to SMTP
+ # B should be merged, but C will conflict with B
+ B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+ B.addPatchset(['conflict'])
+ C = self.fake_gerrit.addFakeChange('org/project', 'master', 'C')
+ C.addPatchset(['conflict'])
+ B.addApproval('code-review', 2)
+ C.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(B.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(C.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ self.assertEqual(6, len(self.history)) # A and B jobs
+ self.assertEqual(1, len(self.smtp_messages))
+ self.assertEqual('The merge failed! For more information...',
+ self.smtp_messages[0]['body'])
+
+ @skip("Disabled for early v3 development")
+ def test_default_merge_failure_reports(self):
+ """Check that the default merge failure reports are correct."""
+
+ # A should report success, B should report merge failure.
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ A.addPatchset(['conflict'])
+ B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+ B.addPatchset(['conflict'])
+ A.addApproval('code-review', 2)
+ B.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(B.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ self.assertEqual(3, len(self.history)) # A jobs
+ self.assertEqual(A.reported, 2)
+ self.assertEqual(B.reported, 2)
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(B.data['status'], 'NEW')
+ self.assertIn('Build succeeded', A.messages[1])
+ self.assertIn('Merge Failed', B.messages[1])
+ self.assertIn('automatically merged', B.messages[1])
+ self.assertNotIn('logs.example.com', B.messages[1])
+ self.assertNotIn('SKIPPED', B.messages[1])
+
+ @skip("Disabled for early v3 development")
+ def test_swift_instructions(self):
+ "Test that the correct swift instructions are sent to the workers"
+ self.updateConfigLayout(
+ 'tests/fixtures/layout-swift.yaml')
+ self.sched.reconfigure(self.config)
+ self.registerJobs()
+
+ self.launch_server.hold_jobs_in_build = True
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+
+ A.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ self.assertEqual(
+ "https://storage.example.org/V1/AUTH_account/merge_logs/1/1/1/"
+ "gate/test-merge/",
+ self.builds[0].parameters['SWIFT_logs_URL'][:-7])
+ self.assertEqual(5,
+ len(self.builds[0].parameters['SWIFT_logs_HMAC_BODY'].
+ split('\n')))
+ self.assertIn('SWIFT_logs_SIGNATURE', self.builds[0].parameters)
+
+ self.assertEqual(
+ "https://storage.example.org/V1/AUTH_account/logs/1/1/1/"
+ "gate/test-test/",
+ self.builds[1].parameters['SWIFT_logs_URL'][:-7])
+ self.assertEqual(5,
+ len(self.builds[1].parameters['SWIFT_logs_HMAC_BODY'].
+ split('\n')))
+ self.assertIn('SWIFT_logs_SIGNATURE', self.builds[1].parameters)
+
+ self.assertEqual(
+ "https://storage.example.org/V1/AUTH_account/stash/1/1/1/"
+ "gate/test-test/",
+ self.builds[1].parameters['SWIFT_MOSTLY_URL'][:-7])
+ self.assertEqual(5,
+ len(self.builds[1].
+ parameters['SWIFT_MOSTLY_HMAC_BODY'].split('\n')))
+ self.assertIn('SWIFT_MOSTLY_SIGNATURE', self.builds[1].parameters)
+
+ self.launch_server.hold_jobs_in_build = False
+ self.launch_server.release()
+ self.waitUntilSettled()
+
+ def test_client_get_running_jobs(self):
+ "Test that the RPC client can get a list of running jobs"
+ self.launch_server.hold_jobs_in_build = True
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ A.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ client = zuul.rpcclient.RPCClient('127.0.0.1',
+ self.gearman_server.port)
+
+ # Wait for gearman server to send the initial workData back to zuul
+ start = time.time()
+ while True:
+ if time.time() - start > 10:
+ raise Exception("Timeout waiting for gearman server to report "
+ + "back to the client")
+ build = self.launch_client.builds.values()[0]
+ if build.worker.name == "My Worker":
+ break
+ else:
+ time.sleep(0)
+
+ running_items = client.get_running_jobs()
+
+ self.assertEqual(1, len(running_items))
+ running_item = running_items[0]
+ self.assertEqual([], running_item['failing_reasons'])
+ self.assertEqual([], running_item['items_behind'])
+ self.assertEqual('https://hostname/1', running_item['url'])
+ self.assertEqual(None, running_item['item_ahead'])
+ self.assertEqual('org/project', running_item['project'])
+ self.assertEqual(None, running_item['remaining_time'])
+ self.assertEqual(True, running_item['active'])
+ self.assertEqual('1,1', running_item['id'])
+
+ self.assertEqual(3, len(running_item['jobs']))
+ for job in running_item['jobs']:
+ if job['name'] == 'project-merge':
+ self.assertEqual('project-merge', job['name'])
+ self.assertEqual('gate', job['pipeline'])
+ self.assertEqual(False, job['retry'])
+ self.assertEqual('https://server/job/project-merge/0/',
+ job['url'])
+ self.assertEqual(7, len(job['worker']))
+ self.assertEqual(False, job['canceled'])
+ self.assertEqual(True, job['voting'])
+ self.assertEqual(None, job['result'])
+ self.assertEqual('gate', job['pipeline'])
+ break
+
+ self.launch_server.hold_jobs_in_build = False
+ self.launch_server.release()
+ self.waitUntilSettled()
+
+ running_items = client.get_running_jobs()
+ self.assertEqual(0, len(running_items))
+
+ def test_nonvoting_pipeline(self):
+ "Test that a nonvoting pipeline (experimental) can still report"
+
+ A = self.fake_gerrit.addFakeChange('org/experimental-project',
+ 'master', 'A')
+ self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+
+ self.assertEqual(self.getJobFromHistory('project-merge').result,
+ 'SUCCESS')
+ self.assertEqual(
+ self.getJobFromHistory('experimental-project-test').result,
+ 'SUCCESS')
+ self.assertEqual(A.reported, 1)
+
+ def test_crd_gate(self):
+ "Test cross-repo dependencies"
+ A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project2', 'master', 'B')
+ A.addApproval('code-review', 2)
+ B.addApproval('code-review', 2)
+
+ AM2 = self.fake_gerrit.addFakeChange('org/project1', 'master', 'AM2')
+ AM1 = self.fake_gerrit.addFakeChange('org/project1', 'master', 'AM1')
+ AM2.setMerged()
+ AM1.setMerged()
+
+ BM2 = self.fake_gerrit.addFakeChange('org/project2', 'master', 'BM2')
+ BM1 = self.fake_gerrit.addFakeChange('org/project2', 'master', 'BM1')
+ BM2.setMerged()
+ BM1.setMerged()
+
+ # A -> AM1 -> AM2
+ # B -> BM1 -> BM2
+ # A Depends-On: B
+ # M2 is here to make sure it is never queried. If it is, it
+ # means zuul is walking down the entire history of merged
+ # changes.
+
+ B.setDependsOn(BM1, 1)
+ BM1.setDependsOn(BM2, 1)
+
+ A.setDependsOn(AM1, 1)
+ AM1.setDependsOn(AM2, 1)
+
+ A.data['commitMessage'] = '%s\n\nDepends-On: %s\n' % (
+ A.subject, B.data['id'])
+
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ self.assertEqual(A.data['status'], 'NEW')
+ self.assertEqual(B.data['status'], 'NEW')
+
+ for connection in self.connections.connections.values():
+ connection.maintainCache([])
+
+ self.launch_server.hold_jobs_in_build = True
+ B.addApproval('approved', 1)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.launch_server.hold_jobs_in_build = False
+ self.launch_server.release()
+ self.waitUntilSettled()
+
+ self.assertEqual(AM2.queried, 0)
+ self.assertEqual(BM2.queried, 0)
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(B.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2)
+ self.assertEqual(B.reported, 2)
+
+ changes = self.getJobFromHistory(
+ 'project-merge', 'org/project1').changes
+ self.assertEqual(changes, '2,1 1,1')
+
+ def test_crd_branch(self):
+ "Test cross-repo dependencies in multiple branches"
+
+ self.create_branch('org/project2', 'mp')
+ A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project2', 'master', 'B')
+ C = self.fake_gerrit.addFakeChange('org/project2', 'mp', 'C')
+ C.data['id'] = B.data['id']
+ A.addApproval('code-review', 2)
+ B.addApproval('code-review', 2)
+ C.addApproval('code-review', 2)
+
+ # A Depends-On: B+C
+ A.data['commitMessage'] = '%s\n\nDepends-On: %s\n' % (
+ A.subject, B.data['id'])
+
+ self.launch_server.hold_jobs_in_build = True
+ B.addApproval('approved', 1)
+ C.addApproval('approved', 1)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.launch_server.hold_jobs_in_build = False
+ self.launch_server.release()
+ self.waitUntilSettled()
+
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(B.data['status'], 'MERGED')
+ self.assertEqual(C.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2)
+ self.assertEqual(B.reported, 2)
+ self.assertEqual(C.reported, 2)
+
+ changes = self.getJobFromHistory(
+ 'project-merge', 'org/project1').changes
+ self.assertEqual(changes, '2,1 3,1 1,1')
+
+ def test_crd_multiline(self):
+ "Test multiple depends-on lines in commit"
+ A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project2', 'master', 'B')
+ C = self.fake_gerrit.addFakeChange('org/project2', 'master', 'C')
+ A.addApproval('code-review', 2)
+ B.addApproval('code-review', 2)
+ C.addApproval('code-review', 2)
+
+ # A Depends-On: B+C
+ A.data['commitMessage'] = '%s\n\nDepends-On: %s\nDepends-On: %s\n' % (
+ A.subject, B.data['id'], C.data['id'])
+
+ self.launch_server.hold_jobs_in_build = True
+ B.addApproval('approved', 1)
+ C.addApproval('approved', 1)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.launch_server.hold_jobs_in_build = False
+ self.launch_server.release()
+ self.waitUntilSettled()
+
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(B.data['status'], 'MERGED')
+ self.assertEqual(C.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2)
+ self.assertEqual(B.reported, 2)
+ self.assertEqual(C.reported, 2)
+
+ changes = self.getJobFromHistory(
+ 'project-merge', 'org/project1').changes
+ self.assertEqual(changes, '2,1 3,1 1,1')
+
+ def test_crd_unshared_gate(self):
+ "Test cross-repo dependencies in unshared gate queues"
+ A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+ A.addApproval('code-review', 2)
+ B.addApproval('code-review', 2)
+
+ # A Depends-On: B
+ A.data['commitMessage'] = '%s\n\nDepends-On: %s\n' % (
+ A.subject, B.data['id'])
+
+ # A and B do not share a queue, make sure that A is unable to
+ # enqueue B (and therefore, A is unable to be enqueued).
+ B.addApproval('approved', 1)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ self.assertEqual(A.data['status'], 'NEW')
+ self.assertEqual(B.data['status'], 'NEW')
+ self.assertEqual(A.reported, 0)
+ self.assertEqual(B.reported, 0)
+ self.assertEqual(len(self.history), 0)
+
+ # Enqueue and merge B alone.
+ self.fake_gerrit.addEvent(B.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ self.assertEqual(B.data['status'], 'MERGED')
+ self.assertEqual(B.reported, 2)
+
+ # Now that B is merged, A should be able to be enqueued and
+ # merged.
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2)
+
+ def test_crd_gate_reverse(self):
+ "Test reverse cross-repo dependencies"
+ A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project2', 'master', 'B')
+ A.addApproval('code-review', 2)
+ B.addApproval('code-review', 2)
+
+ # A Depends-On: B
+
+ A.data['commitMessage'] = '%s\n\nDepends-On: %s\n' % (
+ A.subject, B.data['id'])
+
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ self.assertEqual(A.data['status'], 'NEW')
+ self.assertEqual(B.data['status'], 'NEW')
+
+ self.launch_server.hold_jobs_in_build = True
+ A.addApproval('approved', 1)
+ self.fake_gerrit.addEvent(B.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+ self.launch_server.hold_jobs_in_build = False
+ self.launch_server.release()
+ self.waitUntilSettled()
+
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(B.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2)
+ self.assertEqual(B.reported, 2)
+
+ changes = self.getJobFromHistory(
+ 'project-merge', 'org/project1').changes
+ self.assertEqual(changes, '2,1 1,1')
+
+ def test_crd_cycle(self):
+ "Test cross-repo dependency cycles"
+ A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project2', 'master', 'B')
+ A.addApproval('code-review', 2)
+ B.addApproval('code-review', 2)
+
+ # A -> B -> A (via commit-depends)
+
+ A.data['commitMessage'] = '%s\n\nDepends-On: %s\n' % (
+ A.subject, B.data['id'])
+ B.data['commitMessage'] = '%s\n\nDepends-On: %s\n' % (
+ B.subject, A.data['id'])
+
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ self.assertEqual(A.reported, 0)
+ self.assertEqual(B.reported, 0)
+ self.assertEqual(A.data['status'], 'NEW')
+ self.assertEqual(B.data['status'], 'NEW')
+
+ def test_crd_gate_unknown(self):
+ "Test unknown projects in dependent pipeline"
+ self.init_repo("org/unknown")
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/unknown', 'master', 'B')
+ A.addApproval('code-review', 2)
+ B.addApproval('code-review', 2)
+
+ # A Depends-On: B
+ A.data['commitMessage'] = '%s\n\nDepends-On: %s\n' % (
+ A.subject, B.data['id'])
+
+ B.addApproval('approved', 1)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ # Unknown projects cannot share a queue with any other
+ # since they don't have common jobs with any other (they have no jobs).
+ # Changes which depend on unknown project changes
+ # should not be processed in dependent pipeline
+ self.assertEqual(A.data['status'], 'NEW')
+ self.assertEqual(B.data['status'], 'NEW')
+ self.assertEqual(A.reported, 0)
+ self.assertEqual(B.reported, 0)
+ self.assertEqual(len(self.history), 0)
+
+ # Simulate change B being gated outside this layout
+ self.fake_gerrit.addEvent(B.addApproval('approved', 1))
+ B.setMerged()
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 0)
+
+ # Now that B is merged, A should be able to be enqueued and
+ # merged.
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2)
+ self.assertEqual(B.data['status'], 'MERGED')
+ self.assertEqual(B.reported, 0)
+
+ def test_crd_check(self):
+ "Test cross-repo dependencies in independent pipelines"
+
+ self.launch_server.hold_jobs_in_build = True
+ self.gearman_server.hold_jobs_in_queue = True
+ A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project2', 'master', 'B')
+
+ # A Depends-On: B
+ A.data['commitMessage'] = '%s\n\nDepends-On: %s\n' % (
+ A.subject, B.data['id'])
+
+ self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+
+ queue = self.gearman_server.getQueue()
+ ref = self.getParameter(queue[-1], 'ZUUL_REF')
+ self.gearman_server.hold_jobs_in_queue = False
+ self.gearman_server.release()
+ self.waitUntilSettled()
+
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+
+ path = os.path.join(self.builds[0].jobdir.git_root, "org/project1")
+ repo = git.Repo(path)
+ repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
+ repo_messages.reverse()
+ correct_messages = [
+ 'initial commit', 'add content from fixture', 'A-1']
+ self.assertEqual(repo_messages, correct_messages)
+
+ path = os.path.join(self.builds[0].jobdir.git_root, "org/project2")
+ repo = git.Repo(path)
+ repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
+ repo_messages.reverse()
+ correct_messages = [
+ 'initial commit', 'add content from fixture', 'B-1']
+ self.assertEqual(repo_messages, correct_messages)
+
+ self.launch_server.hold_jobs_in_build = False
+ self.launch_server.release()
+ self.waitUntilSettled()
+
+ self.assertEqual(A.data['status'], 'NEW')
+ self.assertEqual(B.data['status'], 'NEW')
+ self.assertEqual(A.reported, 1)
+ self.assertEqual(B.reported, 0)
+
+ self.assertEqual(self.history[0].changes, '2,1 1,1')
+ tenant = self.sched.abide.tenants.get('tenant-one')
+ self.assertEqual(len(tenant.layout.pipelines['check'].queues), 0)
+
+ def test_crd_check_git_depends(self):
+ "Test single-repo dependencies in independent pipelines"
+ self.gearman_server.hold_jobs_in_build = True
+ A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project1', 'master', 'B')
+
+ # Add two git-dependent changes and make sure they both report
+ # success.
+ B.setDependsOn(A, 1)
+ self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+ self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+
+ self.orderedRelease()
+ self.gearman_server.hold_jobs_in_build = False
+ self.waitUntilSettled()
+
+ self.assertEqual(A.data['status'], 'NEW')
+ self.assertEqual(B.data['status'], 'NEW')
+ self.assertEqual(A.reported, 1)
+ self.assertEqual(B.reported, 1)
+
+ self.assertEqual(self.history[0].changes, '1,1')
+ self.assertEqual(self.history[-1].changes, '1,1 2,1')
+ tenant = self.sched.abide.tenants.get('tenant-one')
+ self.assertEqual(len(tenant.layout.pipelines['check'].queues), 0)
+
+ self.assertIn('Build succeeded', A.messages[0])
+ self.assertIn('Build succeeded', B.messages[0])
+
+ def test_crd_check_duplicate(self):
+ "Test duplicate check in independent pipelines"
+ self.launch_server.hold_jobs_in_build = True
+ A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project1', 'master', 'B')
+ tenant = self.sched.abide.tenants.get('tenant-one')
+ check_pipeline = tenant.layout.pipelines['check']
+
+ # Add two git-dependent changes...
+ B.setDependsOn(A, 1)
+ self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+ self.assertEqual(len(check_pipeline.getAllItems()), 2)
+
+ # ...make sure the live one is not duplicated...
+ self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+ self.assertEqual(len(check_pipeline.getAllItems()), 2)
+
+ # ...but the non-live one is able to be.
+ self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+ self.assertEqual(len(check_pipeline.getAllItems()), 3)
+
+ # Release jobs in order to avoid races with change A jobs
+ # finishing before change B jobs.
+ self.orderedRelease()
+ self.launch_server.hold_jobs_in_build = False
+ self.launch_server.release()
+ self.waitUntilSettled()
+
+ self.assertEqual(A.data['status'], 'NEW')
+ self.assertEqual(B.data['status'], 'NEW')
+ self.assertEqual(A.reported, 1)
+ self.assertEqual(B.reported, 1)
+
+ self.assertEqual(self.history[0].changes, '1,1 2,1')
+ self.assertEqual(self.history[1].changes, '1,1')
+ self.assertEqual(len(tenant.layout.pipelines['check'].queues), 0)
+
+ self.assertIn('Build succeeded', A.messages[0])
+ self.assertIn('Build succeeded', B.messages[0])
+
+ def _test_crd_check_reconfiguration(self, project1, project2):
+ "Test cross-repo dependencies re-enqueued in independent pipelines"
+
+ self.gearman_server.hold_jobs_in_queue = True
+ A = self.fake_gerrit.addFakeChange(project1, 'master', 'A')
+ B = self.fake_gerrit.addFakeChange(project2, 'master', 'B')
+
+ # A Depends-On: B
+ A.data['commitMessage'] = '%s\n\nDepends-On: %s\n' % (
+ A.subject, B.data['id'])
+
+ self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+
+ self.sched.reconfigure(self.config)
+
+ # Make sure the items still share a change queue, and the
+ # first one is not live.
+ tenant = self.sched.abide.tenants.get('tenant-one')
+ self.assertEqual(len(tenant.layout.pipelines['check'].queues), 1)
+ queue = tenant.layout.pipelines['check'].queues[0]
+ first_item = queue.queue[0]
+ for item in queue.queue:
+ self.assertEqual(item.queue, first_item.queue)
+ self.assertFalse(first_item.live)
+ self.assertTrue(queue.queue[1].live)
+
+ self.gearman_server.hold_jobs_in_queue = False
+ self.gearman_server.release()
+ self.waitUntilSettled()
+
+ self.assertEqual(A.data['status'], 'NEW')
+ self.assertEqual(B.data['status'], 'NEW')
+ self.assertEqual(A.reported, 1)
+ self.assertEqual(B.reported, 0)
+
+ self.assertEqual(self.history[0].changes, '2,1 1,1')
+ self.assertEqual(len(tenant.layout.pipelines['check'].queues), 0)
+
+ def test_crd_check_reconfiguration(self):
+ self._test_crd_check_reconfiguration('org/project1', 'org/project2')
+
+ def test_crd_undefined_project(self):
+ """Test that undefined projects in dependencies are handled for
+ independent pipelines"""
+ # It's a hack for fake gerrit,
+ # as it implies repo creation upon the creation of any change
+ self.init_repo("org/unknown")
+ self._test_crd_check_reconfiguration('org/project1', 'org/unknown')
+
+ @skip("Disabled for early v3 development")
+ def test_crd_check_ignore_dependencies(self):
+ "Test cross-repo dependencies can be ignored"
+ self.updateConfigLayout(
+ 'tests/fixtures/layout-ignore-dependencies.yaml')
+ self.sched.reconfigure(self.config)
+ self.registerJobs()
+
+ self.gearman_server.hold_jobs_in_queue = True
+ A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project2', 'master', 'B')
+ C = self.fake_gerrit.addFakeChange('org/project2', 'master', 'C')
+
+ # A Depends-On: B
+ A.data['commitMessage'] = '%s\n\nDepends-On: %s\n' % (
+ A.subject, B.data['id'])
+ # C git-depends on B
+ C.setDependsOn(B, 1)
+ self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+ self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1))
+ self.fake_gerrit.addEvent(C.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+
+ # Make sure none of the items share a change queue, and all
+ # are live.
+ check_pipeline = self.sched.layout.pipelines['check']
+ self.assertEqual(len(check_pipeline.queues), 3)
+ self.assertEqual(len(check_pipeline.getAllItems()), 3)
+ for item in check_pipeline.getAllItems():
+ self.assertTrue(item.live)
+
+ self.gearman_server.hold_jobs_in_queue = False
+ self.gearman_server.release()
+ self.waitUntilSettled()
+
+ self.assertEqual(A.data['status'], 'NEW')
+ self.assertEqual(B.data['status'], 'NEW')
+ self.assertEqual(C.data['status'], 'NEW')
+ self.assertEqual(A.reported, 1)
+ self.assertEqual(B.reported, 1)
+ self.assertEqual(C.reported, 1)
+
+ # Each job should have tested exactly one change
+ for job in self.history:
+ self.assertEqual(len(job.changes.split()), 1)
+
+ @skip("Disabled for early v3 development")
+ def test_crd_check_transitive(self):
+ "Test transitive cross-repo dependencies"
+ # Specifically, if A -> B -> C, and C gets a new patchset and
+ # A gets a new patchset, ensure the test of A,2 includes B,1
+ # and C,2 (not C,1 which would indicate stale data in the
+ # cache for B).
+ A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project2', 'master', 'B')
+ C = self.fake_gerrit.addFakeChange('org/project3', 'master', 'C')
+
+ # A Depends-On: B
+ A.data['commitMessage'] = '%s\n\nDepends-On: %s\n' % (
+ A.subject, B.data['id'])
+
+ # B Depends-On: C
+ B.data['commitMessage'] = '%s\n\nDepends-On: %s\n' % (
+ B.subject, C.data['id'])
+
+ self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+ self.assertEqual(self.history[-1].changes, '3,1 2,1 1,1')
+
+ self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+ self.assertEqual(self.history[-1].changes, '3,1 2,1')
+
+ self.fake_gerrit.addEvent(C.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+ self.assertEqual(self.history[-1].changes, '3,1')
+
+ C.addPatchset()
+ self.fake_gerrit.addEvent(C.getPatchsetCreatedEvent(2))
+ self.waitUntilSettled()
+ self.assertEqual(self.history[-1].changes, '3,2')
+
+ A.addPatchset()
+ self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(2))
+ self.waitUntilSettled()
+ self.assertEqual(self.history[-1].changes, '3,2 2,1 1,2')
+
+ def test_crd_check_unknown(self):
+ "Test unknown projects in independent pipeline"
+ self.init_repo("org/unknown")
+ A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/unknown', 'master', 'D')
+ # A Depends-On: B
+ A.data['commitMessage'] = '%s\n\nDepends-On: %s\n' % (
+ A.subject, B.data['id'])
+
+ # Make sure zuul has seen an event on B.
+ self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1))
+ self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+
+ self.assertEqual(A.data['status'], 'NEW')
+ self.assertEqual(A.reported, 1)
+ self.assertEqual(B.data['status'], 'NEW')
+ self.assertEqual(B.reported, 0)
+
+ def test_crd_cycle_join(self):
+ "Test an updated change creates a cycle"
+ A = self.fake_gerrit.addFakeChange('org/project2', 'master', 'A')
+
+ self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+
+ # Create B->A
+ B = self.fake_gerrit.addFakeChange('org/project1', 'master', 'B')
+ B.data['commitMessage'] = '%s\n\nDepends-On: %s\n' % (
+ B.subject, A.data['id'])
+ self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+
+ # Update A to add A->B (a cycle).
+ A.addPatchset()
+ A.data['commitMessage'] = '%s\n\nDepends-On: %s\n' % (
+ A.subject, B.data['id'])
+ # Normally we would submit the patchset-created event for
+ # processing here, however, we have no way of noting whether
+ # the dependency cycle detection correctly raised an
+ # exception, so instead, we reach into the source driver and
+ # call the method that would ultimately be called by the event
+ # processing.
+
+ tenant = self.sched.abide.tenants.get('tenant-one')
+ source = tenant.layout.pipelines['gate'].source
+
+ # TODO(pabelanger): As we add more source / trigger APIs we should make
+ # it easier for users to create events for testing.
+ event = zuul.model.TriggerEvent()
+ event.trigger_name = 'gerrit'
+ event.change_number = '1'
+ event.patch_number = '2'
+ with testtools.ExpectedException(
+ Exception, "Dependency cycle detected"):
+ source.getChange(event, True)
+ self.log.debug("Got expected dependency cycle exception")
+
+ # Now if we update B to remove the depends-on, everything
+ # should be okay. B; A->B
+
+ B.addPatchset()
+ B.data['commitMessage'] = '%s\n' % (B.subject,)
+
+ source.getChange(event, True)
+ event.change_number = '2'
+ source.getChange(event, True)
+
+ def test_disable_at(self):
+ "Test a pipeline will only report to the disabled trigger when failing"
+
+ self.updateConfigLayout('layout-disabled-at')
+ self.sched.reconfigure(self.config)
+
+ tenant = self.sched.abide.tenants.get('openstack')
+ self.assertEqual(3, tenant.layout.pipelines['check'].disable_at)
+ self.assertEqual(
+ 0, tenant.layout.pipelines['check']._consecutive_failures)
+ self.assertFalse(tenant.layout.pipelines['check']._disabled)
+
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+ C = self.fake_gerrit.addFakeChange('org/project', 'master', 'C')
+ D = self.fake_gerrit.addFakeChange('org/project', 'master', 'D')
+ E = self.fake_gerrit.addFakeChange('org/project', 'master', 'E')
+ F = self.fake_gerrit.addFakeChange('org/project', 'master', 'F')
+ G = self.fake_gerrit.addFakeChange('org/project', 'master', 'G')
+ H = self.fake_gerrit.addFakeChange('org/project', 'master', 'H')
+ I = self.fake_gerrit.addFakeChange('org/project', 'master', 'I')
+ J = self.fake_gerrit.addFakeChange('org/project', 'master', 'J')
+ K = self.fake_gerrit.addFakeChange('org/project', 'master', 'K')
+
+ self.launch_server.failJob('project-test1', A)
+ self.launch_server.failJob('project-test1', B)
+ # Let C pass, resetting the counter
+ self.launch_server.failJob('project-test1', D)
+ self.launch_server.failJob('project-test1', E)
+ self.launch_server.failJob('project-test1', F)
+ self.launch_server.failJob('project-test1', G)
+ self.launch_server.failJob('project-test1', H)
+ # I also passes but should only report to the disabled reporters
+ self.launch_server.failJob('project-test1', J)
+ self.launch_server.failJob('project-test1', K)
+
+ self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+ self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+
+ self.assertEqual(
+ 2, tenant.layout.pipelines['check']._consecutive_failures)
+ self.assertFalse(tenant.layout.pipelines['check']._disabled)
+
+ self.fake_gerrit.addEvent(C.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+
+ self.assertEqual(
+ 0, tenant.layout.pipelines['check']._consecutive_failures)
+ self.assertFalse(tenant.layout.pipelines['check']._disabled)
+
+ self.fake_gerrit.addEvent(D.getPatchsetCreatedEvent(1))
+ self.fake_gerrit.addEvent(E.getPatchsetCreatedEvent(1))
+ self.fake_gerrit.addEvent(F.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+
+ # We should be disabled now
+ self.assertEqual(
+ 3, tenant.layout.pipelines['check']._consecutive_failures)
+ self.assertTrue(tenant.layout.pipelines['check']._disabled)
+
+ # We need to wait between each of these patches to make sure the
+ # smtp messages come back in an expected order
+ self.fake_gerrit.addEvent(G.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+ self.fake_gerrit.addEvent(H.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+ self.fake_gerrit.addEvent(I.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+
+ # The first 6 (ABCDEF) jobs should have reported back to gerrt thus
+ # leaving a message on each change
+ self.assertEqual(1, len(A.messages))
+ self.assertIn('Build failed.', A.messages[0])
+ self.assertEqual(1, len(B.messages))
+ self.assertIn('Build failed.', B.messages[0])
+ self.assertEqual(1, len(C.messages))
+ self.assertIn('Build succeeded.', C.messages[0])
+ self.assertEqual(1, len(D.messages))
+ self.assertIn('Build failed.', D.messages[0])
+ self.assertEqual(1, len(E.messages))
+ self.assertIn('Build failed.', E.messages[0])
+ self.assertEqual(1, len(F.messages))
+ self.assertIn('Build failed.', F.messages[0])
+
+ # The last 3 (GHI) would have only reported via smtp.
+ self.assertEqual(3, len(self.smtp_messages))
+ self.assertEqual(0, len(G.messages))
+ self.assertIn('Build failed.', self.smtp_messages[0]['body'])
+ self.assertIn(
+ 'project-test1 https://server/job', self.smtp_messages[0]['body'])
+ self.assertEqual(0, len(H.messages))
+ self.assertIn('Build failed.', self.smtp_messages[1]['body'])
+ self.assertIn(
+ 'project-test1 https://server/job', self.smtp_messages[1]['body'])
+ self.assertEqual(0, len(I.messages))
+ self.assertIn('Build succeeded.', self.smtp_messages[2]['body'])
+ self.assertIn(
+ 'project-test1 https://server/job', self.smtp_messages[2]['body'])
+
+ # Now reload the configuration (simulate a HUP) to check the pipeline
+ # comes out of disabled
+ self.sched.reconfigure(self.config)
+
+ tenant = self.sched.abide.tenants.get('openstack')
+
+ self.assertEqual(3, tenant.layout.pipelines['check'].disable_at)
+ self.assertEqual(
+ 0, tenant.layout.pipelines['check']._consecutive_failures)
+ self.assertFalse(tenant.layout.pipelines['check']._disabled)
+
+ self.fake_gerrit.addEvent(J.getPatchsetCreatedEvent(1))
+ self.fake_gerrit.addEvent(K.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+
+ self.assertEqual(
+ 2, tenant.layout.pipelines['check']._consecutive_failures)
+ self.assertFalse(tenant.layout.pipelines['check']._disabled)
+
+ # J and K went back to gerrit
+ self.assertEqual(1, len(J.messages))
+ self.assertIn('Build failed.', J.messages[0])
+ self.assertEqual(1, len(K.messages))
+ self.assertIn('Build failed.', K.messages[0])
+ # No more messages reported via smtp
+ self.assertEqual(3, len(self.smtp_messages))
+
+ def test_rerun_on_abort(self):
+ "Test that if a launch server fails to run a job, it is run again"
+
+ self.launch_server.hold_jobs_in_build = True
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+
+ self.launch_server.release('.*-merge')
+ self.waitUntilSettled()
+
+ self.assertEqual(len(self.builds), 2)
+ self.builds[0].requeue = True
+ self.launch_server.release('.*-test*')
+ self.waitUntilSettled()
+
+ for x in range(3):
+ self.assertEqual(len(self.builds), 1,
+ 'len of builds at x=%d is wrong' % x)
+ self.builds[0].requeue = True
+ self.launch_server.release('.*-test1')
+ self.waitUntilSettled()
+
+ self.launch_server.hold_jobs_in_build = False
+ self.launch_server.release()
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 6)
+ self.assertEqual(self.countJobResults(self.history, 'SUCCESS'), 2)
+ self.assertEqual(A.reported, 1)
+ self.assertIn('RETRY_LIMIT', A.messages[0])
+
+ def test_zookeeper_disconnect(self):
+ "Test that jobs are launched after a zookeeper disconnect"
+
+ self.fake_nodepool.paused = True
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ A.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ self.zk.client.stop()
+ self.zk.client.start()
+ self.fake_nodepool.paused = False
+ self.waitUntilSettled()
+
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2)
+
+ def test_nodepool_failure(self):
+ "Test that jobs are reported after a nodepool failure"
+
+ self.fake_nodepool.paused = True
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ A.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ req = self.fake_nodepool.getNodeRequests()[0]
+ self.fake_nodepool.addFailRequest(req)
+
+ self.fake_nodepool.paused = False
+ self.waitUntilSettled()
+
+ self.assertEqual(A.data['status'], 'NEW')
+ self.assertEqual(A.reported, 2)
+ self.assertIn('project-merge : NODE_FAILURE', A.messages[1])
+ self.assertIn('project-test1 : SKIPPED', A.messages[1])
+ self.assertIn('project-test2 : SKIPPED', A.messages[1])
+
+
+class TestDuplicatePipeline(ZuulTestCase):
+ tenant_config_file = 'config/duplicate-pipeline/main.yaml'
+
+ def test_duplicate_pipelines(self):
+ "Test that a change matching multiple pipelines works"
+
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ self.fake_gerrit.addEvent(A.getChangeRestoredEvent())
+ self.waitUntilSettled()
+
+ self.assertHistory([
+ dict(name='project-test1', result='SUCCESS', changes='1,1',
+ pipeline='dup1'),
+ dict(name='project-test1', result='SUCCESS', changes='1,1',
+ pipeline='dup2'),
+ ], ordered=False)
+
+ self.assertEqual(len(A.messages), 2)
+
+ if 'dup1' in A.messages[0]:
+ self.assertIn('dup1', A.messages[0])
+ self.assertNotIn('dup2', A.messages[0])
+ self.assertIn('project-test1', A.messages[0])
+ self.assertIn('dup2', A.messages[1])
+ self.assertNotIn('dup1', A.messages[1])
+ self.assertIn('project-test1', A.messages[1])
+ else:
+ self.assertIn('dup1', A.messages[1])
+ self.assertNotIn('dup2', A.messages[1])
+ self.assertIn('project-test1', A.messages[1])
+ self.assertIn('dup2', A.messages[0])
+ self.assertNotIn('dup1', A.messages[0])
+ self.assertIn('project-test1', A.messages[0])
+
+
+class TestSchedulerOneJobProject(ZuulTestCase):
+ tenant_config_file = 'config/one-job-project/main.yaml'
+
+ def test_one_job_project(self):
+ "Test that queueing works with one job"
+ A = self.fake_gerrit.addFakeChange('org/one-job-project',
+ 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/one-job-project',
+ 'master', 'B')
+ A.addApproval('code-review', 2)
+ B.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(B.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2)
+ self.assertEqual(B.data['status'], 'MERGED')
+ self.assertEqual(B.reported, 2)
+
+
+class TestSchedulerTemplatedProject(ZuulTestCase):
+ tenant_config_file = 'config/templated-project/main.yaml'
+
+ def test_job_from_templates_launched(self):
+ "Test whether a job generated via a template can be launched"
+
+ A = self.fake_gerrit.addFakeChange(
+ 'org/templated-project', 'master', 'A')
+ self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+
+ self.assertEqual(self.getJobFromHistory('project-test1').result,
+ 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('project-test2').result,
+ 'SUCCESS')
+
+ def test_layered_templates(self):
+ "Test whether a job generated via a template can be launched"
+
+ A = self.fake_gerrit.addFakeChange(
+ 'org/layered-project', 'master', 'A')
+ self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+
+ self.assertEqual(self.getJobFromHistory('project-test1').result,
+ 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('project-test2').result,
+ 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('layered-project-test3'
+ ).result, 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('layered-project-test4'
+ ).result, 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('layered-project-foo-test5'
+ ).result, 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('project-test6').result,
+ 'SUCCESS')
+
+
+class TestSchedulerSuccessURL(ZuulTestCase):
+ tenant_config_file = 'config/success-url/main.yaml'
+
+ def test_success_url(self):
+ "Ensure bad build params are ignored"
+ self.sched.reconfigure(self.config)
+ self.init_repo('org/docs')
+
+ A = self.fake_gerrit.addFakeChange('org/docs', 'master', 'A')
+ self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+
+ # Both builds ran: docs-draft-test + docs-draft-test2
+ self.assertEqual(len(self.history), 2)
+
+ # Grab build id
+ for build in self.history:
+ if build.name == 'docs-draft-test':
+ uuid = build.uuid[:7]
+ break
+
+ # Two msgs: 'Starting...' + results
+ self.assertEqual(len(self.smtp_messages), 2)
+ body = self.smtp_messages[1]['body'].splitlines()
+ self.assertEqual('Build succeeded.', body[0])
+
+ self.assertIn(
+ '- docs-draft-test http://docs-draft.example.org/1/1/1/check/'
+ 'docs-draft-test/%s/publish-docs/' % uuid,
+ body[2])
+
+ # NOTE: This default URL is currently hard-coded in launcher/server.py
+ self.assertIn(
+ '- docs-draft-test2 https://server/job',
+ body[3])
+
+
+class TestSchedulerMergeModes(ZuulTestCase):
+ tenant_config_file = 'config/merge-modes/main.yaml'
+
+ def _test_project_merge_mode(self, mode):
+ self.launch_server.keep_jobdir = False
+ project = 'org/project-%s' % mode
+ self.launch_server.hold_jobs_in_build = True
+ A = self.fake_gerrit.addFakeChange(project, 'master', 'A')
+ B = self.fake_gerrit.addFakeChange(project, 'master', 'B')
+ C = self.fake_gerrit.addFakeChange(project, 'master', 'C')
+ A.addApproval('code-review', 2)
+ B.addApproval('code-review', 2)
+ C.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(B.addApproval('approved', 1))
+ self.fake_gerrit.addEvent(C.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ build = self.builds[-1]
+ ref = self.getParameter(build, 'ZUUL_REF')
+
+ path = os.path.join(build.jobdir.git_root, project)
+ repo = git.Repo(path)
+ repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
+ repo_messages.reverse()
+
+ self.launch_server.hold_jobs_in_build = False
+ self.launch_server.release()
+ self.waitUntilSettled()
+
+ return repo_messages
+
+ def _test_merge(self, mode):
+ us_path = os.path.join(
+ self.upstream_root, 'org/project-%s' % mode)
+ expected_messages = [
+ 'initial commit',
+ 'add content from fixture',
+ # the intermediate commits order is nondeterministic
+ "Merge commit 'refs/changes/1/2/1' of %s into HEAD" % us_path,
+ "Merge commit 'refs/changes/1/3/1' of %s into HEAD" % us_path,
+ ]
+ result = self._test_project_merge_mode(mode)
+ self.assertEqual(result[:2], expected_messages[:2])
+ self.assertEqual(result[-2:], expected_messages[-2:])
+
+ def test_project_merge_mode_merge(self):
+ self._test_merge('merge')
+
+ def test_project_merge_mode_merge_resolve(self):
+ self._test_merge('merge-resolve')
+
+ def test_project_merge_mode_cherrypick(self):
+ expected_messages = [
+ 'initial commit',
+ 'add content from fixture',
+ 'A-1',
+ 'B-1',
+ 'C-1']
+ result = self._test_project_merge_mode('cherry-pick')
+ self.assertEqual(result, expected_messages)
diff --git a/tests/unit/test_stack_dump.py b/tests/unit/test_stack_dump.py
new file mode 100644
index 0000000..824e04c
--- /dev/null
+++ b/tests/unit/test_stack_dump.py
@@ -0,0 +1,34 @@
+# Copyright 2013 Hewlett-Packard Development Company, L.P.
+#
+# 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.
+
+import fixtures
+import logging
+import signal
+import testtools
+
+import zuul.cmd
+
+
+class TestStackDump(testtools.TestCase):
+ def setUp(self):
+ super(TestStackDump, self).setUp()
+ self.log_fixture = self.useFixture(
+ fixtures.FakeLogger(level=logging.DEBUG))
+
+ def test_stack_dump_logs(self):
+ "Test that stack dumps end up in logs."
+
+ zuul.cmd.stack_dump_handler(signal.SIGUSR2, None)
+ self.assertIn("Thread", self.log_fixture.output)
+ self.assertIn("test_stack_dump_logs", self.log_fixture.output)
diff --git a/tests/unit/test_v3.py b/tests/unit/test_v3.py
new file mode 100644
index 0000000..8853302
--- /dev/null
+++ b/tests/unit/test_v3.py
@@ -0,0 +1,112 @@
+#!/usr/bin/env python
+
+# Copyright 2012 Hewlett-Packard Development Company, L.P.
+#
+# 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.
+
+import logging
+import textwrap
+
+from tests.base import AnsibleZuulTestCase
+
+logging.basicConfig(level=logging.DEBUG,
+ format='%(asctime)s %(name)-32s '
+ '%(levelname)-8s %(message)s')
+
+
+class TestMultipleTenants(AnsibleZuulTestCase):
+ # A temporary class to hold new tests while others are disabled
+
+ tenant_config_file = 'config/multi-tenant/main.yaml'
+
+ def test_multiple_tenants(self):
+ A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A')
+ A.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+ self.assertEqual(self.getJobFromHistory('project1-test1').result,
+ 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('python27').result,
+ 'SUCCESS')
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2,
+ "A should report start and success")
+ self.assertIn('tenant-one-gate', A.messages[1],
+ "A should transit tenant-one gate")
+ self.assertNotIn('tenant-two-gate', A.messages[1],
+ "A should *not* transit tenant-two gate")
+
+ B = self.fake_gerrit.addFakeChange('org/project2', 'master', 'B')
+ B.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(B.addApproval('approved', 1))
+ self.waitUntilSettled()
+ self.assertEqual(self.getJobFromHistory('python27',
+ 'org/project2').result,
+ 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('project2-test1').result,
+ 'SUCCESS')
+ self.assertEqual(B.data['status'], 'MERGED')
+ self.assertEqual(B.reported, 2,
+ "B should report start and success")
+ self.assertIn('tenant-two-gate', B.messages[1],
+ "B should transit tenant-two gate")
+ self.assertNotIn('tenant-one-gate', B.messages[1],
+ "B should *not* transit tenant-one gate")
+
+ self.assertEqual(A.reported, 2, "Activity in tenant two should"
+ "not affect tenant one")
+
+
+class TestInRepoConfig(AnsibleZuulTestCase):
+ # A temporary class to hold new tests while others are disabled
+
+ tenant_config_file = 'config/in-repo/main.yaml'
+
+ def test_in_repo_config(self):
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ A.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+ self.assertEqual(self.getJobFromHistory('project-test1').result,
+ 'SUCCESS')
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2,
+ "A should report start and success")
+ self.assertIn('tenant-one-gate', A.messages[1],
+ "A should transit tenant-one gate")
+
+ def test_dynamic_config(self):
+ in_repo_conf = textwrap.dedent(
+ """
+ - job:
+ name: project-test2
+
+ - project:
+ name: org/project
+ tenant-one-gate:
+ jobs:
+ - project-test2
+ """)
+
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A',
+ files={'.zuul.yaml': in_repo_conf})
+ A.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+ self.assertEqual(self.getJobFromHistory('project-test2').result,
+ 'SUCCESS')
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2,
+ "A should report start and success")
+ self.assertIn('tenant-one-gate', A.messages[1],
+ "A should transit tenant-one gate")
diff --git a/tests/unit/test_webapp.py b/tests/unit/test_webapp.py
new file mode 100644
index 0000000..2211d1b
--- /dev/null
+++ b/tests/unit/test_webapp.py
@@ -0,0 +1,87 @@
+#!/usr/bin/env python
+
+# Copyright 2014 Hewlett-Packard Development Company, L.P.
+# Copyright 2014 Rackspace Australia
+#
+# 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.
+
+import json
+
+from six.moves import urllib
+
+from tests.base import ZuulTestCase
+
+
+class TestWebapp(ZuulTestCase):
+ tenant_config_file = 'config/single-tenant/main.yaml'
+
+ def setUp(self):
+ super(TestWebapp, self).setUp()
+ self.launch_server.hold_jobs_in_build = True
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ A.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ B = self.fake_gerrit.addFakeChange('org/project1', 'master', 'B')
+ B.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(B.addApproval('approved', 1))
+ self.waitUntilSettled()
+ self.port = self.webapp.server.socket.getsockname()[1]
+
+ def tearDown(self):
+ self.launch_server.hold_jobs_in_build = False
+ self.launch_server.release()
+ self.waitUntilSettled()
+ super(TestWebapp, self).tearDown()
+
+ def test_webapp_status(self):
+ "Test that we can filter to only certain changes in the webapp."
+
+ req = urllib.request.Request(
+ "http://localhost:%s/tenant-one/status" % self.port)
+ f = urllib.request.urlopen(req)
+ data = json.loads(f.read())
+
+ self.assertIn('pipelines', data)
+
+ def test_webapp_status_compat(self):
+ # testing compat with status.json
+ req = urllib.request.Request(
+ "http://localhost:%s/tenant-one/status.json" % self.port)
+ f = urllib.request.urlopen(req)
+ data = json.loads(f.read())
+
+ self.assertIn('pipelines', data)
+
+ def test_webapp_bad_url(self):
+ # do we 404 correctly
+ req = urllib.request.Request(
+ "http://localhost:%s/status/foo" % self.port)
+ self.assertRaises(urllib.error.HTTPError, urllib.request.urlopen, req)
+
+ def test_webapp_find_change(self):
+ # can we filter by change id
+ req = urllib.request.Request(
+ "http://localhost:%s/tenant-one/status/change/1,1" % self.port)
+ f = urllib.request.urlopen(req)
+ data = json.loads(f.read())
+
+ self.assertEqual(1, len(data), data)
+ self.assertEqual("org/project", data[0]['project'])
+
+ req = urllib.request.Request(
+ "http://localhost:%s/tenant-one/status/change/2,1" % self.port)
+ f = urllib.request.urlopen(req)
+ data = json.loads(f.read())
+
+ self.assertEqual(1, len(data), data)
+ self.assertEqual("org/project1", data[0]['project'], data)
diff --git a/tests/unit/test_zuultrigger.py b/tests/unit/test_zuultrigger.py
new file mode 100644
index 0000000..b36e5a4
--- /dev/null
+++ b/tests/unit/test_zuultrigger.py
@@ -0,0 +1,139 @@
+#!/usr/bin/env python
+
+# Copyright 2014 Hewlett-Packard Development Company, L.P.
+#
+# 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.
+
+import logging
+
+from tests.base import ZuulTestCase
+
+logging.basicConfig(level=logging.DEBUG,
+ format='%(asctime)s %(name)-32s '
+ '%(levelname)-8s %(message)s')
+
+
+class TestZuulTriggerParentChangeEnqueued(ZuulTestCase):
+ tenant_config_file = 'config/zuultrigger/parent-change-enqueued/main.yaml'
+
+ def test_zuul_trigger_parent_change_enqueued(self):
+ "Test Zuul trigger event: parent-change-enqueued"
+ # This test has the following three changes:
+ # B1 -> A; B2 -> A
+ # When A is enqueued in the gate, B1 and B2 should both attempt
+ # to be enqueued in both pipelines. B1 should end up in check
+ # and B2 in gate because of differing pipeline requirements.
+ self.launch_server.hold_jobs_in_build = True
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ B1 = self.fake_gerrit.addFakeChange('org/project', 'master', 'B1')
+ B2 = self.fake_gerrit.addFakeChange('org/project', 'master', 'B2')
+ A.addApproval('code-review', 2)
+ B1.addApproval('code-review', 2)
+ B2.addApproval('code-review', 2)
+ A.addApproval('verified', 1) # required by gate
+ B1.addApproval('verified', -1) # should go to check
+ B2.addApproval('verified', 1) # should go to gate
+ B1.addApproval('approved', 1)
+ B2.addApproval('approved', 1)
+ B1.setDependsOn(A, 1)
+ B2.setDependsOn(A, 1)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ # Jobs are being held in build to make sure that 3,1 has time
+ # to enqueue behind 1,1 so that the test is more
+ # deterministic.
+ self.waitUntilSettled()
+ self.launch_server.hold_jobs_in_build = False
+ self.launch_server.release()
+ self.waitUntilSettled()
+
+ self.assertEqual(len(self.history), 3)
+ for job in self.history:
+ if job.changes == '1,1':
+ self.assertEqual(job.name, 'project-gate')
+ elif job.changes == '1,1 2,1':
+ self.assertEqual(job.name, 'project-check')
+ elif job.changes == '1,1 3,1':
+ self.assertEqual(job.name, 'project-gate')
+ else:
+ raise Exception("Unknown job")
+
+
+class TestZuulTriggerProjectChangeMerged(ZuulTestCase):
+
+ def setUp(self):
+ self.skip("Disabled because v3 noop job does not perform merge")
+
+ tenant_config_file = 'config/zuultrigger/project-change-merged/main.yaml'
+
+ def test_zuul_trigger_project_change_merged(self):
+ # This test has the following three changes:
+ # A, B, C; B conflicts with A, but C does not.
+ # When A is merged, B and C should be checked for conflicts,
+ # and B should receive a -1.
+ # D and E are used to repeat the test in the second part, but
+ # are defined here to that they end up in the trigger cache.
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+ C = self.fake_gerrit.addFakeChange('org/project', 'master', 'C')
+ D = self.fake_gerrit.addFakeChange('org/project', 'master', 'D')
+ E = self.fake_gerrit.addFakeChange('org/project', 'master', 'E')
+ A.addPatchset({'conflict': 'foo'})
+ B.addPatchset({'conflict': 'bar'})
+ D.addPatchset({'conflict2': 'foo'})
+ E.addPatchset({'conflict2': 'bar'})
+ A.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ self.assertEqual(len(self.history), 1)
+ self.assertEqual(self.history[0].name, 'project-gate')
+ self.assertEqual(A.reported, 2)
+ self.assertEqual(B.reported, 1)
+ self.assertEqual(C.reported, 0)
+ self.assertEqual(D.reported, 0)
+ self.assertEqual(E.reported, 0)
+ self.assertEqual(
+ B.messages[0],
+ "Merge Failed.\n\nThis change or one of its cross-repo "
+ "dependencies was unable to be automatically merged with the "
+ "current state of its repository. Please rebase the change and "
+ "upload a new patchset.")
+
+ self.assertTrue("project:org/project status:open" in
+ self.fake_gerrit.queries)
+
+ # Reconfigure and run the test again. This is a regression
+ # check to make sure that we don't end up with a stale trigger
+ # cache that has references to projects from the old
+ # configuration.
+ self.sched.reconfigure(self.config)
+
+ D.addApproval('code-review', 2)
+ self.fake_gerrit.addEvent(D.addApproval('approved', 1))
+ self.waitUntilSettled()
+
+ self.assertEqual(len(self.history), 2)
+ self.assertEqual(self.history[1].name, 'project-gate')
+ self.assertEqual(A.reported, 2)
+ self.assertEqual(B.reported, 1)
+ self.assertEqual(C.reported, 0)
+ self.assertEqual(D.reported, 2)
+ self.assertEqual(E.reported, 1)
+ self.assertEqual(
+ E.messages[0],
+ "Merge Failed.\n\nThis change or one of its cross-repo "
+ "dependencies was unable to be automatically merged with the "
+ "current state of its repository. Please rebase the change and "
+ "upload a new patchset.")
+ self.assertEqual(self.fake_gerrit.queries[1],
+ "project:org/project status:open")