Home | History | Annotate | Download | only in port
      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