| OLD | NEW |
| (Empty) |
| 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 tests/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 tests/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/CSS
Properties.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" add
ed | |
| 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 testh
arness.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 writte
n 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, shut
ils, 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_fa
lse', default=True, | |
| 136 help='Flag to prevent duplicate test files from overwriting existing tes
ts. 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 te
sts. 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 tests root. By d
efault, files are imported under tests/w3c.') | |
| 141 parser.add_option('--ignore-expectations', action='store_true', default=Fals
e, | |
| 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('tests'
) | |
| 166 self.destination_directory = self.filesystem.normpath(self.filesystem.jo
in(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 | |
| 170 self.changeset = CHANGESET_NOT_AVAILABLE | |
| 171 | |
| 172 self.import_list = [] | |
| 173 | |
| 174 def do_import(self): | |
| 175 _log.info("Importing %s into %s", self.dir_to_import, self.destination_d
irectory) | |
| 176 self.find_importable_tests(self.dir_to_import) | |
| 177 self.load_changeset() | |
| 178 self.import_tests() | |
| 179 | |
| 180 def load_changeset(self): | |
| 181 """Returns the current changeset from mercurial or "Not Available".""" | |
| 182 try: | |
| 183 self.changeset = self.host.executive.run_command(['hg', 'tip']).spli
t('changeset:')[1] | |
| 184 except (OSError, ScriptError): | |
| 185 self.changeset = CHANGESET_NOT_AVAILABLE | |
| 186 | |
| 187 def find_importable_tests(self, directory): | |
| 188 # FIXME: use filesystem | |
| 189 paths_to_skip = self.find_paths_to_skip() | |
| 190 | |
| 191 for root, dirs, files in os.walk(directory): | |
| 192 cur_dir = root.replace(self.layout_tests_dir + '/', '') + '/' | |
| 193 _log.info(' scanning ' + cur_dir + '...') | |
| 194 total_tests = 0 | |
| 195 reftests = 0 | |
| 196 jstests = 0 | |
| 197 | |
| 198 DIRS_TO_SKIP = ('.git', '.hg') | |
| 199 if dirs: | |
| 200 for d in DIRS_TO_SKIP: | |
| 201 if d in dirs: | |
| 202 dirs.remove(d) | |
| 203 | |
| 204 for path in paths_to_skip: | |
| 205 path_base = path.replace(cur_dir, '') | |
| 206 path_full = self.filesystem.join(root, path_base) | |
| 207 if path_base in dirs: | |
| 208 dirs.remove(path_base) | |
| 209 if not self.options.dry_run and self.import_in_place: | |
| 210 _log.info(" pruning %s" % path_base) | |
| 211 self.filesystem.rmtree(path_full) | |
| 212 | |
| 213 copy_list = [] | |
| 214 | |
| 215 for filename in files: | |
| 216 path_full = self.filesystem.join(root, filename) | |
| 217 path_base = path_full.replace(self.layout_tests_dir + '/', '') | |
| 218 if path_base in paths_to_skip: | |
| 219 if not self.options.dry_run and self.import_in_place: | |
| 220 _log.info(" pruning %s" % path_base) | |
| 221 self.filesystem.remove(path_full) | |
| 222 continue | |
| 223 # FIXME: This block should really be a separate function, but th
e early-continues make that difficult. | |
| 224 | |
| 225 if filename.startswith('.') or filename.endswith('.pl'): | |
| 226 continue # For some reason the w3c repo contains random per
l scripts we don't care about. | |
| 227 | |
| 228 fullpath = os.path.join(root, filename) | |
| 229 | |
| 230 mimetype = mimetypes.guess_type(fullpath) | |
| 231 if not 'html' in str(mimetype[0]) and not 'xml' in str(mimetype[
0]): | |
| 232 copy_list.append({'src': fullpath, 'dest': filename}) | |
| 233 continue | |
| 234 | |
| 235 if root.endswith('resources'): | |
| 236 copy_list.append({'src': fullpath, 'dest': filename}) | |
| 237 continue | |
| 238 | |
| 239 test_parser = TestParser(vars(self.options), filename=fullpath) | |
| 240 test_info = test_parser.analyze_test() | |
| 241 if test_info is None: | |
| 242 continue | |
| 243 | |
| 244 if 'reference' in test_info.keys(): | |
| 245 reftests += 1 | |
| 246 total_tests += 1 | |
| 247 test_basename = os.path.basename(test_info['test']) | |
| 248 | |
| 249 # Add the ref file, following WebKit style. | |
| 250 # FIXME: Ideally we'd support reading the metadata | |
| 251 # directly rather than relying on a naming convention. | |
| 252 # Using a naming convention creates duplicate copies of the | |
| 253 # reference files. | |
| 254 ref_file = os.path.splitext(test_basename)[0] + '-expected' | |
| 255 ref_file += os.path.splitext(test_basename)[1] | |
| 256 | |
| 257 copy_list.append({'src': test_info['reference'], 'dest': ref
_file}) | |
| 258 copy_list.append({'src': test_info['test'], 'dest': filename
}) | |
| 259 | |
| 260 # Update any support files that need to move as well to rema
in relative to the -expected file. | |
| 261 if 'refsupport' in test_info.keys(): | |
| 262 for support_file in test_info['refsupport']: | |
| 263 source_file = os.path.join(os.path.dirname(test_info
['reference']), support_file) | |
| 264 source_file = os.path.normpath(source_file) | |
| 265 | |
| 266 # Keep the dest as it was | |
| 267 to_copy = {'src': source_file, 'dest': support_file} | |
| 268 | |
| 269 # Only add it once | |
| 270 if not(to_copy in copy_list): | |
| 271 copy_list.append(to_copy) | |
| 272 elif 'jstest' in test_info.keys(): | |
| 273 jstests += 1 | |
| 274 total_tests += 1 | |
| 275 copy_list.append({'src': fullpath, 'dest': filename}) | |
| 276 else: | |
| 277 total_tests += 1 | |
| 278 copy_list.append({'src': fullpath, 'dest': filename}) | |
| 279 | |
| 280 if not total_tests: | |
| 281 # We can skip the support directory if no tests were found. | |
| 282 if 'support' in dirs: | |
| 283 dirs.remove('support') | |
| 284 | |
| 285 if copy_list: | |
| 286 # Only add this directory to the list if there's something to im
port | |
| 287 self.import_list.append({'dirname': root, 'copy_list': copy_list
, | |
| 288 'reftests': reftests, 'jstests': jstests, 'total_tests': tot
al_tests}) | |
| 289 | |
| 290 def find_paths_to_skip(self): | |
| 291 if self.options.ignore_expectations: | |
| 292 return set() | |
| 293 | |
| 294 paths_to_skip = set() | |
| 295 port = self.host.port_factory.get() | |
| 296 w3c_import_expectations_path = self.webkit_finder.path_from_webkit_base(
'tests', 'W3CImportExpectations') | |
| 297 w3c_import_expectations = self.filesystem.read_text_file(w3c_import_expe
ctations_path) | |
| 298 parser = TestExpectationParser(port, full_test_list=(), is_lint_mode=Fal
se) | |
| 299 expectation_lines = parser.parse(w3c_import_expectations_path, w3c_impor
t_expectations) | |
| 300 for line in expectation_lines: | |
| 301 if 'SKIP' in line.expectations: | |
| 302 if line.specifiers: | |
| 303 _log.warning("W3CImportExpectations:%s should not have any s
pecifiers" % line.line_numbers) | |
| 304 continue | |
| 305 paths_to_skip.add(line.name) | |
| 306 return paths_to_skip | |
| 307 | |
| 308 def import_tests(self): | |
| 309 total_imported_tests = 0 | |
| 310 total_imported_reftests = 0 | |
| 311 total_imported_jstests = 0 | |
| 312 total_prefixed_properties = {} | |
| 313 | |
| 314 for dir_to_copy in self.import_list: | |
| 315 total_imported_tests += dir_to_copy['total_tests'] | |
| 316 total_imported_reftests += dir_to_copy['reftests'] | |
| 317 total_imported_jstests += dir_to_copy['jstests'] | |
| 318 | |
| 319 prefixed_properties = [] | |
| 320 | |
| 321 if not dir_to_copy['copy_list']: | |
| 322 continue | |
| 323 | |
| 324 orig_path = dir_to_copy['dirname'] | |
| 325 | |
| 326 subpath = os.path.relpath(orig_path, self.top_of_repo) | |
| 327 new_path = os.path.join(self.destination_directory, subpath) | |
| 328 | |
| 329 if not(os.path.exists(new_path)): | |
| 330 os.makedirs(new_path) | |
| 331 | |
| 332 copied_files = [] | |
| 333 | |
| 334 for file_to_copy in dir_to_copy['copy_list']: | |
| 335 # FIXME: Split this block into a separate function. | |
| 336 orig_filepath = os.path.normpath(file_to_copy['src']) | |
| 337 | |
| 338 if os.path.isdir(orig_filepath): | |
| 339 # FIXME: Figure out what is triggering this and what to do a
bout it. | |
| 340 _log.error('%s refers to a directory' % orig_filepath) | |
| 341 continue | |
| 342 | |
| 343 if not(os.path.exists(orig_filepath)): | |
| 344 _log.warning('%s not found. Possible error in the test.', or
ig_filepath) | |
| 345 continue | |
| 346 | |
| 347 new_filepath = os.path.join(new_path, file_to_copy['dest']) | |
| 348 | |
| 349 if not(os.path.exists(os.path.dirname(new_filepath))): | |
| 350 if not self.import_in_place and not self.options.dry_run: | |
| 351 os.makedirs(os.path.dirname(new_filepath)) | |
| 352 | |
| 353 if not self.options.overwrite and os.path.exists(new_filepath): | |
| 354 _log.info(' skipping import of existing file ' + new_filepa
th) | |
| 355 else: | |
| 356 # FIXME: Maybe doing a file diff is in order here for existi
ng files? | |
| 357 # In other words, there's no sense in overwriting identical
files, but | |
| 358 # there's no harm in copying the identical thing. | |
| 359 _log.info(' importing %s', os.path.relpath(new_filepath, se
lf.layout_tests_dir)) | |
| 360 | |
| 361 # Only html, xml, or css should be converted | |
| 362 # FIXME: Eventually, so should js when support is added for this
type of conversion | |
| 363 mimetype = mimetypes.guess_type(orig_filepath) | |
| 364 if 'html' in str(mimetype[0]) or 'xml' in str(mimetype[0]) or '
css' in str(mimetype[0]): | |
| 365 converted_file = convert_for_webkit(new_path, filename=orig_
filepath) | |
| 366 | |
| 367 if not converted_file: | |
| 368 if not self.import_in_place and not self.options.dry_run
: | |
| 369 shutil.copyfile(orig_filepath, new_filepath) # The
file was unmodified. | |
| 370 else: | |
| 371 for prefixed_property in converted_file[0]: | |
| 372 total_prefixed_properties.setdefault(prefixed_proper
ty, 0) | |
| 373 total_prefixed_properties[prefixed_property] += 1 | |
| 374 | |
| 375 prefixed_properties.extend(set(converted_file[0]) - set(
prefixed_properties)) | |
| 376 if not self.options.dry_run: | |
| 377 outfile = open(new_filepath, 'wb') | |
| 378 outfile.write(converted_file[1]) | |
| 379 outfile.close() | |
| 380 else: | |
| 381 if not self.import_in_place and not self.options.dry_run: | |
| 382 shutil.copyfile(orig_filepath, new_filepath) | |
| 383 | |
| 384 copied_files.append(new_filepath.replace(self._webkit_root, '')) | |
| 385 | |
| 386 if not self.import_in_place and not self.options.dry_run: | |
| 387 self.remove_deleted_files(new_path, copied_files) | |
| 388 self.write_import_log(new_path, copied_files, prefixed_propertie
s) | |
| 389 | |
| 390 _log.info('') | |
| 391 _log.info('Import complete') | |
| 392 _log.info('') | |
| 393 _log.info('IMPORTED %d TOTAL TESTS', total_imported_tests) | |
| 394 _log.info('Imported %d reftests', total_imported_reftests) | |
| 395 _log.info('Imported %d JS tests', total_imported_jstests) | |
| 396 _log.info('Imported %d pixel/manual tests', total_imported_tests - total
_imported_jstests - total_imported_reftests) | |
| 397 _log.info('') | |
| 398 _log.info('Properties needing prefixes (by count):') | |
| 399 for prefixed_property in sorted(total_prefixed_properties, key=lambda p:
total_prefixed_properties[p]): | |
| 400 _log.info(' %s: %s', prefixed_property, total_prefixed_properties[p
refixed_property]) | |
| 401 | |
| 402 def setup_destination_directory(self): | |
| 403 """ Creates a destination directory that mirrors that of the source dire
ctory """ | |
| 404 | |
| 405 new_subpath = self.dir_to_import[len(self.top_of_repo):] | |
| 406 | |
| 407 destination_directory = os.path.join(self.destination_directory, new_sub
path) | |
| 408 | |
| 409 if not os.path.exists(destination_directory): | |
| 410 os.makedirs(destination_directory) | |
| 411 | |
| 412 _log.info('Tests will be imported into: %s', destination_directory) | |
| 413 | |
| 414 def remove_deleted_files(self, dir_to_import, new_file_list): | |
| 415 previous_file_list = [] | |
| 416 | |
| 417 import_log_file = os.path.join(dir_to_import, 'w3c-import.log') | |
| 418 if not os.path.exists(import_log_file): | |
| 419 return | |
| 420 | |
| 421 import_log = open(import_log_file, 'r') | |
| 422 contents = import_log.readlines() | |
| 423 | |
| 424 if 'List of files\n' in contents: | |
| 425 list_index = contents.index('List of files:\n') + 1 | |
| 426 previous_file_list = [filename.strip() for filename in contents[list
_index:]] | |
| 427 | |
| 428 deleted_files = set(previous_file_list) - set(new_file_list) | |
| 429 for deleted_file in deleted_files: | |
| 430 _log.info('Deleting file removed from the W3C repo: %s', deleted_fil
e) | |
| 431 deleted_file = os.path.join(self._webkit_root, deleted_file) | |
| 432 os.remove(deleted_file) | |
| 433 | |
| 434 import_log.close() | |
| 435 | |
| 436 def write_import_log(self, dir_to_import, file_list, prop_list): | |
| 437 now = datetime.datetime.now() | |
| 438 | |
| 439 import_log = open(os.path.join(dir_to_import, 'w3c-import.log'), 'w') | |
| 440 import_log.write('The tests in this directory were imported from the W3C
repository.\n') | |
| 441 import_log.write('Do NOT modify these tests directly in Webkit. Instead,
push changes to the W3C CSS repo:\n\n') | |
| 442 import_log.write('http://hg.csswg.org/test\n\n') | |
| 443 import_log.write('Then run the Tools/Scripts/import-w3c-tests in Webkit
to reimport\n\n') | |
| 444 import_log.write('Do NOT modify or remove this file\n\n') | |
| 445 import_log.write('------------------------------------------------------
------------------\n') | |
| 446 import_log.write('Last Import: ' + now.strftime('%Y-%m-%d %H:%M') + '\n'
) | |
| 447 import_log.write('W3C Mercurial changeset: ' + self.changeset + '\n') | |
| 448 import_log.write('------------------------------------------------------
------------------\n') | |
| 449 import_log.write('Properties requiring vendor prefixes:\n') | |
| 450 if prop_list: | |
| 451 for prop in prop_list: | |
| 452 import_log.write(prop + '\n') | |
| 453 else: | |
| 454 import_log.write('None\n') | |
| 455 import_log.write('------------------------------------------------------
------------------\n') | |
| 456 import_log.write('List of files:\n') | |
| 457 for item in file_list: | |
| 458 import_log.write(item + '\n') | |
| 459 | |
| 460 import_log.close() | |
| OLD | NEW |