Merge lp:~mwhudson/lava-dashboard/compare-testrun-view into lp:lava-dashboard
- compare-testrun-view
- Merge into trunk
Proposed by
Michael Hudson-Doyle
Status: | Merged |
---|---|
Merged at revision: | 385 |
Proposed branch: | lp:~mwhudson/lava-dashboard/compare-testrun-view |
Merge into: | lp:lava-dashboard |
Diff against target: |
569 lines (+439/-3) 9 files modified
dashboard_app/filters.py (+16/-0) dashboard_app/static/css/filter-detail.css (+52/-0) dashboard_app/static/js/filter-detail.js (+135/-0) dashboard_app/templates/dashboard_app/filter_compare_matches.html (+28/-0) dashboard_app/templates/dashboard_app/filter_detail.html (+19/-0) dashboard_app/urls.py (+2/-1) dashboard_app/views/__init__.py (+1/-1) dashboard_app/views/filters/tables.py (+35/-0) dashboard_app/views/filters/views.py (+151/-1) |
To merge this branch: | bzr merge lp:~mwhudson/lava-dashboard/compare-testrun-view |
Related bugs: | |
Related blueprints: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Andy Doan (community) | Approve | ||
Review via email: mp+142421@code.launchpad.net |
Commit message
Description of the change
This branch adds a way to compare matches of a filter. You can play with it at http://
The js stuff to access the page is slightly hackish, but it seems to work for me...
To post a comment you must log in.
- 402. By Michael Hudson-Doyle
-
merge trunk
- 403. By Michael Hudson-Doyle
-
start to make compare builds selection a bit more ordinary
- 404. By Michael Hudson-Doyle
-
finish that
- 405. By Michael Hudson-Doyle
-
one more
- 406. By Michael Hudson-Doyle
-
fixes++
Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote : | # |
- 407. By Michael Hudson-Doyle
-
move js and css to separate files
- 408. By Michael Hudson-Doyle
-
css tweak
Revision history for this message
Andy Doan (doanac) wrote : | # |
I think this works. You've now passed your job interview question with this algorithm
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'dashboard_app/filters.py' |
2 | --- dashboard_app/filters.py 2012-12-18 00:47:08 +0000 |
3 | +++ dashboard_app/filters.py 2013-01-10 20:39:21 +0000 |
4 | @@ -300,6 +300,22 @@ |
5 | q = self.queryset.filter(bundle__uploaded_on__gt=since) |
6 | return self._wrap(q) |
7 | |
8 | + def with_tags(self, tag1, tag2): |
9 | + if self.key == 'build_number': |
10 | + q = self.queryset.extra( |
11 | + where=['convert_to_integer("dashboard_app_namedattribute"."value") in (%s, %s)' % (tag1, tag2)] |
12 | + ) |
13 | + else: |
14 | + tag1 = datetime.datetime.strptime(tag1, "%Y-%m-%d %H:%M:%S.%f") |
15 | + tag2 = datetime.datetime.strptime(tag2, "%Y-%m-%d %H:%M:%S.%f") |
16 | + q = self.queryset.filter(bundle__uploaded_on__in=(tag1, tag2)) |
17 | + matches = list(self._wrap(q)) |
18 | + if matches[0].tag == tag1: |
19 | + return matches |
20 | + else: |
21 | + matches.reverse() |
22 | + return matches |
23 | + |
24 | def count(self): |
25 | return self.queryset.count() |
26 | |
27 | |
28 | === added file 'dashboard_app/static/css/filter-detail.css' |
29 | --- dashboard_app/static/css/filter-detail.css 1970-01-01 00:00:00 +0000 |
30 | +++ dashboard_app/static/css/filter-detail.css 2013-01-10 20:39:21 +0000 |
31 | @@ -0,0 +1,52 @@ |
32 | +table.select-compare1 td { cursor: pointer; } |
33 | +table.select-compare1 tr.even td { |
34 | + background-color: #ccf; |
35 | +} |
36 | +table.select-compare1 tr.even.hover td { |
37 | + background-color: #77f; |
38 | +} |
39 | +table.select-compare1 tr.odd td { |
40 | + background-color: #aaf; |
41 | +} |
42 | +table.select-compare1 tr.odd.hover td { |
43 | + background-color: #77f; |
44 | +} |
45 | + |
46 | +table.select-compare2 td { cursor: pointer; } |
47 | +table.select-compare2 tr.even td { |
48 | + background-color: #fcc; |
49 | +} |
50 | +table.select-compare2 tr.odd td { |
51 | + background-color: #faa; |
52 | +} |
53 | +table.select-compare2 tr.selected-1 td { |
54 | + background-color: #77f; |
55 | +} |
56 | +table.select-compare2 tr.selected-1.hover td { |
57 | + background-color: #77f; |
58 | +} |
59 | +table.select-compare2 tr.hover td { |
60 | + background-color: #f77; |
61 | +} |
62 | +table.select-compare3 tr.selected-1 td { |
63 | + background-color: #77f; |
64 | +} |
65 | +table.select-compare3 tr.selected-1.hover td { |
66 | + background-color: #77f; |
67 | +} |
68 | +table.select-compare3 tr.selected-2 td { |
69 | + background-color: #f77; |
70 | +} |
71 | +table.select-compare3 tr.selected-2.hover td { |
72 | + background-color: #f77; |
73 | +} |
74 | +table.select-compare3 tr.selected-1 { |
75 | + cursor: pointer; |
76 | +} |
77 | +table.select-compare3 tr.selected-2 { |
78 | + cursor: pointer; |
79 | +} |
80 | +#filter-table input { |
81 | + margin-top: 0; |
82 | + margin-bottom: 0; |
83 | +} |
84 | \ No newline at end of file |
85 | |
86 | === added file 'dashboard_app/static/js/filter-detail.js' |
87 | --- dashboard_app/static/js/filter-detail.js 1970-01-01 00:00:00 +0000 |
88 | +++ dashboard_app/static/js/filter-detail.js 2013-01-10 20:39:21 +0000 |
89 | @@ -0,0 +1,135 @@ |
90 | +var compareState = 0; |
91 | +var compare1 = null, compare2 = null; |
92 | +function cancelCompare () { |
93 | + $("#filter-table").removeClass("select-compare1"); |
94 | + $("#filter-table").removeClass("select-compare2"); |
95 | + $("#filter-table").removeClass("select-compare3"); |
96 | + $("#filter-table tr").removeClass("selected-1"); |
97 | + $("#filter-table tr").removeClass("selected-2"); |
98 | + $("#filter-table tr").unbind("click"); |
99 | + $("#filter-table tr").unbind("hover"); |
100 | + $("#filter-table tr").each(removeCheckbox); |
101 | + $("#first-prompt").hide(); |
102 | + $("#second-prompt").hide(); |
103 | + $("#third-prompt").hide(); |
104 | + $("#compare-button").button({label:"Compare builds"}); |
105 | + compareState = 0; |
106 | +} |
107 | +function startCompare () { |
108 | + $("#compare-button").button({label:"Cancel"}); |
109 | + $("#filter-table").addClass("select-compare1"); |
110 | + $("#filter-table tr").click(rowClickHandler); |
111 | + $("#filter-table tr").each(insertCheckbox); |
112 | + $("#filter-table tr").hover(rowHoverHandlerIn, rowHoverHandlerOut); |
113 | + $("#first-prompt").show(); |
114 | + compareState = 1; |
115 | +} |
116 | +function tagFromRow(tr) { |
117 | + var firstCell = $(tr).find("td:eq(0)"); |
118 | + return { |
119 | + machinetag: firstCell.find("span").data("machinetag"), |
120 | + usertag: firstCell.text() |
121 | + }; |
122 | +} |
123 | +function rowClickHandler() { |
124 | + if (compareState == 1) { |
125 | + compare1 = tagFromRow($(this)); |
126 | + $(this).addClass("selected-1"); |
127 | + $(this).find("input").attr("checked", true); |
128 | + $("#p2-build").text(compare1.usertag); |
129 | + $("#first-prompt").hide(); |
130 | + $("#second-prompt").show(); |
131 | + $("#filter-table").removeClass("select-compare1"); |
132 | + $("#filter-table").addClass("select-compare2"); |
133 | + compareState = 2; |
134 | + } else if (compareState == 2) { |
135 | + var thistag = tagFromRow($(this)); |
136 | + if (compare1.machinetag == thistag.machinetag) { |
137 | + cancelCompare(); |
138 | + startCompare(); |
139 | + } else { |
140 | + compare2 = thistag; |
141 | + $(this).find("input").attr("checked", true); |
142 | + $(this).addClass("selected-2"); |
143 | + $("#second-prompt").hide(); |
144 | + $("#third-prompt").show(); |
145 | + $("#filter-table").removeClass("select-compare2"); |
146 | + $("#filter-table").addClass("select-compare3"); |
147 | + $("#filter-table input").attr("disabled", true); |
148 | + $("#filter-table .selected-1 input").attr("disabled", false); |
149 | + $("#filter-table .selected-2 input").attr("disabled", false); |
150 | + $("#p3-build-1").text(compare1.usertag); |
151 | + $("#p3-build-2").text(compare2.usertag); |
152 | + $("#third-prompt a").attr("href", window.location + '/+compare/' + compare1.machinetag + '/' + compare2.machinetag); |
153 | + compareState = 3; |
154 | + } |
155 | + } else if (compareState == 3) { |
156 | + var thistag = tagFromRow($(this)); |
157 | + if (thistag.machinetag == compare1.machinetag || thistag.machinetag == compare2.machinetag) { |
158 | + $("#second-prompt").show(); |
159 | + $("#third-prompt").hide(); |
160 | + $("#filter-table").addClass("select-compare2"); |
161 | + $("#filter-table").removeClass("select-compare3"); |
162 | + $("#filter-table input").attr("disabled", false); |
163 | + compareState = 2; |
164 | + $(this).find("input").attr("checked", false); |
165 | + if (thistag.machinetag == compare1.machinetag) { |
166 | + compare1 = compare2; |
167 | + $("#filter-table .selected-1").removeClass("selected-1"); |
168 | + $("#filter-table .selected-2").addClass("selected-1"); |
169 | + $("#p2-build").text(compare1.usertag); |
170 | + } |
171 | + $("#filter-table .selected-2").removeClass("selected-2"); |
172 | + } |
173 | + } |
174 | + tagFromRow(this); |
175 | +} |
176 | +function rowHoverHandlerIn() { |
177 | + $(this).addClass("hover"); |
178 | +} |
179 | +function rowHoverHandlerOut() { |
180 | + $(this).removeClass("hover"); |
181 | +} |
182 | +function insertCheckbox() { |
183 | + var row = $(this); |
184 | + var checkbox = $('<input type="checkbox">'); |
185 | + row.find("td:first").prepend(checkbox); |
186 | +} |
187 | +function removeCheckbox() { |
188 | + var row = $(this); |
189 | + row.find('input').remove(); |
190 | +} |
191 | +$(window).load( |
192 | + function () { |
193 | + $("#filter-table").dataTable().fnSettings().fnRowCallback = function(tr, data, index) { |
194 | + if (compareState) { |
195 | + insertCheckbox.call(tr); |
196 | + $(tr).click(rowClickHandler); |
197 | + $("#filter-table tr").hover(rowHoverHandlerIn, rowHoverHandlerOut); |
198 | + if (compareState >= 2 && tagFromRow(tr).machinetag == compare1.machinetag) { |
199 | + $(tr).addClass("selected-1"); |
200 | + $(tr).find("input").attr("checked", true); |
201 | + } |
202 | + if (compareState >= 3) { |
203 | + if (tagFromRow(tr).machinetag == compare2.machinetag) { |
204 | + $(tr).addClass("selected-2"); |
205 | + $(tr).find("input").attr("checked", true); |
206 | + } else if (tagFromRow(tr).machinetag != compare1.machinetag) { |
207 | + $(tr).find("input").attr("disabled", true); |
208 | + } |
209 | + } |
210 | + } |
211 | + return tr; |
212 | + }; |
213 | + $("#compare-button").button(); |
214 | + $("#compare-button").click( |
215 | + function (e) { |
216 | + if (compareState == 0) { |
217 | + startCompare(); |
218 | + } else { |
219 | + cancelCompare(); |
220 | + } |
221 | + } |
222 | + ); |
223 | + } |
224 | +); |
225 | |
226 | === added file 'dashboard_app/templates/dashboard_app/filter_compare_matches.html' |
227 | --- dashboard_app/templates/dashboard_app/filter_compare_matches.html 1970-01-01 00:00:00 +0000 |
228 | +++ dashboard_app/templates/dashboard_app/filter_compare_matches.html 2013-01-10 20:39:21 +0000 |
229 | @@ -0,0 +1,28 @@ |
230 | +{% extends "dashboard_app/_content.html" %} |
231 | + |
232 | +{% load django_tables2 %} |
233 | + |
234 | +{% block extrahead %} |
235 | +{{ block.super }} |
236 | +<style type="text/css"> |
237 | + th.orderable.sortable a { |
238 | + color: rgb(0, 136, 204); |
239 | + text-decoration: underline; |
240 | + } |
241 | +</style> |
242 | +{% endblock %} |
243 | + |
244 | +{% block content %} |
245 | +{% for trinfo in test_run_info %} |
246 | +<h3>{{ trinfo.key }} results</h3> |
247 | +{% if trinfo.only %} |
248 | +<p style="text-align: {{ trinfo.only }}"> |
249 | + Results were only present in <a href="{{ trinfo.tr.get_absolute_url }}">build {{ trinfo.tag }}</a>. |
250 | +</p> |
251 | +{% elif trinfo.table %} |
252 | +{% render_table trinfo.table %} |
253 | +{% else %} |
254 | +<p>No difference in {{ trinfo.key }} results{% if trinfo.cases %} for {{ trinfo.cases }}{% endif %}.</p> |
255 | +{% endif %} |
256 | +{% endfor %} |
257 | +{% endblock %} |
258 | |
259 | === modified file 'dashboard_app/templates/dashboard_app/filter_detail.html' |
260 | --- dashboard_app/templates/dashboard_app/filter_detail.html 2013-01-09 00:05:14 +0000 |
261 | +++ dashboard_app/templates/dashboard_app/filter_detail.html 2013-01-10 20:39:21 +0000 |
262 | @@ -2,6 +2,12 @@ |
263 | {% load i18n %} |
264 | {% load django_tables2 %} |
265 | |
266 | +{% block extrahead %} |
267 | +{{ block.super }} |
268 | +<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}dashboard_app/css/filter-detail.css"/> |
269 | +<script type="text/javascript" src="{{ STATIC_URL }}dashboard_app/js/filter-detail.js"></script> |
270 | +{% endblock %} |
271 | + |
272 | {% block content %} |
273 | |
274 | <h1>Filter {{ filter.name }}</h1> |
275 | @@ -27,4 +33,17 @@ |
276 | |
277 | {% render_table filter_table %} |
278 | |
279 | +<p> |
280 | + <button id="compare-button">Compare builds</button> |
281 | + <span id="first-prompt" style="display:none"> |
282 | + Click a build to compare. |
283 | + </span> |
284 | + <span id="second-prompt" style="display:none"> |
285 | + Click build to compare with build <span id="p2-build">XXX</span>. |
286 | + </span> |
287 | + <span id="third-prompt" style="display:none"> |
288 | + Click <a href="#">here</a> to compare with build <span id="p3-build-1">XXX</span> with build <span id="p3-build-2">XXX</span>. |
289 | + </span> |
290 | +</p> |
291 | + |
292 | {% endblock %} |
293 | |
294 | === modified file 'dashboard_app/urls.py' |
295 | --- dashboard_app/urls.py 2012-12-11 02:01:37 +0000 |
296 | +++ dashboard_app/urls.py 2013-01-10 20:39:21 +0000 |
297 | @@ -45,7 +45,8 @@ |
298 | url(r'^filters/~(?P<username>[a-zA-Z0-9-_]+)/(?P<name>[a-zA-Z0-9-_]+)/\+edit$', 'filters.views.filter_edit'), |
299 | url(r'^filters/~(?P<username>[a-zA-Z0-9-_]+)/(?P<name>[a-zA-Z0-9-_]+)/\+subscribe$', 'filters.views.filter_subscribe'), |
300 | url(r'^filters/~(?P<username>[a-zA-Z0-9-_]+)/(?P<name>[a-zA-Z0-9-_]+)/\+delete$', 'filters.views.filter_delete'), |
301 | - url(r'^xml-rpc/$', linaro_django_xmlrpc.views.handler, |
302 | + url(r'^filters/~(?P<username>[a-zA-Z0-9-_]+)/(?P<name>[a-zA-Z0-9-_]+)/\+compare/(?P<tag1>[a-zA-Z0-9-_: .]+)/(?P<tag2>[a-zA-Z0-9-_: .]+)$', 'filters.views.compare_matches'), |
303 | + url(r'^xml-rpc/$', linaro_django_xmlrpc.views.handler, |
304 | name='dashboard_app.views.dashboard_xml_rpc_handler', |
305 | kwargs={ |
306 | 'mapper': legacy_mapper, |
307 | |
308 | === modified file 'dashboard_app/views/__init__.py' |
309 | --- dashboard_app/views/__init__.py 2012-12-17 00:10:53 +0000 |
310 | +++ dashboard_app/views/__init__.py 2013-01-10 20:39:21 +0000 |
311 | @@ -38,7 +38,6 @@ |
312 | ) |
313 | from django.shortcuts import render_to_response, redirect, get_object_or_404 |
314 | from django.template import RequestContext, loader |
315 | -from django.utils.html import escape |
316 | from django.utils.safestring import mark_safe |
317 | from django.views.generic.list_detail import object_list, object_detail |
318 | |
319 | @@ -825,3 +824,4 @@ |
320 | pk=effort.pk) |
321 | }) |
322 | return HttpResponse(t.render(c)) |
323 | + |
324 | |
325 | === modified file 'dashboard_app/views/filters/tables.py' |
326 | --- dashboard_app/views/filters/tables.py 2012-12-13 00:51:07 +0000 |
327 | +++ dashboard_app/views/filters/tables.py 2013-01-10 20:39:21 +0000 |
328 | @@ -16,8 +16,11 @@ |
329 | # You should have received a copy of the GNU Affero General Public License |
330 | # along with Launch Control. If not, see <http://www.gnu.org/licenses/>. |
331 | |
332 | +import datetime |
333 | import operator |
334 | |
335 | +from django.conf import settings |
336 | +from django.template import defaultfilters |
337 | from django.utils.html import escape |
338 | from django.utils.safestring import mark_safe |
339 | |
340 | @@ -184,6 +187,12 @@ |
341 | self.base_columns.insert(0, 'bundle_stream', bundle_stream_col) |
342 | self.base_columns.insert(0, 'tag', tag_col) |
343 | |
344 | + def render_tag(self, value): |
345 | + if isinstance(value, datetime.datetime): |
346 | + strvalue = defaultfilters.date(value, settings.DATETIME_FORMAT) |
347 | + else: |
348 | + strvalue = value |
349 | + return mark_safe('<span data-machinetag="%s">%s</span>' % (escape(str(value)), strvalue)) |
350 | tag = Column() |
351 | |
352 | def render_bundle_stream(self, record): |
353 | @@ -225,3 +234,29 @@ |
354 | datatable_opts.update({ |
355 | "iDisplayLength": 10, |
356 | }) |
357 | + |
358 | + |
359 | +class TestResultDifferenceTable(DataTablesTable): |
360 | + test_case_id = Column(verbose_name=mark_safe('test_case_id')) |
361 | + first_result = TemplateColumn(''' |
362 | + {% if record.first_result %} |
363 | + <img src="{{ STATIC_URL }}dashboard_app/images/icon-{{ record.first_result }}.png" |
364 | + alt="{{ record.first_result }}" width="16" height="16" border="0"/>{{ record.first_result }} |
365 | + {% else %} |
366 | + <i>missing</i> |
367 | + {% endif %} |
368 | + ''') |
369 | + second_result = TemplateColumn(''' |
370 | + {% if record.second_result %} |
371 | + <img src="{{ STATIC_URL }}dashboard_app/images/icon-{{ record.second_result }}.png" |
372 | + alt="{{ record.second_result }}" width="16" height="16" border="0"/>{{ record.second_result }} |
373 | + {% else %} |
374 | + <i>missing</i> |
375 | + {% endif %} |
376 | + ''') |
377 | + |
378 | + datatable_opts = { |
379 | + 'iDisplayLength': 25, |
380 | + 'sPaginationType': "full_numbers", |
381 | + } |
382 | + |
383 | |
384 | === modified file 'dashboard_app/views/filters/views.py' |
385 | --- dashboard_app/views/filters/views.py 2012-11-27 05:07:00 +0000 |
386 | +++ dashboard_app/views/filters/views.py 2013-01-10 20:39:21 +0000 |
387 | @@ -25,12 +25,17 @@ |
388 | from django.http import HttpResponse, HttpResponseRedirect |
389 | from django.shortcuts import render_to_response |
390 | from django.template import RequestContext |
391 | +from django.utils.html import escape |
392 | +from django.utils.safestring import mark_safe |
393 | |
394 | from lava_server.bread_crumbs import ( |
395 | BreadCrumb, |
396 | BreadCrumbTrail, |
397 | ) |
398 | |
399 | +from dashboard_app.filters import ( |
400 | + evaluate_filter, |
401 | + ) |
402 | from dashboard_app.models import ( |
403 | NamedAttribute, |
404 | Test, |
405 | @@ -39,7 +44,9 @@ |
406 | TestRunFilter, |
407 | TestRunFilterSubscription, |
408 | ) |
409 | -from dashboard_app.views import index |
410 | +from dashboard_app.views import ( |
411 | + index, |
412 | + ) |
413 | from dashboard_app.views.filters.forms import ( |
414 | TestRunFilterForm, |
415 | TestRunFilterSubscriptionForm, |
416 | @@ -48,6 +55,7 @@ |
417 | FilterTable, |
418 | FilterPreviewTable, |
419 | PublicFiltersTable, |
420 | + TestResultDifferenceTable, |
421 | UserFiltersTable, |
422 | ) |
423 | |
424 | @@ -248,3 +256,145 @@ |
425 | json.dumps(list(result)), |
426 | mimetype='application/json') |
427 | |
428 | + |
429 | +def _iter_matching(seq1, seq2, key): |
430 | + """Iterate over sequences in the order given by the key function, matching |
431 | + elements with matching key values. |
432 | + |
433 | + For example: |
434 | + |
435 | + >>> seq1 = [(1, 2), (2, 3)] |
436 | + >>> seq2 = [(1, 3), (3, 4)] |
437 | + >>> def key(pair): return pair[0] |
438 | + >>> list(_iter_matching(seq1, seq2, key)) |
439 | + [(1, (1, 2), (1, 3)), (2, (2, 3), None), (3, None, (3, 4))] |
440 | + """ |
441 | + seq1.sort(key=key) |
442 | + seq2.sort(key=key) |
443 | + sentinel = object() |
444 | + def next(it): |
445 | + try: |
446 | + o = it.next() |
447 | + return (key(o), o) |
448 | + except StopIteration: |
449 | + return (sentinel, None) |
450 | + iter1 = iter(seq1) |
451 | + iter2 = iter(seq2) |
452 | + k1, o1 = next(iter1) |
453 | + k2, o2 = next(iter2) |
454 | + while k1 is not sentinel or k2 is not sentinel: |
455 | + if k1 is sentinel: |
456 | + yield (k2, None, o2) |
457 | + k2, o2 = next(iter2) |
458 | + elif k2 is sentinel: |
459 | + yield (k1, o1, None) |
460 | + k1, o1 = next(iter1) |
461 | + elif k1 == k2: |
462 | + yield (k1, o1, o2) |
463 | + k1, o1 = next(iter1) |
464 | + k2, o2 = next(iter2) |
465 | + elif k1 < k2: |
466 | + yield (k1, o1, None) |
467 | + k1, o1 = next(iter1) |
468 | + else: # so k1 > k2... |
469 | + yield (k2, None, o2) |
470 | + k2, o2 = next(iter2) |
471 | + |
472 | + |
473 | +def _test_run_difference(test_run1, test_run2, cases=None): |
474 | + test_results1 = list(test_run1.test_results.all().select_related('test_case')) |
475 | + test_results2 = list(test_run2.test_results.all().select_related('test_case')) |
476 | + def key(tr): |
477 | + return tr.test_case.test_case_id |
478 | + differences = [] |
479 | + for tc_id, tc1, tc2 in _iter_matching(test_results1, test_results2, key): |
480 | + if cases is not None and tc_id not in cases: |
481 | + continue |
482 | + if tc1: |
483 | + tc1 = tc1.result_code |
484 | + if tc2: |
485 | + tc2 = tc2.result_code |
486 | + if tc1 != tc2: |
487 | + differences.append({ |
488 | + 'test_case_id': tc_id, |
489 | + 'first_result': tc1, |
490 | + 'second_result': tc2, |
491 | + }) |
492 | + return differences |
493 | + |
494 | + |
495 | +@BreadCrumb( |
496 | + "Comparing builds {tag1} and {tag2}", |
497 | + parent=filter_detail, |
498 | + needs=['username', 'name', 'tag1', 'tag2']) |
499 | +def compare_matches(request, username, name, tag1, tag2): |
500 | + filter = TestRunFilter.objects.get(owner__username=username, name=name) |
501 | + if not filter.public and filter.owner != request.user: |
502 | + raise PermissionDenied() |
503 | + filter_data = filter.as_data() |
504 | + matches = evaluate_filter(request.user, filter_data) |
505 | + match1, match2 = matches.with_tags(tag1, tag2) |
506 | + test_cases_for_test_id = {} |
507 | + for test in filter_data['tests']: |
508 | + test_cases = test['test_cases'] |
509 | + if test_cases: |
510 | + test_cases = set([tc.test_case_id for tc in test_cases]) |
511 | + else: |
512 | + test_cases = None |
513 | + test_cases_for_test_id[test['test'].test_id] = test_cases |
514 | + test_run_info = [] |
515 | + def key(tr): |
516 | + return tr.test.test_id |
517 | + for key, tr1, tr2 in _iter_matching(match1.test_runs, match2.test_runs, key): |
518 | + if tr1 is None: |
519 | + table = None |
520 | + only = 'right' |
521 | + tr = tr2 |
522 | + tag = tag2 |
523 | + cases = None |
524 | + elif tr2 is None: |
525 | + table = None |
526 | + only = 'left' |
527 | + tr = tr1 |
528 | + tag = tag1 |
529 | + cases = None |
530 | + else: |
531 | + only = None |
532 | + tr = None |
533 | + tag = None |
534 | + cases = test_cases_for_test_id.get(key) |
535 | + test_result_differences = _test_run_difference(tr1, tr2, cases) |
536 | + if test_result_differences: |
537 | + table = TestResultDifferenceTable( |
538 | + "test-result-difference-" + escape(key), data=test_result_differences) |
539 | + table.base_columns['first_result'].verbose_name = mark_safe( |
540 | + '<a href="%s">build %s: %s</a>' % ( |
541 | + tr1.get_absolute_url(), escape(tag1), escape(key))) |
542 | + table.base_columns['second_result'].verbose_name = mark_safe( |
543 | + '<a href="%s">build %s: %s</a>' % ( |
544 | + tr2.get_absolute_url(), escape(tag2), escape(key))) |
545 | + else: |
546 | + table = None |
547 | + if cases: |
548 | + cases = sorted(cases) |
549 | + if len(cases) > 1: |
550 | + cases = ', '.join(cases[:-1]) + ' or ' + cases[-1] |
551 | + else: |
552 | + cases = cases[0] |
553 | + test_run_info.append(dict( |
554 | + only=only, |
555 | + key=key, |
556 | + table=table, |
557 | + tr=tr, |
558 | + tag=tag, |
559 | + cases=cases)) |
560 | + return render_to_response( |
561 | + "dashboard_app/filter_compare_matches.html", { |
562 | + 'test_run_info': test_run_info, |
563 | + 'bread_crumb_trail': BreadCrumbTrail.leading_to( |
564 | + compare_matches, |
565 | + name=name, |
566 | + username=username, |
567 | + tag1=tag1, |
568 | + tag2=tag2), |
569 | + }, RequestContext(request)) |
I've tried to make the way of getting to the comparison page a little less odd. Let me know what you think.