Allow local_dash reports to be regenerated from the ui.

For simply keeping an eye on local test activity, it's
helpful to have a ui based regenerate click to avoid dropping
into the chroot to update after running tests.

BUG=chromium-os:34919
TEST=result_viewer, then run_remote_tests.sh, then click regenerate.

Change-Id: I8465edee83f91d205401781ab9c5f127becd9979
Reviewed-on: https://gerrit.chromium.org/gerrit/38996
Reviewed-by: Yusuf Mohsinally <mohsinally@google.com>
Reviewed-by: Yusuf Mohsinally <mohsinally@chromium.org>
Commit-Ready: Mike Truty <truty@chromium.org>
Tested-by: Mike Truty <truty@chromium.org>
diff --git a/utils_py/local_dash/dash_template/result_viewer b/utils_py/local_dash/dash_template/result_viewer
index 2b14cb3..f0f90bf 100755
--- a/utils_py/local_dash/dash_template/result_viewer
+++ b/utils_py/local_dash/dash_template/result_viewer
@@ -9,7 +9,8 @@
 By default pages are shown
 """
 
-import glob, json, operator, optparse, os, socket, sys, time
+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
@@ -152,6 +153,23 @@
   """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):
diff --git a/utils_py/local_dash/dash_template/views/header.tpl b/utils_py/local_dash/dash_template/views/header.tpl
index a914f86..4cf31a2 100644
--- a/utils_py/local_dash/dash_template/views/header.tpl
+++ b/utils_py/local_dash/dash_template/views/header.tpl
@@ -11,5 +11,6 @@
      target="_blank">External buildbot</a> |
   <a href="http://chromegw/i/chromeos/console"
      target="_blank">Internal buildbot</a> |
+  <a href="/regenerate">Regenerate</a> |
   <a href="/help">Help</a>
 </div>
diff --git a/utils_py/local_dash/local_dash b/utils_py/local_dash/local_dash
index f014487..2e44df1 100755
--- a/utils_py/local_dash/local_dash
+++ b/utils_py/local_dash/local_dash
@@ -76,6 +76,9 @@
     0: Shows ERROR logged messages.
     1: Shows ERROR, INFO logged messages.
     2: Shows ERROR, INFO and DEBUG logged messages.
+
+  Returns:
+    Tuple of (options, args) parsed.
   """
   parser = optparse.OptionParser()
   parser.add_option('-c', '--config-file',
@@ -87,7 +90,7 @@
   parser.add_option('-v', '--verbosity-level',
                     help='1=debug, 2=most verbose [default: %default]',
                     dest='verbosity', type='int', default=0)
-  options = parser.parse_args()[0]
+  options, args = parser.parse_args()
 
   logging_level = logging.ERROR
   if options.verbosity == 1:
@@ -96,7 +99,7 @@
     logging_level = logging.DEBUG
   LOG.setLevel(logging_level)
 
-  return options
+  return options, args
 
 
 def get_json_config(current_dir, json_file):
@@ -152,6 +155,10 @@
   result dashboard. Each run_remote_tests.XXX folder will be an
   individual dictionary entry in the list.
 
+  The result model is made up of actual _results, some hardware details,
+  a list of tests executed (for building report headers) and the
+  details of the command line execution for aiding re-runs of local_dash.
+
   The _results data structure is organized as follows.
 
   [{'path': path1 # path to the result folder,
@@ -215,20 +222,30 @@
     suite_path = '/'.join(suite_parts[:2])
     return path_, suite_path, is_suite
 
-  def get_model(self):
+  def get_model(self, cmd=None, config=None, args=None):
     """Some final post-processing before the data is returned.
 
     The model will commonly be viewed in date-descending order.
+
+    Args:
+      cmd: if present, added to the model to aid re-runs.
+      config: if present, added to the model to aid re-runs.
+      args: if present, added to the model to aid re-runs.
+
     Returns:
       A dictionary with 3 elements: the test-result-list-of-dictionaries, a
       list of unique hardware on which the tests were attempted and a list of
       unique tests that were attempted.  These 3 things allow the recipient to
       format some organized tables by hardware and test.
     """
-    return {'results': sorted(self._results, key=itemgetter('timestamp'),
-                              reverse=True),
-            'hardware': sorted(self._hardware_set),
-            'tests': sorted(self._test_set)}
+    model = {'results': sorted(self._results, key=itemgetter('timestamp'),
+                               reverse=True),
+             'hardware': sorted(self._hardware_set),
+             'tests': sorted(self._test_set)}
+    for k, var in [('cmd', cmd), ('config', config), ('args', args)]:
+      if var:
+        model[k] = var
+    return model
 
   def print_result_model(self):
     """For diagnostic purposes allow the model to be reviewed."""
@@ -435,7 +452,7 @@
   base_dir = os.path.dirname(os.path.abspath(argv[0]))
 
   # Parse options
-  options = parse_args()
+  options, args = parse_args()
 
   try:
     config_dash = get_json_config(base_dir, options.config_file)
@@ -451,7 +468,11 @@
   if test_results:
     if options.print_model:
       test_results.print_result_model()
-    file_name = write_json_file(test_results.get_model())
+    result_model = test_results.get_model(os.path.abspath(argv[0]),
+                                          options.config_file, args)
+    # To enable re-running this command from a web-ui, stash the
+    # details of this command invocation in the results.
+    file_name = write_json_file(result_model)
     print 'Wrote %s.' % file_name
     viewer_path = os.path.join(base_dir, 'dash_template', 'result_viewer')
     # Touch forces the bottle web server to refresh.