OLD | NEW |
(Empty) | |
| 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 rs_fixpypath # 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._imageA_base_url = None |
| 61 self._imageB_base_url = None |
| 62 self._diff_base_url = diff_base_url |
| 63 |
| 64 # We build self._image_pair_objects incrementally as calls come into |
| 65 # add_image_pair(); self._image_pair_dicts is filled in lazily (so that |
| 66 # we put off asking ImageDiffDB for results as long as possible). |
| 67 self._image_pair_objects = [] |
| 68 self._image_pair_dicts = None |
| 69 |
| 70 def add_image_pair(self, image_pair): |
| 71 """Adds an ImagePair; this may be repeated any number of times.""" |
| 72 # Special handling when we add the first ImagePair... |
| 73 if not self._image_pair_objects: |
| 74 self._imageA_base_url = image_pair.imageA_base_url |
| 75 self._imageB_base_url = image_pair.imageB_base_url |
| 76 |
| 77 if(image_pair.imageA_base_url != self._imageA_base_url): |
| 78 raise Exception('added ImagePair with base_url "%s" instead of "%s"' % ( |
| 79 image_pair.imageA_base_url, self._imageA_base_url)) |
| 80 if(image_pair.imageB_base_url != self._imageB_base_url): |
| 81 raise Exception('added ImagePair with base_url "%s" instead of "%s"' % ( |
| 82 image_pair.imageB_base_url, self._imageB_base_url)) |
| 83 self._image_pair_objects.append(image_pair) |
| 84 extra_columns_dict = image_pair.extra_columns_dict |
| 85 if extra_columns_dict: |
| 86 for column_id, value in extra_columns_dict.iteritems(): |
| 87 self._add_extra_column_value_to_summary(column_id, value) |
| 88 |
| 89 def set_column_header_factory(self, column_id, column_header_factory): |
| 90 """Overrides the default settings for one of the extraColumn headers. |
| 91 |
| 92 Args: |
| 93 column_id: string; unique ID of this column (must match a key within |
| 94 an ImagePair's extra_columns dictionary) |
| 95 column_header_factory: a ColumnHeaderFactory object |
| 96 """ |
| 97 self._column_header_factories[column_id] = column_header_factory |
| 98 |
| 99 def get_column_header_factory(self, column_id): |
| 100 """Returns the ColumnHeaderFactory object for a particular extraColumn. |
| 101 |
| 102 Args: |
| 103 column_id: string; unique ID of this column (must match a key within |
| 104 an ImagePair's extra_columns dictionary) |
| 105 """ |
| 106 column_header_factory = self._column_header_factories.get(column_id, None) |
| 107 if not column_header_factory: |
| 108 column_header_factory = column.ColumnHeaderFactory(header_text=column_id) |
| 109 self._column_header_factories[column_id] = column_header_factory |
| 110 return column_header_factory |
| 111 |
| 112 def ensure_extra_column_values_in_summary(self, column_id, values): |
| 113 """Ensure this column_id/value pair is part of the extraColumns summary. |
| 114 |
| 115 Args: |
| 116 column_id: string; unique ID of this column |
| 117 value: string; a possible value for this column |
| 118 """ |
| 119 for value in values: |
| 120 self._add_extra_column_value_to_summary( |
| 121 column_id=column_id, value=value, addend=0) |
| 122 |
| 123 def _add_extra_column_value_to_summary(self, column_id, value, addend=1): |
| 124 """Records one column_id/value extraColumns pair found within an ImagePair. |
| 125 |
| 126 We use this information to generate tallies within the column header |
| 127 (how many instances we saw of a particular value, within a particular |
| 128 extraColumn). |
| 129 |
| 130 Args: |
| 131 column_id: string; unique ID of this column (must match a key within |
| 132 an ImagePair's extra_columns dictionary) |
| 133 value: string; a possible value for this column |
| 134 addend: integer; how many instances to add to the tally |
| 135 """ |
| 136 known_values_for_column = self._extra_column_tallies.get(column_id, None) |
| 137 if not known_values_for_column: |
| 138 known_values_for_column = {} |
| 139 self._extra_column_tallies[column_id] = known_values_for_column |
| 140 instances_of_this_value = known_values_for_column.get(value, 0) |
| 141 instances_of_this_value += addend |
| 142 known_values_for_column[value] = instances_of_this_value |
| 143 |
| 144 def _column_headers_as_dict(self): |
| 145 """Returns all column headers as a dictionary.""" |
| 146 asdict = {} |
| 147 for column_id, values_for_column in self._extra_column_tallies.iteritems(): |
| 148 column_header_factory = self.get_column_header_factory(column_id) |
| 149 asdict[column_id] = column_header_factory.create_as_dict( |
| 150 values_for_column) |
| 151 return asdict |
| 152 |
| 153 def as_dict(self, column_ids_in_order=None): |
| 154 """Returns a dictionary describing this package of ImagePairs. |
| 155 |
| 156 Uses the KEY__* constants as keys. |
| 157 |
| 158 Args: |
| 159 column_ids_in_order: A list of all extracolumn IDs in the desired display |
| 160 order. If unspecified, they will be displayed in alphabetical order. |
| 161 If specified, this list must contain all the extracolumn IDs! |
| 162 (It may contain extra column IDs; they will be ignored.) |
| 163 """ |
| 164 all_column_ids = set(self._extra_column_tallies.keys()) |
| 165 if column_ids_in_order == None: |
| 166 column_ids_in_order = sorted(all_column_ids) |
| 167 else: |
| 168 # Make sure the caller listed all column IDs, and throw away any extras. |
| 169 specified_column_ids = set(column_ids_in_order) |
| 170 forgotten_column_ids = all_column_ids - specified_column_ids |
| 171 assert not forgotten_column_ids, ( |
| 172 'column_ids_in_order %s missing these column_ids: %s' % ( |
| 173 column_ids_in_order, forgotten_column_ids)) |
| 174 column_ids_in_order = [c for c in column_ids_in_order |
| 175 if c in all_column_ids] |
| 176 |
| 177 key_description = KEY__IMAGESETS__FIELD__DESCRIPTION |
| 178 key_base_url = KEY__IMAGESETS__FIELD__BASE_URL |
| 179 if gs_utils.GSUtils.is_gs_url(self._imageA_base_url): |
| 180 valueA_base_url = self._convert_gs_url_to_http_url(self._imageA_base_url) |
| 181 else: |
| 182 valueA_base_url = self._imageA_base_url |
| 183 if gs_utils.GSUtils.is_gs_url(self._imageB_base_url): |
| 184 valueB_base_url = self._convert_gs_url_to_http_url(self._imageB_base_url) |
| 185 else: |
| 186 valueB_base_url = self._imageB_base_url |
| 187 |
| 188 # We've waited as long as we can to ask ImageDiffDB for details of the |
| 189 # image diffs, so that it has time to compute them. |
| 190 if self._image_pair_dicts == None: |
| 191 self._image_pair_dicts = [ip.as_dict() for ip in self._image_pair_objects] |
| 192 |
| 193 return { |
| 194 KEY__ROOT__EXTRACOLUMNHEADERS: self._column_headers_as_dict(), |
| 195 KEY__ROOT__EXTRACOLUMNORDER: column_ids_in_order, |
| 196 KEY__ROOT__IMAGEPAIRS: self._image_pair_dicts, |
| 197 KEY__ROOT__IMAGESETS: { |
| 198 KEY__IMAGESETS__SET__IMAGE_A: { |
| 199 key_description: self._descriptions[0], |
| 200 key_base_url: valueA_base_url, |
| 201 }, |
| 202 KEY__IMAGESETS__SET__IMAGE_B: { |
| 203 key_description: self._descriptions[1], |
| 204 key_base_url: valueB_base_url, |
| 205 }, |
| 206 KEY__IMAGESETS__SET__DIFFS: { |
| 207 key_description: 'color difference per channel', |
| 208 key_base_url: posixpath.join( |
| 209 self._diff_base_url, imagediffdb.RGBDIFFS_SUBDIR), |
| 210 }, |
| 211 KEY__IMAGESETS__SET__WHITEDIFFS: { |
| 212 key_description: 'differing pixels in white', |
| 213 key_base_url: posixpath.join( |
| 214 self._diff_base_url, imagediffdb.WHITEDIFFS_SUBDIR), |
| 215 }, |
| 216 }, |
| 217 } |
| 218 |
| 219 @staticmethod |
| 220 def _convert_gs_url_to_http_url(gs_url): |
| 221 """Returns HTTP URL that can be used to download this Google Storage file. |
| 222 |
| 223 TODO(epoger): Create functionality like this within gs_utils.py instead of |
| 224 here? See https://codereview.chromium.org/428493005/ ('create |
| 225 anyfile_utils.py for copying files between HTTP/GS/local filesystem') |
| 226 |
| 227 Args: |
| 228 gs_url: "gs://bucket/path" format URL |
| 229 """ |
| 230 bucket, path = gs_utils.GSUtils.split_gs_url(gs_url) |
| 231 http_url = 'http://storage.cloud.google.com/' + bucket |
| 232 if path: |
| 233 http_url += '/' + path |
| 234 return http_url |
OLD | NEW |