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 | |
epoger
2013/10/22 21:17:45
I think your fear is well-placed. I think we are
| |
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) |
134 if builder.endswith('-Trybot'): | 181 if builder.endswith('-Trybot'): |
135 continue | 182 continue |
136 fullpath = os.path.join(dirpath, matching_filename) | 183 fullpath = os.path.join(dirpath, matching_filename) |
137 meta_dict[builder] = gm_json.LoadFromFile(fullpath) | 184 meta_dict[builder] = gm_json.LoadFromFile(fullpath) |
138 return meta_dict | 185 return meta_dict |
139 | 186 |
140 def _combine_actual_and_expected(self): | 187 @staticmethod |
141 """Gathers the results of all tests, across all builders (based on the | 188 def _write_dicts_to_root(meta_dict, root, pattern='*.json'): |
142 contents of self._actual_builder_dicts and self._expected_builder_dicts), | 189 """Write all per-builder dictionaries within meta_dict to files under |
190 the root path. | |
191 | |
192 Security note: this will only write to files that already exist within | |
193 the root path (as found by os.walk() within root), so we don't need to | |
194 worry about malformed content writing to disk outside of root. | |
195 However, the data written to those files is not double-checked, so it | |
196 could contain poisonous data. | |
borenet
2013/10/23 14:22:03
Thanks! We can feel somewhat secure since we're us
| |
197 | |
198 Args: | |
199 meta_dict: a builder-keyed meta-dictionary containing all the JSON | |
200 dictionaries we want to write out | |
201 root: path to root of directory tree within which to write files | |
202 pattern: which files to write within root (fnmatch-style pattern) | |
203 | |
204 Raises: | |
205 IOError if root does not refer to an existing directory | |
206 KeyError if the set of per-builder dictionaries written out was | |
207 different than expected | |
208 """ | |
209 if not os.path.isdir(root): | |
210 raise IOError('no directory found at path %s' % root) | |
211 actual_builders_written = [] | |
212 for dirpath, dirnames, filenames in os.walk(root): | |
213 for matching_filename in fnmatch.filter(filenames, pattern): | |
214 builder = os.path.basename(dirpath) | |
215 if builder.endswith('-Trybot'): | |
216 continue | |
borenet
2013/10/23 14:22:03
We don't have any expectations with "-Trybot". Do
epoger
2013/10/23 14:42:14
Added some comments here and in _read_dicts_from_r
borenet
2013/10/23 14:47:34
Thanks!
| |
217 per_builder_dict = meta_dict.get(builder) | |
218 if per_builder_dict: | |
219 fullpath = os.path.join(dirpath, matching_filename) | |
220 gm_json.WriteToFile(per_builder_dict, fullpath) | |
221 actual_builders_written.append(builder) | |
222 | |
223 # Check: did we write out the set of per-builder dictionaries we | |
224 # expected to? | |
225 expected_builders_written = sorted(meta_dict.keys()) | |
226 actual_builders_written.sort() | |
227 if expected_builders_written != actual_builders_written: | |
228 raise KeyError( | |
229 'expected to write dicts for builders %s, but actually wrote them ' | |
230 'for builders %s' % ( | |
231 expected_builders_written, actual_builders_written)) | |
232 | |
233 def _load_actual_and_expected(self): | |
234 """Loads the results of all tests, across all builders (based on the | |
235 files within self._actuals_root and self._expected_root), | |
143 and stores them in self._results. | 236 and stores them in self._results. |
144 """ | 237 """ |
238 actual_builder_dicts = Results._read_dicts_from_root(self._actuals_root) | |
239 expected_builder_dicts = Results._read_dicts_from_root(self._expected_root) | |
240 | |
145 categories_all = {} | 241 categories_all = {} |
146 categories_failures = {} | 242 categories_failures = {} |
147 Results._ensure_included_in_category_dict(categories_all, | 243 Results._ensure_included_in_category_dict(categories_all, |
148 'resultType', [ | 244 'resultType', [ |
149 gm_json.JSONKEY_ACTUALRESULTS_FAILED, | 245 gm_json.JSONKEY_ACTUALRESULTS_FAILED, |
150 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED, | 246 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED, |
151 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON, | 247 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON, |
152 gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED, | 248 gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED, |
153 ]) | 249 ]) |
154 Results._ensure_included_in_category_dict(categories_failures, | 250 Results._ensure_included_in_category_dict(categories_failures, |
155 'resultType', [ | 251 'resultType', [ |
156 gm_json.JSONKEY_ACTUALRESULTS_FAILED, | 252 gm_json.JSONKEY_ACTUALRESULTS_FAILED, |
157 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED, | 253 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED, |
158 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON, | 254 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON, |
159 ]) | 255 ]) |
160 | 256 |
161 data_all = [] | 257 data_all = [] |
162 data_failures = [] | 258 data_failures = [] |
163 for builder in sorted(self._actual_builder_dicts.keys()): | 259 for builder in sorted(actual_builder_dicts.keys()): |
164 actual_results_for_this_builder = ( | 260 actual_results_for_this_builder = ( |
165 self._actual_builder_dicts[builder][gm_json.JSONKEY_ACTUALRESULTS]) | 261 actual_builder_dicts[builder][gm_json.JSONKEY_ACTUALRESULTS]) |
166 for result_type in sorted(actual_results_for_this_builder.keys()): | 262 for result_type in sorted(actual_results_for_this_builder.keys()): |
167 results_of_this_type = actual_results_for_this_builder[result_type] | 263 results_of_this_type = actual_results_for_this_builder[result_type] |
168 if not results_of_this_type: | 264 if not results_of_this_type: |
169 continue | 265 continue |
170 for image_name in sorted(results_of_this_type.keys()): | 266 for image_name in sorted(results_of_this_type.keys()): |
171 actual_image = results_of_this_type[image_name] | 267 actual_image = results_of_this_type[image_name] |
172 try: | 268 try: |
173 # TODO(epoger): assumes a single allowed digest per test | 269 # TODO(epoger): assumes a single allowed digest per test |
174 expected_image = ( | 270 expected_image = ( |
175 self._expected_builder_dicts | 271 expected_builder_dicts |
176 [builder][gm_json.JSONKEY_EXPECTEDRESULTS] | 272 [builder][gm_json.JSONKEY_EXPECTEDRESULTS] |
177 [image_name][gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS] | 273 [image_name][gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS] |
178 [0]) | 274 [0]) |
179 except (KeyError, TypeError): | 275 except (KeyError, TypeError): |
180 # There are several cases in which we would expect to find | 276 # There are several cases in which we would expect to find |
181 # no expectations for a given test: | 277 # no expectations for a given test: |
182 # | 278 # |
183 # 1. result_type == NOCOMPARISON | 279 # 1. result_type == NOCOMPARISON |
184 # There are no expectations for this test yet! | 280 # There are no expectations for this test yet! |
185 # | 281 # |
(...skipping 105 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
291 category_dict: category dict-of-dicts to modify | 387 category_dict: category dict-of-dicts to modify |
292 category_name: category name, as a string | 388 category_name: category name, as a string |
293 category_values: list of values we want to make sure are represented | 389 category_values: list of values we want to make sure are represented |
294 for this category | 390 for this category |
295 """ | 391 """ |
296 if not category_dict.get(category_name): | 392 if not category_dict.get(category_name): |
297 category_dict[category_name] = {} | 393 category_dict[category_name] = {} |
298 for category_value in category_values: | 394 for category_value in category_values: |
299 if not category_dict[category_name].get(category_value): | 395 if not category_dict[category_name].get(category_value): |
300 category_dict[category_name][category_value] = 0 | 396 category_dict[category_name][category_value] = 0 |
OLD | NEW |