| 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 """ |
| 11 | 11 |
| 12 # System-level imports | 12 # System-level imports |
| 13 import fnmatch | 13 import fnmatch |
| 14 import json | 14 import json |
| 15 import logging |
| 15 import os | 16 import os |
| 16 import re | 17 import re |
| 17 import sys | 18 import sys |
| 18 | 19 |
| 19 # Imports from within Skia | 20 # Imports from within Skia |
| 20 # | 21 # |
| 21 # We need to add the 'gm' directory, so that we can import gm_json.py within | 22 # We need to add the 'gm' directory, so that we can import gm_json.py within |
| 22 # that directory. That script allows us to parse the actual-results.json file | 23 # that directory. That script allows us to parse the actual-results.json file |
| 23 # written out by the GM tool. | 24 # written out by the GM tool. |
| 24 # Make sure that the 'gm' dir is in the PYTHONPATH, but add it at the *end* | 25 # Make sure that the 'gm' dir is in the PYTHONPATH, but add it at the *end* |
| 25 # so any dirs that are already in the PYTHONPATH will be preferred. | 26 # so any dirs that are already in the PYTHONPATH will be preferred. |
| 26 GM_DIRECTORY = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) | 27 GM_DIRECTORY = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) |
| 27 if GM_DIRECTORY not in sys.path: | 28 if GM_DIRECTORY not in sys.path: |
| 28 sys.path.append(GM_DIRECTORY) | 29 sys.path.append(GM_DIRECTORY) |
| 29 import gm_json | 30 import gm_json |
| 30 | 31 |
| 31 IMAGE_FILENAME_RE = re.compile(gm_json.IMAGE_FILENAME_PATTERN) | 32 IMAGE_FILENAME_RE = re.compile(gm_json.IMAGE_FILENAME_PATTERN) |
| 32 CATEGORIES_TO_SUMMARIZE = [ | 33 CATEGORIES_TO_SUMMARIZE = [ |
| 33 'builder', 'test', 'config', 'resultType', | 34 'builder', 'test', 'config', 'resultType', |
| 34 ] | 35 ] |
| 36 RESULTS_ALL = 'all' |
| 37 RESULTS_FAILURES = 'failures' |
| 35 | 38 |
| 36 class Results(object): | 39 class Results(object): |
| 37 """ Loads actual and expected results from all builders, supplying combined | 40 """ Loads actual and expected results from all builders, supplying combined |
| 38 reports as requested. """ | 41 reports as requested. |
| 42 |
| 43 Once this object has been constructed, the results are immutable. If you |
| 44 want to update the results based on updated JSON file contents, you will |
| 45 need to create a new Results object.""" |
| 39 | 46 |
| 40 def __init__(self, actuals_root, expected_root): | 47 def __init__(self, actuals_root, expected_root): |
| 41 """ | 48 """ |
| 42 Args: | 49 Args: |
| 43 actuals_root: root directory containing all actual-results.json files | 50 actuals_root: root directory containing all actual-results.json files |
| 44 expected_root: root directory containing all expected-results.json files | 51 expected_root: root directory containing all expected-results.json files |
| 45 """ | 52 """ |
| 46 self._actual_builder_dicts = Results._GetDictsFromRoot(actuals_root) | 53 self._actual_builder_dicts = Results._get_dicts_from_root(actuals_root) |
| 47 self._expected_builder_dicts = Results._GetDictsFromRoot(expected_root) | 54 self._expected_builder_dicts = Results._get_dicts_from_root(expected_root) |
| 48 self._all_results = Results._Combine( | 55 self._combine_actual_and_expected() |
| 49 actual_builder_dicts=self._actual_builder_dicts, | |
| 50 expected_builder_dicts=self._expected_builder_dicts) | |
| 51 | 56 |
| 52 def GetAll(self): | 57 def get_results_of_type(self, type): |
| 53 """Return results of all tests, as a dictionary in this form: | 58 """Return results of some/all tests (depending on 'type' parameter). |
| 59 |
| 60 Args: |
| 61 type: string describing which types of results to include; must be one |
| 62 of the RESULTS_* constants |
| 63 |
| 64 Results are returned as a dictionary in this form: |
| 54 | 65 |
| 55 { | 66 { |
| 56 'categories': # dictionary of categories listed in | 67 'categories': # dictionary of categories listed in |
| 57 # CATEGORIES_TO_SUMMARIZE, with the number of times | 68 # CATEGORIES_TO_SUMMARIZE, with the number of times |
| 58 # each value appears within its category | 69 # each value appears within its category |
| 59 { | 70 { |
| 60 'resultType': # category name | 71 'resultType': # category name |
| 61 { | 72 { |
| 62 'failed': 29, # category value and total number found of that value | 73 'failed': 29, # category value and total number found of that value |
| 63 'failure-ignored': 948, | 74 'failure-ignored': 948, |
| 64 'no-comparison': 4502, | 75 'no-comparison': 4502, |
| 65 'succeeded': 38609, | 76 'succeeded': 38609, |
| 66 }, | 77 }, |
| 67 'builder': | 78 'builder': |
| 68 { | 79 { |
| 69 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Debug': 1286, | 80 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Debug': 1286, |
| 70 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Release': 1134, | 81 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Release': 1134, |
| 71 ... | 82 ... |
| 72 }, | 83 }, |
| 73 ... # other categories from CATEGORIES_TO_SUMMARIZE | 84 ... # other categories from CATEGORIES_TO_SUMMARIZE |
| 74 }, # end of 'categories' dictionary | 85 }, # end of 'categories' dictionary |
| 75 | 86 |
| 76 'testData': # list of test results, with a dictionary for each | 87 'testData': # list of test results, with a dictionary for each |
| 77 [ | 88 [ |
| 78 { | 89 { |
| 79 'index': 0, # index of this result within testData list | |
| 80 'builder': 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Debug', | 90 'builder': 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Debug', |
| 81 'test': 'bigmatrix', | 91 'test': 'bigmatrix', |
| 82 'config': '8888', | 92 'config': '8888', |
| 83 'resultType': 'failed', | 93 'resultType': 'failed', |
| 84 'expectedHashType': 'bitmap-64bitMD5', | 94 'expectedHashType': 'bitmap-64bitMD5', |
| 85 'expectedHashDigest': '10894408024079689926', | 95 'expectedHashDigest': '10894408024079689926', |
| 86 'actualHashType': 'bitmap-64bitMD5', | 96 'actualHashType': 'bitmap-64bitMD5', |
| 87 'actualHashDigest': '2409857384569', | 97 'actualHashDigest': '2409857384569', |
| 88 }, | 98 }, |
| 89 ... | 99 ... |
| 90 ], # end of 'testData' list | 100 ], # end of 'testData' list |
| 91 } | 101 } |
| 92 """ | 102 """ |
| 93 return self._all_results | 103 return self._results[type] |
| 94 | 104 |
| 95 @staticmethod | 105 @staticmethod |
| 96 def _GetDictsFromRoot(root, pattern='*.json'): | 106 def _get_dicts_from_root(root, pattern='*.json'): |
| 97 """Read all JSON dictionaries within a directory tree. | 107 """Read all JSON dictionaries within a directory tree. |
| 98 | 108 |
| 99 Args: | 109 Args: |
| 100 root: path to root of directory tree | 110 root: path to root of directory tree |
| 101 pattern: which files to read within root (fnmatch-style pattern) | 111 pattern: which files to read within root (fnmatch-style pattern) |
| 102 | 112 |
| 103 Returns: | 113 Returns: |
| 104 A meta-dictionary containing all the JSON dictionaries found within | 114 A meta-dictionary containing all the JSON dictionaries found within |
| 105 the directory tree, keyed by the builder name of each dictionary. | 115 the directory tree, keyed by the builder name of each dictionary. |
| 106 """ | 116 """ |
| 107 meta_dict = {} | 117 meta_dict = {} |
| 108 for dirpath, dirnames, filenames in os.walk(root): | 118 for dirpath, dirnames, filenames in os.walk(root): |
| 109 for matching_filename in fnmatch.filter(filenames, pattern): | 119 for matching_filename in fnmatch.filter(filenames, pattern): |
| 110 builder = os.path.basename(dirpath) | 120 builder = os.path.basename(dirpath) |
| 111 if builder.endswith('-Trybot'): | 121 if builder.endswith('-Trybot'): |
| 112 continue | 122 continue |
| 113 fullpath = os.path.join(dirpath, matching_filename) | 123 fullpath = os.path.join(dirpath, matching_filename) |
| 114 meta_dict[builder] = gm_json.LoadFromFile(fullpath) | 124 meta_dict[builder] = gm_json.LoadFromFile(fullpath) |
| 115 return meta_dict | 125 return meta_dict |
| 116 | 126 |
| 117 @staticmethod | 127 def _combine_actual_and_expected(self): |
| 118 def _Combine(actual_builder_dicts, expected_builder_dicts): | |
| 119 """Gathers the results of all tests, across all builders (based on the | 128 """Gathers the results of all tests, across all builders (based on the |
| 120 contents of actual_builder_dicts and expected_builder_dicts). | 129 contents of self._actual_builder_dicts and self._expected_builder_dicts), |
| 121 | 130 and stores them in self._results. |
| 122 This is a static method, because once we start refreshing results | |
| 123 asynchronously, we need to make sure we are not corrupting the object's | |
| 124 member variables. | |
| 125 | |
| 126 Args: | |
| 127 actual_builder_dicts: a meta-dictionary of all actual JSON results, | |
| 128 as returned by _GetDictsFromRoot(). | |
| 129 actual_builder_dicts: a meta-dictionary of all expected JSON results, | |
| 130 as returned by _GetDictsFromRoot(). | |
| 131 | |
| 132 Returns: | |
| 133 A list of all the results of all tests, in the same form returned by | |
| 134 self.GetAll(). | |
| 135 """ | 131 """ |
| 136 test_data = [] | 132 categories_all = {} |
| 137 category_dict = {} | 133 categories_failures = {} |
| 138 Results._EnsureIncludedInCategoryDict(category_dict, 'resultType', [ | 134 Results._ensure_included_in_category_dict(categories_all, |
| 135 'resultType', [ |
| 139 gm_json.JSONKEY_ACTUALRESULTS_FAILED, | 136 gm_json.JSONKEY_ACTUALRESULTS_FAILED, |
| 140 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED, | 137 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED, |
| 141 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON, | 138 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON, |
| 142 gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED, | 139 gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED, |
| 143 ]) | 140 ]) |
| 141 Results._ensure_included_in_category_dict(categories_failures, |
| 142 'resultType', [ |
| 143 gm_json.JSONKEY_ACTUALRESULTS_FAILED, |
| 144 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED, |
| 145 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON, |
| 146 ]) |
| 144 | 147 |
| 145 for builder in sorted(actual_builder_dicts.keys()): | 148 data_all = [] |
| 149 data_failures = [] |
| 150 for builder in sorted(self._actual_builder_dicts.keys()): |
| 146 actual_results_for_this_builder = ( | 151 actual_results_for_this_builder = ( |
| 147 actual_builder_dicts[builder][gm_json.JSONKEY_ACTUALRESULTS]) | 152 self._actual_builder_dicts[builder][gm_json.JSONKEY_ACTUALRESULTS]) |
| 148 for result_type in sorted(actual_results_for_this_builder.keys()): | 153 for result_type in sorted(actual_results_for_this_builder.keys()): |
| 149 results_of_this_type = actual_results_for_this_builder[result_type] | 154 results_of_this_type = actual_results_for_this_builder[result_type] |
| 150 if not results_of_this_type: | 155 if not results_of_this_type: |
| 151 continue | 156 continue |
| 152 for image_name in sorted(results_of_this_type.keys()): | 157 for image_name in sorted(results_of_this_type.keys()): |
| 153 actual_image = results_of_this_type[image_name] | 158 actual_image = results_of_this_type[image_name] |
| 154 try: | 159 try: |
| 155 # TODO(epoger): assumes a single allowed digest per test | 160 # TODO(epoger): assumes a single allowed digest per test |
| 156 expected_image = ( | 161 expected_image = ( |
| 157 expected_builder_dicts | 162 self._expected_builder_dicts |
| 158 [builder][gm_json.JSONKEY_EXPECTEDRESULTS] | 163 [builder][gm_json.JSONKEY_EXPECTEDRESULTS] |
| 159 [image_name][gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS] | 164 [image_name][gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS] |
| 160 [0]) | 165 [0]) |
| 161 except (KeyError, TypeError): | 166 except (KeyError, TypeError): |
| 162 # There are several cases in which we would expect to find | 167 # There are several cases in which we would expect to find |
| 163 # no expectations for a given test: | 168 # no expectations for a given test: |
| 164 # | 169 # |
| 165 # 1. result_type == NOCOMPARISON | 170 # 1. result_type == NOCOMPARISON |
| 166 # There are no expectations for this test yet! | 171 # There are no expectations for this test yet! |
| 167 # | 172 # |
| (...skipping 11 matching lines...) Expand all Loading... |
| 179 # for the test (the implicit expectation is that it must | 184 # for the test (the implicit expectation is that it must |
| 180 # render the same in all rendering modes). | 185 # render the same in all rendering modes). |
| 181 # | 186 # |
| 182 # Don't log types 1 or 2, because they are common. | 187 # Don't log types 1 or 2, because they are common. |
| 183 # Log other types, because they are rare and we should know about | 188 # Log other types, because they are rare and we should know about |
| 184 # them, but don't throw an exception, because we need to keep our | 189 # them, but don't throw an exception, because we need to keep our |
| 185 # tools working in the meanwhile! | 190 # tools working in the meanwhile! |
| 186 if result_type not in [ | 191 if result_type not in [ |
| 187 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON, | 192 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON, |
| 188 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED] : | 193 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED] : |
| 189 print 'WARNING: No expectations found for test: %s' % { | 194 logging.warning('No expectations found for test: %s' % { |
| 190 'builder': builder, | 195 'builder': builder, |
| 191 'image_name': image_name, | 196 'image_name': image_name, |
| 192 'result_type': result_type, | 197 'result_type': result_type, |
| 193 } | 198 }) |
| 194 expected_image = [None, None] | 199 expected_image = [None, None] |
| 195 | 200 |
| 196 # If this test was recently rebaselined, it will remain in | 201 # If this test was recently rebaselined, it will remain in |
| 197 # the 'failed' set of actuals until all the bots have | 202 # the 'failed' set of actuals until all the bots have |
| 198 # cycled (although the expectations have indeed been set | 203 # cycled (although the expectations have indeed been set |
| 199 # from the most recent actuals). Treat these as successes | 204 # from the most recent actuals). Treat these as successes |
| 200 # instead of failures. | 205 # instead of failures. |
| 201 # | 206 # |
| 202 # TODO(epoger): Do we need to do something similar in | 207 # TODO(epoger): Do we need to do something similar in |
| 203 # other cases, such as when we have recently marked a test | 208 # other cases, such as when we have recently marked a test |
| 204 # as ignoreFailure but it still shows up in the 'failed' | 209 # as ignoreFailure but it still shows up in the 'failed' |
| 205 # category? Maybe we should not rely on the result_type | 210 # category? Maybe we should not rely on the result_type |
| 206 # categories recorded within the gm_actuals AT ALL, and | 211 # categories recorded within the gm_actuals AT ALL, and |
| 207 # instead evaluate the result_type ourselves based on what | 212 # instead evaluate the result_type ourselves based on what |
| 208 # we see in expectations vs actual checksum? | 213 # we see in expectations vs actual checksum? |
| 209 if expected_image == actual_image: | 214 if expected_image == actual_image: |
| 210 updated_result_type = gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED | 215 updated_result_type = gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED |
| 211 else: | 216 else: |
| 212 updated_result_type = result_type | 217 updated_result_type = result_type |
| 213 | 218 |
| 214 (test, config) = IMAGE_FILENAME_RE.match(image_name).groups() | 219 (test, config) = IMAGE_FILENAME_RE.match(image_name).groups() |
| 215 results_for_this_test = { | 220 results_for_this_test = { |
| 216 'index': len(test_data), | |
| 217 'builder': builder, | 221 'builder': builder, |
| 218 'test': test, | 222 'test': test, |
| 219 'config': config, | 223 'config': config, |
| 220 'resultType': updated_result_type, | 224 'resultType': updated_result_type, |
| 221 'actualHashType': actual_image[0], | 225 'actualHashType': actual_image[0], |
| 222 'actualHashDigest': str(actual_image[1]), | 226 'actualHashDigest': str(actual_image[1]), |
| 223 'expectedHashType': expected_image[0], | 227 'expectedHashType': expected_image[0], |
| 224 'expectedHashDigest': str(expected_image[1]), | 228 'expectedHashDigest': str(expected_image[1]), |
| 225 } | 229 } |
| 226 Results._AddToCategoryDict(category_dict, results_for_this_test) | 230 Results._add_to_category_dict(categories_all, results_for_this_test) |
| 227 test_data.append(results_for_this_test) | 231 data_all.append(results_for_this_test) |
| 228 return {'categories': category_dict, 'testData': test_data} | 232 if updated_result_type != gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED: |
| 233 Results._add_to_category_dict(categories_failures, |
| 234 results_for_this_test) |
| 235 data_failures.append(results_for_this_test) |
| 236 |
| 237 self._results = { |
| 238 RESULTS_ALL: |
| 239 {'categories': categories_all, 'testData': data_all}, |
| 240 RESULTS_FAILURES: |
| 241 {'categories': categories_failures, 'testData': data_failures}, |
| 242 } |
| 229 | 243 |
| 230 @staticmethod | 244 @staticmethod |
| 231 def _AddToCategoryDict(category_dict, test_results): | 245 def _add_to_category_dict(category_dict, test_results): |
| 232 """Add test_results to the category dictionary we are building. | 246 """Add test_results to the category dictionary we are building. |
| 233 (See documentation of self.GetAll() for the format of this dictionary.) | 247 (See documentation of self.get_results_of_type() for the format of this |
| 248 dictionary.) |
| 234 | 249 |
| 235 Args: | 250 Args: |
| 236 category_dict: category dict-of-dicts to add to; modify this in-place | 251 category_dict: category dict-of-dicts to add to; modify this in-place |
| 237 test_results: test data with which to update category_list, in a dict: | 252 test_results: test data with which to update category_list, in a dict: |
| 238 { | 253 { |
| 239 'category_name': 'category_value', | 254 'category_name': 'category_value', |
| 240 'category_name': 'category_value', | 255 'category_name': 'category_value', |
| 241 ... | 256 ... |
| 242 } | 257 } |
| 243 """ | 258 """ |
| 244 for category in CATEGORIES_TO_SUMMARIZE: | 259 for category in CATEGORIES_TO_SUMMARIZE: |
| 245 category_value = test_results.get(category) | 260 category_value = test_results.get(category) |
| 246 if not category_value: | 261 if not category_value: |
| 247 continue # test_results did not include this category, keep going | 262 continue # test_results did not include this category, keep going |
| 248 if not category_dict.get(category): | 263 if not category_dict.get(category): |
| 249 category_dict[category] = {} | 264 category_dict[category] = {} |
| 250 if not category_dict[category].get(category_value): | 265 if not category_dict[category].get(category_value): |
| 251 category_dict[category][category_value] = 0 | 266 category_dict[category][category_value] = 0 |
| 252 category_dict[category][category_value] += 1 | 267 category_dict[category][category_value] += 1 |
| 253 | 268 |
| 254 @staticmethod | 269 @staticmethod |
| 255 def _EnsureIncludedInCategoryDict(category_dict, | 270 def _ensure_included_in_category_dict(category_dict, |
| 256 category_name, category_values): | 271 category_name, category_values): |
| 257 """Ensure that the category name/value pairs are included in category_dict, | 272 """Ensure that the category name/value pairs are included in category_dict, |
| 258 even if there aren't any results with that name/value pair. | 273 even if there aren't any results with that name/value pair. |
| 259 (See documentation of self.GetAll() for the format of this dictionary.) | 274 (See documentation of self.get_results_of_type() for the format of this |
| 275 dictionary.) |
| 260 | 276 |
| 261 Args: | 277 Args: |
| 262 category_dict: category dict-of-dicts to modify | 278 category_dict: category dict-of-dicts to modify |
| 263 category_name: category name, as a string | 279 category_name: category name, as a string |
| 264 category_values: list of values we want to make sure are represented | 280 category_values: list of values we want to make sure are represented |
| 265 for this category | 281 for this category |
| 266 """ | 282 """ |
| 267 if not category_dict.get(category_name): | 283 if not category_dict.get(category_name): |
| 268 category_dict[category_name] = {} | 284 category_dict[category_name] = {} |
| 269 for category_value in category_values: | 285 for category_value in category_values: |
| 270 if not category_dict[category_name].get(category_value): | 286 if not category_dict[category_name].get(category_value): |
| 271 category_dict[category_name][category_value] = 0 | 287 category_dict[category_name][category_value] = 0 |
| OLD | NEW |