OLD | NEW |
1 #!/usr/bin/python | 1 #!/usr/bin/python |
2 | 2 |
3 """ | 3 """ |
4 Copyright 2014 Google Inc. | 4 Copyright 2014 Google Inc. |
5 | 5 |
6 Use of this source code is governed by a BSD-style license that can be | 6 Use of this source code is governed by a BSD-style license that can be |
7 found in the LICENSE file. | 7 found in the LICENSE file. |
8 | 8 |
9 Compare results of two render_pictures runs. | 9 Compare results of two render_pictures runs. |
10 """ | 10 """ |
(...skipping 21 matching lines...) Expand all Loading... |
32 TRUNK_DIRECTORY = os.path.dirname(GM_DIRECTORY) | 32 TRUNK_DIRECTORY = os.path.dirname(GM_DIRECTORY) |
33 if GM_DIRECTORY not in sys.path: | 33 if GM_DIRECTORY not in sys.path: |
34 sys.path.append(GM_DIRECTORY) | 34 sys.path.append(GM_DIRECTORY) |
35 import download_actuals | 35 import download_actuals |
36 import gm_json | 36 import gm_json |
37 import imagediffdb | 37 import imagediffdb |
38 import imagepair | 38 import imagepair |
39 import imagepairset | 39 import imagepairset |
40 import results | 40 import results |
41 | 41 |
42 # Characters we don't want popping up just anywhere within filenames. | |
43 DISALLOWED_FILEPATH_CHAR_REGEX = re.compile('[^\w\-]') | |
44 | |
45 # URL under which all render_pictures images can be found in Google Storage. | 42 # URL under which all render_pictures images can be found in Google Storage. |
46 # TODO(epoger): Move this default value into | 43 # TODO(epoger): Move this default value into |
47 # https://skia.googlesource.com/buildbot/+/master/site_config/global_variables.j
son | 44 # https://skia.googlesource.com/buildbot/+/master/site_config/global_variables.j
son |
48 DEFAULT_IMAGE_BASE_URL = 'http://chromium-skia-gm.commondatastorage.googleapis.c
om/render_pictures/images' | 45 DEFAULT_IMAGE_BASE_URL = 'http://chromium-skia-gm.commondatastorage.googleapis.c
om/render_pictures/images' |
49 | 46 |
50 | 47 |
51 class RenderedPicturesComparisons(results.BaseComparisons): | 48 class RenderedPicturesComparisons(results.BaseComparisons): |
52 """Loads results from two different render_pictures runs into an ImagePairSet. | 49 """Loads results from two different render_pictures runs into an ImagePairSet. |
53 """ | 50 """ |
54 | 51 |
(...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
90 Args: | 87 Args: |
91 actuals_root: root directory containing all render_pictures-generated | 88 actuals_root: root directory containing all render_pictures-generated |
92 JSON files | 89 JSON files |
93 subdirs: (string, string) tuple; pair of subdirectories within | 90 subdirs: (string, string) tuple; pair of subdirectories within |
94 actuals_root to compare | 91 actuals_root to compare |
95 """ | 92 """ |
96 logging.info( | 93 logging.info( |
97 'Reading actual-results JSON files from %s subdirs within %s...' % ( | 94 'Reading actual-results JSON files from %s subdirs within %s...' % ( |
98 subdirs, actuals_root)) | 95 subdirs, actuals_root)) |
99 subdirA, subdirB = subdirs | 96 subdirA, subdirB = subdirs |
100 subdirA_builder_dicts = self._read_dicts_from_root( | 97 subdirA_dicts = self._read_dicts_from_root( |
101 os.path.join(actuals_root, subdirA)) | 98 os.path.join(actuals_root, subdirA)) |
102 subdirB_builder_dicts = self._read_dicts_from_root( | 99 subdirB_dicts = self._read_dicts_from_root( |
103 os.path.join(actuals_root, subdirB)) | 100 os.path.join(actuals_root, subdirB)) |
104 logging.info('Comparing subdirs %s and %s...' % (subdirA, subdirB)) | 101 logging.info('Comparing subdirs %s and %s...' % (subdirA, subdirB)) |
105 | 102 |
106 all_image_pairs = imagepairset.ImagePairSet( | 103 all_image_pairs = imagepairset.ImagePairSet( |
107 descriptions=subdirs, | 104 descriptions=subdirs, |
108 diff_base_url=self._diff_base_url) | 105 diff_base_url=self._diff_base_url) |
109 failing_image_pairs = imagepairset.ImagePairSet( | 106 failing_image_pairs = imagepairset.ImagePairSet( |
110 descriptions=subdirs, | 107 descriptions=subdirs, |
111 diff_base_url=self._diff_base_url) | 108 diff_base_url=self._diff_base_url) |
112 | 109 |
113 all_image_pairs.ensure_extra_column_values_in_summary( | 110 all_image_pairs.ensure_extra_column_values_in_summary( |
114 column_id=results.KEY__EXTRACOLUMN__RESULT_TYPE, values=[ | 111 column_id=results.KEY__EXTRACOLUMN__RESULT_TYPE, values=[ |
115 results.KEY__RESULT_TYPE__FAILED, | 112 results.KEY__RESULT_TYPE__FAILED, |
116 results.KEY__RESULT_TYPE__NOCOMPARISON, | 113 results.KEY__RESULT_TYPE__NOCOMPARISON, |
117 results.KEY__RESULT_TYPE__SUCCEEDED, | 114 results.KEY__RESULT_TYPE__SUCCEEDED, |
118 ]) | 115 ]) |
119 failing_image_pairs.ensure_extra_column_values_in_summary( | 116 failing_image_pairs.ensure_extra_column_values_in_summary( |
120 column_id=results.KEY__EXTRACOLUMN__RESULT_TYPE, values=[ | 117 column_id=results.KEY__EXTRACOLUMN__RESULT_TYPE, values=[ |
121 results.KEY__RESULT_TYPE__FAILED, | 118 results.KEY__RESULT_TYPE__FAILED, |
122 results.KEY__RESULT_TYPE__NOCOMPARISON, | 119 results.KEY__RESULT_TYPE__NOCOMPARISON, |
123 ]) | 120 ]) |
124 | 121 |
125 builders = sorted(set(subdirA_builder_dicts.keys() + | 122 common_dict_paths = sorted(set(subdirA_dicts.keys() + subdirB_dicts.keys())) |
126 subdirB_builder_dicts.keys())) | 123 num_common_dict_paths = len(common_dict_paths) |
127 num_builders = len(builders) | 124 dict_num = 0 |
128 builder_num = 0 | 125 for dict_path in common_dict_paths: |
129 for builder in builders: | 126 dict_num += 1 |
130 builder_num += 1 | 127 logging.info('Generating pixel diffs for dict #%d of %d, "%s"...' % |
131 logging.info('Generating pixel diffs for builder #%d of %d, "%s"...' % | 128 (dict_num, num_common_dict_paths, dict_path)) |
132 (builder_num, num_builders, builder)) | 129 dictA = subdirA_dicts[dict_path] |
133 # TODO(epoger): This will fail if we have results for this builder in | 130 dictB = subdirB_dicts[dict_path] |
134 # subdirA but not subdirB (or vice versa). | 131 self._validate_dict_version(dictA) |
135 subdirA_results = results.BaseComparisons.combine_subdicts( | 132 self._validate_dict_version(dictB) |
136 subdirA_builder_dicts[builder][gm_json.JSONKEY_ACTUALRESULTS]) | 133 dictA_results = dictA[gm_json.JSONKEY_ACTUALRESULTS] |
137 subdirB_results = results.BaseComparisons.combine_subdicts( | 134 dictB_results = dictB[gm_json.JSONKEY_ACTUALRESULTS] |
138 subdirB_builder_dicts[builder][gm_json.JSONKEY_ACTUALRESULTS]) | 135 skp_names = sorted(set(dictA_results.keys() + dictB_results.keys())) |
139 image_names = sorted(set(subdirA_results.keys() + | 136 for skp_name in skp_names: |
140 subdirB_results.keys())) | 137 imagepairs_for_this_skp = [] |
141 for image_name in image_names: | |
142 # The image name may contain funny characters or be ridiculously long | |
143 # (see https://code.google.com/p/skia/issues/detail?id=2344#c10 ), | |
144 # so make sure we sanitize it before using it in a URL path. | |
145 # | |
146 # TODO(epoger): Rather than sanitizing/truncating the image name here, | |
147 # do it in render_pictures instead. | |
148 # Reason: we will need to be consistent in applying this rule, so that | |
149 # the process which uploads the files to GS using these paths will | |
150 # match the paths created by downstream processes. | |
151 # So, we should make render_pictures write out images to paths that are | |
152 # "ready to upload" to Google Storage, like gm does. | |
153 sanitized_test_name = DISALLOWED_FILEPATH_CHAR_REGEX.sub( | |
154 '_', image_name)[:30] | |
155 | 138 |
156 subdirA_image_relative_url = ( | 139 whole_image_A = RenderedPicturesComparisons.get_multilevel( |
157 results.BaseComparisons._create_relative_url( | 140 dictA_results, skp_name, gm_json.JSONKEY_SOURCE_WHOLEIMAGE) |
158 hashtype_and_digest=subdirA_results.get(image_name), | 141 whole_image_B = RenderedPicturesComparisons.get_multilevel( |
159 test_name=sanitized_test_name)) | 142 dictB_results, skp_name, gm_json.JSONKEY_SOURCE_WHOLEIMAGE) |
160 subdirB_image_relative_url = ( | 143 imagepairs_for_this_skp.append(self._create_image_pair( |
161 results.BaseComparisons._create_relative_url( | 144 test=skp_name, config=gm_json.JSONKEY_SOURCE_WHOLEIMAGE, |
162 hashtype_and_digest=subdirB_results.get(image_name), | 145 image_dict_A=whole_image_A, image_dict_B=whole_image_B)) |
163 test_name=sanitized_test_name)) | |
164 | 146 |
165 # If we have images for at least one of these two subdirs, | 147 tiled_images_A = RenderedPicturesComparisons.get_multilevel( |
166 # add them to our list. | 148 dictA_results, skp_name, gm_json.JSONKEY_SOURCE_TILEDIMAGES) |
167 if subdirA_image_relative_url or subdirB_image_relative_url: | 149 tiled_images_B = RenderedPicturesComparisons.get_multilevel( |
168 if subdirA_image_relative_url == subdirB_image_relative_url: | 150 dictB_results, skp_name, gm_json.JSONKEY_SOURCE_TILEDIMAGES) |
169 result_type = results.KEY__RESULT_TYPE__SUCCEEDED | 151 # TODO(epoger): Report an error if we find tiles for A but not B? |
170 elif not subdirA_image_relative_url: | 152 if tiled_images_A and tiled_images_B: |
171 result_type = results.KEY__RESULT_TYPE__NOCOMPARISON | 153 # TODO(epoger): Report an error if we find a different number of tiles |
172 elif not subdirB_image_relative_url: | 154 # for A and B? |
173 result_type = results.KEY__RESULT_TYPE__NOCOMPARISON | 155 num_tiles = len(tiled_images_A) |
174 else: | 156 for tile_num in range(num_tiles): |
175 result_type = results.KEY__RESULT_TYPE__FAILED | 157 imagepairs_for_this_skp.append(self._create_image_pair( |
| 158 test=skp_name, |
| 159 config='%s-%d' % (gm_json.JSONKEY_SOURCE_TILEDIMAGES, tile_num), |
| 160 image_dict_A=tiled_images_A[tile_num], |
| 161 image_dict_B=tiled_images_B[tile_num])) |
176 | 162 |
177 extra_columns_dict = { | 163 for imagepair in imagepairs_for_this_skp: |
178 results.KEY__EXTRACOLUMN__RESULT_TYPE: result_type, | 164 if imagepair: |
179 results.KEY__EXTRACOLUMN__BUILDER: builder, | 165 all_image_pairs.add_image_pair(imagepair) |
180 results.KEY__EXTRACOLUMN__TEST: image_name, | 166 result_type = imagepair.extra_columns_dict\ |
181 # TODO(epoger): Right now, the client UI crashes if it receives | 167 [results.KEY__EXTRACOLUMN__RESULT_TYPE] |
182 # results that do not include a 'config' column. | 168 if result_type != results.KEY__RESULT_TYPE__SUCCEEDED: |
183 # Until we fix that, keep the client happy. | 169 failing_image_pairs.add_image_pair(imagepair) |
184 results.KEY__EXTRACOLUMN__CONFIG: 'TODO', | |
185 } | |
186 | |
187 try: | |
188 image_pair = imagepair.ImagePair( | |
189 image_diff_db=self._image_diff_db, | |
190 base_url=self._image_base_url, | |
191 imageA_relative_url=subdirA_image_relative_url, | |
192 imageB_relative_url=subdirB_image_relative_url, | |
193 extra_columns=extra_columns_dict) | |
194 all_image_pairs.add_image_pair(image_pair) | |
195 if result_type != results.KEY__RESULT_TYPE__SUCCEEDED: | |
196 failing_image_pairs.add_image_pair(image_pair) | |
197 except (KeyError, TypeError): | |
198 logging.exception( | |
199 'got exception while creating ImagePair for image_name ' | |
200 '"%s", builder "%s"' % (image_name, builder)) | |
201 | 170 |
202 self._results = { | 171 self._results = { |
203 results.KEY__HEADER__RESULTS_ALL: all_image_pairs.as_dict(), | 172 results.KEY__HEADER__RESULTS_ALL: all_image_pairs.as_dict(), |
204 results.KEY__HEADER__RESULTS_FAILURES: failing_image_pairs.as_dict(), | 173 results.KEY__HEADER__RESULTS_FAILURES: failing_image_pairs.as_dict(), |
205 } | 174 } |
206 | 175 |
| 176 def _validate_dict_version(self, result_dict): |
| 177 """Raises Exception if the dict is not the type/version we know how to read. |
| 178 |
| 179 Args: |
| 180 result_dict: dictionary holding output of render_pictures |
| 181 """ |
| 182 expected_header_type = 'ChecksummedImages' |
| 183 expected_header_revision = 1 |
| 184 |
| 185 header = result_dict[gm_json.JSONKEY_HEADER] |
| 186 header_type = header[gm_json.JSONKEY_HEADER_TYPE] |
| 187 if header_type != expected_header_type: |
| 188 raise Exception('expected header_type "%s", but got "%s"' % ( |
| 189 expected_header_type, header_type)) |
| 190 header_revision = header[gm_json.JSONKEY_HEADER_REVISION] |
| 191 if header_revision != expected_header_revision: |
| 192 raise Exception('expected header_revision %d, but got %d' % ( |
| 193 expected_header_revision, header_revision)) |
| 194 |
| 195 def _create_image_pair(self, test, config, image_dict_A, image_dict_B): |
| 196 """Creates an ImagePair object for this pair of images. |
| 197 |
| 198 Args: |
| 199 test: string; name of the test |
| 200 config: string; name of the config |
| 201 image_dict_A: dict with JSONKEY_IMAGE_* keys, or None if no image |
| 202 image_dict_B: dict with JSONKEY_IMAGE_* keys, or None if no image |
| 203 |
| 204 Returns: |
| 205 An ImagePair object, or None if both image_dict_A and image_dict_B are |
| 206 None. |
| 207 """ |
| 208 if (not image_dict_A) and (not image_dict_B): |
| 209 return None |
| 210 |
| 211 def _checksum_and_relative_url(dic): |
| 212 if dic: |
| 213 return ((dic[gm_json.JSONKEY_IMAGE_CHECKSUMALGORITHM], |
| 214 dic[gm_json.JSONKEY_IMAGE_CHECKSUMVALUE]), |
| 215 dic[gm_json.JSONKEY_IMAGE_FILEPATH]) |
| 216 else: |
| 217 return None, None |
| 218 |
| 219 imageA_checksum, imageA_relative_url = _checksum_and_relative_url( |
| 220 image_dict_A) |
| 221 imageB_checksum, imageB_relative_url = _checksum_and_relative_url( |
| 222 image_dict_B) |
| 223 |
| 224 if not imageA_checksum: |
| 225 result_type = results.KEY__RESULT_TYPE__NOCOMPARISON |
| 226 elif not imageB_checksum: |
| 227 result_type = results.KEY__RESULT_TYPE__NOCOMPARISON |
| 228 elif imageA_checksum == imageB_checksum: |
| 229 result_type = results.KEY__RESULT_TYPE__SUCCEEDED |
| 230 else: |
| 231 result_type = results.KEY__RESULT_TYPE__FAILED |
| 232 |
| 233 extra_columns_dict = { |
| 234 results.KEY__EXTRACOLUMN__CONFIG: config, |
| 235 results.KEY__EXTRACOLUMN__RESULT_TYPE: result_type, |
| 236 results.KEY__EXTRACOLUMN__TEST: test, |
| 237 # TODO(epoger): Right now, the client UI crashes if it receives |
| 238 # results that do not include this column. |
| 239 # Until we fix that, keep the client happy. |
| 240 results.KEY__EXTRACOLUMN__BUILDER: 'TODO', |
| 241 } |
| 242 |
| 243 try: |
| 244 return imagepair.ImagePair( |
| 245 image_diff_db=self._image_diff_db, |
| 246 base_url=self._image_base_url, |
| 247 imageA_relative_url=imageA_relative_url, |
| 248 imageB_relative_url=imageB_relative_url, |
| 249 extra_columns=extra_columns_dict) |
| 250 except (KeyError, TypeError): |
| 251 logging.exception( |
| 252 'got exception while creating ImagePair for' |
| 253 ' test="%s", config="%s", urlPair=("%s","%s")' % ( |
| 254 test, config, imageA_relative_url, imageB_relative_url)) |
| 255 return None |
| 256 |
207 | 257 |
208 # TODO(epoger): Add main() so this can be called by vm_run_skia_try.sh | 258 # TODO(epoger): Add main() so this can be called by vm_run_skia_try.sh |
OLD | NEW |