Merge lp:~doanac/qa-dashboard/live-status-base into lp:qa-dashboard
- live-status-base
- Merge into dev
Status: | Merged |
---|---|
Approved by: | Chris Johnston |
Approved revision: | 690 |
Merged at revision: | 683 |
Proposed branch: | lp:~doanac/qa-dashboard/live-status-base |
Merge into: | lp:qa-dashboard |
Diff against target: |
615 lines (+386/-11) 10 files modified
common/templatetags/dashboard_extras.py (+13/-0) common/utils.py (+4/-2) qa_dashboard/settings.py (+2/-2) smokeng/admin.py (+2/-1) smokeng/management/commands/jenkins_pull_smokeng.py (+6/-1) smokeng/migrations/0008_auto__add_field_smokeresult_status.py (+124/-0) smokeng/models.py (+76/-1) smokeng/tables.py (+10/-3) smokeng/tests.py (+144/-1) smokeng/views.py (+5/-0) |
To merge this branch: | bzr merge lp:~doanac/qa-dashboard/live-status-base |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Chris Johnston | Approve | ||
PS Jenkins bot | continuous-integration | Approve | |
Joe Talbott | Approve | ||
Review via email: mp+196167@code.launchpad.net |
Commit message
This provides the base model and view updates required for live-status of smoke jobs. It adds a new "status" field to results so we can see if they are:
queued: waiting for jenkins to run the test
running: jenkins is running the test
syncing: the test is complete with results, but hasn't made it to the public jenkins (and pulled by our scripts)
complete: its gone through the pull-script and we have all its job artifacts
Description of the change
This provides the base model and view updates required for live-status of smoke jobs. It adds a new "status" field to results so we can see if they are:
queued: waiting for jenkins to run the test
running: jenkins is running the test
syncing: the test is complete with results, but hasn't made it to the public jenkins (and pulled by our scripts)
complete: its gone through the pull-script and we have all its job artifacts
Andy Doan (doanac) wrote : | # |
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:689
http://
Executed test runs:
Click here to trigger a rebuild:
http://
Chris Johnston (cjohnston) wrote : | # |
I don't really have any issues.. I wouldn't mind http://
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:690
http://
Executed test runs:
Click here to trigger a rebuild:
http://
Chris Johnston (cjohnston) : | # |
Preview Diff
1 | === modified file 'common/templatetags/dashboard_extras.py' |
2 | --- common/templatetags/dashboard_extras.py 2013-10-10 16:11:46 +0000 |
3 | +++ common/templatetags/dashboard_extras.py 2013-11-25 18:08:44 +0000 |
4 | @@ -22,6 +22,7 @@ |
5 | from common.plugin_helper import ExtensionManager |
6 | |
7 | from smokeng.config import get_internal_jenkins |
8 | +from smokeng.models import SmokeResult |
9 | |
10 | register = template.Library() |
11 | |
12 | @@ -216,3 +217,15 @@ |
13 | jenkins = get_internal_jenkins(variant='') |
14 | jenkins_url = (jenkins + m.group(2) + job + build) |
15 | return jenkins_url |
16 | + |
17 | + |
18 | +@register.filter |
19 | +def smokeresult_ran_at(smokeresult): |
20 | + status = smokeresult['status'] |
21 | + if status == SmokeResult.COMPLETE: |
22 | + return smokeresult['ran_at'] |
23 | + else: |
24 | + for s in SmokeResult.STATUS_CHOICES: |
25 | + if s[0] == status: |
26 | + return s[1] |
27 | + raise RuntimeError('Unexpected smokeresult status: %d' % status) |
28 | |
29 | === modified file 'common/utils.py' |
30 | --- common/utils.py 2013-11-05 22:44:55 +0000 |
31 | +++ common/utils.py 2013-11-25 18:08:44 +0000 |
32 | @@ -221,7 +221,7 @@ |
33 | image.save() |
34 | |
35 | |
36 | -def split_build_number(image): |
37 | +def split_build_number(image, save=True): |
38 | if ':' in image.build_number: |
39 | bn_data = image.build_number.split(':') |
40 | bn_parts = len(bn_data) |
41 | @@ -241,7 +241,9 @@ |
42 | else: |
43 | image.rootfs_id = image.build_number |
44 | |
45 | - image.save() |
46 | + if save: |
47 | + image.save() |
48 | + |
49 | |
50 | def txstatsd_interval(metric, append_days=False): |
51 | def dec(func): |
52 | |
53 | === modified file 'qa_dashboard/settings.py' |
54 | --- qa_dashboard/settings.py 2013-11-05 22:44:55 +0000 |
55 | +++ qa_dashboard/settings.py 2013-11-25 18:08:44 +0000 |
56 | @@ -190,8 +190,6 @@ |
57 | 'south', |
58 | ) |
59 | |
60 | -INSTALLED_APPS = PLUGIN_APPS + LOCAL_APPS + INSTALLED_APPS |
61 | - |
62 | TEST_RUNNER = "qa_dashboard.local_tests.LocalAppsTestSuiteRunner" |
63 | |
64 | AUTHENTICATION_BACKENDS = ( |
65 | @@ -319,3 +317,5 @@ |
66 | from debug_settings import * |
67 | except ImportError: |
68 | pass |
69 | + |
70 | +INSTALLED_APPS = PLUGIN_APPS + LOCAL_APPS + INSTALLED_APPS |
71 | |
72 | === modified file 'smokeng/admin.py' |
73 | --- smokeng/admin.py 2013-08-16 15:20:39 +0000 |
74 | +++ smokeng/admin.py 2013-11-25 18:08:44 +0000 |
75 | @@ -36,6 +36,7 @@ |
76 | list_display = ( |
77 | 'image', |
78 | 'name', |
79 | + 'status', |
80 | 'fail_count', |
81 | 'error_count', |
82 | 'pass_count', |
83 | @@ -43,7 +44,7 @@ |
84 | 'ran_at', |
85 | 'publish', |
86 | ) |
87 | - list_filter = ['publish', 'name'] |
88 | + list_filter = ['publish', 'status', 'name'] |
89 | search_fields = ['name'] |
90 | date_hierarchy = 'ran_at' |
91 | ordering = ['image'] |
92 | |
93 | === modified file 'smokeng/management/commands/jenkins_pull_smokeng.py' |
94 | --- smokeng/management/commands/jenkins_pull_smokeng.py 2013-10-22 16:36:32 +0000 |
95 | +++ smokeng/management/commands/jenkins_pull_smokeng.py 2013-11-25 18:08:44 +0000 |
96 | @@ -13,6 +13,7 @@ |
97 | # You should have received a copy of the GNU Affero General Public License |
98 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
99 | |
100 | +import datetime |
101 | import urllib2 |
102 | import re |
103 | import logging |
104 | @@ -215,23 +216,27 @@ |
105 | result, new_result = SmokeResult.objects.get_or_create( |
106 | image=self.image, |
107 | jenkins_build=self.install_data['jenkins_build'], |
108 | - ran_at=dashboard_data['ran_at'], |
109 | name=self.install_data['test_name'], |
110 | defaults=dict( |
111 | + ran_at=dashboard_data['ran_at'], |
112 | pass_count=dashboard_data['passes'], |
113 | fail_count=dashboard_data['failures'], |
114 | error_count=dashboard_data['errors'], |
115 | total_count=dashboard_data['total'], |
116 | pass_rate=pass_rate, |
117 | + status=SmokeResult.COMPLETE, |
118 | ) |
119 | ) |
120 | |
121 | if not new_result: |
122 | + result.ran_at = datetime.datetime.strptime( |
123 | + dashboard_data['ran_at'], '%Y-%m-%d %H:%M:%S') |
124 | result.pass_count = dashboard_data['passes'] |
125 | result.fail_count = dashboard_data['failures'] |
126 | result.error_count = dashboard_data['errors'] |
127 | result.total_count = dashboard_data['total'] |
128 | result.pass_rate = pass_rate |
129 | + result.status = SmokeResult.COMPLETE |
130 | result.save() |
131 | |
132 | all_results = SmokeResult.objects.filter( |
133 | |
134 | === added file 'smokeng/migrations/0008_auto__add_field_smokeresult_status.py' |
135 | --- smokeng/migrations/0008_auto__add_field_smokeresult_status.py 1970-01-01 00:00:00 +0000 |
136 | +++ smokeng/migrations/0008_auto__add_field_smokeresult_status.py 2013-11-25 18:08:44 +0000 |
137 | @@ -0,0 +1,124 @@ |
138 | +# -*- coding: utf-8 -*- |
139 | +import datetime |
140 | +from south.db import db |
141 | +from south.v2 import SchemaMigration |
142 | +from django.db import models |
143 | + |
144 | + |
145 | +class Migration(SchemaMigration): |
146 | + |
147 | + def forwards(self, orm): |
148 | + # Adding field 'SmokeResult.status' |
149 | + db.add_column('smoke_results', 'status', |
150 | + self.gf('django.db.models.fields.IntegerField')(default=3), |
151 | + keep_default=False) |
152 | + |
153 | + |
154 | + def backwards(self, orm): |
155 | + # Deleting field 'SmokeResult.status' |
156 | + db.delete_column('smoke_results', 'status') |
157 | + |
158 | + |
159 | + models = { |
160 | + u'common.bug': { |
161 | + 'Meta': {'object_name': 'Bug', 'db_table': "'bugs'"}, |
162 | + 'assignee': ('django.db.models.fields.CharField', [], {'max_length': '4096', 'null': 'True'}), |
163 | + 'bug_no': ('django.db.models.fields.CharField', [], {'max_length': '4096'}), |
164 | + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), |
165 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
166 | + 'importance': ('django.db.models.fields.CharField', [], {'default': "u'unknown'", 'max_length': '4096'}), |
167 | + 'internal': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), |
168 | + 'project': ('django.db.models.fields.CharField', [], {'max_length': '4096', 'null': 'True'}), |
169 | + 'publish': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), |
170 | + 'status': ('django.db.models.fields.CharField', [], {'default': "u'unknown'", 'max_length': '4096'}), |
171 | + 'title': ('django.db.models.fields.CharField', [], {'max_length': '4096', 'null': 'True'}), |
172 | + 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) |
173 | + }, |
174 | + u'common.jenkinsbuild': { |
175 | + 'Meta': {'unique_together': "(('job', 'build_number'),)", 'object_name': 'JenkinsBuild', 'db_table': "'jenkins_builds'"}, |
176 | + 'bugs': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'build_bugs'", 'symmetrical': 'False', 'to': u"orm['common.Bug']"}), |
177 | + 'build_description': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '4096'}), |
178 | + 'build_number': ('django.db.models.fields.CharField', [], {'max_length': '4096'}), |
179 | + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), |
180 | + 'failed': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), |
181 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
182 | + 'internal': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), |
183 | + 'job': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['common.JenkinsJob']"}), |
184 | + 'publish': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), |
185 | + 'ran_at': ('django.db.models.fields.DateTimeField', [], {}), |
186 | + 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) |
187 | + }, |
188 | + u'common.jenkinsjob': { |
189 | + 'Meta': {'object_name': 'JenkinsJob', 'db_table': "'jenkins_jobs'"}, |
190 | + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), |
191 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
192 | + 'internal': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), |
193 | + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '4096'}), |
194 | + 'publish': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), |
195 | + 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), |
196 | + 'url': ('django.db.models.fields.URLField', [], {'max_length': '4096'}) |
197 | + }, |
198 | + u'smokeng.smokeimage': { |
199 | + 'Meta': {'object_name': 'SmokeImage', 'db_table': "'smoke_images'"}, |
200 | + 'arch': ('django.db.models.fields.CharField', [], {'max_length': '4096'}), |
201 | + 'build_id': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '4096'}), |
202 | + 'build_number': ('django.db.models.fields.CharField', [], {'max_length': '4096'}), |
203 | + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), |
204 | + 'customization_id': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '4096'}), |
205 | + 'device_specific_id': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '4096'}), |
206 | + 'flavor': ('django.db.models.fields.CharField', [], {'max_length': '4096'}), |
207 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
208 | + 'internal': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), |
209 | + 'publish': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), |
210 | + 'release': ('django.db.models.fields.CharField', [], {'max_length': '4096'}), |
211 | + 'rootfs_id': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '4096'}), |
212 | + 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), |
213 | + 'variant': ('django.db.models.fields.CharField', [], {'max_length': '4096'}) |
214 | + }, |
215 | + u'smokeng.smokeresult': { |
216 | + 'Meta': {'object_name': 'SmokeResult', 'db_table': "'smoke_results'"}, |
217 | + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), |
218 | + 'error_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), |
219 | + 'fail_count': ('django.db.models.fields.IntegerField', [], {}), |
220 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
221 | + 'image': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['smokeng.SmokeImage']"}), |
222 | + 'internal': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), |
223 | + 'jenkins_build': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['common.JenkinsBuild']"}), |
224 | + 'name': ('django.db.models.fields.CharField', [], {'max_length': '4096'}), |
225 | + 'pass_count': ('django.db.models.fields.IntegerField', [], {}), |
226 | + 'pass_rate': ('django.db.models.fields.FloatField', [], {'default': '0.0'}), |
227 | + 'publish': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), |
228 | + 'ran_at': ('django.db.models.fields.DateTimeField', [], {}), |
229 | + 'status': ('django.db.models.fields.IntegerField', [], {'default': '3'}), |
230 | + 'total_count': ('django.db.models.fields.IntegerField', [], {}), |
231 | + 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) |
232 | + }, |
233 | + u'smokeng.smokeresultbug': { |
234 | + 'Meta': {'object_name': 'SmokeResultBug', 'db_table': "'smoke_result_bugs'"}, |
235 | + 'bug': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['common.Bug']"}), |
236 | + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), |
237 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
238 | + 'internal': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), |
239 | + 'publish': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), |
240 | + 'result': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['smokeng.SmokeResult']"}), |
241 | + 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) |
242 | + }, |
243 | + u'smokeng.smoketestresult': { |
244 | + 'Meta': {'object_name': 'SmokeTestResult', 'db_table': "'smoke_tests'"}, |
245 | + 'command': ('django.db.models.fields.CharField', [], {'max_length': '4096'}), |
246 | + 'command_type': ('django.db.models.fields.CharField', [], {'max_length': '4096'}), |
247 | + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), |
248 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
249 | + 'internal': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), |
250 | + 'publish': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), |
251 | + 'result': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['smokeng.SmokeResult']"}), |
252 | + 'returncode': ('django.db.models.fields.IntegerField', [], {}), |
253 | + 'stderr': ('django.db.models.fields.TextField', [], {'null': 'True'}), |
254 | + 'stdout': ('django.db.models.fields.TextField', [], {'null': 'True'}), |
255 | + 'testcase': ('django.db.models.fields.CharField', [], {'max_length': '4096'}), |
256 | + 'testsuite': ('django.db.models.fields.CharField', [], {'max_length': '4096'}), |
257 | + 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) |
258 | + } |
259 | + } |
260 | + |
261 | + complete_apps = ['smokeng'] |
262 | \ No newline at end of file |
263 | |
264 | === modified file 'smokeng/models.py' |
265 | --- smokeng/models.py 2013-10-22 17:54:35 +0000 |
266 | +++ smokeng/models.py 2013-11-25 18:08:44 +0000 |
267 | @@ -13,6 +13,8 @@ |
268 | # You should have received a copy of the GNU Affero General Public License |
269 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
270 | |
271 | +import datetime |
272 | + |
273 | from django.db import models |
274 | |
275 | from common.models import ( |
276 | @@ -21,6 +23,8 @@ |
277 | JenkinsBuild, |
278 | ) |
279 | |
280 | +from common.utils import split_build_number |
281 | + |
282 | |
283 | class SmokeImage(DashboardBaseModel): |
284 | class Meta: |
285 | @@ -41,6 +45,12 @@ |
286 | arch = models.CharField(max_length=4096) |
287 | variant = models.CharField(max_length=4096) |
288 | |
289 | + def save(self, **kwargs): |
290 | + '''ensure we have build_number parts''' |
291 | + if not self.build_id: |
292 | + split_build_number(self, False) |
293 | + return super(SmokeImage, self).save(**kwargs) |
294 | + |
295 | def __unicode__(self): |
296 | return "{} {} {} {}".format( |
297 | self.release, |
298 | @@ -74,6 +84,7 @@ |
299 | 'total_count', |
300 | 'name', |
301 | 'ran_at', |
302 | + 'status', |
303 | 'jenkins_build__job__url', |
304 | 'jenkins_build__build_number', |
305 | ) |
306 | @@ -124,7 +135,8 @@ |
307 | if r['total_count']: |
308 | r['pass_rate'] = float(r['pass_count']) / r['total_count'] |
309 | r['pass_rate_pct'] = 100 * r['pass_rate'] |
310 | - passrates.append(r['pass_rate']) |
311 | + if r['status'] >= SmokeResult.SYNCING: |
312 | + passrates.append(r['pass_rate']) |
313 | |
314 | for b in bugs: |
315 | if b['result__id'] == r['id']: |
316 | @@ -149,6 +161,19 @@ |
317 | class SmokeResult(DashboardBaseModel): |
318 | class Meta: |
319 | db_table = 'smoke_results' |
320 | + |
321 | + QUEUED = 0 |
322 | + RUNNING = 1 |
323 | + SYNCING = 2 |
324 | + COMPLETE = 3 |
325 | + |
326 | + STATUS_CHOICES = ( |
327 | + (QUEUED, 'Queued'), |
328 | + (RUNNING, 'Running'), |
329 | + (SYNCING, 'Syncing'), |
330 | + (COMPLETE, 'Complete'), |
331 | + ) |
332 | + |
333 | image = models.ForeignKey(SmokeImage) |
334 | jenkins_build = models.ForeignKey(JenkinsBuild) |
335 | name = models.CharField(max_length=4096) |
336 | @@ -157,8 +182,58 @@ |
337 | pass_count = models.IntegerField() |
338 | total_count = models.IntegerField() |
339 | ran_at = models.DateTimeField('date run') |
340 | + status = models.IntegerField(choices=STATUS_CHOICES, default=COMPLETE) |
341 | pass_rate = models.FloatField(default=0.0) |
342 | |
343 | + @staticmethod |
344 | + def create_queued_results(image, build, tests): |
345 | + ran_at = datetime.datetime.now() |
346 | + for test in tests: |
347 | + result, created = SmokeResult.objects.get_or_create( |
348 | + image=image, |
349 | + name=test, |
350 | + defaults={ |
351 | + 'fail_count': 0, |
352 | + 'error_count': 0, |
353 | + 'pass_count': 0, |
354 | + 'total_count': 0, |
355 | + 'ran_at': ran_at, |
356 | + 'jenkins_build': build, |
357 | + 'status': SmokeResult.QUEUED, |
358 | + }, |
359 | + ) |
360 | + if not created: |
361 | + result.status = SmokeResult.QUEUED |
362 | + result.ran_at = ran_at |
363 | + result.jenkins_build = build |
364 | + result.save() |
365 | + |
366 | + @staticmethod |
367 | + def set_running(image, build, tests): |
368 | + for test in tests: |
369 | + result = SmokeResult.objects.get( |
370 | + image=image, |
371 | + jenkins_build=build, |
372 | + name=test, |
373 | + ) |
374 | + result.ran_at = datetime.datetime.now() |
375 | + result.status = SmokeResult.RUNNING |
376 | + result.save() |
377 | + |
378 | + @staticmethod |
379 | + def set_syncing(image, build, test, passes, fails, errors): |
380 | + result = SmokeResult.objects.get( |
381 | + image=image, |
382 | + jenkins_build=build, |
383 | + name=test, |
384 | + ) |
385 | + result.status = SmokeResult.SYNCING |
386 | + result.pass_count = passes |
387 | + result.fail_count = fails |
388 | + result.error_count = errors |
389 | + result.total_count = passes + fails + errors |
390 | + result.save() |
391 | + |
392 | def __unicode__(self): |
393 | return "{} - {}".format( |
394 | self.name, |
395 | |
396 | === modified file 'smokeng/tables.py' |
397 | --- smokeng/tables.py 2013-09-27 16:55:15 +0000 |
398 | +++ smokeng/tables.py 2013-11-25 18:08:44 +0000 |
399 | @@ -63,8 +63,12 @@ |
400 | attrs={'td': {'class': 'num'}}, |
401 | ) |
402 | pass_rate = tables.TemplateColumn( |
403 | - '{% load dashboard_extras %}' |
404 | - '{{ record.pass_rate|decimal_to_percent }}', |
405 | + '''{% load dashboard_extras %} |
406 | + {% if record.inprogress %} |
407 | + Running |
408 | + {% else %} |
409 | + {{ record.pass_rate|decimal_to_percent }} |
410 | + {% endif %}''', |
411 | attrs={'td': {'class': 'num pass_rate_color'}}, |
412 | ) |
413 | td_class_extra = tables.TemplateColumn( |
414 | @@ -127,7 +131,10 @@ |
415 | "name": A('name'), |
416 | }, |
417 | ) |
418 | - ran_at = tables.Column() |
419 | + ran_at = tables.TemplateColumn( |
420 | + '{% load dashboard_extras %}' |
421 | + '{{ record|smokeresult_ran_at }}' |
422 | + ) |
423 | |
424 | |
425 | class SmokeKPITable(tables.Table): |
426 | |
427 | === modified file 'smokeng/tests.py' |
428 | --- smokeng/tests.py 2013-11-18 23:04:16 +0000 |
429 | +++ smokeng/tests.py 2013-11-25 18:08:44 +0000 |
430 | @@ -467,7 +467,7 @@ |
431 | '<li><a href="http://q-jenkins.ubuntu-ci:8080/job/saucy-touch_mir-mako-' + |
432 | 'smoke-notes-app-autopilot/1/artifact/clientlogs/utah.yaml/' + |
433 | '*view*/">utah.yaml</a></li>' |
434 | - ) |
435 | + ) |
436 | _setUp(self) |
437 | |
438 | self.artifact = Artifact.objects.create( |
439 | @@ -625,3 +625,146 @@ |
440 | page = self._test_result_detail() |
441 | self.assertContains(page, self.private_jenkins_build) |
442 | self.assertContains(page, self.private_jenkins_artifact) |
443 | + |
444 | + |
445 | +class TestSmokeStatus(TestCase): |
446 | + """ Tests for live status operations """ |
447 | + |
448 | + def setUp(self): |
449 | + _setUp(self) |
450 | + |
451 | + # make a basic assumption on what's already in the DB for |
452 | + # tests below |
453 | + self.assertEqual(1, SmokeResult.objects.all().count()) |
454 | + |
455 | + self.client = Client() |
456 | + |
457 | + def _get_results_url(self): |
458 | + return '/smokeng/%s/%s/%s/%s/%d/' % ( |
459 | + self.image.release, |
460 | + self.image.variant, |
461 | + self.image.arch, |
462 | + self.image.build_number, |
463 | + self.image.id, |
464 | + ) |
465 | + |
466 | + def testQueued(self): |
467 | + tests = ['t1', 't2', 't3'] |
468 | + SmokeResult.create_queued_results( |
469 | + self.image, self.jenkins_build, tests) |
470 | + |
471 | + qs = SmokeResult.objects.filter(status=SmokeResult.QUEUED) |
472 | + self.assertEqual(len(tests), qs.count()) |
473 | + |
474 | + # ensure pass-rates aren't affected |
475 | + summary = self.image.summary |
476 | + self.assertEqual(3, summary[0]['total_count']) |
477 | + self.assertEqual(1.0 / 3.0, summary[0]['pass_rate']) |
478 | + |
479 | + resp = self.client.get('/smokeng/') |
480 | + pass_rate = resp.context[-1]['table'].rows[0]['pass_rate'] |
481 | + self.assertEqual(u'Running', pass_rate.strip()) |
482 | + |
483 | + resp = self.client.get(self._get_results_url()) |
484 | + rows = resp.context[-1]['table'].rows |
485 | + for i in range(len(rows)): |
486 | + if i < len(rows) - 1: |
487 | + self.assertEqual('Queued', rows[i]['ran_at']) |
488 | + else: |
489 | + self.assertNotEqual('Queued', rows[i]['ran_at']) |
490 | + |
491 | + def testRunning(self): |
492 | + tests = ['t1', 't2', 't3'] |
493 | + SmokeResult.create_queued_results( |
494 | + self.image, self.jenkins_build, tests) |
495 | + |
496 | + qs = SmokeResult.objects.filter(status=SmokeResult.QUEUED) |
497 | + self.assertEqual(len(tests), qs.count()) |
498 | + ran_at = qs[0].ran_at |
499 | + |
500 | + # put the first few into running and make sure the counts add up |
501 | + SmokeResult.set_running(self.image, self.jenkins_build, tests[:-1]) |
502 | + self.assertEqual(len(tests) + 1, SmokeResult.objects.all().count()) |
503 | + qs = SmokeResult.objects.filter(status=SmokeResult.QUEUED) |
504 | + self.assertEqual(1, qs.count()) |
505 | + self.assertEqual(ran_at, qs[0].ran_at) |
506 | + qs = SmokeResult.objects.filter(status=SmokeResult.RUNNING) |
507 | + self.assertEqual(len(tests) - 1, qs.count()) |
508 | + |
509 | + # put the last result into running and make sure the counts add up |
510 | + SmokeResult.set_running(self.image, self.jenkins_build, tests[-1:]) |
511 | + self.assertEqual(len(tests) + 1, SmokeResult.objects.all().count()) |
512 | + qs = SmokeResult.objects.filter(status=SmokeResult.QUEUED) |
513 | + self.assertEqual(0, qs.count()) |
514 | + qs = SmokeResult.objects.filter(status=SmokeResult.RUNNING) |
515 | + self.assertEqual(len(tests), qs.count()) |
516 | + self.assertGreater(qs[0].ran_at, ran_at) |
517 | + |
518 | + # ensure pass-rates aren't affected |
519 | + self.assertEqual(3, self.image.summary[0]['total_count']) |
520 | + self.assertEqual(1.0 / 3.0, self.image.summary[0]['pass_rate']) |
521 | + |
522 | + def testSyncing(self): |
523 | + tests = ['t1', 't2', 't3'] |
524 | + SmokeResult.create_queued_results( |
525 | + self.image, self.jenkins_build, tests) |
526 | + |
527 | + qs = SmokeResult.objects.filter(status=SmokeResult.QUEUED) |
528 | + self.assertEqual(len(tests), qs.count()) |
529 | + |
530 | + # put tests into a few states to ensure it can handle them all |
531 | + SmokeResult.set_running(self.image, self.jenkins_build, tests[:-1]) |
532 | + |
533 | + # this will have wind up with 4 results: |
534 | + # the original from _setup with a pass-rate 33% |
535 | + # t1 = which will be 0, t2 and t3 which will be 33% |
536 | + i = 0 |
537 | + for t in tests: |
538 | + SmokeResult.set_syncing(self.image, self.jenkins_build, t, i, i, i) |
539 | + i += 1 |
540 | + qs = SmokeResult.objects.filter(status=SmokeResult.SYNCING) |
541 | + self.assertEqual(len(tests), qs.count()) |
542 | + self.assertEqual(12, self.image.summary[0]['total_count']) |
543 | + self.assertEqual(.25, self.image.summary[0]['pass_rate']) |
544 | + |
545 | + resp = self.client.get('/smokeng/') |
546 | + pass_rate = resp.context[-1]['table'].rows[0]['pass_rate'] |
547 | + self.assertEqual(u'25%', pass_rate.strip()) |
548 | + |
549 | + resp = self.client.get(self._get_results_url()) |
550 | + rows = resp.context[-1]['table'].rows |
551 | + for i in range(len(rows)): |
552 | + if i < len(rows) - 1: |
553 | + self.assertEqual('Syncing', rows[i]['ran_at']) |
554 | + else: |
555 | + self.assertNotEqual('Syncing', rows[i]['ran_at']) |
556 | + |
557 | + def testRepeat(self): |
558 | + '''Ensure we can move a result back to QUEUED properly.''' |
559 | + tests = ['t1', 't2', 't3'] |
560 | + SmokeResult.create_queued_results( |
561 | + self.image, self.jenkins_build, tests) |
562 | + |
563 | + # put tests into a few states to ensure it can handle them all |
564 | + # the syncing result bumps up the "total count" by 3 to 6 |
565 | + SmokeResult.set_running(self.image, self.jenkins_build, tests[:-1]) |
566 | + SmokeResult.set_syncing(self.image, self.jenkins_build, |
567 | + tests[-1], 1, 1, 1) |
568 | + |
569 | + SmokeResult.create_queued_results( |
570 | + self.image, self.jenkins_build, tests) |
571 | + |
572 | + # ensure we haven't created new rows: |
573 | + self.assertEqual(len(tests) + 1, SmokeResult.objects.all().count()) |
574 | + |
575 | + qs = SmokeResult.objects.filter(status=SmokeResult.QUEUED) |
576 | + self.assertEqual(len(tests), qs.count()) |
577 | + |
578 | + # ensure pass-rates go back properly |
579 | + summary = self.image.summary |
580 | + self.assertEqual(6, summary[0]['total_count']) |
581 | + self.assertEqual(1.0 / 3.0, summary[0]['pass_rate']) |
582 | + |
583 | + resp = self.client.get('/smokeng/') |
584 | + pass_rate = resp.context[-1]['table'].rows[0]['pass_rate'] |
585 | + self.assertEqual(u'Running', pass_rate.strip()) |
586 | |
587 | === modified file 'smokeng/views.py' |
588 | --- smokeng/views.py 2013-10-25 15:48:45 +0000 |
589 | +++ smokeng/views.py 2013-11-25 18:08:44 +0000 |
590 | @@ -179,6 +179,9 @@ |
591 | if 'ran_at' in result: |
592 | totals[key]['ran_at'] = result['ran_at'] |
593 | |
594 | + if result['status'] < SmokeResult.SYNCING: |
595 | + totals[key]['inprogress'] = True |
596 | + |
597 | totals[key]['total_count'] += result['total_count'] |
598 | totals[key]['pass_count'] += result['pass_count'] |
599 | totals[key]['error_count'] += result['error_count'] |
600 | @@ -222,6 +225,7 @@ |
601 | 'image__release', |
602 | 'name', |
603 | 'ran_at', |
604 | + 'status', |
605 | 'jenkins_build', |
606 | ).annotate( |
607 | pass_count=models.Sum('pass_count'), |
608 | @@ -240,6 +244,7 @@ |
609 | 'image__release', |
610 | 'name', |
611 | 'ran_at', |
612 | + 'status', |
613 | 'jenkins_build', |
614 | 'pass_count', |
615 | 'fail_count', |
it might be easier reading commit by commit instead of the full diff.