03fdbe5945613acedac3453e6fb0ec6129211bf8
[WebKit-https.git] / Tools / Scripts / webkitpy / w3c / test_importer.py
1 #!/usr/bin/env python
2
3 # Copyright (C) 2013 Adobe Systems Incorporated. All rights reserved.
4 #
5 # Redistribution and use in source and binary forms, with or without
6 # modification, are permitted provided that the following conditions
7 # are met:
8 #
9 # 1. Redistributions of source code must retain the above
10 #    copyright notice, this list of conditions and the following
11 #    disclaimer.
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.
16 #
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
28 # SUCH DAMAGE.
29
30 """
31  This script imports a directory of W3C CSS tests into WebKit.
32
33  You must have checked out the W3C repository to your local drive.
34
35  This script will import the tests into WebKit following these rules:
36
37     - Only tests that are approved or officially submitted awaiting review are imported
38
39     - All tests are imported into LayoutTests/csswg
40
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
44
45     - If the tests are submitted, they'll be brought in as LayoutTests/csswg/submitted and will also
46       maintain their directory structure under that. For example, everything under
47       <csswg_repo_root>/contributors/adobe/submitted is brought into submitted, mirroring its
48       directory structure in the csswg repo
49
50     - If the import directory specified is just a contributor folder, only the submitted folder
51       for that contributor is brought in. For example, to import all of Mozilla's tests, either
52       <csswg_repo_root>/contributors/mozilla or <csswg_repo_root>/contributors/mozilla/submitted
53       will work and are equivalent
54
55     - For the time being, this script won't work if you try to import the full set of submitted
56       tests under contributors/*/submitted. Since these are awaiting review, this is just a small
57       control mechanism to enforce carefully selecting what non-approved tests are imported.
58       It can obviously and easily be changed.
59
60     - By default, only reftests and jstest are imported. This can be overridden with a -a or --all
61       argument
62
63     - Also by default, if test files by the same name already exist in the destination directory,
64       they are overwritten with the idea that running this script would refresh files periodically.
65       This can also be overridden by a -n or --no-overwrite flag
66
67     - All files are converted to work in WebKit:
68          1. .xht extensions are changed to .xhtml to make new-run-webkit-tests happy
69          2. Paths to testharness.js files are modified point to Webkit's copy of them in
70             LayoutTests/resources, using the correct relative path from the new location
71          3. All CSS properties requiring the -webkit-vendor prefix are prefixed - this current
72             list of what needs prefixes is read from Source/WebCore/CSS/CSSProperties.in
73          4. Each reftest has its own copy of its reference file following the naming conventions
74             new-run-webkit-tests expects
75          5. If a a reference files lives outside the directory of the test that uses it, it is checked
76             for paths to support files as it will be imported into a different relative position to the
77             test file (in the same directory)
78
79      - Upon completion, script outputs the total number tests imported, broken down by test type
80
81      - Also upon completion, each directory where files are imported will have w3c-import.log written
82        with a timestamp, the W3C Mercurial changeset if available, the list of CSS properties used that
83        require prefixes, the list of imported files, and guidance for future test modification and
84        maintenance.
85
86      - On subsequent imports, this file is read to determine if files have been removed in the newer changesets.
87        The script removes these files accordingly.
88 """
89
90 # FIXME: Change this file to use the Host abstractions rather that os, sys, shutils, etc.
91
92 import datetime
93 import mimetypes
94 import optparse
95 import os
96 import shutil
97 import sys
98
99 from webkitpy.common.host import Host
100 from webkitpy.common.system.executive import ScriptError
101 from webkitpy.w3c.test_parser import TestParser
102 from webkitpy.w3c.test_converter import W3CTestConverter
103
104
105 TEST_STATUS_UNKNOWN = 'unknown'
106 TEST_STATUS_APPROVED = 'approved'
107 TEST_STATUS_SUBMITTED = 'submitted'
108
109 CHANGESET_NOT_AVAILABLE = 'Not Available'
110
111
112 def main(_argv, _stdout, _stderr):
113     options, args = parse_args()
114     import_dir = validate_import_directory(args[0])
115     test_importer = TestImporter(Host(), import_dir, options)
116     test_importer.do_import()
117
118
119 def parse_args():
120     parser = optparse.OptionParser(usage='usage: %prog [options] w3c_test_directory')
121     parser.add_option('-n', '--no-overwrite', dest='overwrite', action='store_false', default=True,
122         help='Flag to prevent duplicate test files from overwriting existing tests. By default, they will be overwritten')
123     parser.add_option('-a', '--all', action='store_true', default=False,
124         help='Import all tests including reftests, JS tests, and manual/pixel tests. By default, only reftests and JS tests are imported')
125
126     options, args = parser.parse_args()
127     if len(args) != 1:
128         parser.error('Incorrect number of arguments')
129     return options, args
130
131
132 def validate_import_directory(import_dir):
133     if not os.path.exists(import_dir):
134         sys.exit('Source directory %s not found!' % import_dir)
135
136     # Make sure the tests are officially submitted to the W3C, either approved or
137     # submitted following their directory naming conventions
138     if import_dir.find('approved') == -1 and import_dir.find('submitted') == -1:
139         # If not pointed directly to the approved directory or to any submitted
140         # directory, check for a submitted subdirectory and go with that
141         import_dir = os.path.join(import_dir, 'submitted')
142         if not os.path.exists(os.path.join(import_dir)):
143             sys.exit('Unable to import tests that aren\'t approved or submitted to the W3C')
144
145     return import_dir
146
147
148 class TestImporter(object):
149
150     def __init__(self, host, source_directory, options):
151         self.host = host
152         self.source_directory = source_directory
153         self.options = options
154
155         self.filesystem = self.host.filesystem
156         self._webkit_root = __file__.split(self.filesystem.sep + 'Tools')[0]
157
158         self.destination_directory = self.path_from_webkit_root("LayoutTests", "csswg")
159
160         self.changeset = CHANGESET_NOT_AVAILABLE
161         self.test_status = TEST_STATUS_UNKNOWN
162
163         self.import_list = []
164
165     def path_from_webkit_root(self, *comps):
166         return self.filesystem.abspath(self.filesystem.join(self._webkit_root, *comps))
167
168     def do_import(self):
169         self.find_importable_tests(self.source_directory)
170         self.load_changeset()
171         self.import_tests()
172
173     def load_changeset(self):
174         """Returns the current changeset from mercurial or "Not Available"."""
175         try:
176             self.changeset = self.host.executive.run_command(['hg', 'tip']).split('changeset:')[1]
177         except (OSError, ScriptError):
178             self.changeset = CHANGESET_NOT_AVAILABLE
179
180     def find_importable_tests(self, directory):
181         # FIXME: use filesystem
182         for root, dirs, files in os.walk(directory):
183             print 'Scanning ' + root + '...'
184             total_tests = 0
185             reftests = 0
186             jstests = 0
187
188             # Ignore any repo stuff
189             if '.git' in dirs:
190                 dirs.remove('.git')
191             if '.hg' in dirs:
192                 dirs.remove('.hg')
193
194             # archive and data dirs are internal csswg things that live in every approved directory
195             if 'data' in dirs:
196                 dirs.remove('data')
197             if 'archive' in dirs:
198                 dirs.remove('archive')
199
200             copy_list = []
201
202             for filename in files:
203                 # FIXME: This block should really be a separate function, but the early-continues make that difficult.
204
205                 if filename.startswith('.') or filename.endswith('.pl'):
206                     continue  # For some reason the w3c repo contains random perl scripts we don't care about.
207
208                 fullpath = os.path.join(root, filename)
209
210                 mimetype = mimetypes.guess_type(fullpath)
211                 if not 'html' in str(mimetype[0]) and not 'xml' in str(mimetype[0]):
212                     copy_list.append({'src': fullpath, 'dest': filename})
213                     continue
214
215                 test_parser = TestParser(vars(self.options), filename=fullpath)
216                 test_info = test_parser.analyze_test()
217                 if test_info is None:
218                     continue
219
220                 if 'reference' in test_info.keys():
221                     reftests += 1
222                     total_tests += 1
223                     test_basename = os.path.basename(test_info['test'])
224
225                     # Add the ref file, following WebKit style.
226                     # FIXME: Ideally we'd support reading the metadata
227                     # directly rather than relying  on a naming convention.
228                     # Using a naming convention creates duplicate copies of the
229                     # reference files.
230                     ref_file = os.path.splitext(test_basename)[0] + '-expected'
231                     ref_file += os.path.splitext(test_basename)[1]
232
233                     copy_list.append({'src': test_info['reference'], 'dest': ref_file})
234                     copy_list.append({'src': test_info['test'], 'dest': filename})
235
236                     # Update any support files that need to move as well to remain relative to the -expected file.
237                     if 'refsupport' in test_info.keys():
238                         for support_file in test_info['refsupport']:
239                             source_file = os.path.join(os.path.dirname(test_info['reference']), support_file)
240                             source_file = os.path.normpath(source_file)
241
242                             # Keep the dest as it was
243                             to_copy = {'src': source_file, 'dest': support_file}
244
245                             # Only add it once
246                             if not(to_copy in copy_list):
247                                 copy_list.append(to_copy)
248                 elif 'jstest' in test_info.keys():
249                     jstests += 1
250                     total_tests += 1
251                     copy_list.append({'src': fullpath, 'dest': filename})
252                 else:
253                     total_tests += 1
254                     copy_list.append({'src': fullpath, 'dest': filename})
255
256             if not total_tests:
257                 # We can skip the support directory if no tests were found.
258                 if 'support' in dirs:
259                     dirs.remove('support')
260
261                 if copy_list:
262                     # Only add this directory to the list if there's something to import
263                     self.import_list.append({'dirname': root, 'copy_list': copy_list,
264                         'reftests': reftests, 'jstests': jstests, 'total_tests': total_tests})
265
266     def import_tests(self):
267         if self.import_list:
268             self.setup_destination_directory()
269
270         converter = W3CTestConverter()
271         total_imported_tests = 0
272         total_imported_reftests = 0
273         total_imported_jstests = 0
274
275         for dir_to_copy in self.import_list:
276
277             total_imported_tests += dir_to_copy['total_tests']
278             total_imported_reftests += dir_to_copy['reftests']
279             total_imported_jstests += dir_to_copy['jstests']
280
281             prefixed_properties = []
282
283             if not dir_to_copy['copy_list']:
284                 continue
285
286             # Build the subpath starting with the approved/submitted directory
287             orig_path = dir_to_copy['dirname']
288             start = orig_path.find(self.test_status)
289             new_subpath = orig_path[start:len(orig_path)]
290
291             # Append the new subpath to the destination_directory
292             new_path = os.path.join(self.destination_directory, new_subpath)
293
294             # Create the destination subdirectories if not there
295             if not(os.path.exists(new_path)):
296                 os.makedirs(new_path)
297
298             copied_files = []
299
300             for file_to_copy in dir_to_copy['copy_list']:
301                 # FIXME: Split this block into a separate function.
302                 orig_filepath = os.path.normpath(file_to_copy['src'])
303
304                 assert(not os.path.isdir(orig_filepath))
305
306                 if not(os.path.exists(orig_filepath)):
307                     print 'Warning: ' + orig_filepath + ' not found. Possible error in the test.'
308                     continue
309
310                 new_filepath = os.path.join(new_path, file_to_copy['dest'])
311
312                 # FIXME: we should just support '.xht' directly.
313                 new_filepath = new_filepath.replace('.xht', '.xhtml')
314
315                 if not(os.path.exists(os.path.dirname(new_filepath))):
316                     os.makedirs(os.path.dirname(new_filepath))
317
318                 if not self.options.overwrite and os.path.exists(new_filepath):
319                     print 'Skipping import of existing file ' + new_filepath
320                 else:
321                     # FIXME: Maybe doing a file diff is in order here for existing files?
322                     # In other words, there's no sense in overwriting identical files, but
323                     # there's no harm in copying the identical thing.
324                     print 'Importing:', orig_filepath
325                     print '       As:', new_filepath
326
327                 # Only html, xml, or css should be converted
328                 # FIXME: Eventually, so should js when support is added for this type of conversion
329                 mimetype = mimetypes.guess_type(orig_filepath)
330                 if 'html' in str(mimetype[0]) or 'xml' in str(mimetype[0])  or 'css' in str(mimetype[0]):
331
332                     converted_file = converter.convert_for_webkit(new_path, filename=orig_filepath)
333
334                     if not converted_file:
335                         shutil.copyfile(orig_filepath, new_filepath)  # The file was unmodified.
336                     else:
337                         prefixed_properties.extend(set(converted_file[0]) - set(prefixed_properties))
338                         outfile = open(new_filepath, 'w')
339                         outfile.write(converted_file[1])
340                         outfile.close()
341                 else:
342                     shutil.copyfile(orig_filepath, new_filepath)
343
344                 copied_files.append(new_filepath.replace(self._webkit_root, ''))
345
346             self.remove_deleted_files(new_path, copied_files)
347             self.write_import_log(new_path, copied_files, prefixed_properties)
348
349         print 'Import complete'
350
351         print 'IMPORTED ' + str(total_imported_tests) + ' TOTAL TESTS'
352         print 'Imported ' + str(total_imported_reftests) + ' reftests'
353         print 'Imported ' + str(total_imported_jstests) + ' JS tests'
354         print 'Imported ' + str(total_imported_tests - total_imported_jstests - total_imported_reftests) + ' pixel/manual tests'
355
356     def setup_destination_directory(self):
357         """ Creates a destination directory that mirrors that of the source approved or submitted directory """
358
359         self.update_test_status()
360
361         start = self.source_directory.find(self.test_status)
362         new_subpath = self.source_directory[start:len(self.source_directory)]
363
364         destination_directory = os.path.join(self.destination_directory, new_subpath)
365
366         if not os.path.exists(destination_directory):
367             os.makedirs(destination_directory)
368
369         print 'Tests will be imported into: ' + destination_directory
370
371     def update_test_status(self):
372         """ Sets the test status to either 'approved' or 'submitted' """
373
374         status = TEST_STATUS_UNKNOWN
375
376         if 'approved' in self.source_directory.split(os.path.sep):
377             status = TEST_STATUS_APPROVED
378         elif 'submitted' in self.source_directory.split(os.path.sep):
379             status = TEST_STATUS_SUBMITTED
380
381         self.test_status = status
382
383     def remove_deleted_files(self, import_directory, new_file_list):
384         """ Reads an import log in |import_directory|, compares it to the |new_file_list|, and removes files not in the new list."""
385
386         previous_file_list = []
387
388         import_log_file = os.path.join(import_directory, 'w3c-import.log')
389         if not os.path.exists(import_log_file):
390             return
391
392         import_log = open(import_log_file, 'r')
393         contents = import_log.readlines()
394
395         if 'List of files\n' in contents:
396             list_index = contents.index('List of files:\n') + 1
397             previous_file_list = [filename.strip() for filename in contents[list_index:]]
398
399         deleted_files = set(previous_file_list) - set(new_file_list)
400         for deleted_file in deleted_files:
401             print 'Deleting file removed from the W3C repo:' + deleted_file
402             deleted_file = os.path.join(self._webkit_root, deleted_file)
403             os.remove(deleted_file)
404
405         import_log.close()
406
407     def write_import_log(self, import_directory, file_list, prop_list):
408         """ Writes a w3c-import.log file in each directory with imported files. """
409
410         now = datetime.datetime.now()
411
412         import_log = open(os.path.join(import_directory, 'w3c-import.log'), 'w')
413         import_log.write('The tests in this directory were imported from the W3C repository.\n')
414         import_log.write('Do NOT modify these tests directly in Webkit. Instead, push changes to the W3C CSS repo:\n\n')
415         import_log.write('http://hg.csswg.org/test\n\n')
416         import_log.write('Then run the Tools/Scripts/import-w3c-tests in Webkit to reimport\n\n')
417         import_log.write('Do NOT modify or remove this file\n\n')
418         import_log.write('------------------------------------------------------------------------\n')
419         import_log.write('Last Import: ' + now.strftime('%Y-%m-%d %H:%M') + '\n')
420         import_log.write('W3C Mercurial changeset: ' + self.changeset + '\n')
421         import_log.write('Test status at time of import: ' + self.test_status + '\n')
422         import_log.write('------------------------------------------------------------------------\n')
423         import_log.write('Properties requiring vendor prefixes:\n')
424         if prop_list:
425             for prop in prop_list:
426                 import_log.write(prop + '\n')
427         else:
428             import_log.write('None\n')
429         import_log.write('------------------------------------------------------------------------\n')
430         import_log.write('List of files:\n')
431         for item in file_list:
432             import_log.write(item + '\n')
433
434         import_log.close()