Merge "Provider helper classes for plugin data management"

This commit is contained in:
Jenkins 2015-09-28 17:34:05 +00:00 committed by Gerrit Code Review
commit 344b5f3ffa
6 changed files with 332 additions and 17 deletions

View File

@ -6,3 +6,8 @@ API reference
.. automodule:: jenkins
:members:
:undoc-members:
.. automodule:: jenkins.plugins
:members:
:noindex:
:undoc-members:

View File

@ -54,6 +54,7 @@ import sys
import time
import warnings
import multi_key_dict
import six
from six.moves.http_client import BadStatusLine
from six.moves.urllib.error import HTTPError
@ -61,6 +62,10 @@ from six.moves.urllib.error import URLError
from six.moves.urllib.parse import quote, urlencode, urljoin, urlparse
from six.moves.urllib.request import Request, urlopen
from jenkins import plugins
warnings.simplefilter("default", DeprecationWarning)
if sys.version_info < (2, 7, 0):
warnings.warn("Support for python 2.6 is deprecated and will be removed.")
@ -497,7 +502,10 @@ class Jenkins(object):
"""Get all installed plugins information on this Master.
This method retrieves information about each plugin that is installed
on master.
on master returning the raw plugin data in a JSON format.
.. deprecated:: 0.4.9
Use :func:`get_plugins` instead.
:param depth: JSON depth, ``int``
:returns: info on all plugins ``[dict]``
@ -515,24 +523,24 @@ class Jenkins(object):
u'gearman-plugin', u'bundled': False}, ..]
"""
try:
plugins_info = json.loads(self.jenkins_open(
Request(self._build_url(PLUGIN_INFO, locals()))
))
return plugins_info['plugins']
except (HTTPError, BadStatusLine):
raise BadHTTPException("Error communicating with server[%s]"
% self.server)
except ValueError:
raise JenkinsException("Could not parse JSON info for server[%s]"
% self.server)
warnings.warn("get_plugins_info() is deprecated, use get_plugins()",
DeprecationWarning)
return [plugin_data for plugin_data in self.get_plugins(depth).values()]
def get_plugin_info(self, name, depth=2):
"""Get an installed plugin information on this Master.
This method retrieves information about a speicifc plugin.
This method retrieves information about a specific plugin and returns
the raw plugin data in a JSON format.
The passed in plugin name (short or long) must be an exact match.
.. note:: Calling this method will query Jenkins fresh for the
information for all plugins on each call. If you need to retrieve
information for multiple plugins it's recommended to use
:func:`get_plugins` instead, which will return a multi key
dictionary that can be accessed via either the short or long name
of the plugin.
:param name: Name (short or long) of plugin, ``str``
:param depth: JSON depth, ``int``
:returns: a specific plugin ``dict``
@ -550,12 +558,45 @@ class Jenkins(object):
u'gearman-plugin', u'bundled': False}
"""
plugins_info = self.get_plugins(depth)
try:
plugins_info = json.loads(self.jenkins_open(
return plugins_info[name]
except KeyError:
pass
def get_plugins(self, depth=2):
"""Return plugins info using helper class for version comparison
This method retrieves information about all the installed plugins and
uses a Plugin helper class to simplify version comparison. Also uses
a multi key dict to allow retrieval via either short or long names.
When printing/dumping the data, the version will transparently return
a unicode string, which is exactly what was previously returned by the
API.
:param depth: JSON depth, ``int``
:returns: info on all plugins ``[dict]``
Example::
>>> j = Jenkins()
>>> info = j.get_plugins()
>>> print(info)
{('gearman-plugin', 'Gearman Plugin'):
{u'backupVersion': None, u'version': u'0.0.4',
u'deleted': False, u'supportsDynamicLoad': u'MAYBE',
u'hasUpdate': True, u'enabled': True, u'pinned': False,
u'downgradable': False, u'dependencies': [], u'url':
u'http://wiki.jenkins-ci.org/display/JENKINS/Gearman+Plugin',
u'longName': u'Gearman Plugin', u'active': True, u'shortName':
u'gearman-plugin', u'bundled': False}, ...}
"""
try:
plugins_info_json = json.loads(self.jenkins_open(
Request(self._build_url(PLUGIN_INFO, locals()))))
for plugin in plugins_info['plugins']:
if plugin['longName'] == name or plugin['shortName'] == name:
return plugin
except (HTTPError, BadStatusLine):
raise BadHTTPException("Error communicating with server[%s]"
% self.server)
@ -563,6 +604,13 @@ class Jenkins(object):
raise JenkinsException("Could not parse JSON info for server[%s]"
% self.server)
plugins_data = multi_key_dict.multi_key_dict()
for plugin_data in plugins_info_json['plugins']:
keys = (str(plugin_data['shortName']), str(plugin_data['longName']))
plugins_data[keys] = plugins.Plugin(**plugin_data)
return plugins_data
def get_jobs(self, folder_depth=0):
"""Get list of jobs.

111
jenkins/plugins.py Normal file
View File

@ -0,0 +1,111 @@
# Software License Agreement (BSD License)
#
# Copyright (c) 2015 Hewlett-Packard Development Company, L.P.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# 'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# Authors:
# Darragh Bailey <dbailey@hp.com>
'''
.. module:: jenkins.plugins
:platform: Unix, Windows
:synopsis: Class for interacting with plugins
'''
import operator
import re
import pkg_resources
class Plugin(dict):
'''Dictionary object containing plugin metadata.'''
def __init__(self, *args, **kwargs):
'''Populates dictionary using json object input.
accepts same arguments as python `dict` class.
'''
version = kwargs.pop('version', None)
super(Plugin, self).__init__(*args, **kwargs)
self['version'] = version
def __setitem__(self, key, value):
'''Overrides default setter to ensure that the version key is always
a PluginVersion class to abstract and simplify version comparisons
'''
if key == 'version':
value = PluginVersion(value)
super(Plugin, self).__setitem__(key, value)
class PluginVersion(object):
'''Class providing comparison capabilities for plugin versions.'''
_VERSION_RE = re.compile(r'(.*)-(?:SNAPSHOT|BETA)')
def __init__(self, version):
'''Parse plugin version and store it for comparison.'''
self._version = version
self.parsed_version = pkg_resources.parse_version(
self.__convert_version(version))
def __convert_version(self, version):
return self._VERSION_RE.sub(r'\g<1>.preview', str(version))
def __compare(self, op, version):
return op(self.parsed_version, pkg_resources.parse_version(
self.__convert_version(version)))
def __le__(self, version):
return self.__compare(operator.le, version)
def __lt__(self, version):
return self.__compare(operator.lt, version)
def __ge__(self, version):
return self.__compare(operator.ge, version)
def __gt__(self, version):
return self.__compare(operator.gt, version)
def __eq__(self, version):
return self.__compare(operator.eq, version)
def __ne__(self, version):
return self.__compare(operator.ne, version)
def __str__(self):
return str(self._version)
def __repr__(self):
return str(self._version)

View File

@ -1,2 +1,3 @@
six>=1.3.0
pbr>=0.8.2,<2.0
multi_key_dict

View File

@ -6,4 +6,5 @@ unittest2
python-subunit
sphinx>=1.2,<1.3.0
testrepository
testscenarios
testtools

View File

@ -1,7 +1,42 @@
# Software License Agreement (BSD License)
#
# Copyright (c) 2015 Hewlett-Packard Development Company, L.P.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# 'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import json
from mock import patch
from testscenarios.testcase import TestWithScenarios
import jenkins
from jenkins import plugins
from tests.base import JenkinsTestBase
@ -29,6 +64,14 @@ class JenkinsPluginsBase(JenkinsTestBase):
]
}
updated_plugin_info_json = {
u"plugins":
[
dict(plugin_info_json[u"plugins"][0],
**{u"version": u"1.6"})
]
}
class JenkinsPluginsInfoTest(JenkinsPluginsBase):
@ -129,6 +172,28 @@ class JenkinsPluginInfoTest(JenkinsPluginsBase):
self.assertEqual(plugin_info, self.plugin_info_json['plugins'][0])
self._check_requests(jenkins_mock.call_args_list)
@patch.object(jenkins.Jenkins, 'jenkins_open')
def test_get_plugin_info_updated(self, jenkins_mock):
jenkins_mock.side_effect = [
json.dumps(self.plugin_info_json),
json.dumps(self.updated_plugin_info_json)
]
j = jenkins.Jenkins('http://example.com/', 'test', 'test')
plugins_info = j.get_plugins()
self.assertEqual(plugins_info["mailer"]["version"],
self.plugin_info_json['plugins'][0]["version"])
self.assertNotEqual(
plugins_info["mailer"]["version"],
self.updated_plugin_info_json['plugins'][0]["version"])
plugins_info = j.get_plugins()
self.assertEqual(
plugins_info["mailer"]["version"],
self.updated_plugin_info_json['plugins'][0]["version"])
@patch.object(jenkins.Jenkins, 'jenkins_open')
def test_return_none(self, jenkins_mock):
jenkins_mock.return_value = json.dumps(self.plugin_info_json)
@ -191,3 +256,87 @@ class JenkinsPluginInfoTest(JenkinsPluginsBase):
str(context_manager.exception),
'Error communicating with server[http://example.com/]')
self._check_requests(jenkins_mock.call_args_list)
class PluginsTestScenarios(TestWithScenarios, JenkinsPluginsBase):
scenarios = [
('s1', dict(v1='1.0.0', op='__gt__', v2='0.8.0')),
('s2', dict(v1='1.0.1alpha', op='__gt__', v2='1.0.0')),
('s3', dict(v1='1.0', op='__eq__', v2='1.0.0')),
('s4', dict(v1='1.0', op='__eq__', v2='1.0')),
('s5', dict(v1='1.0', op='__lt__', v2='1.8.0')),
('s6', dict(v1='1.0.1alpha', op='__lt__', v2='1.0.1')),
('s7', dict(v1='1.0alpha', op='__lt__', v2='1.0.0')),
('s8', dict(v1='1.0-alpha', op='__lt__', v2='1.0.0')),
('s9', dict(v1='1.1-alpha', op='__gt__', v2='1.0')),
('s10', dict(v1='1.0-SNAPSHOT', op='__lt__', v2='1.0')),
('s11', dict(v1='1.0.preview', op='__lt__', v2='1.0')),
('s12', dict(v1='1.1-SNAPSHOT', op='__gt__', v2='1.0')),
('s13', dict(v1='1.0a-SNAPSHOT', op='__lt__', v2='1.0a')),
]
def setUp(self):
super(PluginsTestScenarios, self).setUp()
plugin_info_json = dict(self.plugin_info_json)
plugin_info_json[u"plugins"][0][u"version"] = self.v1
patcher = patch.object(jenkins.Jenkins, 'jenkins_open')
self.jenkins_mock = patcher.start()
self.addCleanup(patcher.stop)
self.jenkins_mock.return_value = json.dumps(plugin_info_json)
def test_plugin_version_comparison(self):
"""Verify that valid versions are ordinally correct.
That is, for each given scenario, v1.op(v2)==True where 'op' is the
equality operator defined for the scenario.
"""
plugin_name = "Jenkins Mailer Plugin"
j = jenkins.Jenkins('http://example.com/', 'test', 'test')
plugin_info = j.get_plugins()[plugin_name]
v1 = plugin_info.get("version")
op = getattr(v1, self.op)
self.assertTrue(op(self.v2),
msg="Unexpectedly found {0} {2} {1} == False "
"when comparing versions!"
.format(v1, self.v2, self.op))
def test_plugin_version_object_comparison(self):
"""Verify use of PluginVersion for comparison
Verify that converting the version to be compared to the same object
type of PluginVersion before comparing provides the same result.
"""
plugin_name = "Jenkins Mailer Plugin"
j = jenkins.Jenkins('http://example.com/', 'test', 'test')
plugin_info = j.get_plugins()[plugin_name]
v1 = plugin_info.get("version")
op = getattr(v1, self.op)
v2 = plugins.PluginVersion(self.v2)
self.assertTrue(op(v2),
msg="Unexpectedly found {0} {2} {1} == False "
"when comparing versions!"
.format(v1, v2, self.op))
class PluginsTest(JenkinsPluginsBase):
def test_plugin_equal(self):
p1 = plugins.Plugin(self.plugin_info_json)
p2 = plugins.Plugin(self.plugin_info_json)
self.assertEqual(p1, p2)
def test_plugin_not_equal(self):
p1 = plugins.Plugin(self.plugin_info_json)
p2 = plugins.Plugin(self.plugin_info_json)
p2[u'version'] = u"1.6"
self.assertNotEqual(p1, p2)