| OLD | NEW |
| 1 # Copyright (c) 2009 The Chromium Authors. All rights reserved. | 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 | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 import logging | 5 import logging |
| 6 import os | 6 import os |
| 7 import re | 7 import re |
| 8 import sys | 8 import sys |
| 9 | 9 |
| 10 from layout_package import path_utils | 10 from layout_package import path_utils |
| 11 from layout_package import test_failures | 11 from layout_package import test_failures |
| 12 | 12 |
| 13 sys.path.append(path_utils.PathFromBase('third_party')) | 13 sys.path.append(path_utils.PathFromBase('third_party')) |
| 14 import simplejson | 14 import simplejson |
| 15 | 15 |
| 16 class JSONResultsGenerator: | 16 class JSONResultsGenerator: |
| 17 | 17 |
| 18 MAX_NUMBER_OF_BUILD_RESULTS_TO_LOG = 200 | 18 MAX_NUMBER_OF_BUILD_RESULTS_TO_LOG = 500 |
| 19 # Min time (seconds) that will be added to the JSON. | 19 # Min time (seconds) that will be added to the JSON. |
| 20 MIN_TIME = 1 | 20 MIN_TIME = 1 |
| 21 JSON_PREFIX = "ADD_RESULTS(" | 21 JSON_PREFIX = "ADD_RESULTS(" |
| 22 JSON_SUFFIX = ");" | 22 JSON_SUFFIX = ");" |
| 23 WEBKIT_PATH = "WebKit" | 23 WEBKIT_PATH = "WebKit" |
| 24 LAYOUT_TESTS_PATH = "layout_tests" | 24 LAYOUT_TESTS_PATH = "layout_tests" |
| 25 PASS_RESULT = "P" | 25 PASS_RESULT = "P" |
| 26 NO_DATA_RESULT = "N" | 26 NO_DATA_RESULT = "N" |
| 27 VERSION = 1 |
| 28 VERSION_KEY = "version" |
| 29 RESULTS = "results" |
| 30 TIMES = "times" |
| 31 BUILD_NUMBERS = "buildNumbers" |
| 32 TESTS = "tests" |
| 27 | 33 |
| 28 def __init__(self, failures, individual_test_timings, builder_name, | 34 def __init__(self, failures, individual_test_timings, builder_name, |
| 29 build_number, results_file_path, all_tests): | 35 build_number, results_file_path, all_tests): |
| 30 """ | 36 """ |
| 31 failures: Map of test name to list of failures. | 37 failures: Map of test name to list of failures. |
| 32 individual_test_times: Map of test name to a tuple containing the | 38 individual_test_times: Map of test name to a tuple containing the |
| 33 test_run-time. | 39 test_run-time. |
| 34 builder_name: The name of the builder the tests are being run on. | 40 builder_name: The name of the builder the tests are being run on. |
| 35 build_number: The build number for this run. | 41 build_number: The build number for this run. |
| 36 results_file_path: Absolute path to the results json file. | 42 results_file_path: Absolute path to the results json file. |
| (...skipping 75 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 112 | 118 |
| 113 if self._builder_name not in results_json: | 119 if self._builder_name not in results_json: |
| 114 logging.error("Builder name (%s) is not in the results.json file." % | 120 logging.error("Builder name (%s) is not in the results.json file." % |
| 115 self._builder_name); | 121 self._builder_name); |
| 116 else: | 122 else: |
| 117 # TODO(ojan): If the build output directory gets clobbered, we should | 123 # TODO(ojan): If the build output directory gets clobbered, we should |
| 118 # grab this file off wherever it's archived to. Maybe we should always | 124 # grab this file off wherever it's archived to. Maybe we should always |
| 119 # just grab it from wherever it's archived to. | 125 # just grab it from wherever it's archived to. |
| 120 results_json = {} | 126 results_json = {} |
| 121 | 127 |
| 128 self._ConvertJSONToCurrentVersion(results_json) |
| 129 |
| 122 if self._builder_name not in results_json: | 130 if self._builder_name not in results_json: |
| 123 results_json[self._builder_name] = self._CreateResultsForBuilderJSON() | 131 results_json[self._builder_name] = self._CreateResultsForBuilderJSON() |
| 124 | 132 |
| 125 tests = results_json[self._builder_name]["tests"] | 133 tests = results_json[self._builder_name][self.TESTS] |
| 126 all_failing_tests = set(self._failures.iterkeys()) | 134 all_failing_tests = set(self._failures.iterkeys()) |
| 127 all_failing_tests.update(tests.iterkeys()) | 135 all_failing_tests.update(tests.iterkeys()) |
| 128 | 136 |
| 129 build_numbers = results_json[self._builder_name]["buildNumbers"] | 137 build_numbers = results_json[self._builder_name][self.BUILD_NUMBERS] |
| 130 build_numbers.insert(0, self._build_number) | 138 build_numbers.insert(0, self._build_number) |
| 131 build_numbers = build_numbers[:self.MAX_NUMBER_OF_BUILD_RESULTS_TO_LOG] | 139 build_numbers = build_numbers[:self.MAX_NUMBER_OF_BUILD_RESULTS_TO_LOG] |
| 132 results_json[self._builder_name]["buildNumbers"] = build_numbers | 140 results_json[self._builder_name][self.BUILD_NUMBERS] = build_numbers |
| 133 num_build_numbers = len(build_numbers) | 141 num_build_numbers = len(build_numbers) |
| 134 | 142 |
| 135 for test in all_failing_tests: | 143 for test in all_failing_tests: |
| 136 if test in failures_for_json: | 144 if test in failures_for_json: |
| 137 result_and_time = failures_for_json[test] | 145 result_and_time = failures_for_json[test] |
| 138 else: | 146 else: |
| 139 result_and_time = ResultAndTime(test, self._all_tests) | 147 result_and_time = ResultAndTime(test, self._all_tests) |
| 140 | 148 |
| 141 if test not in tests: | 149 if test not in tests: |
| 142 tests[test] = self._CreateResultsAndTimesJSON() | 150 tests[test] = self._CreateResultsAndTimesJSON() |
| 143 | 151 |
| 144 thisTest = tests[test] | 152 thisTest = tests[test] |
| 145 thisTest["results"] = result_and_time.result + thisTest["results"] | 153 self._InsertItemRunLengthEncoded(result_and_time.result, |
| 146 thisTest["times"].insert(0, result_and_time.time) | 154 thisTest[self.RESULTS]) |
| 147 | 155 self._InsertItemRunLengthEncoded(result_and_time.time, |
| 156 thisTest[self.TIMES]) |
| 148 self._NormalizeResultsJSON(thisTest, test, tests, num_build_numbers) | 157 self._NormalizeResultsJSON(thisTest, test, tests, num_build_numbers) |
| 149 | 158 |
| 150 # Specify separators in order to get compact encoding. | 159 # Specify separators in order to get compact encoding. |
| 151 results_str = simplejson.dumps(results_json, separators=(',', ':')) | 160 results_str = simplejson.dumps(results_json, separators=(',', ':')) |
| 152 return self.JSON_PREFIX + results_str + self.JSON_SUFFIX | 161 return self.JSON_PREFIX + results_str + self.JSON_SUFFIX |
| 153 | 162 |
| 163 def _InsertItemRunLengthEncoded(self, item, encoded_results): |
| 164 """Inserts the item into the run-length encoded results. |
| 165 |
| 166 Args: |
| 167 item: String or number to insert. |
| 168 encoded_results: run-length encoded results. An array of arrays, e.g. |
| 169 [[3,'A'],[1,'Q']] encodes AAAQ. |
| 170 """ |
| 171 if len(encoded_results) and item == encoded_results[0][1]: |
| 172 encoded_results[0][0] += 1 |
| 173 else: |
| 174 # Use a list instead of a class for the run-length encoding since we |
| 175 # want the serialized form to be concise. |
| 176 encoded_results.insert(0, [1, item]) |
| 177 |
| 178 def _ConvertJSONToCurrentVersion(self, results_json): |
| 179 """If the JSON does not match the current version, converts it to the |
| 180 current version and adds in the new version number. |
| 181 """ |
| 182 if (self.VERSION_KEY in results_json and |
| 183 results_json[self.VERSION_KEY] == self.VERSION): |
| 184 return |
| 185 |
| 186 for builder in results_json: |
| 187 tests = results_json[builder][self.TESTS] |
| 188 for path in tests: |
| 189 test = tests[path] |
| 190 test[self.RESULTS] = self._RunLengthEncode(test[self.RESULTS]) |
| 191 test[self.TIMES] = self._RunLengthEncode(test[self.TIMES]) |
| 192 |
| 193 results_json[self.VERSION_KEY] = self.VERSION |
| 194 |
| 195 def _RunLengthEncode(self, result_list): |
| 196 """Run-length encodes a list or string of results.""" |
| 197 encoded_results = []; |
| 198 current_result = None; |
| 199 for item in reversed(result_list): |
| 200 self._InsertItemRunLengthEncoded(item, encoded_results) |
| 201 return encoded_results |
| 202 |
| 154 def _CreateResultsAndTimesJSON(self): | 203 def _CreateResultsAndTimesJSON(self): |
| 155 results_and_times = {} | 204 results_and_times = {} |
| 156 results_and_times["results"] = "" | 205 results_and_times[self.RESULTS] = [] |
| 157 results_and_times["times"] = [] | 206 results_and_times[self.TIMES] = [] |
| 158 return results_and_times | 207 return results_and_times |
| 159 | 208 |
| 160 def _CreateResultsForBuilderJSON(self): | 209 def _CreateResultsForBuilderJSON(self): |
| 161 results_for_builder = {} | 210 results_for_builder = {} |
| 162 results_for_builder['buildNumbers'] = [] | 211 results_for_builder[self.BUILD_NUMBERS] = [] |
| 163 results_for_builder['tests'] = {} | 212 results_for_builder[self.TESTS] = {} |
| 164 return results_for_builder | 213 return results_for_builder |
| 165 | 214 |
| 166 def _GetResultsCharForFailure(self, test): | 215 def _GetResultsCharForFailure(self, test): |
| 167 """Returns the worst failure from the list of failures for this test | 216 """Returns the worst failure from the list of failures for this test |
| 168 since we can only show one failure per run for each test on the dashboard. | 217 since we can only show one failure per run for each test on the dashboard. |
| 169 """ | 218 """ |
| 170 failures = [failure.__class__ for failure in self._failures[test]] | 219 failures = [failure.__class__ for failure in self._failures[test]] |
| 171 | 220 |
| 172 if test_failures.FailureCrash in failures: | 221 if test_failures.FailureCrash in failures: |
| 173 return "C" | 222 return "C" |
| 174 elif test_failures.FailureTimeout in failures: | 223 elif test_failures.FailureTimeout in failures: |
| 175 return "T" | 224 return "T" |
| 176 elif test_failures.FailureImageHashMismatch in failures: | 225 elif test_failures.FailureImageHashMismatch in failures: |
| 177 return "I" | 226 return "I" |
| 178 elif test_failures.FailureSimplifiedTextMismatch in failures: | 227 elif test_failures.FailureSimplifiedTextMismatch in failures: |
| 179 return "S" | 228 return "S" |
| 180 elif test_failures.FailureTextMismatch in failures: | 229 elif test_failures.FailureTextMismatch in failures: |
| 181 return "F" | 230 return "F" |
| 182 else: | 231 else: |
| 183 return "O" | 232 return "O" |
| 184 | 233 |
| 234 def _RemoveItemsOverMaxNumberOfBuilds(self, encoded_list): |
| 235 """Removes items from the run-length encoded list after the final itme that |
| 236 exceeds the max number of builds to track. |
| 237 |
| 238 Args: |
| 239 encoded_results: run-length encoded results. An array of arrays, e.g. |
| 240 [[3,'A'],[1,'Q']] encodes AAAQ. |
| 241 """ |
| 242 num_builds = 0 |
| 243 index = 0 |
| 244 for result in encoded_list: |
| 245 num_builds = num_builds + result[0] |
| 246 index = index + 1 |
| 247 if num_builds > self.MAX_NUMBER_OF_BUILD_RESULTS_TO_LOG: |
| 248 return encoded_list[:index] |
| 249 return encoded_list |
| 250 |
| 185 def _NormalizeResultsJSON(self, test, test_path, tests, num_build_numbers): | 251 def _NormalizeResultsJSON(self, test, test_path, tests, num_build_numbers): |
| 186 """ Prune tests where all runs pass or tests that no longer exist and | 252 """ Prune tests where all runs pass or tests that no longer exist and |
| 187 truncate all results to maxNumberOfBuilds and pad results that don't | 253 truncate all results to maxNumberOfBuilds and pad results that don't |
| 188 have encough runs for maxNumberOfBuilds. | 254 have encough runs for maxNumberOfBuilds. |
| 189 | 255 |
| 190 Args: | 256 Args: |
| 191 test: ResultsAndTimes object for this test. | 257 test: ResultsAndTimes object for this test. |
| 192 test_path: Path to the test. | 258 test_path: Path to the test. |
| 193 tests: The JSON object with all the test results for this builder. | 259 tests: The JSON object with all the test results for this builder. |
| 194 num_build_numbers: The number to truncate/pad results to. | 260 num_build_numbers: The number to truncate/pad results to. |
| 195 """ | 261 """ |
| 196 results = test["results"] | 262 test[self.RESULTS] = self._RemoveItemsOverMaxNumberOfBuilds( |
| 197 num_results = len(results) | 263 test[self.RESULTS]) |
| 198 times = test["times"] | 264 test[self.TIMES] = self._RemoveItemsOverMaxNumberOfBuilds(test[self.TIMES]) |
| 199 | |
| 200 if num_results != len(times): | |
| 201 logging.error("Test has different number of build times versus results") | |
| 202 times = [] | |
| 203 results = "" | |
| 204 num_results = 0 | |
| 205 | |
| 206 # Truncate or right-pad so there are exactly maxNumberOfBuilds results. | |
| 207 if num_results > num_build_numbers: | |
| 208 results = results[:num_build_numbers] | |
| 209 times = times[:num_build_numbers] | |
| 210 elif num_results < num_build_numbers: | |
| 211 num_to_pad = num_build_numbers - num_results | |
| 212 results = results + num_to_pad * self.NO_DATA_RESULT | |
| 213 times.extend(num_to_pad * [0]) | |
| 214 | |
| 215 test["results"] = results | |
| 216 test["times"] = times | |
| 217 | 265 |
| 218 # Remove all passes/no-data from the results to reduce noise and filesize. | 266 # Remove all passes/no-data from the results to reduce noise and filesize. |
| 219 if (results == num_build_numbers * self.NO_DATA_RESULT or | 267 if (self._IsResultsAllOfType(test[self.RESULTS], self.PASS_RESULT) or |
| 220 (max(times) <= self.MIN_TIME and num_results and | 268 (self._IsResultsAllOfType(test[self.RESULTS], self.NO_DATA_RESULT) and |
| 221 results == num_build_numbers * self.PASS_RESULT)): | 269 max(test[self.TIMES], |
| 270 lambda x, y : cmp(x[1], y[1])) <= self.MIN_TIME)): |
| 222 del tests[test_path] | 271 del tests[test_path] |
| 223 | 272 |
| 224 # Remove tests that don't exist anymore. | 273 # Remove tests that don't exist anymore. |
| 225 full_path = os.path.join(path_utils.LayoutTestsDir(test_path), test_path) | 274 full_path = os.path.join(path_utils.LayoutTestsDir(test_path), test_path) |
| 226 full_path = os.path.normpath(full_path) | 275 full_path = os.path.normpath(full_path) |
| 227 if not os.path.exists(full_path): | 276 if not os.path.exists(full_path): |
| 228 del tests[test_path] | 277 del tests[test_path] |
| 229 | 278 |
| 279 def _IsResultsAllOfType(self, results, type): |
| 280 """Returns whether all teh results are of the given type (e.g. all passes). |
| 281 """ |
| 282 return len(results) == 1 and results[0][1] == type |
| 283 |
| 230 class ResultAndTime: | 284 class ResultAndTime: |
| 231 """A holder for a single result and runtime for a test.""" | 285 """A holder for a single result and runtime for a test.""" |
| 232 def __init__(self, test, all_tests): | 286 def __init__(self, test, all_tests): |
| 233 self.time = 0 | 287 self.time = 0 |
| 234 # If the test was run, then we don't want to default the result to nodata. | 288 # If the test was run, then we don't want to default the result to nodata. |
| 235 if test in all_tests: | 289 if test in all_tests: |
| 236 self.result = JSONResultsGenerator.PASS_RESULT | 290 self.result = JSONResultsGenerator.PASS_RESULT |
| 237 else: | 291 else: |
| 238 self.result = JSONResultsGenerator.NO_DATA_RESULT | 292 self.result = JSONResultsGenerator.NO_DATA_RESULT |
| OLD | NEW |