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:
parent
7fe219f681
commit
d4f0a0e2bc
@ -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)
|
@ -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()
|
||||
|
38
tools/quality_tracking/constants.py
Normal file
38
tools/quality_tracking/constants.py
Normal 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'
|
57
tools/quality_tracking/dashboard/add_build_status_data.py
Normal file
57
tools/quality_tracking/dashboard/add_build_status_data.py
Normal 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()
|
65
tools/quality_tracking/dashboard/add_coverage_data.py
Normal file
65
tools/quality_tracking/dashboard/add_coverage_data.py
Normal 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()
|
||||
|
73
tools/quality_tracking/dashboard/dashboard.py
Normal file
73
tools/quality_tracking/dashboard/dashboard.py
Normal 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)
|
@ -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)
|
137
tools/quality_tracking/dashboard_connection.py
Normal file
137
tools/quality_tracking/dashboard_connection.py
Normal 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
|
||||
|
@ -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()
|
90
tools/quality_tracking/track_build_status.py
Executable file
90
tools/quality_tracking/track_build_status.py
Executable 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()
|
130
tools/quality_tracking/track_coverage.py
Executable file
130
tools/quality_tracking/track_coverage.py
Executable 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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user