| Index: gm/rebaseline_server/results.py
|
| diff --git a/gm/rebaseline_server/results.py b/gm/rebaseline_server/results.py
|
| deleted file mode 100755
|
| index b0027d22af3d19fd751217b067a0aa010cb2e784..0000000000000000000000000000000000000000
|
| --- a/gm/rebaseline_server/results.py
|
| +++ /dev/null
|
| @@ -1,343 +0,0 @@
|
| -#!/usr/bin/python
|
| -
|
| -"""
|
| -Copyright 2013 Google Inc.
|
| -
|
| -Use of this source code is governed by a BSD-style license that can be
|
| -found in the LICENSE file.
|
| -
|
| -Repackage expected/actual GM results as needed by our HTML rebaseline viewer.
|
| -"""
|
| -
|
| -# System-level imports
|
| -import fnmatch
|
| -import os
|
| -import re
|
| -
|
| -# Must fix up PYTHONPATH before importing from within Skia
|
| -import rs_fixpypath # pylint: disable=W0611
|
| -
|
| -# Imports from within Skia
|
| -import gm_json
|
| -import imagepairset
|
| -
|
| -# Keys used to link an image to a particular GM test.
|
| -# NOTE: Keep these in sync with static/constants.js
|
| -VALUE__HEADER__SCHEMA_VERSION = 5
|
| -KEY__EXPECTATIONS__BUGS = gm_json.JSONKEY_EXPECTEDRESULTS_BUGS
|
| -KEY__EXPECTATIONS__IGNOREFAILURE = gm_json.JSONKEY_EXPECTEDRESULTS_IGNOREFAILURE
|
| -KEY__EXPECTATIONS__REVIEWED = gm_json.JSONKEY_EXPECTEDRESULTS_REVIEWED
|
| -KEY__EXTRACOLUMNS__BUILDER = 'builder'
|
| -KEY__EXTRACOLUMNS__CONFIG = 'config'
|
| -KEY__EXTRACOLUMNS__RESULT_TYPE = 'resultType'
|
| -KEY__EXTRACOLUMNS__TEST = 'test'
|
| -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__SCHEMA_VERSION = 'schemaVersion'
|
| -KEY__HEADER__SET_A_DESCRIPTIONS = 'setA'
|
| -KEY__HEADER__SET_B_DESCRIPTIONS = 'setB'
|
| -KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE = 'timeNextUpdateAvailable'
|
| -KEY__HEADER__TIME_UPDATED = 'timeUpdated'
|
| -KEY__HEADER__TYPE = 'type'
|
| -KEY__RESULT_TYPE__FAILED = gm_json.JSONKEY_ACTUALRESULTS_FAILED
|
| -KEY__RESULT_TYPE__FAILUREIGNORED = gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED
|
| -KEY__RESULT_TYPE__NOCOMPARISON = gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON
|
| -KEY__RESULT_TYPE__SUCCEEDED = gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED
|
| -KEY__SET_DESCRIPTIONS__DIR = 'dir'
|
| -KEY__SET_DESCRIPTIONS__REPO_REVISION = 'repoRevision'
|
| -KEY__SET_DESCRIPTIONS__SECTION = 'section'
|
| -
|
| -IMAGE_FILENAME_RE = re.compile(gm_json.IMAGE_FILENAME_PATTERN)
|
| -IMAGE_FILENAME_FORMATTER = '%s_%s.png' # pass in (testname, config)
|
| -
|
| -PARENT_DIRECTORY = os.path.dirname(os.path.realpath(__file__))
|
| -DEFAULT_ACTUALS_DIR = '.gm-actuals'
|
| -DEFAULT_GENERATED_IMAGES_ROOT = os.path.join(
|
| - PARENT_DIRECTORY, '.generated-images')
|
| -
|
| -# Define the default set of builders we will process expectations/actuals for.
|
| -# This allows us to ignore builders for which we don't maintain expectations
|
| -# (trybots, Valgrind, ASAN, TSAN), and avoid problems like
|
| -# https://code.google.com/p/skia/issues/detail?id=2036 ('rebaseline_server
|
| -# produces error when trying to add baselines for ASAN/TSAN builders')
|
| -DEFAULT_MATCH_BUILDERS_PATTERN_LIST = ['.*']
|
| -DEFAULT_SKIP_BUILDERS_PATTERN_LIST = [
|
| - '.*-Trybot', '.*Valgrind.*', '.*TSAN.*', '.*ASAN.*']
|
| -
|
| -
|
| -class BaseComparisons(object):
|
| - """Base class for generating summary of comparisons between two image sets.
|
| - """
|
| -
|
| - def __init__(self):
|
| - """Base constructor; most subclasses will override."""
|
| - self._setA_descriptions = None
|
| - self._setB_descriptions = None
|
| -
|
| - def get_results_of_type(self, results_type):
|
| - """Return results of some/all tests (depending on 'results_type' parameter).
|
| -
|
| - Args:
|
| - 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[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()
|
| - header_dict = {
|
| - KEY__HEADER__SCHEMA_VERSION: (
|
| - VALUE__HEADER__SCHEMA_VERSION),
|
| -
|
| - # 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__ROOT__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,
|
| - }
|
| - if self._setA_descriptions:
|
| - header_dict[KEY__HEADER__SET_A_DESCRIPTIONS] = self._setA_descriptions
|
| - if self._setB_descriptions:
|
| - header_dict[KEY__HEADER__SET_B_DESCRIPTIONS] = self._setB_descriptions
|
| - response_dict[imagepairset.KEY__ROOT__HEADER] = header_dict
|
| - return response_dict
|
| -
|
| - def get_timestamp(self):
|
| - """Return the time at which this object was created, in seconds past epoch
|
| - (UTC).
|
| - """
|
| - return self._timestamp
|
| -
|
| - _match_builders_pattern_list = [
|
| - re.compile(p) for p in DEFAULT_MATCH_BUILDERS_PATTERN_LIST]
|
| - _skip_builders_pattern_list = [
|
| - re.compile(p) for p in DEFAULT_SKIP_BUILDERS_PATTERN_LIST]
|
| -
|
| - def set_match_builders_pattern_list(self, pattern_list):
|
| - """Override the default set of builders we should process.
|
| -
|
| - The default is DEFAULT_MATCH_BUILDERS_PATTERN_LIST .
|
| -
|
| - Note that skip_builders_pattern_list overrides this; regardless of whether a
|
| - builder is in the "match" list, if it's in the "skip" list, we will skip it.
|
| -
|
| - Args:
|
| - pattern_list: list of regex patterns; process builders that match any
|
| - entry within this list
|
| - """
|
| - if pattern_list == None:
|
| - pattern_list = []
|
| - self._match_builders_pattern_list = [re.compile(p) for p in pattern_list]
|
| -
|
| - def set_skip_builders_pattern_list(self, pattern_list):
|
| - """Override the default set of builders we should skip while processing.
|
| -
|
| - The default is DEFAULT_SKIP_BUILDERS_PATTERN_LIST .
|
| -
|
| - This overrides match_builders_pattern_list; regardless of whether a
|
| - builder is in the "match" list, if it's in the "skip" list, we will skip it.
|
| -
|
| - Args:
|
| - pattern_list: list of regex patterns; skip builders that match any
|
| - entry within this list
|
| - """
|
| - if pattern_list == None:
|
| - pattern_list = []
|
| - self._skip_builders_pattern_list = [re.compile(p) for p in pattern_list]
|
| -
|
| - def _ignore_builder(self, builder):
|
| - """Returns True if we should skip processing this builder.
|
| -
|
| - Args:
|
| - builder: name of this builder, as a string
|
| -
|
| - Returns:
|
| - True if we should ignore expectations and actuals for this builder.
|
| - """
|
| - for pattern in self._skip_builders_pattern_list:
|
| - if pattern.match(builder):
|
| - return True
|
| - for pattern in self._match_builders_pattern_list:
|
| - if pattern.match(builder):
|
| - return False
|
| - return True
|
| -
|
| - def _read_builder_dicts_from_root(self, root, pattern='*.json'):
|
| - """Read all JSON dictionaries within a directory tree.
|
| -
|
| - Skips any dictionaries belonging to a builder we have chosen to ignore.
|
| -
|
| - Args:
|
| - root: path to root of directory tree
|
| - pattern: which files to read within root (fnmatch-style pattern)
|
| -
|
| - Returns:
|
| - A meta-dictionary containing all the JSON dictionaries found within
|
| - the directory tree, keyed by builder name (the basename of the directory
|
| - where each JSON dictionary was found).
|
| -
|
| - Raises:
|
| - IOError if root does not refer to an existing directory
|
| - """
|
| - # I considered making this call read_dicts_from_root(), but I decided
|
| - # it was better to prune out the ignored builders within the os.walk().
|
| - if not os.path.isdir(root):
|
| - raise IOError('no directory found at path %s' % root)
|
| - meta_dict = {}
|
| - for dirpath, _, filenames in os.walk(root):
|
| - for matching_filename in fnmatch.filter(filenames, pattern):
|
| - builder = os.path.basename(dirpath)
|
| - if self._ignore_builder(builder):
|
| - continue
|
| - full_path = os.path.join(dirpath, matching_filename)
|
| - meta_dict[builder] = gm_json.LoadFromFile(full_path)
|
| - return meta_dict
|
| -
|
| - @staticmethod
|
| - def read_dicts_from_root(root, pattern='*.json'):
|
| - """Read all JSON dictionaries within a directory tree.
|
| -
|
| - TODO(stephana): Factor this out into a utility module, as a standalone
|
| - function (not part of a class).
|
| -
|
| - Args:
|
| - root: path to root of directory tree
|
| - pattern: which files to read within root (fnmatch-style pattern)
|
| -
|
| - Returns:
|
| - A meta-dictionary containing all the JSON dictionaries found within
|
| - the directory tree, keyed by the pathname (relative to root) of each JSON
|
| - dictionary.
|
| -
|
| - Raises:
|
| - IOError if root does not refer to an existing directory
|
| - """
|
| - if not os.path.isdir(root):
|
| - raise IOError('no directory found at path %s' % root)
|
| - meta_dict = {}
|
| - for abs_dirpath, _, filenames in os.walk(root):
|
| - rel_dirpath = os.path.relpath(abs_dirpath, root)
|
| - for matching_filename in fnmatch.filter(filenames, pattern):
|
| - abs_path = os.path.join(abs_dirpath, matching_filename)
|
| - rel_path = os.path.join(rel_dirpath, matching_filename)
|
| - meta_dict[rel_path] = gm_json.LoadFromFile(abs_path)
|
| - return meta_dict
|
| -
|
| - @staticmethod
|
| - def _read_noncomment_lines(path):
|
| - """Return a list of all noncomment lines within a file.
|
| -
|
| - (A "noncomment" line is one that does not start with a '#'.)
|
| -
|
| - Args:
|
| - path: path to file
|
| - """
|
| - lines = []
|
| - with open(path, 'r') as fh:
|
| - for line in fh:
|
| - if not line.startswith('#'):
|
| - lines.append(line.strip())
|
| - return lines
|
| -
|
| - @staticmethod
|
| - def _create_relative_url(hashtype_and_digest, test_name):
|
| - """Returns the URL for this image, relative to GM_ACTUALS_ROOT_HTTP_URL.
|
| -
|
| - If we don't have a record of this image, returns None.
|
| -
|
| - Args:
|
| - hashtype_and_digest: (hash_type, hash_digest) tuple, or None if we
|
| - don't have a record of this image
|
| - test_name: string; name of the GM test that created this image
|
| - """
|
| - if not hashtype_and_digest:
|
| - return None
|
| - return gm_json.CreateGmRelativeUrl(
|
| - test_name=test_name,
|
| - hash_type=hashtype_and_digest[0],
|
| - hash_digest=hashtype_and_digest[1])
|
| -
|
| - @staticmethod
|
| - def combine_subdicts(input_dict):
|
| - """ Flatten out a dictionary structure by one level.
|
| -
|
| - Input:
|
| - {
|
| - KEY_A1 : {
|
| - KEY_B1 : VALUE_B1,
|
| - },
|
| - KEY_A2 : {
|
| - KEY_B2 : VALUE_B2,
|
| - }
|
| - }
|
| -
|
| - Output:
|
| - {
|
| - KEY_B1 : VALUE_B1,
|
| - KEY_B2 : VALUE_B2,
|
| - }
|
| -
|
| - If this would result in any repeated keys, it will raise an Exception.
|
| - """
|
| - output_dict = {}
|
| - for subdict in input_dict.values():
|
| - for subdict_key, subdict_value in subdict.iteritems():
|
| - if subdict_key in output_dict:
|
| - raise Exception('duplicate key %s in combine_subdicts' % subdict_key)
|
| - output_dict[subdict_key] = subdict_value
|
| - return output_dict
|
| -
|
| - @staticmethod
|
| - def get_default(input_dict, default_value, *keys):
|
| - """Returns input_dict[key1][key2][...], or default_value.
|
| -
|
| - If input_dict is None, or any key is missing along the way, this returns
|
| - default_value.
|
| -
|
| - Args:
|
| - input_dict: dictionary to look within
|
| - key: key indicating which value to return from input_dict
|
| - default_value: value to return if input_dict is None or any key cannot
|
| - be found along the way
|
| - """
|
| - if input_dict == None:
|
| - return default_value
|
| - for key in keys:
|
| - input_dict = input_dict.get(key, None)
|
| - if input_dict == None:
|
| - return default_value
|
| - return input_dict
|
|
|