| OLD | NEW |
| (Empty) |
| 1 # Copyright (c) 2009 The Chromium Authors. All rights reserved. | |
| 2 # Use of this source code is governed by a BSD-style license that can be | |
| 3 # found in the LICENSE file. | |
| 4 | |
| 5 import logging | |
| 6 import os | |
| 7 import subprocess | |
| 8 import sys | |
| 9 import time | |
| 10 import urllib2 | |
| 11 import xml.dom.minidom | |
| 12 | |
| 13 from layout_package import path_utils | |
| 14 from layout_package import test_expectations | |
| 15 | |
| 16 sys.path.append(path_utils.PathFromBase('third_party')) | |
| 17 import simplejson | |
| 18 | |
| 19 | |
| 20 class JSONResultsGenerator(object): | |
| 21 | |
| 22 MAX_NUMBER_OF_BUILD_RESULTS_TO_LOG = 750 | |
| 23 # Min time (seconds) that will be added to the JSON. | |
| 24 MIN_TIME = 1 | |
| 25 JSON_PREFIX = "ADD_RESULTS(" | |
| 26 JSON_SUFFIX = ");" | |
| 27 PASS_RESULT = "P" | |
| 28 SKIP_RESULT = "X" | |
| 29 NO_DATA_RESULT = "N" | |
| 30 VERSION = 3 | |
| 31 VERSION_KEY = "version" | |
| 32 RESULTS = "results" | |
| 33 TIMES = "times" | |
| 34 BUILD_NUMBERS = "buildNumbers" | |
| 35 WEBKIT_SVN = "webkitRevision" | |
| 36 CHROME_SVN = "chromeRevision" | |
| 37 TIME = "secondsSinceEpoch" | |
| 38 TESTS = "tests" | |
| 39 | |
| 40 FIXABLE_COUNT = "fixableCount" | |
| 41 FIXABLE = "fixableCounts" | |
| 42 ALL_FIXABLE_COUNT = "allFixableCount" | |
| 43 | |
| 44 # Note that we omit test_expectations.FAIL from this list because | |
| 45 # it should never show up (it's a legacy input expectation, never | |
| 46 # an output expectation). | |
| 47 FAILURE_TO_CHAR = {test_expectations.CRASH: "C", | |
| 48 test_expectations.TIMEOUT: "T", | |
| 49 test_expectations.IMAGE: "I", | |
| 50 test_expectations.TEXT: "F", | |
| 51 test_expectations.MISSING: "O", | |
| 52 test_expectations.IMAGE_PLUS_TEXT: "Z"} | |
| 53 FAILURE_CHARS = FAILURE_TO_CHAR.values() | |
| 54 | |
| 55 RESULTS_FILENAME = "results.json" | |
| 56 | |
| 57 def __init__(self, builder_name, build_name, build_number, | |
| 58 results_file_base_path, builder_base_url, | |
| 59 test_timings, failures, passed_tests, skipped_tests, all_tests): | |
| 60 """Modifies the results.json file. Grabs it off the archive directory | |
| 61 if it is not found locally. | |
| 62 | |
| 63 Args | |
| 64 builder_name: the builder name (e.g. Webkit). | |
| 65 build_name: the build name (e.g. webkit-rel). | |
| 66 build_number: the build number. | |
| 67 results_file_base_path: Absolute path to the directory containing the | |
| 68 results json file. | |
| 69 builder_base_url: the URL where we have the archived test results. | |
| 70 test_timings: Map of test name to a test_run-time. | |
| 71 failures: Map of test name to a failure type (of test_expectations). | |
| 72 passed_tests: A set containing all the passed tests. | |
| 73 skipped_tests: A set containing all the skipped tests. | |
| 74 all_tests: List of all the tests that were run. This should not | |
| 75 include skipped tests. | |
| 76 """ | |
| 77 self._builder_name = builder_name | |
| 78 self._build_name = build_name | |
| 79 self._build_number = build_number | |
| 80 self._builder_base_url = builder_base_url | |
| 81 self._results_file_path = os.path.join(results_file_base_path, | |
| 82 self.RESULTS_FILENAME) | |
| 83 self._test_timings = test_timings | |
| 84 self._failures = failures | |
| 85 self._passed_tests = passed_tests | |
| 86 self._skipped_tests = skipped_tests | |
| 87 self._all_tests = all_tests | |
| 88 | |
| 89 self._GenerateJSONOutput() | |
| 90 | |
| 91 def _GenerateJSONOutput(self): | |
| 92 """Generates the JSON output file.""" | |
| 93 json = self._GetJSON() | |
| 94 if json: | |
| 95 results_file = open(self._results_file_path, "w") | |
| 96 results_file.write(json) | |
| 97 results_file.close() | |
| 98 | |
| 99 def _GetSVNRevision(self, in_directory=None): | |
| 100 """Returns the svn revision for the given directory. | |
| 101 | |
| 102 Args: | |
| 103 in_directory: The directory where svn is to be run. | |
| 104 """ | |
| 105 output = subprocess.Popen(["svn", "info", "--xml"], | |
| 106 cwd=in_directory, | |
| 107 shell=(sys.platform == 'win32'), | |
| 108 stdout=subprocess.PIPE).communicate()[0] | |
| 109 try: | |
| 110 dom = xml.dom.minidom.parseString(output) | |
| 111 return dom.getElementsByTagName('entry')[0].getAttribute( | |
| 112 'revision') | |
| 113 except xml.parsers.expat.ExpatError: | |
| 114 return "" | |
| 115 | |
| 116 def _GetArchivedJSONResults(self): | |
| 117 """Reads old results JSON file if it exists. | |
| 118 Returns (archived_results, error) tuple where error is None if results | |
| 119 were successfully read. | |
| 120 """ | |
| 121 results_json = {} | |
| 122 old_results = None | |
| 123 error = None | |
| 124 | |
| 125 if os.path.exists(self._results_file_path): | |
| 126 old_results_file = open(self._results_file_path, "r") | |
| 127 old_results = old_results_file.read() | |
| 128 elif self._builder_base_url: | |
| 129 # Check if we have the archived JSON file on the buildbot server. | |
| 130 results_file_url = (self._builder_base_url + | |
| 131 self._build_name + "/" + self.RESULTS_FILENAME) | |
| 132 logging.error("Local results.json file does not exist. Grabbing " | |
| 133 "it off the archive at " + results_file_url) | |
| 134 | |
| 135 try: | |
| 136 results_file = urllib2.urlopen(results_file_url) | |
| 137 info = results_file.info() | |
| 138 old_results = results_file.read() | |
| 139 except urllib2.HTTPError, http_error: | |
| 140 # A non-4xx status code means the bot is hosed for some reason | |
| 141 # and we can't grab the results.json file off of it. | |
| 142 if (http_error.code < 400 and http_error.code >= 500): | |
| 143 error = http_error | |
| 144 except urllib2.URLError, url_error: | |
| 145 error = url_error | |
| 146 | |
| 147 if old_results: | |
| 148 # Strip the prefix and suffix so we can get the actual JSON object. | |
| 149 old_results = old_results[len(self.JSON_PREFIX): | |
| 150 len(old_results) - len(self.JSON_SUFFIX)] | |
| 151 | |
| 152 try: | |
| 153 results_json = simplejson.loads(old_results) | |
| 154 except: | |
| 155 logging.debug("results.json was not valid JSON. Clobbering.") | |
| 156 # The JSON file is not valid JSON. Just clobber the results. | |
| 157 results_json = {} | |
| 158 else: | |
| 159 logging.debug('Old JSON results do not exist. Starting fresh.') | |
| 160 results_json = {} | |
| 161 | |
| 162 return results_json, error | |
| 163 | |
| 164 def _GetJSON(self): | |
| 165 """Gets the results for the results.json file.""" | |
| 166 results_json, error = self._GetArchivedJSONResults() | |
| 167 if error: | |
| 168 # If there was an error don't write a results.json | |
| 169 # file at all as it would lose all the information on the bot. | |
| 170 logging.error("Archive directory is inaccessible. Not modifying " | |
| 171 "or clobbering the results.json file: " + str(error)) | |
| 172 return None | |
| 173 | |
| 174 builder_name = self._builder_name | |
| 175 if results_json and builder_name not in results_json: | |
| 176 logging.debug("Builder name (%s) is not in the results.json file." | |
| 177 % builder_name) | |
| 178 | |
| 179 self._ConvertJSONToCurrentVersion(results_json) | |
| 180 | |
| 181 if builder_name not in results_json: | |
| 182 results_json[builder_name] = self._CreateResultsForBuilderJSON() | |
| 183 | |
| 184 results_for_builder = results_json[builder_name] | |
| 185 | |
| 186 self._InsertGenericMetadata(results_for_builder) | |
| 187 | |
| 188 self._InsertFailureSummaries(results_for_builder) | |
| 189 | |
| 190 # Update the all failing tests with result type and time. | |
| 191 tests = results_for_builder[self.TESTS] | |
| 192 all_failing_tests = set(self._failures.iterkeys()) | |
| 193 all_failing_tests.update(tests.iterkeys()) | |
| 194 for test in all_failing_tests: | |
| 195 self._InsertTestTimeAndResult(test, tests) | |
| 196 | |
| 197 # Specify separators in order to get compact encoding. | |
| 198 results_str = simplejson.dumps(results_json, separators=(',', ':')) | |
| 199 return self.JSON_PREFIX + results_str + self.JSON_SUFFIX | |
| 200 | |
| 201 def _InsertFailureSummaries(self, results_for_builder): | |
| 202 """Inserts aggregate pass/failure statistics into the JSON. | |
| 203 This method reads self._skipped_tests, self._passed_tests and | |
| 204 self._failures and inserts FIXABLE, FIXABLE_COUNT and ALL_FIXABLE_COUNT | |
| 205 entries. | |
| 206 | |
| 207 Args: | |
| 208 results_for_builder: Dictionary containing the test results for a | |
| 209 single builder. | |
| 210 """ | |
| 211 # Insert the number of tests that failed. | |
| 212 self._InsertItemIntoRawList(results_for_builder, | |
| 213 len(set(self._failures.keys()) | self._skipped_tests), | |
| 214 self.FIXABLE_COUNT) | |
| 215 | |
| 216 # Create a pass/skip/failure summary dictionary. | |
| 217 entry = {} | |
| 218 entry[self.SKIP_RESULT] = len(self._skipped_tests) | |
| 219 entry[self.PASS_RESULT] = len(self._passed_tests) | |
| 220 get = entry.get | |
| 221 for failure_type in self._failures.values(): | |
| 222 failure_char = self.FAILURE_TO_CHAR[failure_type] | |
| 223 entry[failure_char] = get(failure_char, 0) + 1 | |
| 224 | |
| 225 # Insert the pass/skip/failure summary dictionary. | |
| 226 self._InsertItemIntoRawList(results_for_builder, entry, self.FIXABLE) | |
| 227 | |
| 228 # Insert the number of all the tests that are supposed to pass. | |
| 229 self._InsertItemIntoRawList(results_for_builder, | |
| 230 len(self._skipped_tests | self._all_tests), | |
| 231 self.ALL_FIXABLE_COUNT) | |
| 232 | |
| 233 def _InsertItemIntoRawList(self, results_for_builder, item, key): | |
| 234 """Inserts the item into the list with the given key in the results for | |
| 235 this builder. Creates the list if no such list exists. | |
| 236 | |
| 237 Args: | |
| 238 results_for_builder: Dictionary containing the test results for a | |
| 239 single builder. | |
| 240 item: Number or string to insert into the list. | |
| 241 key: Key in results_for_builder for the list to insert into. | |
| 242 """ | |
| 243 if key in results_for_builder: | |
| 244 raw_list = results_for_builder[key] | |
| 245 else: | |
| 246 raw_list = [] | |
| 247 | |
| 248 raw_list.insert(0, item) | |
| 249 raw_list = raw_list[:self.MAX_NUMBER_OF_BUILD_RESULTS_TO_LOG] | |
| 250 results_for_builder[key] = raw_list | |
| 251 | |
| 252 def _InsertItemRunLengthEncoded(self, item, encoded_results): | |
| 253 """Inserts the item into the run-length encoded results. | |
| 254 | |
| 255 Args: | |
| 256 item: String or number to insert. | |
| 257 encoded_results: run-length encoded results. An array of arrays, e.g. | |
| 258 [[3,'A'],[1,'Q']] encodes AAAQ. | |
| 259 """ | |
| 260 if len(encoded_results) and item == encoded_results[0][1]: | |
| 261 num_results = encoded_results[0][0] | |
| 262 if num_results <= self.MAX_NUMBER_OF_BUILD_RESULTS_TO_LOG: | |
| 263 encoded_results[0][0] = num_results + 1 | |
| 264 else: | |
| 265 # Use a list instead of a class for the run-length encoding since | |
| 266 # we want the serialized form to be concise. | |
| 267 encoded_results.insert(0, [1, item]) | |
| 268 | |
| 269 def _InsertGenericMetadata(self, results_for_builder): | |
| 270 """ Inserts generic metadata (such as version number, current time etc) | |
| 271 into the JSON. | |
| 272 | |
| 273 Args: | |
| 274 results_for_builder: Dictionary containing the test results for | |
| 275 a single builder. | |
| 276 """ | |
| 277 self._InsertItemIntoRawList(results_for_builder, | |
| 278 self._build_number, self.BUILD_NUMBERS) | |
| 279 | |
| 280 path_to_webkit = path_utils.PathFromBase('third_party', 'WebKit', | |
| 281 'WebCore') | |
| 282 self._InsertItemIntoRawList(results_for_builder, | |
| 283 self._GetSVNRevision(path_to_webkit), | |
| 284 self.WEBKIT_SVN) | |
| 285 | |
| 286 path_to_chrome_base = path_utils.PathFromBase() | |
| 287 self._InsertItemIntoRawList(results_for_builder, | |
| 288 self._GetSVNRevision(path_to_chrome_base), | |
| 289 self.CHROME_SVN) | |
| 290 | |
| 291 self._InsertItemIntoRawList(results_for_builder, | |
| 292 int(time.time()), | |
| 293 self.TIME) | |
| 294 | |
| 295 def _InsertTestTimeAndResult(self, test_name, tests): | |
| 296 """ Insert a test item with its results to the given tests dictionary. | |
| 297 | |
| 298 Args: | |
| 299 tests: Dictionary containing test result entries. | |
| 300 """ | |
| 301 | |
| 302 result = JSONResultsGenerator.PASS_RESULT | |
| 303 time = 0 | |
| 304 | |
| 305 if test_name not in self._all_tests: | |
| 306 result = JSONResultsGenerator.NO_DATA_RESULT | |
| 307 | |
| 308 if test_name in self._failures: | |
| 309 result = self.FAILURE_TO_CHAR[self._failures[test_name]] | |
| 310 | |
| 311 if test_name in self._test_timings: | |
| 312 # Floor for now to get time in seconds. | |
| 313 time = int(self._test_timings[test_name]) | |
| 314 | |
| 315 if test_name not in tests: | |
| 316 tests[test_name] = self._CreateResultsAndTimesJSON() | |
| 317 | |
| 318 thisTest = tests[test_name] | |
| 319 self._InsertItemRunLengthEncoded(result, thisTest[self.RESULTS]) | |
| 320 self._InsertItemRunLengthEncoded(time, thisTest[self.TIMES]) | |
| 321 self._NormalizeResultsJSON(thisTest, test_name, tests) | |
| 322 | |
| 323 def _ConvertJSONToCurrentVersion(self, results_json): | |
| 324 """If the JSON does not match the current version, converts it to the | |
| 325 current version and adds in the new version number. | |
| 326 """ | |
| 327 if (self.VERSION_KEY in results_json and | |
| 328 results_json[self.VERSION_KEY] == self.VERSION): | |
| 329 return | |
| 330 | |
| 331 results_json[self.VERSION_KEY] = self.VERSION | |
| 332 | |
| 333 def _CreateResultsAndTimesJSON(self): | |
| 334 results_and_times = {} | |
| 335 results_and_times[self.RESULTS] = [] | |
| 336 results_and_times[self.TIMES] = [] | |
| 337 return results_and_times | |
| 338 | |
| 339 def _CreateResultsForBuilderJSON(self): | |
| 340 results_for_builder = {} | |
| 341 results_for_builder[self.TESTS] = {} | |
| 342 return results_for_builder | |
| 343 | |
| 344 def _RemoveItemsOverMaxNumberOfBuilds(self, encoded_list): | |
| 345 """Removes items from the run-length encoded list after the final | |
| 346 item that exceeds the max number of builds to track. | |
| 347 | |
| 348 Args: | |
| 349 encoded_results: run-length encoded results. An array of arrays, e.g. | |
| 350 [[3,'A'],[1,'Q']] encodes AAAQ. | |
| 351 """ | |
| 352 num_builds = 0 | |
| 353 index = 0 | |
| 354 for result in encoded_list: | |
| 355 num_builds = num_builds + result[0] | |
| 356 index = index + 1 | |
| 357 if num_builds > self.MAX_NUMBER_OF_BUILD_RESULTS_TO_LOG: | |
| 358 return encoded_list[:index] | |
| 359 return encoded_list | |
| 360 | |
| 361 def _NormalizeResultsJSON(self, test, test_name, tests): | |
| 362 """ Prune tests where all runs pass or tests that no longer exist and | |
| 363 truncate all results to maxNumberOfBuilds. | |
| 364 | |
| 365 Args: | |
| 366 test: ResultsAndTimes object for this test. | |
| 367 test_name: Name of the test. | |
| 368 tests: The JSON object with all the test results for this builder. | |
| 369 """ | |
| 370 test[self.RESULTS] = self._RemoveItemsOverMaxNumberOfBuilds( | |
| 371 test[self.RESULTS]) | |
| 372 test[self.TIMES] = self._RemoveItemsOverMaxNumberOfBuilds( | |
| 373 test[self.TIMES]) | |
| 374 | |
| 375 is_all_pass = self._IsResultsAllOfType(test[self.RESULTS], | |
| 376 self.PASS_RESULT) | |
| 377 is_all_no_data = self._IsResultsAllOfType(test[self.RESULTS], | |
| 378 self.NO_DATA_RESULT) | |
| 379 max_time = max([time[1] for time in test[self.TIMES]]) | |
| 380 | |
| 381 # Remove all passes/no-data from the results to reduce noise and | |
| 382 # filesize. If a test passes every run, but takes > MIN_TIME to run, | |
| 383 # don't throw away the data. | |
| 384 if is_all_no_data or (is_all_pass and max_time <= self.MIN_TIME): | |
| 385 del tests[test_name] | |
| 386 | |
| 387 def _IsResultsAllOfType(self, results, type): | |
| 388 """Returns whether all the results are of the given type | |
| 389 (e.g. all passes).""" | |
| 390 return len(results) == 1 and results[0][1] == type | |
| OLD | NEW |