Merge lp:~pfalcon/linaro-ci-dashboard/kernel_chain into lp:linaro-ci-dashboard
- kernel_chain
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 68 |
Proposed branch: | lp:~pfalcon/linaro-ci-dashboard/kernel_chain |
Merge into: | lp:linaro-ci-dashboard |
Diff against target: |
457 lines (+238/-13) 15 files modified
config_template/jenkins-jobs/AndroidLoop.xml (+1/-1) config_template/jenkins-jobs/HwpackLoop.xml (+1/-1) config_template/jenkins-jobs/IntegrationLoop.xml (+6/-1) config_template/jenkins-jobs/KernelLoop.xml (+1/-1) dashboard/frontend/hwpack_loop/models/hwpack_loop.py (+1/-1) dashboard/frontend/kernel_build/models/kernel_loop.py (+20/-0) dashboard/frontend/kernel_build/tests/__init__.py (+2/-0) dashboard/frontend/kernel_build/tests/test_kernel_chaining.py (+70/-0) dashboard/frontend/models/loop.py (+20/-2) dashboard/frontend/models/loop_build.py (+38/-0) dashboard/frontend/models/loop_reference.py (+1/-0) dashboard/jenkinsserver/models/jenkins_server.py (+36/-5) dashboard/jenkinsserver/tests/test_jenkins_server.py (+35/-0) dashboard/lib/jenkins_dashboard.py (+2/-1) dashboard/settings.py (+4/-0) |
To merge this branch: | bzr merge lp:~pfalcon/linaro-ci-dashboard/kernel_chain |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Stevan Radaković | Approve | ||
Linaro Infrastructure | Pending | ||
Review via email:
|
Commit message
Description of the change
This is preliminary merge proposal for Kernel->Hwpack loop chaining implementation. This so far lacks tests, on which I'm working in the meantime, but I'd like to collect any early feedback. As usual with more or less big changesets, it may be more comfortable to review the changes commit by commit, instead of aggregated diff.
Otherwise, these changes already underwent some refactoring comparing to the initial implementation I coded, and should be pretty architecturally clean IMHO. The main change is to introduce Loop method process_
- 69. By Paul Sokolovsky
-
[merge] Merge from trunk.
- 70. By Paul Sokolovsky
-
Typo fix.

Milo Casagrande (milo) wrote : | # |
Hi Paul!
Some comments follow inline.
On Fri, Sep 21, 2012 at 10:34 AM, Paul Sokolovsky
<email address hidden> wrote:
> https:/
> Your team Linaro Infrastructure is requested to review the proposed merge of lp:~pfalcon/linaro-ci-dashboard/kernel_chain into lp:linaro-ci-dashboard.
>
> === modified file 'config_
> --- config_
> +++ config_
> @@ -178,7 +178,7 @@
> </builders>
> <publishers>
> <hudson.
> - <artifacts>
> + <artifacts>
Is it possible to be more greedy here? Like "*.img.*", so to avoid
file suffix, or there might be other file types that we do not want?
Minor suggestion though...
> @models.permalink
> def get_detail_
> return 'KernelLoopDetail', (), {'slug': self.name}
> @@ -135,3 +141,17 @@
> @models.permalink
> def get_update_
> return 'KernelLoopUpdate', (), {'slug': self.name}
> +
> + def process_
> + """This method gets called with list of artifact files from a build.
> + Its responsibility is to categorize artifacts, and prepare result data
> + to be passed to downstream loop in chain. If needed, this method can
> + access content of artifacts (not just list of them).
> + """
> + files = loop_build.
> + kernel_debs = filter(lambda fname: "linux-image" in fname, files)
> + if len(kernel_debs) == 1:
> + return {"kernel_deb_url": loop_build.
> + else:
> + self.log.
> + return {}
This is more a personal thing than anything else: why not having only
a single exit point? Just using a single return statement.
Overall it looks good and clean, maybe a couple of tests wouldn't hurt.
Thanks!
--
Milo Casagrande
Infrastructure Engineer
Linaro.org <www.linaro.org> │ Open source software for ARM SoCs
- 71. By Paul Sokolovsky
-
preconfigure(): Check that argument is not None before iterating over it.
- 72. By Paul Sokolovsky
-
Factor out variable with Jenkins URL to ease debugging.
This regulalrly needs to be dumped to find out what's wrong with Jenkins.
- 73. By Paul Sokolovsky
-
Add integration test for for artifact querying methods of JenkinsServer.
This required making IntegrationLoop to export artifacts (also exposes
shell code injection with loop build scripts). TODO: Provide special Loop
just for integration testing. - 74. By Paul Sokolovsky
-
Update another reference self.get_
build_info( ) -> self.update_ build_info( ) - 75. By Paul Sokolovsky
-
self.remote_number can be None during tests, so print it as string.
- 76. By Paul Sokolovsky
-
Add high-level unit test for kernel => hwpack loop chaining.
- 77. By Paul Sokolovsky
-
[merge] Merge from trunk.

Paul Sokolovsky (pfalcon) wrote : | # |
Milo:
> Is it possible to be more greedy here? Like "*.img.*", so to avoid
file suffix, or there might be other file types that we do not want?
Minor suggestion though...
Well, if you looked carefully, what I did to that line was adding "MANIFEST" file import. The original line likely was added by yourself while working on Android loop. But as it apparently was taken from a build config as set up be me, here's reply to the very question:
What about "1.img.
>This is more a personal thing than anything else: why not having only
a single exit point? Just using a single return statement.
Single statement of course would be possible, but that would program transformation skewing the original idea of the code. It's because 2 branches have different semantics: one executes if data is valid and returns expected processed data, while another executes when data invalid and returns "data invalid" token. To make the point more clear, in the next incarnation of this function, return in "data invalid" branch may become raise.

Paul Sokolovsky (pfalcon) wrote : | # |
Ok, so I finally get to complete tests for this. 2 tests were added:
1. Integrational test for new JenkinsServer methods to query artifacts.
2. Unit tests which mocks JenkinsServer methods and assures proper data flow between upstream and downstream tests in chain.

Stevan Radaković (stevanr) wrote : | # |
Looks good to me Paul, thanks!
Approve +1

Milo Casagrande (milo) wrote : | # |
Hi Paul,
thanks for fixing this, and also for the previous comments.
It looks good to me, I only have some errors when running the tests, but overall all the tests pass (total number is 96). Is that normal?
This is the kind of Python errors I see:
https:/

Stevan Radaković (stevanr) wrote : | # |
Strange I don't get such errors. Let's check tomorrow with Paul.

Paul Sokolovsky (pfalcon) wrote : | # |
Milo:
Here's my story: I didn't do work on ci-dashboard for a while, so when I restarted with work on these tested, I was greeted with the need to upgrade jenkins.war. I did, and then on first "python dashboard/manage.py test" run I got errors similar to those you quoted, except that in my case there were more errors "JenkinsException: create[
====
FAIL: test_login_teams (django_
-------
Traceback (most recent call last):
File "/usr/lib/
self.
AssertionError: False is not true
====
But that's known by all of us (?) heizenbug, except that it has much lower oscillation frequency (i.e. I have it stable, including with the trunk now even with several reruns, tomorrow it may be gone, day after appear again).
So, answering your question: it's "normal" (in quotes), in the sense that it's matter of fact that it does happen for more than one developer. It's not normal in other senses of this word, and we should take measures to investigate and fix those non-deterministic failures. First measure would be to use well-known jenkins version, instead of non-deterministic one - I proposed that some time ago, but didn't make that change at that time, so now it seems time has come.
Preview Diff
1 | === modified file 'config_template/jenkins-jobs/AndroidLoop.xml' |
2 | --- config_template/jenkins-jobs/AndroidLoop.xml 2012-09-18 15:08:05 +0000 |
3 | +++ config_template/jenkins-jobs/AndroidLoop.xml 2012-10-04 15:43:21 +0000 |
4 | @@ -178,7 +178,7 @@ |
5 | </builders> |
6 | <publishers> |
7 | <hudson.tasks.ArtifactArchiver> |
8 | - <artifacts>build/out/target/*/*/*.img.bz2,build/out/target/*/*/*.tar.bz2,build/out/target/*/*/MD5SUMS,build/out/*.tar.bz2,build/out/*.xml,build/out/*_config,build/out/lava-job-info,build/out/linaro_kernel_build_cmds.sh,build/out/linaro_android_build_cmds.sh,build/out/**/*EULA*</artifacts> |
9 | + <artifacts>build/out/target/*/*/*.img.bz2,build/out/target/*/*/*.tar.bz2,build/out/target/*/*/MD5SUMS,build/out/*.tar.bz2,build/out/*.xml,build/out/*_config,build/out/lava-job-info,build/out/linaro_kernel_build_cmds.sh,build/out/linaro_android_build_cmds.sh,build/out/**/*EULA*,MANIFEST</artifacts> |
10 | <latestOnly>false</latestOnly> |
11 | </hudson.tasks.ArtifactArchiver> |
12 | <hudson.plugins.logparser.LogParserPublisher> |
13 | |
14 | === modified file 'config_template/jenkins-jobs/HwpackLoop.xml' |
15 | --- config_template/jenkins-jobs/HwpackLoop.xml 2012-09-18 14:44:17 +0000 |
16 | +++ config_template/jenkins-jobs/HwpackLoop.xml 2012-10-04 15:43:21 +0000 |
17 | @@ -19,7 +19,7 @@ |
18 | </builders> |
19 | <publishers> |
20 | <hudson.tasks.ArtifactArchiver> |
21 | - <artifacts>*.tar.gz</artifacts> |
22 | + <artifacts>*.tar.gz,MANIFEST</artifacts> |
23 | <latestOnly>false</latestOnly> |
24 | </hudson.tasks.ArtifactArchiver> |
25 | </publishers> |
26 | |
27 | === modified file 'config_template/jenkins-jobs/IntegrationLoop.xml' |
28 | --- config_template/jenkins-jobs/IntegrationLoop.xml 2012-09-17 10:27:00 +0000 |
29 | +++ config_template/jenkins-jobs/IntegrationLoop.xml 2012-10-04 15:43:21 +0000 |
30 | @@ -17,6 +17,11 @@ |
31 | <command>{build_script}</command> |
32 | </hudson.tasks.Shell> |
33 | </builders> |
34 | - <publishers/> |
35 | + <publishers> |
36 | + <hudson.tasks.ArtifactArchiver> |
37 | + <artifacts>*.result,MANIFEST</artifacts> |
38 | + <latestOnly>false</latestOnly> |
39 | + </hudson.tasks.ArtifactArchiver> |
40 | + </publishers> |
41 | <buildWrappers/> |
42 | </project> |
43 | |
44 | === modified file 'config_template/jenkins-jobs/KernelLoop.xml' |
45 | --- config_template/jenkins-jobs/KernelLoop.xml 2012-09-18 13:29:37 +0000 |
46 | +++ config_template/jenkins-jobs/KernelLoop.xml 2012-10-04 15:43:21 +0000 |
47 | @@ -19,7 +19,7 @@ |
48 | </builders> |
49 | <publishers> |
50 | <hudson.tasks.ArtifactArchiver> |
51 | - <artifacts>*.deb</artifacts> |
52 | + <artifacts>*.deb,MANIFEST</artifacts> |
53 | <latestOnly>false</latestOnly> |
54 | </hudson.tasks.ArtifactArchiver> |
55 | </publishers> |
56 | |
57 | === modified file 'dashboard/frontend/hwpack_loop/models/hwpack_loop.py' |
58 | --- dashboard/frontend/hwpack_loop/models/hwpack_loop.py 2012-09-17 13:00:42 +0000 |
59 | +++ dashboard/frontend/hwpack_loop/models/hwpack_loop.py 2012-10-04 15:43:21 +0000 |
60 | @@ -32,7 +32,7 @@ |
61 | |
62 | # Version of hwpack tarball to build (passed directly to |
63 | # linaro-hwpack-create) |
64 | - hwpack_build_version = models.CharField(max_length=15) |
65 | + hwpack_build_version = models.CharField(max_length=15, default="1") |
66 | |
67 | # URL of kernel deb package to use for hwpack creation |
68 | kernel_deb_url = models.CharField(max_length=255, default="", blank=True) |
69 | |
70 | === modified file 'dashboard/frontend/kernel_build/models/kernel_loop.py' |
71 | --- dashboard/frontend/kernel_build/models/kernel_loop.py 2012-09-12 12:48:46 +0000 |
72 | +++ dashboard/frontend/kernel_build/models/kernel_loop.py 2012-10-04 15:43:21 +0000 |
73 | @@ -19,6 +19,7 @@ |
74 | from django.db import models |
75 | from frontend.models.loop import Loop |
76 | from jenkinsserver.models.jenkins_server import JenkinsServer |
77 | +from frontend.models.loop_reference import HWPACK_LOOP |
78 | |
79 | |
80 | LLT_URL = (r'git://git.linaro.org/kernel/linux-linaro-tracking.git', |
81 | @@ -124,6 +125,11 @@ |
82 | # TODO: Log error. |
83 | pass |
84 | |
85 | + @staticmethod |
86 | + def can_chain_into(): |
87 | + chain = [HWPACK_LOOP] |
88 | + return chain |
89 | + |
90 | @models.permalink |
91 | def get_detail_url(self): |
92 | return 'KernelLoopDetail', (), {'slug': self.name} |
93 | @@ -135,3 +141,17 @@ |
94 | @models.permalink |
95 | def get_update_url(self): |
96 | return 'KernelLoopUpdate', (), {'slug': self.name} |
97 | + |
98 | + def process_build_results(self, loop_build): |
99 | + """This method gets called with list of artifact files from a build. |
100 | + Its responsibility is to categorize artifacts, and prepare result data |
101 | + to be passed to downstream loop in chain. If needed, this method can |
102 | + access content of artifacts (not just list of them). |
103 | + """ |
104 | + files = loop_build.list_artifacts() |
105 | + kernel_debs = filter(lambda fname: "linux-image" in fname, files) |
106 | + if len(kernel_debs) == 1: |
107 | + return {"kernel_deb_url": loop_build.get_artifact_url(kernel_debs[0])} |
108 | + else: |
109 | + self.log.warning("No single kernel deb found among: %s", files) |
110 | + return {} |
111 | |
112 | === modified file 'dashboard/frontend/kernel_build/tests/__init__.py' |
113 | --- dashboard/frontend/kernel_build/tests/__init__.py 2012-08-23 11:22:21 +0000 |
114 | +++ dashboard/frontend/kernel_build/tests/__init__.py 2012-10-04 15:43:21 +0000 |
115 | @@ -1,9 +1,11 @@ |
116 | from dashboard.frontend.kernel_build.tests.test_kernel_build_views import * |
117 | from dashboard.frontend.kernel_build.tests.test_kernel_build_clientresponse \ |
118 | import * |
119 | +from dashboard.frontend.kernel_build.tests.test_kernel_chaining import * |
120 | |
121 | |
122 | __test__ = { |
123 | 'KernelBuildViewsTest': KernelBuildViewsTest, |
124 | 'KernelBuildClientResponseTests': KernelBuildClientResponseTests, |
125 | + 'KernelHwpackChainingTest': KernelHwpackChainingTest, |
126 | } |
127 | |
128 | === added file 'dashboard/frontend/kernel_build/tests/test_kernel_chaining.py' |
129 | --- dashboard/frontend/kernel_build/tests/test_kernel_chaining.py 1970-01-01 00:00:00 +0000 |
130 | +++ dashboard/frontend/kernel_build/tests/test_kernel_chaining.py 2012-10-04 15:43:21 +0000 |
131 | @@ -0,0 +1,70 @@ |
132 | +# Copyright (C) 2012 Linaro |
133 | +# |
134 | +# This file is part of linaro-ci-dashboard. |
135 | +# |
136 | +# linaro-ci-dashboard is free software: you can redistribute it and/or modify |
137 | +# it under the terms of the GNU Affero General Public License as published by |
138 | +# the Free Software Foundation, either version 3 of the License, or |
139 | +# (at your option) any later version. |
140 | +# |
141 | +# linaro-ci-dashboard is distributed in the hope that it will be useful, |
142 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
143 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
144 | +# GNU Affero General Public License for more details. |
145 | +# |
146 | +# You should have received a copy of the GNU Affero General Public License |
147 | +# along with linaro-ci-dashboard. If not, see <http://www.gnu.org/licenses/>. |
148 | + |
149 | +from django.test import TestCase |
150 | +from mock import patch |
151 | + |
152 | +from jenkinsserver.models.jenkins_server import JenkinsServer |
153 | +from frontend.models.loop_build import LoopBuild |
154 | +from frontend.kernel_build.models.kernel_loop import KernelLoop |
155 | +from frontend.hwpack_loop.models.hwpack_loop import HwpackLoop |
156 | + |
157 | + |
158 | +class KernelHwpackChainingTest(TestCase): |
159 | + |
160 | + def setUp(self): |
161 | + self.server = JenkinsServer.objects.get(id=1) |
162 | + |
163 | + @patch('jenkinsserver.models.jenkins_server.JenkinsServer.create_or_update_loop') |
164 | + @patch('jenkinsserver.models.jenkins_server.JenkinsServer.get_build_artifact_content') |
165 | + @patch('lib.jenkins_dashboard.JenkinsDashboard.get_build_info') |
166 | + def test_chaining(self, get_build_info_m, get_build_artifact_content_m, mck1): |
167 | + kernel_package = "linux-image.deb" |
168 | + get_build_info_m.return_value = {"result": LoopBuild.SUCCESS} |
169 | + get_build_artifact_content_m.return_value = kernel_package + "\n" |
170 | + |
171 | + hwpack_loop = HwpackLoop() |
172 | + hwpack_loop.name = "hwpack_test" |
173 | + hwpack_loop.server = self.server |
174 | + hwpack_loop.save() |
175 | + self.assertFalse(hwpack_loop.kernel_deb_url) |
176 | + |
177 | + loop = KernelLoop() |
178 | + loop.name = "kernel_test" |
179 | + loop.server = self.server |
180 | + loop.next_loop = hwpack_loop |
181 | + loop.save() |
182 | + build = loop.schedule_build() |
183 | + |
184 | + self.server.sync_unfinished_builds() |
185 | + |
186 | + # Reload object from DB in Django-speak |
187 | + build = LoopBuild.objects.get(id=build.id) |
188 | + |
189 | + # Make sure kernel build status/results were updated |
190 | + self.assertEquals(build.status, LoopBuild.SUCCESS) |
191 | + self.assertTrue(kernel_package in build.result_xml) |
192 | + |
193 | + # Make sure results from kernel loop were propagated to hwpack |
194 | + hwpack_loop = HwpackLoop.objects.get(id=hwpack_loop.id) |
195 | + self.assertTrue(kernel_package in hwpack_loop.kernel_deb_url) |
196 | + |
197 | + # Make sure that hwpack build was scheduled |
198 | + self.assertEquals(len(LoopBuild.objects.all()), 2) |
199 | + hwpack_build = LoopBuild.objects.get(loop=hwpack_loop) |
200 | + self.assertTrue(hwpack_build) |
201 | + self.assertEquals(hwpack_build.status, LoopBuild.SCHEDULED) |
202 | |
203 | === modified file 'dashboard/frontend/models/loop.py' |
204 | --- dashboard/frontend/models/loop.py 2012-09-20 13:51:27 +0000 |
205 | +++ dashboard/frontend/models/loop.py 2012-10-04 15:43:21 +0000 |
206 | @@ -259,8 +259,26 @@ |
207 | |
208 | def preconfigure(self, configuration=None): |
209 | """Makes a build parameter dict from build result configuration. |
210 | - |
211 | - This method should be overridden by subclasses. |
212 | + Default implementation just assigns loop's attributes from dictionary |
213 | + keys. This method may be overridden by subclasses. |
214 | + |
215 | + :return Extra Jenkins build scheduling parameters. TODO: breaks separation |
216 | + """ |
217 | + if configuration: |
218 | + for key, value in configuration.iteritems(): |
219 | + if hasattr(self, key): |
220 | + setattr(self, key, value) |
221 | + else: |
222 | + self.log.warning("Input build parameters for %s contain "\ |
223 | + "unknown key/value pair %s=%s", self, key, value) |
224 | + self.save() |
225 | + return {} |
226 | + |
227 | + def process_build_results(self, loop_build): |
228 | + """This method gets called with list of artifact files from a build. |
229 | + Its responsibility is to categorize artifacts, and prepare result data |
230 | + to be passed to downstream loop in chain. If needed, this method can |
231 | + access content of artifacts (not just list of them). |
232 | """ |
233 | return {} |
234 | |
235 | |
236 | === modified file 'dashboard/frontend/models/loop_build.py' |
237 | --- dashboard/frontend/models/loop_build.py 2012-09-19 10:50:22 +0000 |
238 | +++ dashboard/frontend/models/loop_build.py 2012-10-04 15:43:21 +0000 |
239 | @@ -84,3 +84,41 @@ |
240 | def build_finished(self): |
241 | """Method to be called after the build is executed.""" |
242 | self.loop.continue_down_the_chain(self.get_build_result()) |
243 | + |
244 | + def list_artifacts(self): |
245 | + """ |
246 | + List artifacts as produced by the build. |
247 | + |
248 | + :return List containing full paths of artifacts files |
249 | + """ |
250 | + return self.loop.server.list_build_artifacts(self.loop.name, |
251 | + self.remote_number) |
252 | + |
253 | + def get_artifact_url(self, artifact_name): |
254 | + """ |
255 | + Get a URL from which given artifact can be downloaded (for example, |
256 | + for passing to a build script). |
257 | + |
258 | + :param artifact_name Full artifact path, as returned by |
259 | + list_artifacts() |
260 | + :return A URL |
261 | + """ |
262 | + return self.loop.server.get_build_artifact_url(self.loop.name, |
263 | + self.remote_number, artifact_name) |
264 | + |
265 | + def get_artifact_content(self, artifact_name): |
266 | + """ |
267 | + Get contents of an artifact. This function loads entire artifact |
268 | + into memory, so should not be used with big artifacts. |
269 | + |
270 | + :param artifact_name Full artifact path, as returned by |
271 | + list_artifacts() |
272 | + :return Raw binary data of artifact |
273 | + """ |
274 | + return self.loop.server.get_build_artifact_url(self.loop.name, |
275 | + self.remote_number, artifact_name) |
276 | + |
277 | + def __repr__(self): |
278 | + return "%s(id=%s, loop.name=%s, remote_numer=%s)" % \ |
279 | + (self.__class__.__name__, self.id, self.loop.name, |
280 | + self.remote_number) |
281 | |
282 | === modified file 'dashboard/frontend/models/loop_reference.py' |
283 | --- dashboard/frontend/models/loop_reference.py 2012-08-31 11:53:01 +0000 |
284 | +++ dashboard/frontend/models/loop_reference.py 2012-10-04 15:43:21 +0000 |
285 | @@ -22,3 +22,4 @@ |
286 | KERNEL_LOOP = ('KernelLoop') |
287 | INTEGRATION_LOOP = ('IntegrationLoop') |
288 | ANDROID_TEXTFIELD_LOOP = ('AndroidTextFieldLoop') |
289 | +HWPACK_LOOP = ('HwpackLoop') |
290 | |
291 | === modified file 'dashboard/jenkinsserver/models/jenkins_server.py' |
292 | --- dashboard/jenkinsserver/models/jenkins_server.py 2012-09-18 16:01:01 +0000 |
293 | +++ dashboard/jenkinsserver/models/jenkins_server.py 2012-10-04 15:43:21 +0000 |
294 | @@ -16,11 +16,14 @@ |
295 | # You should have received a copy of the GNU Affero General Public License |
296 | # along with linaro-ci-dashboard. If not, see <http://www.gnu.org/licenses/>. |
297 | |
298 | +import urllib2 |
299 | + |
300 | from django.db import models |
301 | from django.conf import settings |
302 | from lib.jenkins_dashboard import JenkinsDashboard |
303 | from lib.logger import Logger |
304 | from lib.template import TextTemplate, XMLTemplate |
305 | +from dashboard.lib.xml_to_dict import DictToXml |
306 | |
307 | |
308 | class JenkinsServer(models.Model): |
309 | @@ -71,9 +74,9 @@ |
310 | # update the build info in CI. |
311 | if build.remote_number and \ |
312 | jenkins_build["number"] == build.remote_number: |
313 | - self.get_build_info(build) |
314 | + self.update_build_info(build) |
315 | |
316 | - def get_build_info(self, build): |
317 | + def update_build_info(self, build): |
318 | jenkins_build = self.jenkins.get_build_info(build.loop.name, |
319 | build.remote_number) |
320 | # Duration in jenkins is presented in milliseconds |
321 | @@ -82,18 +85,22 @@ |
322 | |
323 | if jenkins_build.get("result", None): |
324 | build.status = jenkins_build.get("result", None).lower() |
325 | - # TODO: Add build result xml, but from where? |
326 | + # Let loop process the build outcome and extract anything relevant |
327 | + # to store as build results. |
328 | + loop = build.loop.get_child_object() |
329 | + values = loop.process_build_results(build) |
330 | + build.result_xml = DictToXml(values).dict_to_tree() |
331 | build.save() |
332 | |
333 | def sync_unfinished_builds(self): |
334 | from frontend.models.loop_build import LoopBuild |
335 | builds = LoopBuild.objects.filter( |
336 | - status__in=LoopBuild.NON_FINISHED_STATUSES) |
337 | + status__in=LoopBuild.NON_FINISHED_STATUSES).order_by('id') |
338 | for build in builds: |
339 | # We don't wont the sync proces to fail if there is no particular |
340 | # loop present in jenkins. Log the error and continue. |
341 | try: |
342 | - self.get_build_info(build) |
343 | + self.update_build_info(build) |
344 | except: |
345 | self.log.exception("build[%s] does not exist for job %s" % (build.remote_number, build.loop.name)) |
346 | pass |
347 | @@ -148,3 +155,27 @@ |
348 | self.jenkins.build_job(jobname, parameters) |
349 | job_info = self.jenkins.get_job_info(jobname) |
350 | return job_info["nextBuildNumber"] |
351 | + |
352 | + def get_build_artifact_url(self, jobname, build_no, artifact_name): |
353 | + url = settings.ARTIFACT_TEMPLATE_URL % locals() |
354 | + return url |
355 | + |
356 | + def get_build_artifact_content(self, jobname, build_no, artifact_name): |
357 | + "Get contents of a build artifact." |
358 | + url = self.get_build_artifact_url(jobname, build_no, artifact_name) |
359 | + response = urllib2.urlopen(url) |
360 | + contents = response.read() |
361 | + response.close() |
362 | + return contents |
363 | + |
364 | + def list_build_artifacts(self, jobname, build_no): |
365 | + try: |
366 | + manifest = self.get_build_artifact_content(jobname, build_no, "MANIFEST") |
367 | + except urllib2.HTTPError, e: |
368 | + if e.code == 404: |
369 | + self.log.warn("MANIFEST artifact not found for %s/%s, assumed empty", jobname, build_no) |
370 | + manifest = "" |
371 | + else: |
372 | + raise |
373 | + files = [f for f in manifest.split("\n") if f] |
374 | + return files |
375 | |
376 | === modified file 'dashboard/jenkinsserver/tests/test_jenkins_server.py' |
377 | --- dashboard/jenkinsserver/tests/test_jenkins_server.py 2012-09-18 16:01:01 +0000 |
378 | +++ dashboard/jenkinsserver/tests/test_jenkins_server.py 2012-10-04 15:43:21 +0000 |
379 | @@ -15,9 +15,12 @@ |
380 | # |
381 | # You should have received a copy of the GNU Affero General Public License |
382 | # along with linaro-ci-dashboard. If not, see <http://www.gnu.org/licenses/>. |
383 | +import time |
384 | + |
385 | from django.test import TestCase |
386 | from jenkinsserver.models.jenkins_server import JenkinsServer |
387 | import jenkins |
388 | +from frontend.models.loop_build import LoopBuild |
389 | from frontend.integration_loop.models.integration_loop import IntegrationLoop |
390 | |
391 | |
392 | @@ -118,3 +121,35 @@ |
393 | self.assertIsNotNone(build.duration) |
394 | self.assertIsNot(build.duration, 0.0) |
395 | self.server.delete_job(self.test_job_name) |
396 | + |
397 | + def test_build_artifacts(self): |
398 | + integration_loop = IntegrationLoop() |
399 | + integration_loop.name = self.test_job_name |
400 | + integration_loop.server = self.server |
401 | + integration_loop.branch = 'none:none || mkdir -p ' |
402 | + integration_loop.precommand = 'cd ..; echo "foobar" >file.result' |
403 | + integration_loop.command = 'ls -1 *.result >MANIFEST' |
404 | + integration_loop.save() |
405 | + build = integration_loop.schedule_build() |
406 | + for i in range(10): |
407 | + self.server.sync_unfinished_builds() |
408 | + # Reload object from DB in Django-speak |
409 | + build = LoopBuild.objects.get(id=build.id) |
410 | + if build.status not in (LoopBuild.SCHEDULED, LoopBuild.RUNNING): |
411 | + break |
412 | + time.sleep(2) |
413 | + self.assertEquals(build.status, LoopBuild.SUCCESS) |
414 | + # We could test LoopBuild methods here, that would give us even more |
415 | + # coverage, but - we're testing JenkinsServer here, so let's test it. |
416 | + artifacts = self.server.list_build_artifacts(integration_loop.name, |
417 | + build.remote_number) |
418 | + self.assertEquals(artifacts, ["file.result"]) |
419 | + |
420 | + url = self.server.get_build_artifact_url(integration_loop.name, |
421 | + build.remote_number, artifacts[0]) |
422 | + self.assertTrue(url.startswith("http://")) |
423 | + self.assertTrue(artifacts[0] in url) |
424 | + |
425 | + data = self.server.get_build_artifact_content(integration_loop.name, |
426 | + build.remote_number, artifacts[0]) |
427 | + self.assertEquals(data, "foobar\n") |
428 | |
429 | === modified file 'dashboard/lib/jenkins_dashboard.py' |
430 | --- dashboard/lib/jenkins_dashboard.py 2012-09-18 15:06:02 +0000 |
431 | +++ dashboard/lib/jenkins_dashboard.py 2012-10-04 15:43:21 +0000 |
432 | @@ -30,8 +30,9 @@ |
433 | def get_build_info(self, name, build_number): |
434 | |
435 | try: |
436 | + url = self.server + self.BUILD_INFO % locals() |
437 | response = self.jenkins_open( |
438 | - urllib2.Request(self.server + self.BUILD_INFO % locals())) |
439 | + urllib2.Request(url)) |
440 | if response: |
441 | return json.loads(response) |
442 | else: |
443 | |
444 | === modified file 'dashboard/settings.py' |
445 | --- dashboard/settings.py 2012-09-25 08:07:10 +0000 |
446 | +++ dashboard/settings.py 2012-10-04 15:43:21 +0000 |
447 | @@ -227,6 +227,10 @@ |
448 | JENKINS_LOG_NAME = 'jenkins.log' |
449 | JENKINS_DB_ID = 1 |
450 | |
451 | +# URL from which build artifacts can be downloaded |
452 | +ARTIFACT_TEMPLATE_URL = "http://localhost:" + JENKINS_PORT + \ |
453 | + "/jenkins/job/%(jobname)s/%(build_no)s/artifact/%(artifact_name)s" |
454 | + |
455 | # LAVA specific values |
456 | LAVA_URL = 'validation.linaro.org/lava-server/RPC2/' |
457 | LAVA_ACCEPTING_ADDRESS = '0.0.0.0' |
Nice work Paul, thanks!!
I have no minor things to note, architecture looks pretty good.
One thing I'd like to see though are tests. There aren't any for one single method we implemented here.