diff --git a/README.rst b/README.rst index dc35a92..313c72a 100644 --- a/README.rst +++ b/README.rst @@ -23,6 +23,7 @@ the things you can use it for: * Create/delete/reconfig views * Put server in shutdown mode (quiet down) * List running builds +* Create/delete/update folders [#f1]_ * and many more.. To install:: @@ -76,3 +77,11 @@ Then install the required python packages using pip_:: .. _flake8: https://pypi.python.org/pypi/flake8 .. _tox: https://testrun.org/tox .. _pip: https://pypi.python.org/pypi/pip + + +.. rubric:: Footnotes + +.. [#f1] The free `Cloudbees Folders Plugin + `_ + provides support for a subset of the full folders functionality. For the + complete capabilities you will need the paid for version of the plugin. diff --git a/doc/source/examples.rst b/doc/source/examples.rst index d45b859..2154eee 100644 --- a/doc/source/examples.rst +++ b/doc/source/examples.rst @@ -116,3 +116,21 @@ This is an example showing how to retrieve information on the Jenkins queue. queue_info = server.get_queue_info() id = queue_info[0].get('id') server.cancel_queue(id) + + +Example 7: Working with Jenkins Cloudbees Folders +------------------------------------------------- + +Requires the `Cloudbees Folders Plugin +`_ for +Jenkins. + +This is an example showing how to create, configure and delete Jenkins folders. + +:: + + server.create_job('folder', jenkins.EMPTY_FOLDER_XML) + server.create_job('folder/empty', jenkins.EMPTY_FOLDER_XML) + server.copy_job('folder/empty', 'folder/empty_copy') + server.delete_job('folder/empty_copy') + server.delete_job('folder') diff --git a/jenkins/__init__.py b/jenkins/__init__.py index 7e67040..45d1909 100644 --- a/jenkins/__init__.py +++ b/jenkins/__init__.py @@ -74,22 +74,23 @@ DEFAULT_HEADERS = {'Content-Type': 'text/xml; charset=utf-8'} INFO = 'api/json' PLUGIN_INFO = 'pluginManager/api/json?depth=%(depth)s' CRUMB_URL = 'crumbIssuer/api/json' -JOB_INFO = 'job/%(name)s/api/json?depth=%(depth)s' -JOB_NAME = 'job/%(name)s/api/json?tree=name' +JOBS_QUERY = '?tree=jobs[url,color,name,jobs]' +JOB_INFO = '%(folder_url)sjob/%(short_name)s/api/json?depth=%(depth)s' +JOB_NAME = '%(folder_url)sjob/%(short_name)s/api/json?tree=name' Q_INFO = 'queue/api/json?depth=0' CANCEL_QUEUE = 'queue/cancelItem?id=%(id)s' -CREATE_JOB = 'createItem?name=%(name)s' # also post config.xml -CONFIG_JOB = 'job/%(name)s/config.xml' -DELETE_JOB = 'job/%(name)s/doDelete' -ENABLE_JOB = 'job/%(name)s/enable' -DISABLE_JOB = 'job/%(name)s/disable' -COPY_JOB = 'createItem?name=%(to_name)s&mode=copy&from=%(from_name)s' -RENAME_JOB = 'job/%(from_name)s/doRename?newName=%(to_name)s' -BUILD_JOB = 'job/%(name)s/build' -STOP_BUILD = 'job/%(name)s/%(number)s/stop' -BUILD_WITH_PARAMS_JOB = 'job/%(name)s/buildWithParameters' -BUILD_INFO = 'job/%(name)s/%(number)d/api/json?depth=%(depth)s' -BUILD_CONSOLE_OUTPUT = 'job/%(name)s/%(number)d/consoleText' +CREATE_JOB = '%(folder_url)screateItem?name=%(short_name)s' # also post config.xml +CONFIG_JOB = '%(folder_url)sjob/%(short_name)s/config.xml' +DELETE_JOB = '%(folder_url)sjob/%(short_name)s/doDelete' +ENABLE_JOB = '%(folder_url)sjob/%(short_name)s/enable' +DISABLE_JOB = '%(folder_url)sjob/%(short_name)s/disable' +COPY_JOB = '%(from_folder_url)screateItem?name=%(to_short_name)s&mode=copy&from=%(from_short_name)s' +RENAME_JOB = '%(from_folder_url)sjob/%(from_short_name)s/doRename?newName=%(to_short_name)s' +BUILD_JOB = '%(folder_url)sjob/%(short_name)s/build' +STOP_BUILD = '%(folder_url)sjob/%(short_name)s/%(number)s/stop' +BUILD_WITH_PARAMS_JOB = '%(folder_url)sjob/%(short_name)s/buildWithParameters' +BUILD_INFO = '%(folder_url)sjob/%(short_name)s/%(number)d/api/json?depth=%(depth)s' +BUILD_CONSOLE_OUTPUT = '%(folder_url)sjob/%(short_name)s/%(number)d/consoleText' NODE_LIST = 'computer/api/json' CREATE_NODE = 'computer/doCreateItem?%s' DELETE_NODE = 'computer/%(name)s/doDelete' @@ -224,7 +225,8 @@ class Jenkins(object): def _get_encoded_params(self, params): for k, v in params.items(): - if k in ["name", "to_name", "from_name", "msg"]: + if k in ["name", "msg", "short_name", "from_short_name", + "to_short_name", "folder_url", "from_folder_url", "to_folder_url"]: params[k] = quote(v) return params @@ -257,6 +259,7 @@ class Jenkins(object): :param depth: JSON depth, ``int`` :returns: dictionary of job information ''' + folder_url, short_name = self._get_job_folder(name) try: response = self.jenkins_open(Request( self._build_url(JOB_INFO, locals()) @@ -271,16 +274,17 @@ class Jenkins(object): raise JenkinsException( "Could not parse JSON info for job[%s]" % name) - def get_job_info_regex(self, pattern, depth=0): + def get_job_info_regex(self, pattern, depth=0, folder_depth=0): '''Get a list of jobs information that contain names which match the regex pattern. :param pattern: regex pattern, ``str`` :param depth: JSON depth, ``int`` + :param folder_depth: folder level depth to search ``int`` :returns: List of jobs info, ``list`` ''' result = [] - jobs = self.get_jobs() + jobs = self.get_all_jobs(folder_depth) for job in jobs: if re.search(pattern, job['name']): result.append(self.get_job_info(job['name'], depth=depth)) @@ -297,6 +301,7 @@ class Jenkins(object): :param name: Job name, ``str`` :returns: Name of job or None ''' + folder_url, short_name = self._get_job_folder(name) try: response = self.jenkins_open(Request( self._build_url(JOB_NAME, locals()) @@ -305,7 +310,7 @@ class Jenkins(object): return None else: actual = json.loads(response)['name'] - if actual != name: + if actual != short_name: raise JenkinsException( 'Jenkins returned an unexpected job name %s ' '(expected: %s)' % (actual, name)) @@ -375,6 +380,7 @@ class Jenkins(object): >>> print(build_info) {u'building': False, u'changeSet': {u'items': [{u'date': u'2011-12-19T18:01:52.540557Z', u'msg': u'test', u'revision': 66, u'user': u'unknown', u'paths': [{u'editType': u'edit', u'file': u'/branches/demo/index.html'}]}], u'kind': u'svn', u'revisions': [{u'module': u'http://eaas-svn01.i3.level3.com/eaas', u'revision': 66}]}, u'builtOn': u'', u'description': None, u'artifacts': [{u'relativePath': u'dist/eaas-87-2011-12-19_18-01-57.war', u'displayPath': u'eaas-87-2011-12-19_18-01-57.war', u'fileName': u'eaas-87-2011-12-19_18-01-57.war'}, {u'relativePath': u'dist/eaas-87-2011-12-19_18-01-57.war.zip', u'displayPath': u'eaas-87-2011-12-19_18-01-57.war.zip', u'fileName': u'eaas-87-2011-12-19_18-01-57.war.zip'}], u'timestamp': 1324317717000, u'number': 87, u'actions': [{u'parameters': [{u'name': u'SERVICE_NAME', u'value': u'eaas'}, {u'name': u'PROJECT_NAME', u'value': u'demo'}]}, {u'causes': [{u'userName': u'anonymous', u'shortDescription': u'Started by user anonymous'}]}, {}, {}, {}], u'id': u'2011-12-19_18-01-57', u'keepLog': False, u'url': u'http://eaas-jenkins01.i3.level3.com:9080/job/build_war/87/', u'culprits': [{u'absoluteUrl': u'http://eaas-jenkins01.i3.level3.com:9080/user/unknown', u'fullName': u'unknown'}], u'result': u'SUCCESS', u'duration': 8826, u'fullDisplayName': u'build_war #87'} ''' + folder_url, short_name = self._get_job_folder(name) try: response = self.jenkins_open(Request( self._build_url(BUILD_INFO, locals()) @@ -421,12 +427,15 @@ class Jenkins(object): # mechanism, so ignore it pass - def get_info(self): - """Get information on this Master. + def get_info(self, item="", query=None): + """Get information on this Master or item on Master. - This information includes job list and view information. + This information includes job list and view information and can be + used to retreive information on items such as job folders. - :returns: dictionary of information about Master, ``dict`` + :param item: item to get information about on this Master + :param query: xpath to extract information about on this Master + :returns: dictionary of information about Master or item, ``dict`` Example:: @@ -437,9 +446,12 @@ class Jenkins(object): u'name': u'my_job'} """ + url = "/".join((item, INFO)) + if query: + url += query try: return json.loads(self.jenkins_open( - Request(self._build_url(INFO)) + Request(self._build_url(url)) )) except (HTTPError, BadStatusLine): raise BadHTTPException("Error communicating with server[%s]" @@ -549,21 +561,106 @@ class Jenkins(object): raise JenkinsException("Could not parse JSON info for server[%s]" % self.server) - def get_jobs(self): - """Get list of jobs running. + def get_jobs(self, folder_depth=0): + """Get list of jobs. - Each job is a dictionary with 'name', 'url', and 'color' keys. + Each job is a dictionary with 'name', 'url', 'color' and 'fullname' + keys. + :param folder_depth: Number of levels to search, ``int``. By default + 0, which will limit search to toplevel. None disables the limit. :returns: list of jobs, ``[ { str: str} ]`` """ - return self.get_info()['jobs'] + + return self.get_all_jobs(folder_depth=folder_depth) + + def get_all_jobs(self, folder_depth=None): + """Get list of all jobs recursively to the given folder depth. + + Each job is a dictionary with 'name', 'url', 'color' and 'fullname' + keys. + + :param folder_depth: Number of levels to search, ``int``. By default + None, which will search all levels. 0 limits to toplevel. + :returns: list of jobs, ``[ { str: str} ]`` + + .. note:: + + On instances with many folders it may be more efficient to use the + run_script method to retrieve all jobs instead. + + Example:: + + server.run_script(\"\"\" + import groovy.json.JsonBuilder; + + // get all projects excluding matrix configuration + // as they are simply part of a matrix project. + // there may be better ways to get just jobs + items = Jenkins.instance.getAllItems(AbstractProject); + items.removeAll { + it instanceof hudson.matrix.MatrixConfiguration + }; + + def json = new JsonBuilder() + def root = json { + jobs items.collect { + [ + name: it.name, + url: Jenkins.instance.getRootUrl() + it.getUrl(), + color: it.getIconColor().toString(), + fullname: it.getFullName() + ] + } + } + + // use json.toPrettyString() if viewing + println json.toString() + \"\"\") + + """ + jobs_list = [] + + jobs = [(0, "", self.get_info(query=JOBS_QUERY)['jobs'])] + for lvl, root, lvl_jobs in jobs: + if not isinstance(lvl_jobs, list): + lvl_jobs = [lvl_jobs] + for job in lvl_jobs: + if 'jobs' in job: # folder + if folder_depth is None or lvl < folder_depth: + path = '/job/'.join((root, job[u'name'])) + jobs.append( + (lvl + 1, path, + self.get_info(path, + query=JOBS_QUERY)['jobs'])) + else: + # insert fullname info if it doesn't exist to + # allow callers to easily reference unambiguously + if u'fullname' not in job: + job[u'fullname'] = '/'.join( + [p for p in root.split('/') + if p and p != 'job'] + + [job[u'name']]) + jobs_list.append(job) + return jobs_list def copy_job(self, from_name, to_name): - '''Copy a Jenkins job + '''Copy a Jenkins job. + + Will raise an exception whenever the source and destination folder + for this jobs won't be the same. :param from_name: Name of Jenkins job to copy from, ``str`` :param to_name: Name of Jenkins job to copy to, ``str`` + :throws: :class:`JenkinsException` whenever the source and destination + folder are not the same ''' + from_folder_url, from_short_name = self._get_job_folder(from_name) + to_folder_url, to_short_name = self._get_job_folder(to_name) + if from_folder_url != to_folder_url: + raise JenkinsException('copy[%s to %s] failed, source and destination ' + 'folder must be the same' % (from_name, to_name)) + self.jenkins_open(Request( self._build_url(COPY_JOB, locals()), b'')) self.assert_job_exists(to_name, 'create[%s] failed') @@ -571,9 +668,19 @@ class Jenkins(object): def rename_job(self, from_name, to_name): '''Rename an existing Jenkins job + Will raise an exception whenever the source and destination folder + for this jobs won't be the same. + :param from_name: Name of Jenkins job to rename, ``str`` :param to_name: New Jenkins job name, ``str`` + :throws: :class:`JenkinsException` whenever the source and destination + folder are not the same ''' + from_folder_url, from_short_name = self._get_job_folder(from_name) + to_folder_url, to_short_name = self._get_job_folder(to_name) + if from_folder_url != to_folder_url: + raise JenkinsException('rename[%s to %s] failed, source and destination folder ' + 'must be the same' % (from_name, to_name)) self.jenkins_open(Request( self._build_url(RENAME_JOB, locals()), b'')) self.assert_job_exists(to_name, 'rename[%s] failed') @@ -583,6 +690,7 @@ class Jenkins(object): :param name: Name of Jenkins job, ``str`` ''' + folder_url, short_name = self._get_job_folder(name) self.jenkins_open(Request( self._build_url(DELETE_JOB, locals()), b'')) if self.job_exists(name): @@ -593,6 +701,7 @@ class Jenkins(object): :param name: Name of Jenkins job, ``str`` ''' + folder_url, short_name = self._get_job_folder(name) self.jenkins_open(Request( self._build_url(ENABLE_JOB, locals()), b'')) @@ -603,6 +712,7 @@ class Jenkins(object): :param name: Name of Jenkins job, ``str`` ''' + folder_url, short_name = self._get_job_folder(name) self.jenkins_open(Request( self._build_url(DISABLE_JOB, locals()), b'')) @@ -612,15 +722,32 @@ class Jenkins(object): :param name: Name of Jenkins job, ``str`` :returns: ``True`` if Jenkins job exists ''' - if self.get_job_name(name) == name: + folder_url, short_name = self._get_job_folder(name) + if self.get_job_name(name) == short_name: return True def jobs_count(self): '''Get the number of jobs on the Jenkins server :returns: Total number of jobs, ``int`` + + .. note:: + + On instances with many folders it may be more efficient to use the + run_script method to retrieve the total number of jobs instead. + + Example:: + + # get all projects excluding matrix configuration + # as they are simply part of a matrix project. + server.run_script( + "print(Hudson.instance.getAllItems(" + " hudson.model.AbstractProject).count{" + " !(it instanceof hudson.matrix.MatrixConfiguration)" + " })") + ''' - return len(self.get_jobs()) + return len(self.get_all_jobs()) def assert_job_exists(self, name, exception_message='job[%s] does not exist'): @@ -640,12 +767,17 @@ class Jenkins(object): :param name: Name of Jenkins job, ``str`` :param config_xml: config file text, ``str`` ''' + folder_url, short_name = self._get_job_folder(name) if self.job_exists(name): raise JenkinsException('job[%s] already exists' % (name)) - self.jenkins_open(Request( - self._build_url(CREATE_JOB, locals()), - config_xml.encode('utf-8'), DEFAULT_HEADERS)) + try: + self.jenkins_open(Request( + self._build_url(CREATE_JOB, locals()), + config_xml.encode('utf-8'), DEFAULT_HEADERS)) + except NotFoundException: + raise JenkinsException('Cannot create job[%s] because folder ' + 'for the job does not exist' % (name)) self.assert_job_exists(name, 'create[%s] failed') def get_job_config(self, name): @@ -654,6 +786,7 @@ class Jenkins(object): :param name: Name of Jenkins job, ``str`` :returns: job configuration (XML format) ''' + folder_url, short_name = self._get_job_folder(name) request = Request(self._build_url(CONFIG_JOB, locals())) return self.jenkins_open(request) @@ -665,6 +798,7 @@ class Jenkins(object): :param name: Name of Jenkins job, ``str`` :param config_xml: New XML configuration, ``str`` ''' + folder_url, short_name = self._get_job_folder(name) reconfig_url = self._build_url(CONFIG_JOB, locals()) self.jenkins_open(Request(reconfig_url, config_xml.encode('utf-8'), DEFAULT_HEADERS)) @@ -679,6 +813,7 @@ class Jenkins(object): :param token: (optional) token for building job, ``str`` :returns: URL for building job ''' + folder_url, short_name = self._get_job_folder(name) if parameters: if token: parameters['token'] = token @@ -723,6 +858,7 @@ class Jenkins(object): :param name: Name of Jenkins job, ``str`` :param number: Jenkins build number for the job, ``int`` ''' + folder_url, short_name = self._get_job_folder(name) self.jenkins_open(Request( self._build_url(STOP_BUILD, locals()), b'')) @@ -937,6 +1073,7 @@ class Jenkins(object): :param name: Build number, ``int`` :returns: Build console output, ``str`` ''' + folder_url, short_name = self._get_job_folder(name) try: response = self.jenkins_open(Request( self._build_url(BUILD_CONSOLE_OUTPUT, locals()) @@ -950,6 +1087,24 @@ class Jenkins(object): raise JenkinsException('job[%s] number[%d] does not exist' % (name, number)) + def _get_job_folder(self, name): + '''Return the name and folder (see cloudbees plugin). + + This is a method to support cloudbees folder plugin. + Url request should take into account folder path when the job name specify it + (ex.: 'folder/job') + + :param name: Job name, ``str`` + :returns: Tuple [ 'folder path for Request', 'Name of job without folder path' ] + ''' + + a_path = name.split('/') + short_name = a_path[-1] + folder_url = (('job/' + '/job/'.join(a_path[:-1]) + '/') + if len(a_path) > 1 else '') + + return folder_url, short_name + def get_view_name(self, name): '''Return the name of a view using the API. diff --git a/test-requirements.txt b/test-requirements.txt index e21b1ce..0d2aa87 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -4,6 +4,6 @@ hacking>=0.5.6,<0.11 mock<1.1 unittest2 python-subunit -sphinx>=1.1.2,<1.2 +sphinx>=1.2,<1.3.0 testrepository testtools diff --git a/tests/jobs/base.py b/tests/jobs/base.py index de1827d..0ddf67b 100644 --- a/tests/jobs/base.py +++ b/tests/jobs/base.py @@ -1,3 +1,6 @@ +import copy +import json + from tests.base import JenkinsTestBase @@ -8,3 +11,49 @@ class JenkinsJobsTestBase(JenkinsTestBase): Foo """ + + +class JenkinsGetJobsTestBase(JenkinsJobsTestBase): + + jobs_in_folder = [ + [ + {'name': 'my_job1'}, + {'name': 'my_folder1', 'jobs': None}, + {'name': 'my_job2'} + ], + # my_folder1 jobs + [ + {'name': 'my_job3'}, + {'name': 'my_job4'} + ] + ] + + jobs_in_multiple_folders = copy.deepcopy(jobs_in_folder) + jobs_in_multiple_folders[1].insert( + 0, {'name': 'my_folder2', 'jobs': None}) + jobs_in_multiple_folders.append( + # my_folder1/my_folder2 jobs + [ + {'name': 'my_job1'}, + {'name': 'my_job2'} + ] + ) + + +def build_jobs_list_responses(jobs_list, server_url): + responses = [] + for jobs in jobs_list: + get_jobs_response = [] + for job in jobs: + job_json = { + u'url': u'%s/job/%s' % (server_url.rstrip('/'), job['name']), + u'name': job['name'], + u'color': u'blue' + } + if 'jobs' in job: + job_json[u'jobs'] = "null" + get_jobs_response.append(job_json) + + responses.append(json.dumps({u'jobs': get_jobs_response})) + + return responses diff --git a/tests/jobs/test_assert.py b/tests/jobs/test_assert.py index 83e34f6..ad02c37 100644 --- a/tests/jobs/test_assert.py +++ b/tests/jobs/test_assert.py @@ -18,6 +18,17 @@ class JenkinsAssertJobExistsTest(JenkinsJobsTestBase): 'job[NonExistent] does not exist') self._check_requests(jenkins_mock.call_args_list) + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_job_missing_in_folder(self, jenkins_mock): + jenkins_mock.side_effect = jenkins.NotFoundException() + + with self.assertRaises(jenkins.JenkinsException) as context_manager: + self.j.assert_job_exists('a Folder/NonExistent') + self.assertEqual( + str(context_manager.exception), + 'job[a Folder/NonExistent] does not exist') + self._check_requests(jenkins_mock.call_args_list) + @patch.object(jenkins.Jenkins, 'jenkins_open') def test_job_exists(self, jenkins_mock): jenkins_mock.side_effect = [ @@ -25,3 +36,11 @@ class JenkinsAssertJobExistsTest(JenkinsJobsTestBase): ] self.j.assert_job_exists('ExistingJob') self._check_requests(jenkins_mock.call_args_list) + + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_job_exists_in_folder(self, jenkins_mock): + jenkins_mock.side_effect = [ + json.dumps({'name': 'ExistingJob'}), + ] + self.j.assert_job_exists('a Folder/ExistingJob') + self._check_requests(jenkins_mock.call_args_list) diff --git a/tests/jobs/test_build.py b/tests/jobs/test_build.py index 2ab6025..83eb664 100644 --- a/tests/jobs/test_build.py +++ b/tests/jobs/test_build.py @@ -19,6 +19,19 @@ class JenkinsBuildJobTest(JenkinsJobsTestBase): self.assertEqual(build_info, {'foo': 'bar'}) self._check_requests(jenkins_mock.call_args_list) + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_in_folder(self, jenkins_mock): + jenkins_mock.side_effect = [ + {'foo': 'bar'}, + ] + + build_info = self.j.build_job(u'a Folder/Test Job') + + self.assertEqual(jenkins_mock.call_args[0][0].get_full_url(), + u'http://example.com/job/a%20Folder/job/Test%20Job/build') + self.assertEqual(build_info, {'foo': 'bar'}) + self._check_requests(jenkins_mock.call_args_list) + @patch.object(jenkins.Jenkins, 'jenkins_open') def test_with_token(self, jenkins_mock): jenkins_mock.side_effect = [ @@ -32,6 +45,19 @@ class JenkinsBuildJobTest(JenkinsJobsTestBase): self.assertEqual(build_info, {'foo': 'bar'}) self._check_requests(jenkins_mock.call_args_list) + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_in_folder_with_token(self, jenkins_mock): + jenkins_mock.side_effect = [ + {'foo': 'bar'}, + ] + + build_info = self.j.build_job(u'a Folder/TestJob', token='some_token') + + self.assertEqual(jenkins_mock.call_args[0][0].get_full_url(), + u'http://example.com/job/a%20Folder/job/TestJob/build?token=some_token') + self.assertEqual(build_info, {'foo': 'bar'}) + self._check_requests(jenkins_mock.call_args_list) + @patch.object(jenkins.Jenkins, 'jenkins_open') def test_with_parameters_and_token(self, jenkins_mock): jenkins_mock.side_effect = [ diff --git a/tests/jobs/test_config.py b/tests/jobs/test_config.py index 7299258..f880de7 100644 --- a/tests/jobs/test_config.py +++ b/tests/jobs/test_config.py @@ -14,3 +14,12 @@ class JenkinsGetJobConfigTest(JenkinsJobsTestBase): jenkins_mock.call_args[0][0].get_full_url(), u'http://example.com/job/Test%20Job/config.xml') self._check_requests(jenkins_mock.call_args_list) + + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_encodes_job_name_in_folder(self, jenkins_mock): + self.j.get_job_config(u'a folder/Test Job') + + self.assertEqual( + jenkins_mock.call_args[0][0].get_full_url(), + u'http://example.com/job/a%20folder/job/Test%20Job/config.xml') + self._check_requests(jenkins_mock.call_args_list) diff --git a/tests/jobs/test_copy.py b/tests/jobs/test_copy.py index 71c4736..bded76e 100644 --- a/tests/jobs/test_copy.py +++ b/tests/jobs/test_copy.py @@ -24,6 +24,23 @@ class JenkinsCopyJobTest(JenkinsJobsTestBase): self.assertTrue(self.j.job_exists('Test Job_2')) self._check_requests(jenkins_mock.call_args_list) + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_in_folder(self, jenkins_mock): + jenkins_mock.side_effect = [ + json.dumps({'name': 'Test Job_2'}), + json.dumps({'name': 'Test Job_2'}), + json.dumps({'name': 'Test Job_2'}), + ] + + self.j.copy_job(u'a Folder/Test Job', u'a Folder/Test Job_2') + + self.assertEqual( + jenkins_mock.call_args_list[0][0][0].get_full_url(), + 'http://example.com/job/a%20Folder/createItem' + '?name=Test%20Job_2&mode=copy&from=Test%20Job') + self.assertTrue(self.j.job_exists('a Folder/Test Job_2')) + self._check_requests(jenkins_mock.call_args_list) + @patch.object(jenkins.Jenkins, 'jenkins_open') def test_failed(self, jenkins_mock): jenkins_mock.side_effect = [ @@ -41,3 +58,35 @@ class JenkinsCopyJobTest(JenkinsJobsTestBase): str(context_manager.exception), 'create[TestJob_2] failed') self._check_requests(jenkins_mock.call_args_list) + + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_in_folder_failed(self, jenkins_mock): + jenkins_mock.side_effect = [ + None, + jenkins.NotFoundException(), + ] + + with self.assertRaises(jenkins.JenkinsException) as context_manager: + self.j.copy_job(u'a Folder/TestJob', u'a Folder/TestJob_2') + self.assertEqual( + jenkins_mock.call_args_list[0][0][0].get_full_url(), + 'http://example.com/job/a%20Folder/createItem' + '?name=TestJob_2&mode=copy&from=TestJob') + self.assertEqual( + str(context_manager.exception), + 'create[a Folder/TestJob_2] failed') + self._check_requests(jenkins_mock.call_args_list) + + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_in_another_folder_failed(self, jenkins_mock): + jenkins_mock.side_effect = [ + jenkins.JenkinsException() + ] + + with self.assertRaises(jenkins.JenkinsException) as context_manager: + self.j.copy_job(u'a Folder/TestJob', u'another Folder/TestJob_2') + self.assertEqual( + str(context_manager.exception), + ('copy[a Folder/TestJob to another Folder/TestJob_2] failed, ' + 'source and destination folder must be the same')) + self._check_requests(jenkins_mock.call_args_list) diff --git a/tests/jobs/test_create.py b/tests/jobs/test_create.py index 37ce3ae..828df51 100644 --- a/tests/jobs/test_create.py +++ b/tests/jobs/test_create.py @@ -22,6 +22,21 @@ class JenkinsCreateJobTest(JenkinsJobsTestBase): 'http://example.com/createItem?name=Test%20Job') self._check_requests(jenkins_mock.call_args_list) + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_in_folder(self, jenkins_mock): + jenkins_mock.side_effect = [ + jenkins.NotFoundException(), + None, + json.dumps({'name': 'Test Job'}), + ] + + self.j.create_job(u'a Folder/Test Job', self.config_xml) + + self.assertEqual( + jenkins_mock.call_args_list[1][0][0].get_full_url(), + 'http://example.com/job/a%20Folder/createItem?name=Test%20Job') + self._check_requests(jenkins_mock.call_args_list) + @patch.object(jenkins.Jenkins, 'jenkins_open') def test_already_exists(self, jenkins_mock): jenkins_mock.side_effect = [ @@ -39,6 +54,23 @@ class JenkinsCreateJobTest(JenkinsJobsTestBase): 'job[TestJob] already exists') self._check_requests(jenkins_mock.call_args_list) + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_already_exists_in_folder(self, jenkins_mock): + jenkins_mock.side_effect = [ + json.dumps({'name': 'TestJob'}), + None, + ] + + with self.assertRaises(jenkins.JenkinsException) as context_manager: + self.j.create_job(u'a Folder/TestJob', self.config_xml) + self.assertEqual( + jenkins_mock.call_args_list[0][0][0].get_full_url(), + 'http://example.com/job/a%20Folder/job/TestJob/api/json?tree=name') + self.assertEqual( + str(context_manager.exception), + 'job[a Folder/TestJob] already exists') + self._check_requests(jenkins_mock.call_args_list) + @patch.object(jenkins.Jenkins, 'jenkins_open') def test_failed(self, jenkins_mock): jenkins_mock.side_effect = [ @@ -59,3 +91,24 @@ class JenkinsCreateJobTest(JenkinsJobsTestBase): str(context_manager.exception), 'create[TestJob] failed') self._check_requests(jenkins_mock.call_args_list) + + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_failed_in_folder(self, jenkins_mock): + jenkins_mock.side_effect = [ + jenkins.NotFoundException(), + None, + jenkins.NotFoundException(), + ] + + with self.assertRaises(jenkins.JenkinsException) as context_manager: + self.j.create_job(u'a Folder/TestJob', self.config_xml) + self.assertEqual( + jenkins_mock.call_args_list[0][0][0].get_full_url(), + 'http://example.com/job/a%20Folder/job/TestJob/api/json?tree=name') + self.assertEqual( + jenkins_mock.call_args_list[1][0][0].get_full_url(), + 'http://example.com/job/a%20Folder/createItem?name=TestJob') + self.assertEqual( + str(context_manager.exception), + 'create[a Folder/TestJob] failed') + self._check_requests(jenkins_mock.call_args_list) diff --git a/tests/jobs/test_debug.py b/tests/jobs/test_debug.py index 6d6379a..ac54773 100644 --- a/tests/jobs/test_debug.py +++ b/tests/jobs/test_debug.py @@ -23,3 +23,20 @@ class JenkinsDebugJobInfoTest(JenkinsJobsTestBase): jenkins_mock.call_args[0][0].get_full_url(), u'http://example.com/job/Test%20Job/api/json?depth=0') self._check_requests(jenkins_mock.call_args_list) + + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_in_folder(self, jenkins_mock): + job_info_to_return = { + u'building': False, + u'msg': u'test', + u'revision': 66, + u'user': u'unknown' + } + jenkins_mock.return_value = json.dumps(job_info_to_return) + + self.j.debug_job_info(u'a Folder/Test Job') + + self.assertEqual( + jenkins_mock.call_args[0][0].get_full_url(), + u'http://example.com/job/a%20Folder/job/Test%20Job/api/json?depth=0') + self._check_requests(jenkins_mock.call_args_list) diff --git a/tests/jobs/test_delete.py b/tests/jobs/test_delete.py index ca96b60..e168e14 100644 --- a/tests/jobs/test_delete.py +++ b/tests/jobs/test_delete.py @@ -21,6 +21,20 @@ class JenkinsDeleteJobTest(JenkinsJobsTestBase): 'http://example.com/job/Test%20Job/doDelete') self._check_requests(jenkins_mock.call_args_list) + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_in_folder(self, jenkins_mock): + jenkins_mock.side_effect = [ + None, + jenkins.NotFoundException(), + ] + + self.j.delete_job(u'a Folder/Test Job') + + self.assertEqual( + jenkins_mock.call_args_list[0][0][0].get_full_url(), + 'http://example.com/job/a%20Folder/job/Test%20Job/doDelete') + self._check_requests(jenkins_mock.call_args_list) + @patch.object(jenkins.Jenkins, 'jenkins_open') def test_failed(self, jenkins_mock): jenkins_mock.side_effect = [ @@ -38,3 +52,21 @@ class JenkinsDeleteJobTest(JenkinsJobsTestBase): str(context_manager.exception), 'delete[TestJob] failed') self._check_requests(jenkins_mock.call_args_list) + + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_in_folder_failed(self, jenkins_mock): + jenkins_mock.side_effect = [ + json.dumps({'name': 'TestJob'}), + json.dumps({'name': 'TestJob'}), + json.dumps({'name': 'TestJob'}), + ] + + with self.assertRaises(jenkins.JenkinsException) as context_manager: + self.j.delete_job(u'a Folder/TestJob') + self.assertEqual( + jenkins_mock.call_args_list[0][0][0].get_full_url(), + 'http://example.com/job/a%20Folder/job/TestJob/doDelete') + self.assertEqual( + str(context_manager.exception), + 'delete[a Folder/TestJob] failed') + self._check_requests(jenkins_mock.call_args_list) diff --git a/tests/jobs/test_disable.py b/tests/jobs/test_disable.py index 06b0ed0..8e1fb7d 100644 --- a/tests/jobs/test_disable.py +++ b/tests/jobs/test_disable.py @@ -21,3 +21,18 @@ class JenkinsDisableJobTest(JenkinsJobsTestBase): 'http://example.com/job/Test%20Job/disable') self.assertTrue(self.j.job_exists('Test Job')) self._check_requests(jenkins_mock.call_args_list) + + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_in_folder(self, jenkins_mock): + jenkins_mock.side_effect = [ + json.dumps({'name': 'Test Job'}), + json.dumps({'name': 'Test Job'}), + ] + + self.j.disable_job(u'a Folder/Test Job') + + self.assertEqual( + jenkins_mock.call_args_list[0][0][0].get_full_url(), + 'http://example.com/job/a%20Folder/job/Test%20Job/disable') + self.assertTrue(self.j.job_exists('a Folder/Test Job')) + self._check_requests(jenkins_mock.call_args_list) diff --git a/tests/jobs/test_enable.py b/tests/jobs/test_enable.py index aeffb64..e6354c9 100644 --- a/tests/jobs/test_enable.py +++ b/tests/jobs/test_enable.py @@ -21,3 +21,18 @@ class JenkinsEnableJobTest(JenkinsJobsTestBase): 'http://example.com/job/TestJob/enable') self.assertTrue(self.j.job_exists('TestJob')) self._check_requests(jenkins_mock.call_args_list) + + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_in_folder(self, jenkins_mock): + jenkins_mock.side_effect = [ + json.dumps({'name': 'TestJob'}), + json.dumps({'name': 'TestJob'}), + ] + + self.j.enable_job(u'a Folder/TestJob') + + self.assertEqual( + jenkins_mock.call_args_list[0][0][0].get_full_url(), + 'http://example.com/job/a%20Folder/job/TestJob/enable') + self.assertTrue(self.j.job_exists('a Folder/TestJob')) + self._check_requests(jenkins_mock.call_args_list) diff --git a/tests/jobs/test_get.py b/tests/jobs/test_get.py index b57f8ae..d93f2d5 100644 --- a/tests/jobs/test_get.py +++ b/tests/jobs/test_get.py @@ -2,10 +2,11 @@ import json from mock import patch import jenkins -from tests.jobs.base import JenkinsJobsTestBase +from tests.jobs.base import build_jobs_list_responses +from tests.jobs.base import JenkinsGetJobsTestBase -class JenkinsGetJobsTest(JenkinsJobsTestBase): +class JenkinsGetJobsTest(JenkinsGetJobsTestBase): @patch.object(jenkins.Jenkins, 'jenkins_open') def test_simple(self, jenkins_mock): @@ -19,8 +20,40 @@ class JenkinsGetJobsTest(JenkinsJobsTestBase): job_info = self.j.get_jobs() - self.assertEqual(job_info, jobs) + jobs[u'fullname'] = jobs[u'name'] + self.assertEqual(job_info, [jobs]) self.assertEqual( jenkins_mock.call_args[0][0].get_full_url(), - u'http://example.com/api/json') + u'http://example.com/api/json?tree=jobs[url,color,name,jobs]') self._check_requests(jenkins_mock.call_args_list) + + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_folders_simple(self, jenkins_mock): + response = build_jobs_list_responses( + self.jobs_in_folder, 'http://example.com/') + jenkins_mock.side_effect = iter(response) + + jobs_info = self.j.get_jobs() + + expected_fullnames = [ + u"my_job1", u"my_job2" + ] + self.assertEqual(len(expected_fullnames), len(jobs_info)) + got_fullnames = [job[u"fullname"] for job in jobs_info] + self.assertEqual(expected_fullnames, got_fullnames) + + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_folders_additional_level(self, jenkins_mock): + response = build_jobs_list_responses( + self.jobs_in_folder, 'http://example.com/') + jenkins_mock.side_effect = iter(response) + + jobs_info = self.j.get_jobs(folder_depth=1) + + expected_fullnames = [ + u"my_job1", u"my_job2", + u"my_folder1/my_job3", u"my_folder1/my_job4" + ] + self.assertEqual(len(expected_fullnames), len(jobs_info)) + got_fullnames = [job[u"fullname"] for job in jobs_info] + self.assertEqual(expected_fullnames, got_fullnames) diff --git a/tests/jobs/test_getall.py b/tests/jobs/test_getall.py new file mode 100644 index 0000000..fad2570 --- /dev/null +++ b/tests/jobs/test_getall.py @@ -0,0 +1,61 @@ +from mock import patch + +import jenkins +from tests.jobs.base import build_jobs_list_responses +from tests.jobs.base import JenkinsGetJobsTestBase + + +class JenkinsGetAllJobsTest(JenkinsGetJobsTestBase): + + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_simple(self, jenkins_mock): + response = build_jobs_list_responses( + self.jobs_in_folder, 'http://example.com/') + jenkins_mock.side_effect = iter(response) + + jobs_info = self.j.get_all_jobs() + + expected_fullnames = [ + u"my_job1", u"my_job2", + u"my_folder1/my_job3", u"my_folder1/my_job4" + ] + self.assertEqual(len(expected_fullnames), len(jobs_info)) + got_fullnames = [job[u"fullname"] for job in jobs_info] + self.assertEqual(expected_fullnames, got_fullnames) + + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_multi_level(self, jenkins_mock): + response = build_jobs_list_responses( + self.jobs_in_multiple_folders, 'http://example.com/') + jenkins_mock.side_effect = iter(response) + + jobs_info = self.j.get_all_jobs() + + expected_fullnames = [ + u"my_job1", u"my_job2", + u"my_folder1/my_job3", u"my_folder1/my_job4", + u"my_folder1/my_folder2/my_job1", u"my_folder1/my_folder2/my_job2" + ] + self.assertEqual(len(expected_fullnames), len(jobs_info)) + got_fullnames = [job[u"fullname"] for job in jobs_info] + self.assertEqual(expected_fullnames, got_fullnames) + # multiple jobs with same name + self.assertEqual(2, len([True + for job in jobs_info + if job['name'] == u"my_job1"])) + + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_folders_depth(self, jenkins_mock): + response = build_jobs_list_responses( + self.jobs_in_multiple_folders, 'http://example.com/') + jenkins_mock.side_effect = iter(response) + + jobs_info = self.j.get_all_jobs(folder_depth=1) + + expected_fullnames = [ + u"my_job1", u"my_job2", + u"my_folder1/my_job3", u"my_folder1/my_job4" + ] + self.assertEqual(len(expected_fullnames), len(jobs_info)) + got_fullnames = [job[u"fullname"] for job in jobs_info] + self.assertEqual(expected_fullnames, got_fullnames) diff --git a/tests/jobs/test_info.py b/tests/jobs/test_info.py index 3df80ff..923974e 100644 --- a/tests/jobs/test_info.py +++ b/tests/jobs/test_info.py @@ -25,6 +25,24 @@ class JenkinsGetJobInfoTest(JenkinsJobsTestBase): u'http://example.com/job/Test%20Job/api/json?depth=0') self._check_requests(jenkins_mock.call_args_list) + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_in_folder(self, jenkins_mock): + job_info_to_return = { + u'building': False, + u'msg': u'test', + u'revision': 66, + u'user': u'unknown' + } + jenkins_mock.return_value = json.dumps(job_info_to_return) + + job_info = self.j.get_job_info(u'a Folder/Test Job') + + self.assertEqual(job_info, job_info_to_return) + self.assertEqual( + jenkins_mock.call_args[0][0].get_full_url(), + u'http://example.com/job/a%20Folder/job/Test%20Job/api/json?depth=0') + self._check_requests(jenkins_mock.call_args_list) + @patch.object(jenkins.Jenkins, 'jenkins_open') def test_regex(self, jenkins_mock): jobs = [ @@ -91,3 +109,22 @@ class JenkinsGetJobInfoTest(JenkinsJobsTestBase): str(context_manager.exception), 'job[TestJob] does not exist') self._check_requests(jenkins_mock.call_args_list) + + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_in_folder_raise_HTTPError(self, jenkins_mock): + jenkins_mock.side_effect = jenkins.HTTPError( + 'http://example.com/job/a%20Folder/job/TestJob/api/json?depth=0', + code=401, + msg="basic auth failed", + hdrs=[], + fp=None) + + with self.assertRaises(jenkins.JenkinsException) as context_manager: + self.j.get_job_info(u'a Folder/TestJob') + self.assertEqual( + jenkins_mock.call_args[0][0].get_full_url(), + u'http://example.com/job/a%20Folder/job/TestJob/api/json?depth=0') + self.assertEqual( + str(context_manager.exception), + 'job[a Folder/TestJob] does not exist') + self._check_requests(jenkins_mock.call_args_list) diff --git a/tests/jobs/test_name.py b/tests/jobs/test_name.py index bbcc8f8..47d44ce 100644 --- a/tests/jobs/test_name.py +++ b/tests/jobs/test_name.py @@ -20,6 +20,19 @@ class JenkinsGetJobNameTest(JenkinsJobsTestBase): u'http://example.com/job/Test%20Job/api/json?tree=name') self._check_requests(jenkins_mock.call_args_list) + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_in_folder(self, jenkins_mock): + job_name_to_return = {u'name': 'Test Job'} + jenkins_mock.return_value = json.dumps(job_name_to_return) + + job_name = self.j.get_job_name(u'a Folder/Test Job') + + self.assertEqual(job_name, 'Test Job') + self.assertEqual( + jenkins_mock.call_args[0][0].get_full_url(), + u'http://example.com/job/a%20Folder/job/Test%20Job/api/json?tree=name') + self._check_requests(jenkins_mock.call_args_list) + @patch.object(jenkins.Jenkins, 'jenkins_open') def test_return_none(self, jenkins_mock): jenkins_mock.side_effect = jenkins.NotFoundException() @@ -32,6 +45,18 @@ class JenkinsGetJobNameTest(JenkinsJobsTestBase): u'http://example.com/job/TestJob/api/json?tree=name') self._check_requests(jenkins_mock.call_args_list) + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_in_folder_return_none(self, jenkins_mock): + jenkins_mock.side_effect = jenkins.NotFoundException() + + job_name = self.j.get_job_name(u'a Folder/TestJob') + + self.assertEqual(job_name, None) + self.assertEqual( + jenkins_mock.call_args[0][0].get_full_url(), + u'http://example.com/job/a%20Folder/job/TestJob/api/json?tree=name') + self._check_requests(jenkins_mock.call_args_list) + @patch.object(jenkins.Jenkins, 'jenkins_open') def test_unexpected_job_name(self, jenkins_mock): job_name_to_return = {u'name': 'not the right name'} @@ -47,3 +72,19 @@ class JenkinsGetJobNameTest(JenkinsJobsTestBase): 'Jenkins returned an unexpected job name {0} ' '(expected: {1})'.format(job_name_to_return['name'], 'TestJob')) self._check_requests(jenkins_mock.call_args_list) + + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_in_folder_unexpected_job_name(self, jenkins_mock): + job_name_to_return = {u'name': 'not the right name'} + jenkins_mock.return_value = json.dumps(job_name_to_return) + + with self.assertRaises(jenkins.JenkinsException) as context_manager: + self.j.get_job_name(u'a Folder/TestJob') + self.assertEqual( + jenkins_mock.call_args_list[0][0][0].get_full_url(), + 'http://example.com/job/a%20Folder/job/TestJob/api/json?tree=name') + self.assertEqual( + str(context_manager.exception), + 'Jenkins returned an unexpected job name {0} (expected: ' + '{1})'.format(job_name_to_return['name'], 'a Folder/TestJob')) + self._check_requests(jenkins_mock.call_args_list) diff --git a/tests/jobs/test_reconfig.py b/tests/jobs/test_reconfig.py index 09eef5d..007be15 100644 --- a/tests/jobs/test_reconfig.py +++ b/tests/jobs/test_reconfig.py @@ -19,3 +19,16 @@ class JenkinsReconfigJobTest(JenkinsJobsTestBase): self.assertEqual(jenkins_mock.call_args[0][0].get_full_url(), u'http://example.com/job/Test%20Job/config.xml') self._check_requests(jenkins_mock.call_args_list) + + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_in_folder(self, jenkins_mock): + jenkins_mock.side_effect = [ + json.dumps({'name': 'Test Job'}), + None, + ] + + self.j.reconfig_job(u'a Folder/Test Job', self.config_xml) + + self.assertEqual(jenkins_mock.call_args[0][0].get_full_url(), + u'http://example.com/job/a%20Folder/job/Test%20Job/config.xml') + self._check_requests(jenkins_mock.call_args_list) diff --git a/tests/jobs/test_rename.py b/tests/jobs/test_rename.py index 27d9267..24f2e44 100644 --- a/tests/jobs/test_rename.py +++ b/tests/jobs/test_rename.py @@ -23,6 +23,22 @@ class JenkinsRenameJobTest(JenkinsJobsTestBase): self.assertTrue(self.j.job_exists('Test Job_2')) self._check_requests(jenkins_mock.call_args_list) + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_in_folder(self, jenkins_mock): + jenkins_mock.side_effect = [ + json.dumps({'name': 'Test Job_2'}), + json.dumps({'name': 'Test Job_2'}), + json.dumps({'name': 'Test Job_2'}), + ] + + self.j.rename_job(u'a Folder/Test Job', u'a Folder/Test Job_2') + + self.assertEqual( + jenkins_mock.call_args_list[0][0][0].get_full_url(), + 'http://example.com/job/a%20Folder/job/Test%20Job/doRename?newName=Test%20Job_2') + self.assertTrue(self.j.job_exists('Test Job_2')) + self._check_requests(jenkins_mock.call_args_list) + @patch.object(jenkins.Jenkins, 'jenkins_open') def test_failed(self, jenkins_mock): jenkins_mock.side_effect = [ @@ -39,3 +55,34 @@ class JenkinsRenameJobTest(JenkinsJobsTestBase): str(context_manager.exception), 'rename[TestJob_2] failed') self._check_requests(jenkins_mock.call_args_list) + + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_in_folder_failed(self, jenkins_mock): + jenkins_mock.side_effect = [ + None, + jenkins.NotFoundException(), + ] + + with self.assertRaises(jenkins.JenkinsException) as context_manager: + self.j.rename_job(u'a Folder/TestJob', u'a Folder/TestJob_2') + self.assertEqual( + jenkins_mock.call_args_list[0][0][0].get_full_url(), + 'http://example.com/job/a%20Folder/job/TestJob/doRename?newName=TestJob_2') + self.assertEqual( + str(context_manager.exception), + 'rename[a Folder/TestJob_2] failed') + self._check_requests(jenkins_mock.call_args_list) + + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_in_another_folder_failed(self, jenkins_mock): + jenkins_mock.side_effect = [ + jenkins.JenkinsException() + ] + + with self.assertRaises(jenkins.JenkinsException) as context_manager: + self.j.rename_job(u'a Folder/TestJob', u'another Folder/TestJob_2') + self.assertEqual( + str(context_manager.exception), + ('rename[a Folder/TestJob to another Folder/TestJob_2] failed, ' + 'source and destination folder must be the same')) + self._check_requests(jenkins_mock.call_args_list) diff --git a/tests/test_build.py b/tests/test_build.py index 0e76e2f..fc0b057 100644 --- a/tests/test_build.py +++ b/tests/test_build.py @@ -19,6 +19,18 @@ class JenkinsBuildConsoleTest(JenkinsTestBase): u'http://example.com/job/Test%20Job/52/consoleText') self._check_requests(jenkins_mock.call_args_list) + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_in_folder(self, jenkins_mock): + jenkins_mock.return_value = "build console output..." + + build_info = self.j.get_build_console_output(u'a Folder/Test Job', number=52) + + self.assertEqual(build_info, jenkins_mock.return_value) + self.assertEqual( + jenkins_mock.call_args[0][0].get_full_url(), + u'http://example.com/job/a%20Folder/job/Test%20Job/52/consoleText') + self._check_requests(jenkins_mock.call_args_list) + @patch.object(jenkins.Jenkins, 'jenkins_open') def test_return_none(self, jenkins_mock): jenkins_mock.return_value = None @@ -30,6 +42,17 @@ class JenkinsBuildConsoleTest(JenkinsTestBase): 'job[TestJob] number[52] does not exist') self._check_requests(jenkins_mock.call_args_list) + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_in_folder_return_none(self, jenkins_mock): + jenkins_mock.return_value = None + + with self.assertRaises(jenkins.JenkinsException) as context_manager: + self.j.get_build_console_output(u'A Folder/TestJob', number=52) + self.assertEqual( + str(context_manager.exception), + 'job[A Folder/TestJob] number[52] does not exist') + self._check_requests(jenkins_mock.call_args_list) + @patch.object(jenkins.Jenkins, 'jenkins_open') def test_return_invalid_json(self, jenkins_mock): jenkins_mock.return_value = 'Invalid JSON' @@ -57,6 +80,25 @@ class JenkinsBuildConsoleTest(JenkinsTestBase): 'job[TestJob] number[52] does not exist') self._check_requests(jenkins_mock.call_args_list) + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_in_folder_raise_HTTPError(self, jenkins_mock): + jenkins_mock.side_effect = jenkins.HTTPError( + 'http://example.com/job/a%20Folder/job/TestJob/52/consoleText', + code=401, + msg="basic auth failed", + hdrs=[], + fp=None) + + with self.assertRaises(jenkins.JenkinsException) as context_manager: + self.j.get_build_console_output(u'a Folder/TestJob', number=52) + self.assertEqual( + jenkins_mock.call_args[0][0].get_full_url(), + u'http://example.com/job/a%20Folder/job/TestJob/52/consoleText') + self.assertEqual( + str(context_manager.exception), + 'job[a Folder/TestJob] number[52] does not exist') + self._check_requests(jenkins_mock.call_args_list) + class JenkinsBuildInfoTest(JenkinsTestBase): @@ -78,6 +120,24 @@ class JenkinsBuildInfoTest(JenkinsTestBase): u'http://example.com/job/Test%20Job/52/api/json?depth=0') self._check_requests(jenkins_mock.call_args_list) + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_in_folder(self, jenkins_mock): + build_info_to_return = { + u'building': False, + u'msg': u'test', + u'revision': 66, + u'user': u'unknown' + } + jenkins_mock.return_value = json.dumps(build_info_to_return) + + build_info = self.j.get_build_info(u'a Folder/Test Job', number=52) + + self.assertEqual(build_info, build_info_to_return) + self.assertEqual( + jenkins_mock.call_args[0][0].get_full_url(), + u'http://example.com/job/a%20Folder/job/Test%20Job/52/api/json?depth=0') + self._check_requests(jenkins_mock.call_args_list) + @patch.object(jenkins.Jenkins, 'jenkins_open') def test_return_none(self, jenkins_mock): jenkins_mock.return_value = None @@ -116,6 +176,22 @@ class JenkinsBuildInfoTest(JenkinsTestBase): 'job[TestJob] number[52] does not exist') self._check_requests(jenkins_mock.call_args_list) + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_in_folder_raise_HTTPError(self, jenkins_mock): + jenkins_mock.side_effect = jenkins.HTTPError( + 'http://example.com/job/a%20Folder/job/TestJob/api/json?depth=0', + code=401, + msg="basic auth failed", + hdrs=[], + fp=None) + + with self.assertRaises(jenkins.JenkinsException) as context_manager: + self.j.get_build_info(u'a Folder/TestJob', number=52) + self.assertEqual( + str(context_manager.exception), + 'job[a Folder/TestJob] number[52] does not exist') + self._check_requests(jenkins_mock.call_args_list) + class JenkinsStopBuildTest(JenkinsTestBase): @@ -128,6 +204,16 @@ class JenkinsStopBuildTest(JenkinsTestBase): u'http://example.com/job/Test%20Job/52/stop') self._check_requests(jenkins_mock.call_args_list) + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_in_folder(self, jenkins_mock): + + self.j.stop_build(u'a Folder/Test Job', number=52) + + self.assertEqual( + jenkins_mock.call_args[0][0].get_full_url(), + u'http://example.com/job/a%20Folder/job/Test%20Job/52/stop') + self._check_requests(jenkins_mock.call_args_list) + class JenkinsListRunningBuildsTest(JenkinsTestBase): @patch.object(jenkins.Jenkins, 'get_node_info') diff --git a/tests/test_job_folder.py b/tests/test_job_folder.py new file mode 100644 index 0000000..94fb56c --- /dev/null +++ b/tests/test_job_folder.py @@ -0,0 +1,25 @@ +from mock import patch + +import jenkins +from tests.base import JenkinsTestBase + + +class JenkinsGetJobFolderTest(JenkinsTestBase): + + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_simple(self, jenkins_mock): + folder, name = self.j._get_job_folder('my job') + self.assertEqual(folder, '') + self.assertEqual(name, 'my job') + + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_single_level(self, jenkins_mock): + folder, name = self.j._get_job_folder('my folder/my job') + self.assertEqual(folder, 'job/my folder/') + self.assertEqual(name, 'my job') + + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_multi_level(self, jenkins_mock): + folder, name = self.j._get_job_folder('folder1/folder2/my job') + self.assertEqual(folder, 'job/folder1/job/folder2/') + self.assertEqual(name, 'my job')