Home | History | Annotate | Download | only in port
      1 # Copyright (C) 2011 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 Google name 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 base64
     30 import copy
     31 import logging
     32 import re
     33 import shlex
     34 import sys
     35 import time
     36 import os
     37 
     38 from webkitpy.common.system import path
     39 from webkitpy.common.system.profiler import ProfilerFactory
     40 
     41 
     42 _log = logging.getLogger(__name__)
     43 
     44 
     45 DRIVER_START_TIMEOUT_SECS = 30
     46 
     47 
     48 class DriverInput(object):
     49     def __init__(self, test_name, timeout, image_hash, should_run_pixel_test, args):
     50         self.test_name = test_name
     51         self.timeout = timeout  # in ms
     52         self.image_hash = image_hash
     53         self.should_run_pixel_test = should_run_pixel_test
     54         self.args = args
     55 
     56 
     57 class DriverOutput(object):
     58     """Groups information about a output from driver for easy passing
     59     and post-processing of data."""
     60 
     61     def __init__(self, text, image, image_hash, audio, crash=False,
     62             test_time=0, measurements=None, timeout=False, error='', crashed_process_name='??',
     63             crashed_pid=None, crash_log=None, pid=None):
     64         # FIXME: Args could be renamed to better clarify what they do.
     65         self.text = text
     66         self.image = image  # May be empty-string if the test crashes.
     67         self.image_hash = image_hash
     68         self.image_diff = None  # image_diff gets filled in after construction.
     69         self.audio = audio  # Binary format is port-dependent.
     70         self.crash = crash
     71         self.crashed_process_name = crashed_process_name
     72         self.crashed_pid = crashed_pid
     73         self.crash_log = crash_log
     74         self.test_time = test_time
     75         self.measurements = measurements
     76         self.timeout = timeout
     77         self.error = error  # stderr output
     78         self.pid = pid
     79 
     80     def has_stderr(self):
     81         return bool(self.error)
     82 
     83 
     84 class DeviceFailure(Exception):
     85     pass
     86 
     87 
     88 class Driver(object):
     89     """object for running test(s) using content_shell or other driver."""
     90 
     91     def __init__(self, port, worker_number, pixel_tests, no_timeout=False):
     92         """Initialize a Driver to subsequently run tests.
     93 
     94         Typically this routine will spawn content_shell in a config
     95         ready for subsequent input.
     96 
     97         port - reference back to the port object.
     98         worker_number - identifier for a particular worker/driver instance
     99         """
    100         self._port = port
    101         self._worker_number = worker_number
    102         self._no_timeout = no_timeout
    103 
    104         self._driver_tempdir = None
    105         # WebKitTestRunner can report back subprocess crashes by printing
    106         # "#CRASHED - PROCESSNAME".  Since those can happen at any time
    107         # and ServerProcess won't be aware of them (since the actual tool
    108         # didn't crash, just a subprocess) we record the crashed subprocess name here.
    109         self._crashed_process_name = None
    110         self._crashed_pid = None
    111 
    112         # WebKitTestRunner can report back subprocesses that became unresponsive
    113         # This could mean they crashed.
    114         self._subprocess_was_unresponsive = False
    115 
    116         # stderr reading is scoped on a per-test (not per-block) basis, so we store the accumulated
    117         # stderr output, as well as if we've seen #EOF on this driver instance.
    118         # FIXME: We should probably remove _read_first_block and _read_optional_image_block and
    119         # instead scope these locally in run_test.
    120         self.error_from_test = str()
    121         self.err_seen_eof = False
    122         self._server_process = None
    123         self._current_cmd_line = None
    124 
    125         self._measurements = {}
    126         if self._port.get_option("profile"):
    127             profiler_name = self._port.get_option("profiler")
    128             self._profiler = ProfilerFactory.create_profiler(self._port.host,
    129                 self._port._path_to_driver(), self._port.results_directory(), profiler_name)
    130         else:
    131             self._profiler = None
    132 
    133     def __del__(self):
    134         self.stop()
    135 
    136     def run_test(self, driver_input, stop_when_done):
    137         """Run a single test and return the results.
    138 
    139         Note that it is okay if a test times out or crashes and leaves
    140         the driver in an indeterminate state. The upper layers of the program
    141         are responsible for cleaning up and ensuring things are okay.
    142 
    143         Returns a DriverOutput object.
    144         """
    145         start_time = time.time()
    146         self.start(driver_input.should_run_pixel_test, driver_input.args)
    147         test_begin_time = time.time()
    148         self.error_from_test = str()
    149         self.err_seen_eof = False
    150 
    151         command = self._command_from_driver_input(driver_input)
    152         deadline = test_begin_time + int(driver_input.timeout) / 1000.0
    153 
    154         self._server_process.write(command)
    155         text, audio = self._read_first_block(deadline)  # First block is either text or audio
    156         image, actual_image_hash = self._read_optional_image_block(deadline)  # The second (optional) block is image data.
    157 
    158         crashed = self.has_crashed()
    159         timed_out = self._server_process.timed_out
    160         pid = self._server_process.pid()
    161 
    162         if stop_when_done or crashed or timed_out:
    163             # We call stop() even if we crashed or timed out in order to get any remaining stdout/stderr output.
    164             # In the timeout case, we kill the hung process as well.
    165             out, err = self._server_process.stop(self._port.driver_stop_timeout() if stop_when_done else 0.0)
    166             if out:
    167                 text += out
    168             if err:
    169                 self.error_from_test += err
    170             self._server_process = None
    171 
    172         crash_log = None
    173         if crashed:
    174             self.error_from_test, crash_log = self._get_crash_log(text, self.error_from_test, newer_than=start_time)
    175 
    176             # If we don't find a crash log use a placeholder error message instead.
    177             if not crash_log:
    178                 pid_str = str(self._crashed_pid) if self._crashed_pid else "unknown pid"
    179                 crash_log = 'No crash log found for %s:%s.\n' % (self._crashed_process_name, pid_str)
    180                 # If we were unresponsive append a message informing there may not have been a crash.
    181                 if self._subprocess_was_unresponsive:
    182                     crash_log += 'Process failed to become responsive before timing out.\n'
    183 
    184                 # Print stdout and stderr to the placeholder crash log; we want as much context as possible.
    185                 if self.error_from_test:
    186                     crash_log += '\nstdout:\n%s\nstderr:\n%s\n' % (text, self.error_from_test)
    187 
    188         return DriverOutput(text, image, actual_image_hash, audio,
    189             crash=crashed, test_time=time.time() - test_begin_time, measurements=self._measurements,
    190             timeout=timed_out, error=self.error_from_test,
    191             crashed_process_name=self._crashed_process_name,
    192             crashed_pid=self._crashed_pid, crash_log=crash_log, pid=pid)
    193 
    194     def _get_crash_log(self, stdout, stderr, newer_than):
    195         return self._port._get_crash_log(self._crashed_process_name, self._crashed_pid, stdout, stderr, newer_than)
    196 
    197     # FIXME: Seems this could just be inlined into callers.
    198     @classmethod
    199     def _command_wrapper(cls, wrapper_option):
    200         # Hook for injecting valgrind or other runtime instrumentation,
    201         # used by e.g. tools/valgrind/valgrind_tests.py.
    202         return shlex.split(wrapper_option) if wrapper_option else []
    203 
    204     HTTP_DIR = "http/tests/"
    205     HTTP_LOCAL_DIR = "http/tests/local/"
    206 
    207     def is_http_test(self, test_name):
    208         return test_name.startswith(self.HTTP_DIR) and not test_name.startswith(self.HTTP_LOCAL_DIR)
    209 
    210     def test_to_uri(self, test_name):
    211         """Convert a test name to a URI."""
    212         if not self.is_http_test(test_name):
    213             return path.abspath_to_uri(self._port.host.platform, self._port.abspath_for_test(test_name))
    214 
    215         relative_path = test_name[len(self.HTTP_DIR):]
    216 
    217         # TODO(dpranke): remove the SSL reference?
    218         if relative_path.startswith("ssl/"):
    219             return "https://127.0.0.1:8443/" + relative_path
    220         return "http://127.0.0.1:8000/" + relative_path
    221 
    222     def uri_to_test(self, uri):
    223         """Return the base layout test name for a given URI.
    224 
    225         This returns the test name for a given URI, e.g., if you passed in
    226         "file:///src/LayoutTests/fast/html/keygen.html" it would return
    227         "fast/html/keygen.html".
    228 
    229         """
    230         if uri.startswith("file:///"):
    231             prefix = path.abspath_to_uri(self._port.host.platform, self._port.layout_tests_dir())
    232             if not prefix.endswith('/'):
    233                 prefix += '/'
    234             return uri[len(prefix):]
    235         if uri.startswith("http://"):
    236             return uri.replace('http://127.0.0.1:8000/', self.HTTP_DIR)
    237         if uri.startswith("https://"):
    238             return uri.replace('https://127.0.0.1:8443/', self.HTTP_DIR)
    239         raise NotImplementedError('unknown url type: %s' % uri)
    240 
    241     def has_crashed(self):
    242         if self._server_process is None:
    243             return False
    244         if self._crashed_process_name:
    245             return True
    246         if self._server_process.has_crashed():
    247             self._crashed_process_name = self._server_process.name()
    248             self._crashed_pid = self._server_process.pid()
    249             return True
    250         return False
    251 
    252     def start(self, pixel_tests, per_test_args):
    253         new_cmd_line = self.cmd_line(pixel_tests, per_test_args)
    254         if not self._server_process or new_cmd_line != self._current_cmd_line:
    255             self._start(pixel_tests, per_test_args)
    256             self._run_post_start_tasks()
    257 
    258     def _setup_environ_for_driver(self, environment):
    259         if self._profiler:
    260             environment = self._profiler.adjusted_environment(environment)
    261         return environment
    262 
    263     def _start(self, pixel_tests, per_test_args, wait_for_ready=True):
    264         self.stop()
    265         self._driver_tempdir = self._port._filesystem.mkdtemp(prefix='%s-' % self._port.driver_name())
    266         server_name = self._port.driver_name()
    267         environment = self._port.setup_environ_for_server(server_name)
    268         environment = self._setup_environ_for_driver(environment)
    269         self._crashed_process_name = None
    270         self._crashed_pid = None
    271         cmd_line = self.cmd_line(pixel_tests, per_test_args)
    272         self._server_process = self._port._server_process_constructor(self._port, server_name, cmd_line, environment, logging=self._port.get_option("driver_logging"))
    273         self._server_process.start()
    274         self._current_cmd_line = cmd_line
    275 
    276         if wait_for_ready:
    277             deadline = time.time() + DRIVER_START_TIMEOUT_SECS
    278             if not self._wait_for_server_process_output(self._server_process, deadline, '#READY'):
    279                 _log.error("content_shell took too long to startup.")
    280 
    281     def _wait_for_server_process_output(self, server_process, deadline, text):
    282         output = ''
    283         line = server_process.read_stdout_line(deadline)
    284         while not server_process.timed_out and not server_process.has_crashed() and not text in line.rstrip():
    285             output += line
    286             line = server_process.read_stdout_line(deadline)
    287 
    288         if server_process.timed_out or server_process.has_crashed():
    289             _log.error('Failed to start the %s process: \n%s' % (server_process.name(), output))
    290             return False
    291 
    292         return True
    293 
    294     def _run_post_start_tasks(self):
    295         # Remote drivers may override this to delay post-start tasks until the server has ack'd.
    296         if self._profiler:
    297             self._profiler.attach_to_pid(self._pid_on_target())
    298 
    299     def _pid_on_target(self):
    300         # Remote drivers will override this method to return the pid on the device.
    301         return self._server_process.pid()
    302 
    303     def stop(self):
    304         if self._server_process:
    305             self._server_process.stop()
    306             self._server_process = None
    307             if self._profiler:
    308                 self._profiler.profile_after_exit()
    309 
    310         if self._driver_tempdir:
    311             self._port._filesystem.rmtree(str(self._driver_tempdir))
    312             self._driver_tempdir = None
    313 
    314         self._current_cmd_line = None
    315 
    316     def cmd_line(self, pixel_tests, per_test_args):
    317         cmd = self._command_wrapper(self._port.get_option('wrapper'))
    318         cmd.append(self._port._path_to_driver())
    319         if self._no_timeout:
    320             cmd.append('--no-timeout')
    321         cmd.extend(self._port.get_option('additional_drt_flag', []))
    322         cmd.extend(self._port.additional_drt_flag())
    323         cmd.extend(per_test_args)
    324         cmd.append('-')
    325         return cmd
    326 
    327     def _check_for_driver_crash(self, error_line):
    328         if error_line == "#CRASHED\n":
    329             # This is used on Windows to report that the process has crashed
    330             # See http://trac.webkit.org/changeset/65537.
    331             self._crashed_process_name = self._server_process.name()
    332             self._crashed_pid = self._server_process.pid()
    333         elif (error_line.startswith("#CRASHED - ")
    334             or error_line.startswith("#PROCESS UNRESPONSIVE - ")):
    335             # WebKitTestRunner uses this to report that the WebProcess subprocess crashed.
    336             match = re.match('#(?:CRASHED|PROCESS UNRESPONSIVE) - (\S+)', error_line)
    337             self._crashed_process_name = match.group(1) if match else 'WebProcess'
    338             match = re.search('pid (\d+)', error_line)
    339             pid = int(match.group(1)) if match else None
    340             self._crashed_pid = pid
    341             # FIXME: delete this after we're sure this code is working :)
    342             _log.debug('%s crash, pid = %s, error_line = %s' % (self._crashed_process_name, str(pid), error_line))
    343             if error_line.startswith("#PROCESS UNRESPONSIVE - "):
    344                 self._subprocess_was_unresponsive = True
    345                 self._port.sample_process(self._crashed_process_name, self._crashed_pid)
    346                 # We want to show this since it's not a regular crash and probably we don't have a crash log.
    347                 self.error_from_test += error_line
    348             return True
    349         return self.has_crashed()
    350 
    351     def _command_from_driver_input(self, driver_input):
    352         # FIXME: performance tests pass in full URLs instead of test names.
    353         if driver_input.test_name.startswith('http://') or driver_input.test_name.startswith('https://')  or driver_input.test_name == ('about:blank'):
    354             command = driver_input.test_name
    355         elif self.is_http_test(driver_input.test_name):
    356             command = self.test_to_uri(driver_input.test_name)
    357         else:
    358             command = self._port.abspath_for_test(driver_input.test_name)
    359             if sys.platform == 'cygwin':
    360                 command = path.cygpath(command)
    361 
    362         assert not driver_input.image_hash or driver_input.should_run_pixel_test
    363 
    364         # ' is the separator between arguments.
    365         if self._port.supports_per_test_timeout():
    366             command += "'--timeout'%s" % driver_input.timeout
    367         if driver_input.should_run_pixel_test:
    368             command += "'--pixel-test"
    369         if driver_input.image_hash:
    370             command += "'" + driver_input.image_hash
    371         return command + "\n"
    372 
    373     def _read_first_block(self, deadline):
    374         # returns (text_content, audio_content)
    375         block = self._read_block(deadline)
    376         if block.malloc:
    377             self._measurements['Malloc'] = float(block.malloc)
    378         if block.js_heap:
    379             self._measurements['JSHeap'] = float(block.js_heap)
    380         if block.content_type == 'audio/wav':
    381             return (None, block.decoded_content)
    382         return (block.decoded_content, None)
    383 
    384     def _read_optional_image_block(self, deadline):
    385         # returns (image, actual_image_hash)
    386         block = self._read_block(deadline, wait_for_stderr_eof=True)
    387         if block.content and block.content_type == 'image/png':
    388             return (block.decoded_content, block.content_hash)
    389         return (None, block.content_hash)
    390 
    391     def _read_header(self, block, line, header_text, header_attr, header_filter=None):
    392         if line.startswith(header_text) and getattr(block, header_attr) is None:
    393             value = line.split()[1]
    394             if header_filter:
    395                 value = header_filter(value)
    396             setattr(block, header_attr, value)
    397             return True
    398         return False
    399 
    400     def _process_stdout_line(self, block, line):
    401         if (self._read_header(block, line, 'Content-Type: ', 'content_type')
    402             or self._read_header(block, line, 'Content-Transfer-Encoding: ', 'encoding')
    403             or self._read_header(block, line, 'Content-Length: ', '_content_length', int)
    404             or self._read_header(block, line, 'ActualHash: ', 'content_hash')
    405             or self._read_header(block, line, 'DumpMalloc: ', 'malloc')
    406             or self._read_header(block, line, 'DumpJSHeap: ', 'js_heap')):
    407             return
    408         # Note, we're not reading ExpectedHash: here, but we could.
    409         # If the line wasn't a header, we just append it to the content.
    410         block.content += line
    411 
    412     def _strip_eof(self, line):
    413         if line and line.endswith("#EOF\n"):
    414             return line[:-5], True
    415         if line and line.endswith("#EOF\r\n"):
    416             _log.error("Got a CRLF-terminated #EOF - this is a driver bug.")
    417             return line[:-6], True
    418         return line, False
    419 
    420     def _read_block(self, deadline, wait_for_stderr_eof=False):
    421         block = ContentBlock()
    422         out_seen_eof = False
    423 
    424         while not self.has_crashed():
    425             if out_seen_eof and (self.err_seen_eof or not wait_for_stderr_eof):
    426                 break
    427 
    428             if self.err_seen_eof:
    429                 out_line = self._server_process.read_stdout_line(deadline)
    430                 err_line = None
    431             elif out_seen_eof:
    432                 out_line = None
    433                 err_line = self._server_process.read_stderr_line(deadline)
    434             else:
    435                 out_line, err_line = self._server_process.read_either_stdout_or_stderr_line(deadline)
    436 
    437             if self._server_process.timed_out or self.has_crashed():
    438                 break
    439 
    440             if out_line:
    441                 assert not out_seen_eof
    442                 out_line, out_seen_eof = self._strip_eof(out_line)
    443             if err_line:
    444                 assert not self.err_seen_eof
    445                 err_line, self.err_seen_eof = self._strip_eof(err_line)
    446 
    447             if out_line:
    448                 if out_line[-1] != "\n":
    449                     _log.error("Last character read from DRT stdout line was not a newline!  This indicates either a NRWT or DRT bug.")
    450                 content_length_before_header_check = block._content_length
    451                 self._process_stdout_line(block, out_line)
    452                 # FIXME: Unlike HTTP, DRT dumps the content right after printing a Content-Length header.
    453                 # Don't wait until we're done with headers, just read the binary blob right now.
    454                 if content_length_before_header_check != block._content_length:
    455                     if block._content_length > 0:
    456                         block.content = self._server_process.read_stdout(deadline, block._content_length)
    457                     else:
    458                         _log.error("Received content of type %s with Content-Length of 0!  This indicates a bug in %s.",
    459                                    block.content_type, self._server_process.name())
    460 
    461             if err_line:
    462                 if self._check_for_driver_crash(err_line):
    463                     break
    464                 self.error_from_test += err_line
    465 
    466         block.decode_content()
    467         return block
    468 
    469 
    470 class ContentBlock(object):
    471     def __init__(self):
    472         self.content_type = None
    473         self.encoding = None
    474         self.content_hash = None
    475         self._content_length = None
    476         # Content is treated as binary data even though the text output is usually UTF-8.
    477         self.content = str()  # FIXME: Should be bytearray() once we require Python 2.6.
    478         self.decoded_content = None
    479         self.malloc = None
    480         self.js_heap = None
    481 
    482     def decode_content(self):
    483         if self.encoding == 'base64' and self.content is not None:
    484             self.decoded_content = base64.b64decode(self.content)
    485         else:
    486             self.decoded_content = self.content
    487