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 15 matching lines...) Expand all Loading... |
26 # Make sure that the 'gm' dir is in the PYTHONPATH, but add it at the *end* | 26 # Make sure that the 'gm' dir is in the PYTHONPATH, but add it at the *end* |
27 # so any dirs that are already in the PYTHONPATH will be preferred. | 27 # so any dirs that are already in the PYTHONPATH will be preferred. |
28 GM_DIRECTORY = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) | 28 GM_DIRECTORY = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) |
29 if GM_DIRECTORY not in sys.path: | 29 if GM_DIRECTORY not in sys.path: |
30 sys.path.append(GM_DIRECTORY) | 30 sys.path.append(GM_DIRECTORY) |
31 import gm_json | 31 import gm_json |
32 | 32 |
33 IMAGE_FILENAME_RE = re.compile(gm_json.IMAGE_FILENAME_PATTERN) | 33 IMAGE_FILENAME_RE = re.compile(gm_json.IMAGE_FILENAME_PATTERN) |
34 IMAGE_FILENAME_FORMATTER = '%s_%s.png' # pass in (testname, config) | 34 IMAGE_FILENAME_FORMATTER = '%s_%s.png' # pass in (testname, config) |
35 | 35 |
| 36 FIELDS_PASSED_THRU_VERBATIM = [ |
| 37 gm_json.JSONKEY_EXPECTEDRESULTS_BUGS, |
| 38 gm_json.JSONKEY_EXPECTEDRESULTS_IGNOREFAILURE, |
| 39 gm_json.JSONKEY_EXPECTEDRESULTS_REVIEWED, |
| 40 ] |
36 CATEGORIES_TO_SUMMARIZE = [ | 41 CATEGORIES_TO_SUMMARIZE = [ |
37 'builder', 'test', 'config', 'resultType', | 42 'builder', 'test', 'config', 'resultType', |
| 43 gm_json.JSONKEY_EXPECTEDRESULTS_IGNOREFAILURE, |
| 44 gm_json.JSONKEY_EXPECTEDRESULTS_REVIEWED, |
38 ] | 45 ] |
| 46 |
39 RESULTS_ALL = 'all' | 47 RESULTS_ALL = 'all' |
40 RESULTS_FAILURES = 'failures' | 48 RESULTS_FAILURES = 'failures' |
41 | 49 |
42 class Results(object): | 50 class Results(object): |
43 """ Loads actual and expected results from all builders, supplying combined | 51 """ Loads actual and expected results from all builders, supplying combined |
44 reports as requested. | 52 reports as requested. |
45 | 53 |
46 Once this object has been constructed, the results (in self._results[]) | 54 Once this object has been constructed, the results (in self._results[]) |
47 are immutable. If you want to update the results based on updated JSON | 55 are immutable. If you want to update the results based on updated JSON |
48 file contents, you will need to create a new Results object.""" | 56 file contents, you will need to create a new Results object.""" |
(...skipping 26 matching lines...) Expand all Loading... |
75 Args: | 83 Args: |
76 modifications: a list of dictionaries, one for each expectation to update: | 84 modifications: a list of dictionaries, one for each expectation to update: |
77 | 85 |
78 [ | 86 [ |
79 { | 87 { |
80 'builder': 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Debug', | 88 'builder': 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Debug', |
81 'test': 'bigmatrix', | 89 'test': 'bigmatrix', |
82 'config': '8888', | 90 'config': '8888', |
83 'expectedHashType': 'bitmap-64bitMD5', | 91 'expectedHashType': 'bitmap-64bitMD5', |
84 'expectedHashDigest': '10894408024079689926', | 92 'expectedHashDigest': '10894408024079689926', |
| 93 'bugs': [123, 456], |
| 94 'ignore-failure': false, |
| 95 'reviewed-by-human': true, |
85 }, | 96 }, |
86 ... | 97 ... |
87 ] | 98 ] |
88 | 99 |
89 TODO(epoger): For now, this does not allow the caller to set any fields | |
90 other than expectedHashType/expectedHashDigest, and assumes that | |
91 ignore-failure should be set to False. We need to add support | |
92 for other fields (notes, bugs, etc.) and ignore-failure=True. | |
93 """ | 100 """ |
94 expected_builder_dicts = Results._read_dicts_from_root(self._expected_root) | 101 expected_builder_dicts = Results._read_dicts_from_root(self._expected_root) |
95 for mod in modifications: | 102 for mod in modifications: |
96 image_name = IMAGE_FILENAME_FORMATTER % (mod['test'], mod['config']) | 103 image_name = IMAGE_FILENAME_FORMATTER % (mod['test'], mod['config']) |
97 # TODO(epoger): assumes a single allowed digest per test | 104 # TODO(epoger): assumes a single allowed digest per test |
98 allowed_digests = [[mod['expectedHashType'], | 105 allowed_digests = [[mod['expectedHashType'], |
99 int(mod['expectedHashDigest'])]] | 106 int(mod['expectedHashDigest'])]] |
100 new_expectations = { | 107 new_expectations = { |
101 gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS: allowed_digests, | 108 gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS: allowed_digests, |
102 gm_json.JSONKEY_EXPECTEDRESULTS_IGNOREFAILURE: False, | |
103 } | 109 } |
| 110 for field in FIELDS_PASSED_THRU_VERBATIM: |
| 111 value = mod.get(field) |
| 112 if value is not None: |
| 113 new_expectations[field] = value |
104 builder_dict = expected_builder_dicts[mod['builder']] | 114 builder_dict = expected_builder_dicts[mod['builder']] |
105 builder_expectations = builder_dict.get(gm_json.JSONKEY_EXPECTEDRESULTS) | 115 builder_expectations = builder_dict.get(gm_json.JSONKEY_EXPECTEDRESULTS) |
106 if not builder_expectations: | 116 if not builder_expectations: |
107 builder_expectations = {} | 117 builder_expectations = {} |
108 builder_dict[gm_json.JSONKEY_EXPECTEDRESULTS] = builder_expectations | 118 builder_dict[gm_json.JSONKEY_EXPECTEDRESULTS] = builder_expectations |
109 builder_expectations[image_name] = new_expectations | 119 builder_expectations[image_name] = new_expectations |
110 Results._write_dicts_to_root(expected_builder_dicts, self._expected_root) | 120 Results._write_dicts_to_root(expected_builder_dicts, self._expected_root) |
111 | 121 |
112 def get_results_of_type(self, type): | 122 def get_results_of_type(self, type): |
113 """Return results of some/all tests (depending on 'type' parameter). | 123 """Return results of some/all tests (depending on 'type' parameter). |
(...skipping 21 matching lines...) Expand all Loading... |
135 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Debug': 1286, | 145 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Debug': 1286, |
136 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Release': 1134, | 146 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Release': 1134, |
137 ... | 147 ... |
138 }, | 148 }, |
139 ... # other categories from CATEGORIES_TO_SUMMARIZE | 149 ... # other categories from CATEGORIES_TO_SUMMARIZE |
140 }, # end of 'categories' dictionary | 150 }, # end of 'categories' dictionary |
141 | 151 |
142 'testData': # list of test results, with a dictionary for each | 152 'testData': # list of test results, with a dictionary for each |
143 [ | 153 [ |
144 { | 154 { |
| 155 'resultType': 'failed', |
145 'builder': 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Debug', | 156 'builder': 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Debug', |
146 'test': 'bigmatrix', | 157 'test': 'bigmatrix', |
147 'config': '8888', | 158 'config': '8888', |
148 'resultType': 'failed', | |
149 'expectedHashType': 'bitmap-64bitMD5', | 159 'expectedHashType': 'bitmap-64bitMD5', |
150 'expectedHashDigest': '10894408024079689926', | 160 'expectedHashDigest': '10894408024079689926', |
151 'actualHashType': 'bitmap-64bitMD5', | 161 'actualHashType': 'bitmap-64bitMD5', |
152 'actualHashDigest': '2409857384569', | 162 'actualHashDigest': '2409857384569', |
| 163 'bugs': [123, 456], |
| 164 'ignore-failure': false, |
| 165 'reviewed-by-human': true, |
153 }, | 166 }, |
154 ... | 167 ... |
155 ], # end of 'testData' list | 168 ], # end of 'testData' list |
156 } | 169 } |
157 """ | 170 """ |
158 return self._results[type] | 171 return self._results[type] |
159 | 172 |
160 @staticmethod | 173 @staticmethod |
161 def _read_dicts_from_root(root, pattern='*.json'): | 174 def _read_dicts_from_root(root, pattern='*.json'): |
162 """Read all JSON dictionaries within a directory tree. | 175 """Read all JSON dictionaries within a directory tree. |
(...skipping 76 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
239 def _load_actual_and_expected(self): | 252 def _load_actual_and_expected(self): |
240 """Loads the results of all tests, across all builders (based on the | 253 """Loads the results of all tests, across all builders (based on the |
241 files within self._actuals_root and self._expected_root), | 254 files within self._actuals_root and self._expected_root), |
242 and stores them in self._results. | 255 and stores them in self._results. |
243 """ | 256 """ |
244 actual_builder_dicts = Results._read_dicts_from_root(self._actuals_root) | 257 actual_builder_dicts = Results._read_dicts_from_root(self._actuals_root) |
245 expected_builder_dicts = Results._read_dicts_from_root(self._expected_root) | 258 expected_builder_dicts = Results._read_dicts_from_root(self._expected_root) |
246 | 259 |
247 categories_all = {} | 260 categories_all = {} |
248 categories_failures = {} | 261 categories_failures = {} |
| 262 |
249 Results._ensure_included_in_category_dict(categories_all, | 263 Results._ensure_included_in_category_dict(categories_all, |
250 'resultType', [ | 264 'resultType', [ |
251 gm_json.JSONKEY_ACTUALRESULTS_FAILED, | 265 gm_json.JSONKEY_ACTUALRESULTS_FAILED, |
252 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED, | 266 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED, |
253 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON, | 267 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON, |
254 gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED, | 268 gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED, |
255 ]) | 269 ]) |
256 Results._ensure_included_in_category_dict(categories_failures, | 270 Results._ensure_included_in_category_dict(categories_failures, |
257 'resultType', [ | 271 'resultType', [ |
258 gm_json.JSONKEY_ACTUALRESULTS_FAILED, | 272 gm_json.JSONKEY_ACTUALRESULTS_FAILED, |
259 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED, | 273 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED, |
260 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON, | 274 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON, |
261 ]) | 275 ]) |
262 | 276 |
263 data_all = [] | 277 data_all = [] |
264 data_failures = [] | 278 data_failures = [] |
265 for builder in sorted(actual_builder_dicts.keys()): | 279 for builder in sorted(actual_builder_dicts.keys()): |
266 actual_results_for_this_builder = ( | 280 actual_results_for_this_builder = ( |
267 actual_builder_dicts[builder][gm_json.JSONKEY_ACTUALRESULTS]) | 281 actual_builder_dicts[builder][gm_json.JSONKEY_ACTUALRESULTS]) |
268 for result_type in sorted(actual_results_for_this_builder.keys()): | 282 for result_type in sorted(actual_results_for_this_builder.keys()): |
269 results_of_this_type = actual_results_for_this_builder[result_type] | 283 results_of_this_type = actual_results_for_this_builder[result_type] |
270 if not results_of_this_type: | 284 if not results_of_this_type: |
271 continue | 285 continue |
272 for image_name in sorted(results_of_this_type.keys()): | 286 for image_name in sorted(results_of_this_type.keys()): |
273 actual_image = results_of_this_type[image_name] | 287 actual_image = results_of_this_type[image_name] |
| 288 |
| 289 # Default empty expectations; overwrite these if we find any real ones |
| 290 expectations_per_test = None |
| 291 expected_image = [None, None] |
274 try: | 292 try: |
| 293 expectations_per_test = ( |
| 294 expected_builder_dicts |
| 295 [builder][gm_json.JSONKEY_EXPECTEDRESULTS][image_name]) |
275 # TODO(epoger): assumes a single allowed digest per test | 296 # TODO(epoger): assumes a single allowed digest per test |
276 expected_image = ( | 297 expected_image = ( |
277 expected_builder_dicts | 298 expectations_per_test |
278 [builder][gm_json.JSONKEY_EXPECTEDRESULTS] | 299 [gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS][0]) |
279 [image_name][gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS] | |
280 [0]) | |
281 except (KeyError, TypeError): | 300 except (KeyError, TypeError): |
282 # There are several cases in which we would expect to find | 301 # There are several cases in which we would expect to find |
283 # no expectations for a given test: | 302 # no expectations for a given test: |
284 # | 303 # |
285 # 1. result_type == NOCOMPARISON | 304 # 1. result_type == NOCOMPARISON |
286 # There are no expectations for this test yet! | 305 # There are no expectations for this test yet! |
287 # | 306 # |
288 # 2. ignore-tests.txt | 307 # 2. alternate rendering mode failures (e.g. serialized) |
289 # If a test has been listed in ignore-tests.txt, then its status | |
290 # may show as FAILUREIGNORED even if it doesn't have any | |
291 # expectations yet. | |
292 # | |
293 # 3. alternate rendering mode failures (e.g. serialized) | |
294 # In cases like | 308 # In cases like |
295 # https://code.google.com/p/skia/issues/detail?id=1684 | 309 # https://code.google.com/p/skia/issues/detail?id=1684 |
296 # ('tileimagefilter GM test failing in serialized render mode'), | 310 # ('tileimagefilter GM test failing in serialized render mode'), |
297 # the gm-actuals will list a failure for the alternate | 311 # the gm-actuals will list a failure for the alternate |
298 # rendering mode even though we don't have explicit expectations | 312 # rendering mode even though we don't have explicit expectations |
299 # for the test (the implicit expectation is that it must | 313 # for the test (the implicit expectation is that it must |
300 # render the same in all rendering modes). | 314 # render the same in all rendering modes). |
301 # | 315 # |
302 # Don't log types 1 or 2, because they are common. | 316 # Don't log type 1, because it is common. |
303 # Log other types, because they are rare and we should know about | 317 # Log other types, because they are rare and we should know about |
304 # them, but don't throw an exception, because we need to keep our | 318 # them, but don't throw an exception, because we need to keep our |
305 # tools working in the meanwhile! | 319 # tools working in the meanwhile! |
306 if result_type not in [ | 320 if result_type != gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON: |
307 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON, | |
308 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED] : | |
309 logging.warning('No expectations found for test: %s' % { | 321 logging.warning('No expectations found for test: %s' % { |
310 'builder': builder, | 322 'builder': builder, |
311 'image_name': image_name, | 323 'image_name': image_name, |
312 'result_type': result_type, | 324 'result_type': result_type, |
313 }) | 325 }) |
314 expected_image = [None, None] | |
315 | 326 |
316 # If this test was recently rebaselined, it will remain in | 327 # If this test was recently rebaselined, it will remain in |
317 # the 'failed' set of actuals until all the bots have | 328 # the 'failed' set of actuals until all the bots have |
318 # cycled (although the expectations have indeed been set | 329 # cycled (although the expectations have indeed been set |
319 # from the most recent actuals). Treat these as successes | 330 # from the most recent actuals). Treat these as successes |
320 # instead of failures. | 331 # instead of failures. |
321 # | 332 # |
322 # TODO(epoger): Do we need to do something similar in | 333 # TODO(epoger): Do we need to do something similar in |
323 # other cases, such as when we have recently marked a test | 334 # other cases, such as when we have recently marked a test |
324 # as ignoreFailure but it still shows up in the 'failed' | 335 # as ignoreFailure but it still shows up in the 'failed' |
325 # category? Maybe we should not rely on the result_type | 336 # category? Maybe we should not rely on the result_type |
326 # categories recorded within the gm_actuals AT ALL, and | 337 # categories recorded within the gm_actuals AT ALL, and |
327 # instead evaluate the result_type ourselves based on what | 338 # instead evaluate the result_type ourselves based on what |
328 # we see in expectations vs actual checksum? | 339 # we see in expectations vs actual checksum? |
329 if expected_image == actual_image: | 340 if expected_image == actual_image: |
330 updated_result_type = gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED | 341 updated_result_type = gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED |
331 else: | 342 else: |
332 updated_result_type = result_type | 343 updated_result_type = result_type |
333 | 344 |
334 (test, config) = IMAGE_FILENAME_RE.match(image_name).groups() | 345 (test, config) = IMAGE_FILENAME_RE.match(image_name).groups() |
335 results_for_this_test = { | 346 results_for_this_test = { |
| 347 'resultType': updated_result_type, |
336 'builder': builder, | 348 'builder': builder, |
337 'test': test, | 349 'test': test, |
338 'config': config, | 350 'config': config, |
339 'resultType': updated_result_type, | |
340 'actualHashType': actual_image[0], | 351 'actualHashType': actual_image[0], |
341 'actualHashDigest': str(actual_image[1]), | 352 'actualHashDigest': str(actual_image[1]), |
342 'expectedHashType': expected_image[0], | 353 'expectedHashType': expected_image[0], |
343 'expectedHashDigest': str(expected_image[1]), | 354 'expectedHashDigest': str(expected_image[1]), |
| 355 |
| 356 # FIELDS_PASSED_THRU_VERBATIM that may be overwritten below... |
| 357 gm_json.JSONKEY_EXPECTEDRESULTS_IGNOREFAILURE: False, |
344 } | 358 } |
| 359 if expectations_per_test: |
| 360 for field in FIELDS_PASSED_THRU_VERBATIM: |
| 361 results_for_this_test[field] = expectations_per_test.get(field) |
345 Results._add_to_category_dict(categories_all, results_for_this_test) | 362 Results._add_to_category_dict(categories_all, results_for_this_test) |
346 data_all.append(results_for_this_test) | 363 data_all.append(results_for_this_test) |
| 364 |
| 365 # TODO(epoger): In effect, we have a list of resultTypes that we |
| 366 # include in the different result lists (data_all and data_failures). |
| 367 # This same list should be used by the calls to |
| 368 # Results._ensure_included_in_category_dict() earlier on. |
347 if updated_result_type != gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED: | 369 if updated_result_type != gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED: |
348 Results._add_to_category_dict(categories_failures, | 370 Results._add_to_category_dict(categories_failures, |
349 results_for_this_test) | 371 results_for_this_test) |
350 data_failures.append(results_for_this_test) | 372 data_failures.append(results_for_this_test) |
351 | 373 |
352 self._results = { | 374 self._results = { |
353 RESULTS_ALL: | 375 RESULTS_ALL: |
354 {'categories': categories_all, 'testData': data_all}, | 376 {'categories': categories_all, 'testData': data_all}, |
355 RESULTS_FAILURES: | 377 RESULTS_FAILURES: |
356 {'categories': categories_failures, 'testData': data_failures}, | 378 {'categories': categories_failures, 'testData': data_failures}, |
357 } | 379 } |
358 | 380 |
359 @staticmethod | 381 @staticmethod |
360 def _add_to_category_dict(category_dict, test_results): | 382 def _add_to_category_dict(category_dict, test_results): |
361 """Add test_results to the category dictionary we are building. | 383 """Add test_results to the category dictionary we are building. |
362 (See documentation of self.get_results_of_type() for the format of this | 384 (See documentation of self.get_results_of_type() for the format of this |
363 dictionary.) | 385 dictionary.) |
364 | 386 |
365 Args: | 387 Args: |
366 category_dict: category dict-of-dicts to add to; modify this in-place | 388 category_dict: category dict-of-dicts to add to; modify this in-place |
367 test_results: test data with which to update category_list, in a dict: | 389 test_results: test data with which to update category_list, in a dict: |
368 { | 390 { |
369 'category_name': 'category_value', | 391 'category_name': 'category_value', |
370 'category_name': 'category_value', | 392 'category_name': 'category_value', |
371 ... | 393 ... |
372 } | 394 } |
373 """ | 395 """ |
374 for category in CATEGORIES_TO_SUMMARIZE: | 396 for category in CATEGORIES_TO_SUMMARIZE: |
375 category_value = test_results.get(category) | 397 category_value = test_results.get(category) |
376 if not category_value: | |
377 continue # test_results did not include this category, keep going | |
378 if not category_dict.get(category): | 398 if not category_dict.get(category): |
379 category_dict[category] = {} | 399 category_dict[category] = {} |
380 if not category_dict[category].get(category_value): | 400 if not category_dict[category].get(category_value): |
381 category_dict[category][category_value] = 0 | 401 category_dict[category][category_value] = 0 |
382 category_dict[category][category_value] += 1 | 402 category_dict[category][category_value] += 1 |
383 | 403 |
384 @staticmethod | 404 @staticmethod |
385 def _ensure_included_in_category_dict(category_dict, | 405 def _ensure_included_in_category_dict(category_dict, |
386 category_name, category_values): | 406 category_name, category_values): |
387 """Ensure that the category name/value pairs are included in category_dict, | 407 """Ensure that the category name/value pairs are included in category_dict, |
388 even if there aren't any results with that name/value pair. | 408 even if there aren't any results with that name/value pair. |
389 (See documentation of self.get_results_of_type() for the format of this | 409 (See documentation of self.get_results_of_type() for the format of this |
390 dictionary.) | 410 dictionary.) |
391 | 411 |
392 Args: | 412 Args: |
393 category_dict: category dict-of-dicts to modify | 413 category_dict: category dict-of-dicts to modify |
394 category_name: category name, as a string | 414 category_name: category name, as a string |
395 category_values: list of values we want to make sure are represented | 415 category_values: list of values we want to make sure are represented |
396 for this category | 416 for this category |
397 """ | 417 """ |
398 if not category_dict.get(category_name): | 418 if not category_dict.get(category_name): |
399 category_dict[category_name] = {} | 419 category_dict[category_name] = {} |
400 for category_value in category_values: | 420 for category_value in category_values: |
401 if not category_dict[category_name].get(category_value): | 421 if not category_dict[category_name].get(category_value): |
402 category_dict[category_name][category_value] = 0 | 422 category_dict[category_name][category_value] = 0 |
OLD | NEW |