test_importer.py 19 KB

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