From: Christophe Bliard Date: Fri, 4 Oct 2013 16:12:18 +0000 (+0200) Subject: Fetch full build list when incomplete X-Git-Tag: v0.2.23~93^2 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=0a6eece44b13235a56fc75122fc679506443a671;p=tools%2Fpython-jenkinsapi.git Fetch full build list when incomplete By default, Jenkins returns the last 100 builds. If there are more builds, an extra api call is made to have all builds in the job object --- diff --git a/jenkinsapi/jenkinsbase.py b/jenkinsapi/jenkinsbase.py index fe97914..dbada6e 100644 --- a/jenkinsapi/jenkinsbase.py +++ b/jenkinsapi/jenkinsbase.py @@ -52,9 +52,9 @@ class JenkinsBase(object): url = self.python_api_url(self.baseurl) return self.get_data(url) - def get_data(self, url): + def get_data(self, url, params=None): requester = self.get_jenkins_obj().requester - response = requester.get_url(url) + response = requester.get_url(url, params) try: return eval(response.text) except Exception: diff --git a/jenkinsapi/job.py b/jenkinsapi/job.py index 282f5df..3d3057d 100644 --- a/jenkinsapi/job.py +++ b/jenkinsapi/job.py @@ -55,6 +55,31 @@ class Job(JenkinsBase, MutableJenkinsThing): def get_jenkins_obj(self): return self.jenkins + def _poll(self): + data = JenkinsBase._poll(self) + # jenkins loads only the first 100 builds, load more if needed + data = self._add_missing_builds(data) + return data + + def _add_missing_builds(self, data): + '''Query Jenkins to get all builds of the job in the data object. + + Jenkins API loads the first 100 builds and thus may not contain all builds + information. This method checks if all builds are loaded in the data object + and updates it with the missing builds if needed.''' + if not data.has_key("builds") or not data["builds"]: + return data + # do not call _buildid_for_type here: it would poll and do an infinite loop + oldest_loaded_build_number = data["builds"][-1]["number"] + first_build_number = data["firstBuild"]["number"] + all_builds_loaded = (oldest_loaded_build_number == first_build_number) + if all_builds_loaded: + return data + api_url = self.python_api_url(self.baseurl) + response = self.get_data(api_url, params={'tree': 'allBuilds[number,url]'}) + data['builds'] = response['allBuilds'] + return data + def _get_config_element_tree(self): """ The ElementTree objects creation is unnecessary, it can be a singleton per job diff --git a/jenkinsapi_tests/unittests/test_job_get_all_builds.py b/jenkinsapi_tests/unittests/test_job_get_all_builds.py new file mode 100644 index 0000000..1196291 --- /dev/null +++ b/jenkinsapi_tests/unittests/test_job_get_all_builds.py @@ -0,0 +1,185 @@ +import mock +import unittest + +from jenkinsapi import config +from jenkinsapi.job import Job +from jenkinsapi.jenkinsbase import JenkinsBase +from jenkinsapi.exceptions import NoBuildData + + +class TestJobGetAllBuilds(unittest.TestCase): + # this job has builds + JOB1_DATA = {"actions": [], + "description": "test job", + "displayName": "foo", + "displayNameOrNull": None, + "name": "foo", + "url": "http://halob:8080/job/foo/", + "buildable": True, + # do as if build 1 & 2 are not returned by jenkins + "builds": [{"number": 3, "url": "http://halob:8080/job/foo/3/"}], + "color": "blue", + "firstBuild": {"number": 1, "url": "http://halob:8080/job/foo/1/"}, + "healthReport": [{"description": "Build stability: No recent builds failed.", + "iconUrl": "health-80plus.png", "score": 100}], + "inQueue": False, + "keepDependencies": False, + "lastBuild": {"number": 4, "url": "http://halob:8080/job/foo/4/"}, # build running + "lastCompletedBuild": {"number": 3, "url": "http://halob:8080/job/foo/3/"}, + "lastFailedBuild": None, + "lastStableBuild": {"number": 3, "url": "http://halob:8080/job/foo/3/"}, + "lastSuccessfulBuild": {"number": 3, "url": "http://halob:8080/job/foo/3/"}, + "lastUnstableBuild": None, + "lastUnsuccessfulBuild": None, + "nextBuildNumber": 4, + "property": [], + "queueItem": None, + "concurrentBuild": False, + "downstreamProjects": [], + "scm": {}, + "upstreamProjects": []} + JOB1_ALL_BUILDS_DATA = {"allBuilds": [ + {"number": 3, "url": "http://halob:8080/job/foo/3/"}, + {"number": 2, "url": "http://halob:8080/job/foo/2/"}, + {"number": 1, "url": "http://halob:8080/job/foo/1/"}], + } + JOB1_API_URL = 'http://halob:8080/job/foo/%s' % config.JENKINS_API + + JOB2_DATA = {'actions': [], + 'buildable': True, + 'builds': [], + 'color': 'notbuilt', + 'concurrentBuild': False, + 'description': '', + 'displayName': 'look_ma_no_builds', + 'displayNameOrNull': None, + 'downstreamProjects': [], + 'firstBuild': None, + 'healthReport': [], + 'inQueue': False, + 'keepDependencies': False, + 'lastBuild': None, + 'lastCompletedBuild': None, + 'lastFailedBuild': None, + 'lastStableBuild': None, + 'lastSuccessfulBuild': None, + 'lastUnstableBuild': None, + 'lastUnsuccessfulBuild': None, + 'name': 'look_ma_no_builds', + 'nextBuildNumber': 1, + 'property': [{}], + 'queueItem': None, + 'scm': {}, + 'upstreamProjects': [], + 'url': 'http://halob:8080/job/look_ma_no_builds/'} + JOB2_API_URL = 'http://halob:8080/job/look_ma_no_builds/%s' % config.JENKINS_API + + # Full list available immediatly + JOB3_DATA = {"actions": [], + "description": "test job", + "displayName": "fullfoo", + "displayNameOrNull": None, + "name": "fullfoo", + "url": "http://halob:8080/job/fullfoo/", + "buildable": True, + # all builds have been returned by Jenkins + "builds": [{"number": 3, "url": "http://halob:8080/job/fullfoo/3/"}, + {"number": 2, "url": "http://halob:8080/job/fullfoo/2/"}, + {"number": 1, "url": "http://halob:8080/job/fullfoo/1/"}], + "color": "blue", + "firstBuild": {"number": 1, "url": "http://halob:8080/job/fullfoo/1/"}, + "healthReport": [{"description": "Build stability: No recent builds failed.", + "iconUrl": "health-80plus.png", "score": 100}], + "inQueue": False, + "keepDependencies": False, + "lastBuild": {"number": 4, "url": "http://halob:8080/job/fullfoo/4/"}, # build running + "lastCompletedBuild": {"number": 3, "url": "http://halob:8080/job/fullfoo/3/"}, + "lastFailedBuild": None, + "lastStableBuild": {"number": 3, "url": "http://halob:8080/job/fullfoo/3/"}, + "lastSuccessfulBuild": {"number": 3, "url": "http://halob:8080/job/fullfoo/3/"}, + "lastUnstableBuild": None, + "lastUnsuccessfulBuild": None, + "nextBuildNumber": 4, + "property": [], + "queueItem": None, + "concurrentBuild": False, + "downstreamProjects": [], + "scm": {}, + "upstreamProjects": []} + JOB3_ALL_BUILDS_DATA = {"allBuilds": [ + {"number": 3, "url": "http://halob:8080/job/fullfoo/3/"}, + {"number": 2, "url": "http://halob:8080/job/fullfoo/2/"}, + {"number": 1, "url": "http://halob:8080/job/fullfoo/1/"}], + } + JOB3_API_URL = 'http://halob:8080/job/fullfoo/%s' % config.JENKINS_API + + URL_DATA = { + JOB1_API_URL: JOB1_DATA, + (JOB1_API_URL, str({'tree': 'allBuilds[number,url]'})): JOB1_ALL_BUILDS_DATA, + JOB2_API_URL: JOB2_DATA, + JOB3_API_URL: JOB3_DATA, + # this one below should never be used + (JOB3_API_URL, str({'tree': 'allBuilds[number,url]'})): JOB3_ALL_BUILDS_DATA, + } + + + def fakeGetData(self, url, params=None): + TestJobGetAllBuilds.__get_data_call_count += 1 + if params is None: + try: + return dict(TestJobGetAllBuilds.URL_DATA[url]) + except KeyError: + raise Exception("Missing data for url: %s" % url) + else: + try: + return dict(TestJobGetAllBuilds.URL_DATA[(url, str(params))]) + except KeyError: + raise Exception("Missing data for url: %s with parameters %s" % (url, repr(params))) + + @mock.patch.object(JenkinsBase, 'get_data', fakeGetData) + def setUp(self): + TestJobGetAllBuilds.__get_data_call_count = 0 + self.J = mock.MagicMock() # Jenkins object + self.j = Job('http://halob:8080/job/foo/', 'foo', self.J) + + def test_get_build_dict(self): + # The job data contains only one build, so we expect that the + # remaining jobs will be fetched automatically + ret = self.j.get_build_dict() + self.assertTrue(isinstance(ret, dict)) + self.assertEquals(len(ret), 4) + + @mock.patch.object(JenkinsBase, 'get_data', fakeGetData) + def test_incomplete_builds_list_will_call_jenkins_twice(self): + # The job data contains only one build, so we expect that the + # remaining jobs will be fetched automatically, and to have two calls to + # the Jenkins API + TestJobGetAllBuilds.__get_data_call_count = 0 + self.j = Job('http://halob:8080/job/foo/', 'foo', self.J) + self.assertEquals(TestJobGetAllBuilds.__get_data_call_count, 2) + + @mock.patch.object(JenkinsBase, 'get_data', fakeGetData) + def test_complete_builds_list_will_call_jenkins_once(self): + # The job data contains all builds, so we will not gather remaining builds + TestJobGetAllBuilds.__get_data_call_count = 0 + self.j = Job('http://halob:8080/job/fullfoo/', 'fullfoo', self.J) + self.assertEquals(TestJobGetAllBuilds.__get_data_call_count, 1) + + @mock.patch.object(JenkinsBase, 'get_data', fakeGetData) + def test_nobuilds_get_build_dict(self): + j = Job('http://halob:8080/job/look_ma_no_builds/', 'look_ma_no_builds', self.J) + + ret = j.get_build_dict() + self.assertTrue(isinstance(ret, dict)) + self.assertEquals(len(ret), 0) + + def test_get_build_ids(self): + # The job data contains only one build, so we expect that the + # remaining jobs will be fetched automatically + ret = list(self.j.get_build_ids()) + self.assertTrue(isinstance(ret, list)) + self.assertEquals(len(ret), 4) + + +if __name__ == '__main__': + unittest.main()