| 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 |