Index: webkit/tools/layout_tests/layout_package/json_results_generator.py |
=================================================================== |
--- webkit/tools/layout_tests/layout_package/json_results_generator.py (revision 25738) |
+++ webkit/tools/layout_tests/layout_package/json_results_generator.py (working copy) |
@@ -15,7 +15,7 @@ |
class JSONResultsGenerator: |
- MAX_NUMBER_OF_BUILD_RESULTS_TO_LOG = 200 |
+ MAX_NUMBER_OF_BUILD_RESULTS_TO_LOG = 500 |
# Min time (seconds) that will be added to the JSON. |
MIN_TIME = 1 |
JSON_PREFIX = "ADD_RESULTS(" |
@@ -24,6 +24,12 @@ |
LAYOUT_TESTS_PATH = "layout_tests" |
PASS_RESULT = "P" |
NO_DATA_RESULT = "N" |
+ VERSION = 1 |
+ VERSION_KEY = "version" |
+ RESULTS = "results" |
+ TIMES = "times" |
+ BUILD_NUMBERS = "buildNumbers" |
+ TESTS = "tests" |
def __init__(self, failures, individual_test_timings, builder_name, |
build_number, results_file_path, all_tests): |
@@ -119,17 +125,19 @@ |
# just grab it from wherever it's archived to. |
results_json = {} |
+ self._ConvertJSONToCurrentVersion(results_json) |
+ |
if self._builder_name not in results_json: |
results_json[self._builder_name] = self._CreateResultsForBuilderJSON() |
- tests = results_json[self._builder_name]["tests"] |
+ tests = results_json[self._builder_name][self.TESTS] |
all_failing_tests = set(self._failures.iterkeys()) |
all_failing_tests.update(tests.iterkeys()) |
- build_numbers = results_json[self._builder_name]["buildNumbers"] |
+ build_numbers = results_json[self._builder_name][self.BUILD_NUMBERS] |
build_numbers.insert(0, self._build_number) |
build_numbers = build_numbers[:self.MAX_NUMBER_OF_BUILD_RESULTS_TO_LOG] |
- results_json[self._builder_name]["buildNumbers"] = build_numbers |
+ results_json[self._builder_name][self.BUILD_NUMBERS] = build_numbers |
num_build_numbers = len(build_numbers) |
for test in all_failing_tests: |
@@ -142,25 +150,66 @@ |
tests[test] = self._CreateResultsAndTimesJSON() |
thisTest = tests[test] |
- thisTest["results"] = result_and_time.result + thisTest["results"] |
- thisTest["times"].insert(0, result_and_time.time) |
- |
+ self._InsertItemRunLengthEncoded(result_and_time.result, |
+ thisTest[self.RESULTS]) |
+ self._InsertItemRunLengthEncoded(result_and_time.time, |
+ thisTest[self.TIMES]) |
self._NormalizeResultsJSON(thisTest, test, tests, num_build_numbers) |
# Specify separators in order to get compact encoding. |
results_str = simplejson.dumps(results_json, separators=(',', ':')) |
return self.JSON_PREFIX + results_str + self.JSON_SUFFIX |
+ def _InsertItemRunLengthEncoded(self, item, encoded_results): |
+ """Inserts the item into the run-length encoded results. |
+ |
+ Args: |
+ item: String or number to insert. |
+ encoded_results: run-length encoded results. An array of arrays, e.g. |
+ [[3,'A'],[1,'Q']] encodes AAAQ. |
+ """ |
+ if len(encoded_results) and item == encoded_results[0][1]: |
+ encoded_results[0][0] += 1 |
+ else: |
+ # Use a list instead of a class for the run-length encoding since we |
+ # want the serialized form to be concise. |
+ encoded_results.insert(0, [1, item]) |
+ |
+ def _ConvertJSONToCurrentVersion(self, results_json): |
+ """If the JSON does not match the current version, converts it to the |
+ current version and adds in the new version number. |
+ """ |
+ if (self.VERSION_KEY in results_json and |
+ results_json[self.VERSION_KEY] == self.VERSION): |
+ return |
+ |
+ for builder in results_json: |
+ tests = results_json[builder][self.TESTS] |
+ for path in tests: |
+ test = tests[path] |
+ test[self.RESULTS] = self._RunLengthEncode(test[self.RESULTS]) |
+ test[self.TIMES] = self._RunLengthEncode(test[self.TIMES]) |
+ |
+ results_json[self.VERSION_KEY] = self.VERSION |
+ |
+ def _RunLengthEncode(self, result_list): |
+ """Run-length encodes a list or string of results.""" |
+ encoded_results = []; |
+ current_result = None; |
+ for item in reversed(result_list): |
+ self._InsertItemRunLengthEncoded(item, encoded_results) |
+ return encoded_results |
+ |
def _CreateResultsAndTimesJSON(self): |
results_and_times = {} |
- results_and_times["results"] = "" |
- results_and_times["times"] = [] |
+ results_and_times[self.RESULTS] = [] |
+ results_and_times[self.TIMES] = [] |
return results_and_times |
def _CreateResultsForBuilderJSON(self): |
results_for_builder = {} |
- results_for_builder['buildNumbers'] = [] |
- results_for_builder['tests'] = {} |
+ results_for_builder[self.BUILD_NUMBERS] = [] |
+ results_for_builder[self.TESTS] = {} |
return results_for_builder |
def _GetResultsCharForFailure(self, test): |
@@ -182,6 +231,23 @@ |
else: |
return "O" |
+ def _RemoveItemsOverMaxNumberOfBuilds(self, encoded_list): |
+ """Removes items from the run-length encoded list after the final itme that |
+ exceeds the max number of builds to track. |
+ |
+ Args: |
+ encoded_results: run-length encoded results. An array of arrays, e.g. |
+ [[3,'A'],[1,'Q']] encodes AAAQ. |
+ """ |
+ num_builds = 0 |
+ index = 0 |
+ for result in encoded_list: |
+ num_builds = num_builds + result[0] |
+ index = index + 1 |
+ if num_builds > self.MAX_NUMBER_OF_BUILD_RESULTS_TO_LOG: |
+ return encoded_list[:index] |
+ return encoded_list |
+ |
def _NormalizeResultsJSON(self, test, test_path, tests, num_build_numbers): |
""" Prune tests where all runs pass or tests that no longer exist and |
truncate all results to maxNumberOfBuilds and pad results that don't |
@@ -193,32 +259,15 @@ |
tests: The JSON object with all the test results for this builder. |
num_build_numbers: The number to truncate/pad results to. |
""" |
- results = test["results"] |
- num_results = len(results) |
- times = test["times"] |
+ test[self.RESULTS] = self._RemoveItemsOverMaxNumberOfBuilds( |
+ test[self.RESULTS]) |
+ test[self.TIMES] = self._RemoveItemsOverMaxNumberOfBuilds(test[self.TIMES]) |
- if num_results != len(times): |
- logging.error("Test has different number of build times versus results") |
- times = [] |
- results = "" |
- num_results = 0 |
- |
- # Truncate or right-pad so there are exactly maxNumberOfBuilds results. |
- if num_results > num_build_numbers: |
- results = results[:num_build_numbers] |
- times = times[:num_build_numbers] |
- elif num_results < num_build_numbers: |
- num_to_pad = num_build_numbers - num_results |
- results = results + num_to_pad * self.NO_DATA_RESULT |
- times.extend(num_to_pad * [0]) |
- |
- test["results"] = results |
- test["times"] = times |
- |
# Remove all passes/no-data from the results to reduce noise and filesize. |
- if (results == num_build_numbers * self.NO_DATA_RESULT or |
- (max(times) <= self.MIN_TIME and num_results and |
- results == num_build_numbers * self.PASS_RESULT)): |
+ if (self._IsResultsAllOfType(test[self.RESULTS], self.PASS_RESULT) or |
+ (self._IsResultsAllOfType(test[self.RESULTS], self.NO_DATA_RESULT) and |
+ max(test[self.TIMES], |
+ lambda x, y : cmp(x[1], y[1])) <= self.MIN_TIME)): |
del tests[test_path] |
# Remove tests that don't exist anymore. |
@@ -227,6 +276,11 @@ |
if not os.path.exists(full_path): |
del tests[test_path] |
+ def _IsResultsAllOfType(self, results, type): |
+ """Returns whether all teh results are of the given type (e.g. all passes). |
+ """ |
+ return len(results) == 1 and results[0][1] == type |
+ |
class ResultAndTime: |
"""A holder for a single result and runtime for a test.""" |
def __init__(self, test, all_tests): |