rebaseline_server: generate JSON that can be viewed without a live server
authorcommit-bot@chromium.org <commit-bot@chromium.org@2bbb7eff-a529-9590-31e7-b0007b416f81>
Thu, 13 Mar 2014 14:56:29 +0000 (14:56 +0000)
committercommit-bot@chromium.org <commit-bot@chromium.org@2bbb7eff-a529-9590-31e7-b0007b416f81>
Thu, 13 Mar 2014 14:56:29 +0000 (14:56 +0000)
BUG=skia:1919
NOTREECHECKS=True
NOTRY=True
R=rmistry@google.com

Author: epoger@google.com

Review URL: https://codereview.chromium.org/195943004

git-svn-id: http://skia.googlecode.com/svn/trunk@13783 2bbb7eff-a529-9590-31e7-b0007b416f81

gm/rebaseline_server/results.py
gm/rebaseline_server/server.py
gm/rebaseline_server/static/constants.js
gm/rebaseline_server/static/index.html
gm/rebaseline_server/static/loader.js

index 8ff24ec..5c33e30 100755 (executable)
@@ -28,6 +28,7 @@ import time
 # so any dirs that are already in the PYTHONPATH will be preferred.
 PARENT_DIRECTORY = os.path.dirname(os.path.realpath(__file__))
 GM_DIRECTORY = os.path.dirname(PARENT_DIRECTORY)
+TRUNK_DIRECTORY = os.path.dirname(GM_DIRECTORY)
 if GM_DIRECTORY not in sys.path:
   sys.path.append(GM_DIRECTORY)
 import gm_json
@@ -44,8 +45,16 @@ KEY__EXTRACOLUMN__BUILDER = 'builder'
 KEY__EXTRACOLUMN__CONFIG = 'config'
 KEY__EXTRACOLUMN__RESULT_TYPE = 'resultType'
 KEY__EXTRACOLUMN__TEST = 'test'
+KEY__HEADER = 'header'
+KEY__HEADER__DATAHASH = 'dataHash'
+KEY__HEADER__IS_EDITABLE = 'isEditable'
+KEY__HEADER__IS_EXPORTED = 'isExported'
+KEY__HEADER__IS_STILL_LOADING = 'resultsStillLoading'
 KEY__HEADER__RESULTS_ALL = 'all'
 KEY__HEADER__RESULTS_FAILURES = 'failures'
+KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE = 'timeNextUpdateAvailable'
+KEY__HEADER__TIME_UPDATED = 'timeUpdated'
+KEY__HEADER__TYPE = 'type'
 KEY__NEW_IMAGE_URL = 'newImageUrl'
 KEY__RESULT_TYPE__FAILED = gm_json.JSONKEY_ACTUALRESULTS_FAILED
 KEY__RESULT_TYPE__FAILUREIGNORED = gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED
@@ -63,6 +72,11 @@ IMAGE_FILENAME_FORMATTER = '%s_%s.png'  # pass in (testname, config)
 
 IMAGEPAIR_SET_DESCRIPTIONS = ('expected image', 'actual image')
 
+DEFAULT_ACTUALS_DIR = '.gm-actuals'
+DEFAULT_EXPECTATIONS_DIR = os.path.join(TRUNK_DIRECTORY, 'expectations', 'gm')
+DEFAULT_GENERATED_IMAGES_ROOT = os.path.join(PARENT_DIRECTORY, 'static',
+                                             'generated-images')
+
 
 class Results(object):
   """ Loads actual and expected GM results into an ImagePairSet.
@@ -74,7 +88,9 @@ class Results(object):
   are immutable.  If you want to update the results based on updated JSON
   file contents, you will need to create a new Results object."""
 
-  def __init__(self, actuals_root, expected_root, generated_images_root):
+  def __init__(self, actuals_root=DEFAULT_ACTUALS_DIR,
+               expected_root=DEFAULT_EXPECTATIONS_DIR,
+               generated_images_root=DEFAULT_GENERATED_IMAGES_ROOT):
     """
     Args:
       actuals_root: root directory containing all actual-results.json files
@@ -150,16 +166,55 @@ class Results(object):
       builder_expectations[image_name] = new_expectations
     Results._write_dicts_to_root(expected_builder_dicts, self._expected_root)
 
-  def get_results_of_type(self, type):
-    """Return results of some/all tests (depending on 'type' parameter).
+  def get_results_of_type(self, results_type):
+    """Return results of some/all tests (depending on 'results_type' parameter).
 
     Args:
-      type: string describing which types of results to include; must be one
-            of the RESULTS_* constants
+      results_type: string describing which types of results to include; must
+          be one of the RESULTS_* constants
 
     Results are returned in a dictionary as output by ImagePairSet.as_dict().
     """
-    return self._results[type]
+    return self._results[results_type]
+
+  def get_packaged_results_of_type(self, results_type, reload_seconds=None,
+                                   is_editable=False, is_exported=True):
+    """ Package the results of some/all tests as a complete response_dict.
+
+    Args:
+      results_type: string indicating which set of results to return;
+          must be one of the RESULTS_* constants
+      reload_seconds: if specified, note that new results may be available once
+          these results are reload_seconds old
+      is_editable: whether clients are allowed to submit new baselines
+      is_exported: whether these results are being made available to other
+          network hosts
+    """
+    response_dict = self._results[results_type]
+    time_updated = self.get_timestamp()
+    response_dict[KEY__HEADER] = {
+        # Timestamps:
+        # 1. when this data was last updated
+        # 2. when the caller should check back for new data (if ever)
+        KEY__HEADER__TIME_UPDATED: time_updated,
+        KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE: (
+            (time_updated+reload_seconds) if reload_seconds else None),
+
+        # The type we passed to get_results_of_type()
+        KEY__HEADER__TYPE: results_type,
+
+        # Hash of dataset, which the client must return with any edits--
+        # this ensures that the edits were made to a particular dataset.
+        KEY__HEADER__DATAHASH: str(hash(repr(
+            response_dict[imagepairset.KEY__IMAGEPAIRS]))),
+
+        # Whether the server will accept edits back.
+        KEY__HEADER__IS_EDITABLE: is_editable,
+
+        # Whether the service is accessible from other hosts.
+        KEY__HEADER__IS_EXPORTED: is_exported,
+    }
+    return response_dict
 
   @staticmethod
   def _ignore_builder(builder):
@@ -415,23 +470,31 @@ def main():
                       level=logging.INFO)
   parser = argparse.ArgumentParser()
   parser.add_argument(
-      '--actuals', required=True,
+      '--actuals', default=DEFAULT_ACTUALS_DIR,
       help='Directory containing all actual-result JSON files')
   parser.add_argument(
-      '--expectations', required=True,
-      help='Directory containing all expected-result JSON files')
+      '--expectations', default=DEFAULT_EXPECTATIONS_DIR,
+      help='Directory containing all expected-result JSON files; defaults to '
+      '\'%(default)s\' .')
   parser.add_argument(
       '--outfile', required=True,
-      help='File to write result summary into, in JSON format')
+      help='File to write result summary into, in JSON format.')
+  parser.add_argument(
+      '--results', default=KEY__HEADER__RESULTS_FAILURES,
+      help='Which result types to include. Defaults to \'%(default)s\'; '
+      'must be one of ' +
+      str([KEY__HEADER__RESULTS_FAILURES, KEY__HEADER__RESULTS_ALL]))
   parser.add_argument(
-      '--workdir', default='.workdir',
-      help='Directory within which to download images and generate diffs')
+      '--workdir', default=DEFAULT_GENERATED_IMAGES_ROOT,
+      help='Directory within which to download images and generate diffs; '
+      'defaults to \'%(default)s\' .')
   args = parser.parse_args()
   results = Results(actuals_root=args.actuals,
                     expected_root=args.expectations,
                     generated_images_root=args.workdir)
-  gm_json.WriteToFile(results.get_results_of_type(KEY__HEADER__RESULTS_ALL),
-                      args.outfile)
+  gm_json.WriteToFile(
+      results.get_packaged_results_of_type(results_type=args.results),
+      args.outfile)
 
 
 if __name__ == '__main__':
index fc090d2..19b0035 100755 (executable)
@@ -40,13 +40,14 @@ if TOOLS_DIRECTORY not in sys.path:
 import svn
 
 # Imports from local dir
+#
+# Note: we import results under a different name, to avoid confusion with the
+# Server.results() property. See discussion at
+# https://codereview.chromium.org/195943004/diff/1/gm/rebaseline_server/server.py#newcode44
 import imagepairset
-import results
+import results as results_mod
 
 PATHSPLIT_RE = re.compile('/([^/]+)/(.+)')
-EXPECTATIONS_DIR = os.path.join(TRUNK_DIRECTORY, 'expectations', 'gm')
-GENERATED_IMAGES_ROOT = os.path.join(PARENT_DIRECTORY, 'static',
-                                     'generated-images')
 
 # A simple dictionary of file name extensions to MIME types. The empty string
 # entry is used as the default when no extension was given or if the extension
@@ -64,16 +65,8 @@ MIME_TYPE_MAP = {'': 'application/octet-stream',
 KEY__EDITS__MODIFICATIONS = 'modifications'
 KEY__EDITS__OLD_RESULTS_HASH = 'oldResultsHash'
 KEY__EDITS__OLD_RESULTS_TYPE = 'oldResultsType'
-KEY__HEADER = 'header'
-KEY__HEADER__DATAHASH = 'dataHash'
-KEY__HEADER__IS_EDITABLE = 'isEditable'
-KEY__HEADER__IS_EXPORTED = 'isExported'
-KEY__HEADER__IS_STILL_LOADING = 'resultsStillLoading'
-KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE = 'timeNextUpdateAvailable'
-KEY__HEADER__TIME_UPDATED = 'timeUpdated'
-KEY__HEADER__TYPE = 'type'
-
-DEFAULT_ACTUALS_DIR = '.gm-actuals'
+
+DEFAULT_ACTUALS_DIR = results_mod.DEFAULT_ACTUALS_DIR
 DEFAULT_ACTUALS_REPO_REVISION = 'HEAD'
 DEFAULT_ACTUALS_REPO_URL = 'http://skia-autogen.googlecode.com/svn/gm-actual'
 DEFAULT_PORT = 8888
@@ -201,8 +194,8 @@ class Server(object):
     return self._reload_seconds
 
   def update_results(self, invalidate=False):
-    """ Create or update self._results, based on the expectations in
-    EXPECTATIONS_DIR and the latest actuals from skia-autogen.
+    """ Create or update self._results, based on the latest expectations and
+    actuals.
 
     We hold self.results_rlock while we do this, to guarantee that no other
     thread attempts to update either self._results or the underlying files at
@@ -236,13 +229,10 @@ class Server(object):
       if self._reload_seconds:
         logging.info(
             'Updating expected GM results in %s by syncing Skia repo ...' %
-            EXPECTATIONS_DIR)
+            results_mod.DEFAULT_EXPECTATIONS_DIR)
         _run_command(['gclient', 'sync'], TRUNK_DIRECTORY)
 
-      self._results = results.Results(
-          actuals_root=self._actuals_dir,
-          expected_root=EXPECTATIONS_DIR,
-          generated_images_root=GENERATED_IMAGES_ROOT)
+      self._results = results_mod.Results(actuals_root=self._actuals_dir)
 
   def _result_loader(self, reload_seconds=0):
     """ Call self.update_results(), either once or periodically.
@@ -315,14 +305,14 @@ class HTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
       self.send_error(404)
       raise
 
-  def do_GET_results(self, type):
+  def do_GET_results(self, results_type):
     """ Handle a GET request for GM results.
 
     Args:
-      type: string indicating which set of results to return;
-            must be one of the results.RESULTS_* constants
+      results_type: string indicating which set of results to return;
+            must be one of the results_mod.RESULTS_* constants
     """
-    logging.debug('do_GET_results: sending results of type "%s"' % type)
+    logging.debug('do_GET_results: sending results of type "%s"' % results_type)
     # Since we must make multiple calls to the Results object, grab a
     # reference to it in case it is updated to point at a new Results
     # object within another thread.
@@ -333,60 +323,21 @@ class HTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
     # the handler's .server instance variable.
     results_obj = _SERVER.results
     if results_obj:
-      response_dict = self.package_results(results_obj, type)
+      response_dict = results_obj.get_packaged_results_of_type(
+          results_type=results_type, reload_seconds=_SERVER.reload_seconds,
+          is_editable=_SERVER.is_editable, is_exported=_SERVER.is_exported)
     else:
       now = int(time.time())
       response_dict = {
-          KEY__HEADER: {
-              KEY__HEADER__IS_STILL_LOADING: True,
-              KEY__HEADER__TIME_UPDATED: now,
-              KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE: (
+          results_mod.KEY__HEADER: {
+              results_mod.KEY__HEADER__IS_STILL_LOADING: True,
+              results_mod.KEY__HEADER__TIME_UPDATED: now,
+              results_mod.KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE: (
                   now + RELOAD_INTERVAL_UNTIL_READY),
           },
       }
     self.send_json_dict(response_dict)
 
-  def package_results(self, results_obj, type):
-    """ Given a nonempty "results" object, package it as a response_dict
-    as needed within do_GET_results.
-
-    Args:
-      results_obj: nonempty "results" object
-      type: string indicating which set of results to return;
-            must be one of the results.RESULTS_* constants
-    """
-    response_dict = results_obj.get_results_of_type(type)
-    time_updated = results_obj.get_timestamp()
-    response_dict[KEY__HEADER] = {
-        # Timestamps:
-        # 1. when this data was last updated
-        # 2. when the caller should check back for new data (if ever)
-        #
-        # We only return these timestamps if the --reload argument was passed;
-        # otherwise, we have no idea when the expectations were last updated
-        # (we allow the user to maintain her own expectations as she sees fit).
-        KEY__HEADER__TIME_UPDATED:
-            time_updated if _SERVER.reload_seconds else None,
-        KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE:
-            (time_updated+_SERVER.reload_seconds) if _SERVER.reload_seconds
-            else None,
-
-        # The type we passed to get_results_of_type()
-        KEY__HEADER__TYPE: type,
-
-        # Hash of dataset, which the client must return with any edits--
-        # this ensures that the edits were made to a particular dataset.
-        KEY__HEADER__DATAHASH: str(hash(repr(
-            response_dict[imagepairset.KEY__IMAGEPAIRS]))),
-
-        # Whether the server will accept edits back.
-        KEY__HEADER__IS_EDITABLE: _SERVER.is_editable,
-
-        # Whether the service is accessible from other hosts.
-        KEY__HEADER__IS_EXPORTED: _SERVER.is_exported,
-    }
-    return response_dict
-
   def do_GET_static(self, path):
     """ Handle a GET request for a file under the 'static' directory.
     Only allow serving of files within the 'static' directory that is a
@@ -441,7 +392,7 @@ class HTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
                                               # client and server apply
                                               # modifications to the same base)
       KEY__EDITS__MODIFICATIONS: [
-        # as needed by results.edit_expectations()
+        # as needed by results_mod.edit_expectations()
         ...
       ],
     }
index 00c1852..f795b6f 100644 (file)
@@ -45,8 +45,16 @@ module.constant('constants', (function() {
     KEY__EXTRACOLUMN__CONFIG: 'config',
     KEY__EXTRACOLUMN__RESULT_TYPE: 'resultType',
     KEY__EXTRACOLUMN__TEST: 'test',
+    KEY__HEADER: 'header',
+    KEY__HEADER__DATAHASH: 'dataHash',
+    KEY__HEADER__IS_EDITABLE: 'isEditable',
+    KEY__HEADER__IS_EXPORTED: 'isExported',
+    KEY__HEADER__IS_STILL_LOADING: 'resultsStillLoading',
     KEY__HEADER__RESULTS_ALL: 'all',
     KEY__HEADER__RESULTS_FAILURES: 'failures',
+    KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE: 'timeNextUpdateAvailable',
+    KEY__HEADER__TIME_UPDATED: 'timeUpdated',
+    KEY__HEADER__TYPE: 'type',
     KEY__NEW_IMAGE_URL: 'newImageUrl',
     KEY__RESULT_TYPE__FAILED: 'failed',
     KEY__RESULT_TYPE__FAILUREIGNORED: 'failure-ignored',
@@ -57,13 +65,5 @@ module.constant('constants', (function() {
     KEY__EDITS__MODIFICATIONS: 'modifications',
     KEY__EDITS__OLD_RESULTS_HASH: 'oldResultsHash',
     KEY__EDITS__OLD_RESULTS_TYPE: 'oldResultsType',
-    KEY__HEADER: 'header',
-    KEY__HEADER__DATAHASH: 'dataHash',
-    KEY__HEADER__IS_EDITABLE: 'isEditable',
-    KEY__HEADER__IS_EXPORTED: 'isExported',
-    KEY__HEADER__IS_STILL_LOADING: 'resultsStillLoading',
-    KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE: 'timeNextUpdateAvailable',
-    KEY__HEADER__TIME_UPDATED: 'timeUpdated',
-    KEY__HEADER__TYPE: 'type',
   }
 })())
index 39991e0..df9bb0e 100644 (file)
@@ -9,13 +9,13 @@
     Here are links to the result pages:
     <ul>
       <li>
-        <a href="/static/view.html#/view.html?resultsToLoad=failures">
+        <a href="/static/view.html#/view.html?resultsToLoad=/results/failures">
           failures only
         </a>
         (loads faster)
       </li>
       <li>
-        <a href="/static/view.html#/view.html?resultsToLoad=all">
+        <a href="/static/view.html#/view.html?resultsToLoad=/results/all">
           all results
         </a>
         (includes successful test results)
index bd3be2d..645724c 100644 (file)
@@ -63,7 +63,7 @@ Loader.controller(
     $scope.constants = constants;
     $scope.windowTitle = "Loading GM Results...";
     $scope.resultsToLoad = $location.search().resultsToLoad;
-    $scope.loadingMessage = "Loading results of type '" + $scope.resultsToLoad +
+    $scope.loadingMessage = "Loading results from '" + $scope.resultsToLoad +
         "', please wait...";
 
     /**
@@ -71,7 +71,7 @@ Loader.controller(
      * Once the dictionary is loaded, unhide the page elements so they can
      * render the data.
      */
-    $http.get("/results/" + $scope.resultsToLoad).success(
+    $http.get($scope.resultsToLoad).success(
       function(data, status, header, config) {
         var dataHeader = data[constants.KEY__HEADER];
         if (dataHeader[constants.KEY__HEADER__IS_STILL_LOADING]) {
@@ -166,7 +166,7 @@ Loader.controller(
       }
     ).error(
       function(data, status, header, config) {
-        $scope.loadingMessage = "Failed to load results of type '"
+        $scope.loadingMessage = "Failed to load results from '"
             + $scope.resultsToLoad + "'";
         $scope.windowTitle = "Failed to Load GM Results";
       }