blob: 5053c7ab24e7405814f3c2e63de25abb6df6e50e [file] [log] [blame]
# Copyright (C) 2010 Google Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
# * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import json
import logging
from webkitpy.common.iteration_compatibility import iteritems, iterkeys
# A JSON results generator for generic tests.
# FIXME: move this code out of the layout_package directory.
_log = logging.getLogger(__name__)
_JSON_PREFIX = "ADD_RESULTS("
_JSON_SUFFIX = ");"
def has_json_wrapper(string):
return string.startswith(_JSON_PREFIX) and string.endswith(_JSON_SUFFIX)
def strip_json_wrapper(json_content):
# FIXME: Kill this code once the server returns json instead of jsonp.
if has_json_wrapper(json_content):
return json_content[len(_JSON_PREFIX):len(json_content) - len(_JSON_SUFFIX)]
return json_content
def load_json(filesystem, file_path):
content = filesystem.read_text_file(file_path)
content = strip_json_wrapper(content)
return json.loads(content)
def write_json(filesystem, json_object, file_path, callback=None):
# Specify separators in order to get compact encoding.
json_string = json.dumps(json_object, separators=(',', ':'))
if callback:
json_string = callback + "(" + json_string + ");"
filesystem.write_text_file(file_path, json_string)
def convert_trie_to_flat_paths(trie, prefix=None):
"""Converts the directory structure in the given trie to flat paths, prepending a prefix to each."""
result = {}
for name, data in iteritems(trie):
if prefix:
name = prefix + "/" + name
if len(data) and not "results" in data:
result.update(convert_trie_to_flat_paths(data, name))
else:
result[name] = data
return result
def add_path_to_trie(path, value, trie):
"""Inserts a single flat directory path and associated value into a directory trie structure."""
if not "/" in path:
trie[path] = value
return
directory, slash, rest = path.partition("/")
if not directory in trie:
trie[directory] = {}
add_path_to_trie(rest, value, trie[directory])
def test_timings_trie(port, individual_test_timings):
"""Breaks a test name into chunks by directory and puts the test time as a value in the lowest part, e.g.
foo/bar/baz.html: 1ms
foo/bar/baz1.html: 3ms
becomes
foo: {
bar: {
baz.html: 1,
baz1.html: 3
}
}
"""
trie = {}
for test_result in individual_test_timings:
test = test_result.test_name
add_path_to_trie(test, int(1000 * test_result.test_run_time), trie)
return trie
def _add_perf_metric_for_test(path, time, tests, depth, depth_limit):
"""
Aggregate test time to result for a given test at a specified depth_limit.
"""
if not "/" in path:
tests["tests"][path] = {
"metrics": {
"Time": {
"current": [time],
}}}
return
directory, slash, rest = path.partition("/")
if depth == depth_limit:
if directory not in tests["tests"]:
tests["tests"][directory] = {
"metrics": {
"Time": {
"current": [time],
}}}
else:
tests["tests"][directory]["metrics"]["Time"]["current"][0] += time
return
else:
if directory not in tests["tests"]:
tests["tests"][directory] = {
"metrics": {
"Time": ["Total", "Arithmetic"],
},
"tests": {}
}
_add_perf_metric_for_test(rest, time, tests["tests"][directory], depth + 1, depth_limit)
def perf_metrics_for_test(run_time, individual_test_timings):
"""
Output two performace metrics
1. run time, which is how much time consumed by the layout tests script
2. run time of first-level and second-level of test directories
"""
total_run_time = 0
for test_result in individual_test_timings:
total_run_time += int(1000 * test_result.test_run_time)
perf_metric = {
"layout_tests": {
"metrics": {
"Time": ["Total", "Arithmetic"],
},
"tests": {}
},
"layout_tests_run_time": {
"metrics": {
"Time": {"current": [run_time]},
}}}
for test_result in individual_test_timings:
test = test_result.test_name
# for now, we only send two levels of directories
_add_perf_metric_for_test(test, int(1000 * test_result.test_run_time), perf_metric["layout_tests"], 1, 2)
return perf_metric
# FIXME: We already have a TestResult class in test_results.py
class TestResult(object):
"""A simple class that represents a single test result."""
# Test modifier constants.
(NONE, FAILS, FLAKY, DISABLED) = range(4)
def __init__(self, test, failed=False, elapsed_time=0):
self.test_name = test
self.failed = failed
self.test_run_time = elapsed_time
test_name = test
try:
test_name = test.split('.')[1]
except IndexError:
_log.warn("Invalid test name: %s.", test)
pass
if test_name.startswith('FAILS_'):
self.modifier = self.FAILS
elif test_name.startswith('FLAKY_'):
self.modifier = self.FLAKY
elif test_name.startswith('DISABLED_'):
self.modifier = self.DISABLED
else:
self.modifier = self.NONE
def fixable(self):
return self.failed or self.modifier == self.DISABLED
class JSONResultsGenerator(object):
"""A JSON results generator for generic tests."""
MAX_NUMBER_OF_BUILD_RESULTS_TO_LOG = 750
# Min time (seconds) that will be added to the JSON.
MIN_TIME = 1
# Note that in non-chromium tests those chars are used to indicate
# test modifiers (FAILS, FLAKY, etc) but not actual test results.
PASS_RESULT = "P"
SKIP_RESULT = "X"
FAIL_RESULT = "F"
FLAKY_RESULT = "L"
NO_DATA_RESULT = "N"
MODIFIER_TO_CHAR = {TestResult.NONE: PASS_RESULT,
TestResult.DISABLED: SKIP_RESULT,
TestResult.FAILS: FAIL_RESULT,
TestResult.FLAKY: FLAKY_RESULT}
RESULTS = "results"
TIMES = "times"
TIME = "secondsSinceEpoch"
TESTS = "tests"
FIXABLE_COUNT = "fixableCount"
FIXABLE = "fixableCounts"
ALL_FIXABLE_COUNT = "allFixableCount"
RESULTS_FILENAME = "results.json"
TIMES_MS_FILENAME = "times_ms.json"
INCREMENTAL_RESULTS_FILENAME = "incremental_results.json"
URL_FOR_TEST_LIST_JSON = "http://%s/testfile?builder=%s&name=%s&testlistjson=1&testtype=%s&master=%s"
def __init__(self, port,
results_file_base_path,
test_results_map, svn_repositories=None,
test_type=""):
"""Modifies the results.json file. Grabs it off the archive directory
if it is not found locally.
Args
port: port-specific wrapper
results_file_base_path: Absolute path to the directory containing the
results json file.
test_results_map: A dictionary that maps test_name to TestResult.
svn_repositories: A (json_field_name, svn_path) pair for SVN
repositories that tests rely on. The SVN revision will be
included in the JSON with the given json_field_name.
test_type: test type string (e.g. 'layout-tests').
"""
self._port = port
self._filesystem = port._filesystem
self._executive = port._executive
self._results_directory = results_file_base_path
self._test_results_map = test_results_map
self._test_results = test_results_map.values()
self._svn_repositories = svn_repositories
if not self._svn_repositories:
self._svn_repositories = {}
self._test_type = test_type
self._archived_results = None
def generate_json_output(self):
json_object = self.get_json()
if not json_object:
return False
file_path = self._filesystem.join(self._results_directory, self.INCREMENTAL_RESULTS_FILENAME)
write_json(self._filesystem, json_object, file_path)
return True
def generate_times_ms_file(self):
# FIXME: rename to generate_times_ms_file. This needs to be coordinated with
# changing the calls to this on the chromium build workers.
times = test_timings_trie(self._port, self._test_results_map.values())
file_path = self._filesystem.join(self._results_directory, self.TIMES_MS_FILENAME)
write_json(self._filesystem, times, file_path)
def get_json(self):
"""Gets the results for the results.json file."""
results_for_builder = self._create_results_for_builder_json()
self._insert_failure_summaries(results_for_builder)
# Update the all failing tests with result type and time.
tests = results_for_builder[self.TESTS]
all_failing_tests = self._get_failed_test_names()
all_failing_tests.update(convert_trie_to_flat_paths(tests))
for test in all_failing_tests:
self._insert_test_time_and_result(test, tests)
return dict(
results=results_for_builder,
)
def set_archived_results(self, archived_results):
self._archived_results = archived_results
def _get_test_timing(self, test_name):
"""Returns test timing data (elapsed time) in second
for the given test_name."""
if test_name in self._test_results_map:
# Floor for now to get time in seconds.
return int(self._test_results_map[test_name].test_run_time)
return 0
def _get_failed_test_names(self):
"""Returns a set of failed test names."""
return set([r.test_name for r in self._test_results if r.failed])
def _get_modifier_char(self, test_name):
"""Returns a single char (e.g. SKIP_RESULT, FAIL_RESULT,
PASS_RESULT, NO_DATA_RESULT, etc) that indicates the test modifier
for the given test_name.
"""
if test_name not in self._test_results_map:
return self.__class__.NO_DATA_RESULT
test_result = self._test_results_map[test_name]
if test_result.modifier in self.MODIFIER_TO_CHAR.keys():
return self.MODIFIER_TO_CHAR[test_result.modifier]
return self.__class__.PASS_RESULT
def _get_result_char(self, test_name):
"""Returns a single char (e.g. SKIP_RESULT, FAIL_RESULT,
PASS_RESULT, NO_DATA_RESULT, etc) that indicates the test result
for the given test_name.
"""
if test_name not in self._test_results_map:
return self.__class__.NO_DATA_RESULT
test_result = self._test_results_map[test_name]
if test_result.modifier == TestResult.DISABLED:
return self.__class__.SKIP_RESULT
if test_result.failed:
return self.__class__.FAIL_RESULT
return self.__class__.PASS_RESULT
def _insert_failure_summaries(self, results_for_builder):
"""Inserts aggregate pass/failure statistics into the JSON.
This method reads self._test_results and generates
FIXABLE, FIXABLE_COUNT and ALL_FIXABLE_COUNT entries.
Args:
results_for_builder: Dictionary containing the test results for a
single builder.
"""
# Insert the number of tests that failed or skipped.
fixable_count = len([r for r in self._test_results if r.fixable()])
self._insert_item_into_raw_list(results_for_builder,
fixable_count, self.FIXABLE_COUNT)
# Create a test modifiers (FAILS, FLAKY etc) summary dictionary.
entry = {}
for test_name in iterkeys(self._test_results_map):
result_char = self._get_modifier_char(test_name)
entry[result_char] = entry.get(result_char, 0) + 1
# Insert the pass/skip/failure summary dictionary.
self._insert_item_into_raw_list(results_for_builder, entry,
self.FIXABLE)
# Insert the number of all the tests that are supposed to pass.
all_test_count = len(self._test_results)
self._insert_item_into_raw_list(results_for_builder,
all_test_count, self.ALL_FIXABLE_COUNT)
def _insert_item_into_raw_list(self, results_for_builder, item, key):
"""Inserts the item into the list with the given key in the results for
this builder. Creates the list if no such list exists.
Args:
results_for_builder: Dictionary containing the test results for a
single builder.
item: Number or string to insert into the list.
key: Key in results_for_builder for the list to insert into.
"""
if key in results_for_builder:
raw_list = results_for_builder[key]
else:
raw_list = []
raw_list.insert(0, item)
raw_list = raw_list[:self.MAX_NUMBER_OF_BUILD_RESULTS_TO_LOG]
results_for_builder[key] = raw_list
def _insert_item_run_length_encoded(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]:
num_results = encoded_results[0][0]
if num_results <= self.MAX_NUMBER_OF_BUILD_RESULTS_TO_LOG:
encoded_results[0][0] = num_results + 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 _insert_test_time_and_result(self, test_name, tests):
""" Insert a test item with its results to the given tests dictionary.
Args:
tests: Dictionary containing test result entries.
"""
result = self._get_result_char(test_name)
time = self._get_test_timing(test_name)
this_test = tests
for segment in test_name.split("/"):
if segment not in this_test:
this_test[segment] = {}
this_test = this_test[segment]
if not len(this_test):
self._populate_results_and_times_json(this_test)
if self.RESULTS in this_test:
self._insert_item_run_length_encoded(result, this_test[self.RESULTS])
else:
this_test[self.RESULTS] = [[1, result]]
if self.TIMES in this_test:
self._insert_item_run_length_encoded(time, this_test[self.TIMES])
else:
this_test[self.TIMES] = [[1, time]]
def _populate_results_and_times_json(self, results_and_times):
results_and_times[self.RESULTS] = []
results_and_times[self.TIMES] = []
return results_and_times
def _create_results_for_builder_json(self):
results_for_builder = {}
results_for_builder[self.TESTS] = {}
return results_for_builder