Changeset 62989 in webkit
- Timestamp:
- Jul 9, 2010 1:38:28 PM (14 years ago)
- Location:
- trunk/WebKitTools
- Files:
-
- 1 added
- 3 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/WebKitTools/ChangeLog
r62983 r62989 1 2010-07-08 Kinuko Yasuda <kinuko@chromium.org> 2 3 Reviewed by Ojan Vafai. 4 5 cleanup json_results_generator dependencies so that non-layout-tests can also use it safely 6 https://bugs.webkit.org/show_bug.cgi?id=38693 7 8 Introduced a new base class JSONResultsGeneratorBase that doesn't 9 have any dependency on layout_tests packages. 10 Turned JSONResultsGenerator into a wrapper class of the base class 11 so that the old code can work with it during the cleanup. 12 13 Added json_results_generator_unittest.py. 14 15 * Scripts/webkitpy/layout_tests/layout_package/json_layout_results_generator.py: 16 * Scripts/webkitpy/layout_tests/layout_package/json_results_generator.py: 17 * Scripts/webkitpy/layout_tests/layout_package/json_results_generator_unittest.py: Added 18 1 19 2010-07-09 Abhishek Arya <inferno@chromium.org> 2 20 -
trunk/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/json_layout_results_generator.py
r60690 r62989 1 #!/usr/bin/env python2 1 # Copyright (C) 2010 Google Inc. All rights reserved. 3 2 # … … 36 35 import webkitpy.thirdparty.simplejson as simplejson 37 36 38 class JSONLayoutResultsGenerator(json_results_generator.JSONResultsGenerator): 37 38 class JSONLayoutResultsGenerator(json_results_generator.JSONResultsGeneratorBase): 39 39 """A JSON results generator for layout tests.""" 40 40 … … 44 44 WONTFIX = "wontfixCounts" 45 45 DEFERRED = "deferredCounts" 46 47 # Note that we omit test_expectations.FAIL from this list because 48 # it should never show up (it's a legacy input expectation, never 49 # an output expectation). 50 FAILURE_TO_CHAR = {test_expectations.CRASH: "C", 51 test_expectations.TIMEOUT: "T", 52 test_expectations.IMAGE: "I", 53 test_expectations.TEXT: "F", 54 test_expectations.MISSING: "O", 55 test_expectations.IMAGE_PLUS_TEXT: "Z"} 46 56 47 57 def __init__(self, port, builder_name, build_name, build_number, … … 54 64 result_summary: ResultsSummary object storing the summary of the test 55 65 results. 56 (see the comment of JSONResultsGenerator.__init__ for other Args)57 66 """ 67 super(JSONLayoutResultsGenerator, self).__init__( 68 builder_name, build_name, build_number, results_file_base_path, 69 builder_base_url, {}, port.test_repository_paths()) 70 58 71 self._port = port 59 self._builder_name = builder_name60 self._build_name = build_name61 self._build_number = build_number62 self._builder_base_url = builder_base_url63 self._results_file_path = os.path.join(results_file_base_path,64 self.RESULTS_FILENAME)65 72 self._expectations = expectations 66 67 # We don't use self._skipped_tests and self._passed_tests as we68 # override _InsertFailureSummaries.69 73 70 74 # We want relative paths to LayoutTest root for JSON output. … … 78 82 (path_to_name(test_tuple.filename), test_tuple.test_run_time) 79 83 for test_tuple in test_timings) 80 self._svn_repositories = port.test_repository_paths() 81 82 self._generate_json_output() 84 85 self.generate_json_output() 83 86 84 87 def _get_path_relative_to_layout_test_root(self, test): … … 101 104 # Make sure all paths are unix-style. 102 105 return relativePath.replace('\\', '/') 106 107 # override 108 def _get_test_timing(self, test_name): 109 if test_name in self._test_timings: 110 # Floor for now to get time in seconds. 111 return int(self._test_timings[test_name]) 112 return 0 113 114 # override 115 def _get_failed_test_names(self): 116 return set(self._failures.keys()) 117 118 # override 119 def _get_result_type_char(self, test_name): 120 if test_name not in self._all_tests: 121 return self.NO_DATA_RESULT 122 123 if test_name in self._failures: 124 return self.FAILURE_TO_CHAR[self._failures[test_name]] 125 126 return self.PASS_RESULT 103 127 104 128 # override -
trunk/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/json_results_generator.py
r60714 r62989 1 #!/usr/bin/env python2 1 # Copyright (C) 2010 Google Inc. All rights reserved. 3 2 # … … 39 38 import xml.dom.minidom 40 39 41 from webkitpy.common.checkout import scm42 from webkitpy.common.system.executive import ScriptError43 from webkitpy.layout_tests.layout_package import test_expectations44 40 import webkitpy.thirdparty.simplejson as simplejson 45 41 46 _log = logging.getLogger("webkitpy.layout_tests.layout_package." 47 "json_results_generator") 48 49 50 class JSONResultsGenerator(object): 42 # A JSON results generator for generic tests. 43 # FIXME: move this code out of the layout_package directory. 44 45 _log = logging.getLogger("webkitpy.layout_tests.layout_package.json_results_generator") 46 47 48 class TestResult(object): 49 """A simple class that represents a single test result.""" 50 def __init__(self, name, failed=False, skipped=False, elapsed_time=0): 51 self.name = name 52 self.failed = failed 53 self.skipped = skipped 54 self.time = elapsed_time 55 56 def fixable(self): 57 return self.failed or self.skipped 58 59 60 class JSONResultsGeneratorBase(object): 51 61 """A JSON results generator for generic tests.""" 52 62 … … 58 68 PASS_RESULT = "P" 59 69 SKIP_RESULT = "X" 70 FAIL_RESULT = "F" 60 71 NO_DATA_RESULT = "N" 61 72 VERSION = 3 … … 71 82 ALL_FIXABLE_COUNT = "allFixableCount" 72 83 73 # Note that we omit test_expectations.FAIL from this list because74 # it should never show up (it's a legacy input expectation, never75 # an output expectation).76 FAILURE_TO_CHAR = {test_expectations.CRASH: "C",77 test_expectations.TIMEOUT: "T",78 test_expectations.IMAGE: "I",79 test_expectations.TEXT: "F",80 test_expectations.MISSING: "O",81 test_expectations.IMAGE_PLUS_TEXT: "Z"}82 FAILURE_CHARS = FAILURE_TO_CHAR.values()83 84 84 RESULTS_FILENAME = "results.json" 85 86 def __init__(self, builder_name, build_name, build_number, 87 results_file_base_path, builder_base_url, 88 test_results_map, svn_repositories=None): 89 """Modifies the results.json file. Grabs it off the archive directory 90 if it is not found locally. 91 92 Args 93 builder_name: the builder name (e.g. Webkit). 94 build_name: the build name (e.g. webkit-rel). 95 build_number: the build number. 96 results_file_base_path: Absolute path to the directory containing the 97 results json file. 98 builder_base_url: the URL where we have the archived test results. 99 If this is None no archived results will be retrieved. 100 test_results_map: A dictionary that maps test_name to TestResult. 101 svn_repositories: A (json_field_name, svn_path) pair for SVN 102 repositories that tests rely on. The SVN revision will be 103 included in the JSON with the given json_field_name. 104 """ 105 self._builder_name = builder_name 106 self._build_name = build_name 107 self._build_number = build_number 108 self._builder_base_url = builder_base_url 109 self._results_file_path = os.path.join(results_file_base_path, 110 self.RESULTS_FILENAME) 111 112 self._test_results_map = test_results_map 113 self._test_results = test_results_map.values() 114 115 self._svn_repositories = svn_repositories 116 if not self._svn_repositories: 117 self._svn_repositories = {} 118 119 self._json = None 120 121 def generate_json_output(self): 122 """Generates the JSON output file.""" 123 if not self._json: 124 self._json = self.get_json() 125 if self._json: 126 # Specify separators in order to get compact encoding. 127 json_data = simplejson.dumps(self._json, separators=(',', ':')) 128 json_string = self.JSON_PREFIX + json_data + self.JSON_SUFFIX 129 130 results_file = codecs.open(self._results_file_path, "w", "utf-8") 131 results_file.write(json_string) 132 results_file.close() 133 134 def get_json(self): 135 """Gets the results for the results.json file.""" 136 if self._json: 137 return self._json 138 139 results_json, error = self._get_archived_json_results() 140 if error: 141 # If there was an error don't write a results.json 142 # file at all as it would lose all the information on the bot. 143 _log.error("Archive directory is inaccessible. Not modifying " 144 "or clobbering the results.json file: " + str(error)) 145 return None 146 147 builder_name = self._builder_name 148 if results_json and builder_name not in results_json: 149 _log.debug("Builder name (%s) is not in the results.json file." 150 % builder_name) 151 152 self._convert_json_to_current_version(results_json) 153 154 if builder_name not in results_json: 155 results_json[builder_name] = ( 156 self._create_results_for_builder_json()) 157 158 results_for_builder = results_json[builder_name] 159 160 self._insert_generic_metadata(results_for_builder) 161 162 self._insert_failure_summaries(results_for_builder) 163 164 # Update the all failing tests with result type and time. 165 tests = results_for_builder[self.TESTS] 166 all_failing_tests = self._get_failed_test_names() 167 all_failing_tests.update(tests.iterkeys()) 168 for test in all_failing_tests: 169 self._insert_test_time_and_result(test, tests) 170 171 self._json = results_json 172 return self._json 173 174 def _get_test_timing(self, test_name): 175 """Returns test timing data (elapsed time) in second 176 for the given test_name.""" 177 if test_name in self._test_results_map: 178 # Floor for now to get time in seconds. 179 return int(self._test_results_map[test_name].time) 180 return 0 181 182 def _get_failed_test_names(self): 183 """Returns a set of failed test names.""" 184 return set([r.name for r in self._test_results if r.failed]) 185 186 def _get_result_type_char(self, test_name): 187 """Returns a single char (e.g. SKIP_RESULT, FAIL_RESULT, 188 PASS_RESULT, NO_DATA_RESULT, etc) that indicates the test result 189 for the given test_name. 190 """ 191 if test_name not in self._test_results_map: 192 return JSONResultsGenerator.NO_DATA_RESULT 193 194 test_result = self._test_results_map[test_name] 195 if test_result.skipped: 196 return JSONResultsGenerator.SKIP_RESULT 197 if test_result.failed: 198 return JSONResultsGenerator.FAIL_RESULT 199 200 return JSONResultsGenerator.PASS_RESULT 201 202 # FIXME: Callers should use scm.py instead. 203 # FIXME: Identify and fix the run-time errors that were observed on Windows 204 # chromium buildbot when we had updated this code to use scm.py once before. 205 def _get_svn_revision(self, in_directory): 206 """Returns the svn revision for the given directory. 207 208 Args: 209 in_directory: The directory where svn is to be run. 210 """ 211 if os.path.exists(os.path.join(in_directory, '.svn')): 212 # Note: Not thread safe: http://bugs.python.org/issue2320 213 output = subprocess.Popen(["svn", "info", "--xml"], 214 cwd=in_directory, 215 shell=(sys.platform == 'win32'), 216 stdout=subprocess.PIPE).communicate()[0] 217 try: 218 dom = xml.dom.minidom.parseString(output) 219 return dom.getElementsByTagName('entry')[0].getAttribute( 220 'revision') 221 except xml.parsers.expat.ExpatError: 222 return "" 223 return "" 224 225 def _get_archived_json_results(self): 226 """Reads old results JSON file if it exists. 227 Returns (archived_results, error) tuple where error is None if results 228 were successfully read. 229 """ 230 results_json = {} 231 old_results = None 232 error = None 233 234 if os.path.exists(self._results_file_path): 235 with codecs.open(self._results_file_path, "r", "utf-8") as file: 236 old_results = file.read() 237 elif self._builder_base_url: 238 # Check if we have the archived JSON file on the buildbot server. 239 results_file_url = (self._builder_base_url + 240 self._build_name + "/" + self.RESULTS_FILENAME) 241 _log.error("Local results.json file does not exist. Grabbing " 242 "it off the archive at " + results_file_url) 243 244 try: 245 results_file = urllib2.urlopen(results_file_url) 246 info = results_file.info() 247 old_results = results_file.read() 248 except urllib2.HTTPError, http_error: 249 # A non-4xx status code means the bot is hosed for some reason 250 # and we can't grab the results.json file off of it. 251 if (http_error.code < 400 and http_error.code >= 500): 252 error = http_error 253 except urllib2.URLError, url_error: 254 error = url_error 255 256 if old_results: 257 # Strip the prefix and suffix so we can get the actual JSON object. 258 old_results = old_results[len(self.JSON_PREFIX): 259 len(old_results) - len(self.JSON_SUFFIX)] 260 261 try: 262 results_json = simplejson.loads(old_results) 263 except: 264 _log.debug("results.json was not valid JSON. Clobbering.") 265 # The JSON file is not valid JSON. Just clobber the results. 266 results_json = {} 267 else: 268 _log.debug('Old JSON results do not exist. Starting fresh.') 269 results_json = {} 270 271 return results_json, error 272 273 def _insert_failure_summaries(self, results_for_builder): 274 """Inserts aggregate pass/failure statistics into the JSON. 275 This method reads self._test_results and generates 276 FIXABLE, FIXABLE_COUNT and ALL_FIXABLE_COUNT entries. 277 278 Args: 279 results_for_builder: Dictionary containing the test results for a 280 single builder. 281 """ 282 # Insert the number of tests that failed or skipped. 283 fixable_count = len([r for r in self._test_results if r.fixable()]) 284 self._insert_item_into_raw_list(results_for_builder, 285 fixable_count, self.FIXABLE_COUNT) 286 287 # Create a pass/skip/failure summary dictionary. 288 entry = {} 289 for test_name in self._test_results_map.iterkeys(): 290 result_char = self._get_result_type_char(test_name) 291 entry[result_char] = entry.get(result_char, 0) + 1 292 293 # Insert the pass/skip/failure summary dictionary. 294 self._insert_item_into_raw_list(results_for_builder, entry, 295 self.FIXABLE) 296 297 # Insert the number of all the tests that are supposed to pass. 298 all_test_count = len(self._test_results) 299 self._insert_item_into_raw_list(results_for_builder, 300 all_test_count, self.ALL_FIXABLE_COUNT) 301 302 def _insert_item_into_raw_list(self, results_for_builder, item, key): 303 """Inserts the item into the list with the given key in the results for 304 this builder. Creates the list if no such list exists. 305 306 Args: 307 results_for_builder: Dictionary containing the test results for a 308 single builder. 309 item: Number or string to insert into the list. 310 key: Key in results_for_builder for the list to insert into. 311 """ 312 if key in results_for_builder: 313 raw_list = results_for_builder[key] 314 else: 315 raw_list = [] 316 317 raw_list.insert(0, item) 318 raw_list = raw_list[:self.MAX_NUMBER_OF_BUILD_RESULTS_TO_LOG] 319 results_for_builder[key] = raw_list 320 321 def _insert_item_run_length_encoded(self, item, encoded_results): 322 """Inserts the item into the run-length encoded results. 323 324 Args: 325 item: String or number to insert. 326 encoded_results: run-length encoded results. An array of arrays, e.g. 327 [[3,'A'],[1,'Q']] encodes AAAQ. 328 """ 329 if len(encoded_results) and item == encoded_results[0][1]: 330 num_results = encoded_results[0][0] 331 if num_results <= self.MAX_NUMBER_OF_BUILD_RESULTS_TO_LOG: 332 encoded_results[0][0] = num_results + 1 333 else: 334 # Use a list instead of a class for the run-length encoding since 335 # we want the serialized form to be concise. 336 encoded_results.insert(0, [1, item]) 337 338 def _insert_generic_metadata(self, results_for_builder): 339 """ Inserts generic metadata (such as version number, current time etc) 340 into the JSON. 341 342 Args: 343 results_for_builder: Dictionary containing the test results for 344 a single builder. 345 """ 346 self._insert_item_into_raw_list(results_for_builder, 347 self._build_number, self.BUILD_NUMBERS) 348 349 # Include SVN revisions for the given repositories. 350 for (name, path) in self._svn_repositories: 351 self._insert_item_into_raw_list(results_for_builder, 352 self._get_svn_revision(path), 353 name + 'Revision') 354 355 self._insert_item_into_raw_list(results_for_builder, 356 int(time.time()), 357 self.TIME) 358 359 def _insert_test_time_and_result(self, test_name, tests): 360 """ Insert a test item with its results to the given tests dictionary. 361 362 Args: 363 tests: Dictionary containing test result entries. 364 """ 365 366 result = self._get_result_type_char(test_name) 367 time = self._get_test_timing(test_name) 368 369 if test_name not in tests: 370 tests[test_name] = self._create_results_and_times_json() 371 372 thisTest = tests[test_name] 373 self._insert_item_run_length_encoded(result, thisTest[self.RESULTS]) 374 self._insert_item_run_length_encoded(time, thisTest[self.TIMES]) 375 self._normalize_results_json(thisTest, test_name, tests) 376 377 def _convert_json_to_current_version(self, results_json): 378 """If the JSON does not match the current version, converts it to the 379 current version and adds in the new version number. 380 """ 381 if (self.VERSION_KEY in results_json and 382 results_json[self.VERSION_KEY] == self.VERSION): 383 return 384 385 results_json[self.VERSION_KEY] = self.VERSION 386 387 def _create_results_and_times_json(self): 388 results_and_times = {} 389 results_and_times[self.RESULTS] = [] 390 results_and_times[self.TIMES] = [] 391 return results_and_times 392 393 def _create_results_for_builder_json(self): 394 results_for_builder = {} 395 results_for_builder[self.TESTS] = {} 396 return results_for_builder 397 398 def _remove_items_over_max_number_of_builds(self, encoded_list): 399 """Removes items from the run-length encoded list after the final 400 item that exceeds the max number of builds to track. 401 402 Args: 403 encoded_results: run-length encoded results. An array of arrays, e.g. 404 [[3,'A'],[1,'Q']] encodes AAAQ. 405 """ 406 num_builds = 0 407 index = 0 408 for result in encoded_list: 409 num_builds = num_builds + result[0] 410 index = index + 1 411 if num_builds > self.MAX_NUMBER_OF_BUILD_RESULTS_TO_LOG: 412 return encoded_list[:index] 413 return encoded_list 414 415 def _normalize_results_json(self, test, test_name, tests): 416 """ Prune tests where all runs pass or tests that no longer exist and 417 truncate all results to maxNumberOfBuilds. 418 419 Args: 420 test: ResultsAndTimes object for this test. 421 test_name: Name of the test. 422 tests: The JSON object with all the test results for this builder. 423 """ 424 test[self.RESULTS] = self._remove_items_over_max_number_of_builds( 425 test[self.RESULTS]) 426 test[self.TIMES] = self._remove_items_over_max_number_of_builds( 427 test[self.TIMES]) 428 429 is_all_pass = self._is_results_all_of_type(test[self.RESULTS], 430 self.PASS_RESULT) 431 is_all_no_data = self._is_results_all_of_type(test[self.RESULTS], 432 self.NO_DATA_RESULT) 433 max_time = max([time[1] for time in test[self.TIMES]]) 434 435 # Remove all passes/no-data from the results to reduce noise and 436 # filesize. If a test passes every run, but takes > MIN_TIME to run, 437 # don't throw away the data. 438 if is_all_no_data or (is_all_pass and max_time <= self.MIN_TIME): 439 del tests[test_name] 440 441 def _is_results_all_of_type(self, results, type): 442 """Returns whether all the results are of the given type 443 (e.g. all passes).""" 444 return len(results) == 1 and results[0][1] == type 445 446 447 # A wrapper class for JSONResultsGeneratorBase. 448 # Note: There's a script outside the WebKit codebase calling this script. 449 # FIXME: Please keep the interface until the other script is cleaned up. 450 # (http://src.chromium.org/viewvc/chrome/trunk/src/webkit/tools/layout_tests/webkitpy/layout_tests/test_output_xml_to_json.py?view=markup) 451 class JSONResultsGenerator(JSONResultsGeneratorBase): 452 # The flag is for backward compatibility. 453 output_json_in_init = True 85 454 86 455 def __init__(self, port, builder_name, build_name, build_number, 87 456 results_file_base_path, builder_base_url, 88 457 test_timings, failures, passed_tests, skipped_tests, all_tests): 89 """Modifies the results.json file. Grabs it off the archive directory 90 if it is not found locally. 458 """Generates a JSON results file. 91 459 92 460 Args … … 104 472 include skipped tests. 105 473 """ 106 self._builder_name = builder_name 107 self._build_name = build_name 108 self._build_number = build_number 109 self._builder_base_url = builder_base_url 110 self._results_file_path = os.path.join(results_file_base_path, 111 self.RESULTS_FILENAME) 112 self._test_timings = test_timings 113 self._failures = failures 114 self._passed_tests = passed_tests 115 self._skipped_tests = skipped_tests 116 self._all_tests = all_tests 117 self._svn_repositories = port.test_repository_paths() 118 119 self._generate_json_output() 120 121 def _generate_json_output(self): 122 """Generates the JSON output file.""" 123 json = self._get_json() 124 if json: 125 results_file = codecs.open(self._results_file_path, "w", "utf-8") 126 results_file.write(json) 127 results_file.close() 128 129 # FIXME: Callers should use scm.py instead. 130 def _get_svn_revision(self, in_directory): 131 """Returns the svn revision for the given directory. 132 133 Args: 134 in_directory: The directory where svn is to be run. 135 """ 136 137 if os.path.exists(os.path.join(in_directory, '.svn')): 138 # Note: Not thread safe: http://bugs.python.org/issue2320 139 output = subprocess.Popen(["svn", "info", "--xml"], 140 cwd=in_directory, 141 shell=(sys.platform == 'win32'), 142 stdout=subprocess.PIPE).communicate()[0] 143 try: 144 dom = xml.dom.minidom.parseString(output) 145 return dom.getElementsByTagName('entry')[0].getAttribute( 146 'revision') 147 except xml.parsers.expat.ExpatError: 148 return "" 149 return "" 150 151 def _get_archived_json_results(self): 152 """Reads old results JSON file if it exists. 153 Returns (archived_results, error) tuple where error is None if results 154 were successfully read. 155 """ 156 results_json = {} 157 old_results = None 158 error = None 159 160 if os.path.exists(self._results_file_path): 161 with codecs.open(self._results_file_path, "r", "utf-8") as file: 162 old_results = file.read() 163 elif self._builder_base_url: 164 # Check if we have the archived JSON file on the buildbot server. 165 results_file_url = (self._builder_base_url + 166 self._build_name + "/" + self.RESULTS_FILENAME) 167 _log.error("Local results.json file does not exist. Grabbing " 168 "it off the archive at " + results_file_url) 169 170 try: 171 results_file = urllib2.urlopen(results_file_url) 172 info = results_file.info() 173 old_results = results_file.read() 174 except urllib2.HTTPError, http_error: 175 # A non-4xx status code means the bot is hosed for some reason 176 # and we can't grab the results.json file off of it. 177 if (http_error.code < 400 and http_error.code >= 500): 178 error = http_error 179 except urllib2.URLError, url_error: 180 error = url_error 181 182 if old_results: 183 # Strip the prefix and suffix so we can get the actual JSON object. 184 old_results = old_results[len(self.JSON_PREFIX): 185 len(old_results) - len(self.JSON_SUFFIX)] 186 187 try: 188 results_json = simplejson.loads(old_results) 189 except: 190 _log.debug("results.json was not valid JSON. Clobbering.") 191 # The JSON file is not valid JSON. Just clobber the results. 192 results_json = {} 193 else: 194 _log.debug('Old JSON results do not exist. Starting fresh.') 195 results_json = {} 196 197 return results_json, error 198 199 def _get_json(self): 200 """Gets the results for the results.json file.""" 201 results_json, error = self._get_archived_json_results() 202 if error: 203 # If there was an error don't write a results.json 204 # file at all as it would lose all the information on the bot. 205 _log.error("Archive directory is inaccessible. Not modifying " 206 "or clobbering the results.json file: " + str(error)) 207 return None 208 209 builder_name = self._builder_name 210 if results_json and builder_name not in results_json: 211 _log.debug("Builder name (%s) is not in the results.json file." 212 % builder_name) 213 214 self._convert_json_to_current_version(results_json) 215 216 if builder_name not in results_json: 217 results_json[builder_name] = ( 218 self._create_results_for_builder_json()) 219 220 results_for_builder = results_json[builder_name] 221 222 self._insert_generic_metadata(results_for_builder) 223 224 self._insert_failure_summaries(results_for_builder) 225 226 # Update the all failing tests with result type and time. 227 tests = results_for_builder[self.TESTS] 228 all_failing_tests = set(self._failures.iterkeys()) 229 all_failing_tests.update(tests.iterkeys()) 230 for test in all_failing_tests: 231 self._insert_test_time_and_result(test, tests) 232 233 # Specify separators in order to get compact encoding. 234 results_str = simplejson.dumps(results_json, separators=(',', ':')) 235 return self.JSON_PREFIX + results_str + self.JSON_SUFFIX 236 237 def _insert_failure_summaries(self, results_for_builder): 238 """Inserts aggregate pass/failure statistics into the JSON. 239 This method reads self._skipped_tests, self._passed_tests and 240 self._failures and inserts FIXABLE, FIXABLE_COUNT and ALL_FIXABLE_COUNT 241 entries. 242 243 Args: 244 results_for_builder: Dictionary containing the test results for a 245 single builder. 246 """ 247 # Insert the number of tests that failed. 248 self._insert_item_into_raw_list(results_for_builder, 249 len(set(self._failures.keys()) | self._skipped_tests), 250 self.FIXABLE_COUNT) 251 252 # Create a pass/skip/failure summary dictionary. 253 entry = {} 254 entry[self.SKIP_RESULT] = len(self._skipped_tests) 255 entry[self.PASS_RESULT] = len(self._passed_tests) 256 get = entry.get 257 for failure_type in self._failures.values(): 258 failure_char = self.FAILURE_TO_CHAR[failure_type] 259 entry[failure_char] = get(failure_char, 0) + 1 260 261 # Insert the pass/skip/failure summary dictionary. 262 self._insert_item_into_raw_list(results_for_builder, entry, 263 self.FIXABLE) 264 265 # Insert the number of all the tests that are supposed to pass. 266 self._insert_item_into_raw_list(results_for_builder, 267 len(self._skipped_tests | self._all_tests), 268 self.ALL_FIXABLE_COUNT) 269 270 def _insert_item_into_raw_list(self, results_for_builder, item, key): 271 """Inserts the item into the list with the given key in the results for 272 this builder. Creates the list if no such list exists. 273 274 Args: 275 results_for_builder: Dictionary containing the test results for a 276 single builder. 277 item: Number or string to insert into the list. 278 key: Key in results_for_builder for the list to insert into. 279 """ 280 if key in results_for_builder: 281 raw_list = results_for_builder[key] 282 else: 283 raw_list = [] 284 285 raw_list.insert(0, item) 286 raw_list = raw_list[:self.MAX_NUMBER_OF_BUILD_RESULTS_TO_LOG] 287 results_for_builder[key] = raw_list 288 289 def _insert_item_run_length_encoded(self, item, encoded_results): 290 """Inserts the item into the run-length encoded results. 291 292 Args: 293 item: String or number to insert. 294 encoded_results: run-length encoded results. An array of arrays, e.g. 295 [[3,'A'],[1,'Q']] encodes AAAQ. 296 """ 297 if len(encoded_results) and item == encoded_results[0][1]: 298 num_results = encoded_results[0][0] 299 if num_results <= self.MAX_NUMBER_OF_BUILD_RESULTS_TO_LOG: 300 encoded_results[0][0] = num_results + 1 301 else: 302 # Use a list instead of a class for the run-length encoding since 303 # we want the serialized form to be concise. 304 encoded_results.insert(0, [1, item]) 305 306 def _insert_generic_metadata(self, results_for_builder): 307 """ Inserts generic metadata (such as version number, current time etc) 308 into the JSON. 309 310 Args: 311 results_for_builder: Dictionary containing the test results for 312 a single builder. 313 """ 314 self._insert_item_into_raw_list(results_for_builder, 315 self._build_number, self.BUILD_NUMBERS) 316 317 # Include SVN revisions for the given repositories. 318 for (name, path) in self._svn_repositories: 319 self._insert_item_into_raw_list(results_for_builder, 320 self._get_svn_revision(path), 321 name + 'Revision') 322 323 self._insert_item_into_raw_list(results_for_builder, 324 int(time.time()), 325 self.TIME) 326 327 def _insert_test_time_and_result(self, test_name, tests): 328 """ Insert a test item with its results to the given tests dictionary. 329 330 Args: 331 tests: Dictionary containing test result entries. 332 """ 333 334 result = JSONResultsGenerator.PASS_RESULT 335 time = 0 336 337 if test_name not in self._all_tests: 338 result = JSONResultsGenerator.NO_DATA_RESULT 339 340 if test_name in self._failures: 341 result = self.FAILURE_TO_CHAR[self._failures[test_name]] 342 343 if test_name in self._test_timings: 344 # Floor for now to get time in seconds. 345 time = int(self._test_timings[test_name]) 346 347 if test_name not in tests: 348 tests[test_name] = self._create_results_and_times_json() 349 350 thisTest = tests[test_name] 351 self._insert_item_run_length_encoded(result, thisTest[self.RESULTS]) 352 self._insert_item_run_length_encoded(time, thisTest[self.TIMES]) 353 self._normalize_results_json(thisTest, test_name, tests) 354 355 def _convert_json_to_current_version(self, results_json): 356 """If the JSON does not match the current version, converts it to the 357 current version and adds in the new version number. 358 """ 359 if (self.VERSION_KEY in results_json and 360 results_json[self.VERSION_KEY] == self.VERSION): 361 return 362 363 results_json[self.VERSION_KEY] = self.VERSION 364 365 def _create_results_and_times_json(self): 366 results_and_times = {} 367 results_and_times[self.RESULTS] = [] 368 results_and_times[self.TIMES] = [] 369 return results_and_times 370 371 def _create_results_for_builder_json(self): 372 results_for_builder = {} 373 results_for_builder[self.TESTS] = {} 374 return results_for_builder 375 376 def _remove_items_over_max_number_of_builds(self, encoded_list): 377 """Removes items from the run-length encoded list after the final 378 item that exceeds the max number of builds to track. 379 380 Args: 381 encoded_results: run-length encoded results. An array of arrays, e.g. 382 [[3,'A'],[1,'Q']] encodes AAAQ. 383 """ 384 num_builds = 0 385 index = 0 386 for result in encoded_list: 387 num_builds = num_builds + result[0] 388 index = index + 1 389 if num_builds > self.MAX_NUMBER_OF_BUILD_RESULTS_TO_LOG: 390 return encoded_list[:index] 391 return encoded_list 392 393 def _normalize_results_json(self, test, test_name, tests): 394 """ Prune tests where all runs pass or tests that no longer exist and 395 truncate all results to maxNumberOfBuilds. 396 397 Args: 398 test: ResultsAndTimes object for this test. 399 test_name: Name of the test. 400 tests: The JSON object with all the test results for this builder. 401 """ 402 test[self.RESULTS] = self._remove_items_over_max_number_of_builds( 403 test[self.RESULTS]) 404 test[self.TIMES] = self._remove_items_over_max_number_of_builds( 405 test[self.TIMES]) 406 407 is_all_pass = self._is_results_all_of_type(test[self.RESULTS], 408 self.PASS_RESULT) 409 is_all_no_data = self._is_results_all_of_type(test[self.RESULTS], 410 self.NO_DATA_RESULT) 411 max_time = max([time[1] for time in test[self.TIMES]]) 412 413 # Remove all passes/no-data from the results to reduce noise and 414 # filesize. If a test passes every run, but takes > MIN_TIME to run, 415 # don't throw away the data. 416 if is_all_no_data or (is_all_pass and max_time <= self.MIN_TIME): 417 del tests[test_name] 418 419 def _is_results_all_of_type(self, results, type): 420 """Returns whether all the results are of the given type 421 (e.g. all passes).""" 422 return len(results) == 1 and results[0][1] == type 474 475 # Create a map of (name, TestResult). 476 test_results_map = dict() 477 get = test_results_map.get 478 for (test, time) in test_timings.iteritems(): 479 test_results_map[test] = TestResult(test, elapsed_time=time) 480 for test in failures.iterkeys(): 481 test_results_map[test] = test_result = get(test, TestResult(test)) 482 test_result.failed = True 483 for test in skipped_tests: 484 test_results_map[test] = test_result = get(test, TestResult(test)) 485 test_result.skipped = True 486 for test in passed_tests: 487 test_results_map[test] = test_result = get(test, TestResult(test)) 488 test_result.failed = False 489 test_result.skipped = False 490 for test in all_tests: 491 if test not in test_results_map: 492 test_results_map[test] = TestResult(test) 493 494 super(JSONResultsGenerator, self).__init__( 495 builder_name, build_name, build_number, 496 results_file_base_path, builder_base_url, test_results_map, 497 svn_repositories=port.test_repository_paths()) 498 499 if self.__class__.output_json_in_init: 500 self.generate_json_output()
Note: See TracChangeset
for help on using the changeset viewer.