| OLD | NEW |
| (Empty) |
| 1 # Copyright (c) 2010 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 import fnmatch | |
| 30 import os | |
| 31 import os.path | |
| 32 import BaseHTTPServer | |
| 33 | |
| 34 from webkitpy.common.host import Host # FIXME: This should not be needed! | |
| 35 from webkitpy.layout_tests.port.base import Port | |
| 36 from webkitpy.tool.servers.reflectionhandler import ReflectionHandler | |
| 37 | |
| 38 | |
| 39 STATE_NEEDS_REBASELINE = 'needs_rebaseline' | |
| 40 STATE_REBASELINE_FAILED = 'rebaseline_failed' | |
| 41 STATE_REBASELINE_SUCCEEDED = 'rebaseline_succeeded' | |
| 42 | |
| 43 | |
| 44 def _get_actual_result_files(test_file, test_config): | |
| 45 test_name, _ = os.path.splitext(test_file) | |
| 46 test_directory = os.path.dirname(test_file) | |
| 47 | |
| 48 test_results_directory = test_config.filesystem.join( | |
| 49 test_config.results_directory, test_directory) | |
| 50 actual_pattern = os.path.basename(test_name) + '-actual.*' | |
| 51 actual_files = [] | |
| 52 for filename in test_config.filesystem.listdir(test_results_directory): | |
| 53 if fnmatch.fnmatch(filename, actual_pattern): | |
| 54 actual_files.append(filename) | |
| 55 actual_files.sort() | |
| 56 return tuple(actual_files) | |
| 57 | |
| 58 | |
| 59 def _rebaseline_test(test_file, baseline_target, baseline_move_to, test_config,
log): | |
| 60 test_name, _ = os.path.splitext(test_file) | |
| 61 test_directory = os.path.dirname(test_name) | |
| 62 | |
| 63 log('Rebaselining %s...' % test_name) | |
| 64 | |
| 65 actual_result_files = _get_actual_result_files(test_file, test_config) | |
| 66 filesystem = test_config.filesystem | |
| 67 scm = test_config.scm | |
| 68 layout_tests_directory = test_config.layout_tests_directory | |
| 69 results_directory = test_config.results_directory | |
| 70 target_expectations_directory = filesystem.join( | |
| 71 layout_tests_directory, 'platform', baseline_target, test_directory) | |
| 72 test_results_directory = test_config.filesystem.join( | |
| 73 test_config.results_directory, test_directory) | |
| 74 | |
| 75 # If requested, move current baselines out | |
| 76 current_baselines = get_test_baselines(test_file, test_config) | |
| 77 if baseline_target in current_baselines and baseline_move_to != 'none': | |
| 78 log(' Moving current %s baselines to %s' % | |
| 79 (baseline_target, baseline_move_to)) | |
| 80 | |
| 81 # See which ones we need to move (only those that are about to be | |
| 82 # updated), and make sure we're not clobbering any files in the | |
| 83 # destination. | |
| 84 current_extensions = set(current_baselines[baseline_target].keys()) | |
| 85 actual_result_extensions = [ | |
| 86 os.path.splitext(f)[1] for f in actual_result_files] | |
| 87 extensions_to_move = current_extensions.intersection( | |
| 88 actual_result_extensions) | |
| 89 | |
| 90 if extensions_to_move.intersection( | |
| 91 current_baselines.get(baseline_move_to, {}).keys()): | |
| 92 log(' Already had baselines in %s, could not move existing ' | |
| 93 '%s ones' % (baseline_move_to, baseline_target)) | |
| 94 return False | |
| 95 | |
| 96 # Do the actual move. | |
| 97 if extensions_to_move: | |
| 98 if not _move_test_baselines( | |
| 99 test_file, | |
| 100 list(extensions_to_move), | |
| 101 baseline_target, | |
| 102 baseline_move_to, | |
| 103 test_config, | |
| 104 log): | |
| 105 return False | |
| 106 else: | |
| 107 log(' No current baselines to move') | |
| 108 | |
| 109 log(' Updating baselines for %s' % baseline_target) | |
| 110 filesystem.maybe_make_directory(target_expectations_directory) | |
| 111 for source_file in actual_result_files: | |
| 112 source_path = filesystem.join(test_results_directory, source_file) | |
| 113 destination_file = source_file.replace('-actual', '-expected') | |
| 114 destination_path = filesystem.join( | |
| 115 target_expectations_directory, destination_file) | |
| 116 filesystem.copyfile(source_path, destination_path) | |
| 117 exit_code = scm.add(destination_path, return_exit_code=True) | |
| 118 if exit_code: | |
| 119 log(' Could not update %s in SCM, exit code %d' % | |
| 120 (destination_file, exit_code)) | |
| 121 return False | |
| 122 else: | |
| 123 log(' Updated %s' % destination_file) | |
| 124 | |
| 125 return True | |
| 126 | |
| 127 | |
| 128 def _move_test_baselines(test_file, extensions_to_move, source_platform, destina
tion_platform, test_config, log): | |
| 129 test_file_name = os.path.splitext(os.path.basename(test_file))[0] | |
| 130 test_directory = os.path.dirname(test_file) | |
| 131 filesystem = test_config.filesystem | |
| 132 | |
| 133 # Want predictable output order for unit tests. | |
| 134 extensions_to_move.sort() | |
| 135 | |
| 136 source_directory = os.path.join( | |
| 137 test_config.layout_tests_directory, | |
| 138 'platform', | |
| 139 source_platform, | |
| 140 test_directory) | |
| 141 destination_directory = os.path.join( | |
| 142 test_config.layout_tests_directory, | |
| 143 'platform', | |
| 144 destination_platform, | |
| 145 test_directory) | |
| 146 filesystem.maybe_make_directory(destination_directory) | |
| 147 | |
| 148 for extension in extensions_to_move: | |
| 149 file_name = test_file_name + '-expected' + extension | |
| 150 source_path = filesystem.join(source_directory, file_name) | |
| 151 destination_path = filesystem.join(destination_directory, file_name) | |
| 152 filesystem.copyfile(source_path, destination_path) | |
| 153 exit_code = test_config.scm.add(destination_path, return_exit_code=True) | |
| 154 if exit_code: | |
| 155 log(' Could not update %s in SCM, exit code %d' % | |
| 156 (file_name, exit_code)) | |
| 157 return False | |
| 158 else: | |
| 159 log(' Moved %s' % file_name) | |
| 160 | |
| 161 return True | |
| 162 | |
| 163 | |
| 164 def get_test_baselines(test_file, test_config): | |
| 165 # FIXME: This seems like a hack. This only seems used to access the Port.exp
ected_baselines logic. | |
| 166 class AllPlatformsPort(Port): | |
| 167 def __init__(self, host): | |
| 168 super(AllPlatformsPort, self).__init__(host, 'mac') | |
| 169 self._platforms_by_directory = dict([(self._webkit_baseline_path(p),
p) for p in test_config.platforms]) | |
| 170 | |
| 171 def baseline_search_path(self): | |
| 172 return self._platforms_by_directory.keys() | |
| 173 | |
| 174 def platform_from_directory(self, directory): | |
| 175 return self._platforms_by_directory[directory] | |
| 176 | |
| 177 test_path = test_config.filesystem.join(test_config.layout_tests_directory,
test_file) | |
| 178 | |
| 179 # FIXME: This should get the Host from the test_config to be mockable! | |
| 180 host = Host() | |
| 181 host.initialize_scm() | |
| 182 host.filesystem = test_config.filesystem | |
| 183 all_platforms_port = AllPlatformsPort(host) | |
| 184 | |
| 185 all_test_baselines = {} | |
| 186 for baseline_extension in ('.txt', '.checksum', '.png'): | |
| 187 test_baselines = test_config.test_port.expected_baselines(test_file, bas
eline_extension) | |
| 188 baselines = all_platforms_port.expected_baselines(test_file, baseline_ex
tension, all_baselines=True) | |
| 189 for platform_directory, expected_filename in baselines: | |
| 190 if not platform_directory: | |
| 191 continue | |
| 192 if platform_directory == test_config.layout_tests_directory: | |
| 193 platform = 'base' | |
| 194 else: | |
| 195 platform = all_platforms_port.platform_from_directory(platform_d
irectory) | |
| 196 platform_baselines = all_test_baselines.setdefault(platform, {}) | |
| 197 was_used_for_test = (platform_directory, expected_filename) in test_
baselines | |
| 198 platform_baselines[baseline_extension] = was_used_for_test | |
| 199 | |
| 200 return all_test_baselines | |
| 201 | |
| 202 | |
| 203 class RebaselineHTTPServer(BaseHTTPServer.HTTPServer): | |
| 204 def __init__(self, httpd_port, config): | |
| 205 server_name = "" | |
| 206 BaseHTTPServer.HTTPServer.__init__(self, (server_name, httpd_port), Reba
selineHTTPRequestHandler) | |
| 207 self.test_config = config['test_config'] | |
| 208 self.results_json = config['results_json'] | |
| 209 self.platforms_json = config['platforms_json'] | |
| 210 | |
| 211 | |
| 212 class RebaselineHTTPRequestHandler(ReflectionHandler): | |
| 213 STATIC_FILE_NAMES = frozenset([ | |
| 214 "index.html", | |
| 215 "loupe.js", | |
| 216 "main.js", | |
| 217 "main.css", | |
| 218 "queue.js", | |
| 219 "util.js", | |
| 220 ]) | |
| 221 | |
| 222 STATIC_FILE_DIRECTORY = os.path.join(os.path.dirname(__file__), "data", "reb
aselineserver") | |
| 223 | |
| 224 def results_json(self): | |
| 225 self._serve_json(self.server.results_json) | |
| 226 | |
| 227 def test_config(self): | |
| 228 self._serve_json(self.server.test_config) | |
| 229 | |
| 230 def platforms_json(self): | |
| 231 self._serve_json(self.server.platforms_json) | |
| 232 | |
| 233 def rebaseline(self): | |
| 234 test = self.query['test'][0] | |
| 235 baseline_target = self.query['baseline-target'][0] | |
| 236 baseline_move_to = self.query['baseline-move-to'][0] | |
| 237 test_json = self.server.results_json['tests'][test] | |
| 238 | |
| 239 if test_json['state'] != STATE_NEEDS_REBASELINE: | |
| 240 self.send_error(400, "Test %s is in unexpected state: %s" % (test, t
est_json["state"])) | |
| 241 return | |
| 242 | |
| 243 log = [] | |
| 244 success = _rebaseline_test( | |
| 245 test, | |
| 246 baseline_target, | |
| 247 baseline_move_to, | |
| 248 self.server.test_config, | |
| 249 log=lambda l: log.append(l)) | |
| 250 | |
| 251 if success: | |
| 252 test_json['state'] = STATE_REBASELINE_SUCCEEDED | |
| 253 self.send_response(200) | |
| 254 else: | |
| 255 test_json['state'] = STATE_REBASELINE_FAILED | |
| 256 self.send_response(500) | |
| 257 | |
| 258 self.send_header('Content-type', 'text/plain') | |
| 259 self.end_headers() | |
| 260 self.wfile.write('\n'.join(log)) | |
| 261 | |
| 262 def test_result(self): | |
| 263 test_name, _ = os.path.splitext(self.query['test'][0]) | |
| 264 mode = self.query['mode'][0] | |
| 265 if mode == 'expected-image': | |
| 266 file_name = test_name + '-expected.png' | |
| 267 elif mode == 'actual-image': | |
| 268 file_name = test_name + '-actual.png' | |
| 269 if mode == 'expected-checksum': | |
| 270 file_name = test_name + '-expected.checksum' | |
| 271 elif mode == 'actual-checksum': | |
| 272 file_name = test_name + '-actual.checksum' | |
| 273 elif mode == 'diff-image': | |
| 274 file_name = test_name + '-diff.png' | |
| 275 if mode == 'expected-text': | |
| 276 file_name = test_name + '-expected.txt' | |
| 277 elif mode == 'actual-text': | |
| 278 file_name = test_name + '-actual.txt' | |
| 279 elif mode == 'diff-text': | |
| 280 file_name = test_name + '-diff.txt' | |
| 281 elif mode == 'diff-text-pretty': | |
| 282 file_name = test_name + '-pretty-diff.html' | |
| 283 | |
| 284 file_path = os.path.join(self.server.test_config.results_directory, file
_name) | |
| 285 | |
| 286 # Let results be cached for 60 seconds, so that they can be pre-fetched | |
| 287 # by the UI | |
| 288 self._serve_file(file_path, cacheable_seconds=60) | |
| OLD | NEW |