Cleaned up and completed current dashboard milestone.
Left to do: - Directory reorganization. BUG= TEST= Review URL: https://webrtc-codereview.appspot.com/384003 git-svn-id: http://webrtc.googlecode.com/svn/trunk@1605 4adac7df-926f-26a2-2b94-8c16560cd09d
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -39,6 +39,7 @@
|
|||||||
/third_party/asan
|
/third_party/asan
|
||||||
/third_party/cygwin
|
/third_party/cygwin
|
||||||
/third_party/expat
|
/third_party/expat
|
||||||
|
/third_party/gaeunit
|
||||||
/third_party/google-gflags/src
|
/third_party/google-gflags/src
|
||||||
/third_party/google-visualization-python
|
/third_party/google-visualization-python
|
||||||
/third_party/jsoncpp
|
/third_party/jsoncpp
|
||||||
|
10
DEPS
10
DEPS
@@ -84,13 +84,17 @@ deps = {
|
|||||||
"trunk/third_party/libyuv":
|
"trunk/third_party/libyuv":
|
||||||
(Var("googlecode_url") % "libyuv") + "/trunk@121",
|
(Var("googlecode_url") % "libyuv") + "/trunk@121",
|
||||||
|
|
||||||
# Used by tools/coverage/dashboard and tools/python_charts
|
# Used by tools/quality_tracking/dashboard and tools/python_charts
|
||||||
"trunk/third_party/google-visualization-python":
|
"trunk/third_party/google-visualization-python":
|
||||||
(Var("googlecode_url") % "google-visualization-python") + "/trunk@15",
|
(Var("googlecode_url") % "google-visualization-python") + "/trunk@15",
|
||||||
|
|
||||||
# Used by tools/coverage
|
# Used by tools/quality_tracking
|
||||||
"trunk/third_party/oauth2":
|
"trunk/third_party/oauth2":
|
||||||
"https://github.com/simplegeo/python-oauth2.git@a83f4a297336b631e75cba102910c19231518159"
|
"https://github.com/simplegeo/python-oauth2.git@a83f4a29",
|
||||||
|
|
||||||
|
# Used by tools/quality_tracking
|
||||||
|
"trunk/third_party/gaeunit":
|
||||||
|
"https://code.google.com/p/gaeunit.git@e16d5bd4",
|
||||||
}
|
}
|
||||||
|
|
||||||
deps_os = {
|
deps_os = {
|
||||||
|
@@ -12,6 +12,8 @@
|
|||||||
|
|
||||||
__author__ = 'phoglund@webrtc.org (Patrik Höglund)'
|
__author__ = 'phoglund@webrtc.org (Patrik Höglund)'
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
from google.appengine.ext import db
|
from google.appengine.ext import db
|
||||||
|
|
||||||
import oauth_post_request_handler
|
import oauth_post_request_handler
|
||||||
@@ -24,12 +26,13 @@ class OrphanedBuildStatusesExistException(Exception):
|
|||||||
|
|
||||||
|
|
||||||
class BuildStatusRoot(db.Model):
|
class BuildStatusRoot(db.Model):
|
||||||
"""Exists solely to be the root parent for all build status data.
|
"""Exists solely to be the root parent for all build status data and to keep
|
||||||
|
track of when the last update was made.
|
||||||
|
|
||||||
Since all build status data will refer to this as their parent,
|
Since all build status data will refer to this as their parent,
|
||||||
we can run transactions on the build status data as a whole.
|
we can run transactions on the build status data as a whole.
|
||||||
"""
|
"""
|
||||||
pass
|
last_updated_at = db.DateTimeProperty()
|
||||||
|
|
||||||
|
|
||||||
class BuildStatusData(db.Model):
|
class BuildStatusData(db.Model):
|
||||||
@@ -121,7 +124,7 @@ class AddBuildStatusData(oauth_post_request_handler.OAuthPostRequestHandler):
|
|||||||
combination. Now we will effectively update the bot's status instead.
|
combination. Now we will effectively update the bot's status instead.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def post(self):
|
def _parse_and_store_data(self):
|
||||||
build_status_root = _ensure_build_status_root_exists()
|
build_status_root = _ensure_build_status_root_exists()
|
||||||
build_status_data = _filter_oauth_parameters(self.request.arguments())
|
build_status_data = _filter_oauth_parameters(self.request.arguments())
|
||||||
|
|
||||||
@@ -130,6 +133,7 @@ class AddBuildStatusData(oauth_post_request_handler.OAuthPostRequestHandler):
|
|||||||
|
|
||||||
def _parse_and_store_data_in_transaction(self, build_status_root,
|
def _parse_and_store_data_in_transaction(self, build_status_root,
|
||||||
build_status_data):
|
build_status_data):
|
||||||
|
|
||||||
encountered_revisions = set()
|
encountered_revisions = set()
|
||||||
for revision_and_bot_name in build_status_data:
|
for revision_and_bot_name in build_status_data:
|
||||||
build_number_and_status = self.request.get(revision_and_bot_name)
|
build_number_and_status = self.request.get(revision_and_bot_name)
|
||||||
@@ -154,3 +158,8 @@ class AddBuildStatusData(oauth_post_request_handler.OAuthPostRequestHandler):
|
|||||||
build_number=build_number,
|
build_number=build_number,
|
||||||
status=status)
|
status=status)
|
||||||
item.put()
|
item.put()
|
||||||
|
|
||||||
|
request_posix_timestamp = float(self.request.get('oauth_timestamp'))
|
||||||
|
request_datetime = datetime.datetime.fromtimestamp(request_posix_timestamp)
|
||||||
|
build_status_root.last_updated_at = request_datetime
|
||||||
|
build_status_root.put()
|
||||||
|
@@ -43,7 +43,7 @@ class AddCoverageData(oauth_post_request_handler.OAuthPostRequestHandler):
|
|||||||
function_coverage: A float percentage in the interval 0-100.0.
|
function_coverage: A float percentage in the interval 0-100.0.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def post(self):
|
def _parse_and_store_data(self):
|
||||||
try:
|
try:
|
||||||
posix_time = int(self.request.get('date'))
|
posix_time = int(self.request.get('date'))
|
||||||
parsed_date = datetime.datetime.fromtimestamp(posix_time)
|
parsed_date = datetime.datetime.fromtimestamp(posix_time)
|
||||||
|
@@ -2,8 +2,13 @@ application: dashboard
|
|||||||
version: 1
|
version: 1
|
||||||
runtime: python27
|
runtime: python27
|
||||||
api_version: 1
|
api_version: 1
|
||||||
threadsafe: true
|
threadsafe: false
|
||||||
|
|
||||||
handlers:
|
handlers:
|
||||||
|
- url: /stylesheets
|
||||||
|
static_dir: stylesheets
|
||||||
|
# Note: tests should be disabled in production.
|
||||||
|
# - url: /test.*
|
||||||
|
# script: gaeunit.py
|
||||||
- url: /.*
|
- url: /.*
|
||||||
script: dashboard.app
|
script: dashboard.app
|
@@ -12,12 +12,13 @@
|
|||||||
|
|
||||||
__author__ = 'phoglund@webrtc.org (Patrik Höglund)'
|
__author__ = 'phoglund@webrtc.org (Patrik Höglund)'
|
||||||
|
|
||||||
from google.appengine.ext import db
|
from google.appengine.ext.webapp import template
|
||||||
import gviz_api
|
|
||||||
import webapp2
|
import webapp2
|
||||||
|
|
||||||
import add_build_status_data
|
import add_build_status_data
|
||||||
import add_coverage_data
|
import add_coverage_data
|
||||||
|
import load_build_status
|
||||||
|
import load_coverage
|
||||||
|
|
||||||
|
|
||||||
class ShowDashboard(webapp2.RequestHandler):
|
class ShowDashboard(webapp2.RequestHandler):
|
||||||
@@ -28,38 +29,18 @@ class ShowDashboard(webapp2.RequestHandler):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
page_template_filename = 'templates/dashboard_template.html'
|
build_status_loader = load_build_status.BuildStatusLoader()
|
||||||
|
build_status_data = build_status_loader.load_build_status_data()
|
||||||
|
last_updated_at = build_status_loader.load_last_modified_at()
|
||||||
|
last_updated_at = last_updated_at.strftime("%Y-%m-%d %H:%M")
|
||||||
|
lkgr = build_status_loader.compute_lkgr()
|
||||||
|
|
||||||
# Load the page HTML template.
|
coverage_loader = load_coverage.CoverageDataLoader()
|
||||||
try:
|
coverage_json_data = coverage_loader.load_coverage_json_data()
|
||||||
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:
|
# Fill in the template with the data and respond:
|
||||||
self.response.write(page_template % vars())
|
page_template_filename = 'templates/dashboard_template.html'
|
||||||
|
self.response.write(template.render(page_template_filename, vars()))
|
||||||
|
|
||||||
def _show_error_page(self, error_message):
|
def _show_error_page(self, error_message):
|
||||||
self.response.write('<html><body>%s</body></html>' % error_message)
|
self.response.write('<html><body>%s</body></html>' % error_message)
|
||||||
|
1
tools/quality_tracking/dashboard/gaeunit.py
Symbolic link
1
tools/quality_tracking/dashboard/gaeunit.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../../third_party/gaeunit/gaeunit.py
|
116
tools/quality_tracking/dashboard/load_build_status.py
Normal file
116
tools/quality_tracking/dashboard/load_build_status.py
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
#-*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2012 The WebRTC project authors. All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Use of this source code is governed by a BSD-style license
|
||||||
|
# that can be found in the LICENSE file in the root of the source
|
||||||
|
# tree. An additional intellectual property rights grant can be found
|
||||||
|
# in the file PATENTS. All contributing project authors may
|
||||||
|
# be found in the AUTHORS file in the root of the source tree.
|
||||||
|
|
||||||
|
"""Loads build status data for the dashboard."""
|
||||||
|
|
||||||
|
__author__ = 'phoglund@webrtc.org (Patrik Höglund)'
|
||||||
|
|
||||||
|
from google.appengine.ext import db
|
||||||
|
|
||||||
|
|
||||||
|
def _all_ok(statuses):
|
||||||
|
return filter(lambda status: status != "OK", statuses) == []
|
||||||
|
|
||||||
|
|
||||||
|
def _get_first_entry(iterable):
|
||||||
|
if not iterable:
|
||||||
|
return None
|
||||||
|
for item in iterable:
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
class BuildStatusLoader:
|
||||||
|
""" Loads various build status data from the database."""
|
||||||
|
|
||||||
|
def load_build_status_data(self):
|
||||||
|
"""Returns the latest conclusive build status for each bot.
|
||||||
|
|
||||||
|
The statuses OK or failed are considered to be conclusive.
|
||||||
|
|
||||||
|
The two most recent revisions are considered. The set of bots returned
|
||||||
|
will therefore be the bots that were reported the two most recent
|
||||||
|
revisions. This script will therefore adapt automatically to any changes
|
||||||
|
in the set of available bots.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list of BuildStatusData entities with one entity per bot.
|
||||||
|
"""
|
||||||
|
|
||||||
|
build_status_entries = db.GqlQuery('SELECT * '
|
||||||
|
'FROM BuildStatusData '
|
||||||
|
'ORDER BY revision DESC ')
|
||||||
|
|
||||||
|
bots_to_latest_conclusive_entry = dict()
|
||||||
|
for entry in build_status_entries:
|
||||||
|
if entry.status == "building":
|
||||||
|
# The 'building' status it not conclusive, so discard this entry and
|
||||||
|
# pick up the entry for this bot on the next revision instead. That
|
||||||
|
# entry is guaranteed to have a status != 'building' since a bot cannot
|
||||||
|
# be building two revisions simultaneously.
|
||||||
|
continue
|
||||||
|
if bots_to_latest_conclusive_entry.has_key(entry.bot_name):
|
||||||
|
# We've already determined this bot's status.
|
||||||
|
continue
|
||||||
|
|
||||||
|
bots_to_latest_conclusive_entry[entry.bot_name] = entry
|
||||||
|
|
||||||
|
return bots_to_latest_conclusive_entry.values()
|
||||||
|
|
||||||
|
def load_last_modified_at(self):
|
||||||
|
build_status_root = db.GqlQuery('SELECT * '
|
||||||
|
'FROM BuildStatusRoot').get()
|
||||||
|
if not build_status_root:
|
||||||
|
# Operating on completely empty database
|
||||||
|
return None
|
||||||
|
|
||||||
|
return build_status_root.last_updated_at
|
||||||
|
|
||||||
|
def compute_lkgr(self):
|
||||||
|
""" Finds the most recent revision for which all bots are green.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The last known good revision (as an integer) or None if there
|
||||||
|
is no green revision in the database.
|
||||||
|
|
||||||
|
Implementation note: The data store fetches stuff as we go, so we won't
|
||||||
|
read in the whole status table unless the LKGR is right at the end or
|
||||||
|
we don't have a LKGR.
|
||||||
|
"""
|
||||||
|
build_status_entries = db.GqlQuery('SELECT * '
|
||||||
|
'FROM BuildStatusData '
|
||||||
|
'ORDER BY revision DESC ')
|
||||||
|
|
||||||
|
first_entry = _get_first_entry(build_status_entries)
|
||||||
|
if first_entry is None:
|
||||||
|
# No entries => no LKGR
|
||||||
|
return None
|
||||||
|
|
||||||
|
current_lkgr = first_entry.revision
|
||||||
|
statuses_for_current_lkgr = [first_entry.status]
|
||||||
|
|
||||||
|
for entry in build_status_entries:
|
||||||
|
if current_lkgr == entry.revision:
|
||||||
|
statuses_for_current_lkgr.append(entry.status)
|
||||||
|
else:
|
||||||
|
# Starting on new revision, check previous revision.
|
||||||
|
if _all_ok(statuses_for_current_lkgr):
|
||||||
|
# All bots are green; LKGR found.
|
||||||
|
return current_lkgr
|
||||||
|
else:
|
||||||
|
# Not all bots are green, so start over on the next revision.
|
||||||
|
current_lkgr = entry.revision
|
||||||
|
statuses_for_current_lkgr = [entry.status]
|
||||||
|
|
||||||
|
if _all_ok(statuses_for_current_lkgr):
|
||||||
|
# There was only one revision and it was OK.
|
||||||
|
return current_lkgr
|
||||||
|
|
||||||
|
# There is no all-green revision in the database.
|
||||||
|
return None
|
39
tools/quality_tracking/dashboard/load_coverage.py
Normal file
39
tools/quality_tracking/dashboard/load_coverage.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
#-*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2012 The WebRTC project authors. All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Use of this source code is governed by a BSD-style license
|
||||||
|
# that can be found in the LICENSE file in the root of the source
|
||||||
|
# tree. An additional intellectual property rights grant can be found
|
||||||
|
# in the file PATENTS. All contributing project authors may
|
||||||
|
# be found in the AUTHORS file in the root of the source tree.
|
||||||
|
|
||||||
|
"""Loads coverage data from the database."""
|
||||||
|
|
||||||
|
__author__ = 'phoglund@webrtc.org (Patrik Höglund)'
|
||||||
|
|
||||||
|
from google.appengine.ext import db
|
||||||
|
import gviz_api
|
||||||
|
|
||||||
|
|
||||||
|
class CoverageDataLoader:
|
||||||
|
""" Loads coverage data from the database."""
|
||||||
|
|
||||||
|
def load_coverage_json_data(self):
|
||||||
|
coverage_entries = db.GqlQuery('SELECT * '
|
||||||
|
'FROM CoverageData '
|
||||||
|
'ORDER BY date ASC')
|
||||||
|
data = []
|
||||||
|
for coverage_entry in coverage_entries:
|
||||||
|
data.append({'date': coverage_entry.date,
|
||||||
|
'line_coverage': coverage_entry.line_coverage,
|
||||||
|
'function_coverage': coverage_entry.function_coverage,
|
||||||
|
})
|
||||||
|
|
||||||
|
description = {
|
||||||
|
'date': ('datetime', 'Date'),
|
||||||
|
'line_coverage': ('number', 'Line Coverage'),
|
||||||
|
'function_coverage': ('number', 'Function Coverage')
|
||||||
|
}
|
||||||
|
coverage_data = gviz_api.DataTable(description, data)
|
||||||
|
return coverage_data.ToJSon(order_by='date')
|
40
tools/quality_tracking/dashboard/stylesheets/stylesheet.css
Normal file
40
tools/quality_tracking/dashboard/stylesheets/stylesheet.css
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
/********************************************************************
|
||||||
|
*
|
||||||
|
* Copyright (c) 2012 The WebRTC project authors. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by a BSD-style license
|
||||||
|
* that can be found in the LICENSE file in the root of the source
|
||||||
|
* tree. An additional intellectual property rights grant can be found
|
||||||
|
* in the file PATENTS. All contributing project authors may
|
||||||
|
* be found in the AUTHORS file in the root of the source tree.
|
||||||
|
*
|
||||||
|
*********************************************************************/
|
||||||
|
|
||||||
|
.status_OK {
|
||||||
|
color: #FFFFFF;
|
||||||
|
background-color: #8fdf5f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status_failed {
|
||||||
|
color: #FFFFFF;
|
||||||
|
background-color: #e98080;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status_building {
|
||||||
|
color: #666666;
|
||||||
|
background-color: #fffc6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.last_known_good_revision {
|
||||||
|
font-size: 800%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status_cell {
|
||||||
|
width: 100px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin-left: 35px;
|
||||||
|
margin-top: 25px;
|
||||||
|
}
|
@@ -1,6 +1,6 @@
|
|||||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
|
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
|
||||||
"http://www.w3.org/TR/html4/strict.dtd">
|
"http://www.w3.org/TR/html4/strict.dtd">
|
||||||
<html>
|
<html>
|
||||||
<!--
|
<!--
|
||||||
Copyright (c) 2012 The WebRTC project authors. All Rights Reserved.
|
Copyright (c) 2012 The WebRTC project authors. All Rights Reserved.
|
||||||
|
|
||||||
@@ -16,16 +16,24 @@
|
|||||||
<title>WebRTC Coverage Dashboard</title>
|
<title>WebRTC Coverage Dashboard</title>
|
||||||
<link href="http://code.google.com/css/codesite.pack.04102009.css"
|
<link href="http://code.google.com/css/codesite.pack.04102009.css"
|
||||||
rel="stylesheet" type="text/css">
|
rel="stylesheet" type="text/css">
|
||||||
|
<link href="stylesheets/stylesheet.css"
|
||||||
|
rel="stylesheet" type="text/css">
|
||||||
|
|
||||||
<script src="https://www.google.com/jsapi" type="text/javascript"></script>
|
<script src="https://www.google.com/jsapi" type="text/javascript"></script>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
google.load('visualization', '1', {packages:['table', 'corechart']});
|
google.load('visualization', '1', {packages:['table', 'corechart']});
|
||||||
|
|
||||||
google.setOnLoadCallback(drawTable);
|
google.setOnLoadCallback(drawTable);
|
||||||
function drawTable() {
|
function drawTable() {
|
||||||
/* Build data tables and views */
|
/* Build data tables and views */
|
||||||
|
{% comment %}
|
||||||
|
Disable Django auto-escaping here since that will mess up our
|
||||||
|
coverage table JSON data otherwise.
|
||||||
|
{% endcomment %}
|
||||||
|
{% autoescape off %}
|
||||||
var coverage_data_table =
|
var coverage_data_table =
|
||||||
new google.visualization.DataTable(%(coverage_json_data)s);
|
new google.visualization.DataTable({{ coverage_json_data }});
|
||||||
|
{% endautoescape %}
|
||||||
|
|
||||||
/* Display tables and charts */
|
/* Display tables and charts */
|
||||||
var coverage_chart = new google.visualization.LineChart(
|
var coverage_chart = new google.visualization.LineChart(
|
||||||
@@ -40,8 +48,38 @@
|
|||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>WebRTC Dashboard</h1>
|
|
||||||
<h3>Coverage:</h3>
|
<h1>WebRTC Quality Dashboard</h1>
|
||||||
|
<h2>Current Build Status</h2>
|
||||||
|
<div>(as of {{ last_updated_at }})</div>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
{% for entry in build_status_data %}
|
||||||
|
<th class="status_cell">{{ entry.bot_name }}</th>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
{% for entry in build_status_data %}
|
||||||
|
<td title="Last built revision {{ entry.revision }}"
|
||||||
|
class="status_cell status_{{entry.status}}">
|
||||||
|
{{entry.status}}
|
||||||
|
</td>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<p></p>
|
||||||
|
|
||||||
|
<h2>Last Known Good Revision (LKGR)</h2>
|
||||||
|
<div class="last_known_good_revision">
|
||||||
|
{% if lkgr %}
|
||||||
|
<a href="http://code.google.com/p/webrtc/source/detail?r={{ lkgr }}">
|
||||||
|
{{ lkgr }}</a>
|
||||||
|
{% else %}
|
||||||
|
????
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Code Coverage History</h2>
|
||||||
<div id="table_div_coverage"></div>
|
<div id="table_div_coverage"></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
114
tools/quality_tracking/dashboard/test/load_build_status_test.py
Executable file
114
tools/quality_tracking/dashboard/test/load_build_status_test.py
Executable file
@@ -0,0 +1,114 @@
|
|||||||
|
#!/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.
|
||||||
|
|
||||||
|
__author__ = 'phoglund@webrtc.org (Patrik Höglund)'
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from google.appengine.ext import db
|
||||||
|
from google.appengine.ext import testbed
|
||||||
|
|
||||||
|
from add_build_status_data import BuildStatusData
|
||||||
|
import load_build_status
|
||||||
|
|
||||||
|
class LoadBuildStatusTest(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
# First, create an instance of the Testbed class.
|
||||||
|
self.testbed = testbed.Testbed()
|
||||||
|
# Then activate the testbed, which prepares the service stubs for use.
|
||||||
|
self.testbed.activate()
|
||||||
|
# Next, declare which service stubs you want to use.
|
||||||
|
self.testbed.init_datastore_v3_stub()
|
||||||
|
|
||||||
|
def test_returns_latest_nonbuilding_entries_when_loading_build_status(self):
|
||||||
|
BuildStatusData(bot_name="Bot1", revision=17,
|
||||||
|
build_number=499, status="OK").put()
|
||||||
|
BuildStatusData(bot_name="Bot2", revision=17,
|
||||||
|
build_number=505, status="OK").put()
|
||||||
|
BuildStatusData(bot_name="Bot3", revision=17,
|
||||||
|
build_number=344, status="failed").put()
|
||||||
|
BuildStatusData(bot_name="Bot1", revision=18,
|
||||||
|
build_number=499, status="building").put()
|
||||||
|
BuildStatusData(bot_name="Bot2", revision=18,
|
||||||
|
build_number=505, status="failed").put()
|
||||||
|
BuildStatusData(bot_name="Bot3", revision=18,
|
||||||
|
build_number=344, status="OK").put()
|
||||||
|
|
||||||
|
loader = load_build_status.BuildStatusLoader()
|
||||||
|
result = loader.load_build_status_data()
|
||||||
|
|
||||||
|
self.assertEqual(3, len(result))
|
||||||
|
|
||||||
|
# We make no guarantees on order, but we can use the fact that the testbed
|
||||||
|
# is deterministic to evaluate that the corrects bots were selected like so:
|
||||||
|
self.assertEqual("Bot1", result[0].bot_name)
|
||||||
|
self.assertEqual(17, result[0].revision)
|
||||||
|
self.assertEqual("OK", result[0].status)
|
||||||
|
|
||||||
|
self.assertEqual("Bot3", result[1].bot_name)
|
||||||
|
self.assertEqual(18, result[1].revision)
|
||||||
|
self.assertEqual("OK", result[1].status)
|
||||||
|
|
||||||
|
self.assertEqual("Bot2", result[2].bot_name)
|
||||||
|
self.assertEqual(18, result[2].revision)
|
||||||
|
self.assertEqual("failed", result[2].status)
|
||||||
|
|
||||||
|
def test_returns_lkgr_for_single_green_revision(self):
|
||||||
|
BuildStatusData(bot_name="Bot1", revision=17,
|
||||||
|
build_number=499, status="OK").put()
|
||||||
|
BuildStatusData(bot_name="Bot2", revision=17,
|
||||||
|
build_number=505, status="OK").put()
|
||||||
|
BuildStatusData(bot_name="Bot3", revision=17,
|
||||||
|
build_number=344, status="OK").put()
|
||||||
|
|
||||||
|
loader = load_build_status.BuildStatusLoader()
|
||||||
|
self.assertEqual(17, loader.compute_lkgr())
|
||||||
|
|
||||||
|
def test_returns_correct_lkgr_with_most_recent_revision_failed(self):
|
||||||
|
BuildStatusData(bot_name="Bot1", revision=17,
|
||||||
|
build_number=499, status="OK").put()
|
||||||
|
BuildStatusData(bot_name="Bot2", revision=17,
|
||||||
|
build_number=505, status="OK").put()
|
||||||
|
BuildStatusData(bot_name="Bot3", revision=17,
|
||||||
|
build_number=344, status="OK").put()
|
||||||
|
BuildStatusData(bot_name="Bot1", revision=18,
|
||||||
|
build_number=499, status="OK").put()
|
||||||
|
BuildStatusData(bot_name="Bot2", revision=18,
|
||||||
|
build_number=505, status="failed").put()
|
||||||
|
BuildStatusData(bot_name="Bot3", revision=18,
|
||||||
|
build_number=344, status="OK").put()
|
||||||
|
|
||||||
|
loader = load_build_status.BuildStatusLoader()
|
||||||
|
self.assertEqual(17, loader.compute_lkgr())
|
||||||
|
|
||||||
|
def test_returns_none_if_no_revisions(self):
|
||||||
|
loader = load_build_status.BuildStatusLoader()
|
||||||
|
self.assertEqual(None, loader.compute_lkgr())
|
||||||
|
|
||||||
|
def test_returns_none_if_no_green_revisions(self):
|
||||||
|
BuildStatusData(bot_name="Bot2", revision=18,
|
||||||
|
build_number=505, status="failed").put()
|
||||||
|
|
||||||
|
loader = load_build_status.BuildStatusLoader()
|
||||||
|
self.assertEqual(None, loader.compute_lkgr())
|
||||||
|
|
||||||
|
def test_skips_partially_building_revisions(self):
|
||||||
|
BuildStatusData(bot_name="Bot1", revision=18,
|
||||||
|
build_number=499, status="building").put()
|
||||||
|
BuildStatusData(bot_name="Bot2", revision=18,
|
||||||
|
build_number=505, status="OK").put()
|
||||||
|
BuildStatusData(bot_name="Bot1", revision=17,
|
||||||
|
build_number=344, status="OK").put()
|
||||||
|
|
||||||
|
loader = load_build_status.BuildStatusLoader()
|
||||||
|
self.assertEqual(17, loader.compute_lkgr())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
@@ -67,17 +67,18 @@ class DashboardConnection:
|
|||||||
make the OAuth request. These concepts are described in the class
|
make the OAuth request. These concepts are described in the class
|
||||||
description.
|
description.
|
||||||
|
|
||||||
|
The server is expected to respond with HTTP status 200 and a completely
|
||||||
|
empty response if the call failed. The server may put diagnostic
|
||||||
|
information in the response.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
sub_url: A relative url within the dashboard domain, for example
|
sub_url: A relative url within the dashboard domain, for example
|
||||||
/add_coverage_data.
|
/add_coverage_data.
|
||||||
parameters: A dict which maps from POST parameter names to values.
|
parameters: A dict which maps from POST parameter names to values.
|
||||||
|
|
||||||
Returns:
|
|
||||||
A httplib response object.
|
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
FailedToReportToDashboard: If the dashboard didn't respond
|
FailedToReportToDashboard: If the dashboard didn't respond
|
||||||
with HTTP 200 to our request.
|
with HTTP 200 to our request or if the response is non-empty.
|
||||||
"""
|
"""
|
||||||
consumer = oauth.OAuthConsumer(self.consumer_key_, self.consumer_secret_)
|
consumer = oauth.OAuthConsumer(self.consumer_key_, self.consumer_secret_)
|
||||||
create_oauth_request = oauth.OAuthRequest.from_consumer_and_token
|
create_oauth_request = oauth.OAuthRequest.from_consumer_and_token
|
||||||
@@ -101,12 +102,17 @@ class DashboardConnection:
|
|||||||
connection.close()
|
connection.close()
|
||||||
|
|
||||||
if response.status != 200:
|
if response.status != 200:
|
||||||
message = ('Error: Failed to report to %s%s: got response %d (%s)' %
|
message = ('Failed to report to %s%s: got response %d (%s)' %
|
||||||
(constants.DASHBOARD_SERVER, sub_url, response.status,
|
(constants.DASHBOARD_SERVER, sub_url, response.status,
|
||||||
response.reason))
|
response.reason))
|
||||||
raise FailedToReportToDashboard(message)
|
raise FailedToReportToDashboard(message)
|
||||||
|
|
||||||
return response
|
# The response content should be empty on success, so check that:
|
||||||
|
response_content = response.read()
|
||||||
|
if response_content:
|
||||||
|
message = ('Dashboard reported the following error: %s.' %
|
||||||
|
response_content)
|
||||||
|
raise FailedToReportToDashboard(message)
|
||||||
|
|
||||||
def _read_access_token(self, filename):
|
def _read_access_token(self, filename):
|
||||||
input_file = shelve.open(filename)
|
input_file = shelve.open(filename)
|
||||||
|
@@ -36,6 +36,7 @@ import oauth2 as oauth
|
|||||||
|
|
||||||
import constants
|
import constants
|
||||||
|
|
||||||
|
|
||||||
class FailedToRequestPermissionException(Exception):
|
class FailedToRequestPermissionException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@@ -66,7 +66,7 @@ def parse_tgrid_page(html):
|
|||||||
result.update(_parse_builds(revision, builds_for_revision_html))
|
result.update(_parse_builds(revision, builds_for_revision_html))
|
||||||
|
|
||||||
if not result:
|
if not result:
|
||||||
raise FailedToParseBuildStatus('Could not find any build statuses in %s.' %
|
raise FailedToParseBuildStatus('Could not find any build statuses in %s.' %
|
||||||
html)
|
html)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
@@ -16,7 +16,6 @@ __author__ = 'phoglund@webrtc.org (Patrik Höglund)'
|
|||||||
|
|
||||||
|
|
||||||
import httplib
|
import httplib
|
||||||
import re
|
|
||||||
|
|
||||||
import constants
|
import constants
|
||||||
import dashboard_connection
|
import dashboard_connection
|
||||||
@@ -50,10 +49,9 @@ def _main():
|
|||||||
|
|
||||||
bot_to_status_mapping = _download_and_parse_build_status()
|
bot_to_status_mapping = _download_and_parse_build_status()
|
||||||
|
|
||||||
response = dashboard.send_post_request(constants.ADD_BUILD_STATUS_DATA_URL,
|
dashboard.send_post_request(constants.ADD_BUILD_STATUS_DATA_URL,
|
||||||
bot_to_status_mapping)
|
bot_to_status_mapping)
|
||||||
|
|
||||||
print response.read()
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
_main()
|
_main()
|
||||||
|
@@ -28,7 +28,6 @@ __author__ = 'phoglund@webrtc.org (Patrik Höglund)'
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import constants
|
import constants
|
||||||
@@ -90,15 +89,7 @@ def _report_coverage_to_dashboard(dashboard, now, line_coverage,
|
|||||||
'function_coverage': '%f' % function_coverage
|
'function_coverage': '%f' % function_coverage
|
||||||
}
|
}
|
||||||
|
|
||||||
response = dashboard.send_post_request(constants.ADD_COVERAGE_DATA_URL,
|
dashboard.send_post_request(constants.ADD_COVERAGE_DATA_URL, parameters)
|
||||||
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():
|
def _main():
|
||||||
|
Reference in New Issue
Block a user