4 Copyright 2014 Google Inc.
6 Use of this source code is governed by a BSD-style license that can be
7 found in the LICENSE file.
9 ImagePairSet class; see its docstring below.
12 # System-level imports
15 # Must fix up PYTHONPATH before importing from within Skia
16 import fix_pythonpath # pylint: disable=W0611
18 # Imports from within Skia
21 from py.utils import gs_utils
23 # Keys used within dictionary representation of ImagePairSet.
24 # NOTE: Keep these in sync with static/constants.js
25 KEY__ROOT__EXTRACOLUMNHEADERS = 'extraColumnHeaders'
26 KEY__ROOT__EXTRACOLUMNORDER = 'extraColumnOrder'
27 KEY__ROOT__HEADER = 'header'
28 KEY__ROOT__IMAGEPAIRS = 'imagePairs'
29 KEY__ROOT__IMAGESETS = 'imageSets'
30 KEY__IMAGESETS__FIELD__BASE_URL = 'baseUrl'
31 KEY__IMAGESETS__FIELD__DESCRIPTION = 'description'
32 KEY__IMAGESETS__SET__DIFFS = 'diffs'
33 KEY__IMAGESETS__SET__IMAGE_A = 'imageA'
34 KEY__IMAGESETS__SET__IMAGE_B = 'imageB'
35 KEY__IMAGESETS__SET__WHITEDIFFS = 'whiteDiffs'
37 DEFAULT_DESCRIPTIONS = ('setA', 'setB')
40 class ImagePairSet(object):
41 """A collection of ImagePairs, representing two arbitrary sets of images.
44 - images generated before and after a code patch
45 - expected and actual images for some tests
46 - or any other pairwise set of images.
49 def __init__(self, diff_base_url, descriptions=None):
52 diff_base_url: base URL indicating where diff images can be loaded from
53 descriptions: a (string, string) tuple describing the two image sets.
54 If not specified, DEFAULT_DESCRIPTIONS will be used.
56 self._column_header_factories = {}
57 self._descriptions = descriptions or DEFAULT_DESCRIPTIONS
58 self._extra_column_tallies = {} # maps column_id -> values
59 # -> instances_per_value
60 self._image_base_url = None
61 self._diff_base_url = diff_base_url
63 # We build self._image_pair_objects incrementally as calls come into
64 # add_image_pair(); self._image_pair_dicts is filled in lazily (so that
65 # we put off asking ImageDiffDB for results as long as possible).
66 self._image_pair_objects = []
67 self._image_pair_dicts = None
69 def add_image_pair(self, image_pair):
70 """Adds an ImagePair; this may be repeated any number of times."""
71 # Special handling when we add the first ImagePair...
72 if not self._image_pair_objects:
73 self._image_base_url = image_pair.base_url
75 if image_pair.base_url != self._image_base_url:
76 raise Exception('added ImagePair with base_url "%s" instead of "%s"' % (
77 image_pair.base_url, self._image_base_url))
78 self._image_pair_objects.append(image_pair)
79 extra_columns_dict = image_pair.extra_columns_dict
80 if extra_columns_dict:
81 for column_id, value in extra_columns_dict.iteritems():
82 self._add_extra_column_value_to_summary(column_id, value)
84 def set_column_header_factory(self, column_id, column_header_factory):
85 """Overrides the default settings for one of the extraColumn headers.
88 column_id: string; unique ID of this column (must match a key within
89 an ImagePair's extra_columns dictionary)
90 column_header_factory: a ColumnHeaderFactory object
92 self._column_header_factories[column_id] = column_header_factory
94 def get_column_header_factory(self, column_id):
95 """Returns the ColumnHeaderFactory object for a particular extraColumn.
98 column_id: string; unique ID of this column (must match a key within
99 an ImagePair's extra_columns dictionary)
101 column_header_factory = self._column_header_factories.get(column_id, None)
102 if not column_header_factory:
103 column_header_factory = column.ColumnHeaderFactory(header_text=column_id)
104 self._column_header_factories[column_id] = column_header_factory
105 return column_header_factory
107 def ensure_extra_column_values_in_summary(self, column_id, values):
108 """Ensure this column_id/value pair is part of the extraColumns summary.
111 column_id: string; unique ID of this column
112 value: string; a possible value for this column
115 self._add_extra_column_value_to_summary(
116 column_id=column_id, value=value, addend=0)
118 def _add_extra_column_value_to_summary(self, column_id, value, addend=1):
119 """Records one column_id/value extraColumns pair found within an ImagePair.
121 We use this information to generate tallies within the column header
122 (how many instances we saw of a particular value, within a particular
126 column_id: string; unique ID of this column (must match a key within
127 an ImagePair's extra_columns dictionary)
128 value: string; a possible value for this column
129 addend: integer; how many instances to add to the tally
131 known_values_for_column = self._extra_column_tallies.get(column_id, None)
132 if not known_values_for_column:
133 known_values_for_column = {}
134 self._extra_column_tallies[column_id] = known_values_for_column
135 instances_of_this_value = known_values_for_column.get(value, 0)
136 instances_of_this_value += addend
137 known_values_for_column[value] = instances_of_this_value
139 def _column_headers_as_dict(self):
140 """Returns all column headers as a dictionary."""
142 for column_id, values_for_column in self._extra_column_tallies.iteritems():
143 column_header_factory = self.get_column_header_factory(column_id)
144 asdict[column_id] = column_header_factory.create_as_dict(
148 def as_dict(self, column_ids_in_order=None):
149 """Returns a dictionary describing this package of ImagePairs.
151 Uses the KEY__* constants as keys.
154 column_ids_in_order: A list of all extracolumn IDs in the desired display
155 order. If unspecified, they will be displayed in alphabetical order.
156 If specified, this list must contain all the extracolumn IDs!
157 (It may contain extra column IDs; they will be ignored.)
159 all_column_ids = set(self._extra_column_tallies.keys())
160 if column_ids_in_order == None:
161 column_ids_in_order = sorted(all_column_ids)
163 # Make sure the caller listed all column IDs, and throw away any extras.
164 specified_column_ids = set(column_ids_in_order)
165 forgotten_column_ids = all_column_ids - specified_column_ids
166 assert not forgotten_column_ids, (
167 'column_ids_in_order %s missing these column_ids: %s' % (
168 column_ids_in_order, forgotten_column_ids))
169 column_ids_in_order = [c for c in column_ids_in_order
170 if c in all_column_ids]
172 key_description = KEY__IMAGESETS__FIELD__DESCRIPTION
173 key_base_url = KEY__IMAGESETS__FIELD__BASE_URL
174 if gs_utils.GSUtils.is_gs_url(self._image_base_url):
175 value_base_url = self._convert_gs_url_to_http_url(self._image_base_url)
177 value_base_url = self._image_base_url
179 # We've waited as long as we can to ask ImageDiffDB for details of the
180 # image diffs, so that it has time to compute them.
181 if self._image_pair_dicts == None:
182 self._image_pair_dicts = [ip.as_dict() for ip in self._image_pair_objects]
185 KEY__ROOT__EXTRACOLUMNHEADERS: self._column_headers_as_dict(),
186 KEY__ROOT__EXTRACOLUMNORDER: column_ids_in_order,
187 KEY__ROOT__IMAGEPAIRS: self._image_pair_dicts,
188 KEY__ROOT__IMAGESETS: {
189 KEY__IMAGESETS__SET__IMAGE_A: {
190 key_description: self._descriptions[0],
191 key_base_url: value_base_url,
193 KEY__IMAGESETS__SET__IMAGE_B: {
194 key_description: self._descriptions[1],
195 key_base_url: value_base_url,
197 KEY__IMAGESETS__SET__DIFFS: {
198 key_description: 'color difference per channel',
199 key_base_url: posixpath.join(
200 self._diff_base_url, imagediffdb.RGBDIFFS_SUBDIR),
202 KEY__IMAGESETS__SET__WHITEDIFFS: {
203 key_description: 'differing pixels in white',
204 key_base_url: posixpath.join(
205 self._diff_base_url, imagediffdb.WHITEDIFFS_SUBDIR),
211 def _convert_gs_url_to_http_url(gs_url):
212 """Returns HTTP URL that can be used to download this Google Storage file.
214 TODO(epoger): Create functionality like this within gs_utils.py instead of
215 here? See https://codereview.chromium.org/428493005/ ('create
216 anyfile_utils.py for copying files between HTTP/GS/local filesystem')
219 gs_url: "gs://bucket/path" format URL
221 bucket, path = gs_utils.GSUtils.split_gs_url(gs_url)
222 http_url = 'http://storage.cloud.google.com/' + bucket
224 http_url += '/' + path