Merge "Provider helper classes for plugin data management"
This commit is contained in:
commit
344b5f3ffa
@ -6,3 +6,8 @@ API reference
|
||||
.. automodule:: jenkins
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
.. automodule:: jenkins.plugins
|
||||
:members:
|
||||
:noindex:
|
||||
:undoc-members:
|
||||
|
@ -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
111
jenkins/plugins.py
Normal 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)
|
@ -1,2 +1,3 @@
|
||||
six>=1.3.0
|
||||
pbr>=0.8.2,<2.0
|
||||
multi_key_dict
|
||||
|
@ -6,4 +6,5 @@ unittest2
|
||||
python-subunit
|
||||
sphinx>=1.2,<1.3.0
|
||||
testrepository
|
||||
testscenarios
|
||||
testtools
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user