Repository: pycontribs/jenkinsapi

Issue:
Artifact save failing due to incorrect fingerprint
I am trying to trigger a Jenkins build and then download the artifact generated to my system.
In my case the artifact gets downloaded but probably the verification gets failed and the following gets printed on the console.

`
ERROR:root:Failed request at http://jenkins:8080/fingerprint/95d55fcb03623e130e099edadca1fa93/api/python with params: None
`

On my Jenkins UI the fingerprint of the file is shown as 
MD5: 1bccb64e60524d933fa2d3f3334ce738


I then tried the following:

`
curl http://jenkins:8080/fingerprint/1bccb64e60524d933fa2d3f3334ce738/api/python --user username:password
`

which gives me the required JSON response

while trying the below returns

`
curl http://jenkins:8080/fingerprint/95d55fcb03623e130e099edadca1fa93/api/python --user username:password
`

<body><h2>HTTP ERROR 404</h2>
<p>Problem accessing /fingerprint/95d55fcb03623e130e099edadca1fa93/api/python. Reason:
<pre>    Not Found</pre></p><hr /><i><small>Powered by Jetty://</small></i><br/>


It looks like the md5 computed by Jenkins and the md5 computed by jenkinsapi don't match.


My system information:
Mac OSX El Capitan 10.11.5
Python version 2.7
jenkinsapi (0.2.30)
Jenkins v1.639


Comments:

Patch:
diff --git a/jenkinsapi/build.py b/jenkinsapi/build.py
index a616579..fe2d063 100644
--- a/jenkinsapi/build.py
+++ b/jenkinsapi/build.py
@@ -179,12 +179,31 @@ class Build(JenkinsBase):
     def get_duration(self):
         return datetime.timedelta(milliseconds=self._data["duration"])
 
+    def _get_build(self, job_name, buildno, cache):
+        key = (job_name, buildno)
+        if key not in cache:
+            cache[key] = self.get_jenkins_obj()[job_name].get_build(buildno)
+        return cache[key]
+
+    def _get_artifact_builds(self):
+        data = self.poll(tree='fingerprint[fileName,original[name,number]]')
+        build_cache = {(self.job.name, self.buildno): self}
+        builds = {}
+        for fpinfo in data["fingerprint"]:
+            buildno = fpinfo["original"]["number"]
+            job_name = fpinfo["original"]["name"]
+            build = self._get_build(job_name, buildno, build_cache)
+            builds[fpinfo["fileName"]] = build
+        return builds
+
     def get_artifacts(self):
         data = self.poll(tree='artifacts[relativePath,fileName]')
+        builds = self._get_artifact_builds()
         for afinfo in data["artifacts"]:
             url = "%s/artifact/%s" % (self.baseurl,
                                       quote(afinfo["relativePath"]))
-            af = Artifact(afinfo["fileName"], url, self,
+            fn = afinfo["fileName"]
+            af = Artifact(fn, url, builds.get(fn, self),
                           relative_path=afinfo["relativePath"])
             yield af
 


Test patch:
diff --git a/jenkinsapi_tests/unittests/test_build.py b/jenkinsapi_tests/unittests/test_build.py
index fd2203a..d82e0ac 100644
--- a/jenkinsapi_tests/unittests/test_build.py
+++ b/jenkinsapi_tests/unittests/test_build.py
@@ -160,6 +160,78 @@ class TestBuildCase(unittest.TestCase):
 
         self.assertDictEqual(params, expected)
 
+    @mock.patch.object(Build, 'poll')
+    def test_get_artifacts_from_other_builds(self, poll_mock):
+        fingerprint = {
+            "fingerprint": [
+                {
+                    "fileName": "artifact1.fn",
+                    "original": {
+                        "name": 'FooJob',
+                        "number": 97,
+                    },
+                },
+                {
+                    "fileName": "artifact2.fn",
+                    "original": {
+                        "name": 'FooJob',
+                        "number": 95,
+                    },
+                },
+                {
+                    "fileName": "artifact3.fn",
+                    "original": {
+                        "name": 'BarJob',
+                        "number": 97,
+                    },
+                },
+            ],
+        }
+        artifacts = {
+            'artifacts': [
+                {
+                    "fileName": "artifact1.fn",
+                    "relativePath": "dir/artifact1.fn"
+                },
+                {
+                    "fileName": "artifact2.fn",
+                    "relativePath": "dir/artifact2.fn"
+                },
+                {
+                    "fileName": "artifact3.fn",
+                    "relativePath": "dir/artifact3.fn"
+                },
+            ],
+        }
+
+        # set up poll for get_artifacts calls
+        def poll_fn(tree):
+            if tree == 'fingerprint[fileName,original[name,number]]':
+                return fingerprint
+            elif tree == 'artifacts[relativePath,fileName]':
+                return artifacts
+            else:
+                raise ValueError('bad tree')
+        poll_mock.side_effect = poll_fn
+
+        # set up jenkins for retrieving other builds
+        foo_mock = mock.Mock()
+        bar_mock = mock.Mock()
+        self.j.get_jenkins_obj.return_value = {
+            'FooJob': foo_mock,
+            'BarJob': bar_mock,
+        }
+        foo_mock.get_build.return_value = 'FooJob95'
+        bar_mock.get_build.return_value = 'BarJob97'
+
+        # check artifacts
+        artifacts = self.b.get_artifact_dict()
+        self.assertEqual(self.b, artifacts['artifact1.fn'].build)
+        self.assertEqual('FooJob95', artifacts['artifact2.fn'].build)
+        self.assertEqual('BarJob97', artifacts['artifact3.fn'].build)
+        foo_mock.get_build.assert_called_once_with(95)
+        bar_mock.get_build.assert_called_once_with(97)
+
     # TEST DISABLED - DOES NOT WORK
     # def test_downstream(self):
     #     expected = ['SingleJob','MultipleJobs']