1# Copyright (c) 2012 The Chromium Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5"""Layout tests module that is necessary for the layout analyzer. 6 7Layout tests are stored in an SVN repository and LayoutTestCaseManager collects 8these layout test cases (including description). 9""" 10 11import copy 12import csv 13import locale 14import re 15import sys 16import urllib2 17 18import pysvn 19 20# LayoutTests SVN root location. 21DEFAULT_LAYOUTTEST_LOCATION = ( 22 'http://src.chromium.org/blink/trunk/LayoutTests/') 23# LayoutTests SVN view link 24DEFAULT_LAYOUTTEST_SVN_VIEW_LOCATION = ( 25 'http://src.chromium.org/viewvc/blink/trunk/LayoutTests/') 26 27 28# When parsing the test HTML file and finding the test description, 29# this script tries to find the test description using sentences 30# starting with these keywords. This is adhoc but it is the only way 31# since there is no standard for writing test description. 32KEYWORDS_FOR_TEST_DESCRIPTION = ['This test', 'Tests that', 'Test '] 33 34# If cannot find the keywords, this script tries to find test case 35# description by the following tags. 36TAGS_FOR_TEST_DESCRIPTION = ['title', 'p', 'div'] 37 38# If cannot find the tags, this script tries to find the test case 39# description in the sentence containing following words. 40KEYWORD_FOR_TEST_DESCRIPTION_FAIL_SAFE = ['PASSED ', 'PASS:'] 41 42 43class LayoutTests(object): 44 """A class to store test names in layout tests. 45 46 The test names (including regular expression patterns) are read from a CSV 47 file and used for getting layout test names from repository. 48 """ 49 50 def __init__(self, layouttest_root_path=DEFAULT_LAYOUTTEST_LOCATION, 51 parent_location_list=None, filter_names=None, 52 recursion=False): 53 """Initialize LayoutTests using root and CSV file. 54 55 Args: 56 layouttest_root_path: A location string where layout tests are stored. 57 parent_location_list: A list of parent directories that are needed for 58 getting layout tests. 59 filter_names: A list of test name patterns that are used for filtering 60 test names (e.g., media/*.html). 61 recursion: a boolean indicating whether the test names are sought 62 recursively. 63 """ 64 65 if layouttest_root_path.startswith('http://'): 66 name_map = self.GetLayoutTestNamesFromSVN(parent_location_list, 67 layouttest_root_path, 68 recursion) 69 else: 70 # TODO(imasaki): support other forms such as CSV for reading test names. 71 pass 72 self.name_map = copy.copy(name_map) 73 if filter_names: 74 # Filter names. 75 for lt_name in name_map.iterkeys(): 76 match = False 77 for filter_name in filter_names: 78 if re.search(filter_name, lt_name): 79 match = True 80 break 81 if not match: 82 del self.name_map[lt_name] 83 # We get description only for the filtered names. 84 for lt_name in self.name_map.iterkeys(): 85 self.name_map[lt_name] = 'No description available' 86 87 @staticmethod 88 def ExtractTestDescription(txt): 89 """Extract the description description from test code in HTML. 90 91 Currently, we have 4 rules described in the code below. 92 (This example falls into rule 1): 93 <p> 94 This tests the intrinsic size of a video element is the default 95 300,150 before metadata is loaded, and 0,0 after 96 metadata is loaded for an audio-only file. 97 </p> 98 The strategy is very adhoc since the original test case files 99 (in HTML format) do not have standard way to store test description. 100 101 Args: 102 txt: A HTML text which may or may not contain test description. 103 104 Returns: 105 A string that contains test description. Returns 'UNKNOWN' if the 106 test description is not found. 107 """ 108 # (1) Try to find test description that contains keywords such as 109 # 'test that' and surrounded by p tag. 110 # This is the most common case. 111 for keyword in KEYWORDS_FOR_TEST_DESCRIPTION: 112 # Try to find <p> and </p>. 113 pattern = r'<p>(.*' + keyword + '.*)</p>' 114 matches = re.search(pattern, txt) 115 if matches is not None: 116 return matches.group(1).strip() 117 118 # (2) Try to find it by using more generic keywords such as 'PASS' etc. 119 for keyword in KEYWORD_FOR_TEST_DESCRIPTION_FAIL_SAFE: 120 # Try to find new lines. 121 pattern = r'\n(.*' + keyword + '.*)\n' 122 matches = re.search(pattern, txt) 123 if matches is not None: 124 # Remove 'p' tag. 125 text = matches.group(1).strip() 126 return text.replace('<p>', '').replace('</p>', '') 127 128 # (3) Try to find it by using HTML tag such as title. 129 for tag in TAGS_FOR_TEST_DESCRIPTION: 130 pattern = r'<' + tag + '>(.*)</' + tag + '>' 131 matches = re.search(pattern, txt) 132 if matches is not None: 133 return matches.group(1).strip() 134 135 # (4) Try to find it by using test description and remove 'p' tag. 136 for keyword in KEYWORDS_FOR_TEST_DESCRIPTION: 137 # Try to find <p> and </p>. 138 pattern = r'\n(.*' + keyword + '.*)\n' 139 matches = re.search(pattern, txt) 140 if matches is not None: 141 # Remove 'p' tag. 142 text = matches.group(1).strip() 143 return text.replace('<p>', '').replace('</p>', '') 144 145 # (5) cannot find test description using existing rules. 146 return 'UNKNOWN' 147 148 @staticmethod 149 def GetLayoutTestNamesFromSVN(parent_location_list, 150 layouttest_root_path, recursion): 151 """Get LayoutTest names from SVN. 152 153 Args: 154 parent_location_list: a list of locations of parent directories. This is 155 used when getting layout tests using PySVN.list(). 156 layouttest_root_path: the root path of layout tests directory. 157 recursion: a boolean indicating whether the test names are sought 158 recursively. 159 160 Returns: 161 a map containing test names as keys for de-dupe. 162 """ 163 client = pysvn.Client() 164 # Get directory structure in the repository SVN. 165 name_map = {} 166 for parent_location in parent_location_list: 167 if parent_location.endswith('/'): 168 full_path = layouttest_root_path + parent_location 169 try: 170 file_list = client.list(full_path, recurse=recursion) 171 for file_name in file_list: 172 if sys.stdout.isatty(): 173 default_encoding = sys.stdout.encoding 174 else: 175 default_encoding = locale.getpreferredencoding() 176 file_name = file_name[0].repos_path.encode(default_encoding) 177 # Remove the word '/truck/LayoutTests'. 178 file_name = file_name.replace('/trunk/LayoutTests/', '') 179 if file_name.endswith('.html'): 180 name_map[file_name] = True 181 except: 182 print 'Unable to list tests in %s.' % full_path 183 return name_map 184 185 @staticmethod 186 def GetLayoutTestNamesFromCSV(csv_file_path): 187 """Get layout test names from CSV file. 188 189 Args: 190 csv_file_path: the path for the CSV file containing test names (including 191 regular expression patterns). The CSV file content has one column and 192 each row contains a test name. 193 194 Returns: 195 a list of test names in string. 196 """ 197 file_object = file(csv_file_path, 'r') 198 reader = csv.reader(file_object) 199 names = [row[0] for row in reader] 200 file_object.close() 201 return names 202 203 @staticmethod 204 def GetParentDirectoryList(names): 205 """Get parent directory list from test names. 206 207 Args: 208 names: a list of test names. The test names also have path information as 209 well (e.g., media/video-zoom.html). 210 211 Returns: 212 a list of parent directories for the given test names. 213 """ 214 pd_map = {} 215 for name in names: 216 p_dir = name[0:name.rfind('/') + 1] 217 pd_map[p_dir] = True 218 return list(pd_map.iterkeys()) 219 220 def JoinWithTestExpectation(self, test_expectations): 221 """Join layout tests with the test expectation file using test name as key. 222 223 Args: 224 test_expectations: a test expectations object. 225 226 Returns: 227 test_info_map contains test name as key and another map as value. The 228 other map contains test description and the test expectation 229 information which contains keyword (e.g., 'GPU') as key (we do 230 not care about values). The map data structure is used since we 231 have to look up these keywords several times. 232 """ 233 test_info_map = {} 234 for (lt_name, desc) in self.name_map.items(): 235 test_info_map[lt_name] = {} 236 test_info_map[lt_name]['desc'] = desc 237 for (te_name, te_info) in ( 238 test_expectations.all_test_expectation_info.items()): 239 if te_name == lt_name or ( 240 te_name in lt_name and te_name.endswith('/')): 241 # Only keep the first match when found. 242 test_info_map[lt_name]['te_info'] = te_info 243 break 244 return test_info_map 245