| #!/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('/' + ''.join(bottle.request.url.partition('?')[1:])) |
| |
| @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) |