Refactored the dashboard in order to add new functionality and added some new functionality.

Note that all files were moved to a new directory. The diffs won't be 100% friendly because of this.

Extracted common handling for OAuth on both sides of the connection in order to add a new build status
data handler. This data handler will be used to report build status data. Don't look too closely at the
details of what data is transferred as this will change in the next patch. We will also extract data from
a different page in a slightly different way, but there won't be huge differences.

In particular, we won't look at the /one_box_per_builder page on the master but rather at the transposed
grid (/tgrid) on the build master since we also need the revision number. The regular expressions to
extract the data will be slightly more complex.

BUG=
TEST=

Review URL: https://webrtc-codereview.appspot.com/367023

git-svn-id: http://webrtc.googlecode.com/svn/trunk@1586 4adac7df-926f-26a2-2b94-8c16560cd09d
This commit is contained in:
phoglund@webrtc.org 2012-02-01 10:59:23 +00:00
parent 7fe219f681
commit d4f0a0e2bc
17 changed files with 673 additions and 335 deletions

View File

@ -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<br>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('<html><body>%s</body></html>' % 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('<html><body>%s</body></html>' % error_message)
app = webapp2.WSGIApplication([('/', ShowDashboard),
('/add_coverage_data', AddCoverageData)],
debug=True)

View File

@ -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/<build bot user>/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_<number>. 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('<td[^>]*>' + label + '</td>.*?(\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()

View File

@ -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'

View File

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

View File

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

View File

@ -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<br>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('<html><body>%s</body></html>' % error_message)
app = webapp2.WSGIApplication([('/', ShowDashboard),
('/add_coverage_data',
add_coverage_data.AddCoverageData),
('/add_build_status_data',
add_build_status_data.AddBuildStatusData)],
debug=True)

View File

@ -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('<html><body>%s</body></html>' % error_message)

View File

@ -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

View File

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

View File

@ -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: <a href="builders/Win32Debug/builds/430">#430</a>
# <br />build<br />successful</td>
# 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('<a href="builders/([^/]*)/builds/([0-9]+)">'
'.*?(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()

View File

@ -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/<build bot user>/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_<number>. 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('<td[^>]*>' + label + '</td>.*?(\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()