Fetch full build list when incomplete
authorChristophe Bliard <christophe.bliard@smartesting.com>
Fri, 4 Oct 2013 16:12:18 +0000 (18:12 +0200)
committerChristophe Bliard <christophe.bliard@smartesting.com>
Fri, 4 Oct 2013 16:12:18 +0000 (18:12 +0200)
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

jenkinsapi/jenkinsbase.py
jenkinsapi/job.py
jenkinsapi_tests/unittests/test_job_get_all_builds.py [new file with mode: 0644]

index fe97914..dbada6e 100644 (file)
@@ -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:
index 282f5df..3d3057d 100644 (file)
@@ -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 (file)
index 0000000..1196291
--- /dev/null
@@ -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()