1 # Copyright (C) 2013 Adobe Systems Incorporated. All rights reserved.
3 # Redistribution and use in source and binary forms, with or without
4 # modification, are permitted provided that the following conditions
7 # 1. Redistributions of source code must retain the above
8 # copyright notice, this list of conditions and the following
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.
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
29 This script imports a directory of W3C tests into WebKit.
31 This script will import the tests into WebKit following these rules:
33 - By default, all tests are imported under LayoutTests/w3c/[repo-name].
35 - By default, only reftests and jstest are imported. This can be overridden
36 with a -a or --all argument
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
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).
60 - Upon completion, script outputs the total number tests imported, broken
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
73 # FIXME: Change this file to use the Host abstractions rather that os, sys, shutils, etc.
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
91 CHANGESET_NOT_AVAILABLE = 'Not Available'
94 _log = logging.getLogger(__name__)
97 def main(_argv, _stdout, _stderr):
98 options, args = parse_args()
99 dir_to_import = os.path.normpath(os.path.abspath(args[0]))
101 top_of_repo = dir_to_import
103 top_of_repo = os.path.normpath(os.path.abspath(args[1]))
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))
113 test_importer = TestImporter(Host(), dir_to_import, top_of_repo, options)
114 test_importer.do_import()
117 def configure_logging():
118 class LogHandler(logging.StreamHandler):
120 def format(self, record):
121 if record.levelno > logging.INFO:
122 return "%s: %s" % (record.levelname, record.getMessage())
123 return record.getMessage()
125 logger = logging.getLogger()
126 logger.setLevel(logging.INFO)
127 handler = LogHandler()
128 handler.setLevel(logging.INFO)
129 logger.addHandler(handler)
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).')
146 options, args = parser.parse_args()
148 parser.error('Incorrect number of arguments')
150 args = (os.getcwd(),)
154 class TestImporter(object):
156 def __init__(self, host, dir_to_import, top_of_repo, options):
158 self.dir_to_import = dir_to_import
159 self.top_of_repo = top_of_repo
160 self.options = options
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)
171 self.changeset = CHANGESET_NOT_AVAILABLE
173 self.import_list = []
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()
181 def load_changeset(self):
182 """Returns the current changeset from mercurial or "Not Available"."""
184 self.changeset = self.host.executive.run_command(['hg', 'tip']).split('changeset:')[1]
185 except (OSError, ScriptError):
186 self.changeset = CHANGESET_NOT_AVAILABLE
188 def find_importable_tests(self, directory):
189 # FIXME: use filesystem
190 paths_to_skip = self.find_paths_to_skip()
192 for root, dirs, files in os.walk(directory):
193 cur_dir = root.replace(self.dir_above_repo + '/', '') + '/'
194 _log.info(' scanning ' + cur_dir + '...')
199 DIRS_TO_SKIP = ('.git', '.hg')
201 for d in DIRS_TO_SKIP:
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)
215 _log.info(" skipping %s" % path_base)
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)
230 # FIXME: This block should really be a separate function, but the early-continues make that difficult.
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.
235 fullpath = os.path.join(root, filename)
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})
242 if root.endswith('resources'):
243 copy_list.append({'src': fullpath, 'dest': filename})
246 test_parser = TestParser(vars(self.options), filename=fullpath)
247 test_info = test_parser.analyze_test()
248 if test_info is None:
251 if 'reference' in test_info.keys():
254 test_basename = os.path.basename(test_info['test'])
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
261 ref_file = os.path.splitext(test_basename)[0] + '-expected'
262 ref_file += os.path.splitext(test_basename)[1]
264 copy_list.append({'src': test_info['reference'], 'dest': ref_file, 'reference_support_info': test_info['reference_support_info']})
265 copy_list.append({'src': test_info['test'], 'dest': filename})
267 elif 'jstest' in test_info.keys():
270 copy_list.append({'src': fullpath, 'dest': filename})
273 copy_list.append({'src': fullpath, 'dest': filename})
276 # Only add this directory to the list if there's something to import
277 self.import_list.append({'dirname': root, 'copy_list': copy_list,
278 'reftests': reftests, 'jstests': jstests, 'total_tests': total_tests})
280 def find_paths_to_skip(self):
281 if self.options.ignore_expectations:
284 paths_to_skip = set()
285 port = self.host.port_factory.get()
286 w3c_import_expectations_path = self.webkit_finder.path_from_webkit_base('LayoutTests', 'W3CImportExpectations')
287 w3c_import_expectations = self.filesystem.read_text_file(w3c_import_expectations_path)
288 parser = TestExpectationParser(port, full_test_list=(), is_lint_mode=False)
289 expectation_lines = parser.parse(w3c_import_expectations_path, w3c_import_expectations)
290 for line in expectation_lines:
291 if 'SKIP' in line.expectations:
293 _log.warning("W3CImportExpectations:%s should not have any specifiers" % line.line_numbers)
295 paths_to_skip.add(line.name)
298 def import_tests(self):
299 total_imported_tests = 0
300 total_imported_reftests = 0
301 total_imported_jstests = 0
302 total_prefixed_properties = {}
304 for dir_to_copy in self.import_list:
305 total_imported_tests += dir_to_copy['total_tests']
306 total_imported_reftests += dir_to_copy['reftests']
307 total_imported_jstests += dir_to_copy['jstests']
309 prefixed_properties = []
311 if not dir_to_copy['copy_list']:
314 orig_path = dir_to_copy['dirname']
316 subpath = os.path.relpath(orig_path, self.top_of_repo)
317 new_path = os.path.join(self.destination_directory, subpath)
319 if not(os.path.exists(new_path)):
320 os.makedirs(new_path)
324 for file_to_copy in dir_to_copy['copy_list']:
325 # FIXME: Split this block into a separate function.
326 orig_filepath = os.path.normpath(file_to_copy['src'])
328 if os.path.isdir(orig_filepath):
329 # FIXME: Figure out what is triggering this and what to do about it.
330 _log.error('%s refers to a directory' % orig_filepath)
333 if not(os.path.exists(orig_filepath)):
334 _log.warning('%s not found. Possible error in the test.', orig_filepath)
337 new_filepath = os.path.join(new_path, file_to_copy['dest'])
338 if 'reference_support_info' in file_to_copy.keys() and file_to_copy['reference_support_info'] != {}:
339 reference_support_info = file_to_copy['reference_support_info']
341 reference_support_info = None
343 if not(os.path.exists(os.path.dirname(new_filepath))):
344 if not self.import_in_place and not self.options.dry_run:
345 os.makedirs(os.path.dirname(new_filepath))
347 relpath = os.path.relpath(new_filepath, self.layout_tests_dir)
348 if not self.options.overwrite and os.path.exists(new_filepath):
349 _log.info(' skipping %s' % relpath)
351 # FIXME: Maybe doing a file diff is in order here for existing files?
352 # In other words, there's no sense in overwriting identical files, but
353 # there's no harm in copying the identical thing.
354 _log.info(' %s' % relpath)
356 # Only html, xml, or css should be converted
357 # FIXME: Eventually, so should js when support is added for this type of conversion
358 mimetype = mimetypes.guess_type(orig_filepath)
359 if 'html' in str(mimetype[0]) or 'xml' in str(mimetype[0]) or 'css' in str(mimetype[0]):
360 converted_file = convert_for_webkit(new_path, filename=orig_filepath, reference_support_info=reference_support_info)
362 if not converted_file:
363 if not self.import_in_place and not self.options.dry_run:
364 shutil.copyfile(orig_filepath, new_filepath) # The file was unmodified.
366 for prefixed_property in converted_file[0]:
367 total_prefixed_properties.setdefault(prefixed_property, 0)
368 total_prefixed_properties[prefixed_property] += 1
370 prefixed_properties.extend(set(converted_file[0]) - set(prefixed_properties))
371 if not self.options.dry_run:
372 outfile = open(new_filepath, 'wb')
373 outfile.write(converted_file[1])
376 if not self.import_in_place and not self.options.dry_run:
377 shutil.copyfile(orig_filepath, new_filepath)
379 copied_files.append(new_filepath.replace(self._webkit_root, ''))
382 _log.info('Import complete')
384 _log.info('IMPORTED %d TOTAL TESTS', total_imported_tests)
385 _log.info('Imported %d reftests', total_imported_reftests)
386 _log.info('Imported %d JS tests', total_imported_jstests)
387 _log.info('Imported %d pixel/manual tests', total_imported_tests - total_imported_jstests - total_imported_reftests)
390 if total_prefixed_properties:
391 _log.info('Properties needing prefixes (by count):')
392 for prefixed_property in sorted(total_prefixed_properties, key=lambda p: total_prefixed_properties[p]):
393 _log.info(' %s: %s', prefixed_property, total_prefixed_properties[prefixed_property])
395 def setup_destination_directory(self):
396 """ Creates a destination directory that mirrors that of the source directory """
398 new_subpath = self.dir_to_import[len(self.top_of_repo):]
400 destination_directory = os.path.join(self.destination_directory, new_subpath)
402 if not os.path.exists(destination_directory):
403 os.makedirs(destination_directory)
405 _log.info('Tests will be imported into: %s', destination_directory)