blob: f0f90bf313c557e1250605c07d04c1f714362fa6 [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.
"""Local dash result viewer - http server.
Allows easy review of multiple runs of run_remote_tests.sh via http pages.
By default pages are shown
"""
import glob, json, operator, optparse, os, socket, subprocess, sys
import datetime, time
# Bottle is a fast, simple and lightweight WSGI micro web-framework for Python.
# It enables simple webpage rendering from a single file (bottle.py) with no
# other dependencies.
# Until a proper repo is identified where bottle.py may reside, download
# and install a recent copy.
try:
import bottle
except ImportError:
print 'Bottle library not found. Please install from http://bottlepy.org.'
print 'You can use:'
if not os.path.exists('/etc/debian_chroot'):
# Outside chroot
print '$ sudo apt-get install python-setuptools'
print '$ sudo easy_install bottle'
sys.exit(1)
base_cmd = os.path.realpath(sys.argv[0])
sys.argv[0] = base_cmd
base_dir = os.path.dirname(base_cmd)
views_dir = os.path.join(base_dir, 'views')
static_root = os.path.join(base_dir, 'static')
default_logs_dir = '/tmp'
usage = ('USAGE: %s [result_dir]\n e.g. %s'
% (os.path.basename(base_cmd), default_logs_dir))
# Handle command line arguments.
parser = optparse.OptionParser(usage=usage)
parser.add_option('', '--run-debug',
help='Enable host debug messages [default: %default]',
dest='run_debug', action='store_true', default=False)
parser.add_option('', '--run-host',
help='Supply host, e.g. w.x.y.z [default: %default]',
dest='run_host', default=socket.gethostname())
parser.add_option('', '--run-port',
help='Supply host port [default: %default]',
dest='run_port', type='int', default=8080)
options, args = parser.parse_args()
bottle.TEMPLATE_PATH.append(views_dir)
app = bottle.Bottle()
def _check_logs_dir(args_):
"""Helper to verify that the result directory is properly established."""
if len(args_) == 1:
logs_dir = args_[0]
else:
logs_dir = default_logs_dir
if not os.path.isdir(logs_dir):
print 'ERROR: cannot find result dir %s.\n%s' % (logs_dir, usage)
sys.exit(1)
return logs_dir
# Need to bind our base url '/logs' to the root of the logs tree
# for result folder traversal.
result_dir = _check_logs_dir(args)
print 'Using logs from: %s' % result_dir
def _make_local_link(filepath, is_dir=False):
"""Helper to populate the directory view with clickable links to folders."""
# Show only the base file/dir name in the list.
base_name = os.path.basename(filepath)
# Distinguish dirs from files with a trailing '/'.
if is_dir:
dir_char = '/'
else:
dir_char = ''
link_template = '<a href="/logs/%s%s">%s%s</a>'
return link_template % (filepath.lstrip('/'), dir_char, base_name, dir_char)
def _server_result_dir(filepath, match=''):
"""Traverses a folder and renders a view for easy review of results files.
Uses the 'dir_view' template to format the resulting view.
"""
# Bottle can use '*' as an url terminator and we use '/' for dirs.
filepath = filepath.strip('/*')
result_subdir = os.path.join(result_dir, filepath)
if not os.path.isdir(result_subdir):
return bottle.HTTPError(404, "Directory does not exist.")
glob_results = glob.glob(os.path.join(result_subdir, match) + '*')
body_lines = []
len_prefix = len(result_dir)
for dir_or_file in glob_results:
if os.path.islink(dir_or_file):
continue
is_dir = os.path.isdir(dir_or_file)
dir_or_file_link = _make_local_link(dir_or_file[len_prefix:],
is_dir=is_dir)
file_size = os.stat(dir_or_file).st_size if not is_dir else '-'
unformatted = time.localtime(os.stat(dir_or_file).st_mtime)
dir_or_file_time = time.strftime("%a, %d %b %Y %H:%M:%S", unformatted)
# Use 'd' to indicate directory vs '-' for a file.
body_lines.append(['d' if is_dir else '-', dir_or_file_link,
dir_or_file_time, unformatted, file_size])
# Sort reverse-chronological top-down to see latest first.
body_lines = sorted(body_lines, key=operator.itemgetter(3), reverse=True)
return bottle.template('dir_view', filepath=filepath, body_lines=body_lines)
def read_test_results():
test_results = {}
test_results_file = os.path.join('/tmp', 'local_dash_test_results.latest')
try:
with open(test_results_file) as jf:
test_results = json.load(jf)
except Exception as e:
# If no local_dash_test_results.json or file is corrupted, serve empty.
print '****\nError retrieving test results:\n%s.\n****' % str(e)
return test_results
# -----------------------------------------------------------------------------
# URL request handlers defined as routes.
# -----------------------------------------------------------------------------
@app.route('/favicon.ico')
def favicon_view():
"""Return the favicon.ico."""
return bottle.static_file('favicon.ico', root=static_root)
@app.route('/static/<filepath:path>')
def server_static(filepath):
"""Serve static files from within a 'static' folder (for css)."""
return bottle.static_file(filepath, root=static_root)
@app.route('/help')
def urls_view():
"""Render a view that shows possible urls for help."""
return bottle.template('urlhelp')
@app.route('/regenerate')
def regenerate_results():
"""Regenerate all reports from test results folder"""
test_results = read_test_results()
cmd = test_results.get('cmd')
if cmd:
print '%s: Regenerating report from test results.' % datetime.datetime.now()
cmd = [cmd]
config = test_results.get('config')
args = test_results.get('args')
if config:
cmd.append('-c')
cmd.append(config)
if args:
cmd.append(args)
subprocess.check_call(cmd)
bottle.redirect('/')
@app.route('/logs/<filepath:path>')
def server_result_file(filepath):
"""Render a view of a file or directory view for results debugging."""
if filepath.endswith('*') or filepath.endswith('/'):
return _server_result_dir(filepath=filepath)
# Default is text/plain, since most files that bottle can't
# detect automatically need to be text/plain, otherwise
# they are rendered ugly by the browser.
mimetype='text/plain'
if filepath.endswith('.htm') or filepath.endswith('.html'):
mimetype = 'text/html'
return bottle.static_file(filepath, mimetype=mimetype, root=result_dir)
@app.route('/logs')
@app.route('/logs/')
def server_logs_root():
"""Render a view of the physical run_remote_tests result folders."""
return _server_result_dir(filepath='', match='run_remote_tests')
@app.route('/')
@app.route('/tests')
def tests_view():
"""Render a logical view of the suites and tests of run_remote_tests."""
return bottle.template('test_view', test_results=read_test_results())
# Run the http server that serves the local dashboard.
if options.run_debug:
print 'Serving with "debug" enabled.'
bottle.run(app, host=options.run_host, port=options.run_port,
debug=options.run_debug, reloader=True)
sys.exit(0)