3 # Copyright (C) 2013 Adobe Systems Incorporated. All rights reserved.
4 # Copyright (C) 2015 Canon Inc. All rights reserved.
6 # Redistribution and use in source and binary forms, with or without
7 # modification, are permitted provided that the following conditions
10 # 1. Redistributions of source code must retain the above
11 # copyright notice, this list of conditions and the following
13 # 2. Redistributions in binary form must reproduce the above
14 # copyright notice, this list of conditions and the following
15 # disclaimer in the documentation and/or other materials
16 # provided with the distribution.
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER "AS IS" AND ANY
19 # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE
22 # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
23 # OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
24 # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
25 # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
27 # TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
28 # THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
32 This script imports W3C tests into WebKit, either from a local folder or by cloning W3C CSS and WPT repositories.
34 This script will import the tests into WebKit following these rules:
36 - All tests are by default imported into LayoutTests/imported/w3c
38 - Tests will be imported into a directory tree that
39 mirrors the CSS and WPT repositories. For example, <csswg_repo_root>/css2.1 should be brought in
40 as LayoutTests/imported/w3c/csswg-tests/css2.1, maintaining the entire directory structure under that
42 - By default, only reftests and jstest are imported. This can be overridden with a -a or --all
45 - Also by default, if test files by the same name already exist in the destination directory,
46 they are overwritten with the idea that running this script would refresh files periodically.
47 This can also be overridden by a -n or --no-overwrite flag
49 - If no import_directory is provided, the script will download the tests from the W3C github repositories.
50 The selection of tests and folders to import will be based on the following files:
51 1. LayoutTests/imported/w3c/resources/TestRepositories lists the repositories to clone, the corresponding revision to checkout and the infrastructure folders that need to be imported/skipped.
52 2. LayoutTests/imported/w3c/resources/ImportExpectations list the test suites or tests to NOT import.
54 - All files are converted to work in WebKit:
55 1. Paths to testharness.js files are modified to point to Webkit's copy of them in
56 LayoutTests/resources, using the correct relative path from the new location.
57 This is applied to CSS tests but not to WPT tests.
58 2. All CSS properties requiring the -webkit-vendor prefix are prefixed - this current
59 list of what needs prefixes is read from Source/WebCore/CSS/CSSProperties.in
60 3. Each reftest has its own copy of its reference file following the naming conventions
61 new-run-webkit-tests expects
62 4. If a reference files lives outside the directory of the test that uses it, it is checked
63 for paths to support files as it will be imported into a different relative position to the
64 test file (in the same directory)
66 - Upon completion, script outputs the total number tests imported, broken down by test type
68 - Also upon completion, each directory where files are imported will have w3c-import.log written
69 with a timestamp, the list of CSS properties used that require prefixes, the list of imported files,
70 and guidance for future test modification and maintenance.
72 - On subsequent imports, this file is read to determine if files have been removed in the newer changesets.
73 The script removes these files accordingly.
83 from webkitpy.common.host import Host
84 from webkitpy.common.system.filesystem import FileSystem
85 from webkitpy.common.webkit_finder import WebKitFinder
86 from webkitpy.common.system.executive import ScriptError
87 from webkitpy.w3c.test_parser import TestParser
88 from webkitpy.w3c.test_converter import convert_for_webkit
89 from webkitpy.w3c.test_downloader import TestDownloader
91 CHANGESET_NOT_AVAILABLE = 'Not Available'
93 _log = logging.getLogger(__name__)
96 def main(_argv, _stdout, _stderr):
97 options, args = parse_args(_argv)
98 import_dir = args[0] if args else None
99 filesystem = FileSystem()
100 if import_dir and not filesystem.exists(import_dir):
101 sys.exit('Source directory %s not found!' % import_dir)
105 test_importer = TestImporter(Host(), import_dir, options)
106 test_importer.do_import()
109 def configure_logging():
110 class LogHandler(logging.StreamHandler):
112 def format(self, record):
113 if record.levelno > logging.INFO:
114 return "%s: %s" % (record.levelname, record.getMessage())
115 return record.getMessage()
117 logger = logging.getLogger()
118 logger.setLevel(logging.INFO)
119 handler = LogHandler()
120 handler.setLevel(logging.INFO)
121 logger.addHandler(handler)
125 def parse_args(args):
126 parser = argparse.ArgumentParser(prog='import-w3c-tests [w3c_test_source_directory]')
128 parser.add_argument('-n', '--no-overwrite', dest='overwrite', action='store_false', default=True,
129 help='Flag to prevent duplicate test files from overwriting existing tests. By default, they will be overwritten')
130 parser.add_argument('-l', '--no-links-conversion', dest='convert_test_harness_links', action='store_false', default=True,
131 help='Do not change links (testharness js or css e.g.). This option only applies when providing a source directory, in which case by default, links are converted to point to WebKit testharness files. When tests are downloaded from W3C repository, links are converted for CSS tests and remain unchanged for WPT tests')
133 parser.add_argument('-a', '--all', action='store_true', default=False,
134 help='Import all tests including reftests, JS tests, and manual/pixel tests. By default, only reftests and JS tests are imported')
136 parser.add_argument('-d', '--dest-dir', dest='destination', default=fs.join('imported', 'w3c'),
137 help='Import into a specified directory relative to the LayoutTests root. By default, imports into imported/w3c')
139 list_of_repositories = ' or '.join([test_repository['name'] for test_repository in TestDownloader.load_test_repositories()])
140 parser.add_argument('-t', '--test-path', action='append', dest='test_paths', default=[],
141 help='Import only tests in the supplied subdirectory of the source directory. Can be supplied multiple times to give multiple paths. For tests directly cloned from W3C repositories, use ' + list_of_repositories + ' prefixes to filter specific tests')
143 parser.add_argument('-v', '--verbose', action='store_true', default=False,
144 help='Print maximal log')
145 parser.add_argument('--no-fetch', action='store_false', dest='fetch', default=True,
146 help='Do not fetch the repositories. By default, repositories are fetched if a source directory is not provided')
147 parser.add_argument('--import-all', action='store_true', default=False,
148 help='Ignore the ImportExpectations file. All tests will be imported. This option only applies when tests are downloaded from W3C repository')
150 options, args = parser.parse_known_args(args)
152 parser.error('Incorrect number of arguments')
156 class TestImporter(object):
158 def __init__(self, host, source_directory, options):
160 self.source_directory = source_directory
161 self.options = options
163 self.filesystem = self.host.filesystem
165 webkit_finder = WebKitFinder(self.filesystem)
166 self._webkit_root = webkit_finder.webkit_base()
168 self.destination_directory = webkit_finder.path_from_webkit_base("LayoutTests", options.destination)
169 self.tests_w3c_relative_path = self.filesystem.join('imported', 'w3c')
170 self.layout_tests_w3c_path = webkit_finder.path_from_webkit_base('LayoutTests', self.tests_w3c_relative_path)
171 self.tests_download_path = webkit_finder.path_from_webkit_base('WebKitBuild', 'w3c-tests')
173 self._test_downloader = None
175 self._potential_test_resource_files = []
177 self.import_list = []
178 self._importing_downloaded_tests = source_directory is None
181 if not self.source_directory:
182 _log.info('Downloading W3C test repositories')
183 self.source_directory = self.filesystem.join(self.tests_download_path, 'to-be-imported')
184 self.filesystem.maybe_make_directory(self.tests_download_path)
185 self.filesystem.maybe_make_directory(self.source_directory)
186 self.test_downloader().download_tests(self.source_directory, self.options.test_paths)
188 if not self.options.test_paths or self._importing_downloaded_tests:
189 self.find_importable_tests(self.source_directory)
191 for test_path in self.options.test_paths:
192 self.find_importable_tests(self.filesystem.join(self.source_directory, test_path))
196 if self._importing_downloaded_tests:
197 self.generate_git_submodules_description_for_all_repositories()
199 def generate_git_submodules_description_for_all_repositories(self):
200 for test_repository in self._test_downloader.test_repositories:
201 if 'generate_git_submodules_description' in test_repository['import_options']:
202 self.filesystem.maybe_make_directory(self.filesystem.join(self.destination_directory, 'resources'))
203 self._test_downloader.generate_git_submodules_description(test_repository, self.filesystem.join(self.destination_directory, 'resources', test_repository['name'] + '-modules.json'))
204 # FIXME: Generate WPT .gitignore and main __init__.py
206 def test_downloader(self):
207 if not self._test_downloader:
208 download_options = TestDownloader.default_options()
209 download_options.fetch = self.options.fetch
210 download_options.verbose = self.options.verbose
211 download_options.import_all = self.options.import_all
212 self._test_downloader = TestDownloader(self.tests_download_path, self.host, download_options)
213 return self._test_downloader
215 def should_skip_file(self, filename):
216 # For some reason the w3c repo contains random perl scripts we don't care about.
217 if filename.endswith('.pl'):
219 if filename.startswith('.'):
220 return not filename == '.htaccess'
223 def find_importable_tests(self, directory):
224 def should_keep_subdir(filesystem, path):
225 if self._importing_downloaded_tests:
227 subdir = path[len(directory):]
228 DIRS_TO_SKIP = ('work-in-progress', 'tools', 'support')
229 should_skip = filesystem.basename(subdir).startswith('.') or (subdir in DIRS_TO_SKIP)
230 return not should_skip
232 directories = self.filesystem.dirs_under(directory, should_keep_subdir)
233 for root in directories:
234 _log.info('Scanning ' + root + '...')
241 for filename in self.filesystem.listdir(root):
242 if self.filesystem.isdir(self.filesystem.join(root, filename)):
244 # FIXME: This block should really be a separate function, but the early-continues make that difficult.
246 if self.should_skip_file(filename):
249 fullpath = self.filesystem.join(root, filename)
251 mimetype = mimetypes.guess_type(fullpath)
252 if not 'html' in str(mimetype[0]) and not 'application/xhtml+xml' in str(mimetype[0]) and not 'application/xml' in str(mimetype[0]):
253 copy_list.append({'src': fullpath, 'dest': filename})
256 test_parser = TestParser(vars(self.options), filename=fullpath, host=self.host)
257 test_info = test_parser.analyze_test()
258 if test_info is None:
259 # This is probably a resource file.
260 if self.filesystem.basename(self.filesystem.dirname(fullpath)) != "resources":
261 self._potential_test_resource_files.append({'src': fullpath, 'dest': filename})
262 copy_list.append({'src': fullpath, 'dest': filename})
265 if 'manualtest' in test_info.keys():
268 if 'referencefile' in test_info.keys():
269 # Skip it since, the corresponding reference test should have a link to this file
272 if 'reference' in test_info.keys():
275 test_basename = self.filesystem.basename(test_info['test'])
277 # Add the ref file, following WebKit style.
278 # FIXME: Ideally we'd support reading the metadata
279 # directly rather than relying on a naming convention.
280 # Using a naming convention creates duplicate copies of the
282 ref_file = self.filesystem.splitext(test_basename)[0] + '-expected'
283 ref_file += self.filesystem.splitext(test_info['reference'])[1]
285 copy_list.append({'src': test_info['reference'], 'dest': ref_file, 'reference_support_info': test_info['reference_support_info']})
286 copy_list.append({'src': test_info['test'], 'dest': filename})
288 elif 'jstest' in test_info.keys():
291 copy_list.append({'src': fullpath, 'dest': filename})
294 copy_list.append({'src': fullpath, 'dest': filename})
297 # Only add this directory to the list if there's something to import
298 self.import_list.append({'dirname': root, 'copy_list': copy_list,
299 'reftests': reftests, 'jstests': jstests, 'total_tests': total_tests})
301 def should_convert_test_harness_links(self, test):
302 if self._importing_downloaded_tests:
303 for test_repository in self.test_downloader().test_repositories:
304 if test.startswith(test_repository['name']):
305 return 'convert_test_harness_links' in test_repository['import_options']
307 return self.options.convert_test_harness_links
309 def import_tests(self):
310 total_imported_tests = 0
311 total_imported_reftests = 0
312 total_imported_jstests = 0
313 total_prefixed_properties = {}
314 total_prefixed_property_values = {}
316 failed_conversion_files = []
318 for dir_to_copy in self.import_list:
319 total_imported_tests += dir_to_copy['total_tests']
320 total_imported_reftests += dir_to_copy['reftests']
321 total_imported_jstests += dir_to_copy['jstests']
323 prefixed_properties = []
324 prefixed_property_values = []
326 if not dir_to_copy['copy_list']:
329 orig_path = dir_to_copy['dirname']
331 subpath = self.filesystem.relpath(orig_path, self.source_directory)
332 new_path = self.filesystem.join(self.destination_directory, subpath)
334 if not(self.filesystem.exists(new_path)):
335 self.filesystem.maybe_make_directory(new_path)
339 for file_to_copy in dir_to_copy['copy_list']:
340 # FIXME: Split this block into a separate function.
341 orig_filepath = self.filesystem.normpath(file_to_copy['src'])
343 if self.filesystem.isdir(orig_filepath):
344 # FIXME: Figure out what is triggering this and what to do about it.
345 _log.error('%s refers to a directory' % orig_filepath)
348 if not(self.filesystem.exists(orig_filepath)):
349 _log.warning('%s not found. Possible error in the test.', orig_filepath)
352 new_filepath = self.filesystem.join(new_path, file_to_copy['dest'])
353 if 'reference_support_info' in file_to_copy.keys() and file_to_copy['reference_support_info'] != {}:
354 reference_support_info = file_to_copy['reference_support_info']
356 reference_support_info = None
358 if not(self.filesystem.exists(self.filesystem.dirname(new_filepath))):
359 self.filesystem.maybe_make_directory(self.filesystem.dirname(new_filepath))
361 if not self.options.overwrite and self.filesystem.exists(new_filepath):
362 _log.info('Skipping import of existing file ' + new_filepath)
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('Importing: %s', orig_filepath)
368 _log.info(' As: %s', new_filepath)
370 # Only html, xml, or css should be converted
371 # FIXME: Eventually, so should js when support is added for this type of conversion
372 mimetype = mimetypes.guess_type(orig_filepath)
373 if 'html' in str(mimetype[0]) or 'xml' in str(mimetype[0]) or 'css' in str(mimetype[0]):
375 converted_file = convert_for_webkit(new_path, filename=orig_filepath, reference_support_info=reference_support_info, host=self.host, convert_test_harness_links=self.should_convert_test_harness_links(subpath))
377 _log.warn('Failed converting %s', orig_filepath)
378 failed_conversion_files.append(orig_filepath)
379 converted_file = None
381 if not converted_file:
382 self.filesystem.copyfile(orig_filepath, new_filepath) # The file was unmodified.
384 for prefixed_property in converted_file[0]:
385 total_prefixed_properties.setdefault(prefixed_property, 0)
386 total_prefixed_properties[prefixed_property] += 1
388 prefixed_properties.extend(set(converted_file[0]) - set(prefixed_properties))
390 for prefixed_value in converted_file[1]:
391 total_prefixed_property_values.setdefault(prefixed_value, 0)
392 total_prefixed_property_values[prefixed_value] += 1
394 prefixed_property_values.extend(set(converted_file[1]) - set(prefixed_property_values))
396 self.filesystem.write_binary_file(new_filepath, converted_file[2])
397 elif orig_filepath.endswith('__init__.py') and not self.filesystem.getsize(orig_filepath):
398 # Some bots dislike empty __init__.py.
399 self.filesystem.write_text_file(new_filepath, '# This file is required for Python to search this directory for modules.')
401 self.filesystem.copyfile(orig_filepath, new_filepath)
403 copied_files.append(new_filepath.replace(self._webkit_root, ''))
405 self.remove_deleted_files(new_path, copied_files)
406 self.write_import_log(new_path, copied_files, prefixed_properties, prefixed_property_values)
408 _log.info('Import complete')
410 _log.info('IMPORTED %d TOTAL TESTS', total_imported_tests)
411 _log.info('Imported %d reftests', total_imported_reftests)
412 _log.info('Imported %d JS tests', total_imported_jstests)
413 _log.info('Imported %d pixel/manual tests', total_imported_tests - total_imported_jstests - total_imported_reftests)
414 if len(failed_conversion_files):
415 _log.warn('Failed converting %d files (files copied without being converted)', len(failed_conversion_files))
417 _log.info('Properties needing prefixes (by count):')
419 for prefixed_property in sorted(total_prefixed_properties, key=lambda p: total_prefixed_properties[p]):
420 _log.info(' %s: %s', prefixed_property, total_prefixed_properties[prefixed_property])
422 _log.info('Property values needing prefixes (by count):')
424 for prefixed_value in sorted(total_prefixed_property_values, key=lambda p: total_prefixed_property_values[p]):
425 _log.info(' %s: %s', prefixed_value, total_prefixed_property_values[prefixed_value])
427 if self._potential_test_resource_files:
428 _log.info('The following files may be resource files and should be marked as skipped in the TestExpectations:')
429 for filename in sorted([test['src'] for test in self._potential_test_resource_files]):
430 _log.info(filename.replace(self.source_directory, self.tests_w3c_relative_path) + ' [ Skip ]')
432 def remove_deleted_files(self, import_directory, new_file_list):
433 """ Reads an import log in |import_directory|, compares it to the |new_file_list|, and removes files not in the new list."""
435 previous_file_list = []
437 import_log_file = self.filesystem.join(import_directory, 'w3c-import.log')
438 if not self.filesystem.exists(import_log_file):
441 contents = self.filesystem.read_text_file(import_log_file).split('\n')
443 if 'List of files\n' in contents:
444 list_index = contents.index('List of files:\n') + 1
445 previous_file_list = [filename.strip() for filename in contents[list_index:]]
447 deleted_files = set(previous_file_list) - set(new_file_list)
448 for deleted_file in deleted_files:
449 _log.info('Deleting file removed from the W3C repo: %s', deleted_file)
450 deleted_file = self.filesystem.join(self._webkit_root, deleted_file)
451 self.filesystem.remove(deleted_file)
453 def write_import_log(self, import_directory, file_list, prop_list, property_values_list):
454 """ Writes a w3c-import.log file in each directory with imported files. """
457 import_log.append('The tests in this directory were imported from the W3C repository.\n')
458 import_log.append('Do NOT modify these tests directly in Webkit.\n')
459 import_log.append('Instead, create a pull request on the W3C CSS or WPT github:\n')
460 import_log.append('\thttps://github.com/w3c/csswg-test\n')
461 import_log.append('\thttps://github.com/w3c/web-platform-tests\n\n')
462 import_log.append('Then run the Tools/Scripts/import-w3c-tests in Webkit to reimport\n\n')
463 import_log.append('Do NOT modify or remove this file\n\n')
464 import_log.append('------------------------------------------------------------------------\n')
465 import_log.append('Properties requiring vendor prefixes:\n')
467 for prop in prop_list:
468 import_log.append(prop + '\n')
470 import_log.append('None\n')
471 import_log.append('Property values requiring vendor prefixes:\n')
472 if property_values_list:
473 for value in property_values_list:
474 import_log.append(value + '\n')
476 import_log.append('None\n')
477 import_log.append('------------------------------------------------------------------------\n')
478 import_log.append('List of files:\n')
479 for item in sorted(file_list):
480 import_log.append(item + '\n')
482 self.filesystem.write_text_file(self.filesystem.join(import_directory, 'w3c-import.log'), ''.join(import_log))