- add third_party src.
[platform/framework/web/crosswalk.git] / src / third_party / WebKit / 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 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
49       csswg repo
50
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.
55
56     - By default, only reftests and jstest are imported. This can be overridden with a -a or --all
57       argument
58
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
62
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)
73
74      - Upon completion, script outputs the total number tests imported, broken down by test type
75
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
79        maintenance.
80
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.
83 """
84
85 # FIXME: Change this file to use the Host abstractions rather that os, sys, shutils, etc.
86
87 import datetime
88 import logging
89 import mimetypes
90 import optparse
91 import os
92 import shutil
93 import sys
94
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
100
101
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]
106
107 CONTRIBUTOR_DIR_NAME = 'contributors'
108
109 CHANGESET_NOT_AVAILABLE = 'Not Available'
110
111
112 _log = logging.getLogger(__name__)
113
114
115 def main(_argv, _stdout, _stderr):
116     options, args = parse_args()
117     import_dir = args[0]
118     if len(args) == 1:
119         repo_dir_parts = []
120         for part in import_dir.split(os.path.sep):
121             if part in VALID_TEST_STATUSES:
122                 break
123             else:
124                 repo_dir_parts.append(part)
125         repo_dir = os.path.sep.join(repo_dir_parts)
126     else:
127         repo_dir = args[1]
128
129     if not os.path.exists(import_dir):
130         sys.exit('Source directory %s not found!' % import_dir)
131
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))
136
137     configure_logging()
138
139     test_importer = TestImporter(Host(), import_dir, repo_dir, options)
140     test_importer.do_import()
141
142
143 def configure_logging():
144     class LogHandler(logging.StreamHandler):
145
146         def format(self, record):
147             if record.levelno > logging.INFO:
148                 return "%s: %s" % (record.levelname, record.getMessage())
149             return record.getMessage()
150
151     logger = logging.getLogger()
152     logger.setLevel(logging.INFO)
153     handler = LogHandler()
154     handler.setLevel(logging.INFO)
155     logger.addHandler(handler)
156     return handler
157
158
159 def parse_args():
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')
167
168     options, args = parser.parse_args()
169     if len(args) not in (1, 2):
170         parser.error('Incorrect number of arguments')
171     return options, args
172
173
174 class TestImporter(object):
175
176     def __init__(self, host, source_directory, repo_dir, options):
177         self.host = host
178         self.source_directory = source_directory
179         self.options = options
180
181         self.filesystem = self.host.filesystem
182
183         webkit_finder = WebKitFinder(self.filesystem)
184         self._webkit_root = webkit_finder.webkit_base()
185         self.repo_dir = repo_dir
186
187         self.destination_directory = webkit_finder.path_from_webkit_base("LayoutTests", options.destination)
188
189         self.changeset = CHANGESET_NOT_AVAILABLE
190         self.test_status = TEST_STATUS_UNKNOWN
191
192         self.import_list = []
193
194     def do_import(self):
195         self.find_importable_tests(self.source_directory)
196         self.load_changeset()
197         self.import_tests()
198
199     def load_changeset(self):
200         """Returns the current changeset from mercurial or "Not Available"."""
201         try:
202             self.changeset = self.host.executive.run_command(['hg', 'tip']).split('changeset:')[1]
203         except (OSError, ScriptError):
204             self.changeset = CHANGESET_NOT_AVAILABLE
205
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 + '...')
210             total_tests = 0
211             reftests = 0
212             jstests = 0
213
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:
219                 if d in dirs:
220                     dirs.remove(d)
221
222             copy_list = []
223
224             for filename in files:
225                 # FIXME: This block should really be a separate function, but the early-continues make that difficult.
226
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.
229
230                 fullpath = os.path.join(root, filename)
231
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})
235                     continue
236
237                 test_parser = TestParser(vars(self.options), filename=fullpath)
238                 test_info = test_parser.analyze_test()
239                 if test_info is None:
240                     continue
241
242                 if 'reference' in test_info.keys():
243                     reftests += 1
244                     total_tests += 1
245                     test_basename = os.path.basename(test_info['test'])
246
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
251                     # reference files.
252                     ref_file = os.path.splitext(test_basename)[0] + '-expected'
253                     ref_file += os.path.splitext(test_basename)[1]
254
255                     copy_list.append({'src': test_info['reference'], 'dest': ref_file})
256                     copy_list.append({'src': test_info['test'], 'dest': filename})
257
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)
263
264                             # Keep the dest as it was
265                             to_copy = {'src': source_file, 'dest': support_file}
266
267                             # Only add it once
268                             if not(to_copy in copy_list):
269                                 copy_list.append(to_copy)
270                 elif 'jstest' in test_info.keys():
271                     jstests += 1
272                     total_tests += 1
273                     copy_list.append({'src': fullpath, 'dest': filename})
274                 else:
275                     total_tests += 1
276                     copy_list.append({'src': fullpath, 'dest': filename})
277
278             if not total_tests:
279                 # We can skip the support directory if no tests were found.
280                 if 'support' in dirs:
281                     dirs.remove('support')
282
283             if copy_list:
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})
287
288     def import_tests(self):
289         total_imported_tests = 0
290         total_imported_reftests = 0
291         total_imported_jstests = 0
292         total_prefixed_properties = {}
293
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']
298
299             prefixed_properties = []
300
301             if not dir_to_copy['copy_list']:
302                 continue
303
304             orig_path = dir_to_copy['dirname']
305
306             subpath = os.path.relpath(orig_path, self.repo_dir)
307             new_path = os.path.join(self.destination_directory, subpath)
308
309             if not(os.path.exists(new_path)):
310                 os.makedirs(new_path)
311
312             copied_files = []
313
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'])
317
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)
321                     continue
322
323                 if not(os.path.exists(orig_filepath)):
324                     _log.warning('%s not found. Possible error in the test.', orig_filepath)
325                     continue
326
327                 new_filepath = os.path.join(new_path, file_to_copy['dest'])
328
329                 if not(os.path.exists(os.path.dirname(new_filepath))):
330                     os.makedirs(os.path.dirname(new_filepath))
331
332                 if not self.options.overwrite and os.path.exists(new_filepath):
333                     _log.info('Skipping import of existing file ' + new_filepath)
334                 else:
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)
340
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)
346
347                     if not converted_file:
348                         shutil.copyfile(orig_filepath, new_filepath)  # The file was unmodified.
349                     else:
350                         for prefixed_property in converted_file[0]:
351                             total_prefixed_properties.setdefault(prefixed_property, 0)
352                             total_prefixed_properties[prefixed_property] += 1
353
354                         prefixed_properties.extend(set(converted_file[0]) - set(prefixed_properties))
355                         outfile = open(new_filepath, 'wb')
356                         outfile.write(converted_file[1])
357                         outfile.close()
358                 else:
359                     shutil.copyfile(orig_filepath, new_filepath)
360
361                 copied_files.append(new_filepath.replace(self._webkit_root, ''))
362
363             self.remove_deleted_files(new_path, copied_files)
364             self.write_import_log(new_path, copied_files, prefixed_properties)
365
366         _log.info('Import complete')
367
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)
372         _log.info('')
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])
376
377     def setup_destination_directory(self):
378         """ Creates a destination directory that mirrors that of the source approved or submitted directory """
379
380         self.update_test_status()
381
382         start = self.source_directory.find(self.test_status)
383         new_subpath = self.source_directory[len(self.repo_dir):]
384
385         destination_directory = os.path.join(self.destination_directory, new_subpath)
386
387         if not os.path.exists(destination_directory):
388             os.makedirs(destination_directory)
389
390         _log.info('Tests will be imported into: %s', destination_directory)
391
392     def update_test_status(self):
393         """ Sets the test status to either 'approved' or 'submitted' """
394
395         status = TEST_STATUS_UNKNOWN
396
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:
400                 status = test_status
401
402         self.test_status = status
403
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."""
406
407         previous_file_list = []
408
409         import_log_file = os.path.join(import_directory, 'w3c-import.log')
410         if not os.path.exists(import_log_file):
411             return
412
413         import_log = open(import_log_file, 'r')
414         contents = import_log.readlines()
415
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:]]
419
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)
425
426         import_log.close()
427
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. """
430
431         now = datetime.datetime.now()
432
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')
445         if prop_list:
446             for prop in prop_list:
447                 import_log.write(prop + '\n')
448         else:
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')
454
455         import_log.close()