diff --git a/tools/coverage/dashboard/dashboard.py b/tools/coverage/dashboard/dashboard.py
deleted file mode 100644
index 3d82e239c9..0000000000
--- a/tools/coverage/dashboard/dashboard.py
+++ /dev/null
@@ -1,129 +0,0 @@
-#!/usr/bin/env python
-# 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.
-
-"""Implements the coverage tracker dashboard and reporting facilities."""
-
-__author__ = 'phoglund@webrtc.org (Patrik Hoglund)'
-
-import datetime
-from google.appengine.api import oauth
-from google.appengine.api import users
-from google.appengine.ext import db
-import webapp2
-import gviz_api
-
-
-class UserNotAuthenticatedException(Exception):
- """Gets thrown if a user is not permitted to store coverage data."""
- pass
-
-
-class CoverageData(db.Model):
- """This represents one coverage report from the build bot."""
- date = db.DateTimeProperty(required=True)
- line_coverage = db.FloatProperty(required=True)
- function_coverage = db.FloatProperty(required=True)
-
-
-class ShowDashboard(webapp2.RequestHandler):
- """Shows the dashboard page.
-
- The page is shown by grabbing data we have stored previously
- in the App Engine database using the AddCoverageData handler.
- """
-
- def get(self):
- page_template_filename = 'templates/dashboard_template.html'
-
- # 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')
-
- # Fill in the template with the data and respond:
- self.response.write(page_template % vars())
-
- def _show_error_page(self, error_message):
- self.response.write('
%s' % error_message)
-
-
-class AddCoverageData(webapp2.RequestHandler):
- """Used to report coverage data.
-
- The user is required to have obtained an OAuth access token from an
- administrator for this application earlier.
- """
-
- def post(self):
- try:
- self._authenticate_user()
- except UserNotAuthenticatedException as exception:
- self._show_error_page('Failed to authenticate user: %s' % exception)
- return
-
- try:
- posix_time = int(self.request.get('date'))
- parsed_date = datetime.datetime.fromtimestamp(posix_time)
-
- line_coverage = float(self.request.get('line_coverage'))
- function_coverage = float(self.request.get('function_coverage'))
- except ValueError as exception:
- self._show_error_page('Invalid parameter in request. Details: %s' %
- exception)
- return
-
- item = CoverageData(date=parsed_date,
- line_coverage=line_coverage,
- function_coverage=function_coverage)
- item.put()
-
- def _authenticate_user(self):
- try:
- if oauth.is_current_user_admin():
- # The user on whose behalf we are acting is indeed an administrator
- # of this application, so we're good to go.
- return
- else:
- raise UserNotAuthenticatedException('We are acting on behalf of '
- 'user %s, but that user is not '
- 'an administrator.' %
- oauth.get_current_user())
- except oauth.OAuthRequestError as exception:
- raise UserNotAuthenticatedException('Invalid OAuth request: %s' %
- exception)
-
- def _show_error_page(self, error_message):
- self.response.write('%s' % error_message)
-
-app = webapp2.WSGIApplication([('/', ShowDashboard),
- ('/add_coverage_data', AddCoverageData)],
- debug=True)
diff --git a/tools/coverage/track_coverage.py b/tools/coverage/track_coverage.py
deleted file mode 100755
index 8bc9f37673..0000000000
--- a/tools/coverage/track_coverage.py
+++ /dev/null
@@ -1,184 +0,0 @@
-#!/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.
-
-"""This script grabs and reports coverage information.
-
- It grabs coverage information from the latest Linux 32-bit build and
- pushes it to the coverage tracker, enabling us to track code coverage
- over time. This script is intended to run on the 32-bit Linux slave.
-
- This script requires an access.token file in the current directory, as
- generated by the request_oauth_permission.py script. It also expects a file
- customer.secret with a single line containing the customer secret. The
- customer secret is an OAuth concept and is received when one registers the
- application with the appengine running the dashboard.
-
- The script assumes that all coverage data is stored under
- /home//www.
-"""
-
-__author__ = 'phoglund@webrtc.org (Patrik Höglund)'
-
-import httplib
-import os
-import re
-import shelve
-import sys
-import time
-import oauth.oauth as oauth
-
-# The build-bot user which runs build bot jobs.
-BUILD_BOT_USER = 'phoglund'
-
-# The server to send coverage data to.
-# TODO(phoglund): replace with real server once we get it up.
-DASHBOARD_SERVER = 'localhost:8080'
-CONSUMER_KEY = DASHBOARD_SERVER
-ADD_COVERAGE_DATA_URL = '/add_coverage_data'
-
-
-class FailedToParseCoverageHtml(Exception):
- pass
-
-
-class FailedToReportToDashboard(Exception):
- pass
-
-
-class FailedToReadRequiredInputFile(Exception):
- pass
-
-
-def _read_access_token_from_file(filename):
- input_file = shelve.open(filename)
-
- if not input_file.has_key('access_token'):
- raise FailedToReadRequiredInputFile('Missing %s file in current directory. '
- 'You may have to run '
- 'request_oauth_permission.py.')
-
- return oauth.OAuthToken.from_string(input_file['access_token'])
-
-
-def _read_customer_secret_from_file(filename):
- try:
- input_file = open(filename, 'r')
- except IOError as error:
- raise FailedToReadRequiredInputFile(error)
-
- whole_file = input_file.read()
- if '\n' in whole_file or not whole_file.strip():
- raise FailedToReadRequiredInputFile('Expected a single line with the '
- 'customer secret in file %s.' %
- filename)
- return whole_file
-
-
-def _find_latest_32bit_debug_build(www_directory_contents):
- # Build directories have the form Linux32bitDebug_. There may be other
- # directories in the list though, for instance for other build configurations.
- # This sort ensures we will encounter the directory with the highest number
- # first.
- www_directory_contents.sort(reverse=True)
-
- for entry in www_directory_contents:
- match = re.match('Linux32bitDBG_\d+', entry)
- if match is not None:
- return entry
-
- # Didn't find it
- return None
-
-
-def _grab_coverage_percentage(label, index_html_contents):
- """Extracts coverage from a LCOV coverage report.
-
- Grabs coverage by assuming that the label in the coverage HTML report
- is close to the actual number and that the number is followed by a space
- and a percentage sign.
- """
- match = re.search(']*>' + label + ' | .*?(\d+\.\d) %',
- index_html_contents, re.DOTALL)
- if match is None:
- raise FailedToParseCoverageHtml('Missing coverage at label "%s".' % label)
-
- try:
- return float(match.group(1))
- except ValueError:
- raise FailedToParseCoverageHtml('%s is not a float.' % match.group(1))
-
-
-def _report_coverage_to_dashboard(now, line_coverage, function_coverage,
- access_token, consumer_key, consumer_secret):
- parameters = {'date': '%d' % now,
- 'line_coverage': '%f' % line_coverage,
- 'function_coverage': '%f' % function_coverage
- }
- consumer = oauth.OAuthConsumer(consumer_key, consumer_secret)
- create_oauth_request = oauth.OAuthRequest.from_consumer_and_token
- oauth_request = create_oauth_request(consumer,
- token=access_token,
- http_method='POST',
- http_url=ADD_COVERAGE_DATA_URL,
- parameters=parameters)
-
- signature_method_hmac_sha1 = oauth.OAuthSignatureMethod_HMAC_SHA1()
- oauth_request.sign_request(signature_method_hmac_sha1, consumer, access_token)
-
- connection = httplib.HTTPConnection(DASHBOARD_SERVER)
-
- headers = {'Content-Type': 'application/x-www-form-urlencoded'}
- connection.request('POST', ADD_COVERAGE_DATA_URL,
- body=oauth_request.to_postdata(),
- headers=headers)
-
- response = connection.getresponse()
- if response.status != 200:
- message = ('Error: Failed to report to %s%s: got response %d (%s)' %
- (DASHBOARD_SERVER, request_string, response.status,
- response.reason))
- raise FailedToReportToDashboard(message)
-
- # The response content should be empty on success, so check that:
- response_content = response.read()
- if response_content:
- message = ('Error: Dashboard reported the following error: %s.' %
- response_content)
- raise FailedToReportToDashboard(message)
-
-
-def _main():
- access_token = _read_access_token_from_file('access.token')
- customer_secret = _read_customer_secret_from_file('customer.secret')
-
- coverage_www_dir = os.path.join('/home', BUILD_BOT_USER, 'www')
-
- www_dir_contents = os.listdir(coverage_www_dir)
- latest_build_directory = _find_latest_32bit_debug_build(www_dir_contents)
-
- if latest_build_directory is None:
- print 'Error: Found no 32-bit debug build in directory ' + coverage_www_dir
- sys.exit(1)
-
- index_html_path = os.path.join(coverage_www_dir, latest_build_directory,
- 'index.html')
- index_html_file = open(index_html_path)
- whole_file = index_html_file.read()
-
- line_coverage = _grab_coverage_percentage('Lines:', whole_file)
- function_coverage = _grab_coverage_percentage('Functions:', whole_file)
- now = int(time.time())
-
- _report_coverage_to_dashboard(now, line_coverage, function_coverage,
- access_token, CONSUMER_KEY, customer_secret)
-
-if __name__ == '__main__':
- _main()
-
diff --git a/tools/coverage/OWNERS b/tools/quality_tracking/OWNERS
similarity index 100%
rename from tools/coverage/OWNERS
rename to tools/quality_tracking/OWNERS
diff --git a/tools/coverage/README b/tools/quality_tracking/README
similarity index 100%
rename from tools/coverage/README
rename to tools/quality_tracking/README
diff --git a/tools/quality_tracking/constants.py b/tools/quality_tracking/constants.py
new file mode 100644
index 0000000000..a38c733733
--- /dev/null
+++ b/tools/quality_tracking/constants.py
@@ -0,0 +1,38 @@
+#!/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.
+
+"""Contains tweakable constants for quality dashboard utility scripts."""
+
+__author__ = 'phoglund@webrtc.org (Patrik Höglund)'
+
+# This identifies our application using the information we got when we
+# registered the application on Google appengine.
+# TODO(phoglund): update to the right value when we have registered the app.
+DASHBOARD_SERVER = 'localhost:8080'
+DASHBOARD_SERVER_HTTP = 'http://' + DASHBOARD_SERVER
+CONSUMER_KEY = DASHBOARD_SERVER
+CONSUMER_SECRET_FILE = 'consumer.secret'
+ACCESS_TOKEN_FILE = 'access.token'
+
+# OAuth URL:s.
+REQUEST_TOKEN_URL = DASHBOARD_SERVER_HTTP + '/_ah/OAuthGetRequestToken'
+AUTHORIZE_TOKEN_URL = DASHBOARD_SERVER_HTTP + '/_ah/OAuthAuthorizeToken'
+ACCESS_TOKEN_URL = DASHBOARD_SERVER_HTTP + '/_ah/OAuthGetAccessToken'
+
+# The build master URL.
+BUILD_MASTER_SERVER = 'webrtc-cb-linux-master.cbf.corp.google.com:8010'
+BUILD_MASTER_LATEST_BUILD_URL = '/one_box_per_builder'
+
+# The build-bot user which runs build bot jobs.
+BUILD_BOT_USER = 'phoglund'
+
+# Dashboard data input URLs.
+ADD_COVERAGE_DATA_URL = '/add_coverage_data'
+ADD_BUILD_STATUS_DATA_URL = '/add_build_status_data'
diff --git a/tools/quality_tracking/dashboard/add_build_status_data.py b/tools/quality_tracking/dashboard/add_build_status_data.py
new file mode 100644
index 0000000000..f5dbb729e2
--- /dev/null
+++ b/tools/quality_tracking/dashboard/add_build_status_data.py
@@ -0,0 +1,57 @@
+#!/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.
+
+"""Implements a handler for adding build status data."""
+
+__author__ = 'phoglund@webrtc.org (Patrik Höglund)'
+
+from google.appengine.ext import db
+
+import oauth_post_request_handler
+
+
+SUCCESSFUL_STRING_TO_BOOLEAN = {'successful': True, 'failed': False}
+
+
+class BuildStatusData(db.Model):
+ """This represents one build status report from the build bot."""
+ bot_name = db.StringProperty(required=True)
+ build_number = db.IntegerProperty(required=True)
+ successful = db.BooleanProperty(required=True)
+
+
+def _filter_oauth_parameters(post_keys):
+ return filter(lambda post_key: not post_key.startswith('oauth_'),
+ post_keys)
+
+
+class AddBuildStatusData(oauth_post_request_handler.OAuthPostRequestHandler):
+ """Used to report build status data."""
+
+ def post(self):
+ for bot_name in _filter_oauth_parameters(self.request.arguments()):
+ status = self.request.get(bot_name)
+ parsed_status = status.split('-')
+ if len(parsed_status) != 2:
+ raise ValueError('Malformed status string %s for bot %s.' %
+ (status, bot_name))
+
+ parsed_build_number = int(parsed_status[0])
+ successful = parsed_status[1]
+
+ if successful not in SUCCESSFUL_STRING_TO_BOOLEAN:
+ raise ValueError('Malformed status string %s for bot %s.' % (status,
+ bot_name))
+ parsed_successful = SUCCESSFUL_STRING_TO_BOOLEAN[successful]
+
+ item = BuildStatusData(bot_name=bot_name,
+ build_number=parsed_build_number,
+ successful=parsed_successful)
+ item.put()
diff --git a/tools/quality_tracking/dashboard/add_coverage_data.py b/tools/quality_tracking/dashboard/add_coverage_data.py
new file mode 100644
index 0000000000..e5f5048cfd
--- /dev/null
+++ b/tools/quality_tracking/dashboard/add_coverage_data.py
@@ -0,0 +1,65 @@
+#!/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.
+
+"""Implements a handler for adding coverage data."""
+
+__author__ = 'phoglund@webrtc.org (Patrik Höglund)'
+
+import datetime
+
+from google.appengine.ext import db
+
+import oauth_post_request_handler
+
+class CoverageData(db.Model):
+ """This represents one coverage report from the build bot."""
+ date = db.DateTimeProperty(required=True)
+ line_coverage = db.FloatProperty(required=True)
+ function_coverage = db.FloatProperty(required=True)
+
+
+def _parse_percentage(string_value):
+ percentage = float(string_value)
+ if percentage < 0.0 or percentage > 100.0:
+ raise ValueError('%s is not a valid percentage.' % string_value)
+ return percentage
+
+
+class AddCoverageData(oauth_post_request_handler.OAuthPostRequestHandler):
+ """Used to report coverage data.
+
+ Coverage data is reported as a POST request and should contain, aside from
+ the regular oauth_* parameters, these values:
+
+ date: The POSIX timestamp for when the coverage observation was made.
+ line_coverage: A float percentage in the interval 0-100.0.
+ function_coverage: A float percentage in the interval 0-100.0.
+ """
+
+ def post(self):
+ try:
+ posix_time = int(self.request.get('date'))
+ parsed_date = datetime.datetime.fromtimestamp(posix_time)
+
+ line_coverage_string = self.request.get('line_coverage')
+ line_coverage = _parse_percentage(line_coverage_string)
+ function_coverage_string = self.request.get('function_coverage')
+ function_coverage = _parse_percentage(function_coverage_string)
+
+ except ValueError as exception:
+ self._show_error_page('Invalid parameter in request. Details: %s' %
+ exception)
+ return
+
+ item = CoverageData(date=parsed_date,
+ line_coverage=line_coverage,
+ function_coverage=function_coverage)
+ item.put()
+
diff --git a/tools/coverage/dashboard/app.yaml b/tools/quality_tracking/dashboard/app.yaml
similarity index 100%
rename from tools/coverage/dashboard/app.yaml
rename to tools/quality_tracking/dashboard/app.yaml
diff --git a/tools/quality_tracking/dashboard/dashboard.py b/tools/quality_tracking/dashboard/dashboard.py
new file mode 100644
index 0000000000..6b7374e4c1
--- /dev/null
+++ b/tools/quality_tracking/dashboard/dashboard.py
@@ -0,0 +1,73 @@
+#!/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.
+
+"""Implements the quality tracker dashboard and reporting facilities."""
+
+__author__ = 'phoglund@webrtc.org (Patrik Höglund)'
+
+from google.appengine.ext import db
+import gviz_api
+import webapp2
+
+import add_build_status_data
+import add_coverage_data
+
+
+class ShowDashboard(webapp2.RequestHandler):
+ """Shows the dashboard page.
+
+ The page is shown by grabbing data we have stored previously
+ in the App Engine database using the AddCoverageData handler.
+ """
+
+ def get(self):
+ page_template_filename = 'templates/dashboard_template.html'
+
+ # 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')
+
+ # Fill in the template with the data and respond:
+ self.response.write(page_template % vars())
+
+ def _show_error_page(self, error_message):
+ self.response.write('%s' % error_message)
+
+
+app = webapp2.WSGIApplication([('/', ShowDashboard),
+ ('/add_coverage_data',
+ add_coverage_data.AddCoverageData),
+ ('/add_build_status_data',
+ add_build_status_data.AddBuildStatusData)],
+ debug=True)
diff --git a/tools/coverage/dashboard/gviz_api.py b/tools/quality_tracking/dashboard/gviz_api.py
similarity index 100%
rename from tools/coverage/dashboard/gviz_api.py
rename to tools/quality_tracking/dashboard/gviz_api.py
diff --git a/tools/quality_tracking/dashboard/oauth_post_request_handler.py b/tools/quality_tracking/dashboard/oauth_post_request_handler.py
new file mode 100644
index 0000000000..648b68b3d0
--- /dev/null
+++ b/tools/quality_tracking/dashboard/oauth_post_request_handler.py
@@ -0,0 +1,68 @@
+#!/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.
+
+"""Provides a OAuth request handler base class."""
+
+__author__ = 'phoglund@webrtc.org (Patrik Höglund)'
+
+from google.appengine.api import oauth
+import webapp2
+
+
+class UserNotAuthenticatedException(Exception):
+ """Gets thrown if a user is not permitted to store data."""
+ pass
+
+
+class OAuthPostRequestHandler(webapp2.RequestHandler):
+ """Works like a normal request handler but adds OAuth authentication.
+
+ This handler will expect a proper OAuth request over POST. This abstract
+ class deals with the authentication but leaves user-defined data handling
+ to its subclasses. Subclasses should not implement the post() method but
+ the _parse_and_store_data() method. Otherwise they may act like regular
+ request handlers. Subclasses should NOT override the get() method.
+
+ The handler will accept an OAuth request if it is correctly formed and
+ the consumer is acting on behalf of an administrator for the dashboard.
+ """
+
+ def post(self):
+ try:
+ self._authenticate_user()
+ except UserNotAuthenticatedException as exception:
+ self._show_error_page('Failed to authenticate user: %s' % exception)
+ return
+
+ # Do the actual work.
+ self._parse_and_store_data()
+
+ def _parse_and_store_data(self):
+ """Reads data from POST request and responds accordingly."""
+
+ raise NotImplementedError('You must override this method!')
+
+ def _authenticate_user(self):
+ try:
+ if oauth.is_current_user_admin():
+ # The user on whose behalf we are acting is indeed an administrator
+ # of this application, so we're good to go.
+ return
+ else:
+ raise UserNotAuthenticatedException('We are acting on behalf of '
+ 'user %s, but that user is not '
+ 'an administrator.' %
+ oauth.get_current_user())
+ except oauth.OAuthRequestError as exception:
+ raise UserNotAuthenticatedException('Invalid OAuth request: %s' %
+ exception)
+
+ def _show_error_page(self, error_message):
+ self.response.write('%s' % error_message)
diff --git a/tools/coverage/dashboard/templates/dashboard_template.html b/tools/quality_tracking/dashboard/templates/dashboard_template.html
similarity index 100%
rename from tools/coverage/dashboard/templates/dashboard_template.html
rename to tools/quality_tracking/dashboard/templates/dashboard_template.html
diff --git a/tools/quality_tracking/dashboard_connection.py b/tools/quality_tracking/dashboard_connection.py
new file mode 100644
index 0000000000..dd9a729e5a
--- /dev/null
+++ b/tools/quality_tracking/dashboard_connection.py
@@ -0,0 +1,137 @@
+#!/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.
+
+"""Contains utilities for communicating with the dashboard."""
+
+__author__ = 'phoglund@webrtc.org (Patrik Höglund)'
+
+import httplib
+import shelve
+import oauth.oauth as oauth
+
+import constants
+
+
+class FailedToReadRequiredInputFile(Exception):
+ pass
+
+
+class FailedToReportToDashboard(Exception):
+ pass
+
+
+class DashboardConnection:
+ """Helper class for pushing data to the dashboard.
+
+ This class deals with most of details for accessing protected resources
+ (i.e. data-writing operations) on the dashboard. Such operations are
+ authenticated using OAuth. This class requires a consumer secret and a
+ access token.
+
+ The consumer secret is created manually on the machine running the script
+ (only authorized users should be able to log into the machine and see the
+ secret though). The access token is generated by the
+ request_oauth_permission.py script. Both these values are stored as files
+ on disk, in the scripts' working directory, according to the formats
+ prescribed in the read_required_files method.
+ """
+
+ def __init__(self, consumer_key):
+ self.consumer_key_ = consumer_key
+
+ def read_required_files(self, consumer_secret_file, access_token_file):
+ """Reads required data for making OAuth requests.
+
+ Args:
+ consumer_secret_file: A plain text file with a single line containing
+ the consumer secret string.
+ access_token_file: A shelve file with an entry access_token
+ containing the access token in string form.
+ """
+ self.access_token_ = self._read_access_token(access_token_file)
+ self.consumer_secret_ = self._read_consumer_secret(consumer_secret_file)
+
+ def send_post_request(self, sub_url, parameters):
+ """Sends an OAuth request for a protected resource in the dashboard.
+
+ Use this when you want to report new data to the dashboard. You must have
+ called the read_required_files method prior to calling this method, since
+ that method will read in the consumer secret and access token we need to
+ make the OAuth request. These concepts are described in the class
+ description.
+
+ Args:
+ sub_url: A relative url within the dashboard domain, for example
+ /add_coverage_data.
+ parameters: A dict which maps from POST parameter names to values.
+
+ Returns:
+ A httplib response object.
+
+ Raises:
+ FailedToReportToDashboard: If the dashboard didn't respond
+ with HTTP 200 to our request.
+ """
+ consumer = oauth.OAuthConsumer(self.consumer_key_, self.consumer_secret_)
+ create_oauth_request = oauth.OAuthRequest.from_consumer_and_token
+ oauth_request = create_oauth_request(consumer,
+ token=self.access_token_,
+ http_method='POST',
+ http_url=sub_url,
+ parameters=parameters)
+
+ signature_method_hmac_sha1 = oauth.OAuthSignatureMethod_HMAC_SHA1()
+ oauth_request.sign_request(signature_method_hmac_sha1, consumer,
+ self.access_token_)
+
+ connection = httplib.HTTPConnection(constants.DASHBOARD_SERVER)
+
+ headers = {'Content-Type': 'application/x-www-form-urlencoded'}
+ connection.request('POST', sub_url, body=oauth_request.to_postdata(),
+ headers=headers)
+
+ response = connection.getresponse()
+ connection.close()
+
+ if response.status != 200:
+ message = ('Error: Failed to report to %s%s: got response %d (%s)' %
+ (constants.DASHBOARD_SERVER, sub_url, response.status,
+ response.reason))
+ raise FailedToReportToDashboard(message)
+
+ return response
+
+ def _read_access_token(self, filename):
+ input_file = shelve.open(filename)
+
+ if not input_file.has_key('access_token'):
+ raise FailedToReadRequiredInputFile('Missing correct %s file in current '
+ 'directory. You may have to run '
+ 'request_oauth_permission.py.' %
+ filename)
+
+ token = input_file['access_token']
+ input_file.close()
+
+ return oauth.OAuthToken.from_string(token)
+
+ def _read_consumer_secret(self, filename):
+ try:
+ input_file = open(filename, 'r')
+ except IOError as error:
+ raise FailedToReadRequiredInputFile(error)
+
+ whole_file = input_file.read()
+ if whole_file.count('\n') > 1 or not whole_file.strip():
+ raise FailedToReadRequiredInputFile('Expected a single line with the '
+ 'consumer secret in file %s.' %
+ filename)
+ return whole_file
+
diff --git a/tools/coverage/oauth2 b/tools/quality_tracking/oauth2
similarity index 100%
rename from tools/coverage/oauth2
rename to tools/quality_tracking/oauth2
diff --git a/tools/coverage/request_oauth_permission.py b/tools/quality_tracking/request_oauth_permission.py
similarity index 77%
rename from tools/coverage/request_oauth_permission.py
rename to tools/quality_tracking/request_oauth_permission.py
index bad0902bd1..3bc20f6d27 100755
--- a/tools/coverage/request_oauth_permission.py
+++ b/tools/quality_tracking/request_oauth_permission.py
@@ -19,7 +19,7 @@
script. This script will present a link during its execution, which the new
administrator should follow and then click approve on the web page that
appears. The new administrator should have admin rights on the coverage
- dashboard, otherwise the track_coverage.py will not work.
+ dashboard, otherwise track_coverage.py will not work.
If successful, this script will write the access token to a file access.token
in the current directory, which later can be read by track_coverage.py.
@@ -34,22 +34,12 @@ import sys
import urlparse
import oauth2 as oauth
+import constants
class FailedToRequestPermissionException(Exception):
pass
-# This identifies our application using the information we got when we
-# registered the application on Google appengine.
-# TODO(phoglund): update to the right value when we have registered the app.
-DASHBOARD_SERVER = 'http://127.0.0.1:8080'
-CONSUMER_KEY = DASHBOARD_SERVER
-
-REQUEST_TOKEN_URL = DASHBOARD_SERVER + '/_ah/OAuthGetRequestToken'
-AUTHORIZE_TOKEN_URL = DASHBOARD_SERVER + '/_ah/OAuthAuthorizeToken'
-ACCESS_TOKEN_URL = DASHBOARD_SERVER + '/_ah/OAuthGetAccessToken'
-
-
def _ensure_token_response_is_200(response, queried_url, token_type):
if response.status != 200:
raise FailedToRequestPermissionException('Failed to request %s from %s: '
@@ -59,8 +49,9 @@ def _ensure_token_response_is_200(response, queried_url, token_type):
response.status,
response.reason))
+
def _request_unauthorized_token(consumer, request_token_url):
- """Requests the initial token from the dashboard service.
+ """Requests the initial token from the dashboard service.
Given that the response from the server is correct, we will return a
dictionary containing oauth_token and oauth_token_secret mapped to the
@@ -73,12 +64,12 @@ def _request_unauthorized_token(consumer, request_token_url):
except AttributeError as error:
# This catch handler is here since we'll get very confusing messages
# if the target server is down for some reason.
- raise FailedToRequestPermissionException("Failed to request token: "
- "the dashboard is likely down.",
+ raise FailedToRequestPermissionException('Failed to request token: '
+ 'the dashboard is likely down.',
error)
_ensure_token_response_is_200(response, request_token_url,
- "unauthorized token")
+ 'unauthorized token')
return dict(urlparse.parse_qsl(content))
@@ -86,7 +77,7 @@ def _request_unauthorized_token(consumer, request_token_url):
def _ask_user_to_authorize_us(unauthorized_token):
"""This function will block until the user enters y + newline."""
print 'Go to the following link in your browser:'
- print '%s?oauth_token=%s' % (AUTHORIZE_TOKEN_URL,
+ print '%s?oauth_token=%s' % (constants.AUTHORIZE_TOKEN_URL,
unauthorized_token['oauth_token'])
accepted = 'n'
@@ -98,9 +89,10 @@ def _request_access_token(consumer, unauthorized_token):
token = oauth.Token(unauthorized_token['oauth_token'],
unauthorized_token['oauth_token_secret'])
client = oauth.Client(consumer, token)
- response, content = client.request(ACCESS_TOKEN_URL, 'POST')
+ response, content = client.request(constants.ACCESS_TOKEN_URL, 'POST')
- _ensure_token_response_is_200(response, ACCESS_TOKEN_URL, "access token")
+ _ensure_token_response_is_200(response, constants.ACCESS_TOKEN_URL,
+ 'access token')
return content
@@ -121,15 +113,16 @@ def _main():
return
consumer_secret = sys.argv[1]
- consumer = oauth.Consumer(CONSUMER_KEY, consumer_secret)
+ consumer = oauth.Consumer(constants.CONSUMER_KEY, consumer_secret)
- unauthorized_token = _request_unauthorized_token(consumer, REQUEST_TOKEN_URL)
+ unauthorized_token = _request_unauthorized_token(consumer,
+ constants.REQUEST_TOKEN_URL)
_ask_user_to_authorize_us(unauthorized_token)
access_token_string = _request_access_token(consumer, unauthorized_token)
- _write_access_token_to_file(access_token_string, 'access.token')
+ _write_access_token_to_file(access_token_string, constants.ACCESS_TOKEN_FILE)
if __name__ == '__main__':
_main()
diff --git a/tools/quality_tracking/track_build_status.py b/tools/quality_tracking/track_build_status.py
new file mode 100755
index 0000000000..53f5301418
--- /dev/null
+++ b/tools/quality_tracking/track_build_status.py
@@ -0,0 +1,90 @@
+#!/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.
+
+"""This script checks the current build status on the master and submits
+ it to the dashboard. It is adapted to build bot version 0.7.12.
+"""
+
+__author__ = 'phoglund@webrtc.org (Patrik Höglund)'
+
+
+import httplib
+import re
+
+import constants
+import dashboard_connection
+
+
+class FailedToGetStatusFromMaster(Exception):
+ pass
+
+
+class FailedToParseBuildStatus(Exception):
+ pass
+
+
+def _parse_status_page(html):
+ """Parses the build master's one_box_per_builder page.
+
+ Args:
+ html: The raw HTML from the one_box_per_builder page.
+
+ Returns: the bot name mapped to a string with the build number and the
+ build status separated by a dash (e.g. 456-successful, 114-failed).
+ """
+ result = {}
+
+ # Example target string: #430
+ #
build
successful
+ # Group 1 captures 'Win32Debug', Group 2 captures '430', group 3 'successful'.
+ # Implementation note: We match non-greedily (.*?) between the link and
+ # successful / failed, otherwise we would only find the first status.
+ for match in re.finditer(''
+ '.*?(successful|failed)',
+ html, re.DOTALL):
+ result[match.group(1)] = match.group(2) + '-' + match.group(3)
+
+ if not result:
+ raise FailedToParseBuildStatus('Could not find any build statuses in %s.' %
+ html)
+
+ return result
+
+
+def _download_and_parse_build_status():
+ connection = httplib.HTTPConnection(constants.BUILD_MASTER_SERVER)
+ connection.request('GET', constants.BUILD_MASTER_LATEST_BUILD_URL)
+ response = connection.getresponse()
+
+ if response.status != 200:
+ raise FailedToGetStatusFromMaster(('Failed to get build status from master:'
+ ' got status %d, reason %s.' %
+ (response.status, response.reason)))
+
+ full_response = response.read()
+ connection.close()
+
+ return _parse_status_page(full_response)
+
+
+def _main():
+ dashboard = dashboard_connection.DashboardConnection(constants.CONSUMER_KEY)
+ dashboard.read_required_files(constants.CONSUMER_SECRET_FILE,
+ constants.ACCESS_TOKEN_FILE)
+
+ bot_to_status_mapping = _download_and_parse_build_status()
+
+ response = dashboard.send_post_request(constants.ADD_BUILD_STATUS_DATA_URL,
+ bot_to_status_mapping)
+
+ print response.read()
+
+if __name__ == '__main__':
+ _main()
diff --git a/tools/quality_tracking/track_coverage.py b/tools/quality_tracking/track_coverage.py
new file mode 100755
index 0000000000..6f9754a693
--- /dev/null
+++ b/tools/quality_tracking/track_coverage.py
@@ -0,0 +1,130 @@
+#!/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.
+
+"""This script grabs and reports coverage information.
+
+ It grabs coverage information from the latest Linux 32-bit build and
+ pushes it to the coverage tracker, enabling us to track code coverage
+ over time. This script is intended to run on the 32-bit Linux slave.
+
+ This script requires an access.token file in the current directory, as
+ generated by the request_oauth_permission.py script. It also expects a file
+ customer.secret with a single line containing the customer secret. The
+ customer secret is an OAuth concept and is received when one registers the
+ application with the App Engine running the dashboard.
+
+ The script assumes that all coverage data is stored under
+ /home//www.
+"""
+
+__author__ = 'phoglund@webrtc.org (Patrik Höglund)'
+
+import os
+import re
+import sys
+import time
+
+import constants
+import dashboard_connection
+
+
+class FailedToParseCoverageHtml(Exception):
+ pass
+
+
+class CouldNotFindCoverageDirectory(Exception):
+ pass
+
+
+def _find_latest_32bit_debug_build(www_directory_contents, coverage_www_dir):
+ """Finds the latest 32-bit coverage directory in the directory listing.
+
+ Coverage directories have the form Linux32bitDBG_. There may be
+ other directories in the list though, for instance for other build
+ configurations.
+ """
+
+ # This sort ensures we will encounter the directory with the highest number
+ # first.
+ www_directory_contents.sort(reverse=True)
+
+ for entry in www_directory_contents:
+ match = re.match('Linux32bitDBG_\d+', entry)
+ if match is not None:
+ return entry
+
+ raise CouldNotFindCoverageDirectory('Error: Found no 32-bit '
+ 'debug build in directory %s.' %
+ coverage_www_dir)
+
+
+def _grab_coverage_percentage(label, index_html_contents):
+ """Extracts coverage from a LCOV coverage report.
+
+ Grabs coverage by assuming that the label in the coverage HTML report
+ is close to the actual number and that the number is followed by a space
+ and a percentage sign.
+ """
+ match = re.search(']*>' + label + ' | .*?(\d+\.\d) %',
+ index_html_contents, re.DOTALL)
+ if match is None:
+ raise FailedToParseCoverageHtml('Missing coverage at label "%s".' % label)
+
+ try:
+ return float(match.group(1))
+ except ValueError:
+ raise FailedToParseCoverageHtml('%s is not a float.' % match.group(1))
+
+
+def _report_coverage_to_dashboard(dashboard, now, line_coverage,
+ function_coverage):
+ parameters = {'date': '%d' % now,
+ 'line_coverage': '%f' % line_coverage,
+ 'function_coverage': '%f' % function_coverage
+ }
+
+ response = dashboard.send_post_request(constants.ADD_COVERAGE_DATA_URL,
+ parameters)
+
+ # The response content should be empty on success, so check that:
+ response_content = response.read()
+ if response_content:
+ message = ('Error: Dashboard reported the following error: %s.' %
+ response_content)
+ raise dashboard_connection.FailedToReportToDashboard(message)
+
+
+def _main():
+ dashboard = dashboard_connection.DashboardConnection(constants.CONSUMER_KEY)
+ dashboard.read_required_files(constants.CONSUMER_SECRET_FILE,
+ constants.ACCESS_TOKEN_FILE)
+
+ coverage_www_dir = os.path.join('/home', constants.BUILD_BOT_USER, 'www')
+
+ www_dir_contents = os.listdir(coverage_www_dir)
+ latest_build_directory = _find_latest_32bit_debug_build(www_dir_contents,
+ coverage_www_dir)
+
+ index_html_path = os.path.join(coverage_www_dir, latest_build_directory,
+ 'index.html')
+ index_html_file = open(index_html_path)
+ whole_file = index_html_file.read()
+
+ line_coverage = _grab_coverage_percentage('Lines:', whole_file)
+ function_coverage = _grab_coverage_percentage('Functions:', whole_file)
+ now = int(time.time())
+
+ _report_coverage_to_dashboard(dashboard, now, line_coverage,
+ function_coverage)
+
+
+if __name__ == '__main__':
+ _main()
+