blob: e4c6d989a8e5eeccf48c7dbc268dc9c17e81b18c [file] [log] [blame]
#!/usr/bin/python
# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Views to support the urls defined in urls.py.
Modeled after the Django views.py file but with the benefits of bottlepy.
"""
import cgi, collections
import bottle
import whining_errors
import wmutils
from models import named_filters
from models import pivot_table
from models import whining_model
from src import settings
def catch_whining_errors(fn):
"""@Decorator to catch & handle frequent errors (e.g. bad user input)."""
def wrapped(*args, **kwargs):
try:
return fn(*args, **kwargs)
except whining_errors.WhiningError as e:
def dummy_query_string(add={}, remove=[], list_qps=False):
if list_qps:
return []
return ''
exception_message = str(e)
return bottle.template('error_view', tpl_vars=locals(),
query_string=dummy_query_string)
return wrapped
def static_file(file, root):
"""Serve a static file from /static."""
return bottle.static_file(file, root)
@catch_whining_errors
def render_builds():
"""Serve a lookup view which is a list of known builds."""
db = whining_model.model_factory(bottle.request)
builds = db.get_builds(hyperlinks=True)
filter_tag = 'lookups'
return bottle.template('builds_view', tpl_vars=locals(),
query_string=db.query_string)
@catch_whining_errors
def render_comments():
db = whining_model.model_factory(bottle.request)
data = db.get_comments()
return bottle.template('comment_view', tpl_vars=locals(),
query_string=db.query_string)
@catch_whining_errors
def post_comment():
comment = bottle.request.forms.get('comment')
test_id = bottle.request.forms.get('test_id')
db = whining_model.model_factory(bottle.request)
db.run_query_cmd("CALL add_triage_comment('%s', '%s')" % (test_id, comment))
return
@catch_whining_errors
def render_dashboard(filter_tag):
"""Serve our main aggregate view of summary + failures."""
db = whining_model.model_factory(bottle.request)
releases = db.get_releases()
return bottle.template('dash_view', tpl_vars=locals(),
query_string=db.query_string)
def render_debug():
"""Serve debug data for troubleshooting."""
db = whining_model.model_factory(bottle.request)
# Insert anything here to display it and debug issues.
test_results = {}
return bottle.template('debug_view', tpl_vars=locals(),
query_string=db.query_string)
@catch_whining_errors
def render_summary(filter_tag):
"""Serve a summary view of recent test failures."""
db = whining_model.model_factory(bottle.request)
releases = db.get_releases()
data = db.get_summary(filter_tag)
return bottle.template('summary_view', tpl_vars=locals(),
query_string=db.query_string)
@catch_whining_errors
def render_filters():
"""Serve filter data to aid filter understanding."""
db = whining_model.model_factory(bottle.request)
filters = named_filters.NAMED_FILTERS
return bottle.template('filter_view', tpl_vars=locals(),
query_string=db.query_string)
@catch_whining_errors
def render_help():
"""Serve a list of valid urls to aid users."""
db = whining_model.model_factory(bottle.request)
filter_tag = named_filters.default_filter()
known_qvars = ', '.join(sorted(named_filters.KNOWN_QVARS))
filter_names = sorted(named_filters.NAMED_FILTERS.keys())
return bottle.template('urlhelp', tpl_vars=locals(),
query_string=db.query_string)
@catch_whining_errors
def render_jobs(filter_tag):
db = whining_model.model_factory(bottle.request)
data = db.get_jobs(filter_tag)
return bottle.template('jobs_view', tpl_vars=locals(),
query_string=db.query_string)
@catch_whining_errors
def render_modems():
"""Serve a modems view for 3G tests."""
db = whining_model.model_factory(bottle.request)
data = db.get_modems()
filter_tag = '3g'
return bottle.template('modems_view', tpl_vars=locals(),
query_string=db.query_string)
@catch_whining_errors
def render_platform(filter_tag):
db = whining_model.model_factory(bottle.request)
releases = db.get_releases()
data = db.get_platform_summary(filter_tag)
return bottle.template('platform_view', tpl_vars=locals(),
query_string=db.query_string)
@catch_whining_errors
def render_platforms():
"""Serve a lookup view which is a list of known platforms."""
db = whining_model.model_factory(bottle.request)
platforms = db.get_platforms(hyperlinks=True)
filter_tag = 'lookups'
return bottle.template('platforms_view', tpl_vars=locals(),
query_string=db.query_string)
@catch_whining_errors
def render_releases(filter_tag):
"""Serve a lookup view which is a list of known builds."""
db = whining_model.model_factory(bottle.request)
data = db.get_releases(hyperlinks=True)
return bottle.template('releases_view', tpl_vars=locals(),
query_string=db.query_string)
@catch_whining_errors
def render_suites():
"""Serve a lookup view which is a list of known suites."""
db = whining_model.model_factory(bottle.request)
suites = db.get_suites(hyperlinks=True)
filter_tag = 'lookups'
return bottle.template('suites_view', tpl_vars=locals(),
query_string=db.query_string)
@catch_whining_errors
def render_suite_stats(filter_tag):
"""Serve a lookup view which is a table of suite statistics."""
db = whining_model.model_factory(bottle.request)
data = db.get_suite_stats(filter_tag)
return bottle.template('suitestats_view', tpl_vars=locals(),
query_string=db.query_string)
@catch_whining_errors
def render_matrix(filter_tag):
"""Serve a summary matrix of recent test results."""
db = whining_model.model_factory(bottle.request)
releases = db.get_releases()
data = db.get_matrix(filter_tag)
return bottle.template('matrix_view', tpl_vars=locals(),
query_string=db.query_string)
@catch_whining_errors
def render_temp_filter():
"""Serve a page that can create a temporary filter (url).
Produces a custom URL. Does not add any filtes to named_filters
at this time.
"""
def _make_list_string(list_obj):
return "['%s']" % "', '".join(str(x) for x in list_obj)
db = whining_model.model_factory(bottle.request)
builds = _make_list_string(db.get_builds())
platforms = _make_list_string(db.get_platforms())
releases = _make_list_string([str(r) for r in db.get_releases()])
suites = _make_list_string(db.get_suites())
# The angular js code for handling object queries gets overwhelmed
# by the large number of tests in each of these suites. We do not
# show them separately in the searchable criteria.
#| SUITE | #tests |
#| wificell | 143 |
#| pyauto | 192 |
#| sync | 261 |
#| browsertests | 3033 |
tests = _make_list_string(db.get_tests(
exclude_suites=['browsertests', 'pyauto', 'sync', 'wificell']))
return bottle.template('temp_filter_view', tpl_vars=locals(),
query_string=db.query_string)
@catch_whining_errors
def render_tests():
"""Serve a lookup view which is a list of known tests."""
db = whining_model.model_factory(bottle.request)
tests = db.get_tests(hyperlinks=True)
filter_tag = 'lookups'
return bottle.template('tests_view', tpl_vars=locals(),
query_string=db.query_string)
@catch_whining_errors
def render_testrun(filter_tag):
"""Serve a view with full details of a test run."""
db = whining_model.model_factory(bottle.request)
test_runs = db.get_testrun(filter_tag)
status_classes = collections.defaultdict(lambda:'warning_summary')
status_classes.update({'GOOD': 'success', 'FAIL': 'failure'})
host = settings.settings.autotest_host
user = settings.settings.autotest_user
return bottle.template('testrun_view', tpl_vars=locals(),
query_string=db.query_string)
@catch_whining_errors
def render_failures(filter_tag):
"""Serve a list of recent failures."""
db = whining_model.model_factory(bottle.request)
releases = db.get_releases()
data = db.get_failures(filter_tag)
return bottle.template('failures_view', tpl_vars=locals(),
query_string=db.query_string)
@catch_whining_errors
def redirect_to_testrun_log(filter_tag):
"""Redirect to the link of a test run's log."""
db = whining_model.model_factory(bottle.request)
# A test job may have multiple runs, this function redirect to the log url
# of the test job, not individual test runs.
test_runs = db.get_testrun(filter_tag)
log_url = ('%s/tko/retrieve_logs.cgi?job=/results/%s-%s' %
(settings.settings.autotest_host, test_runs[0]['afe_job_id'],
settings.settings.autotest_user))
# Redirect to the test log if there is only 1 test run.
if len(test_runs) == 1:
log_url += ('/%s/%s' %
(test_runs[0]['hostname'], test_runs[0]['test_name']))
return bottle.redirect(log_url)
@catch_whining_errors
def render_suitehealth():
"""A suite health matrix - failure rates per suite per platform."""
db = whining_model.model_factory(bottle.request)
releases = db.get_releases()
filter_tag = ''
pt = db.get_suitehealth()
# Sort suites and platforms by failure rates.
suites = pt.keylists['suite']
suites.sort(reverse=True, key=lambda s: pt.data['suite'][s].rate)
platforms = pt.keylists['platform']
platforms.sort()
# Put all the data into an HTML table generator.
tbl = pivot_table.HtmlTable(suites, platforms, pt.data['sp'])
qs_add = {}
qs_add['days_back'] = pt.days_back
qs = db.query_string(add=qs_add).strip('?')
rh_tpl = ('<th>'
'<a href="%s/unfiltered?%s&suites={0}">matrix</a>, '
'<a href="%s/testhealth/unfiltered?%s&suites={0}">'
'tests</a> &#9679; {0}</th>'
) % (settings.settings.relative_root, qs,
settings.settings.relative_root, qs)
tbl.format_row_header = rh_tpl.format
tbl.row_headers.append(pt.data['suite'])
tbl.col_headers.append(pt.data['platform'])
totals = pt.data['totals'][tuple()]
tbl.title_class = 'centered headeritem'
days_str = ' in the last %d days' % pt.days_back
title = ('Failure rates per suite. (Global failure rate: {} - '
'{:,} out of {:,} runs{})'
)
tbl.title = title.format(totals.str_rate,
totals.bad_count,
totals.all_count,
days_str,
)
# Secondary headers - 2nd row and 2nd column of the table.
header_template = (
'<td>\n'
' <div title="{bad_count}/{all_count}"> {str_rate} </div>\n'
'</td>\n'
)
for cell in pt.cell_iterator(['suite', 'platform']):
cell.template = header_template
# Format inner cells with links and color heatmap.
for cell in pt.data['sp'].values():
color = wmutils.logcolor(cell.rate)
platform_href = db.make_href('platform',
'unfiltered',
suites=cell.suite,
platforms=cell.platform,
days_back=pt.days_back,
)
cell.html = (
'<td style="background:%s"><a href="%s">%s</a></td>'
% (color, platform_href, cell.str_rate)
)
return bottle.template('generic_table_view',
view='suitehealth',
title='Suite Health',
tpl_vars=locals(),
query_string=db.query_string,
)
@catch_whining_errors
def render_testhealth(filter_tag):
"""Show most frequently failing tests."""
db = whining_model.model_factory(bottle.request)
releases = db.get_releases()
pt = db.get_testhealth(filter_tag)
# Sort suites and platforms by failure rates.
tests = pt.keylists['test']
tests.sort(reverse=True, key=lambda t: pt.data['test'][t].rate)
platforms = pt.keylists['platform']
platforms.sort()
# HTML templates for table cells.
header_template = (
'<td>\n'
' <div title="{bad_count}/{all_count}"> {str_rate} </div>\n'
'</td>\n'
)
for cell in pt.cell_iterator(['test', 'platform']):
cell.template = header_template
inner_cell_template = ''.join([
'\n<td style="background:{color}">\n',
' <div title="{bad_count}/{all_count}"> {str_rate} </div>\n',
'</td>\n',
])
# Calculate colors based on failure rate.
for cell in pt.data['tp'].values():
cell.template = inner_cell_template
cell.color = wmutils.logcolor(cell.rate)
# Function for HTML formatting cells that are row headers as:
# "matrix, reasons * suite/test_name" with links matrix and reasons views
def format_test_name(test):
suite, test_name = test.split('/', 1)
href_matrix = db.make_href('matrix', filter_tag,
tests=test_name, suites=suite)
href_reasons = db.make_href('reasons',
tests=test_name, suites=suite)
extra = ' class="experimental"' if pt.data['test'][test].is_exp else ''
td = (
'<th%s><a href="%s">matrix</a>, <a href="%s">reasons</a> '
'&#9679; %s</th>'
) % (extra, href_matrix, href_reasons, test)
return td
# Put all the data into an HTML table generator.
tbl = pivot_table.HtmlTable(tests, platforms, pt.data['tp'])
tbl.format_row_header = format_test_name
tbl.row_headers.append(pt.data['test'])
tbl.col_headers.append(pt.data['platform'])
tbl.title_class = 'centered headeritem'
tbl.title = 'Test failure rates for the last %s days' % pt.days_back
return bottle.template('generic_table_view',
view='testhealth',
title='Test Failure Rates',
tpl_vars=locals(),
query_string=db.query_string)
@catch_whining_errors
def render_reasons():
"""Frequencies of reason strings per platform for a given test."""
db = whining_model.model_factory(bottle.request)
releases = db.get_releases()
filter_tag = ''
pt = db.get_reasons()
test_name = pt.test_name
cell_tpl = '<td><div>{str_percent} ({count})</div></td>'
for cell in pt.cell_iterator(['rsn', 'rp']):
cell.template = cell_tpl
for cell in pt.data['platform'].values():
cell.value = cell.count # Use default template with 'value'
# Sorting
reasons = pt.keylists['rsn']
reasons.sort(reverse=True, key=lambda r: pt.data['rsn'][r].count)
platforms = pt.keylists['platform']
platforms.sort()
matrix_href = db.make_href('matrix', 'unfiltered', hide_missing=True,
tests=test_name)
matrix_link = '<a href="%s">matrix</a>' % matrix_href
# Put all the data into an HTML table generator.
tbl = pivot_table.HtmlTable(reasons, platforms, pt.data['rp'])
tbl.title = 'Reason string frequency for the last %d days - %s - links: %s'
tbl.title %= (pt.days_back, test_name, matrix_link)
tbl.title_class = 'centered headeritem'
def format_platform(platform):
href = db.make_href('failures', 'unfiltered', platforms=platform)
return (
'<th class="centered"><a href="%s">%s</a></th>'
) % (href, platform)
# There are some manipulations for grouping similar reasons
def format_reason(rsn):
th = '<th>'
cell = pt.data['rsn'][rsn]
reasons = set(d['reason'].strip() for d in cell.lst)
# If several similar reasons, add a note as tooltip on an asterisk
if len(reasons) > 1:
msg = 'Several reason strings grouped by omitting all numbers'
th += '<span title="%s" style="color: red">*</span>' % msg
reason = max(reasons).strip()
# Trim reason string to max_len characters for reasonably looking UI.
max_len = 120
tooltip = ''
if len(reason) > (max_len + 1):
tooltip = reason
reason = reason[:max_len] + '...'
reason = cgi.escape(reason)
if tooltip:
th += '<span title="%s">%s</span>' % (tooltip, reason)
else:
th += '<span>%s</span>' % reason
return th + '</th>'
tbl.format_row_header = format_reason
tbl.format_col_header = format_platform
tbl.row_headers.append(pt.data['rsn'])
tbl.col_headers.append(pt.data['platform'])
return bottle.template('generic_table_view',
view='reasons',
title='Test Failure Reasons',
tpl_vars=locals(),
query_string=db.query_string,
)
@catch_whining_errors
def render_retry_teststats(filter_tag):
"""Show retry pass rate of tests."""
db = whining_model.model_factory(bottle.request)
releases = db.get_releases()
pt = db.get_retry_teststats(filter_tag)
# Sort suites and platforms by retry pass rates.
tests = pt.keylists['test']
tests.sort(reverse=True, key=lambda t: pt.data['test'][t].rate)
platforms = pt.keylists['platform']
platforms.sort()
# HTML templates for table cells.
header_template = (
'<td>\n'
' <div title="pass/total: {good_count}/{all_count}"> '
'{str_rate} ({all_count}) </div>\n'
'</td>\n'
)
for cell in pt.cell_iterator(['test', 'platform']):
cell.template = header_template
inner_cell_template = ''.join([
'\n<td style="background:{color}">\n',
' <div title="pass/total: {good_count}/{all_count}, '
'avg retry counts: {str_avg_retry_count}"> '
'{str_rate} <a href="' + settings.settings.relative_root +
'/testrun/?test_ids={test_list}">'
'({good_count}/{all_count})</a> </div>\n',
'</td>\n',
])
# Calculate colors based on failure rate.
for cell in pt.data['tp'].values():
cell.template = inner_cell_template
cell.color = wmutils.logcolor(cell.rate)
# Function for HTML formatting cells that are row headers as:
# "matrix, reasons * suite/test_name" with links matrix and reasons views
def format_test_name(test):
suite, test_name = test.split('/', 1)
extra = ' class="experimental"' if pt.data['test'][test].is_exp else ''
return "<th%s>%s</th>" % (extra, test)
# Put all the data into an HTML table generator.
tbl = pivot_table.HtmlTable(tests, platforms, pt.data['tp'])
tbl.format_row_header = format_test_name
tbl.row_headers.append(pt.data['test'])
tbl.col_headers.append(pt.data['platform'])
tbl.title_class = 'centered headeritem'
tbl.title = ('Test retry stats for the last %s days - '
'retry pass rate (total retries)') % pt.days_back
return bottle.template('retry_teststats_view',
view='retry_teststats',
title='Test retry stats',
tpl_vars=locals(),
query_string=db.query_string)
@catch_whining_errors
def render_retry_hostinfo(filter_tag):
"""Show top flaky reasons per host"""
db = whining_model.model_factory(bottle.request)
data = db.get_retry_hostinfo(filter_tag)
title = 'Top 10 flaky reasons for host(s)'
return bottle.template(
'retry_hostinfo_view',
view='retry_hostinfo',
tpl_vars=locals(),
query_string=db.query_string,
title=title)
@catch_whining_errors
def render_retry_hoststats(filter_tag):
"""Show retry pass rate of hosts"""
db = whining_model.model_factory(bottle.request)
pt = db.get_retry_hoststats(filter_tag)
host = pt.keylists['host']
platforms = pt.keylists['platform']
platforms.sort()
def list_format(*args, **kargs):
"""Override the cell's format function."""
html = ['<td>']
for host_info in kargs['lst']:
qs_add = {}
qs_add['hostnames'] = host_info['hostname']
qs = db.query_string(add=qs_add)
tpl = '\n'.join([
'<div title="pass/total: {good_count}/{all_count}">',
' <a href="{root}/retry_hostinfo/{qs}"> {hostname} </a>',
' ({rate}, {good_count}/{all_count})',
'</div>',
])
tpl = tpl.format(good_count=host_info['good_count'],
all_count=host_info['all_count'],
filter_tag=filter_tag,
qs=qs,
hostname=host_info['hostname'],
rate=wmutils.format_rate(host_info['rate']),
root=settings.settings.relative_root)
html.append(tpl)
html.append('</td>')
return '\n'.join(html)
for cell in pt.data['ph'].values():
cell.format = list_format
# Put all the data into an HTML table generator.
tbl = pivot_table.HtmlTable(platforms, host, pt.data['ph'])
tbl.title_class = 'centered headeritem'
tbl.title = 'Per host retry pass rate (most flaky hosts)'
if pt.days_back:
tbl.title += ' for the last %s days' % pt.days_back
return bottle.template('retry_hoststats_view',
view='retry_hoststats',
title='Per host retry pass rate',
tpl_vars=locals(),
query_string=db.query_string)