=== modified file 'dashboard_app/admin.py'
@@ -137,8 +137,13 @@
class TestRunAdmin(admin.ModelAdmin):
class NamedAttributeInline(generic.GenericTabularInline):
model = NamedAttribute
- list_display = ('analyzer_assigned_uuid',
- 'analyzer_assigned_date', 'import_assigned_date')
+ list_filter = ('test'),
+ list_display = (
+ 'test',
+ 'analyzer_assigned_uuid',
+ 'bundle',
+ 'analyzer_assigned_date',
+ 'import_assigned_date')
inlines = [NamedAttributeInline]
=== removed file 'dashboard_app/dataview.py'
@@ -1,284 +0,0 @@
-# Copyright (C) 2011 Linaro Limited
-#
-# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org>
-#
-# This file is part of Launch Control.
-#
-# Launch Control is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License version 3
-# as published by the Free Software Foundation
-#
-# Launch Control is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with Launch Control. If not, see <http://www.gnu.org/licenses/>.
-
-"""
-DataViews: Encapsulated SQL query definitions.
-
-Implementation of the following launchpad blueprint:
-https://blueprints.launchpad.net/launch-control/+spec/other-linaro-n-data-views-for-launch-control
-"""
-
-from contextlib import closing
-from xml.sax import parseString
-from xml.sax.handler import ContentHandler
-import logging
-import os
-import re
-
-
-class DataView(object):
- """
- Data view, a container for SQL query and optional arguments
- """
-
- def __init__(self, name, backend_queries, arguments, documentation, summary):
- self.name = name
- self.backend_queries = backend_queries
- self.arguments = arguments
- self.documentation = documentation
- self.summary = summary
-
- def _get_connection_backend_name(self, connection):
- backend = str(type(connection))
- if "sqlite" in backend:
- return "sqlite"
- elif "postgresql" in backend:
- return "postgresql"
- else:
- return ""
-
- def get_backend_specific_query(self, connection):
- """
- Return BackendSpecificQuery for the specified connection
- """
- sql_backend_name = self._get_connection_backend_name(connection)
- try:
- return self.backend_queries[sql_backend_name]
- except KeyError:
- return self.backend_queries.get(None, None)
-
- def lookup_argument(self, name):
- """
- Return Argument with the specified name
-
- Raises LookupError if the argument cannot be found
- """
- for argument in self.arguments:
- if argument.name == name:
- return argument
- raise LookupError(name)
-
- @classmethod
- def load_from_xml(self, xml_string):
- """
- Load a data view instance from XML description
-
- This raises ValueError in several error situations.
- TODO: check what kind of exceptions this can raise
- """
- handler = _DataViewHandler()
- parseString(xml_string, handler)
- return handler.data_view
-
- @classmethod
- def get_connection(cls):
- """
- Get the appropriate connection for data views
- """
- from django.db import connection, connections
- from django.db.utils import ConnectionDoesNotExist
- try:
- return connections['dataview']
- except ConnectionDoesNotExist:
- logging.warning("dataview-specific database connection not available, dataview query is NOT sandboxed")
- return connection # NOTE: it's connection not connectionS (the default connection)
-
- def __call__(self, connection, **arguments):
- # Check if arguments have any bogus names
- valid_arg_names = frozenset([argument.name for argument in self.arguments])
- for arg_name in arguments:
- if arg_name not in valid_arg_names:
- raise TypeError("Data view %s has no argument %r" % (self.name, arg_name))
- # Get the SQL template for our database connection
- query = self.get_backend_specific_query(connection)
- if query is None:
- raise LookupError("Specified data view has no SQL implementation "
- "for current database")
- # Replace SQL aruments with django placeholders (connection agnostic)
- template = query.sql_template
- template = template.replace("%", "%%")
- # template = template.replace("{", "{{").replace("}", "}}")
- sql = template.format(
- **dict([
- (arg_name, "%s")
- for arg_name in query.argument_list]))
- # Construct argument list using defaults for missing values
- sql_args = [
- arguments.get(arg_name, self.lookup_argument(arg_name).default)
- for arg_name in query.argument_list]
- with closing(connection.cursor()) as cursor:
- # Execute the query with the specified arguments
- cursor.execute(sql, sql_args)
- # Get and return the results
- rows = cursor.fetchall()
- columns = cursor.description
- return rows, columns
-
-
-class Argument(object):
- """
- Data view argument for SQL prepared statements
- """
-
- def __init__(self, name, type, default, help):
- self.name = name
- self.type = type
- self.default = default
- self.help = help
-
-
-class BackendSpecificQuery(object):
- """
- Backend-specific query and argument list
- """
-
- def __init__(self, backend, sql_template, argument_list):
- self.backend = backend
- self.sql_template = sql_template
- self.argument_list = argument_list
-
-
-class _DataViewHandler(ContentHandler):
- """
- ContentHandler subclass for parsing DataView documents
- """
-
- def _end_text(self):
- """
- Stop collecting text and produce a stripped string with deduplicated whitespace
- """
- full_text = re.sub("\s+", " ", u''.join(self._text)).strip()
- self.text = None
- return full_text
-
- def _start_text(self):
- """
- Start collecting text
- """
- self._text = []
-
- def startDocument(self):
- # Text can be None or a [] that accumulates all detected text
- self._text = None
- # Data view object
- self.data_view = DataView(None, {}, [], None, None)
- # Internal variables
- self._current_backend_query = None
-
- def endDocument(self):
- # TODO: check if we have anything defined
- if self.data_view.name is None:
- raise ValueError("No data view definition found")
-
- def startElement(self, name, attrs):
- if name == "data-view":
- self.data_view.name = attrs["name"]
- elif name == "summary" or name == "documentation":
- self._start_text()
- elif name == "sql":
- self._start_text()
- self._current_backend_query = BackendSpecificQuery(attrs.get("backend"), None, [])
- self.data_view.backend_queries[self._current_backend_query.backend] = self._current_backend_query
- elif name == "value":
- if "name" not in attrs:
- raise ValueError("<value> requires attribute 'name'")
- self._text.append("{" + attrs["name"] + "}")
- self._current_backend_query.argument_list.append(attrs["name"])
- elif name == "argument":
- if "name" not in attrs:
- raise ValueError("<argument> requires attribute 'name'")
- if "type" not in attrs:
- raise ValueError("<argument> requires attribute 'type'")
- if attrs["type"] not in ("string", "number", "boolean", "timestamp"):
- raise ValueError("invalid value for argument 'type' on <argument>")
- argument = Argument(name=attrs["name"], type=attrs["type"],
- default=attrs.get("default", None),
- help=attrs.get("help", None))
- self.data_view.arguments.append(argument)
-
- def endElement(self, name):
- if name == "sql":
- self._current_backend_query.sql_template = self._end_text()
- self._current_backend_query = None
- elif name == "documentation":
- self.data_view.documentation = self._end_text()
- elif name == "summary":
- self.data_view.summary = self._end_text()
-
- def characters(self, content):
- if isinstance(self._text, list):
- self._text.append(content)
-
-
-class DataViewRepository(object):
-
- _instance = None
-
- def __init__(self):
- self.data_views = []
-
- def __iter__(self):
- return iter(self.data_views)
-
- def __getitem__(self, name):
- for item in self:
- if item.name == name:
- return item
- else:
- raise KeyError(name)
-
- def load_from_directory(self, directory):
- for name in os.listdir(directory):
- pathname = os.path.join(directory, name)
- if os.path.isfile(pathname) and pathname.endswith(".xml"):
- self.load_from_file(pathname)
-
- def load_from_file(self, pathname):
- try:
- with open(pathname, "rt") as stream:
- text = stream.read()
- data_view = DataView.load_from_xml(text)
- self.data_views.append(data_view)
- except Exception as exc:
- logging.error("Unable to load data view from %s: %s", pathname, exc)
-
- @classmethod
- def get_instance(cls):
- from django.conf import settings
- if cls._instance is None:
- cls._instance = cls()
- cls._instance.load_default()
-
- # I development mode always reload data views
- if getattr(settings, "DEBUG", False) is True:
- cls._instance.data_views = []
- cls._instance.load_default()
- return cls._instance
-
- def load_default(self):
- from django.conf import settings
- for dirname in getattr(settings, "DATAVIEW_DIRS", []):
- self.load_from_directory(dirname)
-
-
-__all__ = [
- "Argument",
- "BackendSpecificQuery",
- "DataView",
- "DataViewRepository",
-]
=== removed file 'dashboard_app/dispatcher.py'
@@ -1,188 +0,0 @@
-# Copyright (C) 2010 Linaro Limited
-#
-# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org>
-#
-# This file is part of Launch Control.
-#
-# Launch Control is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License version 3
-# as published by the Free Software Foundation
-#
-# Launch Control is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with Launch Control. If not, see <http://www.gnu.org/licenses/>.
-
-"""
-XML-RPC Dispatcher for the dashboard
-"""
-import SimpleXMLRPCServer
-import logging
-import sys
-import xmlrpclib
-
-class _NullHandler(logging.Handler):
- def emit(self, record):
- pass
-
-
-class FaultCodes:
- """
- Common fault codes.
-
- See: http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php
- """
- class ParseError:
- NOT_WELL_FORMED = -32700
- UNSUPPORTED_ENCODING = -32701
- INVALID_CHARACTER_FOR_ENCODING = -32702
- class ServerError:
- INVALID_XML_RPC = -32600
- REQUESTED_METHOD_NOT_FOUND = -32601
- INVALID_METHOD_PARAMETERS = -32602
- INTERNAL_XML_RPC_ERROR = -32603
- APPLICATION_ERROR = -32500
- SYSTEM_ERROR = -32400
- TRANSPORT_ERROR = -32300
-
-
-class DjangoXMLRPCDispatcher(SimpleXMLRPCServer.SimpleXMLRPCDispatcher):
- """
- Slightly extended XML-RPC dispatcher class suitable for embedding in
- django applications.
- """
- #TODO: Implement _marshaled_dispatch() and capture XML errors to
- # translate them to appropriate standardized fault codes. There
- # might be some spill to the view code to make this complete.
-
- #TODO: Implement and expose system.getCapabilities() and advertise
- # support for standardised fault codes.
- # See: http://tech.groups.yahoo.com/group/xml-rpc/message/2897
- def __init__(self):
- # it's a classic class, no super
- SimpleXMLRPCServer.SimpleXMLRPCDispatcher.__init__(self,
- allow_none=True)
- self.logger = logging.getLogger(__name__)
- self.logger.addHandler(_NullHandler())
-
- def _lookup_func(self, method):
- """
- Lookup implementation of method named `method`.
-
- Returns implementation of `method` or None if the method is not
- registered anywhere in the dispatcher.
-
- This new function is taken directly out of the base class
- implementation of _dispatch. The point is to be able to
- detect a situation where method is not known and return
- appropriate XML-RPC fault. With plain dispatcher it is
- not possible as the implementation raises a plain Exception
- to signal this error condition and capturing and interpreting
- arbitrary exceptions is flaky.
- """
- func = None
- try:
- # check to see if a matching function has been registered
- func = self.funcs[method]
- except KeyError:
- if self.instance is not None:
- # check for a _dispatch method
- if hasattr(self.instance, '_dispatch'):
- # FIXME: pyflakes, params is undefined
- return self.instance._dispatch(method, params)
- else:
- # call instance method directly
- try:
- func = SimpleXMLRPCServer.resolve_dotted_attribute(
- self.instance,
- method,
- self.allow_dotted_names
- )
- except AttributeError:
- pass
- return func
-
- def _dispatch(self, method, params):
- """
- Improved dispatch method from the base dispatcher.
-
- The primary improvement is exception handling:
- - xml-rpc faults are passed back to the caller
- - missing methods return standardized fault code (
- FaultCodes.ServerError.REQUESTED_METHOD_NOT_FOUND)
- - all other exceptions in the called method are translated
- to standardized internal xml-rpc fault code
- (FaultCodes.ServerError.INTERNAL_XML_RPC_ERROR). In
- addition such errors cause _report_incident() to be
- called. This allows to hook a notification mechanism for
- deployed servers where exceptions are, for example, mailed
- to the administrator.
- """
- func = self._lookup_func(method)
- if func is None:
- raise xmlrpclib.Fault(
- FaultCodes.ServerError.REQUESTED_METHOD_NOT_FOUND,
- "No such method: %r" % method)
- try:
- # TODO: check parameter types before calling
- return func(*params)
- except xmlrpclib.Fault:
- # Forward XML-RPC Faults to the client
- raise
- except:
- # Treat all other exceptions as internal errors
- # This prevents the clients from seeing internals
- exc_type, exc_value, exc_tb = sys.exc_info()
- incident_id = self._report_incident(method, params, exc_type, exc_value, exc_tb)
- string = ("Dashboard has encountered internal error. "
- "Incident ID is: %s" % (incident_id,))
- raise xmlrpclib.Fault(
- FaultCodes.ServerError.INTERNAL_XML_RPC_ERROR,
- string)
-
- def _report_incident(self, method, params, exc_type, exc_value, exc_tb):
- """
- Report an exception that happened
- """
- self.logger.exception("Internal error when dispatching "
- "XML-RPC method: %s%r", method, params)
- # TODO: store the exception somewhere and assign fault codes
- return None
-
- def system_methodSignature(self, method):
- if method.startswith("_"):
- return ""
- if self.instance is not None:
- func = getattr(self.instance, method, None)
- else:
- func = self.funcs.get(method)
- # When function is not known return empty string
- if func is None:
- return ""
- # When signature is not known return "undef"
- # See: http://xmlrpc-c.sourceforge.net/introspection.html
- return getattr(func, 'xml_rpc_signature', "undef")
-
-
-def xml_rpc_signature(*sig):
- """
- Small helper that attaches "xml_rpc_signature" attribute to the
- function. The attribute is a list of values that is then reported
- by system_methodSignature().
-
- This is a simplification of the XML-RPC spec that allows to attach a
- list of variants (like I may accept this set of arguments, or that
- set or that other one). This version has only one set of arguments.
-
- Note that it's a purely presentational argument for our
- implementation. Putting bogus values here won't spoil the day.
-
- The first element is the signature of the return type.
- """
- def decorator(func):
- func.xml_rpc_signature = sig
- return func
- return decorator
=== modified file 'dashboard_app/models.py'
@@ -26,6 +26,7 @@
import os
import simplejson
import traceback
+import contextlib
from django.contrib.auth.models import User
from django.contrib.contenttypes import generic
@@ -43,6 +44,7 @@
from dashboard_app.managers import BundleManager
from dashboard_app.repositories import RepositoryItem
from dashboard_app.repositories.data_report import DataReportRepository
+from dashboard_app.repositories.data_view import DataViewRepository
# Fix some django issues we ran into
from dashboard_app.patches import patch
@@ -364,6 +366,9 @@
def get_absolute_url(self):
return ("dashboard_app.views.bundle_detail", [self.bundle_stream.pathname, self.content_sha1])
+ def get_permalink(self):
+ return reverse("dashboard_app.views.redirect_to_bundle", args=[self.content_sha1])
+
def save(self, *args, **kwargs):
if self.content:
try:
@@ -749,6 +754,9 @@
self.bundle.content_sha1,
self.analyzer_assigned_uuid])
+ def get_permalink(self):
+ return reverse("dashboard_app.views.redirect_to_test_run", args=[self.analyzer_assigned_uuid])
+
def get_summary_results(self):
stats = self.test_results.values('result').annotate(
count=models.Count('result')).order_by()
@@ -932,6 +940,20 @@
def __unicode__(self):
return "Result {0}/{1}".format(self.test_run.analyzer_assigned_uuid, self.relative_index)
+ @models.permalink
+ def get_absolute_url(self):
+ return ("dashboard_app.views.test_result_detail", [
+ self.test_run.bundle.bundle_stream.pathname,
+ self.test_run.bundle.content_sha1,
+ self.test_run.analyzer_assigned_uuid,
+ self.relative_index,
+ ])
+
+ def get_permalink(self):
+ return reverse("dashboard_app.views.redirect_to_test_result",
+ args=[self.test_run.analyzer_assigned_uuid,
+ self.relative_index])
+
@property
def result_code(self):
"""
@@ -973,15 +995,6 @@
duration = property(_get_duration, _set_duration)
- @models.permalink
- def get_absolute_url(self):
- return ("dashboard_app.views.test_result_detail", [
- self.test_run.bundle.bundle_stream.pathname,
- self.test_run.bundle.content_sha1,
- self.test_run.analyzer_assigned_uuid,
- self.relative_index,
- ])
-
def related_attachment_available(self):
"""
Check if there is a log file attached to the test run that has
@@ -1001,6 +1014,105 @@
order_with_respect_to = 'test_run'
+class DataView(RepositoryItem):
+ """
+ Data view, a container for SQL query and optional arguments
+ """
+
+ repository = DataViewRepository()
+
+ def __init__(self, name, backend_queries, arguments, documentation, summary):
+ self.name = name
+ self.backend_queries = backend_queries
+ self.arguments = arguments
+ self.documentation = documentation
+ self.summary = summary
+
+ def __unicode__(self):
+ return self.name
+
+ def __repr__(self):
+ return "<DataView name=%r>" % (self.name,)
+
+ @models.permalink
+ def get_absolute_url(self):
+ return ("dashboard_app.views.data_view_detail", [self.name])
+
+ def _get_connection_backend_name(self, connection):
+ backend = str(type(connection))
+ if "sqlite" in backend:
+ return "sqlite"
+ elif "postgresql" in backend:
+ return "postgresql"
+ else:
+ return ""
+
+ def get_backend_specific_query(self, connection):
+ """
+ Return BackendSpecificQuery for the specified connection
+ """
+ sql_backend_name = self._get_connection_backend_name(connection)
+ try:
+ return self.backend_queries[sql_backend_name]
+ except KeyError:
+ return self.backend_queries.get(None, None)
+
+ def lookup_argument(self, name):
+ """
+ Return Argument with the specified name
+
+ Raises LookupError if the argument cannot be found
+ """
+ for argument in self.arguments:
+ if argument.name == name:
+ return argument
+ raise LookupError(name)
+
+ @classmethod
+ def get_connection(cls):
+ """
+ Get the appropriate connection for data views
+ """
+ from django.db import connection, connections
+ from django.db.utils import ConnectionDoesNotExist
+ try:
+ return connections['dataview']
+ except ConnectionDoesNotExist:
+ logging.warning("dataview-specific database connection not available, dataview query is NOT sandboxed")
+ return connection # NOTE: it's connection not connectionS (the default connection)
+
+ def __call__(self, connection, **arguments):
+ # Check if arguments have any bogus names
+ valid_arg_names = frozenset([argument.name for argument in self.arguments])
+ for arg_name in arguments:
+ if arg_name not in valid_arg_names:
+ raise TypeError("Data view %s has no argument %r" % (self.name, arg_name))
+ # Get the SQL template for our database connection
+ query = self.get_backend_specific_query(connection)
+ if query is None:
+ raise LookupError("Specified data view has no SQL implementation "
+ "for current database")
+ # Replace SQL aruments with django placeholders (connection agnostic)
+ template = query.sql_template
+ template = template.replace("%", "%%")
+ # template = template.replace("{", "{{").replace("}", "}}")
+ sql = template.format(
+ **dict([
+ (arg_name, "%s")
+ for arg_name in query.argument_list]))
+ # Construct argument list using defaults for missing values
+ sql_args = [
+ arguments.get(arg_name, self.lookup_argument(arg_name).default)
+ for arg_name in query.argument_list]
+ with contextlib.closing(connection.cursor()) as cursor:
+ # Execute the query with the specified arguments
+ cursor.execute(sql, sql_args)
+ # Get and return the results
+ rows = cursor.fetchall()
+ columns = cursor.description
+ return rows, columns
+
+
class DataReport(RepositoryItem):
"""
Data reports are small snippets of xml that define
@@ -1013,6 +1125,16 @@
self._html = None
self._data = kwargs
+ def __unicode__(self):
+ return self.title
+
+ def __repr__(self):
+ return "<DataReport name=%r>" % (self.name,)
+
+ @models.permalink
+ def get_absolute_url(self):
+ return ("dashboard_app.views.report_detail", [self.name])
+
def _get_raw_html(self):
pathname = os.path.join(self.base_path, self.path)
try:
@@ -1041,13 +1163,6 @@
self._html = template.render(context)
return self._html
- def __unicode__(self):
- return self.title
-
- @models.permalink
- def get_absolute_url(self):
- return ("dashboard_app.views.report_detail", [self.name])
-
@property
def title(self):
return self._data['title']
@@ -1067,3 +1182,90 @@
@property
def author(self):
return self._data.get('author')
+
+
+class ImageHealth(object):
+
+ def __init__(self, rootfs_type, hwpack_type):
+ self.rootfs_type = rootfs_type
+ self.hwpack_type = hwpack_type
+
+ @models.permalink
+ def get_absolute_url(self):
+ return ("dashboard_app.views.image_status_detail", [
+ self.rootfs_type, self.hwpack_type])
+
+ def get_tests(self):
+ return Test.objects.filter(test_runs__in=self.get_test_runs()).distinct()
+
+ def current_health_for_test(self, test):
+ test_run = self.get_current_test_run(test)
+ test_results = test_run.test_results
+ fail_count = test_results.filter(
+ result=TestResult.RESULT_FAIL).count()
+ pass_count = test_results.filter(
+ result=TestResult.RESULT_PASS).count()
+ total_count = test_results.count()
+ return {
+ "test_run": test_run,
+ "total_count": total_count,
+ "fail_count": fail_count,
+ "pass_count": pass_count,
+ "other_count": total_count - (fail_count + pass_count),
+ "fail_percent": fail_count * 100.0 / total_count if total_count > 0 else None,
+ }
+
+ def overall_health_for_test(self, test):
+ test_run_list = self.get_all_test_runs_for_test(test)
+ test_result_list = TestResult.objects.filter(test_run__in=test_run_list)
+ fail_result_list = test_result_list.filter(result=TestResult.RESULT_FAIL)
+ pass_result_list = test_result_list.filter(result=TestResult.RESULT_PASS)
+
+ total_count = test_result_list.count()
+ fail_count = fail_result_list.count()
+ pass_count = pass_result_list.count()
+ fail_percent = fail_count * 100.0 / total_count if total_count > 0 else None
+ return {
+ "total_count": total_count,
+ "total_run_count": test_run_list.count(),
+ "fail_count": fail_count,
+ "pass_count": pass_count,
+ "other_count": total_count - fail_count - pass_count,
+ "fail_percent": fail_percent,
+ }
+
+ def get_test_runs(self):
+ return TestRun.objects.filter(
+ bundle__bundle_stream__pathname="/anonymous/lava-daily/"
+ ).filter(
+ attributes__name='rootfs.type',
+ attributes__value=self.rootfs_type
+ ).filter(
+ attributes__name='hwpack.type',
+ attributes__value=self.hwpack_type)
+
+ def get_current_test_run(self, test):
+ return self.get_all_test_runs_for_test(test).order_by('-analyzer_assigned_date')[0]
+
+ def get_all_test_runs_for_test(self, test):
+ return self.get_test_runs().filter(test=test)
+
+ @classmethod
+ def get_rootfs_list(self):
+ rootfs_list = [
+ attr['value']
+ for attr in NamedAttribute.objects.filter(
+ name='rootfs.type').values('value').distinct()]
+ try:
+ rootfs_list.remove('android')
+ except ValueError:
+ pass
+ return rootfs_list
+
+ @classmethod
+ def get_hwpack_list(self):
+ hwpack_list = [
+ attr['value']
+ for attr in NamedAttribute.objects.filter(
+ name='hwpack.type').values('value').distinct()]
+ return hwpack_list
=== modified file 'dashboard_app/repositories/__init__.py'
@@ -32,7 +32,8 @@
"""
def __new__(mcls, name, bases, namespace):
- cls = super(RepositoryItemMeta, mcls).__new__(mcls, name, bases, namespace)
+ cls = super(RepositoryItemMeta, mcls).__new__(
+ mcls, name, bases, namespace)
if "repository" in namespace:
repo = cls.repository
repo.item_cls = cls
@@ -46,7 +47,7 @@
Each repository item is loaded from a XML file.
"""
- __metaclass__ = RepositoryItemMeta
+ __metaclass__ = RepositoryItemMeta
_base_path = None
@@ -57,7 +58,6 @@
def base_path(self):
return self._base_path
-
class DoesNotExist(Exception):
pass
@@ -89,12 +89,12 @@
def all(self):
return self
-
+
def get(self, **kwargs):
query = self.filter(**kwargs)
if len(query) == 1:
return query[0]
- if not query:
+ if not query:
raise self.model.DoesNotExist()
else:
raise self.model.MultipleValuesReturned()
@@ -127,7 +127,7 @@
__metaclass__ = abc.ABCMeta
def __init__(self):
- self.item_cls = None # later patched by RepositoryItemMeta
+ self.item_cls = None # later patched by RepositoryItemMeta
self._items = []
self._did_load = False
@@ -154,9 +154,10 @@
try:
items = os.listdir(directory)
except (OSError, IOError) as exc:
- logging.exception("Unable to enumreate directory: %s: %s", directory, exc)
+ logging.exception("Unable to enumreate directory: %s: %s",
+ directory, exc)
else:
- for name in items:
+ for name in items:
pathname = os.path.join(directory, name)
if os.path.isfile(pathname) and pathname.endswith(".xml"):
self.load_from_file(pathname)
@@ -176,7 +177,8 @@
item._load_from_external_representation(pathname)
self._items.append(item)
except Exception as exc:
- logging.exception("Unable to load object into repository %s: %s", pathname, exc)
+ logging.exception("Unable to load object into repository %s: %s",
+ pathname, exc)
@abc.abstractproperty
def settings_variable(self):
=== added file 'dashboard_app/repositories/common.py'
@@ -0,0 +1,47 @@
+# Copyright (C) 2011 Linaro Limited
+#
+# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org>
+#
+# This file is part of Launch Control.
+#
+# Launch Control is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License version 3
+# as published by the Free Software Foundation
+#
+# Launch Control is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Launch Control. If not, see <http://www.gnu.org/licenses/>.
+
+
+from xml.sax.handler import ContentHandler
+import re
+
+
+class BaseContentHandler(ContentHandler):
+
+ def _end_text(self):
+ """
+ Stop collecting text and produce a stripped string with de-duplicated
+ whitespace
+ """
+ full_text = re.sub("\s+", " ", u''.join(self._text)).strip()
+ self.text = None
+ return full_text
+
+ def _start_text(self):
+ """
+ Start collecting text
+ """
+ self._text = []
+
+ def characters(self, content):
+ if isinstance(self._text, list):
+ self._text.append(content)
+
+ def startDocument(self):
+ # Text can be None or a [] that accumulates all detected text
+ self._text = None
=== modified file 'dashboard_app/repositories/data_report.py'
@@ -18,34 +18,19 @@
from xml.sax import parseString
-from xml.sax.handler import ContentHandler
-import re
from dashboard_app.repositories import Repository, Undefined, Object
-
-
-class _DataReportHandler(ContentHandler):
+from dashboard_app.repositories.common import BaseContentHandler
+
+
+class _DataReportHandler(BaseContentHandler):
"""
- ContentHandler subclass for parsing DataView documents
+ ContentHandler subclass for parsing DataReport documents
"""
- def _end_text(self):
- """
- Stop collecting text and produce a stripped string with deduplicated whitespace
- """
- full_text = re.sub("\s+", " ", u''.join(self._text)).strip()
- self.text = None
- return full_text
-
- def _start_text(self):
- """
- Start collecting text
- """
- self._text = []
-
def startDocument(self):
- # Text can be None or a [] that accumulates all detected text
- self._text = None
+ # Classic-classes
+ BaseContentHandler.startDocument(self)
# Data report object
self.obj = Object()
@@ -70,10 +55,6 @@
self.obj.title = self._end_text()
elif name == "path":
self.obj.path = self._end_text()
-
- def characters(self, content):
- if isinstance(self._text, list):
- self._text.append(content)
class DataReportRepository(Repository):
=== added file 'dashboard_app/repositories/data_view.py'
@@ -0,0 +1,133 @@
+# Copyright (C) 2011 Linaro Limited
+#
+# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org>
+#
+# This file is part of Launch Control.
+#
+# Launch Control is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License version 3
+# as published by the Free Software Foundation
+#
+# Launch Control is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Launch Control. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+DataViews: Encapsulated SQL query definitions.
+
+Implementation of the following launchpad blueprint:
+https://blueprints.launchpad.net/launch-control/+spec/other-linaro-n-data-views-for-launch-control
+"""
+
+
+from xml.sax import parseString
+
+from dashboard_app.repositories import Repository, Undefined, Object
+from dashboard_app.repositories.common import BaseContentHandler
+
+
+class _DataViewHandler(BaseContentHandler):
+
+ """
+ ContentHandler subclass for parsing DataView documents
+ """
+
+ def startDocument(self):
+ # Classic-classes
+ BaseContentHandler.startDocument(self)
+ # Data view object
+ self.obj = Object()
+ # Set default values
+ self.obj.name = Undefined
+ self.obj.backend_queries = {}
+ self.obj.arguments = []
+ self.obj.documentation = None
+ self.obj.summary = None
+ # Internal variables
+ self._current_backend_query = None
+
+ def endDocument(self):
+ # TODO: check if we have anything defined
+ if self.obj.name is Undefined:
+ raise ValueError("No data view definition found")
+
+ def startElement(self, name, attrs):
+ if name == "data-view":
+ self.obj.name = attrs["name"]
+ elif name == "summary" or name == "documentation":
+ self._start_text()
+ elif name == "sql":
+ self._start_text()
+ self._current_backend_query = BackendSpecificQuery(
+ attrs.get("backend"), None, [])
+ self.obj.backend_queries[
+ self._current_backend_query.backend] = self._current_backend_query
+ elif name == "value":
+ if "name" not in attrs:
+ raise ValueError("<value> requires attribute 'name'")
+ self._text.append("{" + attrs["name"] + "}")
+ self._current_backend_query.argument_list.append(attrs["name"])
+ elif name == "argument":
+ if "name" not in attrs:
+ raise ValueError("<argument> requires attribute 'name'")
+ if "type" not in attrs:
+ raise ValueError("<argument> requires attribute 'type'")
+ if attrs["type"] not in ("string", "number", "boolean", "timestamp"):
+ raise ValueError("invalid value for argument 'type' on <argument>")
+ argument = Argument(name=attrs["name"], type=attrs["type"],
+ default=attrs.get("default", None),
+ help=attrs.get("help", None))
+ self.obj.arguments.append(argument)
+
+ def endElement(self, name):
+ if name == "sql":
+ self._current_backend_query.sql_template = self._end_text()
+ self._current_backend_query = None
+ elif name == "documentation":
+ self.obj.documentation = self._end_text()
+ elif name == "summary":
+ self.obj.summary = self._end_text()
+
+
+class Argument(object):
+ """
+ Data view argument for SQL prepared statements
+ """
+
+ def __init__(self, name, type, default, help):
+ self.name = name
+ self.type = type
+ self.default = default
+ self.help = help
+
+
+class BackendSpecificQuery(object):
+ """
+ Backend-specific query and argument list
+ """
+
+ def __init__(self, backend, sql_template, argument_list):
+ self.backend = backend
+ self.sql_template = sql_template
+ self.argument_list = argument_list
+
+
+class DataViewRepository(Repository):
+
+ @property
+ def settings_variable(self):
+ return "DATAVIEW_DIRS"
+
+ def load_from_xml_string(self, text):
+ handler = _DataViewHandler()
+ parseString(text, handler)
+ return self.item_cls(**handler.obj.__dict__)
+
+
+__all__ = [
+ "DataViewRepository",
+]
=== modified file 'dashboard_app/static/js/jquery.dashboard.js'
@@ -76,7 +76,7 @@
}
if (column.name == "UUID") {
/* This is a bit hacky but will work for now */
- cell_link = _url + ".." + "/test-runs/" + cell + "/";
+ cell_link = _url + ".." + "/permalink/test-run/" + cell + "/";
}
html += "<td>";
if (cell_link) {
=== modified file 'dashboard_app/templates/dashboard_app/_extension_navigation.html'
@@ -1,6 +1,8 @@
{% load i18n %}
<div id="lava-server-extension-navigation" class="lava-server-sub-toolbar">
<ul>
+ <li><a href="{% url dashboard_app.views.image_status_list %}"
+ >{% trans "Image Status" %}</a></li>
<li><a href="{% url dashboard_app.views.bundle_stream_list %}"
>{% trans "Bundle Streams" %}</a></li>
<li><a href="{% url dashboard_app.views.test_list %}"
=== modified file 'dashboard_app/templates/dashboard_app/bundle_detail.html'
@@ -11,6 +11,20 @@
{% block content %}
+<div class="ui-widget">
+ <div class="ui-state-highlight ui-corner-all" style="margin-top: 20px; padding: 0.7em">
+ <span
+ class="ui-icon ui-icon-info"
+ style="float: left; margin-right: 0.3em;"></span>
+ <strong>{% trans "Note:" %}</strong>
+ {% blocktrans %}
+ You can navigate to this bundle, regardless of the bundle stream it is
+ located in, by using this
+ {% endblocktrans %}
+ <a href="{{ bundle.get_permalink }}" >{% trans "permalink" %}</a>
+ </div>
+</div>
+<br/>
<script type="text/javascript">
$(document).ready(function() {
$("#tabs").tabs({
=== modified file 'dashboard_app/templates/dashboard_app/data_view_list.html'
@@ -34,7 +34,7 @@
<tbody>
{% for data_view in data_view_list %}
<tr>
- <td><a href="{% url dashboard_app.views.data_view_detail data_view.name %}">{{ data_view.name }}</a></td>
+ <td><a href="{{ data_view.get_absolute_url }}">{{ data_view.name }}</a></td>
<td>{{ data_view.summary }}</td>
</tr>
{% endfor %}
=== added file 'dashboard_app/templates/dashboard_app/image_status_detail.html'
@@ -0,0 +1,71 @@
+{% extends "dashboard_app/_content.html" %}
+{% load call %}
+
+{% block content %}
+<script type="text/javascript" charset="utf-8">
+ $(document).ready(function() {
+ oTable = $('#tests').dataTable({
+ "bJQueryUI": true,
+ "bPaginate": false,
+ });
+ });
+</script>
+<table class="demo_jui display" id="tests">
+ <thead>
+ <tr>
+ <th rowspan="2">Test</th>
+ <th colspan="5">Totals</th>
+ <th colspan="4" style="border-left: 1px solid black">Most Recent Test Run</th>
+ <th rowspan="2" style="width: 30%">Description</th>
+ </th>
+ <tr>
+ <th>PASS</th>
+ <th>FAIL</th>
+ <th>FAIL rate</th>
+ <th>Test Runs</th>
+ <th>Test Results</th>
+ <th>PASS</th>
+ <th>FAIL</th>
+ <th>FAIL rate</th>
+ <th>Test Results</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for test in image_health.get_tests %}
+ {% call image_health.current_health_for_test test as current_test_health %}
+ {% call image_health.overall_health_for_test test as overall_test_health %}
+ <tr
+ {% if current_test_health.fail_count > 0 %}
+ {% if current_test_health.pass_count == 0 %}
+ style="background-color: rgba(255, 0, 0, 0.5)"
+ {% else %}
+ style="background-color: rgba(255, 165, 0, 0.5)"
+ {% endif %}
+ {% else %}
+ {% if current_test_health.pass_count > 0 %}
+ style="background-color: rgba(173, 255, 47, 0.5)"
+ {% endif %}
+ {% endif %}
+ >
+ <td>{{ test.test_id }}</td>
+ <td>{{ overall_test_health.pass_count }}</td>
+ <td>{{ overall_test_health.fail_count }}</td>
+ <td>{{ overall_test_health.fail_percent|default_if_none:0|floatformat }}%</td>
+ <td><a
+ href="{% url dashboard_app.views.image_test_history image_health.rootfs_type image_health.hwpack_type test.test_id %}"
+ >{{ overall_test_health.total_run_count }}</a></td>
+ <td>{{ overall_test_health.total_count }}</td>
+ <td>{{ current_test_health.pass_count|default:0 }}</td>
+ <td>{{ current_test_health.fail_count|default:0 }}</td>
+ <td>{{ current_test_health.fail_percent|default_if_none:0|floatformat }}%</td>
+ <td><a
+ href="{{ current_test_health.test_run.get_absolute_url }}"
+ >{{ current_test_health.total_count|default:0 }}</a></td>
+ <td>{{ test.name|default:"<em>not set</em>" }}</td>
+ </tr>
+ {% endcall %}
+ {% endcall %}
+ {% endfor %}
+ </tbody>
+</table>
+{% endblock %}
=== added file 'dashboard_app/templates/dashboard_app/image_status_list.html'
@@ -0,0 +1,38 @@
+{% extends "dashboard_app/_content.html" %}
+{% load call %}
+
+
+{% block content %}
+<script type="text/javascript" charset="utf-8">
+ $(document).ready(function() {
+ oTable = $('#LEBs').dataTable({
+ "bJQueryUI": true,
+ "bPaginate": false,
+ });
+ });
+</script>
+<table class="demo_jui display" id="LEBs">
+ <thead>
+ <tr>
+ <th></th>
+ {% for hwpack in hwpack_list %}
+ <th>{{ hwpack }}</th>
+ {% endfor %}
+ </tr>
+ </thead>
+ <tbody>
+ {% for rootfs in rootfs_list %}
+ <tr>
+ <th>{{ rootfs }}</th>
+ {% for hwpack in hwpack_list %}
+ <td>
+ {% call ImageHealth rootfs hwpack as image_health %}
+ <a href="{{ image_health.get_absolute_url }}">{{ image_health.get_test_runs.count }} test runs</a>
+ {% endcall %}
+ </td>
+ {% endfor %}
+ </tr>
+ {% endfor %}
+ </tbody>
+</table>
+{% endblock %}
=== added file 'dashboard_app/templates/dashboard_app/image_test_history.html'
@@ -0,0 +1,15 @@
+{% extends "dashboard_app/_content.html" %}
+
+
+{% block content %}
+<script type="text/javascript">
+ $(document).ready(function() {
+ $('#test_runs').dataTable({
+ bJQueryUI: true,
+ sPaginationType: "full_numbers",
+ aaSorting: [[0, "desc"]],
+ });
+ });
+</script>
+{% include "dashboard_app/_test_run_list_table.html" %}
+{% endblock %}
=== modified file 'dashboard_app/templates/dashboard_app/index.html'
@@ -1,10 +1,58 @@
{% extends "dashboard_app/_content.html" %}
+
{% block content %}
-<h1>TODO</h1>
-<ul>
- <li>Briefly mention key dashboard features</li>
- <li>Link to readthedocs dashboard manual</li>
- <li>Add sensible dashboard index page (recent/interesting stuff)</li>
+<h1>Welcome</h1>
+<p>The <em>Validation Dashboard</em> is your window to
+test results, regardless of how your run your tests you
+can upload the results here and analyze them with simple
+built-in reports as well as arbitrary custom reports and
+data mining queries.</p>
+
+<h2>Key Features</h2>
+<ul>
+ <li>Online repository of test results, with simple to use, web APIs and
+ command line tools for uploading test results.</li>
+ <li>Test results are packaged in documents (bundles) that you can easily sync
+ across systems, model is similar to the one used by git</li>
+ <li>Test results can refer to software and hardware context so that you know
+ exactly what software and hardware combination fails</li>
+ <li>Data mining and reporting allows users to create custom tailored reports
+ based on the data in the system</li>
+ <li>Distributed work-flow model, with some data privacy out of the box, fully
+ private installation can be deployed in minutes.</li>
+</ul>
+
+<h2>Documentation & Get Started</h2>
+<p>To get started quickly follow the link below, if you feel that an important
+content is missing please <a
+ href="https://bugs.launchpad.net/lava-dashboard/+filebug"
+ >report a bug</a> or <a
+ href="https://answers.launchpad.net/lava-dashboard/+addquestion"
+ >ask a question</a>. Please make sure to report dashboard version (you are
+currently using version {{lava.extensions.as_mapping.dashboard_app.version}})</p>
+<p>All documentation is hosted on <a
+ href="http://readthedocs.org/docs/lava-dashboard/en/latest/">ReadTheDocs.org</a>.</p>
+
+<h3>Developers</h3>
+<ul>
+ <li>How to put test results of my test suite into the Dashboard?</li>
+ <li>How to integrate my testing toolkit with the Dashboard?</li>
+ <li>How to allow users of my application to send anonymous qualitative and
+ quantitative (tests and benchmarks) data from their systems?</li>
+</ul>
+
+<h3>Managers</h3>
+<ul>
+ <li>What kind of reporting features are available out of the box?</li>
+ <li>How to create additional reports?</li>
+ <li>What kind of data is available in the system</li>
+</ul>
+
+<h3>System Administrators</h3>
+<ul>
+ <li>System requirements</li>
+ <li>How to deploy or upgrade the dashboard?</li>
+ <li>How to backup and restore the data</li>
</ul>
{% endblock %}
=== modified file 'dashboard_app/templates/dashboard_app/report_detail.html'
@@ -48,3 +48,12 @@
});
</script>
{% endblock %}
+
+{% block body %}
+{% if is_iframe %}
+{{ report.get_html|safe}}
+{% else %}
+{{ block.super }}
+{% endif %}
+{% endblock %}
+
=== modified file 'dashboard_app/templates/dashboard_app/test_result_detail.html'
@@ -4,15 +4,18 @@
{% block sidebar %}
-<h3>{% trans "Hints" %}</h3>
-<p class="hint">
-{% blocktrans %}
-This is all the information that launch control has about this result. Log
-analyzers can provide additional information by scrubbing it from the log file.
-Information that is global to a test run can be attached to test run attributes
-instead.
-{% endblocktrans %}
-</p>
+<div class="ui-widget">
+ <div class="ui-state-highlight ui-corner-all" style="margin-top: 20px; padding: 0.7em">
+ <span
+ class="ui-icon ui-icon-info"
+ style="float: left; margin-right: 0.3em;"></span>
+ <strong>Note:</strong> This is all the information that the dashboard has
+ about this result. Log analyzers can provide additional information by
+ scrubbing it from the log file. Information that is global to a test run
+ can be attached to test run attributes instead.
+ </div>
+</div>
+<br/>
{% if test_result.test_run.test_results.count > 1 %}
<h3>Other results</h3>
<p class="hint">Results from the same test run are available here</p>
@@ -42,11 +45,25 @@
{% block content %}
-<h2>{% trans "Detailed information about test result" %}</h2>
-<p>{% trans "Launch Control has the following information about this test result" %}</p>
+<h2>{% trans "Test Result Details" %}</h2>
<dl>
<dt>{% trans "Result ID:" %}</dt>
- <dd>{{ test_result }}</dd>
+ <dd>
+ {{ test_result }}
+ <div class="ui-widget" style="width: 30em">
+ <div class="ui-state-highlight ui-corner-all" style="padding: 0.7em">
+ <span
+ class="ui-icon ui-icon-info"
+ style="float: left; margin-right: 0.3em;"></span>
+ <strong>{% trans "Note:" %}</strong>
+ {% blocktrans %}
+ You can navigate to this test result, regardless of the bundle stream it is
+ located in, by using this
+ {% endblocktrans %}
+ <a href="{{ test_result.get_permalink }}" >{% trans "permalink" %}</a>
+ </div>
+ </div>
+ </dd>
<dt>{% trans "Test case:" %}</dt>
<dd>
{% if test_result.test_case %}
@@ -54,7 +71,7 @@
{% else %}
<i>{% trans "unknown test case" %}</i>
{% endif %}
- {% trans "from test" %} <b>{{ test_result.test_run.test }}</b>
+ {% trans "from test" %} <b><a href="{{ test_result.test_run.test.get_absolute_url }}">{{ test_result.test_run.test }}</a></b>
</dd>
<dt>{% trans "Test outcome:" %}</dt>
<dd>{{ test_result.get_result_display }}</dd>
=== modified file 'dashboard_app/templates/dashboard_app/test_run_detail.html'
@@ -43,9 +43,9 @@
{% block sidebar %}
<dl>
<dt>{% trans "Test Run UUID" %}</dt>
- <dd>{{ test_run.analyzer_assigned_uuid }}</dd>
+ <dd>{{ test_run.analyzer_assigned_uuid }} <a href="{% url dashboard_app.views.redirect_to_test_run test_run.analyzer_assigned_uuid %}">{% trans "permalink" %}</a></dd>
<dt>{% trans "Test Name" %}</dt>
- <dd>{{ test_run.test }}</dd>
+ <dd><a href="{{ test_run.test.get_absolute_url }}">{{ test_run.test }}</a></dd>
<dt>{% trans "OS Distribution" %}</dt>
<dd>{{ test_run.sw_image_desc|default:"<i>Unspecified</i>" }}</dd>
<dt>{% trans "Bundle SHA1" %}</dt>
=== added file 'dashboard_app/templatetags/call.py'
@@ -0,0 +1,64 @@
+from django import template
+
+
+register = template.Library()
+
+
+class CallNode(template.Node):
+ def __init__(self, func, args, name, nodelist):
+ self.func = func
+ self.args = args
+ self.name = name
+ self.nodelist = nodelist
+
+ def __repr__(self):
+ return "<CallNode>"
+
+ def _lookup_func(self, context):
+ parts = self.func.split('.')
+ current = context[parts[0]]
+ for part in parts[1:]:
+ if part.startswith("_"):
+ raise ValueError(
+ "Function cannot traverse private implementation attributes")
+ current = getattr(current, part)
+ return current
+
+ def render(self, context):
+ try:
+ func = self._lookup_func(context)
+ values = [template.Variable(arg).resolve(context) for arg in self.args]
+ context.push()
+ context[self.name] = func(*values)
+ output = self.nodelist.render(context)
+ context.pop()
+ return output
+ except Exception as ex:
+ import logging
+ logging.exception("Unable to call %s with %r: %s",
+ self.func, self.args, ex)
+ raise
+
+def do_call(parser, token):
+ """
+ Adds a value to the context (inside of this block) for caching and easy
+ access.
+
+ For example::
+
+ {% call func 1 2 3 as result %}
+ {{ result }}
+ {% endcall %}
+ """
+ bits = list(token.split_contents())
+ if len(bits) < 2 or bits[-2] != "as":
+ raise template.TemplateSyntaxError(
+ "%r expected format is 'call func [args] [as name]'" % bits[0])
+ func = bits[1]
+ args = bits[2:-2]
+ name = bits[-1]
+ nodelist = parser.parse(('endcall',))
+ parser.delete_first_token()
+ return CallNode(func, args, name, nodelist)
+
+do_call = register.tag('call', do_call)
=== modified file 'dashboard_app/tests/__init__.py'
@@ -24,11 +24,11 @@
'other.deserialization',
'other.login',
'other.test_client',
- 'other.xml_rpc',
'regressions.LP658917',
'views.bundle_stream_list_view',
'views.test_run_detail_view',
'views.test_run_list_view',
+ 'views.redirects',
]
def load_tests_from_submodules(_locals):
=== modified file 'dashboard_app/tests/other/dataview.py'
@@ -16,12 +16,10 @@
# You should have received a copy of the GNU Affero General Public License
# along with Launch Control. If not, see <http://www.gnu.org/licenses/>.
-import unittest
-
from mocker import Mocker, expect
from testtools import TestCase
-from dashboard_app.dataview import DataView
+from dashboard_app.models import DataView
class DataViewHandlerTests(TestCase):
@@ -45,7 +43,7 @@
def setUp(self):
super(DataViewHandlerTests, self).setUp()
- self.dataview = DataView.load_from_xml(self.text)
+ self.dataview = DataView.repository.load_from_xml_string(self.text)
def test_name_parsed_ok(self):
self.assertEqual(self.dataview.name, "foo")
@@ -97,7 +95,6 @@
Test for DataView.get_connection()
"""
# Mock connections['dataview'] to return special connection
- from django.db.utils import ConnectionDoesNotExist
mocker = Mocker()
connections = mocker.replace("django.db.connections")
special_connection = mocker.mock()
=== removed file 'dashboard_app/tests/other/xml_rpc.py'
@@ -1,132 +0,0 @@
-# Copyright (C) 2010 Linaro Limited
-#
-# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org>
-#
-# This file is part of Launch Control.
-#
-# Launch Control is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License version 3
-# as published by the Free Software Foundation
-#
-# Launch Control is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with Launch Control. If not, see <http://www.gnu.org/licenses/>.
-
-"""
-Unit tests of the Dashboard application
-"""
-import xmlrpclib
-
-from django_testscenarios.ubertest import TestCaseWithScenarios
-
-from dashboard_app.dispatcher import (
- DjangoXMLRPCDispatcher,
- FaultCodes,
- xml_rpc_signature,
- )
-
-
-class TestAPI(object):
- """
- Test API that gets exposed by the dispatcher for test runs.
- """
-
- @xml_rpc_signature()
- def ping(self):
- """
- Return "pong" message
- """
- return "pong"
-
- def echo(self, arg):
- """
- Return the argument back to the caller
- """
- return arg
-
- def boom(self, code, string):
- """
- Raise a Fault exception with the specified code and string
- """
- raise xmlrpclib.Fault(code, string)
-
- def internal_boom(self):
- """
- Raise a regular python exception (this should be hidden behind
- an internal error fault)
- """
- raise Exception("internal boom")
-
-
-class DjangoXMLRPCDispatcherTestCase(TestCaseWithScenarios):
-
- def setUp(self):
- super(DjangoXMLRPCDispatcherTestCase, self).setUp()
- self.dispatcher = DjangoXMLRPCDispatcher()
- self.dispatcher.register_instance(TestAPI())
-
- def xml_rpc_call(self, method, *args):
- """
- Perform XML-RPC call on our internal dispatcher instance
-
- This calls the method just like we would have normally from our view.
- All arguments are marshaled and un-marshaled. XML-RPC fault exceptions
- are raised like normal python exceptions (by xmlrpclib.loads)
- """
- request = xmlrpclib.dumps(tuple(args), methodname=method)
- response = self.dispatcher._marshaled_dispatch(request)
- # This returns return value wrapped in a tuple and method name
- # (which we don't have here as this is a response message).
- return xmlrpclib.loads(response)[0][0]
-
-
-class DjangoXMLRPCDispatcherTests(DjangoXMLRPCDispatcherTestCase):
-
- def test_standard_fault_code_for_missing_method(self):
- try:
- self.xml_rpc_call("method_that_hopefully_does_not_exist")
- except xmlrpclib.Fault as ex:
- self.assertEqual(
- ex.faultCode,
- FaultCodes.ServerError.REQUESTED_METHOD_NOT_FOUND)
- else:
- self.fail("Calling missing method did not raise an exception")
-
- def test_ping(self):
- retval = self.xml_rpc_call("ping")
- self.assertEqual(retval, "pong")
-
- def test_echo(self):
- self.assertEqual(self.xml_rpc_call("echo", 1), 1)
- self.assertEqual(self.xml_rpc_call("echo", "string"), "string")
- self.assertEqual(self.xml_rpc_call("echo", 1.5), 1.5)
-
- def test_boom(self):
- self.assertRaises(xmlrpclib.Fault,
- self.xml_rpc_call, "boom", 1, "str")
-
-
-class DjangoXMLRPCDispatcherFaultCodeTests(DjangoXMLRPCDispatcherTestCase):
-
- scenarios = [
- ('method_not_found', {
- 'method': "method_that_hopefully_does_not_exist",
- 'faultCode': FaultCodes.ServerError.REQUESTED_METHOD_NOT_FOUND,
- }),
- ('internal_error', {
- 'method': "internal_boom",
- 'faultCode': FaultCodes.ServerError.INTERNAL_XML_RPC_ERROR,
- }),
- ]
-
- def test_standard_fault_codes(self):
- try:
- self.xml_rpc_call(self.method)
- except xmlrpclib.Fault as ex:
- self.assertEqual(ex.faultCode, self.faultCode)
- else:
- self.fail("Exception not raised")
=== modified file 'dashboard_app/tests/utils.py'
@@ -1,7 +1,6 @@
"""
Django-specific test utilities
"""
-import os
import xmlrpclib
from django.conf import settings
=== added file 'dashboard_app/tests/views/redirects.py'
@@ -0,0 +1,75 @@
+# Copyright (C) 2010 Linaro Limited
+#
+# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org>
+#
+# This file is part of Launch Control.
+#
+# Launch Control is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License version 3
+# as published by the Free Software Foundation
+#
+# Launch Control is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Launch Control. If not, see <http://www.gnu.org/licenses/>.
+
+from django.core.urlresolvers import reverse
+from django_testscenarios.ubertest import TestCase
+
+from dashboard_app.tests import fixtures
+
+
+class RedirectTests(TestCase):
+
+ _PATHNAME = "/anonymous/"
+ _BUNDLE_TEXT = """
+{
+ "test_runs": [
+ {
+ "test_results": [
+ {
+ "test_case_id": "test-case-0",
+ "result": "pass"
+ }
+ ],
+ "analyzer_assigned_date": "2010-10-15T22:04:46Z",
+ "time_check_performed": false,
+ "analyzer_assigned_uuid": "00000000-0000-0000-0000-000000000001",
+ "test_id": "examples"
+ }
+ ],
+ "format": "Dashboard Bundle Format 1.0"
+}
+ """
+ _BUNDLE_NAME = "whatever.json"
+
+ def setUp(self):
+ super(RedirectTests, self).setUp()
+ self.bundle = fixtures.create_bundle(self._PATHNAME, self._BUNDLE_TEXT, self._BUNDLE_NAME)
+ self.bundle.deserialize()
+ self.assertTrue(self.bundle.is_deserialized)
+
+ def test_bundle_permalink(self):
+ response = self.client.get(
+ reverse("dashboard_app.views.redirect_to_bundle",
+ args=(self.bundle.content_sha1, )))
+ self.assertRedirects(response, self.bundle.get_absolute_url())
+
+ def test_test_run_permalink(self):
+ test_run = self.bundle.test_runs.all()[0]
+ response = self.client.get(
+ reverse("dashboard_app.views.redirect_to_test_run",
+ args=(test_run.analyzer_assigned_uuid, )))
+ self.assertRedirects(response, test_run.get_absolute_url())
+
+ def test_test_result_permalink(self):
+ test_run = self.bundle.test_runs.all()[0]
+ test_result = test_run.test_results.all()[0]
+ response = self.client.get(
+ reverse("dashboard_app.views.redirect_to_test_result",
+ args=(test_run.analyzer_assigned_uuid,
+ test_result.relative_index)))
+ self.assertRedirects(response, test_result.get_absolute_url())
=== modified file 'dashboard_app/tests/views/test_run_detail_view.py'
@@ -20,7 +20,6 @@
from django_testscenarios.ubertest import (TestCase, TestCaseWithScenarios)
from dashboard_app.models import BundleStream, TestRun
from django.contrib.auth.models import (User, Group)
-from django.core.urlresolvers import reverse
from dashboard_app.tests.utils import TestClient
=== modified file 'dashboard_app/urls.py'
@@ -55,4 +55,10 @@
url(r'^streams(?P<pathname>/[a-zA-Z0-9/_-]+)bundles/(?P<content_sha1>[0-9a-z]+)/(?P<analyzer_assigned_uuid>[a-zA-Z0-9-]+)/hardware-context/$', 'test_run_hardware_context'),
url(r'^streams(?P<pathname>/[a-zA-Z0-9/_-]+)bundles/(?P<content_sha1>[0-9a-z]+)/(?P<analyzer_assigned_uuid>[a-zA-Z0-9-]+)/software-context/$', 'test_run_software_context'),
url(r'^streams(?P<pathname>/[a-zA-Z0-9/_-]+)test-runs/$', 'test_run_list'),
+ url(r'^permalink/test-run/(?P<analyzer_assigned_uuid>[a-zA-Z0-9-]+)/$', 'redirect_to_test_run'),
+ url(r'^permalink/test-result/(?P<analyzer_assigned_uuid>[a-zA-Z0-9-]+)/(?P<relative_index>[0-9]+)/$', 'redirect_to_test_result'),
+ url(r'^permalink/bundle/(?P<content_sha1>[0-9a-z]+)/$', 'redirect_to_bundle'),
+ url(r'^image_status/$', 'image_status_list'),
+ url(r'^image_status/(?P<rootfs_type>[a-zA-Z0-9_-]+)\+(?P<hwpack_type>[a-zA-Z0-9_-]+)/$', 'image_status_detail'),
+ url(r'^image_status/(?P<rootfs_type>[a-zA-Z0-9_-]+)\+(?P<hwpack_type>[a-zA-Z0-9_-]+)/test-history/(?P<test_id>[^/]+)/$', 'image_test_history'),
)
=== modified file 'dashboard_app/views.py'
@@ -20,25 +20,21 @@
Views for the Dashboard application
"""
-from django.contrib.auth.decorators import login_required
-from django.contrib.csrf.middleware import csrf_exempt
-from django.contrib.sites.models import Site
from django.db.models.manager import Manager
from django.db.models.query import QuerySet
-from django.http import (HttpResponse, Http404)
-from django.shortcuts import render_to_response
+from django.http import Http404
+from django.shortcuts import render_to_response, redirect, get_object_or_404
from django.template import RequestContext
from django.views.generic.list_detail import object_list, object_detail
-from dashboard_app.dataview import DataView, DataViewRepository
-from dashboard_app.dispatcher import DjangoXMLRPCDispatcher
from dashboard_app.models import (
Attachment,
Bundle,
BundleStream,
DataReport,
+ DataView,
+ ImageHealth,
Test,
- TestCase,
TestResult,
TestRun,
)
@@ -276,7 +272,7 @@
@BreadCrumb(
- "Result {relative_index}",
+ "Details of result {relative_index}",
parent=test_run_detail,
needs=['pathname', 'content_sha1', 'analyzer_assigned_uuid', 'relative_index'])
def test_result_detail(request, pathname, content_sha1, analyzer_assigned_uuid, relative_index):
@@ -384,31 +380,39 @@
raise Http404('No report matches given name.')
return render_to_response(
"dashboard_app/report_detail.html", {
- 'bread_crumb_trail': BreadCrumbTrail.leading_to(report_detail, name=report.name, title=report.title),
+ "is_iframe": request.GET.get("iframe") == "yes",
+ 'bread_crumb_trail': BreadCrumbTrail.leading_to(
+ report_detail,
+ name=report.name,
+ title=report.title),
"report": report,
}, RequestContext(request))
@BreadCrumb("Data views", parent=index)
def data_view_list(request):
- repo = DataViewRepository.get_instance()
return render_to_response(
"dashboard_app/data_view_list.html", {
'bread_crumb_trail': BreadCrumbTrail.leading_to(data_view_list),
- "data_view_list": repo.data_views
+ "data_view_list": DataView.repository.all(),
}, RequestContext(request))
-@BreadCrumb("Details of {name}", parent=data_view_list, needs=['name'])
+@BreadCrumb(
+ "Details of {name}",
+ parent=data_view_list,
+ needs=['name'])
def data_view_detail(request, name):
- repo = DataViewRepository.get_instance()
try:
- data_view = repo[name]
- except KeyError:
+ data_view = DataView.repository.get(name=name)
+ except DataView.DoesNotExist:
raise Http404('No data view matches the given query.')
return render_to_response(
"dashboard_app/data_view_detail.html", {
- 'bread_crumb_trail': BreadCrumbTrail.leading_to(data_view_detail, name=data_view.name, summary=data_view.summary),
+ 'bread_crumb_trail': BreadCrumbTrail.leading_to(
+ data_view_detail,
+ name=data_view.name,
+ summary=data_view.summary),
"data_view": data_view
}, RequestContext(request))
@@ -437,3 +441,80 @@
extra_context={
'bread_crumb_trail': BreadCrumbTrail.leading_to(test_detail, test_id=test_id)
})
+
+
+def redirect_to_test_run(request, analyzer_assigned_uuid):
+ test_run = get_restricted_object_or_404(
+ TestRun,
+ lambda test_run: test_run.bundle.bundle_stream,
+ request.user,
+ analyzer_assigned_uuid=analyzer_assigned_uuid)
+ return redirect(test_run.get_absolute_url())
+
+
+def redirect_to_test_result(request, analyzer_assigned_uuid, relative_index):
+ test_result = get_restricted_object_or_404(
+ TestResult,
+ lambda test_result: test_result.test_run.bundle.bundle_stream,
+ request.user,
+ test_run__analyzer_assigned_uuid=analyzer_assigned_uuid,
+ relative_index=relative_index)
+ return redirect(test_result.get_absolute_url())
+
+
+def redirect_to_bundle(request, content_sha1):
+ bundle = get_restricted_object_or_404(
+ Bundle,
+ lambda bundle: bundle.bundle_stream,
+ request.user,
+ content_sha1=content_sha1)
+ return redirect(bundle.get_absolute_url())
+
+
+@BreadCrumb("Image Status Matrix", parent=index)
+def image_status_list(request):
+ return render_to_response(
+ "dashboard_app/image_status_list.html", {
+ 'hwpack_list': ImageHealth.get_hwpack_list(),
+ 'rootfs_list': ImageHealth.get_rootfs_list(),
+ 'ImageHealth': ImageHealth,
+ 'bread_crumb_trail': BreadCrumbTrail.leading_to(image_status_list)
+ }, RequestContext(request))
+
+
+@BreadCrumb(
+ "Image Status for {rootfs_type} + {hwpack_type}",
+ parent=image_status_list,
+ needs=["rootfs_type", "hwpack_type"])
+def image_status_detail(request, rootfs_type, hwpack_type):
+ image_health = ImageHealth(rootfs_type, hwpack_type)
+ return render_to_response(
+ "dashboard_app/image_status_detail.html", {
+ 'image_health': image_health,
+ 'bread_crumb_trail': BreadCrumbTrail.leading_to(
+ image_status_detail,
+ rootfs_type=rootfs_type,
+ hwpack_type=hwpack_type),
+ }, RequestContext(request))
+
+
+@BreadCrumb(
+ "Test history for {test_id}",
+ parent=image_status_detail,
+ needs=["rootfs_type", "hwpack_type", "test_id"])
+def image_test_history(request, rootfs_type, hwpack_type, test_id):
+ image_health = ImageHealth(rootfs_type, hwpack_type)
+ test = get_object_or_404(Test, test_id=test_id)
+ test_run_list = image_health.get_test_runs().filter(test=test)
+ return render_to_response(
+ "dashboard_app/image_test_history.html", {
+ 'test_run_list': test_run_list,
+ 'test': test,
+ 'image_health': image_health,
+ 'bread_crumb_trail': BreadCrumbTrail.leading_to(
+ image_test_history,
+ rootfs_type=rootfs_type,
+ hwpack_type=hwpack_type,
+ test=test,
+ test_id=test_id),
+ }, RequestContext(request))
=== modified file 'dashboard_app/xmlrpc.py'
@@ -24,15 +24,20 @@
import logging
import xmlrpclib
-from django.contrib.auth.models import User
+from django.contrib.auth.models import User, Group
from django.db import IntegrityError, DatabaseError
-from linaro_django_xmlrpc.models import ExposedAPI
-from linaro_django_xmlrpc.models import Mapper
+from linaro_django_xmlrpc.models import (
+ ExposedAPI,
+ Mapper,
+ xml_rpc_signature,
+)
from dashboard_app import __version__
-from dashboard_app.dataview import DataView, DataViewRepository
-from dashboard_app.dispatcher import xml_rpc_signature
-from dashboard_app.models import Bundle, BundleStream
+from dashboard_app.models import (
+ Bundle,
+ BundleStream,
+ DataView,
+)
class errors:
@@ -57,10 +62,8 @@
All public methods are automatically exposed as XML-RPC methods
"""
-
data_view_connection = DataView.get_connection()
-
@xml_rpc_signature('str')
def version(self):
"""
@@ -137,8 +140,8 @@
------------------------------
The following rules govern bundle stream upload access rights:
- all anonymous streams are accessible
- - personal streams are accessible by owners
- - team streams are accessible by team members
+ - personal streams are accessible to owners
+ - team streams are accessible to team members
"""
try:
@@ -201,8 +204,8 @@
------------------------------
The following rules govern bundle stream download access rights:
- all anonymous streams are accessible
- - personal streams are accessible by owners
- - team streams are accessible by team members
+ - personal streams are accessible to owners
+ - team streams are accessible to team members
"""
try:
bundle = Bundle.objects.get(content_sha1=content_sha1)
@@ -256,8 +259,8 @@
------------------------------
The following rules govern bundle stream download access rights:
- all anonymous streams are accessible
- - personal streams are accessible by owners
- - team streams are accessible by team members
+ - personal streams are accessible to owners
+ - team streams are accessible to team members
"""
bundle_streams = BundleStream.objects.accessible_by_principal(self.user)
return [{
@@ -319,8 +322,8 @@
------------------------------
The following rules govern bundle stream download access rights:
- all anonymous streams are accessible
- - personal streams are accessible by owners
- - team streams are accessible by team members
+ - personal streams are accessible to owners
+ - team streams are accessible to team members
"""
try:
bundle_stream = BundleStream.objects.accessible_by_principal(self.user).get(pathname=pathname)
@@ -418,20 +421,55 @@
if name is None:
name = ""
try:
- user, group, slug, is_public, is_anonymous = BundleStream.parse_pathname(pathname)
+ user_name, group_name, slug, is_public, is_anonymous = BundleStream.parse_pathname(pathname)
except ValueError as ex:
raise xmlrpclib.Fault(errors.FORBIDDEN, str(ex))
- if user is None and group is None:
+
+ # Start with those to simplify the logic below
+ user = None
+ group = None
+ if is_anonymous is False:
+ if self.user is not None:
+ assert is_anonymous is False
+ assert self.user is not None
+ if user_name is not None:
+ if user_name != self.user.username:
+ raise xmlrpclib.Fault(
+ errors.FORBIDDEN,
+ "Only user {user!r} could create this stream".format(user=user_name))
+ user = self.user # map to real user object
+ elif group_name is not None:
+ try:
+ group = self.user.groups.get(name=group_name)
+ except Group.DoesNotExist:
+ raise xmlrpclib.Fault(
+ errors.FORBIDDEN,
+ "Only a member of group {group!r} could create this stream".format(group=group_name))
+ else:
+ assert is_anonymous is False
+ assert self.user is None
+ raise xmlrpclib.Fault(
+ errors.FORBIDDEN, "Only anonymous streams can be constructed (you are not signed in)")
+ else:
+ assert is_anonymous is True
+ assert user_name is None
+ assert group_name is None
# Hacky but will suffice for now
user = User.objects.get_or_create(username="anonymous-owner")[0]
- try:
- bundle_stream = BundleStream.objects.create(user=user, group=group, slug=slug, is_public=is_public, is_anonymous=is_anonymous, name=name)
- except IntegrityError:
- raise xmlrpclib.Fault(errors.CONFLICT, "Stream with the specified pathname already exists")
+ try:
+ bundle_stream = BundleStream.objects.create(
+ user=user,
+ group=group,
+ slug=slug,
+ is_public=is_public,
+ is_anonymous=is_anonymous,
+ name=name)
+ except IntegrityError:
+ raise xmlrpclib.Fault(
+ errors.CONFLICT,
+ "Stream with the specified pathname already exists")
else:
- # TODO: Make this constraint unnecessary
- raise xmlrpclib.Fault(errors.FORBIDDEN, "Only anonymous streams can be constructed")
- return bundle_stream.pathname
+ return bundle_stream.pathname
def data_views(self):
"""
@@ -461,7 +499,6 @@
-----------------
None
"""
- repo = DataViewRepository.get_instance()
return [{
'name': data_view.name,
'summary': data_view.summary,
@@ -472,7 +509,7 @@
"help": arg.help,
"default": arg.default
} for arg in data_view.arguments]
- } for data_view in repo]
+ } for data_view in DataView.repository.all()]
def data_view_info(self, name):
"""
@@ -516,10 +553,9 @@
404
Name does not designate a data view
"""
- repo = DataViewRepository.get_instance()
try:
- data_view = repo[name]
- except KeyError:
+ data_view = DataView.repository.get(name=name)
+ except DataView.DoesNotExist:
raise xmlrpclib.Fault(errors.NOT_FOUND, "Data view not found")
else:
query = data_view.get_backend_specific_query(self.data_view_connection)
@@ -564,10 +600,9 @@
-----------------
TBD
"""
- repo = DataViewRepository.get_instance()
try:
- data_view = repo[name]
- except KeyError:
+ data_view = DataView.repository.get(name=name)
+ except DataView.DoesNotExist:
raise xmlrpclib.Fault(errors.NOT_FOUND, "Data view not found")
try:
rows, columns = data_view(self.data_view_connection, **arguments)
@@ -584,7 +619,6 @@
}
-
# Mapper used by the legacy URL
legacy_mapper = Mapper()
legacy_mapper.register_introspection_methods()
=== modified file 'doc/changes.rst'
@@ -1,6 +1,45 @@
Version History
***************
+.. _version_0_6:
+
+Version 0.6
+===========
+
+This version was released as 2011.07 in the Linaro monthly release process.
+
+Release highlights:
+
+* New UI synchronized with lava-server, the UI is going to be changed in the
+ next release to be more in line with the official Linaro theme. Currently
+ most changes are under-the-hood, sporting more jQuery UI CSS.
+* New test browser that allows to see all the registered tests and their test
+ cases.
+* New data view browser, similar to data view browser.
+* New permalink system that allows easy linking to bundles, test runs and test results.
+* New image status views that allow for quick inspection of interesting
+ hardware pack + root filesystem combinations.
+* New image status detail view with color-coded information about test failures
+ affecting current and historic instances of a particular root filesystem +
+ hardware pack combination.
+* New image test history view showing all the runs of a particular test on a
+ particular combination of root filesystem + hardware pack.
+* New table widget for better table display with support for client side
+ sorting and searching.
+* New option to render data reports without any navigation that is suitable for
+ embedding inside an iframe (by appending &iframe=yes to the URL)
+* New view for showing text attachments associated with test runs.
+* New view showing test runs associated with a specific bundle.
+* New view showing the raw JSON text of a bundle.
+* New view for inspecting bundle deserialization failures.
+* Integration with lava-server/RPC2/ for web APIs
+* Added support for non-anonymous submissions (test results uploaded by
+ authenticated users), including uploading results to personal (owned by
+ person), team (owned by group), public (visible) and private (hidden from
+ non-owners) bundle streams.
+* Added support for creating non-anonymous bundle streams with
+ dashboard.make_stream() (for authenticated users)
+
.. _version_0_5:
Version 0.5
=== modified file 'doc/index.rst'
@@ -5,7 +5,7 @@
.. automodule:: dashboard_app
.. seealso:: To get started quickly see :ref:`usage`
-.. seealso:: See what's new in :ref:`version_0_5`
+.. seealso:: See what's new in :ref:`version_0_6`
Features
========