3 # Copyright (C) 2013 Adobe Systems Incorporated. All rights reserved.
5 # Redistribution and use in source and binary forms, with or without
6 # modification, are permitted provided that the following conditions
9 # 1. Redistributions of source code must retain the above
10 # copyright notice, this list of conditions and the following
12 # 2. Redistributions in binary form must reproduce the above
13 # copyright notice, this list of conditions and the following
14 # disclaimer in the documentation and/or other materials
15 # provided with the distribution.
17 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER "AS IS" AND ANY
18 # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
20 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE
21 # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
22 # OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
23 # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
24 # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
26 # TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
27 # THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
31 This script imports a directory of W3C CSS tests into WebKit.
33 You must have checked out the W3C repository to your local drive.
35 This script will import the tests into WebKit following these rules:
37 - Only tests that are approved or officially submitted awaiting review are imported
39 - All tests are imported into LayoutTests/csswg
41 - If the tests are approved, they'll be imported into a directory tree that
42 mirrors the CSS Mercurial repo. For example, <csswg_repo_root>/approved/css2.1 is brought in
43 as LayoutTests/csswg/approved/css2.1, maintaining the entire directory structure under that
45 - If the tests are submitted, they'll maintain their directory structure under
46 LayoutTests/csswg. For example, everything under
47 <csswg_repo_root>/contributors/adobe/submitted is brought into
48 LayoutTests/csswg/contributors/adobe/submitted, mirroring its directory structure in the
51 - If the import directory specified is just a contributor folder, only the submitted folder
52 for that contributor is brought in. For example, to import all of Mozilla's tests, either
53 <csswg_repo_root>/contributors/mozilla or <csswg_repo_root>/contributors/mozilla/submitted
54 will work and are equivalent.
56 - By default, only reftests and jstest are imported. This can be overridden with a -a or --all
59 - Also by default, if test files by the same name already exist in the destination directory,
60 they are overwritten with the idea that running this script would refresh files periodically.
61 This can also be overridden by a -n or --no-overwrite flag
63 - All files are converted to work in WebKit:
64 1. Paths to testharness.js files are modified point to Webkit's copy of them in
65 LayoutTests/resources, using the correct relative path from the new location
66 2. All CSS properties requiring the -webkit-vendor prefix are prefixed - this current
67 list of what needs prefixes is read from Source/WebCore/CSS/CSSProperties.in
68 3. Each reftest has its own copy of its reference file following the naming conventions
69 new-run-webkit-tests expects
70 4. If a reference files lives outside the directory of the test that uses it, it is checked
71 for paths to support files as it will be imported into a different relative position to the
72 test file (in the same directory)
74 - Upon completion, script outputs the total number tests imported, broken down by test type
76 - Also upon completion, each directory where files are imported will have w3c-import.log written
77 with a timestamp, the W3C Mercurial changeset if available, the list of CSS properties used that
78 require prefixes, the list of imported files, and guidance for future test modification and
81 - On subsequent imports, this file is read to determine if files have been removed in the newer changesets.
82 The script removes these files accordingly.
85 # FIXME: Change this file to use the Host abstractions rather that os, sys, shutils, etc.
95 from webkitpy.common.host import Host
96 from webkitpy.common.webkit_finder import WebKitFinder
97 from webkitpy.common.system.executive import ScriptError
98 from webkitpy.w3c.test_parser import TestParser
99 from webkitpy.w3c.test_converter import convert_for_webkit
102 TEST_STATUS_UNKNOWN = 'unknown'
103 TEST_STATUS_APPROVED = 'approved'
104 TEST_STATUS_SUBMITTED = 'submitted'
105 VALID_TEST_STATUSES = [TEST_STATUS_APPROVED, TEST_STATUS_SUBMITTED]
107 CONTRIBUTOR_DIR_NAME = 'contributors'
109 CHANGESET_NOT_AVAILABLE = 'Not Available'
112 _log = logging.getLogger(__name__)
115 def main(_argv, _stdout, _stderr):
116 options, args = parse_args()
120 for part in import_dir.split(os.path.sep):
121 if part in VALID_TEST_STATUSES:
124 repo_dir_parts.append(part)
125 repo_dir = os.path.sep.join(repo_dir_parts)
129 if not os.path.exists(import_dir):
130 sys.exit('Source directory %s not found!' % import_dir)
132 if not os.path.exists(repo_dir):
133 sys.exit('Repository directory %s not found!' % repo_dir)
134 if not repo_dir in import_dir:
135 sys.exit('Repository directory %s must be a parent of %s' % (repo_dir, import_dir))
139 test_importer = TestImporter(Host(), import_dir, repo_dir, options)
140 test_importer.do_import()
143 def configure_logging():
144 class LogHandler(logging.StreamHandler):
146 def format(self, record):
147 if record.levelno > logging.INFO:
148 return "%s: %s" % (record.levelname, record.getMessage())
149 return record.getMessage()
151 logger = logging.getLogger()
152 logger.setLevel(logging.INFO)
153 handler = LogHandler()
154 handler.setLevel(logging.INFO)
155 logger.addHandler(handler)
160 parser = optparse.OptionParser(usage='usage: %prog [options] w3c_test_directory [repo_directory]')
161 parser.add_option('-n', '--no-overwrite', dest='overwrite', action='store_false', default=True,
162 help='Flag to prevent duplicate test files from overwriting existing tests. By default, they will be overwritten')
163 parser.add_option('-a', '--all', action='store_true', default=False,
164 help='Import all tests including reftests, JS tests, and manual/pixel tests. By default, only reftests and JS tests are imported')
165 parser.add_option('-d', '--dest-dir', dest='destination', default='w3c',
166 help='Import into a specified directory relative to the LayoutTests root. By default, imports into w3c')
168 options, args = parser.parse_args()
169 if len(args) not in (1, 2):
170 parser.error('Incorrect number of arguments')
174 class TestImporter(object):
176 def __init__(self, host, source_directory, repo_dir, options):
178 self.source_directory = source_directory
179 self.options = options
181 self.filesystem = self.host.filesystem
183 webkit_finder = WebKitFinder(self.filesystem)
184 self._webkit_root = webkit_finder.webkit_base()
185 self.repo_dir = repo_dir
187 self.destination_directory = webkit_finder.path_from_webkit_base("LayoutTests", options.destination)
189 self.changeset = CHANGESET_NOT_AVAILABLE
190 self.test_status = TEST_STATUS_UNKNOWN
192 self.import_list = []
195 self.find_importable_tests(self.source_directory)
196 self.load_changeset()
199 def load_changeset(self):
200 """Returns the current changeset from mercurial or "Not Available"."""
202 self.changeset = self.host.executive.run_command(['hg', 'tip']).split('changeset:')[1]
203 except (OSError, ScriptError):
204 self.changeset = CHANGESET_NOT_AVAILABLE
206 def find_importable_tests(self, directory):
207 # FIXME: use filesystem
208 for root, dirs, files in os.walk(directory):
209 _log.info('Scanning ' + root + '...')
214 # "archive" and "data" dirs are internal csswg things that live in every approved directory.
215 # FIXME: skip 'incoming' tests for now, but we should rework the 'test_status' concept and
216 # support reading them as well.
217 DIRS_TO_SKIP = ('.git', '.hg', 'data', 'archive', 'incoming')
218 for d in DIRS_TO_SKIP:
224 for filename in files:
225 # FIXME: This block should really be a separate function, but the early-continues make that difficult.
227 if filename.startswith('.') or filename.endswith('.pl'):
228 continue # For some reason the w3c repo contains random perl scripts we don't care about.
230 fullpath = os.path.join(root, filename)
232 mimetype = mimetypes.guess_type(fullpath)
233 if not 'html' in str(mimetype[0]) and not 'xml' in str(mimetype[0]):
234 copy_list.append({'src': fullpath, 'dest': filename})
237 test_parser = TestParser(vars(self.options), filename=fullpath)
238 test_info = test_parser.analyze_test()
239 if test_info is None:
242 if 'reference' in test_info.keys():
245 test_basename = os.path.basename(test_info['test'])
247 # Add the ref file, following WebKit style.
248 # FIXME: Ideally we'd support reading the metadata
249 # directly rather than relying on a naming convention.
250 # Using a naming convention creates duplicate copies of the
252 ref_file = os.path.splitext(test_basename)[0] + '-expected'
253 ref_file += os.path.splitext(test_basename)[1]
255 copy_list.append({'src': test_info['reference'], 'dest': ref_file})
256 copy_list.append({'src': test_info['test'], 'dest': filename})
258 # Update any support files that need to move as well to remain relative to the -expected file.
259 if 'refsupport' in test_info.keys():
260 for support_file in test_info['refsupport']:
261 source_file = os.path.join(os.path.dirname(test_info['reference']), support_file)
262 source_file = os.path.normpath(source_file)
264 # Keep the dest as it was
265 to_copy = {'src': source_file, 'dest': support_file}
268 if not(to_copy in copy_list):
269 copy_list.append(to_copy)
270 elif 'jstest' in test_info.keys():
273 copy_list.append({'src': fullpath, 'dest': filename})
276 copy_list.append({'src': fullpath, 'dest': filename})
279 # We can skip the support directory if no tests were found.
280 if 'support' in dirs:
281 dirs.remove('support')
284 # Only add this directory to the list if there's something to import
285 self.import_list.append({'dirname': root, 'copy_list': copy_list,
286 'reftests': reftests, 'jstests': jstests, 'total_tests': total_tests})
288 def import_tests(self):
289 total_imported_tests = 0
290 total_imported_reftests = 0
291 total_imported_jstests = 0
292 total_prefixed_properties = {}
294 for dir_to_copy in self.import_list:
295 total_imported_tests += dir_to_copy['total_tests']
296 total_imported_reftests += dir_to_copy['reftests']
297 total_imported_jstests += dir_to_copy['jstests']
299 prefixed_properties = []
301 if not dir_to_copy['copy_list']:
304 orig_path = dir_to_copy['dirname']
306 subpath = os.path.relpath(orig_path, self.repo_dir)
307 new_path = os.path.join(self.destination_directory, subpath)
309 if not(os.path.exists(new_path)):
310 os.makedirs(new_path)
314 for file_to_copy in dir_to_copy['copy_list']:
315 # FIXME: Split this block into a separate function.
316 orig_filepath = os.path.normpath(file_to_copy['src'])
318 if os.path.isdir(orig_filepath):
319 # FIXME: Figure out what is triggering this and what to do about it.
320 _log.error('%s refers to a directory' % orig_filepath)
323 if not(os.path.exists(orig_filepath)):
324 _log.warning('%s not found. Possible error in the test.', orig_filepath)
327 new_filepath = os.path.join(new_path, file_to_copy['dest'])
329 if not(os.path.exists(os.path.dirname(new_filepath))):
330 os.makedirs(os.path.dirname(new_filepath))
332 if not self.options.overwrite and os.path.exists(new_filepath):
333 _log.info('Skipping import of existing file ' + new_filepath)
335 # FIXME: Maybe doing a file diff is in order here for existing files?
336 # In other words, there's no sense in overwriting identical files, but
337 # there's no harm in copying the identical thing.
338 _log.info('Importing: %s', orig_filepath)
339 _log.info(' As: %s', new_filepath)
341 # Only html, xml, or css should be converted
342 # FIXME: Eventually, so should js when support is added for this type of conversion
343 mimetype = mimetypes.guess_type(orig_filepath)
344 if 'html' in str(mimetype[0]) or 'xml' in str(mimetype[0]) or 'css' in str(mimetype[0]):
345 converted_file = convert_for_webkit(new_path, filename=orig_filepath)
347 if not converted_file:
348 shutil.copyfile(orig_filepath, new_filepath) # The file was unmodified.
350 for prefixed_property in converted_file[0]:
351 total_prefixed_properties.setdefault(prefixed_property, 0)
352 total_prefixed_properties[prefixed_property] += 1
354 prefixed_properties.extend(set(converted_file[0]) - set(prefixed_properties))
355 outfile = open(new_filepath, 'wb')
356 outfile.write(converted_file[1])
359 shutil.copyfile(orig_filepath, new_filepath)
361 copied_files.append(new_filepath.replace(self._webkit_root, ''))
363 self.remove_deleted_files(new_path, copied_files)
364 self.write_import_log(new_path, copied_files, prefixed_properties)
366 _log.info('Import complete')
368 _log.info('IMPORTED %d TOTAL TESTS', total_imported_tests)
369 _log.info('Imported %d reftests', total_imported_reftests)
370 _log.info('Imported %d JS tests', total_imported_jstests)
371 _log.info('Imported %d pixel/manual tests', total_imported_tests - total_imported_jstests - total_imported_reftests)
373 _log.info('Properties needing prefixes (by count):')
374 for prefixed_property in sorted(total_prefixed_properties, key=lambda p: total_prefixed_properties[p]):
375 _log.info(' %s: %s', prefixed_property, total_prefixed_properties[prefixed_property])
377 def setup_destination_directory(self):
378 """ Creates a destination directory that mirrors that of the source approved or submitted directory """
380 self.update_test_status()
382 start = self.source_directory.find(self.test_status)
383 new_subpath = self.source_directory[len(self.repo_dir):]
385 destination_directory = os.path.join(self.destination_directory, new_subpath)
387 if not os.path.exists(destination_directory):
388 os.makedirs(destination_directory)
390 _log.info('Tests will be imported into: %s', destination_directory)
392 def update_test_status(self):
393 """ Sets the test status to either 'approved' or 'submitted' """
395 status = TEST_STATUS_UNKNOWN
397 directory_parts = self.source_directory.split(os.path.sep)
398 for test_status in VALID_TEST_STATUSES:
399 if test_status in directory_parts:
402 self.test_status = status
404 def remove_deleted_files(self, import_directory, new_file_list):
405 """ Reads an import log in |import_directory|, compares it to the |new_file_list|, and removes files not in the new list."""
407 previous_file_list = []
409 import_log_file = os.path.join(import_directory, 'w3c-import.log')
410 if not os.path.exists(import_log_file):
413 import_log = open(import_log_file, 'r')
414 contents = import_log.readlines()
416 if 'List of files\n' in contents:
417 list_index = contents.index('List of files:\n') + 1
418 previous_file_list = [filename.strip() for filename in contents[list_index:]]
420 deleted_files = set(previous_file_list) - set(new_file_list)
421 for deleted_file in deleted_files:
422 _log.info('Deleting file removed from the W3C repo: %s', deleted_file)
423 deleted_file = os.path.join(self._webkit_root, deleted_file)
424 os.remove(deleted_file)
428 def write_import_log(self, import_directory, file_list, prop_list):
429 """ Writes a w3c-import.log file in each directory with imported files. """
431 now = datetime.datetime.now()
433 import_log = open(os.path.join(import_directory, 'w3c-import.log'), 'w')
434 import_log.write('The tests in this directory were imported from the W3C repository.\n')
435 import_log.write('Do NOT modify these tests directly in Webkit. Instead, push changes to the W3C CSS repo:\n\n')
436 import_log.write('http://hg.csswg.org/test\n\n')
437 import_log.write('Then run the Tools/Scripts/import-w3c-tests in Webkit to reimport\n\n')
438 import_log.write('Do NOT modify or remove this file\n\n')
439 import_log.write('------------------------------------------------------------------------\n')
440 import_log.write('Last Import: ' + now.strftime('%Y-%m-%d %H:%M') + '\n')
441 import_log.write('W3C Mercurial changeset: ' + self.changeset + '\n')
442 import_log.write('Test status at time of import: ' + self.test_status + '\n')
443 import_log.write('------------------------------------------------------------------------\n')
444 import_log.write('Properties requiring vendor prefixes:\n')
446 for prop in prop_list:
447 import_log.write(prop + '\n')
449 import_log.write('None\n')
450 import_log.write('------------------------------------------------------------------------\n')
451 import_log.write('List of files:\n')
452 for item in file_list:
453 import_log.write(item + '\n')