1 # Copyright (c) 2010 Google Inc. All rights reserved.
3 # Redistribution and use in source and binary forms, with or without
4 # modification, are permitted provided that the following conditions are
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
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.
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.
34 from webkitpy.layout_tests.port.base import Port
35 from webkitpy.tool.servers.reflectionhandler import ReflectionHandler
38 STATE_NEEDS_REBASELINE = 'needs_rebaseline'
39 STATE_REBASELINE_FAILED = 'rebaseline_failed'
40 STATE_REBASELINE_SUCCEEDED = 'rebaseline_succeeded'
43 def _get_actual_result_files(test_file, test_config):
44 test_name, _ = os.path.splitext(test_file)
45 test_directory = os.path.dirname(test_file)
47 test_results_directory = test_config.filesystem.join(
48 test_config.results_directory, test_directory)
49 actual_pattern = os.path.basename(test_name) + '-actual.*'
51 for filename in test_config.filesystem.listdir(test_results_directory):
52 if fnmatch.fnmatch(filename, actual_pattern):
53 actual_files.append(filename)
55 return tuple(actual_files)
58 def _rebaseline_test(test_file, baseline_target, baseline_move_to, test_config, log):
59 test_name, _ = os.path.splitext(test_file)
60 test_directory = os.path.dirname(test_name)
62 log('Rebaselining %s...' % test_name)
64 actual_result_files = _get_actual_result_files(test_file, test_config)
65 filesystem = test_config.filesystem
67 layout_tests_directory = test_config.layout_tests_directory
68 results_directory = test_config.results_directory
69 target_expectations_directory = filesystem.join(
70 layout_tests_directory, 'platform', baseline_target, test_directory)
71 test_results_directory = test_config.filesystem.join(
72 test_config.results_directory, test_directory)
74 # If requested, move current baselines out
75 current_baselines = get_test_baselines(test_file, test_config)
76 if baseline_target in current_baselines and baseline_move_to != 'none':
77 log(' Moving current %s baselines to %s' %
78 (baseline_target, baseline_move_to))
80 # See which ones we need to move (only those that are about to be
81 # updated), and make sure we're not clobbering any files in the
83 current_extensions = set(current_baselines[baseline_target].keys())
84 actual_result_extensions = [
85 os.path.splitext(f)[1] for f in actual_result_files]
86 extensions_to_move = current_extensions.intersection(
87 actual_result_extensions)
89 if extensions_to_move.intersection(
90 current_baselines.get(baseline_move_to, {}).keys()):
91 log(' Already had baselines in %s, could not move existing '
92 '%s ones' % (baseline_move_to, baseline_target))
96 if extensions_to_move:
97 if not _move_test_baselines(
99 list(extensions_to_move),
106 log(' No current baselines to move')
108 log(' Updating baselines for %s' % baseline_target)
109 filesystem.maybe_make_directory(target_expectations_directory)
110 for source_file in actual_result_files:
111 source_path = filesystem.join(test_results_directory, source_file)
112 destination_file = source_file.replace('-actual', '-expected')
113 destination_path = filesystem.join(
114 target_expectations_directory, destination_file)
115 filesystem.copyfile(source_path, destination_path)
116 exit_code = scm.add(destination_path, return_exit_code=True)
118 log(' Could not update %s in SCM, exit code %d' %
119 (destination_file, exit_code))
122 log(' Updated %s' % destination_file)
127 def _move_test_baselines(test_file, extensions_to_move, source_platform, destination_platform, test_config, log):
128 test_file_name = os.path.splitext(os.path.basename(test_file))[0]
129 test_directory = os.path.dirname(test_file)
130 filesystem = test_config.filesystem
132 # Want predictable output order for unit tests.
133 extensions_to_move.sort()
135 source_directory = os.path.join(
136 test_config.layout_tests_directory,
140 destination_directory = os.path.join(
141 test_config.layout_tests_directory,
143 destination_platform,
145 filesystem.maybe_make_directory(destination_directory)
147 for extension in extensions_to_move:
148 file_name = test_file_name + '-expected' + extension
149 source_path = filesystem.join(source_directory, file_name)
150 destination_path = filesystem.join(destination_directory, file_name)
151 filesystem.copyfile(source_path, destination_path)
152 exit_code = test_config.scm.add(destination_path, return_exit_code=True)
154 log(' Could not update %s in SCM, exit code %d' %
155 (file_name, exit_code))
158 log(' Moved %s' % file_name)
163 def get_test_baselines(test_file, test_config):
164 # FIXME: This seems like a hack. This only seems used to access the Port.expected_baselines logic.
165 class AllPlatformsPort(Port):
166 def __init__(self, host):
167 super(AllPlatformsPort, self).__init__(host, 'mac')
168 self._platforms_by_directory = dict([(self._webkit_baseline_path(p), p) for p in test_config.platforms])
170 def baseline_search_path(self):
171 return self._platforms_by_directory.keys()
173 def platform_from_directory(self, directory):
174 return self._platforms_by_directory[directory]
176 test_path = test_config.filesystem.join(test_config.layout_tests_directory, test_file)
177 host = test_config.host
178 all_platforms_port = AllPlatformsPort(host)
180 all_test_baselines = {}
181 for baseline_extension in ('.txt', '.checksum', '.png'):
182 test_baselines = test_config.test_port.expected_baselines(test_file, baseline_extension)
183 baselines = all_platforms_port.expected_baselines(test_file, baseline_extension, all_baselines=True)
184 for platform_directory, expected_filename in baselines:
185 if not platform_directory:
187 if platform_directory == test_config.layout_tests_directory:
190 platform = all_platforms_port.platform_from_directory(platform_directory)
191 platform_baselines = all_test_baselines.setdefault(platform, {})
192 was_used_for_test = (platform_directory, expected_filename) in test_baselines
193 platform_baselines[baseline_extension] = was_used_for_test
195 return all_test_baselines
198 class RebaselineHTTPServer(BaseHTTPServer.HTTPServer):
199 def __init__(self, httpd_port, config):
201 BaseHTTPServer.HTTPServer.__init__(self, (server_name, httpd_port), RebaselineHTTPRequestHandler)
202 self.test_config = config['test_config']
203 self.results_json = config['results_json']
204 self.platforms_json = config['platforms_json']
207 class RebaselineHTTPRequestHandler(ReflectionHandler):
208 STATIC_FILE_NAMES = frozenset([
217 STATIC_FILE_DIRECTORY = os.path.join(os.path.dirname(__file__), "data", "rebaselineserver")
219 def results_json(self):
220 self._serve_json(self.server.results_json)
222 def test_config(self):
223 self._serve_json(self.server.test_config)
225 def platforms_json(self):
226 self._serve_json(self.server.platforms_json)
228 def rebaseline(self):
229 test = self.query['test'][0]
230 baseline_target = self.query['baseline-target'][0]
231 baseline_move_to = self.query['baseline-move-to'][0]
232 test_json = self.server.results_json['tests'][test]
234 if test_json['state'] != STATE_NEEDS_REBASELINE:
235 self.send_error(400, "Test %s is in unexpected state: %s" % (test, test_json["state"]))
239 success = _rebaseline_test(
243 self.server.test_config,
244 log=lambda l: log.append(l))
247 test_json['state'] = STATE_REBASELINE_SUCCEEDED
248 self.send_response(200)
250 test_json['state'] = STATE_REBASELINE_FAILED
251 self.send_response(500)
253 self.send_header('Content-type', 'text/plain')
255 self.wfile.write('\n'.join(log))
257 def test_result(self):
258 test_name, _ = os.path.splitext(self.query['test'][0])
259 mode = self.query['mode'][0]
260 if mode == 'expected-image':
261 file_name = test_name + '-expected.png'
262 elif mode == 'actual-image':
263 file_name = test_name + '-actual.png'
264 if mode == 'expected-checksum':
265 file_name = test_name + '-expected.checksum'
266 elif mode == 'actual-checksum':
267 file_name = test_name + '-actual.checksum'
268 elif mode == 'diff-image':
269 file_name = test_name + '-diff.png'
270 if mode == 'expected-text':
271 file_name = test_name + '-expected.txt'
272 elif mode == 'actual-text':
273 file_name = test_name + '-actual.txt'
274 elif mode == 'diff-text':
275 file_name = test_name + '-diff.txt'
276 elif mode == 'diff-text-pretty':
277 file_name = test_name + '-pretty-diff.html'
279 file_path = os.path.join(self.server.test_config.results_directory, file_name)
281 # Let results be cached for 60 seconds, so that they can be pre-fetched
283 self._serve_file(file_path, cacheable_seconds=60)