| # Copyright (C) 2011 Google Inc. All rights reserved. |
| # Copyright (c) 2015-2019 Apple 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 logging |
| import re |
| import shlex |
| import sys |
| import time |
| import os |
| |
| from collections import defaultdict |
| from webkitcorepy import string_utils |
| |
| 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, should_dump_jsconsolelog_in_stderr=None, args=None, self_comparison_header=None, force_dump_pixels=False): |
| 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.should_dump_jsconsolelog_in_stderr = should_dump_jsconsolelog_in_stderr |
| self.args = args or [] |
| self.self_comparison_header = self_comparison_header |
| self.force_dump_pixels = force_dump_pixels |
| |
| def __repr__(self): |
| return "DriverInput(test_name='{}', timeout={}, image_hash={}, should_run_pixel_test={}, should_dump_jsconsolelog_in_stderr={}, self_comparison_header={}, force_dump_pixels={}'".format(self.test_name, self.timeout, self.image_hash, self.should_run_pixel_test, self.should_dump_jsconsolelog_in_stderr, self.self_comparison_header, self.force_dump_pixels) |
| |
| |
| class DriverOutput(object): |
| """Groups information about a output from driver for easy passing |
| and post-processing of data.""" |
| |
| metrics_patterns = [] |
| metrics_patterns.append((re.compile(r'at \(-?[0-9]+,-?[0-9]+\) *'), '')) |
| metrics_patterns.append((re.compile('size -?[0-9]+x-?[0-9]+ *'), '')) |
| metrics_patterns.append((re.compile('text run width -?[0-9]+: '), '')) |
| metrics_patterns.append((re.compile('text run width -?[0-9]+ [a-zA-Z ]+: '), '')) |
| metrics_patterns.append((re.compile('RenderButton {BUTTON} .*'), 'RenderButton {BUTTON}')) |
| metrics_patterns.append((re.compile('RenderImage {INPUT} .*'), 'RenderImage {INPUT}')) |
| metrics_patterns.append((re.compile('RenderBlock {INPUT} .*'), 'RenderBlock {INPUT}')) |
| metrics_patterns.append((re.compile('RenderTextControl {INPUT} .*'), 'RenderTextControl {INPUT}')) |
| metrics_patterns.append((re.compile(r'\([0-9]+px'), 'px')) |
| metrics_patterns.append((re.compile(' *" *\n +" *'), ' ')) |
| metrics_patterns.append((re.compile('" +$'), '"')) |
| metrics_patterns.append((re.compile('- '), '-')) |
| metrics_patterns.append((re.compile('\n( *)"\\s+'), '\n\\g<1>"')) |
| metrics_patterns.append((re.compile('\\s+"\n'), '"\n')) |
| metrics_patterns.append((re.compile('scrollWidth [0-9]+'), 'scrollWidth')) |
| metrics_patterns.append((re.compile('scrollHeight [0-9]+'), 'scrollHeight')) |
| metrics_patterns.append((re.compile('scrollX [0-9]+'), 'scrollX')) |
| metrics_patterns.append((re.compile('scrollY [0-9]+'), 'scrollY')) |
| metrics_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 = string_utils.decode(text, target_type=str) if text else None |
| 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 = string_utils.encode(audio) if audio else None # 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): |
| self.strip_patterns(self.metrics_patterns) |
| |
| def strip_patterns(self, patterns): |
| if not self.text: |
| return |
| for pattern in patterns: |
| self.text = re.sub(pattern[0], pattern[1], self.text) |
| |
| def strip_text_start_if_needed(self, detectors): |
| if not self.text or not len(detectors): |
| return |
| |
| result = self.text.split('Content-Type: text/plain\n') |
| if len(result) != 2: |
| return |
| |
| for detector in detectors: |
| if detector in result[0]: |
| self.text = result[1] |
| self.error += '\nRemoved logging from stdout:\n' + result[0] |
| return |
| |
| def strip_stderror_patterns(self, patterns): |
| if not self.error: |
| return |
| for pattern in patterns: |
| self.error = re.sub(pattern[0], pattern[1], self.error) |
| |
| |
| class DriverPostTestOutput(object): |
| """Groups data collected for a set of tests, collected after all those testse have run |
| (for example, data about leaked objects)""" |
| def __init__(self, world_leaks_dict): |
| self.world_leaks_dict = world_leaks_dict |
| |
| |
| 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._target_host = port.target_host(worker_number) |
| |
| self._driver_tempdir = None |
| self._driver_user_directory_suffix = None |
| self._driver_user_cache_directory = 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 |
| |
| self._driver_timed_out = 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 = '' |
| self.err_seen_eof = False |
| |
| self._server_name = self._port.driver_name() |
| 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 |
| |
| self.web_platform_test_server_doc_root = self._port.web_platform_test_server_doc_root() |
| self.web_platform_test_server_base_http_url = self._port.web_platform_test_server_base_http_url() |
| self.web_platform_test_server_base_https_url = self._port.web_platform_test_server_base_https_url() |
| |
| 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._driver_timed_out = False |
| self._crash_report_from_driver = None |
| self.error_from_test = '' |
| self.err_seen_eof = False |
| |
| command = self._command_from_driver_input(driver_input) |
| |
| # Certain timeouts are detected by the tool itself; tool detection is better, |
| # because results contain partial output in this case. Make script timeout longer |
| # by 5 seconds to avoid racing for which timeout is detected first. |
| # FIXME: It's not the job of the driver to decide what the timeouts should be. |
| # Move the additional timeout to driver_input. |
| if self._no_timeout: |
| deadline = test_begin_time + 60 * 60 * 24 * 7 # 7 days. Using sys.maxint causes a hang. |
| else: |
| deadline = test_begin_time + int(driver_input.timeout) / 1000.0 + 5 |
| |
| self._server_process.write(command) |
| text, audio = self._read_first_block(deadline, driver_input.test_name) # First block is either text or audio |
| image, actual_image_hash = self._read_optional_image_block(deadline, driver_input.test_name) # The second (optional) block is image data. |
| text = string_utils.decode(text, target_type=str) |
| |
| crashed = self.has_crashed() |
| timed_out = self._server_process.timed_out |
| driver_timed_out = self._driver_timed_out |
| pid = self._server_process.pid() |
| |
| if stop_when_done or crashed or timed_out: |
| if stop_when_done and not (crashed or timed_out): |
| self.do_post_tests_work() |
| # 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 += string_utils.decode(out, target_type=str, errors='backslashreplace') |
| if err: |
| self.error_from_test += string_utils.decode(err, target_type=str) |
| self._server_process = None |
| |
| crash_log = None |
| if self._crash_report_from_driver: |
| crash_log = self._crash_report_from_driver |
| elif 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) |
| |
| # 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 or driver_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 do_post_tests_work(self): |
| if not self._server_process: |
| return None |
| |
| if self._port.get_option('leaks'): |
| _log.debug('Gathering child processes...') |
| self._server_process.write('#LIST CHILD PROCESSES\n') |
| deadline = time.time() + 20 |
| block = self._read_block(deadline, '', wait_for_stderr_eof=True) |
| self._server_process.set_child_processes(self._parse_child_processes_output(block.decoded_content)) |
| |
| if self._port.get_option('world_leaks'): |
| _log.debug('Checking for world leaks...') |
| self._server_process.write('#CHECK FOR WORLD LEAKS\n') |
| deadline = time.time() + 20 |
| block = self._read_block(deadline, '', wait_for_stderr_eof=True) |
| |
| _log.debug('World leak result: %s' % (block.decoded_content)) |
| |
| return self._parse_world_leaks_output(block.decoded_content) |
| |
| return None |
| |
| @staticmethod |
| def _parse_child_processes_output(output): |
| child_processes = defaultdict(list) |
| |
| for line in output.splitlines(): |
| m = re.match(b'^([^:]+): ([0-9]+)$', line) |
| if m: |
| process_name = string_utils.decode(m.group(1), target_type=str) |
| process_id = string_utils.decode(m.group(2), target_type=str) |
| child_processes[process_name].append(process_id) |
| |
| return child_processes |
| |
| def _parse_world_leaks_output(self, output): |
| tests_with_world_leaks = defaultdict(list) |
| |
| last_test = None |
| for line in output.splitlines(): |
| m = re.match('^TEST: (.+)$', line) |
| if m: |
| last_test = self.uri_to_test(m.group(1)) |
| m = re.match('^ABANDONED DOCUMENT: (.+)$', line) |
| if m: |
| leaked_document_url = m.group(1) |
| if last_test: |
| tests_with_world_leaks[last_test].append(leaked_document_url) |
| |
| return DriverPostTestOutput(tests_with_world_leaks) |
| |
| 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, target_host=self._target_host) |
| |
| def _command_wrapper(self): |
| # Hook for injecting valgrind or other runtime instrumentation, used by e.g. tools/valgrind/valgrind_tests.py. |
| wrapper_arguments = [] |
| if self._profiler: |
| wrapper_arguments = self._profiler.wrapper_arguments() |
| if self._port.get_option('wrapper'): |
| return shlex.split(self._port.get_option('wrapper')) + wrapper_arguments |
| return wrapper_arguments |
| |
| HTTP_DIR = "http/tests/" |
| HTTP_LOCAL_DIR = "http/tests/local/" |
| WEBKIT_SPECIFIC_WEB_PLATFORM_TEST_SUBDIR = "http/wpt/" |
| WEBKIT_WEB_PLATFORM_TEST_SERVER_ROUTE = "WebKit/" |
| |
| def is_http_test(self, test_name): |
| return test_name.startswith(self.HTTP_DIR) and not test_name.startswith(self.HTTP_LOCAL_DIR) |
| |
| def is_webkit_specific_web_platform_test(self, test_name): |
| return test_name.startswith(self.WEBKIT_SPECIFIC_WEB_PLATFORM_TEST_SUBDIR) |
| |
| def is_web_platform_test(self, test_name): |
| return test_name.startswith(self.web_platform_test_server_doc_root) |
| |
| def wpt_test_path_to_uri(self, path): |
| return self.web_platform_test_server_base_https_url + path if ".https." in path else self.web_platform_test_server_base_http_url + path |
| |
| def http_test_path_to_uri(self, path): |
| path = path.replace(os.sep, '/') |
| return self.http_base_url(secure=self.is_secure_path(path)) + path |
| |
| def is_secure_path(self, path): |
| return path.startswith("ssl") or ".https." in path |
| |
| def http_base_url(self, secure=None): |
| return "%s://127.0.0.1:%d/" % (('https', 8443) if secure else ('http', 8000)) |
| |
| def test_to_uri(self, test_name): |
| """Convert a test name to a URI.""" |
| if self.is_web_platform_test(test_name): |
| return self.wpt_test_path_to_uri(test_name[len(self.web_platform_test_server_doc_root):]) |
| if self.is_webkit_specific_web_platform_test(test_name): |
| return self.wpt_test_path_to_uri(self.WEBKIT_WEB_PLATFORM_TEST_SERVER_ROUTE + test_name[len(self.WEBKIT_SPECIFIC_WEB_PLATFORM_TEST_SUBDIR):]) |
| |
| if not self.is_http_test(test_name): |
| return path.abspath_to_uri(self._port.host.platform, self._port.abspath_for_test(test_name)) |
| |
| return self.http_test_path_to_uri(test_name[len(self.HTTP_DIR):]) |
| |
| 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(self.web_platform_test_server_base_http_url + self.WEBKIT_WEB_PLATFORM_TEST_SERVER_ROUTE): |
| return uri.replace(self.web_platform_test_server_base_http_url + self.WEBKIT_WEB_PLATFORM_TEST_SERVER_ROUTE, self.WEBKIT_SPECIFIC_WEB_PLATFORM_TEST_SUBDIR) |
| if uri.startswith(self.web_platform_test_server_base_https_url + self.WEBKIT_WEB_PLATFORM_TEST_SERVER_ROUTE): |
| return uri.replace(self.web_platform_test_server_base_https_url + self.WEBKIT_WEB_PLATFORM_TEST_SERVER_ROUTE, self.WEBKIT_SPECIFIC_WEB_PLATFORM_TEST_SUBDIR) |
| if uri.startswith(self.web_platform_test_server_base_http_url): |
| return uri.replace(self.web_platform_test_server_base_http_url, self.web_platform_test_server_doc_root) |
| if uri.startswith(self.web_platform_test_server_base_https_url): |
| return uri.replace(self.web_platform_test_server_base_https_url, self.web_platform_test_server_doc_root) |
| if uri.startswith("http://"): |
| return uri.replace(self.http_base_url(secure=False), self.HTTP_DIR) |
| if uri.startswith("https://"): |
| return uri.replace(self.http_base_url(secure=True), 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.process_name() |
| self._crashed_pid = self._server_process.system_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 _append_environment_variable_path(self, environment, variable, path): |
| if variable in environment: |
| environment[variable] = environment[variable] + os.pathsep + path |
| else: |
| environment[variable] = path |
| |
| def _setup_environ_for_driver(self, environment): |
| self._port._clear_global_caches_and_temporary_files() |
| self._create_temporal_directories() |
| build_root_path = str(self._port._build_path()) |
| self._append_environment_variable_path(environment, 'DYLD_LIBRARY_PATH', build_root_path) |
| self._append_environment_variable_path(environment, '__XPC_DYLD_LIBRARY_PATH', build_root_path) |
| self._append_environment_variable_path(environment, 'DYLD_FRAMEWORK_PATH', build_root_path) |
| self._append_environment_variable_path(environment, '__XPC_DYLD_FRAMEWORK_PATH', build_root_path) |
| |
| self._port.port_adjust_environment_for_test_driver(environment) |
| |
| # Use an isolated temp directory that can be deleted after testing (especially important on Mac, as |
| # CoreMedia disk cache is in the temp directory). |
| environment['TMPDIR'] = str(self._driver_tempdir) |
| environment['DIRHELPER_USER_DIR_SUFFIX'] = self._driver_user_directory_suffix |
| # Put certain normally persistent files into the temp directory (e.g. IndexedDB storage). |
| if sys.platform == 'cygwin': |
| environment['DUMPRENDERTREE_TEMP'] = path.cygpath(str(self._driver_tempdir)) |
| else: |
| environment['DUMPRENDERTREE_TEMP'] = str(self._driver_tempdir) |
| environment['LOCAL_RESOURCE_ROOT'] = str(self._port.layout_tests_dir()) |
| environment['ASAN_OPTIONS'] = "allocator_may_return_null=1" |
| environment['__XPC_ASAN_OPTIONS'] = environment['ASAN_OPTIONS'] |
| |
| # Disable vnode-guard related simulated crashes for WKTR / DRT (rdar://problem/40674034). |
| environment['SQLITE_EXEMPT_PATH_FROM_VNODE_GUARDS'] = '/' |
| environment['__XPC_SQLITE_EXEMPT_PATH_FROM_VNODE_GUARDS'] = environment['SQLITE_EXEMPT_PATH_FROM_VNODE_GUARDS'] |
| |
| environment['JSC_useKernTCSM'] = 'false' |
| environment['__XPC_JSC_useKernTCSM'] = environment['JSC_useKernTCSM'] |
| |
| if sys.platform.startswith('linux'): |
| # Currently on WebKit2, there is no API for setting the application cache directory. |
| # Each worker should have it's own and it should be cleaned afterwards. |
| # Set it to inside the temporary folder by prepending XDG_CACHE_HOME with DRIVER_TEMPDIR. |
| environment['XDG_CACHE_HOME'] = self._port.host.filesystem.join(str(self._driver_tempdir), 'appcache') |
| # Use an empty/volatile home inside DRIVER_TEMPDIR to ensure that the test results |
| # are not affected by the user settings of any library. |
| environment['HOME'] = self._port.host.filesystem.join(str(self._driver_tempdir), 'home') |
| self._target_host.filesystem.maybe_make_directory(environment['HOME']) |
| |
| if self._profiler: |
| environment = self._profiler.adjusted_environment(environment) |
| return environment |
| |
| def _setup_environ_for_test(self): |
| environment = self._port.setup_environ_for_server(self._server_name) |
| environment = self._setup_environ_for_driver(environment) |
| return environment |
| |
| def _create_temporal_directories(self): |
| # Each driver process should be using individual directories under _driver_tempdir (which is deleted when stopping), |
| # however some subsystems on some platforms could end up using process default ones. |
| if self._driver_tempdir is None: |
| self._driver_tempdir = self._port._driver_tempdir(self._target_host) |
| self._driver_user_directory_suffix = os.path.basename(str(self._driver_tempdir)) |
| user_cache_directory = self._port._path_to_user_cache_directory(self._driver_user_directory_suffix) |
| if user_cache_directory: |
| self._target_host.filesystem.maybe_make_directory(user_cache_directory) |
| self._driver_user_cache_directory = user_cache_directory |
| |
| def _start(self, pixel_tests, per_test_args): |
| self.stop() |
| environment = self._setup_environ_for_test() |
| self._crashed_process_name = None |
| self._crashed_pid = None |
| self._server_process = self._port._test_runner_process_constructor(self._port, self._server_name, self.cmd_line(pixel_tests, per_test_args), environment, target_host=self._target_host) |
| 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 _delete_temporal_directories(self): |
| if self._driver_tempdir: |
| self._target_host.filesystem.rmtree(str(self._driver_tempdir)) |
| self._driver_tempdir = None |
| if self._driver_user_cache_directory: |
| self._target_host.filesystem.rmtree(self._driver_user_cache_directory) |
| self._driver_user_cache_directory = None |
| |
| 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() |
| self._delete_temporal_directories() |
| |
| def cmd_line(self, pixel_tests, per_test_args): |
| cmd = self._command_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('accelerated_drawing'): |
| cmd.append('--accelerated-drawing') |
| if self._port.get_option('remote_layer_tree'): |
| cmd.append('--remote-layer-tree') |
| if self._port.get_option('world_leaks'): |
| cmd.append('--world-leaks') |
| if self._port.get_option('threaded'): |
| cmd.append('--threaded') |
| if self._no_timeout: |
| cmd.append('--no-timeout') |
| if self._port.get_option('show_touches'): |
| cmd.append('--show-touches') |
| if self._port.get_option('show_window'): |
| cmd.append('--show-window') |
| if self._port.get_option('accessibility_isolated_tree'): |
| cmd.append('--accessibility-isolated-tree') |
| |
| for allowed_host in self._port.allowed_hosts(): |
| cmd.append('--allowed-host') |
| cmd.append(allowed_host) |
| |
| for feature in self._port.internal_feature(): |
| cmd.append('--internal-feature') |
| cmd.append(feature) |
| |
| if not self._port.get_option('enable_all_experimental_features'): |
| cmd.append('--no-enable-all-experimental-features') |
| |
| for feature in self._port.experimental_feature(): |
| cmd.append('--experimental-feature') |
| cmd.append(feature) |
| |
| 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_timeout(self, out_line): |
| if out_line.startswith(b"#PID UNRESPONSIVE - "): |
| match = re.match(br'#PID UNRESPONSIVE - (\S+)', out_line) |
| child_process_name = string_utils.decode(match.group(1), target_type=str) if match else 'WebProcess' |
| match = re.search(br'pid (\d+)', out_line) |
| child_process_pid = int(match.group(1)) if match else None |
| err_line = 'Wait on notifyDone timed out, process ' + child_process_name + ' pid = ' + str(child_process_pid) |
| self.error_from_test += err_line |
| _log.debug(err_line) |
| if self._port.get_option("sample_on_timeout"): |
| self._port.sample_process(child_process_name, child_process_pid, self._target_host) |
| if out_line == b"FAIL: Timed out waiting for notifyDone to be called\n": |
| self._driver_timed_out = True |
| |
| def _check_for_address_sanitizer_violation(self, error_line): |
| if b"ERROR: AddressSanitizer" in error_line: |
| return True |
| |
| def _check_for_driver_crash_or_unresponsiveness(self, error_line): |
| crashed_check = error_line.rstrip(b'\r\n') |
| if crashed_check == b"#CRASHED": |
| self._crashed_process_name = self._server_process.process_name() |
| self._crashed_pid = self._server_process.system_pid() |
| return True |
| elif error_line.startswith(b"#CRASHED - "): |
| match = re.match(br'#CRASHED - (\S+)', error_line) |
| self._crashed_process_name = string_utils.decode(match.group(1), target_type=str) if match else 'WebProcess' |
| match = re.search(br'pid (\d+)', error_line) |
| self._crashed_pid = int(match.group(1)) if match else None |
| _log.debug('%s crash, pid = %s' % (self._crashed_process_name, str(self._crashed_pid))) |
| return True |
| elif error_line.startswith(b"#PROCESS UNRESPONSIVE - "): |
| match = re.match(br'#PROCESS UNRESPONSIVE - (\S+)', error_line) |
| child_process_name = string_utils.decode(match.group(1), target_type=str) if match else 'WebProcess' |
| match = re.search(br'pid (\d+)', error_line) |
| child_process_pid = int(match.group(1)) if match else None |
| _log.debug('%s is unresponsive, pid = %s' % (child_process_name, str(child_process_pid))) |
| self._driver_timed_out = True |
| if child_process_pid: |
| self._port.sample_process(child_process_name, child_process_pid, self._target_host) |
| self.error_from_test += string_utils.decode(error_line, target_type=str) |
| self._server_process.write('#SAMPLE FINISHED\n', True) # Must be able to ignore a broken pipe here, target process may already be closed. |
| 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_web_platform_test(driver_input.test_name) or self.is_webkit_specific_web_platform_test(driver_input.test_name) or self.is_http_test(driver_input.test_name): |
| command = self.test_to_uri(driver_input.test_name) |
| command += "'--absolutePath'" |
| absPath = self._port.abspath_for_test(driver_input.test_name, self._target_host) |
| if sys.platform == 'cygwin': |
| absPath = path.cygpath(absPath) |
| command += absPath |
| else: |
| command = self._port.abspath_for_test(driver_input.test_name, self._target_host) |
| 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_dump_jsconsolelog_in_stderr: |
| command += "'--dump-jsconsolelog-in-stderr" |
| if driver_input.self_comparison_header: |
| command += "'--self-compare-with-header'%s" % driver_input.self_comparison_header |
| if driver_input.force_dump_pixels: |
| command += "'--force-dump-pixels" |
| |
| # --pixel-test must be the last argument, because the hash is optional, |
| # and any argument put in its place will be incorrectly consumed as the hash. |
| 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, test_name): |
| # returns (text_content, audio_content) |
| block = self._read_block(deadline, test_name) |
| 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, test_name): |
| # returns (image, actual_image_hash) |
| block = self._read_block(deadline, test_name, wait_for_stderr_eof=True) |
| if block.content and block.content_type == 'image/png': |
| return (block.decoded_content if block.encoding == 'base64' else block.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 = string_utils.decode(line.split()[1], target_type=str) |
| if header_filter: |
| value = header_filter(value) |
| setattr(block, header_attr, value) |
| return True |
| return False |
| |
| def _process_stdout_line(self, block, line): |
| for header in [ |
| (b'Content-Type: ', 'content_type', None), |
| (b'Content-Transfer-Encoding: ', 'encoding', None), |
| (b'Content-Length: ', '_content_length', int), |
| (b'ActualHash: ', 'content_hash', None), |
| (b'DumpMalloc: ', 'malloc', None), |
| (b'DumpJSHeap: ', 'js_heap', None), |
| ]: |
| if self._read_header(block, line, header[0], header[1], header[2]): |
| 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 = string_utils.encode(block.content) + line |
| |
| def _strip_eof(self, line): |
| if line and line.endswith(b"#EOF\n"): |
| return line[:-5], True |
| return line, False |
| |
| def _read_block(self, deadline, test_name, wait_for_stderr_eof=False): |
| block = ContentBlock() |
| out_seen_eof = False |
| asan_violation_detected = False |
| |
| while True: |
| 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) |
| |
| # ServerProcess returns None for time outs and crashes. |
| if out_line is None and err_line is None: |
| 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: |
| self._check_for_driver_timeout(out_line) |
| if out_line[-1] != '\n' and out_line[-1] != 10: |
| _log.error(" %s -> Last character read from DRT stdout line was not a newline! This indicates either a NRWT or DRT bug." % test_name) |
| 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 = string_utils.encode(self._server_process.read_stdout(deadline, block._content_length)) |
| |
| if err_line: |
| if self._check_for_driver_crash_or_unresponsiveness(err_line): |
| break |
| elif self._check_for_address_sanitizer_violation(err_line): |
| asan_violation_detected = True |
| self._crash_report_from_driver = '' |
| # ASan report starts with a nondescript line, we only detect the second line. |
| end_of_previous_error_line = self.error_from_test.rfind('\n', 0, -1) |
| if end_of_previous_error_line > 0: |
| self.error_from_test = self.error_from_test[:end_of_previous_error_line] |
| else: |
| self.error_from_test = '' |
| # Symbolication can take a very long time, give it 10 extra minutes to finish. |
| # FIXME: This can likely be removed once <rdar://problem/18701447> is fixed. |
| deadline += 10 * 60 * 1000 |
| if asan_violation_detected: |
| self._crash_report_from_driver += string_utils.decode(err_line, target_type=str) |
| else: |
| self.error_from_test += string_utils.decode(err_line, target_type=str) |
| |
| if asan_violation_detected and not self._crashed_process_name: |
| self._crashed_process_name = self._server_process.process_name() |
| self._crashed_pid = self._server_process.system_pid() |
| |
| block.decode_content() |
| return block |
| |
| @staticmethod |
| def check_driver(port): |
| # This checks if the required system dependencies for the driver are met. |
| # Since this is the generic class implementation, just return True. |
| return True |
| |
| |
| 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 = b'' |
| 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: |
| try: |
| self.decoded_content = string_utils.decode(self.content, target_type=str) |
| except UnicodeDecodeError: |
| self.decoded_content = None |
| |
| |
| 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) |
| |
| @property |
| def host(self): |
| return self._driver._target_host |
| |
| # 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) |
| |
| def is_web_platform_test(self, test_name): |
| return self._driver.is_web_platform_test(test_name) |
| |
| def is_webkit_specific_web_platform_test(self, test_name): |
| return self._driver.is_webkit_specific_web_platform_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): |
| 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 do_post_tests_work(self): |
| return self._driver.do_post_tests_work() |
| |
| 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)) |