OLD | NEW |
1 #!/usr/bin/python | 1 #!/usr/bin/python |
2 | 2 |
3 """ | 3 """ |
4 Copyright 2013 Google Inc. | 4 Copyright 2013 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 Repackage expected/actual GM results as needed by our HTML rebaseline viewer. | 9 Repackage expected/actual GM results as needed by our HTML rebaseline viewer. |
10 """ | 10 """ |
(...skipping 14 matching lines...) Expand all Loading... |
25 # that directory. That script allows us to parse the actual-results.json file | 25 # that directory. That script allows us to parse the actual-results.json file |
26 # written out by the GM tool. | 26 # written out by the GM tool. |
27 # Make sure that the 'gm' dir is in the PYTHONPATH, but add it at the *end* | 27 # Make sure that the 'gm' dir is in the PYTHONPATH, but add it at the *end* |
28 # so any dirs that are already in the PYTHONPATH will be preferred. | 28 # so any dirs that are already in the PYTHONPATH will be preferred. |
29 PARENT_DIRECTORY = os.path.dirname(os.path.realpath(__file__)) | 29 PARENT_DIRECTORY = os.path.dirname(os.path.realpath(__file__)) |
30 GM_DIRECTORY = os.path.dirname(PARENT_DIRECTORY) | 30 GM_DIRECTORY = os.path.dirname(PARENT_DIRECTORY) |
31 if GM_DIRECTORY not in sys.path: | 31 if GM_DIRECTORY not in sys.path: |
32 sys.path.append(GM_DIRECTORY) | 32 sys.path.append(GM_DIRECTORY) |
33 import gm_json | 33 import gm_json |
34 import imagediffdb | 34 import imagediffdb |
| 35 import imagepair |
| 36 import imagepairset |
| 37 |
| 38 # Keys used to link an image to a particular GM test. |
| 39 # NOTE: Keep these in sync with static/constants.js |
| 40 KEY__EXPECTATIONS__BUGS = gm_json.JSONKEY_EXPECTEDRESULTS_BUGS |
| 41 KEY__EXPECTATIONS__IGNOREFAILURE = gm_json.JSONKEY_EXPECTEDRESULTS_IGNOREFAILURE |
| 42 KEY__EXPECTATIONS__REVIEWED = gm_json.JSONKEY_EXPECTEDRESULTS_REVIEWED |
| 43 KEY__EXTRACOLUMN__BUILDER = 'builder' |
| 44 KEY__EXTRACOLUMN__CONFIG = 'config' |
| 45 KEY__EXTRACOLUMN__RESULT_TYPE = 'resultType' |
| 46 KEY__EXTRACOLUMN__TEST = 'test' |
| 47 KEY__HEADER__RESULTS_ALL = 'all' |
| 48 KEY__HEADER__RESULTS_FAILURES = 'failures' |
| 49 KEY__NEW_IMAGE_URL = 'newImageUrl' |
| 50 KEY__RESULT_TYPE__FAILED = gm_json.JSONKEY_ACTUALRESULTS_FAILED |
| 51 KEY__RESULT_TYPE__FAILUREIGNORED = gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED |
| 52 KEY__RESULT_TYPE__NOCOMPARISON = gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON |
| 53 KEY__RESULT_TYPE__SUCCEEDED = gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED |
| 54 |
| 55 EXPECTATION_FIELDS_PASSED_THRU_VERBATIM = [ |
| 56 KEY__EXPECTATIONS__BUGS, |
| 57 KEY__EXPECTATIONS__IGNOREFAILURE, |
| 58 KEY__EXPECTATIONS__REVIEWED, |
| 59 ] |
35 | 60 |
36 IMAGE_FILENAME_RE = re.compile(gm_json.IMAGE_FILENAME_PATTERN) | 61 IMAGE_FILENAME_RE = re.compile(gm_json.IMAGE_FILENAME_PATTERN) |
37 IMAGE_FILENAME_FORMATTER = '%s_%s.png' # pass in (testname, config) | 62 IMAGE_FILENAME_FORMATTER = '%s_%s.png' # pass in (testname, config) |
38 | 63 |
39 FIELDS_PASSED_THRU_VERBATIM = [ | 64 IMAGEPAIR_SET_DESCRIPTIONS = ('expected image', 'actual image') |
40 gm_json.JSONKEY_EXPECTEDRESULTS_BUGS, | |
41 gm_json.JSONKEY_EXPECTEDRESULTS_IGNOREFAILURE, | |
42 gm_json.JSONKEY_EXPECTEDRESULTS_REVIEWED, | |
43 ] | |
44 CATEGORIES_TO_SUMMARIZE = [ | |
45 'builder', 'test', 'config', 'resultType', | |
46 gm_json.JSONKEY_EXPECTEDRESULTS_IGNOREFAILURE, | |
47 gm_json.JSONKEY_EXPECTEDRESULTS_REVIEWED, | |
48 ] | |
49 | 65 |
50 RESULTS_ALL = 'all' | |
51 RESULTS_FAILURES = 'failures' | |
52 | 66 |
53 class Results(object): | 67 class Results(object): |
54 """ Loads actual and expected results from all builders, supplying combined | 68 """ Loads actual and expected GM results into an ImagePairSet. |
55 reports as requested. | 69 |
| 70 Loads actual and expected results from all builders, except for those skipped |
| 71 by _ignore_builder(). |
56 | 72 |
57 Once this object has been constructed, the results (in self._results[]) | 73 Once this object has been constructed, the results (in self._results[]) |
58 are immutable. If you want to update the results based on updated JSON | 74 are immutable. If you want to update the results based on updated JSON |
59 file contents, you will need to create a new Results object.""" | 75 file contents, you will need to create a new Results object.""" |
60 | 76 |
61 def __init__(self, actuals_root, expected_root, generated_images_root): | 77 def __init__(self, actuals_root, expected_root, generated_images_root): |
62 """ | 78 """ |
63 Args: | 79 Args: |
64 actuals_root: root directory containing all actual-results.json files | 80 actuals_root: root directory containing all actual-results.json files |
65 expected_root: root directory containing all expected-results.json files | 81 expected_root: root directory containing all expected-results.json files |
(...skipping 21 matching lines...) Expand all Loading... |
87 | 103 |
88 Note that this will NOT update the results stored in self._results[] ; | 104 Note that this will NOT update the results stored in self._results[] ; |
89 in order to see those updates, you must instantiate a new Results object | 105 in order to see those updates, you must instantiate a new Results object |
90 based on the (now updated) files on disk. | 106 based on the (now updated) files on disk. |
91 | 107 |
92 Args: | 108 Args: |
93 modifications: a list of dictionaries, one for each expectation to update: | 109 modifications: a list of dictionaries, one for each expectation to update: |
94 | 110 |
95 [ | 111 [ |
96 { | 112 { |
97 'builder': 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Debug', | 113 imagepair.KEY__EXPECTATIONS_DATA: { |
98 'test': 'bigmatrix', | 114 KEY__EXPECTATIONS__BUGS: [123, 456], |
99 'config': '8888', | 115 KEY__EXPECTATIONS__IGNOREFAILURE: false, |
100 'expectedHashType': 'bitmap-64bitMD5', | 116 KEY__EXPECTATIONS__REVIEWED: true, |
101 'expectedHashDigest': '10894408024079689926', | 117 }, |
102 'bugs': [123, 456], | 118 imagepair.KEY__EXTRA_COLUMN_VALUES: { |
103 'ignore-failure': false, | 119 KEY__EXTRACOLUMN__BUILDER: 'Test-Mac10.6-MacMini4.1-GeForce320M-x
86-Debug', |
104 'reviewed-by-human': true, | 120 KEY__EXTRACOLUMN__CONFIG: '8888', |
| 121 KEY__EXTRACOLUMN__TEST: 'bigmatrix', |
| 122 }, |
| 123 KEY__NEW_IMAGE_URL: 'bitmap-64bitMD5/bigmatrix/10894408024079689926
.png', |
105 }, | 124 }, |
106 ... | 125 ... |
107 ] | 126 ] |
108 | 127 |
109 """ | 128 """ |
110 expected_builder_dicts = Results._read_dicts_from_root(self._expected_root) | 129 expected_builder_dicts = Results._read_dicts_from_root(self._expected_root) |
111 for mod in modifications: | 130 for mod in modifications: |
112 image_name = IMAGE_FILENAME_FORMATTER % (mod['test'], mod['config']) | 131 image_name = IMAGE_FILENAME_FORMATTER % ( |
113 # TODO(epoger): assumes a single allowed digest per test | 132 mod[imagepair.KEY__EXTRA_COLUMN_VALUES][KEY__EXTRACOLUMN__TEST], |
114 allowed_digests = [[mod['expectedHashType'], | 133 mod[imagepair.KEY__EXTRA_COLUMN_VALUES][KEY__EXTRACOLUMN__CONFIG]) |
115 int(mod['expectedHashDigest'])]] | 134 _, hash_type, hash_digest = gm_json.SplitGmRelativeUrl( |
| 135 mod[KEY__NEW_IMAGE_URL]) |
| 136 allowed_digests = [[hash_type, int(hash_digest)]] |
116 new_expectations = { | 137 new_expectations = { |
117 gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS: allowed_digests, | 138 gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS: allowed_digests, |
118 } | 139 } |
119 for field in FIELDS_PASSED_THRU_VERBATIM: | 140 for field in EXPECTATION_FIELDS_PASSED_THRU_VERBATIM: |
120 value = mod.get(field) | 141 value = mod[imagepair.KEY__EXPECTATIONS_DATA].get(field) |
121 if value is not None: | 142 if value is not None: |
122 new_expectations[field] = value | 143 new_expectations[field] = value |
123 builder_dict = expected_builder_dicts[mod['builder']] | 144 builder_dict = expected_builder_dicts[ |
| 145 mod[imagepair.KEY__EXTRA_COLUMN_VALUES][KEY__EXTRACOLUMN__BUILDER]] |
124 builder_expectations = builder_dict.get(gm_json.JSONKEY_EXPECTEDRESULTS) | 146 builder_expectations = builder_dict.get(gm_json.JSONKEY_EXPECTEDRESULTS) |
125 if not builder_expectations: | 147 if not builder_expectations: |
126 builder_expectations = {} | 148 builder_expectations = {} |
127 builder_dict[gm_json.JSONKEY_EXPECTEDRESULTS] = builder_expectations | 149 builder_dict[gm_json.JSONKEY_EXPECTEDRESULTS] = builder_expectations |
128 builder_expectations[image_name] = new_expectations | 150 builder_expectations[image_name] = new_expectations |
129 Results._write_dicts_to_root(expected_builder_dicts, self._expected_root) | 151 Results._write_dicts_to_root(expected_builder_dicts, self._expected_root) |
130 | 152 |
131 def get_results_of_type(self, type): | 153 def get_results_of_type(self, type): |
132 """Return results of some/all tests (depending on 'type' parameter). | 154 """Return results of some/all tests (depending on 'type' parameter). |
133 | 155 |
134 Args: | 156 Args: |
135 type: string describing which types of results to include; must be one | 157 type: string describing which types of results to include; must be one |
136 of the RESULTS_* constants | 158 of the RESULTS_* constants |
137 | 159 |
138 Results are returned as a dictionary in this form: | 160 Results are returned in a dictionary as output by ImagePairSet.as_dict(). |
139 | |
140 { | |
141 'categories': # dictionary of categories listed in | |
142 # CATEGORIES_TO_SUMMARIZE, with the number of times | |
143 # each value appears within its category | |
144 { | |
145 'resultType': # category name | |
146 { | |
147 'failed': 29, # category value and total number found of that value | |
148 'failure-ignored': 948, | |
149 'no-comparison': 4502, | |
150 'succeeded': 38609, | |
151 }, | |
152 'builder': | |
153 { | |
154 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Debug': 1286, | |
155 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Release': 1134, | |
156 ... | |
157 }, | |
158 ... # other categories from CATEGORIES_TO_SUMMARIZE | |
159 }, # end of 'categories' dictionary | |
160 | |
161 'testData': # list of test results, with a dictionary for each | |
162 [ | |
163 { | |
164 'resultType': 'failed', | |
165 'builder': 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Debug', | |
166 'test': 'bigmatrix', | |
167 'config': '8888', | |
168 'expectedHashType': 'bitmap-64bitMD5', | |
169 'expectedHashDigest': '10894408024079689926', | |
170 'actualHashType': 'bitmap-64bitMD5', | |
171 'actualHashDigest': '2409857384569', | |
172 'bugs': [123, 456], | |
173 'ignore-failure': false, | |
174 'reviewed-by-human': true, | |
175 }, | |
176 ... | |
177 ], # end of 'testData' list | |
178 } | |
179 """ | 161 """ |
180 return self._results[type] | 162 return self._results[type] |
181 | 163 |
182 @staticmethod | 164 @staticmethod |
183 def _ignore_builder(builder): | 165 def _ignore_builder(builder): |
184 """Returns True if we should ignore expectations and actuals for a builder. | 166 """Returns True if we should ignore expectations and actuals for a builder. |
185 | 167 |
186 This allows us to ignore builders for which we don't maintain expectations | 168 This allows us to ignore builders for which we don't maintain expectations |
187 (trybots, Valgrind, ASAN, TSAN), and avoid problems like | 169 (trybots, Valgrind, ASAN, TSAN), and avoid problems like |
188 https://code.google.com/p/skia/issues/detail?id=2036 ('rebaseline_server | 170 https://code.google.com/p/skia/issues/detail?id=2036 ('rebaseline_server |
(...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
220 for dirpath, dirnames, filenames in os.walk(root): | 202 for dirpath, dirnames, filenames in os.walk(root): |
221 for matching_filename in fnmatch.filter(filenames, pattern): | 203 for matching_filename in fnmatch.filter(filenames, pattern): |
222 builder = os.path.basename(dirpath) | 204 builder = os.path.basename(dirpath) |
223 if Results._ignore_builder(builder): | 205 if Results._ignore_builder(builder): |
224 continue | 206 continue |
225 fullpath = os.path.join(dirpath, matching_filename) | 207 fullpath = os.path.join(dirpath, matching_filename) |
226 meta_dict[builder] = gm_json.LoadFromFile(fullpath) | 208 meta_dict[builder] = gm_json.LoadFromFile(fullpath) |
227 return meta_dict | 209 return meta_dict |
228 | 210 |
229 @staticmethod | 211 @staticmethod |
| 212 def _create_relative_url(hashtype_and_digest, test_name): |
| 213 """Returns the URL for this image, relative to GM_ACTUALS_ROOT_HTTP_URL. |
| 214 |
| 215 If we don't have a record of this image, returns None. |
| 216 |
| 217 Args: |
| 218 hashtype_and_digest: (hash_type, hash_digest) tuple, or None if we |
| 219 don't have a record of this image |
| 220 test_name: string; name of the GM test that created this image |
| 221 """ |
| 222 if not hashtype_and_digest: |
| 223 return None |
| 224 return gm_json.CreateGmRelativeUrl( |
| 225 test_name=test_name, |
| 226 hash_type=hashtype_and_digest[0], |
| 227 hash_digest=hashtype_and_digest[1]) |
| 228 |
| 229 @staticmethod |
230 def _write_dicts_to_root(meta_dict, root, pattern='*.json'): | 230 def _write_dicts_to_root(meta_dict, root, pattern='*.json'): |
231 """Write all per-builder dictionaries within meta_dict to files under | 231 """Write all per-builder dictionaries within meta_dict to files under |
232 the root path. | 232 the root path. |
233 | 233 |
234 Security note: this will only write to files that already exist within | 234 Security note: this will only write to files that already exist within |
235 the root path (as found by os.walk() within root), so we don't need to | 235 the root path (as found by os.walk() within root), so we don't need to |
236 worry about malformed content writing to disk outside of root. | 236 worry about malformed content writing to disk outside of root. |
237 However, the data written to those files is not double-checked, so it | 237 However, the data written to those files is not double-checked, so it |
238 could contain poisonous data. | 238 could contain poisonous data. |
239 | 239 |
(...skipping 25 matching lines...) Expand all Loading... |
265 # Check: did we write out the set of per-builder dictionaries we | 265 # Check: did we write out the set of per-builder dictionaries we |
266 # expected to? | 266 # expected to? |
267 expected_builders_written = sorted(meta_dict.keys()) | 267 expected_builders_written = sorted(meta_dict.keys()) |
268 actual_builders_written.sort() | 268 actual_builders_written.sort() |
269 if expected_builders_written != actual_builders_written: | 269 if expected_builders_written != actual_builders_written: |
270 raise KeyError( | 270 raise KeyError( |
271 'expected to write dicts for builders %s, but actually wrote them ' | 271 'expected to write dicts for builders %s, but actually wrote them ' |
272 'for builders %s' % ( | 272 'for builders %s' % ( |
273 expected_builders_written, actual_builders_written)) | 273 expected_builders_written, actual_builders_written)) |
274 | 274 |
275 def _generate_pixel_diffs_if_needed(self, test, expected_image, actual_image): | |
276 """If expected_image and actual_image both exist but are different, | |
277 add the image pair to self._image_diff_db and generate pixel diffs. | |
278 | |
279 Args: | |
280 test: string; name of test | |
281 expected_image: (hashType, hashDigest) tuple describing the expected image | |
282 actual_image: (hashType, hashDigest) tuple describing the actual image | |
283 """ | |
284 if expected_image == actual_image: | |
285 return | |
286 | |
287 (expected_hashtype, expected_hashdigest) = expected_image | |
288 (actual_hashtype, actual_hashdigest) = actual_image | |
289 if None in [expected_hashtype, expected_hashdigest, | |
290 actual_hashtype, actual_hashdigest]: | |
291 return | |
292 | |
293 expected_url = gm_json.CreateGmActualUrl( | |
294 test_name=test, hash_type=expected_hashtype, | |
295 hash_digest=expected_hashdigest) | |
296 actual_url = gm_json.CreateGmActualUrl( | |
297 test_name=test, hash_type=actual_hashtype, | |
298 hash_digest=actual_hashdigest) | |
299 self._image_diff_db.add_image_pair( | |
300 expected_image_locator=expected_hashdigest, | |
301 expected_image_url=expected_url, | |
302 actual_image_locator=actual_hashdigest, | |
303 actual_image_url=actual_url) | |
304 | |
305 def _load_actual_and_expected(self): | 275 def _load_actual_and_expected(self): |
306 """Loads the results of all tests, across all builders (based on the | 276 """Loads the results of all tests, across all builders (based on the |
307 files within self._actuals_root and self._expected_root), | 277 files within self._actuals_root and self._expected_root), |
308 and stores them in self._results. | 278 and stores them in self._results. |
309 """ | 279 """ |
310 logging.info('Reading actual-results JSON files from %s...' % | 280 logging.info('Reading actual-results JSON files from %s...' % |
311 self._actuals_root) | 281 self._actuals_root) |
312 actual_builder_dicts = Results._read_dicts_from_root(self._actuals_root) | 282 actual_builder_dicts = Results._read_dicts_from_root(self._actuals_root) |
313 logging.info('Reading expected-results JSON files from %s...' % | 283 logging.info('Reading expected-results JSON files from %s...' % |
314 self._expected_root) | 284 self._expected_root) |
315 expected_builder_dicts = Results._read_dicts_from_root(self._expected_root) | 285 expected_builder_dicts = Results._read_dicts_from_root(self._expected_root) |
316 | 286 |
317 categories_all = {} | 287 all_image_pairs = imagepairset.ImagePairSet(IMAGEPAIR_SET_DESCRIPTIONS) |
318 categories_failures = {} | 288 failing_image_pairs = imagepairset.ImagePairSet(IMAGEPAIR_SET_DESCRIPTIONS) |
319 | 289 |
320 Results._ensure_included_in_category_dict(categories_all, | 290 all_image_pairs.ensure_extra_column_values_in_summary( |
321 'resultType', [ | 291 column_id=KEY__EXTRACOLUMN__RESULT_TYPE, values=[ |
322 gm_json.JSONKEY_ACTUALRESULTS_FAILED, | 292 KEY__RESULT_TYPE__FAILED, |
323 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED, | 293 KEY__RESULT_TYPE__FAILUREIGNORED, |
324 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON, | 294 KEY__RESULT_TYPE__NOCOMPARISON, |
325 gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED, | 295 KEY__RESULT_TYPE__SUCCEEDED, |
326 ]) | 296 ]) |
327 Results._ensure_included_in_category_dict(categories_failures, | 297 failing_image_pairs.ensure_extra_column_values_in_summary( |
328 'resultType', [ | 298 column_id=KEY__EXTRACOLUMN__RESULT_TYPE, values=[ |
329 gm_json.JSONKEY_ACTUALRESULTS_FAILED, | 299 KEY__RESULT_TYPE__FAILED, |
330 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED, | 300 KEY__RESULT_TYPE__FAILUREIGNORED, |
331 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON, | 301 KEY__RESULT_TYPE__NOCOMPARISON, |
332 ]) | 302 ]) |
333 | 303 |
334 data_all = [] | |
335 data_failures = [] | |
336 builders = sorted(actual_builder_dicts.keys()) | 304 builders = sorted(actual_builder_dicts.keys()) |
337 num_builders = len(builders) | 305 num_builders = len(builders) |
338 builder_num = 0 | 306 builder_num = 0 |
339 for builder in builders: | 307 for builder in builders: |
340 builder_num += 1 | 308 builder_num += 1 |
341 logging.info('Generating pixel diffs for builder #%d of %d, "%s"...' % | 309 logging.info('Generating pixel diffs for builder #%d of %d, "%s"...' % |
342 (builder_num, num_builders, builder)) | 310 (builder_num, num_builders, builder)) |
343 actual_results_for_this_builder = ( | 311 actual_results_for_this_builder = ( |
344 actual_builder_dicts[builder][gm_json.JSONKEY_ACTUALRESULTS]) | 312 actual_builder_dicts[builder][gm_json.JSONKEY_ACTUALRESULTS]) |
345 for result_type in sorted(actual_results_for_this_builder.keys()): | 313 for result_type in sorted(actual_results_for_this_builder.keys()): |
346 results_of_this_type = actual_results_for_this_builder[result_type] | 314 results_of_this_type = actual_results_for_this_builder[result_type] |
347 if not results_of_this_type: | 315 if not results_of_this_type: |
348 continue | 316 continue |
349 for image_name in sorted(results_of_this_type.keys()): | 317 for image_name in sorted(results_of_this_type.keys()): |
350 actual_image = results_of_this_type[image_name] | 318 (test, config) = IMAGE_FILENAME_RE.match(image_name).groups() |
| 319 actual_image_relative_url = Results._create_relative_url( |
| 320 hashtype_and_digest=results_of_this_type[image_name], |
| 321 test_name=test) |
351 | 322 |
352 # Default empty expectations; overwrite these if we find any real ones | 323 # Default empty expectations; overwrite these if we find any real ones |
353 expectations_per_test = None | 324 expectations_per_test = None |
354 expected_image = [None, None] | 325 expected_image_relative_url = None |
| 326 expectations_dict = None |
355 try: | 327 try: |
356 expectations_per_test = ( | 328 expectations_per_test = ( |
357 expected_builder_dicts | 329 expected_builder_dicts |
358 [builder][gm_json.JSONKEY_EXPECTEDRESULTS][image_name]) | 330 [builder][gm_json.JSONKEY_EXPECTEDRESULTS][image_name]) |
359 # TODO(epoger): assumes a single allowed digest per test | 331 # TODO(epoger): assumes a single allowed digest per test, which is |
360 expected_image = ( | 332 # fine; see https://code.google.com/p/skia/issues/detail?id=1787 |
| 333 expected_image_hashtype_and_digest = ( |
361 expectations_per_test | 334 expectations_per_test |
362 [gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS][0]) | 335 [gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS][0]) |
| 336 expected_image_relative_url = Results._create_relative_url( |
| 337 hashtype_and_digest=expected_image_hashtype_and_digest, |
| 338 test_name=test) |
| 339 expectations_dict = {} |
| 340 for field in EXPECTATION_FIELDS_PASSED_THRU_VERBATIM: |
| 341 expectations_dict[field] = expectations_per_test.get(field) |
363 except (KeyError, TypeError): | 342 except (KeyError, TypeError): |
364 # There are several cases in which we would expect to find | 343 # There are several cases in which we would expect to find |
365 # no expectations for a given test: | 344 # no expectations for a given test: |
366 # | 345 # |
367 # 1. result_type == NOCOMPARISON | 346 # 1. result_type == NOCOMPARISON |
368 # There are no expectations for this test yet! | 347 # There are no expectations for this test yet! |
369 # | 348 # |
370 # 2. alternate rendering mode failures (e.g. serialized) | 349 # 2. alternate rendering mode failures (e.g. serialized) |
371 # In cases like | 350 # In cases like |
372 # https://code.google.com/p/skia/issues/detail?id=1684 | 351 # https://code.google.com/p/skia/issues/detail?id=1684 |
373 # ('tileimagefilter GM test failing in serialized render mode'), | 352 # ('tileimagefilter GM test failing in serialized render mode'), |
374 # the gm-actuals will list a failure for the alternate | 353 # the gm-actuals will list a failure for the alternate |
375 # rendering mode even though we don't have explicit expectations | 354 # rendering mode even though we don't have explicit expectations |
376 # for the test (the implicit expectation is that it must | 355 # for the test (the implicit expectation is that it must |
377 # render the same in all rendering modes). | 356 # render the same in all rendering modes). |
378 # | 357 # |
379 # Don't log type 1, because it is common. | 358 # Don't log type 1, because it is common. |
380 # Log other types, because they are rare and we should know about | 359 # Log other types, because they are rare and we should know about |
381 # them, but don't throw an exception, because we need to keep our | 360 # them, but don't throw an exception, because we need to keep our |
382 # tools working in the meanwhile! | 361 # tools working in the meanwhile! |
383 if result_type != gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON: | 362 if result_type != KEY__RESULT_TYPE__NOCOMPARISON: |
384 logging.warning('No expectations found for test: %s' % { | 363 logging.warning('No expectations found for test: %s' % { |
385 'builder': builder, | 364 KEY__EXTRACOLUMN__BUILDER: builder, |
| 365 KEY__EXTRACOLUMN__RESULT_TYPE: result_type, |
386 'image_name': image_name, | 366 'image_name': image_name, |
387 'result_type': result_type, | |
388 }) | 367 }) |
389 | 368 |
390 # If this test was recently rebaselined, it will remain in | 369 # If this test was recently rebaselined, it will remain in |
391 # the 'failed' set of actuals until all the bots have | 370 # the 'failed' set of actuals until all the bots have |
392 # cycled (although the expectations have indeed been set | 371 # cycled (although the expectations have indeed been set |
393 # from the most recent actuals). Treat these as successes | 372 # from the most recent actuals). Treat these as successes |
394 # instead of failures. | 373 # instead of failures. |
395 # | 374 # |
396 # TODO(epoger): Do we need to do something similar in | 375 # TODO(epoger): Do we need to do something similar in |
397 # other cases, such as when we have recently marked a test | 376 # other cases, such as when we have recently marked a test |
398 # as ignoreFailure but it still shows up in the 'failed' | 377 # as ignoreFailure but it still shows up in the 'failed' |
399 # category? Maybe we should not rely on the result_type | 378 # category? Maybe we should not rely on the result_type |
400 # categories recorded within the gm_actuals AT ALL, and | 379 # categories recorded within the gm_actuals AT ALL, and |
401 # instead evaluate the result_type ourselves based on what | 380 # instead evaluate the result_type ourselves based on what |
402 # we see in expectations vs actual checksum? | 381 # we see in expectations vs actual checksum? |
403 if expected_image == actual_image: | 382 if expected_image_relative_url == actual_image_relative_url: |
404 updated_result_type = gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED | 383 updated_result_type = KEY__RESULT_TYPE__SUCCEEDED |
405 else: | 384 else: |
406 updated_result_type = result_type | 385 updated_result_type = result_type |
407 | 386 extra_columns_dict = { |
408 (test, config) = IMAGE_FILENAME_RE.match(image_name).groups() | 387 KEY__EXTRACOLUMN__RESULT_TYPE: updated_result_type, |
409 self._generate_pixel_diffs_if_needed( | 388 KEY__EXTRACOLUMN__BUILDER: builder, |
410 test=test, expected_image=expected_image, | 389 KEY__EXTRACOLUMN__TEST: test, |
411 actual_image=actual_image) | 390 KEY__EXTRACOLUMN__CONFIG: config, |
412 results_for_this_test = { | |
413 'resultType': updated_result_type, | |
414 'builder': builder, | |
415 'test': test, | |
416 'config': config, | |
417 'actualHashType': actual_image[0], | |
418 'actualHashDigest': str(actual_image[1]), | |
419 'expectedHashType': expected_image[0], | |
420 'expectedHashDigest': str(expected_image[1]), | |
421 | |
422 # FIELDS_PASSED_THRU_VERBATIM that may be overwritten below... | |
423 gm_json.JSONKEY_EXPECTEDRESULTS_IGNOREFAILURE: False, | |
424 } | 391 } |
425 if expectations_per_test: | 392 image_pair = imagepair.ImagePair( |
426 for field in FIELDS_PASSED_THRU_VERBATIM: | 393 image_diff_db=self._image_diff_db, |
427 results_for_this_test[field] = expectations_per_test.get(field) | 394 base_url=gm_json.GM_ACTUALS_ROOT_HTTP_URL, |
428 | 395 imageA_relative_url=expected_image_relative_url, |
429 if updated_result_type == gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON: | 396 imageB_relative_url=actual_image_relative_url, |
430 pass # no diff record to calculate at all | 397 expectations=expectations_dict, |
431 elif updated_result_type == gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED: | 398 extra_columns=extra_columns_dict) |
432 results_for_this_test['numDifferingPixels'] = 0 | 399 all_image_pairs.add_image_pair(image_pair) |
433 results_for_this_test['percentDifferingPixels'] = 0 | 400 if updated_result_type != KEY__RESULT_TYPE__SUCCEEDED: |
434 results_for_this_test['weightedDiffMeasure'] = 0 | 401 failing_image_pairs.add_image_pair(image_pair) |
435 results_for_this_test['perceptualDifference'] = 0 | |
436 results_for_this_test['maxDiffPerChannel'] = 0 | |
437 else: | |
438 try: | |
439 diff_record = self._image_diff_db.get_diff_record( | |
440 expected_image_locator=expected_image[1], | |
441 actual_image_locator=actual_image[1]) | |
442 results_for_this_test['numDifferingPixels'] = ( | |
443 diff_record.get_num_pixels_differing()) | |
444 results_for_this_test['percentDifferingPixels'] = ( | |
445 diff_record.get_percent_pixels_differing()) | |
446 results_for_this_test['weightedDiffMeasure'] = ( | |
447 diff_record.get_weighted_diff_measure()) | |
448 results_for_this_test['perceptualDifference'] = ( | |
449 diff_record.get_perceptual_difference()) | |
450 results_for_this_test['maxDiffPerChannel'] = ( | |
451 diff_record.get_max_diff_per_channel()) | |
452 except KeyError: | |
453 logging.warning('unable to find diff_record for ("%s", "%s")' % | |
454 (expected_image[1], actual_image[1])) | |
455 pass | |
456 | |
457 Results._add_to_category_dict(categories_all, results_for_this_test) | |
458 data_all.append(results_for_this_test) | |
459 | |
460 # TODO(epoger): In effect, we have a list of resultTypes that we | |
461 # include in the different result lists (data_all and data_failures). | |
462 # This same list should be used by the calls to | |
463 # Results._ensure_included_in_category_dict() earlier on. | |
464 if updated_result_type != gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED: | |
465 Results._add_to_category_dict(categories_failures, | |
466 results_for_this_test) | |
467 data_failures.append(results_for_this_test) | |
468 | 402 |
469 self._results = { | 403 self._results = { |
470 RESULTS_ALL: | 404 KEY__HEADER__RESULTS_ALL: all_image_pairs.as_dict(), |
471 {'categories': categories_all, 'testData': data_all}, | 405 KEY__HEADER__RESULTS_FAILURES: failing_image_pairs.as_dict(), |
472 RESULTS_FAILURES: | |
473 {'categories': categories_failures, 'testData': data_failures}, | |
474 } | 406 } |
475 | 407 |
476 @staticmethod | |
477 def _add_to_category_dict(category_dict, test_results): | |
478 """Add test_results to the category dictionary we are building. | |
479 (See documentation of self.get_results_of_type() for the format of this | |
480 dictionary.) | |
481 | |
482 Args: | |
483 category_dict: category dict-of-dicts to add to; modify this in-place | |
484 test_results: test data with which to update category_list, in a dict: | |
485 { | |
486 'category_name': 'category_value', | |
487 'category_name': 'category_value', | |
488 ... | |
489 } | |
490 """ | |
491 for category in CATEGORIES_TO_SUMMARIZE: | |
492 category_value = test_results.get(category) | |
493 if not category_dict.get(category): | |
494 category_dict[category] = {} | |
495 if not category_dict[category].get(category_value): | |
496 category_dict[category][category_value] = 0 | |
497 category_dict[category][category_value] += 1 | |
498 | |
499 @staticmethod | |
500 def _ensure_included_in_category_dict(category_dict, | |
501 category_name, category_values): | |
502 """Ensure that the category name/value pairs are included in category_dict, | |
503 even if there aren't any results with that name/value pair. | |
504 (See documentation of self.get_results_of_type() for the format of this | |
505 dictionary.) | |
506 | |
507 Args: | |
508 category_dict: category dict-of-dicts to modify | |
509 category_name: category name, as a string | |
510 category_values: list of values we want to make sure are represented | |
511 for this category | |
512 """ | |
513 if not category_dict.get(category_name): | |
514 category_dict[category_name] = {} | |
515 for category_value in category_values: | |
516 if not category_dict[category_name].get(category_value): | |
517 category_dict[category_name][category_value] = 0 | |
518 | |
519 | 408 |
520 def main(): | 409 def main(): |
521 logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s', | 410 logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s', |
522 datefmt='%m/%d/%Y %H:%M:%S', | 411 datefmt='%m/%d/%Y %H:%M:%S', |
523 level=logging.INFO) | 412 level=logging.INFO) |
524 parser = argparse.ArgumentParser() | 413 parser = argparse.ArgumentParser() |
525 parser.add_argument( | 414 parser.add_argument( |
526 '--actuals', required=True, | 415 '--actuals', required=True, |
527 help='Directory containing all actual-result JSON files') | 416 help='Directory containing all actual-result JSON files') |
528 parser.add_argument( | 417 parser.add_argument( |
529 '--expectations', required=True, | 418 '--expectations', required=True, |
530 help='Directory containing all expected-result JSON files') | 419 help='Directory containing all expected-result JSON files') |
531 parser.add_argument( | 420 parser.add_argument( |
532 '--outfile', required=True, | 421 '--outfile', required=True, |
533 help='File to write result summary into, in JSON format') | 422 help='File to write result summary into, in JSON format') |
534 parser.add_argument( | 423 parser.add_argument( |
535 '--workdir', default='.workdir', | 424 '--workdir', default='.workdir', |
536 help='Directory within which to download images and generate diffs') | 425 help='Directory within which to download images and generate diffs') |
537 args = parser.parse_args() | 426 args = parser.parse_args() |
538 results = Results(actuals_root=args.actuals, | 427 results = Results(actuals_root=args.actuals, |
539 expected_root=args.expectations, | 428 expected_root=args.expectations, |
540 generated_images_root=args.workdir) | 429 generated_images_root=args.workdir) |
541 gm_json.WriteToFile(results.get_results_of_type(RESULTS_ALL), args.outfile) | 430 gm_json.WriteToFile(results.get_results_of_type(KEY__HEADER__RESULTS_ALL), |
| 431 args.outfile) |
542 | 432 |
543 | 433 |
544 if __name__ == '__main__': | 434 if __name__ == '__main__': |
545 main() | 435 main() |
OLD | NEW |