Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(222)

Side by Side Diff: third_party/WebKit/Tools/Scripts/webkitpy/layout_tests/layout_tests_mover.py

Issue 1809283002: Remove layout_tests_mover.py and obsolete local commit param. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Created 4 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(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)
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698