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 |