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 13 matching lines...) Expand all Loading... |
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 | 32 |
33 IMAGE_FILENAME_RE = re.compile(gm_json.IMAGE_FILENAME_PATTERN) | 33 IMAGE_FILENAME_RE = re.compile(gm_json.IMAGE_FILENAME_PATTERN) |
| 34 IMAGE_FILENAME_FORMATTER = '%s_%s.png' # pass in (testname, config) |
| 35 |
34 CATEGORIES_TO_SUMMARIZE = [ | 36 CATEGORIES_TO_SUMMARIZE = [ |
35 'builder', 'test', 'config', 'resultType', | 37 'builder', 'test', 'config', 'resultType', |
36 ] | 38 ] |
37 RESULTS_ALL = 'all' | 39 RESULTS_ALL = 'all' |
38 RESULTS_FAILURES = 'failures' | 40 RESULTS_FAILURES = 'failures' |
39 | 41 |
40 class Results(object): | 42 class Results(object): |
41 """ Loads actual and expected results from all builders, supplying combined | 43 """ Loads actual and expected results from all builders, supplying combined |
42 reports as requested. | 44 reports as requested. |
43 | 45 |
44 Once this object has been constructed, the results are immutable. If you | 46 Once this object has been constructed, the results (in self._results[]) |
45 want to update the results based on updated JSON file contents, you will | 47 are immutable. If you want to update the results based on updated JSON |
46 need to create a new Results object.""" | 48 file contents, you will need to create a new Results object.""" |
47 | 49 |
48 def __init__(self, actuals_root, expected_root): | 50 def __init__(self, actuals_root, expected_root): |
49 """ | 51 """ |
50 Args: | 52 Args: |
51 actuals_root: root directory containing all actual-results.json files | 53 actuals_root: root directory containing all actual-results.json files |
52 expected_root: root directory containing all expected-results.json files | 54 expected_root: root directory containing all expected-results.json files |
53 """ | 55 """ |
54 self._actual_builder_dicts = Results._get_dicts_from_root(actuals_root) | 56 self._actuals_root = actuals_root |
55 self._expected_builder_dicts = Results._get_dicts_from_root(expected_root) | 57 self._expected_root = expected_root |
56 self._combine_actual_and_expected() | 58 self._load_actual_and_expected() |
57 self._timestamp = int(time.time()) | 59 self._timestamp = int(time.time()) |
58 | 60 |
59 def get_timestamp(self): | 61 def get_timestamp(self): |
60 """Return the time at which this object was created, in seconds past epoch | 62 """Return the time at which this object was created, in seconds past epoch |
61 (UTC). | 63 (UTC). |
62 """ | 64 """ |
63 return self._timestamp | 65 return self._timestamp |
64 | 66 |
| 67 def edit_expectations(self, modifications): |
| 68 """Edit the expectations stored within this object and write them back |
| 69 to disk. |
| 70 |
| 71 Note that this will NOT update the results stored in self._results[] ; |
| 72 in order to see those updates, you must instantiate a new Results object |
| 73 based on the (now updated) files on disk. |
| 74 |
| 75 Args: |
| 76 modifications: a list of dictionaries, one for each expectation to update: |
| 77 |
| 78 [ |
| 79 { |
| 80 'builder': 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Debug', |
| 81 'test': 'bigmatrix', |
| 82 'config': '8888', |
| 83 'expectedHashType': 'bitmap-64bitMD5', |
| 84 'expectedHashDigest': '10894408024079689926', |
| 85 }, |
| 86 ... |
| 87 ] |
| 88 |
| 89 TODO(epoger): For now, this does not allow the caller to set any fields |
| 90 other than expectedHashType/expectedHashDigest, and assumes that |
| 91 ignore-failure should be set to False. We need to add support |
| 92 for other fields (notes, bugs, etc.) and ignore-failure=True. |
| 93 """ |
| 94 expected_builder_dicts = Results._read_dicts_from_root(self._expected_root) |
| 95 for mod in modifications: |
| 96 image_name = IMAGE_FILENAME_FORMATTER % (mod['test'], mod['config']) |
| 97 # TODO(epoger): assumes a single allowed digest per test |
| 98 allowed_digests = [[mod['expectedHashType'], |
| 99 int(mod['expectedHashDigest'])]] |
| 100 new_expectations = { |
| 101 gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS: allowed_digests, |
| 102 gm_json.JSONKEY_EXPECTEDRESULTS_IGNOREFAILURE: False, |
| 103 } |
| 104 builder_dict = expected_builder_dicts[mod['builder']] |
| 105 builder_expectations = builder_dict.get(gm_json.JSONKEY_EXPECTEDRESULTS) |
| 106 if not builder_expectations: |
| 107 builder_expectations = {} |
| 108 builder_dict[gm_json.JSONKEY_EXPECTEDRESULTS] = builder_expectations |
| 109 builder_expectations[image_name] = new_expectations |
| 110 Results._write_dicts_to_root(expected_builder_dicts, self._expected_root) |
| 111 |
65 def get_results_of_type(self, type): | 112 def get_results_of_type(self, type): |
66 """Return results of some/all tests (depending on 'type' parameter). | 113 """Return results of some/all tests (depending on 'type' parameter). |
67 | 114 |
68 Args: | 115 Args: |
69 type: string describing which types of results to include; must be one | 116 type: string describing which types of results to include; must be one |
70 of the RESULTS_* constants | 117 of the RESULTS_* constants |
71 | 118 |
72 Results are returned as a dictionary in this form: | 119 Results are returned as a dictionary in this form: |
73 | 120 |
74 { | 121 { |
(...skipping 29 matching lines...) Expand all Loading... |
104 'actualHashType': 'bitmap-64bitMD5', | 151 'actualHashType': 'bitmap-64bitMD5', |
105 'actualHashDigest': '2409857384569', | 152 'actualHashDigest': '2409857384569', |
106 }, | 153 }, |
107 ... | 154 ... |
108 ], # end of 'testData' list | 155 ], # end of 'testData' list |
109 } | 156 } |
110 """ | 157 """ |
111 return self._results[type] | 158 return self._results[type] |
112 | 159 |
113 @staticmethod | 160 @staticmethod |
114 def _get_dicts_from_root(root, pattern='*.json'): | 161 def _read_dicts_from_root(root, pattern='*.json'): |
115 """Read all JSON dictionaries within a directory tree. | 162 """Read all JSON dictionaries within a directory tree. |
116 | 163 |
117 Args: | 164 Args: |
118 root: path to root of directory tree | 165 root: path to root of directory tree |
119 pattern: which files to read within root (fnmatch-style pattern) | 166 pattern: which files to read within root (fnmatch-style pattern) |
120 | 167 |
121 Returns: | 168 Returns: |
122 A meta-dictionary containing all the JSON dictionaries found within | 169 A meta-dictionary containing all the JSON dictionaries found within |
123 the directory tree, keyed by the builder name of each dictionary. | 170 the directory tree, keyed by the builder name of each dictionary. |
124 | 171 |
125 Raises: | 172 Raises: |
126 IOError if root does not refer to an existing directory | 173 IOError if root does not refer to an existing directory |
127 """ | 174 """ |
128 if not os.path.isdir(root): | 175 if not os.path.isdir(root): |
129 raise IOError('no directory found at path %s' % root) | 176 raise IOError('no directory found at path %s' % root) |
130 meta_dict = {} | 177 meta_dict = {} |
131 for dirpath, dirnames, filenames in os.walk(root): | 178 for dirpath, dirnames, filenames in os.walk(root): |
132 for matching_filename in fnmatch.filter(filenames, pattern): | 179 for matching_filename in fnmatch.filter(filenames, pattern): |
133 builder = os.path.basename(dirpath) | 180 builder = os.path.basename(dirpath) |
| 181 # If we are reading from the collection of actual results, skip over |
| 182 # the Trybot results (we don't maintain baselines for them). |
134 if builder.endswith('-Trybot'): | 183 if builder.endswith('-Trybot'): |
135 continue | 184 continue |
136 fullpath = os.path.join(dirpath, matching_filename) | 185 fullpath = os.path.join(dirpath, matching_filename) |
137 meta_dict[builder] = gm_json.LoadFromFile(fullpath) | 186 meta_dict[builder] = gm_json.LoadFromFile(fullpath) |
138 return meta_dict | 187 return meta_dict |
139 | 188 |
140 def _combine_actual_and_expected(self): | 189 @staticmethod |
141 """Gathers the results of all tests, across all builders (based on the | 190 def _write_dicts_to_root(meta_dict, root, pattern='*.json'): |
142 contents of self._actual_builder_dicts and self._expected_builder_dicts), | 191 """Write all per-builder dictionaries within meta_dict to files under |
| 192 the root path. |
| 193 |
| 194 Security note: this will only write to files that already exist within |
| 195 the root path (as found by os.walk() within root), so we don't need to |
| 196 worry about malformed content writing to disk outside of root. |
| 197 However, the data written to those files is not double-checked, so it |
| 198 could contain poisonous data. |
| 199 |
| 200 Args: |
| 201 meta_dict: a builder-keyed meta-dictionary containing all the JSON |
| 202 dictionaries we want to write out |
| 203 root: path to root of directory tree within which to write files |
| 204 pattern: which files to write within root (fnmatch-style pattern) |
| 205 |
| 206 Raises: |
| 207 IOError if root does not refer to an existing directory |
| 208 KeyError if the set of per-builder dictionaries written out was |
| 209 different than expected |
| 210 """ |
| 211 if not os.path.isdir(root): |
| 212 raise IOError('no directory found at path %s' % root) |
| 213 actual_builders_written = [] |
| 214 for dirpath, dirnames, filenames in os.walk(root): |
| 215 for matching_filename in fnmatch.filter(filenames, pattern): |
| 216 builder = os.path.basename(dirpath) |
| 217 # We should never encounter Trybot *expectations*, but if we are |
| 218 # writing into the actual-results dir, skip the Trybot actuals. |
| 219 # (I don't know why we would ever write into the actual-results dir, |
| 220 # though.) |
| 221 if builder.endswith('-Trybot'): |
| 222 continue |
| 223 per_builder_dict = meta_dict.get(builder) |
| 224 if per_builder_dict: |
| 225 fullpath = os.path.join(dirpath, matching_filename) |
| 226 gm_json.WriteToFile(per_builder_dict, fullpath) |
| 227 actual_builders_written.append(builder) |
| 228 |
| 229 # Check: did we write out the set of per-builder dictionaries we |
| 230 # expected to? |
| 231 expected_builders_written = sorted(meta_dict.keys()) |
| 232 actual_builders_written.sort() |
| 233 if expected_builders_written != actual_builders_written: |
| 234 raise KeyError( |
| 235 'expected to write dicts for builders %s, but actually wrote them ' |
| 236 'for builders %s' % ( |
| 237 expected_builders_written, actual_builders_written)) |
| 238 |
| 239 def _load_actual_and_expected(self): |
| 240 """Loads the results of all tests, across all builders (based on the |
| 241 files within self._actuals_root and self._expected_root), |
143 and stores them in self._results. | 242 and stores them in self._results. |
144 """ | 243 """ |
| 244 actual_builder_dicts = Results._read_dicts_from_root(self._actuals_root) |
| 245 expected_builder_dicts = Results._read_dicts_from_root(self._expected_root) |
| 246 |
145 categories_all = {} | 247 categories_all = {} |
146 categories_failures = {} | 248 categories_failures = {} |
147 Results._ensure_included_in_category_dict(categories_all, | 249 Results._ensure_included_in_category_dict(categories_all, |
148 'resultType', [ | 250 'resultType', [ |
149 gm_json.JSONKEY_ACTUALRESULTS_FAILED, | 251 gm_json.JSONKEY_ACTUALRESULTS_FAILED, |
150 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED, | 252 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED, |
151 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON, | 253 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON, |
152 gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED, | 254 gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED, |
153 ]) | 255 ]) |
154 Results._ensure_included_in_category_dict(categories_failures, | 256 Results._ensure_included_in_category_dict(categories_failures, |
155 'resultType', [ | 257 'resultType', [ |
156 gm_json.JSONKEY_ACTUALRESULTS_FAILED, | 258 gm_json.JSONKEY_ACTUALRESULTS_FAILED, |
157 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED, | 259 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED, |
158 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON, | 260 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON, |
159 ]) | 261 ]) |
160 | 262 |
161 data_all = [] | 263 data_all = [] |
162 data_failures = [] | 264 data_failures = [] |
163 for builder in sorted(self._actual_builder_dicts.keys()): | 265 for builder in sorted(actual_builder_dicts.keys()): |
164 actual_results_for_this_builder = ( | 266 actual_results_for_this_builder = ( |
165 self._actual_builder_dicts[builder][gm_json.JSONKEY_ACTUALRESULTS]) | 267 actual_builder_dicts[builder][gm_json.JSONKEY_ACTUALRESULTS]) |
166 for result_type in sorted(actual_results_for_this_builder.keys()): | 268 for result_type in sorted(actual_results_for_this_builder.keys()): |
167 results_of_this_type = actual_results_for_this_builder[result_type] | 269 results_of_this_type = actual_results_for_this_builder[result_type] |
168 if not results_of_this_type: | 270 if not results_of_this_type: |
169 continue | 271 continue |
170 for image_name in sorted(results_of_this_type.keys()): | 272 for image_name in sorted(results_of_this_type.keys()): |
171 actual_image = results_of_this_type[image_name] | 273 actual_image = results_of_this_type[image_name] |
172 try: | 274 try: |
173 # TODO(epoger): assumes a single allowed digest per test | 275 # TODO(epoger): assumes a single allowed digest per test |
174 expected_image = ( | 276 expected_image = ( |
175 self._expected_builder_dicts | 277 expected_builder_dicts |
176 [builder][gm_json.JSONKEY_EXPECTEDRESULTS] | 278 [builder][gm_json.JSONKEY_EXPECTEDRESULTS] |
177 [image_name][gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS] | 279 [image_name][gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS] |
178 [0]) | 280 [0]) |
179 except (KeyError, TypeError): | 281 except (KeyError, TypeError): |
180 # There are several cases in which we would expect to find | 282 # There are several cases in which we would expect to find |
181 # no expectations for a given test: | 283 # no expectations for a given test: |
182 # | 284 # |
183 # 1. result_type == NOCOMPARISON | 285 # 1. result_type == NOCOMPARISON |
184 # There are no expectations for this test yet! | 286 # There are no expectations for this test yet! |
185 # | 287 # |
(...skipping 105 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
291 category_dict: category dict-of-dicts to modify | 393 category_dict: category dict-of-dicts to modify |
292 category_name: category name, as a string | 394 category_name: category name, as a string |
293 category_values: list of values we want to make sure are represented | 395 category_values: list of values we want to make sure are represented |
294 for this category | 396 for this category |
295 """ | 397 """ |
296 if not category_dict.get(category_name): | 398 if not category_dict.get(category_name): |
297 category_dict[category_name] = {} | 399 category_dict[category_name] = {} |
298 for category_value in category_values: | 400 for category_value in category_values: |
299 if not category_dict[category_name].get(category_value): | 401 if not category_dict[category_name].get(category_value): |
300 category_dict[category_name][category_value] = 0 | 402 category_dict[category_name][category_value] = 0 |
OLD | NEW |