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 |