Upstream version 9.38.198.0
[platform/framework/web/crosswalk.git] / src / third_party / skia / gm / rebaseline_server / imagepairset.py
1 #!/usr/bin/python
2
3 """
4 Copyright 2014 Google Inc.
5
6 Use of this source code is governed by a BSD-style license that can be
7 found in the LICENSE file.
8
9 ImagePairSet class; see its docstring below.
10 """
11
12 # System-level imports
13 import posixpath
14
15 # Must fix up PYTHONPATH before importing from within Skia
16 import fix_pythonpath  # pylint: disable=W0611
17
18 # Imports from within Skia
19 import column
20 import imagediffdb
21 from py.utils import gs_utils
22
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'
36
37 DEFAULT_DESCRIPTIONS = ('setA', 'setB')
38
39
40 class ImagePairSet(object):
41   """A collection of ImagePairs, representing two arbitrary sets of images.
42
43   These could be:
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.
47   """
48
49   def __init__(self, diff_base_url, descriptions=None):
50     """
51     Args:
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.
55     """
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
62
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
68
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
74
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)
83
84   def set_column_header_factory(self, column_id, column_header_factory):
85     """Overrides the default settings for one of the extraColumn headers.
86
87     Args:
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
91     """
92     self._column_header_factories[column_id] = column_header_factory
93
94   def get_column_header_factory(self, column_id):
95     """Returns the ColumnHeaderFactory object for a particular extraColumn.
96
97     Args:
98       column_id: string; unique ID of this column (must match a key within
99           an ImagePair's extra_columns dictionary)
100     """
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
106
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.
109
110     Args:
111       column_id: string; unique ID of this column
112       value: string; a possible value for this column
113     """
114     for value in values:
115       self._add_extra_column_value_to_summary(
116           column_id=column_id, value=value, addend=0)
117
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.
120
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
123     extraColumn).
124
125     Args:
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
130     """
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
138
139   def _column_headers_as_dict(self):
140     """Returns all column headers as a dictionary."""
141     asdict = {}
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(
145           values_for_column)
146     return asdict
147
148   def as_dict(self, column_ids_in_order=None):
149     """Returns a dictionary describing this package of ImagePairs.
150
151     Uses the KEY__* constants as keys.
152
153     Args:
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.)
158     """
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)
162     else:
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]
171
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)
176     else:
177       value_base_url = self._image_base_url
178
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]
183
184     return {
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,
192             },
193             KEY__IMAGESETS__SET__IMAGE_B: {
194                 key_description: self._descriptions[1],
195                 key_base_url: value_base_url,
196             },
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),
201             },
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),
206             },
207         },
208     }
209
210   @staticmethod
211   def _convert_gs_url_to_http_url(gs_url):
212     """Returns HTTP URL that can be used to download this Google Storage file.
213
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')
217
218     Args:
219       gs_url: "gs://bucket/path" format URL
220     """
221     bucket, path = gs_utils.GSUtils.split_gs_url(gs_url)
222     http_url = 'http://storage.cloud.google.com/' + bucket
223     if path:
224       http_url += '/' + path
225     return http_url