| 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 # so any dirs that are already in the PYTHONPATH will be preferred. | 25 # 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__))) | 26 GM_DIRECTORY = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) |
| 27 if GM_DIRECTORY not in sys.path: | 27 if GM_DIRECTORY not in sys.path: |
| 28 sys.path.append(GM_DIRECTORY) | 28 sys.path.append(GM_DIRECTORY) |
| 29 import gm_json | 29 import gm_json |
| 30 | 30 |
| 31 IMAGE_FILENAME_RE = re.compile(gm_json.IMAGE_FILENAME_PATTERN) | 31 IMAGE_FILENAME_RE = re.compile(gm_json.IMAGE_FILENAME_PATTERN) |
| 32 CATEGORIES_TO_SUMMARIZE = [ | 32 CATEGORIES_TO_SUMMARIZE = [ |
| 33 'builder', 'test', 'config', 'resultType', | 33 'builder', 'test', 'config', 'resultType', |
| 34 ] | 34 ] |
| 35 RESULTS_ALL = 'all' |
| 36 RESULTS_FAILURES = 'failures' |
| 35 | 37 |
| 36 class Results(object): | 38 class Results(object): |
| 37 """ Loads actual and expected results from all builders, supplying combined | 39 """ Loads actual and expected results from all builders, supplying combined |
| 38 reports as requested. """ | 40 reports as requested. |
| 41 |
| 42 Once this object has been constructed, the results are immutable. If you |
| 43 want to update the results based on updated JSON file contents, you will |
| 44 need to create a new Results object.""" |
| 39 | 45 |
| 40 def __init__(self, actuals_root, expected_root): | 46 def __init__(self, actuals_root, expected_root): |
| 41 """ | 47 """ |
| 42 Args: | 48 Args: |
| 43 actuals_root: root directory containing all actual-results.json files | 49 actuals_root: root directory containing all actual-results.json files |
| 44 expected_root: root directory containing all expected-results.json files | 50 expected_root: root directory containing all expected-results.json files |
| 45 """ | 51 """ |
| 46 self._actual_builder_dicts = Results._GetDictsFromRoot(actuals_root) | 52 self._actual_builder_dicts = Results._get_dicts_from_root(actuals_root) |
| 47 self._expected_builder_dicts = Results._GetDictsFromRoot(expected_root) | 53 self._expected_builder_dicts = Results._get_dicts_from_root(expected_root) |
| 48 self._all_results = Results._Combine( | 54 self._combine_actual_and_expected() |
| 49 actual_builder_dicts=self._actual_builder_dicts, | |
| 50 expected_builder_dicts=self._expected_builder_dicts) | |
| 51 | 55 |
| 52 def GetAll(self): | 56 def get_results_of_type(self, type): |
| 53 """Return results of all tests, as a dictionary in this form: | 57 """Return results of some/all tests (depending on 'type' parameter). |
| 58 |
| 59 Args: |
| 60 type: string describing which types of results to include; must be one |
| 61 of the RESULTS_* constants |
| 62 |
| 63 Results are returned as a dictionary in this form: |
| 54 | 64 |
| 55 { | 65 { |
| 56 'categories': # dictionary of categories listed in | 66 'categories': # dictionary of categories listed in |
| 57 # CATEGORIES_TO_SUMMARIZE, with the number of times | 67 # CATEGORIES_TO_SUMMARIZE, with the number of times |
| 58 # each value appears within its category | 68 # each value appears within its category |
| 59 { | 69 { |
| 60 'resultType': # category name | 70 'resultType': # category name |
| 61 { | 71 { |
| 62 'failed': 29, # category value and total number found of that value | 72 'failed': 29, # category value and total number found of that value |
| 63 'failure-ignored': 948, | 73 'failure-ignored': 948, |
| 64 'no-comparison': 4502, | 74 'no-comparison': 4502, |
| 65 'succeeded': 38609, | 75 'succeeded': 38609, |
| 66 }, | 76 }, |
| 67 'builder': | 77 'builder': |
| 68 { | 78 { |
| 69 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Debug': 1286, | 79 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Debug': 1286, |
| 70 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Release': 1134, | 80 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Release': 1134, |
| 71 ... | 81 ... |
| 72 }, | 82 }, |
| 73 ... # other categories from CATEGORIES_TO_SUMMARIZE | 83 ... # other categories from CATEGORIES_TO_SUMMARIZE |
| 74 }, # end of 'categories' dictionary | 84 }, # end of 'categories' dictionary |
| 75 | 85 |
| 76 'testData': # list of test results, with a dictionary for each | 86 'testData': # list of test results, with a dictionary for each |
| 77 [ | 87 [ |
| 78 { | 88 { |
| 79 'index': 0, # index of this result within testData list | |
| 80 'builder': 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Debug', | 89 'builder': 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Debug', |
| 81 'test': 'bigmatrix', | 90 'test': 'bigmatrix', |
| 82 'config': '8888', | 91 'config': '8888', |
| 83 'resultType': 'failed', | 92 'resultType': 'failed', |
| 84 'expectedHashType': 'bitmap-64bitMD5', | 93 'expectedHashType': 'bitmap-64bitMD5', |
| 85 'expectedHashDigest': '10894408024079689926', | 94 'expectedHashDigest': '10894408024079689926', |
| 86 'actualHashType': 'bitmap-64bitMD5', | 95 'actualHashType': 'bitmap-64bitMD5', |
| 87 'actualHashDigest': '2409857384569', | 96 'actualHashDigest': '2409857384569', |
| 88 }, | 97 }, |
| 89 ... | 98 ... |
| 90 ], # end of 'testData' list | 99 ], # end of 'testData' list |
| 91 } | 100 } |
| 92 """ | 101 """ |
| 93 return self._all_results | 102 return self._results[type] |
| 94 | 103 |
| 95 @staticmethod | 104 @staticmethod |
| 96 def _GetDictsFromRoot(root, pattern='*.json'): | 105 def _get_dicts_from_root(root, pattern='*.json'): |
| 97 """Read all JSON dictionaries within a directory tree. | 106 """Read all JSON dictionaries within a directory tree. |
| 98 | 107 |
| 99 Args: | 108 Args: |
| 100 root: path to root of directory tree | 109 root: path to root of directory tree |
| 101 pattern: which files to read within root (fnmatch-style pattern) | 110 pattern: which files to read within root (fnmatch-style pattern) |
| 102 | 111 |
| 103 Returns: | 112 Returns: |
| 104 A meta-dictionary containing all the JSON dictionaries found within | 113 A meta-dictionary containing all the JSON dictionaries found within |
| 105 the directory tree, keyed by the builder name of each dictionary. | 114 the directory tree, keyed by the builder name of each dictionary. |
| 106 """ | 115 """ |
| 107 meta_dict = {} | 116 meta_dict = {} |
| 108 for dirpath, dirnames, filenames in os.walk(root): | 117 for dirpath, dirnames, filenames in os.walk(root): |
| 109 for matching_filename in fnmatch.filter(filenames, pattern): | 118 for matching_filename in fnmatch.filter(filenames, pattern): |
| 110 builder = os.path.basename(dirpath) | 119 builder = os.path.basename(dirpath) |
| 111 if builder.endswith('-Trybot'): | 120 if builder.endswith('-Trybot'): |
| 112 continue | 121 continue |
| 113 fullpath = os.path.join(dirpath, matching_filename) | 122 fullpath = os.path.join(dirpath, matching_filename) |
| 114 meta_dict[builder] = gm_json.LoadFromFile(fullpath) | 123 meta_dict[builder] = gm_json.LoadFromFile(fullpath) |
| 115 return meta_dict | 124 return meta_dict |
| 116 | 125 |
| 117 @staticmethod | 126 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 | 127 """Gathers the results of all tests, across all builders (based on the |
| 120 contents of actual_builder_dicts and expected_builder_dicts). | 128 contents of self._actual_builder_dicts and self._expected_builder_dicts), |
| 121 | 129 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 """ | 130 """ |
| 136 test_data = [] | 131 categories_all = {} |
| 137 category_dict = {} | 132 categories_failures = {} |
| 138 Results._EnsureIncludedInCategoryDict(category_dict, 'resultType', [ | 133 Results._ensure_included_in_category_dict(categories_all, |
| 134 'resultType', [ |
| 139 gm_json.JSONKEY_ACTUALRESULTS_FAILED, | 135 gm_json.JSONKEY_ACTUALRESULTS_FAILED, |
| 140 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED, | 136 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED, |
| 141 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON, | 137 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON, |
| 142 gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED, | 138 gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED, |
| 143 ]) | 139 ]) |
| 140 Results._ensure_included_in_category_dict(categories_failures, |
| 141 'resultType', [ |
| 142 gm_json.JSONKEY_ACTUALRESULTS_FAILED, |
| 143 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED, |
| 144 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON, |
| 145 ]) |
| 144 | 146 |
| 145 for builder in sorted(actual_builder_dicts.keys()): | 147 data_all = [] |
| 148 data_failures = [] |
| 149 for builder in sorted(self._actual_builder_dicts.keys()): |
| 146 actual_results_for_this_builder = ( | 150 actual_results_for_this_builder = ( |
| 147 actual_builder_dicts[builder][gm_json.JSONKEY_ACTUALRESULTS]) | 151 self._actual_builder_dicts[builder][gm_json.JSONKEY_ACTUALRESULTS]) |
| 148 for result_type in sorted(actual_results_for_this_builder.keys()): | 152 for result_type in sorted(actual_results_for_this_builder.keys()): |
| 149 results_of_this_type = actual_results_for_this_builder[result_type] | 153 results_of_this_type = actual_results_for_this_builder[result_type] |
| 150 if not results_of_this_type: | 154 if not results_of_this_type: |
| 151 continue | 155 continue |
| 152 for image_name in sorted(results_of_this_type.keys()): | 156 for image_name in sorted(results_of_this_type.keys()): |
| 153 actual_image = results_of_this_type[image_name] | 157 actual_image = results_of_this_type[image_name] |
| 154 try: | 158 try: |
| 155 # TODO(epoger): assumes a single allowed digest per test | 159 # TODO(epoger): assumes a single allowed digest per test |
| 156 expected_image = ( | 160 expected_image = ( |
| 157 expected_builder_dicts | 161 self._expected_builder_dicts |
| 158 [builder][gm_json.JSONKEY_EXPECTEDRESULTS] | 162 [builder][gm_json.JSONKEY_EXPECTEDRESULTS] |
| 159 [image_name][gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS] | 163 [image_name][gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS] |
| 160 [0]) | 164 [0]) |
| 161 except (KeyError, TypeError): | 165 except (KeyError, TypeError): |
| 162 # There are several cases in which we would expect to find | 166 # There are several cases in which we would expect to find |
| 163 # no expectations for a given test: | 167 # no expectations for a given test: |
| 164 # | 168 # |
| 165 # 1. result_type == NOCOMPARISON | 169 # 1. result_type == NOCOMPARISON |
| 166 # There are no expectations for this test yet! | 170 # There are no expectations for this test yet! |
| 167 # | 171 # |
| (...skipping 38 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 206 # categories recorded within the gm_actuals AT ALL, and | 210 # categories recorded within the gm_actuals AT ALL, and |
| 207 # instead evaluate the result_type ourselves based on what | 211 # instead evaluate the result_type ourselves based on what |
| 208 # we see in expectations vs actual checksum? | 212 # we see in expectations vs actual checksum? |
| 209 if expected_image == actual_image: | 213 if expected_image == actual_image: |
| 210 updated_result_type = gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED | 214 updated_result_type = gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED |
| 211 else: | 215 else: |
| 212 updated_result_type = result_type | 216 updated_result_type = result_type |
| 213 | 217 |
| 214 (test, config) = IMAGE_FILENAME_RE.match(image_name).groups() | 218 (test, config) = IMAGE_FILENAME_RE.match(image_name).groups() |
| 215 results_for_this_test = { | 219 results_for_this_test = { |
| 216 'index': len(test_data), | |
| 217 'builder': builder, | 220 'builder': builder, |
| 218 'test': test, | 221 'test': test, |
| 219 'config': config, | 222 'config': config, |
| 220 'resultType': updated_result_type, | 223 'resultType': updated_result_type, |
| 221 'actualHashType': actual_image[0], | 224 'actualHashType': actual_image[0], |
| 222 'actualHashDigest': str(actual_image[1]), | 225 'actualHashDigest': str(actual_image[1]), |
| 223 'expectedHashType': expected_image[0], | 226 'expectedHashType': expected_image[0], |
| 224 'expectedHashDigest': str(expected_image[1]), | 227 'expectedHashDigest': str(expected_image[1]), |
| 225 } | 228 } |
| 226 Results._AddToCategoryDict(category_dict, results_for_this_test) | 229 Results._add_to_category_dict(categories_all, results_for_this_test) |
| 227 test_data.append(results_for_this_test) | 230 data_all.append(results_for_this_test) |
| 228 return {'categories': category_dict, 'testData': test_data} | 231 if updated_result_type != gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED: |
| 232 Results._add_to_category_dict(categories_failures, |
| 233 results_for_this_test) |
| 234 data_failures.append(results_for_this_test) |
| 235 |
| 236 self._results = { |
| 237 RESULTS_ALL: |
| 238 {'categories': categories_all, 'testData': data_all}, |
| 239 RESULTS_FAILURES: |
| 240 {'categories': categories_failures, 'testData': data_failures}, |
| 241 } |
| 229 | 242 |
| 230 @staticmethod | 243 @staticmethod |
| 231 def _AddToCategoryDict(category_dict, test_results): | 244 def _add_to_category_dict(category_dict, test_results): |
| 232 """Add test_results to the category dictionary we are building. | 245 """Add test_results to the category dictionary we are building. |
| 233 (See documentation of self.GetAll() for the format of this dictionary.) | 246 (See documentation of self.get_results_of_type() for the format of this |
| 247 dictionary.) |
| 234 | 248 |
| 235 Args: | 249 Args: |
| 236 category_dict: category dict-of-dicts to add to; modify this in-place | 250 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: | 251 test_results: test data with which to update category_list, in a dict: |
| 238 { | 252 { |
| 239 'category_name': 'category_value', | 253 'category_name': 'category_value', |
| 240 'category_name': 'category_value', | 254 'category_name': 'category_value', |
| 241 ... | 255 ... |
| 242 } | 256 } |
| 243 """ | 257 """ |
| 244 for category in CATEGORIES_TO_SUMMARIZE: | 258 for category in CATEGORIES_TO_SUMMARIZE: |
| 245 category_value = test_results.get(category) | 259 category_value = test_results.get(category) |
| 246 if not category_value: | 260 if not category_value: |
| 247 continue # test_results did not include this category, keep going | 261 continue # test_results did not include this category, keep going |
| 248 if not category_dict.get(category): | 262 if not category_dict.get(category): |
| 249 category_dict[category] = {} | 263 category_dict[category] = {} |
| 250 if not category_dict[category].get(category_value): | 264 if not category_dict[category].get(category_value): |
| 251 category_dict[category][category_value] = 0 | 265 category_dict[category][category_value] = 0 |
| 252 category_dict[category][category_value] += 1 | 266 category_dict[category][category_value] += 1 |
| 253 | 267 |
| 254 @staticmethod | 268 @staticmethod |
| 255 def _EnsureIncludedInCategoryDict(category_dict, | 269 def _ensure_included_in_category_dict(category_dict, |
| 256 category_name, category_values): | 270 category_name, category_values): |
| 257 """Ensure that the category name/value pairs are included in category_dict, | 271 """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. | 272 even if there aren't any results with that name/value pair. |
| 259 (See documentation of self.GetAll() for the format of this dictionary.) | 273 (See documentation of self.get_results_of_type() for the format of this |
| 274 dictionary.) |
| 260 | 275 |
| 261 Args: | 276 Args: |
| 262 category_dict: category dict-of-dicts to modify | 277 category_dict: category dict-of-dicts to modify |
| 263 category_name: category name, as a string | 278 category_name: category name, as a string |
| 264 category_values: list of values we want to make sure are represented | 279 category_values: list of values we want to make sure are represented |
| 265 for this category | 280 for this category |
| 266 """ | 281 """ |
| 267 if not category_dict.get(category_name): | 282 if not category_dict.get(category_name): |
| 268 category_dict[category_name] = {} | 283 category_dict[category_name] = {} |
| 269 for category_value in category_values: | 284 for category_value in category_values: |
| 270 if not category_dict[category_name].get(category_value): | 285 if not category_dict[category_name].get(category_value): |
| 271 category_dict[category_name][category_value] = 0 | 286 category_dict[category_name][category_value] = 0 |
| OLD | NEW |