1 #!/usr/bin/env python 2 # Copyright (C) 2010 Google Inc. All rights reserved. 3 # 4 # Redistribution and use in source and binary forms, with or without 5 # modification, are permitted provided that the following conditions are 6 # met: 7 # 8 # * Redistributions of source code must retain the above copyright 9 # notice, this list of conditions and the following disclaimer. 10 # * Redistributions in binary form must reproduce the above 11 # copyright notice, this list of conditions and the following disclaimer 12 # in the documentation and/or other materials provided with the 13 # distribution. 14 # * Neither the Google name nor the names of its 15 # contributors may be used to endorse or promote products derived from 16 # this software without specific prior written permission. 17 # 18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 30 """WebKit Mac implementation of the Port interface.""" 31 32 import fcntl 33 import logging 34 import os 35 import pdb 36 import platform 37 import select 38 import signal 39 import subprocess 40 import sys 41 import time 42 import webbrowser 43 44 import base 45 46 import webkitpy 47 from webkitpy import executive 48 49 class MacPort(base.Port): 50 """WebKit Mac implementation of the Port class.""" 51 52 def __init__(self, port_name=None, options=None): 53 if port_name is None: 54 port_name = 'mac' + self.version() 55 base.Port.__init__(self, port_name, options) 56 self._cached_build_root = None 57 58 def baseline_search_path(self): 59 dirs = [] 60 if self._name == 'mac-tiger': 61 dirs.append(self._webkit_baseline_path(self._name)) 62 if self._name in ('mac-tiger', 'mac-leopard'): 63 dirs.append(self._webkit_baseline_path('mac-leopard')) 64 if self._name in ('mac-tiger', 'mac-leopard', 'mac-snowleopard'): 65 dirs.append(self._webkit_baseline_path('mac-snowleopard')) 66 dirs.append(self._webkit_baseline_path('mac')) 67 return dirs 68 69 def check_sys_deps(self): 70 # FIXME: This should run build-dumprendertree. 71 # This should also validate that all of the tool paths are valid. 72 return True 73 74 def num_cores(self): 75 return int(os.popen2("sysctl -n hw.ncpu")[1].read()) 76 77 def results_directory(self): 78 return ('/tmp/run-chromium-webkit-tests-' + 79 self._options.results_directory) 80 81 def setup_test_run(self): 82 # This port doesn't require any specific configuration. 83 pass 84 85 def show_results_html_file(self, results_filename): 86 uri = self.filename_to_uri(results_filename) 87 webbrowser.open(uri, new=1) 88 89 def start_driver(self, image_path, options): 90 """Starts a new Driver and returns a handle to it.""" 91 return MacDriver(self, image_path, options) 92 93 def start_helper(self): 94 # This port doesn't use a helper process. 95 pass 96 97 def stop_helper(self): 98 # This port doesn't use a helper process. 99 pass 100 101 def test_base_platform_names(self): 102 # At the moment we don't use test platform names, but we have 103 # to return something. 104 return ('mac',) 105 106 def test_expectations(self): 107 # 108 # The WebKit mac port uses 'Skipped' files at the moment. Each 109 # file contains a list of files or directories to be skipped during 110 # the test run. The total list of tests to skipped is given by the 111 # contents of the generic Skipped file found in platform/X plus 112 # a version-specific file found in platform/X-version. Duplicate 113 # entries are allowed. This routine reads those files and turns 114 # contents into the format expected by test_expectations. 115 expectations = [] 116 skipped_files = [] 117 if self._name in ('mac-tiger', 'mac-leopard', 'mac-snowleopard'): 118 skipped_files.append(os.path.join( 119 self._webkit_baseline_path(self._name), 'Skipped')) 120 skipped_files.append(os.path.join(self._webkit_baseline_path('mac'), 121 'Skipped')) 122 for filename in skipped_files: 123 if os.path.exists(filename): 124 f = file(filename) 125 for l in f.readlines(): 126 l = l.strip() 127 if not l.startswith('#') and len(l): 128 l = 'BUG_SKIPPED SKIP : ' + l + ' = FAIL' 129 if l not in expectations: 130 expectations.append(l) 131 f.close() 132 133 # TODO - figure out how to check for these dynamically 134 expectations.append('BUG_SKIPPED SKIP : fast/wcss = FAIL') 135 expectations.append('BUG_SKIPPED SKIP : fast/xhtmlmp = FAIL') 136 expectations.append('BUG_SKIPPED SKIP : http/tests/wml = FAIL') 137 expectations.append('BUG_SKIPPED SKIP : mathml = FAIL') 138 expectations.append('BUG_SKIPPED SKIP : platform/chromium = FAIL') 139 expectations.append('BUG_SKIPPED SKIP : platform/gtk = FAIL') 140 expectations.append('BUG_SKIPPED SKIP : platform/qt = FAIL') 141 expectations.append('BUG_SKIPPED SKIP : platform/win = FAIL') 142 expectations.append('BUG_SKIPPED SKIP : wml = FAIL') 143 144 # TODO - figure out how to handle webarchive tests 145 expectations.append('BUG_SKIPPED SKIP : webarchive = PASS') 146 expectations.append('BUG_SKIPPED SKIP : svg/webarchive = PASS') 147 expectations.append('BUG_SKIPPED SKIP : http/tests/webarchive = PASS') 148 expectations.append('BUG_SKIPPED SKIP : svg/custom/' 149 'image-with-prefix-in-webarchive.svg = PASS') 150 151 expectations_str = '\n'.join(expectations) 152 return expectations_str 153 154 def test_platform_name(self): 155 # At the moment we don't use test platform names, but we have 156 # to return something. 157 return 'mac' 158 159 def test_platform_names(self): 160 # At the moment we don't use test platform names, but we have 161 # to return something. 162 return ('mac',) 163 164 def version(self): 165 os_version_string = platform.mac_ver()[0] # e.g. "10.5.6" 166 if not os_version_string: 167 return '-leopard' 168 release_version = int(os_version_string.split('.')[1]) 169 if release_version == 4: 170 return '-tiger' 171 elif release_version == 5: 172 return '-leopard' 173 elif release_version == 6: 174 return '-snowleopard' 175 return '' 176 177 # 178 # PROTECTED METHODS 179 # 180 181 def _build_path(self, *comps): 182 if not self._cached_build_root: 183 self._cached_build_root = executive.run_command(["webkit-build-directory", "--base"]).rstrip() 184 return os.path.join(self._cached_build_root, self._options.target, *comps) 185 186 def _kill_process(self, pid): 187 """Forcefully kill the process. 188 189 Args: 190 pid: The id of the process to be killed. 191 """ 192 os.kill(pid, signal.SIGKILL) 193 194 def _kill_all_process(self, process_name): 195 # On Mac OS X 10.6, killall has a new constraint: -SIGNALNAME or 196 # -SIGNALNUMBER must come first. Example problem: 197 # $ killall -u $USER -TERM lighttpd 198 # killall: illegal option -- T 199 # Use of the earlier -TERM placement is just fine on 10.5. 200 null = open(os.devnull) 201 subprocess.call(['killall', '-TERM', '-u', os.getenv('USER'), 202 process_name], stderr=null) 203 null.close() 204 205 def _path_to_apache(self): 206 return '/usr/sbin/httpd' 207 208 def _path_to_apache_config_file(self): 209 return os.path.join(self.layout_tests_dir(), 'http', 'conf', 210 'apache2-httpd.conf') 211 212 def _path_to_driver(self): 213 return self._build_path('DumpRenderTree') 214 215 def _path_to_helper(self): 216 return None 217 218 def _path_to_image_diff(self): 219 return self._build_path('image_diff') # FIXME: This is wrong and should be "ImageDiff", but having the correct path causes other parts of the script to hang. 220 221 def _path_to_wdiff(self): 222 return 'wdiff' # FIXME: This does not exist on a default Mac OS X Leopard install. 223 224 def _shut_down_http_server(self, server_pid): 225 """Shut down the lighttpd web server. Blocks until it's fully 226 shut down. 227 228 Args: 229 server_pid: The process ID of the running server. 230 """ 231 # server_pid is not set when "http_server.py stop" is run manually. 232 if server_pid is None: 233 # TODO(mmoss) This isn't ideal, since it could conflict with 234 # lighttpd processes not started by http_server.py, 235 # but good enough for now. 236 self._kill_all_process('httpd') 237 else: 238 try: 239 os.kill(server_pid, signal.SIGTERM) 240 # TODO(mmoss) Maybe throw in a SIGKILL just to be sure? 241 except OSError: 242 # Sometimes we get a bad PID (e.g. from a stale httpd.pid 243 # file), so if kill fails on the given PID, just try to 244 # 'killall' web servers. 245 self._shut_down_http_server(None) 246 247 248 class MacDriver(base.Driver): 249 """implementation of the DumpRenderTree interface.""" 250 251 def __init__(self, port, image_path, driver_options): 252 self._port = port 253 self._driver_options = driver_options 254 self._target = port._options.target 255 self._image_path = image_path 256 self._stdout_fd = None 257 self._cmd = None 258 self._env = None 259 self._proc = None 260 self._read_buffer = '' 261 262 cmd = [] 263 # Hook for injecting valgrind or other runtime instrumentation, 264 # used by e.g. tools/valgrind/valgrind_tests.py. 265 wrapper = os.environ.get("BROWSER_WRAPPER", None) 266 if wrapper != None: 267 cmd += [wrapper] 268 if self._port._options.wrapper: 269 # This split() isn't really what we want -- it incorrectly will 270 # split quoted strings within the wrapper argument -- but in 271 # practice it shouldn't come up and the --help output warns 272 # about it anyway. 273 cmd += self._options.wrapper.split() 274 # FIXME: Using arch here masks any possible file-not-found errors from a non-existant driver executable. 275 cmd += ['arch', '-i386', port._path_to_driver(), '-'] 276 277 # FIXME: This is a hack around our lack of ImageDiff support for now. 278 if not self._port._options.no_pixel_tests: 279 logging.warn("This port does not yet support pixel tests.") 280 self._port._options.no_pixel_tests = True 281 #cmd.append('--pixel-tests') 282 283 #if driver_options: 284 # cmd += driver_options 285 env = os.environ 286 env['DYLD_FRAMEWORK_PATH'] = self._port._build_path() 287 self._cmd = cmd 288 self._env = env 289 self.restart() 290 291 def poll(self): 292 return self._proc.poll() 293 294 def restart(self): 295 self.stop() 296 self._proc = subprocess.Popen(self._cmd, stdin=subprocess.PIPE, 297 stdout=subprocess.PIPE, 298 stderr=subprocess.PIPE, 299 env=self._env) 300 301 def returncode(self): 302 return self._proc.returncode 303 304 def run_test(self, uri, timeoutms, image_hash): 305 output = [] 306 error = [] 307 image = '' 308 crash = False 309 timeout = False 310 actual_uri = None 311 actual_image_hash = None 312 313 if uri.startswith("file:///"): 314 cmd = uri[7:] 315 else: 316 cmd = uri 317 318 if image_hash: 319 cmd += "'" + image_hash 320 cmd += "\n" 321 322 self._proc.stdin.write(cmd) 323 self._stdout_fd = self._proc.stdout.fileno() 324 fl = fcntl.fcntl(self._stdout_fd, fcntl.F_GETFL) 325 fcntl.fcntl(self._stdout_fd, fcntl.F_SETFL, fl | os.O_NONBLOCK) 326 327 stop_time = time.time() + (int(timeoutms) / 1000.0) 328 resp = '' 329 (timeout, line) = self._read_line(timeout, stop_time) 330 resp += line 331 have_seen_content_type = False 332 while not timeout and line.rstrip() != "#EOF": 333 # Make sure we haven't crashed. 334 if line == '' and self.poll() is not None: 335 # This is hex code 0xc000001d, which is used for abrupt 336 # termination. This happens if we hit ctrl+c from the prompt 337 # and we happen to be waiting on the test_shell. 338 # sdoyon: Not sure for which OS and in what circumstances the 339 # above code is valid. What works for me under Linux to detect 340 # ctrl+c is for the subprocess returncode to be negative 341 # SIGINT. And that agrees with the subprocess documentation. 342 if (-1073741510 == self.returncode() or 343 - signal.SIGINT == self.returncode()): 344 raise KeyboardInterrupt 345 crash = True 346 break 347 348 elif (line.startswith('Content-Type:') and not 349 have_seen_content_type): 350 have_seen_content_type = True 351 pass 352 else: 353 output.append(line) 354 355 (timeout, line) = self._read_line(timeout, stop_time) 356 resp += line 357 358 # Now read a second block of text for the optional image data 359 image_length = 0 360 (timeout, line) = self._read_line(timeout, stop_time) 361 resp += line 362 HASH_HEADER = 'ActualHash: ' 363 LENGTH_HEADER = 'Content-Length: ' 364 while not timeout and not crash and line.rstrip() != "#EOF": 365 if line == '' and self.poll() is not None: 366 if (-1073741510 == self.returncode() or 367 - signal.SIGINT == self.returncode()): 368 raise KeyboardInterrupt 369 crash = True 370 break 371 elif line.startswith(HASH_HEADER): 372 actual_image_hash = line[len(HASH_HEADER):].strip() 373 elif line.startswith('Content-Type:'): 374 pass 375 elif line.startswith(LENGTH_HEADER): 376 image_length = int(line[len(LENGTH_HEADER):]) 377 elif image_length: 378 image += line 379 380 (timeout, line) = self._read_line(timeout, stop_time, image_length) 381 resp += line 382 383 if timeout: 384 self.restart() 385 386 if self._image_path and len(self._image_path): 387 image_file = file(self._image_path, "wb") 388 image_file.write(image) 389 image_file.close() 390 391 return (crash, timeout, actual_image_hash, 392 ''.join(output), ''.join(error)) 393 pass 394 395 def stop(self): 396 if self._proc: 397 self._proc.stdin.close() 398 self._proc.stdout.close() 399 if self._proc.stderr: 400 self._proc.stderr.close() 401 if (sys.platform not in ('win32', 'cygwin') and 402 not self._proc.poll()): 403 # Closing stdin/stdout/stderr hangs sometimes on OS X. 404 null = open(os.devnull, "w") 405 subprocess.Popen(["kill", "-9", 406 str(self._proc.pid)], stderr=null) 407 null.close() 408 409 def _read_line(self, timeout, stop_time, image_length=0): 410 now = time.time() 411 read_fds = [] 412 413 # first check to see if we have a line already read or if we've 414 # read the entire image 415 if image_length and len(self._read_buffer) >= image_length: 416 out = self._read_buffer[0:image_length] 417 self._read_buffer = self._read_buffer[image_length:] 418 return (timeout, out) 419 420 idx = self._read_buffer.find('\n') 421 if not image_length and idx != -1: 422 out = self._read_buffer[0:idx + 1] 423 self._read_buffer = self._read_buffer[idx + 1:] 424 return (timeout, out) 425 426 # If we've timed out, return just what we have, if anything 427 if timeout or now >= stop_time: 428 out = self._read_buffer 429 self._read_buffer = '' 430 return (True, out) 431 432 (read_fds, write_fds, err_fds) = select.select( 433 [self._stdout_fd], [], [], stop_time - now) 434 try: 435 if timeout or len(read_fds) == 1: 436 self._read_buffer += self._proc.stdout.read() 437 except IOError, e: 438 read = [] 439 return self._read_line(timeout, stop_time) 440