# 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
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
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.
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
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):
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__':
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
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
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
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.
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.
# 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
# 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()
...
],
}
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',
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',
}
})())
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)
$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...";
/**
* 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]) {
}
).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";
}