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 |
| 11 EPOGER: Rename this module, because it is more generally useful now... it can do
wnload and compare any types of images. |
10 """ | 12 """ |
11 | 13 |
12 # System-level imports | 14 # System-level imports |
13 import logging | 15 import logging |
14 import os | 16 import os |
| 17 import shutil |
| 18 import tempfile |
15 import time | 19 import time |
16 | 20 |
17 # Must fix up PYTHONPATH before importing from within Skia | 21 # Must fix up PYTHONPATH before importing from within Skia |
18 import fix_pythonpath # pylint: disable=W0611 | 22 import fix_pythonpath # pylint: disable=W0611 |
19 | 23 |
20 # Imports from within Skia | 24 # Imports from within Skia |
21 from py.utils import url_utils | 25 from py.utils import url_utils |
| 26 import buildbot_globals |
22 import gm_json | 27 import gm_json |
23 import imagediffdb | 28 import imagediffdb |
24 import imagepair | 29 import imagepair |
25 import imagepairset | 30 import imagepairset |
26 import results | 31 import results |
27 | 32 |
28 # URL under which all render_pictures images can be found in Google Storage. | 33 # URL under which all render_pictures images can be found in Google Storage. |
29 # | 34 # |
30 # pylint: disable=C0301 | 35 # EPOGER: In order to allow live-view of GMs and other images, read this from th
e input summary files, or allow the caller to set it within the GET_live_results
call? |
31 # TODO(epoger): Move this default value into | 36 DEFAULT_IMAGE_BASE_GS_URL = 'gs://' + buildbot_globals.Get('skp_images_bucket') |
32 # https://skia.googlesource.com/buildbot/+/master/site_config/global_variables.j
son | |
33 # pylint: enable=C0301 | |
34 DEFAULT_IMAGE_BASE_URL = ( | |
35 'http://chromium-skia-gm.commondatastorage.googleapis.com/' | |
36 'render_pictures/images') | |
37 | 37 |
38 | 38 |
39 class RenderedPicturesComparisons(results.BaseComparisons): | 39 class RenderedPicturesComparisons(results.BaseComparisons): |
40 """Loads results from two different render_pictures runs into an ImagePairSet. | 40 """Loads results from multiple render_pictures runs into an ImagePairSet. |
41 """ | 41 """ |
42 | 42 |
43 def __init__(self, subdirs, actuals_root, | 43 def __init__(self, actuals_dirs, expectations_dirs, image_diff_db, |
44 generated_images_root=results.DEFAULT_GENERATED_IMAGES_ROOT, | 44 image_base_gs_url=DEFAULT_IMAGE_BASE_GS_URL, |
45 image_base_url=DEFAULT_IMAGE_BASE_URL, | 45 diff_base_url=None, actuals_label='actuals', |
46 diff_base_url=None): | 46 expectations_label='expectations'): |
47 """ | 47 """ |
48 Args: | 48 Args: |
49 actuals_root: root directory containing all render_pictures-generated | 49 actuals_dirs: list of root directories to copy all JSON summaries from, |
50 JSON files | 50 and to use as actual results |
51 subdirs: (string, string) tuple; pair of subdirectories within | 51 expectations_dirs: list of root directories to copy all JSON summaries |
52 actuals_root to compare | 52 from, and to use as expected results |
53 generated_images_root: directory within which to create all pixel diffs; | 53 image_diff_db: ImageDiffDB instance |
54 if this directory does not yet exist, it will be created | 54 image_base_gs_url: "gs://" URL pointing at the Google Storage bucket/dir |
55 image_base_url: URL under which all render_pictures result images can | 55 under which all render_pictures result images can |
56 be found; this will be used to read images for comparison within | 56 be found; this will be used to read images for comparison within |
57 this code, and included in the ImagePairSet so its consumers know | 57 this code, and included in the ImagePairSet (as an HTTP URL) so its |
58 where to download the images from | 58 consumers know where to download the images from |
59 diff_base_url: base URL within which the client should look for diff | 59 diff_base_url: base URL within which the client should look for diff |
60 images; if not specified, defaults to a "file:///" URL representation | 60 images; if not specified, defaults to a "file:///" URL representation |
61 of generated_images_root | 61 of image_diff_db's storage_root |
| 62 actuals_label: description to use for actual results |
| 63 expectations_label: description to use for expected results |
62 """ | 64 """ |
63 time_start = int(time.time()) | 65 self._image_diff_db = image_diff_db |
64 self._image_diff_db = imagediffdb.ImageDiffDB(generated_images_root) | 66 self._image_base_gs_url = image_base_gs_url |
65 self._image_base_url = image_base_url | |
66 self._diff_base_url = ( | 67 self._diff_base_url = ( |
67 diff_base_url or | 68 diff_base_url or |
68 url_utils.create_filepath_url(generated_images_root)) | 69 url_utils.create_filepath_url(image_diff_db.storage_root)) |
69 self._load_result_pairs(actuals_root, subdirs) | 70 self._actuals_label = actuals_label |
70 self._timestamp = int(time.time()) | 71 self._expectations_label = expectations_label |
71 logging.info('Results complete; took %d seconds.' % | |
72 (self._timestamp - time_start)) | |
73 | 72 |
74 def _load_result_pairs(self, actuals_root, subdirs): | 73 tempdir = tempfile.mkdtemp() |
75 """Loads all JSON files found within two subdirs in actuals_root, | 74 try: |
76 compares across those two subdirs, and stores the summary in self._results. | 75 actuals_root = os.path.join(tempdir, 'actuals') |
| 76 expectations_root = os.path.join(tempdir, 'expectations') |
| 77 # Copy all summary files into actuals_root and expectations_root |
| 78 # EPOGER: this won't work when *_dirs have more than one entry |
| 79 for dir in actuals_dirs: |
| 80 shutil.copytree(dir, actuals_root) |
| 81 for dir in expectations_dirs: |
| 82 shutil.copytree(dir, expectations_root) |
| 83 |
| 84 time_start = int(time.time()) |
| 85 self._load_result_pairs(actuals_root, expectations_root) |
| 86 self._timestamp = int(time.time()) |
| 87 logging.info('Results complete; took %d seconds.' % |
| 88 (self._timestamp - time_start)) |
| 89 finally: |
| 90 shutil.rmtree(tempdir) |
| 91 |
| 92 def _load_result_pairs(self, actuals_root, expectations_root): |
| 93 """Loads all JSON image summaries from 2 directory trees and compares them. |
| 94 |
| 95 The summary of all image diff results is stored in in self._results. |
| 96 EPOGER: we don't want to store the results in self._results anymore... now,
we just want to return them |
77 | 97 |
78 Args: | 98 Args: |
79 actuals_root: root directory containing all render_pictures-generated | 99 actuals_root: root directory containing JSON summaries of actual results |
80 JSON files | 100 expectations_root: root dir containing JSON summaries of expected results |
81 subdirs: (string, string) tuple; pair of subdirectories within | |
82 actuals_root to compare | |
83 """ | 101 """ |
84 logging.info( | 102 logging.info('Reading JSON image summaries from dirs %s and %s...' % ( |
85 'Reading actual-results JSON files from %s subdirs within %s...' % ( | 103 actuals_root, expectations_root)) |
86 subdirs, actuals_root)) | 104 actuals_dicts = self._read_dicts_from_root(actuals_root) |
87 subdirA, subdirB = subdirs | 105 expectations_dicts = self._read_dicts_from_root(expectations_root) |
88 subdirA_dicts = self._read_dicts_from_root( | 106 logging.info('Comparing summary dicts...') |
89 os.path.join(actuals_root, subdirA)) | |
90 subdirB_dicts = self._read_dicts_from_root( | |
91 os.path.join(actuals_root, subdirB)) | |
92 logging.info('Comparing subdirs %s and %s...' % (subdirA, subdirB)) | |
93 | 107 |
94 all_image_pairs = imagepairset.ImagePairSet( | 108 all_image_pairs = imagepairset.ImagePairSet( |
95 descriptions=subdirs, | 109 descriptions=(self._actuals_label, self._expectations_label), |
96 diff_base_url=self._diff_base_url) | 110 diff_base_url=self._diff_base_url) |
97 failing_image_pairs = imagepairset.ImagePairSet( | 111 failing_image_pairs = imagepairset.ImagePairSet( |
98 descriptions=subdirs, | 112 descriptions=(self._actuals_label, self._expectations_label), |
99 diff_base_url=self._diff_base_url) | 113 diff_base_url=self._diff_base_url) |
100 | 114 |
101 all_image_pairs.ensure_extra_column_values_in_summary( | 115 all_image_pairs.ensure_extra_column_values_in_summary( |
102 column_id=results.KEY__EXTRACOLUMNS__RESULT_TYPE, values=[ | 116 column_id=results.KEY__EXTRACOLUMNS__RESULT_TYPE, values=[ |
103 results.KEY__RESULT_TYPE__FAILED, | 117 results.KEY__RESULT_TYPE__FAILED, |
104 results.KEY__RESULT_TYPE__NOCOMPARISON, | 118 results.KEY__RESULT_TYPE__NOCOMPARISON, |
105 results.KEY__RESULT_TYPE__SUCCEEDED, | 119 results.KEY__RESULT_TYPE__SUCCEEDED, |
106 ]) | 120 ]) |
107 failing_image_pairs.ensure_extra_column_values_in_summary( | 121 failing_image_pairs.ensure_extra_column_values_in_summary( |
108 column_id=results.KEY__EXTRACOLUMNS__RESULT_TYPE, values=[ | 122 column_id=results.KEY__EXTRACOLUMNS__RESULT_TYPE, values=[ |
109 results.KEY__RESULT_TYPE__FAILED, | 123 results.KEY__RESULT_TYPE__FAILED, |
110 results.KEY__RESULT_TYPE__NOCOMPARISON, | 124 results.KEY__RESULT_TYPE__NOCOMPARISON, |
111 ]) | 125 ]) |
112 | 126 |
113 common_dict_paths = sorted(set(subdirA_dicts.keys() + subdirB_dicts.keys())) | 127 # Every actuals_dict should be paired up with a corresponding |
| 128 # expectations_dict. |
| 129 actuals_dict_paths = sorted(actuals_dicts.keys()) |
| 130 expectations_dict_paths = sorted(expectations_dicts.keys()) |
| 131 if actuals_dict_paths != expectations_dict_paths: |
| 132 raise Exception('actuals_dict_paths %s != expectations_dict_paths %s' % ( |
| 133 actuals_dict_paths, expectations_dict_paths)) |
| 134 common_dict_paths = actuals_dict_paths |
| 135 |
114 num_common_dict_paths = len(common_dict_paths) | 136 num_common_dict_paths = len(common_dict_paths) |
115 dict_num = 0 | 137 dict_num = 0 |
116 for dict_path in common_dict_paths: | 138 for dict_path in common_dict_paths: |
117 dict_num += 1 | 139 dict_num += 1 |
118 logging.info('Generating pixel diffs for dict #%d of %d, "%s"...' % | 140 logging.info('Generating pixel diffs for dict #%d of %d, "%s"...' % |
119 (dict_num, num_common_dict_paths, dict_path)) | 141 (dict_num, num_common_dict_paths, dict_path)) |
120 dictA = subdirA_dicts[dict_path] | 142 dictA = actuals_dicts[dict_path] |
121 dictB = subdirB_dicts[dict_path] | 143 dictB = expectations_dicts[dict_path] |
122 self._validate_dict_version(dictA) | 144 self._validate_dict_version(dictA) |
123 self._validate_dict_version(dictB) | 145 self._validate_dict_version(dictB) |
124 dictA_results = dictA[gm_json.JSONKEY_ACTUALRESULTS] | 146 dictA_results = dictA[gm_json.JSONKEY_ACTUALRESULTS] |
125 dictB_results = dictB[gm_json.JSONKEY_ACTUALRESULTS] | 147 dictB_results = dictB[gm_json.JSONKEY_ACTUALRESULTS] |
126 skp_names = sorted(set(dictA_results.keys() + dictB_results.keys())) | 148 skp_names = sorted(set(dictA_results.keys() + dictB_results.keys())) |
127 for skp_name in skp_names: | 149 for skp_name in skp_names: |
128 imagepairs_for_this_skp = [] | 150 imagepairs_for_this_skp = [] |
129 | 151 |
130 whole_image_A = RenderedPicturesComparisons.get_multilevel( | 152 whole_image_A = RenderedPicturesComparisons.get_multilevel( |
131 dictA_results, skp_name, gm_json.JSONKEY_SOURCE_WHOLEIMAGE) | 153 dictA_results, skp_name, gm_json.JSONKEY_SOURCE_WHOLEIMAGE) |
(...skipping 96 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
228 results.KEY__EXTRACOLUMNS__TEST: test, | 250 results.KEY__EXTRACOLUMNS__TEST: test, |
229 # TODO(epoger): Right now, the client UI crashes if it receives | 251 # TODO(epoger): Right now, the client UI crashes if it receives |
230 # results that do not include this column. | 252 # results that do not include this column. |
231 # Until we fix that, keep the client happy. | 253 # Until we fix that, keep the client happy. |
232 results.KEY__EXTRACOLUMNS__BUILDER: 'TODO', | 254 results.KEY__EXTRACOLUMNS__BUILDER: 'TODO', |
233 } | 255 } |
234 | 256 |
235 try: | 257 try: |
236 return imagepair.ImagePair( | 258 return imagepair.ImagePair( |
237 image_diff_db=self._image_diff_db, | 259 image_diff_db=self._image_diff_db, |
238 base_url=self._image_base_url, | 260 base_url=self._image_base_gs_url, |
239 imageA_relative_url=imageA_relative_url, | 261 imageA_relative_url=imageA_relative_url, |
240 imageB_relative_url=imageB_relative_url, | 262 imageB_relative_url=imageB_relative_url, |
241 extra_columns=extra_columns_dict) | 263 extra_columns=extra_columns_dict) |
242 except (KeyError, TypeError): | 264 except (KeyError, TypeError): |
243 logging.exception( | 265 logging.exception( |
244 'got exception while creating ImagePair for' | 266 'got exception while creating ImagePair for' |
245 ' test="%s", config="%s", urlPair=("%s","%s")' % ( | 267 ' test="%s", config="%s", urlPair=("%s","%s")' % ( |
246 test, config, imageA_relative_url, imageB_relative_url)) | 268 test, config, imageA_relative_url, imageB_relative_url)) |
247 return None | 269 return None |
248 | |
249 | |
250 # TODO(epoger): Add main() so this can be called by vm_run_skia_try.sh | |
OLD | NEW |