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 |