From patchwork Fri Jan 11 02:17:11 2013 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Michael-Doyle Hudson X-Patchwork-Id: 13965 Return-Path: X-Original-To: patchwork@peony.canonical.com Delivered-To: patchwork@peony.canonical.com Received: from fiordland.canonical.com (fiordland.canonical.com [91.189.94.145]) by peony.canonical.com (Postfix) with ESMTP id 7CF9223ED1 for ; Fri, 11 Jan 2013 02:17:14 +0000 (UTC) Received: from mail-vb0-f46.google.com (mail-vb0-f46.google.com [209.85.212.46]) by fiordland.canonical.com (Postfix) with ESMTP id EE71BA19AA7 for ; Fri, 11 Jan 2013 02:17:13 +0000 (UTC) Received: by mail-vb0-f46.google.com with SMTP id b13so984984vby.19 for ; Thu, 10 Jan 2013 18:17:13 -0800 (PST) X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=20120113; h=x-received:x-forwarded-to:x-forwarded-for:delivered-to:x-received :received-spf:content-type:mime-version:x-launchpad-project :x-launchpad-branch:x-launchpad-message-rationale :x-launchpad-branch-revision-number:x-launchpad-notification-type:to :from:subject:message-id:date:reply-to:sender:errors-to:precedence :x-generated-by:x-launchpad-hash:x-gm-message-state; bh=mpfLFRKvDfTLZS26P3xjz1AATPKaIsDK7DkKFsuXM1w=; b=jGPciX7ApdsvNa7cD1NmBXQfuURIB8fNs7ePtFE0Dl3zxfiKamvJanO4fZL0Et/Tny 30ssgdQvVAh3SbMtNkqrqfJtGz/hk/4M1HeT1/i0L0QMIqtldJit5t4zFcPt40gwqjpA i3xgDrXAaFdz23DCraNYtjc0iLhoFHb7HlBn35ibLnh+HG4CnQNyTheNgIfsPQKosmgr if46qOjDTKbb556kOzGZ4f+NUK3T3xyLQ1IzDRl45gW0yz/rIsvtZBr31zkcsGWnPv/h bZy4BfYHGuNDbsJsHUti0Jb5vO6YmrS5qCcAYhpbG124e1m8C5ge4nceL+xahoYyfUHE 9zmA== X-Received: by 10.52.18.207 with SMTP id y15mr80349550vdd.8.1357870633373; Thu, 10 Jan 2013 18:17:13 -0800 (PST) X-Forwarded-To: linaro-patchwork@canonical.com X-Forwarded-For: patch@linaro.org linaro-patchwork@canonical.com Delivered-To: patches@linaro.org Received: by 10.58.145.101 with SMTP id st5csp92288veb; Thu, 10 Jan 2013 18:17:12 -0800 (PST) X-Received: by 10.194.57.82 with SMTP id g18mr25322617wjq.25.1357870632256; Thu, 10 Jan 2013 18:17:12 -0800 (PST) Received: from indium.canonical.com (indium.canonical.com. [91.189.90.7]) by mx.google.com with ESMTPS id dm9si14120352wib.21.2013.01.10.18.17.11 (version=TLSv1 cipher=RC4-SHA bits=128/128); Thu, 10 Jan 2013 18:17:12 -0800 (PST) Received-SPF: pass (google.com: best guess record for domain of bounces@canonical.com designates 91.189.90.7 as permitted sender) client-ip=91.189.90.7; Authentication-Results: mx.google.com; spf=pass (google.com: best guess record for domain of bounces@canonical.com designates 91.189.90.7 as permitted sender) smtp.mail=bounces@canonical.com Received: from ackee.canonical.com ([91.189.89.26]) by indium.canonical.com with esmtp (Exim 4.71 #1 (Debian)) id 1TtUBD-0001WV-LX for ; Fri, 11 Jan 2013 02:17:11 +0000 Received: from ackee.canonical.com (localhost [127.0.0.1]) by ackee.canonical.com (Postfix) with ESMTP id 92872E00F4 for ; Fri, 11 Jan 2013 02:17:11 +0000 (UTC) MIME-Version: 1.0 X-Launchpad-Project: lava-dashboard X-Launchpad-Branch: ~linaro-validation/lava-dashboard/trunk X-Launchpad-Message-Rationale: Subscriber X-Launchpad-Branch-Revision-Number: 385 X-Launchpad-Notification-Type: branch-revision To: Linaro Patch Tracker From: noreply@launchpad.net Subject: [Branch ~linaro-validation/lava-dashboard/trunk] Rev 385: add a way to compare different matches of a filter Message-Id: <20130111021711.29524.12975.launchpad@ackee.canonical.com> Date: Fri, 11 Jan 2013 02:17:11 -0000 Reply-To: noreply@launchpad.net Sender: bounces@canonical.com Errors-To: bounces@canonical.com Precedence: bulk X-Generated-By: Launchpad (canonical.com); Revision="16412"; Instance="launchpad-lazr.conf" X-Launchpad-Hash: d62669acc754d5eb04c5ba6c7a90ea1248e5d57a X-Gm-Message-State: ALoCoQnDugRd+BUTxHKRDYUkYK6HIKzw5fDnu6gcetB/FvAsQFwTWqDKEw8JC6I/4AcWY8RY05cA Merge authors: Michael Hudson-Doyle (mwhudson) Related merge proposals: https://code.launchpad.net/~mwhudson/lava-dashboard/compare-testrun-view/+merge/142421 proposed by: Michael Hudson-Doyle (mwhudson) review: Approve - Andy Doan (doanac) ------------------------------------------------------------ revno: 385 [merge] committer: Michael Hudson-Doyle branch nick: trunk timestamp: Fri 2013-01-11 15:16:14 +1300 message: add a way to compare different matches of a filter added: dashboard_app/static/css/filter-detail.css dashboard_app/static/js/filter-detail.js dashboard_app/templates/dashboard_app/filter_compare_matches.html modified: dashboard_app/filters.py dashboard_app/templates/dashboard_app/filter_detail.html dashboard_app/urls.py dashboard_app/views/__init__.py dashboard_app/views/filters/tables.py dashboard_app/views/filters/views.py --- lp:lava-dashboard https://code.launchpad.net/~linaro-validation/lava-dashboard/trunk You are subscribed to branch lp:lava-dashboard. To unsubscribe from this branch go to https://code.launchpad.net/~linaro-validation/lava-dashboard/trunk/+edit-subscription === modified file 'dashboard_app/filters.py' --- dashboard_app/filters.py 2012-12-18 00:47:08 +0000 +++ dashboard_app/filters.py 2013-01-08 01:27:34 +0000 @@ -300,6 +300,22 @@ q = self.queryset.filter(bundle__uploaded_on__gt=since) return self._wrap(q) + def with_tags(self, tag1, tag2): + if self.key == 'build_number': + q = self.queryset.extra( + where=['convert_to_integer("dashboard_app_namedattribute"."value") in (%s, %s)' % (tag1, tag2)] + ) + else: + tag1 = datetime.datetime.strptime(tag1, "%Y-%m-%d %H:%M:%S.%f") + tag2 = datetime.datetime.strptime(tag2, "%Y-%m-%d %H:%M:%S.%f") + q = self.queryset.filter(bundle__uploaded_on__in=(tag1, tag2)) + matches = list(self._wrap(q)) + if matches[0].tag == tag1: + return matches + else: + matches.reverse() + return matches + def count(self): return self.queryset.count() === added file 'dashboard_app/static/css/filter-detail.css' --- dashboard_app/static/css/filter-detail.css 1970-01-01 00:00:00 +0000 +++ dashboard_app/static/css/filter-detail.css 2013-01-10 20:37:00 +0000 @@ -0,0 +1,52 @@ +table.select-compare1 td { cursor: pointer; } +table.select-compare1 tr.even td { + background-color: #ccf; +} +table.select-compare1 tr.even.hover td { + background-color: #77f; +} +table.select-compare1 tr.odd td { + background-color: #aaf; +} +table.select-compare1 tr.odd.hover td { + background-color: #77f; +} + +table.select-compare2 td { cursor: pointer; } +table.select-compare2 tr.even td { + background-color: #fcc; +} +table.select-compare2 tr.odd td { + background-color: #faa; +} +table.select-compare2 tr.selected-1 td { + background-color: #77f; +} +table.select-compare2 tr.selected-1.hover td { + background-color: #77f; +} +table.select-compare2 tr.hover td { + background-color: #f77; +} +table.select-compare3 tr.selected-1 td { + background-color: #77f; +} +table.select-compare3 tr.selected-1.hover td { + background-color: #77f; +} +table.select-compare3 tr.selected-2 td { + background-color: #f77; +} +table.select-compare3 tr.selected-2.hover td { + background-color: #f77; +} +table.select-compare3 tr.selected-1 { + cursor: pointer; +} +table.select-compare3 tr.selected-2 { + cursor: pointer; +} +#filter-table input { + margin-top: 0; + margin-bottom: 0; +} \ No newline at end of file === added file 'dashboard_app/static/js/filter-detail.js' --- dashboard_app/static/js/filter-detail.js 1970-01-01 00:00:00 +0000 +++ dashboard_app/static/js/filter-detail.js 2013-01-10 01:34:27 +0000 @@ -0,0 +1,135 @@ +var compareState = 0; +var compare1 = null, compare2 = null; +function cancelCompare () { + $("#filter-table").removeClass("select-compare1"); + $("#filter-table").removeClass("select-compare2"); + $("#filter-table").removeClass("select-compare3"); + $("#filter-table tr").removeClass("selected-1"); + $("#filter-table tr").removeClass("selected-2"); + $("#filter-table tr").unbind("click"); + $("#filter-table tr").unbind("hover"); + $("#filter-table tr").each(removeCheckbox); + $("#first-prompt").hide(); + $("#second-prompt").hide(); + $("#third-prompt").hide(); + $("#compare-button").button({label:"Compare builds"}); + compareState = 0; +} +function startCompare () { + $("#compare-button").button({label:"Cancel"}); + $("#filter-table").addClass("select-compare1"); + $("#filter-table tr").click(rowClickHandler); + $("#filter-table tr").each(insertCheckbox); + $("#filter-table tr").hover(rowHoverHandlerIn, rowHoverHandlerOut); + $("#first-prompt").show(); + compareState = 1; +} +function tagFromRow(tr) { + var firstCell = $(tr).find("td:eq(0)"); + return { + machinetag: firstCell.find("span").data("machinetag"), + usertag: firstCell.text() + }; +} +function rowClickHandler() { + if (compareState == 1) { + compare1 = tagFromRow($(this)); + $(this).addClass("selected-1"); + $(this).find("input").attr("checked", true); + $("#p2-build").text(compare1.usertag); + $("#first-prompt").hide(); + $("#second-prompt").show(); + $("#filter-table").removeClass("select-compare1"); + $("#filter-table").addClass("select-compare2"); + compareState = 2; + } else if (compareState == 2) { + var thistag = tagFromRow($(this)); + if (compare1.machinetag == thistag.machinetag) { + cancelCompare(); + startCompare(); + } else { + compare2 = thistag; + $(this).find("input").attr("checked", true); + $(this).addClass("selected-2"); + $("#second-prompt").hide(); + $("#third-prompt").show(); + $("#filter-table").removeClass("select-compare2"); + $("#filter-table").addClass("select-compare3"); + $("#filter-table input").attr("disabled", true); + $("#filter-table .selected-1 input").attr("disabled", false); + $("#filter-table .selected-2 input").attr("disabled", false); + $("#p3-build-1").text(compare1.usertag); + $("#p3-build-2").text(compare2.usertag); + $("#third-prompt a").attr("href", window.location + '/+compare/' + compare1.machinetag + '/' + compare2.machinetag); + compareState = 3; + } + } else if (compareState == 3) { + var thistag = tagFromRow($(this)); + if (thistag.machinetag == compare1.machinetag || thistag.machinetag == compare2.machinetag) { + $("#second-prompt").show(); + $("#third-prompt").hide(); + $("#filter-table").addClass("select-compare2"); + $("#filter-table").removeClass("select-compare3"); + $("#filter-table input").attr("disabled", false); + compareState = 2; + $(this).find("input").attr("checked", false); + if (thistag.machinetag == compare1.machinetag) { + compare1 = compare2; + $("#filter-table .selected-1").removeClass("selected-1"); + $("#filter-table .selected-2").addClass("selected-1"); + $("#p2-build").text(compare1.usertag); + } + $("#filter-table .selected-2").removeClass("selected-2"); + } + } + tagFromRow(this); +} +function rowHoverHandlerIn() { + $(this).addClass("hover"); +} +function rowHoverHandlerOut() { + $(this).removeClass("hover"); +} +function insertCheckbox() { + var row = $(this); + var checkbox = $(''); + row.find("td:first").prepend(checkbox); +} +function removeCheckbox() { + var row = $(this); + row.find('input').remove(); +} +$(window).load( + function () { + $("#filter-table").dataTable().fnSettings().fnRowCallback = function(tr, data, index) { + if (compareState) { + insertCheckbox.call(tr); + $(tr).click(rowClickHandler); + $("#filter-table tr").hover(rowHoverHandlerIn, rowHoverHandlerOut); + if (compareState >= 2 && tagFromRow(tr).machinetag == compare1.machinetag) { + $(tr).addClass("selected-1"); + $(tr).find("input").attr("checked", true); + } + if (compareState >= 3) { + if (tagFromRow(tr).machinetag == compare2.machinetag) { + $(tr).addClass("selected-2"); + $(tr).find("input").attr("checked", true); + } else if (tagFromRow(tr).machinetag != compare1.machinetag) { + $(tr).find("input").attr("disabled", true); + } + } + } + return tr; + }; + $("#compare-button").button(); + $("#compare-button").click( + function (e) { + if (compareState == 0) { + startCompare(); + } else { + cancelCompare(); + } + } + ); + } +); === added file 'dashboard_app/templates/dashboard_app/filter_compare_matches.html' --- dashboard_app/templates/dashboard_app/filter_compare_matches.html 1970-01-01 00:00:00 +0000 +++ dashboard_app/templates/dashboard_app/filter_compare_matches.html 2013-01-08 21:16:54 +0000 @@ -0,0 +1,28 @@ +{% extends "dashboard_app/_content.html" %} + +{% load django_tables2 %} + +{% block extrahead %} +{{ block.super }} + +{% endblock %} + +{% block content %} +{% for trinfo in test_run_info %} +

{{ trinfo.key }} results

+{% if trinfo.only %} +

+ Results were only present in build {{ trinfo.tag }}. +

+{% elif trinfo.table %} +{% render_table trinfo.table %} +{% else %} +

No difference in {{ trinfo.key }} results{% if trinfo.cases %} for {{ trinfo.cases }}{% endif %}.

+{% endif %} +{% endfor %} +{% endblock %} === modified file 'dashboard_app/templates/dashboard_app/filter_detail.html' --- dashboard_app/templates/dashboard_app/filter_detail.html 2013-01-09 00:05:14 +0000 +++ dashboard_app/templates/dashboard_app/filter_detail.html 2013-01-10 01:34:27 +0000 @@ -2,6 +2,12 @@ {% load i18n %} {% load django_tables2 %} +{% block extrahead %} +{{ block.super }} + + +{% endblock %} + {% block content %}

Filter {{ filter.name }}

@@ -27,4 +33,17 @@ {% render_table filter_table %} +

+ + + + +

+ {% endblock %} === modified file 'dashboard_app/urls.py' --- dashboard_app/urls.py 2012-12-11 02:01:37 +0000 +++ dashboard_app/urls.py 2013-01-08 23:44:15 +0000 @@ -45,7 +45,8 @@ url(r'^filters/~(?P[a-zA-Z0-9-_]+)/(?P[a-zA-Z0-9-_]+)/\+edit$', 'filters.views.filter_edit'), url(r'^filters/~(?P[a-zA-Z0-9-_]+)/(?P[a-zA-Z0-9-_]+)/\+subscribe$', 'filters.views.filter_subscribe'), url(r'^filters/~(?P[a-zA-Z0-9-_]+)/(?P[a-zA-Z0-9-_]+)/\+delete$', 'filters.views.filter_delete'), - url(r'^xml-rpc/$', linaro_django_xmlrpc.views.handler, + url(r'^filters/~(?P[a-zA-Z0-9-_]+)/(?P[a-zA-Z0-9-_]+)/\+compare/(?P[a-zA-Z0-9-_: .]+)/(?P[a-zA-Z0-9-_: .]+)$', 'filters.views.compare_matches'), + url(r'^xml-rpc/$', linaro_django_xmlrpc.views.handler, name='dashboard_app.views.dashboard_xml_rpc_handler', kwargs={ 'mapper': legacy_mapper, === modified file 'dashboard_app/views/__init__.py' --- dashboard_app/views/__init__.py 2012-12-17 00:10:53 +0000 +++ dashboard_app/views/__init__.py 2013-01-08 01:27:34 +0000 @@ -38,7 +38,6 @@ ) from django.shortcuts import render_to_response, redirect, get_object_or_404 from django.template import RequestContext, loader -from django.utils.html import escape from django.utils.safestring import mark_safe from django.views.generic.list_detail import object_list, object_detail @@ -825,3 +824,4 @@ pk=effort.pk) }) return HttpResponse(t.render(c)) + === modified file 'dashboard_app/views/filters/tables.py' --- dashboard_app/views/filters/tables.py 2012-12-13 00:51:07 +0000 +++ dashboard_app/views/filters/tables.py 2013-01-08 23:11:01 +0000 @@ -16,8 +16,11 @@ # You should have received a copy of the GNU Affero General Public License # along with Launch Control. If not, see . +import datetime import operator +from django.conf import settings +from django.template import defaultfilters from django.utils.html import escape from django.utils.safestring import mark_safe @@ -184,6 +187,12 @@ self.base_columns.insert(0, 'bundle_stream', bundle_stream_col) self.base_columns.insert(0, 'tag', tag_col) + def render_tag(self, value): + if isinstance(value, datetime.datetime): + strvalue = defaultfilters.date(value, settings.DATETIME_FORMAT) + else: + strvalue = value + return mark_safe('%s' % (escape(str(value)), strvalue)) tag = Column() def render_bundle_stream(self, record): @@ -225,3 +234,29 @@ datatable_opts.update({ "iDisplayLength": 10, }) + + +class TestResultDifferenceTable(DataTablesTable): + test_case_id = Column(verbose_name=mark_safe('test_case_id')) + first_result = TemplateColumn(''' + {% if record.first_result %} + {{ record.first_result }}{{ record.first_result }} + {% else %} + missing + {% endif %} + ''') + second_result = TemplateColumn(''' + {% if record.second_result %} + {{ record.second_result }}{{ record.second_result }} + {% else %} + missing + {% endif %} + ''') + + datatable_opts = { + 'iDisplayLength': 25, + 'sPaginationType': "full_numbers", + } + === modified file 'dashboard_app/views/filters/views.py' --- dashboard_app/views/filters/views.py 2012-11-27 05:07:00 +0000 +++ dashboard_app/views/filters/views.py 2013-01-10 00:34:58 +0000 @@ -25,12 +25,17 @@ from django.http import HttpResponse, HttpResponseRedirect from django.shortcuts import render_to_response from django.template import RequestContext +from django.utils.html import escape +from django.utils.safestring import mark_safe from lava_server.bread_crumbs import ( BreadCrumb, BreadCrumbTrail, ) +from dashboard_app.filters import ( + evaluate_filter, + ) from dashboard_app.models import ( NamedAttribute, Test, @@ -39,7 +44,9 @@ TestRunFilter, TestRunFilterSubscription, ) -from dashboard_app.views import index +from dashboard_app.views import ( + index, + ) from dashboard_app.views.filters.forms import ( TestRunFilterForm, TestRunFilterSubscriptionForm, @@ -48,6 +55,7 @@ FilterTable, FilterPreviewTable, PublicFiltersTable, + TestResultDifferenceTable, UserFiltersTable, ) @@ -248,3 +256,145 @@ json.dumps(list(result)), mimetype='application/json') + +def _iter_matching(seq1, seq2, key): + """Iterate over sequences in the order given by the key function, matching + elements with matching key values. + + For example: + + >>> seq1 = [(1, 2), (2, 3)] + >>> seq2 = [(1, 3), (3, 4)] + >>> def key(pair): return pair[0] + >>> list(_iter_matching(seq1, seq2, key)) + [(1, (1, 2), (1, 3)), (2, (2, 3), None), (3, None, (3, 4))] + """ + seq1.sort(key=key) + seq2.sort(key=key) + sentinel = object() + def next(it): + try: + o = it.next() + return (key(o), o) + except StopIteration: + return (sentinel, None) + iter1 = iter(seq1) + iter2 = iter(seq2) + k1, o1 = next(iter1) + k2, o2 = next(iter2) + while k1 is not sentinel or k2 is not sentinel: + if k1 is sentinel: + yield (k2, None, o2) + k2, o2 = next(iter2) + elif k2 is sentinel: + yield (k1, o1, None) + k1, o1 = next(iter1) + elif k1 == k2: + yield (k1, o1, o2) + k1, o1 = next(iter1) + k2, o2 = next(iter2) + elif k1 < k2: + yield (k1, o1, None) + k1, o1 = next(iter1) + else: # so k1 > k2... + yield (k2, None, o2) + k2, o2 = next(iter2) + + +def _test_run_difference(test_run1, test_run2, cases=None): + test_results1 = list(test_run1.test_results.all().select_related('test_case')) + test_results2 = list(test_run2.test_results.all().select_related('test_case')) + def key(tr): + return tr.test_case.test_case_id + differences = [] + for tc_id, tc1, tc2 in _iter_matching(test_results1, test_results2, key): + if cases is not None and tc_id not in cases: + continue + if tc1: + tc1 = tc1.result_code + if tc2: + tc2 = tc2.result_code + if tc1 != tc2: + differences.append({ + 'test_case_id': tc_id, + 'first_result': tc1, + 'second_result': tc2, + }) + return differences + + +@BreadCrumb( + "Comparing builds {tag1} and {tag2}", + parent=filter_detail, + needs=['username', 'name', 'tag1', 'tag2']) +def compare_matches(request, username, name, tag1, tag2): + filter = TestRunFilter.objects.get(owner__username=username, name=name) + if not filter.public and filter.owner != request.user: + raise PermissionDenied() + filter_data = filter.as_data() + matches = evaluate_filter(request.user, filter_data) + match1, match2 = matches.with_tags(tag1, tag2) + test_cases_for_test_id = {} + for test in filter_data['tests']: + test_cases = test['test_cases'] + if test_cases: + test_cases = set([tc.test_case_id for tc in test_cases]) + else: + test_cases = None + test_cases_for_test_id[test['test'].test_id] = test_cases + test_run_info = [] + def key(tr): + return tr.test.test_id + for key, tr1, tr2 in _iter_matching(match1.test_runs, match2.test_runs, key): + if tr1 is None: + table = None + only = 'right' + tr = tr2 + tag = tag2 + cases = None + elif tr2 is None: + table = None + only = 'left' + tr = tr1 + tag = tag1 + cases = None + else: + only = None + tr = None + tag = None + cases = test_cases_for_test_id.get(key) + test_result_differences = _test_run_difference(tr1, tr2, cases) + if test_result_differences: + table = TestResultDifferenceTable( + "test-result-difference-" + escape(key), data=test_result_differences) + table.base_columns['first_result'].verbose_name = mark_safe( + 'build %s: %s' % ( + tr1.get_absolute_url(), escape(tag1), escape(key))) + table.base_columns['second_result'].verbose_name = mark_safe( + 'build %s: %s' % ( + tr2.get_absolute_url(), escape(tag2), escape(key))) + else: + table = None + if cases: + cases = sorted(cases) + if len(cases) > 1: + cases = ', '.join(cases[:-1]) + ' or ' + cases[-1] + else: + cases = cases[0] + test_run_info.append(dict( + only=only, + key=key, + table=table, + tr=tr, + tag=tag, + cases=cases)) + return render_to_response( + "dashboard_app/filter_compare_matches.html", { + 'test_run_info': test_run_info, + 'bread_crumb_trail': BreadCrumbTrail.leading_to( + compare_matches, + name=name, + username=username, + tag1=tag1, + tag2=tag2), + }, RequestContext(request))