Merge "Fixed several typos in the codebase"
diff --git a/doc/source/zuul.rst b/doc/source/zuul.rst
index ef6259c..21d3bae 100644
--- a/doc/source/zuul.rst
+++ b/doc/source/zuul.rst
@@ -476,6 +476,10 @@
A boolean value (``true`` or ``false``) that indicates whether the change
must be open or closed in order to be enqueued.
+ **current-patchset**
+ A boolean value (``true`` or ``false``) that indicates whether the change
+ must be the current patchset in order to be enqueued.
+
**status**
A string value that corresponds with the status of the change
reported by the trigger. For example, when using the Gerrit
diff --git a/etc/status/public_html/index.html b/etc/status/public_html/index.html
index aac5024..8884069 100644
--- a/etc/status/public_html/index.html
+++ b/etc/status/public_html/index.html
@@ -21,64 +21,7 @@
<title>Zuul Status</title>
<link rel="stylesheet" href="bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="bootstrap/css/bootstrap-responsive.min.css">
- <style>
- .zuul-change {
- margin-bottom: 10px;
- }
-
- .zuul-change-id {
- float: right;
- }
-
- .zuul-job-result {
- float: right;
- width: 70px;
- height: 15px;
- margin: 2px 0 0 0;
- }
-
- .zuul-change-total-result {
- height: 10px;
- width: 100px;
- margin: 5px 0 0 0;
- }
-
- .zuul-spinner,
- .zuul-spinner:hover {
- opacity: 0;
- transition: opacity 0.5s ease-out;
- cursor: default;
- pointer-events: none;
- }
-
- .zuul-spinner-on,
- .zuul-spinner-on:hover {
- opacity: 1;
- transition-duration: 0.2s;
- cursor: progress;
- }
-
- .zuul-change-cell {
- padding-left: 5px;
- }
-
- .zuul-change-job {
- padding: 2px 8px;
- }
-
- .zuul-job-name {
- font-size: small;
- }
-
- .zuul-non-voting-desc {
- font-size: smaller;
- }
-
- .zuul-patchset-header {
- font-size: small;
- padding: 8px 12px;
- }
- </style>
+ <link rel="stylesheet" href="styles/zuul.css" />
</head>
<body>
<div class="container">
diff --git a/etc/status/public_html/styles/zuul.css b/etc/status/public_html/styles/zuul.css
new file mode 100644
index 0000000..e833f4b
--- /dev/null
+++ b/etc/status/public_html/styles/zuul.css
@@ -0,0 +1,56 @@
+.zuul-change {
+ margin-bottom: 10px;
+}
+
+.zuul-change-id {
+ float: right;
+}
+
+.zuul-job-result {
+ float: right;
+ width: 70px;
+ height: 15px;
+ margin: 2px 0 0 0;
+}
+
+.zuul-change-total-result {
+ height: 10px;
+ width: 100px;
+ margin: 5px 0 0 0;
+}
+
+.zuul-spinner,
+.zuul-spinner:hover {
+ opacity: 0;
+ transition: opacity 0.5s ease-out;
+ cursor: default;
+ pointer-events: none;
+}
+
+.zuul-spinner-on,
+.zuul-spinner-on:hover {
+ opacity: 1;
+ transition-duration: 0.2s;
+ cursor: progress;
+}
+
+.zuul-change-cell {
+ padding-left: 5px;
+}
+
+.zuul-change-job {
+ padding: 2px 8px;
+}
+
+.zuul-job-name {
+ font-size: small;
+}
+
+.zuul-non-voting-desc {
+ font-size: smaller;
+}
+
+.zuul-patchset-header {
+ font-size: small;
+ padding: 8px 12px;
+}
\ No newline at end of file
diff --git a/tests/fixtures/layout-current-patchset.yaml b/tests/fixtures/layout-current-patchset.yaml
new file mode 100644
index 0000000..dc8f768
--- /dev/null
+++ b/tests/fixtures/layout-current-patchset.yaml
@@ -0,0 +1,24 @@
+includes:
+ - python-file: custom_functions.py
+
+pipelines:
+ - name: check
+ manager: IndependentPipelineManager
+ require:
+ current-patchset: True
+ trigger:
+ gerrit:
+ - event: patchset-created
+ - event: comment-added
+ success:
+ gerrit:
+ verified: 1
+ failure:
+ gerrit:
+ verified: -1
+
+projects:
+ - name: org/project
+ merge-mode: cherry-pick
+ check:
+ - project-check
diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py
index d9f9afe..d489ff1 100755
--- a/tests/test_scheduler.py
+++ b/tests/test_scheduler.py
@@ -132,6 +132,7 @@
self.upstream_root = upstream_root
self.addPatchset()
self.data['submitRecords'] = self.getSubmitRecords()
+ self.open = True
def add_fake_change_to_repo(self, msg, fn, large):
path = os.path.join(self.upstream_root, self.project)
@@ -221,6 +222,23 @@
"reason": ""}
return event
+ def getChangeCommentEvent(self, patchset):
+ event = {"type": "comment-added",
+ "change": {"project": self.project,
+ "branch": self.branch,
+ "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
+ "number": str(self.number),
+ "subject": self.subject,
+ "owner": {"name": "User Name"},
+ "url": "https://hostname/3"},
+ "patchSet": self.patchsets[patchset - 1],
+ "author": {"name": "User Name"},
+ "approvals": [{"type": "Code-Review",
+ "description": "Code-Review",
+ "value": "0"}],
+ "comment": "This is a comment"}
+ return event
+
def addApproval(self, category, value, username='jenkins',
granted_on=None):
if not granted_on:
@@ -4063,3 +4081,35 @@
self.getJobFromHistory('experimental-project-test').result,
'SUCCESS')
self.assertEqual(A.reported, 1)
+
+ def test_old_patchset_doesnt_trigger(self):
+ "Test that jobs never run against old patchsets"
+ self.config.set('zuul', 'layout_config',
+ 'tests/fixtures/layout-current-patchset.yaml')
+ self.sched.reconfigure(self.config)
+ self.registerJobs()
+ # 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('org/project', 'master', 'A')
+ # Added because the layout file really wants an approval but this
+ # doesn't match anyways.
+ self.fake_gerrit.addEvent(A.addApproval('CRVW', 1))
+ self.waitUntilSettled()
+ A.addPatchset()
+ self.fake_gerrit.addEvent(A.addApproval('CRVW', 1))
+ self.waitUntilSettled()
+
+ old_history_count = len(self.history)
+ self.assertEqual(old_history_count, 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), old_history_count)
+
+ # The last thing we did was add an event for a change then do
+ # nothing with a pipeline, so it will be in the cache;
+ # clean it up so it does not fail the test.
+ for pipeline in self.sched.layout.pipelines.values():
+ pipeline.trigger.maintainCache([])
diff --git a/tests/test_stack_dump.py b/tests/test_stack_dump.py
index cc8cf8f..824e04c 100644
--- a/tests/test_stack_dump.py
+++ b/tests/test_stack_dump.py
@@ -17,7 +17,7 @@
import signal
import testtools
-import zuul.cmd.server
+import zuul.cmd
class TestStackDump(testtools.TestCase):
@@ -29,6 +29,6 @@
def test_stack_dump_logs(self):
"Test that stack dumps end up in logs."
- zuul.cmd.server.stack_dump_handler(signal.SIGUSR2, None)
+ 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/zuul/cmd/__init__.py b/zuul/cmd/__init__.py
index e69de29..e17ad5b 100644
--- a/zuul/cmd/__init__.py
+++ b/zuul/cmd/__init__.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python
+# Copyright 2012 Hewlett-Packard Development Company, L.P.
+# 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.
+
+import ConfigParser
+import logging
+import logging.config
+import os
+import signal
+import sys
+import traceback
+
+# No zuul imports here because they pull in paramiko which must not be
+# imported until after the daemonization.
+# https://github.com/paramiko/paramiko/issues/59
+# Similar situation with gear and statsd.
+
+
+def stack_dump_handler(signum, frame):
+ signal.signal(signal.SIGUSR2, signal.SIG_IGN)
+ log_str = ""
+ for thread_id, stack_frame in sys._current_frames().items():
+ log_str += "Thread: %s\n" % thread_id
+ log_str += "".join(traceback.format_stack(stack_frame))
+ log = logging.getLogger("zuul.stack_dump")
+ log.debug(log_str)
+ signal.signal(signal.SIGUSR2, stack_dump_handler)
+
+
+class ZuulApp(object):
+
+ def __init__(self):
+ self.args = None
+ self.config = None
+
+ def _get_version(self):
+ from zuul.version import version_info as zuul_version_info
+ return "Zuul version: %s" % zuul_version_info.version_string()
+
+ def read_config(self):
+ self.config = ConfigParser.ConfigParser()
+ if self.args.config:
+ locations = [self.args.config]
+ else:
+ locations = ['/etc/zuul/zuul.conf',
+ '~/zuul.conf']
+ for fp in locations:
+ if os.path.exists(os.path.expanduser(fp)):
+ self.config.read(os.path.expanduser(fp))
+ return
+ raise Exception("Unable to locate config file in %s" % locations)
+
+ def setup_logging(self, section, parameter):
+ if self.config.has_option(section, parameter):
+ fp = os.path.expanduser(self.config.get(section, parameter))
+ if not os.path.exists(fp):
+ raise Exception("Unable to read logging config file at %s" %
+ fp)
+ logging.config.fileConfig(fp)
+ else:
+ logging.basicConfig(level=logging.DEBUG)
diff --git a/zuul/cmd/client.py b/zuul/cmd/client.py
index 147fade..766a4ef 100644
--- a/zuul/cmd/client.py
+++ b/zuul/cmd/client.py
@@ -16,26 +16,20 @@
import argparse
import babel.dates
-import ConfigParser
import datetime
import logging
-import logging.config
-import os
import prettytable
import sys
import time
+
import zuul.rpcclient
+import zuul.cmd
-class Client(object):
+class Client(zuul.cmd.ZuulApp):
log = logging.getLogger("zuul.Client")
- def __init__(self):
- self.args = None
- self.config = None
- self.gear_server_pid = None
-
def parse_arguments(self):
parser = argparse.ArgumentParser(
description='Zuul Project Gating System Client.')
@@ -89,24 +83,8 @@
self.args = parser.parse_args()
- def _get_version(self):
- from zuul.version import version_info as zuul_version_info
- return "Zuul version: %s" % zuul_version_info.version_string()
-
- def read_config(self):
- self.config = ConfigParser.ConfigParser()
- if self.args.config:
- locations = [self.args.config]
- else:
- locations = ['/etc/zuul/zuul.conf',
- '~/zuul.conf']
- for fp in locations:
- if os.path.exists(os.path.expanduser(fp)):
- self.config.read(os.path.expanduser(fp))
- return
- raise Exception("Unable to locate config file in %s" % locations)
-
def setup_logging(self):
+ """Client logging does not rely on conf file"""
if self.args.verbose:
logging.basicConfig(level=logging.DEBUG)
diff --git a/zuul/cmd/merger.py b/zuul/cmd/merger.py
index edf8da9..dc3484a 100644
--- a/zuul/cmd/merger.py
+++ b/zuul/cmd/merger.py
@@ -15,7 +15,6 @@
# under the License.
import argparse
-import ConfigParser
import daemon
import extras
@@ -23,12 +22,11 @@
# instead it depends on lockfile-0.9.1 which uses pidfile.
pid_file_module = extras.try_imports(['daemon.pidlockfile', 'daemon.pidfile'])
-import logging
-import logging.config
import os
import sys
import signal
-import traceback
+
+import zuul.cmd
# No zuul imports here because they pull in paramiko which must not be
# imported until after the daemonization.
@@ -36,21 +34,7 @@
# Similar situation with gear and statsd.
-def stack_dump_handler(signum, frame):
- signal.signal(signal.SIGUSR2, signal.SIG_IGN)
- log_str = ""
- for thread_id, stack_frame in sys._current_frames().items():
- log_str += "Thread: %s\n" % thread_id
- log_str += "".join(traceback.format_stack(stack_frame))
- log = logging.getLogger("zuul.stack_dump")
- log.debug(log_str)
- signal.signal(signal.SIGUSR2, stack_dump_handler)
-
-
-class Merger(object):
- def __init__(self):
- self.args = None
- self.config = None
+class Merger(zuul.cmd.ZuulApp):
def parse_arguments(self):
parser = argparse.ArgumentParser(description='Zuul merge worker.')
@@ -58,33 +42,11 @@
help='specify the config file')
parser.add_argument('-d', dest='nodaemon', action='store_true',
help='do not run as a daemon')
- parser.add_argument('--version', dest='version', action='store_true',
+ parser.add_argument('--version', dest='version', action='version',
+ version=self._get_version(),
help='show zuul version')
self.args = parser.parse_args()
- def read_config(self):
- self.config = ConfigParser.ConfigParser()
- if self.args.config:
- locations = [self.args.config]
- else:
- locations = ['/etc/zuul/zuul.conf',
- '~/zuul.conf']
- for fp in locations:
- if os.path.exists(os.path.expanduser(fp)):
- self.config.read(os.path.expanduser(fp))
- return
- raise Exception("Unable to locate config file in %s" % locations)
-
- def setup_logging(self, section, parameter):
- if self.config.has_option(section, parameter):
- fp = os.path.expanduser(self.config.get(section, parameter))
- if not os.path.exists(fp):
- raise Exception("Unable to read logging config file at %s" %
- fp)
- logging.config.fileConfig(fp)
- else:
- logging.basicConfig(level=logging.DEBUG)
-
def exit_handler(self, signum, frame):
signal.signal(signal.SIGUSR1, signal.SIG_IGN)
self.merger.stop()
@@ -100,7 +62,7 @@
self.merger.start()
signal.signal(signal.SIGUSR1, self.exit_handler)
- signal.signal(signal.SIGUSR2, stack_dump_handler)
+ signal.signal(signal.SIGUSR2, zuul.cmd.stack_dump_handler)
while True:
try:
signal.pause()
@@ -113,11 +75,6 @@
server = Merger()
server.parse_arguments()
- if server.args.version:
- from zuul.version import version_info as zuul_version_info
- print "Zuul version: %s" % zuul_version_info.version_string()
- sys.exit(0)
-
server.read_config()
if server.config.has_option('zuul', 'state_dir'):
diff --git a/zuul/cmd/server.py b/zuul/cmd/server.py
index 8caa1fd..06ea780 100755
--- a/zuul/cmd/server.py
+++ b/zuul/cmd/server.py
@@ -15,7 +15,6 @@
# under the License.
import argparse
-import ConfigParser
import daemon
import extras
@@ -24,11 +23,11 @@
pid_file_module = extras.try_imports(['daemon.pidlockfile', 'daemon.pidfile'])
import logging
-import logging.config
import os
import sys
import signal
-import traceback
+
+import zuul.cmd
# No zuul imports here because they pull in paramiko which must not be
# imported until after the daemonization.
@@ -36,21 +35,9 @@
# Similar situation with gear and statsd.
-def stack_dump_handler(signum, frame):
- signal.signal(signal.SIGUSR2, signal.SIG_IGN)
- log_str = ""
- for thread_id, stack_frame in sys._current_frames().items():
- log_str += "Thread: %s\n" % thread_id
- log_str += "".join(traceback.format_stack(stack_frame))
- log = logging.getLogger("zuul.stack_dump")
- log.debug(log_str)
- signal.signal(signal.SIGUSR2, stack_dump_handler)
-
-
-class Server(object):
+class Server(zuul.cmd.ZuulApp):
def __init__(self):
- self.args = None
- self.config = None
+ super(Server, self).__init__()
self.gear_server_pid = None
def parse_arguments(self):
@@ -71,33 +58,6 @@
help='show zuul version')
self.args = parser.parse_args()
- def _get_version(self):
- from zuul.version import version_info as zuul_version_info
- return "Zuul version: %s" % zuul_version_info.version_string()
-
- def read_config(self):
- self.config = ConfigParser.ConfigParser()
- if self.args.config:
- locations = [self.args.config]
- else:
- locations = ['/etc/zuul/zuul.conf',
- '~/zuul.conf']
- for fp in locations:
- if os.path.exists(os.path.expanduser(fp)):
- self.config.read(os.path.expanduser(fp))
- return
- raise Exception("Unable to locate config file in %s" % locations)
-
- def setup_logging(self, section, parameter):
- if self.config.has_option(section, parameter):
- fp = os.path.expanduser(self.config.get(section, parameter))
- if not os.path.exists(fp):
- raise Exception("Unable to read logging config file at %s" %
- fp)
- logging.config.fileConfig(fp)
- else:
- logging.basicConfig(level=logging.DEBUG)
-
def reconfigure_handler(self, signum, frame):
signal.signal(signal.SIGHUP, signal.SIG_IGN)
self.read_config()
@@ -235,7 +195,7 @@
signal.signal(signal.SIGHUP, self.reconfigure_handler)
signal.signal(signal.SIGUSR1, self.exit_handler)
- signal.signal(signal.SIGUSR2, stack_dump_handler)
+ signal.signal(signal.SIGUSR2, zuul.cmd.stack_dump_handler)
signal.signal(signal.SIGTERM, self.term_handler)
while True:
try:
diff --git a/zuul/layoutvalidator.py b/zuul/layoutvalidator.py
index 3e0a0ab..9a448a3 100644
--- a/zuul/layoutvalidator.py
+++ b/zuul/layoutvalidator.py
@@ -71,6 +71,7 @@
require = {'approval': toList(require_approval),
'open': bool,
+ 'current-patchset': bool,
'status': toList(str)}
window = v.All(int, v.Range(min=0))
diff --git a/zuul/model.py b/zuul/model.py
index 884ba09..1d103a7 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -245,17 +245,6 @@
items.extend(shared_queue.queue)
return items
- def formatStatusHTML(self):
- ret = ''
- for queue in self.queues:
- if len(self.queues) > 1:
- s = 'Change queue: %s' % queue.name
- ret += s + '\n'
- ret += '-' * len(s) + '\n'
- for item in queue.queue:
- ret += self.formatStatus(item, html=True)
- return ret
-
def formatStatusJSON(self):
j_pipeline = dict(name=self.name,
description=self.description)
@@ -1158,8 +1147,10 @@
class ChangeishFilter(object):
- def __init__(self, open=None, statuses=[], approvals=[]):
+ def __init__(self, open=None, current_patchset=None,
+ statuses=[], approvals=[]):
self.open = open
+ self.current_patchset = current_patchset
self.statuses = statuses
self.approvals = approvals
@@ -1176,10 +1167,12 @@
if self.open is not None:
ret += ' open: %s' % self.open
+ if self.current_patchset is not None:
+ ret += ' current-patchset: %s' % self.current_patchset
if self.statuses:
ret += ' statuses: %s' % ', '.join(self.statuses)
if self.approvals:
- ret += ' approvals: %s' % ', '.join(str(self.approvals))
+ ret += ' approvals: %s' % str(self.approvals)
ret += '>'
return ret
@@ -1189,6 +1182,10 @@
if self.open != change.open:
return False
+ if self.current_patchset is not None:
+ if self.current_patchset != change.is_current_patchset:
+ return False
+
if self.statuses:
if change.status not in self.statuses:
return False
diff --git a/zuul/scheduler.py b/zuul/scheduler.py
index 819bcf3..922d815 100644
--- a/zuul/scheduler.py
+++ b/zuul/scheduler.py
@@ -276,9 +276,11 @@
if 'require' in conf_pipeline:
require = conf_pipeline['require']
- f = ChangeishFilter(open=require.get('open'),
- statuses=toList(require.get('status')),
- approvals=toList(require.get('approval')))
+ f = ChangeishFilter(
+ open=require.get('open'),
+ current_patchset=require.get('current-patchset'),
+ statuses=toList(require.get('status')),
+ approvals=toList(require.get('approval')))
manager.changeish_filters.append(f)
# TODO: move this into triggers (may require pluggable
@@ -849,29 +851,6 @@
return
pipeline.manager.onMergeCompleted(event)
- def formatStatusHTML(self):
- ret = '<html><pre>'
- if self._pause:
- ret += '<p><b>Queue only mode:</b> preparing to '
- if self._exit:
- ret += 'exit'
- ret += ', queue length: %s' % self.trigger_event_queue.qsize()
- ret += '</p>'
-
- if self.last_reconfigured:
- ret += '<p>Last reconfigured: %s</p>' % self.last_reconfigured
-
- keys = self.layout.pipelines.keys()
- for key in keys:
- pipeline = self.layout.pipelines[key]
- s = 'Pipeline: %s' % pipeline.name
- ret += s + '\n'
- ret += '-' * len(s) + '\n'
- ret += pipeline.formatStatusHTML()
- ret += '\n'
- ret += '</pre></html>'
- return ret
-
def formatStatusJSON(self):
data = {}