1 # Copyright (C) 2013 Adobe Systems Incorporated. All rights reserved. 2 # 3 # Redistribution and use in source and binary forms, with or without 4 # modification, are permitted provided that the following conditions 5 # are met: 6 # 7 # 1. Redistributions of source code must retain the above 8 # copyright notice, this list of conditions and the following 9 # disclaimer. 10 # 2. Redistributions in binary form must reproduce the above 11 # copyright notice, this list of conditions and the following 12 # disclaimer in the documentation and/or other materials 13 # provided with the distribution. 14 # 15 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER "AS IS" AND ANY 16 # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 18 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE 19 # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, 20 # OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 21 # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 22 # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 23 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR 24 # TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 25 # THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 26 # SUCH DAMAGE. 27 28 """ 29 This script imports a directory of W3C tests into WebKit. 30 31 This script will import the tests into WebKit following these rules: 32 33 - By default, all tests are imported under LayoutTests/w3c/[repo-name]. 34 35 - By default, only reftests and jstest are imported. This can be overridden 36 with a -a or --all argument 37 38 - Also by default, if test files by the same name already exist in the 39 destination directory, they are overwritten with the idea that running 40 this script would refresh files periodically. This can also be 41 overridden by a -n or --no-overwrite flag 42 43 - All files are converted to work in WebKit: 44 1. Paths to testharness.js and vendor-prefix.js files are modified to 45 point to Webkit's copy of them in LayoutTests/resources, using the 46 correct relative path from the new location. 47 2. All CSS properties requiring the -webkit-vendor prefix are prefixed 48 (the list of what needs prefixes is read from Source/WebCore/CSS/CSSProperties.in). 49 3. Each reftest has its own copy of its reference file following 50 the naming conventions new-run-webkit-tests expects. 51 4. If a reference files lives outside the directory of the test that 52 uses it, it is checked for paths to support files as it will be 53 imported into a different relative position to the test file 54 (in the same directory). 55 5. Any tags with the class "instructions" have style="display:none" added 56 to them. Some w3c tests contain instructions to manual testers which we 57 want to strip out (the test result parser only recognizes pure testharness.js 58 output and not those instructions). 59 60 - Upon completion, script outputs the total number tests imported, broken 61 down by test type 62 63 - Also upon completion, if we are not importing the files in place, each 64 directory where files are imported will have a w3c-import.log file written with 65 a timestamp, the W3C Mercurial changeset if available, the list of CSS 66 properties used that require prefixes, the list of imported files, and 67 guidance for future test modification and maintenance. On subsequent 68 imports, this file is read to determine if files have been 69 removed in the newer changesets. The script removes these files 70 accordingly. 71 """ 72 73 # FIXME: Change this file to use the Host abstractions rather that os, sys, shutils, etc. 74 75 import datetime 76 import logging 77 import mimetypes 78 import optparse 79 import os 80 import shutil 81 import sys 82 83 from webkitpy.common.host import Host 84 from webkitpy.common.webkit_finder import WebKitFinder 85 from webkitpy.common.system.executive import ScriptError 86 from webkitpy.layout_tests.models.test_expectations import TestExpectationParser 87 from webkitpy.w3c.test_parser import TestParser 88 from webkitpy.w3c.test_converter import convert_for_webkit 89 90 91 CHANGESET_NOT_AVAILABLE = 'Not Available' 92 93 94 _log = logging.getLogger(__name__) 95 96 97 def main(_argv, _stdout, _stderr): 98 options, args = parse_args() 99 dir_to_import = os.path.normpath(os.path.abspath(args[0])) 100 if len(args) == 1: 101 top_of_repo = dir_to_import 102 else: 103 top_of_repo = os.path.normpath(os.path.abspath(args[1])) 104 105 if not os.path.exists(dir_to_import): 106 sys.exit('Directory %s not found!' % dir_to_import) 107 if not os.path.exists(top_of_repo): 108 sys.exit('Repository directory %s not found!' % top_of_repo) 109 if top_of_repo not in dir_to_import: 110 sys.exit('Repository directory %s must be a parent of %s' % (top_of_repo, dir_to_import)) 111 112 configure_logging() 113 test_importer = TestImporter(Host(), dir_to_import, top_of_repo, options) 114 test_importer.do_import() 115 116 117 def configure_logging(): 118 class LogHandler(logging.StreamHandler): 119 120 def format(self, record): 121 if record.levelno > logging.INFO: 122 return "%s: %s" % (record.levelname, record.getMessage()) 123 return record.getMessage() 124 125 logger = logging.getLogger() 126 logger.setLevel(logging.INFO) 127 handler = LogHandler() 128 handler.setLevel(logging.INFO) 129 logger.addHandler(handler) 130 return handler 131 132 133 def parse_args(): 134 parser = optparse.OptionParser(usage='usage: %prog [options] [dir_to_import] [top_of_repo]') 135 parser.add_option('-n', '--no-overwrite', dest='overwrite', action='store_false', default=True, 136 help='Flag to prevent duplicate test files from overwriting existing tests. By default, they will be overwritten.') 137 parser.add_option('-a', '--all', action='store_true', default=False, 138 help='Import all tests including reftests, JS tests, and manual/pixel tests. By default, only reftests and JS tests are imported.') 139 parser.add_option('-d', '--dest-dir', dest='destination', default='w3c', 140 help='Import into a specified directory relative to the LayoutTests root. By default, files are imported under LayoutTests/w3c.') 141 parser.add_option('--ignore-expectations', action='store_true', default=False, 142 help='Ignore the W3CImportExpectations file and import everything.') 143 parser.add_option('--dry-run', action='store_true', default=False, 144 help='Dryrun only (don\'t actually write any results).') 145 146 options, args = parser.parse_args() 147 if len(args) > 2: 148 parser.error('Incorrect number of arguments') 149 elif len(args) == 0: 150 args = (os.getcwd(),) 151 return options, args 152 153 154 class TestImporter(object): 155 156 def __init__(self, host, dir_to_import, top_of_repo, options): 157 self.host = host 158 self.dir_to_import = dir_to_import 159 self.top_of_repo = top_of_repo 160 self.options = options 161 162 self.filesystem = self.host.filesystem 163 self.webkit_finder = WebKitFinder(self.filesystem) 164 self._webkit_root = self.webkit_finder.webkit_base() 165 self.layout_tests_dir = self.webkit_finder.path_from_webkit_base('LayoutTests') 166 self.destination_directory = self.filesystem.normpath(self.filesystem.join(self.layout_tests_dir, options.destination, 167 self.filesystem.basename(self.top_of_repo))) 168 self.import_in_place = (self.dir_to_import == self.destination_directory) 169 self.dir_above_repo = self.filesystem.dirname(self.top_of_repo) 170 171 self.changeset = CHANGESET_NOT_AVAILABLE 172 173 self.import_list = [] 174 175 def do_import(self): 176 _log.info("Importing %s into %s", self.dir_to_import, self.destination_directory) 177 self.find_importable_tests(self.dir_to_import) 178 self.load_changeset() 179 self.import_tests() 180 181 def load_changeset(self): 182 """Returns the current changeset from mercurial or "Not Available".""" 183 try: 184 self.changeset = self.host.executive.run_command(['hg', 'tip']).split('changeset:')[1] 185 except (OSError, ScriptError): 186 self.changeset = CHANGESET_NOT_AVAILABLE 187 188 def find_importable_tests(self, directory): 189 # FIXME: use filesystem 190 paths_to_skip = self.find_paths_to_skip() 191 192 for root, dirs, files in os.walk(directory): 193 cur_dir = root.replace(self.dir_above_repo + '/', '') + '/' 194 _log.info(' scanning ' + cur_dir + '...') 195 total_tests = 0 196 reftests = 0 197 jstests = 0 198 199 DIRS_TO_SKIP = ('.git', '.hg') 200 if dirs: 201 for d in DIRS_TO_SKIP: 202 if d in dirs: 203 dirs.remove(d) 204 205 for path in paths_to_skip: 206 path_base = path.replace(self.options.destination + '/', '') 207 path_base = path_base.replace(cur_dir, '') 208 path_full = self.filesystem.join(root, path_base) 209 if path_base in dirs: 210 dirs.remove(path_base) 211 if not self.options.dry_run and self.import_in_place: 212 _log.info(" pruning %s" % path_base) 213 self.filesystem.rmtree(path_full) 214 else: 215 _log.info(" skipping %s" % path_base) 216 217 218 copy_list = [] 219 220 for filename in files: 221 path_full = self.filesystem.join(root, filename) 222 path_base = path_full.replace(self.layout_tests_dir + '/', '') 223 if path_base in paths_to_skip: 224 if not self.options.dry_run and self.import_in_place: 225 _log.info(" pruning %s" % path_base) 226 self.filesystem.remove(path_full) 227 continue 228 else: 229 continue 230 # FIXME: This block should really be a separate function, but the early-continues make that difficult. 231 232 if filename.startswith('.') or filename.endswith('.pl'): 233 continue # For some reason the w3c repo contains random perl scripts we don't care about. 234 235 fullpath = os.path.join(root, filename) 236 237 mimetype = mimetypes.guess_type(fullpath) 238 if not 'html' in str(mimetype[0]) and not 'application/xhtml+xml' in str(mimetype[0]) and not 'application/xml' in str(mimetype[0]): 239 copy_list.append({'src': fullpath, 'dest': filename}) 240 continue 241 242 if root.endswith('resources'): 243 copy_list.append({'src': fullpath, 'dest': filename}) 244 continue 245 246 test_parser = TestParser(vars(self.options), filename=fullpath) 247 test_info = test_parser.analyze_test() 248 if test_info is None: 249 continue 250 251 if 'reference' in test_info.keys(): 252 reftests += 1 253 total_tests += 1 254 test_basename = os.path.basename(test_info['test']) 255 256 # Add the ref file, following WebKit style. 257 # FIXME: Ideally we'd support reading the metadata 258 # directly rather than relying on a naming convention. 259 # Using a naming convention creates duplicate copies of the 260 # reference files. 261 ref_file = os.path.splitext(test_basename)[0] + '-expected' 262 ref_file += os.path.splitext(test_basename)[1] 263 264 copy_list.append({'src': test_info['reference'], 'dest': ref_file}) 265 copy_list.append({'src': test_info['test'], 'dest': filename}) 266 267 # Update any support files that need to move as well to remain relative to the -expected file. 268 if 'refsupport' in test_info.keys(): 269 for support_file in test_info['refsupport']: 270 source_file = os.path.join(os.path.dirname(test_info['reference']), support_file) 271 source_file = os.path.normpath(source_file) 272 273 # Keep the dest as it was 274 to_copy = {'src': source_file, 'dest': support_file} 275 276 # Only add it once 277 if not(to_copy in copy_list): 278 copy_list.append(to_copy) 279 elif 'jstest' in test_info.keys(): 280 jstests += 1 281 total_tests += 1 282 copy_list.append({'src': fullpath, 'dest': filename}) 283 else: 284 total_tests += 1 285 copy_list.append({'src': fullpath, 'dest': filename}) 286 287 if not total_tests: 288 # We can skip the support directory if no tests were found. 289 if 'support' in dirs: 290 dirs.remove('support') 291 292 if copy_list: 293 # Only add this directory to the list if there's something to import 294 self.import_list.append({'dirname': root, 'copy_list': copy_list, 295 'reftests': reftests, 'jstests': jstests, 'total_tests': total_tests}) 296 297 def find_paths_to_skip(self): 298 if self.options.ignore_expectations: 299 return set() 300 301 paths_to_skip = set() 302 port = self.host.port_factory.get() 303 w3c_import_expectations_path = self.webkit_finder.path_from_webkit_base('LayoutTests', 'W3CImportExpectations') 304 w3c_import_expectations = self.filesystem.read_text_file(w3c_import_expectations_path) 305 parser = TestExpectationParser(port, full_test_list=(), is_lint_mode=False) 306 expectation_lines = parser.parse(w3c_import_expectations_path, w3c_import_expectations) 307 for line in expectation_lines: 308 if 'SKIP' in line.expectations: 309 if line.specifiers: 310 _log.warning("W3CImportExpectations:%s should not have any specifiers" % line.line_numbers) 311 continue 312 paths_to_skip.add(line.name) 313 return paths_to_skip 314 315 def import_tests(self): 316 total_imported_tests = 0 317 total_imported_reftests = 0 318 total_imported_jstests = 0 319 total_prefixed_properties = {} 320 321 for dir_to_copy in self.import_list: 322 total_imported_tests += dir_to_copy['total_tests'] 323 total_imported_reftests += dir_to_copy['reftests'] 324 total_imported_jstests += dir_to_copy['jstests'] 325 326 prefixed_properties = [] 327 328 if not dir_to_copy['copy_list']: 329 continue 330 331 orig_path = dir_to_copy['dirname'] 332 333 subpath = os.path.relpath(orig_path, self.top_of_repo) 334 new_path = os.path.join(self.destination_directory, subpath) 335 336 if not(os.path.exists(new_path)): 337 os.makedirs(new_path) 338 339 copied_files = [] 340 341 for file_to_copy in dir_to_copy['copy_list']: 342 # FIXME: Split this block into a separate function. 343 orig_filepath = os.path.normpath(file_to_copy['src']) 344 345 if os.path.isdir(orig_filepath): 346 # FIXME: Figure out what is triggering this and what to do about it. 347 _log.error('%s refers to a directory' % orig_filepath) 348 continue 349 350 if not(os.path.exists(orig_filepath)): 351 _log.warning('%s not found. Possible error in the test.', orig_filepath) 352 continue 353 354 new_filepath = os.path.join(new_path, file_to_copy['dest']) 355 356 if not(os.path.exists(os.path.dirname(new_filepath))): 357 if not self.import_in_place and not self.options.dry_run: 358 os.makedirs(os.path.dirname(new_filepath)) 359 360 relpath = os.path.relpath(new_filepath, self.layout_tests_dir) 361 if not self.options.overwrite and os.path.exists(new_filepath): 362 _log.info(' skipping %s' % relpath) 363 else: 364 # FIXME: Maybe doing a file diff is in order here for existing files? 365 # In other words, there's no sense in overwriting identical files, but 366 # there's no harm in copying the identical thing. 367 _log.info(' %s' % relpath) 368 369 # Only html, xml, or css should be converted 370 # FIXME: Eventually, so should js when support is added for this type of conversion 371 mimetype = mimetypes.guess_type(orig_filepath) 372 if 'html' in str(mimetype[0]) or 'xml' in str(mimetype[0]) or 'css' in str(mimetype[0]): 373 converted_file = convert_for_webkit(new_path, filename=orig_filepath) 374 375 if not converted_file: 376 if not self.import_in_place and not self.options.dry_run: 377 shutil.copyfile(orig_filepath, new_filepath) # The file was unmodified. 378 else: 379 for prefixed_property in converted_file[0]: 380 total_prefixed_properties.setdefault(prefixed_property, 0) 381 total_prefixed_properties[prefixed_property] += 1 382 383 prefixed_properties.extend(set(converted_file[0]) - set(prefixed_properties)) 384 if not self.options.dry_run: 385 outfile = open(new_filepath, 'wb') 386 outfile.write(converted_file[1]) 387 outfile.close() 388 else: 389 if not self.import_in_place and not self.options.dry_run: 390 shutil.copyfile(orig_filepath, new_filepath) 391 392 copied_files.append(new_filepath.replace(self._webkit_root, '')) 393 394 _log.info('') 395 _log.info('Import complete') 396 _log.info('') 397 _log.info('IMPORTED %d TOTAL TESTS', total_imported_tests) 398 _log.info('Imported %d reftests', total_imported_reftests) 399 _log.info('Imported %d JS tests', total_imported_jstests) 400 _log.info('Imported %d pixel/manual tests', total_imported_tests - total_imported_jstests - total_imported_reftests) 401 _log.info('') 402 403 if total_prefixed_properties: 404 _log.info('Properties needing prefixes (by count):') 405 for prefixed_property in sorted(total_prefixed_properties, key=lambda p: total_prefixed_properties[p]): 406 _log.info(' %s: %s', prefixed_property, total_prefixed_properties[prefixed_property]) 407 408 def setup_destination_directory(self): 409 """ Creates a destination directory that mirrors that of the source directory """ 410 411 new_subpath = self.dir_to_import[len(self.top_of_repo):] 412 413 destination_directory = os.path.join(self.destination_directory, new_subpath) 414 415 if not os.path.exists(destination_directory): 416 os.makedirs(destination_directory) 417 418 _log.info('Tests will be imported into: %s', destination_directory) 419