Chromium Code Reviews| 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 |