diff options
-rw-r--r-- | doc/source/triggers.rst | 4 | ||||
-rw-r--r-- | doc/source/zuul.rst | 41 | ||||
-rw-r--r-- | etc/status/.gitignore | 2 | ||||
-rwxr-xr-x | etc/status/fetch-dependencies.sh | 4 | ||||
-rw-r--r-- | etc/status/public_html/index.html | 3 | ||||
-rw-r--r-- | requirements.txt | 4 | ||||
-rw-r--r-- | test-requirements.txt | 3 | ||||
-rwxr-xr-x | tests/base.py | 48 | ||||
-rw-r--r-- | tests/test_cloner.py | 177 | ||||
-rw-r--r-- | tests/test_daemon.py | 63 | ||||
-rwxr-xr-x | tests/test_scheduler.py | 8 | ||||
-rw-r--r-- | tests/test_zuultrigger.py | 20 | ||||
-rw-r--r-- | tox.ini | 7 | ||||
-rwxr-xr-x | zuul/cmd/cloner.py | 8 | ||||
-rwxr-xr-x | zuul/cmd/server.py | 5 | ||||
-rw-r--r-- | zuul/launcher/gearman.py | 19 | ||||
-rw-r--r-- | zuul/layoutvalidator.py | 13 | ||||
-rw-r--r-- | zuul/lib/cloner.py | 7 | ||||
-rw-r--r-- | zuul/lib/gerrit.py | 3 | ||||
-rw-r--r-- | zuul/lib/swift.py | 3 | ||||
-rw-r--r-- | zuul/merger/client.py | 3 | ||||
-rw-r--r-- | zuul/merger/merger.py | 2 | ||||
-rw-r--r-- | zuul/model.py | 8 | ||||
-rw-r--r-- | zuul/rpcclient.py | 2 | ||||
-rw-r--r-- | zuul/scheduler.py | 50 | ||||
-rw-r--r-- | zuul/trigger/gerrit.py | 14 | ||||
-rw-r--r-- | zuul/trigger/zuultrigger.py | 13 |
27 files changed, 373 insertions, 161 deletions
diff --git a/doc/source/triggers.rst b/doc/source/triggers.rst index dd650f2ff..5b745e643 100644 --- a/doc/source/triggers.rst +++ b/doc/source/triggers.rst @@ -34,6 +34,10 @@ want Zuul to gate. For instance, you may want to grant ``Verified be added to Gerrit. Zuul is very flexible and can take advantage of those. +If using Gerrit 2.7 or later, make sure the user is a member of a group +that is granted the ``Stream Events`` permission, otherwise it will not +be able to invoke the ``gerrit stream-events`` command over SSH. + Timer ----- diff --git a/doc/source/zuul.rst b/doc/source/zuul.rst index 6cb5d5902..2883253d5 100644 --- a/doc/source/zuul.rst +++ b/doc/source/zuul.rst @@ -86,6 +86,8 @@ gerrit zuul """" +.. _layout_config: + **layout_config** Path to layout config file. Used by zuul-server only. ``layout_config=/etc/zuul/layout.yaml`` @@ -272,10 +274,12 @@ include, and currently supports one type of inclusion, a python file:: - python-file: local_functions.py **python-file** - The path to a python file. The file will be loaded and objects that - it defines will be placed in a special environment which can be - referenced in the Zuul configuration. Currently only the - parameter-function attribute of a Job uses this feature. + The path to a python file (either an absolute path or relative to the + directory name of :ref:`layout_config <layout_config>`). The + file will be loaded and objects that it defines will be placed in a + special environment which can be referenced in the Zuul configuration. + Currently only the parameter-function attribute of a Job uses this + feature. Pipelines """"""""" @@ -462,7 +466,8 @@ explanation of each of the parameters:: This may be used for any event. It requires that a certain kind of approval be present for the current patchset of the change (the approval could be added by the event in question). It follows the - same syntax as the "approval" pipeline requirement below. + same syntax as the :ref:`"approval" pipeline requirement below + <pipeline-require-approval>`. **timer** This trigger will run based on a cron-style time specification. @@ -497,7 +502,8 @@ explanation of each of the parameters:: This may be used for any event. It requires that a certain kind of approval be present for the current patchset of the change (the approval could be added by the event in question). It follows the - same syntax as the "approval" pipeline requirement below. + same syntax as the :ref:`"approval" pipeline requirement below + <pipeline-require-approval>`. **require** @@ -507,6 +513,8 @@ explanation of each of the parameters:: the conditions specified here must be met or the item will not be enqueued. +.. _pipeline-require-approval: + **approval** This requires that a certain kind of approval be present for the current patchset of the change (the approval could be added by the @@ -1038,10 +1046,19 @@ example, this would give you a list of Gerrit commands to reverify or recheck changes for the gate and check pipelines respectively:: ./tools/zuul-changes.py --review-host=review.openstack.org \ - http://zuul.openstack.org/ gate 'reverify no bug' + http://zuul.openstack.org/ gate 'reverify' ./tools/zuul-changes.py --review-host=review.openstack.org \ - http://zuul.openstack.org/ check 'recheck no bug' - -If you send a SIGUSR2 to the zuul-server process, Zuul will dump a stack -trace for each running thread into its debug log. This is useful for -tracking down deadlock or otherwise slow threads. + http://zuul.openstack.org/ check 'recheck' + +If you send a SIGUSR2 to the zuul-server process, or the forked process +that runs the Gearman daemon, Zuul will dump a stack trace for each +running thread into its debug log. It is written under the log bucket +``zuul.stack_dump``. This is useful for tracking down deadlock or +otherwise slow threads. + +When `yappi <https://code.google.com/p/yappi/>`_ (Yet Another Python +Profiler) is available, additional functions' and threads' stats are +emitted as well. The first SIGUSR2 will enable yappi, on the second +SIGUSR2 it dumps the information collected, resets all yappi state and +stops profiling. This is to minimize the impact of yappi on a running +system. diff --git a/etc/status/.gitignore b/etc/status/.gitignore index 8b94cad18..1ecdbed42 100644 --- a/etc/status/.gitignore +++ b/etc/status/.gitignore @@ -1,4 +1,4 @@ public_html/jquery.min.js -public_html/jquery-visibility.min.js +public_html/jquery-visibility.js public_html/bootstrap public_html/jquery.graphite.js diff --git a/etc/status/fetch-dependencies.sh b/etc/status/fetch-dependencies.sh index 4868310ad..b31d0de5f 100755 --- a/etc/status/fetch-dependencies.sh +++ b/etc/status/fetch-dependencies.sh @@ -3,10 +3,10 @@ BASE_DIR=$(cd $(dirname $0); pwd) echo "Destination: $BASE_DIR/public_html" echo "Fetching jquery.min.js..." -curl --silent http://code.jquery.com/jquery.min.js > $BASE_DIR/public_html/jquery.min.js +curl -L --silent http://code.jquery.com/jquery.min.js > $BASE_DIR/public_html/jquery.min.js echo "Fetching jquery-visibility.min.js..." -curl --silent https://raw.githubusercontent.com/mathiasbynens/jquery-visibility/master/jquery-visibility.js > $BASE_DIR/public_html/jquery-visibility.min.js +curl -L --silent https://raw.githubusercontent.com/mathiasbynens/jquery-visibility/master/jquery-visibility.js > $BASE_DIR/public_html/jquery-visibility.js echo "Fetching jquery.graphite.js..." curl -L --silent https://github.com/prestontimmons/graphitejs/archive/master.zip > jquery-graphite.zip diff --git a/etc/status/public_html/index.html b/etc/status/public_html/index.html index d77470bb7..3bd7a12fc 100644 --- a/etc/status/public_html/index.html +++ b/etc/status/public_html/index.html @@ -20,7 +20,6 @@ under the License. <head> <title>Zuul Status</title> <link rel="stylesheet" href="bootstrap/css/bootstrap.min.css"> - <link rel="stylesheet" href="bootstrap/css/bootstrap-responsive.min.css"> <link rel="stylesheet" href="styles/zuul.css" /> </head> <body> @@ -28,7 +27,7 @@ under the License. <div id="zuul_container"></div> <script src="jquery.min.js"></script> - <script src="jquery-visibility.min.js"></script> + <script src="jquery-visibility.js"></script> <script src="jquery.graphite.js"></script> <script src="jquery.zuul.js"></script> <script src="zuul.app.js"></script> diff --git a/requirements.txt b/requirements.txt index dd947d67f..b24d1710f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,10 +5,10 @@ PyYAML>=3.1.0 Paste WebOb>=1.2.3,<1.3 paramiko>=1.8.0 -GitPython==0.3.2.RC1 +GitPython>=0.3.2.1 lockfile>=0.8 ordereddict -python-daemon +python-daemon>=2.0.4 extras statsd>=1.0.0,<3.0 voluptuous>=0.7 diff --git a/test-requirements.txt b/test-requirements.txt index 5192de701..c68b2db0a 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,9 +1,8 @@ hacking>=0.9.2,<0.10 coverage>=3.6 -sphinx>=1.1.2,<1.2 +sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 sphinxcontrib-blockdiag>=0.5.5 -docutils==0.9.1 discover fixtures>=0.3.14 python-keystoneclient>=0.4.2 diff --git a/tests/base.py b/tests/base.py index 46c708787..b872a8581 100755 --- a/tests/base.py +++ b/tests/base.py @@ -40,6 +40,7 @@ import fixtures import six.moves.urllib.parse as urlparse import statsd import testtools +from git import GitCommandError import zuul.scheduler import zuul.webapp @@ -78,6 +79,16 @@ def random_sha1(): return hashlib.sha1(str(random.random())).hexdigest() +def iterate_timeout(max_seconds, purpose): + start = time.time() + count = 0 + while (time.time() < start + max_seconds): + count += 1 + yield count + time.sleep(0) + raise Exception("Timeout waiting for %s" % purpose) + + class ChangeReference(git.Reference): _common_path_default = "refs/changes" _points_to_commits_only = True @@ -251,14 +262,16 @@ class FakeChange(object): granted_on=None): if not granted_on: granted_on = time.time() - approval = {'description': self.categories[category][0], - 'type': category, - 'value': str(value), - 'by': { - 'username': username, - 'email': username + '@example.com', - }, - 'grantedOn': int(granted_on)} + approval = { + 'description': self.categories[category][0], + 'type': category, + 'value': str(value), + 'by': { + 'username': username, + 'email': username + '@example.com', + }, + 'grantedOn': int(granted_on) + } for i, x in enumerate(self.patchsets[-1]['approvals'][:]): if x['by']['username'] == username and x['type'] == category: del self.patchsets[-1]['approvals'][i] @@ -348,7 +361,7 @@ class FakeChange(object): def setMerged(self): if (self.depends_on_change and - self.depends_on_change.data['status'] != 'MERGED'): + self.depends_on_change.data['status'] != 'MERGED'): return if self.fail_merge: return @@ -409,7 +422,7 @@ class FakeGerrit(object): # project self.queries.append(query) l = [change.query() for change in self.changes.values()] - l.append({"type":"stats","rowCount":1,"runTimeMilliseconds":3}) + l.append({"type": "stats", "rowCount": 1, "runTimeMilliseconds": 3}) return l def startWatching(self, *args, **kw): @@ -824,7 +837,8 @@ class ZuulTestCase(testtools.TestCase): '%(levelname)-8s %(message)s')) if USE_TEMPDIR: tmp_root = self.useFixture(fixtures.TempDir( - rootdir=os.environ.get("ZUUL_TEST_ROOT"))).path + rootdir=os.environ.get("ZUUL_TEST_ROOT")) + ).path else: tmp_root = os.environ.get("ZUUL_TEST_ROOT") self.test_root = os.path.join(tmp_root, "zuul-test") @@ -923,7 +937,8 @@ class ZuulTestCase(testtools.TestCase): self.sched.registerTrigger(self.gerrit) self.timer = zuul.trigger.timer.Timer(self.config, self.sched) self.sched.registerTrigger(self.timer) - self.zuultrigger = zuul.trigger.zuultrigger.ZuulTrigger(self.config, self.sched) + self.zuultrigger = zuul.trigger.zuultrigger.ZuulTrigger(self.config, + self.sched) self.sched.registerTrigger(self.zuultrigger) self.sched.registerReporter( @@ -1031,9 +1046,12 @@ class ZuulTestCase(testtools.TestCase): def ref_has_change(self, ref, change): path = os.path.join(self.git_root, change.project) repo = git.Repo(path) - for commit in repo.iter_commits(ref): - if commit.message.strip() == ('%s-1' % change.subject): - return True + try: + for commit in repo.iter_commits(ref): + if commit.message.strip() == ('%s-1' % change.subject): + return True + except GitCommandError: + pass return False def job_has_changes(self, *args): diff --git a/tests/test_cloner.py b/tests/test_cloner.py index ab2683d81..137c1570e 100644 --- a/tests/test_cloner.py +++ b/tests/test_cloner.py @@ -18,13 +18,13 @@ import logging import os import shutil +import time import git import zuul.lib.cloner from tests.base import ZuulTestCase -from tests.base import FIXTURE_DIR logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(name)-32s ' @@ -80,11 +80,10 @@ class TestCloner(ZuulTestCase): B.setMerged() upstream = self.getUpstreamRepos(projects) - states = [ - {'org/project1': self.builds[0].parameters['ZUUL_COMMIT'], - 'org/project2': str(upstream['org/project2'].commit('master')), - }, - ] + 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) @@ -96,7 +95,7 @@ class TestCloner(ZuulTestCase): zuul_ref=build.parameters['ZUUL_REF'], zuul_url=self.git_root, cache_dir=cache_root, - ) + ) cloner.execute() work = self.getWorkspaceRepos(projects) state = states[number] @@ -109,9 +108,11 @@ class TestCloner(ZuulTestCase): 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.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() @@ -140,7 +141,7 @@ class TestCloner(ZuulTestCase): {'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) @@ -151,7 +152,7 @@ class TestCloner(ZuulTestCase): 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] @@ -176,7 +177,8 @@ class TestCloner(ZuulTestCase): 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') + 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) @@ -209,7 +211,7 @@ class TestCloner(ZuulTestCase): 'org/project4': str(upstream['org/project4']. commit('master')), }, - ] + ] for number, build in enumerate(self.builds): self.log.debug("Build parameters: %s", build.parameters) @@ -220,7 +222,7 @@ class TestCloner(ZuulTestCase): 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] @@ -248,9 +250,11 @@ class TestCloner(ZuulTestCase): 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') + 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') + E = self.fake_gerrit.addFakeChange('org/project4', 'stable/havana', + 'E') A.addApproval('CRVW', 2) B.addApproval('CRVW', 2) C.addApproval('CRVW', 2) @@ -270,46 +274,61 @@ class TestCloner(ZuulTestCase): 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/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/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/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/project4': str(upstream['org/project4'].commit( + 'stable/havana')), - 'org/project5': str(upstream['org/project5'].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/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/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/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/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) - change_number = int(build.parameters['ZUUL_CHANGE']) cloner = zuul.lib.cloner.Cloner( git_base_url=self.upstream_root, projects=projects, @@ -317,8 +336,8 @@ class TestCloner(ZuulTestCase): zuul_branch=build.parameters['ZUUL_BRANCH'], zuul_ref=build.parameters['ZUUL_REF'], zuul_url=self.git_root, - branch='stable/havana', # Old branch for upgrade - ) + branch='stable/havana', # Old branch for upgrade + ) cloner.execute() work = self.getWorkspaceRepos(projects) state = states[number] @@ -368,11 +387,10 @@ class TestCloner(ZuulTestCase): '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) - change_number = int(build.parameters['ZUUL_CHANGE']) cloner = zuul.lib.cloner.Cloner( git_base_url=self.upstream_root, projects=projects, @@ -380,8 +398,8 @@ class TestCloner(ZuulTestCase): zuul_branch=build.parameters['ZUUL_BRANCH'], zuul_ref=build.parameters['ZUUL_REF'], zuul_url=self.git_root, - branch='master', # New branch for upgrade - ) + branch='master', # New branch for upgrade + ) cloner.execute() work = self.getWorkspaceRepos(projects) state = states[number] @@ -409,7 +427,8 @@ class TestCloner(ZuulTestCase): 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') + D = self.fake_gerrit.addFakeChange('org/project3', 'stable/havana', + 'D') A.addApproval('CRVW', 2) B.addApproval('CRVW', 2) C.addApproval('CRVW', 2) @@ -451,13 +470,13 @@ class TestCloner(ZuulTestCase): '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')), + 'org/project6': str(upstream['org/project6'].commit( + 'stable/havana')), }, - ] + ] for number, build in enumerate(self.builds): self.log.debug("Build parameters: %s", build.parameters) - change_number = int(build.parameters['ZUUL_CHANGE']) cloner = zuul.lib.cloner.Cloner( git_base_url=self.upstream_root, projects=projects, @@ -466,7 +485,72 @@ class TestCloner(ZuulTestCase): 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.config.set('zuul', 'layout_config', + '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.config.set('zuul', 'layout_config', + '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] @@ -476,6 +560,7 @@ class TestCloner(ZuulTestCase): 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 diff --git a/tests/test_daemon.py b/tests/test_daemon.py new file mode 100644 index 000000000..689d4f709 --- /dev/null +++ b/tests/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/test_scheduler.py b/tests/test_scheduler.py index a7548c132..89056f4cd 100755 --- a/tests/test_scheduler.py +++ b/tests/test_scheduler.py @@ -1427,10 +1427,10 @@ class TestScheduler(ZuulTestCase): self.waitUntilSettled() # For debugging purposes... - #for pipeline in self.sched.layout.pipelines.values(): - # for queue in pipeline.queues: - # self.log.info("pipepline %s queue %s contents %s" % ( - # pipeline.name, queue.name, queue.queue)) + # for pipeline in self.sched.layout.pipelines.values(): + # for queue in pipeline.queues: + # self.log.info("pipepline %s queue %s contents %s" % ( + # pipeline.name, queue.name, queue.queue)) self.worker.release('.*-merge') self.waitUntilSettled() diff --git a/tests/test_zuultrigger.py b/tests/test_zuultrigger.py index 9a90a982e..3f339beff 100644 --- a/tests/test_zuultrigger.py +++ b/tests/test_zuultrigger.py @@ -15,7 +15,6 @@ # under the License. import logging -import time from tests.base import ZuulTestCase @@ -46,9 +45,9 @@ class TestZuulTrigger(ZuulTestCase): A.addApproval('CRVW', 2) B1.addApproval('CRVW', 2) B2.addApproval('CRVW', 2) - A.addApproval('VRFY', 1) # required by gate - B1.addApproval('VRFY', -1) # should go to check - B2.addApproval('VRFY', 1) # should go to gate + A.addApproval('VRFY', 1) # required by gate + B1.addApproval('VRFY', -1) # should go to check + B2.addApproval('VRFY', 1) # should go to gate B1.addApproval('APRV', 1) B2.addApproval('APRV', 1) B1.setDependsOn(A, 1) @@ -106,11 +105,14 @@ class TestZuulTrigger(ZuulTestCase): self.assertEqual(C.reported, 0) self.assertEqual(D.reported, 0) self.assertEqual(E.reported, 0) - self.assertEqual(B.messages[0], + self.assertEqual( + B.messages[0], "Merge Failed.\n\nThis change was unable to be automatically " "merged with the current state of the repository. Please rebase " "your change and upload a new patchset.") - self.assertEqual(self.fake_gerrit.queries[0], "project:org/project status:open") + + self.assertEqual(self.fake_gerrit.queries[0], + "project:org/project status:open") # Reconfigure and run the test again. This is a regression # check to make sure that we don't end up with a stale trigger @@ -129,8 +131,10 @@ class TestZuulTrigger(ZuulTestCase): self.assertEqual(C.reported, 0) self.assertEqual(D.reported, 2) self.assertEqual(E.reported, 1) - self.assertEqual(E.messages[0], + self.assertEqual( + E.messages[0], "Merge Failed.\n\nThis change was unable to be automatically " "merged with the current state of the repository. Please rebase " "your change and upload a new patchset.") - self.assertEqual(self.fake_gerrit.queries[1], "project:org/project status:open") + self.assertEqual(self.fake_gerrit.queries[1], + "project:org/project status:open") @@ -20,7 +20,7 @@ commands = downloadcache = ~/cache/pip [testenv:pep8] -commands = flake8 +commands = flake8 {posargs} [testenv:cover] commands = @@ -36,7 +36,8 @@ commands = {posargs} commands = zuul-server -c etc/zuul.conf-sample -t -l {posargs} [flake8] -ignore = E125,H -select = H231 +# These are ignored intentionally in openstack-infra projects; +# please don't submit patches that solely correct them or enable them. +ignore = E125,E129,H show-source = True exclude = .venv,.tox,dist,doc,build,*.egg diff --git a/zuul/cmd/cloner.py b/zuul/cmd/cloner.py index a895f2433..d0bb96694 100755 --- a/zuul/cmd/cloner.py +++ b/zuul/cmd/cloner.py @@ -65,20 +65,20 @@ class Cloner(zuul.cmd.ZuulApp): project_env = parser.add_argument_group( 'project tuning' - ) + ) project_env.add_argument( '--branch', help=('branch to checkout instead of Zuul selected branch, ' 'for example to specify an alternate branch to test ' 'client library compatibility.') - ) + ) project_env.add_argument( '--project-branch', nargs=1, action='append', metavar='PROJECT=BRANCH', help=('project-specific branch to checkout which takes precedence ' 'over --branch if it is provided; may be specified multiple ' 'times.') - ) + ) zuul_env = parser.add_argument_group( 'zuul environnement', @@ -97,7 +97,7 @@ class Cloner(zuul.cmd.ZuulApp): zuul_missing = [zuul_opt for zuul_opt, val in vars(args).items() if zuul_opt.startswith('zuul') and val is None] if zuul_missing: - parser.error(("Some Zuul parameters are not properly set:\n\t%s\n" + parser.error(("Some Zuul parameters are not set:\n\t%s\n" "Define them either via environment variables or " "using options above." % "\n\t".join(sorted(zuul_missing)))) diff --git a/zuul/cmd/server.py b/zuul/cmd/server.py index 25dab6f42..832eae412 100755 --- a/zuul/cmd/server.py +++ b/zuul/cmd/server.py @@ -150,6 +150,7 @@ class Server(zuul.cmd.ZuulApp): import zuul.webapp import zuul.rpclistener + signal.signal(signal.SIGUSR2, zuul.cmd.stack_dump_handler) if (self.config.has_option('gearman_server', 'start') and self.config.getboolean('gearman_server', 'start')): self.start_gear_server() @@ -165,7 +166,8 @@ class Server(zuul.cmd.ZuulApp): merger = zuul.merger.client.MergeClient(self.config, self.sched) gerrit = zuul.trigger.gerrit.Gerrit(self.config, self.sched) timer = zuul.trigger.timer.Timer(self.config, self.sched) - zuultrigger = zuul.trigger.zuultrigger.ZuulTrigger(self.config, self.sched) + zuultrigger = zuul.trigger.zuultrigger.ZuulTrigger(self.config, + self.sched) if self.config.has_option('zuul', 'status_expiry'): cache_expiry = self.config.getint('zuul', 'status_expiry') else: @@ -203,7 +205,6 @@ class Server(zuul.cmd.ZuulApp): signal.signal(signal.SIGHUP, self.reconfigure_handler) signal.signal(signal.SIGUSR1, self.exit_handler) - signal.signal(signal.SIGUSR2, zuul.cmd.stack_dump_handler) signal.signal(signal.SIGTERM, self.term_handler) while True: try: diff --git a/zuul/launcher/gearman.py b/zuul/launcher/gearman.py index 57db7dc28..564a554ba 100644 --- a/zuul/launcher/gearman.py +++ b/zuul/launcher/gearman.py @@ -128,7 +128,7 @@ class ZuulGearmanClient(gear.Client): for connection in self.active_connections: try: req = gear.StatusAdminRequest() - connection.sendAdminRequest(req) + connection.sendAdminRequest(req, timeout=300) except Exception: self.log.exception("Exception while checking functions") continue @@ -203,7 +203,7 @@ class Gearman(object): for connection in self.gearman.active_connections: try: req = gear.StatusAdminRequest() - connection.sendAdminRequest(req) + connection.sendAdminRequest(req, timeout=300) except Exception: self.log.exception("Exception while checking functions") continue @@ -361,15 +361,16 @@ class Gearman(object): precedence = gear.PRECEDENCE_LOW try: - self.gearman.submitJob(gearman_job, precedence=precedence) + self.gearman.submitJob(gearman_job, precedence=precedence, + timeout=300) except Exception: self.log.exception("Unable to submit job to Gearman") self.onBuildCompleted(gearman_job, 'EXCEPTION') return build if not gearman_job.handle: - self.log.error("No job handle was received for %s after 30 seconds" - " marking as lost." % + self.log.error("No job handle was received for %s after" + " 300 seconds; marking as lost." % gearman_job) self.onBuildCompleted(gearman_job, 'NO_HANDLE') @@ -467,7 +468,7 @@ class Gearman(object): job = build.__gearman_job req = gear.CancelJobAdminRequest(job.handle) - job.connection.sendAdminRequest(req) + job.connection.sendAdminRequest(req, timeout=300) self.log.debug("Response to cancel build %s request: %s" % (build, req.response.strip())) if req.response.startswith("OK"): @@ -486,7 +487,8 @@ class Gearman(object): json.dumps(data), unique=stop_uuid) self.meta_jobs[stop_uuid] = stop_job self.log.debug("Submitting stop job: %s", stop_job) - self.gearman.submitJob(stop_job, precedence=gear.PRECEDENCE_HIGH) + self.gearman.submitJob(stop_job, precedence=gear.PRECEDENCE_HIGH, + timeout=300) return True def setBuildDescription(self, build, desc): @@ -507,7 +509,8 @@ class Gearman(object): desc_job = gear.Job(name, json.dumps(data), unique=desc_uuid) self.meta_jobs[desc_uuid] = desc_job self.log.debug("Submitting describe job: %s", desc_job) - self.gearman.submitJob(desc_job, precedence=gear.PRECEDENCE_LOW) + self.gearman.submitJob(desc_job, precedence=gear.PRECEDENCE_LOW, + timeout=300) return True def lookForLostBuilds(self): diff --git a/zuul/layoutvalidator.py b/zuul/layoutvalidator.py index 696965399..f71c853f4 100644 --- a/zuul/layoutvalidator.py +++ b/zuul/layoutvalidator.py @@ -20,6 +20,7 @@ import string from zuul.trigger import gerrit + # Several forms accept either a single item or a list, this makes # specifying that in the schema easy (and explicit). def toList(x): @@ -152,11 +153,11 @@ class LayoutSchema(object): def validateJob(self, value, path=[]): if isinstance(value, list): - for (i, v) in enumerate(value): - self.validateJob(v, path + [i]) + for (i, val) in enumerate(value): + self.validateJob(val, path + [i]) elif isinstance(value, dict): - for k, v in value.items(): - self.validateJob(v, path + [k]) + for k, val in value.items(): + self.validateJob(val, path + [k]) else: self.job_name.schema(value) @@ -193,6 +194,9 @@ class LayoutSchema(object): return parameters def getSchema(self, data): + if not isinstance(data, dict): + raise Exception("Malformed layout configuration: top-level type " + "should be a dictionary") pipelines = data.get('pipelines') if not pipelines: pipelines = [] @@ -275,4 +279,3 @@ class LayoutValidator(object): for pipeline in data['pipelines']: if 'gerrit' in pipeline['trigger']: gerrit.validate_trigger(pipeline['trigger']) - diff --git a/zuul/lib/cloner.py b/zuul/lib/cloner.py index 89ebada0e..2b35e41a3 100644 --- a/zuul/lib/cloner.py +++ b/zuul/lib/cloner.py @@ -39,8 +39,8 @@ class Cloner(object): self.cache_dir = cache_dir self.projects = projects self.workspace = workspace - self.zuul_branch = zuul_branch - self.zuul_ref = zuul_ref + self.zuul_branch = zuul_branch or '' + self.zuul_ref = zuul_ref or '' self.zuul_url = zuul_url self.project_branches = project_branches or {} @@ -80,8 +80,7 @@ class Cloner(object): new_repo = git.Repo.clone_from(git_cache, dest) self.log.info("Updating origin remote in repo %s to %s", project, git_upstream) - origin = new_repo.remotes.origin.config_writer.set( - 'url', git_upstream) + new_repo.remotes.origin.config_writer.set('url', git_upstream) else: self.log.info("Creating repo %s from upstream %s", project, git_upstream) diff --git a/zuul/lib/gerrit.py b/zuul/lib/gerrit.py index 67400216a..9aeff3df8 100644 --- a/zuul/lib/gerrit.py +++ b/zuul/lib/gerrit.py @@ -156,7 +156,8 @@ class Gerrit(object): lines = out.split('\n') if not lines: return False - data = [json.loads(line) for line in lines[:-1]] + data = [json.loads(line) for line in lines + if "sortKey" in line] if not data: return False self.log.debug("Received data from Gerrit query: \n%s" % diff --git a/zuul/lib/swift.py b/zuul/lib/swift.py index 5781a4d82..9b9bea375 100644 --- a/zuul/lib/swift.py +++ b/zuul/lib/swift.py @@ -45,7 +45,8 @@ class Swift(object): try: if self.config.has_section('swift'): if (not self.config.has_option('swift', 'Send-Temp-Url-Key') - or self.config.getboolean('swift', 'Send-Temp-Url-Key')): + or self.config.getboolean('swift', + 'Send-Temp-Url-Key')): self.connect() # Tell swift of our key diff --git a/zuul/merger/client.py b/zuul/merger/client.py index 8c4156351..8d8f7eebd 100644 --- a/zuul/merger/client.py +++ b/zuul/merger/client.py @@ -89,7 +89,8 @@ class MergeClient(object): json.dumps(data), unique=uuid) self.build_sets[uuid] = build_set - self.gearman.submitJob(job, precedence=precedence) + self.gearman.submitJob(job, precedence=precedence, + timeout=300) def mergeChanges(self, items, build_set, precedence=zuul.model.PRECEDENCE_NORMAL): diff --git a/zuul/merger/merger.py b/zuul/merger/merger.py index 922c67ef3..8774f109e 100644 --- a/zuul/merger/merger.py +++ b/zuul/merger/merger.py @@ -105,7 +105,7 @@ class Repo(object): def getCommitFromRef(self, refname): repo = self.createRepoObject() - if not refname in repo.refs: + if refname not in repo.refs: return None ref = repo.refs[refname] return ref.commit diff --git a/zuul/model.py b/zuul/model.py index 67ce8be96..b705982bf 100644 --- a/zuul/model.py +++ b/zuul/model.py @@ -266,8 +266,8 @@ class Pipeline(object): j_changes = [] j_changes.append(e.formatJSON()) if (len(j_changes) > 1 and - (j_changes[-2]['remaining_time'] is not None) and - (j_changes[-1]['remaining_time'] is not None)): + (j_changes[-2]['remaining_time'] is not None) and + (j_changes[-1]['remaining_time'] is not None)): j_changes[-1]['remaining_time'] = max( j_changes[-2]['remaining_time'], j_changes[-1]['remaining_time']) @@ -340,7 +340,7 @@ class ChangeQueue(object): for job in self._jobs: if job.queue_name: if (self.assigned_name and - job.queue_name != self.assigned_name): + job.queue_name != self.assigned_name): raise Exception("More than one name assigned to " "change queue: %s != %s" % (self.assigned_name, job.queue_name)) @@ -1150,7 +1150,7 @@ class EventFilter(BaseFilter): matches_email_re = False for email_re in self.emails: if (account_email is not None and - email_re.search(account_email)): + email_re.search(account_email)): matches_email_re = True if self.emails and not matches_email_re: return False diff --git a/zuul/rpcclient.py b/zuul/rpcclient.py index 7f572be76..f43c3b905 100644 --- a/zuul/rpcclient.py +++ b/zuul/rpcclient.py @@ -38,7 +38,7 @@ class RPCClient(object): job = gear.Job(name, json.dumps(data), unique=str(time.time())) - self.gearman.submitJob(job) + self.gearman.submitJob(job, timeout=300) self.log.debug("Waiting for job completion") while not job.complete: diff --git a/zuul/scheduler.py b/zuul/scheduler.py index 9effcb896..7d41fb182 100644 --- a/zuul/scheduler.py +++ b/zuul/scheduler.py @@ -226,7 +226,7 @@ class Scheduler(threading.Thread): if 'python-file' in include: fn = include['python-file'] if not os.path.isabs(fn): - base = os.path.dirname(config_path) + base = os.path.dirname(os.path.realpath(config_path)) fn = os.path.join(base, fn) fn = os.path.expanduser(fn) execfile(fn, config_env) @@ -235,7 +235,8 @@ class Scheduler(threading.Thread): pipeline = Pipeline(conf_pipeline['name']) pipeline.description = conf_pipeline.get('description') # TODO(jeblair): remove backwards compatibility: - pipeline.source = self.triggers[conf_pipeline.get('source', 'gerrit')] + pipeline.source = self.triggers[conf_pipeline.get('source', + 'gerrit')] precedence = model.PRECEDENCE_MAP[conf_pipeline.get('precedence')] pipeline.precedence = precedence pipeline.failure_message = conf_pipeline.get('failure-message', @@ -314,16 +315,19 @@ class Scheduler(threading.Thread): usernames = toList(trigger.get('username')) if not usernames: usernames = toList(trigger.get('username_filter')) - f = EventFilter(trigger=self.triggers['gerrit'], - types=toList(trigger['event']), - branches=toList(trigger.get('branch')), - refs=toList(trigger.get('ref')), - event_approvals=approvals, - comments=comments, - emails=emails, - usernames=usernames, - required_approvals= - toList(trigger.get('require-approval'))) + f = EventFilter( + trigger=self.triggers['gerrit'], + types=toList(trigger['event']), + branches=toList(trigger.get('branch')), + refs=toList(trigger.get('ref')), + event_approvals=approvals, + comments=comments, + emails=emails, + usernames=usernames, + required_approvals=toList( + trigger.get('require-approval') + ) + ) manager.event_filters.append(f) if 'timer' in conf_pipeline['trigger']: for trigger in toList(conf_pipeline['trigger']['timer']): @@ -333,11 +337,14 @@ class Scheduler(threading.Thread): manager.event_filters.append(f) if 'zuul' in conf_pipeline['trigger']: for trigger in toList(conf_pipeline['trigger']['zuul']): - f = EventFilter(trigger=self.triggers['zuul'], - types=toList(trigger['event']), - pipelines=toList(trigger.get('pipeline')), - required_approvals= - toList(trigger.get('require-approval'))) + f = EventFilter( + trigger=self.triggers['zuul'], + types=toList(trigger['event']), + pipelines=toList(trigger.get('pipeline')), + required_approvals=toList( + trigger.get('require-approval') + ) + ) manager.event_filters.append(f) for project_template in data.get('project-templates', []): @@ -692,7 +699,7 @@ class Scheduler(threading.Thread): break for item in shared_queue.queue: if (item.change.number == change_ids[0][0] and - item.change.patchset == change_ids[0][1]): + item.change.patchset == change_ids[0][1]): change_queue = shared_queue break if not change_queue: @@ -702,7 +709,7 @@ class Scheduler(threading.Thread): found = False for item in change_queue.queue: if (item.change.number == number and - item.change.patchset == patchset): + item.change.patchset == patchset): found = True items_to_enqueue.append(item) break @@ -810,7 +817,7 @@ class Scheduler(threading.Thread): try: project = self.layout.projects.get(event.project_name) if not project: - self.log.warning("Project %s not found" % event.project_name) + self.log.debug("Project %s not found" % event.project_name) return for pipeline in self.layout.pipelines.values(): @@ -1157,7 +1164,8 @@ class BasePipelineManager(object): item.enqueue_time = enqueue_time self.reportStats(item) self.enqueueChangesBehind(change, quiet, ignore_requirements) - self.sched.triggers['zuul'].onChangeEnqueued(item.change, self.pipeline) + self.sched.triggers['zuul'].onChangeEnqueued(item.change, + self.pipeline) else: self.log.error("Unable to find change queue for project %s" % change.project) diff --git a/zuul/trigger/gerrit.py b/zuul/trigger/gerrit.py index 0c0a37654..6782534c2 100644 --- a/zuul/trigger/gerrit.py +++ b/zuul/trigger/gerrit.py @@ -250,7 +250,7 @@ class Gerrit(object): data = change._data if not data: return False - if not 'submitRecords' in data: + if 'submitRecords' not in data: return False try: for sr in data['submitRecords']: @@ -328,15 +328,18 @@ class Gerrit(object): # This is a best-effort function in case Gerrit is unable to return # a particular change. It happens. query = "project:%s status:open" % (project.name,) - self.log.debug("Running query %s to get project open changes" % (query,)) + self.log.debug("Running query %s to get project open changes" % + (query,)) data = self.gerrit.simpleQuery(query) changes = [] for record in data: try: - changes.append(self._getChange(record['number'], - record['currentPatchSet']['number'])) + changes.append( + self._getChange(record['number'], + record['currentPatchSet']['number'])) except Exception: - self.log.exception("Unable to query change %s" % (record.get('number'),)) + self.log.exception("Unable to query change %s" % + (record.get('number'),)) return changes def updateChange(self, change): @@ -423,4 +426,3 @@ def validate_trigger(trigger_data): raise voluptuous.Invalid( "The event %s does not include ref information, Zuul cannot " "use ref filter 'ref: %s'" % (event['event'], event['ref'])) - diff --git a/zuul/trigger/zuultrigger.py b/zuul/trigger/zuultrigger.py index 27098ab81..4418d6f8e 100644 --- a/zuul/trigger/zuultrigger.py +++ b/zuul/trigger/zuultrigger.py @@ -47,8 +47,9 @@ class ZuulTrigger(object): try: self._createProjectChangeMergedEvents(change) except Exception: - self.log.exception("Unable to create project-change-merged events for %s" % - (change,)) + self.log.exception( + "Unable to create project-change-merged events for " + "%s" % (change,)) def onChangeEnqueued(self, change, pipeline): # Called each time a change is enqueued in a pipeline @@ -56,11 +57,13 @@ class ZuulTrigger(object): try: self._createParentChangeEnqueuedEvents(change, pipeline) except Exception: - self.log.exception("Unable to create parent-change-enqueued events for %s in %s" % - (change, pipeline)) + self.log.exception( + "Unable to create parent-change-enqueued events for " + "%s in %s" % (change, pipeline)) def _createProjectChangeMergedEvents(self, change): - changes = self.sched.triggers['gerrit'].getProjectOpenChanges(change.project) + changes = self.sched.triggers['gerrit'].getProjectOpenChanges( + change.project) for open_change in changes: self._createProjectChangeMergedEvent(open_change) |