diff --git a/.gitignore b/.gitignore index 7fc252559a..95355782dc 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ /third_party/asan /third_party/cygwin /third_party/expat +/third_party/gaeunit /third_party/google-gflags/src /third_party/google-visualization-python /third_party/jsoncpp diff --git a/DEPS b/DEPS index 19e5303c7f..5386b8c365 100644 --- a/DEPS +++ b/DEPS @@ -84,13 +84,17 @@ deps = { "trunk/third_party/libyuv": (Var("googlecode_url") % "libyuv") + "/trunk@121", - # Used by tools/coverage/dashboard and tools/python_charts + # Used by tools/quality_tracking/dashboard and tools/python_charts "trunk/third_party/google-visualization-python": (Var("googlecode_url") % "google-visualization-python") + "/trunk@15", - # Used by tools/coverage + # Used by tools/quality_tracking "trunk/third_party/oauth2": - "https://github.com/simplegeo/python-oauth2.git@a83f4a297336b631e75cba102910c19231518159" + "https://github.com/simplegeo/python-oauth2.git@a83f4a29", + + # Used by tools/quality_tracking + "trunk/third_party/gaeunit": + "https://code.google.com/p/gaeunit.git@e16d5bd4", } deps_os = { diff --git a/tools/quality_tracking/dashboard/add_build_status_data.py b/tools/quality_tracking/dashboard/add_build_status_data.py index 533b2b27dd..fc5ff69344 100644 --- a/tools/quality_tracking/dashboard/add_build_status_data.py +++ b/tools/quality_tracking/dashboard/add_build_status_data.py @@ -12,6 +12,8 @@ __author__ = 'phoglund@webrtc.org (Patrik Höglund)' +import datetime + from google.appengine.ext import db import oauth_post_request_handler @@ -24,12 +26,13 @@ class OrphanedBuildStatusesExistException(Exception): class BuildStatusRoot(db.Model): - """Exists solely to be the root parent for all build status data. + """Exists solely to be the root parent for all build status data and to keep + track of when the last update was made. Since all build status data will refer to this as their parent, we can run transactions on the build status data as a whole. """ - pass + last_updated_at = db.DateTimeProperty() class BuildStatusData(db.Model): @@ -121,7 +124,7 @@ class AddBuildStatusData(oauth_post_request_handler.OAuthPostRequestHandler): combination. Now we will effectively update the bot's status instead. """ - def post(self): + def _parse_and_store_data(self): build_status_root = _ensure_build_status_root_exists() build_status_data = _filter_oauth_parameters(self.request.arguments()) @@ -130,6 +133,7 @@ class AddBuildStatusData(oauth_post_request_handler.OAuthPostRequestHandler): def _parse_and_store_data_in_transaction(self, build_status_root, build_status_data): + encountered_revisions = set() for revision_and_bot_name in build_status_data: build_number_and_status = self.request.get(revision_and_bot_name) @@ -154,3 +158,8 @@ class AddBuildStatusData(oauth_post_request_handler.OAuthPostRequestHandler): build_number=build_number, status=status) item.put() + + request_posix_timestamp = float(self.request.get('oauth_timestamp')) + request_datetime = datetime.datetime.fromtimestamp(request_posix_timestamp) + build_status_root.last_updated_at = request_datetime + build_status_root.put() diff --git a/tools/quality_tracking/dashboard/add_coverage_data.py b/tools/quality_tracking/dashboard/add_coverage_data.py index e5f5048cfd..1ec9f6a1c4 100644 --- a/tools/quality_tracking/dashboard/add_coverage_data.py +++ b/tools/quality_tracking/dashboard/add_coverage_data.py @@ -43,7 +43,7 @@ class AddCoverageData(oauth_post_request_handler.OAuthPostRequestHandler): function_coverage: A float percentage in the interval 0-100.0. """ - def post(self): + def _parse_and_store_data(self): try: posix_time = int(self.request.get('date')) parsed_date = datetime.datetime.fromtimestamp(posix_time) diff --git a/tools/quality_tracking/dashboard/app.yaml b/tools/quality_tracking/dashboard/app.yaml index 647f6e40f1..496954e97c 100644 --- a/tools/quality_tracking/dashboard/app.yaml +++ b/tools/quality_tracking/dashboard/app.yaml @@ -2,8 +2,13 @@ application: dashboard version: 1 runtime: python27 api_version: 1 -threadsafe: true +threadsafe: false handlers: +- url: /stylesheets + static_dir: stylesheets +# Note: tests should be disabled in production. +# - url: /test.* +# script: gaeunit.py - url: /.* script: dashboard.app \ No newline at end of file diff --git a/tools/quality_tracking/dashboard/dashboard.py b/tools/quality_tracking/dashboard/dashboard.py index 6b7374e4c1..9ce1d7478c 100644 --- a/tools/quality_tracking/dashboard/dashboard.py +++ b/tools/quality_tracking/dashboard/dashboard.py @@ -12,12 +12,13 @@ __author__ = 'phoglund@webrtc.org (Patrik Höglund)' -from google.appengine.ext import db -import gviz_api +from google.appengine.ext.webapp import template import webapp2 import add_build_status_data import add_coverage_data +import load_build_status +import load_coverage class ShowDashboard(webapp2.RequestHandler): @@ -28,38 +29,18 @@ class ShowDashboard(webapp2.RequestHandler): """ def get(self): - page_template_filename = 'templates/dashboard_template.html' + build_status_loader = load_build_status.BuildStatusLoader() + build_status_data = build_status_loader.load_build_status_data() + last_updated_at = build_status_loader.load_last_modified_at() + last_updated_at = last_updated_at.strftime("%Y-%m-%d %H:%M") + lkgr = build_status_loader.compute_lkgr() - # Load the page HTML template. - try: - template_file = open(page_template_filename) - page_template = template_file.read() - template_file.close() - except IOError as exception: - self._show_error_page('Cannot open page template file: %s
Details: %s' - % (page_template_filename, exception)) - return - - coverage_entries = db.GqlQuery('SELECT * ' - 'FROM CoverageData ' - 'ORDER BY date ASC') - data = [] - for coverage_entry in coverage_entries: - data.append({'date': coverage_entry.date, - 'line_coverage': coverage_entry.line_coverage, - 'function_coverage': coverage_entry.function_coverage, - }) - - description = { - 'date': ('datetime', 'Date'), - 'line_coverage': ('number', 'Line Coverage'), - 'function_coverage': ('number', 'Function Coverage') - } - coverage_data = gviz_api.DataTable(description, data) - coverage_json_data = coverage_data.ToJSon(order_by='date') + coverage_loader = load_coverage.CoverageDataLoader() + coverage_json_data = coverage_loader.load_coverage_json_data() # Fill in the template with the data and respond: - self.response.write(page_template % vars()) + page_template_filename = 'templates/dashboard_template.html' + self.response.write(template.render(page_template_filename, vars())) def _show_error_page(self, error_message): self.response.write('%s' % error_message) diff --git a/tools/quality_tracking/dashboard/gaeunit.py b/tools/quality_tracking/dashboard/gaeunit.py new file mode 120000 index 0000000000..a93f6bd005 --- /dev/null +++ b/tools/quality_tracking/dashboard/gaeunit.py @@ -0,0 +1 @@ +../../../third_party/gaeunit/gaeunit.py \ No newline at end of file diff --git a/tools/quality_tracking/dashboard/load_build_status.py b/tools/quality_tracking/dashboard/load_build_status.py new file mode 100644 index 0000000000..4b51ed585d --- /dev/null +++ b/tools/quality_tracking/dashboard/load_build_status.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python +#-*- coding: utf-8 -*- +# Copyright (c) 2012 The WebRTC project authors. All Rights Reserved. +# +# Use of this source code is governed by a BSD-style license +# that can be found in the LICENSE file in the root of the source +# tree. An additional intellectual property rights grant can be found +# in the file PATENTS. All contributing project authors may +# be found in the AUTHORS file in the root of the source tree. + +"""Loads build status data for the dashboard.""" + +__author__ = 'phoglund@webrtc.org (Patrik Höglund)' + +from google.appengine.ext import db + + +def _all_ok(statuses): + return filter(lambda status: status != "OK", statuses) == [] + + +def _get_first_entry(iterable): + if not iterable: + return None + for item in iterable: + return item + + +class BuildStatusLoader: + """ Loads various build status data from the database.""" + + def load_build_status_data(self): + """Returns the latest conclusive build status for each bot. + + The statuses OK or failed are considered to be conclusive. + + The two most recent revisions are considered. The set of bots returned + will therefore be the bots that were reported the two most recent + revisions. This script will therefore adapt automatically to any changes + in the set of available bots. + + Returns: + A list of BuildStatusData entities with one entity per bot. + """ + + build_status_entries = db.GqlQuery('SELECT * ' + 'FROM BuildStatusData ' + 'ORDER BY revision DESC ') + + bots_to_latest_conclusive_entry = dict() + for entry in build_status_entries: + if entry.status == "building": + # The 'building' status it not conclusive, so discard this entry and + # pick up the entry for this bot on the next revision instead. That + # entry is guaranteed to have a status != 'building' since a bot cannot + # be building two revisions simultaneously. + continue + if bots_to_latest_conclusive_entry.has_key(entry.bot_name): + # We've already determined this bot's status. + continue + + bots_to_latest_conclusive_entry[entry.bot_name] = entry + + return bots_to_latest_conclusive_entry.values() + + def load_last_modified_at(self): + build_status_root = db.GqlQuery('SELECT * ' + 'FROM BuildStatusRoot').get() + if not build_status_root: + # Operating on completely empty database + return None + + return build_status_root.last_updated_at + + def compute_lkgr(self): + """ Finds the most recent revision for which all bots are green. + + Returns: + The last known good revision (as an integer) or None if there + is no green revision in the database. + + Implementation note: The data store fetches stuff as we go, so we won't + read in the whole status table unless the LKGR is right at the end or + we don't have a LKGR. + """ + build_status_entries = db.GqlQuery('SELECT * ' + 'FROM BuildStatusData ' + 'ORDER BY revision DESC ') + + first_entry = _get_first_entry(build_status_entries) + if first_entry is None: + # No entries => no LKGR + return None + + current_lkgr = first_entry.revision + statuses_for_current_lkgr = [first_entry.status] + + for entry in build_status_entries: + if current_lkgr == entry.revision: + statuses_for_current_lkgr.append(entry.status) + else: + # Starting on new revision, check previous revision. + if _all_ok(statuses_for_current_lkgr): + # All bots are green; LKGR found. + return current_lkgr + else: + # Not all bots are green, so start over on the next revision. + current_lkgr = entry.revision + statuses_for_current_lkgr = [entry.status] + + if _all_ok(statuses_for_current_lkgr): + # There was only one revision and it was OK. + return current_lkgr + + # There is no all-green revision in the database. + return None diff --git a/tools/quality_tracking/dashboard/load_coverage.py b/tools/quality_tracking/dashboard/load_coverage.py new file mode 100644 index 0000000000..eafed3b330 --- /dev/null +++ b/tools/quality_tracking/dashboard/load_coverage.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python +#-*- coding: utf-8 -*- +# Copyright (c) 2012 The WebRTC project authors. All Rights Reserved. +# +# Use of this source code is governed by a BSD-style license +# that can be found in the LICENSE file in the root of the source +# tree. An additional intellectual property rights grant can be found +# in the file PATENTS. All contributing project authors may +# be found in the AUTHORS file in the root of the source tree. + +"""Loads coverage data from the database.""" + +__author__ = 'phoglund@webrtc.org (Patrik Höglund)' + +from google.appengine.ext import db +import gviz_api + + +class CoverageDataLoader: + """ Loads coverage data from the database.""" + + def load_coverage_json_data(self): + coverage_entries = db.GqlQuery('SELECT * ' + 'FROM CoverageData ' + 'ORDER BY date ASC') + data = [] + for coverage_entry in coverage_entries: + data.append({'date': coverage_entry.date, + 'line_coverage': coverage_entry.line_coverage, + 'function_coverage': coverage_entry.function_coverage, + }) + + description = { + 'date': ('datetime', 'Date'), + 'line_coverage': ('number', 'Line Coverage'), + 'function_coverage': ('number', 'Function Coverage') + } + coverage_data = gviz_api.DataTable(description, data) + return coverage_data.ToJSon(order_by='date') diff --git a/tools/quality_tracking/dashboard/stylesheets/stylesheet.css b/tools/quality_tracking/dashboard/stylesheets/stylesheet.css new file mode 100644 index 0000000000..d48e7bc851 --- /dev/null +++ b/tools/quality_tracking/dashboard/stylesheets/stylesheet.css @@ -0,0 +1,40 @@ +/******************************************************************** +* +* Copyright (c) 2012 The WebRTC project authors. All Rights Reserved. +* +* Use of this source code is governed by a BSD-style license +* that can be found in the LICENSE file in the root of the source +* tree. An additional intellectual property rights grant can be found +* in the file PATENTS. All contributing project authors may +* be found in the AUTHORS file in the root of the source tree. +* +*********************************************************************/ + +.status_OK { + color: #FFFFFF; + background-color: #8fdf5f; +} + +.status_failed { + color: #FFFFFF; + background-color: #e98080; +} + +.status_building { + color: #666666; + background-color: #fffc6c; +} + +.last_known_good_revision { + font-size: 800%; +} + +.status_cell { + width: 100px; + text-align: center; +} + +body { + margin-left: 35px; + margin-top: 25px; +} \ No newline at end of file diff --git a/tools/quality_tracking/dashboard/templates/dashboard_template.html b/tools/quality_tracking/dashboard/templates/dashboard_template.html index 61425e7d35..0fd696c449 100644 --- a/tools/quality_tracking/dashboard/templates/dashboard_template.html +++ b/tools/quality_tracking/dashboard/templates/dashboard_template.html @@ -1,6 +1,6 @@ - +