blob: 8d3003815bb30ac1b09ef82c59fa966bdd7ebbab [file] [log] [blame]
# Copyright (c) 2012 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Layout tests module that is necessary for the layout analyzer.
Layout tests are stored in an SVN repository and LayoutTestCaseManager collects
these layout test cases (including description).
"""
import copy
import csv
import locale
import re
import sys
import urllib2
import pysvn
# LayoutTests SVN root location.
DEFAULT_LAYOUTTEST_LOCATION = (
'http://src.chromium.org/blink/trunk/LayoutTests/')
# LayoutTests SVN view link
DEFAULT_LAYOUTTEST_SVN_VIEW_LOCATION = (
'http://src.chromium.org/viewvc/blink/trunk/LayoutTests/')
# When parsing the test HTML file and finding the test description,
# this script tries to find the test description using sentences
# starting with these keywords. This is adhoc but it is the only way
# since there is no standard for writing test description.
KEYWORDS_FOR_TEST_DESCRIPTION = ['This test', 'Tests that', 'Test ']
# If cannot find the keywords, this script tries to find test case
# description by the following tags.
TAGS_FOR_TEST_DESCRIPTION = ['title', 'p', 'div']
# If cannot find the tags, this script tries to find the test case
# description in the sentence containing following words.
KEYWORD_FOR_TEST_DESCRIPTION_FAIL_SAFE = ['PASSED ', 'PASS:']
class LayoutTests(object):
"""A class to store test names in layout tests.
The test names (including regular expression patterns) are read from a CSV
file and used for getting layout test names from repository.
"""
def __init__(self, layouttest_root_path=DEFAULT_LAYOUTTEST_LOCATION,
parent_location_list=None, filter_names=None,
recursion=False):
"""Initialize LayoutTests using root and CSV file.
Args:
layouttest_root_path: A location string where layout tests are stored.
parent_location_list: A list of parent directories that are needed for
getting layout tests.
filter_names: A list of test name patterns that are used for filtering
test names (e.g., media/*.html).
recursion: a boolean indicating whether the test names are sought
recursively.
"""
if layouttest_root_path.startswith('http://'):
name_map = self.GetLayoutTestNamesFromSVN(parent_location_list,
layouttest_root_path,
recursion)
else:
# TODO(imasaki): support other forms such as CSV for reading test names.
pass
self.name_map = copy.copy(name_map)
if filter_names:
# Filter names.
for lt_name in name_map.iterkeys():
match = False
for filter_name in filter_names:
if re.search(filter_name, lt_name):
match = True
break
if not match:
del self.name_map[lt_name]
# We get description only for the filtered names.
for lt_name in self.name_map.iterkeys():
self.name_map[lt_name] = 'No description available'
@staticmethod
def ExtractTestDescription(txt):
"""Extract the description description from test code in HTML.
Currently, we have 4 rules described in the code below.
(This example falls into rule 1):
<p>
This tests the intrinsic size of a video element is the default
300,150 before metadata is loaded, and 0,0 after
metadata is loaded for an audio-only file.
</p>
The strategy is very adhoc since the original test case files
(in HTML format) do not have standard way to store test description.
Args:
txt: A HTML text which may or may not contain test description.
Returns:
A string that contains test description. Returns 'UNKNOWN' if the
test description is not found.
"""
# (1) Try to find test description that contains keywords such as
# 'test that' and surrounded by p tag.
# This is the most common case.
for keyword in KEYWORDS_FOR_TEST_DESCRIPTION:
# Try to find <p> and </p>.
pattern = r'<p>(.*' + keyword + '.*)</p>'
matches = re.search(pattern, txt)
if matches is not None:
return matches.group(1).strip()
# (2) Try to find it by using more generic keywords such as 'PASS' etc.
for keyword in KEYWORD_FOR_TEST_DESCRIPTION_FAIL_SAFE:
# Try to find new lines.
pattern = r'\n(.*' + keyword + '.*)\n'
matches = re.search(pattern, txt)
if matches is not None:
# Remove 'p' tag.
text = matches.group(1).strip()
return text.replace('<p>', '').replace('</p>', '')
# (3) Try to find it by using HTML tag such as title.
for tag in TAGS_FOR_TEST_DESCRIPTION:
pattern = r'<' + tag + '>(.*)</' + tag + '>'
matches = re.search(pattern, txt)
if matches is not None:
return matches.group(1).strip()
# (4) Try to find it by using test description and remove 'p' tag.
for keyword in KEYWORDS_FOR_TEST_DESCRIPTION:
# Try to find <p> and </p>.
pattern = r'\n(.*' + keyword + '.*)\n'
matches = re.search(pattern, txt)
if matches is not None:
# Remove 'p' tag.
text = matches.group(1).strip()
return text.replace('<p>', '').replace('</p>', '')
# (5) cannot find test description using existing rules.
return 'UNKNOWN'
@staticmethod
def GetLayoutTestNamesFromSVN(parent_location_list,
layouttest_root_path, recursion):
"""Get LayoutTest names from SVN.
Args:
parent_location_list: a list of locations of parent directories. This is
used when getting layout tests using PySVN.list().
layouttest_root_path: the root path of layout tests directory.
recursion: a boolean indicating whether the test names are sought
recursively.
Returns:
a map containing test names as keys for de-dupe.
"""
client = pysvn.Client()
# Get directory structure in the repository SVN.
name_map = {}
for parent_location in parent_location_list:
if parent_location.endswith('/'):
full_path = layouttest_root_path + parent_location
try:
file_list = client.list(full_path, recurse=recursion)
for file_name in file_list:
if sys.stdout.isatty():
default_encoding = sys.stdout.encoding
else:
default_encoding = locale.getpreferredencoding()
file_name = file_name[0].repos_path.encode(default_encoding)
# Remove the word '/truck/LayoutTests'.
file_name = file_name.replace('/trunk/LayoutTests/', '')
if file_name.endswith('.html'):
name_map[file_name] = True
except:
print 'Unable to list tests in %s.' % full_path
return name_map
@staticmethod
def GetLayoutTestNamesFromCSV(csv_file_path):
"""Get layout test names from CSV file.
Args:
csv_file_path: the path for the CSV file containing test names (including
regular expression patterns). The CSV file content has one column and
each row contains a test name.
Returns:
a list of test names in string.
"""
file_object = file(csv_file_path, 'r')
reader = csv.reader(file_object)
names = [row[0] for row in reader]
file_object.close()
return names
@staticmethod
def GetParentDirectoryList(names):
"""Get parent directory list from test names.
Args:
names: a list of test names. The test names also have path information as
well (e.g., media/video-zoom.html).
Returns:
a list of parent directories for the given test names.
"""
pd_map = {}
for name in names:
p_dir = name[0:name.rfind('/') + 1]
pd_map[p_dir] = True
return list(pd_map.iterkeys())
def JoinWithTestExpectation(self, test_expectations):
"""Join layout tests with the test expectation file using test name as key.
Args:
test_expectations: a test expectations object.
Returns:
test_info_map contains test name as key and another map as value. The
other map contains test description and the test expectation
information which contains keyword (e.g., 'GPU') as key (we do
not care about values). The map data structure is used since we
have to look up these keywords several times.
"""
test_info_map = {}
for (lt_name, desc) in self.name_map.items():
test_info_map[lt_name] = {}
test_info_map[lt_name]['desc'] = desc
for (te_name, te_info) in (
test_expectations.all_test_expectation_info.items()):
if te_name == lt_name or (
te_name in lt_name and te_name.endswith('/')):
# Only keep the first match when found.
test_info_map[lt_name]['te_info'] = te_info
break
return test_info_map