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() +