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.layout_tests.port.base import Port 35 from webkitpy.tool.servers.reflectionhandler import ReflectionHandler 36 37 38 STATE_NEEDS_REBASELINE = 'needs_rebaseline' 39 STATE_REBASELINE_FAILED = 'rebaseline_failed' 40 STATE_REBASELINE_SUCCEEDED = 'rebaseline_succeeded' 41 42 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) 46 47 test_results_directory = test_config.filesystem.join( 48 test_config.results_directory, test_directory) 49 actual_pattern = os.path.basename(test_name) + '-actual.*' 50 actual_files = [] 51 for filename in test_config.filesystem.listdir(test_results_directory): 52 if fnmatch.fnmatch(filename, actual_pattern): 53 actual_files.append(filename) 54 actual_files.sort() 55 return tuple(actual_files) 56 57 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) 61 62 log('Rebaselining %s...' % test_name) 63 64 actual_result_files = _get_actual_result_files(test_file, test_config) 65 filesystem = test_config.filesystem 66 scm = test_config.scm 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) 73 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)) 79 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 82 # destination. 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) 88 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)) 93 return False 94 95 # Do the actual move. 96 if extensions_to_move: 97 if not _move_test_baselines( 98 test_file, 99 list(extensions_to_move), 100 baseline_target, 101 baseline_move_to, 102 test_config, 103 log): 104 return False 105 else: 106 log(' No current baselines to move') 107 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) 117 if exit_code: 118 log(' Could not update %s in SCM, exit code %d' % 119 (destination_file, exit_code)) 120 return False 121 else: 122 log(' Updated %s' % destination_file) 123 124 return True 125 126 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 131 132 # Want predictable output order for unit tests. 133 extensions_to_move.sort() 134 135 source_directory = os.path.join( 136 test_config.layout_tests_directory, 137 'platform', 138 source_platform, 139 test_directory) 140 destination_directory = os.path.join( 141 test_config.layout_tests_directory, 142 'platform', 143 destination_platform, 144 test_directory) 145 filesystem.maybe_make_directory(destination_directory) 146 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) 153 if exit_code: 154 log(' Could not update %s in SCM, exit code %d' % 155 (file_name, exit_code)) 156 return False 157 else: 158 log(' Moved %s' % file_name) 159 160 return True 161 162 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]) 169 170 def baseline_search_path(self): 171 return self._platforms_by_directory.keys() 172 173 def platform_from_directory(self, directory): 174 return self._platforms_by_directory[directory] 175 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) 179 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: 186 continue 187 if platform_directory == test_config.layout_tests_directory: 188 platform = 'base' 189 else: 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 194 195 return all_test_baselines 196 197 198 class RebaselineHTTPServer(BaseHTTPServer.HTTPServer): 199 def __init__(self, httpd_port, config): 200 server_name = "" 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'] 205 206 207 class RebaselineHTTPRequestHandler(ReflectionHandler): 208 STATIC_FILE_NAMES = frozenset([ 209 "index.html", 210 "loupe.js", 211 "main.js", 212 "main.css", 213 "queue.js", 214 "util.js", 215 ]) 216 217 STATIC_FILE_DIRECTORY = os.path.join(os.path.dirname(__file__), "data", "rebaselineserver") 218 219 def results_json(self): 220 self._serve_json(self.server.results_json) 221 222 def test_config(self): 223 self._serve_json(self.server.test_config) 224 225 def platforms_json(self): 226 self._serve_json(self.server.platforms_json) 227 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] 233 234 if test_json['state'] != STATE_NEEDS_REBASELINE: 235 self.send_error(400, "Test %s is in unexpected state: %s" % (test, test_json["state"])) 236 return 237 238 log = [] 239 success = _rebaseline_test( 240 test, 241 baseline_target, 242 baseline_move_to, 243 self.server.test_config, 244 log=lambda l: log.append(l)) 245 246 if success: 247 test_json['state'] = STATE_REBASELINE_SUCCEEDED 248 self.send_response(200) 249 else: 250 test_json['state'] = STATE_REBASELINE_FAILED 251 self.send_response(500) 252 253 self.send_header('Content-type', 'text/plain') 254 self.end_headers() 255 self.wfile.write('\n'.join(log)) 256 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' 278 279 file_path = os.path.join(self.server.test_config.results_directory, file_name) 280 281 # Let results be cached for 60 seconds, so that they can be pre-fetched 282 # by the UI 283 self._serve_file(file_path, cacheable_seconds=60) 284