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 11 matching lines...) Expand all Loading... |
22 # | 22 # |
23 # We need to add the 'gm' directory, so that we can import gm_json.py within | 23 # We need to add the 'gm' directory, so that we can import gm_json.py within |
24 # that directory. That script allows us to parse the actual-results.json file | 24 # that directory. That script allows us to parse the actual-results.json file |
25 # written out by the GM tool. | 25 # written out by the GM tool. |
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 import imagediffdb |
32 | 33 |
33 IMAGE_FILENAME_RE = re.compile(gm_json.IMAGE_FILENAME_PATTERN) | 34 IMAGE_FILENAME_RE = re.compile(gm_json.IMAGE_FILENAME_PATTERN) |
34 IMAGE_FILENAME_FORMATTER = '%s_%s.png' # pass in (testname, config) | 35 IMAGE_FILENAME_FORMATTER = '%s_%s.png' # pass in (testname, config) |
35 | 36 |
36 FIELDS_PASSED_THRU_VERBATIM = [ | 37 FIELDS_PASSED_THRU_VERBATIM = [ |
37 gm_json.JSONKEY_EXPECTEDRESULTS_BUGS, | 38 gm_json.JSONKEY_EXPECTEDRESULTS_BUGS, |
38 gm_json.JSONKEY_EXPECTEDRESULTS_IGNOREFAILURE, | 39 gm_json.JSONKEY_EXPECTEDRESULTS_IGNOREFAILURE, |
39 gm_json.JSONKEY_EXPECTEDRESULTS_REVIEWED, | 40 gm_json.JSONKEY_EXPECTEDRESULTS_REVIEWED, |
40 ] | 41 ] |
41 CATEGORIES_TO_SUMMARIZE = [ | 42 CATEGORIES_TO_SUMMARIZE = [ |
42 'builder', 'test', 'config', 'resultType', | 43 'builder', 'test', 'config', 'resultType', |
43 gm_json.JSONKEY_EXPECTEDRESULTS_IGNOREFAILURE, | 44 gm_json.JSONKEY_EXPECTEDRESULTS_IGNOREFAILURE, |
44 gm_json.JSONKEY_EXPECTEDRESULTS_REVIEWED, | 45 gm_json.JSONKEY_EXPECTEDRESULTS_REVIEWED, |
45 ] | 46 ] |
46 | 47 |
47 RESULTS_ALL = 'all' | 48 RESULTS_ALL = 'all' |
48 RESULTS_FAILURES = 'failures' | 49 RESULTS_FAILURES = 'failures' |
49 | 50 |
50 class Results(object): | 51 class Results(object): |
51 """ Loads actual and expected results from all builders, supplying combined | 52 """ Loads actual and expected results from all builders, supplying combined |
52 reports as requested. | 53 reports as requested. |
53 | 54 |
54 Once this object has been constructed, the results (in self._results[]) | 55 Once this object has been constructed, the results (in self._results[]) |
55 are immutable. If you want to update the results based on updated JSON | 56 are immutable. If you want to update the results based on updated JSON |
56 file contents, you will need to create a new Results object.""" | 57 file contents, you will need to create a new Results object.""" |
57 | 58 |
58 def __init__(self, actuals_root, expected_root): | 59 def __init__(self, actuals_root, expected_root, generated_images_root): |
59 """ | 60 """ |
60 Args: | 61 Args: |
61 actuals_root: root directory containing all actual-results.json files | 62 actuals_root: root directory containing all actual-results.json files |
62 expected_root: root directory containing all expected-results.json files | 63 expected_root: root directory containing all expected-results.json files |
| 64 generated_images_root: directory within which to create all pixels diffs; |
| 65 if this directory does not yet exist, it will be created |
63 """ | 66 """ |
| 67 self._image_diff_db = imagediffdb.ImageDiffDB(generated_images_root) |
64 self._actuals_root = actuals_root | 68 self._actuals_root = actuals_root |
65 self._expected_root = expected_root | 69 self._expected_root = expected_root |
66 self._load_actual_and_expected() | 70 self._load_actual_and_expected() |
67 self._timestamp = int(time.time()) | 71 self._timestamp = int(time.time()) |
68 | 72 |
69 def get_timestamp(self): | 73 def get_timestamp(self): |
70 """Return the time at which this object was created, in seconds past epoch | 74 """Return the time at which this object was created, in seconds past epoch |
71 (UTC). | 75 (UTC). |
72 """ | 76 """ |
73 return self._timestamp | 77 return self._timestamp |
(...skipping 168 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
242 # Check: did we write out the set of per-builder dictionaries we | 246 # Check: did we write out the set of per-builder dictionaries we |
243 # expected to? | 247 # expected to? |
244 expected_builders_written = sorted(meta_dict.keys()) | 248 expected_builders_written = sorted(meta_dict.keys()) |
245 actual_builders_written.sort() | 249 actual_builders_written.sort() |
246 if expected_builders_written != actual_builders_written: | 250 if expected_builders_written != actual_builders_written: |
247 raise KeyError( | 251 raise KeyError( |
248 'expected to write dicts for builders %s, but actually wrote them ' | 252 'expected to write dicts for builders %s, but actually wrote them ' |
249 'for builders %s' % ( | 253 'for builders %s' % ( |
250 expected_builders_written, actual_builders_written)) | 254 expected_builders_written, actual_builders_written)) |
251 | 255 |
| 256 def _generate_pixel_diffs_if_needed(self, test, expected_image, actual_image): |
| 257 """If expected_image and actual_image both exist but are different, |
| 258 add the image pair to self._image_diff_db and generate pixel diffs. |
| 259 |
| 260 Args: |
| 261 test: string; name of test |
| 262 expected_image: (hashType, hashDigest) tuple describing the expected image |
| 263 actual_image: (hashType, hashDigest) tuple describing the actual image |
| 264 """ |
| 265 if expected_image == actual_image: |
| 266 return |
| 267 |
| 268 (expected_hashtype, expected_hashdigest) = expected_image |
| 269 (actual_hashtype, actual_hashdigest) = actual_image |
| 270 if None in [expected_hashtype, expected_hashdigest, |
| 271 actual_hashtype, actual_hashdigest]: |
| 272 return |
| 273 |
| 274 expected_url = gm_json.CreateGmActualUrl( |
| 275 test_name=test, hash_type=expected_hashtype, |
| 276 hash_digest=expected_hashdigest) |
| 277 actual_url = gm_json.CreateGmActualUrl( |
| 278 test_name=test, hash_type=actual_hashtype, |
| 279 hash_digest=actual_hashdigest) |
| 280 self._image_diff_db.add_image_pair( |
| 281 expected_image_locator=expected_hashdigest, |
| 282 expected_image_url=expected_url, |
| 283 actual_image_locator=actual_hashdigest, |
| 284 actual_image_url=actual_url) |
| 285 |
252 def _load_actual_and_expected(self): | 286 def _load_actual_and_expected(self): |
253 """Loads the results of all tests, across all builders (based on the | 287 """Loads the results of all tests, across all builders (based on the |
254 files within self._actuals_root and self._expected_root), | 288 files within self._actuals_root and self._expected_root), |
255 and stores them in self._results. | 289 and stores them in self._results. |
256 """ | 290 """ |
257 actual_builder_dicts = Results._read_dicts_from_root(self._actuals_root) | 291 actual_builder_dicts = Results._read_dicts_from_root(self._actuals_root) |
258 expected_builder_dicts = Results._read_dicts_from_root(self._expected_root) | 292 expected_builder_dicts = Results._read_dicts_from_root(self._expected_root) |
259 | 293 |
260 categories_all = {} | 294 categories_all = {} |
261 categories_failures = {} | 295 categories_failures = {} |
(...skipping 74 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
336 # category? Maybe we should not rely on the result_type | 370 # category? Maybe we should not rely on the result_type |
337 # categories recorded within the gm_actuals AT ALL, and | 371 # categories recorded within the gm_actuals AT ALL, and |
338 # instead evaluate the result_type ourselves based on what | 372 # instead evaluate the result_type ourselves based on what |
339 # we see in expectations vs actual checksum? | 373 # we see in expectations vs actual checksum? |
340 if expected_image == actual_image: | 374 if expected_image == actual_image: |
341 updated_result_type = gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED | 375 updated_result_type = gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED |
342 else: | 376 else: |
343 updated_result_type = result_type | 377 updated_result_type = result_type |
344 | 378 |
345 (test, config) = IMAGE_FILENAME_RE.match(image_name).groups() | 379 (test, config) = IMAGE_FILENAME_RE.match(image_name).groups() |
| 380 self._generate_pixel_diffs_if_needed( |
| 381 test=test, expected_image=expected_image, |
| 382 actual_image=actual_image) |
346 results_for_this_test = { | 383 results_for_this_test = { |
347 'resultType': updated_result_type, | 384 'resultType': updated_result_type, |
348 'builder': builder, | 385 'builder': builder, |
349 'test': test, | 386 'test': test, |
350 'config': config, | 387 'config': config, |
351 'actualHashType': actual_image[0], | 388 'actualHashType': actual_image[0], |
352 'actualHashDigest': str(actual_image[1]), | 389 'actualHashDigest': str(actual_image[1]), |
353 'expectedHashType': expected_image[0], | 390 'expectedHashType': expected_image[0], |
354 'expectedHashDigest': str(expected_image[1]), | 391 'expectedHashDigest': str(expected_image[1]), |
355 | 392 |
356 # FIELDS_PASSED_THRU_VERBATIM that may be overwritten below... | 393 # FIELDS_PASSED_THRU_VERBATIM that may be overwritten below... |
357 gm_json.JSONKEY_EXPECTEDRESULTS_IGNOREFAILURE: False, | 394 gm_json.JSONKEY_EXPECTEDRESULTS_IGNOREFAILURE: False, |
358 } | 395 } |
359 if expectations_per_test: | 396 if expectations_per_test: |
360 for field in FIELDS_PASSED_THRU_VERBATIM: | 397 for field in FIELDS_PASSED_THRU_VERBATIM: |
361 results_for_this_test[field] = expectations_per_test.get(field) | 398 results_for_this_test[field] = expectations_per_test.get(field) |
| 399 |
| 400 if updated_result_type == gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON: |
| 401 pass # no diff record to calculate at all |
| 402 elif updated_result_type == gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED: |
| 403 results_for_this_test['percentDifferingPixels'] = 0 |
| 404 results_for_this_test['weightedDiffMeasure'] = 0 |
| 405 else: |
| 406 try: |
| 407 diff_record = self._image_diff_db.get_diff_record( |
| 408 expected_image_locator=expected_image[1], |
| 409 actual_image_locator=actual_image[1]) |
| 410 results_for_this_test['percentDifferingPixels'] = ( |
| 411 diff_record.get_percent_pixels_differing()) |
| 412 results_for_this_test['weightedDiffMeasure'] = ( |
| 413 diff_record.get_weighted_diff_measure()) |
| 414 except KeyError: |
| 415 logging.warning('unable to find diff_record for ("%s", "%s")' % |
| 416 (expected_image[1], actual_image[1])) |
| 417 pass |
| 418 |
362 Results._add_to_category_dict(categories_all, results_for_this_test) | 419 Results._add_to_category_dict(categories_all, results_for_this_test) |
363 data_all.append(results_for_this_test) | 420 data_all.append(results_for_this_test) |
364 | 421 |
365 # TODO(epoger): In effect, we have a list of resultTypes that we | 422 # TODO(epoger): In effect, we have a list of resultTypes that we |
366 # include in the different result lists (data_all and data_failures). | 423 # include in the different result lists (data_all and data_failures). |
367 # This same list should be used by the calls to | 424 # This same list should be used by the calls to |
368 # Results._ensure_included_in_category_dict() earlier on. | 425 # Results._ensure_included_in_category_dict() earlier on. |
369 if updated_result_type != gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED: | 426 if updated_result_type != gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED: |
370 Results._add_to_category_dict(categories_failures, | 427 Results._add_to_category_dict(categories_failures, |
371 results_for_this_test) | 428 results_for_this_test) |
(...skipping 41 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
413 category_dict: category dict-of-dicts to modify | 470 category_dict: category dict-of-dicts to modify |
414 category_name: category name, as a string | 471 category_name: category name, as a string |
415 category_values: list of values we want to make sure are represented | 472 category_values: list of values we want to make sure are represented |
416 for this category | 473 for this category |
417 """ | 474 """ |
418 if not category_dict.get(category_name): | 475 if not category_dict.get(category_name): |
419 category_dict[category_name] = {} | 476 category_dict[category_name] = {} |
420 for category_value in category_values: | 477 for category_value in category_values: |
421 if not category_dict[category_name].get(category_value): | 478 if not category_dict[category_name].get(category_value): |
422 category_dict[category_name][category_value] = 0 | 479 category_dict[category_name][category_value] = 0 |
OLD | NEW |