summaryrefslogtreecommitdiff
path: root/Tools/Scripts/webkitpy/port/driver.py
diff options
context:
space:
mode:
Diffstat (limited to 'Tools/Scripts/webkitpy/port/driver.py')
-rw-r--r--Tools/Scripts/webkitpy/port/driver.py562
1 files changed, 0 insertions, 562 deletions
diff --git a/Tools/Scripts/webkitpy/port/driver.py b/Tools/Scripts/webkitpy/port/driver.py
deleted file mode 100644
index 5061bd6d1..000000000
--- a/Tools/Scripts/webkitpy/port/driver.py
+++ /dev/null
@@ -1,562 +0,0 @@
-# Copyright (C) 2011 Google Inc. All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-#
-# * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-# * Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following disclaimer
-# in the documentation and/or other materials provided with the
-# distribution.
-# * Neither the Google name nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import base64
-import copy
-import logging
-import re
-import shlex
-import sys
-import time
-import os
-
-from webkitpy.common.system import path
-from webkitpy.common.system.profiler import ProfilerFactory
-
-
-_log = logging.getLogger(__name__)
-
-
-class DriverInput(object):
- def __init__(self, test_name, timeout, image_hash, should_run_pixel_test, args=None):
- self.test_name = test_name
- self.timeout = timeout # in ms
- self.image_hash = image_hash
- self.should_run_pixel_test = should_run_pixel_test
- self.args = args or []
-
-
-class DriverOutput(object):
- """Groups information about a output from driver for easy passing
- and post-processing of data."""
-
- strip_patterns = []
- strip_patterns.append((re.compile('at \(-?[0-9]+,-?[0-9]+\) *'), ''))
- strip_patterns.append((re.compile('size -?[0-9]+x-?[0-9]+ *'), ''))
- strip_patterns.append((re.compile('text run width -?[0-9]+: '), ''))
- strip_patterns.append((re.compile('text run width -?[0-9]+ [a-zA-Z ]+: '), ''))
- strip_patterns.append((re.compile('RenderButton {BUTTON} .*'), 'RenderButton {BUTTON}'))
- strip_patterns.append((re.compile('RenderImage {INPUT} .*'), 'RenderImage {INPUT}'))
- strip_patterns.append((re.compile('RenderBlock {INPUT} .*'), 'RenderBlock {INPUT}'))
- strip_patterns.append((re.compile('RenderTextControl {INPUT} .*'), 'RenderTextControl {INPUT}'))
- strip_patterns.append((re.compile('\([0-9]+px'), 'px'))
- strip_patterns.append((re.compile(' *" *\n +" *'), ' '))
- strip_patterns.append((re.compile('" +$'), '"'))
- strip_patterns.append((re.compile('- '), '-'))
- strip_patterns.append((re.compile('\n( *)"\s+'), '\n\g<1>"'))
- strip_patterns.append((re.compile('\s+"\n'), '"\n'))
- strip_patterns.append((re.compile('scrollWidth [0-9]+'), 'scrollWidth'))
- strip_patterns.append((re.compile('scrollHeight [0-9]+'), 'scrollHeight'))
- strip_patterns.append((re.compile('scrollX [0-9]+'), 'scrollX'))
- strip_patterns.append((re.compile('scrollY [0-9]+'), 'scrollY'))
- strip_patterns.append((re.compile('scrolled to [0-9]+,[0-9]+'), 'scrolled'))
-
- def __init__(self, text, image, image_hash, audio, crash=False,
- test_time=0, measurements=None, timeout=False, error='', crashed_process_name='??',
- crashed_pid=None, crash_log=None, pid=None):
- # FIXME: Args could be renamed to better clarify what they do.
- self.text = text
- self.image = image # May be empty-string if the test crashes.
- self.image_hash = image_hash
- self.image_diff = None # image_diff gets filled in after construction.
- self.audio = audio # Binary format is port-dependent.
- self.crash = crash
- self.crashed_process_name = crashed_process_name
- self.crashed_pid = crashed_pid
- self.crash_log = crash_log
- self.test_time = test_time
- self.measurements = measurements
- self.timeout = timeout
- self.error = error # stderr output
- self.pid = pid
-
- def has_stderr(self):
- return bool(self.error)
-
- def strip_metrics(self):
- if not self.text:
- return
- for pattern in self.strip_patterns:
- self.text = re.sub(pattern[0], pattern[1], self.text)
-
-
-class Driver(object):
- """object for running test(s) using DumpRenderTree/WebKitTestRunner."""
-
- def __init__(self, port, worker_number, pixel_tests, no_timeout=False):
- """Initialize a Driver to subsequently run tests.
-
- Typically this routine will spawn DumpRenderTree in a config
- ready for subsequent input.
-
- port - reference back to the port object.
- worker_number - identifier for a particular worker/driver instance
- """
- self._port = port
- self._worker_number = worker_number
- self._no_timeout = no_timeout
-
- self._driver_tempdir = None
- # WebKitTestRunner can report back subprocess crashes by printing
- # "#CRASHED - PROCESSNAME". Since those can happen at any time
- # and ServerProcess won't be aware of them (since the actual tool
- # didn't crash, just a subprocess) we record the crashed subprocess name here.
- self._crashed_process_name = None
- self._crashed_pid = None
-
- # WebKitTestRunner can report back subprocesses that became unresponsive
- # This could mean they crashed.
- self._subprocess_was_unresponsive = False
-
- # stderr reading is scoped on a per-test (not per-block) basis, so we store the accumulated
- # stderr output, as well as if we've seen #EOF on this driver instance.
- # FIXME: We should probably remove _read_first_block and _read_optional_image_block and
- # instead scope these locally in run_test.
- self.error_from_test = str()
- self.err_seen_eof = False
- self._server_process = None
-
- self._measurements = {}
- if self._port.get_option("profile"):
- profiler_name = self._port.get_option("profiler")
- self._profiler = ProfilerFactory.create_profiler(self._port.host,
- self._port._path_to_driver(), self._port.results_directory(), profiler_name)
- else:
- self._profiler = None
-
- def __del__(self):
- self.stop()
-
- def run_test(self, driver_input, stop_when_done):
- """Run a single test and return the results.
-
- Note that it is okay if a test times out or crashes and leaves
- the driver in an indeterminate state. The upper layers of the program
- are responsible for cleaning up and ensuring things are okay.
-
- Returns a DriverOutput object.
- """
- start_time = time.time()
- self.start(driver_input.should_run_pixel_test, driver_input.args)
- test_begin_time = time.time()
- self.error_from_test = str()
- self.err_seen_eof = False
-
- command = self._command_from_driver_input(driver_input)
- deadline = test_begin_time + int(driver_input.timeout) / 1000.0
-
- self._server_process.write(command)
- text, audio = self._read_first_block(deadline) # First block is either text or audio
- image, actual_image_hash = self._read_optional_image_block(deadline) # The second (optional) block is image data.
-
- crashed = self.has_crashed()
- timed_out = self._server_process.timed_out
- pid = self._server_process.pid()
-
- if stop_when_done or crashed or timed_out:
- # We call stop() even if we crashed or timed out in order to get any remaining stdout/stderr output.
- # In the timeout case, we kill the hung process as well.
- out, err = self._server_process.stop(self._port.driver_stop_timeout() if stop_when_done else 0.0)
- if out:
- text += out
- if err:
- self.error_from_test += err
- self._server_process = None
-
- crash_log = None
- if crashed:
- self.error_from_test, crash_log = self._get_crash_log(text, self.error_from_test, newer_than=start_time)
-
- # If we don't find a crash log use a placeholder error message instead.
- if not crash_log:
- pid_str = str(self._crashed_pid) if self._crashed_pid else "unknown pid"
- crash_log = 'No crash log found for %s:%s.\n' % (self._crashed_process_name, pid_str)
- # If we were unresponsive append a message informing there may not have been a crash.
- if self._subprocess_was_unresponsive:
- crash_log += 'Process failed to become responsive before timing out.\n'
-
- # Print stdout and stderr to the placeholder crash log; we want as much context as possible.
- if self.error_from_test:
- crash_log += '\nstdout:\n%s\nstderr:\n%s\n' % (text, self.error_from_test)
-
- return DriverOutput(text, image, actual_image_hash, audio,
- crash=crashed, test_time=time.time() - test_begin_time, measurements=self._measurements,
- timeout=timed_out, error=self.error_from_test,
- crashed_process_name=self._crashed_process_name,
- crashed_pid=self._crashed_pid, crash_log=crash_log, pid=pid)
-
- def _get_crash_log(self, stdout, stderr, newer_than):
- return self._port._get_crash_log(self._crashed_process_name, self._crashed_pid, stdout, stderr, newer_than)
-
- # FIXME: Seems this could just be inlined into callers.
- @classmethod
- def _command_wrapper(cls, wrapper_option):
- # Hook for injecting valgrind or other runtime instrumentation,
- # used by e.g. tools/valgrind/valgrind_tests.py.
- return shlex.split(wrapper_option) if wrapper_option else []
-
- HTTP_DIR = "http/tests/"
- HTTP_LOCAL_DIR = "http/tests/local/"
-
- def is_http_test(self, test_name):
- return test_name.startswith(self.HTTP_DIR) and not test_name.startswith(self.HTTP_LOCAL_DIR)
-
- def test_to_uri(self, test_name):
- """Convert a test name to a URI."""
- if not self.is_http_test(test_name):
- return path.abspath_to_uri(self._port.host.platform, self._port.abspath_for_test(test_name))
-
- relative_path = test_name[len(self.HTTP_DIR):]
-
- # TODO(dpranke): remove the SSL reference?
- if relative_path.startswith("ssl/"):
- return "https://127.0.0.1:8443/" + relative_path
- return "http://127.0.0.1:8000/" + relative_path
-
- def uri_to_test(self, uri):
- """Return the base layout test name for a given URI.
-
- This returns the test name for a given URI, e.g., if you passed in
- "file:///src/LayoutTests/fast/html/keygen.html" it would return
- "fast/html/keygen.html".
-
- """
- if uri.startswith("file:///"):
- prefix = path.abspath_to_uri(self._port.host.platform, self._port.layout_tests_dir())
- if not prefix.endswith('/'):
- prefix += '/'
- return uri[len(prefix):]
- if uri.startswith("http://"):
- return uri.replace('http://127.0.0.1:8000/', self.HTTP_DIR)
- if uri.startswith("https://"):
- return uri.replace('https://127.0.0.1:8443/', self.HTTP_DIR)
- raise NotImplementedError('unknown url type: %s' % uri)
-
- def has_crashed(self):
- if self._server_process is None:
- return False
- if self._crashed_process_name:
- return True
- if self._server_process.has_crashed():
- self._crashed_process_name = self._server_process.name()
- self._crashed_pid = self._server_process.pid()
- return True
- return False
-
- def start(self, pixel_tests, per_test_args):
- # FIXME: Callers shouldn't normally call this, since this routine
- # may not be specifying the correct combination of pixel test and
- # per_test args.
- #
- # The only reason we have this routine at all is so the perftestrunner
- # can pause before running a test; it might be better to push that
- # into run_test() directly.
- if not self._server_process:
- self._start(pixel_tests, per_test_args)
- self._run_post_start_tasks()
-
- def _setup_environ_for_driver(self, environment):
- environment['DYLD_LIBRARY_PATH'] = self._port._build_path()
- environment['DYLD_FRAMEWORK_PATH'] = self._port._build_path()
- # FIXME: We're assuming that WebKitTestRunner checks this DumpRenderTree-named environment variable.
- # FIXME: Commented out for now to avoid tests breaking. Re-enable after
- # we cut over to NRWT
- #environment['DUMPRENDERTREE_TEMP'] = str(self._port._driver_tempdir_for_environment())
- environment['DUMPRENDERTREE_TEMP'] = str(self._driver_tempdir)
- environment['LOCAL_RESOURCE_ROOT'] = self._port.layout_tests_dir()
- if 'WEBKIT_OUTPUTDIR' in os.environ:
- environment['WEBKIT_OUTPUTDIR'] = os.environ['WEBKIT_OUTPUTDIR']
- if self._profiler:
- environment = self._profiler.adjusted_environment(environment)
- return environment
-
- def _start(self, pixel_tests, per_test_args):
- self.stop()
- self._driver_tempdir = self._port._driver_tempdir()
- server_name = self._port.driver_name()
- environment = self._port.setup_environ_for_server(server_name)
- environment = self._setup_environ_for_driver(environment)
- self._crashed_process_name = None
- self._crashed_pid = None
- self._server_process = self._port._server_process_constructor(self._port, server_name, self.cmd_line(pixel_tests, per_test_args), environment)
- self._server_process.start()
-
- def _run_post_start_tasks(self):
- # Remote drivers may override this to delay post-start tasks until the server has ack'd.
- if self._profiler:
- self._profiler.attach_to_pid(self._pid_on_target())
-
- def _pid_on_target(self):
- # Remote drivers will override this method to return the pid on the device.
- return self._server_process.pid()
-
- def stop(self):
- if self._server_process:
- self._server_process.stop(self._port.driver_stop_timeout())
- self._server_process = None
- if self._profiler:
- self._profiler.profile_after_exit()
-
- if self._driver_tempdir:
- self._port._filesystem.rmtree(str(self._driver_tempdir))
- self._driver_tempdir = None
-
- def cmd_line(self, pixel_tests, per_test_args):
- cmd = self._command_wrapper(self._port.get_option('wrapper'))
- cmd.append(self._port._path_to_driver())
- if self._port.get_option('gc_between_tests'):
- cmd.append('--gc-between-tests')
- if self._port.get_option('complex_text'):
- cmd.append('--complex-text')
- if self._port.get_option('threaded'):
- cmd.append('--threaded')
- if self._no_timeout:
- cmd.append('--no-timeout')
- # FIXME: We need to pass --timeout=SECONDS to WebKitTestRunner for WebKit2.
-
- cmd.extend(self._port.get_option('additional_drt_flag', []))
- cmd.extend(self._port.additional_drt_flag())
-
- cmd.extend(per_test_args)
-
- cmd.append('-')
- return cmd
-
- def _check_for_driver_crash(self, error_line):
- if error_line == "#CRASHED\n":
- # This is used on Windows to report that the process has crashed
- # See http://trac.webkit.org/changeset/65537.
- self._crashed_process_name = self._server_process.name()
- self._crashed_pid = self._server_process.pid()
- elif (error_line.startswith("#CRASHED - ")
- or error_line.startswith("#PROCESS UNRESPONSIVE - ")):
- # WebKitTestRunner uses this to report that the WebProcess subprocess crashed.
- match = re.match('#(?:CRASHED|PROCESS UNRESPONSIVE) - (\S+)', error_line)
- self._crashed_process_name = match.group(1) if match else 'WebProcess'
- match = re.search('pid (\d+)', error_line)
- pid = int(match.group(1)) if match else None
- self._crashed_pid = pid
- # FIXME: delete this after we're sure this code is working :)
- _log.debug('%s crash, pid = %s, error_line = %s' % (self._crashed_process_name, str(pid), error_line))
- if error_line.startswith("#PROCESS UNRESPONSIVE - "):
- self._subprocess_was_unresponsive = True
- self._port.sample_process(self._crashed_process_name, self._crashed_pid)
- # We want to show this since it's not a regular crash and probably we don't have a crash log.
- self.error_from_test += error_line
- return True
- return self.has_crashed()
-
- def _command_from_driver_input(self, driver_input):
- # FIXME: performance tests pass in full URLs instead of test names.
- if driver_input.test_name.startswith('http://') or driver_input.test_name.startswith('https://') or driver_input.test_name == ('about:blank'):
- command = driver_input.test_name
- elif self.is_http_test(driver_input.test_name):
- command = self.test_to_uri(driver_input.test_name)
- else:
- command = self._port.abspath_for_test(driver_input.test_name)
- if sys.platform == 'cygwin':
- command = path.cygpath(command)
-
- assert not driver_input.image_hash or driver_input.should_run_pixel_test
-
- # ' is the separator between arguments.
- if self._port.supports_per_test_timeout():
- command += "'--timeout'%s" % driver_input.timeout
- if driver_input.should_run_pixel_test:
- command += "'--pixel-test"
- if driver_input.image_hash:
- command += "'" + driver_input.image_hash
- return command + "\n"
-
- def _read_first_block(self, deadline):
- # returns (text_content, audio_content)
- block = self._read_block(deadline)
- if block.malloc:
- self._measurements['Malloc'] = float(block.malloc)
- if block.js_heap:
- self._measurements['JSHeap'] = float(block.js_heap)
- if block.content_type == 'audio/wav':
- return (None, block.decoded_content)
- return (block.decoded_content, None)
-
- def _read_optional_image_block(self, deadline):
- # returns (image, actual_image_hash)
- block = self._read_block(deadline, wait_for_stderr_eof=True)
- if block.content and block.content_type == 'image/png':
- return (block.decoded_content, block.content_hash)
- return (None, block.content_hash)
-
- def _read_header(self, block, line, header_text, header_attr, header_filter=None):
- if line.startswith(header_text) and getattr(block, header_attr) is None:
- value = line.split()[1]
- if header_filter:
- value = header_filter(value)
- setattr(block, header_attr, value)
- return True
- return False
-
- def _process_stdout_line(self, block, line):
- if (self._read_header(block, line, 'Content-Type: ', 'content_type')
- or self._read_header(block, line, 'Content-Transfer-Encoding: ', 'encoding')
- or self._read_header(block, line, 'Content-Length: ', '_content_length', int)
- or self._read_header(block, line, 'ActualHash: ', 'content_hash')
- or self._read_header(block, line, 'DumpMalloc: ', 'malloc')
- or self._read_header(block, line, 'DumpJSHeap: ', 'js_heap')):
- return
- # Note, we're not reading ExpectedHash: here, but we could.
- # If the line wasn't a header, we just append it to the content.
- block.content += line
-
- def _strip_eof(self, line):
- if line and line.endswith("#EOF\n"):
- return line[:-5], True
- return line, False
-
- def _read_block(self, deadline, wait_for_stderr_eof=False):
- block = ContentBlock()
- out_seen_eof = False
-
- while not self.has_crashed():
- if out_seen_eof and (self.err_seen_eof or not wait_for_stderr_eof):
- break
-
- if self.err_seen_eof:
- out_line = self._server_process.read_stdout_line(deadline)
- err_line = None
- elif out_seen_eof:
- out_line = None
- err_line = self._server_process.read_stderr_line(deadline)
- else:
- out_line, err_line = self._server_process.read_either_stdout_or_stderr_line(deadline)
-
- if self._server_process.timed_out or self.has_crashed():
- break
-
- if out_line:
- assert not out_seen_eof
- out_line, out_seen_eof = self._strip_eof(out_line)
- if err_line:
- assert not self.err_seen_eof
- err_line, self.err_seen_eof = self._strip_eof(err_line)
-
- if out_line:
- if out_line[-1] != "\n":
- _log.error("Last character read from DRT stdout line was not a newline! This indicates either a NRWT or DRT bug.")
- content_length_before_header_check = block._content_length
- self._process_stdout_line(block, out_line)
- # FIXME: Unlike HTTP, DRT dumps the content right after printing a Content-Length header.
- # Don't wait until we're done with headers, just read the binary blob right now.
- if content_length_before_header_check != block._content_length:
- block.content = self._server_process.read_stdout(deadline, block._content_length)
-
- if err_line:
- if self._check_for_driver_crash(err_line):
- break
- self.error_from_test += err_line
-
- block.decode_content()
- return block
-
-
-class ContentBlock(object):
- def __init__(self):
- self.content_type = None
- self.encoding = None
- self.content_hash = None
- self._content_length = None
- # Content is treated as binary data even though the text output is usually UTF-8.
- self.content = str() # FIXME: Should be bytearray() once we require Python 2.6.
- self.decoded_content = None
- self.malloc = None
- self.js_heap = None
-
- def decode_content(self):
- if self.encoding == 'base64' and self.content is not None:
- self.decoded_content = base64.b64decode(self.content)
- else:
- self.decoded_content = self.content
-
-class DriverProxy(object):
- """A wrapper for managing two Driver instances, one with pixel tests and
- one without. This allows us to handle plain text tests and ref tests with a
- single driver."""
-
- def __init__(self, port, worker_number, driver_instance_constructor, pixel_tests, no_timeout):
- self._port = port
- self._worker_number = worker_number
- self._driver_instance_constructor = driver_instance_constructor
- self._no_timeout = no_timeout
-
- # FIXME: We shouldn't need to create a driver until we actually run a test.
- self._driver = self._make_driver(pixel_tests)
- self._driver_cmd_line = None
-
- def _make_driver(self, pixel_tests):
- return self._driver_instance_constructor(self._port, self._worker_number, pixel_tests, self._no_timeout)
-
- # FIXME: this should be a @classmethod (or implemented on Port instead).
- def is_http_test(self, test_name):
- return self._driver.is_http_test(test_name)
-
- # FIXME: this should be a @classmethod (or implemented on Port instead).
- def test_to_uri(self, test_name):
- return self._driver.test_to_uri(test_name)
-
- # FIXME: this should be a @classmethod (or implemented on Port instead).
- def uri_to_test(self, uri):
- return self._driver.uri_to_test(uri)
-
- def run_test(self, driver_input, stop_when_done):
- base = self._port.lookup_virtual_test_base(driver_input.test_name)
- if base:
- virtual_driver_input = copy.copy(driver_input)
- virtual_driver_input.test_name = base
- virtual_driver_input.args = self._port.lookup_virtual_test_args(driver_input.test_name)
- return self.run_test(virtual_driver_input, stop_when_done)
-
- pixel_tests_needed = driver_input.should_run_pixel_test
- cmd_line_key = self._cmd_line_as_key(pixel_tests_needed, driver_input.args)
- if cmd_line_key != self._driver_cmd_line:
- self._driver.stop()
- self._driver = self._make_driver(pixel_tests_needed)
- self._driver_cmd_line = cmd_line_key
-
- return self._driver.run_test(driver_input, stop_when_done)
-
- def has_crashed(self):
- return self._driver.has_crashed()
-
- def stop(self):
- self._driver.stop()
-
- # FIXME: this should be a @classmethod (or implemented on Port instead).
- def cmd_line(self, pixel_tests=None, per_test_args=None):
- return self._driver.cmd_line(pixel_tests, per_test_args or [])
-
- def _cmd_line_as_key(self, pixel_tests, per_test_args):
- return ' '.join(self.cmd_line(pixel_tests, per_test_args))