| OLD | NEW |
| (Empty) |
| 1 # Copyright (C) 2013 Google Inc. All rights reserved. | |
| 2 # | |
| 3 # Redistribution and use in source and binary forms, with or without | |
| 4 # modification, are permitted provided that the following conditions are | |
| 5 # met: | |
| 6 # | |
| 7 # * Redistributions of source code must retain the above copyright | |
| 8 # notice, this list of conditions and the following disclaimer. | |
| 9 # * Redistributions in binary form must reproduce the above | |
| 10 # copyright notice, this list of conditions and the following disclaimer | |
| 11 # in the documentation and/or other materials provided with the | |
| 12 # distribution. | |
| 13 # * Neither the name of Google Inc. nor the names of its | |
| 14 # contributors may be used to endorse or promote products derived from | |
| 15 # this software without specific prior written permission. | |
| 16 # | |
| 17 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 18 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 19 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 20 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 21 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 22 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 23 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 24 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 25 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 26 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 27 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 28 | |
| 29 """Moves a directory of LayoutTests. | |
| 30 | |
| 31 Given a path to a directory of LayoutTests, moves that directory, including all
recursive children, | |
| 32 to the specified destination path. Updates all references in tests and resources
to reflect the new | |
| 33 location. Also moves any corresponding platform-specific expected results and up
dates the test | |
| 34 expectations to reflect the move. | |
| 35 | |
| 36 If the destination directory does not exist, it and any missing parent directori
es are created. If | |
| 37 the destination directory already exists, the child members of the origin direct
ory are added to the | |
| 38 destination directory. If any of the child members clash with existing members o
f the destination | |
| 39 directory, the move fails. | |
| 40 | |
| 41 Note that when new entries are added to the test expectations, no attempt is mad
e to group or merge | |
| 42 them with existing entries. This should be be done manually and with lint-test-e
xpectations. | |
| 43 """ | |
| 44 | |
| 45 import copy | |
| 46 import logging | |
| 47 import optparse | |
| 48 import os | |
| 49 import re | |
| 50 import urlparse | |
| 51 | |
| 52 from webkitpy.common.checkout.scm.detection import SCMDetector | |
| 53 from webkitpy.common.host import Host | |
| 54 from webkitpy.common.system.executive import Executive | |
| 55 from webkitpy.common.system.filesystem import FileSystem | |
| 56 from webkitpy.layout_tests.port.base import Port | |
| 57 from webkitpy.layout_tests.models.test_expectations import TestExpectations | |
| 58 | |
| 59 | |
| 60 logging.basicConfig() | |
| 61 _log = logging.getLogger(__name__) | |
| 62 _log.setLevel(logging.INFO) | |
| 63 | |
| 64 PLATFORM_DIRECTORY = 'platform' | |
| 65 | |
| 66 class LayoutTestsMover(object): | |
| 67 | |
| 68 def __init__(self, port=None): | |
| 69 self._port = port | |
| 70 if not self._port: | |
| 71 host = Host() | |
| 72 # Given that we use include_overrides=False and model_all_expectatio
ns=True when | |
| 73 # constructing the TestExpectations object, it doesn't matter which
Port object we use. | |
| 74 self._port = host.port_factory.get() | |
| 75 self._port.host.initialize_scm() | |
| 76 self._filesystem = self._port.host.filesystem | |
| 77 self._scm = self._port.host.scm() | |
| 78 self._layout_tests_root = self._port.layout_tests_dir() | |
| 79 | |
| 80 def _scm_path(self, *paths): | |
| 81 return self._filesystem.join('LayoutTests', *paths) | |
| 82 | |
| 83 def _is_child_path(self, parent, possible_child): | |
| 84 normalized_parent = self._filesystem.normpath(parent) | |
| 85 normalized_child = self._filesystem.normpath(possible_child) | |
| 86 # We need to add a trailing separator to parent to avoid returning true
for cases like | |
| 87 # parent='/foo/b', and possible_child='/foo/bar/baz'. | |
| 88 return normalized_parent == normalized_child or normalized_child.startsw
ith(normalized_parent + self._filesystem.sep) | |
| 89 | |
| 90 def _move_path(self, path, origin, destination): | |
| 91 if not self._is_child_path(origin, path): | |
| 92 return path | |
| 93 return self._filesystem.normpath(self._filesystem.join(destination, self
._filesystem.relpath(path, origin))) | |
| 94 | |
| 95 def _validate_input(self): | |
| 96 if not self._filesystem.isdir(self._absolute_origin): | |
| 97 raise Exception('Source path %s is not a directory' % self._origin) | |
| 98 if not self._is_child_path(self._layout_tests_root, self._absolute_origi
n): | |
| 99 raise Exception('Source path %s is not in LayoutTests directory' % s
elf._origin) | |
| 100 if self._filesystem.isfile(self._absolute_destination): | |
| 101 raise Exception('Destination path %s is a file' % self._destination) | |
| 102 if not self._is_child_path(self._layout_tests_root, self._absolute_desti
nation): | |
| 103 raise Exception('Destination path %s is not in LayoutTests directory
' % self._destination) | |
| 104 | |
| 105 # If destination is an existing directory, we move the children of origi
n into destination. | |
| 106 # However, if any of the children of origin would clash with existing ch
ildren of | |
| 107 # destination, we fail. | |
| 108 # FIXME: Consider adding support for recursively moving into an existing
directory. | |
| 109 if self._filesystem.isdir(self._absolute_destination): | |
| 110 for file_path in self._filesystem.listdir(self._absolute_origin): | |
| 111 if self._filesystem.exists(self._filesystem.join(self._absolute_
destination, file_path)): | |
| 112 raise Exception('Origin path %s clashes with existing destin
ation path %s' % | |
| 113 (self._filesystem.join(self._origin, file_path), sel
f._filesystem.join(self._destination, file_path))) | |
| 114 | |
| 115 def _get_expectations_for_test(self, model, test_path): | |
| 116 """Given a TestExpectationsModel object, finds all expectations that mat
ch the specified | |
| 117 test, specified as a relative path. Handles the fact that expectations m
ay be keyed by | |
| 118 directory. | |
| 119 """ | |
| 120 expectations = set() | |
| 121 if model.has_test(test_path): | |
| 122 expectations.add(model.get_expectation_line(test_path)) | |
| 123 test_path = self._filesystem.dirname(test_path) | |
| 124 while not test_path == '': | |
| 125 # The model requires a trailing slash for directories. | |
| 126 test_path_for_model = test_path + '/' | |
| 127 if model.has_test(test_path_for_model): | |
| 128 expectations.add(model.get_expectation_line(test_path_for_model)
) | |
| 129 test_path = self._filesystem.dirname(test_path) | |
| 130 return expectations | |
| 131 | |
| 132 def _get_expectations(self, model, path): | |
| 133 """Given a TestExpectationsModel object, finds all expectations for all
tests under the | |
| 134 specified relative path. | |
| 135 """ | |
| 136 expectations = set() | |
| 137 for test in self._filesystem.files_under(self._filesystem.join(self._lay
out_tests_root, path), dirs_to_skip=['script-tests', 'resources'], | |
| 138 file_filter=Port.is_test_file): | |
| 139 expectations = expectations.union(self._get_expectations_for_test(mo
del, self._filesystem.relpath(test, self._layout_tests_root))) | |
| 140 return expectations | |
| 141 | |
| 142 @staticmethod | |
| 143 def _clone_expectation_line_for_path(expectation_line, path): | |
| 144 """Clones a TestExpectationLine object and updates the clone to apply to
the specified | |
| 145 relative path. | |
| 146 """ | |
| 147 clone = copy.copy(expectation_line) | |
| 148 clone.original_string = re.compile(expectation_line.name).sub(path, expe
ctation_line.original_string) | |
| 149 clone.name = path | |
| 150 clone.path = path | |
| 151 # FIXME: Should we search existing expectations for matches, like in | |
| 152 # TestExpectationsParser._collect_matching_tests()? | |
| 153 clone.matching_tests = [path] | |
| 154 return clone | |
| 155 | |
| 156 def _update_expectations(self): | |
| 157 """Updates all test expectations that are affected by the move. | |
| 158 """ | |
| 159 _log.info('Updating expectations') | |
| 160 test_expectations = TestExpectations(self._port, include_overrides=False
, model_all_expectations=True) | |
| 161 | |
| 162 for expectation in self._get_expectations(test_expectations.model(), sel
f._origin): | |
| 163 path = expectation.path | |
| 164 if self._is_child_path(self._origin, path): | |
| 165 # If the existing expectation is a child of the moved path, we s
imply replace it | |
| 166 # with an expectation for the updated path. | |
| 167 new_path = self._move_path(path, self._origin, self._destination
) | |
| 168 _log.debug('Updating expectation for %s to %s' % (path, new_path
)) | |
| 169 test_expectations.remove_expectation_line(path) | |
| 170 test_expectations.add_expectation_line(LayoutTestsMover._clone_e
xpectation_line_for_path(expectation, new_path)) | |
| 171 else: | |
| 172 # If the existing expectation is not a child of the moved path,
we have to leave it | |
| 173 # in place. But we also add a new expectation for the destinatio
n path. | |
| 174 new_path = self._destination | |
| 175 _log.warning('Copying expectation for %s to %s. You should check
that these expectations are still correct.' % | |
| 176 (path, new_path)) | |
| 177 test_expectations.add_expectation_line(LayoutTestsMover._clone_e
xpectation_line_for_path(expectation, new_path)) | |
| 178 | |
| 179 expectations_file = self._port.path_to_generic_test_expectations_file() | |
| 180 self._filesystem.write_text_file(expectations_file, | |
| 181 TestExpectations.list_to_string(test_ex
pectations._expectations, reconstitute_only_these=[])) | |
| 182 self._scm.add(self._filesystem.relpath(expectations_file, self._scm.chec
kout_root)) | |
| 183 | |
| 184 def _find_references(self, input_files): | |
| 185 """Attempts to find all references to other files in the supplied list o
f files. Returns a | |
| 186 dictionary that maps from an absolute file path to an array of reference
strings. | |
| 187 """ | |
| 188 reference_regex = re.compile(r'(?:(?:src=|href=|importScripts\(|url\()(?
:"([^"]+)"|\'([^\']+)\')|url\(([^\)\'"]+)\))') | |
| 189 references = {} | |
| 190 for input_file in input_files: | |
| 191 matches = reference_regex.findall(self._filesystem.read_binary_file(
input_file)) | |
| 192 if matches: | |
| 193 references[input_file] = [filter(None, match)[0] for match in ma
tches] | |
| 194 return references | |
| 195 | |
| 196 def _get_updated_reference(self, root, reference): | |
| 197 """For a reference <reference> in a directory <root>, determines the upd
ated reference. | |
| 198 Returns the the updated reference, or None if no update is required. | |
| 199 """ | |
| 200 # If the reference is an absolute path or url, it's safe. | |
| 201 if reference.startswith('/') or urlparse.urlparse(reference).scheme: | |
| 202 return None | |
| 203 | |
| 204 # Both the root path and the target of the reference my be subject to th
e move, so there are | |
| 205 # four cases to consider. In the case where both or neither are subject
to the move, the | |
| 206 # reference doesn't need updating. | |
| 207 # | |
| 208 # This is true even if the reference includes superfluous dot segments w
hich mention a moved | |
| 209 # directory, as dot segments are collapsed during URL normalization. For
example, if | |
| 210 # foo.html contains a reference 'bar/../script.js', this remains valid (
though ugly) even if | |
| 211 # bar/ is moved to baz/, because the reference is always normalized to '
script.js'. | |
| 212 absolute_reference = self._filesystem.normpath(self._filesystem.join(roo
t, reference)) | |
| 213 if self._is_child_path(self._absolute_origin, root) == self._is_child_pa
th(self._absolute_origin, absolute_reference): | |
| 214 return None; | |
| 215 | |
| 216 new_root = self._move_path(root, self._absolute_origin, self._absolute_d
estination) | |
| 217 new_absolute_reference = self._move_path(absolute_reference, self._absol
ute_origin, self._absolute_destination) | |
| 218 return self._filesystem.relpath(new_absolute_reference, new_root) | |
| 219 | |
| 220 def _get_all_updated_references(self, references): | |
| 221 """Determines the updated references due to the move. Returns a dictiona
ry that maps from an | |
| 222 absolute file path to a dictionary that maps from a reference string to
the corresponding | |
| 223 updated reference. | |
| 224 """ | |
| 225 updates = {} | |
| 226 for file_path in references.keys(): | |
| 227 root = self._filesystem.dirname(file_path) | |
| 228 # script-tests/TEMPLATE.html files contain references which are writ
ten as if the file | |
| 229 # were in the parent directory. This special-casing is ugly, but the
re are plans to | |
| 230 # remove script-tests. | |
| 231 if root.endswith('script-tests') and file_path.endswith('TEMPLATE.ht
ml'): | |
| 232 root = self._filesystem.dirname(root) | |
| 233 local_updates = {} | |
| 234 for reference in references[file_path]: | |
| 235 update = self._get_updated_reference(root, reference) | |
| 236 if update: | |
| 237 local_updates[reference] = update | |
| 238 if local_updates: | |
| 239 updates[file_path] = local_updates | |
| 240 return updates | |
| 241 | |
| 242 def _update_file(self, path, updates): | |
| 243 contents = self._filesystem.read_binary_file(path) | |
| 244 # Note that this regex isn't quite as strict as that used to find the re
ferences, but this | |
| 245 # avoids the need for alternative match groups, which simplifies things. | |
| 246 for target in updates.keys(): | |
| 247 regex = re.compile(r'((?:src=|href=|importScripts\(|url\()["\']?)%s(
["\']?)' % target) | |
| 248 contents = regex.sub(r'\1%s\2' % updates[target], contents) | |
| 249 self._filesystem.write_binary_file(path, contents) | |
| 250 self._scm.add(path) | |
| 251 | |
| 252 def _update_test_source_files(self): | |
| 253 def is_test_source_file(filesystem, dirname, basename): | |
| 254 pass_regex = re.compile(r'\.(css|js)$') | |
| 255 fail_regex = re.compile(r'-expected\.') | |
| 256 return (Port.is_test_file(filesystem, dirname, basename) or pass_reg
ex.search(basename)) and not fail_regex.search(basename) | |
| 257 | |
| 258 test_source_files = self._filesystem.files_under(self._layout_tests_root
, file_filter=is_test_source_file) | |
| 259 _log.info('Considering %s test source files for references' % len(test_s
ource_files)) | |
| 260 references = self._find_references(test_source_files) | |
| 261 _log.info('Considering references in %s files' % len(references)) | |
| 262 updates = self._get_all_updated_references(references) | |
| 263 _log.info('Updating references in %s files' % len(updates)) | |
| 264 count = 0 | |
| 265 for file_path in updates.keys(): | |
| 266 self._update_file(file_path, updates[file_path]) | |
| 267 count += 1 | |
| 268 if count % 1000 == 0 or count == len(updates): | |
| 269 _log.debug('Updated references in %s files' % count) | |
| 270 | |
| 271 def _move_directory(self, origin, destination): | |
| 272 """Moves the directory <origin> to <destination>. If <destination> is a
directory, moves the | |
| 273 children of <origin> into <destination>. Uses relative paths. | |
| 274 """ | |
| 275 absolute_origin = self._filesystem.join(self._layout_tests_root, origin) | |
| 276 if not self._filesystem.isdir(absolute_origin): | |
| 277 return | |
| 278 _log.info('Moving directory %s to %s' % (origin, destination)) | |
| 279 # Note that FileSystem.move() may silently overwrite existing files, but
we | |
| 280 # check for this in _validate_input(). | |
| 281 absolute_destination = self._filesystem.join(self._layout_tests_root, de
stination) | |
| 282 self._filesystem.maybe_make_directory(absolute_destination) | |
| 283 for directory in self._filesystem.listdir(absolute_origin): | |
| 284 self._scm.move(self._scm_path(origin, directory), self._scm_path(des
tination, directory)) | |
| 285 self._filesystem.rmtree(absolute_origin) | |
| 286 | |
| 287 def _move_files(self): | |
| 288 """Moves the all files that correspond to the move, including platform-s
pecific expected | |
| 289 results. | |
| 290 """ | |
| 291 self._move_directory(self._origin, self._destination) | |
| 292 for directory in self._filesystem.listdir(self._filesystem.join(self._la
yout_tests_root, PLATFORM_DIRECTORY)): | |
| 293 self._move_directory(self._filesystem.join(PLATFORM_DIRECTORY, direc
tory, self._origin), | |
| 294 self._filesystem.join(PLATFORM_DIRECTORY, directory,
self._destination)) | |
| 295 | |
| 296 def _commit_changes(self): | |
| 297 if not self._scm.supports_local_commits(): | |
| 298 return | |
| 299 title = 'Move LayoutTests directory %s to %s' % (self._origin, self._des
tination) | |
| 300 _log.info('Committing change \'%s\'' % title) | |
| 301 self._scm.commit_locally_with_message('%s\n\nThis commit was automatical
ly generated by move-layout-tests.' % title, | |
| 302 commit_all_working_directory_chang
es=False) | |
| 303 | |
| 304 def move(self, origin, destination): | |
| 305 self._origin = origin | |
| 306 self._destination = destination | |
| 307 self._absolute_origin = self._filesystem.join(self._layout_tests_root, s
elf._origin) | |
| 308 self._absolute_destination = self._filesystem.join(self._layout_tests_ro
ot, self._destination) | |
| 309 self._validate_input() | |
| 310 self._update_expectations() | |
| 311 self._update_test_source_files() | |
| 312 self._move_files() | |
| 313 # FIXME: Handle virtual test suites. | |
| 314 self._commit_changes() | |
| 315 | |
| 316 def main(argv): | |
| 317 parser = optparse.OptionParser(description=__doc__) | |
| 318 parser.add_option('--origin', | |
| 319 help=('The directory of tests to move, as a relative path
from the LayoutTests directory.')) | |
| 320 parser.add_option('--destination', | |
| 321 help=('The new path for the directory of tests, as a relat
ive path from the LayoutTests directory.')) | |
| 322 options, _ = parser.parse_args() | |
| 323 LayoutTestsMover().move(options.origin, options.destination) | |
| OLD | NEW |